@objectstack/lint 17.0.0-rc.3 → 17.0.0-rc.4
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 +1924 -4
- package/dist/index.cjs +1651 -459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +527 -155
- package/dist/index.d.ts +527 -155
- package/dist/index.js +1601 -440
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +1466 -350
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +1449 -328
- package/dist/runtime.js.map +1 -1
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -3,12 +3,15 @@ import { isIncoherentAggregate } from "@objectstack/spec/data";
|
|
|
3
3
|
import { ChartTypeSchema } from "@objectstack/spec/ui";
|
|
4
4
|
|
|
5
5
|
// src/system-fields.ts
|
|
6
|
-
import { FIELD_GROUP_SYSTEM_FIELDS } from "@objectstack/spec/data";
|
|
6
|
+
import { FIELD_GROUP_SYSTEM_FIELDS, resolveInjectedSystemColumns } from "@objectstack/spec/data";
|
|
7
7
|
import { SystemFieldName } from "@objectstack/spec/system";
|
|
8
8
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
9
9
|
...FIELD_GROUP_SYSTEM_FIELDS,
|
|
10
10
|
...Object.values(SystemFieldName)
|
|
11
11
|
]);
|
|
12
|
+
function injectedColumnsFor(objectDef) {
|
|
13
|
+
return resolveInjectedSystemColumns(objectDef).names;
|
|
14
|
+
}
|
|
12
15
|
|
|
13
16
|
// src/validate-widget-bindings.ts
|
|
14
17
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
@@ -330,21 +333,14 @@ function validateWidgetBindings(stack) {
|
|
|
330
333
|
}
|
|
331
334
|
|
|
332
335
|
// src/validate-expressions.ts
|
|
333
|
-
import { validateExpression } from "@objectstack/formula";
|
|
336
|
+
import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
|
|
334
337
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
335
338
|
|
|
336
339
|
// src/validate-null-guards.ts
|
|
337
|
-
import {
|
|
340
|
+
import { parseCelToAst } from "@objectstack/formula";
|
|
338
341
|
var NULL_GUARD_HINT = `Guard it with '!= null' \u2014 'has(x)' does NOT do that: a declared field holding null is still PRESENT, so has(x) is true.`;
|
|
339
342
|
var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
|
|
340
343
|
var DEFAULT_RECORD_ROOTS = ["record", "previous"];
|
|
341
|
-
var parseEnv;
|
|
342
|
-
function getParseEnv() {
|
|
343
|
-
if (!parseEnv) {
|
|
344
|
-
parseEnv = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
|
|
345
|
-
}
|
|
346
|
-
return parseEnv;
|
|
347
|
-
}
|
|
348
344
|
function isNode(v) {
|
|
349
345
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
350
346
|
}
|
|
@@ -477,12 +473,8 @@ function findUnguardedNullableOperands(source, opts) {
|
|
|
477
473
|
if (typeof source !== "string" || !source.trim()) return [];
|
|
478
474
|
if (opts.nullableFields.size === 0) return [];
|
|
479
475
|
const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
ast = getParseEnv().parse(source).ast;
|
|
483
|
-
} catch {
|
|
484
|
-
return [];
|
|
485
|
-
}
|
|
476
|
+
const ast = parseCelToAst(source);
|
|
477
|
+
if (!ast) return [];
|
|
486
478
|
const hasOperands = /* @__PURE__ */ new Set();
|
|
487
479
|
collectHasOperands(ast, roots, hasOperands);
|
|
488
480
|
const findings = [];
|
|
@@ -545,10 +537,14 @@ function findUnguardedNullableOperands(source, opts) {
|
|
|
545
537
|
visit(ast, /* @__PURE__ */ new Set());
|
|
546
538
|
return findings;
|
|
547
539
|
}
|
|
548
|
-
|
|
540
|
+
var OUTCOME_CLAUSE = {
|
|
541
|
+
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
542
|
+
"fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
|
|
543
|
+
};
|
|
544
|
+
function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
|
|
549
545
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
550
546
|
const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
|
|
551
|
-
return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014
|
|
547
|
+
return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
|
|
552
548
|
}
|
|
553
549
|
|
|
554
550
|
// src/validate-expressions.ts
|
|
@@ -568,7 +564,7 @@ function buildFieldIndex(objects) {
|
|
|
568
564
|
let names = [];
|
|
569
565
|
if (Array.isArray(fields)) names = fields.map((f) => f.name).filter((n) => typeof n === "string");
|
|
570
566
|
else if (fields && typeof fields === "object") names = Object.keys(fields);
|
|
571
|
-
idx.set(name, names);
|
|
567
|
+
idx.set(name, [.../* @__PURE__ */ new Set([...names, ...injectedColumnsFor(obj)])]);
|
|
572
568
|
}
|
|
573
569
|
return idx;
|
|
574
570
|
}
|
|
@@ -628,6 +624,19 @@ function buildNullableFieldIndex(objects) {
|
|
|
628
624
|
}
|
|
629
625
|
return idx;
|
|
630
626
|
}
|
|
627
|
+
function readsParentRoot(source) {
|
|
628
|
+
const roots = collectCelRootIdentifiers(source);
|
|
629
|
+
return roots.ok && roots.roots.includes("parent");
|
|
630
|
+
}
|
|
631
|
+
function masterDetailCount(obj) {
|
|
632
|
+
let n = 0;
|
|
633
|
+
for (const [, def] of fieldEntries(obj)) {
|
|
634
|
+
if (def.type !== "master_detail") continue;
|
|
635
|
+
const ref = def.reference;
|
|
636
|
+
if (typeof ref === "string" && ref.trim() !== "") n += 1;
|
|
637
|
+
}
|
|
638
|
+
return n;
|
|
639
|
+
}
|
|
631
640
|
function celSourceOf(raw) {
|
|
632
641
|
if (typeof raw === "string") return raw;
|
|
633
642
|
if (raw && typeof raw === "object") {
|
|
@@ -641,7 +650,7 @@ function rulePredicates(rule, path) {
|
|
|
641
650
|
const out = [];
|
|
642
651
|
const name = typeof rule.name === "string" ? rule.name : "?";
|
|
643
652
|
const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
|
|
644
|
-
const main = rule.
|
|
653
|
+
const main = rule.condition;
|
|
645
654
|
if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
|
|
646
655
|
if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
|
|
647
656
|
for (const branch of ["then", "otherwise"]) {
|
|
@@ -658,7 +667,7 @@ function validateStackExpressions(stack) {
|
|
|
658
667
|
const fieldIndex = buildFieldIndex(objects);
|
|
659
668
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
660
669
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
661
|
-
const checkNullGuards = (where, subject, raw, objectName) => {
|
|
670
|
+
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
662
671
|
if (!objectName) return;
|
|
663
672
|
const nullableFields = nullableIndex.get(objectName);
|
|
664
673
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
@@ -667,7 +676,7 @@ function validateStackExpressions(stack) {
|
|
|
667
676
|
for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
668
677
|
issues.push({
|
|
669
678
|
where,
|
|
670
|
-
message: nullGuardMessage(subject, objectName, finding),
|
|
679
|
+
message: nullGuardMessage(subject, objectName, finding, outcome),
|
|
671
680
|
source,
|
|
672
681
|
severity: "error"
|
|
673
682
|
});
|
|
@@ -736,31 +745,45 @@ function validateStackExpressions(stack) {
|
|
|
736
745
|
}
|
|
737
746
|
for (const obj of objects) {
|
|
738
747
|
const objectName = typeof obj.name === "string" ? obj.name : void 0;
|
|
739
|
-
const validations = obj.validations
|
|
748
|
+
const validations = obj.validations;
|
|
740
749
|
for (const rule of asArray2(validations)) {
|
|
741
750
|
const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
|
|
742
|
-
check(where, rule.
|
|
751
|
+
check(where, rule.condition, objectName, "record");
|
|
743
752
|
check(`${where} when`, rule.when, objectName, "record");
|
|
744
753
|
for (const p of rulePredicates(rule, "")) {
|
|
745
754
|
checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
|
|
746
755
|
}
|
|
747
756
|
}
|
|
748
757
|
const fields = obj.fields;
|
|
749
|
-
const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
758
|
+
const fieldList = Array.isArray(fields) ? fields.filter((f) => !!f && typeof f === "object").map((f) => [typeof f.name === "string" ? f.name : "?", f]) : fields && typeof fields === "object" ? Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]) : [];
|
|
759
|
+
const masters = masterDetailCount(obj);
|
|
760
|
+
for (const [fname, f] of fieldList) {
|
|
761
|
+
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
762
|
+
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
763
|
+
}
|
|
764
|
+
const roWhenSource = celSourceOf(f.readonlyWhen);
|
|
765
|
+
if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
|
|
766
|
+
issues.push({
|
|
767
|
+
where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
|
|
768
|
+
message: `\`readonlyWhen\` 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 the field would be locked on every write. ` + (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\`.`),
|
|
769
|
+
source: roWhenSource,
|
|
770
|
+
severity: "error"
|
|
771
|
+
});
|
|
756
772
|
}
|
|
757
|
-
|
|
773
|
+
checkNullGuards(
|
|
774
|
+
`object '${objectName}' \xB7 field '${fname}' requiredWhen`,
|
|
775
|
+
`field '${fname}' requiredWhen`,
|
|
776
|
+
f.requiredWhen,
|
|
777
|
+
objectName,
|
|
778
|
+
"fail-open"
|
|
779
|
+
);
|
|
780
|
+
if (f.expression) {
|
|
758
781
|
const res = validateExpression(
|
|
759
782
|
"value",
|
|
760
|
-
f.
|
|
783
|
+
f.expression,
|
|
761
784
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
762
785
|
);
|
|
763
|
-
const fieldWhere = `object '${objectName}' \xB7 field '${
|
|
786
|
+
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
764
787
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
765
788
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
766
789
|
}
|
|
@@ -768,7 +791,7 @@ function validateStackExpressions(stack) {
|
|
|
768
791
|
}
|
|
769
792
|
const seenActions = /* @__PURE__ */ new Set();
|
|
770
793
|
const checkAction = (where, action, objectName) => {
|
|
771
|
-
const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0)
|
|
794
|
+
const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0);
|
|
772
795
|
const name = typeof action.name === "string" ? action.name : "?";
|
|
773
796
|
const key = `${obj ?? ""}:${name}`;
|
|
774
797
|
if (seenActions.has(key)) return;
|
|
@@ -787,10 +810,10 @@ function validateStackExpressions(stack) {
|
|
|
787
810
|
checkAction(`object '${objectName}'`, action, objectName);
|
|
788
811
|
}
|
|
789
812
|
}
|
|
790
|
-
for (const
|
|
791
|
-
const ruleObj = typeof
|
|
792
|
-
const where = `sharingRule '${
|
|
793
|
-
check(where,
|
|
813
|
+
for (const sharingRule of asArray2(stack.sharingRules)) {
|
|
814
|
+
const ruleObj = typeof sharingRule.object === "string" ? sharingRule.object : void 0;
|
|
815
|
+
const where = `sharingRule '${sharingRule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
|
|
816
|
+
check(where, sharingRule.condition, ruleObj, "record");
|
|
794
817
|
}
|
|
795
818
|
for (const hook of asArray2(stack.hooks)) {
|
|
796
819
|
const hookName = hook.name ?? "?";
|
|
@@ -973,9 +996,12 @@ function validateFunctionalCompleteness(stack) {
|
|
|
973
996
|
}
|
|
974
997
|
|
|
975
998
|
// src/validate-flow-trigger-readiness.ts
|
|
999
|
+
import { TimeRelativeTriggerSchema } from "@objectstack/spec/automation";
|
|
976
1000
|
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
977
1001
|
var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
978
1002
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
1003
|
+
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
1004
|
+
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
979
1005
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
980
1006
|
function asArray4(v) {
|
|
981
1007
|
if (Array.isArray(v)) return v;
|
|
@@ -987,6 +1013,12 @@ function asArray4(v) {
|
|
|
987
1013
|
}
|
|
988
1014
|
return [];
|
|
989
1015
|
}
|
|
1016
|
+
function renderNonObject(v) {
|
|
1017
|
+
const t = typeof v;
|
|
1018
|
+
if (t === "string" || t === "number" || t === "boolean") return `${JSON.stringify(v)} (a ${t})`;
|
|
1019
|
+
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
1020
|
+
return `a ${t}`;
|
|
1021
|
+
}
|
|
990
1022
|
function startNodeOf(flow) {
|
|
991
1023
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
992
1024
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -1034,10 +1066,35 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1034
1066
|
hint: `Object names match exactly. Check config.timeRelative.object against the object's registered name. If the object comes from another installed package, this warning can be ignored.`
|
|
1035
1067
|
});
|
|
1036
1068
|
}
|
|
1069
|
+
const parsed = TimeRelativeTriggerSchema.safeParse(tr);
|
|
1070
|
+
if (!parsed.success) {
|
|
1071
|
+
const problems = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message.replace(/\s+/g, " ").trim()}`).join("; ");
|
|
1072
|
+
findings.push({
|
|
1073
|
+
// `error` (#5762): the verdict is `TimeRelativeTriggerSchema`'s, and it
|
|
1074
|
+
// is the same schema the trigger safeParses at bind time. A descriptor
|
|
1075
|
+
// it refuses is refused at bind too — the sweep is never installed, on
|
|
1076
|
+
// every deployment, with no installed package able to change the
|
|
1077
|
+
// answer. Nothing is left for the author to weigh.
|
|
1078
|
+
severity: "error",
|
|
1079
|
+
rule: FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
|
|
1080
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1081
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
|
|
1082
|
+
message: `has a config.timeRelative descriptor the time-relative trigger REFUSES at bind time, so the sweep is never installed \u2014 the flow declares a time-relative trigger and then never runs (the only trace is one warn in the server log). ${problems}`,
|
|
1083
|
+
hint: `Those messages are TimeRelativeTriggerSchema's own \u2014 the same schema the trigger safeParses at bind time, so a descriptor that satisfies them binds. An unrecognized key names the declared key it was probably meant to be; see content/docs/references/automation/time-relative-trigger.mdx.`
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1037
1086
|
}
|
|
1038
1087
|
if (start && isRecordTriggered2 && !VALID_RECORD_TRIGGER.test((triggerType ?? "").trim())) {
|
|
1039
1088
|
findings.push({
|
|
1040
|
-
|
|
1089
|
+
// `error` (#5762). The token grammar is CLOSED and local: the engine
|
|
1090
|
+
// routes any `record-`-prefixed string to the record-change trigger by a
|
|
1091
|
+
// hardcoded prefix test (no registry lookup, so installing a package
|
|
1092
|
+
// cannot claim a new `record-*` token), and that trigger maps the token
|
|
1093
|
+
// with `triggerTypeToHookEvents` — the same regex this file's
|
|
1094
|
+
// `VALID_RECORD_TRIGGER` mirrors. Off-grammar means zero hook events,
|
|
1095
|
+
// which means bound-to-nothing on every deployment. Unlike an object
|
|
1096
|
+
// name, there is no other-package reading that rescues it.
|
|
1097
|
+
severity: "error",
|
|
1041
1098
|
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
1042
1099
|
where: `flow "${flowName}" \u203A start node`,
|
|
1043
1100
|
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
@@ -1047,7 +1104,14 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1047
1104
|
}
|
|
1048
1105
|
if (start && isArrayRecordTriggered) {
|
|
1049
1106
|
findings.push({
|
|
1050
|
-
|
|
1107
|
+
// `error` (#5762), same id and same reason as 1c: an array maps to no
|
|
1108
|
+
// hook event either. The engine routes it to the record-change trigger
|
|
1109
|
+
// for the express purpose of making it loud, and its own comment names
|
|
1110
|
+
// THIS rule as the primary catch — a primary catch that only warns is
|
|
1111
|
+
// the "declared ≠ enforced" shape the registry's tier exists to close.
|
|
1112
|
+
// Multi-event arrays are deferred, not unsupported-by-accident (#3457),
|
|
1113
|
+
// so if they land the grammar widens here in the same commit.
|
|
1114
|
+
severity: "error",
|
|
1051
1115
|
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
1052
1116
|
where: `flow "${flowName}" \u203A start node`,
|
|
1053
1117
|
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
@@ -1055,6 +1119,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1055
1119
|
hint: `Use one triggerType string. For "created or updated" use record-after-write (one flow, both events, #3427). For any other combination, author one flow per event \u2014 multi-event arrays are deferred (#3457).`
|
|
1056
1120
|
});
|
|
1057
1121
|
}
|
|
1122
|
+
if (start && config.timeRelative != null && typeof config.timeRelative !== "object") {
|
|
1123
|
+
const fallback = isRecordTriggered2 || isArrayRecordTriggered ? "its record-change trigger" : config.schedule != null || flow.type === "schedule" ? "its plain `config.schedule` cadence" : triggerType === "api" || flow.type === "api" ? "its api trigger" : void 0;
|
|
1124
|
+
const consequence2 = fallback ? `The flow still binds through ${fallback}, so the descriptor is silently DROPPED \u2014 it fires on that trigger's terms (once per firing, with no record on the context) instead of once per matching record, and nothing anywhere reports the difference.` : `Nothing else on this start node declares a trigger either, so the flow binds to NOTHING and never fires \u2014 with zero diagnostics at any layer, not even the one bind-time warn a descriptor that IS an object gets when the trigger refuses it.`;
|
|
1125
|
+
findings.push({
|
|
1126
|
+
// `error` (#5762). The criterion IS the engine's routing predicate, so a
|
|
1127
|
+
// value that fails it is not routed to the time-relative trigger by any
|
|
1128
|
+
// deployment — the strongest verdict in this file, and the one case with
|
|
1129
|
+
// no runtime channel to fall back on (not even the bind-time warn 1b-ii
|
|
1130
|
+
// moves earlier). Note the two consequences below are both defects: one
|
|
1131
|
+
// never fires, the other silently drops the descriptor. Neither is a
|
|
1132
|
+
// shape the author can have meant, so both gate.
|
|
1133
|
+
severity: "error",
|
|
1134
|
+
rule: FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
1135
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1136
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
|
|
1137
|
+
message: `has config.timeRelative = ${renderNonObject(config.timeRelative)}, which is not the descriptor OBJECT this slot takes \u2014 the engine routes a flow to the time-relative sweep only when config.timeRelative is an object, so this one is never routed there and the sweep is never installed. ${consequence2}`,
|
|
1138
|
+
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
|
+
});
|
|
1140
|
+
}
|
|
1058
1141
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
1059
1142
|
findings.push({
|
|
1060
1143
|
severity: "warning",
|
|
@@ -1882,6 +1965,8 @@ import {
|
|
|
1882
1965
|
REACT_BLOCKS,
|
|
1883
1966
|
RECORD_CONTEXT_BLOCK_TAGS,
|
|
1884
1967
|
REACT_RECORD_BLOCK_ALTERNATIVES,
|
|
1968
|
+
ChartAggregateSchema,
|
|
1969
|
+
ChartDrillDownSchema,
|
|
1885
1970
|
chartAggregateResultKeys,
|
|
1886
1971
|
isRecordContextBlockType
|
|
1887
1972
|
} from "@objectstack/spec/ui";
|
|
@@ -2090,13 +2175,13 @@ function validateSearchableFields(stack) {
|
|
|
2090
2175
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2091
2176
|
const view = views[vi];
|
|
2092
2177
|
if (!isRec3(view)) continue;
|
|
2093
|
-
const
|
|
2178
|
+
const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
2094
2179
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
2095
2180
|
if (isRec3(view.list)) {
|
|
2096
2181
|
check(
|
|
2097
2182
|
view.list.searchableFields,
|
|
2098
2183
|
listViewObject(view.list) ?? viewObject,
|
|
2099
|
-
`view "${
|
|
2184
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2100
2185
|
`views[${vi}].list.searchableFields`,
|
|
2101
2186
|
"list-view searchableFields",
|
|
2102
2187
|
"narrowing"
|
|
@@ -2108,7 +2193,7 @@ function validateSearchableFields(stack) {
|
|
|
2108
2193
|
check(
|
|
2109
2194
|
lv.searchableFields,
|
|
2110
2195
|
listViewObject(lv) ?? viewObject,
|
|
2111
|
-
`view "${
|
|
2196
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2112
2197
|
`views[${vi}].listViews.${key}.searchableFields`,
|
|
2113
2198
|
"list-view searchableFields",
|
|
2114
2199
|
"narrowing"
|
|
@@ -2232,8 +2317,11 @@ function sortFieldRefs(value, basePath) {
|
|
|
2232
2317
|
}
|
|
2233
2318
|
var COMPONENT_FIELD_SPECS = {
|
|
2234
2319
|
"record:highlights": { props: ["fields"] },
|
|
2235
|
-
// `sections
|
|
2236
|
-
//
|
|
2320
|
+
// `sections` (object form) and `hideFields` are what every real page authors,
|
|
2321
|
+
// and since #5611 they are what `RecordDetailsProps` declares — this model and
|
|
2322
|
+
// the spec agree. (Before that, `sections` was declared as an ID `string[]`
|
|
2323
|
+
// and `hideFields` not at all; both survived only because `properties` is
|
|
2324
|
+
// unvalidated.)
|
|
2237
2325
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2238
2326
|
"record:path": { props: ["statusField"] },
|
|
2239
2327
|
"element:number": { props: ["field"] },
|
|
@@ -2296,7 +2384,7 @@ function indexObjectFields(stack) {
|
|
|
2296
2384
|
}
|
|
2297
2385
|
return objectFields;
|
|
2298
2386
|
}
|
|
2299
|
-
function checkFieldRefs(refs, objectName, objectFields, where,
|
|
2387
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2300
2388
|
const findings = [];
|
|
2301
2389
|
if (!objectName) return findings;
|
|
2302
2390
|
const known = objectFields.get(objectName);
|
|
@@ -2305,11 +2393,11 @@ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "sk
|
|
|
2305
2393
|
if (ref.name.includes(".")) continue;
|
|
2306
2394
|
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
|
|
2307
2395
|
findings.push({
|
|
2308
|
-
severity:
|
|
2396
|
+
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2309
2397
|
rule: PAGE_FIELD_UNKNOWN,
|
|
2310
2398
|
where,
|
|
2311
2399
|
path: ref.path,
|
|
2312
|
-
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (
|
|
2400
|
+
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence2 === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
|
|
2313
2401
|
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2314
2402
|
});
|
|
2315
2403
|
}
|
|
@@ -2363,6 +2451,42 @@ function validatePageFieldBindings(stack) {
|
|
|
2363
2451
|
return findings;
|
|
2364
2452
|
}
|
|
2365
2453
|
|
|
2454
|
+
// src/zod-issue-format.ts
|
|
2455
|
+
var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2456
|
+
var valueAtPath = (root, path) => {
|
|
2457
|
+
let cur = root;
|
|
2458
|
+
for (const key of path) {
|
|
2459
|
+
if (!isRec6(cur) && !Array.isArray(cur)) return void 0;
|
|
2460
|
+
cur = cur[key];
|
|
2461
|
+
}
|
|
2462
|
+
return cur;
|
|
2463
|
+
};
|
|
2464
|
+
var preview = (value) => {
|
|
2465
|
+
let text;
|
|
2466
|
+
try {
|
|
2467
|
+
text = JSON.stringify(value) ?? String(value);
|
|
2468
|
+
} catch {
|
|
2469
|
+
text = String(value);
|
|
2470
|
+
}
|
|
2471
|
+
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
2472
|
+
};
|
|
2473
|
+
function describeIssue(issue, root, depth = 0) {
|
|
2474
|
+
const value = depth === 0 ? valueAtPath(root, issue.path) : void 0;
|
|
2475
|
+
const seen = depth > 0 || issue.code === "custom" || issue.message.includes("received ") ? "" : value === void 0 ? " (nothing is set there)" : ` (received ${preview(value)})`;
|
|
2476
|
+
const armIssues = issue.code === "invalid_union" ? issue.errors : void 0;
|
|
2477
|
+
if (!armIssues || armIssues.length === 0) {
|
|
2478
|
+
return `${issue.message}${seen}`;
|
|
2479
|
+
}
|
|
2480
|
+
const arms = armIssues.map(
|
|
2481
|
+
(arm) => arm.map((inner) => {
|
|
2482
|
+
const where = inner.path.length ? `${inner.path.join(".")} \u2014 ` : "";
|
|
2483
|
+
return `${where}${describeIssue(inner, root, depth + 1)}`;
|
|
2484
|
+
}).join("; ")
|
|
2485
|
+
).filter((text) => text.length > 0);
|
|
2486
|
+
if (arms.length === 0) return `${issue.message}${seen}`;
|
|
2487
|
+
return `${issue.message}${seen} \u2014 no accepted form matched: ` + arms.map((text, i) => `(${i + 1}) ${text}`).join(" ");
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2366
2490
|
// src/validate-react-page-props.ts
|
|
2367
2491
|
var cachedTs = null;
|
|
2368
2492
|
function loadTypeScript() {
|
|
@@ -2467,35 +2591,78 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
2467
2591
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
2468
2592
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2469
2593
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2470
|
-
var
|
|
2471
|
-
|
|
2594
|
+
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
2595
|
+
function checkChartDrillDown(raw, push2) {
|
|
2596
|
+
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
2597
|
+
if (!isRec7(raw)) {
|
|
2598
|
+
push2(
|
|
2599
|
+
"error",
|
|
2600
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
2601
|
+
`drillDown must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
|
|
2602
|
+
"Write drillDown={{ \u2026 }} \u2014 or, to turn the drill on with all defaults, drillDown={{}}. Omit the prop entirely to leave drill off."
|
|
2603
|
+
);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
const parsed = ChartDrillDownSchema.safeParse(raw);
|
|
2607
|
+
if (parsed.success) return;
|
|
2608
|
+
for (const issue of parsed.error.issues) {
|
|
2609
|
+
const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
|
|
2610
|
+
push2(
|
|
2611
|
+
"error",
|
|
2612
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
2613
|
+
`${at}: ${issue.message}`,
|
|
2614
|
+
"The drill config is declared by ChartDrillDownSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
function checkChartAggregate(raw, push2) {
|
|
2619
|
+
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
2620
|
+
if (!isRec7(raw)) {
|
|
2621
|
+
push2(
|
|
2622
|
+
"error",
|
|
2623
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2624
|
+
`aggregate must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
|
|
2625
|
+
'Write aggregate={{ function: "count", groupBy: "<field>" }} \u2014 or bind data={\u2026} instead to chart precomputed rows.'
|
|
2626
|
+
);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
const groupByAbsent = raw.groupBy === void 0;
|
|
2630
|
+
if (groupByAbsent) {
|
|
2631
|
+
push2(
|
|
2632
|
+
"warning",
|
|
2633
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2634
|
+
"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. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const parsed = ChartAggregateSchema.safeParse(raw);
|
|
2639
|
+
if (parsed.success) return;
|
|
2640
|
+
for (const issue of parsed.error.issues) {
|
|
2641
|
+
if (groupByAbsent && issue.path[0] === "groupBy") continue;
|
|
2642
|
+
const at = issue.path.length ? `aggregate.${issue.path.join(".")}` : "aggregate";
|
|
2643
|
+
push2(
|
|
2644
|
+
"error",
|
|
2645
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2646
|
+
`${at}: ${describeIssue(issue, raw)}`,
|
|
2647
|
+
"The aggregate is declared by ChartAggregateSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2472
2652
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2473
2653
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
2474
2654
|
const { values, where, path } = attrs;
|
|
2475
2655
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2656
|
+
checkChartDrillDown(values.get("drillDown"), push2);
|
|
2476
2657
|
if (values.has("data")) return;
|
|
2477
2658
|
const aggregate = values.get("aggregate");
|
|
2659
|
+
checkChartAggregate(aggregate, push2);
|
|
2478
2660
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2479
|
-
if (!
|
|
2661
|
+
if (!isRec7(aggregate)) return;
|
|
2480
2662
|
const fn = strOf(aggregate.function);
|
|
2481
2663
|
const field = strOf(aggregate.field);
|
|
2482
2664
|
const groupBy = aggregate.groupBy;
|
|
2483
|
-
const groupByField = strOf(groupBy) ?? (
|
|
2484
|
-
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2485
|
-
push2(
|
|
2486
|
-
"error",
|
|
2487
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
2488
|
-
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2489
|
-
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2490
|
-
);
|
|
2491
|
-
} else if (fn && fn !== "count" && !field) {
|
|
2492
|
-
push2(
|
|
2493
|
-
"error",
|
|
2494
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
2495
|
-
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
2496
|
-
'Add aggregate.field, or use function "count" (the only one that may omit it).'
|
|
2497
|
-
);
|
|
2498
|
-
}
|
|
2665
|
+
const groupByField = strOf(groupBy) ?? (isRec7(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2499
2666
|
const objectName = strOf(values.get("objectName"));
|
|
2500
2667
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
2501
2668
|
if (objectName && known) {
|
|
@@ -2528,18 +2695,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2528
2695
|
);
|
|
2529
2696
|
};
|
|
2530
2697
|
const xAxisRaw = values.get("xAxis");
|
|
2531
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
2698
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec7(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
2532
2699
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
2533
2700
|
axisRef(categoryAxis, categoryProp);
|
|
2534
2701
|
const yAxisRaw = values.get("yAxis");
|
|
2535
2702
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
2536
2703
|
for (const a of yAxisList) {
|
|
2537
|
-
axisRef(strOf(a) ?? (
|
|
2704
|
+
axisRef(strOf(a) ?? (isRec7(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
2538
2705
|
}
|
|
2539
2706
|
const series = values.get("series");
|
|
2540
2707
|
if (Array.isArray(series)) {
|
|
2541
2708
|
for (const s of series) {
|
|
2542
|
-
if (!
|
|
2709
|
+
if (!isRec7(s)) continue;
|
|
2543
2710
|
const dataKey = strOf(s.dataKey);
|
|
2544
2711
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
2545
2712
|
}
|
|
@@ -2595,7 +2762,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
2595
2762
|
if (!Array.isArray(value)) return { child, parent };
|
|
2596
2763
|
for (let i = 0; i < value.length; i++) {
|
|
2597
2764
|
const sub = value[i];
|
|
2598
|
-
if (!
|
|
2765
|
+
if (!isRec7(sub)) continue;
|
|
2599
2766
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2600
2767
|
child.push({
|
|
2601
2768
|
objectName: strOf(sub.childObject),
|
|
@@ -2640,20 +2807,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
2640
2807
|
}
|
|
2641
2808
|
for (const key of spec.nestedFields ?? []) {
|
|
2642
2809
|
const v = readable(key);
|
|
2643
|
-
if (
|
|
2810
|
+
if (isRec7(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2644
2811
|
}
|
|
2645
2812
|
for (const key of spec.sections ?? []) {
|
|
2646
2813
|
const v = readable(key);
|
|
2647
2814
|
if (!Array.isArray(v)) continue;
|
|
2648
2815
|
for (let i = 0; i < v.length; i++) {
|
|
2649
2816
|
const section = v[i];
|
|
2650
|
-
if (!
|
|
2817
|
+
if (!isRec7(section)) continue;
|
|
2651
2818
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2652
2819
|
}
|
|
2653
2820
|
}
|
|
2654
2821
|
for (const key of spec.keyedByField ?? []) {
|
|
2655
2822
|
const v = readable(key);
|
|
2656
|
-
if (!
|
|
2823
|
+
if (!isRec7(v)) continue;
|
|
2657
2824
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2658
2825
|
}
|
|
2659
2826
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -2924,7 +3091,7 @@ function validateSemanticRoles(stack) {
|
|
|
2924
3091
|
const where = `object "${objName}"`;
|
|
2925
3092
|
const path = `objects[${i}]`;
|
|
2926
3093
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
2927
|
-
const fieldNames = new Set(Object.keys(fields));
|
|
3094
|
+
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
2928
3095
|
const declaredGroups = new Set(
|
|
2929
3096
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
2930
3097
|
);
|
|
@@ -3317,7 +3484,7 @@ import {
|
|
|
3317
3484
|
normalizeDecisionOutputs
|
|
3318
3485
|
} from "@objectstack/spec/automation";
|
|
3319
3486
|
import { BUILTIN_MEMBERSHIP_ROLES } from "@objectstack/spec";
|
|
3320
|
-
import { collectCelRootIdentifiers } from "@objectstack/formula";
|
|
3487
|
+
import { collectCelRootIdentifiers as collectCelRootIdentifiers2 } from "@objectstack/formula";
|
|
3321
3488
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
3322
3489
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
3323
3490
|
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
@@ -3392,7 +3559,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3392
3559
|
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.`
|
|
3393
3560
|
});
|
|
3394
3561
|
} else {
|
|
3395
|
-
const parsed =
|
|
3562
|
+
const parsed = collectCelRootIdentifiers2(source);
|
|
3396
3563
|
if (!parsed.ok) {
|
|
3397
3564
|
findings.push({
|
|
3398
3565
|
severity: "error",
|
|
@@ -3650,7 +3817,7 @@ function asArray20(v) {
|
|
|
3650
3817
|
return [];
|
|
3651
3818
|
}
|
|
3652
3819
|
function owdOf(obj) {
|
|
3653
|
-
return obj.sharingModel
|
|
3820
|
+
return obj.sharingModel;
|
|
3654
3821
|
}
|
|
3655
3822
|
function isSystemObject(obj) {
|
|
3656
3823
|
return obj.isSystem === true || String(obj.name ?? "").startsWith("sys_");
|
|
@@ -3664,7 +3831,7 @@ function labelHasRoleWord(label2) {
|
|
|
3664
3831
|
return /\brole(s)?\b/i.test(label2);
|
|
3665
3832
|
}
|
|
3666
3833
|
function refOf(def) {
|
|
3667
|
-
const r = def.reference
|
|
3834
|
+
const r = def.reference;
|
|
3668
3835
|
return typeof r === "string" && r ? r : void 0;
|
|
3669
3836
|
}
|
|
3670
3837
|
function firstMasterDetailField(obj) {
|
|
@@ -3957,6 +4124,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
3957
4124
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
3958
4125
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
3959
4126
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
4127
|
+
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
3960
4128
|
function asArray21(v) {
|
|
3961
4129
|
if (Array.isArray(v)) return v;
|
|
3962
4130
|
if (v && typeof v === "object") {
|
|
@@ -3967,6 +4135,15 @@ function asArray21(v) {
|
|
|
3967
4135
|
function str(v) {
|
|
3968
4136
|
return typeof v === "string" ? v : "";
|
|
3969
4137
|
}
|
|
4138
|
+
function expressionText(v) {
|
|
4139
|
+
if (typeof v === "string") return v;
|
|
4140
|
+
if (v && typeof v === "object") {
|
|
4141
|
+
const rec = v;
|
|
4142
|
+
if (typeof rec.source === "string") return rec.source;
|
|
4143
|
+
if (rec.ast !== void 0) return JSON.stringify(rec.ast) ?? "";
|
|
4144
|
+
}
|
|
4145
|
+
return "";
|
|
4146
|
+
}
|
|
3970
4147
|
function isTenancyDisabled(object) {
|
|
3971
4148
|
const tenancy = object.tenancy;
|
|
3972
4149
|
if (tenancy && typeof tenancy === "object" && tenancy.enabled === false) return true;
|
|
@@ -3978,7 +4155,7 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
3978
4155
|
function validateOrgAxisRedLines(stack) {
|
|
3979
4156
|
const findings = [];
|
|
3980
4157
|
const cfg = stack ?? {};
|
|
3981
|
-
const permissionSets = asArray21(cfg.permissions
|
|
4158
|
+
const permissionSets = asArray21(cfg.permissions);
|
|
3982
4159
|
permissionSets.forEach((ps, psIndex) => {
|
|
3983
4160
|
asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
3984
4161
|
for (const clause of ["using", "check"]) {
|
|
@@ -3994,53 +4171,167 @@ function validateOrgAxisRedLines(stack) {
|
|
|
3994
4171
|
}
|
|
3995
4172
|
});
|
|
3996
4173
|
});
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
severity: "error",
|
|
4005
|
-
rule: ORG_AXIS_PERMISSION_INHERITANCE,
|
|
4006
|
-
where: `object "${objectName}" policy "${str(policy.name) || pIndex}"`,
|
|
4007
|
-
path: `objects[${oIndex}].rowLevelSecurity[${pIndex}].${clause}`,
|
|
4008
|
-
message: `RLS ${clause} reads \`${ORG_PARENT_FIELD}\`, which builds a permission hierarchy along the organization axis. ADR-0105 D6 forbids it: the org tree is a REPORTING dimension only.`,
|
|
4009
|
-
hint: INHERITANCE_HINT
|
|
4010
|
-
});
|
|
4011
|
-
}
|
|
4012
|
-
});
|
|
4013
|
-
});
|
|
4014
|
-
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
4015
|
-
const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
|
|
4016
|
-
const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
|
|
4017
|
-
if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
|
|
4174
|
+
asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4175
|
+
const slots = [
|
|
4176
|
+
{ key: "condition", text: expressionText(rule.condition) },
|
|
4177
|
+
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
4178
|
+
];
|
|
4179
|
+
for (const slot of slots) {
|
|
4180
|
+
if (!slot.text.includes(ORG_PARENT_FIELD)) continue;
|
|
4018
4181
|
findings.push({
|
|
4019
4182
|
severity: "error",
|
|
4020
4183
|
rule: ORG_AXIS_PERMISSION_INHERITANCE,
|
|
4021
4184
|
where: `sharing rule "${str(rule.name) || rIndex}"`,
|
|
4022
|
-
path: `sharingRules[${rIndex}]`,
|
|
4023
|
-
message: `Sharing rule reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
|
|
4185
|
+
path: `sharingRules[${rIndex}].${slot.key}`,
|
|
4186
|
+
message: `Sharing rule ${slot.key} reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
|
|
4024
4187
|
hint: INHERITANCE_HINT
|
|
4025
4188
|
});
|
|
4026
4189
|
}
|
|
4027
4190
|
});
|
|
4028
4191
|
const tenancyDisabledObjects = new Set(
|
|
4029
|
-
objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4192
|
+
asArray21(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4030
4193
|
);
|
|
4031
|
-
asArray21(cfg.sharingRules
|
|
4032
|
-
const target = str(rule.object
|
|
4194
|
+
asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4195
|
+
const target = str(rule.object);
|
|
4033
4196
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
4034
|
-
const
|
|
4035
|
-
const recipientType = str(
|
|
4036
|
-
if (recipientType
|
|
4197
|
+
const sharedWith = rule.sharedWith;
|
|
4198
|
+
const recipientType = str(sharedWith?.type);
|
|
4199
|
+
if (!BU_TREE_RECIPIENT_TYPES.has(recipientType)) return;
|
|
4200
|
+
const reach = recipientType === "unit_and_subordinates" ? "a business unit AND every descendant unit" : "a business unit";
|
|
4037
4201
|
findings.push({
|
|
4038
4202
|
severity: "error",
|
|
4039
4203
|
rule: ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
4040
4204
|
where: `sharing rule "${str(rule.name) || rIndex}" on object "${target}"`,
|
|
4041
|
-
path: `sharingRules[${rIndex}].
|
|
4042
|
-
message: `
|
|
4043
|
-
hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a
|
|
4205
|
+
path: `sharingRules[${rIndex}].sharedWith`,
|
|
4206
|
+
message: `Sharing rule recipient \`${recipientType}\` (${reach}) targets "${target}", which opted out of tenancy (\`tenancy.enabled: false\`). Platform-global objects carry no organization column, so this grant spans EVERY organization \u2014 a cross-organization business-unit grant, which ADR-0105 D6 forbids (BU trees are org-internal).`,
|
|
4207
|
+
hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a \`user\` / \`team\` / \`position\` audience instead \u2014 those expand flat, with no business-unit tree to resolve. A platform-global catalog that everyone should read wants an OWD of \`public_read\`, not a BU grant.`
|
|
4208
|
+
});
|
|
4209
|
+
});
|
|
4210
|
+
return findings;
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
// src/validate-sharing-rule-enforceability.ts
|
|
4214
|
+
import { compileCelToFilter } from "@objectstack/formula";
|
|
4215
|
+
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
4216
|
+
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
4217
|
+
function asArray22(v) {
|
|
4218
|
+
if (Array.isArray(v)) return v;
|
|
4219
|
+
if (v && typeof v === "object") {
|
|
4220
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4221
|
+
}
|
|
4222
|
+
return [];
|
|
4223
|
+
}
|
|
4224
|
+
function str2(v) {
|
|
4225
|
+
return typeof v === "string" ? v : "";
|
|
4226
|
+
}
|
|
4227
|
+
function toCompilerInput(condition) {
|
|
4228
|
+
if (typeof condition === "string") return condition.trim() ? condition : null;
|
|
4229
|
+
if (condition && typeof condition === "object") {
|
|
4230
|
+
const source = condition.source;
|
|
4231
|
+
if (typeof source === "string" && source.trim()) return { source };
|
|
4232
|
+
}
|
|
4233
|
+
return null;
|
|
4234
|
+
}
|
|
4235
|
+
function sourceOf(condition) {
|
|
4236
|
+
const input = toCompilerInput(condition);
|
|
4237
|
+
if (typeof input === "string") return input;
|
|
4238
|
+
return str2(input?.source);
|
|
4239
|
+
}
|
|
4240
|
+
var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
|
|
4241
|
+
function validateSharingRuleEnforceability(stack) {
|
|
4242
|
+
const findings = [];
|
|
4243
|
+
const cfg = stack ?? {};
|
|
4244
|
+
asArray22(cfg.sharingRules).forEach((rule, index) => {
|
|
4245
|
+
const input = toCompilerInput(rule.condition);
|
|
4246
|
+
if (input === null) return;
|
|
4247
|
+
const result = compileCelToFilter(input, { variables: {} });
|
|
4248
|
+
if (result.ok) return;
|
|
4249
|
+
if (result.reason === "parse-error") return;
|
|
4250
|
+
const name = str2(rule.name) || String(index);
|
|
4251
|
+
const object = str2(rule.object);
|
|
4252
|
+
const where = `sharing rule "${name}"${object ? ` on object "${object}"` : ""}`;
|
|
4253
|
+
const path = `sharingRules[${index}].condition`;
|
|
4254
|
+
const source = sourceOf(rule.condition);
|
|
4255
|
+
const skipped = "so `bootstrapDeclaredSharingRules` SKIPS the rule at boot: it is never written to `sys_sharing_rule`, no `sys_record_share` grant is ever materialised, and the only signal is one WARN line in the boot log. The rule is declared and grants nothing (ADR-0049: an unlowerable condition is never seeded as a permissive match-all).";
|
|
4256
|
+
if (result.reason === "unresolved-variable") {
|
|
4257
|
+
findings.push({
|
|
4258
|
+
severity: "error",
|
|
4259
|
+
rule: SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
4260
|
+
where,
|
|
4261
|
+
path,
|
|
4262
|
+
message: `Sharing-rule condition \`${source}\` reads a runtime variable (${result.detail}), ` + skipped,
|
|
4263
|
+
hint: "A criteria sharing rule is MATERIALISED: the seeder compiles ONE static `criteria_json` per rule and the evaluator writes `sys_record_share` rows from it, so there is no \"current user\" for the condition to read. Express per-user access with the mechanism that runs per request instead \u2014 an RLS policy on a permission set (`rowLevelSecurity[].using`, where `current_user.*` IS resolved), or the record-ownership path. Keep this rule for the part of the predicate that is a property of the RECORD (e.g. `record.stage == 'closed_won'`) and name the audience through `sharedWith`."
|
|
4264
|
+
});
|
|
4265
|
+
return;
|
|
4266
|
+
}
|
|
4267
|
+
findings.push({
|
|
4268
|
+
severity: "error",
|
|
4269
|
+
rule: SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
4270
|
+
where,
|
|
4271
|
+
path,
|
|
4272
|
+
message: `Sharing-rule condition \`${source}\` is outside the pushdown subset the runtime can compile (${result.detail}), ` + skipped,
|
|
4273
|
+
hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET + " Two traps in particular: (1) `has(record.x)` is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the condition is COMPILED \u2014 write the null test as `record.x != null`; (2) a related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column, or share the related object instead."
|
|
4274
|
+
});
|
|
4275
|
+
});
|
|
4276
|
+
return findings;
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
// src/validate-rls-predicate-enforceability.ts
|
|
4280
|
+
import { isPushdownableCel, isSupportedRlsExpression, sqlPredicateToCel } from "@objectstack/formula";
|
|
4281
|
+
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
4282
|
+
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
4283
|
+
function asArray23(v) {
|
|
4284
|
+
if (Array.isArray(v)) return v;
|
|
4285
|
+
if (v && typeof v === "object") {
|
|
4286
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4287
|
+
}
|
|
4288
|
+
return [];
|
|
4289
|
+
}
|
|
4290
|
+
function str3(v) {
|
|
4291
|
+
return typeof v === "string" ? v : "";
|
|
4292
|
+
}
|
|
4293
|
+
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.";
|
|
4294
|
+
function consequence(clause) {
|
|
4295
|
+
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
|
+
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.";
|
|
4297
|
+
}
|
|
4298
|
+
function validateRlsPredicateEnforceability(stack) {
|
|
4299
|
+
const findings = [];
|
|
4300
|
+
const cfg = stack ?? {};
|
|
4301
|
+
asArray23(cfg.permissions).forEach((ps, psIndex) => {
|
|
4302
|
+
asArray23(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4303
|
+
for (const clause of ["using", "check"]) {
|
|
4304
|
+
const source = str3(policy[clause]);
|
|
4305
|
+
if (!source.trim()) continue;
|
|
4306
|
+
if (isSupportedRlsExpression(source)) continue;
|
|
4307
|
+
const why = isPushdownableCel(sqlPredicateToCel(source));
|
|
4308
|
+
const detail = why.ok ? "" : why.detail;
|
|
4309
|
+
const parseError = !why.ok && why.reason === "parse-error";
|
|
4310
|
+
const psName = str3(ps.name) || String(psIndex);
|
|
4311
|
+
const policyName = str3(policy.name) || String(pIndex);
|
|
4312
|
+
const object = str3(policy.object);
|
|
4313
|
+
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
4314
|
+
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
4315
|
+
if (parseError) {
|
|
4316
|
+
findings.push({
|
|
4317
|
+
severity: "error",
|
|
4318
|
+
rule: RLS_PREDICATE_UNPARSEABLE,
|
|
4319
|
+
where,
|
|
4320
|
+
path,
|
|
4321
|
+
message: `RLS ${clause} \`${source}\` does not parse as CEL even after the legacy SQL bridge (\`=\` \u2192 \`==\`, \`IN\` \u2192 \`in\`) has been applied (${detail}), ` + consequence(clause),
|
|
4322
|
+
hint: "Author the predicate in canonical CEL (ADR-0058 D1). The bridge covers only the historic SQL subset \u2014 a bare `=` and `IN` \u2014 so everything else must already be CEL: combine with `&&` / `||` rather than SQL `AND` / `OR`, negate with `!`, and use `startsWith` / `endsWith` / `contains` rather than `LIKE`. A subquery has no CEL spelling at all: RLS cannot join (ADR-0055), so pre-resolve the set into a membership key the runtime exposes (`field in current_user.<key>`, ADR-0105 D11) or denormalise the value onto this object."
|
|
4323
|
+
});
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
findings.push({
|
|
4327
|
+
severity: "error",
|
|
4328
|
+
rule: RLS_PREDICATE_UNENFORCEABLE,
|
|
4329
|
+
where,
|
|
4330
|
+
path,
|
|
4331
|
+
message: `RLS ${clause} \`${source}\` is outside the pushdown subset the runtime can compile (${detail}), ` + consequence(clause),
|
|
4332
|
+
hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET2 + " Three traps in particular: (1) a function call \u2014 `size(record.tags) > 0`, `has(record.x)` \u2014 is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the predicate is COMPILED to a filter; write the null test as `field != null`. (2) A related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column. (3) Arithmetic on a column (`amount * 2 > 100`) never lowers \u2014 precompute it into a field, or compare the column against the literal directly."
|
|
4333
|
+
});
|
|
4334
|
+
}
|
|
4044
4335
|
});
|
|
4045
4336
|
});
|
|
4046
4337
|
return findings;
|
|
@@ -4049,7 +4340,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4049
4340
|
// src/validate-dashboard-action-refs.ts
|
|
4050
4341
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
4051
4342
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
4052
|
-
function
|
|
4343
|
+
function asArray24(v) {
|
|
4053
4344
|
if (Array.isArray(v)) return v;
|
|
4054
4345
|
if (v && typeof v === "object") {
|
|
4055
4346
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4083,14 +4374,14 @@ function collectKnownTargets(stack) {
|
|
|
4083
4374
|
const pages = /* @__PURE__ */ new Set();
|
|
4084
4375
|
const views = /* @__PURE__ */ new Set();
|
|
4085
4376
|
const collectNames = (v, into, name) => {
|
|
4086
|
-
for (const item of
|
|
4377
|
+
for (const item of asArray24(v)) {
|
|
4087
4378
|
if (!item || typeof item !== "object") continue;
|
|
4088
4379
|
const n = name(item);
|
|
4089
4380
|
if (n) into.add(n);
|
|
4090
4381
|
}
|
|
4091
4382
|
};
|
|
4092
4383
|
collectNames(stack.actions, actions, (a) => strName5(a.name));
|
|
4093
|
-
for (const obj of
|
|
4384
|
+
for (const obj of asArray24(stack.objects)) {
|
|
4094
4385
|
if (!obj || typeof obj !== "object") continue;
|
|
4095
4386
|
const n = strName5(obj.name);
|
|
4096
4387
|
if (n) objects.add(n);
|
|
@@ -4130,7 +4421,7 @@ function resolveUrlRoute(target, known) {
|
|
|
4130
4421
|
function validateDashboardActionRefs(stack) {
|
|
4131
4422
|
const findings = [];
|
|
4132
4423
|
if (!stack || typeof stack !== "object") return findings;
|
|
4133
|
-
const dashboards =
|
|
4424
|
+
const dashboards = asArray24(stack.dashboards);
|
|
4134
4425
|
if (dashboards.length === 0) return findings;
|
|
4135
4426
|
const known = collectKnownTargets(stack);
|
|
4136
4427
|
const checkOne = (action, where, path) => {
|
|
@@ -4170,7 +4461,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
4170
4461
|
if (!dash || typeof dash !== "object") continue;
|
|
4171
4462
|
const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
|
|
4172
4463
|
const dashPath = `dashboards[${di}]`;
|
|
4173
|
-
const headerActions =
|
|
4464
|
+
const headerActions = asArray24(dash.header?.actions);
|
|
4174
4465
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
4175
4466
|
const action = headerActions[ai];
|
|
4176
4467
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4181,18 +4472,6 @@ function validateDashboardActionRefs(stack) {
|
|
|
4181
4472
|
`${dashPath}.header.actions[${ai}].actionUrl`
|
|
4182
4473
|
);
|
|
4183
4474
|
}
|
|
4184
|
-
const widgets = asArray22(dash.widgets);
|
|
4185
|
-
for (let wi = 0; wi < widgets.length; wi++) {
|
|
4186
|
-
const widget = widgets[wi];
|
|
4187
|
-
if (!widget || typeof widget !== "object") continue;
|
|
4188
|
-
if (!strName5(widget.actionUrl)) continue;
|
|
4189
|
-
const widgetId = strName5(widget.id) ?? `#${wi}`;
|
|
4190
|
-
checkOne(
|
|
4191
|
-
{ actionType: widget.actionType, actionUrl: widget.actionUrl },
|
|
4192
|
-
`dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
|
|
4193
|
-
`${dashPath}.widgets[${wi}].actionUrl`
|
|
4194
|
-
);
|
|
4195
|
-
}
|
|
4196
4475
|
}
|
|
4197
4476
|
return findings;
|
|
4198
4477
|
}
|
|
@@ -4201,7 +4480,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
4201
4480
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
4202
4481
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
4203
4482
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
4204
|
-
function
|
|
4483
|
+
function asArray25(v) {
|
|
4205
4484
|
if (Array.isArray(v)) return v;
|
|
4206
4485
|
if (v && typeof v === "object") {
|
|
4207
4486
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4270,7 +4549,7 @@ function validateFilterTokens(stack) {
|
|
|
4270
4549
|
["apps", "app"]
|
|
4271
4550
|
];
|
|
4272
4551
|
for (const [key, kind] of surfaces) {
|
|
4273
|
-
const items =
|
|
4552
|
+
const items = asArray25(stack[key]);
|
|
4274
4553
|
items.forEach((item, i) => {
|
|
4275
4554
|
const name = label(item.name ?? item.id, `#${i}`);
|
|
4276
4555
|
if (kind === "dashboard") {
|
|
@@ -4304,7 +4583,7 @@ import {
|
|
|
4304
4583
|
var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
4305
4584
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
4306
4585
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
4307
|
-
function
|
|
4586
|
+
function asArray26(v) {
|
|
4308
4587
|
if (Array.isArray(v)) return v;
|
|
4309
4588
|
if (v && typeof v === "object") {
|
|
4310
4589
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4351,7 +4630,7 @@ function distance2(a, b) {
|
|
|
4351
4630
|
function validateObjectReferences(stack) {
|
|
4352
4631
|
const findings = [];
|
|
4353
4632
|
if (!stack || typeof stack !== "object") return findings;
|
|
4354
|
-
const objects =
|
|
4633
|
+
const objects = asArray26(stack.objects);
|
|
4355
4634
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4356
4635
|
for (const obj of objects) {
|
|
4357
4636
|
const n = strName6(obj.name);
|
|
@@ -4384,7 +4663,7 @@ function validateObjectReferences(stack) {
|
|
|
4384
4663
|
});
|
|
4385
4664
|
};
|
|
4386
4665
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
4387
|
-
const params =
|
|
4666
|
+
const params = asArray26(action.params);
|
|
4388
4667
|
for (let pi = 0; pi < params.length; pi++) {
|
|
4389
4668
|
const param = params[pi];
|
|
4390
4669
|
if (!param || typeof param !== "object") continue;
|
|
@@ -4406,7 +4685,7 @@ function validateObjectReferences(stack) {
|
|
|
4406
4685
|
);
|
|
4407
4686
|
}
|
|
4408
4687
|
};
|
|
4409
|
-
const globalActions =
|
|
4688
|
+
const globalActions = asArray26(stack.actions);
|
|
4410
4689
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
4411
4690
|
const action = globalActions[ai];
|
|
4412
4691
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4416,7 +4695,7 @@ function validateObjectReferences(stack) {
|
|
|
4416
4695
|
const obj = objects[oi];
|
|
4417
4696
|
if (!obj || typeof obj !== "object") continue;
|
|
4418
4697
|
const objName = strName6(obj.name) ?? `#${oi}`;
|
|
4419
|
-
const objActions =
|
|
4698
|
+
const objActions = asArray26(obj.actions);
|
|
4420
4699
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
4421
4700
|
const action = objActions[ai];
|
|
4422
4701
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4427,12 +4706,12 @@ function validateObjectReferences(stack) {
|
|
|
4427
4706
|
);
|
|
4428
4707
|
}
|
|
4429
4708
|
}
|
|
4430
|
-
const dashboards =
|
|
4709
|
+
const dashboards = asArray26(stack.dashboards);
|
|
4431
4710
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4432
4711
|
const dash = dashboards[di];
|
|
4433
4712
|
if (!dash || typeof dash !== "object") continue;
|
|
4434
4713
|
const dashName = strName6(dash.name) ?? `#${di}`;
|
|
4435
|
-
const filters =
|
|
4714
|
+
const filters = asArray26(dash.globalFilters);
|
|
4436
4715
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
4437
4716
|
const filter = filters[fi];
|
|
4438
4717
|
if (!filter || typeof filter !== "object") continue;
|
|
@@ -4447,13 +4726,13 @@ function validateObjectReferences(stack) {
|
|
|
4447
4726
|
);
|
|
4448
4727
|
}
|
|
4449
4728
|
}
|
|
4450
|
-
const apps =
|
|
4729
|
+
const apps = asArray26(stack.apps);
|
|
4451
4730
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4452
4731
|
const app = apps[ai];
|
|
4453
4732
|
if (!app || typeof app !== "object") continue;
|
|
4454
4733
|
const appName = strName6(app.name) ?? `#${ai}`;
|
|
4455
4734
|
const walkNav = (items, basePath) => {
|
|
4456
|
-
const navItems =
|
|
4735
|
+
const navItems = asArray26(items);
|
|
4457
4736
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4458
4737
|
const nav = navItems[ni];
|
|
4459
4738
|
if (!nav || typeof nav !== "object") continue;
|
|
@@ -4480,7 +4759,7 @@ function validateObjectReferences(stack) {
|
|
|
4480
4759
|
}
|
|
4481
4760
|
};
|
|
4482
4761
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4483
|
-
const areas =
|
|
4762
|
+
const areas = asArray26(app.areas);
|
|
4484
4763
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4485
4764
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4486
4765
|
}
|
|
@@ -4490,10 +4769,10 @@ function validateObjectReferences(stack) {
|
|
|
4490
4769
|
|
|
4491
4770
|
// src/validate-nav-target-refs.ts
|
|
4492
4771
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4493
|
-
var
|
|
4494
|
-
function
|
|
4495
|
-
if (Array.isArray(v)) return v.filter(
|
|
4496
|
-
if (
|
|
4772
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4773
|
+
function asArray27(v) {
|
|
4774
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
4775
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
4497
4776
|
return [];
|
|
4498
4777
|
}
|
|
4499
4778
|
function strName7(v) {
|
|
@@ -4507,7 +4786,7 @@ var NAV_TARGETS = [
|
|
|
4507
4786
|
];
|
|
4508
4787
|
function namesOf(collection) {
|
|
4509
4788
|
const out = /* @__PURE__ */ new Set();
|
|
4510
|
-
for (const entry of
|
|
4789
|
+
for (const entry of asArray27(collection)) {
|
|
4511
4790
|
const n = strName7(entry.name);
|
|
4512
4791
|
if (n) out.add(n);
|
|
4513
4792
|
}
|
|
@@ -4515,8 +4794,8 @@ function namesOf(collection) {
|
|
|
4515
4794
|
}
|
|
4516
4795
|
function validateNavTargetRefs(stack) {
|
|
4517
4796
|
const findings = [];
|
|
4518
|
-
if (!
|
|
4519
|
-
const apps =
|
|
4797
|
+
if (!isRec8(stack)) return findings;
|
|
4798
|
+
const apps = asArray27(stack.apps);
|
|
4520
4799
|
if (apps.length === 0) return findings;
|
|
4521
4800
|
const declared = /* @__PURE__ */ new Map();
|
|
4522
4801
|
for (const [, , collection] of NAV_TARGETS) {
|
|
@@ -4527,7 +4806,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4527
4806
|
const walk = (items, basePath) => {
|
|
4528
4807
|
if (!Array.isArray(items)) return;
|
|
4529
4808
|
for (const [ni, raw] of items.entries()) {
|
|
4530
|
-
if (!
|
|
4809
|
+
if (!isRec8(raw)) continue;
|
|
4531
4810
|
const nav = raw;
|
|
4532
4811
|
const navPath = `${basePath}[${ni}]`;
|
|
4533
4812
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -4550,7 +4829,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4550
4829
|
}
|
|
4551
4830
|
};
|
|
4552
4831
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4553
|
-
for (const [ari, area] of
|
|
4832
|
+
for (const [ari, area] of asArray27(app.areas).entries()) {
|
|
4554
4833
|
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
4555
4834
|
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
4556
4835
|
}
|
|
@@ -4560,7 +4839,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4560
4839
|
|
|
4561
4840
|
// src/validate-action-name-refs.ts
|
|
4562
4841
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
4563
|
-
function
|
|
4842
|
+
function asArray28(v) {
|
|
4564
4843
|
if (Array.isArray(v)) return v;
|
|
4565
4844
|
if (v && typeof v === "object") {
|
|
4566
4845
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4604,13 +4883,13 @@ function suggest4(target, known) {
|
|
|
4604
4883
|
}
|
|
4605
4884
|
function collectActionNames(stack) {
|
|
4606
4885
|
const names = /* @__PURE__ */ new Set();
|
|
4607
|
-
for (const action of
|
|
4886
|
+
for (const action of asArray28(stack.actions)) {
|
|
4608
4887
|
const n = strName8(action?.name);
|
|
4609
4888
|
if (n) names.add(n);
|
|
4610
4889
|
}
|
|
4611
|
-
for (const obj of
|
|
4890
|
+
for (const obj of asArray28(stack.objects)) {
|
|
4612
4891
|
if (!obj || typeof obj !== "object") continue;
|
|
4613
|
-
for (const action of
|
|
4892
|
+
for (const action of asArray28(obj.actions)) {
|
|
4614
4893
|
const n = strName8(action?.name);
|
|
4615
4894
|
if (n) names.add(n);
|
|
4616
4895
|
}
|
|
@@ -4665,7 +4944,7 @@ function validateActionNameRefs(stack) {
|
|
|
4665
4944
|
);
|
|
4666
4945
|
}
|
|
4667
4946
|
};
|
|
4668
|
-
const views =
|
|
4947
|
+
const views = asArray28(stack.views);
|
|
4669
4948
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4670
4949
|
const view = views[vi];
|
|
4671
4950
|
if (!view || typeof view !== "object") continue;
|
|
@@ -4679,7 +4958,7 @@ function validateActionNameRefs(stack) {
|
|
|
4679
4958
|
}
|
|
4680
4959
|
}
|
|
4681
4960
|
}
|
|
4682
|
-
const objects =
|
|
4961
|
+
const objects = asArray28(stack.objects);
|
|
4683
4962
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4684
4963
|
const obj = objects[oi];
|
|
4685
4964
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -4690,7 +4969,7 @@ function validateActionNameRefs(stack) {
|
|
|
4690
4969
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
4691
4970
|
}
|
|
4692
4971
|
}
|
|
4693
|
-
const pages =
|
|
4972
|
+
const pages = asArray28(stack.pages);
|
|
4694
4973
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4695
4974
|
const page = pages[pi];
|
|
4696
4975
|
if (!page || typeof page !== "object") continue;
|
|
@@ -4709,13 +4988,13 @@ function validateActionNameRefs(stack) {
|
|
|
4709
4988
|
}
|
|
4710
4989
|
}
|
|
4711
4990
|
}
|
|
4712
|
-
const apps =
|
|
4991
|
+
const apps = asArray28(stack.apps);
|
|
4713
4992
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4714
4993
|
const app = apps[ai];
|
|
4715
4994
|
if (!app || typeof app !== "object") continue;
|
|
4716
4995
|
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4717
4996
|
const walkNav = (items, basePath) => {
|
|
4718
|
-
const navItems =
|
|
4997
|
+
const navItems = asArray28(items);
|
|
4719
4998
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4720
4999
|
const nav = navItems[ni];
|
|
4721
5000
|
if (!nav || typeof nav !== "object") continue;
|
|
@@ -4734,7 +5013,7 @@ function validateActionNameRefs(stack) {
|
|
|
4734
5013
|
}
|
|
4735
5014
|
};
|
|
4736
5015
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4737
|
-
const areas =
|
|
5016
|
+
const areas = asArray28(app.areas);
|
|
4738
5017
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4739
5018
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4740
5019
|
}
|
|
@@ -4744,7 +5023,7 @@ function validateActionNameRefs(stack) {
|
|
|
4744
5023
|
|
|
4745
5024
|
// src/validate-action-locations.ts
|
|
4746
5025
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
4747
|
-
function
|
|
5026
|
+
function asArray29(v) {
|
|
4748
5027
|
if (Array.isArray(v)) return v;
|
|
4749
5028
|
if (v && typeof v === "object") {
|
|
4750
5029
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4765,7 +5044,7 @@ function collectNamePlacedActions(stack) {
|
|
|
4765
5044
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
4766
5045
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
4767
5046
|
}
|
|
4768
|
-
for (const def of
|
|
5047
|
+
for (const def of asArray29(list3.bulkActionDefs)) {
|
|
4769
5048
|
const n = strName9(def?.name);
|
|
4770
5049
|
if (n) placed.add(n);
|
|
4771
5050
|
}
|
|
@@ -4774,12 +5053,12 @@ function collectNamePlacedActions(stack) {
|
|
|
4774
5053
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
4775
5054
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
4776
5055
|
};
|
|
4777
|
-
for (const view of
|
|
5056
|
+
for (const view of asArray29(stack.views)) {
|
|
4778
5057
|
if (!view || typeof view !== "object") continue;
|
|
4779
5058
|
harvest(view.list);
|
|
4780
5059
|
harvestListViews(view.listViews);
|
|
4781
5060
|
}
|
|
4782
|
-
for (const obj of
|
|
5061
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4783
5062
|
if (!obj || typeof obj !== "object") continue;
|
|
4784
5063
|
harvestListViews(obj.listViews);
|
|
4785
5064
|
}
|
|
@@ -4804,37 +5083,121 @@ function validateActionLocations(stack) {
|
|
|
4804
5083
|
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); 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."
|
|
4805
5084
|
});
|
|
4806
5085
|
};
|
|
4807
|
-
const actions =
|
|
5086
|
+
const actions = asArray29(stack.actions);
|
|
4808
5087
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
4809
|
-
const objects =
|
|
5088
|
+
const objects = asArray29(stack.objects);
|
|
4810
5089
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4811
5090
|
const obj = objects[oi];
|
|
4812
5091
|
if (!obj || typeof obj !== "object") continue;
|
|
4813
|
-
const own =
|
|
5092
|
+
const own = asArray29(obj.actions);
|
|
4814
5093
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
4815
5094
|
}
|
|
4816
5095
|
return findings;
|
|
4817
5096
|
}
|
|
4818
5097
|
|
|
5098
|
+
// src/validate-component-props.ts
|
|
5099
|
+
import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
5100
|
+
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
5101
|
+
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5102
|
+
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5103
|
+
function isRec9(v) {
|
|
5104
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5105
|
+
}
|
|
5106
|
+
function strName10(v) {
|
|
5107
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5108
|
+
}
|
|
5109
|
+
function asArray30(v) {
|
|
5110
|
+
if (Array.isArray(v)) return v;
|
|
5111
|
+
if (v && typeof v === "object") {
|
|
5112
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5113
|
+
}
|
|
5114
|
+
return [];
|
|
5115
|
+
}
|
|
5116
|
+
var PROPS_SCHEMAS = ComponentPropsMap;
|
|
5117
|
+
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5118
|
+
function suppliedByDataSource(issue, component) {
|
|
5119
|
+
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5120
|
+
const dataSource = isRec9(component.dataSource) ? component.dataSource : void 0;
|
|
5121
|
+
return strName10(dataSource?.object) !== void 0;
|
|
5122
|
+
}
|
|
5123
|
+
function validateComponentProps(stack) {
|
|
5124
|
+
const findings = [];
|
|
5125
|
+
if (!isRec9(stack)) return findings;
|
|
5126
|
+
const pages = asArray30(stack.pages);
|
|
5127
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
5128
|
+
const page = pages[pi];
|
|
5129
|
+
if (!isRec9(page)) continue;
|
|
5130
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
5131
|
+
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5132
|
+
const type = strName10(component.type);
|
|
5133
|
+
if (!type) continue;
|
|
5134
|
+
const schema = PROPS_SCHEMAS[type];
|
|
5135
|
+
if (!schema) continue;
|
|
5136
|
+
const props = isRec9(component.properties) ? component.properties : void 0;
|
|
5137
|
+
if (!props) continue;
|
|
5138
|
+
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5139
|
+
const base = `${path}.properties`;
|
|
5140
|
+
for (const f of lintUnknownKeysAgainstSchema(schema, props, type, base)) {
|
|
5141
|
+
findings.push({
|
|
5142
|
+
severity: "warning",
|
|
5143
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5144
|
+
where,
|
|
5145
|
+
path: f.path,
|
|
5146
|
+
message: `\`${f.key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui), so nothing verifies it: \`properties\` is an untyped bag, the renderer spreads whatever it carries, and a key it does not read is ignored in silence.` + (f.suggestion ? ` Did you mean \`${f.suggestion}\`?` : ""),
|
|
5147
|
+
hint: f.guidance ?? (f.suggestion ? `Rename \`${f.key}\` \u2192 \`${f.suggestion}\`.` : `Remove \`${f.key}\`, or \u2014 if the component really does honour it \u2014 declare it on \`${type}\`'s props schema so the declaration and the renderer agree.`)
|
|
5148
|
+
});
|
|
5149
|
+
}
|
|
5150
|
+
const parsed = schema.safeParse(props);
|
|
5151
|
+
if (parsed.success) continue;
|
|
5152
|
+
for (const issue of parsed.error?.issues ?? []) {
|
|
5153
|
+
if (suppliedByDataSource(issue, component)) continue;
|
|
5154
|
+
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5155
|
+
if (issue.code === "unrecognized_keys") {
|
|
5156
|
+
for (const key of issue.keys ?? []) {
|
|
5157
|
+
findings.push({
|
|
5158
|
+
severity: "warning",
|
|
5159
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5160
|
+
where,
|
|
5161
|
+
path: `${at}.${key}`,
|
|
5162
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${issue.message}`,
|
|
5163
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5164
|
+
});
|
|
5165
|
+
}
|
|
5166
|
+
continue;
|
|
5167
|
+
}
|
|
5168
|
+
findings.push({
|
|
5169
|
+
severity: "warning",
|
|
5170
|
+
rule: COMPONENT_PROPS_INVALID,
|
|
5171
|
+
where,
|
|
5172
|
+
path: at,
|
|
5173
|
+
message: `${at.slice(base.length + 1) || "properties"}: ${describeIssue(issue, props)}`,
|
|
5174
|
+
hint: `\`${type}\`'s props are declared by ComponentPropsMap (@objectstack/spec/ui) \u2014 the rejection above carries the fix. Advisory for now: the props bag is not parsed on the storage path either, so nothing rejects this today (objectstack#5068).`
|
|
5175
|
+
});
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
return findings;
|
|
5180
|
+
}
|
|
5181
|
+
|
|
4819
5182
|
// src/validate-chart-bindings.ts
|
|
4820
5183
|
var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
4821
5184
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
4822
5185
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
4823
5186
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
4824
|
-
function
|
|
5187
|
+
function asArray31(v) {
|
|
4825
5188
|
if (Array.isArray(v)) return v;
|
|
4826
5189
|
if (v && typeof v === "object") {
|
|
4827
5190
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4828
5191
|
}
|
|
4829
5192
|
return [];
|
|
4830
5193
|
}
|
|
4831
|
-
function
|
|
5194
|
+
function strName11(v) {
|
|
4832
5195
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4833
5196
|
}
|
|
4834
5197
|
function strList3(v) {
|
|
4835
5198
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
4836
5199
|
}
|
|
4837
|
-
function
|
|
5200
|
+
function isRec10(v) {
|
|
4838
5201
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4839
5202
|
}
|
|
4840
5203
|
function distance4(a, b) {
|
|
@@ -4872,17 +5235,17 @@ function list2(names) {
|
|
|
4872
5235
|
}
|
|
4873
5236
|
function indexDatasets(stack) {
|
|
4874
5237
|
const out = /* @__PURE__ */ new Map();
|
|
4875
|
-
for (const ds of
|
|
4876
|
-
const name =
|
|
5238
|
+
for (const ds of asArray31(stack.datasets)) {
|
|
5239
|
+
const name = strName11(ds.name);
|
|
4877
5240
|
if (!name) continue;
|
|
4878
5241
|
const dimensions = /* @__PURE__ */ new Set();
|
|
4879
|
-
for (const d of
|
|
4880
|
-
const n =
|
|
5242
|
+
for (const d of asArray31(ds.dimensions)) {
|
|
5243
|
+
const n = strName11(d.name);
|
|
4881
5244
|
if (n) dimensions.add(n);
|
|
4882
5245
|
}
|
|
4883
5246
|
const measures = /* @__PURE__ */ new Set();
|
|
4884
|
-
for (const m of
|
|
4885
|
-
const n =
|
|
5247
|
+
for (const m of asArray31(ds.measures)) {
|
|
5248
|
+
const n = strName11(m.name);
|
|
4886
5249
|
if (n) measures.add(n);
|
|
4887
5250
|
}
|
|
4888
5251
|
out.set(name, { dimensions, measures });
|
|
@@ -4960,29 +5323,29 @@ function validateChartBindings(stack) {
|
|
|
4960
5323
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
4961
5324
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
4962
5325
|
};
|
|
4963
|
-
const reports =
|
|
5326
|
+
const reports = asArray31(stack.reports);
|
|
4964
5327
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
4965
5328
|
const report = reports[ri];
|
|
4966
|
-
if (!
|
|
4967
|
-
const reportName =
|
|
5329
|
+
if (!isRec10(report)) continue;
|
|
5330
|
+
const reportName = strName11(report.name) ?? `#${ri}`;
|
|
4968
5331
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
4969
|
-
if (!
|
|
5332
|
+
if (!isRec10(chart)) return;
|
|
4970
5333
|
check({
|
|
4971
5334
|
dataset,
|
|
4972
5335
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
4973
5336
|
// it in lets the yAxis "declared but not selected" check work without
|
|
4974
5337
|
// reporting the selection itself twice.
|
|
4975
5338
|
values: { names: values, path: `${path}.values` },
|
|
4976
|
-
xAxis:
|
|
4977
|
-
yAxis:
|
|
4978
|
-
series:
|
|
5339
|
+
xAxis: strName11(chart.xAxis) ? { name: strName11(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
5340
|
+
yAxis: strName11(chart.yAxis) ? { name: strName11(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
5341
|
+
series: asArray31(chart.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
4979
5342
|
where,
|
|
4980
5343
|
path: `${path}.chart`
|
|
4981
5344
|
});
|
|
4982
5345
|
};
|
|
4983
5346
|
checkReportChart(
|
|
4984
5347
|
report.chart,
|
|
4985
|
-
|
|
5348
|
+
strName11(report.dataset),
|
|
4986
5349
|
strList3(report.values),
|
|
4987
5350
|
`report "${reportName}" \xB7 chart`,
|
|
4988
5351
|
`reports[${ri}]`
|
|
@@ -4990,45 +5353,45 @@ function validateChartBindings(stack) {
|
|
|
4990
5353
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
4991
5354
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
4992
5355
|
const block = blocks[bi];
|
|
4993
|
-
if (!
|
|
5356
|
+
if (!isRec10(block)) continue;
|
|
4994
5357
|
checkReportChart(
|
|
4995
5358
|
block.chart,
|
|
4996
|
-
|
|
5359
|
+
strName11(block.dataset),
|
|
4997
5360
|
strList3(block.values),
|
|
4998
|
-
`report "${reportName}" \xB7 block "${
|
|
5361
|
+
`report "${reportName}" \xB7 block "${strName11(block.name) ?? `#${bi}`}" chart`,
|
|
4999
5362
|
`reports[${ri}].blocks[${bi}]`
|
|
5000
5363
|
);
|
|
5001
5364
|
}
|
|
5002
5365
|
}
|
|
5003
5366
|
const checkListChart = (container, where, path) => {
|
|
5004
|
-
if (!
|
|
5367
|
+
if (!isRec10(container)) return;
|
|
5005
5368
|
const chart = container.chart;
|
|
5006
|
-
if (!
|
|
5369
|
+
if (!isRec10(chart)) return;
|
|
5007
5370
|
check({
|
|
5008
|
-
dataset:
|
|
5371
|
+
dataset: strName11(chart.dataset),
|
|
5009
5372
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5010
5373
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
5011
5374
|
where,
|
|
5012
5375
|
path: `${path}.chart`
|
|
5013
5376
|
});
|
|
5014
5377
|
};
|
|
5015
|
-
const views =
|
|
5378
|
+
const views = asArray31(stack.views);
|
|
5016
5379
|
for (let vi = 0; vi < views.length; vi++) {
|
|
5017
5380
|
const view = views[vi];
|
|
5018
|
-
if (!
|
|
5019
|
-
const viewName =
|
|
5381
|
+
if (!isRec10(view)) continue;
|
|
5382
|
+
const viewName = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
|
|
5020
5383
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
5021
|
-
if (
|
|
5384
|
+
if (isRec10(view.listViews)) {
|
|
5022
5385
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
5023
5386
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
5024
5387
|
}
|
|
5025
5388
|
}
|
|
5026
5389
|
}
|
|
5027
|
-
const objects =
|
|
5390
|
+
const objects = asArray31(stack.objects);
|
|
5028
5391
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5029
5392
|
const obj = objects[oi];
|
|
5030
|
-
if (!
|
|
5031
|
-
const objName =
|
|
5393
|
+
if (!isRec10(obj) || !isRec10(obj.listViews)) continue;
|
|
5394
|
+
const objName = strName11(obj.name) ?? `#${oi}`;
|
|
5032
5395
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
5033
5396
|
checkListChart(
|
|
5034
5397
|
lv,
|
|
@@ -5037,22 +5400,22 @@ function validateChartBindings(stack) {
|
|
|
5037
5400
|
);
|
|
5038
5401
|
}
|
|
5039
5402
|
}
|
|
5040
|
-
const pages =
|
|
5403
|
+
const pages = asArray31(stack.pages);
|
|
5041
5404
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5042
5405
|
const page = pages[pi];
|
|
5043
|
-
if (!
|
|
5044
|
-
const pageName =
|
|
5406
|
+
if (!isRec10(page)) continue;
|
|
5407
|
+
const pageName = strName11(page.name) ?? `#${pi}`;
|
|
5045
5408
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5046
|
-
const props =
|
|
5047
|
-
if (!props || !
|
|
5048
|
-
const axisRefs =
|
|
5049
|
-
const seriesRefs =
|
|
5409
|
+
const props = isRec10(component.properties) ? component.properties : void 0;
|
|
5410
|
+
if (!props || !strName11(props.dataset)) continue;
|
|
5411
|
+
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName11(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
5412
|
+
const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
5050
5413
|
check({
|
|
5051
|
-
dataset:
|
|
5414
|
+
dataset: strName11(props.dataset),
|
|
5052
5415
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5053
5416
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
5054
5417
|
series: [...axisRefs, ...seriesRefs],
|
|
5055
|
-
where: `page "${pageName}" \xB7 ${
|
|
5418
|
+
where: `page "${pageName}" \xB7 ${strName11(component.type) ?? "chart"}`,
|
|
5056
5419
|
path: `${path}.properties`
|
|
5057
5420
|
});
|
|
5058
5421
|
}
|
|
@@ -5060,28 +5423,280 @@ function validateChartBindings(stack) {
|
|
|
5060
5423
|
return findings;
|
|
5061
5424
|
}
|
|
5062
5425
|
|
|
5063
|
-
// src/validate-
|
|
5064
|
-
import {
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5426
|
+
// src/validate-rule-compilability.ts
|
|
5427
|
+
import { createRequire as createRequire3 } from "module";
|
|
5428
|
+
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
5429
|
+
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
5430
|
+
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
5431
|
+
var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5432
|
+
function asArray32(v) {
|
|
5433
|
+
if (Array.isArray(v)) return v.filter(isRec11);
|
|
5434
|
+
if (isRec11(v)) {
|
|
5435
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ name, ...def }));
|
|
5071
5436
|
}
|
|
5072
5437
|
return [];
|
|
5073
5438
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5439
|
+
var cachedAjv = null;
|
|
5440
|
+
var cachedAddFormats = null;
|
|
5441
|
+
function loadAjv() {
|
|
5442
|
+
if (cachedAjv) return cachedAjv;
|
|
5443
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5444
|
+
let mod;
|
|
5445
|
+
try {
|
|
5446
|
+
mod = createRequire3(anchor)("ajv");
|
|
5447
|
+
} catch (err) {
|
|
5448
|
+
throw new Error(
|
|
5449
|
+
`@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
|
+
);
|
|
5451
|
+
}
|
|
5452
|
+
const ctor = isRec11(mod) && "default" in mod ? mod.default : mod;
|
|
5453
|
+
cachedAjv = ctor;
|
|
5454
|
+
return ctor;
|
|
5455
|
+
}
|
|
5456
|
+
function loadAddFormats() {
|
|
5457
|
+
if (cachedAddFormats) return cachedAddFormats;
|
|
5458
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5459
|
+
let mod;
|
|
5460
|
+
try {
|
|
5461
|
+
mod = createRequire3(anchor)("ajv-formats");
|
|
5462
|
+
} catch (err) {
|
|
5463
|
+
throw new Error(
|
|
5464
|
+
`@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
|
+
);
|
|
5466
|
+
}
|
|
5467
|
+
const plugin = isRec11(mod) && "default" in mod ? mod.default : mod;
|
|
5468
|
+
cachedAddFormats = plugin;
|
|
5469
|
+
return plugin;
|
|
5470
|
+
}
|
|
5471
|
+
function createRuntimeAjv() {
|
|
5472
|
+
const instance = new (loadAjv())(RUNTIME_AJV_OPTIONS);
|
|
5473
|
+
loadAddFormats()(instance);
|
|
5474
|
+
return instance;
|
|
5475
|
+
}
|
|
5476
|
+
function registeredFormatNames() {
|
|
5477
|
+
const names = Object.keys(createRuntimeAjv().formats).sort();
|
|
5478
|
+
if (names.length === 0) {
|
|
5479
|
+
throw new Error(
|
|
5480
|
+
`@objectstack/lint: the runtime-parity ajv instance has no \`format\` registered. "ajv-formats" loaded but added nothing, so the set of legitimate format names is unknown \u2014 refusing to judge format names against an empty vocabulary, which would report every \`format\` in the stack as misspelled. Check that the installed "ajv-formats" is the real package and matches the version @objectstack/lint declares.`
|
|
5481
|
+
);
|
|
5482
|
+
}
|
|
5483
|
+
return names;
|
|
5484
|
+
}
|
|
5485
|
+
function errorText(err) {
|
|
5486
|
+
return err instanceof Error ? err.message : String(err);
|
|
5487
|
+
}
|
|
5488
|
+
var MAX_RULE_NESTING_DEPTH = 16;
|
|
5489
|
+
function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
5490
|
+
const name = typeof rule.name === "string" && rule.name ? rule.name : "?";
|
|
5491
|
+
const label2 = labelTrail ? `${labelTrail} \u2192 '${name}'` : `'${name}'`;
|
|
5492
|
+
const path = pathTrail ? `${pathTrail}.${name}` : name;
|
|
5493
|
+
const out = [{ rule, label: label2, path }];
|
|
5494
|
+
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
5495
|
+
for (const branch of ["then", "otherwise"]) {
|
|
5496
|
+
const nested = rule[branch];
|
|
5497
|
+
if (isRec11(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
5498
|
+
}
|
|
5499
|
+
return out;
|
|
5500
|
+
}
|
|
5501
|
+
function walkObjectValidationRules(stack) {
|
|
5502
|
+
const walked = [];
|
|
5503
|
+
if (!isRec11(stack)) return walked;
|
|
5504
|
+
for (const obj of asArray32(stack.objects)) {
|
|
5505
|
+
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
5506
|
+
const validations = obj.validations;
|
|
5507
|
+
for (const authored of asArray32(validations)) {
|
|
5508
|
+
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
5509
|
+
walked.push({
|
|
5510
|
+
rule,
|
|
5511
|
+
objectName,
|
|
5512
|
+
label: label2,
|
|
5513
|
+
where: `object '${objectName}' \xB7 validation ${label2}`,
|
|
5514
|
+
basePath: `objects.${objectName}.validations.${path}`
|
|
5515
|
+
});
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
5518
|
+
}
|
|
5519
|
+
return walked;
|
|
5520
|
+
}
|
|
5521
|
+
function validateRuleCompilability(stack) {
|
|
5522
|
+
const findings = [];
|
|
5523
|
+
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5524
|
+
if (rule.type === "format" && typeof rule.regex === "string" && rule.regex !== "") {
|
|
5525
|
+
try {
|
|
5526
|
+
new RegExp(rule.regex);
|
|
5527
|
+
} catch (err) {
|
|
5528
|
+
findings.push({
|
|
5529
|
+
severity: "error",
|
|
5530
|
+
rule: VALIDATION_RULE_REGEX_UNCOMPILABLE,
|
|
5531
|
+
where,
|
|
5532
|
+
path: `${basePath}.regex`,
|
|
5533
|
+
message: `\`format\` validation ${label2} on object '${objectName}' declares a \`regex\` that does not compile: ${errorText(err)}. The write path builds it with \`new RegExp(rule.regex)\` and SKIPS the rule when that throws (rule-validator.ts \`checkFormat\`), so the rule is declared, listed in the metadata, and enforces nothing on any record.`,
|
|
5534
|
+
hint: `Fix the pattern so \`new RegExp('${rule.regex}')\` compiles \u2014 a literal \`(\`, \`[\` or \`\\\` must be escaped (\`\\\\(\`, \`\\\\[\`, \`\\\\\\\\\`), and the source is a STRING, so a backslash is written twice in TypeScript ('^\\\\d{2}-\\\\d{7}$'). Or drop \`regex\` and use a named \`format\` ('email' | 'url' | 'phone' | 'json').`
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
}
|
|
5538
|
+
if (rule.type === "json_schema" && isRec11(rule.schema)) {
|
|
5539
|
+
try {
|
|
5540
|
+
createRuntimeAjv().compile(rule.schema);
|
|
5541
|
+
} catch (err) {
|
|
5542
|
+
findings.push({
|
|
5543
|
+
severity: "error",
|
|
5544
|
+
rule: VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
|
|
5545
|
+
where,
|
|
5546
|
+
path: `${basePath}.schema`,
|
|
5547
|
+
message: `\`json_schema\` validation ${label2} on object '${objectName}' declares a \`schema\` ajv cannot compile: ${errorText(err)}. The write path compiles it with the same ajv (\`new Ajv({ allErrors: true, strict: false })\` + \`ajv-formats\`) and SKIPS the rule when that throws (rule-validator.ts \`checkJsonSchema\`), so the rule is declared and enforces nothing on any record.`,
|
|
5548
|
+
hint: `Correct the schema so ajv compiles it \u2014 the message above names the offending keyword. \`type\` must be one of null|boolean|object|array|number|string|integer (or an array of those), \`required\` an array of strings, and every \`$ref\` must resolve. Vendor keywords are fine (the runtime runs \`strict: false\`); a MALFORMED standard keyword is not.`
|
|
5549
|
+
});
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
return findings;
|
|
5554
|
+
}
|
|
5555
|
+
|
|
5556
|
+
// src/validate-rule-schema-formats.ts
|
|
5557
|
+
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
5558
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5559
|
+
var SUBSCHEMA_KEYS = [
|
|
5560
|
+
"additionalItems",
|
|
5561
|
+
"additionalProperties",
|
|
5562
|
+
"contains",
|
|
5563
|
+
"propertyNames",
|
|
5564
|
+
"if",
|
|
5565
|
+
"then",
|
|
5566
|
+
"else",
|
|
5567
|
+
"not",
|
|
5568
|
+
"unevaluatedItems",
|
|
5569
|
+
"unevaluatedProperties"
|
|
5570
|
+
];
|
|
5571
|
+
var SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
5572
|
+
var SUBSCHEMA_MAP_KEYS = [
|
|
5573
|
+
"properties",
|
|
5574
|
+
"patternProperties",
|
|
5575
|
+
"$defs",
|
|
5576
|
+
"definitions",
|
|
5577
|
+
"dependentSchemas"
|
|
5578
|
+
];
|
|
5579
|
+
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
5580
|
+
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
5581
|
+
function collectFormatUses(schema, pointer, out, depth) {
|
|
5582
|
+
if (!isRec12(schema)) return;
|
|
5583
|
+
if (typeof schema.format === "string") {
|
|
5584
|
+
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
5585
|
+
}
|
|
5586
|
+
if (depth >= MAX_SCHEMA_WALK_DEPTH) return;
|
|
5587
|
+
for (const key of SUBSCHEMA_KEYS) {
|
|
5588
|
+
if (key in schema) {
|
|
5589
|
+
collectFormatUses(schema[key], `${pointer}/${escapePointerSegment(key)}`, out, depth + 1);
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
for (const key of SUBSCHEMA_LIST_KEYS) {
|
|
5593
|
+
const value = schema[key];
|
|
5594
|
+
if (!Array.isArray(value)) continue;
|
|
5595
|
+
value.forEach((entry, index) => {
|
|
5596
|
+
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${index}`, out, depth + 1);
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
5600
|
+
const value = schema[key];
|
|
5601
|
+
if (!isRec12(value)) continue;
|
|
5602
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
5603
|
+
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
const items = schema.items;
|
|
5607
|
+
if (Array.isArray(items)) {
|
|
5608
|
+
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
5609
|
+
} else if (isRec12(items)) {
|
|
5610
|
+
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
5611
|
+
}
|
|
5612
|
+
const dependencies = schema.dependencies;
|
|
5613
|
+
if (isRec12(dependencies)) {
|
|
5614
|
+
for (const [name, entry] of Object.entries(dependencies)) {
|
|
5615
|
+
if (!isRec12(entry)) continue;
|
|
5616
|
+
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
function editDistance2(a, b) {
|
|
5621
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
5622
|
+
for (let i = 1; i <= a.length; i++) {
|
|
5623
|
+
const current = [i];
|
|
5624
|
+
for (let j = 1; j <= b.length; j++) {
|
|
5625
|
+
current[j] = Math.min(
|
|
5626
|
+
previous[j] + 1,
|
|
5627
|
+
current[j - 1] + 1,
|
|
5628
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
5629
|
+
);
|
|
5630
|
+
}
|
|
5631
|
+
previous = current;
|
|
5632
|
+
}
|
|
5633
|
+
return previous[b.length];
|
|
5634
|
+
}
|
|
5635
|
+
function nearestRegisteredFormat(name, registered) {
|
|
5636
|
+
const budget = Math.min(3, Math.floor(name.length / 2));
|
|
5637
|
+
if (budget < 1) return null;
|
|
5638
|
+
const authored = name.toLowerCase();
|
|
5639
|
+
let best = null;
|
|
5640
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
5641
|
+
for (const candidate of [...registered].sort()) {
|
|
5642
|
+
const distance7 = editDistance2(authored, candidate);
|
|
5643
|
+
if (distance7 < bestDistance) {
|
|
5644
|
+
bestDistance = distance7;
|
|
5645
|
+
best = candidate;
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
return bestDistance <= budget ? best : null;
|
|
5649
|
+
}
|
|
5650
|
+
function validateRuleSchemaFormats(stack) {
|
|
5651
|
+
const findings = [];
|
|
5652
|
+
const pending = [];
|
|
5653
|
+
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5654
|
+
if (rule.type !== "json_schema" || !isRec12(rule.schema)) continue;
|
|
5655
|
+
const uses = [];
|
|
5656
|
+
collectFormatUses(rule.schema, "", uses, 0);
|
|
5657
|
+
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
5658
|
+
}
|
|
5659
|
+
if (pending.length === 0) return findings;
|
|
5660
|
+
const registered = registeredFormatNames();
|
|
5661
|
+
const known = new Set(registered);
|
|
5662
|
+
for (const { use, where, label: label2, objectName, basePath } of pending) {
|
|
5663
|
+
if (known.has(use.name)) continue;
|
|
5664
|
+
const suggestion = nearestRegisteredFormat(use.name, registered);
|
|
5665
|
+
const pointer = `#${use.pointer}`;
|
|
5666
|
+
findings.push({
|
|
5667
|
+
severity: "error",
|
|
5668
|
+
rule: VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
|
|
5669
|
+
where,
|
|
5670
|
+
path: `${basePath}.schema${pointer}`,
|
|
5671
|
+
message: `\`json_schema\` validation ${label2} on object '${objectName}' names \`format: '${use.name}'\` at \`${pointer}\`, which is not a registered format. ajv logs \`unknown format "${use.name}" ignored\` once at compile time and DROPS the keyword \u2014 in the write path (rule-validator.ts, \`strict: false\`) and in the publish gate alike \u2014 so the schema compiles, the rule ships and runs on every write, its \`type\`/\`required\` keywords are enforced, and this constraint is enforced on no record, ever. The record is ACCEPTED, so nothing downstream reports the gap either.`,
|
|
5672
|
+
hint: (suggestion ? `Did you mean \`format: '${suggestion}'\`? ` : "") + `The registered names are: ${registered.join(", ")} \u2014 the default \`ajv-formats\` set, the one \`rule-validator.ts\` registers (#5029). Names are case-sensitive and hyphenated (\`date-time\`, not \`datetime\`). If you meant a constraint ajv has no format for, express it with \`pattern\` instead \u2014 a regex is enforced, an unknown format name is not.`
|
|
5673
|
+
});
|
|
5674
|
+
}
|
|
5675
|
+
return findings;
|
|
5676
|
+
}
|
|
5677
|
+
|
|
5678
|
+
// src/validate-nav-access.ts
|
|
5679
|
+
import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
|
|
5680
|
+
|
|
5681
|
+
// src/build-access-matrix.ts
|
|
5682
|
+
function asArray33(v) {
|
|
5683
|
+
if (Array.isArray(v)) return v;
|
|
5684
|
+
if (v && typeof v === "object") {
|
|
5685
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5686
|
+
}
|
|
5687
|
+
return [];
|
|
5688
|
+
}
|
|
5689
|
+
function buildAccessMatrix(stack) {
|
|
5690
|
+
const entries = [];
|
|
5691
|
+
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
5077
5692
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
5078
|
-
for (const obj of
|
|
5693
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5079
5694
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
5080
5695
|
if (!name) continue;
|
|
5081
5696
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
5082
5697
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
5083
5698
|
}
|
|
5084
|
-
for (const ps of
|
|
5699
|
+
for (const ps of asArray33(stack.permissions)) {
|
|
5085
5700
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
5086
5701
|
if (!psName) continue;
|
|
5087
5702
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -5154,34 +5769,34 @@ function diffAccessMatrix(before, after) {
|
|
|
5154
5769
|
|
|
5155
5770
|
// src/validate-nav-access.ts
|
|
5156
5771
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
5157
|
-
function
|
|
5772
|
+
function asArray34(v) {
|
|
5158
5773
|
if (Array.isArray(v)) return v;
|
|
5159
5774
|
if (v && typeof v === "object") {
|
|
5160
5775
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5161
5776
|
}
|
|
5162
5777
|
return [];
|
|
5163
5778
|
}
|
|
5164
|
-
function
|
|
5779
|
+
function strName12(v) {
|
|
5165
5780
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5166
5781
|
}
|
|
5167
5782
|
function collectNavExposures(stack) {
|
|
5168
5783
|
const out = [];
|
|
5169
|
-
const apps =
|
|
5784
|
+
const apps = asArray34(stack.apps);
|
|
5170
5785
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
5171
5786
|
const app = apps[ai];
|
|
5172
5787
|
if (!app || typeof app !== "object") continue;
|
|
5173
|
-
const appName =
|
|
5788
|
+
const appName = strName12(app.name) ?? `#${ai}`;
|
|
5174
5789
|
const walk = (items, basePath) => {
|
|
5175
|
-
const navItems =
|
|
5790
|
+
const navItems = asArray34(items);
|
|
5176
5791
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
5177
5792
|
const nav = navItems[ni];
|
|
5178
5793
|
if (!nav || typeof nav !== "object") continue;
|
|
5179
5794
|
const navPath = `${basePath}[${ni}]`;
|
|
5180
|
-
const objectName =
|
|
5795
|
+
const objectName = strName12(nav.objectName);
|
|
5181
5796
|
if (nav.type === "object" && objectName) {
|
|
5182
5797
|
out.push({
|
|
5183
5798
|
objectName,
|
|
5184
|
-
where: `app "${appName}" \xB7 nav "${
|
|
5799
|
+
where: `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
5185
5800
|
path: `${navPath}.objectName`
|
|
5186
5801
|
});
|
|
5187
5802
|
}
|
|
@@ -5189,7 +5804,7 @@ function collectNavExposures(stack) {
|
|
|
5189
5804
|
}
|
|
5190
5805
|
};
|
|
5191
5806
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
5192
|
-
const areas =
|
|
5807
|
+
const areas = asArray34(app.areas);
|
|
5193
5808
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
5194
5809
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
5195
5810
|
}
|
|
@@ -5199,13 +5814,13 @@ function collectNavExposures(stack) {
|
|
|
5199
5814
|
function validateNavAccess(stack) {
|
|
5200
5815
|
const findings = [];
|
|
5201
5816
|
if (!stack || typeof stack !== "object") return findings;
|
|
5202
|
-
const permissionSets =
|
|
5817
|
+
const permissionSets = asArray34(stack.permissions);
|
|
5203
5818
|
if (permissionSets.length === 0) return findings;
|
|
5204
5819
|
const exposures = collectNavExposures(stack);
|
|
5205
5820
|
if (exposures.length === 0) return findings;
|
|
5206
5821
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
5207
|
-
for (const obj of
|
|
5208
|
-
const n =
|
|
5822
|
+
for (const obj of asArray34(stack.objects)) {
|
|
5823
|
+
const n = strName12(obj.name);
|
|
5209
5824
|
if (n) ownObjects.add(n);
|
|
5210
5825
|
}
|
|
5211
5826
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -5237,15 +5852,15 @@ function validateNavAccess(stack) {
|
|
|
5237
5852
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
5238
5853
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
5239
5854
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
5240
|
-
function
|
|
5855
|
+
function isRec13(v) {
|
|
5241
5856
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5242
5857
|
}
|
|
5243
|
-
function
|
|
5858
|
+
function asArray35(v) {
|
|
5244
5859
|
if (Array.isArray(v)) return v;
|
|
5245
|
-
if (
|
|
5860
|
+
if (isRec13(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec13(def) ? def : {} }));
|
|
5246
5861
|
return [];
|
|
5247
5862
|
}
|
|
5248
|
-
function
|
|
5863
|
+
function strName13(v) {
|
|
5249
5864
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5250
5865
|
}
|
|
5251
5866
|
function distance5(a, b) {
|
|
@@ -5303,35 +5918,32 @@ function collectViewRecord(view, factsFor) {
|
|
|
5303
5918
|
const addView = (objectName, name) => {
|
|
5304
5919
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
5305
5920
|
};
|
|
5306
|
-
const
|
|
5307
|
-
|
|
5308
|
-
|
|
5921
|
+
const addSections = (container, binding) => {
|
|
5922
|
+
if (!binding) return;
|
|
5923
|
+
for (const section of asArray35(container.sections)) {
|
|
5924
|
+
const sectionName = strName13(section.name);
|
|
5925
|
+
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
5926
|
+
}
|
|
5927
|
+
};
|
|
5928
|
+
const listBinding = isRec13(view.list) ? bindingOf(view.list) : void 0;
|
|
5929
|
+
if (isRec13(view.list)) addView(listBinding, strName13(view.list.name));
|
|
5930
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
5309
5931
|
for (const key of ["listViews", "formViews"]) {
|
|
5310
5932
|
const container = view[key];
|
|
5311
|
-
if (!
|
|
5933
|
+
if (!isRec13(container)) continue;
|
|
5312
5934
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
5313
|
-
if (!
|
|
5935
|
+
if (!isRec13(sub)) continue;
|
|
5314
5936
|
const binding = bindingOf(sub) ?? listBinding;
|
|
5315
5937
|
addView(binding, subKey);
|
|
5316
|
-
addView(binding,
|
|
5317
|
-
|
|
5318
|
-
for (const section of asArray31(sub.sections)) {
|
|
5319
|
-
const sectionName = strName12(section.name);
|
|
5320
|
-
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
5321
|
-
}
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
}
|
|
5325
|
-
const sectionBinding = recordObject ?? listBinding;
|
|
5326
|
-
if (sectionBinding) {
|
|
5327
|
-
for (const section of asArray31(view.sections)) {
|
|
5328
|
-
const sectionName = strName12(section.name);
|
|
5329
|
-
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
5938
|
+
addView(binding, strName13(sub.name));
|
|
5939
|
+
addSections(sub, binding);
|
|
5330
5940
|
}
|
|
5331
5941
|
}
|
|
5942
|
+
if (isRec13(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
5943
|
+
addSections(view, recordObject ?? listBinding);
|
|
5332
5944
|
}
|
|
5333
5945
|
function viewObjectName(view) {
|
|
5334
|
-
return
|
|
5946
|
+
return strName13(view.objectName) ?? strName13(view.object) ?? (isRec13(view.data) ? strName13(view.data.object) : void 0);
|
|
5335
5947
|
}
|
|
5336
5948
|
function readOptions(field) {
|
|
5337
5949
|
const raw = field.options;
|
|
@@ -5343,14 +5955,14 @@ function readOptions(field) {
|
|
|
5343
5955
|
values.add(opt);
|
|
5344
5956
|
continue;
|
|
5345
5957
|
}
|
|
5346
|
-
if (!
|
|
5347
|
-
const value =
|
|
5958
|
+
if (!isRec13(opt)) continue;
|
|
5959
|
+
const value = strName13(opt.value);
|
|
5348
5960
|
if (!value) continue;
|
|
5349
5961
|
values.add(value);
|
|
5350
|
-
const label2 =
|
|
5962
|
+
const label2 = strName13(opt.label);
|
|
5351
5963
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
5352
5964
|
}
|
|
5353
|
-
} else if (
|
|
5965
|
+
} else if (isRec13(raw)) {
|
|
5354
5966
|
for (const [value, label2] of Object.entries(raw)) {
|
|
5355
5967
|
values.add(value);
|
|
5356
5968
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -5370,48 +5982,48 @@ function buildUniverse(stack) {
|
|
|
5370
5982
|
}
|
|
5371
5983
|
return facts;
|
|
5372
5984
|
};
|
|
5373
|
-
for (const obj of
|
|
5374
|
-
const objectName =
|
|
5985
|
+
for (const obj of asArray35(stack.objects)) {
|
|
5986
|
+
const objectName = strName13(obj.name);
|
|
5375
5987
|
if (!objectName) continue;
|
|
5376
5988
|
const facts = factsFor(objectName);
|
|
5377
|
-
for (const field of
|
|
5378
|
-
const fieldName =
|
|
5989
|
+
for (const field of asArray35(obj.fields)) {
|
|
5990
|
+
const fieldName = strName13(field.name);
|
|
5379
5991
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
5380
5992
|
}
|
|
5381
|
-
for (const action of
|
|
5382
|
-
const actionName =
|
|
5993
|
+
for (const action of asArray35(obj.actions)) {
|
|
5994
|
+
const actionName = strName13(action.name);
|
|
5383
5995
|
if (actionName) facts.actions.set(actionName, action);
|
|
5384
5996
|
}
|
|
5385
|
-
for (const view of
|
|
5386
|
-
collectViewRecord({ ...view, object:
|
|
5997
|
+
for (const view of asArray35(obj.views)) {
|
|
5998
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
5387
5999
|
}
|
|
5388
6000
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
5389
|
-
for (const group of
|
|
5390
|
-
const key =
|
|
6001
|
+
for (const group of asArray35(obj.fieldGroups)) {
|
|
6002
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
5391
6003
|
if (key) facts.sections.add(key);
|
|
5392
6004
|
}
|
|
5393
6005
|
}
|
|
5394
|
-
for (const view of
|
|
6006
|
+
for (const view of asArray35(stack.views)) {
|
|
5395
6007
|
collectViewRecord(view, factsFor);
|
|
5396
6008
|
}
|
|
5397
|
-
const pages =
|
|
6009
|
+
const pages = asArray35(stack.pages);
|
|
5398
6010
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5399
6011
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
5400
6012
|
if (!walked.objectName) continue;
|
|
5401
|
-
const props =
|
|
6013
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
5402
6014
|
if (!props) continue;
|
|
5403
|
-
for (const section of
|
|
5404
|
-
const sectionName =
|
|
6015
|
+
for (const section of asArray35(props.sections)) {
|
|
6016
|
+
const sectionName = strName13(section.name);
|
|
5405
6017
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
5406
6018
|
}
|
|
5407
6019
|
}
|
|
5408
6020
|
}
|
|
5409
6021
|
const globalActions = /* @__PURE__ */ new Map();
|
|
5410
6022
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
5411
|
-
for (const action of
|
|
5412
|
-
const actionName =
|
|
6023
|
+
for (const action of asArray35(stack.actions)) {
|
|
6024
|
+
const actionName = strName13(action.name);
|
|
5413
6025
|
if (!actionName) continue;
|
|
5414
|
-
const owner =
|
|
6026
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
5415
6027
|
if (owner) {
|
|
5416
6028
|
factsFor(owner).actions.set(actionName, action);
|
|
5417
6029
|
actionOwners.set(actionName, owner);
|
|
@@ -5425,41 +6037,41 @@ function buildUniverse(stack) {
|
|
|
5425
6037
|
}
|
|
5426
6038
|
}
|
|
5427
6039
|
const apps = /* @__PURE__ */ new Map();
|
|
5428
|
-
for (const app of
|
|
5429
|
-
const appName =
|
|
6040
|
+
for (const app of asArray35(stack.apps)) {
|
|
6041
|
+
const appName = strName13(app.name);
|
|
5430
6042
|
if (!appName) continue;
|
|
5431
6043
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
5432
6044
|
const walkNav = (items) => {
|
|
5433
|
-
for (const item of
|
|
5434
|
-
const id =
|
|
6045
|
+
for (const item of asArray35(items)) {
|
|
6046
|
+
const id = strName13(item.id);
|
|
5435
6047
|
if (id) navIds.add(id);
|
|
5436
6048
|
if (item.children) walkNav(item.children);
|
|
5437
6049
|
}
|
|
5438
6050
|
};
|
|
5439
6051
|
walkNav(app.navigation);
|
|
5440
|
-
for (const area of
|
|
5441
|
-
const areaId =
|
|
6052
|
+
for (const area of asArray35(app.areas)) {
|
|
6053
|
+
const areaId = strName13(area.id);
|
|
5442
6054
|
if (areaId) navIds.add(areaId);
|
|
5443
6055
|
walkNav(area.navigation);
|
|
5444
6056
|
}
|
|
5445
6057
|
apps.set(appName, navIds);
|
|
5446
6058
|
}
|
|
5447
6059
|
const dashboards = /* @__PURE__ */ new Map();
|
|
5448
|
-
for (const dash of
|
|
5449
|
-
const dashName =
|
|
6060
|
+
for (const dash of asArray35(stack.dashboards)) {
|
|
6061
|
+
const dashName = strName13(dash.name);
|
|
5450
6062
|
if (!dashName) continue;
|
|
5451
6063
|
const widgets = /* @__PURE__ */ new Set();
|
|
5452
|
-
for (const widget of
|
|
5453
|
-
const id =
|
|
6064
|
+
for (const widget of asArray35(dash.widgets)) {
|
|
6065
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
5454
6066
|
if (id) widgets.add(id);
|
|
5455
6067
|
}
|
|
5456
6068
|
const actions = /* @__PURE__ */ new Set();
|
|
5457
6069
|
const headerActions = [
|
|
5458
|
-
...
|
|
5459
|
-
...
|
|
6070
|
+
...asArray35(isRec13(dash.header) ? dash.header.actions : void 0),
|
|
6071
|
+
...asArray35(dash.actions)
|
|
5460
6072
|
];
|
|
5461
6073
|
for (const action of headerActions) {
|
|
5462
|
-
const key =
|
|
6074
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
5463
6075
|
if (key) actions.add(key);
|
|
5464
6076
|
}
|
|
5465
6077
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -5471,7 +6083,7 @@ function localePath(bundleIndex, locale) {
|
|
|
5471
6083
|
}
|
|
5472
6084
|
function validateTranslationReferences(stack) {
|
|
5473
6085
|
const findings = [];
|
|
5474
|
-
if (!
|
|
6086
|
+
if (!isRec13(stack)) return findings;
|
|
5475
6087
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
5476
6088
|
if (bundles.length === 0) return findings;
|
|
5477
6089
|
const universe = buildUniverse(stack);
|
|
@@ -5480,13 +6092,13 @@ function validateTranslationReferences(stack) {
|
|
|
5480
6092
|
};
|
|
5481
6093
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
5482
6094
|
const bundle = bundles[bi];
|
|
5483
|
-
if (!
|
|
6095
|
+
if (!isRec13(bundle)) continue;
|
|
5484
6096
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
5485
|
-
if (!
|
|
6097
|
+
if (!isRec13(rawData)) continue;
|
|
5486
6098
|
const base = localePath(bi, locale);
|
|
5487
6099
|
const inLocale = `locale "${locale}"`;
|
|
5488
6100
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
5489
|
-
if (!
|
|
6101
|
+
if (!isRec13(rawNode)) continue;
|
|
5490
6102
|
const objPath = `${base}.objects.${objectName}`;
|
|
5491
6103
|
const facts = universe.objects.get(objectName);
|
|
5492
6104
|
if (!facts) {
|
|
@@ -5512,7 +6124,7 @@ function validateTranslationReferences(stack) {
|
|
|
5512
6124
|
);
|
|
5513
6125
|
continue;
|
|
5514
6126
|
}
|
|
5515
|
-
if (!
|
|
6127
|
+
if (!isRec13(rawField)) continue;
|
|
5516
6128
|
checkOptionKeys(findings, {
|
|
5517
6129
|
optionMap: rawField.options,
|
|
5518
6130
|
field,
|
|
@@ -5594,7 +6206,7 @@ function validateTranslationReferences(stack) {
|
|
|
5594
6206
|
);
|
|
5595
6207
|
continue;
|
|
5596
6208
|
}
|
|
5597
|
-
if (!
|
|
6209
|
+
if (!isRec13(rawApp)) continue;
|
|
5598
6210
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
5599
6211
|
if (navIds.has(navId)) continue;
|
|
5600
6212
|
orphan(
|
|
@@ -5617,7 +6229,7 @@ function validateTranslationReferences(stack) {
|
|
|
5617
6229
|
);
|
|
5618
6230
|
continue;
|
|
5619
6231
|
}
|
|
5620
|
-
if (!
|
|
6232
|
+
if (!isRec13(rawDash)) continue;
|
|
5621
6233
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
5622
6234
|
if (dash.widgets.has(widgetId)) continue;
|
|
5623
6235
|
orphan(
|
|
@@ -5642,7 +6254,7 @@ function validateTranslationReferences(stack) {
|
|
|
5642
6254
|
return findings;
|
|
5643
6255
|
}
|
|
5644
6256
|
function asRecord(v) {
|
|
5645
|
-
return
|
|
6257
|
+
return isRec13(v) ? v : {};
|
|
5646
6258
|
}
|
|
5647
6259
|
function checkOptionKeys(findings, ctx) {
|
|
5648
6260
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -5654,7 +6266,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5654
6266
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
5655
6267
|
where: ctx.where,
|
|
5656
6268
|
path: ctx.path,
|
|
5657
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
6269
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName13(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
5658
6270
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
5659
6271
|
});
|
|
5660
6272
|
return;
|
|
@@ -5673,11 +6285,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5673
6285
|
}
|
|
5674
6286
|
}
|
|
5675
6287
|
function checkActionParams(findings, ctx) {
|
|
5676
|
-
const rawParams = Object.keys(asRecord(
|
|
6288
|
+
const rawParams = Object.keys(asRecord(isRec13(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
5677
6289
|
if (rawParams.length === 0) return;
|
|
5678
6290
|
const declared = /* @__PURE__ */ new Set();
|
|
5679
|
-
for (const param of
|
|
5680
|
-
const name =
|
|
6291
|
+
for (const param of asArray35(ctx.action.params)) {
|
|
6292
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
5681
6293
|
if (name) declared.add(name);
|
|
5682
6294
|
}
|
|
5683
6295
|
for (const paramName of rawParams) {
|
|
@@ -5693,16 +6305,161 @@ function checkActionParams(findings, ctx) {
|
|
|
5693
6305
|
}
|
|
5694
6306
|
}
|
|
5695
6307
|
|
|
6308
|
+
// src/validate-translatable-sections.ts
|
|
6309
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
6310
|
+
function isRec14(v) {
|
|
6311
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6312
|
+
}
|
|
6313
|
+
function strName14(v) {
|
|
6314
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6315
|
+
}
|
|
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
|
+
function viewLabel(view) {
|
|
6333
|
+
const name = strName14(view.name);
|
|
6334
|
+
return name ? `view "${name}"` : "";
|
|
6335
|
+
}
|
|
6336
|
+
function joinWhere(...parts) {
|
|
6337
|
+
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
6338
|
+
}
|
|
6339
|
+
function collectViewSites(view, basePath, label2, sites) {
|
|
6340
|
+
const recordObject = viewObjectName2(view);
|
|
6341
|
+
const listBinding = isRec14(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
|
|
6342
|
+
const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
|
|
6343
|
+
sites.push({
|
|
6344
|
+
path: `${basePath}.sections`,
|
|
6345
|
+
surface: label2,
|
|
6346
|
+
objectName: recordObject ?? listBinding,
|
|
6347
|
+
sections: view.sections
|
|
6348
|
+
});
|
|
6349
|
+
if (isRec14(view.form)) {
|
|
6350
|
+
sites.push({
|
|
6351
|
+
path: `${basePath}.form.sections`,
|
|
6352
|
+
surface: joinWhere(label2, "form"),
|
|
6353
|
+
objectName: bindingOf(view.form) ?? listBinding,
|
|
6354
|
+
sections: view.form.sections
|
|
6355
|
+
});
|
|
6356
|
+
}
|
|
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
|
+
}
|
|
6371
|
+
function translatedObjectNames(stack) {
|
|
6372
|
+
const out = /* @__PURE__ */ new Set();
|
|
6373
|
+
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6374
|
+
for (const bundle of bundles) {
|
|
6375
|
+
if (!isRec14(bundle)) continue;
|
|
6376
|
+
for (const data of Object.values(bundle)) {
|
|
6377
|
+
if (!isRec14(data) || !isRec14(data.objects)) continue;
|
|
6378
|
+
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
6379
|
+
if (isRec14(node)) out.add(objectName);
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
return out;
|
|
6384
|
+
}
|
|
6385
|
+
function suggestedName(label2) {
|
|
6386
|
+
const slug = label2.toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
6387
|
+
return slug.length > 0 ? slug : void 0;
|
|
6388
|
+
}
|
|
6389
|
+
function validateTranslatableSections(stack) {
|
|
6390
|
+
const findings = [];
|
|
6391
|
+
if (!isRec14(stack)) return findings;
|
|
6392
|
+
const translated = translatedObjectNames(stack);
|
|
6393
|
+
if (translated.size === 0) return findings;
|
|
6394
|
+
const sites = [];
|
|
6395
|
+
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
6396
|
+
const objectName = strName14(obj.name);
|
|
6397
|
+
if (!objectName) continue;
|
|
6398
|
+
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
6399
|
+
collectViewSites(
|
|
6400
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
6401
|
+
path,
|
|
6402
|
+
viewLabel(view),
|
|
6403
|
+
sites
|
|
6404
|
+
);
|
|
6405
|
+
}
|
|
6406
|
+
if (isRec14(obj.listViews)) {
|
|
6407
|
+
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6410
|
+
for (const { rec: view, path } of collectionEntries(stack.views, "views")) {
|
|
6411
|
+
collectViewSites(view, path, viewLabel(view), sites);
|
|
6412
|
+
}
|
|
6413
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6414
|
+
const pageName = strName14(page.name);
|
|
6415
|
+
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
6416
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6417
|
+
if (!walked.objectName) continue;
|
|
6418
|
+
const props = isRec14(walked.component.properties) ? walked.component.properties : void 0;
|
|
6419
|
+
if (!props) continue;
|
|
6420
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
6421
|
+
sites.push({
|
|
6422
|
+
path: `${walked.path}.properties.sections`,
|
|
6423
|
+
surface: joinWhere(pageLabel, type),
|
|
6424
|
+
objectName: walked.objectName,
|
|
6425
|
+
sections: props.sections
|
|
6426
|
+
});
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
for (const site of sites) {
|
|
6430
|
+
const objectName = site.objectName;
|
|
6431
|
+
if (!objectName || !translated.has(objectName)) continue;
|
|
6432
|
+
if (!Array.isArray(site.sections)) continue;
|
|
6433
|
+
for (let i = 0; i < site.sections.length; i++) {
|
|
6434
|
+
const section = site.sections[i];
|
|
6435
|
+
if (!isRec14(section)) continue;
|
|
6436
|
+
if (strName14(section.name)) continue;
|
|
6437
|
+
const heading = strName14(section.label) ?? strName14(section.title);
|
|
6438
|
+
if (!heading) continue;
|
|
6439
|
+
const slug = suggestedName(heading);
|
|
6440
|
+
findings.push({
|
|
6441
|
+
severity: "warning",
|
|
6442
|
+
rule: TRANSLATION_SECTION_NAME_MISSING,
|
|
6443
|
+
where: joinWhere(`object "${objectName}"`, site.surface, `section "${heading}"`),
|
|
6444
|
+
path: `${site.path}[${i}]`,
|
|
6445
|
+
message: `Section "${heading}" declares a label but no \`name\`. Headings resolve through \`objects.${objectName}._sections.<name>.label\`, so a section with no name has no key a bundle can carry \u2014 this heading can never be translated and renders in the source locale in EVERY locale. Object "${objectName}" IS translated, which is what makes the hole invisible: every neighbouring label resolves and only the heading stays behind. The i18n coverage report cannot see it either \u2014 it walks \`sections[].name\`, and a nameless section contributes nothing to walk.`,
|
|
6446
|
+
hint: `Give the section a stable \`name\` (snake_case)` + (slug ? `, e.g. \`name: '${slug}'\`` : "") + `, then translate it as \`objects.${objectName}._sections.${slug ?? "<name>"}.label\` in each locale bundle. The renderers look the heading up by name only \u2014 the name above is a suggestion to write down, never a key derived from the label, so renaming the heading later cannot break the lookup.`
|
|
6447
|
+
});
|
|
6448
|
+
}
|
|
6449
|
+
}
|
|
6450
|
+
return findings;
|
|
6451
|
+
}
|
|
6452
|
+
|
|
5696
6453
|
// src/validate-ai-surface-affinity.ts
|
|
5697
6454
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
5698
|
-
function
|
|
6455
|
+
function asArray36(v) {
|
|
5699
6456
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5700
6457
|
if (v && typeof v === "object") {
|
|
5701
6458
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5702
6459
|
}
|
|
5703
6460
|
return [];
|
|
5704
6461
|
}
|
|
5705
|
-
function
|
|
6462
|
+
function strName15(v) {
|
|
5706
6463
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5707
6464
|
}
|
|
5708
6465
|
function surfaceOf(v) {
|
|
@@ -5712,18 +6469,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5712
6469
|
const findings = [];
|
|
5713
6470
|
if (!stack || typeof stack !== "object") return findings;
|
|
5714
6471
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
5715
|
-
for (const skill of
|
|
5716
|
-
const n =
|
|
6472
|
+
for (const skill of asArray36(stack.skills)) {
|
|
6473
|
+
const n = strName15(skill.name);
|
|
5717
6474
|
if (n) skillsByName.set(n, skill);
|
|
5718
6475
|
}
|
|
5719
|
-
const agents =
|
|
6476
|
+
const agents = asArray36(stack.agents);
|
|
5720
6477
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5721
6478
|
const agent = agents[ai];
|
|
5722
|
-
const agentName =
|
|
6479
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
5723
6480
|
const agentSurface = surfaceOf(agent.surface);
|
|
5724
6481
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
5725
6482
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
5726
|
-
const ref =
|
|
6483
|
+
const ref = strName15(skillRefs[si]);
|
|
5727
6484
|
if (!ref) continue;
|
|
5728
6485
|
const skill = skillsByName.get(ref);
|
|
5729
6486
|
if (!skill) continue;
|
|
@@ -5745,14 +6502,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5745
6502
|
// src/validate-ai-tool-references.ts
|
|
5746
6503
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
5747
6504
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
5748
|
-
function
|
|
6505
|
+
function asArray37(v) {
|
|
5749
6506
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5750
6507
|
if (v && typeof v === "object") {
|
|
5751
6508
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5752
6509
|
}
|
|
5753
6510
|
return [];
|
|
5754
6511
|
}
|
|
5755
|
-
function
|
|
6512
|
+
function strName16(v) {
|
|
5756
6513
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5757
6514
|
}
|
|
5758
6515
|
function distance6(a, b) {
|
|
@@ -5793,26 +6550,26 @@ function materialisesAsTool(action) {
|
|
|
5793
6550
|
if (!ai || typeof ai !== "object") return false;
|
|
5794
6551
|
const aiRec = ai;
|
|
5795
6552
|
if (aiRec.exposed !== true) return false;
|
|
5796
|
-
if (!
|
|
5797
|
-
const type =
|
|
6553
|
+
if (!strName16(aiRec.description)) return false;
|
|
6554
|
+
const type = strName16(action.type);
|
|
5798
6555
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
5799
6556
|
if (type === "script") return Boolean(action.target || action.body);
|
|
5800
6557
|
return Boolean(action.target);
|
|
5801
6558
|
}
|
|
5802
6559
|
function collectToolUniverse(stack) {
|
|
5803
6560
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
5804
|
-
for (const tool of
|
|
5805
|
-
const n =
|
|
6561
|
+
for (const tool of asArray37(stack.tools)) {
|
|
6562
|
+
const n = strName16(tool.name);
|
|
5806
6563
|
if (n) universe.add(n);
|
|
5807
6564
|
}
|
|
5808
6565
|
const addActionFamily = (actions) => {
|
|
5809
|
-
for (const action of
|
|
5810
|
-
const n =
|
|
6566
|
+
for (const action of asArray37(actions)) {
|
|
6567
|
+
const n = strName16(action.name);
|
|
5811
6568
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
5812
6569
|
}
|
|
5813
6570
|
};
|
|
5814
6571
|
addActionFamily(stack.actions);
|
|
5815
|
-
for (const obj of
|
|
6572
|
+
for (const obj of asArray37(stack.objects)) {
|
|
5816
6573
|
addActionFamily(obj.actions);
|
|
5817
6574
|
}
|
|
5818
6575
|
return universe;
|
|
@@ -5820,13 +6577,13 @@ function collectToolUniverse(stack) {
|
|
|
5820
6577
|
function collectUnexposedActionNames(stack) {
|
|
5821
6578
|
const names = /* @__PURE__ */ new Set();
|
|
5822
6579
|
const scan = (actions) => {
|
|
5823
|
-
for (const action of
|
|
5824
|
-
const n =
|
|
6580
|
+
for (const action of asArray37(actions)) {
|
|
6581
|
+
const n = strName16(action.name);
|
|
5825
6582
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
5826
6583
|
}
|
|
5827
6584
|
};
|
|
5828
6585
|
scan(stack.actions);
|
|
5829
|
-
for (const obj of
|
|
6586
|
+
for (const obj of asArray37(stack.objects)) scan(obj.actions);
|
|
5830
6587
|
return names;
|
|
5831
6588
|
}
|
|
5832
6589
|
function validateAiToolReferences(stack) {
|
|
@@ -5844,13 +6601,13 @@ function validateAiToolReferences(stack) {
|
|
|
5844
6601
|
}
|
|
5845
6602
|
return universe.has(ref);
|
|
5846
6603
|
};
|
|
5847
|
-
const skills =
|
|
6604
|
+
const skills = asArray37(stack.skills);
|
|
5848
6605
|
for (let si = 0; si < skills.length; si++) {
|
|
5849
6606
|
const skill = skills[si];
|
|
5850
|
-
const skillName =
|
|
6607
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
5851
6608
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
5852
6609
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
5853
|
-
const ref =
|
|
6610
|
+
const ref = strName16(refs[ti]);
|
|
5854
6611
|
if (!ref || resolves(ref)) continue;
|
|
5855
6612
|
const isPattern = ref.endsWith("*");
|
|
5856
6613
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -5869,24 +6626,24 @@ function validateAiToolReferences(stack) {
|
|
|
5869
6626
|
|
|
5870
6627
|
// src/validate-ai-agent-authoring.ts
|
|
5871
6628
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
5872
|
-
function
|
|
6629
|
+
function asArray38(v) {
|
|
5873
6630
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5874
6631
|
if (v && typeof v === "object") {
|
|
5875
6632
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5876
6633
|
}
|
|
5877
6634
|
return [];
|
|
5878
6635
|
}
|
|
5879
|
-
function
|
|
6636
|
+
function strName17(v) {
|
|
5880
6637
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5881
6638
|
}
|
|
5882
6639
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
5883
6640
|
function validateAiAgentAuthoring(stack) {
|
|
5884
6641
|
const findings = [];
|
|
5885
6642
|
if (!stack || typeof stack !== "object") return findings;
|
|
5886
|
-
const agents =
|
|
6643
|
+
const agents = asArray38(stack.agents);
|
|
5887
6644
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5888
6645
|
const agent = agents[ai];
|
|
5889
|
-
const name =
|
|
6646
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
5890
6647
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
5891
6648
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
5892
6649
|
findings.push({
|
|
@@ -5902,14 +6659,14 @@ function validateAiAgentAuthoring(stack) {
|
|
|
5902
6659
|
}
|
|
5903
6660
|
|
|
5904
6661
|
// src/validate-hook-body-writes.ts
|
|
5905
|
-
import { createRequire as
|
|
6662
|
+
import { createRequire as createRequire4 } from "module";
|
|
5906
6663
|
import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
|
|
5907
6664
|
var cachedTs2 = null;
|
|
5908
6665
|
function loadTypeScript2() {
|
|
5909
6666
|
if (cachedTs2) return cachedTs2;
|
|
5910
6667
|
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5911
6668
|
try {
|
|
5912
|
-
cachedTs2 =
|
|
6669
|
+
cachedTs2 = createRequire4(anchor)("typescript");
|
|
5913
6670
|
} catch (err) {
|
|
5914
6671
|
throw new Error(
|
|
5915
6672
|
`@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.`
|
|
@@ -5990,24 +6747,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
5990
6747
|
"owner",
|
|
5991
6748
|
"record_type"
|
|
5992
6749
|
]);
|
|
5993
|
-
var
|
|
5994
|
-
function
|
|
5995
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5996
|
-
if (
|
|
6750
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6751
|
+
function asArray39(v) {
|
|
6752
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
6753
|
+
if (isRec15(v)) {
|
|
5997
6754
|
return Object.entries(v).map(([name, def]) => ({
|
|
5998
6755
|
name,
|
|
5999
|
-
...
|
|
6756
|
+
...isRec15(def) ? def : {}
|
|
6000
6757
|
}));
|
|
6001
6758
|
}
|
|
6002
6759
|
return [];
|
|
6003
6760
|
}
|
|
6004
6761
|
function indexObjectFields2(stack) {
|
|
6005
6762
|
const out = /* @__PURE__ */ new Map();
|
|
6006
|
-
for (const obj of
|
|
6763
|
+
for (const obj of asArray39(stack.objects)) {
|
|
6007
6764
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6008
6765
|
if (!name) continue;
|
|
6009
6766
|
const names = /* @__PURE__ */ new Set();
|
|
6010
|
-
for (const f of
|
|
6767
|
+
for (const f of asArray39(obj.fields)) {
|
|
6011
6768
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
6012
6769
|
}
|
|
6013
6770
|
out.set(name, names);
|
|
@@ -6137,12 +6894,12 @@ ${source}
|
|
|
6137
6894
|
}
|
|
6138
6895
|
function validateHookBodyWrites(stack) {
|
|
6139
6896
|
const findings = [];
|
|
6140
|
-
const hooks =
|
|
6897
|
+
const hooks = asArray39(stack.hooks);
|
|
6141
6898
|
if (hooks.length === 0) return findings;
|
|
6142
6899
|
let objectFields = null;
|
|
6143
6900
|
hooks.forEach((hook, hookIndex) => {
|
|
6144
6901
|
const body = hook.body;
|
|
6145
|
-
if (!
|
|
6902
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
6146
6903
|
const source = body.source;
|
|
6147
6904
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
6148
6905
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -6223,13 +6980,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
6223
6980
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
6224
6981
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
6225
6982
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
6226
|
-
var
|
|
6227
|
-
function
|
|
6228
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6229
|
-
if (
|
|
6983
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6984
|
+
function asArray40(v) {
|
|
6985
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
6986
|
+
if (isRec16(v)) {
|
|
6230
6987
|
return Object.entries(v).map(([name, def]) => ({
|
|
6231
6988
|
name,
|
|
6232
|
-
...
|
|
6989
|
+
...isRec16(def) ? def : {}
|
|
6233
6990
|
}));
|
|
6234
6991
|
}
|
|
6235
6992
|
return [];
|
|
@@ -6243,11 +7000,11 @@ function collectActionBodies(stack) {
|
|
|
6243
7000
|
const sites = [];
|
|
6244
7001
|
const seen = /* @__PURE__ */ new Set();
|
|
6245
7002
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
6246
|
-
|
|
7003
|
+
asArray40(actions).forEach((action, index) => {
|
|
6247
7004
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
6248
7005
|
if (type !== "script") return;
|
|
6249
7006
|
const body = action.body;
|
|
6250
|
-
if (!
|
|
7007
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
6251
7008
|
const source = body.source;
|
|
6252
7009
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
6253
7010
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -6258,7 +7015,7 @@ function collectActionBodies(stack) {
|
|
|
6258
7015
|
});
|
|
6259
7016
|
};
|
|
6260
7017
|
collect(stack.actions, "actions");
|
|
6261
|
-
|
|
7018
|
+
asArray40(stack.objects).forEach((obj, objIndex) => {
|
|
6262
7019
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
6263
7020
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
6264
7021
|
});
|
|
@@ -6266,7 +7023,7 @@ function collectActionBodies(stack) {
|
|
|
6266
7023
|
}
|
|
6267
7024
|
function validateActionBodyWrites(stack) {
|
|
6268
7025
|
const findings = [];
|
|
6269
|
-
if (!
|
|
7026
|
+
if (!isRec16(stack)) return findings;
|
|
6270
7027
|
const sites = collectActionBodies(stack);
|
|
6271
7028
|
if (sites.length === 0) return findings;
|
|
6272
7029
|
let objectFields = null;
|
|
@@ -6325,13 +7082,13 @@ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSu
|
|
|
6325
7082
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
6326
7083
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
6327
7084
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
6328
|
-
var
|
|
6329
|
-
function
|
|
6330
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6331
|
-
if (
|
|
7085
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7086
|
+
function asArray41(v) {
|
|
7087
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
7088
|
+
if (isRec17(v)) {
|
|
6332
7089
|
return Object.entries(v).map(([name, def]) => ({
|
|
6333
7090
|
name,
|
|
6334
|
-
...
|
|
7091
|
+
...isRec17(def) ? def : {}
|
|
6335
7092
|
}));
|
|
6336
7093
|
}
|
|
6337
7094
|
return [];
|
|
@@ -6344,8 +7101,8 @@ function readLiteralObjectName2(config) {
|
|
|
6344
7101
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
6345
7102
|
function validateFlowNodeWrites(stack) {
|
|
6346
7103
|
const findings = [];
|
|
6347
|
-
if (!
|
|
6348
|
-
const flows =
|
|
7104
|
+
if (!isRec17(stack)) return findings;
|
|
7105
|
+
const flows = asArray41(stack.flows);
|
|
6349
7106
|
if (flows.length === 0) return findings;
|
|
6350
7107
|
let objectFields = null;
|
|
6351
7108
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -6353,10 +7110,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
6353
7110
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
6354
7111
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
6355
7112
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
6356
|
-
const config =
|
|
7113
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
6357
7114
|
if (!config) return;
|
|
6358
7115
|
const fields = config.fields;
|
|
6359
|
-
if (!
|
|
7116
|
+
if (!isRec17(fields)) return;
|
|
6360
7117
|
const written = Object.keys(fields);
|
|
6361
7118
|
if (written.length === 0) return;
|
|
6362
7119
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -6403,6 +7160,14 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
6403
7160
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
6404
7161
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
6405
7162
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
7163
|
+
// The same family from the other end (#5417). Its sibling above asks "does
|
|
7164
|
+
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
7165
|
+
// section authored with a `label` and no `name` renders a heading that
|
|
7166
|
+
// `_sections` (keyed by name) can never address, so neither the orphan check
|
|
7167
|
+
// nor the coverage walk can see it. A reference that cannot be written is
|
|
7168
|
+
// still a reference question, and warning-only for the same reason its
|
|
7169
|
+
// sibling is: one heading stays in the source locale, nothing breaks.
|
|
7170
|
+
{ name: "validateTranslatableSections", run: validateTranslatableSections },
|
|
6406
7171
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
6407
7172
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
6408
7173
|
{ name: "validateAiToolReferences", run: validateAiToolReferences },
|
|
@@ -6488,7 +7253,12 @@ function validateReferenceIntegrity(stack) {
|
|
|
6488
7253
|
}
|
|
6489
7254
|
|
|
6490
7255
|
// src/lint-flow-patterns.ts
|
|
6491
|
-
|
|
7256
|
+
import {
|
|
7257
|
+
APPROVAL_NODE_TYPE as APPROVAL_NODE_TYPE2,
|
|
7258
|
+
APPROVAL_REVISE_NODE_TYPE,
|
|
7259
|
+
collectFlowGraphs as collectFlowGraphs2
|
|
7260
|
+
} from "@objectstack/spec/automation";
|
|
7261
|
+
function asArray42(v) {
|
|
6492
7262
|
if (Array.isArray(v)) return v;
|
|
6493
7263
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6494
7264
|
return [];
|
|
@@ -6511,6 +7281,7 @@ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
|
|
|
6511
7281
|
var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
|
|
6512
7282
|
var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
|
|
6513
7283
|
var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
|
|
7284
|
+
var FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED = "flow-approval-revise-target-not-service-owned";
|
|
6514
7285
|
var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
|
|
6515
7286
|
var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
|
|
6516
7287
|
var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
|
|
@@ -6518,6 +7289,7 @@ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
|
|
|
6518
7289
|
var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
|
|
6519
7290
|
var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
|
|
6520
7291
|
var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
|
|
7292
|
+
var FLOW_MULTI_WRITE_UNFILTERED = "flow-multi-write-unfiltered";
|
|
6521
7293
|
var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6522
7294
|
"decision",
|
|
6523
7295
|
"assignment",
|
|
@@ -6540,6 +7312,36 @@ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
6540
7312
|
"end"
|
|
6541
7313
|
]);
|
|
6542
7314
|
var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
|
|
7315
|
+
var RUNAS_EFFECTIVE_IDENTITY = "`runAs:'user'` (the default when none is declared)";
|
|
7316
|
+
function findDataNodeAnywhere(nodes, edges) {
|
|
7317
|
+
for (const graph of collectFlowGraphs2({
|
|
7318
|
+
nodes,
|
|
7319
|
+
edges
|
|
7320
|
+
})) {
|
|
7321
|
+
for (const node of graph.nodes) {
|
|
7322
|
+
if (DATA_NODE_TYPES.has(typeof node.type === "string" ? node.type : "")) {
|
|
7323
|
+
return { node, scope: graph.scope };
|
|
7324
|
+
}
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
return null;
|
|
7328
|
+
}
|
|
7329
|
+
var BULK_WRITE_CONSEQUENCE = /* @__PURE__ */ new Map([
|
|
7330
|
+
["delete_record", {
|
|
7331
|
+
verb: "deleted",
|
|
7332
|
+
engineCall: "driver.deleteMany",
|
|
7333
|
+
// The delete dispatch is the one that is EXTRACTED and case-set-pinned
|
|
7334
|
+
// (`engine-delete-dispatch.ts`), so it can be cited by name.
|
|
7335
|
+
dispatchNote: "the engine's delete-dispatch case-set lists `multi with no predicate at all` as a legal `multi` call"
|
|
7336
|
+
}],
|
|
7337
|
+
["update_record", {
|
|
7338
|
+
verb: "overwritten",
|
|
7339
|
+
engineCall: "driver.updateMany",
|
|
7340
|
+
// Update has no extracted dispatch module, so the branch itself is the
|
|
7341
|
+
// authority — and its refusal fires only WITHOUT the declaration.
|
|
7342
|
+
dispatchNote: "the engine takes its bulk branch on `options.multi` alone (`Update requires an ID or options.multi=true` is refused only when the declaration is absent)"
|
|
7343
|
+
}]
|
|
7344
|
+
]);
|
|
6543
7345
|
var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
|
|
6544
7346
|
var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
|
|
6545
7347
|
function isScheduleTriggered(flow, startCfg) {
|
|
@@ -6624,7 +7426,7 @@ function collectTemplateStrings(value, key, out) {
|
|
|
6624
7426
|
function edgeLabelOf(e) {
|
|
6625
7427
|
return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6626
7428
|
}
|
|
6627
|
-
function scanErrorLabelledEdges(
|
|
7429
|
+
function scanErrorLabelledEdges(at, nodes, edges, findings) {
|
|
6628
7430
|
const typeById = /* @__PURE__ */ new Map();
|
|
6629
7431
|
for (const n of nodes) {
|
|
6630
7432
|
if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
|
|
@@ -6637,14 +7439,14 @@ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
|
|
|
6637
7439
|
const src = typeof e.source === "string" ? e.source : "";
|
|
6638
7440
|
if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
|
|
6639
7441
|
findings.push({
|
|
6640
|
-
where:
|
|
7442
|
+
where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6641
7443
|
message: `edge is labelled '${String(e.label)}' but its type is '${String(e.type ?? "default")}', not 'fault' \u2014 so it is an ORDINARY out-edge. Unconditional out-edges all run in parallel, so '${String(e.target)}' executes on every SUCCESSFUL run of '${src}' and never on a failure. The error path the label describes does not exist, and the run still aborts when '${src}' fails.`,
|
|
6642
7444
|
hint: `Add \`type: 'fault'\` to this edge. Only runtime failures route \u2014 a guard refusal (a filter token that resolved to nothing, a missing required config key, an unscoped run) stays fatal by design and must be fixed in the metadata, not handled. (#3863)`,
|
|
6643
7445
|
rule: FLOW_ERROR_LABEL_NOT_FAULT
|
|
6644
7446
|
});
|
|
6645
7447
|
}
|
|
6646
7448
|
}
|
|
6647
|
-
function scanBranchRouting(
|
|
7449
|
+
function scanBranchRouting(at, nodes, edges, findings) {
|
|
6648
7450
|
const outEdgesBySource = /* @__PURE__ */ new Map();
|
|
6649
7451
|
for (const e of edges) {
|
|
6650
7452
|
if (e.type === "fault") continue;
|
|
@@ -6657,7 +7459,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6657
7459
|
for (const e of outs) {
|
|
6658
7460
|
if (e.isDefault === true && e.condition) {
|
|
6659
7461
|
findings.push({
|
|
6660
|
-
where:
|
|
7462
|
+
where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6661
7463
|
message: `edge sets \`isDefault: true\` AND a \`condition\` \u2014 contradictory. \`isDefault\` means "take this edge when NO sibling condition matched"; a condition makes it an ordinary guarded branch. The condition wins and the default marker routes nothing.`,
|
|
6662
7464
|
hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
|
|
6663
7465
|
rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
@@ -6670,7 +7472,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6670
7472
|
const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
|
|
6671
7473
|
if (defaults.length > 1) {
|
|
6672
7474
|
findings.push({
|
|
6673
|
-
where:
|
|
7475
|
+
where: `${at} \xB7 node '${src}'`,
|
|
6674
7476
|
message: `${defaults.length} out-edges are marked \`isDefault: true\` (${defaults.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 a node has at most ONE default path. All of them are traversed together when no condition matches, which is a parallel fan-out, not an "otherwise".`,
|
|
6675
7477
|
hint: `Keep \`isDefault: true\` on the single fallback edge and give the others a \`condition\` (or leave them unconditional if the fan-out really is intended). (#4414)`,
|
|
6676
7478
|
rule: FLOW_MULTIPLE_DEFAULT_EDGES
|
|
@@ -6683,7 +7485,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6683
7485
|
const cfg = node.config ?? {};
|
|
6684
7486
|
if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
|
|
6685
7487
|
findings.push({
|
|
6686
|
-
where:
|
|
7488
|
+
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
6687
7489
|
message: `\`config.condition\` is set but nothing reads it \u2014 the key is the trigger gate on a \`start\` node and is ignored on every other node type, so this predicate never gates anything. (It is still parse-validated at registration, which is why a malformed one is caught and an inert one is not.)`,
|
|
6688
7490
|
hint: nodeType === "decision" ? `Branching lives on the OUT-EDGES: give each branch its own \`condition\` and mark the fallback \`isDefault: true\`. If the edges already carry the predicate, delete this copy. (#4414)` : `Delete it, or move the predicate to the incoming edge's \`condition\` if this step was meant to be conditional. (#4414)`,
|
|
6689
7491
|
rule: FLOW_INERT_NODE_CONDITION
|
|
@@ -6703,7 +7505,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6703
7505
|
const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
|
|
6704
7506
|
if (unclaimed.length > 0) {
|
|
6705
7507
|
findings.push({
|
|
6706
|
-
where:
|
|
7508
|
+
where: `${at} \xB7 decision '${nid}'`,
|
|
6707
7509
|
message: `declares branch label(s) ${unclaimed.map((l) => `'${l}'`).join(", ")} that no out-edge carries \u2014 out-edge labels are [${[...edgeLabels].map((l) => `'${l}'`).join(", ") || "none"}]. Traversal cannot honour a label nothing claims, so it falls back to considering EVERY out-edge and the branch the decision computed is ignored.`,
|
|
6708
7510
|
hint: `Make an out-edge's \`label\` match the declared branch exactly, or drop \`config.conditions\` and branch on the edges instead (\`condition\` per branch + \`isDefault: true\` on the fallback) \u2014 one mechanism per decision, never both. (#4414)`,
|
|
6709
7511
|
rule: FLOW_BRANCH_LABEL_UNMATCHED,
|
|
@@ -6719,7 +7521,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6719
7521
|
);
|
|
6720
7522
|
if (ungated.length > 0) {
|
|
6721
7523
|
findings.push({
|
|
6722
|
-
where:
|
|
7524
|
+
where: `${at} \xB7 decision '${nid}'`,
|
|
6723
7525
|
message: `has guarded out-edge(s) alongside unconditional one(s) (${ungated.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 an unconditional out-edge is traversed on EVERY pass, in parallel with whichever guarded branch matched, so the decision does not actually exclude it. A \`label\` alone does not select a path unless the decision declares a matching \`conditions[].label\`.`,
|
|
6724
7526
|
hint: `Mark the fallback \`isDefault: true\` so it is taken only when no sibling condition matched (BPMN default flow), or give it its own \`condition\`. (#4414)`,
|
|
6725
7527
|
rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
|
|
@@ -6727,10 +7529,39 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6727
7529
|
}
|
|
6728
7530
|
}
|
|
6729
7531
|
}
|
|
6730
|
-
function
|
|
6731
|
-
|
|
7532
|
+
function filterCarriesNoCondition(filter) {
|
|
7533
|
+
if (filter === void 0 || filter === null) return true;
|
|
7534
|
+
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7535
|
+
return Object.keys(filter).length === 0;
|
|
7536
|
+
}
|
|
7537
|
+
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7538
|
+
for (const node of nodes) {
|
|
7539
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
7540
|
+
const consequence2 = BULK_WRITE_CONSEQUENCE.get(nodeType);
|
|
7541
|
+
if (!consequence2) continue;
|
|
7542
|
+
const cfg = node.config ?? {};
|
|
7543
|
+
if (cfg.multi !== true) continue;
|
|
7544
|
+
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7545
|
+
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7546
|
+
const filterState = cfg.filter === void 0 || cfg.filter === null ? "no `filter` key" : "an EMPTY `filter`";
|
|
7547
|
+
findings.push({
|
|
7548
|
+
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: {}\` plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` with no predicate. 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
|
+
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
|
+
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7552
|
+
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
7553
|
+
// not provably wrong — unlike the gating members of this family.
|
|
7554
|
+
rule: FLOW_MULTI_WRITE_UNFILTERED
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
7559
|
+
const approvals = nodes.filter((n) => n.type === APPROVAL_NODE_TYPE2);
|
|
6732
7560
|
if (approvals.length === 0) return;
|
|
6733
7561
|
const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
|
|
7562
|
+
const nodeTypeById = new Map(
|
|
7563
|
+
nodes.filter((n) => typeof n.id === "string").map((n) => [n.id, typeof n.type === "string" ? n.type : ""])
|
|
7564
|
+
);
|
|
6734
7565
|
const outEdges = /* @__PURE__ */ new Map();
|
|
6735
7566
|
for (const e of edges) {
|
|
6736
7567
|
const src = typeof e.source === "string" ? e.source : "";
|
|
@@ -6743,7 +7574,18 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6743
7574
|
if (!aid) continue;
|
|
6744
7575
|
const reviseTargets = edges.filter((e) => e.source === aid && edgeLabelOf(e) === "revise").map((e) => typeof e.target === "string" ? e.target : "").filter((t) => t && nodeIds.has(t));
|
|
6745
7576
|
if (reviseTargets.length === 0) continue;
|
|
6746
|
-
const where =
|
|
7577
|
+
const where = `${at} \xB7 approval '${aid}'`;
|
|
7578
|
+
for (const target of reviseTargets) {
|
|
7579
|
+
const targetType = nodeTypeById.get(target) ?? "";
|
|
7580
|
+
if (targetType === APPROVAL_REVISE_NODE_TYPE) continue;
|
|
7581
|
+
findings.push({
|
|
7582
|
+
where,
|
|
7583
|
+
severity: "error",
|
|
7584
|
+
message: `has a 'revise' out-edge into node '${target}' of type '${targetType || "(untyped)"}' \u2014 the revise window must be an '${APPROVAL_REVISE_NODE_TYPE}' node. Send-back parks the run there while the record is unlocked, and only the approvals service may continue it (submitter-only, audited, and refusing a colliding pending request); \`sendBack\` refuses any other target, so this flow's revise branch cannot run.`,
|
|
7585
|
+
hint: `Set node '${target}' to \`type: '${APPROVAL_REVISE_NODE_TYPE}'\` (drop any \`waitEventConfig\` \u2014 the window is ended by POST /api/v1/approvals/requests/:id/resubmit, not by a signal). ADR-0044 D3 originally said 'wait' here; its 2026-07-28 amendment reversed that, because a 'wait' is resumable by anyone with the run id (#3823, #3801).`,
|
|
7586
|
+
rule: FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED
|
|
7587
|
+
});
|
|
7588
|
+
}
|
|
6747
7589
|
const cfg = a.config ?? {};
|
|
6748
7590
|
if (cfg.maxRevisions === 0) {
|
|
6749
7591
|
findings.push({
|
|
@@ -6771,7 +7613,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6771
7613
|
findings.push({
|
|
6772
7614
|
where,
|
|
6773
7615
|
message: `has a 'revise' out-edge but no path loops back to it \u2014 the submitter reworks the record with nowhere to resubmit, so the revise branch dead-ends. (registerFlow accepts this \u2014 it's a valid DAG.)`,
|
|
6774
|
-
hint: `Close the loop: the 'revise' edge should reach
|
|
7616
|
+
hint: `Close the loop: the 'revise' edge should reach an '${APPROVAL_REVISE_NODE_TYPE}' node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
|
|
6775
7617
|
rule: FLOW_APPROVAL_REVISE_DEAD_END
|
|
6776
7618
|
});
|
|
6777
7619
|
} else if (!returnEdges.some((e) => e.type === "back")) {
|
|
@@ -6786,7 +7628,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6786
7628
|
}
|
|
6787
7629
|
function lintFlowPatterns(stack) {
|
|
6788
7630
|
const findings = [];
|
|
6789
|
-
for (const flow of
|
|
7631
|
+
for (const flow of asArray42(stack.flows)) {
|
|
6790
7632
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
6791
7633
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
6792
7634
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -6807,74 +7649,90 @@ function lintFlowPatterns(stack) {
|
|
|
6807
7649
|
const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
|
|
6808
7650
|
const userLessKind = userLessTriggerKind(flow, startCfg);
|
|
6809
7651
|
if (userLessKind && runAs !== "system") {
|
|
6810
|
-
const dataNode = nodes
|
|
7652
|
+
const dataNode = findDataNodeAnywhere(nodes, edges);
|
|
6811
7653
|
if (dataNode) {
|
|
6812
|
-
const
|
|
7654
|
+
const at = dataNode.scope ? `, in ${dataNode.scope},` : "";
|
|
6813
7655
|
findings.push({
|
|
6814
7656
|
where: `flow '${flowName}' \xB7 runAs`,
|
|
6815
|
-
message: `${userLessKind}-triggered flow runs
|
|
7657
|
+
message: `${userLessKind}-triggered flow runs under ${RUNAS_EFFECTIVE_IDENTITY}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.node.id}' (${dataNode.node.type})${at} has no identity to scope to and will be REFUSED at run time.`,
|
|
6816
7658
|
hint: `Declare \`runAs:'system'\` to make the elevation explicit and intended (the run reads/writes every record). A ${userLessKind} flow cannot scope to a user \u2014 there is none. (ADR-0049, ADR-0073 D5, #1888, #3760)`,
|
|
6817
7659
|
rule: FLOW_RUNAS_UNSCOPED,
|
|
6818
7660
|
severity: "error"
|
|
6819
7661
|
});
|
|
6820
7662
|
}
|
|
6821
7663
|
}
|
|
6822
|
-
for (const
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
const
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
if (
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
7664
|
+
for (const graph of collectFlowGraphs2({
|
|
7665
|
+
// A cast, not a parse. `FlowNodeSchema.config` is an open `z.record`, so a
|
|
7666
|
+
// region's contents arrive as raw authored records even in a parsed stack —
|
|
7667
|
+
// a nested edge `condition` may still be a bare string where a top-level
|
|
7668
|
+
// one is an Expression envelope. Every rule below reads both
|
|
7669
|
+
// (`conditionSource`), and the walk itself only touches `type` / `config`.
|
|
7670
|
+
// The already-guarded arrays are passed rather than `flow` itself so a
|
|
7671
|
+
// non-array `nodes` still cannot throw: this function promises it never does.
|
|
7672
|
+
nodes,
|
|
7673
|
+
edges
|
|
7674
|
+
})) {
|
|
7675
|
+
const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
|
|
7676
|
+
const graphNodes = graph.nodes;
|
|
7677
|
+
const graphEdges = graph.edges;
|
|
7678
|
+
for (const node of graphNodes) {
|
|
7679
|
+
const nodeWhere = `${at} \xB7 node '${node.id}' (${node.type})`;
|
|
7680
|
+
const cfg = node.config ?? {};
|
|
7681
|
+
if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
|
|
7682
|
+
for (const key of Object.keys(cfg)) {
|
|
7683
|
+
if (PHANTOM_AGG_KEYS.has(key)) {
|
|
7684
|
+
findings.push({
|
|
7685
|
+
where: nodeWhere,
|
|
7686
|
+
message: `node config has \`${key}\` \u2014 the automation engine has no aggregate node, so \`${key}\` is silently ignored and this node computes nothing at runtime.`,
|
|
7687
|
+
hint: `Aggregation belongs in the data layer: use \`Field.summary\` for a cross-object rollup (sum/count of children), or \`Field.formula\` for a per-record computed value. (#1870)`,
|
|
7688
|
+
rule: FLOW_PHANTOM_AGGREGATION
|
|
7689
|
+
});
|
|
7690
|
+
}
|
|
6846
7691
|
}
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
7692
|
+
const strings = [];
|
|
7693
|
+
collectTemplateStrings(stripRegions(node.config), void 0, strings);
|
|
7694
|
+
for (const str4 of strings) {
|
|
7695
|
+
if (DOUBLE_BRACE.test(str4)) {
|
|
7696
|
+
findings.push({
|
|
7697
|
+
where: nodeWhere,
|
|
7698
|
+
message: `double-brace interpolation \`${str4.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
|
|
7699
|
+
hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
|
|
7700
|
+
rule: FLOW_DOUBLE_BRACE_INTERP
|
|
7701
|
+
});
|
|
7702
|
+
}
|
|
7703
|
+
if (BARE_DOLLAR_REF.test(str4)) {
|
|
7704
|
+
findings.push({
|
|
7705
|
+
where: nodeWhere,
|
|
7706
|
+
message: `\`${str4.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
|
|
7707
|
+
hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
|
|
7708
|
+
rule: FLOW_BARE_DOLLAR_REF
|
|
7709
|
+
});
|
|
7710
|
+
}
|
|
6854
7711
|
}
|
|
6855
7712
|
}
|
|
7713
|
+
scanApprovalReviseLoops(at, graphNodes, graphEdges, findings);
|
|
7714
|
+
scanErrorLabelledEdges(at, graphNodes, graphEdges, findings);
|
|
7715
|
+
scanBranchRouting(at, graphNodes, graphEdges, findings);
|
|
7716
|
+
scanUnboundedBulkWrites(at, graphNodes, findings);
|
|
6856
7717
|
}
|
|
6857
|
-
scanApprovalReviseLoops(flowName, nodes, edges, findings);
|
|
6858
|
-
scanErrorLabelledEdges(flowName, nodes, edges, findings);
|
|
6859
|
-
scanBranchRouting(flowName, nodes, edges, findings);
|
|
6860
7718
|
}
|
|
6861
7719
|
return findings;
|
|
6862
7720
|
}
|
|
6863
7721
|
|
|
6864
7722
|
// src/lint-liveness-properties.ts
|
|
6865
|
-
import { createRequire as
|
|
7723
|
+
import { createRequire as createRequire5 } from "module";
|
|
6866
7724
|
import { dirname, join } from "path";
|
|
6867
7725
|
import { existsSync, readFileSync } from "fs";
|
|
6868
7726
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
6869
7727
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
6870
|
-
function
|
|
7728
|
+
function asArray43(v) {
|
|
6871
7729
|
if (Array.isArray(v)) return v;
|
|
6872
7730
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6873
7731
|
return [];
|
|
6874
7732
|
}
|
|
6875
7733
|
function resolveLivenessDir() {
|
|
6876
7734
|
try {
|
|
6877
|
-
const require2 =
|
|
7735
|
+
const require2 = createRequire5(import.meta.url);
|
|
6878
7736
|
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
6879
7737
|
const dir = join(dirname(pkgJson), "liveness");
|
|
6880
7738
|
return existsSync(dir) ? dir : null;
|
|
@@ -6980,7 +7838,16 @@ var TYPE_COLLECTIONS = [
|
|
|
6980
7838
|
{ type: "job", key: "jobs" },
|
|
6981
7839
|
{ type: "email_template", key: "emailTemplates" },
|
|
6982
7840
|
{ type: "mapping", key: "mappings" },
|
|
6983
|
-
{ type: "translation", key: "translations" }
|
|
7841
|
+
{ type: "translation", key: "translations" },
|
|
7842
|
+
// #4956 — dashboard joins the list the moment its ledger first warns on
|
|
7843
|
+
// anything, which is exactly the rule the comment above states. Drilling
|
|
7844
|
+
// `widgets` produced five warned keys (`colorVariant`, `actionUrl`,
|
|
7845
|
+
// `actionType`, `actionIcon`, `aria`), all under `widgets[]`; `getNested`
|
|
7846
|
+
// fans a dotted path out over an array level, so `widgets.colorVariant`
|
|
7847
|
+
// checks every widget on the dashboard. Registering it here is not optional
|
|
7848
|
+
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7849
|
+
// silent, which is the shape this lint exists to prevent.
|
|
7850
|
+
{ type: "dashboard", key: "dashboards" }
|
|
6984
7851
|
];
|
|
6985
7852
|
function lintLivenessProperties(stack) {
|
|
6986
7853
|
const dir = resolveLivenessDir();
|
|
@@ -6988,11 +7855,11 @@ function lintLivenessProperties(stack) {
|
|
|
6988
7855
|
const findings = [];
|
|
6989
7856
|
const objectWarn = loadWarnMap(dir, "object");
|
|
6990
7857
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
6991
|
-
for (const obj of
|
|
7858
|
+
for (const obj of asArray43(stack.objects)) {
|
|
6992
7859
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6993
7860
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
6994
7861
|
if (fieldWarn.size > 0) {
|
|
6995
|
-
for (const field of
|
|
7862
|
+
for (const field of asArray43(obj.fields)) {
|
|
6996
7863
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
6997
7864
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
6998
7865
|
}
|
|
@@ -7001,7 +7868,7 @@ function lintLivenessProperties(stack) {
|
|
|
7001
7868
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7002
7869
|
const warnMap = loadWarnMap(dir, type);
|
|
7003
7870
|
if (warnMap.size === 0) continue;
|
|
7004
|
-
for (const item of
|
|
7871
|
+
for (const item of asArray43(stack[key])) {
|
|
7005
7872
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7006
7873
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7007
7874
|
}
|
|
@@ -7015,7 +7882,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
|
7015
7882
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7016
7883
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7017
7884
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7018
|
-
function
|
|
7885
|
+
function asArray44(v) {
|
|
7019
7886
|
if (Array.isArray(v)) return v;
|
|
7020
7887
|
if (v && typeof v === "object") {
|
|
7021
7888
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7024,9 +7891,9 @@ function asArray40(v) {
|
|
|
7024
7891
|
}
|
|
7025
7892
|
function lintAutonumberFormats(stack) {
|
|
7026
7893
|
const findings = [];
|
|
7027
|
-
for (const obj of
|
|
7894
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7028
7895
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7029
|
-
const fields =
|
|
7896
|
+
const fields = asArray44(obj.fields);
|
|
7030
7897
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7031
7898
|
for (const f of fields) {
|
|
7032
7899
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -7092,7 +7959,7 @@ function lintAutonumberFormats(stack) {
|
|
|
7092
7959
|
|
|
7093
7960
|
// src/lint-view-refs.ts
|
|
7094
7961
|
import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
|
|
7095
|
-
function
|
|
7962
|
+
function asArray45(v) {
|
|
7096
7963
|
if (Array.isArray(v)) return v;
|
|
7097
7964
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7098
7965
|
return [];
|
|
@@ -7120,7 +7987,7 @@ function lintViewRefs(stack) {
|
|
|
7120
7987
|
s.add(kind);
|
|
7121
7988
|
};
|
|
7122
7989
|
const containers = [];
|
|
7123
|
-
for (const v of
|
|
7990
|
+
for (const v of asArray45(stack.views)) {
|
|
7124
7991
|
if (v.viewKind) {
|
|
7125
7992
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7126
7993
|
continue;
|
|
@@ -7129,7 +7996,7 @@ function lintViewRefs(stack) {
|
|
|
7129
7996
|
const object = viewContainerObjectName(v);
|
|
7130
7997
|
if (object) containers.push({ object, container: v });
|
|
7131
7998
|
}
|
|
7132
|
-
for (const obj of
|
|
7999
|
+
for (const obj of asArray45(stack.objects)) {
|
|
7133
8000
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7134
8001
|
if (!object) continue;
|
|
7135
8002
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -7183,11 +8050,11 @@ function lintViewRefs(stack) {
|
|
|
7183
8050
|
});
|
|
7184
8051
|
}
|
|
7185
8052
|
};
|
|
7186
|
-
for (const obj of
|
|
8053
|
+
for (const obj of asArray45(stack.objects)) {
|
|
7187
8054
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7188
|
-
for (const action of
|
|
8055
|
+
for (const action of asArray45(obj.actions)) checkAction(action, object);
|
|
7189
8056
|
}
|
|
7190
|
-
for (const action of
|
|
8057
|
+
for (const action of asArray45(stack.actions)) checkAction(action);
|
|
7191
8058
|
return findings;
|
|
7192
8059
|
}
|
|
7193
8060
|
|
|
@@ -7239,8 +8106,43 @@ function refOf2(def) {
|
|
|
7239
8106
|
return def?.reference || def?.reference_to;
|
|
7240
8107
|
}
|
|
7241
8108
|
var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
|
|
8109
|
+
var UNIQUE_UNSCOPED_DECLARED_INDEX = "unique/unscoped-declared-index";
|
|
8110
|
+
var UNIQUE_LEGACY_ORGANIZATION_COMPOSITE = "unique/legacy-organization-composite";
|
|
8111
|
+
function authoredTenantColumn(obj) {
|
|
8112
|
+
const declared = obj?.tenancy?.tenantField;
|
|
8113
|
+
return typeof declared === "string" && declared.trim() ? declared.trim() : "organization_id";
|
|
8114
|
+
}
|
|
7242
8115
|
function uniqueDeclared(u) {
|
|
7243
|
-
return u === true || u === "global";
|
|
8116
|
+
return u === true || u === "global" || u === "organization";
|
|
8117
|
+
}
|
|
8118
|
+
function fieldUniqueScope(u) {
|
|
8119
|
+
return u === "global" ? "global" : "organization";
|
|
8120
|
+
}
|
|
8121
|
+
function indexUniqueScope(u) {
|
|
8122
|
+
return u === "organization" ? "organization" : "global";
|
|
8123
|
+
}
|
|
8124
|
+
function lintUnscopedDeclaredIndexes(objects) {
|
|
8125
|
+
const issues = [];
|
|
8126
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
8127
|
+
for (let i = 0; i < objects.length; i++) {
|
|
8128
|
+
const obj = objects[i];
|
|
8129
|
+
if (!obj?.name) continue;
|
|
8130
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
8131
|
+
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
8132
|
+
const idx = declaredIndexes[j];
|
|
8133
|
+
if (idx?.unique !== true) continue;
|
|
8134
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
|
|
8135
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8136
|
+
issues.push({
|
|
8137
|
+
severity: "warning",
|
|
8138
|
+
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
8139
|
+
message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
|
|
8140
|
+
path: `objects[${i}].indexes[${j}]`,
|
|
8141
|
+
fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
|
|
8142
|
+
});
|
|
8143
|
+
}
|
|
8144
|
+
}
|
|
8145
|
+
return issues;
|
|
7244
8146
|
}
|
|
7245
8147
|
function lintUniqueDeclarations(objects) {
|
|
7246
8148
|
const issues = [];
|
|
@@ -7260,23 +8162,70 @@ function lintUniqueDeclarations(objects) {
|
|
|
7260
8162
|
if (singleColumnUniqueIndexes.size === 0) continue;
|
|
7261
8163
|
for (const { name, def } of fieldEntries2(obj.fields)) {
|
|
7262
8164
|
if (!uniqueDeclared(def?.unique)) continue;
|
|
7263
|
-
if (def.unique === "global") continue;
|
|
7264
8165
|
const idx = singleColumnUniqueIndexes.get(name);
|
|
7265
8166
|
if (!idx) continue;
|
|
8167
|
+
const fScope = fieldUniqueScope(def.unique);
|
|
8168
|
+
const iScope = indexUniqueScope(idx.unique);
|
|
7266
8169
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8170
|
+
const fieldSpelling = `\`unique: ${typeof def.unique === "string" ? `'${def.unique}'` : def.unique}\``;
|
|
8171
|
+
const indexSpelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
|
|
8172
|
+
let message;
|
|
8173
|
+
let fix;
|
|
8174
|
+
if (fScope === iScope) {
|
|
8175
|
+
const boundary = fScope === "global" ? "installation-wide" : "per-organization";
|
|
8176
|
+
message = `"${obj.name}.${name}" declares field-level ${fieldSpelling} AND a single-column unique index${indexLabel} (${indexSpelling}) on the same column. Both ask for the same ${boundary} boundary \u2014 the same unique index declared twice (ADR-0120 D5b). Redundant, not contradictory: drop one so the intent has a single home.`;
|
|
8177
|
+
fix = fScope === "global" ? `Keep ONE spelling of installation-wide uniqueness: \`unique: 'global'\` on '${name}', or the declared index \u2014 not both.` : `Keep ONE spelling of per-organization uniqueness: \`unique: 'organization'\` on '${name}' (preferred), or the declared \`'organization'\` index \u2014 not both.`;
|
|
8178
|
+
} else {
|
|
8179
|
+
const globalSide = fScope === "global" ? `field-level ${fieldSpelling}` : `declared index${indexLabel} (${indexSpelling})`;
|
|
8180
|
+
const orgSide = fScope === "global" ? `declared index${indexLabel} (${indexSpelling})` : `field-level ${fieldSpelling}`;
|
|
8181
|
+
message = `"${obj.name}.${name}" declares an installation-wide unique (${globalSide}) AND a per-organization unique (${orgSide}) on the same column \u2014 the two scopes CONTRADICT (ADR-0120 D5b). The installation-wide index is physically stricter and wins; the per-organization constraint can never be tripped, so one of the two intents you wrote is silently dead.`;
|
|
8182
|
+
fix = `Pick ONE scope and say it once: for installation-wide uniqueness keep \`unique: 'global'\` and drop the per-organization declaration; for per-organization uniqueness set \`unique: 'organization'\` (field-level on '${name}', or on the declared index) and drop the installation-wide one.`;
|
|
8183
|
+
}
|
|
7267
8184
|
issues.push({
|
|
7268
8185
|
severity: "warning",
|
|
7269
8186
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
7270
|
-
message
|
|
8187
|
+
message,
|
|
7271
8188
|
path: `objects[${i}]`,
|
|
7272
|
-
fix
|
|
8189
|
+
fix
|
|
8190
|
+
});
|
|
8191
|
+
}
|
|
8192
|
+
}
|
|
8193
|
+
return issues;
|
|
8194
|
+
}
|
|
8195
|
+
function lintLegacyOrganizationComposites(objects) {
|
|
8196
|
+
const issues = [];
|
|
8197
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
8198
|
+
for (let i = 0; i < objects.length; i++) {
|
|
8199
|
+
const obj = objects[i];
|
|
8200
|
+
if (!obj?.name) continue;
|
|
8201
|
+
const tenantColumn = authoredTenantColumn(obj);
|
|
8202
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
8203
|
+
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
8204
|
+
const idx = declaredIndexes[j];
|
|
8205
|
+
if (!uniqueDeclared(idx?.unique) || idx.unique === "organization") continue;
|
|
8206
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
8207
|
+
if (cols.length < 2) continue;
|
|
8208
|
+
if (!cols.includes(tenantColumn)) continue;
|
|
8209
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8210
|
+
const spelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
|
|
8211
|
+
const rest = cols.filter((c) => c !== tenantColumn);
|
|
8212
|
+
issues.push({
|
|
8213
|
+
severity: "warning",
|
|
8214
|
+
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
8215
|
+
message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
|
|
8216
|
+
path: `objects[${i}].indexes[${j}]`,
|
|
8217
|
+
fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
|
|
7273
8218
|
});
|
|
7274
8219
|
}
|
|
7275
8220
|
}
|
|
7276
8221
|
return issues;
|
|
7277
8222
|
}
|
|
7278
8223
|
function lintDataModel(objects) {
|
|
7279
|
-
const issues =
|
|
8224
|
+
const issues = [
|
|
8225
|
+
...lintUnscopedDeclaredIndexes(objects),
|
|
8226
|
+
...lintUniqueDeclarations(objects),
|
|
8227
|
+
...lintLegacyOrganizationComposites(objects)
|
|
8228
|
+
];
|
|
7280
8229
|
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7281
8230
|
const childrenByParent = {};
|
|
7282
8231
|
for (const child of objects) {
|
|
@@ -7543,6 +8492,42 @@ var AUTHORING_RULES = [
|
|
|
7543
8492
|
runtimeTypes: ["flow"],
|
|
7544
8493
|
run: (stack) => validateReferenceIntegrity(stack)
|
|
7545
8494
|
},
|
|
8495
|
+
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
8496
|
+
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
8497
|
+
// recurse into it, so until this entry existed the 31 typed prop schemas in
|
|
8498
|
+
// `ComponentPropsMap` were parsed by NOTHING (#4001 批 17's `no gate`
|
|
8499
|
+
// verdict): an undeclared or wrongly-typed prop parsed clean, was retained,
|
|
8500
|
+
// and reached objectui's renderer to be ignored there. This dispatches on
|
|
8501
|
+
// `type` and judges the bag; unregistered types are skipped, which is a
|
|
8502
|
+
// required semantic (`type` is an open union — the example corpus authors 87
|
|
8503
|
+
// nodes of 10 types this map does not carry).
|
|
8504
|
+
//
|
|
8505
|
+
// `normalized` for a reason worth stating, since the props bag survives the
|
|
8506
|
+
// Zod parse UNCHANGED and both tiers would otherwise carry the same data: the
|
|
8507
|
+
// ADR-0087 conversion layer runs inside `normalizeStackInput`, so a converted
|
|
8508
|
+
// alias (`page-header-subtitle-alias` rewrites `properties.description` →
|
|
8509
|
+
// `subtitle`) is already canonical here and is never reported as undeclared —
|
|
8510
|
+
// while a schema error elsewhere in the stack cannot take these findings down
|
|
8511
|
+
// with it.
|
|
8512
|
+
//
|
|
8513
|
+
// Advisory, deliberately, and this is the whole shape of #5068's first step:
|
|
8514
|
+
// wiring the parse is the precondition for enforcement, not the enforcement
|
|
8515
|
+
// (#5020, one surface over). The live corpus violates the declarations in two
|
|
8516
|
+
// places that are open contract questions — inline i18n label maps on three
|
|
8517
|
+
// published platform pages (#5728) and the record picker's declared-but-unread
|
|
8518
|
+
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8519
|
+
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8520
|
+
// separate step, once the warning-period inventory is empty.
|
|
8521
|
+
{
|
|
8522
|
+
name: "validateComponentProps",
|
|
8523
|
+
tier: "advisory",
|
|
8524
|
+
input: "normalized",
|
|
8525
|
+
commands: ALL,
|
|
8526
|
+
source: "packages/lint/src/validate-component-props.ts",
|
|
8527
|
+
surfaces: CLI_ONLY,
|
|
8528
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8529
|
+
run: (stack) => validateComponentProps(stack)
|
|
8530
|
+
},
|
|
7546
8531
|
// ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
|
|
7547
8532
|
// `id`), name real CSS properties + design tokens, and carry a `large` base.
|
|
7548
8533
|
{
|
|
@@ -7606,18 +8591,32 @@ var AUTHORING_RULES = [
|
|
|
7606
8591
|
surfaceReason: 'P2 (#4463): the ONE rule the runtime universe makes strictly stronger \u2014 the advisory hedge ("another installed package may provide it") is decidable against the live capability registry, so it graduates from advisory to gating there rather than merely being ported. That promotion is a severity change on a published rule id and belongs in its own PR, not riding a wiring change.',
|
|
7607
8592
|
run: (stack) => validateCapabilityReferences(stack)
|
|
7608
8593
|
},
|
|
7609
|
-
// A
|
|
7610
|
-
//
|
|
7611
|
-
//
|
|
8594
|
+
// A flow that LOOKS armed and never launches — silently. Reads the pre-parse
|
|
8595
|
+
// tier so an author sees what they wrote.
|
|
8596
|
+
//
|
|
8597
|
+
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8598
|
+
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8599
|
+
// dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
|
|
8600
|
+
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8601
|
+
// predicate cannot route at all, and a `record-*` triggerType outside the
|
|
8602
|
+
// closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
|
|
8603
|
+
// can be changed by installing a package, so there is no reading under which
|
|
8604
|
+
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8605
|
+
// (the object may come from another installed package — a hedge this rule
|
|
8606
|
+
// cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
|
|
8607
|
+
// that one is ambiguity of intent, not a dead flow).
|
|
7612
8608
|
{
|
|
7613
8609
|
name: "validateFlowTriggerReadiness",
|
|
7614
|
-
tier: "
|
|
8610
|
+
tier: "gating",
|
|
7615
8611
|
input: "normalized",
|
|
7616
8612
|
commands: ALL,
|
|
7617
8613
|
source: "packages/lint/src/validate-flow-trigger-readiness.ts",
|
|
7618
|
-
// Runtime publish gate (#4463): the FLOW family.
|
|
7619
|
-
//
|
|
7620
|
-
//
|
|
8614
|
+
// Runtime publish gate (#4463): the FLOW family. Its `error` findings now
|
|
8615
|
+
// REFUSE a `state: 'active'` write (P1 gates on `error` only); the rules that
|
|
8616
|
+
// stayed `warning` keep being logged as advisories. The gate judges a
|
|
8617
|
+
// snapshot whose `flows` holds only the written item and subtracts the
|
|
8618
|
+
// baseline's findings, so this refuses the dead flow's own publish — never
|
|
8619
|
+
// another flow's save on account of a stored one.
|
|
7621
8620
|
surfaces: CLI_AND_RUNTIME,
|
|
7622
8621
|
runtimeTypes: ["flow"],
|
|
7623
8622
|
run: (stack) => validateFlowTriggerReadiness(stack)
|
|
@@ -7828,9 +8827,31 @@ var AUTHORING_RULES = [
|
|
|
7828
8827
|
hint: f.hint
|
|
7829
8828
|
}))
|
|
7830
8829
|
},
|
|
7831
|
-
//
|
|
7832
|
-
//
|
|
7833
|
-
//
|
|
8830
|
+
// ADR-0120 D5a — a declared index with bare `unique: true` states no scope
|
|
8831
|
+
// at all (`unique/unscoped-declared-index` — the #4986 trap). Fires on the
|
|
8832
|
+
// spelling alone, no tenancy inference; 17.x warns, protocol 18 rejects the
|
|
8833
|
+
// spelling (#5082).
|
|
8834
|
+
{
|
|
8835
|
+
name: "lintUnscopedDeclaredIndexes",
|
|
8836
|
+
tier: "advisory",
|
|
8837
|
+
input: "parsed",
|
|
8838
|
+
commands: ["validate", "build"],
|
|
8839
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
8840
|
+
surfaces: CLI_ONLY,
|
|
8841
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8842
|
+
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
8843
|
+
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
8844
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
8845
|
+
rule: f.rule,
|
|
8846
|
+
where: f.path,
|
|
8847
|
+
path: f.path,
|
|
8848
|
+
message: f.message,
|
|
8849
|
+
hint: f.fix ?? ""
|
|
8850
|
+
}))
|
|
8851
|
+
},
|
|
8852
|
+
// #3991 / ADR-0120 D5b — a column carrying BOTH a field-level `unique` and a
|
|
8853
|
+
// single-column declared unique index states two scopes of which at most one
|
|
8854
|
+
// takes effect (`unique/double-declaration`, the four-quadrant matrix).
|
|
7834
8855
|
{
|
|
7835
8856
|
name: "lintUniqueDeclarations",
|
|
7836
8857
|
tier: "advisory",
|
|
@@ -7849,6 +8870,28 @@ var AUTHORING_RULES = [
|
|
|
7849
8870
|
hint: f.fix ?? ""
|
|
7850
8871
|
}))
|
|
7851
8872
|
},
|
|
8873
|
+
// ADR-0120 D5c — a declared unique listing the organization column IS the
|
|
8874
|
+
// hand-written per-organization composite (S6). Advisory nudge toward the
|
|
8875
|
+
// `'organization'` respelling, which is also what closes its NULL hole
|
|
8876
|
+
// (#5030). Never auto-fixed: opting in is a real D4 tightening.
|
|
8877
|
+
{
|
|
8878
|
+
name: "lintLegacyOrganizationComposites",
|
|
8879
|
+
tier: "advisory",
|
|
8880
|
+
input: "parsed",
|
|
8881
|
+
commands: ["validate", "build"],
|
|
8882
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
8883
|
+
surfaces: CLI_ONLY,
|
|
8884
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8885
|
+
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
8886
|
+
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
8887
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
8888
|
+
rule: f.rule,
|
|
8889
|
+
where: f.path,
|
|
8890
|
+
path: f.path,
|
|
8891
|
+
message: f.message,
|
|
8892
|
+
hint: f.fix ?? ""
|
|
8893
|
+
}))
|
|
8894
|
+
},
|
|
7852
8895
|
// ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
|
|
7853
8896
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
7854
8897
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
@@ -7876,6 +8919,88 @@ var AUTHORING_RULES = [
|
|
|
7876
8919
|
surfaces: CLI_ONLY,
|
|
7877
8920
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7878
8921
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
8922
|
+
},
|
|
8923
|
+
// #4698 — the "declared but never read" gate, for the one surface where the
|
|
8924
|
+
// predicate is EXACT rather than inferred. A sharing rule's `condition` has a
|
|
8925
|
+
// single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
|
|
8926
|
+
// the key is `compileCelToFilter(condition, { variables: {} })`; a condition
|
|
8927
|
+
// that does not lower means the rule is SKIPPED at boot, so the grant is
|
|
8928
|
+
// declared and does not exist. The lint calls that same compiler, from the
|
|
8929
|
+
// same package, with the same options — the verdict cannot drift from the
|
|
8930
|
+
// consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
|
|
8931
|
+
// docblock states: the whole authorable surface is enforced, and this was the
|
|
8932
|
+
// one field where that sentence was not yet true.
|
|
8933
|
+
{
|
|
8934
|
+
name: "validateSharingRuleEnforceability",
|
|
8935
|
+
tier: "gating",
|
|
8936
|
+
input: "parsed",
|
|
8937
|
+
commands: ALL,
|
|
8938
|
+
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
8939
|
+
surfaces: CLI_ONLY,
|
|
8940
|
+
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
8941
|
+
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
8942
|
+
},
|
|
8943
|
+
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
8944
|
+
// which had never been wired to anything: `isSupportedRlsExpression` existed
|
|
8945
|
+
// solely so an authoring command could reject a predicate the runtime drops,
|
|
8946
|
+
// and no authoring command called it. An unlowerable
|
|
8947
|
+
// `rowLevelSecurity[].using` is DROPPED by `RLSCompiler` and — when it is the
|
|
8948
|
+
// only applicable policy — replaced by `RLS_DENY_FILTER`, so the policy reads
|
|
8949
|
+
// as an authorization and behaves as a blanket refusal. Same construction as
|
|
8950
|
+
// the sharing-rule entry: the verdict is the runtime's own function, reached
|
|
8951
|
+
// through `@objectstack/formula` (where #4983 hoisted it), never a model of it.
|
|
8952
|
+
{
|
|
8953
|
+
name: "validateRlsPredicateEnforceability",
|
|
8954
|
+
tier: "gating",
|
|
8955
|
+
input: "parsed",
|
|
8956
|
+
commands: ALL,
|
|
8957
|
+
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
8958
|
+
surfaces: CLI_ONLY,
|
|
8959
|
+
surfaceReason: "P2 (#4463): the rule reads `stack.permissions[]`, a stack-wide collection the per-write snapshot does not carry, and P1 gates `flow` alone. It is otherwise snapshot-ready \u2014 it needs no other collection \u2014 so widening it is a `runtimeTypes: ['permission_set']` edit once the gate builds that snapshot, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
8960
|
+
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
8961
|
+
},
|
|
8962
|
+
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
8963
|
+
// STATIC artifacts an object validation rule carries. A `format` rule's
|
|
8964
|
+
// `regex` that `new RegExp(...)` throws on, and a `json_schema` rule's schema
|
|
8965
|
+
// ajv cannot compile, are both logged and SKIPPED on the write path
|
|
8966
|
+
// (`rule-validator.ts`), so the rule ships, lists, and protects nothing.
|
|
8967
|
+
// Neither needs a record to judge, so the authoring door is the right one:
|
|
8968
|
+
// rejecting a broken regex at RUNTIME instead would reject every write
|
|
8969
|
+
// touching that field for as long as the metadata is deployed (#4762's own
|
|
8970
|
+
// analysis — the runtime-backstop question stays open for the maintainer).
|
|
8971
|
+
// Gating for the `lint-flow-patterns.ts` bar: no reading of the metadata
|
|
8972
|
+
// behaves as written, because the rule does not run at all.
|
|
8973
|
+
{
|
|
8974
|
+
name: "validateRuleCompilability",
|
|
8975
|
+
tier: "gating",
|
|
8976
|
+
input: "parsed",
|
|
8977
|
+
commands: ALL,
|
|
8978
|
+
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
8979
|
+
surfaces: CLI_ONLY,
|
|
8980
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8981
|
+
run: (stack) => validateRuleCompilability(stack)
|
|
8982
|
+
},
|
|
8983
|
+
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
8984
|
+
// NOT close: under `strict: false` a MISSPELLED format name (`emial`) is
|
|
8985
|
+
// logged once and DROPPED, so the rule compiles, ships, runs on every write
|
|
8986
|
+
// and enforces nothing for the keyword its author wrote — and the record is
|
|
8987
|
+
// accepted, which is the silent direction. Deliberately its own entry rather
|
|
8988
|
+
// than a third finding inside the rule above: that one's whole contract is
|
|
8989
|
+
// compiling in the runtime's exact environment, and a typo'd format compiles
|
|
8990
|
+
// there. This judges the format NAME against the registered set (enumerated
|
|
8991
|
+
// from the same ajv instance, never a hardcoded list) and compiles nothing,
|
|
8992
|
+
// so the #4762/#5029 compile parity is untouched — a judgement beside the
|
|
8993
|
+
// compile, not a divergent compile. Gating for the `lint-flow-patterns.ts`
|
|
8994
|
+
// bar: no reading of the metadata behaves as written.
|
|
8995
|
+
{
|
|
8996
|
+
name: "validateRuleSchemaFormats",
|
|
8997
|
+
tier: "gating",
|
|
8998
|
+
input: "parsed",
|
|
8999
|
+
commands: ALL,
|
|
9000
|
+
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
9001
|
+
surfaces: CLI_ONLY,
|
|
9002
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9003
|
+
run: (stack) => validateRuleSchemaFormats(stack)
|
|
7879
9004
|
}
|
|
7880
9005
|
];
|
|
7881
9006
|
function authoringRulesFor(command) {
|
|
@@ -7985,6 +9110,7 @@ export {
|
|
|
7985
9110
|
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
|
|
7986
9111
|
APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
7987
9112
|
APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
9113
|
+
APPROVAL_APPROVER_TYPE_UNSUPPORTED,
|
|
7988
9114
|
APPROVAL_DECISION_OUTPUTS_RESERVED,
|
|
7989
9115
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
7990
9116
|
APPROVAL_EXPRESSION_INVALID,
|
|
@@ -8003,15 +9129,19 @@ export {
|
|
|
8003
9129
|
CHART_DIMENSION_UNKNOWN,
|
|
8004
9130
|
CHART_FIELD_UNKNOWN,
|
|
8005
9131
|
CHART_MEASURE_UNKNOWN,
|
|
9132
|
+
COMPONENT_PROPS_INVALID,
|
|
9133
|
+
COMPONENT_PROPS_UNKNOWN_KEY,
|
|
8006
9134
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
8007
9135
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
8008
9136
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
8009
9137
|
EXPRESSION_INVALID,
|
|
8010
9138
|
FIELD_GROUP_EMPTY,
|
|
9139
|
+
FIELD_GROUP_SHADOWED,
|
|
8011
9140
|
FIELD_GROUP_UNDECLARED,
|
|
8012
9141
|
FILTER_TOKEN_UNKNOWN,
|
|
8013
9142
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
8014
9143
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
9144
|
+
FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
|
|
8015
9145
|
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
8016
9146
|
FLOW_BARE_DOLLAR_REF,
|
|
8017
9147
|
FLOW_BRANCH_LABEL_UNMATCHED,
|
|
@@ -8023,12 +9153,16 @@ export {
|
|
|
8023
9153
|
FLOW_ERROR_LABEL_NOT_FAULT,
|
|
8024
9154
|
FLOW_INERT_NODE_CONDITION,
|
|
8025
9155
|
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
9156
|
+
FLOW_MULTI_WRITE_UNFILTERED,
|
|
8026
9157
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
8027
9158
|
FLOW_PHANTOM_AGGREGATION,
|
|
8028
9159
|
FLOW_RUNAS_UNSCOPED,
|
|
8029
9160
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
8030
9161
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
8031
9162
|
FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
9163
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
|
|
9164
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
9165
|
+
FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
8032
9166
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
8033
9167
|
FLOW_UPDATE_READONLY_FIELD,
|
|
8034
9168
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -8043,6 +9177,7 @@ export {
|
|
|
8043
9177
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
8044
9178
|
LIVENESS_DEAD_PROPERTY,
|
|
8045
9179
|
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
9180
|
+
MAX_SCHEMA_WALK_DEPTH,
|
|
8046
9181
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
8047
9182
|
NAV_OBJECT_UNGRANTED,
|
|
8048
9183
|
NAV_TARGET_UNRESOLVED,
|
|
@@ -8056,14 +9191,19 @@ export {
|
|
|
8056
9191
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
8057
9192
|
REACT_CHART_AGGREGATE_INVALID,
|
|
8058
9193
|
REACT_CHART_AXIS_UNKNOWN,
|
|
9194
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
8059
9195
|
REACT_CHART_FIELD_UNKNOWN,
|
|
8060
9196
|
REFERENCE_INTEGRITY_RULES,
|
|
9197
|
+
RLS_PREDICATE_UNENFORCEABLE,
|
|
9198
|
+
RLS_PREDICATE_UNPARSEABLE,
|
|
9199
|
+
RUNTIME_AJV_OPTIONS,
|
|
8061
9200
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
8062
9201
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
8063
9202
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
8064
9203
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
8065
9204
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
8066
9205
|
SECURITY_EXTERNAL_WIDER,
|
|
9206
|
+
SECURITY_FLS_UNQUALIFIED_KEY,
|
|
8067
9207
|
SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
8068
9208
|
SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
8069
9209
|
SECURITY_OWD_ALIAS,
|
|
@@ -8074,6 +9214,8 @@ export {
|
|
|
8074
9214
|
SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
|
|
8075
9215
|
SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
8076
9216
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
9217
|
+
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
9218
|
+
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
8077
9219
|
STYLE_CLASSNAME_TAILWIND,
|
|
8078
9220
|
STYLE_NODE_MISSING_ID,
|
|
8079
9221
|
STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -8083,8 +9225,14 @@ export {
|
|
|
8083
9225
|
TITLE_FORMAT_RETIRED,
|
|
8084
9226
|
TITLE_UNRESOLVABLE,
|
|
8085
9227
|
TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
9228
|
+
TRANSLATION_SECTION_NAME_MISSING,
|
|
8086
9229
|
TRANSLATION_TARGET_UNKNOWN,
|
|
8087
9230
|
UNIQUE_DOUBLE_DECLARATION,
|
|
9231
|
+
UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9232
|
+
UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9233
|
+
VALIDATION_RULE_REGEX_UNCOMPILABLE,
|
|
9234
|
+
VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
|
|
9235
|
+
VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
|
|
8088
9236
|
VIEW_CONTAINER_SHAPE,
|
|
8089
9237
|
VIEW_KEY_COLLISION,
|
|
8090
9238
|
VIEW_REF_FORM_TARGET_KIND,
|
|
@@ -8093,6 +9241,8 @@ export {
|
|
|
8093
9241
|
VISIBILITY_ROOT_MISLAYERED,
|
|
8094
9242
|
WIDGET_DATASET_UNKNOWN,
|
|
8095
9243
|
WIDGET_DIMENSION_UNKNOWN,
|
|
9244
|
+
WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
9245
|
+
WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
8096
9246
|
WIDGET_MEASURE_UNKNOWN,
|
|
8097
9247
|
authoringRulesFor,
|
|
8098
9248
|
buildAccessMatrix,
|
|
@@ -8100,12 +9250,16 @@ export {
|
|
|
8100
9250
|
extractHookBodyWriteSet,
|
|
8101
9251
|
extractHookBodyWrites,
|
|
8102
9252
|
findUnguardedNullableOperands,
|
|
9253
|
+
isSourceAuthoredPage,
|
|
8103
9254
|
lintAutonumberFormats,
|
|
8104
9255
|
lintDataModel,
|
|
8105
9256
|
lintFlowPatterns,
|
|
9257
|
+
lintLegacyOrganizationComposites,
|
|
8106
9258
|
lintLivenessProperties,
|
|
8107
9259
|
lintUniqueDeclarations,
|
|
9260
|
+
lintUnscopedDeclaredIndexes,
|
|
8108
9261
|
lintViewRefs,
|
|
9262
|
+
nearestRegisteredFormat,
|
|
8109
9263
|
nullGuardMessage,
|
|
8110
9264
|
runAuthoringRules,
|
|
8111
9265
|
runRuntimeAuthoringRules,
|
|
@@ -8122,6 +9276,7 @@ export {
|
|
|
8122
9276
|
validateApprovalApprovers,
|
|
8123
9277
|
validateCapabilityReferences,
|
|
8124
9278
|
validateChartBindings,
|
|
9279
|
+
validateComponentProps,
|
|
8125
9280
|
validateDashboardActionRefs,
|
|
8126
9281
|
validateFilterTokens,
|
|
8127
9282
|
validateFlowNodeWrites,
|
|
@@ -8144,15 +9299,21 @@ export {
|
|
|
8144
9299
|
validateRecordTitle,
|
|
8145
9300
|
validateReferenceIntegrity,
|
|
8146
9301
|
validateResponsiveStyles,
|
|
9302
|
+
validateRlsPredicateEnforceability,
|
|
9303
|
+
validateRuleCompilability,
|
|
9304
|
+
validateRuleSchemaFormats,
|
|
8147
9305
|
validateSearchableFields,
|
|
8148
9306
|
validateSecurityPosture,
|
|
8149
9307
|
validateSeedReplaySafety,
|
|
8150
9308
|
validateSeedStateMachine,
|
|
8151
9309
|
validateSemanticRoles,
|
|
9310
|
+
validateSharingRuleEnforceability,
|
|
8152
9311
|
validateStackExpressions,
|
|
9312
|
+
validateTranslatableSections,
|
|
8153
9313
|
validateTranslationReferences,
|
|
8154
9314
|
validateViewContainers,
|
|
8155
9315
|
validateVisibilityPredicates,
|
|
8156
|
-
validateWidgetBindings
|
|
9316
|
+
validateWidgetBindings,
|
|
9317
|
+
walkPageComponents
|
|
8157
9318
|
};
|
|
8158
9319
|
//# sourceMappingURL=index.js.map
|