@objectstack/lint 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +622 -0
- package/dist/index.cjs +894 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -66
- package/dist/index.d.ts +310 -66
- package/dist/index.js +871 -278
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-s3X9D9hm.d.cts} +89 -2
- package/dist/{runtime-H-nDodRy.d.ts → runtime-s3X9D9hm.d.ts} +89 -2
- package/dist/runtime.cjs +986 -409
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +959 -378
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/dist/runtime.js
CHANGED
|
@@ -250,10 +250,10 @@ var OUTCOME_CLAUSE = {
|
|
|
250
250
|
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
251
251
|
"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)"
|
|
252
252
|
};
|
|
253
|
-
function nullGuardMessage(subject, objectName,
|
|
253
|
+
function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
|
|
254
254
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
255
|
-
const hasNote =
|
|
256
|
-
return `${subject} applies \`${
|
|
255
|
+
const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
|
|
256
|
+
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}`;
|
|
257
257
|
}
|
|
258
258
|
|
|
259
259
|
// src/validate-expressions.ts
|
|
@@ -436,10 +436,10 @@ function validateStackExpressions(stack) {
|
|
|
436
436
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
437
437
|
const source = celSourceOf(raw);
|
|
438
438
|
if (!source) return;
|
|
439
|
-
for (const
|
|
439
|
+
for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
440
440
|
issues.push({
|
|
441
441
|
where,
|
|
442
|
-
message: nullGuardMessage(subject, objectName,
|
|
442
|
+
message: nullGuardMessage(subject, objectName, finding2, outcome),
|
|
443
443
|
source,
|
|
444
444
|
severity: "error"
|
|
445
445
|
});
|
|
@@ -463,7 +463,7 @@ function validateStackExpressions(stack) {
|
|
|
463
463
|
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";
|
|
464
464
|
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
465
465
|
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)",
|
|
466
|
-
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`).
|
|
466
|
+
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",
|
|
467
467
|
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",
|
|
468
468
|
// Listed rather than left to the `??` below, so the map covers every slot
|
|
469
469
|
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
@@ -522,14 +522,15 @@ function validateStackExpressions(stack) {
|
|
|
522
522
|
if (retired.length > 0) {
|
|
523
523
|
issues.push({
|
|
524
524
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
525
|
-
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)
|
|
526
|
-
// behaviour, never the retired key's fate —
|
|
527
|
-
// over a branch that DELETES the key
|
|
528
|
-
//
|
|
525
|
+
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
|
|
526
|
+
// sentence names the TOOL's behaviour, never the retired key's fate —
|
|
527
|
+
// "rewrite it" read two ways over a branch that DELETES the key
|
|
528
|
+
// (template/recipients/variables/script), and the tool never rewrote a
|
|
529
|
+
// source file at all. Plain-quoted (not a template literal)
|
|
529
530
|
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
530
531
|
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
531
532
|
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
532
|
-
"Run `os migrate meta --from 16` to
|
|
533
|
+
"Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
|
|
533
534
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
534
535
|
});
|
|
535
536
|
} else if (!fn) {
|
|
@@ -600,10 +601,10 @@ function validateStackExpressions(stack) {
|
|
|
600
601
|
f.expression,
|
|
601
602
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
602
603
|
);
|
|
603
|
-
const
|
|
604
|
-
for (const e of res.errors) issues.push({ where:
|
|
605
|
-
for (const w of res.warnings) issues.push({ where:
|
|
606
|
-
warnUnprovisionedAnchors(
|
|
604
|
+
const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
605
|
+
for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
|
|
606
|
+
for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
|
|
607
|
+
warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
|
|
607
608
|
}
|
|
608
609
|
}
|
|
609
610
|
}
|
|
@@ -1259,7 +1260,6 @@ function asArray4(v) {
|
|
|
1259
1260
|
function strName(v) {
|
|
1260
1261
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1261
1262
|
}
|
|
1262
|
-
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
1263
1263
|
var URL_COLLECTION_TO_STACK_KEY = {
|
|
1264
1264
|
object: "objects",
|
|
1265
1265
|
objects: "objects",
|
|
@@ -1304,13 +1304,10 @@ function collectKnownTargets(stack) {
|
|
|
1304
1304
|
return { actions, objects, reports, dashboards, pages, views };
|
|
1305
1305
|
}
|
|
1306
1306
|
function resolveActionTarget(actionType, target, known) {
|
|
1307
|
-
if (known.actions.has(target)) return true;
|
|
1308
1307
|
if (actionType === "modal") {
|
|
1309
|
-
|
|
1310
|
-
const m = MODAL_VERB_RE.exec(target);
|
|
1311
|
-
if (m && known.objects.has(m[1])) return true;
|
|
1308
|
+
return known.pages.has(target);
|
|
1312
1309
|
}
|
|
1313
|
-
return
|
|
1310
|
+
return known.actions.has(target);
|
|
1314
1311
|
}
|
|
1315
1312
|
function resolveUrlRoute(target, known) {
|
|
1316
1313
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
|
|
@@ -1340,14 +1337,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
1340
1337
|
const actionType = strName(action.actionType) ?? "url";
|
|
1341
1338
|
if (actionType === "script" || actionType === "modal") {
|
|
1342
1339
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
1343
|
-
const kindWord = actionType === "script" ? "script" : "modal";
|
|
1344
1340
|
findings.push({
|
|
1345
1341
|
severity: "error",
|
|
1346
1342
|
rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
1347
1343
|
where,
|
|
1348
1344
|
path,
|
|
1349
|
-
message:
|
|
1350
|
-
hint: actionType === "modal" ? `
|
|
1345
|
+
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).`,
|
|
1346
|
+
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.`
|
|
1351
1347
|
});
|
|
1352
1348
|
return;
|
|
1353
1349
|
}
|
|
@@ -1389,7 +1385,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
1389
1385
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
1390
1386
|
|
|
1391
1387
|
// src/filter-walk.ts
|
|
1392
|
-
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1388
|
+
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
|
|
1393
1389
|
function asArray5(v) {
|
|
1394
1390
|
if (Array.isArray(v)) return v;
|
|
1395
1391
|
if (v && typeof v === "object") {
|
|
@@ -1493,6 +1489,149 @@ function validateFilterTokens(stack) {
|
|
|
1493
1489
|
return out;
|
|
1494
1490
|
}
|
|
1495
1491
|
|
|
1492
|
+
// src/validate-preset-comparands.ts
|
|
1493
|
+
import {
|
|
1494
|
+
VALID_AST_OPERATORS,
|
|
1495
|
+
bareDateRangePresetComparandMessage,
|
|
1496
|
+
canonicalAstOperator,
|
|
1497
|
+
isDateRangePresetName
|
|
1498
|
+
} from "@objectstack/spec/data";
|
|
1499
|
+
import { normalizeFilterOperator } from "@objectstack/spec/ui";
|
|
1500
|
+
var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
|
|
1501
|
+
var PRESET_COMPARAND_SURFACES = [
|
|
1502
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1503
|
+
{ key: "objects", kind: "object" },
|
|
1504
|
+
{ key: "views", kind: "view" },
|
|
1505
|
+
{ key: "reports", kind: "report" },
|
|
1506
|
+
{ key: "datasets", kind: "dataset" },
|
|
1507
|
+
{ key: "pages", kind: "page" },
|
|
1508
|
+
{ key: "apps", kind: "app" },
|
|
1509
|
+
{ key: "flows", kind: "flow" }
|
|
1510
|
+
];
|
|
1511
|
+
var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
|
|
1512
|
+
var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
1513
|
+
var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
|
|
1514
|
+
"greater_than",
|
|
1515
|
+
"greater_than_or_equal",
|
|
1516
|
+
"less_than",
|
|
1517
|
+
"less_than_or_equal",
|
|
1518
|
+
"before",
|
|
1519
|
+
"after"
|
|
1520
|
+
]);
|
|
1521
|
+
var MAX_DEPTH = 32;
|
|
1522
|
+
function isPlainObject(v) {
|
|
1523
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
1524
|
+
}
|
|
1525
|
+
function finding(where, path, preset, operator) {
|
|
1526
|
+
return {
|
|
1527
|
+
severity: "error",
|
|
1528
|
+
rule: FILTER_PRESET_COMPARAND,
|
|
1529
|
+
where,
|
|
1530
|
+
path,
|
|
1531
|
+
message: bareDateRangePresetComparandMessage(preset, operator),
|
|
1532
|
+
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."
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
function judgeConditionNode(node, path, where, out, depth) {
|
|
1536
|
+
if (depth > MAX_DEPTH) return;
|
|
1537
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1538
|
+
const here = `${path}.${key}`;
|
|
1539
|
+
if (key === "$and" || key === "$or") {
|
|
1540
|
+
if (Array.isArray(value)) {
|
|
1541
|
+
value.forEach((arm, i) => {
|
|
1542
|
+
if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
if (key === "$not") {
|
|
1548
|
+
if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
if (key.startsWith("$")) continue;
|
|
1552
|
+
if (!isPlainObject(value)) continue;
|
|
1553
|
+
const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
|
|
1554
|
+
if (!hasOps) {
|
|
1555
|
+
judgeConditionNode(value, here, where, out, depth + 1);
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
for (const [op, comparand] of Object.entries(value)) {
|
|
1559
|
+
if (ORDERING_DOLLAR_OPS.has(op) && isDateRangePresetName(comparand)) {
|
|
1560
|
+
out.push(finding(where, `${here}.${op}`, comparand, op));
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (op === "$between" && Array.isArray(comparand)) {
|
|
1564
|
+
comparand.forEach((endpoint, i) => {
|
|
1565
|
+
if (isDateRangePresetName(endpoint)) {
|
|
1566
|
+
out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function judgeFilterRule(rule, path, where, out) {
|
|
1574
|
+
const operator = normalizeFilterOperator(rule.operator);
|
|
1575
|
+
if (typeof operator !== "string") return;
|
|
1576
|
+
const value = rule.value;
|
|
1577
|
+
if (ORDERING_RULE_OPS.has(operator) && isDateRangePresetName(value)) {
|
|
1578
|
+
out.push(finding(where, `${path}.value`, value, operator));
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
if (operator === "between" && Array.isArray(value)) {
|
|
1582
|
+
value.forEach((endpoint, i) => {
|
|
1583
|
+
if (isDateRangePresetName(endpoint)) {
|
|
1584
|
+
out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
function judgeTriple(triple, path, where, out) {
|
|
1590
|
+
const op = triple[1];
|
|
1591
|
+
if (typeof op !== "string") return;
|
|
1592
|
+
const canonical = canonicalAstOperator(op);
|
|
1593
|
+
const value = triple[2];
|
|
1594
|
+
if (ORDERING_INFIX_OPS.has(canonical) && isDateRangePresetName(value)) {
|
|
1595
|
+
out.push(finding(where, `${path}[2]`, value, op));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (canonical === "between" && Array.isArray(value)) {
|
|
1599
|
+
value.forEach((endpoint, i) => {
|
|
1600
|
+
if (isDateRangePresetName(endpoint)) {
|
|
1601
|
+
out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function judgeFilterValue(node, path, where, out, depth) {
|
|
1607
|
+
if (depth > MAX_DEPTH) return;
|
|
1608
|
+
if (Array.isArray(node)) {
|
|
1609
|
+
if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
1610
|
+
judgeTriple(node, path, where, out);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
node.forEach((member, i) => {
|
|
1614
|
+
if (typeof member === "string") return;
|
|
1615
|
+
judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
|
|
1616
|
+
});
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
if (!isPlainObject(node)) return;
|
|
1620
|
+
if (typeof node.field === "string" && typeof node.operator === "string") {
|
|
1621
|
+
judgeFilterRule(node, path, where, out);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
judgeConditionNode(node, path, where, out, depth);
|
|
1625
|
+
}
|
|
1626
|
+
function validatePresetComparands(stack) {
|
|
1627
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1628
|
+
const out = [];
|
|
1629
|
+
walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
|
|
1630
|
+
judgeFilterValue(value, path, where, out, 0);
|
|
1631
|
+
});
|
|
1632
|
+
return out;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1496
1635
|
// src/validate-empty-combinators.ts
|
|
1497
1636
|
import { reduceFilterVerdict } from "@objectstack/spec/data";
|
|
1498
1637
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
@@ -1816,6 +1955,7 @@ import {
|
|
|
1816
1955
|
} from "@objectstack/spec/data";
|
|
1817
1956
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1818
1957
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1958
|
+
var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
|
|
1819
1959
|
function asArray7(v) {
|
|
1820
1960
|
if (Array.isArray(v)) return v;
|
|
1821
1961
|
if (v && typeof v === "object") {
|
|
@@ -1906,7 +2046,7 @@ function indexObjectSearchTargets(stack) {
|
|
|
1906
2046
|
}
|
|
1907
2047
|
return fieldsByObject;
|
|
1908
2048
|
}
|
|
1909
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
2049
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
|
|
1910
2050
|
const findings = [];
|
|
1911
2051
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1912
2052
|
if (!objectName) return findings;
|
|
@@ -1914,6 +2054,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1914
2054
|
const target = fieldsByObject.get(objectName);
|
|
1915
2055
|
if (!target) return findings;
|
|
1916
2056
|
const known = target.names;
|
|
2057
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
1917
2058
|
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
1918
2059
|
for (let i = 0; i < declared.length; i++) {
|
|
1919
2060
|
const entry = declared[i];
|
|
@@ -1931,6 +2072,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1931
2072
|
});
|
|
1932
2073
|
continue;
|
|
1933
2074
|
}
|
|
2075
|
+
if (anchors?.has(name)) {
|
|
2076
|
+
findings.push({
|
|
2077
|
+
severity: "warning",
|
|
2078
|
+
rule: SEARCHABLE_FIELD_UNPROVISIONED,
|
|
2079
|
+
where,
|
|
2080
|
+
path: `${path}[${i}]`,
|
|
2081
|
+
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.`),
|
|
2082
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
1934
2085
|
if (isVirtualSearchField(target.fields[name])) {
|
|
1935
2086
|
const vtype = target.fields[name]?.type;
|
|
1936
2087
|
findings.push({
|
|
@@ -1984,9 +2135,19 @@ function validateSearchableFields(stack) {
|
|
|
1984
2135
|
if (!isRec3(stack)) return findings;
|
|
1985
2136
|
const objects = asArray7(stack.objects);
|
|
1986
2137
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2138
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
1987
2139
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
1988
2140
|
findings.push(
|
|
1989
|
-
...checkSearchableFieldList(
|
|
2141
|
+
...checkSearchableFieldList(
|
|
2142
|
+
declared,
|
|
2143
|
+
objectName,
|
|
2144
|
+
fieldsByObject,
|
|
2145
|
+
where,
|
|
2146
|
+
path,
|
|
2147
|
+
subject,
|
|
2148
|
+
role,
|
|
2149
|
+
unprovisionedAnchors
|
|
2150
|
+
)
|
|
1990
2151
|
);
|
|
1991
2152
|
};
|
|
1992
2153
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -2055,33 +2216,203 @@ function listViewObject(listView) {
|
|
|
2055
2216
|
return isRec3(data) ? strName3(data.object) : void 0;
|
|
2056
2217
|
}
|
|
2057
2218
|
|
|
2058
|
-
// src/
|
|
2219
|
+
// src/validate-sortable-fields.ts
|
|
2220
|
+
import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
|
|
2221
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2222
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2059
2223
|
function isRec4(v) {
|
|
2060
2224
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2061
2225
|
}
|
|
2062
2226
|
function strName4(v) {
|
|
2063
2227
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2064
2228
|
}
|
|
2229
|
+
function readSortKeys(declared) {
|
|
2230
|
+
const fromShorthand = (raw, at) => {
|
|
2231
|
+
const trimmed = raw.trim();
|
|
2232
|
+
if (!trimmed) return void 0;
|
|
2233
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
2234
|
+
return bare ? { field: bare, at } : void 0;
|
|
2235
|
+
};
|
|
2236
|
+
if (typeof declared === "string") {
|
|
2237
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
2238
|
+
}
|
|
2239
|
+
if (Array.isArray(declared)) {
|
|
2240
|
+
const keys = [];
|
|
2241
|
+
for (let i = 0; i < declared.length; i++) {
|
|
2242
|
+
const el = declared[i];
|
|
2243
|
+
if (typeof el === "string") {
|
|
2244
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
2245
|
+
if (k) keys.push(k);
|
|
2246
|
+
continue;
|
|
2247
|
+
}
|
|
2248
|
+
if (!isRec4(el)) continue;
|
|
2249
|
+
const field = strName4(el.field);
|
|
2250
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
2251
|
+
}
|
|
2252
|
+
return keys;
|
|
2253
|
+
}
|
|
2254
|
+
return [];
|
|
2255
|
+
}
|
|
2256
|
+
function suggest4(target, known) {
|
|
2257
|
+
let best;
|
|
2258
|
+
let bestScore = Infinity;
|
|
2259
|
+
for (const candidate of known) {
|
|
2260
|
+
const d = distance3(target, candidate);
|
|
2261
|
+
if (d < bestScore) {
|
|
2262
|
+
bestScore = d;
|
|
2263
|
+
best = candidate;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
2267
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
2268
|
+
}
|
|
2269
|
+
function distance3(a, b) {
|
|
2270
|
+
const m = a.length;
|
|
2271
|
+
const n = b.length;
|
|
2272
|
+
if (m === 0) return n;
|
|
2273
|
+
if (n === 0) return m;
|
|
2274
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2275
|
+
for (let i = 1; i <= m; i++) {
|
|
2276
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
2277
|
+
for (let j = 1; j <= n; j++) {
|
|
2278
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2279
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2280
|
+
}
|
|
2281
|
+
prev = curr;
|
|
2282
|
+
}
|
|
2283
|
+
return prev[n];
|
|
2284
|
+
}
|
|
2285
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
2286
|
+
const findings = [];
|
|
2287
|
+
if (declared === void 0 || declared === null) return findings;
|
|
2288
|
+
if (!objectName) return findings;
|
|
2289
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
2290
|
+
const target = fieldsByObject.get(objectName);
|
|
2291
|
+
if (!target) return findings;
|
|
2292
|
+
const known = target.names;
|
|
2293
|
+
for (const key of readSortKeys(declared)) {
|
|
2294
|
+
const name = key.field;
|
|
2295
|
+
const head = name.split(".")[0];
|
|
2296
|
+
if (SYSTEM_FIELDS.has(head)) continue;
|
|
2297
|
+
if (!known.has(head)) {
|
|
2298
|
+
const dotted = name.includes(".");
|
|
2299
|
+
findings.push({
|
|
2300
|
+
severity: "error",
|
|
2301
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
2302
|
+
where,
|
|
2303
|
+
path: `${path}${key.at}`,
|
|
2304
|
+
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)),
|
|
2305
|
+
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(", ")}.` : "")
|
|
2306
|
+
});
|
|
2307
|
+
continue;
|
|
2308
|
+
}
|
|
2309
|
+
const meta = target.fields[name];
|
|
2310
|
+
if (isVirtualSearchField2(meta)) {
|
|
2311
|
+
const vtype = meta?.type;
|
|
2312
|
+
findings.push({
|
|
2313
|
+
severity: "error",
|
|
2314
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
2315
|
+
where,
|
|
2316
|
+
path: `${path}${key.at}`,
|
|
2317
|
+
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.`,
|
|
2318
|
+
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.`
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
return findings;
|
|
2323
|
+
}
|
|
2324
|
+
function validateSortableFields(stack) {
|
|
2325
|
+
const findings = [];
|
|
2326
|
+
if (!isRec4(stack)) return findings;
|
|
2327
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2328
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2329
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
2330
|
+
findings.push(
|
|
2331
|
+
...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
|
|
2332
|
+
);
|
|
2333
|
+
};
|
|
2334
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
2335
|
+
const obj = objects[oi];
|
|
2336
|
+
if (!isRec4(obj)) continue;
|
|
2337
|
+
const objName = strName4(obj.name);
|
|
2338
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
2339
|
+
if (isRec4(obj.listViews)) {
|
|
2340
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2341
|
+
if (!isRec4(lv)) continue;
|
|
2342
|
+
check(
|
|
2343
|
+
lv.sort,
|
|
2344
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
2345
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
2346
|
+
listViewObject2(lv) ?? objName,
|
|
2347
|
+
`${label2} \u203A listViews.${key}`,
|
|
2348
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
2349
|
+
"list-view sort"
|
|
2350
|
+
);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
2355
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
2356
|
+
const view = views[vi];
|
|
2357
|
+
if (!isRec4(view)) continue;
|
|
2358
|
+
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2359
|
+
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2360
|
+
if (isRec4(view.list)) {
|
|
2361
|
+
check(
|
|
2362
|
+
view.list.sort,
|
|
2363
|
+
listViewObject2(view.list) ?? viewObject,
|
|
2364
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2365
|
+
`views[${vi}].list.sort`,
|
|
2366
|
+
"list-view sort"
|
|
2367
|
+
);
|
|
2368
|
+
}
|
|
2369
|
+
if (isRec4(view.listViews)) {
|
|
2370
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2371
|
+
if (!isRec4(lv)) continue;
|
|
2372
|
+
check(
|
|
2373
|
+
lv.sort,
|
|
2374
|
+
listViewObject2(lv) ?? viewObject,
|
|
2375
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2376
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
2377
|
+
"list-view sort"
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
return findings;
|
|
2383
|
+
}
|
|
2384
|
+
function listViewObject2(listView) {
|
|
2385
|
+
const data = listView.data;
|
|
2386
|
+
return isRec4(data) ? strName4(data.object) : void 0;
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/page-walk.ts
|
|
2390
|
+
function isRec5(v) {
|
|
2391
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2392
|
+
}
|
|
2393
|
+
function strName5(v) {
|
|
2394
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2395
|
+
}
|
|
2065
2396
|
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
2066
2397
|
function isSourceAuthoredPage(page) {
|
|
2067
|
-
const kind =
|
|
2398
|
+
const kind = strName5(page.kind);
|
|
2068
2399
|
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
2069
2400
|
}
|
|
2070
2401
|
function walkPageComponents(page, pagePath) {
|
|
2071
2402
|
const out = [];
|
|
2072
|
-
if (!
|
|
2073
|
-
const pageObject =
|
|
2403
|
+
if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
|
|
2404
|
+
const pageObject = strName5(page.object);
|
|
2074
2405
|
const visit = (node, path, inheritedObject) => {
|
|
2075
|
-
if (!
|
|
2076
|
-
const props =
|
|
2077
|
-
const dataSource =
|
|
2078
|
-
const objectName =
|
|
2406
|
+
if (!isRec5(node)) return;
|
|
2407
|
+
const props = isRec5(node.properties) ? node.properties : void 0;
|
|
2408
|
+
const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
|
|
2409
|
+
const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
|
|
2079
2410
|
out.push({ component: node, path, objectName });
|
|
2080
2411
|
if (!props) return;
|
|
2081
2412
|
if (Array.isArray(props.items)) {
|
|
2082
2413
|
for (let i = 0; i < props.items.length; i++) {
|
|
2083
2414
|
const item = props.items[i];
|
|
2084
|
-
if (!
|
|
2415
|
+
if (!isRec5(item) || !Array.isArray(item.children)) continue;
|
|
2085
2416
|
for (let c = 0; c < item.children.length; c++) {
|
|
2086
2417
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
2087
2418
|
}
|
|
@@ -2103,12 +2434,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
2103
2434
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
2104
2435
|
for (let r = 0; r < regions.length; r++) {
|
|
2105
2436
|
const region = regions[r];
|
|
2106
|
-
if (!
|
|
2437
|
+
if (!isRec5(region) || !Array.isArray(region.components)) continue;
|
|
2107
2438
|
for (let c = 0; c < region.components.length; c++) {
|
|
2108
2439
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
2109
2440
|
}
|
|
2110
2441
|
}
|
|
2111
|
-
const slots =
|
|
2442
|
+
const slots = isRec5(page.slots) ? page.slots : void 0;
|
|
2112
2443
|
if (slots) {
|
|
2113
2444
|
for (const [slot, value] of Object.entries(slots)) {
|
|
2114
2445
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2130,13 +2461,13 @@ function asArray8(v) {
|
|
|
2130
2461
|
}
|
|
2131
2462
|
return [];
|
|
2132
2463
|
}
|
|
2133
|
-
function
|
|
2464
|
+
function strName6(v) {
|
|
2134
2465
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2135
2466
|
}
|
|
2136
2467
|
function strList(v) {
|
|
2137
2468
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2138
2469
|
}
|
|
2139
|
-
function
|
|
2470
|
+
function distance4(a, b) {
|
|
2140
2471
|
const m = a.length;
|
|
2141
2472
|
const n = b.length;
|
|
2142
2473
|
if (m === 0) return n;
|
|
@@ -2152,11 +2483,11 @@ function distance3(a, b) {
|
|
|
2152
2483
|
}
|
|
2153
2484
|
return prev[n];
|
|
2154
2485
|
}
|
|
2155
|
-
function
|
|
2486
|
+
function suggest5(target, known) {
|
|
2156
2487
|
let best;
|
|
2157
2488
|
let bestScore = Infinity;
|
|
2158
2489
|
for (const candidate of known) {
|
|
2159
|
-
const d =
|
|
2490
|
+
const d = distance4(target, candidate);
|
|
2160
2491
|
if (d < bestScore) {
|
|
2161
2492
|
bestScore = d;
|
|
2162
2493
|
best = candidate;
|
|
@@ -2168,13 +2499,13 @@ function suggest4(target, known) {
|
|
|
2168
2499
|
function collectActionNames(stack) {
|
|
2169
2500
|
const names = /* @__PURE__ */ new Set();
|
|
2170
2501
|
for (const action of asArray8(stack.actions)) {
|
|
2171
|
-
const n =
|
|
2502
|
+
const n = strName6(action?.name);
|
|
2172
2503
|
if (n) names.add(n);
|
|
2173
2504
|
}
|
|
2174
2505
|
for (const obj of asArray8(stack.objects)) {
|
|
2175
2506
|
if (!obj || typeof obj !== "object") continue;
|
|
2176
2507
|
for (const action of asArray8(obj.actions)) {
|
|
2177
|
-
const n =
|
|
2508
|
+
const n = strName6(action?.name);
|
|
2178
2509
|
if (n) names.add(n);
|
|
2179
2510
|
}
|
|
2180
2511
|
}
|
|
@@ -2191,7 +2522,7 @@ function validateActionNameRefs(stack) {
|
|
|
2191
2522
|
rule: ACTION_NAME_UNDEFINED,
|
|
2192
2523
|
where,
|
|
2193
2524
|
path,
|
|
2194
|
-
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.` +
|
|
2525
|
+
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),
|
|
2195
2526
|
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(", ")}.` : "")
|
|
2196
2527
|
});
|
|
2197
2528
|
};
|
|
@@ -2217,7 +2548,7 @@ function validateActionNameRefs(stack) {
|
|
|
2217
2548
|
if (!def || typeof def !== "object") continue;
|
|
2218
2549
|
if (def.execution !== "aggregate") continue;
|
|
2219
2550
|
if (def.actionDef !== void 0) continue;
|
|
2220
|
-
const name =
|
|
2551
|
+
const name = strName6(def.name);
|
|
2221
2552
|
if (!name) continue;
|
|
2222
2553
|
check(
|
|
2223
2554
|
name,
|
|
@@ -2232,7 +2563,7 @@ function validateActionNameRefs(stack) {
|
|
|
2232
2563
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2233
2564
|
const view = views[vi];
|
|
2234
2565
|
if (!view || typeof view !== "object") continue;
|
|
2235
|
-
const viewName =
|
|
2566
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
|
|
2236
2567
|
const owner = `view "${viewName}"`;
|
|
2237
2568
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
2238
2569
|
const listViews = view.listViews;
|
|
@@ -2248,7 +2579,7 @@ function validateActionNameRefs(stack) {
|
|
|
2248
2579
|
if (!obj || typeof obj !== "object") continue;
|
|
2249
2580
|
const objListViews = obj.listViews;
|
|
2250
2581
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
2251
|
-
const owner = `object "${
|
|
2582
|
+
const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
|
|
2252
2583
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
2253
2584
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
2254
2585
|
}
|
|
@@ -2257,7 +2588,7 @@ function validateActionNameRefs(stack) {
|
|
|
2257
2588
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2258
2589
|
const page = pages[pi];
|
|
2259
2590
|
if (!page || typeof page !== "object") continue;
|
|
2260
|
-
const pageName =
|
|
2591
|
+
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2261
2592
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2262
2593
|
const props = component.properties;
|
|
2263
2594
|
if (!props || typeof props !== "object") continue;
|
|
@@ -2265,7 +2596,7 @@ function validateActionNameRefs(stack) {
|
|
|
2265
2596
|
for (let ai = 0; ai < names.length; ai++) {
|
|
2266
2597
|
check(
|
|
2267
2598
|
names[ai],
|
|
2268
|
-
`page "${pageName}" \xB7 component "${
|
|
2599
|
+
`page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
|
|
2269
2600
|
`${path}.properties.actionNames[${ai}]`,
|
|
2270
2601
|
"Quick-actions bar"
|
|
2271
2602
|
);
|
|
@@ -2276,7 +2607,7 @@ function validateActionNameRefs(stack) {
|
|
|
2276
2607
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2277
2608
|
const app = apps[ai];
|
|
2278
2609
|
if (!app || typeof app !== "object") continue;
|
|
2279
|
-
const appName =
|
|
2610
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
2280
2611
|
const walkNav = (items, basePath) => {
|
|
2281
2612
|
const navItems = asArray8(items);
|
|
2282
2613
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -2284,20 +2615,20 @@ function validateActionNameRefs(stack) {
|
|
|
2284
2615
|
if (!nav || typeof nav !== "object") continue;
|
|
2285
2616
|
const navPath = `${basePath}[${ni}]`;
|
|
2286
2617
|
const actionDef = nav.actionDef;
|
|
2287
|
-
const actionName =
|
|
2618
|
+
const actionName = strName6(actionDef?.actionName);
|
|
2288
2619
|
if (nav.type === "action" && actionName) {
|
|
2289
2620
|
check(
|
|
2290
2621
|
actionName,
|
|
2291
|
-
`app "${appName}" \xB7 nav "${
|
|
2622
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2292
2623
|
`${navPath}.actionDef.actionName`,
|
|
2293
2624
|
"Navigation action item"
|
|
2294
2625
|
);
|
|
2295
2626
|
}
|
|
2296
|
-
const runAction =
|
|
2627
|
+
const runAction = strName6(nav.runAction);
|
|
2297
2628
|
if (nav.type === "object" && runAction) {
|
|
2298
2629
|
check(
|
|
2299
2630
|
runAction,
|
|
2300
|
-
`app "${appName}" \xB7 nav "${
|
|
2631
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2301
2632
|
`${navPath}.runAction`,
|
|
2302
2633
|
"Navigation deep-link auto-run"
|
|
2303
2634
|
);
|
|
@@ -2324,23 +2655,23 @@ function asArray9(v) {
|
|
|
2324
2655
|
}
|
|
2325
2656
|
return [];
|
|
2326
2657
|
}
|
|
2327
|
-
function
|
|
2658
|
+
function strName7(v) {
|
|
2328
2659
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2329
2660
|
}
|
|
2330
|
-
function
|
|
2661
|
+
function isRec6(v) {
|
|
2331
2662
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2332
2663
|
}
|
|
2333
2664
|
function fieldRefsFrom(value, basePath) {
|
|
2334
2665
|
const out = [];
|
|
2335
2666
|
const one = (v, path) => {
|
|
2336
|
-
const bare =
|
|
2667
|
+
const bare = strName7(v);
|
|
2337
2668
|
if (bare) {
|
|
2338
2669
|
out.push({ name: bare, path });
|
|
2339
2670
|
return;
|
|
2340
2671
|
}
|
|
2341
|
-
if (!
|
|
2342
|
-
const named =
|
|
2343
|
-
if (named) out.push({ name: named, path: `${path}.${
|
|
2672
|
+
if (!isRec6(v)) return;
|
|
2673
|
+
const named = strName7(v.field) ?? strName7(v.name);
|
|
2674
|
+
if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
|
|
2344
2675
|
};
|
|
2345
2676
|
if (Array.isArray(value)) {
|
|
2346
2677
|
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
@@ -2366,7 +2697,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2366
2697
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2367
2698
|
"record:path": { props: ["statusField"] },
|
|
2368
2699
|
"element:number": { props: ["field"] },
|
|
2369
|
-
|
|
2700
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
2701
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
2702
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
2703
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
2704
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
2705
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
2370
2706
|
"element:form": { props: ["fields"] },
|
|
2371
2707
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2372
2708
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -2396,18 +2732,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2396
2732
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2397
2733
|
for (let si = 0; si < sections.length; si++) {
|
|
2398
2734
|
const section = sections[si];
|
|
2399
|
-
if (!
|
|
2735
|
+
if (!isRec6(section)) continue;
|
|
2400
2736
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2401
2737
|
}
|
|
2402
2738
|
}
|
|
2403
2739
|
return refs;
|
|
2404
2740
|
}
|
|
2405
2741
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2406
|
-
const add =
|
|
2407
|
-
const picker = add &&
|
|
2742
|
+
const add = isRec6(props.add) ? props.add : void 0;
|
|
2743
|
+
const picker = add && isRec6(add.picker) ? add.picker : void 0;
|
|
2408
2744
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2409
2745
|
return {
|
|
2410
|
-
relatedObject:
|
|
2746
|
+
relatedObject: strName7(props.objectName),
|
|
2411
2747
|
related: [
|
|
2412
2748
|
...fieldRefsFrom(props.columns, at("columns")),
|
|
2413
2749
|
...sortFieldRefs(props.sort, at("sort")),
|
|
@@ -2416,7 +2752,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2416
2752
|
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
2417
2753
|
],
|
|
2418
2754
|
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
2419
|
-
pickerObject: picker ?
|
|
2755
|
+
pickerObject: picker ? strName7(picker.object) : void 0,
|
|
2420
2756
|
picker: picker ? [
|
|
2421
2757
|
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
2422
2758
|
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
@@ -2425,13 +2761,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2425
2761
|
}
|
|
2426
2762
|
function indexObjectFields(stack) {
|
|
2427
2763
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2428
|
-
if (!
|
|
2764
|
+
if (!isRec6(stack)) return objectFields;
|
|
2429
2765
|
for (const obj of asArray9(stack.objects)) {
|
|
2430
|
-
const name =
|
|
2766
|
+
const name = strName7(obj.name);
|
|
2431
2767
|
if (!name) continue;
|
|
2432
2768
|
const names = /* @__PURE__ */ new Set();
|
|
2433
2769
|
for (const f of asArray9(obj.fields)) {
|
|
2434
|
-
const fn =
|
|
2770
|
+
const fn = strName7(f.name);
|
|
2435
2771
|
if (fn) names.add(fn);
|
|
2436
2772
|
}
|
|
2437
2773
|
objectFields.set(name, names);
|
|
@@ -2479,15 +2815,15 @@ function validatePageFieldBindings(stack) {
|
|
|
2479
2815
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2480
2816
|
const page = pages[pi];
|
|
2481
2817
|
if (!page || typeof page !== "object") continue;
|
|
2482
|
-
const pageName =
|
|
2818
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2483
2819
|
const checkRefs = (refs, objectName, where) => {
|
|
2484
2820
|
findings.push(
|
|
2485
2821
|
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2486
2822
|
);
|
|
2487
2823
|
};
|
|
2488
2824
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2489
|
-
const type =
|
|
2490
|
-
const props =
|
|
2825
|
+
const type = strName7(component.type);
|
|
2826
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2491
2827
|
if (!type || !props) continue;
|
|
2492
2828
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2493
2829
|
const base = `${path}.properties`;
|
|
@@ -2502,16 +2838,16 @@ function validatePageFieldBindings(stack) {
|
|
|
2502
2838
|
if (!refs) continue;
|
|
2503
2839
|
checkRefs(refs, objectName, where);
|
|
2504
2840
|
}
|
|
2505
|
-
const cfg =
|
|
2841
|
+
const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2506
2842
|
if (cfg) {
|
|
2507
|
-
const cfgObject =
|
|
2843
|
+
const cfgObject = strName7(cfg.source) ?? strName7(page.object);
|
|
2508
2844
|
const base = `pages[${pi}].interfaceConfig`;
|
|
2509
2845
|
const refs = [
|
|
2510
2846
|
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
2511
2847
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2512
2848
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2513
2849
|
];
|
|
2514
|
-
const userFilters =
|
|
2850
|
+
const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2515
2851
|
if (userFilters) {
|
|
2516
2852
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2517
2853
|
}
|
|
@@ -2533,16 +2869,16 @@ function asArray10(v) {
|
|
|
2533
2869
|
}
|
|
2534
2870
|
return [];
|
|
2535
2871
|
}
|
|
2536
|
-
function
|
|
2872
|
+
function strName8(v) {
|
|
2537
2873
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2538
2874
|
}
|
|
2539
2875
|
function strList2(v) {
|
|
2540
2876
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2541
2877
|
}
|
|
2542
|
-
function
|
|
2878
|
+
function isRec7(v) {
|
|
2543
2879
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2544
2880
|
}
|
|
2545
|
-
function
|
|
2881
|
+
function distance5(a, b) {
|
|
2546
2882
|
const m = a.length;
|
|
2547
2883
|
const n = b.length;
|
|
2548
2884
|
if (m === 0) return n;
|
|
@@ -2558,11 +2894,11 @@ function distance4(a, b) {
|
|
|
2558
2894
|
}
|
|
2559
2895
|
return prev[n];
|
|
2560
2896
|
}
|
|
2561
|
-
function
|
|
2897
|
+
function suggest6(target, known) {
|
|
2562
2898
|
let best;
|
|
2563
2899
|
let bestScore = Infinity;
|
|
2564
2900
|
for (const c of known) {
|
|
2565
|
-
const d =
|
|
2901
|
+
const d = distance5(target, c);
|
|
2566
2902
|
if (d < bestScore) {
|
|
2567
2903
|
bestScore = d;
|
|
2568
2904
|
best = c;
|
|
@@ -2578,16 +2914,16 @@ function list2(names) {
|
|
|
2578
2914
|
function indexDatasets(stack) {
|
|
2579
2915
|
const out = /* @__PURE__ */ new Map();
|
|
2580
2916
|
for (const ds of asArray10(stack.datasets)) {
|
|
2581
|
-
const name =
|
|
2917
|
+
const name = strName8(ds.name);
|
|
2582
2918
|
if (!name) continue;
|
|
2583
2919
|
const dimensions = /* @__PURE__ */ new Set();
|
|
2584
2920
|
for (const d of asArray10(ds.dimensions)) {
|
|
2585
|
-
const n =
|
|
2921
|
+
const n = strName8(d.name);
|
|
2586
2922
|
if (n) dimensions.add(n);
|
|
2587
2923
|
}
|
|
2588
2924
|
const measures = /* @__PURE__ */ new Set();
|
|
2589
2925
|
for (const m of asArray10(ds.measures)) {
|
|
2590
|
-
const n =
|
|
2926
|
+
const n = strName8(m.name);
|
|
2591
2927
|
if (n) measures.add(n);
|
|
2592
2928
|
}
|
|
2593
2929
|
out.set(name, { dimensions, measures });
|
|
@@ -2610,7 +2946,7 @@ function validateChartBindings(stack) {
|
|
|
2610
2946
|
where: binding.where,
|
|
2611
2947
|
path: `${binding.path}.dataset`,
|
|
2612
2948
|
message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
|
|
2613
|
-
hint: `Declared datasets: ${list2(datasets.keys())}.${
|
|
2949
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
2614
2950
|
});
|
|
2615
2951
|
return;
|
|
2616
2952
|
}
|
|
@@ -2622,7 +2958,7 @@ function validateChartBindings(stack) {
|
|
|
2622
2958
|
where: binding.where,
|
|
2623
2959
|
path,
|
|
2624
2960
|
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.`,
|
|
2625
|
-
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${
|
|
2961
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
2626
2962
|
});
|
|
2627
2963
|
};
|
|
2628
2964
|
const measureRef = (name, path, selected2) => {
|
|
@@ -2633,7 +2969,7 @@ function validateChartBindings(stack) {
|
|
|
2633
2969
|
where: binding.where,
|
|
2634
2970
|
path,
|
|
2635
2971
|
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.`,
|
|
2636
|
-
hint: `Dataset measures: ${list2(ds.measures)}.${
|
|
2972
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
2637
2973
|
});
|
|
2638
2974
|
return;
|
|
2639
2975
|
}
|
|
@@ -2668,26 +3004,26 @@ function validateChartBindings(stack) {
|
|
|
2668
3004
|
const reports = asArray10(stack.reports);
|
|
2669
3005
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2670
3006
|
const report = reports[ri];
|
|
2671
|
-
if (!
|
|
2672
|
-
const reportName =
|
|
3007
|
+
if (!isRec7(report)) continue;
|
|
3008
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
2673
3009
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2674
|
-
if (!
|
|
3010
|
+
if (!isRec7(chart)) return;
|
|
2675
3011
|
check({
|
|
2676
3012
|
dataset,
|
|
2677
3013
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
2678
3014
|
// it in lets the yAxis "declared but not selected" check work without
|
|
2679
3015
|
// reporting the selection itself twice.
|
|
2680
3016
|
values: { names: values, path: `${path}.values` },
|
|
2681
|
-
xAxis:
|
|
2682
|
-
yAxis:
|
|
2683
|
-
series: asArray10(chart.series).map((s, si) => ({ name:
|
|
3017
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
3018
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
3019
|
+
series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
2684
3020
|
where,
|
|
2685
3021
|
path: `${path}.chart`
|
|
2686
3022
|
});
|
|
2687
3023
|
};
|
|
2688
3024
|
checkReportChart(
|
|
2689
3025
|
report.chart,
|
|
2690
|
-
|
|
3026
|
+
strName8(report.dataset),
|
|
2691
3027
|
strList2(report.values),
|
|
2692
3028
|
`report "${reportName}" \xB7 chart`,
|
|
2693
3029
|
`reports[${ri}]`
|
|
@@ -2695,22 +3031,22 @@ function validateChartBindings(stack) {
|
|
|
2695
3031
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2696
3032
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2697
3033
|
const block = blocks[bi];
|
|
2698
|
-
if (!
|
|
3034
|
+
if (!isRec7(block)) continue;
|
|
2699
3035
|
checkReportChart(
|
|
2700
3036
|
block.chart,
|
|
2701
|
-
|
|
3037
|
+
strName8(block.dataset),
|
|
2702
3038
|
strList2(block.values),
|
|
2703
|
-
`report "${reportName}" \xB7 block "${
|
|
3039
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
2704
3040
|
`reports[${ri}].blocks[${bi}]`
|
|
2705
3041
|
);
|
|
2706
3042
|
}
|
|
2707
3043
|
}
|
|
2708
3044
|
const checkListChart = (container, where, path) => {
|
|
2709
|
-
if (!
|
|
3045
|
+
if (!isRec7(container)) return;
|
|
2710
3046
|
const chart = container.chart;
|
|
2711
|
-
if (!
|
|
3047
|
+
if (!isRec7(chart)) return;
|
|
2712
3048
|
check({
|
|
2713
|
-
dataset:
|
|
3049
|
+
dataset: strName8(chart.dataset),
|
|
2714
3050
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
2715
3051
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
2716
3052
|
where,
|
|
@@ -2720,10 +3056,10 @@ function validateChartBindings(stack) {
|
|
|
2720
3056
|
const views = asArray10(stack.views);
|
|
2721
3057
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2722
3058
|
const view = views[vi];
|
|
2723
|
-
if (!
|
|
2724
|
-
const viewName =
|
|
3059
|
+
if (!isRec7(view)) continue;
|
|
3060
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
2725
3061
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2726
|
-
if (
|
|
3062
|
+
if (isRec7(view.listViews)) {
|
|
2727
3063
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2728
3064
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2729
3065
|
}
|
|
@@ -2732,8 +3068,8 @@ function validateChartBindings(stack) {
|
|
|
2732
3068
|
const objects = asArray10(stack.objects);
|
|
2733
3069
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2734
3070
|
const obj = objects[oi];
|
|
2735
|
-
if (!
|
|
2736
|
-
const objName =
|
|
3071
|
+
if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
|
|
3072
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
2737
3073
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2738
3074
|
checkListChart(
|
|
2739
3075
|
lv,
|
|
@@ -2745,19 +3081,19 @@ function validateChartBindings(stack) {
|
|
|
2745
3081
|
const pages = asArray10(stack.pages);
|
|
2746
3082
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2747
3083
|
const page = pages[pi];
|
|
2748
|
-
if (!
|
|
2749
|
-
const pageName =
|
|
3084
|
+
if (!isRec7(page)) continue;
|
|
3085
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
2750
3086
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2751
|
-
const props =
|
|
2752
|
-
if (!props || !
|
|
2753
|
-
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name:
|
|
2754
|
-
const seriesRefs = asArray10(props.series).map((s, si) => ({ name:
|
|
3087
|
+
const props = isRec7(component.properties) ? component.properties : void 0;
|
|
3088
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
3089
|
+
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
3090
|
+
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
2755
3091
|
check({
|
|
2756
|
-
dataset:
|
|
3092
|
+
dataset: strName8(props.dataset),
|
|
2757
3093
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
2758
3094
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
2759
3095
|
series: [...axisRefs, ...seriesRefs],
|
|
2760
|
-
where: `page "${pageName}" \xB7 ${
|
|
3096
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
2761
3097
|
path: `${path}.properties`
|
|
2762
3098
|
});
|
|
2763
3099
|
}
|
|
@@ -2824,7 +3160,7 @@ function asArray12(v) {
|
|
|
2824
3160
|
}
|
|
2825
3161
|
return [];
|
|
2826
3162
|
}
|
|
2827
|
-
function
|
|
3163
|
+
function strName9(v) {
|
|
2828
3164
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2829
3165
|
}
|
|
2830
3166
|
function collectNavExposures(stack) {
|
|
@@ -2833,18 +3169,18 @@ function collectNavExposures(stack) {
|
|
|
2833
3169
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2834
3170
|
const app = apps[ai];
|
|
2835
3171
|
if (!app || typeof app !== "object") continue;
|
|
2836
|
-
const appName =
|
|
3172
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
2837
3173
|
const walk = (items, basePath) => {
|
|
2838
3174
|
const navItems = asArray12(items);
|
|
2839
3175
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
2840
3176
|
const nav = navItems[ni];
|
|
2841
3177
|
if (!nav || typeof nav !== "object") continue;
|
|
2842
3178
|
const navPath = `${basePath}[${ni}]`;
|
|
2843
|
-
const objectName =
|
|
3179
|
+
const objectName = strName9(nav.objectName);
|
|
2844
3180
|
if (nav.type === "object" && objectName) {
|
|
2845
3181
|
out.push({
|
|
2846
3182
|
objectName,
|
|
2847
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3183
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
2848
3184
|
path: `${navPath}.objectName`
|
|
2849
3185
|
});
|
|
2850
3186
|
}
|
|
@@ -2868,7 +3204,7 @@ function validateNavAccess(stack) {
|
|
|
2868
3204
|
if (exposures.length === 0) return findings;
|
|
2869
3205
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
2870
3206
|
for (const obj of asArray12(stack.objects)) {
|
|
2871
|
-
const n =
|
|
3207
|
+
const n = strName9(obj.name);
|
|
2872
3208
|
if (n) ownObjects.add(n);
|
|
2873
3209
|
}
|
|
2874
3210
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -2898,13 +3234,13 @@ function validateNavAccess(stack) {
|
|
|
2898
3234
|
|
|
2899
3235
|
// src/validate-nav-target-refs.ts
|
|
2900
3236
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2901
|
-
var
|
|
3237
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2902
3238
|
function asArray13(v) {
|
|
2903
|
-
if (Array.isArray(v)) return v.filter(
|
|
2904
|
-
if (
|
|
3239
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
3240
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2905
3241
|
return [];
|
|
2906
3242
|
}
|
|
2907
|
-
function
|
|
3243
|
+
function strName10(v) {
|
|
2908
3244
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2909
3245
|
}
|
|
2910
3246
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -2916,14 +3252,14 @@ var NAV_TARGETS = [
|
|
|
2916
3252
|
function namesOf(collection) {
|
|
2917
3253
|
const out = /* @__PURE__ */ new Set();
|
|
2918
3254
|
for (const entry of asArray13(collection)) {
|
|
2919
|
-
const n =
|
|
3255
|
+
const n = strName10(entry.name);
|
|
2920
3256
|
if (n) out.add(n);
|
|
2921
3257
|
}
|
|
2922
3258
|
return out;
|
|
2923
3259
|
}
|
|
2924
3260
|
function validateNavTargetRefs(stack) {
|
|
2925
3261
|
const findings = [];
|
|
2926
|
-
if (!
|
|
3262
|
+
if (!isRec8(stack)) return findings;
|
|
2927
3263
|
const apps = asArray13(stack.apps);
|
|
2928
3264
|
if (apps.length === 0) return findings;
|
|
2929
3265
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2931,16 +3267,16 @@ function validateNavTargetRefs(stack) {
|
|
|
2931
3267
|
declared.set(collection, namesOf(stack[collection]));
|
|
2932
3268
|
}
|
|
2933
3269
|
for (const [ai, app] of apps.entries()) {
|
|
2934
|
-
const appName =
|
|
3270
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2935
3271
|
const walk = (items, basePath) => {
|
|
2936
3272
|
if (!Array.isArray(items)) return;
|
|
2937
3273
|
for (const [ni, raw] of items.entries()) {
|
|
2938
|
-
if (!
|
|
3274
|
+
if (!isRec8(raw)) continue;
|
|
2939
3275
|
const nav = raw;
|
|
2940
3276
|
const navPath = `${basePath}[${ni}]`;
|
|
2941
3277
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
2942
3278
|
if (nav.type !== type) continue;
|
|
2943
|
-
const target =
|
|
3279
|
+
const target = strName10(nav[prop]);
|
|
2944
3280
|
if (!target || isInterpolated2(target)) continue;
|
|
2945
3281
|
const known = declared.get(collection);
|
|
2946
3282
|
if (known.has(target)) continue;
|
|
@@ -2948,7 +3284,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2948
3284
|
findings.push({
|
|
2949
3285
|
severity: "warning",
|
|
2950
3286
|
rule: NAV_TARGET_UNRESOLVED,
|
|
2951
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3287
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
2952
3288
|
path: `${navPath}.${prop}`,
|
|
2953
3289
|
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.`,
|
|
2954
3290
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -2969,42 +3305,42 @@ function validateNavTargetRefs(stack) {
|
|
|
2969
3305
|
// src/validate-nav-object-servability.ts
|
|
2970
3306
|
import { canServeApiOperation } from "@objectstack/spec/data";
|
|
2971
3307
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2972
|
-
var
|
|
3308
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2973
3309
|
function asArray14(v) {
|
|
2974
|
-
if (Array.isArray(v)) return v.filter(
|
|
2975
|
-
if (
|
|
3310
|
+
if (Array.isArray(v)) return v.filter(isRec9);
|
|
3311
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
|
|
2976
3312
|
return [];
|
|
2977
3313
|
}
|
|
2978
|
-
function
|
|
3314
|
+
function strName11(v) {
|
|
2979
3315
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2980
3316
|
}
|
|
2981
3317
|
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2982
3318
|
function validateNavObjectServability(stack) {
|
|
2983
3319
|
const findings = [];
|
|
2984
|
-
if (!
|
|
3320
|
+
if (!isRec9(stack)) return findings;
|
|
2985
3321
|
const apps = asArray14(stack.apps);
|
|
2986
3322
|
if (apps.length === 0) return findings;
|
|
2987
3323
|
const ownEnable = /* @__PURE__ */ new Map();
|
|
2988
3324
|
const objects = asArray14(stack.objects);
|
|
2989
3325
|
for (const [oi, obj] of objects.entries()) {
|
|
2990
|
-
const n =
|
|
3326
|
+
const n = strName11(obj.name);
|
|
2991
3327
|
if (!n) continue;
|
|
2992
3328
|
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
2993
3329
|
}
|
|
2994
3330
|
if (ownEnable.size === 0) return findings;
|
|
2995
3331
|
for (const [ai, app] of apps.entries()) {
|
|
2996
|
-
const appName =
|
|
3332
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
2997
3333
|
const walk = (items, basePath) => {
|
|
2998
3334
|
if (!Array.isArray(items)) return;
|
|
2999
3335
|
for (const [ni, raw] of items.entries()) {
|
|
3000
|
-
if (!
|
|
3336
|
+
if (!isRec9(raw)) continue;
|
|
3001
3337
|
const nav = raw;
|
|
3002
3338
|
const navPath = `${basePath}[${ni}]`;
|
|
3003
3339
|
if (nav.type === "object") {
|
|
3004
|
-
const target =
|
|
3340
|
+
const target = strName11(nav.objectName);
|
|
3005
3341
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3006
3342
|
if (target && declared && !canServeApiOperation(declared.enable, "list")) {
|
|
3007
|
-
const enable =
|
|
3343
|
+
const enable = isRec9(declared.enable) ? declared.enable : {};
|
|
3008
3344
|
const apiDisabled = enable.apiEnabled === false;
|
|
3009
3345
|
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(", ")})` : "");
|
|
3010
3346
|
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
@@ -3012,7 +3348,7 @@ function validateNavObjectServability(stack) {
|
|
|
3012
3348
|
findings.push({
|
|
3013
3349
|
severity: "error",
|
|
3014
3350
|
rule: NAV_OBJECT_UNSERVABLE,
|
|
3015
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3351
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
|
|
3016
3352
|
// The nav entry is where the dead row is authored; the `enable`
|
|
3017
3353
|
// key that condemns it is named in the message, because the fix
|
|
3018
3354
|
// may belong at either end.
|
|
@@ -3039,27 +3375,27 @@ import { expandViewContainer } from "@objectstack/spec";
|
|
|
3039
3375
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
3040
3376
|
|
|
3041
3377
|
// src/view-walk.ts
|
|
3042
|
-
function
|
|
3378
|
+
function isRec10(v) {
|
|
3043
3379
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3044
3380
|
}
|
|
3045
|
-
function
|
|
3381
|
+
function strName12(v) {
|
|
3046
3382
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3047
3383
|
}
|
|
3048
3384
|
function viewObjectName(view) {
|
|
3049
|
-
return
|
|
3385
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
|
|
3050
3386
|
}
|
|
3051
3387
|
function viewContainerSites(view, basePath) {
|
|
3052
|
-
if (!
|
|
3388
|
+
if (!isRec10(view)) return [];
|
|
3053
3389
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3054
|
-
if (
|
|
3390
|
+
if (isRec10(view.form)) {
|
|
3055
3391
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3056
3392
|
}
|
|
3057
3393
|
for (const key of ["listViews", "formViews"]) {
|
|
3058
3394
|
const container = view[key];
|
|
3059
|
-
if (!
|
|
3395
|
+
if (!isRec10(container)) continue;
|
|
3060
3396
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
3061
3397
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
3062
|
-
if (!
|
|
3398
|
+
if (!isRec10(sub)) continue;
|
|
3063
3399
|
sites.push({
|
|
3064
3400
|
view: sub,
|
|
3065
3401
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -3077,18 +3413,18 @@ function formViewSites(view, basePath) {
|
|
|
3077
3413
|
// src/validate-translation-references.ts
|
|
3078
3414
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3079
3415
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3080
|
-
function
|
|
3416
|
+
function isRec11(v) {
|
|
3081
3417
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3082
3418
|
}
|
|
3083
3419
|
function asArray15(v) {
|
|
3084
3420
|
if (Array.isArray(v)) return v;
|
|
3085
|
-
if (
|
|
3421
|
+
if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
|
|
3086
3422
|
return [];
|
|
3087
3423
|
}
|
|
3088
|
-
function
|
|
3424
|
+
function strName13(v) {
|
|
3089
3425
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3090
3426
|
}
|
|
3091
|
-
function
|
|
3427
|
+
function distance6(a, b) {
|
|
3092
3428
|
const m = a.length;
|
|
3093
3429
|
const n = b.length;
|
|
3094
3430
|
if (m === 0) return n;
|
|
@@ -3104,7 +3440,7 @@ function distance5(a, b) {
|
|
|
3104
3440
|
}
|
|
3105
3441
|
return prev[n];
|
|
3106
3442
|
}
|
|
3107
|
-
function
|
|
3443
|
+
function suggest7(target, known) {
|
|
3108
3444
|
const names = [...known];
|
|
3109
3445
|
const segmentMatch = names.find(
|
|
3110
3446
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -3113,7 +3449,7 @@ function suggest6(target, known) {
|
|
|
3113
3449
|
let best;
|
|
3114
3450
|
let bestScore = Infinity;
|
|
3115
3451
|
for (const candidate of names) {
|
|
3116
|
-
const d =
|
|
3452
|
+
const d = distance6(target, candidate);
|
|
3117
3453
|
if (d < bestScore) {
|
|
3118
3454
|
bestScore = d;
|
|
3119
3455
|
best = candidate;
|
|
@@ -3146,33 +3482,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
3146
3482
|
const addSections = (container, binding) => {
|
|
3147
3483
|
if (!binding) return;
|
|
3148
3484
|
for (const section of asArray15(container.sections)) {
|
|
3149
|
-
const sectionName =
|
|
3485
|
+
const sectionName = strName13(section.name);
|
|
3150
3486
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
3151
3487
|
}
|
|
3152
3488
|
};
|
|
3153
|
-
const listBinding =
|
|
3154
|
-
if (
|
|
3155
|
-
addView(recordObject ?? listBinding,
|
|
3489
|
+
const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
|
|
3490
|
+
if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3491
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
3156
3492
|
const named = namedViewKeys(view);
|
|
3157
3493
|
for (const family of ["listViews", "formViews"]) {
|
|
3158
3494
|
const container = view[family];
|
|
3159
|
-
if (!
|
|
3495
|
+
if (!isRec11(container)) continue;
|
|
3160
3496
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3161
3497
|
let at = 0;
|
|
3162
3498
|
for (const sub of Object.values(container)) {
|
|
3163
3499
|
if (!sub || typeof sub !== "object") continue;
|
|
3164
3500
|
const registryKey = registryKeys[at++];
|
|
3165
|
-
if (!
|
|
3501
|
+
if (!isRec11(sub)) continue;
|
|
3166
3502
|
const binding = bindingOf(sub) ?? listBinding;
|
|
3167
3503
|
addView(binding, registryKey);
|
|
3168
3504
|
addSections(sub, binding);
|
|
3169
3505
|
}
|
|
3170
3506
|
}
|
|
3171
|
-
if (
|
|
3507
|
+
if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
3172
3508
|
addSections(view, recordObject ?? listBinding);
|
|
3173
3509
|
}
|
|
3174
3510
|
function defaultListViewKey(object, container) {
|
|
3175
|
-
if (!object || !
|
|
3511
|
+
if (!object || !isRec11(container.list)) return void 0;
|
|
3176
3512
|
const item = expandViewContainer(object, container).find(
|
|
3177
3513
|
(i) => i.viewKind === "list" && i.isDefault
|
|
3178
3514
|
);
|
|
@@ -3184,7 +3520,7 @@ function namedViewKeys(container) {
|
|
|
3184
3520
|
const object = "probe";
|
|
3185
3521
|
const prefix = `${object}.`;
|
|
3186
3522
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3187
|
-
const countEntries = (v) =>
|
|
3523
|
+
const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3188
3524
|
const listCount = countEntries(container.listViews);
|
|
3189
3525
|
const formCount = countEntries(container.formViews);
|
|
3190
3526
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -3202,14 +3538,14 @@ function readOptions(field) {
|
|
|
3202
3538
|
values.add(opt);
|
|
3203
3539
|
continue;
|
|
3204
3540
|
}
|
|
3205
|
-
if (!
|
|
3206
|
-
const value =
|
|
3541
|
+
if (!isRec11(opt)) continue;
|
|
3542
|
+
const value = strName13(opt.value);
|
|
3207
3543
|
if (!value) continue;
|
|
3208
3544
|
values.add(value);
|
|
3209
|
-
const label2 =
|
|
3545
|
+
const label2 = strName13(opt.label);
|
|
3210
3546
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
3211
3547
|
}
|
|
3212
|
-
} else if (
|
|
3548
|
+
} else if (isRec11(raw)) {
|
|
3213
3549
|
for (const [value, label2] of Object.entries(raw)) {
|
|
3214
3550
|
values.add(value);
|
|
3215
3551
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -3230,23 +3566,23 @@ function buildUniverse(stack) {
|
|
|
3230
3566
|
return facts;
|
|
3231
3567
|
};
|
|
3232
3568
|
for (const obj of asArray15(stack.objects)) {
|
|
3233
|
-
const objectName =
|
|
3569
|
+
const objectName = strName13(obj.name);
|
|
3234
3570
|
if (!objectName) continue;
|
|
3235
3571
|
const facts = factsFor(objectName);
|
|
3236
3572
|
for (const field of asArray15(obj.fields)) {
|
|
3237
|
-
const fieldName =
|
|
3573
|
+
const fieldName = strName13(field.name);
|
|
3238
3574
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
3239
3575
|
}
|
|
3240
3576
|
for (const action of asArray15(obj.actions)) {
|
|
3241
|
-
const actionName =
|
|
3577
|
+
const actionName = strName13(action.name);
|
|
3242
3578
|
if (actionName) facts.actions.set(actionName, action);
|
|
3243
3579
|
}
|
|
3244
3580
|
for (const view of asArray15(obj.views)) {
|
|
3245
|
-
collectViewRecord({ ...view, object:
|
|
3581
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
3246
3582
|
}
|
|
3247
3583
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3248
3584
|
for (const group of asArray15(obj.fieldGroups)) {
|
|
3249
|
-
const key =
|
|
3585
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
3250
3586
|
if (key) facts.sections.add(key);
|
|
3251
3587
|
}
|
|
3252
3588
|
}
|
|
@@ -3257,10 +3593,10 @@ function buildUniverse(stack) {
|
|
|
3257
3593
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3258
3594
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3259
3595
|
if (!walked.objectName) continue;
|
|
3260
|
-
const props =
|
|
3596
|
+
const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
|
|
3261
3597
|
if (!props) continue;
|
|
3262
3598
|
for (const section of asArray15(props.sections)) {
|
|
3263
|
-
const sectionName =
|
|
3599
|
+
const sectionName = strName13(section.name);
|
|
3264
3600
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3265
3601
|
}
|
|
3266
3602
|
}
|
|
@@ -3268,9 +3604,9 @@ function buildUniverse(stack) {
|
|
|
3268
3604
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3269
3605
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3270
3606
|
for (const action of asArray15(stack.actions)) {
|
|
3271
|
-
const actionName =
|
|
3607
|
+
const actionName = strName13(action.name);
|
|
3272
3608
|
if (!actionName) continue;
|
|
3273
|
-
const owner =
|
|
3609
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
3274
3610
|
if (owner) {
|
|
3275
3611
|
factsFor(owner).actions.set(actionName, action);
|
|
3276
3612
|
actionOwners.set(actionName, owner);
|
|
@@ -3285,19 +3621,19 @@ function buildUniverse(stack) {
|
|
|
3285
3621
|
}
|
|
3286
3622
|
const apps = /* @__PURE__ */ new Map();
|
|
3287
3623
|
for (const app of asArray15(stack.apps)) {
|
|
3288
|
-
const appName =
|
|
3624
|
+
const appName = strName13(app.name);
|
|
3289
3625
|
if (!appName) continue;
|
|
3290
3626
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3291
3627
|
const walkNav = (items) => {
|
|
3292
3628
|
for (const item of asArray15(items)) {
|
|
3293
|
-
const id =
|
|
3629
|
+
const id = strName13(item.id);
|
|
3294
3630
|
if (id) navIds.add(id);
|
|
3295
3631
|
if (item.children) walkNav(item.children);
|
|
3296
3632
|
}
|
|
3297
3633
|
};
|
|
3298
3634
|
walkNav(app.navigation);
|
|
3299
3635
|
for (const area of asArray15(app.areas)) {
|
|
3300
|
-
const areaId =
|
|
3636
|
+
const areaId = strName13(area.id);
|
|
3301
3637
|
if (areaId) navIds.add(areaId);
|
|
3302
3638
|
walkNav(area.navigation);
|
|
3303
3639
|
}
|
|
@@ -3305,20 +3641,20 @@ function buildUniverse(stack) {
|
|
|
3305
3641
|
}
|
|
3306
3642
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3307
3643
|
for (const dash of asArray15(stack.dashboards)) {
|
|
3308
|
-
const dashName =
|
|
3644
|
+
const dashName = strName13(dash.name);
|
|
3309
3645
|
if (!dashName) continue;
|
|
3310
3646
|
const widgets = /* @__PURE__ */ new Set();
|
|
3311
3647
|
for (const widget of asArray15(dash.widgets)) {
|
|
3312
|
-
const id =
|
|
3648
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
3313
3649
|
if (id) widgets.add(id);
|
|
3314
3650
|
}
|
|
3315
3651
|
const actions = /* @__PURE__ */ new Set();
|
|
3316
3652
|
const headerActions = [
|
|
3317
|
-
...asArray15(
|
|
3653
|
+
...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
|
|
3318
3654
|
...asArray15(dash.actions)
|
|
3319
3655
|
];
|
|
3320
3656
|
for (const action of headerActions) {
|
|
3321
|
-
const key =
|
|
3657
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
3322
3658
|
if (key) actions.add(key);
|
|
3323
3659
|
}
|
|
3324
3660
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3330,7 +3666,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3330
3666
|
}
|
|
3331
3667
|
function validateTranslationReferences(stack) {
|
|
3332
3668
|
const findings = [];
|
|
3333
|
-
if (!
|
|
3669
|
+
if (!isRec11(stack)) return findings;
|
|
3334
3670
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3335
3671
|
if (bundles.length === 0) return findings;
|
|
3336
3672
|
const universe = buildUniverse(stack);
|
|
@@ -3339,13 +3675,13 @@ function validateTranslationReferences(stack) {
|
|
|
3339
3675
|
};
|
|
3340
3676
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3341
3677
|
const bundle = bundles[bi];
|
|
3342
|
-
if (!
|
|
3678
|
+
if (!isRec11(bundle)) continue;
|
|
3343
3679
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3344
|
-
if (!
|
|
3680
|
+
if (!isRec11(rawData)) continue;
|
|
3345
3681
|
const base = localePath(bi, locale);
|
|
3346
3682
|
const inLocale = `locale "${locale}"`;
|
|
3347
3683
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3348
|
-
if (!
|
|
3684
|
+
if (!isRec11(rawNode)) continue;
|
|
3349
3685
|
const objPath = `${base}.objects.${objectName}`;
|
|
3350
3686
|
const facts = universe.objects.get(objectName);
|
|
3351
3687
|
if (!facts) {
|
|
@@ -3353,7 +3689,7 @@ function validateTranslationReferences(stack) {
|
|
|
3353
3689
|
orphan(
|
|
3354
3690
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
3355
3691
|
objPath,
|
|
3356
|
-
hasPlatformObjectPrefix2(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.` +
|
|
3692
|
+
hasPlatformObjectPrefix2(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()),
|
|
3357
3693
|
`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())}.` : "")
|
|
3358
3694
|
);
|
|
3359
3695
|
continue;
|
|
@@ -3366,12 +3702,12 @@ function validateTranslationReferences(stack) {
|
|
|
3366
3702
|
orphan(
|
|
3367
3703
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
3368
3704
|
fieldPath,
|
|
3369
|
-
`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.` +
|
|
3705
|
+
`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()),
|
|
3370
3706
|
`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())}.` : "")
|
|
3371
3707
|
);
|
|
3372
3708
|
continue;
|
|
3373
3709
|
}
|
|
3374
|
-
if (!
|
|
3710
|
+
if (!isRec11(rawField)) continue;
|
|
3375
3711
|
checkOptionKeys(findings, {
|
|
3376
3712
|
optionMap: rawField.options,
|
|
3377
3713
|
field,
|
|
@@ -3386,7 +3722,7 @@ function validateTranslationReferences(stack) {
|
|
|
3386
3722
|
orphan(
|
|
3387
3723
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
3388
3724
|
`${objPath}._views.${viewName}`,
|
|
3389
|
-
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` +
|
|
3725
|
+
`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),
|
|
3390
3726
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
3391
3727
|
);
|
|
3392
3728
|
}
|
|
@@ -3395,7 +3731,7 @@ function validateTranslationReferences(stack) {
|
|
|
3395
3731
|
orphan(
|
|
3396
3732
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
3397
3733
|
`${objPath}._sections.${sectionName}`,
|
|
3398
|
-
`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.` +
|
|
3734
|
+
`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),
|
|
3399
3735
|
`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.`)
|
|
3400
3736
|
);
|
|
3401
3737
|
}
|
|
@@ -3406,7 +3742,7 @@ function validateTranslationReferences(stack) {
|
|
|
3406
3742
|
orphan(
|
|
3407
3743
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
3408
3744
|
actionPath,
|
|
3409
|
-
`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.` +
|
|
3745
|
+
`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()),
|
|
3410
3746
|
`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())}.` : "")
|
|
3411
3747
|
);
|
|
3412
3748
|
continue;
|
|
@@ -3428,7 +3764,7 @@ function validateTranslationReferences(stack) {
|
|
|
3428
3764
|
orphan(
|
|
3429
3765
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
3430
3766
|
actionPath,
|
|
3431
|
-
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.` +
|
|
3767
|
+
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()),
|
|
3432
3768
|
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())}.` : "")
|
|
3433
3769
|
);
|
|
3434
3770
|
continue;
|
|
@@ -3448,18 +3784,18 @@ function validateTranslationReferences(stack) {
|
|
|
3448
3784
|
orphan(
|
|
3449
3785
|
`${inLocale} \xB7 app "${appName}"`,
|
|
3450
3786
|
appPath,
|
|
3451
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
3787
|
+
`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()),
|
|
3452
3788
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
3453
3789
|
);
|
|
3454
3790
|
continue;
|
|
3455
3791
|
}
|
|
3456
|
-
if (!
|
|
3792
|
+
if (!isRec11(rawApp)) continue;
|
|
3457
3793
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3458
3794
|
if (navIds.has(navId)) continue;
|
|
3459
3795
|
orphan(
|
|
3460
3796
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
3461
3797
|
`${appPath}.navigation.${navId}`,
|
|
3462
|
-
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` +
|
|
3798
|
+
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
|
|
3463
3799
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
3464
3800
|
);
|
|
3465
3801
|
}
|
|
@@ -3471,18 +3807,18 @@ function validateTranslationReferences(stack) {
|
|
|
3471
3807
|
orphan(
|
|
3472
3808
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
3473
3809
|
dashPath,
|
|
3474
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
3810
|
+
`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()),
|
|
3475
3811
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
3476
3812
|
);
|
|
3477
3813
|
continue;
|
|
3478
3814
|
}
|
|
3479
|
-
if (!
|
|
3815
|
+
if (!isRec11(rawDash)) continue;
|
|
3480
3816
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3481
3817
|
if (dash.widgets.has(widgetId)) continue;
|
|
3482
3818
|
orphan(
|
|
3483
3819
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
3484
3820
|
`${dashPath}.widgets.${widgetId}`,
|
|
3485
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
3821
|
+
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
|
|
3486
3822
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
3487
3823
|
);
|
|
3488
3824
|
}
|
|
@@ -3491,7 +3827,7 @@ function validateTranslationReferences(stack) {
|
|
|
3491
3827
|
orphan(
|
|
3492
3828
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
3493
3829
|
`${dashPath}.actions.${actionKey}`,
|
|
3494
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
3830
|
+
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
|
|
3495
3831
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
3496
3832
|
);
|
|
3497
3833
|
}
|
|
@@ -3501,7 +3837,7 @@ function validateTranslationReferences(stack) {
|
|
|
3501
3837
|
return findings;
|
|
3502
3838
|
}
|
|
3503
3839
|
function asRecord(v) {
|
|
3504
|
-
return
|
|
3840
|
+
return isRec11(v) ? v : {};
|
|
3505
3841
|
}
|
|
3506
3842
|
function checkOptionKeys(findings, ctx) {
|
|
3507
3843
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3513,7 +3849,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3513
3849
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3514
3850
|
where: ctx.where,
|
|
3515
3851
|
path: ctx.path,
|
|
3516
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3852
|
+
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.`,
|
|
3517
3853
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3518
3854
|
});
|
|
3519
3855
|
return;
|
|
@@ -3526,17 +3862,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3526
3862
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3527
3863
|
where: ctx.where,
|
|
3528
3864
|
path: `${ctx.path}.${key}`,
|
|
3529
|
-
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.` +
|
|
3865
|
+
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),
|
|
3530
3866
|
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)}.`
|
|
3531
3867
|
});
|
|
3532
3868
|
}
|
|
3533
3869
|
}
|
|
3534
3870
|
function checkActionParams(findings, ctx) {
|
|
3535
|
-
const rawParams = Object.keys(asRecord(
|
|
3871
|
+
const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3536
3872
|
if (rawParams.length === 0) return;
|
|
3537
3873
|
const declared = /* @__PURE__ */ new Set();
|
|
3538
3874
|
for (const param of asArray15(ctx.action.params)) {
|
|
3539
|
-
const name =
|
|
3875
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
3540
3876
|
if (name) declared.add(name);
|
|
3541
3877
|
}
|
|
3542
3878
|
for (const paramName of rawParams) {
|
|
@@ -3546,40 +3882,40 @@ function checkActionParams(findings, ctx) {
|
|
|
3546
3882
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
3547
3883
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
3548
3884
|
path: `${ctx.path}.params.${paramName}`,
|
|
3549
|
-
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.` +
|
|
3885
|
+
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),
|
|
3550
3886
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
3551
3887
|
});
|
|
3552
3888
|
}
|
|
3553
3889
|
}
|
|
3554
3890
|
|
|
3555
3891
|
// src/collection-entries.ts
|
|
3556
|
-
function
|
|
3892
|
+
function isRec12(v) {
|
|
3557
3893
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3558
3894
|
}
|
|
3559
3895
|
function collectionEntries(v, base) {
|
|
3560
3896
|
if (Array.isArray(v)) {
|
|
3561
3897
|
const out = [];
|
|
3562
3898
|
for (let i = 0; i < v.length; i++) {
|
|
3563
|
-
if (
|
|
3899
|
+
if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3564
3900
|
}
|
|
3565
3901
|
return out;
|
|
3566
3902
|
}
|
|
3567
|
-
if (
|
|
3568
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3903
|
+
if (isRec12(v)) {
|
|
3904
|
+
return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3569
3905
|
}
|
|
3570
3906
|
return [];
|
|
3571
3907
|
}
|
|
3572
3908
|
|
|
3573
3909
|
// src/validate-translatable-sections.ts
|
|
3574
3910
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3575
|
-
function
|
|
3911
|
+
function isRec13(v) {
|
|
3576
3912
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3577
3913
|
}
|
|
3578
|
-
function
|
|
3914
|
+
function strName14(v) {
|
|
3579
3915
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3580
3916
|
}
|
|
3581
3917
|
function viewLabel(view) {
|
|
3582
|
-
const name =
|
|
3918
|
+
const name = strName14(view.name);
|
|
3583
3919
|
return name ? `view "${name}"` : "";
|
|
3584
3920
|
}
|
|
3585
3921
|
function joinWhere(...parts) {
|
|
@@ -3587,7 +3923,7 @@ function joinWhere(...parts) {
|
|
|
3587
3923
|
}
|
|
3588
3924
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3589
3925
|
const recordObject = viewObjectName(view);
|
|
3590
|
-
const listBinding =
|
|
3926
|
+
const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3591
3927
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3592
3928
|
sites.push({
|
|
3593
3929
|
path: `${site.path}.sections`,
|
|
@@ -3601,11 +3937,11 @@ function translatedObjectNames(stack) {
|
|
|
3601
3937
|
const out = /* @__PURE__ */ new Set();
|
|
3602
3938
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3603
3939
|
for (const bundle of bundles) {
|
|
3604
|
-
if (!
|
|
3940
|
+
if (!isRec13(bundle)) continue;
|
|
3605
3941
|
for (const data of Object.values(bundle)) {
|
|
3606
|
-
if (!
|
|
3942
|
+
if (!isRec13(data) || !isRec13(data.objects)) continue;
|
|
3607
3943
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3608
|
-
if (
|
|
3944
|
+
if (isRec13(node)) out.add(objectName);
|
|
3609
3945
|
}
|
|
3610
3946
|
}
|
|
3611
3947
|
}
|
|
@@ -3617,22 +3953,22 @@ function suggestedName(label2) {
|
|
|
3617
3953
|
}
|
|
3618
3954
|
function validateTranslatableSections(stack) {
|
|
3619
3955
|
const findings = [];
|
|
3620
|
-
if (!
|
|
3956
|
+
if (!isRec13(stack)) return findings;
|
|
3621
3957
|
const translated = translatedObjectNames(stack);
|
|
3622
3958
|
if (translated.size === 0) return findings;
|
|
3623
3959
|
const sites = [];
|
|
3624
3960
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3625
|
-
const objectName =
|
|
3961
|
+
const objectName = strName14(obj.name);
|
|
3626
3962
|
if (!objectName) continue;
|
|
3627
3963
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3628
3964
|
collectViewSites(
|
|
3629
|
-
{ ...view, object:
|
|
3965
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
3630
3966
|
path,
|
|
3631
3967
|
viewLabel(view),
|
|
3632
3968
|
sites
|
|
3633
3969
|
);
|
|
3634
3970
|
}
|
|
3635
|
-
if (
|
|
3971
|
+
if (isRec13(obj.listViews)) {
|
|
3636
3972
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3637
3973
|
}
|
|
3638
3974
|
}
|
|
@@ -3640,13 +3976,13 @@ function validateTranslatableSections(stack) {
|
|
|
3640
3976
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3641
3977
|
}
|
|
3642
3978
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3643
|
-
const pageName =
|
|
3979
|
+
const pageName = strName14(page.name);
|
|
3644
3980
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3645
3981
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3646
3982
|
if (!walked.objectName) continue;
|
|
3647
|
-
const props =
|
|
3983
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
3648
3984
|
if (!props) continue;
|
|
3649
|
-
const type =
|
|
3985
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
3650
3986
|
sites.push({
|
|
3651
3987
|
path: `${walked.path}.properties.sections`,
|
|
3652
3988
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3661,9 +3997,9 @@ function validateTranslatableSections(stack) {
|
|
|
3661
3997
|
if (!Array.isArray(site.sections)) continue;
|
|
3662
3998
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3663
3999
|
const section = site.sections[i];
|
|
3664
|
-
if (!
|
|
3665
|
-
if (
|
|
3666
|
-
const heading =
|
|
4000
|
+
if (!isRec13(section)) continue;
|
|
4001
|
+
if (strName14(section.name)) continue;
|
|
4002
|
+
const heading = strName14(section.label);
|
|
3667
4003
|
if (!heading) continue;
|
|
3668
4004
|
const slug = suggestedName(heading);
|
|
3669
4005
|
findings.push({
|
|
@@ -3681,10 +4017,10 @@ function validateTranslatableSections(stack) {
|
|
|
3681
4017
|
|
|
3682
4018
|
// src/flow-walk.ts
|
|
3683
4019
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
3684
|
-
function
|
|
4020
|
+
function isRec14(v) {
|
|
3685
4021
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3686
4022
|
}
|
|
3687
|
-
function
|
|
4023
|
+
function strName15(v) {
|
|
3688
4024
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3689
4025
|
}
|
|
3690
4026
|
var REGION_SLOTS = new Map(
|
|
@@ -3693,10 +4029,10 @@ var REGION_SLOTS = new Map(
|
|
|
3693
4029
|
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
3694
4030
|
var MAX_REGION_DEPTH = 16;
|
|
3695
4031
|
function flowNodeLabel(node, index) {
|
|
3696
|
-
return
|
|
4032
|
+
return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
|
|
3697
4033
|
}
|
|
3698
4034
|
function stripRegions(config) {
|
|
3699
|
-
if (!
|
|
4035
|
+
if (!isRec14(config)) return void 0;
|
|
3700
4036
|
let out;
|
|
3701
4037
|
for (const key of Object.keys(config)) {
|
|
3702
4038
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3707,11 +4043,11 @@ function stripRegions(config) {
|
|
|
3707
4043
|
}
|
|
3708
4044
|
function walkFlowNodes(flow, flowPath) {
|
|
3709
4045
|
const out = [];
|
|
3710
|
-
if (!
|
|
4046
|
+
if (!isRec14(flow)) return out;
|
|
3711
4047
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3712
4048
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3713
4049
|
nodes.forEach((raw, index) => {
|
|
3714
|
-
if (!
|
|
4050
|
+
if (!isRec14(raw)) return;
|
|
3715
4051
|
const path = `${basePath}[${index}]`;
|
|
3716
4052
|
out.push({
|
|
3717
4053
|
node: raw,
|
|
@@ -3720,9 +4056,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3720
4056
|
regionTrail: trail,
|
|
3721
4057
|
depth
|
|
3722
4058
|
});
|
|
3723
|
-
const type =
|
|
4059
|
+
const type = strName15(raw.type);
|
|
3724
4060
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3725
|
-
if (!slots || !
|
|
4061
|
+
if (!slots || !isRec14(raw.config)) return;
|
|
3726
4062
|
const config = raw.config;
|
|
3727
4063
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3728
4064
|
for (const slot of slots) {
|
|
@@ -3730,8 +4066,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3730
4066
|
if (slot === "branches") {
|
|
3731
4067
|
if (!Array.isArray(value)) continue;
|
|
3732
4068
|
value.forEach((branch, b) => {
|
|
3733
|
-
if (!
|
|
3734
|
-
const branchName =
|
|
4069
|
+
if (!isRec14(branch)) return;
|
|
4070
|
+
const branchName = strName15(branch.name) ?? `#${b}`;
|
|
3735
4071
|
visitList(
|
|
3736
4072
|
branch.nodes,
|
|
3737
4073
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3741,7 +4077,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3741
4077
|
});
|
|
3742
4078
|
continue;
|
|
3743
4079
|
}
|
|
3744
|
-
if (!
|
|
4080
|
+
if (!isRec14(value)) continue;
|
|
3745
4081
|
visitList(
|
|
3746
4082
|
value.nodes,
|
|
3747
4083
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3982,7 +4318,7 @@ function asArray17(v) {
|
|
|
3982
4318
|
}
|
|
3983
4319
|
return [];
|
|
3984
4320
|
}
|
|
3985
|
-
function
|
|
4321
|
+
function strName16(v) {
|
|
3986
4322
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3987
4323
|
}
|
|
3988
4324
|
function surfaceOf(v) {
|
|
@@ -3993,17 +4329,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3993
4329
|
if (!stack || typeof stack !== "object") return findings;
|
|
3994
4330
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3995
4331
|
for (const skill of asArray17(stack.skills)) {
|
|
3996
|
-
const n =
|
|
4332
|
+
const n = strName16(skill.name);
|
|
3997
4333
|
if (n) skillsByName.set(n, skill);
|
|
3998
4334
|
}
|
|
3999
4335
|
const agents = asArray17(stack.agents);
|
|
4000
4336
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4001
4337
|
const agent = agents[ai];
|
|
4002
|
-
const agentName =
|
|
4338
|
+
const agentName = strName16(agent.name) ?? `#${ai}`;
|
|
4003
4339
|
const agentSurface = surfaceOf(agent.surface);
|
|
4004
4340
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4005
4341
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4006
|
-
const ref =
|
|
4342
|
+
const ref = strName16(skillRefs[si]);
|
|
4007
4343
|
if (!ref) continue;
|
|
4008
4344
|
const skill = skillsByName.get(ref);
|
|
4009
4345
|
if (!skill) continue;
|
|
@@ -4032,10 +4368,10 @@ function asArray18(v) {
|
|
|
4032
4368
|
}
|
|
4033
4369
|
return [];
|
|
4034
4370
|
}
|
|
4035
|
-
function
|
|
4371
|
+
function strName17(v) {
|
|
4036
4372
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4037
4373
|
}
|
|
4038
|
-
function
|
|
4374
|
+
function distance7(a, b) {
|
|
4039
4375
|
const m = a.length;
|
|
4040
4376
|
const n = b.length;
|
|
4041
4377
|
if (m === 0) return n;
|
|
@@ -4051,14 +4387,14 @@ function distance6(a, b) {
|
|
|
4051
4387
|
}
|
|
4052
4388
|
return prev[n];
|
|
4053
4389
|
}
|
|
4054
|
-
function
|
|
4390
|
+
function suggest8(target, known) {
|
|
4055
4391
|
for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4056
4392
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4057
4393
|
}
|
|
4058
4394
|
let best;
|
|
4059
4395
|
let bestScore = Infinity;
|
|
4060
4396
|
for (const candidate of known) {
|
|
4061
|
-
const d =
|
|
4397
|
+
const d = distance7(target, candidate);
|
|
4062
4398
|
if (d < bestScore) {
|
|
4063
4399
|
bestScore = d;
|
|
4064
4400
|
best = candidate;
|
|
@@ -4073,8 +4409,8 @@ function materialisesAsTool(action) {
|
|
|
4073
4409
|
if (!ai || typeof ai !== "object") return false;
|
|
4074
4410
|
const aiRec = ai;
|
|
4075
4411
|
if (aiRec.exposed !== true) return false;
|
|
4076
|
-
if (!
|
|
4077
|
-
const type =
|
|
4412
|
+
if (!strName17(aiRec.description)) return false;
|
|
4413
|
+
const type = strName17(action.type);
|
|
4078
4414
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4079
4415
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4080
4416
|
return Boolean(action.target);
|
|
@@ -4082,12 +4418,12 @@ function materialisesAsTool(action) {
|
|
|
4082
4418
|
function collectToolUniverse(stack) {
|
|
4083
4419
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
4084
4420
|
for (const tool of asArray18(stack.tools)) {
|
|
4085
|
-
const n =
|
|
4421
|
+
const n = strName17(tool.name);
|
|
4086
4422
|
if (n) universe.add(n);
|
|
4087
4423
|
}
|
|
4088
4424
|
const addActionFamily = (actions) => {
|
|
4089
4425
|
for (const action of asArray18(actions)) {
|
|
4090
|
-
const n =
|
|
4426
|
+
const n = strName17(action.name);
|
|
4091
4427
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4092
4428
|
}
|
|
4093
4429
|
};
|
|
@@ -4101,7 +4437,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
4101
4437
|
const names = /* @__PURE__ */ new Set();
|
|
4102
4438
|
const scan = (actions) => {
|
|
4103
4439
|
for (const action of asArray18(actions)) {
|
|
4104
|
-
const n =
|
|
4440
|
+
const n = strName17(action.name);
|
|
4105
4441
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4106
4442
|
}
|
|
4107
4443
|
};
|
|
@@ -4127,10 +4463,10 @@ function validateAiToolReferences(stack) {
|
|
|
4127
4463
|
const skills = asArray18(stack.skills);
|
|
4128
4464
|
for (let si = 0; si < skills.length; si++) {
|
|
4129
4465
|
const skill = skills[si];
|
|
4130
|
-
const skillName =
|
|
4466
|
+
const skillName = strName17(skill.name) ?? `#${si}`;
|
|
4131
4467
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4132
4468
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4133
|
-
const ref =
|
|
4469
|
+
const ref = strName17(refs[ti]);
|
|
4134
4470
|
if (!ref || resolves(ref)) continue;
|
|
4135
4471
|
const isPattern = ref.endsWith("*");
|
|
4136
4472
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4139,7 +4475,7 @@ function validateAiToolReferences(stack) {
|
|
|
4139
4475
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4140
4476
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4141
4477
|
path: `skills[${si}].tools[${ti}]`,
|
|
4142
|
-
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.` +
|
|
4478
|
+
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),
|
|
4143
4479
|
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: ${PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
|
|
4144
4480
|
});
|
|
4145
4481
|
}
|
|
@@ -4157,7 +4493,7 @@ function asArray19(v) {
|
|
|
4157
4493
|
}
|
|
4158
4494
|
return [];
|
|
4159
4495
|
}
|
|
4160
|
-
function
|
|
4496
|
+
function strName18(v) {
|
|
4161
4497
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4162
4498
|
}
|
|
4163
4499
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -4167,7 +4503,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4167
4503
|
const agents = asArray19(stack.agents);
|
|
4168
4504
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4169
4505
|
const agent = agents[ai];
|
|
4170
|
-
const name =
|
|
4506
|
+
const name = strName18(agent.name) ?? `#${ai}`;
|
|
4171
4507
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4172
4508
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4173
4509
|
findings.push({
|
|
@@ -4183,9 +4519,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4183
4519
|
const apps = asArray19(stack.apps);
|
|
4184
4520
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4185
4521
|
const app = apps[appIdx];
|
|
4186
|
-
const defaultAgent =
|
|
4522
|
+
const defaultAgent = strName18(app.defaultAgent);
|
|
4187
4523
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4188
|
-
const appName =
|
|
4524
|
+
const appName = strName18(app.name) ?? `#${appIdx}`;
|
|
4189
4525
|
findings.push({
|
|
4190
4526
|
severity: "warning",
|
|
4191
4527
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4215,6 +4551,7 @@ function loadTypeScript() {
|
|
|
4215
4551
|
return cachedTs;
|
|
4216
4552
|
}
|
|
4217
4553
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4554
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4218
4555
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4219
4556
|
{
|
|
4220
4557
|
id: "input-property-assign",
|
|
@@ -4281,13 +4618,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4281
4618
|
"owner",
|
|
4282
4619
|
"record_type"
|
|
4283
4620
|
]);
|
|
4284
|
-
|
|
4621
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
4622
|
+
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).`;
|
|
4623
|
+
}
|
|
4624
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4285
4625
|
function asArray20(v) {
|
|
4286
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4287
|
-
if (
|
|
4626
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4627
|
+
if (isRec15(v)) {
|
|
4288
4628
|
return Object.entries(v).map(([name, def]) => ({
|
|
4289
4629
|
name,
|
|
4290
|
-
...
|
|
4630
|
+
...isRec15(def) ? def : {}
|
|
4291
4631
|
}));
|
|
4292
4632
|
}
|
|
4293
4633
|
return [];
|
|
@@ -4431,14 +4771,16 @@ function validateHookBodyWrites(stack) {
|
|
|
4431
4771
|
const hooks = asArray20(stack.hooks);
|
|
4432
4772
|
if (hooks.length === 0) return findings;
|
|
4433
4773
|
let objectFields = null;
|
|
4774
|
+
let anchors = null;
|
|
4434
4775
|
hooks.forEach((hook, hookIndex) => {
|
|
4435
4776
|
const body = hook.body;
|
|
4436
|
-
if (!
|
|
4777
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4437
4778
|
const source = body.source;
|
|
4438
4779
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4439
4780
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4440
4781
|
if (writes.length === 0) return;
|
|
4441
4782
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4783
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4442
4784
|
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4443
4785
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4444
4786
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
@@ -4453,7 +4795,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4453
4795
|
if (reported.has(dedupeKey)) continue;
|
|
4454
4796
|
if (w.object === void 0) {
|
|
4455
4797
|
if (!inputJudgeable) continue;
|
|
4456
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
4798
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4799
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
4800
|
+
reported.add(dedupeKey);
|
|
4801
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
4802
|
+
findings.push({
|
|
4803
|
+
severity: "warning",
|
|
4804
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4805
|
+
where,
|
|
4806
|
+
path,
|
|
4807
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
4808
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
4809
|
+
});
|
|
4810
|
+
continue;
|
|
4811
|
+
}
|
|
4457
4812
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
4458
4813
|
reported.add(dedupeKey);
|
|
4459
4814
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -4469,7 +4824,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4469
4824
|
} else {
|
|
4470
4825
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4471
4826
|
if (!known) continue;
|
|
4472
|
-
if (
|
|
4827
|
+
if (known.has(w.field)) continue;
|
|
4828
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4829
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4830
|
+
reported.add(dedupeKey);
|
|
4831
|
+
findings.push({
|
|
4832
|
+
severity: "warning",
|
|
4833
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4834
|
+
where,
|
|
4835
|
+
path,
|
|
4836
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4837
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4838
|
+
});
|
|
4839
|
+
continue;
|
|
4840
|
+
}
|
|
4473
4841
|
reported.add(dedupeKey);
|
|
4474
4842
|
findings.push({
|
|
4475
4843
|
severity: "warning",
|
|
@@ -4498,19 +4866,20 @@ function fixHint(field, declared) {
|
|
|
4498
4866
|
import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
|
|
4499
4867
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4500
4868
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4869
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4501
4870
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4502
4871
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
4503
4872
|
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
4504
4873
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4505
4874
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4506
4875
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4507
|
-
var
|
|
4876
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4508
4877
|
function asArray21(v) {
|
|
4509
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4510
|
-
if (
|
|
4878
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4879
|
+
if (isRec16(v)) {
|
|
4511
4880
|
return Object.entries(v).map(([name, def]) => ({
|
|
4512
4881
|
name,
|
|
4513
|
-
...
|
|
4882
|
+
...isRec16(def) ? def : {}
|
|
4514
4883
|
}));
|
|
4515
4884
|
}
|
|
4516
4885
|
return [];
|
|
@@ -4528,7 +4897,7 @@ function collectActionBodies(stack) {
|
|
|
4528
4897
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4529
4898
|
if (type !== "script") return;
|
|
4530
4899
|
const body = action.body;
|
|
4531
|
-
if (!
|
|
4900
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
4532
4901
|
const source = body.source;
|
|
4533
4902
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4534
4903
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4547,10 +4916,11 @@ function collectActionBodies(stack) {
|
|
|
4547
4916
|
}
|
|
4548
4917
|
function validateActionBodyWrites(stack) {
|
|
4549
4918
|
const findings = [];
|
|
4550
|
-
if (!
|
|
4919
|
+
if (!isRec16(stack)) return findings;
|
|
4551
4920
|
const sites = collectActionBodies(stack);
|
|
4552
4921
|
if (sites.length === 0) return findings;
|
|
4553
4922
|
let objectFields = null;
|
|
4923
|
+
let anchors = null;
|
|
4554
4924
|
for (const site of sites) {
|
|
4555
4925
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4556
4926
|
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
@@ -4575,6 +4945,7 @@ function validateActionBodyWrites(stack) {
|
|
|
4575
4945
|
}
|
|
4576
4946
|
if (writes.length === 0) continue;
|
|
4577
4947
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4948
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4578
4949
|
const reported = /* @__PURE__ */ new Set();
|
|
4579
4950
|
for (const w of writes) {
|
|
4580
4951
|
if (w.object === void 0) continue;
|
|
@@ -4582,7 +4953,20 @@ function validateActionBodyWrites(stack) {
|
|
|
4582
4953
|
if (reported.has(dedupeKey)) continue;
|
|
4583
4954
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4584
4955
|
if (!known) continue;
|
|
4585
|
-
if (
|
|
4956
|
+
if (known.has(w.field)) continue;
|
|
4957
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4958
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4959
|
+
reported.add(dedupeKey);
|
|
4960
|
+
findings.push({
|
|
4961
|
+
severity: "warning",
|
|
4962
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4963
|
+
where,
|
|
4964
|
+
path: site.path,
|
|
4965
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4966
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4967
|
+
});
|
|
4968
|
+
continue;
|
|
4969
|
+
}
|
|
4586
4970
|
reported.add(dedupeKey);
|
|
4587
4971
|
findings.push({
|
|
4588
4972
|
severity: "warning",
|
|
@@ -4604,14 +4988,15 @@ function fixHint2(field, declared) {
|
|
|
4604
4988
|
// src/validate-flow-node-writes.ts
|
|
4605
4989
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
4606
4990
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4991
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
4607
4992
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4608
|
-
var
|
|
4993
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4609
4994
|
function asArray22(v) {
|
|
4610
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4611
|
-
if (
|
|
4995
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
4996
|
+
if (isRec17(v)) {
|
|
4612
4997
|
return Object.entries(v).map(([name, def]) => ({
|
|
4613
4998
|
name,
|
|
4614
|
-
...
|
|
4999
|
+
...isRec17(def) ? def : {}
|
|
4615
5000
|
}));
|
|
4616
5001
|
}
|
|
4617
5002
|
return [];
|
|
@@ -4624,30 +5009,44 @@ function readLiteralObjectName(config) {
|
|
|
4624
5009
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4625
5010
|
function validateFlowNodeWrites(stack) {
|
|
4626
5011
|
const findings = [];
|
|
4627
|
-
if (!
|
|
5012
|
+
if (!isRec17(stack)) return findings;
|
|
4628
5013
|
const flows = asArray22(stack.flows);
|
|
4629
5014
|
if (flows.length === 0) return findings;
|
|
4630
5015
|
let objectFields = null;
|
|
5016
|
+
let anchors = null;
|
|
4631
5017
|
flows.forEach((flow, flowIndex) => {
|
|
4632
5018
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
4633
5019
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4634
5020
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4635
5021
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4636
|
-
const config =
|
|
5022
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
4637
5023
|
if (!config) return;
|
|
4638
5024
|
const fields = config.fields;
|
|
4639
|
-
if (!
|
|
5025
|
+
if (!isRec17(fields)) return;
|
|
4640
5026
|
const written = Object.keys(fields);
|
|
4641
5027
|
if (written.length === 0) return;
|
|
4642
5028
|
const objectName = readLiteralObjectName(config);
|
|
4643
5029
|
if (!objectName) return;
|
|
4644
5030
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5031
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4645
5032
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
4646
5033
|
if (!known) return;
|
|
4647
5034
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
4648
5035
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
4649
5036
|
for (const fieldName of written) {
|
|
4650
|
-
if (known.has(fieldName)
|
|
5037
|
+
if (known.has(fieldName)) continue;
|
|
5038
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
5039
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
5040
|
+
findings.push({
|
|
5041
|
+
severity: "warning",
|
|
5042
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
5043
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5044
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5045
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
5046
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
5047
|
+
});
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
4651
5050
|
if (fieldName.includes(".")) continue;
|
|
4652
5051
|
findings.push({
|
|
4653
5052
|
severity: "error",
|
|
@@ -4769,14 +5168,14 @@ import {
|
|
|
4769
5168
|
chartAggregateResultKeys,
|
|
4770
5169
|
isRecordContextBlockType
|
|
4771
5170
|
} from "@objectstack/spec/ui";
|
|
4772
|
-
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
5171
|
+
import { VALID_AST_OPERATORS as VALID_AST_OPERATORS2 } from "@objectstack/spec/data";
|
|
4773
5172
|
|
|
4774
5173
|
// src/zod-issue-format.ts
|
|
4775
|
-
var
|
|
5174
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4776
5175
|
var valueAtPath = (root, path) => {
|
|
4777
5176
|
let cur = root;
|
|
4778
5177
|
for (const key of path) {
|
|
4779
|
-
if (!
|
|
5178
|
+
if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
|
|
4780
5179
|
cur = cur[key];
|
|
4781
5180
|
}
|
|
4782
5181
|
return cur;
|
|
@@ -4915,7 +5314,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4915
5314
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4916
5315
|
function checkChartDrillDown(raw, push2) {
|
|
4917
5316
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4918
|
-
if (!
|
|
5317
|
+
if (!isRec19(raw)) {
|
|
4919
5318
|
push2(
|
|
4920
5319
|
"error",
|
|
4921
5320
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4938,7 +5337,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4938
5337
|
}
|
|
4939
5338
|
function checkChartAggregate(raw, push2) {
|
|
4940
5339
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4941
|
-
if (!
|
|
5340
|
+
if (!isRec19(raw)) {
|
|
4942
5341
|
push2(
|
|
4943
5342
|
"error",
|
|
4944
5343
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4969,7 +5368,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4969
5368
|
);
|
|
4970
5369
|
}
|
|
4971
5370
|
}
|
|
4972
|
-
var
|
|
5371
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4973
5372
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4974
5373
|
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4975
5374
|
const { values, where, path } = attrs;
|
|
@@ -4979,11 +5378,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
4979
5378
|
const aggregate = values.get("aggregate");
|
|
4980
5379
|
checkChartAggregate(aggregate, push2);
|
|
4981
5380
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4982
|
-
if (!
|
|
5381
|
+
if (!isRec19(aggregate)) return;
|
|
4983
5382
|
const fn = strOf(aggregate.function);
|
|
4984
5383
|
const field = strOf(aggregate.field);
|
|
4985
5384
|
const groupBy = aggregate.groupBy;
|
|
4986
|
-
const groupByField = strOf(groupBy) ?? (
|
|
5385
|
+
const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4987
5386
|
const objectName = strOf(values.get("objectName"));
|
|
4988
5387
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4989
5388
|
if (objectName && known) {
|
|
@@ -5027,18 +5426,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5027
5426
|
);
|
|
5028
5427
|
};
|
|
5029
5428
|
const xAxisRaw = values.get("xAxis");
|
|
5030
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5429
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
5031
5430
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
5032
5431
|
axisRef(categoryAxis, categoryProp);
|
|
5033
5432
|
const yAxisRaw = values.get("yAxis");
|
|
5034
5433
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
5035
5434
|
for (const a of yAxisList) {
|
|
5036
|
-
axisRef(strOf(a) ?? (
|
|
5435
|
+
axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
5037
5436
|
}
|
|
5038
5437
|
const series = values.get("series");
|
|
5039
5438
|
if (Array.isArray(series)) {
|
|
5040
5439
|
for (const s of series) {
|
|
5041
|
-
if (!
|
|
5440
|
+
if (!isRec19(s)) continue;
|
|
5042
5441
|
const dataKey = strOf(s.dataKey);
|
|
5043
5442
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
5044
5443
|
}
|
|
@@ -5094,7 +5493,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
5094
5493
|
if (!Array.isArray(value)) return { child, parent };
|
|
5095
5494
|
for (let i = 0; i < value.length; i++) {
|
|
5096
5495
|
const sub = value[i];
|
|
5097
|
-
if (!
|
|
5496
|
+
if (!isRec19(sub)) continue;
|
|
5098
5497
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
5099
5498
|
child.push({
|
|
5100
5499
|
objectName: strOf(sub.childObject),
|
|
@@ -5119,7 +5518,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
5119
5518
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
5120
5519
|
return;
|
|
5121
5520
|
}
|
|
5122
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5521
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
|
|
5123
5522
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
5124
5523
|
}
|
|
5125
5524
|
}
|
|
@@ -5139,20 +5538,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
5139
5538
|
}
|
|
5140
5539
|
for (const key of spec.nestedFields ?? []) {
|
|
5141
5540
|
const v = readable(key);
|
|
5142
|
-
if (
|
|
5541
|
+
if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
5143
5542
|
}
|
|
5144
5543
|
for (const key of spec.sections ?? []) {
|
|
5145
5544
|
const v = readable(key);
|
|
5146
5545
|
if (!Array.isArray(v)) continue;
|
|
5147
5546
|
for (let i = 0; i < v.length; i++) {
|
|
5148
5547
|
const section = v[i];
|
|
5149
|
-
if (!
|
|
5548
|
+
if (!isRec19(section)) continue;
|
|
5150
5549
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
5151
5550
|
}
|
|
5152
5551
|
}
|
|
5153
5552
|
for (const key of spec.keyedByField ?? []) {
|
|
5154
5553
|
const v = readable(key);
|
|
5155
|
-
if (!
|
|
5554
|
+
if (!isRec19(v)) continue;
|
|
5156
5555
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
5157
5556
|
}
|
|
5158
5557
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -5319,7 +5718,12 @@ function validateReactPageProps(stack) {
|
|
|
5319
5718
|
searchTargets,
|
|
5320
5719
|
where,
|
|
5321
5720
|
`${path} \u203A searchableFields`,
|
|
5322
|
-
"searchableFields"
|
|
5721
|
+
"searchableFields",
|
|
5722
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
5723
|
+
// default, spelled out here because the #8404 provenance index
|
|
5724
|
+
// follows it positionally.
|
|
5725
|
+
"narrowing",
|
|
5726
|
+
unprovisionedAnchors
|
|
5323
5727
|
)
|
|
5324
5728
|
);
|
|
5325
5729
|
}
|
|
@@ -5341,6 +5745,13 @@ function validateReactPageProps(stack) {
|
|
|
5341
5745
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5342
5746
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5343
5747
|
{ name: "validateSearchableFields", run: validateSearchableFields },
|
|
5748
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5749
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
5750
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5751
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5752
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5753
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
5754
|
+
{ name: "validateSortableFields", run: validateSortableFields },
|
|
5344
5755
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5345
5756
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5346
5757
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5459,10 +5870,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
5459
5870
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
5460
5871
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5461
5872
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5462
|
-
function
|
|
5873
|
+
function isRec20(v) {
|
|
5463
5874
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5464
5875
|
}
|
|
5465
|
-
function
|
|
5876
|
+
function strName19(v) {
|
|
5466
5877
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5467
5878
|
}
|
|
5468
5879
|
function asArray25(v) {
|
|
@@ -5476,8 +5887,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
5476
5887
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5477
5888
|
function suppliedByDataSource(issue, component) {
|
|
5478
5889
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5479
|
-
const dataSource =
|
|
5480
|
-
return
|
|
5890
|
+
const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
|
|
5891
|
+
return strName19(dataSource?.object) !== void 0;
|
|
5481
5892
|
}
|
|
5482
5893
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
5483
5894
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -5494,18 +5905,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
5494
5905
|
}
|
|
5495
5906
|
function validateComponentProps(stack) {
|
|
5496
5907
|
const findings = [];
|
|
5497
|
-
if (!
|
|
5908
|
+
if (!isRec20(stack)) return findings;
|
|
5498
5909
|
const pages = asArray25(stack.pages);
|
|
5499
5910
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5500
5911
|
const page = pages[pi];
|
|
5501
|
-
if (!
|
|
5502
|
-
const pageName =
|
|
5912
|
+
if (!isRec20(page)) continue;
|
|
5913
|
+
const pageName = strName19(page.name) ?? `#${pi}`;
|
|
5503
5914
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5504
|
-
const type =
|
|
5915
|
+
const type = strName19(component.type);
|
|
5505
5916
|
if (!type) continue;
|
|
5506
5917
|
const schema = PROPS_SCHEMAS[type];
|
|
5507
5918
|
if (!schema) continue;
|
|
5508
|
-
const props =
|
|
5919
|
+
const props = isRec20(component.properties) ? component.properties : void 0;
|
|
5509
5920
|
if (!props) continue;
|
|
5510
5921
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5511
5922
|
const base = `${path}.properties`;
|
|
@@ -6683,10 +7094,10 @@ function asArray35(v) {
|
|
|
6683
7094
|
}
|
|
6684
7095
|
return [];
|
|
6685
7096
|
}
|
|
6686
|
-
function
|
|
7097
|
+
function isRec21(v) {
|
|
6687
7098
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6688
7099
|
}
|
|
6689
|
-
function
|
|
7100
|
+
function strName20(v) {
|
|
6690
7101
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6691
7102
|
}
|
|
6692
7103
|
function fieldNameOf(entry) {
|
|
@@ -6707,7 +7118,7 @@ function validateFormLayout(stack) {
|
|
|
6707
7118
|
objectFields.set(name, new Set(fields));
|
|
6708
7119
|
}
|
|
6709
7120
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6710
|
-
const viewName =
|
|
7121
|
+
const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
|
|
6711
7122
|
const containerObject = viewObjectName(view);
|
|
6712
7123
|
for (const site of formViewSites(view, viewPath)) {
|
|
6713
7124
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6717,7 +7128,7 @@ function validateFormLayout(stack) {
|
|
|
6717
7128
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6718
7129
|
for (let s = 0; s < sections.length; s++) {
|
|
6719
7130
|
const sec = sections[s];
|
|
6720
|
-
const secFields =
|
|
7131
|
+
const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6721
7132
|
for (let f = 0; f < secFields.length; f++) {
|
|
6722
7133
|
const entry = secFields[f];
|
|
6723
7134
|
const fname = fieldNameOf(entry);
|
|
@@ -6732,7 +7143,7 @@ function validateFormLayout(stack) {
|
|
|
6732
7143
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6733
7144
|
});
|
|
6734
7145
|
}
|
|
6735
|
-
const colSpan =
|
|
7146
|
+
const colSpan = isRec21(entry) ? entry.colSpan : void 0;
|
|
6736
7147
|
if (colSpan != null) {
|
|
6737
7148
|
findings.push({
|
|
6738
7149
|
severity: "warning",
|
|
@@ -6895,12 +7306,12 @@ function bareRhsOnlyIdentifiers(ast) {
|
|
|
6895
7306
|
for (const name of elsewhere) rhs.delete(name);
|
|
6896
7307
|
return rhs;
|
|
6897
7308
|
}
|
|
6898
|
-
function
|
|
7309
|
+
function isRec22(v) {
|
|
6899
7310
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6900
7311
|
}
|
|
6901
7312
|
function schemaIdOf(view) {
|
|
6902
7313
|
const data = view.data;
|
|
6903
|
-
if (!
|
|
7314
|
+
if (!isRec22(data)) return void 0;
|
|
6904
7315
|
if (data.provider !== "schema") return void 0;
|
|
6905
7316
|
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6906
7317
|
}
|
|
@@ -7272,7 +7683,7 @@ function predicateSource2(v) {
|
|
|
7272
7683
|
}
|
|
7273
7684
|
return void 0;
|
|
7274
7685
|
}
|
|
7275
|
-
function
|
|
7686
|
+
function isRec23(v) {
|
|
7276
7687
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7277
7688
|
}
|
|
7278
7689
|
function checkPredicate(source, scope, where, path, findings) {
|
|
@@ -7343,7 +7754,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
7343
7754
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
7344
7755
|
for (let i = 0; i < entries.length; i++) {
|
|
7345
7756
|
const entry = entries[i];
|
|
7346
|
-
if (!
|
|
7757
|
+
if (!isRec23(entry)) continue;
|
|
7347
7758
|
const path = `${base}[${i}]`;
|
|
7348
7759
|
for (const key of PREDICATE_KEYS) {
|
|
7349
7760
|
const source = predicateSource2(entry[key]);
|
|
@@ -7377,7 +7788,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
7377
7788
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7378
7789
|
for (let s = 0; s < sections.length; s++) {
|
|
7379
7790
|
const section = sections[s];
|
|
7380
|
-
if (!
|
|
7791
|
+
if (!isRec23(section)) continue;
|
|
7381
7792
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7382
7793
|
for (const key of PREDICATE_KEYS) {
|
|
7383
7794
|
const source = predicateSource2(section[key]);
|
|
@@ -7661,6 +8072,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7661
8072
|
}
|
|
7662
8073
|
}
|
|
7663
8074
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
8075
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
7664
8076
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7665
8077
|
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7666
8078
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -7684,7 +8096,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7684
8096
|
}
|
|
7685
8097
|
}
|
|
7686
8098
|
const delegatedFrom = rec.delegated_from;
|
|
7687
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
8099
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
7688
8100
|
const reason = rec.reason;
|
|
7689
8101
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
7690
8102
|
findings.push({
|
|
@@ -7850,6 +8262,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7850
8262
|
import { compileCelToFilter } from "@objectstack/formula";
|
|
7851
8263
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7852
8264
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
8265
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
8266
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
7853
8267
|
function asArray38(v) {
|
|
7854
8268
|
if (Array.isArray(v)) return v;
|
|
7855
8269
|
if (v && typeof v === "object") {
|
|
@@ -7874,10 +8288,67 @@ function sourceOf(condition) {
|
|
|
7874
8288
|
return str2(input?.source);
|
|
7875
8289
|
}
|
|
7876
8290
|
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).";
|
|
8291
|
+
function effectiveSharingModelOf(obj) {
|
|
8292
|
+
const m = obj.sharingModel;
|
|
8293
|
+
if (m === "private") return "private";
|
|
8294
|
+
if (m === "public_read") return "read";
|
|
8295
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
8296
|
+
if (m == null) {
|
|
8297
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
8298
|
+
return isSystem ? "public" : "private";
|
|
8299
|
+
}
|
|
8300
|
+
return "private";
|
|
8301
|
+
}
|
|
8302
|
+
function masterOf(obj) {
|
|
8303
|
+
for (const f of asArray38(obj.fields)) {
|
|
8304
|
+
if (f.type === "master_detail") {
|
|
8305
|
+
const ref = f.reference;
|
|
8306
|
+
if (typeof ref === "string" && ref) return ref;
|
|
8307
|
+
}
|
|
8308
|
+
}
|
|
8309
|
+
return void 0;
|
|
8310
|
+
}
|
|
8311
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
8312
|
+
const object = str2(rule.object);
|
|
8313
|
+
if (!object) return [];
|
|
8314
|
+
const target = objectsByName.get(object);
|
|
8315
|
+
if (!target) return [];
|
|
8316
|
+
const name = str2(rule.name) || String(index);
|
|
8317
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
8318
|
+
const path = `sharingRules[${index}].object`;
|
|
8319
|
+
const owd = target.sharingModel;
|
|
8320
|
+
if (owd === "controlled_by_parent") {
|
|
8321
|
+
const master = masterOf(target);
|
|
8322
|
+
return [{
|
|
8323
|
+
severity: "error",
|
|
8324
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
8325
|
+
where,
|
|
8326
|
+
path,
|
|
8327
|
+
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.`,
|
|
8328
|
+
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.`
|
|
8329
|
+
}];
|
|
8330
|
+
}
|
|
8331
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
8332
|
+
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`;
|
|
8333
|
+
return [{
|
|
8334
|
+
severity: "error",
|
|
8335
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
8336
|
+
where,
|
|
8337
|
+
path,
|
|
8338
|
+
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.`,
|
|
8339
|
+
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.`
|
|
8340
|
+
}];
|
|
8341
|
+
}
|
|
7877
8342
|
function validateSharingRuleEnforceability(stack) {
|
|
7878
8343
|
const findings = [];
|
|
7879
8344
|
const cfg = stack ?? {};
|
|
8345
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
8346
|
+
for (const obj of asArray38(cfg.objects)) {
|
|
8347
|
+
const name = str2(obj.name);
|
|
8348
|
+
if (name) objectsByName.set(name, obj);
|
|
8349
|
+
}
|
|
7880
8350
|
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
8351
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
7881
8352
|
const input = toCompilerInput(rule.condition);
|
|
7882
8353
|
if (input === null) return;
|
|
7883
8354
|
const result = compileCelToFilter(input, { variables: {} });
|
|
@@ -8007,11 +8478,11 @@ import { createRequire as createRequire4 } from "module";
|
|
|
8007
8478
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
8008
8479
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
8009
8480
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
8010
|
-
var
|
|
8481
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8011
8482
|
function asArray40(v) {
|
|
8012
|
-
if (Array.isArray(v)) return v.filter(
|
|
8013
|
-
if (
|
|
8014
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8483
|
+
if (Array.isArray(v)) return v.filter(isRec24);
|
|
8484
|
+
if (isRec24(v)) {
|
|
8485
|
+
return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
|
|
8015
8486
|
}
|
|
8016
8487
|
return [];
|
|
8017
8488
|
}
|
|
@@ -8028,7 +8499,7 @@ function loadAjv() {
|
|
|
8028
8499
|
`@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.`
|
|
8029
8500
|
);
|
|
8030
8501
|
}
|
|
8031
|
-
const ctor =
|
|
8502
|
+
const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8032
8503
|
cachedAjv = ctor;
|
|
8033
8504
|
return ctor;
|
|
8034
8505
|
}
|
|
@@ -8043,7 +8514,7 @@ function loadAddFormats() {
|
|
|
8043
8514
|
`@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.`
|
|
8044
8515
|
);
|
|
8045
8516
|
}
|
|
8046
|
-
const plugin =
|
|
8517
|
+
const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8047
8518
|
cachedAddFormats = plugin;
|
|
8048
8519
|
return plugin;
|
|
8049
8520
|
}
|
|
@@ -8073,13 +8544,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
8073
8544
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
8074
8545
|
for (const branch of ["then", "otherwise"]) {
|
|
8075
8546
|
const nested = rule[branch];
|
|
8076
|
-
if (
|
|
8547
|
+
if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
8077
8548
|
}
|
|
8078
8549
|
return out;
|
|
8079
8550
|
}
|
|
8080
8551
|
function walkObjectValidationRules(stack) {
|
|
8081
8552
|
const walked = [];
|
|
8082
|
-
if (!
|
|
8553
|
+
if (!isRec24(stack)) return walked;
|
|
8083
8554
|
for (const obj of asArray40(stack.objects)) {
|
|
8084
8555
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8085
8556
|
const validations = obj.validations;
|
|
@@ -8114,7 +8585,7 @@ function validateRuleCompilability(stack) {
|
|
|
8114
8585
|
});
|
|
8115
8586
|
}
|
|
8116
8587
|
}
|
|
8117
|
-
if (rule.type === "json_schema" &&
|
|
8588
|
+
if (rule.type === "json_schema" && isRec24(rule.schema)) {
|
|
8118
8589
|
try {
|
|
8119
8590
|
createRuntimeAjv().compile(rule.schema);
|
|
8120
8591
|
} catch (err) {
|
|
@@ -8134,7 +8605,7 @@ function validateRuleCompilability(stack) {
|
|
|
8134
8605
|
|
|
8135
8606
|
// src/validate-rule-schema-formats.ts
|
|
8136
8607
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
8137
|
-
var
|
|
8608
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8138
8609
|
var SUBSCHEMA_KEYS = [
|
|
8139
8610
|
"additionalItems",
|
|
8140
8611
|
"additionalProperties",
|
|
@@ -8158,7 +8629,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
8158
8629
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
8159
8630
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
8160
8631
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
8161
|
-
if (!
|
|
8632
|
+
if (!isRec25(schema)) return;
|
|
8162
8633
|
if (typeof schema.format === "string") {
|
|
8163
8634
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
8164
8635
|
}
|
|
@@ -8177,7 +8648,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8177
8648
|
}
|
|
8178
8649
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
8179
8650
|
const value = schema[key];
|
|
8180
|
-
if (!
|
|
8651
|
+
if (!isRec25(value)) continue;
|
|
8181
8652
|
for (const [name, entry] of Object.entries(value)) {
|
|
8182
8653
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8183
8654
|
}
|
|
@@ -8185,13 +8656,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8185
8656
|
const items = schema.items;
|
|
8186
8657
|
if (Array.isArray(items)) {
|
|
8187
8658
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
8188
|
-
} else if (
|
|
8659
|
+
} else if (isRec25(items)) {
|
|
8189
8660
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
8190
8661
|
}
|
|
8191
8662
|
const dependencies = schema.dependencies;
|
|
8192
|
-
if (
|
|
8663
|
+
if (isRec25(dependencies)) {
|
|
8193
8664
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
8194
|
-
if (!
|
|
8665
|
+
if (!isRec25(entry)) continue;
|
|
8195
8666
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8196
8667
|
}
|
|
8197
8668
|
}
|
|
@@ -8218,9 +8689,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
8218
8689
|
let best = null;
|
|
8219
8690
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
8220
8691
|
for (const candidate of [...registered].sort()) {
|
|
8221
|
-
const
|
|
8222
|
-
if (
|
|
8223
|
-
bestDistance =
|
|
8692
|
+
const distance8 = editDistance2(authored, candidate);
|
|
8693
|
+
if (distance8 < bestDistance) {
|
|
8694
|
+
bestDistance = distance8;
|
|
8224
8695
|
best = candidate;
|
|
8225
8696
|
}
|
|
8226
8697
|
}
|
|
@@ -8230,7 +8701,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
8230
8701
|
const findings = [];
|
|
8231
8702
|
const pending = [];
|
|
8232
8703
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
8233
|
-
if (rule.type !== "json_schema" || !
|
|
8704
|
+
if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
|
|
8234
8705
|
const uses = [];
|
|
8235
8706
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
8236
8707
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -8263,7 +8734,7 @@ function asArray41(v) {
|
|
|
8263
8734
|
}
|
|
8264
8735
|
return [];
|
|
8265
8736
|
}
|
|
8266
|
-
function
|
|
8737
|
+
function strName21(v) {
|
|
8267
8738
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8268
8739
|
}
|
|
8269
8740
|
function strList3(v) {
|
|
@@ -8278,7 +8749,7 @@ function collectNamePlacedActions(stack) {
|
|
|
8278
8749
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
8279
8750
|
}
|
|
8280
8751
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8281
|
-
const n =
|
|
8752
|
+
const n = strName21(def?.name);
|
|
8282
8753
|
if (n) placed.add(n);
|
|
8283
8754
|
}
|
|
8284
8755
|
};
|
|
@@ -8304,7 +8775,7 @@ function validateActionLocations(stack) {
|
|
|
8304
8775
|
const check = (action, path) => {
|
|
8305
8776
|
if (!action || typeof action !== "object") return;
|
|
8306
8777
|
if ("locations" in action) return;
|
|
8307
|
-
const name =
|
|
8778
|
+
const name = strName21(action.name);
|
|
8308
8779
|
if (!name) return;
|
|
8309
8780
|
if (namePlaced.has(name)) return;
|
|
8310
8781
|
findings.push({
|
|
@@ -9157,6 +9628,17 @@ function lintViewRefs(stack) {
|
|
|
9157
9628
|
}
|
|
9158
9629
|
|
|
9159
9630
|
// src/data-model-rules.ts
|
|
9631
|
+
function objectWhere(obj) {
|
|
9632
|
+
return `object "${obj?.name}"`;
|
|
9633
|
+
}
|
|
9634
|
+
function indexWhere(obj, idx, j, cols) {
|
|
9635
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
9636
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
9637
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
9638
|
+
}
|
|
9639
|
+
function fieldWhere(obj, fieldName) {
|
|
9640
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
9641
|
+
}
|
|
9160
9642
|
function fieldEntries2(fields) {
|
|
9161
9643
|
if (!fields) return [];
|
|
9162
9644
|
if (Array.isArray(fields)) {
|
|
@@ -9190,11 +9672,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9190
9672
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9191
9673
|
const idx = declaredIndexes[j];
|
|
9192
9674
|
if (idx?.unique !== true) continue;
|
|
9193
|
-
const
|
|
9675
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
9676
|
+
const cols = colList.join(", ");
|
|
9194
9677
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9195
9678
|
issues.push({
|
|
9196
9679
|
severity: "warning",
|
|
9197
9680
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9681
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9198
9682
|
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).`,
|
|
9199
9683
|
path: `objects[${i}].indexes[${j}]`,
|
|
9200
9684
|
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).`
|
|
@@ -9243,6 +9727,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9243
9727
|
issues.push({
|
|
9244
9728
|
severity: "warning",
|
|
9245
9729
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
9730
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
9731
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
9732
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
9733
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
9734
|
+
where: fieldWhere(obj, name),
|
|
9246
9735
|
message,
|
|
9247
9736
|
path: `objects[${i}]`,
|
|
9248
9737
|
fix
|
|
@@ -9271,6 +9760,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9271
9760
|
issues.push({
|
|
9272
9761
|
severity: "warning",
|
|
9273
9762
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9763
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9274
9764
|
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.`,
|
|
9275
9765
|
path: `objects[${i}].indexes[${j}]`,
|
|
9276
9766
|
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.`
|
|
@@ -9287,7 +9777,7 @@ var CLI_ONLY = ["cli"];
|
|
|
9287
9777
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
9288
9778
|
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.";
|
|
9289
9779
|
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.";
|
|
9290
|
-
var
|
|
9780
|
+
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).";
|
|
9291
9781
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
9292
9782
|
var AUTHORING_RULES = [
|
|
9293
9783
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -9348,8 +9838,14 @@ var AUTHORING_RULES = [
|
|
|
9348
9838
|
input: "normalized",
|
|
9349
9839
|
commands: ALL,
|
|
9350
9840
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
9351
|
-
|
|
9352
|
-
|
|
9841
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
9842
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
9843
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
9844
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
9845
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
9846
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
9847
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9848
|
+
runtimeTypes: ["object"],
|
|
9353
9849
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
9354
9850
|
},
|
|
9355
9851
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -9373,8 +9869,11 @@ var AUTHORING_RULES = [
|
|
|
9373
9869
|
input: "normalized",
|
|
9374
9870
|
commands: ALL,
|
|
9375
9871
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9376
|
-
|
|
9377
|
-
|
|
9872
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
9873
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
9874
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
9875
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9876
|
+
runtimeTypes: ["object"],
|
|
9378
9877
|
run: (stack) => validateManagedApiMethods(stack)
|
|
9379
9878
|
},
|
|
9380
9879
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -9396,19 +9895,38 @@ var AUTHORING_RULES = [
|
|
|
9396
9895
|
},
|
|
9397
9896
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
9398
9897
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
9898
|
+
//
|
|
9899
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
9900
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
9901
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
9902
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
9903
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
9904
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
9905
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
9906
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
9907
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
9908
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
9909
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
9910
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
9911
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
9912
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
9913
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
9914
|
+
// advisory channel and never block (#4463 P1).
|
|
9399
9915
|
{
|
|
9400
9916
|
name: "validateWidgetBindings",
|
|
9401
9917
|
tier: "gating",
|
|
9402
9918
|
input: "parsed",
|
|
9403
9919
|
commands: ALL,
|
|
9404
9920
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
9405
|
-
surfaces:
|
|
9406
|
-
|
|
9921
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9922
|
+
runtimeTypes: ["dashboard"],
|
|
9407
9923
|
run: (stack) => validateWidgetBindings(stack)
|
|
9408
9924
|
},
|
|
9409
|
-
// ADR-0049 / #3367 — a header
|
|
9410
|
-
//
|
|
9411
|
-
//
|
|
9925
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
9926
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
9927
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
9928
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
9929
|
+
// Unresolved `url` routes stay advisory.
|
|
9412
9930
|
{
|
|
9413
9931
|
name: "validateDashboardActionRefs",
|
|
9414
9932
|
tier: "gating",
|
|
@@ -9433,6 +9951,28 @@ var AUTHORING_RULES = [
|
|
|
9433
9951
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
9434
9952
|
run: (stack) => validateFilterTokens(stack)
|
|
9435
9953
|
},
|
|
9954
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
9955
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
9956
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
9957
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
9958
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
9959
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
9960
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
9961
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
9962
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
9963
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
9964
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
9965
|
+
// vocabulary is near enough to reach for.
|
|
9966
|
+
{
|
|
9967
|
+
name: "validatePresetComparands",
|
|
9968
|
+
tier: "gating",
|
|
9969
|
+
input: "parsed",
|
|
9970
|
+
commands: ALL,
|
|
9971
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
9972
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9973
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
9974
|
+
run: (stack) => validatePresetComparands(stack)
|
|
9975
|
+
},
|
|
9436
9976
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9437
9977
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9438
9978
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -9651,7 +10191,7 @@ var AUTHORING_RULES = [
|
|
|
9651
10191
|
commands: ALL,
|
|
9652
10192
|
source: "packages/lint/src/validate-record-title.ts",
|
|
9653
10193
|
surfaces: CLI_ONLY,
|
|
9654
|
-
surfaceReason:
|
|
10194
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9655
10195
|
run: (stack) => validateRecordTitle(stack)
|
|
9656
10196
|
},
|
|
9657
10197
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -9664,7 +10204,7 @@ var AUTHORING_RULES = [
|
|
|
9664
10204
|
commands: ALL,
|
|
9665
10205
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
9666
10206
|
surfaces: CLI_ONLY,
|
|
9667
|
-
surfaceReason:
|
|
10207
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9668
10208
|
run: (stack) => validateSemanticRoles(stack)
|
|
9669
10209
|
},
|
|
9670
10210
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -9875,7 +10415,7 @@ var AUTHORING_RULES = [
|
|
|
9875
10415
|
commands: ALL,
|
|
9876
10416
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
9877
10417
|
surfaces: CLI_ONLY,
|
|
9878
|
-
surfaceReason:
|
|
10418
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9879
10419
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
9880
10420
|
severity: "warning",
|
|
9881
10421
|
rule: f.rule,
|
|
@@ -9894,8 +10434,12 @@ var AUTHORING_RULES = [
|
|
|
9894
10434
|
input: "parsed",
|
|
9895
10435
|
commands: ALL,
|
|
9896
10436
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
9897
|
-
|
|
9898
|
-
|
|
10437
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
10438
|
+
// object does not carry is broken from the first record; only the error
|
|
10439
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
10440
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
10441
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10442
|
+
runtimeTypes: ["object"],
|
|
9899
10443
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
9900
10444
|
severity: f.severity,
|
|
9901
10445
|
rule: f.rule,
|
|
@@ -9936,12 +10480,12 @@ var AUTHORING_RULES = [
|
|
|
9936
10480
|
commands: ["validate", "build"],
|
|
9937
10481
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9938
10482
|
surfaces: CLI_ONLY,
|
|
9939
|
-
surfaceReason:
|
|
10483
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9940
10484
|
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.",
|
|
9941
10485
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9942
10486
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9943
10487
|
rule: f.rule,
|
|
9944
|
-
where: f.
|
|
10488
|
+
where: f.where,
|
|
9945
10489
|
path: f.path,
|
|
9946
10490
|
message: f.message,
|
|
9947
10491
|
hint: f.fix ?? ""
|
|
@@ -9957,12 +10501,12 @@ var AUTHORING_RULES = [
|
|
|
9957
10501
|
commands: ["validate", "build"],
|
|
9958
10502
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9959
10503
|
surfaces: CLI_ONLY,
|
|
9960
|
-
surfaceReason:
|
|
10504
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9961
10505
|
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.",
|
|
9962
10506
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9963
10507
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9964
10508
|
rule: f.rule,
|
|
9965
|
-
where: f.
|
|
10509
|
+
where: f.where,
|
|
9966
10510
|
path: f.path,
|
|
9967
10511
|
message: f.message,
|
|
9968
10512
|
hint: f.fix ?? ""
|
|
@@ -9979,12 +10523,12 @@ var AUTHORING_RULES = [
|
|
|
9979
10523
|
commands: ["validate", "build"],
|
|
9980
10524
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9981
10525
|
surfaces: CLI_ONLY,
|
|
9982
|
-
surfaceReason:
|
|
10526
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9983
10527
|
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.",
|
|
9984
10528
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9985
10529
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9986
10530
|
rule: f.rule,
|
|
9987
|
-
where: f.
|
|
10531
|
+
where: f.where,
|
|
9988
10532
|
path: f.path,
|
|
9989
10533
|
message: f.message,
|
|
9990
10534
|
hint: f.fix ?? ""
|
|
@@ -10112,16 +10656,26 @@ var AUTHORING_RULES = [
|
|
|
10112
10656
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10113
10657
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10114
10658
|
},
|
|
10115
|
-
// #4698 — the "declared but never read" gate, for the
|
|
10116
|
-
// predicate is EXACT rather than inferred.
|
|
10117
|
-
//
|
|
10118
|
-
//
|
|
10119
|
-
//
|
|
10120
|
-
//
|
|
10121
|
-
//
|
|
10122
|
-
//
|
|
10123
|
-
//
|
|
10124
|
-
//
|
|
10659
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
10660
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
10661
|
+
//
|
|
10662
|
+
// - `condition` has a single runtime consumer
|
|
10663
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
10664
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
10665
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
10666
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
10667
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
10668
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
10669
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
10670
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
10671
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
10672
|
+
//
|
|
10673
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
10674
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
10675
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
10676
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
10677
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
10678
|
+
// yet true.
|
|
10125
10679
|
{
|
|
10126
10680
|
name: "validateSharingRuleEnforceability",
|
|
10127
10681
|
tier: "gating",
|
|
@@ -10129,7 +10683,7 @@ var AUTHORING_RULES = [
|
|
|
10129
10683
|
commands: ALL,
|
|
10130
10684
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10131
10685
|
surfaces: CLI_ONLY,
|
|
10132
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
10686
|
+
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.",
|
|
10133
10687
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10134
10688
|
},
|
|
10135
10689
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10168,8 +10722,15 @@ var AUTHORING_RULES = [
|
|
|
10168
10722
|
input: "parsed",
|
|
10169
10723
|
commands: ALL,
|
|
10170
10724
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10171
|
-
|
|
10172
|
-
|
|
10725
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
10726
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
10727
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
10728
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
10729
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
10730
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
10731
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
10732
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10733
|
+
runtimeTypes: ["object"],
|
|
10173
10734
|
run: (stack) => validateRuleCompilability(stack)
|
|
10174
10735
|
},
|
|
10175
10736
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10190,8 +10751,12 @@ var AUTHORING_RULES = [
|
|
|
10190
10751
|
input: "parsed",
|
|
10191
10752
|
commands: ALL,
|
|
10192
10753
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10193
|
-
|
|
10194
|
-
|
|
10754
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
10755
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
10756
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
10757
|
+
// format set is only enumerated once a schema actually names a format.
|
|
10758
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10759
|
+
runtimeTypes: ["object"],
|
|
10195
10760
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10196
10761
|
}
|
|
10197
10762
|
];
|
|
@@ -10233,7 +10798,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
10233
10798
|
permission: "permissions",
|
|
10234
10799
|
book: "books"
|
|
10235
10800
|
};
|
|
10236
|
-
|
|
10801
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
10802
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
10803
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
10804
|
+
return objects.filter((entry) => {
|
|
10805
|
+
if (!entry || typeof entry !== "object") return true;
|
|
10806
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
10807
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
10808
|
+
if (reachable.has(owner)) return true;
|
|
10809
|
+
return isSystemObject(entry);
|
|
10810
|
+
});
|
|
10811
|
+
}
|
|
10812
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
10813
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
10814
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
10237
10815
|
function runtimeAuthoringRulesFor(type) {
|
|
10238
10816
|
return AUTHORING_RULES.filter(
|
|
10239
10817
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -10259,7 +10837,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10259
10837
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10260
10838
|
const baseline = {};
|
|
10261
10839
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
10262
|
-
const
|
|
10840
|
+
const raw = args.context?.[key] ?? [];
|
|
10841
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
10263
10842
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10264
10843
|
}
|
|
10265
10844
|
const candidate = {
|
|
@@ -10293,7 +10872,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10293
10872
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
10294
10873
|
type: args.type,
|
|
10295
10874
|
item: args.item,
|
|
10296
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
10875
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
10876
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10297
10877
|
});
|
|
10298
10878
|
if (!snapshots) return empty;
|
|
10299
10879
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
@@ -10307,6 +10887,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10307
10887
|
}
|
|
10308
10888
|
export {
|
|
10309
10889
|
buildRuntimeWriteSnapshots,
|
|
10890
|
+
narrowObjectsToPackageClosure,
|
|
10310
10891
|
runRuntimeAuthoringRules,
|
|
10311
10892
|
runtimeAuthoringRulesFor,
|
|
10312
10893
|
runtimeGatedTypes,
|