@objectstack/lint 17.0.0 → 17.2.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 +851 -0
- package/dist/index.cjs +1126 -320
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -72
- package/dist/index.d.ts +520 -72
- package/dist/index.js +1096 -305
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-cV-l_vtC.d.cts} +109 -3
- package/dist/{runtime-H-nDodRy.d.ts → runtime-cV-l_vtC.d.ts} +109 -3
- package/dist/runtime.cjs +1188 -433
- 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 +1161 -402
- package/dist/runtime.js.map +1 -1
- package/package.json +7 -6
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++) {
|
|
@@ -2033,6 +2190,26 @@ function validateSearchableFields(stack) {
|
|
|
2033
2190
|
if (!isRec3(view)) continue;
|
|
2034
2191
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
2035
2192
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
2193
|
+
if (view.viewKind === "list" && !isRec3(view.config)) {
|
|
2194
|
+
check(
|
|
2195
|
+
view.searchableFields,
|
|
2196
|
+
listViewObject(view) ?? viewObject,
|
|
2197
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2198
|
+
`views[${vi}].searchableFields`,
|
|
2199
|
+
"list-view searchableFields",
|
|
2200
|
+
"narrowing"
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
if (view.viewKind === "list" && isRec3(view.config)) {
|
|
2204
|
+
check(
|
|
2205
|
+
view.config.searchableFields,
|
|
2206
|
+
listViewObject(view.config) ?? viewObject,
|
|
2207
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2208
|
+
`views[${vi}].config.searchableFields`,
|
|
2209
|
+
"list-view searchableFields",
|
|
2210
|
+
"narrowing"
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2036
2213
|
if (isRec3(view.list)) {
|
|
2037
2214
|
check(
|
|
2038
2215
|
view.list.searchableFields,
|
|
@@ -2064,33 +2241,244 @@ function listViewObject(listView) {
|
|
|
2064
2241
|
return isRec3(data) ? strName3(data.object) : void 0;
|
|
2065
2242
|
}
|
|
2066
2243
|
|
|
2067
|
-
// src/
|
|
2244
|
+
// src/validate-sortable-fields.ts
|
|
2245
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
2246
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2247
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2248
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
2068
2249
|
function isRec4(v) {
|
|
2069
2250
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2070
2251
|
}
|
|
2071
2252
|
function strName4(v) {
|
|
2072
2253
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2073
2254
|
}
|
|
2255
|
+
function readSortKeys(declared) {
|
|
2256
|
+
const fromShorthand = (raw, at) => {
|
|
2257
|
+
const trimmed = raw.trim();
|
|
2258
|
+
if (!trimmed) return void 0;
|
|
2259
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
2260
|
+
return bare ? { field: bare, at } : void 0;
|
|
2261
|
+
};
|
|
2262
|
+
if (typeof declared === "string") {
|
|
2263
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
2264
|
+
}
|
|
2265
|
+
if (Array.isArray(declared)) {
|
|
2266
|
+
const keys = [];
|
|
2267
|
+
for (let i = 0; i < declared.length; i++) {
|
|
2268
|
+
const el = declared[i];
|
|
2269
|
+
if (typeof el === "string") {
|
|
2270
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
2271
|
+
if (k) keys.push(k);
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
if (!isRec4(el)) continue;
|
|
2275
|
+
const field = strName4(el.field);
|
|
2276
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
2277
|
+
}
|
|
2278
|
+
return keys;
|
|
2279
|
+
}
|
|
2280
|
+
return [];
|
|
2281
|
+
}
|
|
2282
|
+
function suggest4(target, known) {
|
|
2283
|
+
let best;
|
|
2284
|
+
let bestScore = Infinity;
|
|
2285
|
+
for (const candidate of known) {
|
|
2286
|
+
const d = distance3(target, candidate);
|
|
2287
|
+
if (d < bestScore) {
|
|
2288
|
+
bestScore = d;
|
|
2289
|
+
best = candidate;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
2293
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
2294
|
+
}
|
|
2295
|
+
function distance3(a, b) {
|
|
2296
|
+
const m = a.length;
|
|
2297
|
+
const n = b.length;
|
|
2298
|
+
if (m === 0) return n;
|
|
2299
|
+
if (n === 0) return m;
|
|
2300
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2301
|
+
for (let i = 1; i <= m; i++) {
|
|
2302
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
2303
|
+
for (let j = 1; j <= n; j++) {
|
|
2304
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2305
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2306
|
+
}
|
|
2307
|
+
prev = curr;
|
|
2308
|
+
}
|
|
2309
|
+
return prev[n];
|
|
2310
|
+
}
|
|
2311
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
2312
|
+
const findings = [];
|
|
2313
|
+
if (declared === void 0 || declared === null) return findings;
|
|
2314
|
+
if (!objectName) return findings;
|
|
2315
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
2316
|
+
const target = fieldsByObject.get(objectName);
|
|
2317
|
+
if (!target) return findings;
|
|
2318
|
+
const known = target.names;
|
|
2319
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2320
|
+
for (const key of readSortKeys(declared)) {
|
|
2321
|
+
const name = key.field;
|
|
2322
|
+
const head = name.split(".")[0];
|
|
2323
|
+
if (SYSTEM_FIELDS.has(head)) {
|
|
2324
|
+
if (!name.includes(".") && anchors?.has(head)) {
|
|
2325
|
+
findings.push({
|
|
2326
|
+
severity: "warning",
|
|
2327
|
+
rule: SORT_FIELD_UNPROVISIONED,
|
|
2328
|
+
where,
|
|
2329
|
+
path: `${path}${key.at}`,
|
|
2330
|
+
message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
|
|
2331
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2332
|
+
});
|
|
2333
|
+
}
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
if (!known.has(head)) {
|
|
2337
|
+
const dotted = name.includes(".");
|
|
2338
|
+
findings.push({
|
|
2339
|
+
severity: "error",
|
|
2340
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
2341
|
+
where,
|
|
2342
|
+
path: `${path}${key.at}`,
|
|
2343
|
+
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)),
|
|
2344
|
+
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(", ")}.` : "")
|
|
2345
|
+
});
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const meta = target.fields[name];
|
|
2349
|
+
if ((0, import_data8.isVirtualSearchField)(meta)) {
|
|
2350
|
+
const vtype = meta?.type;
|
|
2351
|
+
findings.push({
|
|
2352
|
+
severity: "error",
|
|
2353
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
2354
|
+
where,
|
|
2355
|
+
path: `${path}${key.at}`,
|
|
2356
|
+
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.`,
|
|
2357
|
+
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.`
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return findings;
|
|
2362
|
+
}
|
|
2363
|
+
function validateSortableFields(stack) {
|
|
2364
|
+
const findings = [];
|
|
2365
|
+
if (!isRec4(stack)) return findings;
|
|
2366
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2367
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2368
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2369
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
2370
|
+
findings.push(
|
|
2371
|
+
...checkSortDeclaration(
|
|
2372
|
+
declared,
|
|
2373
|
+
objectName,
|
|
2374
|
+
fieldsByObject,
|
|
2375
|
+
where,
|
|
2376
|
+
path,
|
|
2377
|
+
subject,
|
|
2378
|
+
unprovisionedAnchors
|
|
2379
|
+
)
|
|
2380
|
+
);
|
|
2381
|
+
};
|
|
2382
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
2383
|
+
const obj = objects[oi];
|
|
2384
|
+
if (!isRec4(obj)) continue;
|
|
2385
|
+
const objName = strName4(obj.name);
|
|
2386
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
2387
|
+
if (isRec4(obj.listViews)) {
|
|
2388
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2389
|
+
if (!isRec4(lv)) continue;
|
|
2390
|
+
check(
|
|
2391
|
+
lv.sort,
|
|
2392
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
2393
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
2394
|
+
listViewObject2(lv) ?? objName,
|
|
2395
|
+
`${label2} \u203A listViews.${key}`,
|
|
2396
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
2397
|
+
"list-view sort"
|
|
2398
|
+
);
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
2403
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
2404
|
+
const view = views[vi];
|
|
2405
|
+
if (!isRec4(view)) continue;
|
|
2406
|
+
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2407
|
+
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2408
|
+
if (view.viewKind === "list" && !isRec4(view.config)) {
|
|
2409
|
+
check(
|
|
2410
|
+
view.sort,
|
|
2411
|
+
listViewObject2(view) ?? viewObject,
|
|
2412
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2413
|
+
`views[${vi}].sort`,
|
|
2414
|
+
"list-view sort"
|
|
2415
|
+
);
|
|
2416
|
+
}
|
|
2417
|
+
if (view.viewKind === "list" && isRec4(view.config)) {
|
|
2418
|
+
check(
|
|
2419
|
+
view.config.sort,
|
|
2420
|
+
listViewObject2(view.config) ?? viewObject,
|
|
2421
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2422
|
+
`views[${vi}].config.sort`,
|
|
2423
|
+
"list-view sort"
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
if (isRec4(view.list)) {
|
|
2427
|
+
check(
|
|
2428
|
+
view.list.sort,
|
|
2429
|
+
listViewObject2(view.list) ?? viewObject,
|
|
2430
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2431
|
+
`views[${vi}].list.sort`,
|
|
2432
|
+
"list-view sort"
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
if (isRec4(view.listViews)) {
|
|
2436
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2437
|
+
if (!isRec4(lv)) continue;
|
|
2438
|
+
check(
|
|
2439
|
+
lv.sort,
|
|
2440
|
+
listViewObject2(lv) ?? viewObject,
|
|
2441
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2442
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
2443
|
+
"list-view sort"
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return findings;
|
|
2449
|
+
}
|
|
2450
|
+
function listViewObject2(listView) {
|
|
2451
|
+
const data = listView.data;
|
|
2452
|
+
return isRec4(data) ? strName4(data.object) : void 0;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
// src/page-walk.ts
|
|
2456
|
+
function isRec5(v) {
|
|
2457
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2458
|
+
}
|
|
2459
|
+
function strName5(v) {
|
|
2460
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2461
|
+
}
|
|
2074
2462
|
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
2075
2463
|
function isSourceAuthoredPage(page) {
|
|
2076
|
-
const kind =
|
|
2464
|
+
const kind = strName5(page.kind);
|
|
2077
2465
|
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
2078
2466
|
}
|
|
2079
2467
|
function walkPageComponents(page, pagePath) {
|
|
2080
2468
|
const out = [];
|
|
2081
|
-
if (!
|
|
2082
|
-
const pageObject =
|
|
2469
|
+
if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
|
|
2470
|
+
const pageObject = strName5(page.object);
|
|
2083
2471
|
const visit = (node, path, inheritedObject) => {
|
|
2084
|
-
if (!
|
|
2085
|
-
const props =
|
|
2086
|
-
const dataSource =
|
|
2087
|
-
const objectName =
|
|
2472
|
+
if (!isRec5(node)) return;
|
|
2473
|
+
const props = isRec5(node.properties) ? node.properties : void 0;
|
|
2474
|
+
const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
|
|
2475
|
+
const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
|
|
2088
2476
|
out.push({ component: node, path, objectName });
|
|
2089
2477
|
if (!props) return;
|
|
2090
2478
|
if (Array.isArray(props.items)) {
|
|
2091
2479
|
for (let i = 0; i < props.items.length; i++) {
|
|
2092
2480
|
const item = props.items[i];
|
|
2093
|
-
if (!
|
|
2481
|
+
if (!isRec5(item) || !Array.isArray(item.children)) continue;
|
|
2094
2482
|
for (let c = 0; c < item.children.length; c++) {
|
|
2095
2483
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
2096
2484
|
}
|
|
@@ -2112,12 +2500,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
2112
2500
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
2113
2501
|
for (let r = 0; r < regions.length; r++) {
|
|
2114
2502
|
const region = regions[r];
|
|
2115
|
-
if (!
|
|
2503
|
+
if (!isRec5(region) || !Array.isArray(region.components)) continue;
|
|
2116
2504
|
for (let c = 0; c < region.components.length; c++) {
|
|
2117
2505
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
2118
2506
|
}
|
|
2119
2507
|
}
|
|
2120
|
-
const slots =
|
|
2508
|
+
const slots = isRec5(page.slots) ? page.slots : void 0;
|
|
2121
2509
|
if (slots) {
|
|
2122
2510
|
for (const [slot, value] of Object.entries(slots)) {
|
|
2123
2511
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2139,13 +2527,13 @@ function asArray8(v) {
|
|
|
2139
2527
|
}
|
|
2140
2528
|
return [];
|
|
2141
2529
|
}
|
|
2142
|
-
function
|
|
2530
|
+
function strName6(v) {
|
|
2143
2531
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2144
2532
|
}
|
|
2145
2533
|
function strList(v) {
|
|
2146
2534
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2147
2535
|
}
|
|
2148
|
-
function
|
|
2536
|
+
function distance4(a, b) {
|
|
2149
2537
|
const m = a.length;
|
|
2150
2538
|
const n = b.length;
|
|
2151
2539
|
if (m === 0) return n;
|
|
@@ -2161,11 +2549,11 @@ function distance3(a, b) {
|
|
|
2161
2549
|
}
|
|
2162
2550
|
return prev[n];
|
|
2163
2551
|
}
|
|
2164
|
-
function
|
|
2552
|
+
function suggest5(target, known) {
|
|
2165
2553
|
let best;
|
|
2166
2554
|
let bestScore = Infinity;
|
|
2167
2555
|
for (const candidate of known) {
|
|
2168
|
-
const d =
|
|
2556
|
+
const d = distance4(target, candidate);
|
|
2169
2557
|
if (d < bestScore) {
|
|
2170
2558
|
bestScore = d;
|
|
2171
2559
|
best = candidate;
|
|
@@ -2177,13 +2565,13 @@ function suggest4(target, known) {
|
|
|
2177
2565
|
function collectActionNames(stack) {
|
|
2178
2566
|
const names = /* @__PURE__ */ new Set();
|
|
2179
2567
|
for (const action of asArray8(stack.actions)) {
|
|
2180
|
-
const n =
|
|
2568
|
+
const n = strName6(action?.name);
|
|
2181
2569
|
if (n) names.add(n);
|
|
2182
2570
|
}
|
|
2183
2571
|
for (const obj of asArray8(stack.objects)) {
|
|
2184
2572
|
if (!obj || typeof obj !== "object") continue;
|
|
2185
2573
|
for (const action of asArray8(obj.actions)) {
|
|
2186
|
-
const n =
|
|
2574
|
+
const n = strName6(action?.name);
|
|
2187
2575
|
if (n) names.add(n);
|
|
2188
2576
|
}
|
|
2189
2577
|
}
|
|
@@ -2200,7 +2588,7 @@ function validateActionNameRefs(stack) {
|
|
|
2200
2588
|
rule: ACTION_NAME_UNDEFINED,
|
|
2201
2589
|
where,
|
|
2202
2590
|
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.` +
|
|
2591
|
+
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
2592
|
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
2593
|
});
|
|
2206
2594
|
};
|
|
@@ -2226,7 +2614,7 @@ function validateActionNameRefs(stack) {
|
|
|
2226
2614
|
if (!def || typeof def !== "object") continue;
|
|
2227
2615
|
if (def.execution !== "aggregate") continue;
|
|
2228
2616
|
if (def.actionDef !== void 0) continue;
|
|
2229
|
-
const name =
|
|
2617
|
+
const name = strName6(def.name);
|
|
2230
2618
|
if (!name) continue;
|
|
2231
2619
|
check(
|
|
2232
2620
|
name,
|
|
@@ -2241,7 +2629,7 @@ function validateActionNameRefs(stack) {
|
|
|
2241
2629
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2242
2630
|
const view = views[vi];
|
|
2243
2631
|
if (!view || typeof view !== "object") continue;
|
|
2244
|
-
const viewName =
|
|
2632
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
|
|
2245
2633
|
const owner = `view "${viewName}"`;
|
|
2246
2634
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
2247
2635
|
const listViews = view.listViews;
|
|
@@ -2257,7 +2645,7 @@ function validateActionNameRefs(stack) {
|
|
|
2257
2645
|
if (!obj || typeof obj !== "object") continue;
|
|
2258
2646
|
const objListViews = obj.listViews;
|
|
2259
2647
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
2260
|
-
const owner = `object "${
|
|
2648
|
+
const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
|
|
2261
2649
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
2262
2650
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
2263
2651
|
}
|
|
@@ -2266,7 +2654,7 @@ function validateActionNameRefs(stack) {
|
|
|
2266
2654
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2267
2655
|
const page = pages[pi];
|
|
2268
2656
|
if (!page || typeof page !== "object") continue;
|
|
2269
|
-
const pageName =
|
|
2657
|
+
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2270
2658
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2271
2659
|
const props = component.properties;
|
|
2272
2660
|
if (!props || typeof props !== "object") continue;
|
|
@@ -2274,7 +2662,7 @@ function validateActionNameRefs(stack) {
|
|
|
2274
2662
|
for (let ai = 0; ai < names.length; ai++) {
|
|
2275
2663
|
check(
|
|
2276
2664
|
names[ai],
|
|
2277
|
-
`page "${pageName}" \xB7 component "${
|
|
2665
|
+
`page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
|
|
2278
2666
|
`${path}.properties.actionNames[${ai}]`,
|
|
2279
2667
|
"Quick-actions bar"
|
|
2280
2668
|
);
|
|
@@ -2285,7 +2673,7 @@ function validateActionNameRefs(stack) {
|
|
|
2285
2673
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2286
2674
|
const app = apps[ai];
|
|
2287
2675
|
if (!app || typeof app !== "object") continue;
|
|
2288
|
-
const appName =
|
|
2676
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
2289
2677
|
const walkNav = (items, basePath) => {
|
|
2290
2678
|
const navItems = asArray8(items);
|
|
2291
2679
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -2293,20 +2681,20 @@ function validateActionNameRefs(stack) {
|
|
|
2293
2681
|
if (!nav || typeof nav !== "object") continue;
|
|
2294
2682
|
const navPath = `${basePath}[${ni}]`;
|
|
2295
2683
|
const actionDef = nav.actionDef;
|
|
2296
|
-
const actionName =
|
|
2684
|
+
const actionName = strName6(actionDef?.actionName);
|
|
2297
2685
|
if (nav.type === "action" && actionName) {
|
|
2298
2686
|
check(
|
|
2299
2687
|
actionName,
|
|
2300
|
-
`app "${appName}" \xB7 nav "${
|
|
2688
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2301
2689
|
`${navPath}.actionDef.actionName`,
|
|
2302
2690
|
"Navigation action item"
|
|
2303
2691
|
);
|
|
2304
2692
|
}
|
|
2305
|
-
const runAction =
|
|
2693
|
+
const runAction = strName6(nav.runAction);
|
|
2306
2694
|
if (nav.type === "object" && runAction) {
|
|
2307
2695
|
check(
|
|
2308
2696
|
runAction,
|
|
2309
|
-
`app "${appName}" \xB7 nav "${
|
|
2697
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2310
2698
|
`${navPath}.runAction`,
|
|
2311
2699
|
"Navigation deep-link auto-run"
|
|
2312
2700
|
);
|
|
@@ -2333,23 +2721,23 @@ function asArray9(v) {
|
|
|
2333
2721
|
}
|
|
2334
2722
|
return [];
|
|
2335
2723
|
}
|
|
2336
|
-
function
|
|
2724
|
+
function strName7(v) {
|
|
2337
2725
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2338
2726
|
}
|
|
2339
|
-
function
|
|
2727
|
+
function isRec6(v) {
|
|
2340
2728
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2341
2729
|
}
|
|
2342
2730
|
function fieldRefsFrom(value, basePath) {
|
|
2343
2731
|
const out = [];
|
|
2344
2732
|
const one = (v, path) => {
|
|
2345
|
-
const bare =
|
|
2733
|
+
const bare = strName7(v);
|
|
2346
2734
|
if (bare) {
|
|
2347
2735
|
out.push({ name: bare, path });
|
|
2348
2736
|
return;
|
|
2349
2737
|
}
|
|
2350
|
-
if (!
|
|
2351
|
-
const named =
|
|
2352
|
-
if (named) out.push({ name: named, path: `${path}.${
|
|
2738
|
+
if (!isRec6(v)) return;
|
|
2739
|
+
const named = strName7(v.field) ?? strName7(v.name);
|
|
2740
|
+
if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
|
|
2353
2741
|
};
|
|
2354
2742
|
if (Array.isArray(value)) {
|
|
2355
2743
|
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
@@ -2375,7 +2763,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2375
2763
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2376
2764
|
"record:path": { props: ["statusField"] },
|
|
2377
2765
|
"element:number": { props: ["field"] },
|
|
2378
|
-
|
|
2766
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
2767
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
2768
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
2769
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
2770
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
2771
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
2379
2772
|
"element:form": { props: ["fields"] },
|
|
2380
2773
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2381
2774
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -2405,18 +2798,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2405
2798
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2406
2799
|
for (let si = 0; si < sections.length; si++) {
|
|
2407
2800
|
const section = sections[si];
|
|
2408
|
-
if (!
|
|
2801
|
+
if (!isRec6(section)) continue;
|
|
2409
2802
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2410
2803
|
}
|
|
2411
2804
|
}
|
|
2412
2805
|
return refs;
|
|
2413
2806
|
}
|
|
2414
2807
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2415
|
-
const add =
|
|
2416
|
-
const picker = add &&
|
|
2808
|
+
const add = isRec6(props.add) ? props.add : void 0;
|
|
2809
|
+
const picker = add && isRec6(add.picker) ? add.picker : void 0;
|
|
2417
2810
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2418
2811
|
return {
|
|
2419
|
-
relatedObject:
|
|
2812
|
+
relatedObject: strName7(props.objectName),
|
|
2420
2813
|
related: [
|
|
2421
2814
|
...fieldRefsFrom(props.columns, at("columns")),
|
|
2422
2815
|
...sortFieldRefs(props.sort, at("sort")),
|
|
@@ -2425,7 +2818,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2425
2818
|
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
2426
2819
|
],
|
|
2427
2820
|
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
2428
|
-
pickerObject: picker ?
|
|
2821
|
+
pickerObject: picker ? strName7(picker.object) : void 0,
|
|
2429
2822
|
picker: picker ? [
|
|
2430
2823
|
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
2431
2824
|
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
@@ -2434,13 +2827,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2434
2827
|
}
|
|
2435
2828
|
function indexObjectFields(stack) {
|
|
2436
2829
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2437
|
-
if (!
|
|
2830
|
+
if (!isRec6(stack)) return objectFields;
|
|
2438
2831
|
for (const obj of asArray9(stack.objects)) {
|
|
2439
|
-
const name =
|
|
2832
|
+
const name = strName7(obj.name);
|
|
2440
2833
|
if (!name) continue;
|
|
2441
2834
|
const names = /* @__PURE__ */ new Set();
|
|
2442
2835
|
for (const f of asArray9(obj.fields)) {
|
|
2443
|
-
const fn =
|
|
2836
|
+
const fn = strName7(f.name);
|
|
2444
2837
|
if (fn) names.add(fn);
|
|
2445
2838
|
}
|
|
2446
2839
|
objectFields.set(name, names);
|
|
@@ -2488,15 +2881,15 @@ function validatePageFieldBindings(stack) {
|
|
|
2488
2881
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2489
2882
|
const page = pages[pi];
|
|
2490
2883
|
if (!page || typeof page !== "object") continue;
|
|
2491
|
-
const pageName =
|
|
2884
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2492
2885
|
const checkRefs = (refs, objectName, where) => {
|
|
2493
2886
|
findings.push(
|
|
2494
2887
|
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2495
2888
|
);
|
|
2496
2889
|
};
|
|
2497
2890
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2498
|
-
const type =
|
|
2499
|
-
const props =
|
|
2891
|
+
const type = strName7(component.type);
|
|
2892
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2500
2893
|
if (!type || !props) continue;
|
|
2501
2894
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2502
2895
|
const base = `${path}.properties`;
|
|
@@ -2511,16 +2904,16 @@ function validatePageFieldBindings(stack) {
|
|
|
2511
2904
|
if (!refs) continue;
|
|
2512
2905
|
checkRefs(refs, objectName, where);
|
|
2513
2906
|
}
|
|
2514
|
-
const cfg =
|
|
2907
|
+
const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2515
2908
|
if (cfg) {
|
|
2516
|
-
const cfgObject =
|
|
2909
|
+
const cfgObject = strName7(cfg.source) ?? strName7(page.object);
|
|
2517
2910
|
const base = `pages[${pi}].interfaceConfig`;
|
|
2518
2911
|
const refs = [
|
|
2519
2912
|
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
2520
2913
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2521
2914
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2522
2915
|
];
|
|
2523
|
-
const userFilters =
|
|
2916
|
+
const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2524
2917
|
if (userFilters) {
|
|
2525
2918
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2526
2919
|
}
|
|
@@ -2542,16 +2935,16 @@ function asArray10(v) {
|
|
|
2542
2935
|
}
|
|
2543
2936
|
return [];
|
|
2544
2937
|
}
|
|
2545
|
-
function
|
|
2938
|
+
function strName8(v) {
|
|
2546
2939
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2547
2940
|
}
|
|
2548
2941
|
function strList2(v) {
|
|
2549
2942
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2550
2943
|
}
|
|
2551
|
-
function
|
|
2944
|
+
function isRec7(v) {
|
|
2552
2945
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2553
2946
|
}
|
|
2554
|
-
function
|
|
2947
|
+
function distance5(a, b) {
|
|
2555
2948
|
const m = a.length;
|
|
2556
2949
|
const n = b.length;
|
|
2557
2950
|
if (m === 0) return n;
|
|
@@ -2567,11 +2960,11 @@ function distance4(a, b) {
|
|
|
2567
2960
|
}
|
|
2568
2961
|
return prev[n];
|
|
2569
2962
|
}
|
|
2570
|
-
function
|
|
2963
|
+
function suggest6(target, known) {
|
|
2571
2964
|
let best;
|
|
2572
2965
|
let bestScore = Infinity;
|
|
2573
2966
|
for (const c of known) {
|
|
2574
|
-
const d =
|
|
2967
|
+
const d = distance5(target, c);
|
|
2575
2968
|
if (d < bestScore) {
|
|
2576
2969
|
bestScore = d;
|
|
2577
2970
|
best = c;
|
|
@@ -2587,16 +2980,16 @@ function list2(names) {
|
|
|
2587
2980
|
function indexDatasets(stack) {
|
|
2588
2981
|
const out = /* @__PURE__ */ new Map();
|
|
2589
2982
|
for (const ds of asArray10(stack.datasets)) {
|
|
2590
|
-
const name =
|
|
2983
|
+
const name = strName8(ds.name);
|
|
2591
2984
|
if (!name) continue;
|
|
2592
2985
|
const dimensions = /* @__PURE__ */ new Set();
|
|
2593
2986
|
for (const d of asArray10(ds.dimensions)) {
|
|
2594
|
-
const n =
|
|
2987
|
+
const n = strName8(d.name);
|
|
2595
2988
|
if (n) dimensions.add(n);
|
|
2596
2989
|
}
|
|
2597
2990
|
const measures = /* @__PURE__ */ new Set();
|
|
2598
2991
|
for (const m of asArray10(ds.measures)) {
|
|
2599
|
-
const n =
|
|
2992
|
+
const n = strName8(m.name);
|
|
2600
2993
|
if (n) measures.add(n);
|
|
2601
2994
|
}
|
|
2602
2995
|
out.set(name, { dimensions, measures });
|
|
@@ -2619,7 +3012,7 @@ function validateChartBindings(stack) {
|
|
|
2619
3012
|
where: binding.where,
|
|
2620
3013
|
path: `${binding.path}.dataset`,
|
|
2621
3014
|
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())}.${
|
|
3015
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
2623
3016
|
});
|
|
2624
3017
|
return;
|
|
2625
3018
|
}
|
|
@@ -2631,7 +3024,7 @@ function validateChartBindings(stack) {
|
|
|
2631
3024
|
where: binding.where,
|
|
2632
3025
|
path,
|
|
2633
3026
|
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)}.${
|
|
3027
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
2635
3028
|
});
|
|
2636
3029
|
};
|
|
2637
3030
|
const measureRef = (name, path, selected2) => {
|
|
@@ -2642,7 +3035,7 @@ function validateChartBindings(stack) {
|
|
|
2642
3035
|
where: binding.where,
|
|
2643
3036
|
path,
|
|
2644
3037
|
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)}.${
|
|
3038
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
2646
3039
|
});
|
|
2647
3040
|
return;
|
|
2648
3041
|
}
|
|
@@ -2677,26 +3070,26 @@ function validateChartBindings(stack) {
|
|
|
2677
3070
|
const reports = asArray10(stack.reports);
|
|
2678
3071
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2679
3072
|
const report = reports[ri];
|
|
2680
|
-
if (!
|
|
2681
|
-
const reportName =
|
|
3073
|
+
if (!isRec7(report)) continue;
|
|
3074
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
2682
3075
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2683
|
-
if (!
|
|
3076
|
+
if (!isRec7(chart)) return;
|
|
2684
3077
|
check({
|
|
2685
3078
|
dataset,
|
|
2686
3079
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
2687
3080
|
// it in lets the yAxis "declared but not selected" check work without
|
|
2688
3081
|
// reporting the selection itself twice.
|
|
2689
3082
|
values: { names: values, path: `${path}.values` },
|
|
2690
|
-
xAxis:
|
|
2691
|
-
yAxis:
|
|
2692
|
-
series: asArray10(chart.series).map((s, si) => ({ name:
|
|
3083
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
3084
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
3085
|
+
series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
2693
3086
|
where,
|
|
2694
3087
|
path: `${path}.chart`
|
|
2695
3088
|
});
|
|
2696
3089
|
};
|
|
2697
3090
|
checkReportChart(
|
|
2698
3091
|
report.chart,
|
|
2699
|
-
|
|
3092
|
+
strName8(report.dataset),
|
|
2700
3093
|
strList2(report.values),
|
|
2701
3094
|
`report "${reportName}" \xB7 chart`,
|
|
2702
3095
|
`reports[${ri}]`
|
|
@@ -2704,22 +3097,22 @@ function validateChartBindings(stack) {
|
|
|
2704
3097
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2705
3098
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2706
3099
|
const block = blocks[bi];
|
|
2707
|
-
if (!
|
|
3100
|
+
if (!isRec7(block)) continue;
|
|
2708
3101
|
checkReportChart(
|
|
2709
3102
|
block.chart,
|
|
2710
|
-
|
|
3103
|
+
strName8(block.dataset),
|
|
2711
3104
|
strList2(block.values),
|
|
2712
|
-
`report "${reportName}" \xB7 block "${
|
|
3105
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
2713
3106
|
`reports[${ri}].blocks[${bi}]`
|
|
2714
3107
|
);
|
|
2715
3108
|
}
|
|
2716
3109
|
}
|
|
2717
3110
|
const checkListChart = (container, where, path) => {
|
|
2718
|
-
if (!
|
|
3111
|
+
if (!isRec7(container)) return;
|
|
2719
3112
|
const chart = container.chart;
|
|
2720
|
-
if (!
|
|
3113
|
+
if (!isRec7(chart)) return;
|
|
2721
3114
|
check({
|
|
2722
|
-
dataset:
|
|
3115
|
+
dataset: strName8(chart.dataset),
|
|
2723
3116
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
2724
3117
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
2725
3118
|
where,
|
|
@@ -2729,10 +3122,10 @@ function validateChartBindings(stack) {
|
|
|
2729
3122
|
const views = asArray10(stack.views);
|
|
2730
3123
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2731
3124
|
const view = views[vi];
|
|
2732
|
-
if (!
|
|
2733
|
-
const viewName =
|
|
3125
|
+
if (!isRec7(view)) continue;
|
|
3126
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
2734
3127
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2735
|
-
if (
|
|
3128
|
+
if (isRec7(view.listViews)) {
|
|
2736
3129
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2737
3130
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2738
3131
|
}
|
|
@@ -2741,8 +3134,8 @@ function validateChartBindings(stack) {
|
|
|
2741
3134
|
const objects = asArray10(stack.objects);
|
|
2742
3135
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2743
3136
|
const obj = objects[oi];
|
|
2744
|
-
if (!
|
|
2745
|
-
const objName =
|
|
3137
|
+
if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
|
|
3138
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
2746
3139
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2747
3140
|
checkListChart(
|
|
2748
3141
|
lv,
|
|
@@ -2754,19 +3147,19 @@ function validateChartBindings(stack) {
|
|
|
2754
3147
|
const pages = asArray10(stack.pages);
|
|
2755
3148
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2756
3149
|
const page = pages[pi];
|
|
2757
|
-
if (!
|
|
2758
|
-
const pageName =
|
|
3150
|
+
if (!isRec7(page)) continue;
|
|
3151
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
2759
3152
|
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:
|
|
3153
|
+
const props = isRec7(component.properties) ? component.properties : void 0;
|
|
3154
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
3155
|
+
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
3156
|
+
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
2764
3157
|
check({
|
|
2765
|
-
dataset:
|
|
3158
|
+
dataset: strName8(props.dataset),
|
|
2766
3159
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
2767
3160
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
2768
3161
|
series: [...axisRefs, ...seriesRefs],
|
|
2769
|
-
where: `page "${pageName}" \xB7 ${
|
|
3162
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
2770
3163
|
path: `${path}.properties`
|
|
2771
3164
|
});
|
|
2772
3165
|
}
|
|
@@ -2833,7 +3226,7 @@ function asArray12(v) {
|
|
|
2833
3226
|
}
|
|
2834
3227
|
return [];
|
|
2835
3228
|
}
|
|
2836
|
-
function
|
|
3229
|
+
function strName9(v) {
|
|
2837
3230
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2838
3231
|
}
|
|
2839
3232
|
function collectNavExposures(stack) {
|
|
@@ -2842,18 +3235,18 @@ function collectNavExposures(stack) {
|
|
|
2842
3235
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2843
3236
|
const app = apps[ai];
|
|
2844
3237
|
if (!app || typeof app !== "object") continue;
|
|
2845
|
-
const appName =
|
|
3238
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
2846
3239
|
const walk = (items, basePath) => {
|
|
2847
3240
|
const navItems = asArray12(items);
|
|
2848
3241
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
2849
3242
|
const nav = navItems[ni];
|
|
2850
3243
|
if (!nav || typeof nav !== "object") continue;
|
|
2851
3244
|
const navPath = `${basePath}[${ni}]`;
|
|
2852
|
-
const objectName =
|
|
3245
|
+
const objectName = strName9(nav.objectName);
|
|
2853
3246
|
if (nav.type === "object" && objectName) {
|
|
2854
3247
|
out.push({
|
|
2855
3248
|
objectName,
|
|
2856
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3249
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
2857
3250
|
path: `${navPath}.objectName`
|
|
2858
3251
|
});
|
|
2859
3252
|
}
|
|
@@ -2877,7 +3270,7 @@ function validateNavAccess(stack) {
|
|
|
2877
3270
|
if (exposures.length === 0) return findings;
|
|
2878
3271
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
2879
3272
|
for (const obj of asArray12(stack.objects)) {
|
|
2880
|
-
const n =
|
|
3273
|
+
const n = strName9(obj.name);
|
|
2881
3274
|
if (n) ownObjects.add(n);
|
|
2882
3275
|
}
|
|
2883
3276
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -2907,13 +3300,13 @@ function validateNavAccess(stack) {
|
|
|
2907
3300
|
|
|
2908
3301
|
// src/validate-nav-target-refs.ts
|
|
2909
3302
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2910
|
-
var
|
|
3303
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2911
3304
|
function asArray13(v) {
|
|
2912
|
-
if (Array.isArray(v)) return v.filter(
|
|
2913
|
-
if (
|
|
3305
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
3306
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2914
3307
|
return [];
|
|
2915
3308
|
}
|
|
2916
|
-
function
|
|
3309
|
+
function strName10(v) {
|
|
2917
3310
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2918
3311
|
}
|
|
2919
3312
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -2925,14 +3318,14 @@ var NAV_TARGETS = [
|
|
|
2925
3318
|
function namesOf(collection) {
|
|
2926
3319
|
const out = /* @__PURE__ */ new Set();
|
|
2927
3320
|
for (const entry of asArray13(collection)) {
|
|
2928
|
-
const n =
|
|
3321
|
+
const n = strName10(entry.name);
|
|
2929
3322
|
if (n) out.add(n);
|
|
2930
3323
|
}
|
|
2931
3324
|
return out;
|
|
2932
3325
|
}
|
|
2933
3326
|
function validateNavTargetRefs(stack) {
|
|
2934
3327
|
const findings = [];
|
|
2935
|
-
if (!
|
|
3328
|
+
if (!isRec8(stack)) return findings;
|
|
2936
3329
|
const apps = asArray13(stack.apps);
|
|
2937
3330
|
if (apps.length === 0) return findings;
|
|
2938
3331
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2940,16 +3333,16 @@ function validateNavTargetRefs(stack) {
|
|
|
2940
3333
|
declared.set(collection, namesOf(stack[collection]));
|
|
2941
3334
|
}
|
|
2942
3335
|
for (const [ai, app] of apps.entries()) {
|
|
2943
|
-
const appName =
|
|
3336
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2944
3337
|
const walk = (items, basePath) => {
|
|
2945
3338
|
if (!Array.isArray(items)) return;
|
|
2946
3339
|
for (const [ni, raw] of items.entries()) {
|
|
2947
|
-
if (!
|
|
3340
|
+
if (!isRec8(raw)) continue;
|
|
2948
3341
|
const nav = raw;
|
|
2949
3342
|
const navPath = `${basePath}[${ni}]`;
|
|
2950
3343
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
2951
3344
|
if (nav.type !== type) continue;
|
|
2952
|
-
const target =
|
|
3345
|
+
const target = strName10(nav[prop]);
|
|
2953
3346
|
if (!target || isInterpolated2(target)) continue;
|
|
2954
3347
|
const known = declared.get(collection);
|
|
2955
3348
|
if (known.has(target)) continue;
|
|
@@ -2957,7 +3350,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2957
3350
|
findings.push({
|
|
2958
3351
|
severity: "warning",
|
|
2959
3352
|
rule: NAV_TARGET_UNRESOLVED,
|
|
2960
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3353
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
2961
3354
|
path: `${navPath}.${prop}`,
|
|
2962
3355
|
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
3356
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -2976,44 +3369,44 @@ function validateNavTargetRefs(stack) {
|
|
|
2976
3369
|
}
|
|
2977
3370
|
|
|
2978
3371
|
// src/validate-nav-object-servability.ts
|
|
2979
|
-
var
|
|
3372
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
2980
3373
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2981
|
-
var
|
|
3374
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2982
3375
|
function asArray14(v) {
|
|
2983
|
-
if (Array.isArray(v)) return v.filter(
|
|
2984
|
-
if (
|
|
3376
|
+
if (Array.isArray(v)) return v.filter(isRec9);
|
|
3377
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
|
|
2985
3378
|
return [];
|
|
2986
3379
|
}
|
|
2987
|
-
function
|
|
3380
|
+
function strName11(v) {
|
|
2988
3381
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2989
3382
|
}
|
|
2990
3383
|
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2991
3384
|
function validateNavObjectServability(stack) {
|
|
2992
3385
|
const findings = [];
|
|
2993
|
-
if (!
|
|
3386
|
+
if (!isRec9(stack)) return findings;
|
|
2994
3387
|
const apps = asArray14(stack.apps);
|
|
2995
3388
|
if (apps.length === 0) return findings;
|
|
2996
3389
|
const ownEnable = /* @__PURE__ */ new Map();
|
|
2997
3390
|
const objects = asArray14(stack.objects);
|
|
2998
3391
|
for (const [oi, obj] of objects.entries()) {
|
|
2999
|
-
const n =
|
|
3392
|
+
const n = strName11(obj.name);
|
|
3000
3393
|
if (!n) continue;
|
|
3001
3394
|
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
3002
3395
|
}
|
|
3003
3396
|
if (ownEnable.size === 0) return findings;
|
|
3004
3397
|
for (const [ai, app] of apps.entries()) {
|
|
3005
|
-
const appName =
|
|
3398
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
3006
3399
|
const walk = (items, basePath) => {
|
|
3007
3400
|
if (!Array.isArray(items)) return;
|
|
3008
3401
|
for (const [ni, raw] of items.entries()) {
|
|
3009
|
-
if (!
|
|
3402
|
+
if (!isRec9(raw)) continue;
|
|
3010
3403
|
const nav = raw;
|
|
3011
3404
|
const navPath = `${basePath}[${ni}]`;
|
|
3012
3405
|
if (nav.type === "object") {
|
|
3013
|
-
const target =
|
|
3406
|
+
const target = strName11(nav.objectName);
|
|
3014
3407
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3015
|
-
if (target && declared && !(0,
|
|
3016
|
-
const enable =
|
|
3408
|
+
if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
|
|
3409
|
+
const enable = isRec9(declared.enable) ? declared.enable : {};
|
|
3017
3410
|
const apiDisabled = enable.apiEnabled === false;
|
|
3018
3411
|
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
3412
|
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
@@ -3021,7 +3414,7 @@ function validateNavObjectServability(stack) {
|
|
|
3021
3414
|
findings.push({
|
|
3022
3415
|
severity: "error",
|
|
3023
3416
|
rule: NAV_OBJECT_UNSERVABLE,
|
|
3024
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3417
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
|
|
3025
3418
|
// The nav entry is where the dead row is authored; the `enable`
|
|
3026
3419
|
// key that condemns it is named in the message, because the fix
|
|
3027
3420
|
// may belong at either end.
|
|
@@ -3048,27 +3441,27 @@ var import_spec = require("@objectstack/spec");
|
|
|
3048
3441
|
var import_system4 = require("@objectstack/spec/system");
|
|
3049
3442
|
|
|
3050
3443
|
// src/view-walk.ts
|
|
3051
|
-
function
|
|
3444
|
+
function isRec10(v) {
|
|
3052
3445
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3053
3446
|
}
|
|
3054
|
-
function
|
|
3447
|
+
function strName12(v) {
|
|
3055
3448
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3056
3449
|
}
|
|
3057
3450
|
function viewObjectName(view) {
|
|
3058
|
-
return
|
|
3451
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
|
|
3059
3452
|
}
|
|
3060
3453
|
function viewContainerSites(view, basePath) {
|
|
3061
|
-
if (!
|
|
3454
|
+
if (!isRec10(view)) return [];
|
|
3062
3455
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3063
|
-
if (
|
|
3456
|
+
if (isRec10(view.form)) {
|
|
3064
3457
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3065
3458
|
}
|
|
3066
3459
|
for (const key of ["listViews", "formViews"]) {
|
|
3067
3460
|
const container = view[key];
|
|
3068
|
-
if (!
|
|
3461
|
+
if (!isRec10(container)) continue;
|
|
3069
3462
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
3070
3463
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
3071
|
-
if (!
|
|
3464
|
+
if (!isRec10(sub)) continue;
|
|
3072
3465
|
sites.push({
|
|
3073
3466
|
view: sub,
|
|
3074
3467
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -3086,18 +3479,18 @@ function formViewSites(view, basePath) {
|
|
|
3086
3479
|
// src/validate-translation-references.ts
|
|
3087
3480
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3088
3481
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3089
|
-
function
|
|
3482
|
+
function isRec11(v) {
|
|
3090
3483
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3091
3484
|
}
|
|
3092
3485
|
function asArray15(v) {
|
|
3093
3486
|
if (Array.isArray(v)) return v;
|
|
3094
|
-
if (
|
|
3487
|
+
if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
|
|
3095
3488
|
return [];
|
|
3096
3489
|
}
|
|
3097
|
-
function
|
|
3490
|
+
function strName13(v) {
|
|
3098
3491
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3099
3492
|
}
|
|
3100
|
-
function
|
|
3493
|
+
function distance6(a, b) {
|
|
3101
3494
|
const m = a.length;
|
|
3102
3495
|
const n = b.length;
|
|
3103
3496
|
if (m === 0) return n;
|
|
@@ -3113,7 +3506,7 @@ function distance5(a, b) {
|
|
|
3113
3506
|
}
|
|
3114
3507
|
return prev[n];
|
|
3115
3508
|
}
|
|
3116
|
-
function
|
|
3509
|
+
function suggest7(target, known) {
|
|
3117
3510
|
const names = [...known];
|
|
3118
3511
|
const segmentMatch = names.find(
|
|
3119
3512
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -3122,7 +3515,7 @@ function suggest6(target, known) {
|
|
|
3122
3515
|
let best;
|
|
3123
3516
|
let bestScore = Infinity;
|
|
3124
3517
|
for (const candidate of names) {
|
|
3125
|
-
const d =
|
|
3518
|
+
const d = distance6(target, candidate);
|
|
3126
3519
|
if (d < bestScore) {
|
|
3127
3520
|
bestScore = d;
|
|
3128
3521
|
best = candidate;
|
|
@@ -3155,33 +3548,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
3155
3548
|
const addSections = (container, binding) => {
|
|
3156
3549
|
if (!binding) return;
|
|
3157
3550
|
for (const section of asArray15(container.sections)) {
|
|
3158
|
-
const sectionName =
|
|
3551
|
+
const sectionName = strName13(section.name);
|
|
3159
3552
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
3160
3553
|
}
|
|
3161
3554
|
};
|
|
3162
|
-
const listBinding =
|
|
3163
|
-
if (
|
|
3164
|
-
addView(recordObject ?? listBinding,
|
|
3555
|
+
const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
|
|
3556
|
+
if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3557
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
3165
3558
|
const named = namedViewKeys(view);
|
|
3166
3559
|
for (const family of ["listViews", "formViews"]) {
|
|
3167
3560
|
const container = view[family];
|
|
3168
|
-
if (!
|
|
3561
|
+
if (!isRec11(container)) continue;
|
|
3169
3562
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3170
3563
|
let at = 0;
|
|
3171
3564
|
for (const sub of Object.values(container)) {
|
|
3172
3565
|
if (!sub || typeof sub !== "object") continue;
|
|
3173
3566
|
const registryKey = registryKeys[at++];
|
|
3174
|
-
if (!
|
|
3567
|
+
if (!isRec11(sub)) continue;
|
|
3175
3568
|
const binding = bindingOf(sub) ?? listBinding;
|
|
3176
3569
|
addView(binding, registryKey);
|
|
3177
3570
|
addSections(sub, binding);
|
|
3178
3571
|
}
|
|
3179
3572
|
}
|
|
3180
|
-
if (
|
|
3573
|
+
if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
3181
3574
|
addSections(view, recordObject ?? listBinding);
|
|
3182
3575
|
}
|
|
3183
3576
|
function defaultListViewKey(object, container) {
|
|
3184
|
-
if (!object || !
|
|
3577
|
+
if (!object || !isRec11(container.list)) return void 0;
|
|
3185
3578
|
const item = (0, import_spec.expandViewContainer)(object, container).find(
|
|
3186
3579
|
(i) => i.viewKind === "list" && i.isDefault
|
|
3187
3580
|
);
|
|
@@ -3193,7 +3586,7 @@ function namedViewKeys(container) {
|
|
|
3193
3586
|
const object = "probe";
|
|
3194
3587
|
const prefix = `${object}.`;
|
|
3195
3588
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3196
|
-
const countEntries = (v) =>
|
|
3589
|
+
const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3197
3590
|
const listCount = countEntries(container.listViews);
|
|
3198
3591
|
const formCount = countEntries(container.formViews);
|
|
3199
3592
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -3211,14 +3604,14 @@ function readOptions(field) {
|
|
|
3211
3604
|
values.add(opt);
|
|
3212
3605
|
continue;
|
|
3213
3606
|
}
|
|
3214
|
-
if (!
|
|
3215
|
-
const value =
|
|
3607
|
+
if (!isRec11(opt)) continue;
|
|
3608
|
+
const value = strName13(opt.value);
|
|
3216
3609
|
if (!value) continue;
|
|
3217
3610
|
values.add(value);
|
|
3218
|
-
const label2 =
|
|
3611
|
+
const label2 = strName13(opt.label);
|
|
3219
3612
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
3220
3613
|
}
|
|
3221
|
-
} else if (
|
|
3614
|
+
} else if (isRec11(raw)) {
|
|
3222
3615
|
for (const [value, label2] of Object.entries(raw)) {
|
|
3223
3616
|
values.add(value);
|
|
3224
3617
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -3239,23 +3632,23 @@ function buildUniverse(stack) {
|
|
|
3239
3632
|
return facts;
|
|
3240
3633
|
};
|
|
3241
3634
|
for (const obj of asArray15(stack.objects)) {
|
|
3242
|
-
const objectName =
|
|
3635
|
+
const objectName = strName13(obj.name);
|
|
3243
3636
|
if (!objectName) continue;
|
|
3244
3637
|
const facts = factsFor(objectName);
|
|
3245
3638
|
for (const field of asArray15(obj.fields)) {
|
|
3246
|
-
const fieldName =
|
|
3639
|
+
const fieldName = strName13(field.name);
|
|
3247
3640
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
3248
3641
|
}
|
|
3249
3642
|
for (const action of asArray15(obj.actions)) {
|
|
3250
|
-
const actionName =
|
|
3643
|
+
const actionName = strName13(action.name);
|
|
3251
3644
|
if (actionName) facts.actions.set(actionName, action);
|
|
3252
3645
|
}
|
|
3253
3646
|
for (const view of asArray15(obj.views)) {
|
|
3254
|
-
collectViewRecord({ ...view, object:
|
|
3647
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
3255
3648
|
}
|
|
3256
3649
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3257
3650
|
for (const group of asArray15(obj.fieldGroups)) {
|
|
3258
|
-
const key =
|
|
3651
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
3259
3652
|
if (key) facts.sections.add(key);
|
|
3260
3653
|
}
|
|
3261
3654
|
}
|
|
@@ -3266,10 +3659,10 @@ function buildUniverse(stack) {
|
|
|
3266
3659
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3267
3660
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3268
3661
|
if (!walked.objectName) continue;
|
|
3269
|
-
const props =
|
|
3662
|
+
const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
|
|
3270
3663
|
if (!props) continue;
|
|
3271
3664
|
for (const section of asArray15(props.sections)) {
|
|
3272
|
-
const sectionName =
|
|
3665
|
+
const sectionName = strName13(section.name);
|
|
3273
3666
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3274
3667
|
}
|
|
3275
3668
|
}
|
|
@@ -3277,9 +3670,9 @@ function buildUniverse(stack) {
|
|
|
3277
3670
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3278
3671
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3279
3672
|
for (const action of asArray15(stack.actions)) {
|
|
3280
|
-
const actionName =
|
|
3673
|
+
const actionName = strName13(action.name);
|
|
3281
3674
|
if (!actionName) continue;
|
|
3282
|
-
const owner =
|
|
3675
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
3283
3676
|
if (owner) {
|
|
3284
3677
|
factsFor(owner).actions.set(actionName, action);
|
|
3285
3678
|
actionOwners.set(actionName, owner);
|
|
@@ -3294,19 +3687,19 @@ function buildUniverse(stack) {
|
|
|
3294
3687
|
}
|
|
3295
3688
|
const apps = /* @__PURE__ */ new Map();
|
|
3296
3689
|
for (const app of asArray15(stack.apps)) {
|
|
3297
|
-
const appName =
|
|
3690
|
+
const appName = strName13(app.name);
|
|
3298
3691
|
if (!appName) continue;
|
|
3299
3692
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3300
3693
|
const walkNav = (items) => {
|
|
3301
3694
|
for (const item of asArray15(items)) {
|
|
3302
|
-
const id =
|
|
3695
|
+
const id = strName13(item.id);
|
|
3303
3696
|
if (id) navIds.add(id);
|
|
3304
3697
|
if (item.children) walkNav(item.children);
|
|
3305
3698
|
}
|
|
3306
3699
|
};
|
|
3307
3700
|
walkNav(app.navigation);
|
|
3308
3701
|
for (const area of asArray15(app.areas)) {
|
|
3309
|
-
const areaId =
|
|
3702
|
+
const areaId = strName13(area.id);
|
|
3310
3703
|
if (areaId) navIds.add(areaId);
|
|
3311
3704
|
walkNav(area.navigation);
|
|
3312
3705
|
}
|
|
@@ -3314,20 +3707,20 @@ function buildUniverse(stack) {
|
|
|
3314
3707
|
}
|
|
3315
3708
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3316
3709
|
for (const dash of asArray15(stack.dashboards)) {
|
|
3317
|
-
const dashName =
|
|
3710
|
+
const dashName = strName13(dash.name);
|
|
3318
3711
|
if (!dashName) continue;
|
|
3319
3712
|
const widgets = /* @__PURE__ */ new Set();
|
|
3320
3713
|
for (const widget of asArray15(dash.widgets)) {
|
|
3321
|
-
const id =
|
|
3714
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
3322
3715
|
if (id) widgets.add(id);
|
|
3323
3716
|
}
|
|
3324
3717
|
const actions = /* @__PURE__ */ new Set();
|
|
3325
3718
|
const headerActions = [
|
|
3326
|
-
...asArray15(
|
|
3719
|
+
...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
|
|
3327
3720
|
...asArray15(dash.actions)
|
|
3328
3721
|
];
|
|
3329
3722
|
for (const action of headerActions) {
|
|
3330
|
-
const key =
|
|
3723
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
3331
3724
|
if (key) actions.add(key);
|
|
3332
3725
|
}
|
|
3333
3726
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3339,7 +3732,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3339
3732
|
}
|
|
3340
3733
|
function validateTranslationReferences(stack) {
|
|
3341
3734
|
const findings = [];
|
|
3342
|
-
if (!
|
|
3735
|
+
if (!isRec11(stack)) return findings;
|
|
3343
3736
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3344
3737
|
if (bundles.length === 0) return findings;
|
|
3345
3738
|
const universe = buildUniverse(stack);
|
|
@@ -3348,13 +3741,13 @@ function validateTranslationReferences(stack) {
|
|
|
3348
3741
|
};
|
|
3349
3742
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3350
3743
|
const bundle = bundles[bi];
|
|
3351
|
-
if (!
|
|
3744
|
+
if (!isRec11(bundle)) continue;
|
|
3352
3745
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3353
|
-
if (!
|
|
3746
|
+
if (!isRec11(rawData)) continue;
|
|
3354
3747
|
const base = localePath(bi, locale);
|
|
3355
3748
|
const inLocale = `locale "${locale}"`;
|
|
3356
3749
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3357
|
-
if (!
|
|
3750
|
+
if (!isRec11(rawNode)) continue;
|
|
3358
3751
|
const objPath = `${base}.objects.${objectName}`;
|
|
3359
3752
|
const facts = universe.objects.get(objectName);
|
|
3360
3753
|
if (!facts) {
|
|
@@ -3362,7 +3755,7 @@ function validateTranslationReferences(stack) {
|
|
|
3362
3755
|
orphan(
|
|
3363
3756
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
3364
3757
|
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.` +
|
|
3758
|
+
(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
3759
|
`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
3760
|
);
|
|
3368
3761
|
continue;
|
|
@@ -3375,12 +3768,12 @@ function validateTranslationReferences(stack) {
|
|
|
3375
3768
|
orphan(
|
|
3376
3769
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
3377
3770
|
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.` +
|
|
3771
|
+
`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
3772
|
`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
3773
|
);
|
|
3381
3774
|
continue;
|
|
3382
3775
|
}
|
|
3383
|
-
if (!
|
|
3776
|
+
if (!isRec11(rawField)) continue;
|
|
3384
3777
|
checkOptionKeys(findings, {
|
|
3385
3778
|
optionMap: rawField.options,
|
|
3386
3779
|
field,
|
|
@@ -3395,7 +3788,7 @@ function validateTranslationReferences(stack) {
|
|
|
3395
3788
|
orphan(
|
|
3396
3789
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
3397
3790
|
`${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.` +
|
|
3791
|
+
`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
3792
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
3400
3793
|
);
|
|
3401
3794
|
}
|
|
@@ -3404,7 +3797,7 @@ function validateTranslationReferences(stack) {
|
|
|
3404
3797
|
orphan(
|
|
3405
3798
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
3406
3799
|
`${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.` +
|
|
3800
|
+
`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
3801
|
`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
3802
|
);
|
|
3410
3803
|
}
|
|
@@ -3415,7 +3808,7 @@ function validateTranslationReferences(stack) {
|
|
|
3415
3808
|
orphan(
|
|
3416
3809
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
3417
3810
|
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.` +
|
|
3811
|
+
`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
3812
|
`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
3813
|
);
|
|
3421
3814
|
continue;
|
|
@@ -3437,7 +3830,7 @@ function validateTranslationReferences(stack) {
|
|
|
3437
3830
|
orphan(
|
|
3438
3831
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
3439
3832
|
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.` +
|
|
3833
|
+
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
3834
|
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
3835
|
);
|
|
3443
3836
|
continue;
|
|
@@ -3457,18 +3850,18 @@ function validateTranslationReferences(stack) {
|
|
|
3457
3850
|
orphan(
|
|
3458
3851
|
`${inLocale} \xB7 app "${appName}"`,
|
|
3459
3852
|
appPath,
|
|
3460
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
3853
|
+
`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
3854
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
3462
3855
|
);
|
|
3463
3856
|
continue;
|
|
3464
3857
|
}
|
|
3465
|
-
if (!
|
|
3858
|
+
if (!isRec11(rawApp)) continue;
|
|
3466
3859
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3467
3860
|
if (navIds.has(navId)) continue;
|
|
3468
3861
|
orphan(
|
|
3469
3862
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
3470
3863
|
`${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.` +
|
|
3864
|
+
`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
3865
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
3473
3866
|
);
|
|
3474
3867
|
}
|
|
@@ -3480,18 +3873,18 @@ function validateTranslationReferences(stack) {
|
|
|
3480
3873
|
orphan(
|
|
3481
3874
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
3482
3875
|
dashPath,
|
|
3483
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
3876
|
+
`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
3877
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
3485
3878
|
);
|
|
3486
3879
|
continue;
|
|
3487
3880
|
}
|
|
3488
|
-
if (!
|
|
3881
|
+
if (!isRec11(rawDash)) continue;
|
|
3489
3882
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3490
3883
|
if (dash.widgets.has(widgetId)) continue;
|
|
3491
3884
|
orphan(
|
|
3492
3885
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
3493
3886
|
`${dashPath}.widgets.${widgetId}`,
|
|
3494
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
3887
|
+
`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
3888
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
3496
3889
|
);
|
|
3497
3890
|
}
|
|
@@ -3500,7 +3893,7 @@ function validateTranslationReferences(stack) {
|
|
|
3500
3893
|
orphan(
|
|
3501
3894
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
3502
3895
|
`${dashPath}.actions.${actionKey}`,
|
|
3503
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
3896
|
+
`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
3897
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
3505
3898
|
);
|
|
3506
3899
|
}
|
|
@@ -3510,7 +3903,7 @@ function validateTranslationReferences(stack) {
|
|
|
3510
3903
|
return findings;
|
|
3511
3904
|
}
|
|
3512
3905
|
function asRecord(v) {
|
|
3513
|
-
return
|
|
3906
|
+
return isRec11(v) ? v : {};
|
|
3514
3907
|
}
|
|
3515
3908
|
function checkOptionKeys(findings, ctx) {
|
|
3516
3909
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3522,7 +3915,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3522
3915
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3523
3916
|
where: ctx.where,
|
|
3524
3917
|
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 "${
|
|
3918
|
+
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
3919
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3527
3920
|
});
|
|
3528
3921
|
return;
|
|
@@ -3535,17 +3928,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3535
3928
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3536
3929
|
where: ctx.where,
|
|
3537
3930
|
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.` +
|
|
3931
|
+
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
3932
|
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
3933
|
});
|
|
3541
3934
|
}
|
|
3542
3935
|
}
|
|
3543
3936
|
function checkActionParams(findings, ctx) {
|
|
3544
|
-
const rawParams = Object.keys(asRecord(
|
|
3937
|
+
const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3545
3938
|
if (rawParams.length === 0) return;
|
|
3546
3939
|
const declared = /* @__PURE__ */ new Set();
|
|
3547
3940
|
for (const param of asArray15(ctx.action.params)) {
|
|
3548
|
-
const name =
|
|
3941
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
3549
3942
|
if (name) declared.add(name);
|
|
3550
3943
|
}
|
|
3551
3944
|
for (const paramName of rawParams) {
|
|
@@ -3555,40 +3948,40 @@ function checkActionParams(findings, ctx) {
|
|
|
3555
3948
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
3556
3949
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
3557
3950
|
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.` +
|
|
3951
|
+
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
3952
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
3560
3953
|
});
|
|
3561
3954
|
}
|
|
3562
3955
|
}
|
|
3563
3956
|
|
|
3564
3957
|
// src/collection-entries.ts
|
|
3565
|
-
function
|
|
3958
|
+
function isRec12(v) {
|
|
3566
3959
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3567
3960
|
}
|
|
3568
3961
|
function collectionEntries(v, base) {
|
|
3569
3962
|
if (Array.isArray(v)) {
|
|
3570
3963
|
const out = [];
|
|
3571
3964
|
for (let i = 0; i < v.length; i++) {
|
|
3572
|
-
if (
|
|
3965
|
+
if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3573
3966
|
}
|
|
3574
3967
|
return out;
|
|
3575
3968
|
}
|
|
3576
|
-
if (
|
|
3577
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3969
|
+
if (isRec12(v)) {
|
|
3970
|
+
return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3578
3971
|
}
|
|
3579
3972
|
return [];
|
|
3580
3973
|
}
|
|
3581
3974
|
|
|
3582
3975
|
// src/validate-translatable-sections.ts
|
|
3583
3976
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3584
|
-
function
|
|
3977
|
+
function isRec13(v) {
|
|
3585
3978
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3586
3979
|
}
|
|
3587
|
-
function
|
|
3980
|
+
function strName14(v) {
|
|
3588
3981
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3589
3982
|
}
|
|
3590
3983
|
function viewLabel(view) {
|
|
3591
|
-
const name =
|
|
3984
|
+
const name = strName14(view.name);
|
|
3592
3985
|
return name ? `view "${name}"` : "";
|
|
3593
3986
|
}
|
|
3594
3987
|
function joinWhere(...parts) {
|
|
@@ -3596,7 +3989,7 @@ function joinWhere(...parts) {
|
|
|
3596
3989
|
}
|
|
3597
3990
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3598
3991
|
const recordObject = viewObjectName(view);
|
|
3599
|
-
const listBinding =
|
|
3992
|
+
const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3600
3993
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3601
3994
|
sites.push({
|
|
3602
3995
|
path: `${site.path}.sections`,
|
|
@@ -3610,11 +4003,11 @@ function translatedObjectNames(stack) {
|
|
|
3610
4003
|
const out = /* @__PURE__ */ new Set();
|
|
3611
4004
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3612
4005
|
for (const bundle of bundles) {
|
|
3613
|
-
if (!
|
|
4006
|
+
if (!isRec13(bundle)) continue;
|
|
3614
4007
|
for (const data of Object.values(bundle)) {
|
|
3615
|
-
if (!
|
|
4008
|
+
if (!isRec13(data) || !isRec13(data.objects)) continue;
|
|
3616
4009
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3617
|
-
if (
|
|
4010
|
+
if (isRec13(node)) out.add(objectName);
|
|
3618
4011
|
}
|
|
3619
4012
|
}
|
|
3620
4013
|
}
|
|
@@ -3626,22 +4019,22 @@ function suggestedName(label2) {
|
|
|
3626
4019
|
}
|
|
3627
4020
|
function validateTranslatableSections(stack) {
|
|
3628
4021
|
const findings = [];
|
|
3629
|
-
if (!
|
|
4022
|
+
if (!isRec13(stack)) return findings;
|
|
3630
4023
|
const translated = translatedObjectNames(stack);
|
|
3631
4024
|
if (translated.size === 0) return findings;
|
|
3632
4025
|
const sites = [];
|
|
3633
4026
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3634
|
-
const objectName =
|
|
4027
|
+
const objectName = strName14(obj.name);
|
|
3635
4028
|
if (!objectName) continue;
|
|
3636
4029
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3637
4030
|
collectViewSites(
|
|
3638
|
-
{ ...view, object:
|
|
4031
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
3639
4032
|
path,
|
|
3640
4033
|
viewLabel(view),
|
|
3641
4034
|
sites
|
|
3642
4035
|
);
|
|
3643
4036
|
}
|
|
3644
|
-
if (
|
|
4037
|
+
if (isRec13(obj.listViews)) {
|
|
3645
4038
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3646
4039
|
}
|
|
3647
4040
|
}
|
|
@@ -3649,13 +4042,13 @@ function validateTranslatableSections(stack) {
|
|
|
3649
4042
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3650
4043
|
}
|
|
3651
4044
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3652
|
-
const pageName =
|
|
4045
|
+
const pageName = strName14(page.name);
|
|
3653
4046
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3654
4047
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3655
4048
|
if (!walked.objectName) continue;
|
|
3656
|
-
const props =
|
|
4049
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
3657
4050
|
if (!props) continue;
|
|
3658
|
-
const type =
|
|
4051
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
3659
4052
|
sites.push({
|
|
3660
4053
|
path: `${walked.path}.properties.sections`,
|
|
3661
4054
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3670,9 +4063,9 @@ function validateTranslatableSections(stack) {
|
|
|
3670
4063
|
if (!Array.isArray(site.sections)) continue;
|
|
3671
4064
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3672
4065
|
const section = site.sections[i];
|
|
3673
|
-
if (!
|
|
3674
|
-
if (
|
|
3675
|
-
const heading =
|
|
4066
|
+
if (!isRec13(section)) continue;
|
|
4067
|
+
if (strName14(section.name)) continue;
|
|
4068
|
+
const heading = strName14(section.label);
|
|
3676
4069
|
if (!heading) continue;
|
|
3677
4070
|
const slug = suggestedName(heading);
|
|
3678
4071
|
findings.push({
|
|
@@ -3690,10 +4083,10 @@ function validateTranslatableSections(stack) {
|
|
|
3690
4083
|
|
|
3691
4084
|
// src/flow-walk.ts
|
|
3692
4085
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
3693
|
-
function
|
|
4086
|
+
function isRec14(v) {
|
|
3694
4087
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3695
4088
|
}
|
|
3696
|
-
function
|
|
4089
|
+
function strName15(v) {
|
|
3697
4090
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3698
4091
|
}
|
|
3699
4092
|
var REGION_SLOTS = new Map(
|
|
@@ -3702,10 +4095,10 @@ var REGION_SLOTS = new Map(
|
|
|
3702
4095
|
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
3703
4096
|
var MAX_REGION_DEPTH = 16;
|
|
3704
4097
|
function flowNodeLabel(node, index) {
|
|
3705
|
-
return
|
|
4098
|
+
return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
|
|
3706
4099
|
}
|
|
3707
4100
|
function stripRegions(config) {
|
|
3708
|
-
if (!
|
|
4101
|
+
if (!isRec14(config)) return void 0;
|
|
3709
4102
|
let out;
|
|
3710
4103
|
for (const key of Object.keys(config)) {
|
|
3711
4104
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3716,11 +4109,11 @@ function stripRegions(config) {
|
|
|
3716
4109
|
}
|
|
3717
4110
|
function walkFlowNodes(flow, flowPath) {
|
|
3718
4111
|
const out = [];
|
|
3719
|
-
if (!
|
|
4112
|
+
if (!isRec14(flow)) return out;
|
|
3720
4113
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3721
4114
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3722
4115
|
nodes.forEach((raw, index) => {
|
|
3723
|
-
if (!
|
|
4116
|
+
if (!isRec14(raw)) return;
|
|
3724
4117
|
const path = `${basePath}[${index}]`;
|
|
3725
4118
|
out.push({
|
|
3726
4119
|
node: raw,
|
|
@@ -3729,9 +4122,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3729
4122
|
regionTrail: trail,
|
|
3730
4123
|
depth
|
|
3731
4124
|
});
|
|
3732
|
-
const type =
|
|
4125
|
+
const type = strName15(raw.type);
|
|
3733
4126
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3734
|
-
if (!slots || !
|
|
4127
|
+
if (!slots || !isRec14(raw.config)) return;
|
|
3735
4128
|
const config = raw.config;
|
|
3736
4129
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3737
4130
|
for (const slot of slots) {
|
|
@@ -3739,8 +4132,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3739
4132
|
if (slot === "branches") {
|
|
3740
4133
|
if (!Array.isArray(value)) continue;
|
|
3741
4134
|
value.forEach((branch, b) => {
|
|
3742
|
-
if (!
|
|
3743
|
-
const branchName =
|
|
4135
|
+
if (!isRec14(branch)) return;
|
|
4136
|
+
const branchName = strName15(branch.name) ?? `#${b}`;
|
|
3744
4137
|
visitList(
|
|
3745
4138
|
branch.nodes,
|
|
3746
4139
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3750,7 +4143,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3750
4143
|
});
|
|
3751
4144
|
continue;
|
|
3752
4145
|
}
|
|
3753
|
-
if (!
|
|
4146
|
+
if (!isRec14(value)) continue;
|
|
3754
4147
|
visitList(
|
|
3755
4148
|
value.nodes,
|
|
3756
4149
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3991,7 +4384,7 @@ function asArray17(v) {
|
|
|
3991
4384
|
}
|
|
3992
4385
|
return [];
|
|
3993
4386
|
}
|
|
3994
|
-
function
|
|
4387
|
+
function strName16(v) {
|
|
3995
4388
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3996
4389
|
}
|
|
3997
4390
|
function surfaceOf(v) {
|
|
@@ -4002,17 +4395,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4002
4395
|
if (!stack || typeof stack !== "object") return findings;
|
|
4003
4396
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
4004
4397
|
for (const skill of asArray17(stack.skills)) {
|
|
4005
|
-
const n =
|
|
4398
|
+
const n = strName16(skill.name);
|
|
4006
4399
|
if (n) skillsByName.set(n, skill);
|
|
4007
4400
|
}
|
|
4008
4401
|
const agents = asArray17(stack.agents);
|
|
4009
4402
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4010
4403
|
const agent = agents[ai];
|
|
4011
|
-
const agentName =
|
|
4404
|
+
const agentName = strName16(agent.name) ?? `#${ai}`;
|
|
4012
4405
|
const agentSurface = surfaceOf(agent.surface);
|
|
4013
4406
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4014
4407
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4015
|
-
const ref =
|
|
4408
|
+
const ref = strName16(skillRefs[si]);
|
|
4016
4409
|
if (!ref) continue;
|
|
4017
4410
|
const skill = skillsByName.get(ref);
|
|
4018
4411
|
if (!skill) continue;
|
|
@@ -4041,10 +4434,10 @@ function asArray18(v) {
|
|
|
4041
4434
|
}
|
|
4042
4435
|
return [];
|
|
4043
4436
|
}
|
|
4044
|
-
function
|
|
4437
|
+
function strName17(v) {
|
|
4045
4438
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4046
4439
|
}
|
|
4047
|
-
function
|
|
4440
|
+
function distance7(a, b) {
|
|
4048
4441
|
const m = a.length;
|
|
4049
4442
|
const n = b.length;
|
|
4050
4443
|
if (m === 0) return n;
|
|
@@ -4060,14 +4453,14 @@ function distance6(a, b) {
|
|
|
4060
4453
|
}
|
|
4061
4454
|
return prev[n];
|
|
4062
4455
|
}
|
|
4063
|
-
function
|
|
4456
|
+
function suggest8(target, known) {
|
|
4064
4457
|
for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4065
4458
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4066
4459
|
}
|
|
4067
4460
|
let best;
|
|
4068
4461
|
let bestScore = Infinity;
|
|
4069
4462
|
for (const candidate of known) {
|
|
4070
|
-
const d =
|
|
4463
|
+
const d = distance7(target, candidate);
|
|
4071
4464
|
if (d < bestScore) {
|
|
4072
4465
|
bestScore = d;
|
|
4073
4466
|
best = candidate;
|
|
@@ -4082,8 +4475,8 @@ function materialisesAsTool(action) {
|
|
|
4082
4475
|
if (!ai || typeof ai !== "object") return false;
|
|
4083
4476
|
const aiRec = ai;
|
|
4084
4477
|
if (aiRec.exposed !== true) return false;
|
|
4085
|
-
if (!
|
|
4086
|
-
const type =
|
|
4478
|
+
if (!strName17(aiRec.description)) return false;
|
|
4479
|
+
const type = strName17(action.type);
|
|
4087
4480
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4088
4481
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4089
4482
|
return Boolean(action.target);
|
|
@@ -4091,12 +4484,12 @@ function materialisesAsTool(action) {
|
|
|
4091
4484
|
function collectToolUniverse(stack) {
|
|
4092
4485
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
4093
4486
|
for (const tool of asArray18(stack.tools)) {
|
|
4094
|
-
const n =
|
|
4487
|
+
const n = strName17(tool.name);
|
|
4095
4488
|
if (n) universe.add(n);
|
|
4096
4489
|
}
|
|
4097
4490
|
const addActionFamily = (actions) => {
|
|
4098
4491
|
for (const action of asArray18(actions)) {
|
|
4099
|
-
const n =
|
|
4492
|
+
const n = strName17(action.name);
|
|
4100
4493
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4101
4494
|
}
|
|
4102
4495
|
};
|
|
@@ -4110,7 +4503,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
4110
4503
|
const names = /* @__PURE__ */ new Set();
|
|
4111
4504
|
const scan = (actions) => {
|
|
4112
4505
|
for (const action of asArray18(actions)) {
|
|
4113
|
-
const n =
|
|
4506
|
+
const n = strName17(action.name);
|
|
4114
4507
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4115
4508
|
}
|
|
4116
4509
|
};
|
|
@@ -4136,10 +4529,10 @@ function validateAiToolReferences(stack) {
|
|
|
4136
4529
|
const skills = asArray18(stack.skills);
|
|
4137
4530
|
for (let si = 0; si < skills.length; si++) {
|
|
4138
4531
|
const skill = skills[si];
|
|
4139
|
-
const skillName =
|
|
4532
|
+
const skillName = strName17(skill.name) ?? `#${si}`;
|
|
4140
4533
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4141
4534
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4142
|
-
const ref =
|
|
4535
|
+
const ref = strName17(refs[ti]);
|
|
4143
4536
|
if (!ref || resolves(ref)) continue;
|
|
4144
4537
|
const isPattern = ref.endsWith("*");
|
|
4145
4538
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4148,7 +4541,7 @@ function validateAiToolReferences(stack) {
|
|
|
4148
4541
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4149
4542
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4150
4543
|
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.` +
|
|
4544
|
+
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
4545
|
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
4546
|
});
|
|
4154
4547
|
}
|
|
@@ -4166,7 +4559,7 @@ function asArray19(v) {
|
|
|
4166
4559
|
}
|
|
4167
4560
|
return [];
|
|
4168
4561
|
}
|
|
4169
|
-
function
|
|
4562
|
+
function strName18(v) {
|
|
4170
4563
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4171
4564
|
}
|
|
4172
4565
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -4176,7 +4569,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4176
4569
|
const agents = asArray19(stack.agents);
|
|
4177
4570
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4178
4571
|
const agent = agents[ai];
|
|
4179
|
-
const name =
|
|
4572
|
+
const name = strName18(agent.name) ?? `#${ai}`;
|
|
4180
4573
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4181
4574
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4182
4575
|
findings.push({
|
|
@@ -4192,9 +4585,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4192
4585
|
const apps = asArray19(stack.apps);
|
|
4193
4586
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4194
4587
|
const app = apps[appIdx];
|
|
4195
|
-
const defaultAgent =
|
|
4588
|
+
const defaultAgent = strName18(app.defaultAgent);
|
|
4196
4589
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4197
|
-
const appName =
|
|
4590
|
+
const appName = strName18(app.name) ?? `#${appIdx}`;
|
|
4198
4591
|
findings.push({
|
|
4199
4592
|
severity: "warning",
|
|
4200
4593
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4210,6 +4603,38 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4210
4603
|
// src/validate-hook-body-writes.ts
|
|
4211
4604
|
var import_node_module = require("module");
|
|
4212
4605
|
var import_shared = require("@objectstack/spec/shared");
|
|
4606
|
+
|
|
4607
|
+
// src/checked-parse.ts
|
|
4608
|
+
function createSourceFileChecked(tsc, fileName, source, options) {
|
|
4609
|
+
const sourceFile = tsc.createSourceFile(
|
|
4610
|
+
fileName,
|
|
4611
|
+
source,
|
|
4612
|
+
options.target,
|
|
4613
|
+
options.setParentNodes,
|
|
4614
|
+
options.scriptKind
|
|
4615
|
+
);
|
|
4616
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
4617
|
+
if (!diagnostics || diagnostics.length === 0) return { sourceFile };
|
|
4618
|
+
const first = diagnostics[0];
|
|
4619
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
|
|
4620
|
+
const offset = options.synthesizedLinesBefore ?? 0;
|
|
4621
|
+
return {
|
|
4622
|
+
sourceFile,
|
|
4623
|
+
failure: {
|
|
4624
|
+
message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
|
|
4625
|
+
line: Math.max(1, line + 1 - offset),
|
|
4626
|
+
column: character + 1,
|
|
4627
|
+
count: diagnostics.length
|
|
4628
|
+
}
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
function describeParseFailure(failure) {
|
|
4632
|
+
const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
|
|
4633
|
+
return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
|
|
4634
|
+
}
|
|
4635
|
+
var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
|
|
4636
|
+
|
|
4637
|
+
// src/validate-hook-body-writes.ts
|
|
4213
4638
|
var import_meta = {};
|
|
4214
4639
|
var cachedTs = null;
|
|
4215
4640
|
function loadTypeScript() {
|
|
@@ -4225,6 +4650,8 @@ function loadTypeScript() {
|
|
|
4225
4650
|
return cachedTs;
|
|
4226
4651
|
}
|
|
4227
4652
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4653
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
4654
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4228
4655
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4229
4656
|
{
|
|
4230
4657
|
id: "input-property-assign",
|
|
@@ -4291,13 +4718,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4291
4718
|
"owner",
|
|
4292
4719
|
"record_type"
|
|
4293
4720
|
]);
|
|
4294
|
-
|
|
4721
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
4722
|
+
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).`;
|
|
4723
|
+
}
|
|
4724
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4295
4725
|
function asArray20(v) {
|
|
4296
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4297
|
-
if (
|
|
4726
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4727
|
+
if (isRec15(v)) {
|
|
4298
4728
|
return Object.entries(v).map(([name, def]) => ({
|
|
4299
4729
|
name,
|
|
4300
|
-
...
|
|
4730
|
+
...isRec15(def) ? def : {}
|
|
4301
4731
|
}));
|
|
4302
4732
|
}
|
|
4303
4733
|
return [];
|
|
@@ -4320,23 +4750,23 @@ function judgeableFieldsOf(index, objectName) {
|
|
|
4320
4750
|
if (!declared || declared.size === 0) return void 0;
|
|
4321
4751
|
return declared;
|
|
4322
4752
|
}
|
|
4323
|
-
function extractHookBodyWrites(source) {
|
|
4324
|
-
return extractHookBodyWriteSet(source).writes;
|
|
4325
|
-
}
|
|
4326
4753
|
function extractHookBodyWriteSet(source) {
|
|
4327
4754
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
4328
4755
|
return { writes: [], ctxRecordEscapes: false };
|
|
4329
4756
|
}
|
|
4330
4757
|
const tsc = loadTypeScript();
|
|
4331
|
-
const sf =
|
|
4758
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
4759
|
+
tsc,
|
|
4332
4760
|
"hook-body.ts",
|
|
4333
4761
|
`async function __body(ctx) {
|
|
4334
4762
|
${source}
|
|
4335
4763
|
}`,
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4764
|
+
{
|
|
4765
|
+
target: tsc.ScriptTarget.Latest,
|
|
4766
|
+
setParentNodes: false,
|
|
4767
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
4768
|
+
synthesizedLinesBefore: 1
|
|
4769
|
+
}
|
|
4340
4770
|
);
|
|
4341
4771
|
const writes = [];
|
|
4342
4772
|
const recordRefs = [];
|
|
@@ -4433,7 +4863,8 @@ ${source}
|
|
|
4433
4863
|
visit(sf);
|
|
4434
4864
|
return {
|
|
4435
4865
|
writes,
|
|
4436
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
4866
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
4867
|
+
...parseFailure ? { parseFailure } : {}
|
|
4437
4868
|
};
|
|
4438
4869
|
}
|
|
4439
4870
|
function validateHookBodyWrites(stack) {
|
|
@@ -4441,15 +4872,28 @@ function validateHookBodyWrites(stack) {
|
|
|
4441
4872
|
const hooks = asArray20(stack.hooks);
|
|
4442
4873
|
if (hooks.length === 0) return findings;
|
|
4443
4874
|
let objectFields = null;
|
|
4875
|
+
let anchors = null;
|
|
4444
4876
|
hooks.forEach((hook, hookIndex) => {
|
|
4445
4877
|
const body = hook.body;
|
|
4446
|
-
if (!
|
|
4878
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4447
4879
|
const source = body.source;
|
|
4448
4880
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4449
|
-
const
|
|
4881
|
+
const extracted = extractHookBodyWriteSet(source);
|
|
4882
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4883
|
+
if (extracted.parseFailure) {
|
|
4884
|
+
findings.push({
|
|
4885
|
+
severity: "warning",
|
|
4886
|
+
rule: HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
4887
|
+
where: `hook "${hookName}" \u203A body`,
|
|
4888
|
+
path: `hooks[${hookIndex}].body.source`,
|
|
4889
|
+
message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
4890
|
+
hint: PARSE_FAILURE_HINT
|
|
4891
|
+
});
|
|
4892
|
+
}
|
|
4893
|
+
const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4450
4894
|
if (writes.length === 0) return;
|
|
4451
4895
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4452
|
-
|
|
4896
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4453
4897
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4454
4898
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
4455
4899
|
);
|
|
@@ -4463,7 +4907,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4463
4907
|
if (reported.has(dedupeKey)) continue;
|
|
4464
4908
|
if (w.object === void 0) {
|
|
4465
4909
|
if (!inputJudgeable) continue;
|
|
4466
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
4910
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4911
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
4912
|
+
reported.add(dedupeKey);
|
|
4913
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
4914
|
+
findings.push({
|
|
4915
|
+
severity: "warning",
|
|
4916
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4917
|
+
where,
|
|
4918
|
+
path,
|
|
4919
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
4920
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
4921
|
+
});
|
|
4922
|
+
continue;
|
|
4923
|
+
}
|
|
4467
4924
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
4468
4925
|
reported.add(dedupeKey);
|
|
4469
4926
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -4479,7 +4936,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4479
4936
|
} else {
|
|
4480
4937
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4481
4938
|
if (!known) continue;
|
|
4482
|
-
if (
|
|
4939
|
+
if (known.has(w.field)) continue;
|
|
4940
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4941
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4942
|
+
reported.add(dedupeKey);
|
|
4943
|
+
findings.push({
|
|
4944
|
+
severity: "warning",
|
|
4945
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4946
|
+
where,
|
|
4947
|
+
path,
|
|
4948
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4949
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4950
|
+
});
|
|
4951
|
+
continue;
|
|
4952
|
+
}
|
|
4483
4953
|
reported.add(dedupeKey);
|
|
4484
4954
|
findings.push({
|
|
4485
4955
|
severity: "warning",
|
|
@@ -4508,19 +4978,21 @@ function fixHint(field, declared) {
|
|
|
4508
4978
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
4509
4979
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4510
4980
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4981
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
4982
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4511
4983
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4512
4984
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
4513
4985
|
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
4514
4986
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4515
4987
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4516
4988
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4517
|
-
var
|
|
4989
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4518
4990
|
function asArray21(v) {
|
|
4519
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4520
|
-
if (
|
|
4991
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4992
|
+
if (isRec16(v)) {
|
|
4521
4993
|
return Object.entries(v).map(([name, def]) => ({
|
|
4522
4994
|
name,
|
|
4523
|
-
...
|
|
4995
|
+
...isRec16(def) ? def : {}
|
|
4524
4996
|
}));
|
|
4525
4997
|
}
|
|
4526
4998
|
return [];
|
|
@@ -4538,7 +5010,7 @@ function collectActionBodies(stack) {
|
|
|
4538
5010
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4539
5011
|
if (type !== "script") return;
|
|
4540
5012
|
const body = action.body;
|
|
4541
|
-
if (!
|
|
5013
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
4542
5014
|
const source = body.source;
|
|
4543
5015
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4544
5016
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4557,17 +5029,28 @@ function collectActionBodies(stack) {
|
|
|
4557
5029
|
}
|
|
4558
5030
|
function validateActionBodyWrites(stack) {
|
|
4559
5031
|
const findings = [];
|
|
4560
|
-
if (!
|
|
5032
|
+
if (!isRec16(stack)) return findings;
|
|
4561
5033
|
const sites = collectActionBodies(stack);
|
|
4562
5034
|
if (sites.length === 0) return findings;
|
|
4563
5035
|
let objectFields = null;
|
|
5036
|
+
let anchors = null;
|
|
4564
5037
|
for (const site of sites) {
|
|
4565
5038
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4566
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5039
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
4567
5040
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
4568
5041
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
4569
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4570
5042
|
const where = `action "${site.name}" \u203A body`;
|
|
5043
|
+
if (parseFailure) {
|
|
5044
|
+
findings.push({
|
|
5045
|
+
severity: "warning",
|
|
5046
|
+
rule: ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
5047
|
+
where,
|
|
5048
|
+
path: site.path,
|
|
5049
|
+
message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
5050
|
+
hint: PARSE_FAILURE_HINT
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4571
5054
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
4572
5055
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
4573
5056
|
for (const w of recordWrites) {
|
|
@@ -4585,6 +5068,7 @@ function validateActionBodyWrites(stack) {
|
|
|
4585
5068
|
}
|
|
4586
5069
|
if (writes.length === 0) continue;
|
|
4587
5070
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5071
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4588
5072
|
const reported = /* @__PURE__ */ new Set();
|
|
4589
5073
|
for (const w of writes) {
|
|
4590
5074
|
if (w.object === void 0) continue;
|
|
@@ -4592,7 +5076,20 @@ function validateActionBodyWrites(stack) {
|
|
|
4592
5076
|
if (reported.has(dedupeKey)) continue;
|
|
4593
5077
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4594
5078
|
if (!known) continue;
|
|
4595
|
-
if (
|
|
5079
|
+
if (known.has(w.field)) continue;
|
|
5080
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
5081
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
5082
|
+
reported.add(dedupeKey);
|
|
5083
|
+
findings.push({
|
|
5084
|
+
severity: "warning",
|
|
5085
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
5086
|
+
where,
|
|
5087
|
+
path: site.path,
|
|
5088
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
5089
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
5090
|
+
});
|
|
5091
|
+
continue;
|
|
5092
|
+
}
|
|
4596
5093
|
reported.add(dedupeKey);
|
|
4597
5094
|
findings.push({
|
|
4598
5095
|
severity: "warning",
|
|
@@ -4614,14 +5111,15 @@ function fixHint2(field, declared) {
|
|
|
4614
5111
|
// src/validate-flow-node-writes.ts
|
|
4615
5112
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
4616
5113
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5114
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
4617
5115
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4618
|
-
var
|
|
5116
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4619
5117
|
function asArray22(v) {
|
|
4620
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4621
|
-
if (
|
|
5118
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
5119
|
+
if (isRec17(v)) {
|
|
4622
5120
|
return Object.entries(v).map(([name, def]) => ({
|
|
4623
5121
|
name,
|
|
4624
|
-
...
|
|
5122
|
+
...isRec17(def) ? def : {}
|
|
4625
5123
|
}));
|
|
4626
5124
|
}
|
|
4627
5125
|
return [];
|
|
@@ -4634,30 +5132,44 @@ function readLiteralObjectName(config) {
|
|
|
4634
5132
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4635
5133
|
function validateFlowNodeWrites(stack) {
|
|
4636
5134
|
const findings = [];
|
|
4637
|
-
if (!
|
|
5135
|
+
if (!isRec17(stack)) return findings;
|
|
4638
5136
|
const flows = asArray22(stack.flows);
|
|
4639
5137
|
if (flows.length === 0) return findings;
|
|
4640
5138
|
let objectFields = null;
|
|
5139
|
+
let anchors = null;
|
|
4641
5140
|
flows.forEach((flow, flowIndex) => {
|
|
4642
5141
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
4643
5142
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4644
5143
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4645
5144
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4646
|
-
const config =
|
|
5145
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
4647
5146
|
if (!config) return;
|
|
4648
5147
|
const fields = config.fields;
|
|
4649
|
-
if (!
|
|
5148
|
+
if (!isRec17(fields)) return;
|
|
4650
5149
|
const written = Object.keys(fields);
|
|
4651
5150
|
if (written.length === 0) return;
|
|
4652
5151
|
const objectName = readLiteralObjectName(config);
|
|
4653
5152
|
if (!objectName) return;
|
|
4654
5153
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5154
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4655
5155
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
4656
5156
|
if (!known) return;
|
|
4657
5157
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
4658
5158
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
4659
5159
|
for (const fieldName of written) {
|
|
4660
|
-
if (known.has(fieldName)
|
|
5160
|
+
if (known.has(fieldName)) continue;
|
|
5161
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
5162
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
5163
|
+
findings.push({
|
|
5164
|
+
severity: "warning",
|
|
5165
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
5166
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5167
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5168
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
5169
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
5170
|
+
});
|
|
5171
|
+
continue;
|
|
5172
|
+
}
|
|
4661
5173
|
if (fieldName.includes(".")) continue;
|
|
4662
5174
|
findings.push({
|
|
4663
5175
|
severity: "error",
|
|
@@ -4770,15 +5282,15 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
4770
5282
|
|
|
4771
5283
|
// src/validate-react-page-props.ts
|
|
4772
5284
|
var import_node_module2 = require("module");
|
|
4773
|
-
var
|
|
4774
|
-
var
|
|
5285
|
+
var import_ui3 = require("@objectstack/spec/ui");
|
|
5286
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
4775
5287
|
|
|
4776
5288
|
// src/zod-issue-format.ts
|
|
4777
|
-
var
|
|
5289
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4778
5290
|
var valueAtPath = (root, path) => {
|
|
4779
5291
|
let cur = root;
|
|
4780
5292
|
for (const key of path) {
|
|
4781
|
-
if (!
|
|
5293
|
+
if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
|
|
4782
5294
|
cur = cur[key];
|
|
4783
5295
|
}
|
|
4784
5296
|
return cur;
|
|
@@ -4826,7 +5338,7 @@ function loadTypeScript2() {
|
|
|
4826
5338
|
}
|
|
4827
5339
|
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4828
5340
|
var BLOCKS = new Map(
|
|
4829
|
-
|
|
5341
|
+
import_ui3.REACT_BLOCKS.map((b) => [
|
|
4830
5342
|
b.tag,
|
|
4831
5343
|
{
|
|
4832
5344
|
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
@@ -4911,6 +5423,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4911
5423
|
};
|
|
4912
5424
|
return perPosition(init.expression);
|
|
4913
5425
|
}
|
|
5426
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
4914
5427
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4915
5428
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4916
5429
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -4918,7 +5431,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4918
5431
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4919
5432
|
function checkChartDrillDown(raw, push2) {
|
|
4920
5433
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4921
|
-
if (!
|
|
5434
|
+
if (!isRec19(raw)) {
|
|
4922
5435
|
push2(
|
|
4923
5436
|
"error",
|
|
4924
5437
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4927,7 +5440,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4927
5440
|
);
|
|
4928
5441
|
return;
|
|
4929
5442
|
}
|
|
4930
|
-
const parsed =
|
|
5443
|
+
const parsed = import_ui3.ChartDrillDownSchema.safeParse(raw);
|
|
4931
5444
|
if (parsed.success) return;
|
|
4932
5445
|
for (const issue of parsed.error.issues) {
|
|
4933
5446
|
const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
|
|
@@ -4941,7 +5454,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4941
5454
|
}
|
|
4942
5455
|
function checkChartAggregate(raw, push2) {
|
|
4943
5456
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4944
|
-
if (!
|
|
5457
|
+
if (!isRec19(raw)) {
|
|
4945
5458
|
push2(
|
|
4946
5459
|
"error",
|
|
4947
5460
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4959,7 +5472,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4959
5472
|
"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
5473
|
);
|
|
4961
5474
|
}
|
|
4962
|
-
const parsed =
|
|
5475
|
+
const parsed = import_ui3.ChartAggregateSchema.safeParse(raw);
|
|
4963
5476
|
if (parsed.success) return;
|
|
4964
5477
|
for (const issue of parsed.error.issues) {
|
|
4965
5478
|
if (groupByAbsent && issue.path[0] === "groupBy") continue;
|
|
@@ -4972,7 +5485,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4972
5485
|
);
|
|
4973
5486
|
}
|
|
4974
5487
|
}
|
|
4975
|
-
var
|
|
5488
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4976
5489
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4977
5490
|
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4978
5491
|
const { values, where, path } = attrs;
|
|
@@ -4982,11 +5495,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
4982
5495
|
const aggregate = values.get("aggregate");
|
|
4983
5496
|
checkChartAggregate(aggregate, push2);
|
|
4984
5497
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4985
|
-
if (!
|
|
5498
|
+
if (!isRec19(aggregate)) return;
|
|
4986
5499
|
const fn = strOf(aggregate.function);
|
|
4987
5500
|
const field = strOf(aggregate.field);
|
|
4988
5501
|
const groupBy = aggregate.groupBy;
|
|
4989
|
-
const groupByField = strOf(groupBy) ?? (
|
|
5502
|
+
const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4990
5503
|
const objectName = strOf(values.get("objectName"));
|
|
4991
5504
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4992
5505
|
if (objectName && known) {
|
|
@@ -5015,7 +5528,7 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5015
5528
|
fieldRef(field, "field");
|
|
5016
5529
|
fieldRef(groupByField, "groupBy");
|
|
5017
5530
|
}
|
|
5018
|
-
const keys = (0,
|
|
5531
|
+
const keys = (0, import_ui3.chartAggregateResultKeys)({ field, function: fn, groupBy });
|
|
5019
5532
|
const columns = [keys.category, keys.value].filter((k) => !!k);
|
|
5020
5533
|
if (columns.length === 0) return;
|
|
5021
5534
|
const axisRef = (name, prop) => {
|
|
@@ -5030,18 +5543,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5030
5543
|
);
|
|
5031
5544
|
};
|
|
5032
5545
|
const xAxisRaw = values.get("xAxis");
|
|
5033
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5546
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
5034
5547
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
5035
5548
|
axisRef(categoryAxis, categoryProp);
|
|
5036
5549
|
const yAxisRaw = values.get("yAxis");
|
|
5037
5550
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
5038
5551
|
for (const a of yAxisList) {
|
|
5039
|
-
axisRef(strOf(a) ?? (
|
|
5552
|
+
axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
5040
5553
|
}
|
|
5041
5554
|
const series = values.get("series");
|
|
5042
5555
|
if (Array.isArray(series)) {
|
|
5043
5556
|
for (const s of series) {
|
|
5044
|
-
if (!
|
|
5557
|
+
if (!isRec19(s)) continue;
|
|
5045
5558
|
const dataKey = strOf(s.dataKey);
|
|
5046
5559
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
5047
5560
|
}
|
|
@@ -5081,7 +5594,7 @@ var REACT_FIELD_SPECS = {
|
|
|
5081
5594
|
};
|
|
5082
5595
|
var PATH_SEP = " \u203A ";
|
|
5083
5596
|
var SCHEMA_TYPE_BY_TAG = new Map(
|
|
5084
|
-
|
|
5597
|
+
import_ui3.REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
|
|
5085
5598
|
);
|
|
5086
5599
|
var FILTER_PROPS = new Set(
|
|
5087
5600
|
Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
|
|
@@ -5097,7 +5610,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
5097
5610
|
if (!Array.isArray(value)) return { child, parent };
|
|
5098
5611
|
for (let i = 0; i < value.length; i++) {
|
|
5099
5612
|
const sub = value[i];
|
|
5100
|
-
if (!
|
|
5613
|
+
if (!isRec19(sub)) continue;
|
|
5101
5614
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
5102
5615
|
child.push({
|
|
5103
5616
|
objectName: strOf(sub.childObject),
|
|
@@ -5122,7 +5635,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
5122
5635
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
5123
5636
|
return;
|
|
5124
5637
|
}
|
|
5125
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5638
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data10.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
5126
5639
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
5127
5640
|
}
|
|
5128
5641
|
}
|
|
@@ -5142,20 +5655,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
5142
5655
|
}
|
|
5143
5656
|
for (const key of spec.nestedFields ?? []) {
|
|
5144
5657
|
const v = readable(key);
|
|
5145
|
-
if (
|
|
5658
|
+
if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
5146
5659
|
}
|
|
5147
5660
|
for (const key of spec.sections ?? []) {
|
|
5148
5661
|
const v = readable(key);
|
|
5149
5662
|
if (!Array.isArray(v)) continue;
|
|
5150
5663
|
for (let i = 0; i < v.length; i++) {
|
|
5151
5664
|
const section = v[i];
|
|
5152
|
-
if (!
|
|
5665
|
+
if (!isRec19(section)) continue;
|
|
5153
5666
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
5154
5667
|
}
|
|
5155
5668
|
}
|
|
5156
5669
|
for (const key of spec.keyedByField ?? []) {
|
|
5157
5670
|
const v = readable(key);
|
|
5158
|
-
if (!
|
|
5671
|
+
if (!isRec19(v)) continue;
|
|
5159
5672
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
5160
5673
|
}
|
|
5161
5674
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -5212,7 +5725,7 @@ function recordContextFinding(tag, schemaType, where, path) {
|
|
|
5212
5725
|
where,
|
|
5213
5726
|
path,
|
|
5214
5727
|
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: ${
|
|
5728
|
+
hint: `On a react page bind the record yourself: ${import_ui3.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
|
|
5216
5729
|
};
|
|
5217
5730
|
}
|
|
5218
5731
|
function localComponentNames(tsc, sf) {
|
|
@@ -5239,11 +5752,20 @@ function validateReactPageProps(stack) {
|
|
|
5239
5752
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
5240
5753
|
const name = String(page.name ?? `#${p}`);
|
|
5241
5754
|
const tsc = loadTypeScript2();
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5755
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
|
|
5756
|
+
target: tsc.ScriptTarget.Latest,
|
|
5757
|
+
setParentNodes: true,
|
|
5758
|
+
scriptKind: tsc.ScriptKind.TSX
|
|
5759
|
+
});
|
|
5760
|
+
if (failure) {
|
|
5761
|
+
findings.push({
|
|
5762
|
+
severity: "warning",
|
|
5763
|
+
rule: REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
5764
|
+
where: `page "${name}"`,
|
|
5765
|
+
path: `pages[${p}].source`,
|
|
5766
|
+
message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
|
|
5767
|
+
hint: PARSE_FAILURE_HINT
|
|
5768
|
+
});
|
|
5247
5769
|
}
|
|
5248
5770
|
const locals = localComponentNames(tsc, sf);
|
|
5249
5771
|
const visit = (node) => {
|
|
@@ -5251,7 +5773,7 @@ function validateReactPageProps(stack) {
|
|
|
5251
5773
|
const tag = node.tagName.getText(sf);
|
|
5252
5774
|
const where = `page "${name}" \u203A <${tag}>`;
|
|
5253
5775
|
const path = `pages[${p}].source`;
|
|
5254
|
-
const recordType =
|
|
5776
|
+
const recordType = import_ui3.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
|
|
5255
5777
|
if (recordType && !locals.has(tag)) {
|
|
5256
5778
|
findings.push(recordContextFinding(tag, recordType, where, path));
|
|
5257
5779
|
tsc.forEachChild(node, visit);
|
|
@@ -5278,7 +5800,7 @@ function validateReactPageProps(stack) {
|
|
|
5278
5800
|
}
|
|
5279
5801
|
if (tag === "Block") {
|
|
5280
5802
|
const blockType = strOf(values.get("type"));
|
|
5281
|
-
if (blockType && (0,
|
|
5803
|
+
if (blockType && (0, import_ui3.isRecordContextBlockType)(blockType)) {
|
|
5282
5804
|
findings.push(recordContextFinding(tag, blockType, where, path));
|
|
5283
5805
|
tsc.forEachChild(node, visit);
|
|
5284
5806
|
return;
|
|
@@ -5322,7 +5844,12 @@ function validateReactPageProps(stack) {
|
|
|
5322
5844
|
searchTargets,
|
|
5323
5845
|
where,
|
|
5324
5846
|
`${path} \u203A searchableFields`,
|
|
5325
|
-
"searchableFields"
|
|
5847
|
+
"searchableFields",
|
|
5848
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
5849
|
+
// default, spelled out here because the #8404 provenance index
|
|
5850
|
+
// follows it positionally.
|
|
5851
|
+
"narrowing",
|
|
5852
|
+
unprovisionedAnchors
|
|
5326
5853
|
)
|
|
5327
5854
|
);
|
|
5328
5855
|
}
|
|
@@ -5341,9 +5868,26 @@ function validateReactPageProps(stack) {
|
|
|
5341
5868
|
}
|
|
5342
5869
|
|
|
5343
5870
|
// src/reference-integrity-suite.ts
|
|
5871
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
5344
5872
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5345
5873
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5346
|
-
|
|
5874
|
+
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
|
|
5875
|
+
// both judge a LIST VIEW's field references, and a standalone list view is
|
|
5876
|
+
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
|
|
5877
|
+
// an MCP/AI author has. Their walks read the flattened overlay shape that
|
|
5878
|
+
// door carries (see each rule's `views[]` self rung), and they resolve only
|
|
5879
|
+
// against `stack.objects`, which the per-write snapshot DOES carry — so the
|
|
5880
|
+
// crossing has no missing-collection false-positive channel. Measured over
|
|
5881
|
+
// the shipped view corpus before crossing (0 refusals; population in the
|
|
5882
|
+
// #9313 PR).
|
|
5883
|
+
{ name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
|
|
5884
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5885
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
5886
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5887
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5888
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5889
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
5890
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
5347
5891
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5348
5892
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5349
5893
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5449,23 +5993,25 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5449
5993
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5450
5994
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
5451
5995
|
];
|
|
5452
|
-
function validateReferenceIntegrity(stack) {
|
|
5996
|
+
function validateReferenceIntegrity(stack, options) {
|
|
5453
5997
|
const findings = [];
|
|
5998
|
+
const writeType = options?.runtimeWriteType;
|
|
5454
5999
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
6000
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
5455
6001
|
findings.push(...rule.run(stack));
|
|
5456
6002
|
}
|
|
5457
6003
|
return findings;
|
|
5458
6004
|
}
|
|
5459
6005
|
|
|
5460
6006
|
// src/validate-component-props.ts
|
|
5461
|
-
var
|
|
6007
|
+
var import_ui4 = require("@objectstack/spec/ui");
|
|
5462
6008
|
var import_spec2 = require("@objectstack/spec");
|
|
5463
6009
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5464
6010
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5465
|
-
function
|
|
6011
|
+
function isRec20(v) {
|
|
5466
6012
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5467
6013
|
}
|
|
5468
|
-
function
|
|
6014
|
+
function strName19(v) {
|
|
5469
6015
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5470
6016
|
}
|
|
5471
6017
|
function asArray25(v) {
|
|
@@ -5475,12 +6021,12 @@ function asArray25(v) {
|
|
|
5475
6021
|
}
|
|
5476
6022
|
return [];
|
|
5477
6023
|
}
|
|
5478
|
-
var PROPS_SCHEMAS =
|
|
6024
|
+
var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
|
|
5479
6025
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5480
6026
|
function suppliedByDataSource(issue, component) {
|
|
5481
6027
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5482
|
-
const dataSource =
|
|
5483
|
-
return
|
|
6028
|
+
const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
|
|
6029
|
+
return strName19(dataSource?.object) !== void 0;
|
|
5484
6030
|
}
|
|
5485
6031
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
5486
6032
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -5497,18 +6043,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
5497
6043
|
}
|
|
5498
6044
|
function validateComponentProps(stack) {
|
|
5499
6045
|
const findings = [];
|
|
5500
|
-
if (!
|
|
6046
|
+
if (!isRec20(stack)) return findings;
|
|
5501
6047
|
const pages = asArray25(stack.pages);
|
|
5502
6048
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5503
6049
|
const page = pages[pi];
|
|
5504
|
-
if (!
|
|
5505
|
-
const pageName =
|
|
6050
|
+
if (!isRec20(page)) continue;
|
|
6051
|
+
const pageName = strName19(page.name) ?? `#${pi}`;
|
|
5506
6052
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5507
|
-
const type =
|
|
6053
|
+
const type = strName19(component.type);
|
|
5508
6054
|
if (!type) continue;
|
|
5509
6055
|
const schema = PROPS_SCHEMAS[type];
|
|
5510
6056
|
if (!schema) continue;
|
|
5511
|
-
const props =
|
|
6057
|
+
const props = isRec20(component.properties) ? component.properties : void 0;
|
|
5512
6058
|
if (!props) continue;
|
|
5513
6059
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5514
6060
|
const base = `${path}.properties`;
|
|
@@ -6504,7 +7050,7 @@ function validateApprovalApprovers(stack) {
|
|
|
6504
7050
|
}
|
|
6505
7051
|
|
|
6506
7052
|
// src/validate-record-title.ts
|
|
6507
|
-
var
|
|
7053
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
6508
7054
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
6509
7055
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
6510
7056
|
function asArray33(v) {
|
|
@@ -6532,7 +7078,7 @@ function validateRecordTitle(stack) {
|
|
|
6532
7078
|
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
7079
|
});
|
|
6534
7080
|
}
|
|
6535
|
-
const completeness = (0,
|
|
7081
|
+
const completeness = (0, import_data11.objectTitleCompleteness)(obj);
|
|
6536
7082
|
if (completeness.status === "none") {
|
|
6537
7083
|
findings.push({
|
|
6538
7084
|
severity: "warning",
|
|
@@ -6679,10 +7225,10 @@ function asArray35(v) {
|
|
|
6679
7225
|
}
|
|
6680
7226
|
return [];
|
|
6681
7227
|
}
|
|
6682
|
-
function
|
|
7228
|
+
function isRec21(v) {
|
|
6683
7229
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6684
7230
|
}
|
|
6685
|
-
function
|
|
7231
|
+
function strName20(v) {
|
|
6686
7232
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6687
7233
|
}
|
|
6688
7234
|
function fieldNameOf(entry) {
|
|
@@ -6703,7 +7249,7 @@ function validateFormLayout(stack) {
|
|
|
6703
7249
|
objectFields.set(name, new Set(fields));
|
|
6704
7250
|
}
|
|
6705
7251
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6706
|
-
const viewName =
|
|
7252
|
+
const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
|
|
6707
7253
|
const containerObject = viewObjectName(view);
|
|
6708
7254
|
for (const site of formViewSites(view, viewPath)) {
|
|
6709
7255
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6713,7 +7259,7 @@ function validateFormLayout(stack) {
|
|
|
6713
7259
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6714
7260
|
for (let s = 0; s < sections.length; s++) {
|
|
6715
7261
|
const sec = sections[s];
|
|
6716
|
-
const secFields =
|
|
7262
|
+
const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6717
7263
|
for (let f = 0; f < secFields.length; f++) {
|
|
6718
7264
|
const entry = secFields[f];
|
|
6719
7265
|
const fname = fieldNameOf(entry);
|
|
@@ -6728,7 +7274,7 @@ function validateFormLayout(stack) {
|
|
|
6728
7274
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6729
7275
|
});
|
|
6730
7276
|
}
|
|
6731
|
-
const colSpan =
|
|
7277
|
+
const colSpan = isRec21(entry) ? entry.colSpan : void 0;
|
|
6732
7278
|
if (colSpan != null) {
|
|
6733
7279
|
findings.push({
|
|
6734
7280
|
severity: "warning",
|
|
@@ -6886,12 +7432,12 @@ function bareRhsOnlyIdentifiers(ast) {
|
|
|
6886
7432
|
for (const name of elsewhere) rhs.delete(name);
|
|
6887
7433
|
return rhs;
|
|
6888
7434
|
}
|
|
6889
|
-
function
|
|
7435
|
+
function isRec22(v) {
|
|
6890
7436
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6891
7437
|
}
|
|
6892
7438
|
function schemaIdOf(view) {
|
|
6893
7439
|
const data = view.data;
|
|
6894
|
-
if (!
|
|
7440
|
+
if (!isRec22(data)) return void 0;
|
|
6895
7441
|
if (data.provider !== "schema") return void 0;
|
|
6896
7442
|
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6897
7443
|
}
|
|
@@ -7263,7 +7809,7 @@ function predicateSource2(v) {
|
|
|
7263
7809
|
}
|
|
7264
7810
|
return void 0;
|
|
7265
7811
|
}
|
|
7266
|
-
function
|
|
7812
|
+
function isRec23(v) {
|
|
7267
7813
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7268
7814
|
}
|
|
7269
7815
|
function checkPredicate(source, scope, where, path, findings) {
|
|
@@ -7334,7 +7880,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
7334
7880
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
7335
7881
|
for (let i = 0; i < entries.length; i++) {
|
|
7336
7882
|
const entry = entries[i];
|
|
7337
|
-
if (!
|
|
7883
|
+
if (!isRec23(entry)) continue;
|
|
7338
7884
|
const path = `${base}[${i}]`;
|
|
7339
7885
|
for (const key of PREDICATE_KEYS) {
|
|
7340
7886
|
const source = predicateSource2(entry[key]);
|
|
@@ -7368,7 +7914,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
7368
7914
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7369
7915
|
for (let s = 0; s < sections.length; s++) {
|
|
7370
7916
|
const section = sections[s];
|
|
7371
|
-
if (!
|
|
7917
|
+
if (!isRec23(section)) continue;
|
|
7372
7918
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7373
7919
|
for (const key of PREDICATE_KEYS) {
|
|
7374
7920
|
const source = predicateSource2(section[key]);
|
|
@@ -7652,6 +8198,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7652
8198
|
}
|
|
7653
8199
|
}
|
|
7654
8200
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
8201
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
7655
8202
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7656
8203
|
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7657
8204
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -7675,7 +8222,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7675
8222
|
}
|
|
7676
8223
|
}
|
|
7677
8224
|
const delegatedFrom = rec.delegated_from;
|
|
7678
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
8225
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
7679
8226
|
const reason = rec.reason;
|
|
7680
8227
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
7681
8228
|
findings.push({
|
|
@@ -7841,6 +8388,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7841
8388
|
var import_formula6 = require("@objectstack/formula");
|
|
7842
8389
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7843
8390
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
8391
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
8392
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
7844
8393
|
function asArray38(v) {
|
|
7845
8394
|
if (Array.isArray(v)) return v;
|
|
7846
8395
|
if (v && typeof v === "object") {
|
|
@@ -7865,10 +8414,67 @@ function sourceOf(condition) {
|
|
|
7865
8414
|
return str2(input?.source);
|
|
7866
8415
|
}
|
|
7867
8416
|
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).";
|
|
8417
|
+
function effectiveSharingModelOf(obj) {
|
|
8418
|
+
const m = obj.sharingModel;
|
|
8419
|
+
if (m === "private") return "private";
|
|
8420
|
+
if (m === "public_read") return "read";
|
|
8421
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
8422
|
+
if (m == null) {
|
|
8423
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
8424
|
+
return isSystem ? "public" : "private";
|
|
8425
|
+
}
|
|
8426
|
+
return "private";
|
|
8427
|
+
}
|
|
8428
|
+
function masterOf(obj) {
|
|
8429
|
+
for (const f of asArray38(obj.fields)) {
|
|
8430
|
+
if (f.type === "master_detail") {
|
|
8431
|
+
const ref = f.reference;
|
|
8432
|
+
if (typeof ref === "string" && ref) return ref;
|
|
8433
|
+
}
|
|
8434
|
+
}
|
|
8435
|
+
return void 0;
|
|
8436
|
+
}
|
|
8437
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
8438
|
+
const object = str2(rule.object);
|
|
8439
|
+
if (!object) return [];
|
|
8440
|
+
const target = objectsByName.get(object);
|
|
8441
|
+
if (!target) return [];
|
|
8442
|
+
const name = str2(rule.name) || String(index);
|
|
8443
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
8444
|
+
const path = `sharingRules[${index}].object`;
|
|
8445
|
+
const owd = target.sharingModel;
|
|
8446
|
+
if (owd === "controlled_by_parent") {
|
|
8447
|
+
const master = masterOf(target);
|
|
8448
|
+
return [{
|
|
8449
|
+
severity: "error",
|
|
8450
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
8451
|
+
where,
|
|
8452
|
+
path,
|
|
8453
|
+
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.`,
|
|
8454
|
+
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.`
|
|
8455
|
+
}];
|
|
8456
|
+
}
|
|
8457
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
8458
|
+
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`;
|
|
8459
|
+
return [{
|
|
8460
|
+
severity: "error",
|
|
8461
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
8462
|
+
where,
|
|
8463
|
+
path,
|
|
8464
|
+
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.`,
|
|
8465
|
+
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.`
|
|
8466
|
+
}];
|
|
8467
|
+
}
|
|
7868
8468
|
function validateSharingRuleEnforceability(stack) {
|
|
7869
8469
|
const findings = [];
|
|
7870
8470
|
const cfg = stack ?? {};
|
|
8471
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
8472
|
+
for (const obj of asArray38(cfg.objects)) {
|
|
8473
|
+
const name = str2(obj.name);
|
|
8474
|
+
if (name) objectsByName.set(name, obj);
|
|
8475
|
+
}
|
|
7871
8476
|
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
8477
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
7872
8478
|
const input = toCompilerInput(rule.condition);
|
|
7873
8479
|
if (input === null) return;
|
|
7874
8480
|
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
@@ -7994,11 +8600,11 @@ var import_meta4 = {};
|
|
|
7994
8600
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7995
8601
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7996
8602
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7997
|
-
var
|
|
8603
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7998
8604
|
function asArray40(v) {
|
|
7999
|
-
if (Array.isArray(v)) return v.filter(
|
|
8000
|
-
if (
|
|
8001
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8605
|
+
if (Array.isArray(v)) return v.filter(isRec24);
|
|
8606
|
+
if (isRec24(v)) {
|
|
8607
|
+
return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
|
|
8002
8608
|
}
|
|
8003
8609
|
return [];
|
|
8004
8610
|
}
|
|
@@ -8015,7 +8621,7 @@ function loadAjv() {
|
|
|
8015
8621
|
`@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
8622
|
);
|
|
8017
8623
|
}
|
|
8018
|
-
const ctor =
|
|
8624
|
+
const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8019
8625
|
cachedAjv = ctor;
|
|
8020
8626
|
return ctor;
|
|
8021
8627
|
}
|
|
@@ -8030,7 +8636,7 @@ function loadAddFormats() {
|
|
|
8030
8636
|
`@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
8637
|
);
|
|
8032
8638
|
}
|
|
8033
|
-
const plugin =
|
|
8639
|
+
const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8034
8640
|
cachedAddFormats = plugin;
|
|
8035
8641
|
return plugin;
|
|
8036
8642
|
}
|
|
@@ -8060,13 +8666,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
8060
8666
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
8061
8667
|
for (const branch of ["then", "otherwise"]) {
|
|
8062
8668
|
const nested = rule[branch];
|
|
8063
|
-
if (
|
|
8669
|
+
if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
8064
8670
|
}
|
|
8065
8671
|
return out;
|
|
8066
8672
|
}
|
|
8067
8673
|
function walkObjectValidationRules(stack) {
|
|
8068
8674
|
const walked = [];
|
|
8069
|
-
if (!
|
|
8675
|
+
if (!isRec24(stack)) return walked;
|
|
8070
8676
|
for (const obj of asArray40(stack.objects)) {
|
|
8071
8677
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8072
8678
|
const validations = obj.validations;
|
|
@@ -8101,7 +8707,7 @@ function validateRuleCompilability(stack) {
|
|
|
8101
8707
|
});
|
|
8102
8708
|
}
|
|
8103
8709
|
}
|
|
8104
|
-
if (rule.type === "json_schema" &&
|
|
8710
|
+
if (rule.type === "json_schema" && isRec24(rule.schema)) {
|
|
8105
8711
|
try {
|
|
8106
8712
|
createRuntimeAjv().compile(rule.schema);
|
|
8107
8713
|
} catch (err) {
|
|
@@ -8121,7 +8727,7 @@ function validateRuleCompilability(stack) {
|
|
|
8121
8727
|
|
|
8122
8728
|
// src/validate-rule-schema-formats.ts
|
|
8123
8729
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
8124
|
-
var
|
|
8730
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8125
8731
|
var SUBSCHEMA_KEYS = [
|
|
8126
8732
|
"additionalItems",
|
|
8127
8733
|
"additionalProperties",
|
|
@@ -8145,7 +8751,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
8145
8751
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
8146
8752
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
8147
8753
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
8148
|
-
if (!
|
|
8754
|
+
if (!isRec25(schema)) return;
|
|
8149
8755
|
if (typeof schema.format === "string") {
|
|
8150
8756
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
8151
8757
|
}
|
|
@@ -8164,7 +8770,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8164
8770
|
}
|
|
8165
8771
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
8166
8772
|
const value = schema[key];
|
|
8167
|
-
if (!
|
|
8773
|
+
if (!isRec25(value)) continue;
|
|
8168
8774
|
for (const [name, entry] of Object.entries(value)) {
|
|
8169
8775
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8170
8776
|
}
|
|
@@ -8172,13 +8778,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8172
8778
|
const items = schema.items;
|
|
8173
8779
|
if (Array.isArray(items)) {
|
|
8174
8780
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
8175
|
-
} else if (
|
|
8781
|
+
} else if (isRec25(items)) {
|
|
8176
8782
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
8177
8783
|
}
|
|
8178
8784
|
const dependencies = schema.dependencies;
|
|
8179
|
-
if (
|
|
8785
|
+
if (isRec25(dependencies)) {
|
|
8180
8786
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
8181
|
-
if (!
|
|
8787
|
+
if (!isRec25(entry)) continue;
|
|
8182
8788
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8183
8789
|
}
|
|
8184
8790
|
}
|
|
@@ -8205,9 +8811,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
8205
8811
|
let best = null;
|
|
8206
8812
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
8207
8813
|
for (const candidate of [...registered].sort()) {
|
|
8208
|
-
const
|
|
8209
|
-
if (
|
|
8210
|
-
bestDistance =
|
|
8814
|
+
const distance8 = editDistance2(authored, candidate);
|
|
8815
|
+
if (distance8 < bestDistance) {
|
|
8816
|
+
bestDistance = distance8;
|
|
8211
8817
|
best = candidate;
|
|
8212
8818
|
}
|
|
8213
8819
|
}
|
|
@@ -8217,7 +8823,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
8217
8823
|
const findings = [];
|
|
8218
8824
|
const pending = [];
|
|
8219
8825
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
8220
|
-
if (rule.type !== "json_schema" || !
|
|
8826
|
+
if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
|
|
8221
8827
|
const uses = [];
|
|
8222
8828
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
8223
8829
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -8250,7 +8856,7 @@ function asArray41(v) {
|
|
|
8250
8856
|
}
|
|
8251
8857
|
return [];
|
|
8252
8858
|
}
|
|
8253
|
-
function
|
|
8859
|
+
function strName21(v) {
|
|
8254
8860
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8255
8861
|
}
|
|
8256
8862
|
function strList3(v) {
|
|
@@ -8265,7 +8871,7 @@ function collectNamePlacedActions(stack) {
|
|
|
8265
8871
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
8266
8872
|
}
|
|
8267
8873
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8268
|
-
const n =
|
|
8874
|
+
const n = strName21(def?.name);
|
|
8269
8875
|
if (n) placed.add(n);
|
|
8270
8876
|
}
|
|
8271
8877
|
};
|
|
@@ -8291,7 +8897,7 @@ function validateActionLocations(stack) {
|
|
|
8291
8897
|
const check = (action, path) => {
|
|
8292
8898
|
if (!action || typeof action !== "object") return;
|
|
8293
8899
|
if ("locations" in action) return;
|
|
8294
|
-
const name =
|
|
8900
|
+
const name = strName21(action.name);
|
|
8295
8901
|
if (!name) return;
|
|
8296
8902
|
if (namePlaced.has(name)) return;
|
|
8297
8903
|
findings.push({
|
|
@@ -8317,7 +8923,7 @@ function validateActionLocations(stack) {
|
|
|
8317
8923
|
|
|
8318
8924
|
// src/lint-flow-patterns.ts
|
|
8319
8925
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8320
|
-
var
|
|
8926
|
+
var import_data12 = require("@objectstack/spec/data");
|
|
8321
8927
|
function asArray42(v) {
|
|
8322
8928
|
if (Array.isArray(v)) return v;
|
|
8323
8929
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -8592,7 +9198,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
8592
9198
|
function filterCarriesNoCondition(filter) {
|
|
8593
9199
|
if (filter === void 0 || filter === null) return true;
|
|
8594
9200
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
8595
|
-
return (0,
|
|
9201
|
+
return (0, import_data12.reduceFilterVerdict)(filter) === "true";
|
|
8596
9202
|
}
|
|
8597
9203
|
function describeUnboundedFilter(filter) {
|
|
8598
9204
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -8959,7 +9565,7 @@ function lintLivenessProperties(stack) {
|
|
|
8959
9565
|
}
|
|
8960
9566
|
|
|
8961
9567
|
// src/lint-autonumber-formats.ts
|
|
8962
|
-
var
|
|
9568
|
+
var import_data13 = require("@objectstack/spec/data");
|
|
8963
9569
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
8964
9570
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
8965
9571
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
@@ -8985,8 +9591,8 @@ function lintAutonumberFormats(stack) {
|
|
|
8985
9591
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
8986
9592
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
8987
9593
|
if (!fmt) continue;
|
|
8988
|
-
const tokens = (0,
|
|
8989
|
-
const refs = (0,
|
|
9594
|
+
const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
|
|
9595
|
+
const refs = (0, import_data13.referencedFields)(tokens);
|
|
8990
9596
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
8991
9597
|
for (const t of tokens) {
|
|
8992
9598
|
if (t.kind !== "literal") continue;
|
|
@@ -9141,6 +9747,17 @@ function lintViewRefs(stack) {
|
|
|
9141
9747
|
}
|
|
9142
9748
|
|
|
9143
9749
|
// src/data-model-rules.ts
|
|
9750
|
+
function objectWhere(obj) {
|
|
9751
|
+
return `object "${obj?.name}"`;
|
|
9752
|
+
}
|
|
9753
|
+
function indexWhere(obj, idx, j, cols) {
|
|
9754
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
9755
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
9756
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
9757
|
+
}
|
|
9758
|
+
function fieldWhere(obj, fieldName) {
|
|
9759
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
9760
|
+
}
|
|
9144
9761
|
function fieldEntries2(fields) {
|
|
9145
9762
|
if (!fields) return [];
|
|
9146
9763
|
if (Array.isArray(fields)) {
|
|
@@ -9174,11 +9791,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9174
9791
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9175
9792
|
const idx = declaredIndexes[j];
|
|
9176
9793
|
if (idx?.unique !== true) continue;
|
|
9177
|
-
const
|
|
9794
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
9795
|
+
const cols = colList.join(", ");
|
|
9178
9796
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9179
9797
|
issues.push({
|
|
9180
9798
|
severity: "warning",
|
|
9181
9799
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9800
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9182
9801
|
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
9802
|
path: `objects[${i}].indexes[${j}]`,
|
|
9184
9803
|
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 +9846,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9227
9846
|
issues.push({
|
|
9228
9847
|
severity: "warning",
|
|
9229
9848
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
9849
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
9850
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
9851
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
9852
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
9853
|
+
where: fieldWhere(obj, name),
|
|
9230
9854
|
message,
|
|
9231
9855
|
path: `objects[${i}]`,
|
|
9232
9856
|
fix
|
|
@@ -9255,6 +9879,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9255
9879
|
issues.push({
|
|
9256
9880
|
severity: "warning",
|
|
9257
9881
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9882
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9258
9883
|
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
9884
|
path: `objects[${i}].indexes[${j}]`,
|
|
9260
9885
|
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 +9896,7 @@ var CLI_ONLY = ["cli"];
|
|
|
9271
9896
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
9272
9897
|
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
9898
|
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
|
|
9899
|
+
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
9900
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
9276
9901
|
var AUTHORING_RULES = [
|
|
9277
9902
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -9332,8 +9957,14 @@ var AUTHORING_RULES = [
|
|
|
9332
9957
|
input: "normalized",
|
|
9333
9958
|
commands: ALL,
|
|
9334
9959
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
9335
|
-
|
|
9336
|
-
|
|
9960
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
9961
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
9962
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
9963
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
9964
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
9965
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
9966
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9967
|
+
runtimeTypes: ["object"],
|
|
9337
9968
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
9338
9969
|
},
|
|
9339
9970
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -9357,8 +9988,11 @@ var AUTHORING_RULES = [
|
|
|
9357
9988
|
input: "normalized",
|
|
9358
9989
|
commands: ALL,
|
|
9359
9990
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9360
|
-
|
|
9361
|
-
|
|
9991
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
9992
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
9993
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
9994
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9995
|
+
runtimeTypes: ["object"],
|
|
9362
9996
|
run: (stack) => validateManagedApiMethods(stack)
|
|
9363
9997
|
},
|
|
9364
9998
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -9380,19 +10014,38 @@ var AUTHORING_RULES = [
|
|
|
9380
10014
|
},
|
|
9381
10015
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
9382
10016
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
10017
|
+
//
|
|
10018
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
10019
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
10020
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
10021
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
10022
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
10023
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
10024
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
10025
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
10026
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
10027
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
10028
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
10029
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
10030
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
10031
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
10032
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
10033
|
+
// advisory channel and never block (#4463 P1).
|
|
9383
10034
|
{
|
|
9384
10035
|
name: "validateWidgetBindings",
|
|
9385
10036
|
tier: "gating",
|
|
9386
10037
|
input: "parsed",
|
|
9387
10038
|
commands: ALL,
|
|
9388
10039
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
9389
|
-
surfaces:
|
|
9390
|
-
|
|
10040
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10041
|
+
runtimeTypes: ["dashboard"],
|
|
9391
10042
|
run: (stack) => validateWidgetBindings(stack)
|
|
9392
10043
|
},
|
|
9393
|
-
// ADR-0049 / #3367 — a header
|
|
9394
|
-
//
|
|
9395
|
-
//
|
|
10044
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
10045
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
10046
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
10047
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
10048
|
+
// Unresolved `url` routes stay advisory.
|
|
9396
10049
|
{
|
|
9397
10050
|
name: "validateDashboardActionRefs",
|
|
9398
10051
|
tier: "gating",
|
|
@@ -9417,6 +10070,28 @@ var AUTHORING_RULES = [
|
|
|
9417
10070
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
9418
10071
|
run: (stack) => validateFilterTokens(stack)
|
|
9419
10072
|
},
|
|
10073
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
10074
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
10075
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
10076
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
10077
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
10078
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
10079
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
10080
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
10081
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
10082
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
10083
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
10084
|
+
// vocabulary is near enough to reach for.
|
|
10085
|
+
{
|
|
10086
|
+
name: "validatePresetComparands",
|
|
10087
|
+
tier: "gating",
|
|
10088
|
+
input: "parsed",
|
|
10089
|
+
commands: ALL,
|
|
10090
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
10091
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10092
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
10093
|
+
run: (stack) => validatePresetComparands(stack)
|
|
10094
|
+
},
|
|
9420
10095
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9421
10096
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9422
10097
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -9460,9 +10135,31 @@ var AUTHORING_RULES = [
|
|
|
9460
10135
|
// collection in the snapshot and return nothing, and the two that would
|
|
9461
10136
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
9462
10137
|
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
10138
|
+
//
|
|
10139
|
+
// [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
|
|
10140
|
+
// taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
|
|
10141
|
+
// dispatch the suite, and the suite's own per-member `runtimeTypes`
|
|
10142
|
+
// (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
|
|
10143
|
+
// that snapshot — the gate passes the written type through
|
|
10144
|
+
// `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
|
|
10145
|
+
// members that judge a list view's field references and resolve only
|
|
10146
|
+
// against the `objects` collection the snapshot carries
|
|
10147
|
+
// (`validateSearchableFields`, `validateSortableFields`). The whole suite
|
|
10148
|
+
// is NOT the right granularity for this door, measured not assumed, and
|
|
10149
|
+
// the crossing fails differently per body shape — both ways wrong:
|
|
10150
|
+
// `validateActionNameRefs` (error-tier) resolves `views[].list` /
|
|
10151
|
+
// `views[].listViews.*` action names against `stack.actions`, a collection
|
|
10152
|
+
// no per-write snapshot carries, so on a CONTAINER view write it would
|
|
10153
|
+
// refuse every stack-level action the body names (measured:
|
|
10154
|
+
// `action-name-undefined` on the snapshot shape, clean on the full stack)
|
|
10155
|
+
// — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
|
|
10156
|
+
// type the gate has. On a FLATTENED overlay it has no rung at all, so the
|
|
10157
|
+
// crossing would be a silent no-op that reads as coverage — the very shape
|
|
10158
|
+
// #9313 was filed about. Flow snapshots are unchanged: every member keeps
|
|
10159
|
+
// the default `flow` declaration.
|
|
9463
10160
|
surfaces: CLI_AND_RUNTIME,
|
|
9464
|
-
runtimeTypes: ["flow"],
|
|
9465
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
10161
|
+
runtimeTypes: ["flow", "view"],
|
|
10162
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
9466
10163
|
},
|
|
9467
10164
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
9468
10165
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -9635,7 +10332,7 @@ var AUTHORING_RULES = [
|
|
|
9635
10332
|
commands: ALL,
|
|
9636
10333
|
source: "packages/lint/src/validate-record-title.ts",
|
|
9637
10334
|
surfaces: CLI_ONLY,
|
|
9638
|
-
surfaceReason:
|
|
10335
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9639
10336
|
run: (stack) => validateRecordTitle(stack)
|
|
9640
10337
|
},
|
|
9641
10338
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -9648,7 +10345,7 @@ var AUTHORING_RULES = [
|
|
|
9648
10345
|
commands: ALL,
|
|
9649
10346
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
9650
10347
|
surfaces: CLI_ONLY,
|
|
9651
|
-
surfaceReason:
|
|
10348
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9652
10349
|
run: (stack) => validateSemanticRoles(stack)
|
|
9653
10350
|
},
|
|
9654
10351
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -9859,7 +10556,7 @@ var AUTHORING_RULES = [
|
|
|
9859
10556
|
commands: ALL,
|
|
9860
10557
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
9861
10558
|
surfaces: CLI_ONLY,
|
|
9862
|
-
surfaceReason:
|
|
10559
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9863
10560
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
9864
10561
|
severity: "warning",
|
|
9865
10562
|
rule: f.rule,
|
|
@@ -9878,8 +10575,12 @@ var AUTHORING_RULES = [
|
|
|
9878
10575
|
input: "parsed",
|
|
9879
10576
|
commands: ALL,
|
|
9880
10577
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
9881
|
-
|
|
9882
|
-
|
|
10578
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
10579
|
+
// object does not carry is broken from the first record; only the error
|
|
10580
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
10581
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
10582
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10583
|
+
runtimeTypes: ["object"],
|
|
9883
10584
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
9884
10585
|
severity: f.severity,
|
|
9885
10586
|
rule: f.rule,
|
|
@@ -9920,12 +10621,12 @@ var AUTHORING_RULES = [
|
|
|
9920
10621
|
commands: ["validate", "build"],
|
|
9921
10622
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9922
10623
|
surfaces: CLI_ONLY,
|
|
9923
|
-
surfaceReason:
|
|
10624
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9924
10625
|
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
10626
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9926
10627
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9927
10628
|
rule: f.rule,
|
|
9928
|
-
where: f.
|
|
10629
|
+
where: f.where,
|
|
9929
10630
|
path: f.path,
|
|
9930
10631
|
message: f.message,
|
|
9931
10632
|
hint: f.fix ?? ""
|
|
@@ -9941,12 +10642,12 @@ var AUTHORING_RULES = [
|
|
|
9941
10642
|
commands: ["validate", "build"],
|
|
9942
10643
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9943
10644
|
surfaces: CLI_ONLY,
|
|
9944
|
-
surfaceReason:
|
|
10645
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9945
10646
|
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
10647
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9947
10648
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9948
10649
|
rule: f.rule,
|
|
9949
|
-
where: f.
|
|
10650
|
+
where: f.where,
|
|
9950
10651
|
path: f.path,
|
|
9951
10652
|
message: f.message,
|
|
9952
10653
|
hint: f.fix ?? ""
|
|
@@ -9963,12 +10664,12 @@ var AUTHORING_RULES = [
|
|
|
9963
10664
|
commands: ["validate", "build"],
|
|
9964
10665
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9965
10666
|
surfaces: CLI_ONLY,
|
|
9966
|
-
surfaceReason:
|
|
10667
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9967
10668
|
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
10669
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9969
10670
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9970
10671
|
rule: f.rule,
|
|
9971
|
-
where: f.
|
|
10672
|
+
where: f.where,
|
|
9972
10673
|
path: f.path,
|
|
9973
10674
|
message: f.message,
|
|
9974
10675
|
hint: f.fix ?? ""
|
|
@@ -10096,16 +10797,26 @@ var AUTHORING_RULES = [
|
|
|
10096
10797
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10097
10798
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10098
10799
|
},
|
|
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
|
-
//
|
|
10800
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
10801
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
10802
|
+
//
|
|
10803
|
+
// - `condition` has a single runtime consumer
|
|
10804
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
10805
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
10806
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
10807
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
10808
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
10809
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
10810
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
10811
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
10812
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
10813
|
+
//
|
|
10814
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
10815
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
10816
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
10817
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
10818
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
10819
|
+
// yet true.
|
|
10109
10820
|
{
|
|
10110
10821
|
name: "validateSharingRuleEnforceability",
|
|
10111
10822
|
tier: "gating",
|
|
@@ -10113,7 +10824,7 @@ var AUTHORING_RULES = [
|
|
|
10113
10824
|
commands: ALL,
|
|
10114
10825
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10115
10826
|
surfaces: CLI_ONLY,
|
|
10116
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
10827
|
+
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
10828
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10118
10829
|
},
|
|
10119
10830
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10152,8 +10863,15 @@ var AUTHORING_RULES = [
|
|
|
10152
10863
|
input: "parsed",
|
|
10153
10864
|
commands: ALL,
|
|
10154
10865
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10155
|
-
|
|
10156
|
-
|
|
10866
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
10867
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
10868
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
10869
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
10870
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
10871
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
10872
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
10873
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10874
|
+
runtimeTypes: ["object"],
|
|
10157
10875
|
run: (stack) => validateRuleCompilability(stack)
|
|
10158
10876
|
},
|
|
10159
10877
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10174,8 +10892,12 @@ var AUTHORING_RULES = [
|
|
|
10174
10892
|
input: "parsed",
|
|
10175
10893
|
commands: ALL,
|
|
10176
10894
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10177
|
-
|
|
10178
|
-
|
|
10895
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
10896
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
10897
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
10898
|
+
// format set is only enumerated once a schema actually names a format.
|
|
10899
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10900
|
+
runtimeTypes: ["object"],
|
|
10179
10901
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10180
10902
|
}
|
|
10181
10903
|
];
|
|
@@ -10217,7 +10939,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
10217
10939
|
permission: "permissions",
|
|
10218
10940
|
book: "books"
|
|
10219
10941
|
};
|
|
10220
|
-
|
|
10942
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
10943
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
10944
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
10945
|
+
return objects.filter((entry) => {
|
|
10946
|
+
if (!entry || typeof entry !== "object") return true;
|
|
10947
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
10948
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
10949
|
+
if (reachable.has(owner)) return true;
|
|
10950
|
+
return isSystemObject(entry);
|
|
10951
|
+
});
|
|
10952
|
+
}
|
|
10953
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
10954
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
10955
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
10221
10956
|
function runtimeAuthoringRulesFor(type) {
|
|
10222
10957
|
return AUTHORING_RULES.filter(
|
|
10223
10958
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -10243,7 +10978,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10243
10978
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10244
10979
|
const baseline = {};
|
|
10245
10980
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
10246
|
-
const
|
|
10981
|
+
const raw = args.context?.[key] ?? [];
|
|
10982
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
10247
10983
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10248
10984
|
}
|
|
10249
10985
|
const candidate = {
|
|
@@ -10252,6 +10988,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10252
10988
|
};
|
|
10253
10989
|
return { baseline, candidate };
|
|
10254
10990
|
}
|
|
10991
|
+
var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
|
|
10992
|
+
var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10993
|
+
var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
|
|
10994
|
+
function nameKeyFindingPath(path, candidate) {
|
|
10995
|
+
const m = TOP_LEVEL_INDEX.exec(path);
|
|
10996
|
+
if (!m) return path;
|
|
10997
|
+
const [, stackKey, index, rest] = m;
|
|
10998
|
+
if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
|
|
10999
|
+
const collection = candidate[stackKey];
|
|
11000
|
+
const entry = collection?.[Number(index)];
|
|
11001
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
11002
|
+
if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
|
|
11003
|
+
return `${stackKey}.${name}${rest}`;
|
|
11004
|
+
}
|
|
10255
11005
|
function runRules(rules, stack, ctx) {
|
|
10256
11006
|
const findings = [];
|
|
10257
11007
|
for (const rule of rules) {
|
|
@@ -10277,12 +11027,16 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10277
11027
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
10278
11028
|
type: args.type,
|
|
10279
11029
|
item: args.item,
|
|
10280
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
11030
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
11031
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10281
11032
|
});
|
|
10282
11033
|
if (!snapshots) return empty;
|
|
10283
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11034
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
10284
11035
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10285
|
-
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)))
|
|
11036
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
|
|
11037
|
+
const path = nameKeyFindingPath(f.path, snapshots.candidate);
|
|
11038
|
+
return path === f.path ? f : { ...f, path };
|
|
11039
|
+
});
|
|
10286
11040
|
return {
|
|
10287
11041
|
errors: added.filter((f) => f.severity === "error"),
|
|
10288
11042
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -10292,6 +11046,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10292
11046
|
// Annotate the CommonJS export names for ESM import in node:
|
|
10293
11047
|
0 && (module.exports = {
|
|
10294
11048
|
buildRuntimeWriteSnapshots,
|
|
11049
|
+
narrowObjectsToPackageClosure,
|
|
10295
11050
|
runRuntimeAuthoringRules,
|
|
10296
11051
|
runtimeAuthoringRulesFor,
|
|
10297
11052
|
runtimeGatedTypes,
|