@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.js
CHANGED
|
@@ -333,7 +333,7 @@ function validateWidgetBindings(stack) {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
// src/validate-expressions.ts
|
|
336
|
-
import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
|
|
336
|
+
import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
|
|
337
337
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
338
338
|
|
|
339
339
|
// src/validate-null-guards.ts
|
|
@@ -694,6 +694,38 @@ function validateStackExpressions(stack) {
|
|
|
694
694
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
695
695
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
696
696
|
};
|
|
697
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
698
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
699
|
+
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";
|
|
700
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
701
|
+
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)",
|
|
702
|
+
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",
|
|
703
|
+
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",
|
|
704
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
705
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
706
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
707
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
708
|
+
// one, not a fabricated fourth cell (#6716).
|
|
709
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
710
|
+
};
|
|
711
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
712
|
+
const source = celSourceOf(raw);
|
|
713
|
+
if (!source) return;
|
|
714
|
+
const roots = collectCelRootIdentifiers(source);
|
|
715
|
+
if (!roots.ok) return;
|
|
716
|
+
const kept = SCOPE_ROOTS.filter(
|
|
717
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
718
|
+
);
|
|
719
|
+
if (kept.length === 0) return;
|
|
720
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
721
|
+
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}\`.`;
|
|
722
|
+
issues.push({
|
|
723
|
+
where,
|
|
724
|
+
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,
|
|
725
|
+
source,
|
|
726
|
+
severity: "error"
|
|
727
|
+
});
|
|
728
|
+
};
|
|
697
729
|
const checkDeclaredPredicate = (where, raw) => {
|
|
698
730
|
if (raw == null) return;
|
|
699
731
|
const res = validateExpression("predicate", raw);
|
|
@@ -726,7 +758,14 @@ function validateStackExpressions(stack) {
|
|
|
726
758
|
if (retired.length > 0) {
|
|
727
759
|
issues.push({
|
|
728
760
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
729
|
-
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. `) +
|
|
761
|
+
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
|
|
762
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
763
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
764
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
765
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
766
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
767
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
768
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
730
769
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
731
770
|
});
|
|
732
771
|
} else if (!fn) {
|
|
@@ -760,13 +799,27 @@ function validateStackExpressions(stack) {
|
|
|
760
799
|
for (const [fname, f] of fieldList) {
|
|
761
800
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
762
801
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
802
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
803
|
+
}
|
|
804
|
+
for (const [oi, opt] of asArray2(f.options).entries()) {
|
|
805
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
806
|
+
check(
|
|
807
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
808
|
+
opt.visibleWhen,
|
|
809
|
+
objectName,
|
|
810
|
+
"record"
|
|
811
|
+
);
|
|
763
812
|
}
|
|
764
|
-
const
|
|
765
|
-
|
|
813
|
+
for (const [slot, raw, consequence2] of [
|
|
814
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
815
|
+
["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`]
|
|
816
|
+
]) {
|
|
817
|
+
const source = celSourceOf(raw);
|
|
818
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
766
819
|
issues.push({
|
|
767
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
768
|
-
message:
|
|
769
|
-
source
|
|
820
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
821
|
+
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\`.`),
|
|
822
|
+
source,
|
|
770
823
|
severity: "error"
|
|
771
824
|
});
|
|
772
825
|
}
|
|
@@ -859,6 +912,347 @@ function validateStackExpressions(stack) {
|
|
|
859
912
|
return issues;
|
|
860
913
|
}
|
|
861
914
|
|
|
915
|
+
// src/lint-startup-registry-verdict.ts
|
|
916
|
+
import { createRequire } from "module";
|
|
917
|
+
var cachedTs = null;
|
|
918
|
+
function loadTypeScript() {
|
|
919
|
+
if (cachedTs) return cachedTs;
|
|
920
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
921
|
+
try {
|
|
922
|
+
cachedTs = createRequire(anchor)("typescript");
|
|
923
|
+
} catch (err) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`@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.`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
return cachedTs;
|
|
929
|
+
}
|
|
930
|
+
var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
|
|
931
|
+
var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
|
|
932
|
+
var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
|
|
933
|
+
[
|
|
934
|
+
"getRegisteredNodeTypes",
|
|
935
|
+
"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)"
|
|
936
|
+
],
|
|
937
|
+
[
|
|
938
|
+
"knownNodeTypes",
|
|
939
|
+
"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)"
|
|
940
|
+
],
|
|
941
|
+
[
|
|
942
|
+
"getUnknownNodeTypeAudit",
|
|
943
|
+
'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)'
|
|
944
|
+
],
|
|
945
|
+
[
|
|
946
|
+
"getActionDescriptors",
|
|
947
|
+
'ADR-0018 action descriptors are published by plugins during boot, so a type missing from them here means "not published YET" (#4771)'
|
|
948
|
+
],
|
|
949
|
+
[
|
|
950
|
+
"getRegisteredExecutors",
|
|
951
|
+
"executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
|
|
952
|
+
],
|
|
953
|
+
[
|
|
954
|
+
"listExecutors",
|
|
955
|
+
"executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
|
|
956
|
+
],
|
|
957
|
+
[
|
|
958
|
+
"getRegisteredTools",
|
|
959
|
+
"AI tools are contributed by plugins during boot, so the registered set is not final until every plugin has started"
|
|
960
|
+
],
|
|
961
|
+
[
|
|
962
|
+
"listConnectors",
|
|
963
|
+
"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"
|
|
964
|
+
],
|
|
965
|
+
[
|
|
966
|
+
"getRegisteredConnectors",
|
|
967
|
+
"connectors are contributed by plugins during boot, so the registered set is not final until every plugin has started"
|
|
968
|
+
],
|
|
969
|
+
[
|
|
970
|
+
"listCapabilities",
|
|
971
|
+
"ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
|
|
972
|
+
],
|
|
973
|
+
[
|
|
974
|
+
"getRegisteredCapabilities",
|
|
975
|
+
"ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
|
|
976
|
+
],
|
|
977
|
+
[
|
|
978
|
+
"listProviders",
|
|
979
|
+
"providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
|
|
980
|
+
],
|
|
981
|
+
[
|
|
982
|
+
"getRegisteredProviders",
|
|
983
|
+
"providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
|
|
984
|
+
]
|
|
985
|
+
]);
|
|
986
|
+
var PRE_SEAL_PHASES = /* @__PURE__ */ new Map([
|
|
987
|
+
["constructor", "the constructor runs at composition time \u2014 before ANY plugin has been initialized"],
|
|
988
|
+
["init", "another plugin's init() may not have run yet (ADR-0116), so the vocabulary is still filling"],
|
|
989
|
+
[
|
|
990
|
+
"start",
|
|
991
|
+
"sibling plugins' start() have not all run, and ADR-0018 lets a plugin register its contributions from start() \u2014 the vocabulary is still filling"
|
|
992
|
+
]
|
|
993
|
+
]);
|
|
994
|
+
var SEAL_MARKERS = ["seal"];
|
|
995
|
+
var VERDICT_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
|
|
996
|
+
var ALL_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal", "info", "debug", "trace", "log"]);
|
|
997
|
+
var PERSISTENCE_CALLEES = /* @__PURE__ */ new Set(["insert", "insertOne", "update", "updateOne", "upsert", "save", "saveMetaItem"]);
|
|
998
|
+
var ASSERTIVE_PHRASES = [
|
|
999
|
+
"will fail",
|
|
1000
|
+
"fails at execution",
|
|
1001
|
+
"nothing will register",
|
|
1002
|
+
"no plugin provides",
|
|
1003
|
+
"is not installed",
|
|
1004
|
+
"is not configured",
|
|
1005
|
+
"you need",
|
|
1006
|
+
"you must install",
|
|
1007
|
+
"must be provisioned"
|
|
1008
|
+
];
|
|
1009
|
+
var HEDGE_PHRASES = [
|
|
1010
|
+
"not yet",
|
|
1011
|
+
"yet been registered",
|
|
1012
|
+
"so far",
|
|
1013
|
+
"as of",
|
|
1014
|
+
"may still",
|
|
1015
|
+
"might still",
|
|
1016
|
+
"still be registered",
|
|
1017
|
+
"has started",
|
|
1018
|
+
"have started"
|
|
1019
|
+
];
|
|
1020
|
+
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.';
|
|
1021
|
+
function isFunctionLike(t, node) {
|
|
1022
|
+
return t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunction(node) || t.isMethodDeclaration(node) || t.isConstructorDeclaration(node) || t.isGetAccessorDeclaration(node) || t.isSetAccessorDeclaration(node);
|
|
1023
|
+
}
|
|
1024
|
+
function walkSameTick(t, node, visit) {
|
|
1025
|
+
node.forEachChild((child) => {
|
|
1026
|
+
if (isFunctionLike(t, child) || t.isClassDeclaration(child) || t.isClassExpression(child)) return;
|
|
1027
|
+
visit(child);
|
|
1028
|
+
walkSameTick(t, child, visit);
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
function walkAll(t, node, visit) {
|
|
1032
|
+
node.forEachChild((child) => {
|
|
1033
|
+
visit(child);
|
|
1034
|
+
walkAll(t, child, visit);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
function calleeName(t, node) {
|
|
1038
|
+
if (!t.isCallExpression(node)) return void 0;
|
|
1039
|
+
const expr = node.expression;
|
|
1040
|
+
if (t.isIdentifier(expr)) return expr.text;
|
|
1041
|
+
if (t.isPropertyAccessExpression(expr) && t.isIdentifier(expr.name)) return expr.name.text;
|
|
1042
|
+
return void 0;
|
|
1043
|
+
}
|
|
1044
|
+
function loggerLevel(t, node) {
|
|
1045
|
+
if (!t.isCallExpression(node)) return void 0;
|
|
1046
|
+
const expr = node.expression;
|
|
1047
|
+
if (!t.isPropertyAccessExpression(expr) || !t.isIdentifier(expr.name)) return void 0;
|
|
1048
|
+
const level = expr.name.text;
|
|
1049
|
+
if (!ALL_LEVELS.has(level)) return void 0;
|
|
1050
|
+
const receiver = expr.expression;
|
|
1051
|
+
let receiverName;
|
|
1052
|
+
if (t.isIdentifier(receiver)) receiverName = receiver.text;
|
|
1053
|
+
else if (t.isPropertyAccessExpression(receiver) && t.isIdentifier(receiver.name)) receiverName = receiver.name.text;
|
|
1054
|
+
if (!receiverName) return void 0;
|
|
1055
|
+
return /^(logger|log|console)$/i.test(receiverName) ? level : void 0;
|
|
1056
|
+
}
|
|
1057
|
+
function literalText(t, node) {
|
|
1058
|
+
const parts = [];
|
|
1059
|
+
const take = (n) => {
|
|
1060
|
+
if (t.isStringLiteralLike(n)) parts.push(n.text);
|
|
1061
|
+
else if (t.isTemplateHead(n) || t.isTemplateMiddle(n) || t.isTemplateTail(n)) parts.push(n.text);
|
|
1062
|
+
};
|
|
1063
|
+
take(node);
|
|
1064
|
+
walkAll(t, node, take);
|
|
1065
|
+
return parts.join(" ").toLowerCase();
|
|
1066
|
+
}
|
|
1067
|
+
function mentionsSeal(t, node) {
|
|
1068
|
+
let found = false;
|
|
1069
|
+
const visit = (n) => {
|
|
1070
|
+
if (found) return;
|
|
1071
|
+
if (t.isIdentifier(n)) {
|
|
1072
|
+
const lower = n.text.toLowerCase();
|
|
1073
|
+
if (SEAL_MARKERS.some((m) => lower.includes(m))) found = true;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
visit(node);
|
|
1077
|
+
walkAll(t, node, visit);
|
|
1078
|
+
return found;
|
|
1079
|
+
}
|
|
1080
|
+
function moduleLevelMutableBindings(t, sf) {
|
|
1081
|
+
const names = /* @__PURE__ */ new Set();
|
|
1082
|
+
for (const st of sf.statements) {
|
|
1083
|
+
if (!t.isVariableStatement(st)) continue;
|
|
1084
|
+
if (st.declarationList.flags & t.NodeFlags.Const) continue;
|
|
1085
|
+
for (const d of st.declarationList.declarations) {
|
|
1086
|
+
if (t.isIdentifier(d.name)) names.add(d.name.text);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
return names;
|
|
1090
|
+
}
|
|
1091
|
+
function indexFunctionBodies(t, sf) {
|
|
1092
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1093
|
+
walkAll(t, sf, (node) => {
|
|
1094
|
+
if (t.isFunctionDeclaration(node) && node.name && node.body) byName.set(node.name.text, node.body);
|
|
1095
|
+
else if (t.isMethodDeclaration(node) && t.isIdentifier(node.name) && node.body) byName.set(node.name.text, node.body);
|
|
1096
|
+
else if (t.isVariableDeclaration(node) && t.isIdentifier(node.name) && node.initializer && (t.isArrowFunction(node.initializer) || t.isFunctionExpression(node.initializer)) && node.initializer.body) {
|
|
1097
|
+
byName.set(node.name.text, node.initializer.body);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
return byName;
|
|
1101
|
+
}
|
|
1102
|
+
function collectLifecycleUnits(t, sf) {
|
|
1103
|
+
const units = [];
|
|
1104
|
+
const readClass = (node) => {
|
|
1105
|
+
const phases = [];
|
|
1106
|
+
let hasLifecycle = false;
|
|
1107
|
+
for (const member of node.members) {
|
|
1108
|
+
if (t.isMethodDeclaration(member) && t.isIdentifier(member.name)) {
|
|
1109
|
+
if (member.name.text === "init" || member.name.text === "start") hasLifecycle = true;
|
|
1110
|
+
} 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))) {
|
|
1111
|
+
hasLifecycle = true;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (!hasLifecycle) return;
|
|
1115
|
+
for (const member of node.members) {
|
|
1116
|
+
if (t.isConstructorDeclaration(member) && member.body) {
|
|
1117
|
+
phases.push({ phase: "constructor", body: member.body });
|
|
1118
|
+
} else if (t.isMethodDeclaration(member) && t.isIdentifier(member.name) && member.body) {
|
|
1119
|
+
if (PRE_SEAL_PHASES.has(member.name.text)) phases.push({ phase: member.name.text, body: member.body });
|
|
1120
|
+
} 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) {
|
|
1121
|
+
phases.push({ phase: member.name.text, body: member.initializer.body });
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
if (phases.length) units.push({ label: node.name?.text ?? "<anonymous class>", phases });
|
|
1125
|
+
};
|
|
1126
|
+
const readObjectLiteral = (node) => {
|
|
1127
|
+
const phases = [];
|
|
1128
|
+
let name;
|
|
1129
|
+
let hasLifecycle = false;
|
|
1130
|
+
for (const prop of node.properties) {
|
|
1131
|
+
if (!t.isPropertyAssignment(prop) || !t.isIdentifier(prop.name)) continue;
|
|
1132
|
+
const key = prop.name.text;
|
|
1133
|
+
if (key === "name" && t.isStringLiteralLike(prop.initializer)) name = prop.initializer.text;
|
|
1134
|
+
if ((key === "init" || key === "start") && isFunctionLike(t, prop.initializer)) hasLifecycle = true;
|
|
1135
|
+
if (PRE_SEAL_PHASES.has(key) && (t.isArrowFunction(prop.initializer) || t.isFunctionExpression(prop.initializer)) && prop.initializer.body) {
|
|
1136
|
+
phases.push({ phase: key, body: prop.initializer.body });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (!name || !hasLifecycle || !phases.length) return;
|
|
1140
|
+
units.push({ label: `${name} (object plugin)`, phases });
|
|
1141
|
+
};
|
|
1142
|
+
walkAll(t, sf, (node) => {
|
|
1143
|
+
if (t.isClassDeclaration(node) || t.isClassExpression(node)) readClass(node);
|
|
1144
|
+
else if (t.isObjectLiteralExpression(node)) readObjectLiteral(node);
|
|
1145
|
+
});
|
|
1146
|
+
return units;
|
|
1147
|
+
}
|
|
1148
|
+
function findStartupRegistryVerdicts(source, options = {}) {
|
|
1149
|
+
if (!source || !source.trim()) return [];
|
|
1150
|
+
let mentionsAny = false;
|
|
1151
|
+
for (const probe of OPEN_VOCABULARY_PROBES.keys()) {
|
|
1152
|
+
if (source.includes(probe)) {
|
|
1153
|
+
mentionsAny = true;
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (!mentionsAny) return [];
|
|
1158
|
+
const t = loadTypeScript();
|
|
1159
|
+
const fileLabel = options.file ?? "source";
|
|
1160
|
+
let sf;
|
|
1161
|
+
try {
|
|
1162
|
+
sf = t.createSourceFile(fileLabel, source, t.ScriptTarget.Latest, true, t.ScriptKind.TS);
|
|
1163
|
+
} catch {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
const findings = [];
|
|
1167
|
+
const moduleBindings = moduleLevelMutableBindings(t, sf);
|
|
1168
|
+
const functionBodies = indexFunctionBodies(t, sf);
|
|
1169
|
+
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
1170
|
+
for (const unit of collectLifecycleUnits(t, sf)) {
|
|
1171
|
+
for (const { phase, body } of unit.phases) {
|
|
1172
|
+
const scopes = [];
|
|
1173
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1174
|
+
const collect = (b, depth) => {
|
|
1175
|
+
scopes.push(b);
|
|
1176
|
+
if (depth >= 2) return;
|
|
1177
|
+
walkSameTick(t, b, (child) => {
|
|
1178
|
+
const name = calleeName(t, child);
|
|
1179
|
+
if (!name || seenNames.has(name)) return;
|
|
1180
|
+
const helper = functionBodies.get(name);
|
|
1181
|
+
if (!helper) return;
|
|
1182
|
+
seenNames.add(name);
|
|
1183
|
+
collect(helper, depth + 1);
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1186
|
+
collect(body, 0);
|
|
1187
|
+
const reads = [];
|
|
1188
|
+
for (const scope of scopes) {
|
|
1189
|
+
walkSameTick(t, scope, (node) => {
|
|
1190
|
+
const name = calleeName(t, node);
|
|
1191
|
+
if (name && OPEN_VOCABULARY_PROBES.has(name)) reads.push({ probe: name, node });
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (!reads.length) continue;
|
|
1195
|
+
if (scopes.some((scope) => mentionsSeal(t, scope))) continue;
|
|
1196
|
+
const records = [];
|
|
1197
|
+
for (const scope of scopes) {
|
|
1198
|
+
walkSameTick(t, scope, (node) => {
|
|
1199
|
+
const level = loggerLevel(t, node);
|
|
1200
|
+
if (level && VERDICT_LEVELS.has(level)) {
|
|
1201
|
+
records.push({ kind: "announced", detail: `${level} log`, line: lineOf(node), node });
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const cn = calleeName(t, node);
|
|
1205
|
+
if (cn && PERSISTENCE_CALLEES.has(cn)) {
|
|
1206
|
+
records.push({ kind: "persisted", detail: `${cn}()`, line: lineOf(node), node });
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (t.isBinaryExpression(node) && node.operatorToken.kind === t.SyntaxKind.EqualsToken) {
|
|
1210
|
+
const lhs = node.left;
|
|
1211
|
+
let target;
|
|
1212
|
+
if (t.isPropertyAccessExpression(lhs) && t.isIdentifier(lhs.name)) {
|
|
1213
|
+
target = lhs.expression.kind === t.SyntaxKind.ThisKeyword ? `this.${lhs.name.text}` : `<obj>.${lhs.name.text}`;
|
|
1214
|
+
} else if (t.isIdentifier(lhs) && moduleBindings.has(lhs.text)) {
|
|
1215
|
+
target = `module-level \`${lhs.text}\``;
|
|
1216
|
+
}
|
|
1217
|
+
if (target) records.push({ kind: "cached", detail: target, line: lineOf(node), node });
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
if (!records.length) continue;
|
|
1222
|
+
const read = reads[0];
|
|
1223
|
+
const record = records[0];
|
|
1224
|
+
const where = `${unit.label}.${phase}${phase === "constructor" ? "" : "()"}`;
|
|
1225
|
+
const note = OPEN_VOCABULARY_PROBES.get(read.probe);
|
|
1226
|
+
const phaseNote = PRE_SEAL_PHASES.get(phase);
|
|
1227
|
+
findings.push({
|
|
1228
|
+
severity: "warning",
|
|
1229
|
+
rule: STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
1230
|
+
where,
|
|
1231
|
+
path: `${fileLabel}:${lineOf(read.node)}`,
|
|
1232
|
+
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).`,
|
|
1233
|
+
hint: STARTUP_VERDICT_HINT
|
|
1234
|
+
});
|
|
1235
|
+
for (const rec of records) {
|
|
1236
|
+
if (rec.kind !== "announced") continue;
|
|
1237
|
+
const text = literalText(t, rec.node);
|
|
1238
|
+
if (!text) continue;
|
|
1239
|
+
if (HEDGE_PHRASES.some((h) => text.includes(h))) continue;
|
|
1240
|
+
const hit = ASSERTIVE_PHRASES.find((p) => text.includes(p));
|
|
1241
|
+
if (!hit) continue;
|
|
1242
|
+
findings.push({
|
|
1243
|
+
severity: "warning",
|
|
1244
|
+
rule: STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
1245
|
+
where,
|
|
1246
|
+
path: `${fileLabel}:${rec.line}`,
|
|
1247
|
+
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.`,
|
|
1248
|
+
hint: STARTUP_VERDICT_HINT
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return findings;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
862
1256
|
// src/validate-list-view-mode.ts
|
|
863
1257
|
var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
|
|
864
1258
|
function asArray3(v) {
|
|
@@ -1002,6 +1396,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
1002
1396
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
1003
1397
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
1004
1398
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
1399
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
1005
1400
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
1006
1401
|
function asArray4(v) {
|
|
1007
1402
|
if (Array.isArray(v)) return v;
|
|
@@ -1019,6 +1414,11 @@ function renderNonObject(v) {
|
|
|
1019
1414
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
1020
1415
|
return `a ${t}`;
|
|
1021
1416
|
}
|
|
1417
|
+
function renderTriggerToken(v) {
|
|
1418
|
+
if (typeof v === "string") return `'${v}'`;
|
|
1419
|
+
const json = JSON.stringify(v);
|
|
1420
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
1421
|
+
}
|
|
1022
1422
|
function startNodeOf(flow) {
|
|
1023
1423
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
1024
1424
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -1138,6 +1538,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1138
1538
|
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.`
|
|
1139
1539
|
});
|
|
1140
1540
|
}
|
|
1541
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
1542
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
1543
|
+
const hasTriggerType = config.triggerType != null;
|
|
1544
|
+
findings.push({
|
|
1545
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
1546
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
1547
|
+
// with no registry lookup in them — so no installed package can make
|
|
1548
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
1549
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
1550
|
+
// record-change trigger itself, and it still would not help, because
|
|
1551
|
+
// the flow never reaches the point of asking for one.
|
|
1552
|
+
severity: "error",
|
|
1553
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
1554
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1555
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
1556
|
+
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.`,
|
|
1557
|
+
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.`
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1141
1560
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
1142
1561
|
findings.push({
|
|
1143
1562
|
severity: "warning",
|
|
@@ -1907,13 +2326,13 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
1907
2326
|
}
|
|
1908
2327
|
|
|
1909
2328
|
// src/validate-react-pages.ts
|
|
1910
|
-
import { createRequire } from "module";
|
|
2329
|
+
import { createRequire as createRequire2 } from "module";
|
|
1911
2330
|
var cachedTransform = null;
|
|
1912
2331
|
function loadSucraseTransform() {
|
|
1913
2332
|
if (cachedTransform) return cachedTransform;
|
|
1914
2333
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
1915
2334
|
try {
|
|
1916
|
-
cachedTransform =
|
|
2335
|
+
cachedTransform = createRequire2(anchor)("sucrase").transform;
|
|
1917
2336
|
} catch (err) {
|
|
1918
2337
|
throw new Error(
|
|
1919
2338
|
`@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.`
|
|
@@ -1960,7 +2379,7 @@ function validateReactPages(stack) {
|
|
|
1960
2379
|
}
|
|
1961
2380
|
|
|
1962
2381
|
// src/validate-react-page-props.ts
|
|
1963
|
-
import { createRequire as
|
|
2382
|
+
import { createRequire as createRequire3 } from "module";
|
|
1964
2383
|
import {
|
|
1965
2384
|
REACT_BLOCKS,
|
|
1966
2385
|
RECORD_CONTEXT_BLOCK_TAGS,
|
|
@@ -1975,6 +2394,7 @@ import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
|
1975
2394
|
// src/validate-searchable-fields.ts
|
|
1976
2395
|
import {
|
|
1977
2396
|
resolveSearchFieldResolution,
|
|
2397
|
+
isVirtualSearchField,
|
|
1978
2398
|
SEARCHABLE_TEXTUAL_TYPES,
|
|
1979
2399
|
SEARCHABLE_ENUM_TYPES,
|
|
1980
2400
|
SEARCH_AUTO_EXCLUDED_FIELDS
|
|
@@ -2092,7 +2512,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2092
2512
|
where,
|
|
2093
2513
|
path: `${path}[${i}]`,
|
|
2094
2514
|
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)),
|
|
2095
|
-
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
|
|
2515
|
+
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(", ")}.` : "")
|
|
2516
|
+
});
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
if (isVirtualSearchField(target.fields[name])) {
|
|
2520
|
+
const vtype = target.fields[name]?.type;
|
|
2521
|
+
findings.push({
|
|
2522
|
+
severity: "error",
|
|
2523
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
2524
|
+
where,
|
|
2525
|
+
path: `${path}[${i}]`,
|
|
2526
|
+
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).`,
|
|
2527
|
+
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).`
|
|
2096
2528
|
});
|
|
2097
2529
|
continue;
|
|
2098
2530
|
}
|
|
@@ -2127,7 +2559,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2127
2559
|
where,
|
|
2128
2560
|
path: `${path}[${i}]`,
|
|
2129
2561
|
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...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).`,
|
|
2130
|
-
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
|
|
2562
|
+
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.`
|
|
2131
2563
|
});
|
|
2132
2564
|
}
|
|
2133
2565
|
return findings;
|
|
@@ -2327,8 +2759,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2327
2759
|
"element:number": { props: ["field"] },
|
|
2328
2760
|
"element:filter": { props: ["fields"] },
|
|
2329
2761
|
"element:form": { props: ["fields"] },
|
|
2330
|
-
//
|
|
2331
|
-
|
|
2762
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2763
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
2764
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
2765
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
2766
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
2767
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
2768
|
+
//
|
|
2769
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
2770
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
2771
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
2772
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
2773
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
2774
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
2775
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
2776
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2332
2777
|
};
|
|
2333
2778
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2334
2779
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2488,18 +2933,18 @@ function describeIssue(issue, root, depth = 0) {
|
|
|
2488
2933
|
}
|
|
2489
2934
|
|
|
2490
2935
|
// src/validate-react-page-props.ts
|
|
2491
|
-
var
|
|
2492
|
-
function
|
|
2493
|
-
if (
|
|
2936
|
+
var cachedTs2 = null;
|
|
2937
|
+
function loadTypeScript2() {
|
|
2938
|
+
if (cachedTs2) return cachedTs2;
|
|
2494
2939
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
2495
2940
|
try {
|
|
2496
|
-
|
|
2941
|
+
cachedTs2 = createRequire3(anchor)("typescript");
|
|
2497
2942
|
} catch (err) {
|
|
2498
2943
|
throw new Error(
|
|
2499
2944
|
`@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.`
|
|
2500
2945
|
);
|
|
2501
2946
|
}
|
|
2502
|
-
return
|
|
2947
|
+
return cachedTs2;
|
|
2503
2948
|
}
|
|
2504
2949
|
var asArray12 = (v) => Array.isArray(v) ? v : [];
|
|
2505
2950
|
var BLOCKS = new Map(
|
|
@@ -2632,7 +3077,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
2632
3077
|
"warning",
|
|
2633
3078
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2634
3079
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
2635
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
3080
|
+
"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."
|
|
2636
3081
|
);
|
|
2637
3082
|
}
|
|
2638
3083
|
const parsed = ChartAggregateSchema.safeParse(raw);
|
|
@@ -2892,7 +3337,7 @@ function validateReactPageProps(stack) {
|
|
|
2892
3337
|
const source = page.source;
|
|
2893
3338
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
2894
3339
|
const name = String(page.name ?? `#${p}`);
|
|
2895
|
-
const tsc =
|
|
3340
|
+
const tsc = loadTypeScript2();
|
|
2896
3341
|
let sf;
|
|
2897
3342
|
try {
|
|
2898
3343
|
sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
|
|
@@ -3150,7 +3595,7 @@ function validateSemanticRoles(stack) {
|
|
|
3150
3595
|
(h) => typeof h === "string" && h.length > 0
|
|
3151
3596
|
);
|
|
3152
3597
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
3153
|
-
const declaredTitle = [obj.nameField, obj.
|
|
3598
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
3154
3599
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
3155
3600
|
const stripSet = new Set(
|
|
3156
3601
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -3175,6 +3620,60 @@ function validateSemanticRoles(stack) {
|
|
|
3175
3620
|
return findings;
|
|
3176
3621
|
}
|
|
3177
3622
|
|
|
3623
|
+
// src/collection-entries.ts
|
|
3624
|
+
function isRec8(v) {
|
|
3625
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3626
|
+
}
|
|
3627
|
+
function collectionEntries(v, base) {
|
|
3628
|
+
if (Array.isArray(v)) {
|
|
3629
|
+
const out = [];
|
|
3630
|
+
for (let i = 0; i < v.length; i++) {
|
|
3631
|
+
if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3632
|
+
}
|
|
3633
|
+
return out;
|
|
3634
|
+
}
|
|
3635
|
+
if (isRec8(v)) {
|
|
3636
|
+
return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3637
|
+
}
|
|
3638
|
+
return [];
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
// src/view-walk.ts
|
|
3642
|
+
function isRec9(v) {
|
|
3643
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3644
|
+
}
|
|
3645
|
+
function strName5(v) {
|
|
3646
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3647
|
+
}
|
|
3648
|
+
function viewObjectName(view) {
|
|
3649
|
+
return strName5(view.objectName) ?? strName5(view.object) ?? (isRec9(view.data) ? strName5(view.data.object) : void 0);
|
|
3650
|
+
}
|
|
3651
|
+
function viewContainerSites(view, basePath) {
|
|
3652
|
+
if (!isRec9(view)) return [];
|
|
3653
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3654
|
+
if (isRec9(view.form)) {
|
|
3655
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3656
|
+
}
|
|
3657
|
+
for (const key of ["listViews", "formViews"]) {
|
|
3658
|
+
const container = view[key];
|
|
3659
|
+
if (!isRec9(container)) continue;
|
|
3660
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
3661
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
3662
|
+
if (!isRec9(sub)) continue;
|
|
3663
|
+
sites.push({
|
|
3664
|
+
view: sub,
|
|
3665
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
3666
|
+
surface: `${key}.${subKey}`,
|
|
3667
|
+
kind
|
|
3668
|
+
});
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return sites;
|
|
3672
|
+
}
|
|
3673
|
+
function formViewSites(view, basePath) {
|
|
3674
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3178
3677
|
// src/validate-form-layout.ts
|
|
3179
3678
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
3180
3679
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
@@ -3185,6 +3684,12 @@ function asArray16(v) {
|
|
|
3185
3684
|
}
|
|
3186
3685
|
return [];
|
|
3187
3686
|
}
|
|
3687
|
+
function isRec10(v) {
|
|
3688
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3689
|
+
}
|
|
3690
|
+
function strName6(v) {
|
|
3691
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3692
|
+
}
|
|
3188
3693
|
function fieldNameOf(entry) {
|
|
3189
3694
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
3190
3695
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -3193,13 +3698,6 @@ function fieldNameOf(entry) {
|
|
|
3193
3698
|
}
|
|
3194
3699
|
return null;
|
|
3195
3700
|
}
|
|
3196
|
-
function boundObject(view) {
|
|
3197
|
-
const data = view.data;
|
|
3198
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
3199
|
-
return data.object;
|
|
3200
|
-
}
|
|
3201
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
3202
|
-
}
|
|
3203
3701
|
function validateFormLayout(stack) {
|
|
3204
3702
|
const findings = [];
|
|
3205
3703
|
const objectFields = /* @__PURE__ */ new Map();
|
|
@@ -3209,44 +3707,44 @@ function validateFormLayout(stack) {
|
|
|
3209
3707
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
3210
3708
|
objectFields.set(name, new Set(fields));
|
|
3211
3709
|
}
|
|
3212
|
-
const
|
|
3213
|
-
|
|
3214
|
-
const
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
}
|
|
3710
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
3711
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? viewPath;
|
|
3712
|
+
const containerObject = viewObjectName(view);
|
|
3713
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
3714
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
3715
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
3716
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
3717
|
+
for (const bucket of ["sections", "groups"]) {
|
|
3718
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
3719
|
+
for (let s = 0; s < sections.length; s++) {
|
|
3720
|
+
const sec = sections[s];
|
|
3721
|
+
const secFields = isRec10(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
3722
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
3723
|
+
const entry = secFields[f];
|
|
3724
|
+
const fname = fieldNameOf(entry);
|
|
3725
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
3726
|
+
if (fname && known && !known.has(fname)) {
|
|
3727
|
+
findings.push({
|
|
3728
|
+
severity: "warning",
|
|
3729
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
3730
|
+
where,
|
|
3731
|
+
path: fpath,
|
|
3732
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
3733
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
const colSpan = isRec10(entry) ? entry.colSpan : void 0;
|
|
3737
|
+
if (colSpan != null) {
|
|
3738
|
+
findings.push({
|
|
3739
|
+
severity: "warning",
|
|
3740
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
3741
|
+
where,
|
|
3742
|
+
path: `${fpath}.colSpan`,
|
|
3743
|
+
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`,
|
|
3744
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3250
3748
|
}
|
|
3251
3749
|
}
|
|
3252
3750
|
}
|
|
@@ -3255,17 +3753,17 @@ function validateFormLayout(stack) {
|
|
|
3255
3753
|
}
|
|
3256
3754
|
|
|
3257
3755
|
// src/validate-visibility-predicates.ts
|
|
3258
|
-
|
|
3756
|
+
import {
|
|
3757
|
+
collectCelRootIdentifiers as collectCelRootIdentifiers2,
|
|
3758
|
+
firstUndeclaredReference,
|
|
3759
|
+
parseCelToAst as parseCelToAst2,
|
|
3760
|
+
parseCelToAstWithReason
|
|
3761
|
+
} from "@objectstack/formula";
|
|
3259
3762
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
3763
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
3764
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
3765
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
3260
3766
|
var CANONICAL = "visibleWhen";
|
|
3261
|
-
var ALIASES = ["visibleOn", "visibility"];
|
|
3262
|
-
function asArray17(v) {
|
|
3263
|
-
if (Array.isArray(v)) return v;
|
|
3264
|
-
if (v && typeof v === "object") {
|
|
3265
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3266
|
-
}
|
|
3267
|
-
return [];
|
|
3268
|
-
}
|
|
3269
3767
|
function predicateSource(v) {
|
|
3270
3768
|
if (typeof v === "string") return v;
|
|
3271
3769
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
@@ -3276,6 +3774,67 @@ function predicateSource(v) {
|
|
|
3276
3774
|
function usesRoot(source, root) {
|
|
3277
3775
|
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
3278
3776
|
}
|
|
3777
|
+
function withoutStringLiterals(source) {
|
|
3778
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
3779
|
+
}
|
|
3780
|
+
var NON_CEL_SPELLINGS = [
|
|
3781
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
3782
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
3783
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
3784
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
3785
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
3786
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
3787
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
3788
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
3789
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
3790
|
+
];
|
|
3791
|
+
function quoteSource(source) {
|
|
3792
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
3793
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
3794
|
+
}
|
|
3795
|
+
function celRefusal(source) {
|
|
3796
|
+
if (!source.trim()) return null;
|
|
3797
|
+
const parsed = parseCelToAstWithReason(source);
|
|
3798
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
3799
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
3800
|
+
const identifiers = collectCelRootIdentifiers2(source);
|
|
3801
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
3802
|
+
const scannable = withoutStringLiterals(source);
|
|
3803
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
3804
|
+
}
|
|
3805
|
+
function boundName(overrun) {
|
|
3806
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
3807
|
+
}
|
|
3808
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
3809
|
+
function isNode2(v) {
|
|
3810
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
3811
|
+
}
|
|
3812
|
+
function namespaceRoots(node, out) {
|
|
3813
|
+
if (Array.isArray(node)) {
|
|
3814
|
+
for (const child of node) namespaceRoots(child, out);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
if (!isNode2(node)) return;
|
|
3818
|
+
const args = node.args;
|
|
3819
|
+
if (Array.isArray(args)) {
|
|
3820
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
3821
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
3822
|
+
out.add(receiver.args);
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
namespaceRoots(args, out);
|
|
3826
|
+
}
|
|
3827
|
+
function firstBareIdentifier(source) {
|
|
3828
|
+
const ast = parseCelToAst2(source);
|
|
3829
|
+
if (!ast) return null;
|
|
3830
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
3831
|
+
namespaceRoots(ast, rooted);
|
|
3832
|
+
return firstUndeclaredReference(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
|
|
3833
|
+
}
|
|
3834
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
3835
|
+
runtime: "record",
|
|
3836
|
+
metadata: "data"
|
|
3837
|
+
};
|
|
3279
3838
|
var MISLAYER_BY_LAYER = {
|
|
3280
3839
|
runtime: {
|
|
3281
3840
|
forbiddenRoot: "data",
|
|
@@ -3289,18 +3848,6 @@ var MISLAYER_BY_LAYER = {
|
|
|
3289
3848
|
}
|
|
3290
3849
|
};
|
|
3291
3850
|
function checkElement(el, where, path, layer, findings) {
|
|
3292
|
-
for (const alias of ALIASES) {
|
|
3293
|
-
if (el[alias] !== void 0) {
|
|
3294
|
-
findings.push({
|
|
3295
|
-
severity: "warning",
|
|
3296
|
-
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
3297
|
-
where,
|
|
3298
|
-
path: `${path}.${alias}`,
|
|
3299
|
-
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\`.`,
|
|
3300
|
-
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
3301
|
-
});
|
|
3302
|
-
}
|
|
3303
|
-
}
|
|
3304
3851
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
3305
3852
|
const source = predicateSource(raw);
|
|
3306
3853
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -3314,6 +3861,43 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
3314
3861
|
hint: rule.hint
|
|
3315
3862
|
});
|
|
3316
3863
|
}
|
|
3864
|
+
const refusal = source ? celRefusal(source) : null;
|
|
3865
|
+
if (source && refusal?.kind === "bounds") {
|
|
3866
|
+
const bound = boundName(refusal.overrun);
|
|
3867
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
3868
|
+
findings.push({
|
|
3869
|
+
severity: "error",
|
|
3870
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
3871
|
+
where,
|
|
3872
|
+
path,
|
|
3873
|
+
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).`,
|
|
3874
|
+
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.`
|
|
3875
|
+
});
|
|
3876
|
+
}
|
|
3877
|
+
if (source && refusal?.kind === "syntax") {
|
|
3878
|
+
findings.push({
|
|
3879
|
+
severity: "error",
|
|
3880
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
3881
|
+
where,
|
|
3882
|
+
path,
|
|
3883
|
+
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).`,
|
|
3884
|
+
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\`).`
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
if (source && !refusal) {
|
|
3888
|
+
const bare = firstBareIdentifier(source);
|
|
3889
|
+
if (bare) {
|
|
3890
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
3891
|
+
findings.push({
|
|
3892
|
+
severity: "error",
|
|
3893
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
3894
|
+
where,
|
|
3895
|
+
path,
|
|
3896
|
+
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).`,
|
|
3897
|
+
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`).")
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3317
3901
|
}
|
|
3318
3902
|
function isFieldObject(entry) {
|
|
3319
3903
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
@@ -3321,43 +3905,304 @@ function isFieldObject(entry) {
|
|
|
3321
3905
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
3322
3906
|
const layer = opts.layer ?? "runtime";
|
|
3323
3907
|
const findings = [];
|
|
3324
|
-
const
|
|
3325
|
-
|
|
3326
|
-
const view
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
3908
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
3909
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
3910
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
3911
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
3912
|
+
for (const bucket of ["sections", "groups"]) {
|
|
3913
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
3914
|
+
for (let s = 0; s < sections.length; s++) {
|
|
3915
|
+
const sec = sections[s];
|
|
3916
|
+
if (!sec || typeof sec !== "object") continue;
|
|
3917
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
3918
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
3919
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
3920
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
3921
|
+
const entry = secFields[f];
|
|
3922
|
+
if (isFieldObject(entry)) {
|
|
3923
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
3924
|
+
}
|
|
3342
3925
|
}
|
|
3343
3926
|
}
|
|
3344
3927
|
}
|
|
3345
3928
|
}
|
|
3346
3929
|
}
|
|
3347
|
-
const
|
|
3348
|
-
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3930
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3931
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
3932
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
3933
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3934
|
+
checkElement(walked.component, where, walked.path, layer, findings);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
return findings;
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// src/validate-predicate-path-refs.ts
|
|
3941
|
+
import { parseCelToAst as parseCelToAst3 } from "@objectstack/formula";
|
|
3942
|
+
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
3943
|
+
import { findClosestMatches, formatSuggestion } from "@objectstack/spec";
|
|
3944
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
3945
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
3946
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
3947
|
+
var ROOT = "data";
|
|
3948
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
3949
|
+
function defOf(schema) {
|
|
3950
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
3951
|
+
const s = schema;
|
|
3952
|
+
return s.def ?? s._def;
|
|
3953
|
+
}
|
|
3954
|
+
function peel(schema, depth = 0) {
|
|
3955
|
+
if (!schema || depth > 25) return schema;
|
|
3956
|
+
const d = defOf(schema);
|
|
3957
|
+
if (!d) return schema;
|
|
3958
|
+
switch (d.type) {
|
|
3959
|
+
case "optional":
|
|
3960
|
+
case "nullable":
|
|
3961
|
+
case "default":
|
|
3962
|
+
case "prefault":
|
|
3963
|
+
case "readonly":
|
|
3964
|
+
case "catch":
|
|
3965
|
+
case "nonoptional":
|
|
3966
|
+
return peel(d.innerType, depth + 1);
|
|
3967
|
+
case "lazy":
|
|
3968
|
+
return peel(d.getter(), depth + 1);
|
|
3969
|
+
case "pipe": {
|
|
3970
|
+
const inner = peel(d.in, depth + 1);
|
|
3971
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
3972
|
+
}
|
|
3973
|
+
default:
|
|
3974
|
+
return schema;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
function optionsOf(d) {
|
|
3978
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
3979
|
+
}
|
|
3980
|
+
function keysOf(schema, depth = 0) {
|
|
3981
|
+
if (depth > 25) return null;
|
|
3982
|
+
const u = peel(schema);
|
|
3983
|
+
const d = defOf(u);
|
|
3984
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
3985
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
3986
|
+
const all = /* @__PURE__ */ new Set();
|
|
3987
|
+
let keyBearing = false;
|
|
3988
|
+
for (const option of optionsOf(d)) {
|
|
3989
|
+
const k = keysOf(option, depth + 1);
|
|
3990
|
+
if (!k) continue;
|
|
3991
|
+
keyBearing = true;
|
|
3992
|
+
for (const key of k) all.add(key);
|
|
3993
|
+
}
|
|
3994
|
+
return keyBearing ? [...all] : null;
|
|
3995
|
+
}
|
|
3996
|
+
if (d?.type === "intersection") {
|
|
3997
|
+
const left = keysOf(d.left, depth + 1);
|
|
3998
|
+
const right = keysOf(d.right, depth + 1);
|
|
3999
|
+
if (!left && !right) return null;
|
|
4000
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
4001
|
+
}
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
function propertyOf(schema, key, depth = 0) {
|
|
4005
|
+
if (depth > 25) return void 0;
|
|
4006
|
+
const u = peel(schema);
|
|
4007
|
+
const d = defOf(u);
|
|
4008
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
4009
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
4010
|
+
for (const option of optionsOf(d)) {
|
|
4011
|
+
const found = propertyOf(option, key, depth + 1);
|
|
4012
|
+
if (found !== void 0) return found;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
if (d?.type === "intersection") {
|
|
4016
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
4017
|
+
}
|
|
4018
|
+
return void 0;
|
|
4019
|
+
}
|
|
4020
|
+
function rowScopeOf(scope, key) {
|
|
4021
|
+
const prop = propertyOf(scope, key);
|
|
4022
|
+
if (prop === void 0) return void 0;
|
|
4023
|
+
let node = peel(prop);
|
|
4024
|
+
for (let i = 0; i < 25; i++) {
|
|
4025
|
+
const d = defOf(node);
|
|
4026
|
+
if (d?.type === "array") node = peel(d.element);
|
|
4027
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
4028
|
+
else return node;
|
|
4029
|
+
}
|
|
4030
|
+
return node;
|
|
4031
|
+
}
|
|
4032
|
+
function stepInto(scope, segment) {
|
|
4033
|
+
const u = peel(scope);
|
|
4034
|
+
const d = defOf(u);
|
|
4035
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
4036
|
+
const declared = keysOf(u);
|
|
4037
|
+
if (declared === null) return { kind: "opaque" };
|
|
4038
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
4039
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
4040
|
+
}
|
|
4041
|
+
function isNode3(v) {
|
|
4042
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4043
|
+
}
|
|
4044
|
+
function memberChain(node) {
|
|
4045
|
+
if (!isNode3(node)) return null;
|
|
4046
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
4047
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
4048
|
+
const head = memberChain(node.args[0]);
|
|
4049
|
+
return head ? [...head, node.args[1]] : null;
|
|
4050
|
+
}
|
|
4051
|
+
return null;
|
|
4052
|
+
}
|
|
4053
|
+
function rootedPaths(node, out) {
|
|
4054
|
+
if (Array.isArray(node)) {
|
|
4055
|
+
for (const child of node) rootedPaths(child, out);
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
if (!isNode3(node)) return;
|
|
4059
|
+
if (node.op === ".") {
|
|
4060
|
+
const chain = memberChain(node);
|
|
4061
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
4062
|
+
out.push(chain.slice(1));
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
rootedPaths(node.args, out);
|
|
4067
|
+
}
|
|
4068
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
4069
|
+
if (Array.isArray(node)) {
|
|
4070
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
if (!isNode3(node)) return;
|
|
4074
|
+
const args = node.args;
|
|
4075
|
+
if (Array.isArray(args)) {
|
|
4076
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
4077
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
4078
|
+
excluded.add(receiver.args);
|
|
4079
|
+
}
|
|
4080
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
4081
|
+
const macroArgs = args[2];
|
|
4082
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
4083
|
+
const bound = macroArgs[0];
|
|
4084
|
+
if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
4085
|
+
excluded.add(bound.args);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
4091
|
+
values.add(node.args);
|
|
4092
|
+
return;
|
|
4093
|
+
}
|
|
4094
|
+
classifyIdentifiers(args, values, excluded);
|
|
4095
|
+
}
|
|
4096
|
+
function predicateSource2(v) {
|
|
4097
|
+
if (typeof v === "string") return v;
|
|
4098
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
4099
|
+
return v.source;
|
|
4100
|
+
}
|
|
4101
|
+
return void 0;
|
|
4102
|
+
}
|
|
4103
|
+
function isRec11(v) {
|
|
4104
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4105
|
+
}
|
|
4106
|
+
function schemaIdOf(view) {
|
|
4107
|
+
const data = view.data;
|
|
4108
|
+
if (!isRec11(data)) return void 0;
|
|
4109
|
+
if (data.provider !== "schema") return void 0;
|
|
4110
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
4111
|
+
}
|
|
4112
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
4113
|
+
const ast = parseCelToAst3(source);
|
|
4114
|
+
if (!ast) return;
|
|
4115
|
+
const paths = [];
|
|
4116
|
+
rootedPaths(ast, paths);
|
|
4117
|
+
for (const segments of paths) {
|
|
4118
|
+
let cursor = scope;
|
|
4119
|
+
const walked = [];
|
|
4120
|
+
for (const segment of segments) {
|
|
4121
|
+
const step = stepInto(cursor, segment);
|
|
4122
|
+
if (step.kind === "opaque") break;
|
|
4123
|
+
if (step.kind === "undeclared") {
|
|
4124
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
4125
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
4126
|
+
findings.push({
|
|
4127
|
+
severity: "error",
|
|
4128
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
4129
|
+
where,
|
|
4130
|
+
path,
|
|
4131
|
+
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).`,
|
|
4132
|
+
hint: `${formatSuggestion(findClosestMatches(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
4133
|
+
});
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
4136
|
+
walked.push(segment);
|
|
4137
|
+
cursor = step.next;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
const declaredHere = keysOf(scope);
|
|
4141
|
+
if (!declaredHere) return;
|
|
4142
|
+
const values = /* @__PURE__ */ new Set();
|
|
4143
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
4144
|
+
classifyIdentifiers(ast, values, excluded);
|
|
4145
|
+
for (const id of values) {
|
|
4146
|
+
if (excluded.has(id) || !declaredHere.includes(id)) continue;
|
|
4147
|
+
findings.push({
|
|
4148
|
+
severity: "error",
|
|
4149
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
4150
|
+
where,
|
|
4151
|
+
path,
|
|
4152
|
+
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).`,
|
|
4153
|
+
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).`
|
|
4154
|
+
});
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
4158
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
4159
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4160
|
+
const entry = entries[i];
|
|
4161
|
+
if (!isRec11(entry)) continue;
|
|
4162
|
+
const path = `${base}[${i}]`;
|
|
4163
|
+
for (const key of PREDICATE_KEYS) {
|
|
4164
|
+
const source = predicateSource2(entry[key]);
|
|
4165
|
+
if (source !== void 0 && source.trim()) {
|
|
4166
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
4167
|
+
break;
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
4171
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
4172
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
4177
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
|
|
4178
|
+
const findings = [];
|
|
4179
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
4180
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
4181
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
4182
|
+
const schemaId = schemaIdOf(site.view);
|
|
4183
|
+
if (!schemaId) continue;
|
|
4184
|
+
let root;
|
|
4185
|
+
try {
|
|
4186
|
+
root = resolveSchema(schemaId);
|
|
4187
|
+
} catch {
|
|
4188
|
+
continue;
|
|
4189
|
+
}
|
|
4190
|
+
if (!root) continue;
|
|
4191
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
4192
|
+
for (const bucket of ["sections", "groups"]) {
|
|
4193
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
4194
|
+
for (let s = 0; s < sections.length; s++) {
|
|
4195
|
+
const section = sections[s];
|
|
4196
|
+
if (!isRec11(section)) continue;
|
|
4197
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
4198
|
+
for (const key of PREDICATE_KEYS) {
|
|
4199
|
+
const source = predicateSource2(section[key]);
|
|
4200
|
+
if (source !== void 0 && source.trim()) {
|
|
4201
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
4202
|
+
break;
|
|
4203
|
+
}
|
|
4204
|
+
}
|
|
4205
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
3361
4206
|
}
|
|
3362
4207
|
}
|
|
3363
4208
|
}
|
|
@@ -3368,7 +4213,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
3368
4213
|
// src/validate-capability-references.ts
|
|
3369
4214
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
3370
4215
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
3371
|
-
function
|
|
4216
|
+
function asArray17(v) {
|
|
3372
4217
|
if (Array.isArray(v)) return v;
|
|
3373
4218
|
if (v && typeof v === "object") {
|
|
3374
4219
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3393,13 +4238,13 @@ function validateCapabilityReferences(stack) {
|
|
|
3393
4238
|
const findings = [];
|
|
3394
4239
|
if (!stack || typeof stack !== "object") return findings;
|
|
3395
4240
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
3396
|
-
for (const cap of
|
|
4241
|
+
for (const cap of asArray17(stack.capabilities)) {
|
|
3397
4242
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
3398
4243
|
}
|
|
3399
|
-
for (const ps of
|
|
4244
|
+
for (const ps of asArray17(stack.permissions)) {
|
|
3400
4245
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
3401
4246
|
}
|
|
3402
|
-
for (const seed of
|
|
4247
|
+
for (const seed of asArray17(stack.data)) {
|
|
3403
4248
|
if (seed.object !== "sys_capability") continue;
|
|
3404
4249
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
3405
4250
|
const name = rec?.name;
|
|
@@ -3418,7 +4263,7 @@ function validateCapabilityReferences(stack) {
|
|
|
3418
4263
|
hint
|
|
3419
4264
|
});
|
|
3420
4265
|
};
|
|
3421
|
-
const objects =
|
|
4266
|
+
const objects = asArray17(stack.objects);
|
|
3422
4267
|
for (let i = 0; i < objects.length; i++) {
|
|
3423
4268
|
const obj = objects[i];
|
|
3424
4269
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -3427,27 +4272,27 @@ function validateCapabilityReferences(stack) {
|
|
|
3427
4272
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
3428
4273
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
3429
4274
|
}
|
|
3430
|
-
const fields =
|
|
4275
|
+
const fields = asArray17(obj.fields);
|
|
3431
4276
|
for (const f of fields) {
|
|
3432
4277
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
3433
4278
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
3434
4279
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
3435
4280
|
}
|
|
3436
4281
|
}
|
|
3437
|
-
for (const [ai, action] of
|
|
4282
|
+
for (const [ai, action] of asArray17(obj.actions).entries()) {
|
|
3438
4283
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
3439
4284
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
3440
4285
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
3441
4286
|
}
|
|
3442
4287
|
}
|
|
3443
4288
|
}
|
|
3444
|
-
for (const [i, action] of
|
|
4289
|
+
for (const [i, action] of asArray17(stack.actions).entries()) {
|
|
3445
4290
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
3446
4291
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
3447
4292
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
3448
4293
|
}
|
|
3449
4294
|
}
|
|
3450
|
-
const apps =
|
|
4295
|
+
const apps = asArray17(stack.apps);
|
|
3451
4296
|
for (let i = 0; i < apps.length; i++) {
|
|
3452
4297
|
const app = apps[i];
|
|
3453
4298
|
if (!app || typeof app !== "object") continue;
|
|
@@ -3484,7 +4329,7 @@ import {
|
|
|
3484
4329
|
normalizeDecisionOutputs
|
|
3485
4330
|
} from "@objectstack/spec/automation";
|
|
3486
4331
|
import { BUILTIN_MEMBERSHIP_ROLES } from "@objectstack/spec";
|
|
3487
|
-
import { collectCelRootIdentifiers as
|
|
4332
|
+
import { collectCelRootIdentifiers as collectCelRootIdentifiers3 } from "@objectstack/formula";
|
|
3488
4333
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
3489
4334
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
3490
4335
|
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
@@ -3504,7 +4349,7 @@ var TYPE_FIX = {
|
|
|
3504
4349
|
business_unit: "department",
|
|
3505
4350
|
bu: "department"
|
|
3506
4351
|
};
|
|
3507
|
-
function
|
|
4352
|
+
function asArray18(v) {
|
|
3508
4353
|
if (Array.isArray(v)) return v;
|
|
3509
4354
|
if (v && typeof v === "object") {
|
|
3510
4355
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3514,7 +4359,7 @@ function asArray19(v) {
|
|
|
3514
4359
|
function validateApprovalApprovers(stack) {
|
|
3515
4360
|
const findings = [];
|
|
3516
4361
|
if (!stack || typeof stack !== "object") return findings;
|
|
3517
|
-
const flows =
|
|
4362
|
+
const flows = asArray18(stack.flows);
|
|
3518
4363
|
const validTypes = new Set(ApproverType.options);
|
|
3519
4364
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
3520
4365
|
const flow = flows[fi];
|
|
@@ -3559,7 +4404,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3559
4404
|
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.`
|
|
3560
4405
|
});
|
|
3561
4406
|
} else {
|
|
3562
|
-
const parsed =
|
|
4407
|
+
const parsed = collectCelRootIdentifiers3(source);
|
|
3563
4408
|
if (!parsed.ok) {
|
|
3564
4409
|
findings.push({
|
|
3565
4410
|
severity: "error",
|
|
@@ -3809,7 +4654,7 @@ var OWD_WIDTH = {
|
|
|
3809
4654
|
public_read: 1,
|
|
3810
4655
|
public_read_write: 2
|
|
3811
4656
|
};
|
|
3812
|
-
function
|
|
4657
|
+
function asArray19(v) {
|
|
3813
4658
|
if (Array.isArray(v)) return v;
|
|
3814
4659
|
if (v && typeof v === "object") {
|
|
3815
4660
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3835,7 +4680,7 @@ function refOf(def) {
|
|
|
3835
4680
|
return typeof r === "string" && r ? r : void 0;
|
|
3836
4681
|
}
|
|
3837
4682
|
function firstMasterDetailField(obj) {
|
|
3838
|
-
for (const f of
|
|
4683
|
+
for (const f of asArray19(obj.fields)) {
|
|
3839
4684
|
if (f.type === "master_detail") {
|
|
3840
4685
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
3841
4686
|
}
|
|
@@ -3848,8 +4693,8 @@ function grantsObjectAccess(p) {
|
|
|
3848
4693
|
function validateSecurityPosture(stack, opts) {
|
|
3849
4694
|
const findings = [];
|
|
3850
4695
|
if (!stack || typeof stack !== "object") return findings;
|
|
3851
|
-
const objects =
|
|
3852
|
-
const permissionSets =
|
|
4696
|
+
const objects = asArray19(stack.objects);
|
|
4697
|
+
const permissionSets = asArray19(stack.permissions);
|
|
3853
4698
|
for (let i = 0; i < objects.length; i++) {
|
|
3854
4699
|
const obj = objects[i];
|
|
3855
4700
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -3978,10 +4823,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
3978
4823
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
3979
4824
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
3980
4825
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
3981
|
-
for (const f of
|
|
4826
|
+
for (const f of asArray19(obj.fields)) {
|
|
3982
4827
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
3983
4828
|
}
|
|
3984
|
-
for (const [ai, action] of
|
|
4829
|
+
for (const [ai, action] of asArray19(obj.actions).entries()) {
|
|
3985
4830
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
3986
4831
|
}
|
|
3987
4832
|
}
|
|
@@ -3990,19 +4835,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
3990
4835
|
if (!ps || typeof ps !== "object") continue;
|
|
3991
4836
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
3992
4837
|
}
|
|
3993
|
-
for (const [i, pos] of
|
|
4838
|
+
for (const [i, pos] of asArray19(stack.positions).entries()) {
|
|
3994
4839
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
3995
4840
|
}
|
|
3996
|
-
for (const [i, app] of
|
|
4841
|
+
for (const [i, app] of asArray19(stack.apps).entries()) {
|
|
3997
4842
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
3998
4843
|
}
|
|
3999
|
-
for (const [i, book] of
|
|
4844
|
+
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
4000
4845
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
4001
4846
|
}
|
|
4002
4847
|
const stackSetNames = new Set(
|
|
4003
4848
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
4004
4849
|
);
|
|
4005
|
-
for (const [i, book] of
|
|
4850
|
+
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
4006
4851
|
const audience = book.audience;
|
|
4007
4852
|
if (!audience || typeof audience !== "object") continue;
|
|
4008
4853
|
const setName = audience.permissionSet;
|
|
@@ -4080,7 +4925,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4080
4925
|
}
|
|
4081
4926
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
4082
4927
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
4083
|
-
for (const [i, seed] of
|
|
4928
|
+
for (const [i, seed] of asArray19(stack.data).entries()) {
|
|
4084
4929
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
4085
4930
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
4086
4931
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -4125,7 +4970,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
|
4125
4970
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
4126
4971
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
4127
4972
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
4128
|
-
function
|
|
4973
|
+
function asArray20(v) {
|
|
4129
4974
|
if (Array.isArray(v)) return v;
|
|
4130
4975
|
if (v && typeof v === "object") {
|
|
4131
4976
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4155,9 +5000,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
4155
5000
|
function validateOrgAxisRedLines(stack) {
|
|
4156
5001
|
const findings = [];
|
|
4157
5002
|
const cfg = stack ?? {};
|
|
4158
|
-
const permissionSets =
|
|
5003
|
+
const permissionSets = asArray20(cfg.permissions);
|
|
4159
5004
|
permissionSets.forEach((ps, psIndex) => {
|
|
4160
|
-
|
|
5005
|
+
asArray20(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4161
5006
|
for (const clause of ["using", "check"]) {
|
|
4162
5007
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
4163
5008
|
findings.push({
|
|
@@ -4171,7 +5016,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4171
5016
|
}
|
|
4172
5017
|
});
|
|
4173
5018
|
});
|
|
4174
|
-
|
|
5019
|
+
asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4175
5020
|
const slots = [
|
|
4176
5021
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
4177
5022
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -4189,9 +5034,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4189
5034
|
}
|
|
4190
5035
|
});
|
|
4191
5036
|
const tenancyDisabledObjects = new Set(
|
|
4192
|
-
|
|
5037
|
+
asArray20(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4193
5038
|
);
|
|
4194
|
-
|
|
5039
|
+
asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4195
5040
|
const target = str(rule.object);
|
|
4196
5041
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
4197
5042
|
const sharedWith = rule.sharedWith;
|
|
@@ -4214,7 +5059,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4214
5059
|
import { compileCelToFilter } from "@objectstack/formula";
|
|
4215
5060
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
4216
5061
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
4217
|
-
function
|
|
5062
|
+
function asArray21(v) {
|
|
4218
5063
|
if (Array.isArray(v)) return v;
|
|
4219
5064
|
if (v && typeof v === "object") {
|
|
4220
5065
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4241,7 +5086,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
4241
5086
|
function validateSharingRuleEnforceability(stack) {
|
|
4242
5087
|
const findings = [];
|
|
4243
5088
|
const cfg = stack ?? {};
|
|
4244
|
-
|
|
5089
|
+
asArray21(cfg.sharingRules).forEach((rule, index) => {
|
|
4245
5090
|
const input = toCompilerInput(rule.condition);
|
|
4246
5091
|
if (input === null) return;
|
|
4247
5092
|
const result = compileCelToFilter(input, { variables: {} });
|
|
@@ -4277,10 +5122,16 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
4277
5122
|
}
|
|
4278
5123
|
|
|
4279
5124
|
// src/validate-rls-predicate-enforceability.ts
|
|
4280
|
-
import {
|
|
5125
|
+
import {
|
|
5126
|
+
isPushdownableCel,
|
|
5127
|
+
isSupportedRlsExpression,
|
|
5128
|
+
parseCelToAstWithReason as parseCelToAstWithReason2,
|
|
5129
|
+
sqlPredicateToCel
|
|
5130
|
+
} from "@objectstack/formula";
|
|
4281
5131
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
4282
5132
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
4283
|
-
|
|
5133
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
5134
|
+
function asArray22(v) {
|
|
4284
5135
|
if (Array.isArray(v)) return v;
|
|
4285
5136
|
if (v && typeof v === "object") {
|
|
4286
5137
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4291,6 +5142,13 @@ function str3(v) {
|
|
|
4291
5142
|
return typeof v === "string" ? v : "";
|
|
4292
5143
|
}
|
|
4293
5144
|
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.";
|
|
5145
|
+
function boundsOverrunOf(bridged) {
|
|
5146
|
+
const parsed = parseCelToAstWithReason2(bridged);
|
|
5147
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
5148
|
+
}
|
|
5149
|
+
function quote(source) {
|
|
5150
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
5151
|
+
}
|
|
4294
5152
|
function consequence(clause) {
|
|
4295
5153
|
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). ';
|
|
4296
5154
|
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.";
|
|
@@ -4298,20 +5156,36 @@ function consequence(clause) {
|
|
|
4298
5156
|
function validateRlsPredicateEnforceability(stack) {
|
|
4299
5157
|
const findings = [];
|
|
4300
5158
|
const cfg = stack ?? {};
|
|
4301
|
-
|
|
4302
|
-
|
|
5159
|
+
asArray22(cfg.permissions).forEach((ps, psIndex) => {
|
|
5160
|
+
asArray22(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4303
5161
|
for (const clause of ["using", "check"]) {
|
|
4304
5162
|
const source = str3(policy[clause]);
|
|
4305
5163
|
if (!source.trim()) continue;
|
|
4306
5164
|
if (isSupportedRlsExpression(source)) continue;
|
|
4307
|
-
const
|
|
5165
|
+
const bridged = sqlPredicateToCel(source);
|
|
5166
|
+
const why = isPushdownableCel(bridged);
|
|
4308
5167
|
const detail = why.ok ? "" : why.detail;
|
|
4309
5168
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
5169
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
4310
5170
|
const psName = str3(ps.name) || String(psIndex);
|
|
4311
5171
|
const policyName = str3(policy.name) || String(pIndex);
|
|
4312
5172
|
const object = str3(policy.object);
|
|
4313
5173
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
4314
5174
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
5175
|
+
if (overrun) {
|
|
5176
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
5177
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
5178
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
5179
|
+
findings.push({
|
|
5180
|
+
severity: "error",
|
|
5181
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
5182
|
+
where,
|
|
5183
|
+
path,
|
|
5184
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
5185
|
+
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).`
|
|
5186
|
+
});
|
|
5187
|
+
continue;
|
|
5188
|
+
}
|
|
4315
5189
|
if (parseError) {
|
|
4316
5190
|
findings.push({
|
|
4317
5191
|
severity: "error",
|
|
@@ -4340,14 +5214,14 @@ function validateRlsPredicateEnforceability(stack) {
|
|
|
4340
5214
|
// src/validate-dashboard-action-refs.ts
|
|
4341
5215
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
4342
5216
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
4343
|
-
function
|
|
5217
|
+
function asArray23(v) {
|
|
4344
5218
|
if (Array.isArray(v)) return v;
|
|
4345
5219
|
if (v && typeof v === "object") {
|
|
4346
5220
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4347
5221
|
}
|
|
4348
5222
|
return [];
|
|
4349
5223
|
}
|
|
4350
|
-
function
|
|
5224
|
+
function strName7(v) {
|
|
4351
5225
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4352
5226
|
}
|
|
4353
5227
|
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
@@ -4364,7 +5238,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
|
|
|
4364
5238
|
views: "views"
|
|
4365
5239
|
};
|
|
4366
5240
|
function viewContainerName(item) {
|
|
4367
|
-
return
|
|
5241
|
+
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);
|
|
4368
5242
|
}
|
|
4369
5243
|
function collectKnownTargets(stack) {
|
|
4370
5244
|
const actions = /* @__PURE__ */ new Set();
|
|
@@ -4374,22 +5248,22 @@ function collectKnownTargets(stack) {
|
|
|
4374
5248
|
const pages = /* @__PURE__ */ new Set();
|
|
4375
5249
|
const views = /* @__PURE__ */ new Set();
|
|
4376
5250
|
const collectNames = (v, into, name) => {
|
|
4377
|
-
for (const item of
|
|
5251
|
+
for (const item of asArray23(v)) {
|
|
4378
5252
|
if (!item || typeof item !== "object") continue;
|
|
4379
5253
|
const n = name(item);
|
|
4380
5254
|
if (n) into.add(n);
|
|
4381
5255
|
}
|
|
4382
5256
|
};
|
|
4383
|
-
collectNames(stack.actions, actions, (a) =>
|
|
4384
|
-
for (const obj of
|
|
5257
|
+
collectNames(stack.actions, actions, (a) => strName7(a.name));
|
|
5258
|
+
for (const obj of asArray23(stack.objects)) {
|
|
4385
5259
|
if (!obj || typeof obj !== "object") continue;
|
|
4386
|
-
const n =
|
|
5260
|
+
const n = strName7(obj.name);
|
|
4387
5261
|
if (n) objects.add(n);
|
|
4388
|
-
collectNames(obj.actions, actions, (a) =>
|
|
5262
|
+
collectNames(obj.actions, actions, (a) => strName7(a.name));
|
|
4389
5263
|
}
|
|
4390
|
-
collectNames(stack.reports, reports, (r) =>
|
|
4391
|
-
collectNames(stack.dashboards, dashboards, (d) =>
|
|
4392
|
-
collectNames(stack.pages, pages, (p) =>
|
|
5264
|
+
collectNames(stack.reports, reports, (r) => strName7(r.name));
|
|
5265
|
+
collectNames(stack.dashboards, dashboards, (d) => strName7(d.name));
|
|
5266
|
+
collectNames(stack.pages, pages, (p) => strName7(p.name));
|
|
4393
5267
|
collectNames(stack.views, views, viewContainerName);
|
|
4394
5268
|
for (const o of objects) views.add(o);
|
|
4395
5269
|
return { actions, objects, reports, dashboards, pages, views };
|
|
@@ -4421,14 +5295,14 @@ function resolveUrlRoute(target, known) {
|
|
|
4421
5295
|
function validateDashboardActionRefs(stack) {
|
|
4422
5296
|
const findings = [];
|
|
4423
5297
|
if (!stack || typeof stack !== "object") return findings;
|
|
4424
|
-
const dashboards =
|
|
5298
|
+
const dashboards = asArray23(stack.dashboards);
|
|
4425
5299
|
if (dashboards.length === 0) return findings;
|
|
4426
5300
|
const known = collectKnownTargets(stack);
|
|
4427
5301
|
const checkOne = (action, where, path) => {
|
|
4428
|
-
const target =
|
|
5302
|
+
const target = strName7(action.actionUrl);
|
|
4429
5303
|
if (!target) return;
|
|
4430
5304
|
if (target.includes("${")) return;
|
|
4431
|
-
const actionType =
|
|
5305
|
+
const actionType = strName7(action.actionType) ?? "url";
|
|
4432
5306
|
if (actionType === "script" || actionType === "modal") {
|
|
4433
5307
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
4434
5308
|
const kindWord = actionType === "script" ? "script" : "modal";
|
|
@@ -4459,13 +5333,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
4459
5333
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4460
5334
|
const dash = dashboards[di];
|
|
4461
5335
|
if (!dash || typeof dash !== "object") continue;
|
|
4462
|
-
const dashName =
|
|
5336
|
+
const dashName = strName7(dash.name) ?? `(dashboard ${di})`;
|
|
4463
5337
|
const dashPath = `dashboards[${di}]`;
|
|
4464
|
-
const headerActions =
|
|
5338
|
+
const headerActions = asArray23(dash.header?.actions);
|
|
4465
5339
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
4466
5340
|
const action = headerActions[ai];
|
|
4467
5341
|
if (!action || typeof action !== "object") continue;
|
|
4468
|
-
const label2 =
|
|
5342
|
+
const label2 = strName7(action.label) ?? strName7(action.actionUrl) ?? `#${ai}`;
|
|
4469
5343
|
checkOne(
|
|
4470
5344
|
action,
|
|
4471
5345
|
`dashboard "${dashName}" \xB7 header action "${label2}"`,
|
|
@@ -4478,19 +5352,75 @@ function validateDashboardActionRefs(stack) {
|
|
|
4478
5352
|
|
|
4479
5353
|
// src/validate-filter-tokens.ts
|
|
4480
5354
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
4481
|
-
|
|
5355
|
+
|
|
5356
|
+
// src/filter-walk.ts
|
|
4482
5357
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
4483
|
-
function
|
|
5358
|
+
function asArray24(v) {
|
|
4484
5359
|
if (Array.isArray(v)) return v;
|
|
4485
5360
|
if (v && typeof v === "object") {
|
|
4486
5361
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4487
5362
|
}
|
|
4488
|
-
return [];
|
|
4489
|
-
}
|
|
4490
|
-
function label(v, fallback) {
|
|
4491
|
-
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
5363
|
+
return [];
|
|
5364
|
+
}
|
|
5365
|
+
function label(v, fallback) {
|
|
5366
|
+
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
5367
|
+
}
|
|
5368
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
5369
|
+
if (!node || typeof node !== "object") return;
|
|
5370
|
+
if (seen.has(node)) return;
|
|
5371
|
+
seen.add(node);
|
|
5372
|
+
if (Array.isArray(node)) {
|
|
5373
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
5374
|
+
return;
|
|
5375
|
+
}
|
|
5376
|
+
for (const [k, v] of Object.entries(node)) {
|
|
5377
|
+
const childPath = `${path}.${k}`;
|
|
5378
|
+
if (FILTER_KEYS.has(k)) {
|
|
5379
|
+
visit({ value: v, path: childPath, where });
|
|
5380
|
+
continue;
|
|
5381
|
+
}
|
|
5382
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
5386
|
+
if (!stack || typeof stack !== "object") return;
|
|
5387
|
+
for (const { key, kind } of surfaces) {
|
|
5388
|
+
const items = asArray24(stack[key]);
|
|
5389
|
+
items.forEach((item, i) => {
|
|
5390
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
5391
|
+
if (kind === "dashboard") {
|
|
5392
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
5393
|
+
widgets.forEach((w, wi) => {
|
|
5394
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
5395
|
+
scanForFilters(
|
|
5396
|
+
w,
|
|
5397
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
5398
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
5399
|
+
visit,
|
|
5400
|
+
/* @__PURE__ */ new Set()
|
|
5401
|
+
);
|
|
5402
|
+
});
|
|
5403
|
+
const { widgets: _skip, ...rest } = item;
|
|
5404
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
5405
|
+
return;
|
|
5406
|
+
}
|
|
5407
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
5408
|
+
});
|
|
5409
|
+
}
|
|
4492
5410
|
}
|
|
5411
|
+
|
|
5412
|
+
// src/validate-filter-tokens.ts
|
|
5413
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
4493
5414
|
var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
|
|
5415
|
+
var TOKEN_FILTER_SURFACES = [
|
|
5416
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
5417
|
+
{ key: "objects", kind: "object" },
|
|
5418
|
+
{ key: "views", kind: "view" },
|
|
5419
|
+
{ key: "reports", kind: "report" },
|
|
5420
|
+
{ key: "datasets", kind: "dataset" },
|
|
5421
|
+
{ key: "pages", kind: "page" },
|
|
5422
|
+
{ key: "apps", kind: "app" }
|
|
5423
|
+
];
|
|
4494
5424
|
function walkFilterValues(node, path, where, out, seen) {
|
|
4495
5425
|
if (node === null || node === void 0) return;
|
|
4496
5426
|
if (typeof node === "string") {
|
|
@@ -4519,58 +5449,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
4519
5449
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
4520
5450
|
}
|
|
4521
5451
|
}
|
|
4522
|
-
function
|
|
4523
|
-
if (!
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
5452
|
+
function validateFilterTokens(stack) {
|
|
5453
|
+
if (!stack || typeof stack !== "object") return [];
|
|
5454
|
+
const out = [];
|
|
5455
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
5456
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
5457
|
+
});
|
|
5458
|
+
return out;
|
|
5459
|
+
}
|
|
5460
|
+
|
|
5461
|
+
// src/validate-empty-combinators.ts
|
|
5462
|
+
import { reduceFilterVerdict } from "@objectstack/spec/data";
|
|
5463
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
5464
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
5465
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
5466
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
5467
|
+
{ key: "objects", kind: "object" },
|
|
5468
|
+
{ key: "views", kind: "view" },
|
|
5469
|
+
{ key: "reports", kind: "report" },
|
|
5470
|
+
{ key: "datasets", kind: "dataset" },
|
|
5471
|
+
{ key: "pages", kind: "page" },
|
|
5472
|
+
{ key: "apps", kind: "app" },
|
|
5473
|
+
{ key: "flows", kind: "flow" }
|
|
5474
|
+
];
|
|
5475
|
+
function isFilterNode(value) {
|
|
5476
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
5477
|
+
const proto = Object.getPrototypeOf(value);
|
|
5478
|
+
return proto === Object.prototype || proto === null;
|
|
5479
|
+
}
|
|
5480
|
+
var VERDICT_OF = {
|
|
5481
|
+
$and: reduceFilterVerdict({ $and: [] }),
|
|
5482
|
+
$or: reduceFilterVerdict({ $or: [] }),
|
|
5483
|
+
$not: reduceFilterVerdict({ $not: {} }),
|
|
5484
|
+
node: reduceFilterVerdict({}),
|
|
5485
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
5486
|
+
orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
|
|
5487
|
+
};
|
|
5488
|
+
function rows(verdict) {
|
|
5489
|
+
if (verdict === "true") return "matches EVERY row";
|
|
5490
|
+
if (verdict === "false") return "matches NO row";
|
|
5491
|
+
return "carries a real predicate";
|
|
5492
|
+
}
|
|
5493
|
+
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.";
|
|
5494
|
+
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).';
|
|
5495
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
5496
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
5497
|
+
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.`;
|
|
5498
|
+
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}`;
|
|
5499
|
+
ctx.out.push({
|
|
5500
|
+
severity: "error",
|
|
5501
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
5502
|
+
where: ctx.where,
|
|
5503
|
+
path,
|
|
5504
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
5505
|
+
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.`
|
|
5506
|
+
});
|
|
5507
|
+
}
|
|
5508
|
+
function emitEmptyNode(position, path, ctx) {
|
|
5509
|
+
if (position === "root") {
|
|
5510
|
+
ctx.out.push({
|
|
5511
|
+
severity: "error",
|
|
5512
|
+
rule: FILTER_EMPTY_NODE,
|
|
5513
|
+
where: ctx.where,
|
|
5514
|
+
path,
|
|
5515
|
+
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.`,
|
|
5516
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
5517
|
+
});
|
|
4528
5518
|
return;
|
|
4529
5519
|
}
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
5520
|
+
if (position === "or-branch") {
|
|
5521
|
+
ctx.out.push({
|
|
5522
|
+
severity: "error",
|
|
5523
|
+
rule: FILTER_EMPTY_NODE,
|
|
5524
|
+
where: ctx.where,
|
|
5525
|
+
path,
|
|
5526
|
+
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.`,
|
|
5527
|
+
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.)"
|
|
5528
|
+
});
|
|
5529
|
+
return;
|
|
5530
|
+
}
|
|
5531
|
+
ctx.out.push({
|
|
5532
|
+
severity: "error",
|
|
5533
|
+
rule: FILTER_EMPTY_NODE,
|
|
5534
|
+
where: ctx.where,
|
|
5535
|
+
path,
|
|
5536
|
+
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.",
|
|
5537
|
+
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."
|
|
5538
|
+
});
|
|
5539
|
+
}
|
|
5540
|
+
function scanNodeKeys(node, path, ctx) {
|
|
5541
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5542
|
+
if (key === "$and" || key === "$or") {
|
|
5543
|
+
if (!Array.isArray(value)) continue;
|
|
5544
|
+
if (value.length === 0) {
|
|
5545
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
5546
|
+
continue;
|
|
5547
|
+
}
|
|
5548
|
+
value.forEach((element, index) => {
|
|
5549
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
5550
|
+
});
|
|
5551
|
+
continue;
|
|
5552
|
+
}
|
|
5553
|
+
if (key === "$not") {
|
|
5554
|
+
if (!isFilterNode(value)) continue;
|
|
5555
|
+
if (Object.keys(value).length === 0) {
|
|
5556
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
5557
|
+
continue;
|
|
5558
|
+
}
|
|
5559
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
4534
5560
|
continue;
|
|
4535
5561
|
}
|
|
4536
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
4537
5562
|
}
|
|
4538
5563
|
}
|
|
4539
|
-
function
|
|
5564
|
+
function scanBranch(value, path, position, ctx) {
|
|
5565
|
+
if (!isFilterNode(value)) return;
|
|
5566
|
+
if (Object.keys(value).length === 0) {
|
|
5567
|
+
emitEmptyNode(position, path, ctx);
|
|
5568
|
+
return;
|
|
5569
|
+
}
|
|
5570
|
+
scanNodeKeys(value, path, ctx);
|
|
5571
|
+
}
|
|
5572
|
+
function validateEmptyCombinators(stack) {
|
|
4540
5573
|
if (!stack || typeof stack !== "object") return [];
|
|
4541
5574
|
const out = [];
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
["views", "view"],
|
|
4546
|
-
["reports", "report"],
|
|
4547
|
-
["datasets", "dataset"],
|
|
4548
|
-
["pages", "page"],
|
|
4549
|
-
["apps", "app"]
|
|
4550
|
-
];
|
|
4551
|
-
for (const [key, kind] of surfaces) {
|
|
4552
|
-
const items = asArray25(stack[key]);
|
|
4553
|
-
items.forEach((item, i) => {
|
|
4554
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
4555
|
-
if (kind === "dashboard") {
|
|
4556
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
4557
|
-
widgets.forEach((w, wi) => {
|
|
4558
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
4559
|
-
scanForFilters(
|
|
4560
|
-
w,
|
|
4561
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
4562
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
4563
|
-
out,
|
|
4564
|
-
/* @__PURE__ */ new Set()
|
|
4565
|
-
);
|
|
4566
|
-
});
|
|
4567
|
-
const { widgets: _skip, ...rest } = item;
|
|
4568
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
4569
|
-
return;
|
|
4570
|
-
}
|
|
4571
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
4572
|
-
});
|
|
4573
|
-
}
|
|
5575
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
5576
|
+
scanBranch(value, path, "root", { where, out });
|
|
5577
|
+
});
|
|
4574
5578
|
return out;
|
|
4575
5579
|
}
|
|
4576
5580
|
|
|
@@ -4583,14 +5587,14 @@ import {
|
|
|
4583
5587
|
var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
4584
5588
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
4585
5589
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
4586
|
-
function
|
|
5590
|
+
function asArray25(v) {
|
|
4587
5591
|
if (Array.isArray(v)) return v;
|
|
4588
5592
|
if (v && typeof v === "object") {
|
|
4589
5593
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4590
5594
|
}
|
|
4591
5595
|
return [];
|
|
4592
5596
|
}
|
|
4593
|
-
function
|
|
5597
|
+
function strName8(v) {
|
|
4594
5598
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4595
5599
|
}
|
|
4596
5600
|
function isInterpolated(target) {
|
|
@@ -4630,14 +5634,14 @@ function distance2(a, b) {
|
|
|
4630
5634
|
function validateObjectReferences(stack) {
|
|
4631
5635
|
const findings = [];
|
|
4632
5636
|
if (!stack || typeof stack !== "object") return findings;
|
|
4633
|
-
const objects =
|
|
5637
|
+
const objects = asArray25(stack.objects);
|
|
4634
5638
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4635
5639
|
for (const obj of objects) {
|
|
4636
|
-
const n =
|
|
5640
|
+
const n = strName8(obj.name);
|
|
4637
5641
|
if (n) ownObjects.add(n);
|
|
4638
5642
|
}
|
|
4639
5643
|
const check = (target, where, path, subject, fix) => {
|
|
4640
|
-
const name =
|
|
5644
|
+
const name = strName8(target);
|
|
4641
5645
|
if (!name) return;
|
|
4642
5646
|
if (isInterpolated(name)) return;
|
|
4643
5647
|
if (ownObjects.has(name)) return;
|
|
@@ -4663,21 +5667,21 @@ function validateObjectReferences(stack) {
|
|
|
4663
5667
|
});
|
|
4664
5668
|
};
|
|
4665
5669
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
4666
|
-
const params =
|
|
5670
|
+
const params = asArray25(action.params);
|
|
4667
5671
|
for (let pi = 0; pi < params.length; pi++) {
|
|
4668
5672
|
const param = params[pi];
|
|
4669
5673
|
if (!param || typeof param !== "object") continue;
|
|
4670
|
-
const paramLabel =
|
|
5674
|
+
const paramLabel = strName8(param.name) ?? strName8(param.field) ?? `#${pi}`;
|
|
4671
5675
|
const where = `${actionLabel} \xB7 param "${paramLabel}"`;
|
|
4672
5676
|
check(
|
|
4673
|
-
|
|
5677
|
+
strName8(param.reference),
|
|
4674
5678
|
where,
|
|
4675
5679
|
`${actionPath}.params[${pi}].reference`,
|
|
4676
5680
|
"record-picker target",
|
|
4677
5681
|
"Without a resolvable target the picker degrades to a raw record-id text input."
|
|
4678
5682
|
);
|
|
4679
5683
|
check(
|
|
4680
|
-
|
|
5684
|
+
strName8(param.objectOverride),
|
|
4681
5685
|
where,
|
|
4682
5686
|
`${actionPath}.params[${pi}].objectOverride`,
|
|
4683
5687
|
"field-backed param object",
|
|
@@ -4685,70 +5689,70 @@ function validateObjectReferences(stack) {
|
|
|
4685
5689
|
);
|
|
4686
5690
|
}
|
|
4687
5691
|
};
|
|
4688
|
-
const globalActions =
|
|
5692
|
+
const globalActions = asArray25(stack.actions);
|
|
4689
5693
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
4690
5694
|
const action = globalActions[ai];
|
|
4691
5695
|
if (!action || typeof action !== "object") continue;
|
|
4692
|
-
checkActionParams2(action, `actions[${ai}]`, `action "${
|
|
5696
|
+
checkActionParams2(action, `actions[${ai}]`, `action "${strName8(action.name) ?? `#${ai}`}"`);
|
|
4693
5697
|
}
|
|
4694
5698
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4695
5699
|
const obj = objects[oi];
|
|
4696
5700
|
if (!obj || typeof obj !== "object") continue;
|
|
4697
|
-
const objName =
|
|
4698
|
-
const objActions =
|
|
5701
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
5702
|
+
const objActions = asArray25(obj.actions);
|
|
4699
5703
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
4700
5704
|
const action = objActions[ai];
|
|
4701
5705
|
if (!action || typeof action !== "object") continue;
|
|
4702
5706
|
checkActionParams2(
|
|
4703
5707
|
action,
|
|
4704
5708
|
`objects[${oi}].actions[${ai}]`,
|
|
4705
|
-
`object "${objName}" \xB7 action "${
|
|
5709
|
+
`object "${objName}" \xB7 action "${strName8(action.name) ?? `#${ai}`}"`
|
|
4706
5710
|
);
|
|
4707
5711
|
}
|
|
4708
5712
|
}
|
|
4709
|
-
const dashboards =
|
|
5713
|
+
const dashboards = asArray25(stack.dashboards);
|
|
4710
5714
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4711
5715
|
const dash = dashboards[di];
|
|
4712
5716
|
if (!dash || typeof dash !== "object") continue;
|
|
4713
|
-
const dashName =
|
|
4714
|
-
const filters =
|
|
5717
|
+
const dashName = strName8(dash.name) ?? `#${di}`;
|
|
5718
|
+
const filters = asArray25(dash.globalFilters);
|
|
4715
5719
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
4716
5720
|
const filter = filters[fi];
|
|
4717
5721
|
if (!filter || typeof filter !== "object") continue;
|
|
4718
5722
|
const optionsFrom = filter.optionsFrom;
|
|
4719
5723
|
if (!optionsFrom || typeof optionsFrom !== "object") continue;
|
|
4720
5724
|
check(
|
|
4721
|
-
|
|
4722
|
-
`dashboard "${dashName}" \xB7 filter "${
|
|
5725
|
+
strName8(optionsFrom.object),
|
|
5726
|
+
`dashboard "${dashName}" \xB7 filter "${strName8(filter.name) ?? `#${fi}`}"`,
|
|
4723
5727
|
`dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
|
|
4724
5728
|
"filter options source",
|
|
4725
5729
|
"The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
|
|
4726
5730
|
);
|
|
4727
5731
|
}
|
|
4728
5732
|
}
|
|
4729
|
-
const apps =
|
|
5733
|
+
const apps = asArray25(stack.apps);
|
|
4730
5734
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4731
5735
|
const app = apps[ai];
|
|
4732
5736
|
if (!app || typeof app !== "object") continue;
|
|
4733
|
-
const appName =
|
|
5737
|
+
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4734
5738
|
const walkNav = (items, basePath) => {
|
|
4735
|
-
const navItems =
|
|
5739
|
+
const navItems = asArray25(items);
|
|
4736
5740
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4737
5741
|
const nav = navItems[ni];
|
|
4738
5742
|
if (!nav || typeof nav !== "object") continue;
|
|
4739
|
-
const navId =
|
|
5743
|
+
const navId = strName8(nav.id) ?? `#${ni}`;
|
|
4740
5744
|
const where = `app "${appName}" \xB7 nav "${navId}"`;
|
|
4741
5745
|
const navPath = `${basePath}[${ni}]`;
|
|
4742
5746
|
check(
|
|
4743
|
-
|
|
5747
|
+
strName8(nav.requiresObject),
|
|
4744
5748
|
where,
|
|
4745
5749
|
`${navPath}.requiresObject`,
|
|
4746
5750
|
"capability gate object",
|
|
4747
5751
|
"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."
|
|
4748
5752
|
);
|
|
4749
|
-
if (nav.requiresObject &&
|
|
5753
|
+
if (nav.requiresObject && strName8(nav.objectName)) {
|
|
4750
5754
|
check(
|
|
4751
|
-
|
|
5755
|
+
strName8(nav.objectName),
|
|
4752
5756
|
where,
|
|
4753
5757
|
`${navPath}.objectName`,
|
|
4754
5758
|
"navigation target",
|
|
@@ -4759,7 +5763,7 @@ function validateObjectReferences(stack) {
|
|
|
4759
5763
|
}
|
|
4760
5764
|
};
|
|
4761
5765
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4762
|
-
const areas =
|
|
5766
|
+
const areas = asArray25(app.areas);
|
|
4763
5767
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4764
5768
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4765
5769
|
}
|
|
@@ -4769,13 +5773,13 @@ function validateObjectReferences(stack) {
|
|
|
4769
5773
|
|
|
4770
5774
|
// src/validate-nav-target-refs.ts
|
|
4771
5775
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4772
|
-
var
|
|
4773
|
-
function
|
|
4774
|
-
if (Array.isArray(v)) return v.filter(
|
|
4775
|
-
if (
|
|
5776
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5777
|
+
function asArray26(v) {
|
|
5778
|
+
if (Array.isArray(v)) return v.filter(isRec12);
|
|
5779
|
+
if (isRec12(v)) return Object.entries(v).map(([name, def]) => isRec12(def) ? { name, ...def } : { name });
|
|
4776
5780
|
return [];
|
|
4777
5781
|
}
|
|
4778
|
-
function
|
|
5782
|
+
function strName9(v) {
|
|
4779
5783
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4780
5784
|
}
|
|
4781
5785
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -4786,32 +5790,32 @@ var NAV_TARGETS = [
|
|
|
4786
5790
|
];
|
|
4787
5791
|
function namesOf(collection) {
|
|
4788
5792
|
const out = /* @__PURE__ */ new Set();
|
|
4789
|
-
for (const entry of
|
|
4790
|
-
const n =
|
|
5793
|
+
for (const entry of asArray26(collection)) {
|
|
5794
|
+
const n = strName9(entry.name);
|
|
4791
5795
|
if (n) out.add(n);
|
|
4792
5796
|
}
|
|
4793
5797
|
return out;
|
|
4794
5798
|
}
|
|
4795
5799
|
function validateNavTargetRefs(stack) {
|
|
4796
5800
|
const findings = [];
|
|
4797
|
-
if (!
|
|
4798
|
-
const apps =
|
|
5801
|
+
if (!isRec12(stack)) return findings;
|
|
5802
|
+
const apps = asArray26(stack.apps);
|
|
4799
5803
|
if (apps.length === 0) return findings;
|
|
4800
5804
|
const declared = /* @__PURE__ */ new Map();
|
|
4801
5805
|
for (const [, , collection] of NAV_TARGETS) {
|
|
4802
5806
|
declared.set(collection, namesOf(stack[collection]));
|
|
4803
5807
|
}
|
|
4804
5808
|
for (const [ai, app] of apps.entries()) {
|
|
4805
|
-
const appName =
|
|
5809
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
4806
5810
|
const walk = (items, basePath) => {
|
|
4807
5811
|
if (!Array.isArray(items)) return;
|
|
4808
5812
|
for (const [ni, raw] of items.entries()) {
|
|
4809
|
-
if (!
|
|
5813
|
+
if (!isRec12(raw)) continue;
|
|
4810
5814
|
const nav = raw;
|
|
4811
5815
|
const navPath = `${basePath}[${ni}]`;
|
|
4812
5816
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
4813
5817
|
if (nav.type !== type) continue;
|
|
4814
|
-
const target =
|
|
5818
|
+
const target = strName9(nav[prop]);
|
|
4815
5819
|
if (!target || isInterpolated2(target)) continue;
|
|
4816
5820
|
const known = declared.get(collection);
|
|
4817
5821
|
if (known.has(target)) continue;
|
|
@@ -4819,7 +5823,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4819
5823
|
findings.push({
|
|
4820
5824
|
severity: "warning",
|
|
4821
5825
|
rule: NAV_TARGET_UNRESOLVED,
|
|
4822
|
-
where: `app "${appName}" \xB7 nav "${
|
|
5826
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
|
|
4823
5827
|
path: `${navPath}.${prop}`,
|
|
4824
5828
|
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.`,
|
|
4825
5829
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -4829,7 +5833,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4829
5833
|
}
|
|
4830
5834
|
};
|
|
4831
5835
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4832
|
-
for (const [ari, area] of
|
|
5836
|
+
for (const [ari, area] of asArray26(app.areas).entries()) {
|
|
4833
5837
|
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
4834
5838
|
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
4835
5839
|
}
|
|
@@ -4839,14 +5843,14 @@ function validateNavTargetRefs(stack) {
|
|
|
4839
5843
|
|
|
4840
5844
|
// src/validate-action-name-refs.ts
|
|
4841
5845
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
4842
|
-
function
|
|
5846
|
+
function asArray27(v) {
|
|
4843
5847
|
if (Array.isArray(v)) return v;
|
|
4844
5848
|
if (v && typeof v === "object") {
|
|
4845
5849
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4846
5850
|
}
|
|
4847
5851
|
return [];
|
|
4848
5852
|
}
|
|
4849
|
-
function
|
|
5853
|
+
function strName10(v) {
|
|
4850
5854
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4851
5855
|
}
|
|
4852
5856
|
function strList(v) {
|
|
@@ -4883,14 +5887,14 @@ function suggest4(target, known) {
|
|
|
4883
5887
|
}
|
|
4884
5888
|
function collectActionNames(stack) {
|
|
4885
5889
|
const names = /* @__PURE__ */ new Set();
|
|
4886
|
-
for (const action of
|
|
4887
|
-
const n =
|
|
5890
|
+
for (const action of asArray27(stack.actions)) {
|
|
5891
|
+
const n = strName10(action?.name);
|
|
4888
5892
|
if (n) names.add(n);
|
|
4889
5893
|
}
|
|
4890
|
-
for (const obj of
|
|
5894
|
+
for (const obj of asArray27(stack.objects)) {
|
|
4891
5895
|
if (!obj || typeof obj !== "object") continue;
|
|
4892
|
-
for (const action of
|
|
4893
|
-
const n =
|
|
5896
|
+
for (const action of asArray27(obj.actions)) {
|
|
5897
|
+
const n = strName10(action?.name);
|
|
4894
5898
|
if (n) names.add(n);
|
|
4895
5899
|
}
|
|
4896
5900
|
}
|
|
@@ -4933,7 +5937,7 @@ function validateActionNameRefs(stack) {
|
|
|
4933
5937
|
if (!def || typeof def !== "object") continue;
|
|
4934
5938
|
if (def.execution !== "aggregate") continue;
|
|
4935
5939
|
if (def.actionDef !== void 0) continue;
|
|
4936
|
-
const name =
|
|
5940
|
+
const name = strName10(def.name);
|
|
4937
5941
|
if (!name) continue;
|
|
4938
5942
|
check(
|
|
4939
5943
|
name,
|
|
@@ -4944,11 +5948,11 @@ function validateActionNameRefs(stack) {
|
|
|
4944
5948
|
);
|
|
4945
5949
|
}
|
|
4946
5950
|
};
|
|
4947
|
-
const views =
|
|
5951
|
+
const views = asArray27(stack.views);
|
|
4948
5952
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4949
5953
|
const view = views[vi];
|
|
4950
5954
|
if (!view || typeof view !== "object") continue;
|
|
4951
|
-
const viewName =
|
|
5955
|
+
const viewName = strName10(view.name) ?? strName10(view.object) ?? `#${vi}`;
|
|
4952
5956
|
const owner = `view "${viewName}"`;
|
|
4953
5957
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
4954
5958
|
const listViews = view.listViews;
|
|
@@ -4958,22 +5962,22 @@ function validateActionNameRefs(stack) {
|
|
|
4958
5962
|
}
|
|
4959
5963
|
}
|
|
4960
5964
|
}
|
|
4961
|
-
const objects =
|
|
5965
|
+
const objects = asArray27(stack.objects);
|
|
4962
5966
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4963
5967
|
const obj = objects[oi];
|
|
4964
5968
|
if (!obj || typeof obj !== "object") continue;
|
|
4965
5969
|
const objListViews = obj.listViews;
|
|
4966
5970
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
4967
|
-
const owner = `object "${
|
|
5971
|
+
const owner = `object "${strName10(obj.name) ?? `#${oi}`}"`;
|
|
4968
5972
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
4969
5973
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
4970
5974
|
}
|
|
4971
5975
|
}
|
|
4972
|
-
const pages =
|
|
5976
|
+
const pages = asArray27(stack.pages);
|
|
4973
5977
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4974
5978
|
const page = pages[pi];
|
|
4975
5979
|
if (!page || typeof page !== "object") continue;
|
|
4976
|
-
const pageName =
|
|
5980
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
4977
5981
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4978
5982
|
const props = component.properties;
|
|
4979
5983
|
if (!props || typeof props !== "object") continue;
|
|
@@ -4981,39 +5985,48 @@ function validateActionNameRefs(stack) {
|
|
|
4981
5985
|
for (let ai = 0; ai < names.length; ai++) {
|
|
4982
5986
|
check(
|
|
4983
5987
|
names[ai],
|
|
4984
|
-
`page "${pageName}" \xB7 component "${
|
|
5988
|
+
`page "${pageName}" \xB7 component "${strName10(component.type) ?? "?"}"`,
|
|
4985
5989
|
`${path}.properties.actionNames[${ai}]`,
|
|
4986
5990
|
"Quick-actions bar"
|
|
4987
5991
|
);
|
|
4988
5992
|
}
|
|
4989
5993
|
}
|
|
4990
5994
|
}
|
|
4991
|
-
const apps =
|
|
5995
|
+
const apps = asArray27(stack.apps);
|
|
4992
5996
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4993
5997
|
const app = apps[ai];
|
|
4994
5998
|
if (!app || typeof app !== "object") continue;
|
|
4995
|
-
const appName =
|
|
5999
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
4996
6000
|
const walkNav = (items, basePath) => {
|
|
4997
|
-
const navItems =
|
|
6001
|
+
const navItems = asArray27(items);
|
|
4998
6002
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4999
6003
|
const nav = navItems[ni];
|
|
5000
6004
|
if (!nav || typeof nav !== "object") continue;
|
|
5001
6005
|
const navPath = `${basePath}[${ni}]`;
|
|
5002
6006
|
const actionDef = nav.actionDef;
|
|
5003
|
-
const actionName =
|
|
6007
|
+
const actionName = strName10(actionDef?.actionName);
|
|
5004
6008
|
if (nav.type === "action" && actionName) {
|
|
5005
6009
|
check(
|
|
5006
6010
|
actionName,
|
|
5007
|
-
`app "${appName}" \xB7 nav "${
|
|
6011
|
+
`app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
|
|
5008
6012
|
`${navPath}.actionDef.actionName`,
|
|
5009
6013
|
"Navigation action item"
|
|
5010
6014
|
);
|
|
5011
6015
|
}
|
|
6016
|
+
const runAction = strName10(nav.runAction);
|
|
6017
|
+
if (nav.type === "object" && runAction) {
|
|
6018
|
+
check(
|
|
6019
|
+
runAction,
|
|
6020
|
+
`app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
|
|
6021
|
+
`${navPath}.runAction`,
|
|
6022
|
+
"Navigation deep-link auto-run"
|
|
6023
|
+
);
|
|
6024
|
+
}
|
|
5012
6025
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
5013
6026
|
}
|
|
5014
6027
|
};
|
|
5015
6028
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
5016
|
-
const areas =
|
|
6029
|
+
const areas = asArray27(app.areas);
|
|
5017
6030
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
5018
6031
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
5019
6032
|
}
|
|
@@ -5023,14 +6036,14 @@ function validateActionNameRefs(stack) {
|
|
|
5023
6036
|
|
|
5024
6037
|
// src/validate-action-locations.ts
|
|
5025
6038
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
5026
|
-
function
|
|
6039
|
+
function asArray28(v) {
|
|
5027
6040
|
if (Array.isArray(v)) return v;
|
|
5028
6041
|
if (v && typeof v === "object") {
|
|
5029
6042
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5030
6043
|
}
|
|
5031
6044
|
return [];
|
|
5032
6045
|
}
|
|
5033
|
-
function
|
|
6046
|
+
function strName11(v) {
|
|
5034
6047
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5035
6048
|
}
|
|
5036
6049
|
function strList2(v) {
|
|
@@ -5044,8 +6057,8 @@ function collectNamePlacedActions(stack) {
|
|
|
5044
6057
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
5045
6058
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
5046
6059
|
}
|
|
5047
|
-
for (const def of
|
|
5048
|
-
const n =
|
|
6060
|
+
for (const def of asArray28(list3.bulkActionDefs)) {
|
|
6061
|
+
const n = strName11(def?.name);
|
|
5049
6062
|
if (n) placed.add(n);
|
|
5050
6063
|
}
|
|
5051
6064
|
};
|
|
@@ -5053,12 +6066,12 @@ function collectNamePlacedActions(stack) {
|
|
|
5053
6066
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
5054
6067
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
5055
6068
|
};
|
|
5056
|
-
for (const view of
|
|
6069
|
+
for (const view of asArray28(stack.views)) {
|
|
5057
6070
|
if (!view || typeof view !== "object") continue;
|
|
5058
6071
|
harvest(view.list);
|
|
5059
6072
|
harvestListViews(view.listViews);
|
|
5060
6073
|
}
|
|
5061
|
-
for (const obj of
|
|
6074
|
+
for (const obj of asArray28(stack.objects)) {
|
|
5062
6075
|
if (!obj || typeof obj !== "object") continue;
|
|
5063
6076
|
harvestListViews(obj.listViews);
|
|
5064
6077
|
}
|
|
@@ -5071,7 +6084,7 @@ function validateActionLocations(stack) {
|
|
|
5071
6084
|
const check = (action, path) => {
|
|
5072
6085
|
if (!action || typeof action !== "object") return;
|
|
5073
6086
|
if ("locations" in action) return;
|
|
5074
|
-
const name =
|
|
6087
|
+
const name = strName11(action.name);
|
|
5075
6088
|
if (!name) return;
|
|
5076
6089
|
if (namePlaced.has(name)) return;
|
|
5077
6090
|
findings.push({
|
|
@@ -5080,16 +6093,16 @@ function validateActionLocations(stack) {
|
|
|
5080
6093
|
where: `action "${name}"`,
|
|
5081
6094
|
path,
|
|
5082
6095
|
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.`,
|
|
5083
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
6096
|
+
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."
|
|
5084
6097
|
});
|
|
5085
6098
|
};
|
|
5086
|
-
const actions =
|
|
6099
|
+
const actions = asArray28(stack.actions);
|
|
5087
6100
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
5088
|
-
const objects =
|
|
6101
|
+
const objects = asArray28(stack.objects);
|
|
5089
6102
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5090
6103
|
const obj = objects[oi];
|
|
5091
6104
|
if (!obj || typeof obj !== "object") continue;
|
|
5092
|
-
const own =
|
|
6105
|
+
const own = asArray28(obj.actions);
|
|
5093
6106
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
5094
6107
|
}
|
|
5095
6108
|
return findings;
|
|
@@ -5100,13 +6113,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
5100
6113
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
5101
6114
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5102
6115
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5103
|
-
function
|
|
6116
|
+
function isRec13(v) {
|
|
5104
6117
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5105
6118
|
}
|
|
5106
|
-
function
|
|
6119
|
+
function strName12(v) {
|
|
5107
6120
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5108
6121
|
}
|
|
5109
|
-
function
|
|
6122
|
+
function asArray29(v) {
|
|
5110
6123
|
if (Array.isArray(v)) return v;
|
|
5111
6124
|
if (v && typeof v === "object") {
|
|
5112
6125
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5117,23 +6130,23 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
5117
6130
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5118
6131
|
function suppliedByDataSource(issue, component) {
|
|
5119
6132
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5120
|
-
const dataSource =
|
|
5121
|
-
return
|
|
6133
|
+
const dataSource = isRec13(component.dataSource) ? component.dataSource : void 0;
|
|
6134
|
+
return strName12(dataSource?.object) !== void 0;
|
|
5122
6135
|
}
|
|
5123
6136
|
function validateComponentProps(stack) {
|
|
5124
6137
|
const findings = [];
|
|
5125
|
-
if (!
|
|
5126
|
-
const pages =
|
|
6138
|
+
if (!isRec13(stack)) return findings;
|
|
6139
|
+
const pages = asArray29(stack.pages);
|
|
5127
6140
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5128
6141
|
const page = pages[pi];
|
|
5129
|
-
if (!
|
|
5130
|
-
const pageName =
|
|
6142
|
+
if (!isRec13(page)) continue;
|
|
6143
|
+
const pageName = strName12(page.name) ?? `#${pi}`;
|
|
5131
6144
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5132
|
-
const type =
|
|
6145
|
+
const type = strName12(component.type);
|
|
5133
6146
|
if (!type) continue;
|
|
5134
6147
|
const schema = PROPS_SCHEMAS[type];
|
|
5135
6148
|
if (!schema) continue;
|
|
5136
|
-
const props =
|
|
6149
|
+
const props = isRec13(component.properties) ? component.properties : void 0;
|
|
5137
6150
|
if (!props) continue;
|
|
5138
6151
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5139
6152
|
const base = `${path}.properties`;
|
|
@@ -5184,20 +6197,20 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
|
5184
6197
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
5185
6198
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
5186
6199
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
5187
|
-
function
|
|
6200
|
+
function asArray30(v) {
|
|
5188
6201
|
if (Array.isArray(v)) return v;
|
|
5189
6202
|
if (v && typeof v === "object") {
|
|
5190
6203
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5191
6204
|
}
|
|
5192
6205
|
return [];
|
|
5193
6206
|
}
|
|
5194
|
-
function
|
|
6207
|
+
function strName13(v) {
|
|
5195
6208
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5196
6209
|
}
|
|
5197
6210
|
function strList3(v) {
|
|
5198
6211
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
5199
6212
|
}
|
|
5200
|
-
function
|
|
6213
|
+
function isRec14(v) {
|
|
5201
6214
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5202
6215
|
}
|
|
5203
6216
|
function distance4(a, b) {
|
|
@@ -5235,17 +6248,17 @@ function list2(names) {
|
|
|
5235
6248
|
}
|
|
5236
6249
|
function indexDatasets(stack) {
|
|
5237
6250
|
const out = /* @__PURE__ */ new Map();
|
|
5238
|
-
for (const ds of
|
|
5239
|
-
const name =
|
|
6251
|
+
for (const ds of asArray30(stack.datasets)) {
|
|
6252
|
+
const name = strName13(ds.name);
|
|
5240
6253
|
if (!name) continue;
|
|
5241
6254
|
const dimensions = /* @__PURE__ */ new Set();
|
|
5242
|
-
for (const d of
|
|
5243
|
-
const n =
|
|
6255
|
+
for (const d of asArray30(ds.dimensions)) {
|
|
6256
|
+
const n = strName13(d.name);
|
|
5244
6257
|
if (n) dimensions.add(n);
|
|
5245
6258
|
}
|
|
5246
6259
|
const measures = /* @__PURE__ */ new Set();
|
|
5247
|
-
for (const m of
|
|
5248
|
-
const n =
|
|
6260
|
+
for (const m of asArray30(ds.measures)) {
|
|
6261
|
+
const n = strName13(m.name);
|
|
5249
6262
|
if (n) measures.add(n);
|
|
5250
6263
|
}
|
|
5251
6264
|
out.set(name, { dimensions, measures });
|
|
@@ -5323,29 +6336,29 @@ function validateChartBindings(stack) {
|
|
|
5323
6336
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
5324
6337
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
5325
6338
|
};
|
|
5326
|
-
const reports =
|
|
6339
|
+
const reports = asArray30(stack.reports);
|
|
5327
6340
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
5328
6341
|
const report = reports[ri];
|
|
5329
|
-
if (!
|
|
5330
|
-
const reportName =
|
|
6342
|
+
if (!isRec14(report)) continue;
|
|
6343
|
+
const reportName = strName13(report.name) ?? `#${ri}`;
|
|
5331
6344
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
5332
|
-
if (!
|
|
6345
|
+
if (!isRec14(chart)) return;
|
|
5333
6346
|
check({
|
|
5334
6347
|
dataset,
|
|
5335
6348
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
5336
6349
|
// it in lets the yAxis "declared but not selected" check work without
|
|
5337
6350
|
// reporting the selection itself twice.
|
|
5338
6351
|
values: { names: values, path: `${path}.values` },
|
|
5339
|
-
xAxis:
|
|
5340
|
-
yAxis:
|
|
5341
|
-
series:
|
|
6352
|
+
xAxis: strName13(chart.xAxis) ? { name: strName13(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
6353
|
+
yAxis: strName13(chart.yAxis) ? { name: strName13(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
6354
|
+
series: asArray30(chart.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
5342
6355
|
where,
|
|
5343
6356
|
path: `${path}.chart`
|
|
5344
6357
|
});
|
|
5345
6358
|
};
|
|
5346
6359
|
checkReportChart(
|
|
5347
6360
|
report.chart,
|
|
5348
|
-
|
|
6361
|
+
strName13(report.dataset),
|
|
5349
6362
|
strList3(report.values),
|
|
5350
6363
|
`report "${reportName}" \xB7 chart`,
|
|
5351
6364
|
`reports[${ri}]`
|
|
@@ -5353,45 +6366,45 @@ function validateChartBindings(stack) {
|
|
|
5353
6366
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
5354
6367
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
5355
6368
|
const block = blocks[bi];
|
|
5356
|
-
if (!
|
|
6369
|
+
if (!isRec14(block)) continue;
|
|
5357
6370
|
checkReportChart(
|
|
5358
6371
|
block.chart,
|
|
5359
|
-
|
|
6372
|
+
strName13(block.dataset),
|
|
5360
6373
|
strList3(block.values),
|
|
5361
|
-
`report "${reportName}" \xB7 block "${
|
|
6374
|
+
`report "${reportName}" \xB7 block "${strName13(block.name) ?? `#${bi}`}" chart`,
|
|
5362
6375
|
`reports[${ri}].blocks[${bi}]`
|
|
5363
6376
|
);
|
|
5364
6377
|
}
|
|
5365
6378
|
}
|
|
5366
6379
|
const checkListChart = (container, where, path) => {
|
|
5367
|
-
if (!
|
|
6380
|
+
if (!isRec14(container)) return;
|
|
5368
6381
|
const chart = container.chart;
|
|
5369
|
-
if (!
|
|
6382
|
+
if (!isRec14(chart)) return;
|
|
5370
6383
|
check({
|
|
5371
|
-
dataset:
|
|
6384
|
+
dataset: strName13(chart.dataset),
|
|
5372
6385
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5373
6386
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
5374
6387
|
where,
|
|
5375
6388
|
path: `${path}.chart`
|
|
5376
6389
|
});
|
|
5377
6390
|
};
|
|
5378
|
-
const views =
|
|
6391
|
+
const views = asArray30(stack.views);
|
|
5379
6392
|
for (let vi = 0; vi < views.length; vi++) {
|
|
5380
6393
|
const view = views[vi];
|
|
5381
|
-
if (!
|
|
5382
|
-
const viewName =
|
|
6394
|
+
if (!isRec14(view)) continue;
|
|
6395
|
+
const viewName = strName13(view.name) ?? strName13(view.objectName) ?? `#${vi}`;
|
|
5383
6396
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
5384
|
-
if (
|
|
6397
|
+
if (isRec14(view.listViews)) {
|
|
5385
6398
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
5386
6399
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
5387
6400
|
}
|
|
5388
6401
|
}
|
|
5389
6402
|
}
|
|
5390
|
-
const objects =
|
|
6403
|
+
const objects = asArray30(stack.objects);
|
|
5391
6404
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5392
6405
|
const obj = objects[oi];
|
|
5393
|
-
if (!
|
|
5394
|
-
const objName =
|
|
6406
|
+
if (!isRec14(obj) || !isRec14(obj.listViews)) continue;
|
|
6407
|
+
const objName = strName13(obj.name) ?? `#${oi}`;
|
|
5395
6408
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
5396
6409
|
checkListChart(
|
|
5397
6410
|
lv,
|
|
@@ -5400,22 +6413,22 @@ function validateChartBindings(stack) {
|
|
|
5400
6413
|
);
|
|
5401
6414
|
}
|
|
5402
6415
|
}
|
|
5403
|
-
const pages =
|
|
6416
|
+
const pages = asArray30(stack.pages);
|
|
5404
6417
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5405
6418
|
const page = pages[pi];
|
|
5406
|
-
if (!
|
|
5407
|
-
const pageName =
|
|
6419
|
+
if (!isRec14(page)) continue;
|
|
6420
|
+
const pageName = strName13(page.name) ?? `#${pi}`;
|
|
5408
6421
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5409
|
-
const props =
|
|
5410
|
-
if (!props || !
|
|
5411
|
-
const axisRefs =
|
|
5412
|
-
const seriesRefs =
|
|
6422
|
+
const props = isRec14(component.properties) ? component.properties : void 0;
|
|
6423
|
+
if (!props || !strName13(props.dataset)) continue;
|
|
6424
|
+
const axisRefs = asArray30(props.yAxis).map((a, ai) => ({ name: strName13(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
6425
|
+
const seriesRefs = asArray30(props.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
5413
6426
|
check({
|
|
5414
|
-
dataset:
|
|
6427
|
+
dataset: strName13(props.dataset),
|
|
5415
6428
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5416
6429
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
5417
6430
|
series: [...axisRefs, ...seriesRefs],
|
|
5418
|
-
where: `page "${pageName}" \xB7 ${
|
|
6431
|
+
where: `page "${pageName}" \xB7 ${strName13(component.type) ?? "chart"}`,
|
|
5419
6432
|
path: `${path}.properties`
|
|
5420
6433
|
});
|
|
5421
6434
|
}
|
|
@@ -5424,15 +6437,15 @@ function validateChartBindings(stack) {
|
|
|
5424
6437
|
}
|
|
5425
6438
|
|
|
5426
6439
|
// src/validate-rule-compilability.ts
|
|
5427
|
-
import { createRequire as
|
|
6440
|
+
import { createRequire as createRequire4 } from "module";
|
|
5428
6441
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
5429
6442
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
5430
6443
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
5431
|
-
var
|
|
5432
|
-
function
|
|
5433
|
-
if (Array.isArray(v)) return v.filter(
|
|
5434
|
-
if (
|
|
5435
|
-
return Object.entries(v).filter(([, def]) =>
|
|
6444
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6445
|
+
function asArray31(v) {
|
|
6446
|
+
if (Array.isArray(v)) return v.filter(isRec15);
|
|
6447
|
+
if (isRec15(v)) {
|
|
6448
|
+
return Object.entries(v).filter(([, def]) => isRec15(def)).map(([name, def]) => ({ name, ...def }));
|
|
5436
6449
|
}
|
|
5437
6450
|
return [];
|
|
5438
6451
|
}
|
|
@@ -5443,13 +6456,13 @@ function loadAjv() {
|
|
|
5443
6456
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5444
6457
|
let mod;
|
|
5445
6458
|
try {
|
|
5446
|
-
mod =
|
|
6459
|
+
mod = createRequire4(anchor)("ajv");
|
|
5447
6460
|
} catch (err) {
|
|
5448
6461
|
throw new Error(
|
|
5449
6462
|
`@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.`
|
|
5450
6463
|
);
|
|
5451
6464
|
}
|
|
5452
|
-
const ctor =
|
|
6465
|
+
const ctor = isRec15(mod) && "default" in mod ? mod.default : mod;
|
|
5453
6466
|
cachedAjv = ctor;
|
|
5454
6467
|
return ctor;
|
|
5455
6468
|
}
|
|
@@ -5458,13 +6471,13 @@ function loadAddFormats() {
|
|
|
5458
6471
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5459
6472
|
let mod;
|
|
5460
6473
|
try {
|
|
5461
|
-
mod =
|
|
6474
|
+
mod = createRequire4(anchor)("ajv-formats");
|
|
5462
6475
|
} catch (err) {
|
|
5463
6476
|
throw new Error(
|
|
5464
6477
|
`@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.`
|
|
5465
6478
|
);
|
|
5466
6479
|
}
|
|
5467
|
-
const plugin =
|
|
6480
|
+
const plugin = isRec15(mod) && "default" in mod ? mod.default : mod;
|
|
5468
6481
|
cachedAddFormats = plugin;
|
|
5469
6482
|
return plugin;
|
|
5470
6483
|
}
|
|
@@ -5494,17 +6507,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
5494
6507
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
5495
6508
|
for (const branch of ["then", "otherwise"]) {
|
|
5496
6509
|
const nested = rule[branch];
|
|
5497
|
-
if (
|
|
6510
|
+
if (isRec15(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
5498
6511
|
}
|
|
5499
6512
|
return out;
|
|
5500
6513
|
}
|
|
5501
6514
|
function walkObjectValidationRules(stack) {
|
|
5502
6515
|
const walked = [];
|
|
5503
|
-
if (!
|
|
5504
|
-
for (const obj of
|
|
6516
|
+
if (!isRec15(stack)) return walked;
|
|
6517
|
+
for (const obj of asArray31(stack.objects)) {
|
|
5505
6518
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
5506
6519
|
const validations = obj.validations;
|
|
5507
|
-
for (const authored of
|
|
6520
|
+
for (const authored of asArray31(validations)) {
|
|
5508
6521
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
5509
6522
|
walked.push({
|
|
5510
6523
|
rule,
|
|
@@ -5535,7 +6548,7 @@ function validateRuleCompilability(stack) {
|
|
|
5535
6548
|
});
|
|
5536
6549
|
}
|
|
5537
6550
|
}
|
|
5538
|
-
if (rule.type === "json_schema" &&
|
|
6551
|
+
if (rule.type === "json_schema" && isRec15(rule.schema)) {
|
|
5539
6552
|
try {
|
|
5540
6553
|
createRuntimeAjv().compile(rule.schema);
|
|
5541
6554
|
} catch (err) {
|
|
@@ -5555,7 +6568,7 @@ function validateRuleCompilability(stack) {
|
|
|
5555
6568
|
|
|
5556
6569
|
// src/validate-rule-schema-formats.ts
|
|
5557
6570
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
5558
|
-
var
|
|
6571
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5559
6572
|
var SUBSCHEMA_KEYS = [
|
|
5560
6573
|
"additionalItems",
|
|
5561
6574
|
"additionalProperties",
|
|
@@ -5579,7 +6592,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
5579
6592
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
5580
6593
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
5581
6594
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
5582
|
-
if (!
|
|
6595
|
+
if (!isRec16(schema)) return;
|
|
5583
6596
|
if (typeof schema.format === "string") {
|
|
5584
6597
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
5585
6598
|
}
|
|
@@ -5598,7 +6611,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
5598
6611
|
}
|
|
5599
6612
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
5600
6613
|
const value = schema[key];
|
|
5601
|
-
if (!
|
|
6614
|
+
if (!isRec16(value)) continue;
|
|
5602
6615
|
for (const [name, entry] of Object.entries(value)) {
|
|
5603
6616
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5604
6617
|
}
|
|
@@ -5606,13 +6619,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
5606
6619
|
const items = schema.items;
|
|
5607
6620
|
if (Array.isArray(items)) {
|
|
5608
6621
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
5609
|
-
} else if (
|
|
6622
|
+
} else if (isRec16(items)) {
|
|
5610
6623
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
5611
6624
|
}
|
|
5612
6625
|
const dependencies = schema.dependencies;
|
|
5613
|
-
if (
|
|
6626
|
+
if (isRec16(dependencies)) {
|
|
5614
6627
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
5615
|
-
if (!
|
|
6628
|
+
if (!isRec16(entry)) continue;
|
|
5616
6629
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5617
6630
|
}
|
|
5618
6631
|
}
|
|
@@ -5651,7 +6664,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
5651
6664
|
const findings = [];
|
|
5652
6665
|
const pending = [];
|
|
5653
6666
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5654
|
-
if (rule.type !== "json_schema" || !
|
|
6667
|
+
if (rule.type !== "json_schema" || !isRec16(rule.schema)) continue;
|
|
5655
6668
|
const uses = [];
|
|
5656
6669
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
5657
6670
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -5679,7 +6692,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
5679
6692
|
import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
|
|
5680
6693
|
|
|
5681
6694
|
// src/build-access-matrix.ts
|
|
5682
|
-
function
|
|
6695
|
+
function asArray32(v) {
|
|
5683
6696
|
if (Array.isArray(v)) return v;
|
|
5684
6697
|
if (v && typeof v === "object") {
|
|
5685
6698
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5690,13 +6703,13 @@ function buildAccessMatrix(stack) {
|
|
|
5690
6703
|
const entries = [];
|
|
5691
6704
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
5692
6705
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
5693
|
-
for (const obj of
|
|
6706
|
+
for (const obj of asArray32(stack.objects)) {
|
|
5694
6707
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
5695
6708
|
if (!name) continue;
|
|
5696
6709
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
5697
6710
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
5698
6711
|
}
|
|
5699
|
-
for (const ps of
|
|
6712
|
+
for (const ps of asArray32(stack.permissions)) {
|
|
5700
6713
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
5701
6714
|
if (!psName) continue;
|
|
5702
6715
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -5769,34 +6782,34 @@ function diffAccessMatrix(before, after) {
|
|
|
5769
6782
|
|
|
5770
6783
|
// src/validate-nav-access.ts
|
|
5771
6784
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
5772
|
-
function
|
|
6785
|
+
function asArray33(v) {
|
|
5773
6786
|
if (Array.isArray(v)) return v;
|
|
5774
6787
|
if (v && typeof v === "object") {
|
|
5775
6788
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5776
6789
|
}
|
|
5777
6790
|
return [];
|
|
5778
6791
|
}
|
|
5779
|
-
function
|
|
6792
|
+
function strName14(v) {
|
|
5780
6793
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5781
6794
|
}
|
|
5782
6795
|
function collectNavExposures(stack) {
|
|
5783
6796
|
const out = [];
|
|
5784
|
-
const apps =
|
|
6797
|
+
const apps = asArray33(stack.apps);
|
|
5785
6798
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
5786
6799
|
const app = apps[ai];
|
|
5787
6800
|
if (!app || typeof app !== "object") continue;
|
|
5788
|
-
const appName =
|
|
6801
|
+
const appName = strName14(app.name) ?? `#${ai}`;
|
|
5789
6802
|
const walk = (items, basePath) => {
|
|
5790
|
-
const navItems =
|
|
6803
|
+
const navItems = asArray33(items);
|
|
5791
6804
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
5792
6805
|
const nav = navItems[ni];
|
|
5793
6806
|
if (!nav || typeof nav !== "object") continue;
|
|
5794
6807
|
const navPath = `${basePath}[${ni}]`;
|
|
5795
|
-
const objectName =
|
|
6808
|
+
const objectName = strName14(nav.objectName);
|
|
5796
6809
|
if (nav.type === "object" && objectName) {
|
|
5797
6810
|
out.push({
|
|
5798
6811
|
objectName,
|
|
5799
|
-
where: `app "${appName}" \xB7 nav "${
|
|
6812
|
+
where: `app "${appName}" \xB7 nav "${strName14(nav.id) ?? `#${ni}`}"`,
|
|
5800
6813
|
path: `${navPath}.objectName`
|
|
5801
6814
|
});
|
|
5802
6815
|
}
|
|
@@ -5804,7 +6817,7 @@ function collectNavExposures(stack) {
|
|
|
5804
6817
|
}
|
|
5805
6818
|
};
|
|
5806
6819
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
5807
|
-
const areas =
|
|
6820
|
+
const areas = asArray33(app.areas);
|
|
5808
6821
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
5809
6822
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
5810
6823
|
}
|
|
@@ -5814,13 +6827,13 @@ function collectNavExposures(stack) {
|
|
|
5814
6827
|
function validateNavAccess(stack) {
|
|
5815
6828
|
const findings = [];
|
|
5816
6829
|
if (!stack || typeof stack !== "object") return findings;
|
|
5817
|
-
const permissionSets =
|
|
6830
|
+
const permissionSets = asArray33(stack.permissions);
|
|
5818
6831
|
if (permissionSets.length === 0) return findings;
|
|
5819
6832
|
const exposures = collectNavExposures(stack);
|
|
5820
6833
|
if (exposures.length === 0) return findings;
|
|
5821
6834
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
5822
|
-
for (const obj of
|
|
5823
|
-
const n =
|
|
6835
|
+
for (const obj of asArray33(stack.objects)) {
|
|
6836
|
+
const n = strName14(obj.name);
|
|
5824
6837
|
if (n) ownObjects.add(n);
|
|
5825
6838
|
}
|
|
5826
6839
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -5849,18 +6862,19 @@ function validateNavAccess(stack) {
|
|
|
5849
6862
|
}
|
|
5850
6863
|
|
|
5851
6864
|
// src/validate-translation-references.ts
|
|
6865
|
+
import { expandViewContainer } from "@objectstack/spec";
|
|
5852
6866
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
5853
6867
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
5854
6868
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
5855
|
-
function
|
|
6869
|
+
function isRec17(v) {
|
|
5856
6870
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5857
6871
|
}
|
|
5858
|
-
function
|
|
6872
|
+
function asArray34(v) {
|
|
5859
6873
|
if (Array.isArray(v)) return v;
|
|
5860
|
-
if (
|
|
6874
|
+
if (isRec17(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec17(def) ? def : {} }));
|
|
5861
6875
|
return [];
|
|
5862
6876
|
}
|
|
5863
|
-
function
|
|
6877
|
+
function strName15(v) {
|
|
5864
6878
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5865
6879
|
}
|
|
5866
6880
|
function distance5(a, b) {
|
|
@@ -5920,30 +6934,52 @@ function collectViewRecord(view, factsFor) {
|
|
|
5920
6934
|
};
|
|
5921
6935
|
const addSections = (container, binding) => {
|
|
5922
6936
|
if (!binding) return;
|
|
5923
|
-
for (const section of
|
|
5924
|
-
const sectionName =
|
|
6937
|
+
for (const section of asArray34(container.sections)) {
|
|
6938
|
+
const sectionName = strName15(section.name);
|
|
5925
6939
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
5926
6940
|
}
|
|
5927
6941
|
};
|
|
5928
|
-
const listBinding =
|
|
5929
|
-
if (
|
|
5930
|
-
addView(recordObject ?? listBinding,
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
6942
|
+
const listBinding = isRec17(view.list) ? bindingOf(view.list) : void 0;
|
|
6943
|
+
if (isRec17(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
6944
|
+
addView(recordObject ?? listBinding, strName15(view.name));
|
|
6945
|
+
const named = namedViewKeys(view);
|
|
6946
|
+
for (const family of ["listViews", "formViews"]) {
|
|
6947
|
+
const container = view[family];
|
|
6948
|
+
if (!isRec17(container)) continue;
|
|
6949
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
6950
|
+
let at = 0;
|
|
6951
|
+
for (const sub of Object.values(container)) {
|
|
6952
|
+
if (!sub || typeof sub !== "object") continue;
|
|
6953
|
+
const registryKey = registryKeys[at++];
|
|
6954
|
+
if (!isRec17(sub)) continue;
|
|
5936
6955
|
const binding = bindingOf(sub) ?? listBinding;
|
|
5937
|
-
addView(binding,
|
|
5938
|
-
addView(binding, strName13(sub.name));
|
|
6956
|
+
addView(binding, registryKey);
|
|
5939
6957
|
addSections(sub, binding);
|
|
5940
6958
|
}
|
|
5941
6959
|
}
|
|
5942
|
-
if (
|
|
6960
|
+
if (isRec17(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
5943
6961
|
addSections(view, recordObject ?? listBinding);
|
|
5944
6962
|
}
|
|
5945
|
-
function
|
|
5946
|
-
|
|
6963
|
+
function defaultListViewKey(object, container) {
|
|
6964
|
+
if (!object || !isRec17(container.list)) return void 0;
|
|
6965
|
+
const item = expandViewContainer(object, container).find(
|
|
6966
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
6967
|
+
);
|
|
6968
|
+
if (!item) return void 0;
|
|
6969
|
+
const prefix = `${object}.`;
|
|
6970
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
6971
|
+
}
|
|
6972
|
+
function namedViewKeys(container) {
|
|
6973
|
+
const object = "probe";
|
|
6974
|
+
const prefix = `${object}.`;
|
|
6975
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
6976
|
+
const countEntries = (v) => isRec17(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
6977
|
+
const listCount = countEntries(container.listViews);
|
|
6978
|
+
const formCount = countEntries(container.formViews);
|
|
6979
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
6980
|
+
const items = expandViewContainer(object, container);
|
|
6981
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
6982
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
5947
6983
|
}
|
|
5948
6984
|
function readOptions(field) {
|
|
5949
6985
|
const raw = field.options;
|
|
@@ -5955,14 +6991,14 @@ function readOptions(field) {
|
|
|
5955
6991
|
values.add(opt);
|
|
5956
6992
|
continue;
|
|
5957
6993
|
}
|
|
5958
|
-
if (!
|
|
5959
|
-
const value =
|
|
6994
|
+
if (!isRec17(opt)) continue;
|
|
6995
|
+
const value = strName15(opt.value);
|
|
5960
6996
|
if (!value) continue;
|
|
5961
6997
|
values.add(value);
|
|
5962
|
-
const label2 =
|
|
6998
|
+
const label2 = strName15(opt.label);
|
|
5963
6999
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
5964
7000
|
}
|
|
5965
|
-
} else if (
|
|
7001
|
+
} else if (isRec17(raw)) {
|
|
5966
7002
|
for (const [value, label2] of Object.entries(raw)) {
|
|
5967
7003
|
values.add(value);
|
|
5968
7004
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -5982,48 +7018,48 @@ function buildUniverse(stack) {
|
|
|
5982
7018
|
}
|
|
5983
7019
|
return facts;
|
|
5984
7020
|
};
|
|
5985
|
-
for (const obj of
|
|
5986
|
-
const objectName =
|
|
7021
|
+
for (const obj of asArray34(stack.objects)) {
|
|
7022
|
+
const objectName = strName15(obj.name);
|
|
5987
7023
|
if (!objectName) continue;
|
|
5988
7024
|
const facts = factsFor(objectName);
|
|
5989
|
-
for (const field of
|
|
5990
|
-
const fieldName =
|
|
7025
|
+
for (const field of asArray34(obj.fields)) {
|
|
7026
|
+
const fieldName = strName15(field.name);
|
|
5991
7027
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
5992
7028
|
}
|
|
5993
|
-
for (const action of
|
|
5994
|
-
const actionName =
|
|
7029
|
+
for (const action of asArray34(obj.actions)) {
|
|
7030
|
+
const actionName = strName15(action.name);
|
|
5995
7031
|
if (actionName) facts.actions.set(actionName, action);
|
|
5996
7032
|
}
|
|
5997
|
-
for (const view of
|
|
5998
|
-
collectViewRecord({ ...view, object:
|
|
7033
|
+
for (const view of asArray34(obj.views)) {
|
|
7034
|
+
collectViewRecord({ ...view, object: strName15(view.object) ?? objectName }, factsFor);
|
|
5999
7035
|
}
|
|
6000
7036
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
6001
|
-
for (const group of
|
|
6002
|
-
const key =
|
|
7037
|
+
for (const group of asArray34(obj.fieldGroups)) {
|
|
7038
|
+
const key = strName15(group.key) ?? strName15(group.name);
|
|
6003
7039
|
if (key) facts.sections.add(key);
|
|
6004
7040
|
}
|
|
6005
7041
|
}
|
|
6006
|
-
for (const view of
|
|
7042
|
+
for (const view of asArray34(stack.views)) {
|
|
6007
7043
|
collectViewRecord(view, factsFor);
|
|
6008
7044
|
}
|
|
6009
|
-
const pages =
|
|
7045
|
+
const pages = asArray34(stack.pages);
|
|
6010
7046
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6011
7047
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
6012
7048
|
if (!walked.objectName) continue;
|
|
6013
|
-
const props =
|
|
7049
|
+
const props = isRec17(walked.component.properties) ? walked.component.properties : void 0;
|
|
6014
7050
|
if (!props) continue;
|
|
6015
|
-
for (const section of
|
|
6016
|
-
const sectionName =
|
|
7051
|
+
for (const section of asArray34(props.sections)) {
|
|
7052
|
+
const sectionName = strName15(section.name);
|
|
6017
7053
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
6018
7054
|
}
|
|
6019
7055
|
}
|
|
6020
7056
|
}
|
|
6021
7057
|
const globalActions = /* @__PURE__ */ new Map();
|
|
6022
7058
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
6023
|
-
for (const action of
|
|
6024
|
-
const actionName =
|
|
7059
|
+
for (const action of asArray34(stack.actions)) {
|
|
7060
|
+
const actionName = strName15(action.name);
|
|
6025
7061
|
if (!actionName) continue;
|
|
6026
|
-
const owner =
|
|
7062
|
+
const owner = strName15(action.objectName) ?? strName15(action.object);
|
|
6027
7063
|
if (owner) {
|
|
6028
7064
|
factsFor(owner).actions.set(actionName, action);
|
|
6029
7065
|
actionOwners.set(actionName, owner);
|
|
@@ -6037,41 +7073,41 @@ function buildUniverse(stack) {
|
|
|
6037
7073
|
}
|
|
6038
7074
|
}
|
|
6039
7075
|
const apps = /* @__PURE__ */ new Map();
|
|
6040
|
-
for (const app of
|
|
6041
|
-
const appName =
|
|
7076
|
+
for (const app of asArray34(stack.apps)) {
|
|
7077
|
+
const appName = strName15(app.name);
|
|
6042
7078
|
if (!appName) continue;
|
|
6043
7079
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
6044
7080
|
const walkNav = (items) => {
|
|
6045
|
-
for (const item of
|
|
6046
|
-
const id =
|
|
7081
|
+
for (const item of asArray34(items)) {
|
|
7082
|
+
const id = strName15(item.id);
|
|
6047
7083
|
if (id) navIds.add(id);
|
|
6048
7084
|
if (item.children) walkNav(item.children);
|
|
6049
7085
|
}
|
|
6050
7086
|
};
|
|
6051
7087
|
walkNav(app.navigation);
|
|
6052
|
-
for (const area of
|
|
6053
|
-
const areaId =
|
|
7088
|
+
for (const area of asArray34(app.areas)) {
|
|
7089
|
+
const areaId = strName15(area.id);
|
|
6054
7090
|
if (areaId) navIds.add(areaId);
|
|
6055
7091
|
walkNav(area.navigation);
|
|
6056
7092
|
}
|
|
6057
7093
|
apps.set(appName, navIds);
|
|
6058
7094
|
}
|
|
6059
7095
|
const dashboards = /* @__PURE__ */ new Map();
|
|
6060
|
-
for (const dash of
|
|
6061
|
-
const dashName =
|
|
7096
|
+
for (const dash of asArray34(stack.dashboards)) {
|
|
7097
|
+
const dashName = strName15(dash.name);
|
|
6062
7098
|
if (!dashName) continue;
|
|
6063
7099
|
const widgets = /* @__PURE__ */ new Set();
|
|
6064
|
-
for (const widget of
|
|
6065
|
-
const id =
|
|
7100
|
+
for (const widget of asArray34(dash.widgets)) {
|
|
7101
|
+
const id = strName15(widget.id) ?? strName15(widget.name);
|
|
6066
7102
|
if (id) widgets.add(id);
|
|
6067
7103
|
}
|
|
6068
7104
|
const actions = /* @__PURE__ */ new Set();
|
|
6069
7105
|
const headerActions = [
|
|
6070
|
-
...
|
|
6071
|
-
...
|
|
7106
|
+
...asArray34(isRec17(dash.header) ? dash.header.actions : void 0),
|
|
7107
|
+
...asArray34(dash.actions)
|
|
6072
7108
|
];
|
|
6073
7109
|
for (const action of headerActions) {
|
|
6074
|
-
const key =
|
|
7110
|
+
const key = strName15(action.actionUrl) ?? strName15(action.url) ?? strName15(action.name);
|
|
6075
7111
|
if (key) actions.add(key);
|
|
6076
7112
|
}
|
|
6077
7113
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -6083,7 +7119,7 @@ function localePath(bundleIndex, locale) {
|
|
|
6083
7119
|
}
|
|
6084
7120
|
function validateTranslationReferences(stack) {
|
|
6085
7121
|
const findings = [];
|
|
6086
|
-
if (!
|
|
7122
|
+
if (!isRec17(stack)) return findings;
|
|
6087
7123
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6088
7124
|
if (bundles.length === 0) return findings;
|
|
6089
7125
|
const universe = buildUniverse(stack);
|
|
@@ -6092,13 +7128,13 @@ function validateTranslationReferences(stack) {
|
|
|
6092
7128
|
};
|
|
6093
7129
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
6094
7130
|
const bundle = bundles[bi];
|
|
6095
|
-
if (!
|
|
7131
|
+
if (!isRec17(bundle)) continue;
|
|
6096
7132
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
6097
|
-
if (!
|
|
7133
|
+
if (!isRec17(rawData)) continue;
|
|
6098
7134
|
const base = localePath(bi, locale);
|
|
6099
7135
|
const inLocale = `locale "${locale}"`;
|
|
6100
7136
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
6101
|
-
if (!
|
|
7137
|
+
if (!isRec17(rawNode)) continue;
|
|
6102
7138
|
const objPath = `${base}.objects.${objectName}`;
|
|
6103
7139
|
const facts = universe.objects.get(objectName);
|
|
6104
7140
|
if (!facts) {
|
|
@@ -6124,7 +7160,7 @@ function validateTranslationReferences(stack) {
|
|
|
6124
7160
|
);
|
|
6125
7161
|
continue;
|
|
6126
7162
|
}
|
|
6127
|
-
if (!
|
|
7163
|
+
if (!isRec17(rawField)) continue;
|
|
6128
7164
|
checkOptionKeys(findings, {
|
|
6129
7165
|
optionMap: rawField.options,
|
|
6130
7166
|
field,
|
|
@@ -6206,7 +7242,7 @@ function validateTranslationReferences(stack) {
|
|
|
6206
7242
|
);
|
|
6207
7243
|
continue;
|
|
6208
7244
|
}
|
|
6209
|
-
if (!
|
|
7245
|
+
if (!isRec17(rawApp)) continue;
|
|
6210
7246
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
6211
7247
|
if (navIds.has(navId)) continue;
|
|
6212
7248
|
orphan(
|
|
@@ -6229,7 +7265,7 @@ function validateTranslationReferences(stack) {
|
|
|
6229
7265
|
);
|
|
6230
7266
|
continue;
|
|
6231
7267
|
}
|
|
6232
|
-
if (!
|
|
7268
|
+
if (!isRec17(rawDash)) continue;
|
|
6233
7269
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
6234
7270
|
if (dash.widgets.has(widgetId)) continue;
|
|
6235
7271
|
orphan(
|
|
@@ -6254,7 +7290,7 @@ function validateTranslationReferences(stack) {
|
|
|
6254
7290
|
return findings;
|
|
6255
7291
|
}
|
|
6256
7292
|
function asRecord(v) {
|
|
6257
|
-
return
|
|
7293
|
+
return isRec17(v) ? v : {};
|
|
6258
7294
|
}
|
|
6259
7295
|
function checkOptionKeys(findings, ctx) {
|
|
6260
7296
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -6266,7 +7302,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
6266
7302
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
6267
7303
|
where: ctx.where,
|
|
6268
7304
|
path: ctx.path,
|
|
6269
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
7305
|
+
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.`,
|
|
6270
7306
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
6271
7307
|
});
|
|
6272
7308
|
return;
|
|
@@ -6285,11 +7321,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
6285
7321
|
}
|
|
6286
7322
|
}
|
|
6287
7323
|
function checkActionParams(findings, ctx) {
|
|
6288
|
-
const rawParams = Object.keys(asRecord(
|
|
7324
|
+
const rawParams = Object.keys(asRecord(isRec17(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
6289
7325
|
if (rawParams.length === 0) return;
|
|
6290
7326
|
const declared = /* @__PURE__ */ new Set();
|
|
6291
|
-
for (const param of
|
|
6292
|
-
const name =
|
|
7327
|
+
for (const param of asArray34(ctx.action.params)) {
|
|
7328
|
+
const name = strName15(param.name) ?? strName15(param.field);
|
|
6293
7329
|
if (name) declared.add(name);
|
|
6294
7330
|
}
|
|
6295
7331
|
for (const paramName of rawParams) {
|
|
@@ -6307,76 +7343,40 @@ function checkActionParams(findings, ctx) {
|
|
|
6307
7343
|
|
|
6308
7344
|
// src/validate-translatable-sections.ts
|
|
6309
7345
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
6310
|
-
function
|
|
7346
|
+
function isRec18(v) {
|
|
6311
7347
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6312
7348
|
}
|
|
6313
|
-
function
|
|
7349
|
+
function strName16(v) {
|
|
6314
7350
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6315
7351
|
}
|
|
6316
|
-
function viewObjectName2(view) {
|
|
6317
|
-
return strName14(view.objectName) ?? strName14(view.object) ?? (isRec14(view.data) ? strName14(view.data.object) : void 0);
|
|
6318
|
-
}
|
|
6319
|
-
function collectionEntries(v, base) {
|
|
6320
|
-
if (Array.isArray(v)) {
|
|
6321
|
-
const out = [];
|
|
6322
|
-
for (let i = 0; i < v.length; i++) {
|
|
6323
|
-
if (isRec14(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
6324
|
-
}
|
|
6325
|
-
return out;
|
|
6326
|
-
}
|
|
6327
|
-
if (isRec14(v)) {
|
|
6328
|
-
return Object.entries(v).filter(([, def]) => isRec14(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
6329
|
-
}
|
|
6330
|
-
return [];
|
|
6331
|
-
}
|
|
6332
7352
|
function viewLabel(view) {
|
|
6333
|
-
const name =
|
|
7353
|
+
const name = strName16(view.name);
|
|
6334
7354
|
return name ? `view "${name}"` : "";
|
|
6335
7355
|
}
|
|
6336
7356
|
function joinWhere(...parts) {
|
|
6337
7357
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
6338
7358
|
}
|
|
6339
7359
|
function collectViewSites(view, basePath, label2, sites) {
|
|
6340
|
-
const recordObject =
|
|
6341
|
-
const listBinding =
|
|
6342
|
-
const
|
|
6343
|
-
sites.push({
|
|
6344
|
-
path: `${basePath}.sections`,
|
|
6345
|
-
surface: label2,
|
|
6346
|
-
objectName: recordObject ?? listBinding,
|
|
6347
|
-
sections: view.sections
|
|
6348
|
-
});
|
|
6349
|
-
if (isRec14(view.form)) {
|
|
7360
|
+
const recordObject = viewObjectName(view);
|
|
7361
|
+
const listBinding = isRec18(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
7362
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
6350
7363
|
sites.push({
|
|
6351
|
-
path: `${
|
|
6352
|
-
surface: joinWhere(label2,
|
|
6353
|
-
objectName:
|
|
6354
|
-
sections: view.
|
|
7364
|
+
path: `${site.path}.sections`,
|
|
7365
|
+
surface: joinWhere(label2, site.surface),
|
|
7366
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
7367
|
+
sections: site.view.sections
|
|
6355
7368
|
});
|
|
6356
7369
|
}
|
|
6357
|
-
for (const key of ["listViews", "formViews"]) {
|
|
6358
|
-
const container = view[key];
|
|
6359
|
-
if (!isRec14(container)) continue;
|
|
6360
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
6361
|
-
if (!isRec14(sub)) continue;
|
|
6362
|
-
sites.push({
|
|
6363
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
6364
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
6365
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
6366
|
-
sections: sub.sections
|
|
6367
|
-
});
|
|
6368
|
-
}
|
|
6369
|
-
}
|
|
6370
7370
|
}
|
|
6371
7371
|
function translatedObjectNames(stack) {
|
|
6372
7372
|
const out = /* @__PURE__ */ new Set();
|
|
6373
7373
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6374
7374
|
for (const bundle of bundles) {
|
|
6375
|
-
if (!
|
|
7375
|
+
if (!isRec18(bundle)) continue;
|
|
6376
7376
|
for (const data of Object.values(bundle)) {
|
|
6377
|
-
if (!
|
|
7377
|
+
if (!isRec18(data) || !isRec18(data.objects)) continue;
|
|
6378
7378
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
6379
|
-
if (
|
|
7379
|
+
if (isRec18(node)) out.add(objectName);
|
|
6380
7380
|
}
|
|
6381
7381
|
}
|
|
6382
7382
|
}
|
|
@@ -6388,22 +7388,22 @@ function suggestedName(label2) {
|
|
|
6388
7388
|
}
|
|
6389
7389
|
function validateTranslatableSections(stack) {
|
|
6390
7390
|
const findings = [];
|
|
6391
|
-
if (!
|
|
7391
|
+
if (!isRec18(stack)) return findings;
|
|
6392
7392
|
const translated = translatedObjectNames(stack);
|
|
6393
7393
|
if (translated.size === 0) return findings;
|
|
6394
7394
|
const sites = [];
|
|
6395
7395
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
6396
|
-
const objectName =
|
|
7396
|
+
const objectName = strName16(obj.name);
|
|
6397
7397
|
if (!objectName) continue;
|
|
6398
7398
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
6399
7399
|
collectViewSites(
|
|
6400
|
-
{ ...view, object:
|
|
7400
|
+
{ ...view, object: strName16(view.object) ?? objectName },
|
|
6401
7401
|
path,
|
|
6402
7402
|
viewLabel(view),
|
|
6403
7403
|
sites
|
|
6404
7404
|
);
|
|
6405
7405
|
}
|
|
6406
|
-
if (
|
|
7406
|
+
if (isRec18(obj.listViews)) {
|
|
6407
7407
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
6408
7408
|
}
|
|
6409
7409
|
}
|
|
@@ -6411,13 +7411,13 @@ function validateTranslatableSections(stack) {
|
|
|
6411
7411
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
6412
7412
|
}
|
|
6413
7413
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6414
|
-
const pageName =
|
|
7414
|
+
const pageName = strName16(page.name);
|
|
6415
7415
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
6416
7416
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6417
7417
|
if (!walked.objectName) continue;
|
|
6418
|
-
const props =
|
|
7418
|
+
const props = isRec18(walked.component.properties) ? walked.component.properties : void 0;
|
|
6419
7419
|
if (!props) continue;
|
|
6420
|
-
const type =
|
|
7420
|
+
const type = strName16(walked.component.type) ?? "component";
|
|
6421
7421
|
sites.push({
|
|
6422
7422
|
path: `${walked.path}.properties.sections`,
|
|
6423
7423
|
surface: joinWhere(pageLabel, type),
|
|
@@ -6432,9 +7432,9 @@ function validateTranslatableSections(stack) {
|
|
|
6432
7432
|
if (!Array.isArray(site.sections)) continue;
|
|
6433
7433
|
for (let i = 0; i < site.sections.length; i++) {
|
|
6434
7434
|
const section = site.sections[i];
|
|
6435
|
-
if (!
|
|
6436
|
-
if (
|
|
6437
|
-
const heading =
|
|
7435
|
+
if (!isRec18(section)) continue;
|
|
7436
|
+
if (strName16(section.name)) continue;
|
|
7437
|
+
const heading = strName16(section.label);
|
|
6438
7438
|
if (!heading) continue;
|
|
6439
7439
|
const slug = suggestedName(heading);
|
|
6440
7440
|
findings.push({
|
|
@@ -6452,14 +7452,14 @@ function validateTranslatableSections(stack) {
|
|
|
6452
7452
|
|
|
6453
7453
|
// src/validate-ai-surface-affinity.ts
|
|
6454
7454
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
6455
|
-
function
|
|
7455
|
+
function asArray35(v) {
|
|
6456
7456
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6457
7457
|
if (v && typeof v === "object") {
|
|
6458
7458
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6459
7459
|
}
|
|
6460
7460
|
return [];
|
|
6461
7461
|
}
|
|
6462
|
-
function
|
|
7462
|
+
function strName17(v) {
|
|
6463
7463
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6464
7464
|
}
|
|
6465
7465
|
function surfaceOf(v) {
|
|
@@ -6469,18 +7469,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
6469
7469
|
const findings = [];
|
|
6470
7470
|
if (!stack || typeof stack !== "object") return findings;
|
|
6471
7471
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
6472
|
-
for (const skill of
|
|
6473
|
-
const n =
|
|
7472
|
+
for (const skill of asArray35(stack.skills)) {
|
|
7473
|
+
const n = strName17(skill.name);
|
|
6474
7474
|
if (n) skillsByName.set(n, skill);
|
|
6475
7475
|
}
|
|
6476
|
-
const agents =
|
|
7476
|
+
const agents = asArray35(stack.agents);
|
|
6477
7477
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
6478
7478
|
const agent = agents[ai];
|
|
6479
|
-
const agentName =
|
|
7479
|
+
const agentName = strName17(agent.name) ?? `#${ai}`;
|
|
6480
7480
|
const agentSurface = surfaceOf(agent.surface);
|
|
6481
7481
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
6482
7482
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
6483
|
-
const ref =
|
|
7483
|
+
const ref = strName17(skillRefs[si]);
|
|
6484
7484
|
if (!ref) continue;
|
|
6485
7485
|
const skill = skillsByName.get(ref);
|
|
6486
7486
|
if (!skill) continue;
|
|
@@ -6502,14 +7502,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
6502
7502
|
// src/validate-ai-tool-references.ts
|
|
6503
7503
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
6504
7504
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
6505
|
-
function
|
|
7505
|
+
function asArray36(v) {
|
|
6506
7506
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6507
7507
|
if (v && typeof v === "object") {
|
|
6508
7508
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6509
7509
|
}
|
|
6510
7510
|
return [];
|
|
6511
7511
|
}
|
|
6512
|
-
function
|
|
7512
|
+
function strName18(v) {
|
|
6513
7513
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6514
7514
|
}
|
|
6515
7515
|
function distance6(a, b) {
|
|
@@ -6550,26 +7550,26 @@ function materialisesAsTool(action) {
|
|
|
6550
7550
|
if (!ai || typeof ai !== "object") return false;
|
|
6551
7551
|
const aiRec = ai;
|
|
6552
7552
|
if (aiRec.exposed !== true) return false;
|
|
6553
|
-
if (!
|
|
6554
|
-
const type =
|
|
7553
|
+
if (!strName18(aiRec.description)) return false;
|
|
7554
|
+
const type = strName18(action.type);
|
|
6555
7555
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
6556
7556
|
if (type === "script") return Boolean(action.target || action.body);
|
|
6557
7557
|
return Boolean(action.target);
|
|
6558
7558
|
}
|
|
6559
7559
|
function collectToolUniverse(stack) {
|
|
6560
7560
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
6561
|
-
for (const tool of
|
|
6562
|
-
const n =
|
|
7561
|
+
for (const tool of asArray36(stack.tools)) {
|
|
7562
|
+
const n = strName18(tool.name);
|
|
6563
7563
|
if (n) universe.add(n);
|
|
6564
7564
|
}
|
|
6565
7565
|
const addActionFamily = (actions) => {
|
|
6566
|
-
for (const action of
|
|
6567
|
-
const n =
|
|
7566
|
+
for (const action of asArray36(actions)) {
|
|
7567
|
+
const n = strName18(action.name);
|
|
6568
7568
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
6569
7569
|
}
|
|
6570
7570
|
};
|
|
6571
7571
|
addActionFamily(stack.actions);
|
|
6572
|
-
for (const obj of
|
|
7572
|
+
for (const obj of asArray36(stack.objects)) {
|
|
6573
7573
|
addActionFamily(obj.actions);
|
|
6574
7574
|
}
|
|
6575
7575
|
return universe;
|
|
@@ -6577,13 +7577,13 @@ function collectToolUniverse(stack) {
|
|
|
6577
7577
|
function collectUnexposedActionNames(stack) {
|
|
6578
7578
|
const names = /* @__PURE__ */ new Set();
|
|
6579
7579
|
const scan = (actions) => {
|
|
6580
|
-
for (const action of
|
|
6581
|
-
const n =
|
|
7580
|
+
for (const action of asArray36(actions)) {
|
|
7581
|
+
const n = strName18(action.name);
|
|
6582
7582
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
6583
7583
|
}
|
|
6584
7584
|
};
|
|
6585
7585
|
scan(stack.actions);
|
|
6586
|
-
for (const obj of
|
|
7586
|
+
for (const obj of asArray36(stack.objects)) scan(obj.actions);
|
|
6587
7587
|
return names;
|
|
6588
7588
|
}
|
|
6589
7589
|
function validateAiToolReferences(stack) {
|
|
@@ -6601,13 +7601,13 @@ function validateAiToolReferences(stack) {
|
|
|
6601
7601
|
}
|
|
6602
7602
|
return universe.has(ref);
|
|
6603
7603
|
};
|
|
6604
|
-
const skills =
|
|
7604
|
+
const skills = asArray36(stack.skills);
|
|
6605
7605
|
for (let si = 0; si < skills.length; si++) {
|
|
6606
7606
|
const skill = skills[si];
|
|
6607
|
-
const skillName =
|
|
7607
|
+
const skillName = strName18(skill.name) ?? `#${si}`;
|
|
6608
7608
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
6609
7609
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
6610
|
-
const ref =
|
|
7610
|
+
const ref = strName18(refs[ti]);
|
|
6611
7611
|
if (!ref || resolves(ref)) continue;
|
|
6612
7612
|
const isPattern = ref.endsWith("*");
|
|
6613
7613
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -6626,24 +7626,25 @@ function validateAiToolReferences(stack) {
|
|
|
6626
7626
|
|
|
6627
7627
|
// src/validate-ai-agent-authoring.ts
|
|
6628
7628
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
6629
|
-
|
|
7629
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
7630
|
+
function asArray37(v) {
|
|
6630
7631
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6631
7632
|
if (v && typeof v === "object") {
|
|
6632
7633
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6633
7634
|
}
|
|
6634
7635
|
return [];
|
|
6635
7636
|
}
|
|
6636
|
-
function
|
|
7637
|
+
function strName19(v) {
|
|
6637
7638
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6638
7639
|
}
|
|
6639
7640
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
6640
7641
|
function validateAiAgentAuthoring(stack) {
|
|
6641
7642
|
const findings = [];
|
|
6642
7643
|
if (!stack || typeof stack !== "object") return findings;
|
|
6643
|
-
const agents =
|
|
7644
|
+
const agents = asArray37(stack.agents);
|
|
6644
7645
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
6645
7646
|
const agent = agents[ai];
|
|
6646
|
-
const name =
|
|
7647
|
+
const name = strName19(agent.name) ?? `#${ai}`;
|
|
6647
7648
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
6648
7649
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
6649
7650
|
findings.push({
|
|
@@ -6655,24 +7656,40 @@ function validateAiAgentAuthoring(stack) {
|
|
|
6655
7656
|
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.` : ``)
|
|
6656
7657
|
});
|
|
6657
7658
|
}
|
|
7659
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
7660
|
+
const apps = asArray37(stack.apps);
|
|
7661
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
7662
|
+
const app = apps[appIdx];
|
|
7663
|
+
const defaultAgent = strName19(app.defaultAgent);
|
|
7664
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
7665
|
+
const appName = strName19(app.name) ?? `#${appIdx}`;
|
|
7666
|
+
findings.push({
|
|
7667
|
+
severity: "warning",
|
|
7668
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
7669
|
+
where: `app "${appName}".defaultAgent`,
|
|
7670
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
7671
|
+
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.`,
|
|
7672
|
+
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.`
|
|
7673
|
+
});
|
|
7674
|
+
}
|
|
6658
7675
|
return findings;
|
|
6659
7676
|
}
|
|
6660
7677
|
|
|
6661
7678
|
// src/validate-hook-body-writes.ts
|
|
6662
|
-
import { createRequire as
|
|
6663
|
-
import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
|
|
6664
|
-
var
|
|
6665
|
-
function
|
|
6666
|
-
if (
|
|
7679
|
+
import { createRequire as createRequire5 } from "module";
|
|
7680
|
+
import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
|
|
7681
|
+
var cachedTs3 = null;
|
|
7682
|
+
function loadTypeScript3() {
|
|
7683
|
+
if (cachedTs3) return cachedTs3;
|
|
6667
7684
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
6668
7685
|
try {
|
|
6669
|
-
|
|
7686
|
+
cachedTs3 = createRequire5(anchor)("typescript");
|
|
6670
7687
|
} catch (err) {
|
|
6671
7688
|
throw new Error(
|
|
6672
7689
|
`@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.`
|
|
6673
7690
|
);
|
|
6674
7691
|
}
|
|
6675
|
-
return
|
|
7692
|
+
return cachedTs3;
|
|
6676
7693
|
}
|
|
6677
7694
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
6678
7695
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
@@ -6747,24 +7764,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
6747
7764
|
"owner",
|
|
6748
7765
|
"record_type"
|
|
6749
7766
|
]);
|
|
6750
|
-
var
|
|
6751
|
-
function
|
|
6752
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6753
|
-
if (
|
|
7767
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7768
|
+
function asArray38(v) {
|
|
7769
|
+
if (Array.isArray(v)) return v.filter((x) => isRec19(x));
|
|
7770
|
+
if (isRec19(v)) {
|
|
6754
7771
|
return Object.entries(v).map(([name, def]) => ({
|
|
6755
7772
|
name,
|
|
6756
|
-
...
|
|
7773
|
+
...isRec19(def) ? def : {}
|
|
6757
7774
|
}));
|
|
6758
7775
|
}
|
|
6759
7776
|
return [];
|
|
6760
7777
|
}
|
|
6761
7778
|
function indexObjectFields2(stack) {
|
|
6762
7779
|
const out = /* @__PURE__ */ new Map();
|
|
6763
|
-
for (const obj of
|
|
7780
|
+
for (const obj of asArray38(stack.objects)) {
|
|
6764
7781
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6765
7782
|
if (!name) continue;
|
|
6766
7783
|
const names = /* @__PURE__ */ new Set();
|
|
6767
|
-
for (const f of
|
|
7784
|
+
for (const f of asArray38(obj.fields)) {
|
|
6768
7785
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
6769
7786
|
}
|
|
6770
7787
|
out.set(name, names);
|
|
@@ -6783,7 +7800,7 @@ function extractHookBodyWriteSet(source) {
|
|
|
6783
7800
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
6784
7801
|
return { writes: [], ctxRecordEscapes: false };
|
|
6785
7802
|
}
|
|
6786
|
-
const tsc =
|
|
7803
|
+
const tsc = loadTypeScript3();
|
|
6787
7804
|
const sf = tsc.createSourceFile(
|
|
6788
7805
|
"hook-body.ts",
|
|
6789
7806
|
`async function __body(ctx) {
|
|
@@ -6894,12 +7911,12 @@ ${source}
|
|
|
6894
7911
|
}
|
|
6895
7912
|
function validateHookBodyWrites(stack) {
|
|
6896
7913
|
const findings = [];
|
|
6897
|
-
const hooks =
|
|
7914
|
+
const hooks = asArray38(stack.hooks);
|
|
6898
7915
|
if (hooks.length === 0) return findings;
|
|
6899
7916
|
let objectFields = null;
|
|
6900
7917
|
hooks.forEach((hook, hookIndex) => {
|
|
6901
7918
|
const body = hook.body;
|
|
6902
|
-
if (!
|
|
7919
|
+
if (!isRec19(body) || body.language !== "js") return;
|
|
6903
7920
|
const source = body.source;
|
|
6904
7921
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
6905
7922
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -6956,12 +7973,12 @@ function unionCandidates(targetSets) {
|
|
|
6956
7973
|
return [...out];
|
|
6957
7974
|
}
|
|
6958
7975
|
function fixHint(field, declared) {
|
|
6959
|
-
const suggestion =
|
|
7976
|
+
const suggestion = formatSuggestion2(findClosestMatches2(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
6960
7977
|
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in HOOK_BODY_WRITE_PATTERNS are checked \u2014 computed keys, spreads and aliased input are not \u2014 and this warning never blocks a build.`;
|
|
6961
7978
|
}
|
|
6962
7979
|
|
|
6963
7980
|
// src/validate-action-body-writes.ts
|
|
6964
|
-
import { findClosestMatches as
|
|
7981
|
+
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
6965
7982
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
6966
7983
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
6967
7984
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
@@ -6980,13 +7997,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
6980
7997
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
6981
7998
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
6982
7999
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
6983
|
-
var
|
|
6984
|
-
function
|
|
6985
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6986
|
-
if (
|
|
8000
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8001
|
+
function asArray39(v) {
|
|
8002
|
+
if (Array.isArray(v)) return v.filter((x) => isRec20(x));
|
|
8003
|
+
if (isRec20(v)) {
|
|
6987
8004
|
return Object.entries(v).map(([name, def]) => ({
|
|
6988
8005
|
name,
|
|
6989
|
-
...
|
|
8006
|
+
...isRec20(def) ? def : {}
|
|
6990
8007
|
}));
|
|
6991
8008
|
}
|
|
6992
8009
|
return [];
|
|
@@ -7000,11 +8017,11 @@ function collectActionBodies(stack) {
|
|
|
7000
8017
|
const sites = [];
|
|
7001
8018
|
const seen = /* @__PURE__ */ new Set();
|
|
7002
8019
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
7003
|
-
|
|
8020
|
+
asArray39(actions).forEach((action, index) => {
|
|
7004
8021
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
7005
8022
|
if (type !== "script") return;
|
|
7006
8023
|
const body = action.body;
|
|
7007
|
-
if (!
|
|
8024
|
+
if (!isRec20(body) || body.language !== "js") return;
|
|
7008
8025
|
const source = body.source;
|
|
7009
8026
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
7010
8027
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -7015,7 +8032,7 @@ function collectActionBodies(stack) {
|
|
|
7015
8032
|
});
|
|
7016
8033
|
};
|
|
7017
8034
|
collect(stack.actions, "actions");
|
|
7018
|
-
|
|
8035
|
+
asArray39(stack.objects).forEach((obj, objIndex) => {
|
|
7019
8036
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
7020
8037
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
7021
8038
|
});
|
|
@@ -7023,7 +8040,7 @@ function collectActionBodies(stack) {
|
|
|
7023
8040
|
}
|
|
7024
8041
|
function validateActionBodyWrites(stack) {
|
|
7025
8042
|
const findings = [];
|
|
7026
|
-
if (!
|
|
8043
|
+
if (!isRec20(stack)) return findings;
|
|
7027
8044
|
const sites = collectActionBodies(stack);
|
|
7028
8045
|
if (sites.length === 0) return findings;
|
|
7029
8046
|
let objectFields = null;
|
|
@@ -7073,22 +8090,22 @@ function validateActionBodyWrites(stack) {
|
|
|
7073
8090
|
return findings;
|
|
7074
8091
|
}
|
|
7075
8092
|
function fixHint2(field, declared) {
|
|
7076
|
-
const suggestion =
|
|
8093
|
+
const suggestion = formatSuggestion3(findClosestMatches3(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
7077
8094
|
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in ACTION_BODY_WRITE_PATTERNS are checked \u2014 an action's ctx.input is its params bag, so it is not a record-write surface and is never resolved against fields \u2014 and this warning never blocks a build.`;
|
|
7078
8095
|
}
|
|
7079
8096
|
|
|
7080
8097
|
// src/validate-flow-node-writes.ts
|
|
7081
|
-
import { findClosestMatches as
|
|
8098
|
+
import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec/shared";
|
|
7082
8099
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
7083
8100
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
7084
8101
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
7085
|
-
var
|
|
7086
|
-
function
|
|
7087
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
7088
|
-
if (
|
|
8102
|
+
var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8103
|
+
function asArray40(v) {
|
|
8104
|
+
if (Array.isArray(v)) return v.filter((x) => isRec21(x));
|
|
8105
|
+
if (isRec21(v)) {
|
|
7089
8106
|
return Object.entries(v).map(([name, def]) => ({
|
|
7090
8107
|
name,
|
|
7091
|
-
...
|
|
8108
|
+
...isRec21(def) ? def : {}
|
|
7092
8109
|
}));
|
|
7093
8110
|
}
|
|
7094
8111
|
return [];
|
|
@@ -7101,8 +8118,8 @@ function readLiteralObjectName2(config) {
|
|
|
7101
8118
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
7102
8119
|
function validateFlowNodeWrites(stack) {
|
|
7103
8120
|
const findings = [];
|
|
7104
|
-
if (!
|
|
7105
|
-
const flows =
|
|
8121
|
+
if (!isRec21(stack)) return findings;
|
|
8122
|
+
const flows = asArray40(stack.flows);
|
|
7106
8123
|
if (flows.length === 0) return findings;
|
|
7107
8124
|
let objectFields = null;
|
|
7108
8125
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -7110,10 +8127,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
7110
8127
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
7111
8128
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
7112
8129
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
7113
|
-
const config =
|
|
8130
|
+
const config = isRec21(node.config) ? node.config : void 0;
|
|
7114
8131
|
if (!config) return;
|
|
7115
8132
|
const fields = config.fields;
|
|
7116
|
-
if (!
|
|
8133
|
+
if (!isRec21(fields)) return;
|
|
7117
8134
|
const written = Object.keys(fields);
|
|
7118
8135
|
if (written.length === 0) return;
|
|
7119
8136
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -7140,7 +8157,7 @@ function validateFlowNodeWrites(stack) {
|
|
|
7140
8157
|
return findings;
|
|
7141
8158
|
}
|
|
7142
8159
|
function fixHint3(field, declared) {
|
|
7143
|
-
const suggestion =
|
|
8160
|
+
const suggestion = formatSuggestion4(findClosestMatches4(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
7144
8161
|
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. This gates the build rather than warning: the key is literal and so is the object, so unlike the hook/action body rules there is nothing here that could have been mis-extracted.`;
|
|
7145
8162
|
}
|
|
7146
8163
|
|
|
@@ -7258,7 +8275,8 @@ import {
|
|
|
7258
8275
|
APPROVAL_REVISE_NODE_TYPE,
|
|
7259
8276
|
collectFlowGraphs as collectFlowGraphs2
|
|
7260
8277
|
} from "@objectstack/spec/automation";
|
|
7261
|
-
|
|
8278
|
+
import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
|
|
8279
|
+
function asArray41(v) {
|
|
7262
8280
|
if (Array.isArray(v)) return v;
|
|
7263
8281
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7264
8282
|
return [];
|
|
@@ -7532,7 +8550,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7532
8550
|
function filterCarriesNoCondition(filter) {
|
|
7533
8551
|
if (filter === void 0 || filter === null) return true;
|
|
7534
8552
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7535
|
-
return
|
|
8553
|
+
return reduceFilterVerdict2(filter) === "true";
|
|
8554
|
+
}
|
|
8555
|
+
function describeUnboundedFilter(filter) {
|
|
8556
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8557
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8558
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8559
|
+
}
|
|
8560
|
+
function previewFilter(filter) {
|
|
8561
|
+
try {
|
|
8562
|
+
const json = JSON.stringify(filter);
|
|
8563
|
+
if (typeof json !== "string") return typeof filter;
|
|
8564
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8565
|
+
} catch {
|
|
8566
|
+
return typeof filter;
|
|
8567
|
+
}
|
|
7536
8568
|
}
|
|
7537
8569
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7538
8570
|
for (const node of nodes) {
|
|
@@ -7543,10 +8575,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7543
8575
|
if (cfg.multi !== true) continue;
|
|
7544
8576
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7545
8577
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7546
|
-
const filterState = cfg.filter
|
|
8578
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7547
8579
|
findings.push({
|
|
7548
8580
|
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
7549
|
-
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
|
|
8581
|
+
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.`,
|
|
7550
8582
|
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)`,
|
|
7551
8583
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7552
8584
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7628,7 +8660,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
7628
8660
|
}
|
|
7629
8661
|
function lintFlowPatterns(stack) {
|
|
7630
8662
|
const findings = [];
|
|
7631
|
-
for (const flow of
|
|
8663
|
+
for (const flow of asArray41(stack.flows)) {
|
|
7632
8664
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
7633
8665
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
7634
8666
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -7720,19 +8752,19 @@ function lintFlowPatterns(stack) {
|
|
|
7720
8752
|
}
|
|
7721
8753
|
|
|
7722
8754
|
// src/lint-liveness-properties.ts
|
|
7723
|
-
import { createRequire as
|
|
8755
|
+
import { createRequire as createRequire6 } from "module";
|
|
7724
8756
|
import { dirname, join } from "path";
|
|
7725
8757
|
import { existsSync, readFileSync } from "fs";
|
|
7726
8758
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7727
8759
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7728
|
-
function
|
|
8760
|
+
function asArray42(v) {
|
|
7729
8761
|
if (Array.isArray(v)) return v;
|
|
7730
8762
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7731
8763
|
return [];
|
|
7732
8764
|
}
|
|
7733
8765
|
function resolveLivenessDir() {
|
|
7734
8766
|
try {
|
|
7735
|
-
const require2 =
|
|
8767
|
+
const require2 = createRequire6(import.meta.url);
|
|
7736
8768
|
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
7737
8769
|
const dir = join(dirname(pkgJson), "liveness");
|
|
7738
8770
|
return existsSync(dir) ? dir : null;
|
|
@@ -7847,6 +8879,13 @@ var TYPE_COLLECTIONS = [
|
|
|
7847
8879
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
7848
8880
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7849
8881
|
// silent, which is the shape this lint exists to prevent.
|
|
8882
|
+
//
|
|
8883
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
8884
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
8885
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
8886
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
8887
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
8888
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
7850
8889
|
{ type: "dashboard", key: "dashboards" }
|
|
7851
8890
|
];
|
|
7852
8891
|
function lintLivenessProperties(stack) {
|
|
@@ -7855,11 +8894,11 @@ function lintLivenessProperties(stack) {
|
|
|
7855
8894
|
const findings = [];
|
|
7856
8895
|
const objectWarn = loadWarnMap(dir, "object");
|
|
7857
8896
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
7858
|
-
for (const obj of
|
|
8897
|
+
for (const obj of asArray42(stack.objects)) {
|
|
7859
8898
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7860
8899
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
7861
8900
|
if (fieldWarn.size > 0) {
|
|
7862
|
-
for (const field of
|
|
8901
|
+
for (const field of asArray42(obj.fields)) {
|
|
7863
8902
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
7864
8903
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
7865
8904
|
}
|
|
@@ -7868,7 +8907,7 @@ function lintLivenessProperties(stack) {
|
|
|
7868
8907
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7869
8908
|
const warnMap = loadWarnMap(dir, type);
|
|
7870
8909
|
if (warnMap.size === 0) continue;
|
|
7871
|
-
for (const item of
|
|
8910
|
+
for (const item of asArray42(stack[key])) {
|
|
7872
8911
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7873
8912
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7874
8913
|
}
|
|
@@ -7882,7 +8921,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
|
7882
8921
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7883
8922
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7884
8923
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7885
|
-
function
|
|
8924
|
+
function asArray43(v) {
|
|
7886
8925
|
if (Array.isArray(v)) return v;
|
|
7887
8926
|
if (v && typeof v === "object") {
|
|
7888
8927
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7891,9 +8930,9 @@ function asArray44(v) {
|
|
|
7891
8930
|
}
|
|
7892
8931
|
function lintAutonumberFormats(stack) {
|
|
7893
8932
|
const findings = [];
|
|
7894
|
-
for (const obj of
|
|
8933
|
+
for (const obj of asArray43(stack.objects)) {
|
|
7895
8934
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7896
|
-
const fields =
|
|
8935
|
+
const fields = asArray43(obj.fields);
|
|
7897
8936
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7898
8937
|
for (const f of fields) {
|
|
7899
8938
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -7959,7 +8998,7 @@ function lintAutonumberFormats(stack) {
|
|
|
7959
8998
|
|
|
7960
8999
|
// src/lint-view-refs.ts
|
|
7961
9000
|
import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
|
|
7962
|
-
function
|
|
9001
|
+
function asArray44(v) {
|
|
7963
9002
|
if (Array.isArray(v)) return v;
|
|
7964
9003
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7965
9004
|
return [];
|
|
@@ -7987,7 +9026,7 @@ function lintViewRefs(stack) {
|
|
|
7987
9026
|
s.add(kind);
|
|
7988
9027
|
};
|
|
7989
9028
|
const containers = [];
|
|
7990
|
-
for (const v of
|
|
9029
|
+
for (const v of asArray44(stack.views)) {
|
|
7991
9030
|
if (v.viewKind) {
|
|
7992
9031
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7993
9032
|
continue;
|
|
@@ -7996,7 +9035,7 @@ function lintViewRefs(stack) {
|
|
|
7996
9035
|
const object = viewContainerObjectName(v);
|
|
7997
9036
|
if (object) containers.push({ object, container: v });
|
|
7998
9037
|
}
|
|
7999
|
-
for (const obj of
|
|
9038
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8000
9039
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8001
9040
|
if (!object) continue;
|
|
8002
9041
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -8050,11 +9089,11 @@ function lintViewRefs(stack) {
|
|
|
8050
9089
|
});
|
|
8051
9090
|
}
|
|
8052
9091
|
};
|
|
8053
|
-
for (const obj of
|
|
9092
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8054
9093
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8055
|
-
for (const action of
|
|
9094
|
+
for (const action of asArray44(obj.actions)) checkAction(action, object);
|
|
8056
9095
|
}
|
|
8057
|
-
for (const action of
|
|
9096
|
+
for (const action of asArray44(stack.actions)) checkAction(action);
|
|
8058
9097
|
return findings;
|
|
8059
9098
|
}
|
|
8060
9099
|
|
|
@@ -8242,13 +9281,14 @@ function lintDataModel(objects) {
|
|
|
8242
9281
|
if (!obj?.name) continue;
|
|
8243
9282
|
const objPath = `objects[${i}]`;
|
|
8244
9283
|
const fields = fieldEntries2(obj.fields);
|
|
8245
|
-
const hasNameField = !!obj.
|
|
9284
|
+
const hasNameField = !!obj.nameField || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
|
|
8246
9285
|
if (fields.length > 0 && !hasNameField) {
|
|
8247
9286
|
issues.push({
|
|
8248
9287
|
severity: "suggestion",
|
|
8249
9288
|
rule: "object/missing-name-field",
|
|
8250
|
-
message: `Object "${obj.name}" has no name
|
|
8251
|
-
path: `${objPath}.fields
|
|
9289
|
+
message: `Object "${obj.name}" has no nameField and no name-like field \u2014 records will display as raw IDs`,
|
|
9290
|
+
path: `${objPath}.fields`,
|
|
9291
|
+
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.`
|
|
8252
9292
|
});
|
|
8253
9293
|
}
|
|
8254
9294
|
for (const { name: fieldName, def } of fields) {
|
|
@@ -8357,6 +9397,7 @@ var CLI_ONLY = ["cli"];
|
|
|
8357
9397
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8358
9398
|
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.";
|
|
8359
9399
|
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.";
|
|
9400
|
+
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.";
|
|
8360
9401
|
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.";
|
|
8361
9402
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8362
9403
|
var AUTHORING_RULES = [
|
|
@@ -8386,8 +9427,12 @@ var AUTHORING_RULES = [
|
|
|
8386
9427
|
}))
|
|
8387
9428
|
},
|
|
8388
9429
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8389
|
-
// mode)
|
|
8390
|
-
//
|
|
9430
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
9431
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
9432
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
9433
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
9434
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
9435
|
+
// parse would have eaten".
|
|
8391
9436
|
{
|
|
8392
9437
|
name: "validateListViewMode",
|
|
8393
9438
|
tier: "gating",
|
|
@@ -8418,9 +9463,13 @@ var AUTHORING_RULES = [
|
|
|
8418
9463
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8419
9464
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8420
9465
|
},
|
|
8421
|
-
// A
|
|
8422
|
-
//
|
|
8423
|
-
//
|
|
9466
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
9467
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
9468
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
9469
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
9470
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
9471
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
9472
|
+
// declared and which survives the parse untouched.
|
|
8424
9473
|
{
|
|
8425
9474
|
name: "validateViewContainers",
|
|
8426
9475
|
tier: "gating",
|
|
@@ -8470,6 +9519,31 @@ var AUTHORING_RULES = [
|
|
|
8470
9519
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8471
9520
|
run: (stack) => validateFilterTokens(stack)
|
|
8472
9521
|
},
|
|
9522
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9523
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9524
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
9525
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
9526
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
9527
|
+
// same-direction precedent one shape over.
|
|
9528
|
+
{
|
|
9529
|
+
name: "validateEmptyCombinators",
|
|
9530
|
+
tier: "gating",
|
|
9531
|
+
input: "parsed",
|
|
9532
|
+
commands: ALL,
|
|
9533
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
9534
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
9535
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
9536
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
9537
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
9538
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
9539
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
9540
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
9541
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
9542
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
9543
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9544
|
+
runtimeTypes: ["flow"],
|
|
9545
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
9546
|
+
},
|
|
8473
9547
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8474
9548
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8475
9549
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8518,6 +9592,16 @@ var AUTHORING_RULES = [
|
|
|
8518
9592
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8519
9593
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8520
9594
|
// separate step, once the warning-period inventory is empty.
|
|
9595
|
+
//
|
|
9596
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9597
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9598
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9599
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9600
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9601
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9602
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9603
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9604
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8521
9605
|
{
|
|
8522
9606
|
name: "validateComponentProps",
|
|
8523
9607
|
tier: "advisory",
|
|
@@ -8596,10 +9680,12 @@ var AUTHORING_RULES = [
|
|
|
8596
9680
|
//
|
|
8597
9681
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8598
9682
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8599
|
-
// dead?
|
|
9683
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8600
9684
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8601
|
-
// predicate cannot route at all,
|
|
8602
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9685
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9686
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9687
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9688
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8603
9689
|
// can be changed by installing a package, so there is no reading under which
|
|
8604
9690
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8605
9691
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8726,12 +9812,39 @@ var AUTHORING_RULES = [
|
|
|
8726
9812
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8727
9813
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8728
9814
|
},
|
|
8729
|
-
// ADR-0089 D3b —
|
|
8730
|
-
//
|
|
8731
|
-
//
|
|
9815
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
9816
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
9817
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
9818
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
9819
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
9820
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
9821
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
9822
|
+
//
|
|
9823
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
9824
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
9825
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
9826
|
+
// a change to this package's external input contract, and the maintainer's
|
|
9827
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
9828
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
9829
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
9830
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
9831
|
+
//
|
|
9832
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
9833
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
9834
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
9835
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
9836
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
9837
|
+
// "pre-parse evidence" one.
|
|
9838
|
+
//
|
|
9839
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
9840
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
9841
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
9842
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9843
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9844
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
8732
9845
|
{
|
|
8733
9846
|
name: "validateVisibilityPredicates",
|
|
8734
|
-
tier: "
|
|
9847
|
+
tier: "gating",
|
|
8735
9848
|
input: "normalized",
|
|
8736
9849
|
commands: ALL,
|
|
8737
9850
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
@@ -8739,6 +9852,30 @@ var AUTHORING_RULES = [
|
|
|
8739
9852
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8740
9853
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8741
9854
|
},
|
|
9855
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
9856
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
9857
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
9858
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
9859
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
9860
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
9861
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
9862
|
+
// #6254's 16 bare `type ==` predicates.
|
|
9863
|
+
//
|
|
9864
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
9865
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
9866
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9867
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9868
|
+
// false build errors. See the rule's module note.
|
|
9869
|
+
{
|
|
9870
|
+
name: "validatePredicatePathRefs",
|
|
9871
|
+
tier: "gating",
|
|
9872
|
+
input: "normalized",
|
|
9873
|
+
commands: ALL,
|
|
9874
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9875
|
+
surfaces: CLI_ONLY,
|
|
9876
|
+
surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
|
|
9877
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
9878
|
+
},
|
|
8742
9879
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8743
9880
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8744
9881
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
@@ -9134,10 +10271,13 @@ export {
|
|
|
9134
10271
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
9135
10272
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
9136
10273
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
10274
|
+
DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
9137
10275
|
EXPRESSION_INVALID,
|
|
9138
10276
|
FIELD_GROUP_EMPTY,
|
|
9139
10277
|
FIELD_GROUP_SHADOWED,
|
|
9140
10278
|
FIELD_GROUP_UNDECLARED,
|
|
10279
|
+
FILTER_EMPTY_COMBINATOR,
|
|
10280
|
+
FILTER_EMPTY_NODE,
|
|
9141
10281
|
FILTER_TOKEN_UNKNOWN,
|
|
9142
10282
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
9143
10283
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -9164,6 +10304,7 @@ export {
|
|
|
9164
10304
|
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
9165
10305
|
FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
9166
10306
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
10307
|
+
FLOW_TRIGGER_UNROUTABLE,
|
|
9167
10308
|
FLOW_UPDATE_READONLY_FIELD,
|
|
9168
10309
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
9169
10310
|
FLOW_WRITE_NODE_TYPES,
|
|
@@ -9184,19 +10325,25 @@ export {
|
|
|
9184
10325
|
NULL_GUARD_HINT,
|
|
9185
10326
|
OBJECT_REFERENCE_UNKNOWN,
|
|
9186
10327
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
10328
|
+
OPEN_VOCABULARY_PROBES,
|
|
9187
10329
|
ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
9188
10330
|
ORG_AXIS_PERMISSION_INHERITANCE,
|
|
9189
10331
|
PAGE_FIELD_UNKNOWN,
|
|
9190
10332
|
PAGE_SOURCE_CLASSNAME,
|
|
10333
|
+
PREDICATE_PATH_UNRESOLVED,
|
|
10334
|
+
PREDICATE_PATH_UNROOTED,
|
|
10335
|
+
PRE_SEAL_PHASES,
|
|
9191
10336
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
9192
10337
|
REACT_CHART_AGGREGATE_INVALID,
|
|
9193
10338
|
REACT_CHART_AXIS_UNKNOWN,
|
|
9194
10339
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
9195
10340
|
REACT_CHART_FIELD_UNKNOWN,
|
|
9196
10341
|
REFERENCE_INTEGRITY_RULES,
|
|
10342
|
+
RLS_PREDICATE_OVER_BUDGET,
|
|
9197
10343
|
RLS_PREDICATE_UNENFORCEABLE,
|
|
9198
10344
|
RLS_PREDICATE_UNPARSEABLE,
|
|
9199
10345
|
RUNTIME_AJV_OPTIONS,
|
|
10346
|
+
SEAL_MARKERS,
|
|
9200
10347
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
9201
10348
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
9202
10349
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
@@ -9216,6 +10363,9 @@ export {
|
|
|
9216
10363
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
9217
10364
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
9218
10365
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
10366
|
+
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
10367
|
+
STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
10368
|
+
STARTUP_VERDICT_HINT,
|
|
9219
10369
|
STYLE_CLASSNAME_TAILWIND,
|
|
9220
10370
|
STYLE_NODE_MISSING_ID,
|
|
9221
10371
|
STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -9237,7 +10387,9 @@ export {
|
|
|
9237
10387
|
VIEW_KEY_COLLISION,
|
|
9238
10388
|
VIEW_REF_FORM_TARGET_KIND,
|
|
9239
10389
|
VIEW_REF_FORM_TARGET_MISSING,
|
|
9240
|
-
|
|
10390
|
+
VISIBILITY_BARE_IDENTIFIER,
|
|
10391
|
+
VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
10392
|
+
VISIBILITY_PREDICATE_SYNTAX,
|
|
9241
10393
|
VISIBILITY_ROOT_MISLAYERED,
|
|
9242
10394
|
WIDGET_DATASET_UNKNOWN,
|
|
9243
10395
|
WIDGET_DIMENSION_UNKNOWN,
|
|
@@ -9249,6 +10401,7 @@ export {
|
|
|
9249
10401
|
diffAccessMatrix,
|
|
9250
10402
|
extractHookBodyWriteSet,
|
|
9251
10403
|
extractHookBodyWrites,
|
|
10404
|
+
findStartupRegistryVerdicts,
|
|
9252
10405
|
findUnguardedNullableOperands,
|
|
9253
10406
|
isSourceAuthoredPage,
|
|
9254
10407
|
lintAutonumberFormats,
|
|
@@ -9278,6 +10431,7 @@ export {
|
|
|
9278
10431
|
validateChartBindings,
|
|
9279
10432
|
validateComponentProps,
|
|
9280
10433
|
validateDashboardActionRefs,
|
|
10434
|
+
validateEmptyCombinators,
|
|
9281
10435
|
validateFilterTokens,
|
|
9282
10436
|
validateFlowNodeWrites,
|
|
9283
10437
|
validateFlowTemplatePaths,
|
|
@@ -9293,6 +10447,7 @@ export {
|
|
|
9293
10447
|
validateOrgAxisRedLines,
|
|
9294
10448
|
validatePageFieldBindings,
|
|
9295
10449
|
validatePageSourceStyling,
|
|
10450
|
+
validatePredicatePathRefs,
|
|
9296
10451
|
validateReactPageProps,
|
|
9297
10452
|
validateReactPages,
|
|
9298
10453
|
validateReadonlyFlowWrites,
|