@objectstack/lint 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +622 -0
- package/dist/index.cjs +894 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -66
- package/dist/index.d.ts +310 -66
- package/dist/index.js +871 -278
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-s3X9D9hm.d.cts} +89 -2
- package/dist/{runtime-H-nDodRy.d.ts → runtime-s3X9D9hm.d.ts} +89 -2
- package/dist/runtime.cjs +986 -409
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +959 -378
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/dist/runtime.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var runtime_exports = {};
|
|
22
22
|
__export(runtime_exports, {
|
|
23
23
|
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
24
|
+
narrowObjectsToPackageClosure: () => narrowObjectsToPackageClosure,
|
|
24
25
|
runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
|
|
25
26
|
runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
|
|
26
27
|
runtimeGatedTypes: () => runtimeGatedTypes,
|
|
@@ -276,10 +277,10 @@ var OUTCOME_CLAUSE = {
|
|
|
276
277
|
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
277
278
|
"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)"
|
|
278
279
|
};
|
|
279
|
-
function nullGuardMessage(subject, objectName,
|
|
280
|
+
function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
|
|
280
281
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
281
|
-
const hasNote =
|
|
282
|
-
return `${subject} applies \`${
|
|
282
|
+
const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
|
|
283
|
+
return `${subject} applies \`${finding2.operator}\` to \`${finding2.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding2.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
|
|
283
284
|
}
|
|
284
285
|
|
|
285
286
|
// src/validate-expressions.ts
|
|
@@ -462,10 +463,10 @@ function validateStackExpressions(stack) {
|
|
|
462
463
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
463
464
|
const source = celSourceOf(raw);
|
|
464
465
|
if (!source) return;
|
|
465
|
-
for (const
|
|
466
|
+
for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
466
467
|
issues.push({
|
|
467
468
|
where,
|
|
468
|
-
message: nullGuardMessage(subject, objectName,
|
|
469
|
+
message: nullGuardMessage(subject, objectName, finding2, outcome),
|
|
469
470
|
source,
|
|
470
471
|
severity: "error"
|
|
471
472
|
});
|
|
@@ -489,7 +490,7 @@ function validateStackExpressions(stack) {
|
|
|
489
490
|
const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
|
|
490
491
|
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
491
492
|
visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
|
|
492
|
-
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`).
|
|
493
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). The server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
|
|
493
494
|
requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
|
|
494
495
|
// Listed rather than left to the `??` below, so the map covers every slot
|
|
495
496
|
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
@@ -548,14 +549,15 @@ function validateStackExpressions(stack) {
|
|
|
548
549
|
if (retired.length > 0) {
|
|
549
550
|
issues.push({
|
|
550
551
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
551
|
-
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled)
|
|
552
|
-
// behaviour, never the retired key's fate —
|
|
553
|
-
// over a branch that DELETES the key
|
|
554
|
-
//
|
|
552
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled), reworded under #9529: the house
|
|
553
|
+
// sentence names the TOOL's behaviour, never the retired key's fate —
|
|
554
|
+
// "rewrite it" read two ways over a branch that DELETES the key
|
|
555
|
+
// (template/recipients/variables/script), and the tool never rewrote a
|
|
556
|
+
// source file at all. Plain-quoted (not a template literal)
|
|
555
557
|
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
556
558
|
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
557
559
|
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
558
|
-
"Run `os migrate meta --from 16` to
|
|
560
|
+
"Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
|
|
559
561
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
560
562
|
});
|
|
561
563
|
} else if (!fn) {
|
|
@@ -626,10 +628,10 @@ function validateStackExpressions(stack) {
|
|
|
626
628
|
f.expression,
|
|
627
629
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
628
630
|
);
|
|
629
|
-
const
|
|
630
|
-
for (const e of res.errors) issues.push({ where:
|
|
631
|
-
for (const w of res.warnings) issues.push({ where:
|
|
632
|
-
warnUnprovisionedAnchors(
|
|
631
|
+
const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
632
|
+
for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
|
|
633
|
+
for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
|
|
634
|
+
warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
|
|
633
635
|
}
|
|
634
636
|
}
|
|
635
637
|
}
|
|
@@ -1278,7 +1280,6 @@ function asArray4(v) {
|
|
|
1278
1280
|
function strName(v) {
|
|
1279
1281
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1280
1282
|
}
|
|
1281
|
-
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
1282
1283
|
var URL_COLLECTION_TO_STACK_KEY = {
|
|
1283
1284
|
object: "objects",
|
|
1284
1285
|
objects: "objects",
|
|
@@ -1323,13 +1324,10 @@ function collectKnownTargets(stack) {
|
|
|
1323
1324
|
return { actions, objects, reports, dashboards, pages, views };
|
|
1324
1325
|
}
|
|
1325
1326
|
function resolveActionTarget(actionType, target, known) {
|
|
1326
|
-
if (known.actions.has(target)) return true;
|
|
1327
1327
|
if (actionType === "modal") {
|
|
1328
|
-
|
|
1329
|
-
const m = MODAL_VERB_RE.exec(target);
|
|
1330
|
-
if (m && known.objects.has(m[1])) return true;
|
|
1328
|
+
return known.pages.has(target);
|
|
1331
1329
|
}
|
|
1332
|
-
return
|
|
1330
|
+
return known.actions.has(target);
|
|
1333
1331
|
}
|
|
1334
1332
|
function resolveUrlRoute(target, known) {
|
|
1335
1333
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
|
|
@@ -1359,14 +1357,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
1359
1357
|
const actionType = strName(action.actionType) ?? "url";
|
|
1360
1358
|
if (actionType === "script" || actionType === "modal") {
|
|
1361
1359
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
1362
|
-
const kindWord = actionType === "script" ? "script" : "modal";
|
|
1363
1360
|
findings.push({
|
|
1364
1361
|
severity: "error",
|
|
1365
1362
|
rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
1366
1363
|
where,
|
|
1367
1364
|
path,
|
|
1368
|
-
message:
|
|
1369
|
-
hint: actionType === "modal" ? `
|
|
1365
|
+
message: actionType === "modal" ? `modal action target "${target}" names no declared page \u2014 a modal target names a PAGE, only (objectstack#6739). The button renders but the runtime refuses the dispatch when clicked \u2014 a dangling reference (ADR-0049: a declared reference must resolve).` : `script action target "${target}" resolves to no defined action. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
|
|
1366
|
+
hint: actionType === "modal" ? `Point actionUrl at a declared page (stack.pages), or use actionType: 'form' with an "<object>.<view>" form-view target to open an object's form, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
|
|
1370
1367
|
});
|
|
1371
1368
|
return;
|
|
1372
1369
|
}
|
|
@@ -1408,7 +1405,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
1408
1405
|
var import_data4 = require("@objectstack/spec/data");
|
|
1409
1406
|
|
|
1410
1407
|
// src/filter-walk.ts
|
|
1411
|
-
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1408
|
+
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
|
|
1412
1409
|
function asArray5(v) {
|
|
1413
1410
|
if (Array.isArray(v)) return v;
|
|
1414
1411
|
if (v && typeof v === "object") {
|
|
@@ -1512,8 +1509,146 @@ function validateFilterTokens(stack) {
|
|
|
1512
1509
|
return out;
|
|
1513
1510
|
}
|
|
1514
1511
|
|
|
1515
|
-
// src/validate-
|
|
1512
|
+
// src/validate-preset-comparands.ts
|
|
1516
1513
|
var import_data5 = require("@objectstack/spec/data");
|
|
1514
|
+
var import_ui2 = require("@objectstack/spec/ui");
|
|
1515
|
+
var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
|
|
1516
|
+
var PRESET_COMPARAND_SURFACES = [
|
|
1517
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1518
|
+
{ key: "objects", kind: "object" },
|
|
1519
|
+
{ key: "views", kind: "view" },
|
|
1520
|
+
{ key: "reports", kind: "report" },
|
|
1521
|
+
{ key: "datasets", kind: "dataset" },
|
|
1522
|
+
{ key: "pages", kind: "page" },
|
|
1523
|
+
{ key: "apps", kind: "app" },
|
|
1524
|
+
{ key: "flows", kind: "flow" }
|
|
1525
|
+
];
|
|
1526
|
+
var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
|
|
1527
|
+
var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
1528
|
+
var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
|
|
1529
|
+
"greater_than",
|
|
1530
|
+
"greater_than_or_equal",
|
|
1531
|
+
"less_than",
|
|
1532
|
+
"less_than_or_equal",
|
|
1533
|
+
"before",
|
|
1534
|
+
"after"
|
|
1535
|
+
]);
|
|
1536
|
+
var MAX_DEPTH = 32;
|
|
1537
|
+
function isPlainObject(v) {
|
|
1538
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
1539
|
+
}
|
|
1540
|
+
function finding(where, path, preset, operator) {
|
|
1541
|
+
return {
|
|
1542
|
+
severity: "error",
|
|
1543
|
+
rule: FILTER_PRESET_COMPARAND,
|
|
1544
|
+
where,
|
|
1545
|
+
path,
|
|
1546
|
+
message: (0, import_data5.bareDateRangePresetComparandMessage)(preset, operator),
|
|
1547
|
+
hint: "Presets belong to the dashboard date-filter bar (dateRange.defaultRange, a date global filter's defaultValue). In a filter comparand, write the {date-macro} window the message names, or an ISO date."
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function judgeConditionNode(node, path, where, out, depth) {
|
|
1551
|
+
if (depth > MAX_DEPTH) return;
|
|
1552
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1553
|
+
const here = `${path}.${key}`;
|
|
1554
|
+
if (key === "$and" || key === "$or") {
|
|
1555
|
+
if (Array.isArray(value)) {
|
|
1556
|
+
value.forEach((arm, i) => {
|
|
1557
|
+
if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
if (key === "$not") {
|
|
1563
|
+
if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
if (key.startsWith("$")) continue;
|
|
1567
|
+
if (!isPlainObject(value)) continue;
|
|
1568
|
+
const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
|
|
1569
|
+
if (!hasOps) {
|
|
1570
|
+
judgeConditionNode(value, here, where, out, depth + 1);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
for (const [op, comparand] of Object.entries(value)) {
|
|
1574
|
+
if (ORDERING_DOLLAR_OPS.has(op) && (0, import_data5.isDateRangePresetName)(comparand)) {
|
|
1575
|
+
out.push(finding(where, `${here}.${op}`, comparand, op));
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (op === "$between" && Array.isArray(comparand)) {
|
|
1579
|
+
comparand.forEach((endpoint, i) => {
|
|
1580
|
+
if ((0, import_data5.isDateRangePresetName)(endpoint)) {
|
|
1581
|
+
out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function judgeFilterRule(rule, path, where, out) {
|
|
1589
|
+
const operator = (0, import_ui2.normalizeFilterOperator)(rule.operator);
|
|
1590
|
+
if (typeof operator !== "string") return;
|
|
1591
|
+
const value = rule.value;
|
|
1592
|
+
if (ORDERING_RULE_OPS.has(operator) && (0, import_data5.isDateRangePresetName)(value)) {
|
|
1593
|
+
out.push(finding(where, `${path}.value`, value, operator));
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (operator === "between" && Array.isArray(value)) {
|
|
1597
|
+
value.forEach((endpoint, i) => {
|
|
1598
|
+
if ((0, import_data5.isDateRangePresetName)(endpoint)) {
|
|
1599
|
+
out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
function judgeTriple(triple, path, where, out) {
|
|
1605
|
+
const op = triple[1];
|
|
1606
|
+
if (typeof op !== "string") return;
|
|
1607
|
+
const canonical = (0, import_data5.canonicalAstOperator)(op);
|
|
1608
|
+
const value = triple[2];
|
|
1609
|
+
if (ORDERING_INFIX_OPS.has(canonical) && (0, import_data5.isDateRangePresetName)(value)) {
|
|
1610
|
+
out.push(finding(where, `${path}[2]`, value, op));
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (canonical === "between" && Array.isArray(value)) {
|
|
1614
|
+
value.forEach((endpoint, i) => {
|
|
1615
|
+
if ((0, import_data5.isDateRangePresetName)(endpoint)) {
|
|
1616
|
+
out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function judgeFilterValue(node, path, where, out, depth) {
|
|
1622
|
+
if (depth > MAX_DEPTH) return;
|
|
1623
|
+
if (Array.isArray(node)) {
|
|
1624
|
+
if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && import_data5.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
1625
|
+
judgeTriple(node, path, where, out);
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
node.forEach((member, i) => {
|
|
1629
|
+
if (typeof member === "string") return;
|
|
1630
|
+
judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
if (!isPlainObject(node)) return;
|
|
1635
|
+
if (typeof node.field === "string" && typeof node.operator === "string") {
|
|
1636
|
+
judgeFilterRule(node, path, where, out);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
judgeConditionNode(node, path, where, out, depth);
|
|
1640
|
+
}
|
|
1641
|
+
function validatePresetComparands(stack) {
|
|
1642
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1643
|
+
const out = [];
|
|
1644
|
+
walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
|
|
1645
|
+
judgeFilterValue(value, path, where, out, 0);
|
|
1646
|
+
});
|
|
1647
|
+
return out;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// src/validate-empty-combinators.ts
|
|
1651
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
1517
1652
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1518
1653
|
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1519
1654
|
var EMPTY_COMBINATOR_SURFACES = [
|
|
@@ -1532,12 +1667,12 @@ function isFilterNode(value) {
|
|
|
1532
1667
|
return proto === Object.prototype || proto === null;
|
|
1533
1668
|
}
|
|
1534
1669
|
var VERDICT_OF = {
|
|
1535
|
-
$and: (0,
|
|
1536
|
-
$or: (0,
|
|
1537
|
-
$not: (0,
|
|
1538
|
-
node: (0,
|
|
1670
|
+
$and: (0, import_data6.reduceFilterVerdict)({ $and: [] }),
|
|
1671
|
+
$or: (0, import_data6.reduceFilterVerdict)({ $or: [] }),
|
|
1672
|
+
$not: (0, import_data6.reduceFilterVerdict)({ $not: {} }),
|
|
1673
|
+
node: (0, import_data6.reduceFilterVerdict)({}),
|
|
1539
1674
|
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1540
|
-
orWithEmptyBranch: (0,
|
|
1675
|
+
orWithEmptyBranch: (0, import_data6.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
1541
1676
|
};
|
|
1542
1677
|
function rows(verdict) {
|
|
1543
1678
|
if (verdict === "true") return "matches EVERY row";
|
|
@@ -1822,9 +1957,10 @@ function validateObjectReferences(stack) {
|
|
|
1822
1957
|
}
|
|
1823
1958
|
|
|
1824
1959
|
// src/validate-searchable-fields.ts
|
|
1825
|
-
var
|
|
1960
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
1826
1961
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1827
1962
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1963
|
+
var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
|
|
1828
1964
|
function asArray7(v) {
|
|
1829
1965
|
if (Array.isArray(v)) return v;
|
|
1830
1966
|
if (v && typeof v === "object") {
|
|
@@ -1870,7 +2006,7 @@ function resolveAllowedSet(target) {
|
|
|
1870
2006
|
fields = { ...fields };
|
|
1871
2007
|
for (const f of systemDeclared) fields[f] = {};
|
|
1872
2008
|
}
|
|
1873
|
-
const { allowed, source } = (0,
|
|
2009
|
+
const { allowed, source } = (0, import_data7.resolveSearchFieldResolution)({
|
|
1874
2010
|
fields,
|
|
1875
2011
|
searchableFields: target.searchableFields,
|
|
1876
2012
|
displayField: target.displayField
|
|
@@ -1915,7 +2051,7 @@ function indexObjectSearchTargets(stack) {
|
|
|
1915
2051
|
}
|
|
1916
2052
|
return fieldsByObject;
|
|
1917
2053
|
}
|
|
1918
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
2054
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
|
|
1919
2055
|
const findings = [];
|
|
1920
2056
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1921
2057
|
if (!objectName) return findings;
|
|
@@ -1923,6 +2059,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1923
2059
|
const target = fieldsByObject.get(objectName);
|
|
1924
2060
|
if (!target) return findings;
|
|
1925
2061
|
const known = target.names;
|
|
2062
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
1926
2063
|
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
1927
2064
|
for (let i = 0; i < declared.length; i++) {
|
|
1928
2065
|
const entry = declared[i];
|
|
@@ -1940,7 +2077,17 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1940
2077
|
});
|
|
1941
2078
|
continue;
|
|
1942
2079
|
}
|
|
1943
|
-
if ((
|
|
2080
|
+
if (anchors?.has(name)) {
|
|
2081
|
+
findings.push({
|
|
2082
|
+
severity: "warning",
|
|
2083
|
+
rule: SEARCHABLE_FIELD_UNPROVISIONED,
|
|
2084
|
+
where,
|
|
2085
|
+
path: `${path}[${i}]`,
|
|
2086
|
+
message: `${subject} entry "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)}` + (role === "narrowing" ? ` \u2014 clients echo this declaration verbatim as the '$searchFields' override, so every toolbar search on this list scans a column that is empty on every record: it reads as search coverage and matches nothing.` : ` \u2014 'search' scans it on every record and it can never match, so the object's searchable set is narrower than it declares. Should it be the ONLY entry that resolves, the set scans nothing at all.`),
|
|
2087
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
if ((0, import_data7.isVirtualSearchField)(target.fields[name])) {
|
|
1944
2091
|
const vtype = target.fields[name]?.type;
|
|
1945
2092
|
findings.push({
|
|
1946
2093
|
severity: "error",
|
|
@@ -1968,7 +2115,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1968
2115
|
}
|
|
1969
2116
|
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
1970
2117
|
let why;
|
|
1971
|
-
if (
|
|
2118
|
+
if (import_data7.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
1972
2119
|
why = "a system/audit column, which the auto-default set never includes";
|
|
1973
2120
|
} else if (meta?.hidden) {
|
|
1974
2121
|
why = "hidden";
|
|
@@ -1982,7 +2129,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1982
2129
|
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1983
2130
|
where,
|
|
1984
2131
|
path: `${path}[${i}]`,
|
|
1985
|
-
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...
|
|
2132
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data7.SEARCHABLE_TEXTUAL_TYPES, ...import_data7.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
1986
2133
|
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
|
|
1987
2134
|
});
|
|
1988
2135
|
}
|
|
@@ -1993,9 +2140,19 @@ function validateSearchableFields(stack) {
|
|
|
1993
2140
|
if (!isRec3(stack)) return findings;
|
|
1994
2141
|
const objects = asArray7(stack.objects);
|
|
1995
2142
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2143
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
1996
2144
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
1997
2145
|
findings.push(
|
|
1998
|
-
...checkSearchableFieldList(
|
|
2146
|
+
...checkSearchableFieldList(
|
|
2147
|
+
declared,
|
|
2148
|
+
objectName,
|
|
2149
|
+
fieldsByObject,
|
|
2150
|
+
where,
|
|
2151
|
+
path,
|
|
2152
|
+
subject,
|
|
2153
|
+
role,
|
|
2154
|
+
unprovisionedAnchors
|
|
2155
|
+
)
|
|
1999
2156
|
);
|
|
2000
2157
|
};
|
|
2001
2158
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -2064,33 +2221,203 @@ function listViewObject(listView) {
|
|
|
2064
2221
|
return isRec3(data) ? strName3(data.object) : void 0;
|
|
2065
2222
|
}
|
|
2066
2223
|
|
|
2067
|
-
// src/
|
|
2224
|
+
// src/validate-sortable-fields.ts
|
|
2225
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
2226
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2227
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2068
2228
|
function isRec4(v) {
|
|
2069
2229
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2070
2230
|
}
|
|
2071
2231
|
function strName4(v) {
|
|
2072
2232
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2073
2233
|
}
|
|
2234
|
+
function readSortKeys(declared) {
|
|
2235
|
+
const fromShorthand = (raw, at) => {
|
|
2236
|
+
const trimmed = raw.trim();
|
|
2237
|
+
if (!trimmed) return void 0;
|
|
2238
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
2239
|
+
return bare ? { field: bare, at } : void 0;
|
|
2240
|
+
};
|
|
2241
|
+
if (typeof declared === "string") {
|
|
2242
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
2243
|
+
}
|
|
2244
|
+
if (Array.isArray(declared)) {
|
|
2245
|
+
const keys = [];
|
|
2246
|
+
for (let i = 0; i < declared.length; i++) {
|
|
2247
|
+
const el = declared[i];
|
|
2248
|
+
if (typeof el === "string") {
|
|
2249
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
2250
|
+
if (k) keys.push(k);
|
|
2251
|
+
continue;
|
|
2252
|
+
}
|
|
2253
|
+
if (!isRec4(el)) continue;
|
|
2254
|
+
const field = strName4(el.field);
|
|
2255
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
2256
|
+
}
|
|
2257
|
+
return keys;
|
|
2258
|
+
}
|
|
2259
|
+
return [];
|
|
2260
|
+
}
|
|
2261
|
+
function suggest4(target, known) {
|
|
2262
|
+
let best;
|
|
2263
|
+
let bestScore = Infinity;
|
|
2264
|
+
for (const candidate of known) {
|
|
2265
|
+
const d = distance3(target, candidate);
|
|
2266
|
+
if (d < bestScore) {
|
|
2267
|
+
bestScore = d;
|
|
2268
|
+
best = candidate;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
2272
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
2273
|
+
}
|
|
2274
|
+
function distance3(a, b) {
|
|
2275
|
+
const m = a.length;
|
|
2276
|
+
const n = b.length;
|
|
2277
|
+
if (m === 0) return n;
|
|
2278
|
+
if (n === 0) return m;
|
|
2279
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2280
|
+
for (let i = 1; i <= m; i++) {
|
|
2281
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
2282
|
+
for (let j = 1; j <= n; j++) {
|
|
2283
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2284
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2285
|
+
}
|
|
2286
|
+
prev = curr;
|
|
2287
|
+
}
|
|
2288
|
+
return prev[n];
|
|
2289
|
+
}
|
|
2290
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
2291
|
+
const findings = [];
|
|
2292
|
+
if (declared === void 0 || declared === null) return findings;
|
|
2293
|
+
if (!objectName) return findings;
|
|
2294
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
2295
|
+
const target = fieldsByObject.get(objectName);
|
|
2296
|
+
if (!target) return findings;
|
|
2297
|
+
const known = target.names;
|
|
2298
|
+
for (const key of readSortKeys(declared)) {
|
|
2299
|
+
const name = key.field;
|
|
2300
|
+
const head = name.split(".")[0];
|
|
2301
|
+
if (SYSTEM_FIELDS.has(head)) continue;
|
|
2302
|
+
if (!known.has(head)) {
|
|
2303
|
+
const dotted = name.includes(".");
|
|
2304
|
+
findings.push({
|
|
2305
|
+
severity: "error",
|
|
2306
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
2307
|
+
where,
|
|
2308
|
+
path: `${path}${key.at}`,
|
|
2309
|
+
message: `${subject} orders by "${name}", which is not a field on object "${objectName}". The runtime refuses the sort rather than dropping it: every load of this view answers 400 INVALID_SORT (#6994), because a sort is the view's FIRST fetch and not an optional interaction.` + (dotted ? "" : suggest4(head, known)),
|
|
2310
|
+
hint: (dotted ? `'sort' reaches only whole columns of "${objectName}" itself, never a related record's column \u2014 denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + (known.size > 0 ? `Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2311
|
+
});
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
const meta = target.fields[name];
|
|
2315
|
+
if ((0, import_data8.isVirtualSearchField)(meta)) {
|
|
2316
|
+
const vtype = meta?.type;
|
|
2317
|
+
findings.push({
|
|
2318
|
+
severity: "error",
|
|
2319
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
2320
|
+
where,
|
|
2321
|
+
path: `${path}${key.at}`,
|
|
2322
|
+
message: `${subject} orders by "${name}" on object "${objectName}", a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materialises a column to ORDER BY. Measured, an unrefused sort on one returns 'asc' and 'desc' in byte-identical order \u2014 the rows carry the values they were asked to be ordered by, unordered, under a success.`,
|
|
2323
|
+
hint: `Denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that, or drop "${name}" from this sort. At runtime both doors now refuse it with 400 INVALID_SORT \u2014 the REST ingress (#6994) and the engine itself (#7095) \u2014 so the declaration breaks the view's first fetch, and every fetch after it.`
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
return findings;
|
|
2328
|
+
}
|
|
2329
|
+
function validateSortableFields(stack) {
|
|
2330
|
+
const findings = [];
|
|
2331
|
+
if (!isRec4(stack)) return findings;
|
|
2332
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2333
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2334
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
2335
|
+
findings.push(
|
|
2336
|
+
...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
|
|
2337
|
+
);
|
|
2338
|
+
};
|
|
2339
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
2340
|
+
const obj = objects[oi];
|
|
2341
|
+
if (!isRec4(obj)) continue;
|
|
2342
|
+
const objName = strName4(obj.name);
|
|
2343
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
2344
|
+
if (isRec4(obj.listViews)) {
|
|
2345
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2346
|
+
if (!isRec4(lv)) continue;
|
|
2347
|
+
check(
|
|
2348
|
+
lv.sort,
|
|
2349
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
2350
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
2351
|
+
listViewObject2(lv) ?? objName,
|
|
2352
|
+
`${label2} \u203A listViews.${key}`,
|
|
2353
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
2354
|
+
"list-view sort"
|
|
2355
|
+
);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
2360
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
2361
|
+
const view = views[vi];
|
|
2362
|
+
if (!isRec4(view)) continue;
|
|
2363
|
+
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2364
|
+
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2365
|
+
if (isRec4(view.list)) {
|
|
2366
|
+
check(
|
|
2367
|
+
view.list.sort,
|
|
2368
|
+
listViewObject2(view.list) ?? viewObject,
|
|
2369
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2370
|
+
`views[${vi}].list.sort`,
|
|
2371
|
+
"list-view sort"
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
if (isRec4(view.listViews)) {
|
|
2375
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2376
|
+
if (!isRec4(lv)) continue;
|
|
2377
|
+
check(
|
|
2378
|
+
lv.sort,
|
|
2379
|
+
listViewObject2(lv) ?? viewObject,
|
|
2380
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2381
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
2382
|
+
"list-view sort"
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return findings;
|
|
2388
|
+
}
|
|
2389
|
+
function listViewObject2(listView) {
|
|
2390
|
+
const data = listView.data;
|
|
2391
|
+
return isRec4(data) ? strName4(data.object) : void 0;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// src/page-walk.ts
|
|
2395
|
+
function isRec5(v) {
|
|
2396
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2397
|
+
}
|
|
2398
|
+
function strName5(v) {
|
|
2399
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2400
|
+
}
|
|
2074
2401
|
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
2075
2402
|
function isSourceAuthoredPage(page) {
|
|
2076
|
-
const kind =
|
|
2403
|
+
const kind = strName5(page.kind);
|
|
2077
2404
|
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
2078
2405
|
}
|
|
2079
2406
|
function walkPageComponents(page, pagePath) {
|
|
2080
2407
|
const out = [];
|
|
2081
|
-
if (!
|
|
2082
|
-
const pageObject =
|
|
2408
|
+
if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
|
|
2409
|
+
const pageObject = strName5(page.object);
|
|
2083
2410
|
const visit = (node, path, inheritedObject) => {
|
|
2084
|
-
if (!
|
|
2085
|
-
const props =
|
|
2086
|
-
const dataSource =
|
|
2087
|
-
const objectName =
|
|
2411
|
+
if (!isRec5(node)) return;
|
|
2412
|
+
const props = isRec5(node.properties) ? node.properties : void 0;
|
|
2413
|
+
const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
|
|
2414
|
+
const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
|
|
2088
2415
|
out.push({ component: node, path, objectName });
|
|
2089
2416
|
if (!props) return;
|
|
2090
2417
|
if (Array.isArray(props.items)) {
|
|
2091
2418
|
for (let i = 0; i < props.items.length; i++) {
|
|
2092
2419
|
const item = props.items[i];
|
|
2093
|
-
if (!
|
|
2420
|
+
if (!isRec5(item) || !Array.isArray(item.children)) continue;
|
|
2094
2421
|
for (let c = 0; c < item.children.length; c++) {
|
|
2095
2422
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
2096
2423
|
}
|
|
@@ -2112,12 +2439,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
2112
2439
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
2113
2440
|
for (let r = 0; r < regions.length; r++) {
|
|
2114
2441
|
const region = regions[r];
|
|
2115
|
-
if (!
|
|
2442
|
+
if (!isRec5(region) || !Array.isArray(region.components)) continue;
|
|
2116
2443
|
for (let c = 0; c < region.components.length; c++) {
|
|
2117
2444
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
2118
2445
|
}
|
|
2119
2446
|
}
|
|
2120
|
-
const slots =
|
|
2447
|
+
const slots = isRec5(page.slots) ? page.slots : void 0;
|
|
2121
2448
|
if (slots) {
|
|
2122
2449
|
for (const [slot, value] of Object.entries(slots)) {
|
|
2123
2450
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2139,13 +2466,13 @@ function asArray8(v) {
|
|
|
2139
2466
|
}
|
|
2140
2467
|
return [];
|
|
2141
2468
|
}
|
|
2142
|
-
function
|
|
2469
|
+
function strName6(v) {
|
|
2143
2470
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2144
2471
|
}
|
|
2145
2472
|
function strList(v) {
|
|
2146
2473
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2147
2474
|
}
|
|
2148
|
-
function
|
|
2475
|
+
function distance4(a, b) {
|
|
2149
2476
|
const m = a.length;
|
|
2150
2477
|
const n = b.length;
|
|
2151
2478
|
if (m === 0) return n;
|
|
@@ -2161,11 +2488,11 @@ function distance3(a, b) {
|
|
|
2161
2488
|
}
|
|
2162
2489
|
return prev[n];
|
|
2163
2490
|
}
|
|
2164
|
-
function
|
|
2491
|
+
function suggest5(target, known) {
|
|
2165
2492
|
let best;
|
|
2166
2493
|
let bestScore = Infinity;
|
|
2167
2494
|
for (const candidate of known) {
|
|
2168
|
-
const d =
|
|
2495
|
+
const d = distance4(target, candidate);
|
|
2169
2496
|
if (d < bestScore) {
|
|
2170
2497
|
bestScore = d;
|
|
2171
2498
|
best = candidate;
|
|
@@ -2177,13 +2504,13 @@ function suggest4(target, known) {
|
|
|
2177
2504
|
function collectActionNames(stack) {
|
|
2178
2505
|
const names = /* @__PURE__ */ new Set();
|
|
2179
2506
|
for (const action of asArray8(stack.actions)) {
|
|
2180
|
-
const n =
|
|
2507
|
+
const n = strName6(action?.name);
|
|
2181
2508
|
if (n) names.add(n);
|
|
2182
2509
|
}
|
|
2183
2510
|
for (const obj of asArray8(stack.objects)) {
|
|
2184
2511
|
if (!obj || typeof obj !== "object") continue;
|
|
2185
2512
|
for (const action of asArray8(obj.actions)) {
|
|
2186
|
-
const n =
|
|
2513
|
+
const n = strName6(action?.name);
|
|
2187
2514
|
if (n) names.add(n);
|
|
2188
2515
|
}
|
|
2189
2516
|
}
|
|
@@ -2200,7 +2527,7 @@ function validateActionNameRefs(stack) {
|
|
|
2200
2527
|
rule: ACTION_NAME_UNDEFINED,
|
|
2201
2528
|
where,
|
|
2202
2529
|
path,
|
|
2203
|
-
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` +
|
|
2530
|
+
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest5(name, known),
|
|
2204
2531
|
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
|
|
2205
2532
|
});
|
|
2206
2533
|
};
|
|
@@ -2226,7 +2553,7 @@ function validateActionNameRefs(stack) {
|
|
|
2226
2553
|
if (!def || typeof def !== "object") continue;
|
|
2227
2554
|
if (def.execution !== "aggregate") continue;
|
|
2228
2555
|
if (def.actionDef !== void 0) continue;
|
|
2229
|
-
const name =
|
|
2556
|
+
const name = strName6(def.name);
|
|
2230
2557
|
if (!name) continue;
|
|
2231
2558
|
check(
|
|
2232
2559
|
name,
|
|
@@ -2241,7 +2568,7 @@ function validateActionNameRefs(stack) {
|
|
|
2241
2568
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2242
2569
|
const view = views[vi];
|
|
2243
2570
|
if (!view || typeof view !== "object") continue;
|
|
2244
|
-
const viewName =
|
|
2571
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
|
|
2245
2572
|
const owner = `view "${viewName}"`;
|
|
2246
2573
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
2247
2574
|
const listViews = view.listViews;
|
|
@@ -2257,7 +2584,7 @@ function validateActionNameRefs(stack) {
|
|
|
2257
2584
|
if (!obj || typeof obj !== "object") continue;
|
|
2258
2585
|
const objListViews = obj.listViews;
|
|
2259
2586
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
2260
|
-
const owner = `object "${
|
|
2587
|
+
const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
|
|
2261
2588
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
2262
2589
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
2263
2590
|
}
|
|
@@ -2266,7 +2593,7 @@ function validateActionNameRefs(stack) {
|
|
|
2266
2593
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2267
2594
|
const page = pages[pi];
|
|
2268
2595
|
if (!page || typeof page !== "object") continue;
|
|
2269
|
-
const pageName =
|
|
2596
|
+
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2270
2597
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2271
2598
|
const props = component.properties;
|
|
2272
2599
|
if (!props || typeof props !== "object") continue;
|
|
@@ -2274,7 +2601,7 @@ function validateActionNameRefs(stack) {
|
|
|
2274
2601
|
for (let ai = 0; ai < names.length; ai++) {
|
|
2275
2602
|
check(
|
|
2276
2603
|
names[ai],
|
|
2277
|
-
`page "${pageName}" \xB7 component "${
|
|
2604
|
+
`page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
|
|
2278
2605
|
`${path}.properties.actionNames[${ai}]`,
|
|
2279
2606
|
"Quick-actions bar"
|
|
2280
2607
|
);
|
|
@@ -2285,7 +2612,7 @@ function validateActionNameRefs(stack) {
|
|
|
2285
2612
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2286
2613
|
const app = apps[ai];
|
|
2287
2614
|
if (!app || typeof app !== "object") continue;
|
|
2288
|
-
const appName =
|
|
2615
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
2289
2616
|
const walkNav = (items, basePath) => {
|
|
2290
2617
|
const navItems = asArray8(items);
|
|
2291
2618
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -2293,20 +2620,20 @@ function validateActionNameRefs(stack) {
|
|
|
2293
2620
|
if (!nav || typeof nav !== "object") continue;
|
|
2294
2621
|
const navPath = `${basePath}[${ni}]`;
|
|
2295
2622
|
const actionDef = nav.actionDef;
|
|
2296
|
-
const actionName =
|
|
2623
|
+
const actionName = strName6(actionDef?.actionName);
|
|
2297
2624
|
if (nav.type === "action" && actionName) {
|
|
2298
2625
|
check(
|
|
2299
2626
|
actionName,
|
|
2300
|
-
`app "${appName}" \xB7 nav "${
|
|
2627
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2301
2628
|
`${navPath}.actionDef.actionName`,
|
|
2302
2629
|
"Navigation action item"
|
|
2303
2630
|
);
|
|
2304
2631
|
}
|
|
2305
|
-
const runAction =
|
|
2632
|
+
const runAction = strName6(nav.runAction);
|
|
2306
2633
|
if (nav.type === "object" && runAction) {
|
|
2307
2634
|
check(
|
|
2308
2635
|
runAction,
|
|
2309
|
-
`app "${appName}" \xB7 nav "${
|
|
2636
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2310
2637
|
`${navPath}.runAction`,
|
|
2311
2638
|
"Navigation deep-link auto-run"
|
|
2312
2639
|
);
|
|
@@ -2333,23 +2660,23 @@ function asArray9(v) {
|
|
|
2333
2660
|
}
|
|
2334
2661
|
return [];
|
|
2335
2662
|
}
|
|
2336
|
-
function
|
|
2663
|
+
function strName7(v) {
|
|
2337
2664
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2338
2665
|
}
|
|
2339
|
-
function
|
|
2666
|
+
function isRec6(v) {
|
|
2340
2667
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2341
2668
|
}
|
|
2342
2669
|
function fieldRefsFrom(value, basePath) {
|
|
2343
2670
|
const out = [];
|
|
2344
2671
|
const one = (v, path) => {
|
|
2345
|
-
const bare =
|
|
2672
|
+
const bare = strName7(v);
|
|
2346
2673
|
if (bare) {
|
|
2347
2674
|
out.push({ name: bare, path });
|
|
2348
2675
|
return;
|
|
2349
2676
|
}
|
|
2350
|
-
if (!
|
|
2351
|
-
const named =
|
|
2352
|
-
if (named) out.push({ name: named, path: `${path}.${
|
|
2677
|
+
if (!isRec6(v)) return;
|
|
2678
|
+
const named = strName7(v.field) ?? strName7(v.name);
|
|
2679
|
+
if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
|
|
2353
2680
|
};
|
|
2354
2681
|
if (Array.isArray(value)) {
|
|
2355
2682
|
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
@@ -2375,7 +2702,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2375
2702
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2376
2703
|
"record:path": { props: ["statusField"] },
|
|
2377
2704
|
"element:number": { props: ["field"] },
|
|
2378
|
-
|
|
2705
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
2706
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
2707
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
2708
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
2709
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
2710
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
2379
2711
|
"element:form": { props: ["fields"] },
|
|
2380
2712
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2381
2713
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -2405,18 +2737,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2405
2737
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2406
2738
|
for (let si = 0; si < sections.length; si++) {
|
|
2407
2739
|
const section = sections[si];
|
|
2408
|
-
if (!
|
|
2740
|
+
if (!isRec6(section)) continue;
|
|
2409
2741
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2410
2742
|
}
|
|
2411
2743
|
}
|
|
2412
2744
|
return refs;
|
|
2413
2745
|
}
|
|
2414
2746
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2415
|
-
const add =
|
|
2416
|
-
const picker = add &&
|
|
2747
|
+
const add = isRec6(props.add) ? props.add : void 0;
|
|
2748
|
+
const picker = add && isRec6(add.picker) ? add.picker : void 0;
|
|
2417
2749
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2418
2750
|
return {
|
|
2419
|
-
relatedObject:
|
|
2751
|
+
relatedObject: strName7(props.objectName),
|
|
2420
2752
|
related: [
|
|
2421
2753
|
...fieldRefsFrom(props.columns, at("columns")),
|
|
2422
2754
|
...sortFieldRefs(props.sort, at("sort")),
|
|
@@ -2425,7 +2757,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2425
2757
|
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
2426
2758
|
],
|
|
2427
2759
|
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
2428
|
-
pickerObject: picker ?
|
|
2760
|
+
pickerObject: picker ? strName7(picker.object) : void 0,
|
|
2429
2761
|
picker: picker ? [
|
|
2430
2762
|
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
2431
2763
|
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
@@ -2434,13 +2766,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2434
2766
|
}
|
|
2435
2767
|
function indexObjectFields(stack) {
|
|
2436
2768
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2437
|
-
if (!
|
|
2769
|
+
if (!isRec6(stack)) return objectFields;
|
|
2438
2770
|
for (const obj of asArray9(stack.objects)) {
|
|
2439
|
-
const name =
|
|
2771
|
+
const name = strName7(obj.name);
|
|
2440
2772
|
if (!name) continue;
|
|
2441
2773
|
const names = /* @__PURE__ */ new Set();
|
|
2442
2774
|
for (const f of asArray9(obj.fields)) {
|
|
2443
|
-
const fn =
|
|
2775
|
+
const fn = strName7(f.name);
|
|
2444
2776
|
if (fn) names.add(fn);
|
|
2445
2777
|
}
|
|
2446
2778
|
objectFields.set(name, names);
|
|
@@ -2488,15 +2820,15 @@ function validatePageFieldBindings(stack) {
|
|
|
2488
2820
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2489
2821
|
const page = pages[pi];
|
|
2490
2822
|
if (!page || typeof page !== "object") continue;
|
|
2491
|
-
const pageName =
|
|
2823
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2492
2824
|
const checkRefs = (refs, objectName, where) => {
|
|
2493
2825
|
findings.push(
|
|
2494
2826
|
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2495
2827
|
);
|
|
2496
2828
|
};
|
|
2497
2829
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2498
|
-
const type =
|
|
2499
|
-
const props =
|
|
2830
|
+
const type = strName7(component.type);
|
|
2831
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2500
2832
|
if (!type || !props) continue;
|
|
2501
2833
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2502
2834
|
const base = `${path}.properties`;
|
|
@@ -2511,16 +2843,16 @@ function validatePageFieldBindings(stack) {
|
|
|
2511
2843
|
if (!refs) continue;
|
|
2512
2844
|
checkRefs(refs, objectName, where);
|
|
2513
2845
|
}
|
|
2514
|
-
const cfg =
|
|
2846
|
+
const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2515
2847
|
if (cfg) {
|
|
2516
|
-
const cfgObject =
|
|
2848
|
+
const cfgObject = strName7(cfg.source) ?? strName7(page.object);
|
|
2517
2849
|
const base = `pages[${pi}].interfaceConfig`;
|
|
2518
2850
|
const refs = [
|
|
2519
2851
|
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
2520
2852
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2521
2853
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2522
2854
|
];
|
|
2523
|
-
const userFilters =
|
|
2855
|
+
const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2524
2856
|
if (userFilters) {
|
|
2525
2857
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2526
2858
|
}
|
|
@@ -2542,16 +2874,16 @@ function asArray10(v) {
|
|
|
2542
2874
|
}
|
|
2543
2875
|
return [];
|
|
2544
2876
|
}
|
|
2545
|
-
function
|
|
2877
|
+
function strName8(v) {
|
|
2546
2878
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2547
2879
|
}
|
|
2548
2880
|
function strList2(v) {
|
|
2549
2881
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2550
2882
|
}
|
|
2551
|
-
function
|
|
2883
|
+
function isRec7(v) {
|
|
2552
2884
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2553
2885
|
}
|
|
2554
|
-
function
|
|
2886
|
+
function distance5(a, b) {
|
|
2555
2887
|
const m = a.length;
|
|
2556
2888
|
const n = b.length;
|
|
2557
2889
|
if (m === 0) return n;
|
|
@@ -2567,11 +2899,11 @@ function distance4(a, b) {
|
|
|
2567
2899
|
}
|
|
2568
2900
|
return prev[n];
|
|
2569
2901
|
}
|
|
2570
|
-
function
|
|
2902
|
+
function suggest6(target, known) {
|
|
2571
2903
|
let best;
|
|
2572
2904
|
let bestScore = Infinity;
|
|
2573
2905
|
for (const c of known) {
|
|
2574
|
-
const d =
|
|
2906
|
+
const d = distance5(target, c);
|
|
2575
2907
|
if (d < bestScore) {
|
|
2576
2908
|
bestScore = d;
|
|
2577
2909
|
best = c;
|
|
@@ -2587,16 +2919,16 @@ function list2(names) {
|
|
|
2587
2919
|
function indexDatasets(stack) {
|
|
2588
2920
|
const out = /* @__PURE__ */ new Map();
|
|
2589
2921
|
for (const ds of asArray10(stack.datasets)) {
|
|
2590
|
-
const name =
|
|
2922
|
+
const name = strName8(ds.name);
|
|
2591
2923
|
if (!name) continue;
|
|
2592
2924
|
const dimensions = /* @__PURE__ */ new Set();
|
|
2593
2925
|
for (const d of asArray10(ds.dimensions)) {
|
|
2594
|
-
const n =
|
|
2926
|
+
const n = strName8(d.name);
|
|
2595
2927
|
if (n) dimensions.add(n);
|
|
2596
2928
|
}
|
|
2597
2929
|
const measures = /* @__PURE__ */ new Set();
|
|
2598
2930
|
for (const m of asArray10(ds.measures)) {
|
|
2599
|
-
const n =
|
|
2931
|
+
const n = strName8(m.name);
|
|
2600
2932
|
if (n) measures.add(n);
|
|
2601
2933
|
}
|
|
2602
2934
|
out.set(name, { dimensions, measures });
|
|
@@ -2619,7 +2951,7 @@ function validateChartBindings(stack) {
|
|
|
2619
2951
|
where: binding.where,
|
|
2620
2952
|
path: `${binding.path}.dataset`,
|
|
2621
2953
|
message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
|
|
2622
|
-
hint: `Declared datasets: ${list2(datasets.keys())}.${
|
|
2954
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
2623
2955
|
});
|
|
2624
2956
|
return;
|
|
2625
2957
|
}
|
|
@@ -2631,7 +2963,7 @@ function validateChartBindings(stack) {
|
|
|
2631
2963
|
where: binding.where,
|
|
2632
2964
|
path,
|
|
2633
2965
|
message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
|
|
2634
|
-
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${
|
|
2966
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
2635
2967
|
});
|
|
2636
2968
|
};
|
|
2637
2969
|
const measureRef = (name, path, selected2) => {
|
|
@@ -2642,7 +2974,7 @@ function validateChartBindings(stack) {
|
|
|
2642
2974
|
where: binding.where,
|
|
2643
2975
|
path,
|
|
2644
2976
|
message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
|
|
2645
|
-
hint: `Dataset measures: ${list2(ds.measures)}.${
|
|
2977
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
2646
2978
|
});
|
|
2647
2979
|
return;
|
|
2648
2980
|
}
|
|
@@ -2677,26 +3009,26 @@ function validateChartBindings(stack) {
|
|
|
2677
3009
|
const reports = asArray10(stack.reports);
|
|
2678
3010
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2679
3011
|
const report = reports[ri];
|
|
2680
|
-
if (!
|
|
2681
|
-
const reportName =
|
|
3012
|
+
if (!isRec7(report)) continue;
|
|
3013
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
2682
3014
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2683
|
-
if (!
|
|
3015
|
+
if (!isRec7(chart)) return;
|
|
2684
3016
|
check({
|
|
2685
3017
|
dataset,
|
|
2686
3018
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
2687
3019
|
// it in lets the yAxis "declared but not selected" check work without
|
|
2688
3020
|
// reporting the selection itself twice.
|
|
2689
3021
|
values: { names: values, path: `${path}.values` },
|
|
2690
|
-
xAxis:
|
|
2691
|
-
yAxis:
|
|
2692
|
-
series: asArray10(chart.series).map((s, si) => ({ name:
|
|
3022
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
3023
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
3024
|
+
series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
2693
3025
|
where,
|
|
2694
3026
|
path: `${path}.chart`
|
|
2695
3027
|
});
|
|
2696
3028
|
};
|
|
2697
3029
|
checkReportChart(
|
|
2698
3030
|
report.chart,
|
|
2699
|
-
|
|
3031
|
+
strName8(report.dataset),
|
|
2700
3032
|
strList2(report.values),
|
|
2701
3033
|
`report "${reportName}" \xB7 chart`,
|
|
2702
3034
|
`reports[${ri}]`
|
|
@@ -2704,22 +3036,22 @@ function validateChartBindings(stack) {
|
|
|
2704
3036
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2705
3037
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2706
3038
|
const block = blocks[bi];
|
|
2707
|
-
if (!
|
|
3039
|
+
if (!isRec7(block)) continue;
|
|
2708
3040
|
checkReportChart(
|
|
2709
3041
|
block.chart,
|
|
2710
|
-
|
|
3042
|
+
strName8(block.dataset),
|
|
2711
3043
|
strList2(block.values),
|
|
2712
|
-
`report "${reportName}" \xB7 block "${
|
|
3044
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
2713
3045
|
`reports[${ri}].blocks[${bi}]`
|
|
2714
3046
|
);
|
|
2715
3047
|
}
|
|
2716
3048
|
}
|
|
2717
3049
|
const checkListChart = (container, where, path) => {
|
|
2718
|
-
if (!
|
|
3050
|
+
if (!isRec7(container)) return;
|
|
2719
3051
|
const chart = container.chart;
|
|
2720
|
-
if (!
|
|
3052
|
+
if (!isRec7(chart)) return;
|
|
2721
3053
|
check({
|
|
2722
|
-
dataset:
|
|
3054
|
+
dataset: strName8(chart.dataset),
|
|
2723
3055
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
2724
3056
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
2725
3057
|
where,
|
|
@@ -2729,10 +3061,10 @@ function validateChartBindings(stack) {
|
|
|
2729
3061
|
const views = asArray10(stack.views);
|
|
2730
3062
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2731
3063
|
const view = views[vi];
|
|
2732
|
-
if (!
|
|
2733
|
-
const viewName =
|
|
3064
|
+
if (!isRec7(view)) continue;
|
|
3065
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
2734
3066
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2735
|
-
if (
|
|
3067
|
+
if (isRec7(view.listViews)) {
|
|
2736
3068
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2737
3069
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2738
3070
|
}
|
|
@@ -2741,8 +3073,8 @@ function validateChartBindings(stack) {
|
|
|
2741
3073
|
const objects = asArray10(stack.objects);
|
|
2742
3074
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2743
3075
|
const obj = objects[oi];
|
|
2744
|
-
if (!
|
|
2745
|
-
const objName =
|
|
3076
|
+
if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
|
|
3077
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
2746
3078
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2747
3079
|
checkListChart(
|
|
2748
3080
|
lv,
|
|
@@ -2754,19 +3086,19 @@ function validateChartBindings(stack) {
|
|
|
2754
3086
|
const pages = asArray10(stack.pages);
|
|
2755
3087
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2756
3088
|
const page = pages[pi];
|
|
2757
|
-
if (!
|
|
2758
|
-
const pageName =
|
|
3089
|
+
if (!isRec7(page)) continue;
|
|
3090
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
2759
3091
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2760
|
-
const props =
|
|
2761
|
-
if (!props || !
|
|
2762
|
-
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name:
|
|
2763
|
-
const seriesRefs = asArray10(props.series).map((s, si) => ({ name:
|
|
3092
|
+
const props = isRec7(component.properties) ? component.properties : void 0;
|
|
3093
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
3094
|
+
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
3095
|
+
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
2764
3096
|
check({
|
|
2765
|
-
dataset:
|
|
3097
|
+
dataset: strName8(props.dataset),
|
|
2766
3098
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
2767
3099
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
2768
3100
|
series: [...axisRefs, ...seriesRefs],
|
|
2769
|
-
where: `page "${pageName}" \xB7 ${
|
|
3101
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
2770
3102
|
path: `${path}.properties`
|
|
2771
3103
|
});
|
|
2772
3104
|
}
|
|
@@ -2833,7 +3165,7 @@ function asArray12(v) {
|
|
|
2833
3165
|
}
|
|
2834
3166
|
return [];
|
|
2835
3167
|
}
|
|
2836
|
-
function
|
|
3168
|
+
function strName9(v) {
|
|
2837
3169
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2838
3170
|
}
|
|
2839
3171
|
function collectNavExposures(stack) {
|
|
@@ -2842,18 +3174,18 @@ function collectNavExposures(stack) {
|
|
|
2842
3174
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2843
3175
|
const app = apps[ai];
|
|
2844
3176
|
if (!app || typeof app !== "object") continue;
|
|
2845
|
-
const appName =
|
|
3177
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
2846
3178
|
const walk = (items, basePath) => {
|
|
2847
3179
|
const navItems = asArray12(items);
|
|
2848
3180
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
2849
3181
|
const nav = navItems[ni];
|
|
2850
3182
|
if (!nav || typeof nav !== "object") continue;
|
|
2851
3183
|
const navPath = `${basePath}[${ni}]`;
|
|
2852
|
-
const objectName =
|
|
3184
|
+
const objectName = strName9(nav.objectName);
|
|
2853
3185
|
if (nav.type === "object" && objectName) {
|
|
2854
3186
|
out.push({
|
|
2855
3187
|
objectName,
|
|
2856
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3188
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
2857
3189
|
path: `${navPath}.objectName`
|
|
2858
3190
|
});
|
|
2859
3191
|
}
|
|
@@ -2877,7 +3209,7 @@ function validateNavAccess(stack) {
|
|
|
2877
3209
|
if (exposures.length === 0) return findings;
|
|
2878
3210
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
2879
3211
|
for (const obj of asArray12(stack.objects)) {
|
|
2880
|
-
const n =
|
|
3212
|
+
const n = strName9(obj.name);
|
|
2881
3213
|
if (n) ownObjects.add(n);
|
|
2882
3214
|
}
|
|
2883
3215
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -2907,13 +3239,13 @@ function validateNavAccess(stack) {
|
|
|
2907
3239
|
|
|
2908
3240
|
// src/validate-nav-target-refs.ts
|
|
2909
3241
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2910
|
-
var
|
|
3242
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2911
3243
|
function asArray13(v) {
|
|
2912
|
-
if (Array.isArray(v)) return v.filter(
|
|
2913
|
-
if (
|
|
3244
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
3245
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2914
3246
|
return [];
|
|
2915
3247
|
}
|
|
2916
|
-
function
|
|
3248
|
+
function strName10(v) {
|
|
2917
3249
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2918
3250
|
}
|
|
2919
3251
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -2925,14 +3257,14 @@ var NAV_TARGETS = [
|
|
|
2925
3257
|
function namesOf(collection) {
|
|
2926
3258
|
const out = /* @__PURE__ */ new Set();
|
|
2927
3259
|
for (const entry of asArray13(collection)) {
|
|
2928
|
-
const n =
|
|
3260
|
+
const n = strName10(entry.name);
|
|
2929
3261
|
if (n) out.add(n);
|
|
2930
3262
|
}
|
|
2931
3263
|
return out;
|
|
2932
3264
|
}
|
|
2933
3265
|
function validateNavTargetRefs(stack) {
|
|
2934
3266
|
const findings = [];
|
|
2935
|
-
if (!
|
|
3267
|
+
if (!isRec8(stack)) return findings;
|
|
2936
3268
|
const apps = asArray13(stack.apps);
|
|
2937
3269
|
if (apps.length === 0) return findings;
|
|
2938
3270
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2940,16 +3272,16 @@ function validateNavTargetRefs(stack) {
|
|
|
2940
3272
|
declared.set(collection, namesOf(stack[collection]));
|
|
2941
3273
|
}
|
|
2942
3274
|
for (const [ai, app] of apps.entries()) {
|
|
2943
|
-
const appName =
|
|
3275
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2944
3276
|
const walk = (items, basePath) => {
|
|
2945
3277
|
if (!Array.isArray(items)) return;
|
|
2946
3278
|
for (const [ni, raw] of items.entries()) {
|
|
2947
|
-
if (!
|
|
3279
|
+
if (!isRec8(raw)) continue;
|
|
2948
3280
|
const nav = raw;
|
|
2949
3281
|
const navPath = `${basePath}[${ni}]`;
|
|
2950
3282
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
2951
3283
|
if (nav.type !== type) continue;
|
|
2952
|
-
const target =
|
|
3284
|
+
const target = strName10(nav[prop]);
|
|
2953
3285
|
if (!target || isInterpolated2(target)) continue;
|
|
2954
3286
|
const known = declared.get(collection);
|
|
2955
3287
|
if (known.has(target)) continue;
|
|
@@ -2957,7 +3289,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2957
3289
|
findings.push({
|
|
2958
3290
|
severity: "warning",
|
|
2959
3291
|
rule: NAV_TARGET_UNRESOLVED,
|
|
2960
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3292
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
2961
3293
|
path: `${navPath}.${prop}`,
|
|
2962
3294
|
message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
|
|
2963
3295
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -2976,44 +3308,44 @@ function validateNavTargetRefs(stack) {
|
|
|
2976
3308
|
}
|
|
2977
3309
|
|
|
2978
3310
|
// src/validate-nav-object-servability.ts
|
|
2979
|
-
var
|
|
3311
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
2980
3312
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2981
|
-
var
|
|
3313
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2982
3314
|
function asArray14(v) {
|
|
2983
|
-
if (Array.isArray(v)) return v.filter(
|
|
2984
|
-
if (
|
|
3315
|
+
if (Array.isArray(v)) return v.filter(isRec9);
|
|
3316
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
|
|
2985
3317
|
return [];
|
|
2986
3318
|
}
|
|
2987
|
-
function
|
|
3319
|
+
function strName11(v) {
|
|
2988
3320
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2989
3321
|
}
|
|
2990
3322
|
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2991
3323
|
function validateNavObjectServability(stack) {
|
|
2992
3324
|
const findings = [];
|
|
2993
|
-
if (!
|
|
3325
|
+
if (!isRec9(stack)) return findings;
|
|
2994
3326
|
const apps = asArray14(stack.apps);
|
|
2995
3327
|
if (apps.length === 0) return findings;
|
|
2996
3328
|
const ownEnable = /* @__PURE__ */ new Map();
|
|
2997
3329
|
const objects = asArray14(stack.objects);
|
|
2998
3330
|
for (const [oi, obj] of objects.entries()) {
|
|
2999
|
-
const n =
|
|
3331
|
+
const n = strName11(obj.name);
|
|
3000
3332
|
if (!n) continue;
|
|
3001
3333
|
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
3002
3334
|
}
|
|
3003
3335
|
if (ownEnable.size === 0) return findings;
|
|
3004
3336
|
for (const [ai, app] of apps.entries()) {
|
|
3005
|
-
const appName =
|
|
3337
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
3006
3338
|
const walk = (items, basePath) => {
|
|
3007
3339
|
if (!Array.isArray(items)) return;
|
|
3008
3340
|
for (const [ni, raw] of items.entries()) {
|
|
3009
|
-
if (!
|
|
3341
|
+
if (!isRec9(raw)) continue;
|
|
3010
3342
|
const nav = raw;
|
|
3011
3343
|
const navPath = `${basePath}[${ni}]`;
|
|
3012
3344
|
if (nav.type === "object") {
|
|
3013
|
-
const target =
|
|
3345
|
+
const target = strName11(nav.objectName);
|
|
3014
3346
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3015
|
-
if (target && declared && !(0,
|
|
3016
|
-
const enable =
|
|
3347
|
+
if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
|
|
3348
|
+
const enable = isRec9(declared.enable) ? declared.enable : {};
|
|
3017
3349
|
const apiDisabled = enable.apiEnabled === false;
|
|
3018
3350
|
const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
|
|
3019
3351
|
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
@@ -3021,7 +3353,7 @@ function validateNavObjectServability(stack) {
|
|
|
3021
3353
|
findings.push({
|
|
3022
3354
|
severity: "error",
|
|
3023
3355
|
rule: NAV_OBJECT_UNSERVABLE,
|
|
3024
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3356
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
|
|
3025
3357
|
// The nav entry is where the dead row is authored; the `enable`
|
|
3026
3358
|
// key that condemns it is named in the message, because the fix
|
|
3027
3359
|
// may belong at either end.
|
|
@@ -3048,27 +3380,27 @@ var import_spec = require("@objectstack/spec");
|
|
|
3048
3380
|
var import_system4 = require("@objectstack/spec/system");
|
|
3049
3381
|
|
|
3050
3382
|
// src/view-walk.ts
|
|
3051
|
-
function
|
|
3383
|
+
function isRec10(v) {
|
|
3052
3384
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3053
3385
|
}
|
|
3054
|
-
function
|
|
3386
|
+
function strName12(v) {
|
|
3055
3387
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3056
3388
|
}
|
|
3057
3389
|
function viewObjectName(view) {
|
|
3058
|
-
return
|
|
3390
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
|
|
3059
3391
|
}
|
|
3060
3392
|
function viewContainerSites(view, basePath) {
|
|
3061
|
-
if (!
|
|
3393
|
+
if (!isRec10(view)) return [];
|
|
3062
3394
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3063
|
-
if (
|
|
3395
|
+
if (isRec10(view.form)) {
|
|
3064
3396
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3065
3397
|
}
|
|
3066
3398
|
for (const key of ["listViews", "formViews"]) {
|
|
3067
3399
|
const container = view[key];
|
|
3068
|
-
if (!
|
|
3400
|
+
if (!isRec10(container)) continue;
|
|
3069
3401
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
3070
3402
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
3071
|
-
if (!
|
|
3403
|
+
if (!isRec10(sub)) continue;
|
|
3072
3404
|
sites.push({
|
|
3073
3405
|
view: sub,
|
|
3074
3406
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -3086,18 +3418,18 @@ function formViewSites(view, basePath) {
|
|
|
3086
3418
|
// src/validate-translation-references.ts
|
|
3087
3419
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3088
3420
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3089
|
-
function
|
|
3421
|
+
function isRec11(v) {
|
|
3090
3422
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3091
3423
|
}
|
|
3092
3424
|
function asArray15(v) {
|
|
3093
3425
|
if (Array.isArray(v)) return v;
|
|
3094
|
-
if (
|
|
3426
|
+
if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
|
|
3095
3427
|
return [];
|
|
3096
3428
|
}
|
|
3097
|
-
function
|
|
3429
|
+
function strName13(v) {
|
|
3098
3430
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3099
3431
|
}
|
|
3100
|
-
function
|
|
3432
|
+
function distance6(a, b) {
|
|
3101
3433
|
const m = a.length;
|
|
3102
3434
|
const n = b.length;
|
|
3103
3435
|
if (m === 0) return n;
|
|
@@ -3113,7 +3445,7 @@ function distance5(a, b) {
|
|
|
3113
3445
|
}
|
|
3114
3446
|
return prev[n];
|
|
3115
3447
|
}
|
|
3116
|
-
function
|
|
3448
|
+
function suggest7(target, known) {
|
|
3117
3449
|
const names = [...known];
|
|
3118
3450
|
const segmentMatch = names.find(
|
|
3119
3451
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -3122,7 +3454,7 @@ function suggest6(target, known) {
|
|
|
3122
3454
|
let best;
|
|
3123
3455
|
let bestScore = Infinity;
|
|
3124
3456
|
for (const candidate of names) {
|
|
3125
|
-
const d =
|
|
3457
|
+
const d = distance6(target, candidate);
|
|
3126
3458
|
if (d < bestScore) {
|
|
3127
3459
|
bestScore = d;
|
|
3128
3460
|
best = candidate;
|
|
@@ -3155,33 +3487,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
3155
3487
|
const addSections = (container, binding) => {
|
|
3156
3488
|
if (!binding) return;
|
|
3157
3489
|
for (const section of asArray15(container.sections)) {
|
|
3158
|
-
const sectionName =
|
|
3490
|
+
const sectionName = strName13(section.name);
|
|
3159
3491
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
3160
3492
|
}
|
|
3161
3493
|
};
|
|
3162
|
-
const listBinding =
|
|
3163
|
-
if (
|
|
3164
|
-
addView(recordObject ?? listBinding,
|
|
3494
|
+
const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
|
|
3495
|
+
if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3496
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
3165
3497
|
const named = namedViewKeys(view);
|
|
3166
3498
|
for (const family of ["listViews", "formViews"]) {
|
|
3167
3499
|
const container = view[family];
|
|
3168
|
-
if (!
|
|
3500
|
+
if (!isRec11(container)) continue;
|
|
3169
3501
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3170
3502
|
let at = 0;
|
|
3171
3503
|
for (const sub of Object.values(container)) {
|
|
3172
3504
|
if (!sub || typeof sub !== "object") continue;
|
|
3173
3505
|
const registryKey = registryKeys[at++];
|
|
3174
|
-
if (!
|
|
3506
|
+
if (!isRec11(sub)) continue;
|
|
3175
3507
|
const binding = bindingOf(sub) ?? listBinding;
|
|
3176
3508
|
addView(binding, registryKey);
|
|
3177
3509
|
addSections(sub, binding);
|
|
3178
3510
|
}
|
|
3179
3511
|
}
|
|
3180
|
-
if (
|
|
3512
|
+
if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
3181
3513
|
addSections(view, recordObject ?? listBinding);
|
|
3182
3514
|
}
|
|
3183
3515
|
function defaultListViewKey(object, container) {
|
|
3184
|
-
if (!object || !
|
|
3516
|
+
if (!object || !isRec11(container.list)) return void 0;
|
|
3185
3517
|
const item = (0, import_spec.expandViewContainer)(object, container).find(
|
|
3186
3518
|
(i) => i.viewKind === "list" && i.isDefault
|
|
3187
3519
|
);
|
|
@@ -3193,7 +3525,7 @@ function namedViewKeys(container) {
|
|
|
3193
3525
|
const object = "probe";
|
|
3194
3526
|
const prefix = `${object}.`;
|
|
3195
3527
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3196
|
-
const countEntries = (v) =>
|
|
3528
|
+
const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3197
3529
|
const listCount = countEntries(container.listViews);
|
|
3198
3530
|
const formCount = countEntries(container.formViews);
|
|
3199
3531
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -3211,14 +3543,14 @@ function readOptions(field) {
|
|
|
3211
3543
|
values.add(opt);
|
|
3212
3544
|
continue;
|
|
3213
3545
|
}
|
|
3214
|
-
if (!
|
|
3215
|
-
const value =
|
|
3546
|
+
if (!isRec11(opt)) continue;
|
|
3547
|
+
const value = strName13(opt.value);
|
|
3216
3548
|
if (!value) continue;
|
|
3217
3549
|
values.add(value);
|
|
3218
|
-
const label2 =
|
|
3550
|
+
const label2 = strName13(opt.label);
|
|
3219
3551
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
3220
3552
|
}
|
|
3221
|
-
} else if (
|
|
3553
|
+
} else if (isRec11(raw)) {
|
|
3222
3554
|
for (const [value, label2] of Object.entries(raw)) {
|
|
3223
3555
|
values.add(value);
|
|
3224
3556
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -3239,23 +3571,23 @@ function buildUniverse(stack) {
|
|
|
3239
3571
|
return facts;
|
|
3240
3572
|
};
|
|
3241
3573
|
for (const obj of asArray15(stack.objects)) {
|
|
3242
|
-
const objectName =
|
|
3574
|
+
const objectName = strName13(obj.name);
|
|
3243
3575
|
if (!objectName) continue;
|
|
3244
3576
|
const facts = factsFor(objectName);
|
|
3245
3577
|
for (const field of asArray15(obj.fields)) {
|
|
3246
|
-
const fieldName =
|
|
3578
|
+
const fieldName = strName13(field.name);
|
|
3247
3579
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
3248
3580
|
}
|
|
3249
3581
|
for (const action of asArray15(obj.actions)) {
|
|
3250
|
-
const actionName =
|
|
3582
|
+
const actionName = strName13(action.name);
|
|
3251
3583
|
if (actionName) facts.actions.set(actionName, action);
|
|
3252
3584
|
}
|
|
3253
3585
|
for (const view of asArray15(obj.views)) {
|
|
3254
|
-
collectViewRecord({ ...view, object:
|
|
3586
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
3255
3587
|
}
|
|
3256
3588
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3257
3589
|
for (const group of asArray15(obj.fieldGroups)) {
|
|
3258
|
-
const key =
|
|
3590
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
3259
3591
|
if (key) facts.sections.add(key);
|
|
3260
3592
|
}
|
|
3261
3593
|
}
|
|
@@ -3266,10 +3598,10 @@ function buildUniverse(stack) {
|
|
|
3266
3598
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3267
3599
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3268
3600
|
if (!walked.objectName) continue;
|
|
3269
|
-
const props =
|
|
3601
|
+
const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
|
|
3270
3602
|
if (!props) continue;
|
|
3271
3603
|
for (const section of asArray15(props.sections)) {
|
|
3272
|
-
const sectionName =
|
|
3604
|
+
const sectionName = strName13(section.name);
|
|
3273
3605
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3274
3606
|
}
|
|
3275
3607
|
}
|
|
@@ -3277,9 +3609,9 @@ function buildUniverse(stack) {
|
|
|
3277
3609
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3278
3610
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3279
3611
|
for (const action of asArray15(stack.actions)) {
|
|
3280
|
-
const actionName =
|
|
3612
|
+
const actionName = strName13(action.name);
|
|
3281
3613
|
if (!actionName) continue;
|
|
3282
|
-
const owner =
|
|
3614
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
3283
3615
|
if (owner) {
|
|
3284
3616
|
factsFor(owner).actions.set(actionName, action);
|
|
3285
3617
|
actionOwners.set(actionName, owner);
|
|
@@ -3294,19 +3626,19 @@ function buildUniverse(stack) {
|
|
|
3294
3626
|
}
|
|
3295
3627
|
const apps = /* @__PURE__ */ new Map();
|
|
3296
3628
|
for (const app of asArray15(stack.apps)) {
|
|
3297
|
-
const appName =
|
|
3629
|
+
const appName = strName13(app.name);
|
|
3298
3630
|
if (!appName) continue;
|
|
3299
3631
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3300
3632
|
const walkNav = (items) => {
|
|
3301
3633
|
for (const item of asArray15(items)) {
|
|
3302
|
-
const id =
|
|
3634
|
+
const id = strName13(item.id);
|
|
3303
3635
|
if (id) navIds.add(id);
|
|
3304
3636
|
if (item.children) walkNav(item.children);
|
|
3305
3637
|
}
|
|
3306
3638
|
};
|
|
3307
3639
|
walkNav(app.navigation);
|
|
3308
3640
|
for (const area of asArray15(app.areas)) {
|
|
3309
|
-
const areaId =
|
|
3641
|
+
const areaId = strName13(area.id);
|
|
3310
3642
|
if (areaId) navIds.add(areaId);
|
|
3311
3643
|
walkNav(area.navigation);
|
|
3312
3644
|
}
|
|
@@ -3314,20 +3646,20 @@ function buildUniverse(stack) {
|
|
|
3314
3646
|
}
|
|
3315
3647
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3316
3648
|
for (const dash of asArray15(stack.dashboards)) {
|
|
3317
|
-
const dashName =
|
|
3649
|
+
const dashName = strName13(dash.name);
|
|
3318
3650
|
if (!dashName) continue;
|
|
3319
3651
|
const widgets = /* @__PURE__ */ new Set();
|
|
3320
3652
|
for (const widget of asArray15(dash.widgets)) {
|
|
3321
|
-
const id =
|
|
3653
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
3322
3654
|
if (id) widgets.add(id);
|
|
3323
3655
|
}
|
|
3324
3656
|
const actions = /* @__PURE__ */ new Set();
|
|
3325
3657
|
const headerActions = [
|
|
3326
|
-
...asArray15(
|
|
3658
|
+
...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
|
|
3327
3659
|
...asArray15(dash.actions)
|
|
3328
3660
|
];
|
|
3329
3661
|
for (const action of headerActions) {
|
|
3330
|
-
const key =
|
|
3662
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
3331
3663
|
if (key) actions.add(key);
|
|
3332
3664
|
}
|
|
3333
3665
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3339,7 +3671,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3339
3671
|
}
|
|
3340
3672
|
function validateTranslationReferences(stack) {
|
|
3341
3673
|
const findings = [];
|
|
3342
|
-
if (!
|
|
3674
|
+
if (!isRec11(stack)) return findings;
|
|
3343
3675
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3344
3676
|
if (bundles.length === 0) return findings;
|
|
3345
3677
|
const universe = buildUniverse(stack);
|
|
@@ -3348,13 +3680,13 @@ function validateTranslationReferences(stack) {
|
|
|
3348
3680
|
};
|
|
3349
3681
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3350
3682
|
const bundle = bundles[bi];
|
|
3351
|
-
if (!
|
|
3683
|
+
if (!isRec11(bundle)) continue;
|
|
3352
3684
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3353
|
-
if (!
|
|
3685
|
+
if (!isRec11(rawData)) continue;
|
|
3354
3686
|
const base = localePath(bi, locale);
|
|
3355
3687
|
const inLocale = `locale "${locale}"`;
|
|
3356
3688
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3357
|
-
if (!
|
|
3689
|
+
if (!isRec11(rawNode)) continue;
|
|
3358
3690
|
const objPath = `${base}.objects.${objectName}`;
|
|
3359
3691
|
const facts = universe.objects.get(objectName);
|
|
3360
3692
|
if (!facts) {
|
|
@@ -3362,7 +3694,7 @@ function validateTranslationReferences(stack) {
|
|
|
3362
3694
|
orphan(
|
|
3363
3695
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
3364
3696
|
objPath,
|
|
3365
|
-
(0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` +
|
|
3697
|
+
(0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest7(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest7(objectName, universe.objects.keys()),
|
|
3366
3698
|
`Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
|
|
3367
3699
|
);
|
|
3368
3700
|
continue;
|
|
@@ -3375,12 +3707,12 @@ function validateTranslationReferences(stack) {
|
|
|
3375
3707
|
orphan(
|
|
3376
3708
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
3377
3709
|
fieldPath,
|
|
3378
|
-
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` +
|
|
3710
|
+
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest7(fieldName, facts.fields.keys()),
|
|
3379
3711
|
`Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
|
|
3380
3712
|
);
|
|
3381
3713
|
continue;
|
|
3382
3714
|
}
|
|
3383
|
-
if (!
|
|
3715
|
+
if (!isRec11(rawField)) continue;
|
|
3384
3716
|
checkOptionKeys(findings, {
|
|
3385
3717
|
optionMap: rawField.options,
|
|
3386
3718
|
field,
|
|
@@ -3395,7 +3727,7 @@ function validateTranslationReferences(stack) {
|
|
|
3395
3727
|
orphan(
|
|
3396
3728
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
3397
3729
|
`${objPath}._views.${viewName}`,
|
|
3398
|
-
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` +
|
|
3730
|
+
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest7(viewName, facts.views),
|
|
3399
3731
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
3400
3732
|
);
|
|
3401
3733
|
}
|
|
@@ -3404,7 +3736,7 @@ function validateTranslationReferences(stack) {
|
|
|
3404
3736
|
orphan(
|
|
3405
3737
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
3406
3738
|
`${objPath}._sections.${sectionName}`,
|
|
3407
|
-
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` +
|
|
3739
|
+
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest7(sectionName, facts.sections),
|
|
3408
3740
|
`Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
|
|
3409
3741
|
);
|
|
3410
3742
|
}
|
|
@@ -3415,7 +3747,7 @@ function validateTranslationReferences(stack) {
|
|
|
3415
3747
|
orphan(
|
|
3416
3748
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
3417
3749
|
actionPath,
|
|
3418
|
-
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` +
|
|
3750
|
+
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest7(actionName, facts.actions.keys()),
|
|
3419
3751
|
`Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
|
|
3420
3752
|
);
|
|
3421
3753
|
continue;
|
|
@@ -3437,7 +3769,7 @@ function validateTranslationReferences(stack) {
|
|
|
3437
3769
|
orphan(
|
|
3438
3770
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
3439
3771
|
actionPath,
|
|
3440
|
-
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` +
|
|
3772
|
+
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest7(actionName, universe.globalActions.keys()),
|
|
3441
3773
|
owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
|
|
3442
3774
|
);
|
|
3443
3775
|
continue;
|
|
@@ -3457,18 +3789,18 @@ function validateTranslationReferences(stack) {
|
|
|
3457
3789
|
orphan(
|
|
3458
3790
|
`${inLocale} \xB7 app "${appName}"`,
|
|
3459
3791
|
appPath,
|
|
3460
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
3792
|
+
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest7(appName, universe.apps.keys()),
|
|
3461
3793
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
3462
3794
|
);
|
|
3463
3795
|
continue;
|
|
3464
3796
|
}
|
|
3465
|
-
if (!
|
|
3797
|
+
if (!isRec11(rawApp)) continue;
|
|
3466
3798
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3467
3799
|
if (navIds.has(navId)) continue;
|
|
3468
3800
|
orphan(
|
|
3469
3801
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
3470
3802
|
`${appPath}.navigation.${navId}`,
|
|
3471
|
-
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` +
|
|
3803
|
+
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
|
|
3472
3804
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
3473
3805
|
);
|
|
3474
3806
|
}
|
|
@@ -3480,18 +3812,18 @@ function validateTranslationReferences(stack) {
|
|
|
3480
3812
|
orphan(
|
|
3481
3813
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
3482
3814
|
dashPath,
|
|
3483
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
3815
|
+
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest7(dashName, universe.dashboards.keys()),
|
|
3484
3816
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
3485
3817
|
);
|
|
3486
3818
|
continue;
|
|
3487
3819
|
}
|
|
3488
|
-
if (!
|
|
3820
|
+
if (!isRec11(rawDash)) continue;
|
|
3489
3821
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3490
3822
|
if (dash.widgets.has(widgetId)) continue;
|
|
3491
3823
|
orphan(
|
|
3492
3824
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
3493
3825
|
`${dashPath}.widgets.${widgetId}`,
|
|
3494
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
3826
|
+
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
|
|
3495
3827
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
3496
3828
|
);
|
|
3497
3829
|
}
|
|
@@ -3500,7 +3832,7 @@ function validateTranslationReferences(stack) {
|
|
|
3500
3832
|
orphan(
|
|
3501
3833
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
3502
3834
|
`${dashPath}.actions.${actionKey}`,
|
|
3503
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
3835
|
+
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
|
|
3504
3836
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
3505
3837
|
);
|
|
3506
3838
|
}
|
|
@@ -3510,7 +3842,7 @@ function validateTranslationReferences(stack) {
|
|
|
3510
3842
|
return findings;
|
|
3511
3843
|
}
|
|
3512
3844
|
function asRecord(v) {
|
|
3513
|
-
return
|
|
3845
|
+
return isRec11(v) ? v : {};
|
|
3514
3846
|
}
|
|
3515
3847
|
function checkOptionKeys(findings, ctx) {
|
|
3516
3848
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3522,7 +3854,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3522
3854
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3523
3855
|
where: ctx.where,
|
|
3524
3856
|
path: ctx.path,
|
|
3525
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3857
|
+
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.`,
|
|
3526
3858
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3527
3859
|
});
|
|
3528
3860
|
return;
|
|
@@ -3535,17 +3867,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3535
3867
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3536
3868
|
where: ctx.where,
|
|
3537
3869
|
path: `${ctx.path}.${key}`,
|
|
3538
|
-
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` +
|
|
3870
|
+
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest7(key, declared.values),
|
|
3539
3871
|
hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
|
|
3540
3872
|
});
|
|
3541
3873
|
}
|
|
3542
3874
|
}
|
|
3543
3875
|
function checkActionParams(findings, ctx) {
|
|
3544
|
-
const rawParams = Object.keys(asRecord(
|
|
3876
|
+
const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3545
3877
|
if (rawParams.length === 0) return;
|
|
3546
3878
|
const declared = /* @__PURE__ */ new Set();
|
|
3547
3879
|
for (const param of asArray15(ctx.action.params)) {
|
|
3548
|
-
const name =
|
|
3880
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
3549
3881
|
if (name) declared.add(name);
|
|
3550
3882
|
}
|
|
3551
3883
|
for (const paramName of rawParams) {
|
|
@@ -3555,40 +3887,40 @@ function checkActionParams(findings, ctx) {
|
|
|
3555
3887
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
3556
3888
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
3557
3889
|
path: `${ctx.path}.params.${paramName}`,
|
|
3558
|
-
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` +
|
|
3890
|
+
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest7(paramName, declared),
|
|
3559
3891
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
3560
3892
|
});
|
|
3561
3893
|
}
|
|
3562
3894
|
}
|
|
3563
3895
|
|
|
3564
3896
|
// src/collection-entries.ts
|
|
3565
|
-
function
|
|
3897
|
+
function isRec12(v) {
|
|
3566
3898
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3567
3899
|
}
|
|
3568
3900
|
function collectionEntries(v, base) {
|
|
3569
3901
|
if (Array.isArray(v)) {
|
|
3570
3902
|
const out = [];
|
|
3571
3903
|
for (let i = 0; i < v.length; i++) {
|
|
3572
|
-
if (
|
|
3904
|
+
if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3573
3905
|
}
|
|
3574
3906
|
return out;
|
|
3575
3907
|
}
|
|
3576
|
-
if (
|
|
3577
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3908
|
+
if (isRec12(v)) {
|
|
3909
|
+
return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3578
3910
|
}
|
|
3579
3911
|
return [];
|
|
3580
3912
|
}
|
|
3581
3913
|
|
|
3582
3914
|
// src/validate-translatable-sections.ts
|
|
3583
3915
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3584
|
-
function
|
|
3916
|
+
function isRec13(v) {
|
|
3585
3917
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3586
3918
|
}
|
|
3587
|
-
function
|
|
3919
|
+
function strName14(v) {
|
|
3588
3920
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3589
3921
|
}
|
|
3590
3922
|
function viewLabel(view) {
|
|
3591
|
-
const name =
|
|
3923
|
+
const name = strName14(view.name);
|
|
3592
3924
|
return name ? `view "${name}"` : "";
|
|
3593
3925
|
}
|
|
3594
3926
|
function joinWhere(...parts) {
|
|
@@ -3596,7 +3928,7 @@ function joinWhere(...parts) {
|
|
|
3596
3928
|
}
|
|
3597
3929
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3598
3930
|
const recordObject = viewObjectName(view);
|
|
3599
|
-
const listBinding =
|
|
3931
|
+
const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3600
3932
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3601
3933
|
sites.push({
|
|
3602
3934
|
path: `${site.path}.sections`,
|
|
@@ -3610,11 +3942,11 @@ function translatedObjectNames(stack) {
|
|
|
3610
3942
|
const out = /* @__PURE__ */ new Set();
|
|
3611
3943
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3612
3944
|
for (const bundle of bundles) {
|
|
3613
|
-
if (!
|
|
3945
|
+
if (!isRec13(bundle)) continue;
|
|
3614
3946
|
for (const data of Object.values(bundle)) {
|
|
3615
|
-
if (!
|
|
3947
|
+
if (!isRec13(data) || !isRec13(data.objects)) continue;
|
|
3616
3948
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3617
|
-
if (
|
|
3949
|
+
if (isRec13(node)) out.add(objectName);
|
|
3618
3950
|
}
|
|
3619
3951
|
}
|
|
3620
3952
|
}
|
|
@@ -3626,22 +3958,22 @@ function suggestedName(label2) {
|
|
|
3626
3958
|
}
|
|
3627
3959
|
function validateTranslatableSections(stack) {
|
|
3628
3960
|
const findings = [];
|
|
3629
|
-
if (!
|
|
3961
|
+
if (!isRec13(stack)) return findings;
|
|
3630
3962
|
const translated = translatedObjectNames(stack);
|
|
3631
3963
|
if (translated.size === 0) return findings;
|
|
3632
3964
|
const sites = [];
|
|
3633
3965
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3634
|
-
const objectName =
|
|
3966
|
+
const objectName = strName14(obj.name);
|
|
3635
3967
|
if (!objectName) continue;
|
|
3636
3968
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3637
3969
|
collectViewSites(
|
|
3638
|
-
{ ...view, object:
|
|
3970
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
3639
3971
|
path,
|
|
3640
3972
|
viewLabel(view),
|
|
3641
3973
|
sites
|
|
3642
3974
|
);
|
|
3643
3975
|
}
|
|
3644
|
-
if (
|
|
3976
|
+
if (isRec13(obj.listViews)) {
|
|
3645
3977
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3646
3978
|
}
|
|
3647
3979
|
}
|
|
@@ -3649,13 +3981,13 @@ function validateTranslatableSections(stack) {
|
|
|
3649
3981
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3650
3982
|
}
|
|
3651
3983
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3652
|
-
const pageName =
|
|
3984
|
+
const pageName = strName14(page.name);
|
|
3653
3985
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3654
3986
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3655
3987
|
if (!walked.objectName) continue;
|
|
3656
|
-
const props =
|
|
3988
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
3657
3989
|
if (!props) continue;
|
|
3658
|
-
const type =
|
|
3990
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
3659
3991
|
sites.push({
|
|
3660
3992
|
path: `${walked.path}.properties.sections`,
|
|
3661
3993
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3670,9 +4002,9 @@ function validateTranslatableSections(stack) {
|
|
|
3670
4002
|
if (!Array.isArray(site.sections)) continue;
|
|
3671
4003
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3672
4004
|
const section = site.sections[i];
|
|
3673
|
-
if (!
|
|
3674
|
-
if (
|
|
3675
|
-
const heading =
|
|
4005
|
+
if (!isRec13(section)) continue;
|
|
4006
|
+
if (strName14(section.name)) continue;
|
|
4007
|
+
const heading = strName14(section.label);
|
|
3676
4008
|
if (!heading) continue;
|
|
3677
4009
|
const slug = suggestedName(heading);
|
|
3678
4010
|
findings.push({
|
|
@@ -3690,10 +4022,10 @@ function validateTranslatableSections(stack) {
|
|
|
3690
4022
|
|
|
3691
4023
|
// src/flow-walk.ts
|
|
3692
4024
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
3693
|
-
function
|
|
4025
|
+
function isRec14(v) {
|
|
3694
4026
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3695
4027
|
}
|
|
3696
|
-
function
|
|
4028
|
+
function strName15(v) {
|
|
3697
4029
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3698
4030
|
}
|
|
3699
4031
|
var REGION_SLOTS = new Map(
|
|
@@ -3702,10 +4034,10 @@ var REGION_SLOTS = new Map(
|
|
|
3702
4034
|
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
3703
4035
|
var MAX_REGION_DEPTH = 16;
|
|
3704
4036
|
function flowNodeLabel(node, index) {
|
|
3705
|
-
return
|
|
4037
|
+
return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
|
|
3706
4038
|
}
|
|
3707
4039
|
function stripRegions(config) {
|
|
3708
|
-
if (!
|
|
4040
|
+
if (!isRec14(config)) return void 0;
|
|
3709
4041
|
let out;
|
|
3710
4042
|
for (const key of Object.keys(config)) {
|
|
3711
4043
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3716,11 +4048,11 @@ function stripRegions(config) {
|
|
|
3716
4048
|
}
|
|
3717
4049
|
function walkFlowNodes(flow, flowPath) {
|
|
3718
4050
|
const out = [];
|
|
3719
|
-
if (!
|
|
4051
|
+
if (!isRec14(flow)) return out;
|
|
3720
4052
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3721
4053
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3722
4054
|
nodes.forEach((raw, index) => {
|
|
3723
|
-
if (!
|
|
4055
|
+
if (!isRec14(raw)) return;
|
|
3724
4056
|
const path = `${basePath}[${index}]`;
|
|
3725
4057
|
out.push({
|
|
3726
4058
|
node: raw,
|
|
@@ -3729,9 +4061,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3729
4061
|
regionTrail: trail,
|
|
3730
4062
|
depth
|
|
3731
4063
|
});
|
|
3732
|
-
const type =
|
|
4064
|
+
const type = strName15(raw.type);
|
|
3733
4065
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3734
|
-
if (!slots || !
|
|
4066
|
+
if (!slots || !isRec14(raw.config)) return;
|
|
3735
4067
|
const config = raw.config;
|
|
3736
4068
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3737
4069
|
for (const slot of slots) {
|
|
@@ -3739,8 +4071,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3739
4071
|
if (slot === "branches") {
|
|
3740
4072
|
if (!Array.isArray(value)) continue;
|
|
3741
4073
|
value.forEach((branch, b) => {
|
|
3742
|
-
if (!
|
|
3743
|
-
const branchName =
|
|
4074
|
+
if (!isRec14(branch)) return;
|
|
4075
|
+
const branchName = strName15(branch.name) ?? `#${b}`;
|
|
3744
4076
|
visitList(
|
|
3745
4077
|
branch.nodes,
|
|
3746
4078
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3750,7 +4082,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3750
4082
|
});
|
|
3751
4083
|
continue;
|
|
3752
4084
|
}
|
|
3753
|
-
if (!
|
|
4085
|
+
if (!isRec14(value)) continue;
|
|
3754
4086
|
visitList(
|
|
3755
4087
|
value.nodes,
|
|
3756
4088
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3991,7 +4323,7 @@ function asArray17(v) {
|
|
|
3991
4323
|
}
|
|
3992
4324
|
return [];
|
|
3993
4325
|
}
|
|
3994
|
-
function
|
|
4326
|
+
function strName16(v) {
|
|
3995
4327
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3996
4328
|
}
|
|
3997
4329
|
function surfaceOf(v) {
|
|
@@ -4002,17 +4334,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4002
4334
|
if (!stack || typeof stack !== "object") return findings;
|
|
4003
4335
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
4004
4336
|
for (const skill of asArray17(stack.skills)) {
|
|
4005
|
-
const n =
|
|
4337
|
+
const n = strName16(skill.name);
|
|
4006
4338
|
if (n) skillsByName.set(n, skill);
|
|
4007
4339
|
}
|
|
4008
4340
|
const agents = asArray17(stack.agents);
|
|
4009
4341
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4010
4342
|
const agent = agents[ai];
|
|
4011
|
-
const agentName =
|
|
4343
|
+
const agentName = strName16(agent.name) ?? `#${ai}`;
|
|
4012
4344
|
const agentSurface = surfaceOf(agent.surface);
|
|
4013
4345
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4014
4346
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4015
|
-
const ref =
|
|
4347
|
+
const ref = strName16(skillRefs[si]);
|
|
4016
4348
|
if (!ref) continue;
|
|
4017
4349
|
const skill = skillsByName.get(ref);
|
|
4018
4350
|
if (!skill) continue;
|
|
@@ -4041,10 +4373,10 @@ function asArray18(v) {
|
|
|
4041
4373
|
}
|
|
4042
4374
|
return [];
|
|
4043
4375
|
}
|
|
4044
|
-
function
|
|
4376
|
+
function strName17(v) {
|
|
4045
4377
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4046
4378
|
}
|
|
4047
|
-
function
|
|
4379
|
+
function distance7(a, b) {
|
|
4048
4380
|
const m = a.length;
|
|
4049
4381
|
const n = b.length;
|
|
4050
4382
|
if (m === 0) return n;
|
|
@@ -4060,14 +4392,14 @@ function distance6(a, b) {
|
|
|
4060
4392
|
}
|
|
4061
4393
|
return prev[n];
|
|
4062
4394
|
}
|
|
4063
|
-
function
|
|
4395
|
+
function suggest8(target, known) {
|
|
4064
4396
|
for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4065
4397
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4066
4398
|
}
|
|
4067
4399
|
let best;
|
|
4068
4400
|
let bestScore = Infinity;
|
|
4069
4401
|
for (const candidate of known) {
|
|
4070
|
-
const d =
|
|
4402
|
+
const d = distance7(target, candidate);
|
|
4071
4403
|
if (d < bestScore) {
|
|
4072
4404
|
bestScore = d;
|
|
4073
4405
|
best = candidate;
|
|
@@ -4082,8 +4414,8 @@ function materialisesAsTool(action) {
|
|
|
4082
4414
|
if (!ai || typeof ai !== "object") return false;
|
|
4083
4415
|
const aiRec = ai;
|
|
4084
4416
|
if (aiRec.exposed !== true) return false;
|
|
4085
|
-
if (!
|
|
4086
|
-
const type =
|
|
4417
|
+
if (!strName17(aiRec.description)) return false;
|
|
4418
|
+
const type = strName17(action.type);
|
|
4087
4419
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4088
4420
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4089
4421
|
return Boolean(action.target);
|
|
@@ -4091,12 +4423,12 @@ function materialisesAsTool(action) {
|
|
|
4091
4423
|
function collectToolUniverse(stack) {
|
|
4092
4424
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
4093
4425
|
for (const tool of asArray18(stack.tools)) {
|
|
4094
|
-
const n =
|
|
4426
|
+
const n = strName17(tool.name);
|
|
4095
4427
|
if (n) universe.add(n);
|
|
4096
4428
|
}
|
|
4097
4429
|
const addActionFamily = (actions) => {
|
|
4098
4430
|
for (const action of asArray18(actions)) {
|
|
4099
|
-
const n =
|
|
4431
|
+
const n = strName17(action.name);
|
|
4100
4432
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4101
4433
|
}
|
|
4102
4434
|
};
|
|
@@ -4110,7 +4442,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
4110
4442
|
const names = /* @__PURE__ */ new Set();
|
|
4111
4443
|
const scan = (actions) => {
|
|
4112
4444
|
for (const action of asArray18(actions)) {
|
|
4113
|
-
const n =
|
|
4445
|
+
const n = strName17(action.name);
|
|
4114
4446
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4115
4447
|
}
|
|
4116
4448
|
};
|
|
@@ -4136,10 +4468,10 @@ function validateAiToolReferences(stack) {
|
|
|
4136
4468
|
const skills = asArray18(stack.skills);
|
|
4137
4469
|
for (let si = 0; si < skills.length; si++) {
|
|
4138
4470
|
const skill = skills[si];
|
|
4139
|
-
const skillName =
|
|
4471
|
+
const skillName = strName17(skill.name) ?? `#${si}`;
|
|
4140
4472
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4141
4473
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4142
|
-
const ref =
|
|
4474
|
+
const ref = strName17(refs[ti]);
|
|
4143
4475
|
if (!ref || resolves(ref)) continue;
|
|
4144
4476
|
const isPattern = ref.endsWith("*");
|
|
4145
4477
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4148,7 +4480,7 @@ function validateAiToolReferences(stack) {
|
|
|
4148
4480
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4149
4481
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4150
4482
|
path: `skills[${si}].tools[${ti}]`,
|
|
4151
|
-
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` +
|
|
4483
|
+
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest8(ref, universe),
|
|
4152
4484
|
hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
|
|
4153
4485
|
});
|
|
4154
4486
|
}
|
|
@@ -4166,7 +4498,7 @@ function asArray19(v) {
|
|
|
4166
4498
|
}
|
|
4167
4499
|
return [];
|
|
4168
4500
|
}
|
|
4169
|
-
function
|
|
4501
|
+
function strName18(v) {
|
|
4170
4502
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4171
4503
|
}
|
|
4172
4504
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -4176,7 +4508,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4176
4508
|
const agents = asArray19(stack.agents);
|
|
4177
4509
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4178
4510
|
const agent = agents[ai];
|
|
4179
|
-
const name =
|
|
4511
|
+
const name = strName18(agent.name) ?? `#${ai}`;
|
|
4180
4512
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4181
4513
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4182
4514
|
findings.push({
|
|
@@ -4192,9 +4524,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4192
4524
|
const apps = asArray19(stack.apps);
|
|
4193
4525
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4194
4526
|
const app = apps[appIdx];
|
|
4195
|
-
const defaultAgent =
|
|
4527
|
+
const defaultAgent = strName18(app.defaultAgent);
|
|
4196
4528
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4197
|
-
const appName =
|
|
4529
|
+
const appName = strName18(app.name) ?? `#${appIdx}`;
|
|
4198
4530
|
findings.push({
|
|
4199
4531
|
severity: "warning",
|
|
4200
4532
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4225,6 +4557,7 @@ function loadTypeScript() {
|
|
|
4225
4557
|
return cachedTs;
|
|
4226
4558
|
}
|
|
4227
4559
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4560
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4228
4561
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4229
4562
|
{
|
|
4230
4563
|
id: "input-property-assign",
|
|
@@ -4291,13 +4624,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4291
4624
|
"owner",
|
|
4292
4625
|
"record_type"
|
|
4293
4626
|
]);
|
|
4294
|
-
|
|
4627
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
4628
|
+
return `so the value can never land: the anchor exists only in the registered schema, which is what carries it PAST the write-path validator that refuses an undeclared name outright (INVALID_FIELD). The remote database is what rejects it \u2014 on a SQL remote with an untyped driver error ('no such column') that aborts the whole statement, so the correctly named fields in the same payload never land either; on a schemaless remote the key is persisted into a column no read surface returns (#4271).`;
|
|
4629
|
+
}
|
|
4630
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4295
4631
|
function asArray20(v) {
|
|
4296
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4297
|
-
if (
|
|
4632
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4633
|
+
if (isRec15(v)) {
|
|
4298
4634
|
return Object.entries(v).map(([name, def]) => ({
|
|
4299
4635
|
name,
|
|
4300
|
-
...
|
|
4636
|
+
...isRec15(def) ? def : {}
|
|
4301
4637
|
}));
|
|
4302
4638
|
}
|
|
4303
4639
|
return [];
|
|
@@ -4441,14 +4777,16 @@ function validateHookBodyWrites(stack) {
|
|
|
4441
4777
|
const hooks = asArray20(stack.hooks);
|
|
4442
4778
|
if (hooks.length === 0) return findings;
|
|
4443
4779
|
let objectFields = null;
|
|
4780
|
+
let anchors = null;
|
|
4444
4781
|
hooks.forEach((hook, hookIndex) => {
|
|
4445
4782
|
const body = hook.body;
|
|
4446
|
-
if (!
|
|
4783
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4447
4784
|
const source = body.source;
|
|
4448
4785
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4449
4786
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4450
4787
|
if (writes.length === 0) return;
|
|
4451
4788
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4789
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4452
4790
|
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4453
4791
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4454
4792
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
@@ -4463,7 +4801,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4463
4801
|
if (reported.has(dedupeKey)) continue;
|
|
4464
4802
|
if (w.object === void 0) {
|
|
4465
4803
|
if (!inputJudgeable) continue;
|
|
4466
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
4804
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4805
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
4806
|
+
reported.add(dedupeKey);
|
|
4807
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
4808
|
+
findings.push({
|
|
4809
|
+
severity: "warning",
|
|
4810
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4811
|
+
where,
|
|
4812
|
+
path,
|
|
4813
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
4814
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
4815
|
+
});
|
|
4816
|
+
continue;
|
|
4817
|
+
}
|
|
4467
4818
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
4468
4819
|
reported.add(dedupeKey);
|
|
4469
4820
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -4479,7 +4830,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4479
4830
|
} else {
|
|
4480
4831
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4481
4832
|
if (!known) continue;
|
|
4482
|
-
if (
|
|
4833
|
+
if (known.has(w.field)) continue;
|
|
4834
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4835
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4836
|
+
reported.add(dedupeKey);
|
|
4837
|
+
findings.push({
|
|
4838
|
+
severity: "warning",
|
|
4839
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4840
|
+
where,
|
|
4841
|
+
path,
|
|
4842
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4843
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4844
|
+
});
|
|
4845
|
+
continue;
|
|
4846
|
+
}
|
|
4483
4847
|
reported.add(dedupeKey);
|
|
4484
4848
|
findings.push({
|
|
4485
4849
|
severity: "warning",
|
|
@@ -4508,19 +4872,20 @@ function fixHint(field, declared) {
|
|
|
4508
4872
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
4509
4873
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4510
4874
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4875
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4511
4876
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4512
4877
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
4513
4878
|
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
4514
4879
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4515
4880
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4516
4881
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4517
|
-
var
|
|
4882
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4518
4883
|
function asArray21(v) {
|
|
4519
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4520
|
-
if (
|
|
4884
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4885
|
+
if (isRec16(v)) {
|
|
4521
4886
|
return Object.entries(v).map(([name, def]) => ({
|
|
4522
4887
|
name,
|
|
4523
|
-
...
|
|
4888
|
+
...isRec16(def) ? def : {}
|
|
4524
4889
|
}));
|
|
4525
4890
|
}
|
|
4526
4891
|
return [];
|
|
@@ -4538,7 +4903,7 @@ function collectActionBodies(stack) {
|
|
|
4538
4903
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4539
4904
|
if (type !== "script") return;
|
|
4540
4905
|
const body = action.body;
|
|
4541
|
-
if (!
|
|
4906
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
4542
4907
|
const source = body.source;
|
|
4543
4908
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4544
4909
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4557,10 +4922,11 @@ function collectActionBodies(stack) {
|
|
|
4557
4922
|
}
|
|
4558
4923
|
function validateActionBodyWrites(stack) {
|
|
4559
4924
|
const findings = [];
|
|
4560
|
-
if (!
|
|
4925
|
+
if (!isRec16(stack)) return findings;
|
|
4561
4926
|
const sites = collectActionBodies(stack);
|
|
4562
4927
|
if (sites.length === 0) return findings;
|
|
4563
4928
|
let objectFields = null;
|
|
4929
|
+
let anchors = null;
|
|
4564
4930
|
for (const site of sites) {
|
|
4565
4931
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4566
4932
|
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
@@ -4585,6 +4951,7 @@ function validateActionBodyWrites(stack) {
|
|
|
4585
4951
|
}
|
|
4586
4952
|
if (writes.length === 0) continue;
|
|
4587
4953
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4954
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4588
4955
|
const reported = /* @__PURE__ */ new Set();
|
|
4589
4956
|
for (const w of writes) {
|
|
4590
4957
|
if (w.object === void 0) continue;
|
|
@@ -4592,7 +4959,20 @@ function validateActionBodyWrites(stack) {
|
|
|
4592
4959
|
if (reported.has(dedupeKey)) continue;
|
|
4593
4960
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4594
4961
|
if (!known) continue;
|
|
4595
|
-
if (
|
|
4962
|
+
if (known.has(w.field)) continue;
|
|
4963
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4964
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4965
|
+
reported.add(dedupeKey);
|
|
4966
|
+
findings.push({
|
|
4967
|
+
severity: "warning",
|
|
4968
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4969
|
+
where,
|
|
4970
|
+
path: site.path,
|
|
4971
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4972
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4973
|
+
});
|
|
4974
|
+
continue;
|
|
4975
|
+
}
|
|
4596
4976
|
reported.add(dedupeKey);
|
|
4597
4977
|
findings.push({
|
|
4598
4978
|
severity: "warning",
|
|
@@ -4614,14 +4994,15 @@ function fixHint2(field, declared) {
|
|
|
4614
4994
|
// src/validate-flow-node-writes.ts
|
|
4615
4995
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
4616
4996
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4997
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
4617
4998
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4618
|
-
var
|
|
4999
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4619
5000
|
function asArray22(v) {
|
|
4620
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4621
|
-
if (
|
|
5001
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
5002
|
+
if (isRec17(v)) {
|
|
4622
5003
|
return Object.entries(v).map(([name, def]) => ({
|
|
4623
5004
|
name,
|
|
4624
|
-
...
|
|
5005
|
+
...isRec17(def) ? def : {}
|
|
4625
5006
|
}));
|
|
4626
5007
|
}
|
|
4627
5008
|
return [];
|
|
@@ -4634,30 +5015,44 @@ function readLiteralObjectName(config) {
|
|
|
4634
5015
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4635
5016
|
function validateFlowNodeWrites(stack) {
|
|
4636
5017
|
const findings = [];
|
|
4637
|
-
if (!
|
|
5018
|
+
if (!isRec17(stack)) return findings;
|
|
4638
5019
|
const flows = asArray22(stack.flows);
|
|
4639
5020
|
if (flows.length === 0) return findings;
|
|
4640
5021
|
let objectFields = null;
|
|
5022
|
+
let anchors = null;
|
|
4641
5023
|
flows.forEach((flow, flowIndex) => {
|
|
4642
5024
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
4643
5025
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4644
5026
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4645
5027
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4646
|
-
const config =
|
|
5028
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
4647
5029
|
if (!config) return;
|
|
4648
5030
|
const fields = config.fields;
|
|
4649
|
-
if (!
|
|
5031
|
+
if (!isRec17(fields)) return;
|
|
4650
5032
|
const written = Object.keys(fields);
|
|
4651
5033
|
if (written.length === 0) return;
|
|
4652
5034
|
const objectName = readLiteralObjectName(config);
|
|
4653
5035
|
if (!objectName) return;
|
|
4654
5036
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5037
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4655
5038
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
4656
5039
|
if (!known) return;
|
|
4657
5040
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
4658
5041
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
4659
5042
|
for (const fieldName of written) {
|
|
4660
|
-
if (known.has(fieldName)
|
|
5043
|
+
if (known.has(fieldName)) continue;
|
|
5044
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
5045
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
5046
|
+
findings.push({
|
|
5047
|
+
severity: "warning",
|
|
5048
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
5049
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5050
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5051
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
5052
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
5053
|
+
});
|
|
5054
|
+
continue;
|
|
5055
|
+
}
|
|
4661
5056
|
if (fieldName.includes(".")) continue;
|
|
4662
5057
|
findings.push({
|
|
4663
5058
|
severity: "error",
|
|
@@ -4770,15 +5165,15 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
4770
5165
|
|
|
4771
5166
|
// src/validate-react-page-props.ts
|
|
4772
5167
|
var import_node_module2 = require("module");
|
|
4773
|
-
var
|
|
4774
|
-
var
|
|
5168
|
+
var import_ui3 = require("@objectstack/spec/ui");
|
|
5169
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
4775
5170
|
|
|
4776
5171
|
// src/zod-issue-format.ts
|
|
4777
|
-
var
|
|
5172
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4778
5173
|
var valueAtPath = (root, path) => {
|
|
4779
5174
|
let cur = root;
|
|
4780
5175
|
for (const key of path) {
|
|
4781
|
-
if (!
|
|
5176
|
+
if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
|
|
4782
5177
|
cur = cur[key];
|
|
4783
5178
|
}
|
|
4784
5179
|
return cur;
|
|
@@ -4826,7 +5221,7 @@ function loadTypeScript2() {
|
|
|
4826
5221
|
}
|
|
4827
5222
|
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4828
5223
|
var BLOCKS = new Map(
|
|
4829
|
-
|
|
5224
|
+
import_ui3.REACT_BLOCKS.map((b) => [
|
|
4830
5225
|
b.tag,
|
|
4831
5226
|
{
|
|
4832
5227
|
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
@@ -4918,7 +5313,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4918
5313
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4919
5314
|
function checkChartDrillDown(raw, push2) {
|
|
4920
5315
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4921
|
-
if (!
|
|
5316
|
+
if (!isRec19(raw)) {
|
|
4922
5317
|
push2(
|
|
4923
5318
|
"error",
|
|
4924
5319
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4927,7 +5322,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4927
5322
|
);
|
|
4928
5323
|
return;
|
|
4929
5324
|
}
|
|
4930
|
-
const parsed =
|
|
5325
|
+
const parsed = import_ui3.ChartDrillDownSchema.safeParse(raw);
|
|
4931
5326
|
if (parsed.success) return;
|
|
4932
5327
|
for (const issue of parsed.error.issues) {
|
|
4933
5328
|
const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
|
|
@@ -4941,7 +5336,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4941
5336
|
}
|
|
4942
5337
|
function checkChartAggregate(raw, push2) {
|
|
4943
5338
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4944
|
-
if (!
|
|
5339
|
+
if (!isRec19(raw)) {
|
|
4945
5340
|
push2(
|
|
4946
5341
|
"error",
|
|
4947
5342
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4959,7 +5354,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4959
5354
|
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
|
|
4960
5355
|
);
|
|
4961
5356
|
}
|
|
4962
|
-
const parsed =
|
|
5357
|
+
const parsed = import_ui3.ChartAggregateSchema.safeParse(raw);
|
|
4963
5358
|
if (parsed.success) return;
|
|
4964
5359
|
for (const issue of parsed.error.issues) {
|
|
4965
5360
|
if (groupByAbsent && issue.path[0] === "groupBy") continue;
|
|
@@ -4972,7 +5367,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4972
5367
|
);
|
|
4973
5368
|
}
|
|
4974
5369
|
}
|
|
4975
|
-
var
|
|
5370
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4976
5371
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4977
5372
|
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4978
5373
|
const { values, where, path } = attrs;
|
|
@@ -4982,11 +5377,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
4982
5377
|
const aggregate = values.get("aggregate");
|
|
4983
5378
|
checkChartAggregate(aggregate, push2);
|
|
4984
5379
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4985
|
-
if (!
|
|
5380
|
+
if (!isRec19(aggregate)) return;
|
|
4986
5381
|
const fn = strOf(aggregate.function);
|
|
4987
5382
|
const field = strOf(aggregate.field);
|
|
4988
5383
|
const groupBy = aggregate.groupBy;
|
|
4989
|
-
const groupByField = strOf(groupBy) ?? (
|
|
5384
|
+
const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4990
5385
|
const objectName = strOf(values.get("objectName"));
|
|
4991
5386
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4992
5387
|
if (objectName && known) {
|
|
@@ -5015,7 +5410,7 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5015
5410
|
fieldRef(field, "field");
|
|
5016
5411
|
fieldRef(groupByField, "groupBy");
|
|
5017
5412
|
}
|
|
5018
|
-
const keys = (0,
|
|
5413
|
+
const keys = (0, import_ui3.chartAggregateResultKeys)({ field, function: fn, groupBy });
|
|
5019
5414
|
const columns = [keys.category, keys.value].filter((k) => !!k);
|
|
5020
5415
|
if (columns.length === 0) return;
|
|
5021
5416
|
const axisRef = (name, prop) => {
|
|
@@ -5030,18 +5425,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5030
5425
|
);
|
|
5031
5426
|
};
|
|
5032
5427
|
const xAxisRaw = values.get("xAxis");
|
|
5033
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5428
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
5034
5429
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
5035
5430
|
axisRef(categoryAxis, categoryProp);
|
|
5036
5431
|
const yAxisRaw = values.get("yAxis");
|
|
5037
5432
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
5038
5433
|
for (const a of yAxisList) {
|
|
5039
|
-
axisRef(strOf(a) ?? (
|
|
5434
|
+
axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
5040
5435
|
}
|
|
5041
5436
|
const series = values.get("series");
|
|
5042
5437
|
if (Array.isArray(series)) {
|
|
5043
5438
|
for (const s of series) {
|
|
5044
|
-
if (!
|
|
5439
|
+
if (!isRec19(s)) continue;
|
|
5045
5440
|
const dataKey = strOf(s.dataKey);
|
|
5046
5441
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
5047
5442
|
}
|
|
@@ -5081,7 +5476,7 @@ var REACT_FIELD_SPECS = {
|
|
|
5081
5476
|
};
|
|
5082
5477
|
var PATH_SEP = " \u203A ";
|
|
5083
5478
|
var SCHEMA_TYPE_BY_TAG = new Map(
|
|
5084
|
-
|
|
5479
|
+
import_ui3.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
|
|
5085
5480
|
);
|
|
5086
5481
|
var FILTER_PROPS = new Set(
|
|
5087
5482
|
Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
|
|
@@ -5097,7 +5492,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
5097
5492
|
if (!Array.isArray(value)) return { child, parent };
|
|
5098
5493
|
for (let i = 0; i < value.length; i++) {
|
|
5099
5494
|
const sub = value[i];
|
|
5100
|
-
if (!
|
|
5495
|
+
if (!isRec19(sub)) continue;
|
|
5101
5496
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
5102
5497
|
child.push({
|
|
5103
5498
|
objectName: strOf(sub.childObject),
|
|
@@ -5122,7 +5517,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
5122
5517
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
5123
5518
|
return;
|
|
5124
5519
|
}
|
|
5125
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5520
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data10.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
5126
5521
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
5127
5522
|
}
|
|
5128
5523
|
}
|
|
@@ -5142,20 +5537,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
5142
5537
|
}
|
|
5143
5538
|
for (const key of spec.nestedFields ?? []) {
|
|
5144
5539
|
const v = readable(key);
|
|
5145
|
-
if (
|
|
5540
|
+
if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
5146
5541
|
}
|
|
5147
5542
|
for (const key of spec.sections ?? []) {
|
|
5148
5543
|
const v = readable(key);
|
|
5149
5544
|
if (!Array.isArray(v)) continue;
|
|
5150
5545
|
for (let i = 0; i < v.length; i++) {
|
|
5151
5546
|
const section = v[i];
|
|
5152
|
-
if (!
|
|
5547
|
+
if (!isRec19(section)) continue;
|
|
5153
5548
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
5154
5549
|
}
|
|
5155
5550
|
}
|
|
5156
5551
|
for (const key of spec.keyedByField ?? []) {
|
|
5157
5552
|
const v = readable(key);
|
|
5158
|
-
if (!
|
|
5553
|
+
if (!isRec19(v)) continue;
|
|
5159
5554
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
5160
5555
|
}
|
|
5161
5556
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -5212,7 +5607,7 @@ function recordContextFinding(tag, schemaType, where, path) {
|
|
|
5212
5607
|
where,
|
|
5213
5608
|
path,
|
|
5214
5609
|
message: `<${tag}> renders "${schemaType}", which reads its record from the record context a record page mounts \u2014 a kind:'react' page never mounts one, so the block renders empty no matter how it is bound (its objectName/recordId are not read by the renderer).`,
|
|
5215
|
-
hint: `On a react page bind the record yourself: ${
|
|
5610
|
+
hint: `On a react page bind the record yourself: ${import_ui3.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
|
|
5216
5611
|
};
|
|
5217
5612
|
}
|
|
5218
5613
|
function localComponentNames(tsc, sf) {
|
|
@@ -5251,7 +5646,7 @@ function validateReactPageProps(stack) {
|
|
|
5251
5646
|
const tag = node.tagName.getText(sf);
|
|
5252
5647
|
const where = `page "${name}" \u203A <${tag}>`;
|
|
5253
5648
|
const path = `pages[${p}].source`;
|
|
5254
|
-
const recordType =
|
|
5649
|
+
const recordType = import_ui3.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
|
|
5255
5650
|
if (recordType && !locals.has(tag)) {
|
|
5256
5651
|
findings.push(recordContextFinding(tag, recordType, where, path));
|
|
5257
5652
|
tsc.forEachChild(node, visit);
|
|
@@ -5278,7 +5673,7 @@ function validateReactPageProps(stack) {
|
|
|
5278
5673
|
}
|
|
5279
5674
|
if (tag === "Block") {
|
|
5280
5675
|
const blockType = strOf(values.get("type"));
|
|
5281
|
-
if (blockType && (0,
|
|
5676
|
+
if (blockType && (0, import_ui3.isRecordContextBlockType)(blockType)) {
|
|
5282
5677
|
findings.push(recordContextFinding(tag, blockType, where, path));
|
|
5283
5678
|
tsc.forEachChild(node, visit);
|
|
5284
5679
|
return;
|
|
@@ -5322,7 +5717,12 @@ function validateReactPageProps(stack) {
|
|
|
5322
5717
|
searchTargets,
|
|
5323
5718
|
where,
|
|
5324
5719
|
`${path} \u203A searchableFields`,
|
|
5325
|
-
"searchableFields"
|
|
5720
|
+
"searchableFields",
|
|
5721
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
5722
|
+
// default, spelled out here because the #8404 provenance index
|
|
5723
|
+
// follows it positionally.
|
|
5724
|
+
"narrowing",
|
|
5725
|
+
unprovisionedAnchors
|
|
5326
5726
|
)
|
|
5327
5727
|
);
|
|
5328
5728
|
}
|
|
@@ -5344,6 +5744,13 @@ function validateReactPageProps(stack) {
|
|
|
5344
5744
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5345
5745
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5346
5746
|
{ name: "validateSearchableFields", run: validateSearchableFields },
|
|
5747
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5748
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
5749
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5750
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5751
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5752
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
5753
|
+
{ name: "validateSortableFields", run: validateSortableFields },
|
|
5347
5754
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5348
5755
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5349
5756
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5458,14 +5865,14 @@ function validateReferenceIntegrity(stack) {
|
|
|
5458
5865
|
}
|
|
5459
5866
|
|
|
5460
5867
|
// src/validate-component-props.ts
|
|
5461
|
-
var
|
|
5868
|
+
var import_ui4 = require("@objectstack/spec/ui");
|
|
5462
5869
|
var import_spec2 = require("@objectstack/spec");
|
|
5463
5870
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5464
5871
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5465
|
-
function
|
|
5872
|
+
function isRec20(v) {
|
|
5466
5873
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5467
5874
|
}
|
|
5468
|
-
function
|
|
5875
|
+
function strName19(v) {
|
|
5469
5876
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5470
5877
|
}
|
|
5471
5878
|
function asArray25(v) {
|
|
@@ -5475,12 +5882,12 @@ function asArray25(v) {
|
|
|
5475
5882
|
}
|
|
5476
5883
|
return [];
|
|
5477
5884
|
}
|
|
5478
|
-
var PROPS_SCHEMAS =
|
|
5885
|
+
var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
|
|
5479
5886
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5480
5887
|
function suppliedByDataSource(issue, component) {
|
|
5481
5888
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5482
|
-
const dataSource =
|
|
5483
|
-
return
|
|
5889
|
+
const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
|
|
5890
|
+
return strName19(dataSource?.object) !== void 0;
|
|
5484
5891
|
}
|
|
5485
5892
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
5486
5893
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -5497,18 +5904,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
5497
5904
|
}
|
|
5498
5905
|
function validateComponentProps(stack) {
|
|
5499
5906
|
const findings = [];
|
|
5500
|
-
if (!
|
|
5907
|
+
if (!isRec20(stack)) return findings;
|
|
5501
5908
|
const pages = asArray25(stack.pages);
|
|
5502
5909
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5503
5910
|
const page = pages[pi];
|
|
5504
|
-
if (!
|
|
5505
|
-
const pageName =
|
|
5911
|
+
if (!isRec20(page)) continue;
|
|
5912
|
+
const pageName = strName19(page.name) ?? `#${pi}`;
|
|
5506
5913
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5507
|
-
const type =
|
|
5914
|
+
const type = strName19(component.type);
|
|
5508
5915
|
if (!type) continue;
|
|
5509
5916
|
const schema = PROPS_SCHEMAS[type];
|
|
5510
5917
|
if (!schema) continue;
|
|
5511
|
-
const props =
|
|
5918
|
+
const props = isRec20(component.properties) ? component.properties : void 0;
|
|
5512
5919
|
if (!props) continue;
|
|
5513
5920
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5514
5921
|
const base = `${path}.properties`;
|
|
@@ -6504,7 +6911,7 @@ function validateApprovalApprovers(stack) {
|
|
|
6504
6911
|
}
|
|
6505
6912
|
|
|
6506
6913
|
// src/validate-record-title.ts
|
|
6507
|
-
var
|
|
6914
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
6508
6915
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
6509
6916
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
6510
6917
|
function asArray33(v) {
|
|
@@ -6532,7 +6939,7 @@ function validateRecordTitle(stack) {
|
|
|
6532
6939
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
6533
6940
|
});
|
|
6534
6941
|
}
|
|
6535
|
-
const completeness = (0,
|
|
6942
|
+
const completeness = (0, import_data11.objectTitleCompleteness)(obj);
|
|
6536
6943
|
if (completeness.status === "none") {
|
|
6537
6944
|
findings.push({
|
|
6538
6945
|
severity: "warning",
|
|
@@ -6679,10 +7086,10 @@ function asArray35(v) {
|
|
|
6679
7086
|
}
|
|
6680
7087
|
return [];
|
|
6681
7088
|
}
|
|
6682
|
-
function
|
|
7089
|
+
function isRec21(v) {
|
|
6683
7090
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6684
7091
|
}
|
|
6685
|
-
function
|
|
7092
|
+
function strName20(v) {
|
|
6686
7093
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6687
7094
|
}
|
|
6688
7095
|
function fieldNameOf(entry) {
|
|
@@ -6703,7 +7110,7 @@ function validateFormLayout(stack) {
|
|
|
6703
7110
|
objectFields.set(name, new Set(fields));
|
|
6704
7111
|
}
|
|
6705
7112
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6706
|
-
const viewName =
|
|
7113
|
+
const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
|
|
6707
7114
|
const containerObject = viewObjectName(view);
|
|
6708
7115
|
for (const site of formViewSites(view, viewPath)) {
|
|
6709
7116
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6713,7 +7120,7 @@ function validateFormLayout(stack) {
|
|
|
6713
7120
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6714
7121
|
for (let s = 0; s < sections.length; s++) {
|
|
6715
7122
|
const sec = sections[s];
|
|
6716
|
-
const secFields =
|
|
7123
|
+
const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6717
7124
|
for (let f = 0; f < secFields.length; f++) {
|
|
6718
7125
|
const entry = secFields[f];
|
|
6719
7126
|
const fname = fieldNameOf(entry);
|
|
@@ -6728,7 +7135,7 @@ function validateFormLayout(stack) {
|
|
|
6728
7135
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6729
7136
|
});
|
|
6730
7137
|
}
|
|
6731
|
-
const colSpan =
|
|
7138
|
+
const colSpan = isRec21(entry) ? entry.colSpan : void 0;
|
|
6732
7139
|
if (colSpan != null) {
|
|
6733
7140
|
findings.push({
|
|
6734
7141
|
severity: "warning",
|
|
@@ -6886,12 +7293,12 @@ function bareRhsOnlyIdentifiers(ast) {
|
|
|
6886
7293
|
for (const name of elsewhere) rhs.delete(name);
|
|
6887
7294
|
return rhs;
|
|
6888
7295
|
}
|
|
6889
|
-
function
|
|
7296
|
+
function isRec22(v) {
|
|
6890
7297
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6891
7298
|
}
|
|
6892
7299
|
function schemaIdOf(view) {
|
|
6893
7300
|
const data = view.data;
|
|
6894
|
-
if (!
|
|
7301
|
+
if (!isRec22(data)) return void 0;
|
|
6895
7302
|
if (data.provider !== "schema") return void 0;
|
|
6896
7303
|
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6897
7304
|
}
|
|
@@ -7263,7 +7670,7 @@ function predicateSource2(v) {
|
|
|
7263
7670
|
}
|
|
7264
7671
|
return void 0;
|
|
7265
7672
|
}
|
|
7266
|
-
function
|
|
7673
|
+
function isRec23(v) {
|
|
7267
7674
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7268
7675
|
}
|
|
7269
7676
|
function checkPredicate(source, scope, where, path, findings) {
|
|
@@ -7334,7 +7741,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
7334
7741
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
7335
7742
|
for (let i = 0; i < entries.length; i++) {
|
|
7336
7743
|
const entry = entries[i];
|
|
7337
|
-
if (!
|
|
7744
|
+
if (!isRec23(entry)) continue;
|
|
7338
7745
|
const path = `${base}[${i}]`;
|
|
7339
7746
|
for (const key of PREDICATE_KEYS) {
|
|
7340
7747
|
const source = predicateSource2(entry[key]);
|
|
@@ -7368,7 +7775,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
7368
7775
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7369
7776
|
for (let s = 0; s < sections.length; s++) {
|
|
7370
7777
|
const section = sections[s];
|
|
7371
|
-
if (!
|
|
7778
|
+
if (!isRec23(section)) continue;
|
|
7372
7779
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7373
7780
|
for (const key of PREDICATE_KEYS) {
|
|
7374
7781
|
const source = predicateSource2(section[key]);
|
|
@@ -7652,6 +8059,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7652
8059
|
}
|
|
7653
8060
|
}
|
|
7654
8061
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
8062
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
7655
8063
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7656
8064
|
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7657
8065
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -7675,7 +8083,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7675
8083
|
}
|
|
7676
8084
|
}
|
|
7677
8085
|
const delegatedFrom = rec.delegated_from;
|
|
7678
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
8086
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
7679
8087
|
const reason = rec.reason;
|
|
7680
8088
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
7681
8089
|
findings.push({
|
|
@@ -7841,6 +8249,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7841
8249
|
var import_formula6 = require("@objectstack/formula");
|
|
7842
8250
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7843
8251
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
8252
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
8253
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
7844
8254
|
function asArray38(v) {
|
|
7845
8255
|
if (Array.isArray(v)) return v;
|
|
7846
8256
|
if (v && typeof v === "object") {
|
|
@@ -7865,10 +8275,67 @@ function sourceOf(condition) {
|
|
|
7865
8275
|
return str2(input?.source);
|
|
7866
8276
|
}
|
|
7867
8277
|
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).";
|
|
8278
|
+
function effectiveSharingModelOf(obj) {
|
|
8279
|
+
const m = obj.sharingModel;
|
|
8280
|
+
if (m === "private") return "private";
|
|
8281
|
+
if (m === "public_read") return "read";
|
|
8282
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
8283
|
+
if (m == null) {
|
|
8284
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
8285
|
+
return isSystem ? "public" : "private";
|
|
8286
|
+
}
|
|
8287
|
+
return "private";
|
|
8288
|
+
}
|
|
8289
|
+
function masterOf(obj) {
|
|
8290
|
+
for (const f of asArray38(obj.fields)) {
|
|
8291
|
+
if (f.type === "master_detail") {
|
|
8292
|
+
const ref = f.reference;
|
|
8293
|
+
if (typeof ref === "string" && ref) return ref;
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
return void 0;
|
|
8297
|
+
}
|
|
8298
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
8299
|
+
const object = str2(rule.object);
|
|
8300
|
+
if (!object) return [];
|
|
8301
|
+
const target = objectsByName.get(object);
|
|
8302
|
+
if (!target) return [];
|
|
8303
|
+
const name = str2(rule.name) || String(index);
|
|
8304
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
8305
|
+
const path = `sharingRules[${index}].object`;
|
|
8306
|
+
const owd = target.sharingModel;
|
|
8307
|
+
if (owd === "controlled_by_parent") {
|
|
8308
|
+
const master = masterOf(target);
|
|
8309
|
+
return [{
|
|
8310
|
+
severity: "error",
|
|
8311
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
8312
|
+
where,
|
|
8313
|
+
path,
|
|
8314
|
+
message: `Sharing rule "${name}" is anchored on object "${object}", which declares sharingModel 'controlled_by_parent'. A detail record has no record-level access of its own \u2014 its visibility is DERIVED from its master (ADR-0055), so it holds no shares to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is controlled by its parent (master-detail); share the master record instead"), so the rule's boot backfill fails, no \`sys_record_share\` row is ever written, and the recipients this rule names get whatever the MASTER grants them \u2014 which may be nothing. The grant is declared and does not exist.`,
|
|
8315
|
+
hint: `Move the rule onto the MASTER object` + (master ? ` \u2014 "${object}" derives from "${master}" through its master_detail field, so share "${master}" and the detail rows follow` : `, and share that instead; the detail rows follow`) + `. If "${object}" is meant to carry a record-level baseline of its own, that is a different decision: change its sharingModel to 'private' (owner + shares) or 'public_read', and this rule becomes enforceable where it stands.`
|
|
8316
|
+
}];
|
|
8317
|
+
}
|
|
8318
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
8319
|
+
const declared = owd === "public_read_write" ? `declares sharingModel 'public_read_write'` : `declares no sharingModel and is a system object (\`isSystem: true\` or a \`sys_\` name), which ADR-0090 D1 resolves to public`;
|
|
8320
|
+
return [{
|
|
8321
|
+
severity: "error",
|
|
8322
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
8323
|
+
where,
|
|
8324
|
+
path,
|
|
8325
|
+
message: `Sharing rule "${name}" is anchored on object "${object}", which ${declared}. Its effective sharing model is therefore \`public\`, and sharing only ever WIDENS an OWD baseline \u2014 on the widest baseline there is nothing left to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is not under record-sharing enforcement"), so the rule's boot backfill fails and no \`sys_record_share\` row is ever written. Measured: \`buildReadFilter\` returns \`null\` for this object, i.e. NO record-level filter at all \u2014 every principal already reads every row, so this rule advertises a restriction that does not exist.`,
|
|
8326
|
+
hint: `Decide which half is wrong. If the ACCESS is right \u2014 everyone should read and write these records \u2014 the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right \u2014 only the named audience should reach these records \u2014 then "${object}"'s OWD is the defect: set sharingModel: 'private' (owner + shares) or 'public_read', and this rule starts enforcing. Do NOT re-home the rule onto another public object; that moves the inertness instead of removing it.`
|
|
8327
|
+
}];
|
|
8328
|
+
}
|
|
7868
8329
|
function validateSharingRuleEnforceability(stack) {
|
|
7869
8330
|
const findings = [];
|
|
7870
8331
|
const cfg = stack ?? {};
|
|
8332
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
8333
|
+
for (const obj of asArray38(cfg.objects)) {
|
|
8334
|
+
const name = str2(obj.name);
|
|
8335
|
+
if (name) objectsByName.set(name, obj);
|
|
8336
|
+
}
|
|
7871
8337
|
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
8338
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
7872
8339
|
const input = toCompilerInput(rule.condition);
|
|
7873
8340
|
if (input === null) return;
|
|
7874
8341
|
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
@@ -7994,11 +8461,11 @@ var import_meta4 = {};
|
|
|
7994
8461
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7995
8462
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7996
8463
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7997
|
-
var
|
|
8464
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7998
8465
|
function asArray40(v) {
|
|
7999
|
-
if (Array.isArray(v)) return v.filter(
|
|
8000
|
-
if (
|
|
8001
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8466
|
+
if (Array.isArray(v)) return v.filter(isRec24);
|
|
8467
|
+
if (isRec24(v)) {
|
|
8468
|
+
return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
|
|
8002
8469
|
}
|
|
8003
8470
|
return [];
|
|
8004
8471
|
}
|
|
@@ -8015,7 +8482,7 @@ function loadAjv() {
|
|
|
8015
8482
|
`@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.`
|
|
8016
8483
|
);
|
|
8017
8484
|
}
|
|
8018
|
-
const ctor =
|
|
8485
|
+
const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8019
8486
|
cachedAjv = ctor;
|
|
8020
8487
|
return ctor;
|
|
8021
8488
|
}
|
|
@@ -8030,7 +8497,7 @@ function loadAddFormats() {
|
|
|
8030
8497
|
`@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.`
|
|
8031
8498
|
);
|
|
8032
8499
|
}
|
|
8033
|
-
const plugin =
|
|
8500
|
+
const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8034
8501
|
cachedAddFormats = plugin;
|
|
8035
8502
|
return plugin;
|
|
8036
8503
|
}
|
|
@@ -8060,13 +8527,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
8060
8527
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
8061
8528
|
for (const branch of ["then", "otherwise"]) {
|
|
8062
8529
|
const nested = rule[branch];
|
|
8063
|
-
if (
|
|
8530
|
+
if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
8064
8531
|
}
|
|
8065
8532
|
return out;
|
|
8066
8533
|
}
|
|
8067
8534
|
function walkObjectValidationRules(stack) {
|
|
8068
8535
|
const walked = [];
|
|
8069
|
-
if (!
|
|
8536
|
+
if (!isRec24(stack)) return walked;
|
|
8070
8537
|
for (const obj of asArray40(stack.objects)) {
|
|
8071
8538
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8072
8539
|
const validations = obj.validations;
|
|
@@ -8101,7 +8568,7 @@ function validateRuleCompilability(stack) {
|
|
|
8101
8568
|
});
|
|
8102
8569
|
}
|
|
8103
8570
|
}
|
|
8104
|
-
if (rule.type === "json_schema" &&
|
|
8571
|
+
if (rule.type === "json_schema" && isRec24(rule.schema)) {
|
|
8105
8572
|
try {
|
|
8106
8573
|
createRuntimeAjv().compile(rule.schema);
|
|
8107
8574
|
} catch (err) {
|
|
@@ -8121,7 +8588,7 @@ function validateRuleCompilability(stack) {
|
|
|
8121
8588
|
|
|
8122
8589
|
// src/validate-rule-schema-formats.ts
|
|
8123
8590
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
8124
|
-
var
|
|
8591
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8125
8592
|
var SUBSCHEMA_KEYS = [
|
|
8126
8593
|
"additionalItems",
|
|
8127
8594
|
"additionalProperties",
|
|
@@ -8145,7 +8612,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
8145
8612
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
8146
8613
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
8147
8614
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
8148
|
-
if (!
|
|
8615
|
+
if (!isRec25(schema)) return;
|
|
8149
8616
|
if (typeof schema.format === "string") {
|
|
8150
8617
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
8151
8618
|
}
|
|
@@ -8164,7 +8631,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8164
8631
|
}
|
|
8165
8632
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
8166
8633
|
const value = schema[key];
|
|
8167
|
-
if (!
|
|
8634
|
+
if (!isRec25(value)) continue;
|
|
8168
8635
|
for (const [name, entry] of Object.entries(value)) {
|
|
8169
8636
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8170
8637
|
}
|
|
@@ -8172,13 +8639,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8172
8639
|
const items = schema.items;
|
|
8173
8640
|
if (Array.isArray(items)) {
|
|
8174
8641
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
8175
|
-
} else if (
|
|
8642
|
+
} else if (isRec25(items)) {
|
|
8176
8643
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
8177
8644
|
}
|
|
8178
8645
|
const dependencies = schema.dependencies;
|
|
8179
|
-
if (
|
|
8646
|
+
if (isRec25(dependencies)) {
|
|
8180
8647
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
8181
|
-
if (!
|
|
8648
|
+
if (!isRec25(entry)) continue;
|
|
8182
8649
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8183
8650
|
}
|
|
8184
8651
|
}
|
|
@@ -8205,9 +8672,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
8205
8672
|
let best = null;
|
|
8206
8673
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
8207
8674
|
for (const candidate of [...registered].sort()) {
|
|
8208
|
-
const
|
|
8209
|
-
if (
|
|
8210
|
-
bestDistance =
|
|
8675
|
+
const distance8 = editDistance2(authored, candidate);
|
|
8676
|
+
if (distance8 < bestDistance) {
|
|
8677
|
+
bestDistance = distance8;
|
|
8211
8678
|
best = candidate;
|
|
8212
8679
|
}
|
|
8213
8680
|
}
|
|
@@ -8217,7 +8684,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
8217
8684
|
const findings = [];
|
|
8218
8685
|
const pending = [];
|
|
8219
8686
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
8220
|
-
if (rule.type !== "json_schema" || !
|
|
8687
|
+
if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
|
|
8221
8688
|
const uses = [];
|
|
8222
8689
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
8223
8690
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -8250,7 +8717,7 @@ function asArray41(v) {
|
|
|
8250
8717
|
}
|
|
8251
8718
|
return [];
|
|
8252
8719
|
}
|
|
8253
|
-
function
|
|
8720
|
+
function strName21(v) {
|
|
8254
8721
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8255
8722
|
}
|
|
8256
8723
|
function strList3(v) {
|
|
@@ -8265,7 +8732,7 @@ function collectNamePlacedActions(stack) {
|
|
|
8265
8732
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
8266
8733
|
}
|
|
8267
8734
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8268
|
-
const n =
|
|
8735
|
+
const n = strName21(def?.name);
|
|
8269
8736
|
if (n) placed.add(n);
|
|
8270
8737
|
}
|
|
8271
8738
|
};
|
|
@@ -8291,7 +8758,7 @@ function validateActionLocations(stack) {
|
|
|
8291
8758
|
const check = (action, path) => {
|
|
8292
8759
|
if (!action || typeof action !== "object") return;
|
|
8293
8760
|
if ("locations" in action) return;
|
|
8294
|
-
const name =
|
|
8761
|
+
const name = strName21(action.name);
|
|
8295
8762
|
if (!name) return;
|
|
8296
8763
|
if (namePlaced.has(name)) return;
|
|
8297
8764
|
findings.push({
|
|
@@ -8317,7 +8784,7 @@ function validateActionLocations(stack) {
|
|
|
8317
8784
|
|
|
8318
8785
|
// src/lint-flow-patterns.ts
|
|
8319
8786
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8320
|
-
var
|
|
8787
|
+
var import_data12 = require("@objectstack/spec/data");
|
|
8321
8788
|
function asArray42(v) {
|
|
8322
8789
|
if (Array.isArray(v)) return v;
|
|
8323
8790
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -8592,7 +9059,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
8592
9059
|
function filterCarriesNoCondition(filter) {
|
|
8593
9060
|
if (filter === void 0 || filter === null) return true;
|
|
8594
9061
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
8595
|
-
return (0,
|
|
9062
|
+
return (0, import_data12.reduceFilterVerdict)(filter) === "true";
|
|
8596
9063
|
}
|
|
8597
9064
|
function describeUnboundedFilter(filter) {
|
|
8598
9065
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -8959,7 +9426,7 @@ function lintLivenessProperties(stack) {
|
|
|
8959
9426
|
}
|
|
8960
9427
|
|
|
8961
9428
|
// src/lint-autonumber-formats.ts
|
|
8962
|
-
var
|
|
9429
|
+
var import_data13 = require("@objectstack/spec/data");
|
|
8963
9430
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
8964
9431
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
8965
9432
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
@@ -8985,8 +9452,8 @@ function lintAutonumberFormats(stack) {
|
|
|
8985
9452
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
8986
9453
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
8987
9454
|
if (!fmt) continue;
|
|
8988
|
-
const tokens = (0,
|
|
8989
|
-
const refs = (0,
|
|
9455
|
+
const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
|
|
9456
|
+
const refs = (0, import_data13.referencedFields)(tokens);
|
|
8990
9457
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
8991
9458
|
for (const t of tokens) {
|
|
8992
9459
|
if (t.kind !== "literal") continue;
|
|
@@ -9141,6 +9608,17 @@ function lintViewRefs(stack) {
|
|
|
9141
9608
|
}
|
|
9142
9609
|
|
|
9143
9610
|
// src/data-model-rules.ts
|
|
9611
|
+
function objectWhere(obj) {
|
|
9612
|
+
return `object "${obj?.name}"`;
|
|
9613
|
+
}
|
|
9614
|
+
function indexWhere(obj, idx, j, cols) {
|
|
9615
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
9616
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
9617
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
9618
|
+
}
|
|
9619
|
+
function fieldWhere(obj, fieldName) {
|
|
9620
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
9621
|
+
}
|
|
9144
9622
|
function fieldEntries2(fields) {
|
|
9145
9623
|
if (!fields) return [];
|
|
9146
9624
|
if (Array.isArray(fields)) {
|
|
@@ -9174,11 +9652,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9174
9652
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9175
9653
|
const idx = declaredIndexes[j];
|
|
9176
9654
|
if (idx?.unique !== true) continue;
|
|
9177
|
-
const
|
|
9655
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
9656
|
+
const cols = colList.join(", ");
|
|
9178
9657
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9179
9658
|
issues.push({
|
|
9180
9659
|
severity: "warning",
|
|
9181
9660
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9661
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9182
9662
|
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).`,
|
|
9183
9663
|
path: `objects[${i}].indexes[${j}]`,
|
|
9184
9664
|
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).`
|
|
@@ -9227,6 +9707,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9227
9707
|
issues.push({
|
|
9228
9708
|
severity: "warning",
|
|
9229
9709
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
9710
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
9711
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
9712
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
9713
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
9714
|
+
where: fieldWhere(obj, name),
|
|
9230
9715
|
message,
|
|
9231
9716
|
path: `objects[${i}]`,
|
|
9232
9717
|
fix
|
|
@@ -9255,6 +9740,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9255
9740
|
issues.push({
|
|
9256
9741
|
severity: "warning",
|
|
9257
9742
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9743
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9258
9744
|
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.`,
|
|
9259
9745
|
path: `objects[${i}].indexes[${j}]`,
|
|
9260
9746
|
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.`
|
|
@@ -9271,7 +9757,7 @@ var CLI_ONLY = ["cli"];
|
|
|
9271
9757
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
9272
9758
|
var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
|
|
9273
9759
|
var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
|
|
9274
|
-
var
|
|
9760
|
+
var RUNTIME_OBJECT_ADVISORY_VOLUME = "Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).";
|
|
9275
9761
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
9276
9762
|
var AUTHORING_RULES = [
|
|
9277
9763
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -9332,8 +9818,14 @@ var AUTHORING_RULES = [
|
|
|
9332
9818
|
input: "normalized",
|
|
9333
9819
|
commands: ALL,
|
|
9334
9820
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
9335
|
-
|
|
9336
|
-
|
|
9821
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
9822
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
9823
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
9824
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
9825
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
9826
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
9827
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9828
|
+
runtimeTypes: ["object"],
|
|
9337
9829
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
9338
9830
|
},
|
|
9339
9831
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -9357,8 +9849,11 @@ var AUTHORING_RULES = [
|
|
|
9357
9849
|
input: "normalized",
|
|
9358
9850
|
commands: ALL,
|
|
9359
9851
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9360
|
-
|
|
9361
|
-
|
|
9852
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
9853
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
9854
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
9855
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9856
|
+
runtimeTypes: ["object"],
|
|
9362
9857
|
run: (stack) => validateManagedApiMethods(stack)
|
|
9363
9858
|
},
|
|
9364
9859
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -9380,19 +9875,38 @@ var AUTHORING_RULES = [
|
|
|
9380
9875
|
},
|
|
9381
9876
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
9382
9877
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
9878
|
+
//
|
|
9879
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
9880
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
9881
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
9882
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
9883
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
9884
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
9885
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
9886
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
9887
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
9888
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
9889
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
9890
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
9891
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
9892
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
9893
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
9894
|
+
// advisory channel and never block (#4463 P1).
|
|
9383
9895
|
{
|
|
9384
9896
|
name: "validateWidgetBindings",
|
|
9385
9897
|
tier: "gating",
|
|
9386
9898
|
input: "parsed",
|
|
9387
9899
|
commands: ALL,
|
|
9388
9900
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
9389
|
-
surfaces:
|
|
9390
|
-
|
|
9901
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9902
|
+
runtimeTypes: ["dashboard"],
|
|
9391
9903
|
run: (stack) => validateWidgetBindings(stack)
|
|
9392
9904
|
},
|
|
9393
|
-
// ADR-0049 / #3367 — a header
|
|
9394
|
-
//
|
|
9395
|
-
//
|
|
9905
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
9906
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
9907
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
9908
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
9909
|
+
// Unresolved `url` routes stay advisory.
|
|
9396
9910
|
{
|
|
9397
9911
|
name: "validateDashboardActionRefs",
|
|
9398
9912
|
tier: "gating",
|
|
@@ -9417,6 +9931,28 @@ var AUTHORING_RULES = [
|
|
|
9417
9931
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
9418
9932
|
run: (stack) => validateFilterTokens(stack)
|
|
9419
9933
|
},
|
|
9934
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
9935
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
9936
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
9937
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
9938
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
9939
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
9940
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
9941
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
9942
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
9943
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
9944
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
9945
|
+
// vocabulary is near enough to reach for.
|
|
9946
|
+
{
|
|
9947
|
+
name: "validatePresetComparands",
|
|
9948
|
+
tier: "gating",
|
|
9949
|
+
input: "parsed",
|
|
9950
|
+
commands: ALL,
|
|
9951
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
9952
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9953
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
9954
|
+
run: (stack) => validatePresetComparands(stack)
|
|
9955
|
+
},
|
|
9420
9956
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9421
9957
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9422
9958
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -9635,7 +10171,7 @@ var AUTHORING_RULES = [
|
|
|
9635
10171
|
commands: ALL,
|
|
9636
10172
|
source: "packages/lint/src/validate-record-title.ts",
|
|
9637
10173
|
surfaces: CLI_ONLY,
|
|
9638
|
-
surfaceReason:
|
|
10174
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9639
10175
|
run: (stack) => validateRecordTitle(stack)
|
|
9640
10176
|
},
|
|
9641
10177
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -9648,7 +10184,7 @@ var AUTHORING_RULES = [
|
|
|
9648
10184
|
commands: ALL,
|
|
9649
10185
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
9650
10186
|
surfaces: CLI_ONLY,
|
|
9651
|
-
surfaceReason:
|
|
10187
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9652
10188
|
run: (stack) => validateSemanticRoles(stack)
|
|
9653
10189
|
},
|
|
9654
10190
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -9859,7 +10395,7 @@ var AUTHORING_RULES = [
|
|
|
9859
10395
|
commands: ALL,
|
|
9860
10396
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
9861
10397
|
surfaces: CLI_ONLY,
|
|
9862
|
-
surfaceReason:
|
|
10398
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9863
10399
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
9864
10400
|
severity: "warning",
|
|
9865
10401
|
rule: f.rule,
|
|
@@ -9878,8 +10414,12 @@ var AUTHORING_RULES = [
|
|
|
9878
10414
|
input: "parsed",
|
|
9879
10415
|
commands: ALL,
|
|
9880
10416
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
9881
|
-
|
|
9882
|
-
|
|
10417
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
10418
|
+
// object does not carry is broken from the first record; only the error
|
|
10419
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
10420
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
10421
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10422
|
+
runtimeTypes: ["object"],
|
|
9883
10423
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
9884
10424
|
severity: f.severity,
|
|
9885
10425
|
rule: f.rule,
|
|
@@ -9920,12 +10460,12 @@ var AUTHORING_RULES = [
|
|
|
9920
10460
|
commands: ["validate", "build"],
|
|
9921
10461
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9922
10462
|
surfaces: CLI_ONLY,
|
|
9923
|
-
surfaceReason:
|
|
10463
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9924
10464
|
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.",
|
|
9925
10465
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9926
10466
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9927
10467
|
rule: f.rule,
|
|
9928
|
-
where: f.
|
|
10468
|
+
where: f.where,
|
|
9929
10469
|
path: f.path,
|
|
9930
10470
|
message: f.message,
|
|
9931
10471
|
hint: f.fix ?? ""
|
|
@@ -9941,12 +10481,12 @@ var AUTHORING_RULES = [
|
|
|
9941
10481
|
commands: ["validate", "build"],
|
|
9942
10482
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9943
10483
|
surfaces: CLI_ONLY,
|
|
9944
|
-
surfaceReason:
|
|
10484
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9945
10485
|
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of 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.",
|
|
9946
10486
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9947
10487
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9948
10488
|
rule: f.rule,
|
|
9949
|
-
where: f.
|
|
10489
|
+
where: f.where,
|
|
9950
10490
|
path: f.path,
|
|
9951
10491
|
message: f.message,
|
|
9952
10492
|
hint: f.fix ?? ""
|
|
@@ -9963,12 +10503,12 @@ var AUTHORING_RULES = [
|
|
|
9963
10503
|
commands: ["validate", "build"],
|
|
9964
10504
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9965
10505
|
surfaces: CLI_ONLY,
|
|
9966
|
-
surfaceReason:
|
|
10506
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9967
10507
|
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.",
|
|
9968
10508
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9969
10509
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9970
10510
|
rule: f.rule,
|
|
9971
|
-
where: f.
|
|
10511
|
+
where: f.where,
|
|
9972
10512
|
path: f.path,
|
|
9973
10513
|
message: f.message,
|
|
9974
10514
|
hint: f.fix ?? ""
|
|
@@ -10096,16 +10636,26 @@ var AUTHORING_RULES = [
|
|
|
10096
10636
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10097
10637
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10098
10638
|
},
|
|
10099
|
-
// #4698 — the "declared but never read" gate, for the
|
|
10100
|
-
// predicate is EXACT rather than inferred.
|
|
10101
|
-
//
|
|
10102
|
-
//
|
|
10103
|
-
//
|
|
10104
|
-
//
|
|
10105
|
-
//
|
|
10106
|
-
//
|
|
10107
|
-
//
|
|
10108
|
-
//
|
|
10639
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
10640
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
10641
|
+
//
|
|
10642
|
+
// - `condition` has a single runtime consumer
|
|
10643
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
10644
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
10645
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
10646
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
10647
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
10648
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
10649
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
10650
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
10651
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
10652
|
+
//
|
|
10653
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
10654
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
10655
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
10656
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
10657
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
10658
|
+
// yet true.
|
|
10109
10659
|
{
|
|
10110
10660
|
name: "validateSharingRuleEnforceability",
|
|
10111
10661
|
tier: "gating",
|
|
@@ -10113,7 +10663,7 @@ var AUTHORING_RULES = [
|
|
|
10113
10663
|
commands: ALL,
|
|
10114
10664
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10115
10665
|
surfaces: CLI_ONLY,
|
|
10116
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
10666
|
+
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves `sharingRules[].object` against `stack.objects` to read the anchor's OWD, so the rule is now cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), so the remaining gap is unchanged in SHAPE \u2014 the gate must accept a `sharing_rule` type and the snapshot must carry `sharingRules`, which it does not \u2014 but it is now TWO collections, not one. Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
10117
10667
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10118
10668
|
},
|
|
10119
10669
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10152,8 +10702,15 @@ var AUTHORING_RULES = [
|
|
|
10152
10702
|
input: "parsed",
|
|
10153
10703
|
commands: ALL,
|
|
10154
10704
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10155
|
-
|
|
10156
|
-
|
|
10705
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
10706
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
10707
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
10708
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
10709
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
10710
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
10711
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
10712
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10713
|
+
runtimeTypes: ["object"],
|
|
10157
10714
|
run: (stack) => validateRuleCompilability(stack)
|
|
10158
10715
|
},
|
|
10159
10716
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10174,8 +10731,12 @@ var AUTHORING_RULES = [
|
|
|
10174
10731
|
input: "parsed",
|
|
10175
10732
|
commands: ALL,
|
|
10176
10733
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10177
|
-
|
|
10178
|
-
|
|
10734
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
10735
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
10736
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
10737
|
+
// format set is only enumerated once a schema actually names a format.
|
|
10738
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10739
|
+
runtimeTypes: ["object"],
|
|
10179
10740
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10180
10741
|
}
|
|
10181
10742
|
];
|
|
@@ -10217,7 +10778,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
10217
10778
|
permission: "permissions",
|
|
10218
10779
|
book: "books"
|
|
10219
10780
|
};
|
|
10220
|
-
|
|
10781
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
10782
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
10783
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
10784
|
+
return objects.filter((entry) => {
|
|
10785
|
+
if (!entry || typeof entry !== "object") return true;
|
|
10786
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
10787
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
10788
|
+
if (reachable.has(owner)) return true;
|
|
10789
|
+
return isSystemObject(entry);
|
|
10790
|
+
});
|
|
10791
|
+
}
|
|
10792
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
10793
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
10794
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
10221
10795
|
function runtimeAuthoringRulesFor(type) {
|
|
10222
10796
|
return AUTHORING_RULES.filter(
|
|
10223
10797
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -10243,7 +10817,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10243
10817
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10244
10818
|
const baseline = {};
|
|
10245
10819
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
10246
|
-
const
|
|
10820
|
+
const raw = args.context?.[key] ?? [];
|
|
10821
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
10247
10822
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10248
10823
|
}
|
|
10249
10824
|
const candidate = {
|
|
@@ -10277,7 +10852,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10277
10852
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
10278
10853
|
type: args.type,
|
|
10279
10854
|
item: args.item,
|
|
10280
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
10855
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
10856
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10281
10857
|
});
|
|
10282
10858
|
if (!snapshots) return empty;
|
|
10283
10859
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
@@ -10292,6 +10868,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10292
10868
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10293
10869
|
0 && (module.exports = {
|
|
10294
10870
|
buildRuntimeWriteSnapshots,
|
|
10871
|
+
narrowObjectsToPackageClosure,
|
|
10295
10872
|
runRuntimeAuthoringRules,
|
|
10296
10873
|
runtimeAuthoringRulesFor,
|
|
10297
10874
|
runtimeGatedTypes,
|