@objectstack/lint 17.0.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +851 -0
- package/dist/index.cjs +1126 -320
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -72
- package/dist/index.d.ts +520 -72
- package/dist/index.js +1096 -305
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-cV-l_vtC.d.cts} +109 -3
- package/dist/{runtime-H-nDodRy.d.ts → runtime-cV-l_vtC.d.ts} +109 -3
- package/dist/runtime.cjs +1188 -433
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1161 -402
- package/dist/runtime.js.map +1 -1
- package/package.json +7 -6
package/dist/runtime.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++) {
|
|
@@ -2024,6 +2185,26 @@ function validateSearchableFields(stack) {
|
|
|
2024
2185
|
if (!isRec3(view)) continue;
|
|
2025
2186
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
2026
2187
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
2188
|
+
if (view.viewKind === "list" && !isRec3(view.config)) {
|
|
2189
|
+
check(
|
|
2190
|
+
view.searchableFields,
|
|
2191
|
+
listViewObject(view) ?? viewObject,
|
|
2192
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2193
|
+
`views[${vi}].searchableFields`,
|
|
2194
|
+
"list-view searchableFields",
|
|
2195
|
+
"narrowing"
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
if (view.viewKind === "list" && isRec3(view.config)) {
|
|
2199
|
+
check(
|
|
2200
|
+
view.config.searchableFields,
|
|
2201
|
+
listViewObject(view.config) ?? viewObject,
|
|
2202
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2203
|
+
`views[${vi}].config.searchableFields`,
|
|
2204
|
+
"list-view searchableFields",
|
|
2205
|
+
"narrowing"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2027
2208
|
if (isRec3(view.list)) {
|
|
2028
2209
|
check(
|
|
2029
2210
|
view.list.searchableFields,
|
|
@@ -2055,33 +2236,244 @@ function listViewObject(listView) {
|
|
|
2055
2236
|
return isRec3(data) ? strName3(data.object) : void 0;
|
|
2056
2237
|
}
|
|
2057
2238
|
|
|
2058
|
-
// src/
|
|
2239
|
+
// src/validate-sortable-fields.ts
|
|
2240
|
+
import { isVirtualSearchField as isVirtualSearchField2 } from "@objectstack/spec/data";
|
|
2241
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
2242
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
2243
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
2059
2244
|
function isRec4(v) {
|
|
2060
2245
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2061
2246
|
}
|
|
2062
2247
|
function strName4(v) {
|
|
2063
2248
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2064
2249
|
}
|
|
2250
|
+
function readSortKeys(declared) {
|
|
2251
|
+
const fromShorthand = (raw, at) => {
|
|
2252
|
+
const trimmed = raw.trim();
|
|
2253
|
+
if (!trimmed) return void 0;
|
|
2254
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
2255
|
+
return bare ? { field: bare, at } : void 0;
|
|
2256
|
+
};
|
|
2257
|
+
if (typeof declared === "string") {
|
|
2258
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
2259
|
+
}
|
|
2260
|
+
if (Array.isArray(declared)) {
|
|
2261
|
+
const keys = [];
|
|
2262
|
+
for (let i = 0; i < declared.length; i++) {
|
|
2263
|
+
const el = declared[i];
|
|
2264
|
+
if (typeof el === "string") {
|
|
2265
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
2266
|
+
if (k) keys.push(k);
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
if (!isRec4(el)) continue;
|
|
2270
|
+
const field = strName4(el.field);
|
|
2271
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
2272
|
+
}
|
|
2273
|
+
return keys;
|
|
2274
|
+
}
|
|
2275
|
+
return [];
|
|
2276
|
+
}
|
|
2277
|
+
function suggest4(target, known) {
|
|
2278
|
+
let best;
|
|
2279
|
+
let bestScore = Infinity;
|
|
2280
|
+
for (const candidate of known) {
|
|
2281
|
+
const d = distance3(target, candidate);
|
|
2282
|
+
if (d < bestScore) {
|
|
2283
|
+
bestScore = d;
|
|
2284
|
+
best = candidate;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
2288
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
2289
|
+
}
|
|
2290
|
+
function distance3(a, b) {
|
|
2291
|
+
const m = a.length;
|
|
2292
|
+
const n = b.length;
|
|
2293
|
+
if (m === 0) return n;
|
|
2294
|
+
if (n === 0) return m;
|
|
2295
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2296
|
+
for (let i = 1; i <= m; i++) {
|
|
2297
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
2298
|
+
for (let j = 1; j <= n; j++) {
|
|
2299
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2300
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2301
|
+
}
|
|
2302
|
+
prev = curr;
|
|
2303
|
+
}
|
|
2304
|
+
return prev[n];
|
|
2305
|
+
}
|
|
2306
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
2307
|
+
const findings = [];
|
|
2308
|
+
if (declared === void 0 || declared === null) return findings;
|
|
2309
|
+
if (!objectName) return findings;
|
|
2310
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
2311
|
+
const target = fieldsByObject.get(objectName);
|
|
2312
|
+
if (!target) return findings;
|
|
2313
|
+
const known = target.names;
|
|
2314
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2315
|
+
for (const key of readSortKeys(declared)) {
|
|
2316
|
+
const name = key.field;
|
|
2317
|
+
const head = name.split(".")[0];
|
|
2318
|
+
if (SYSTEM_FIELDS.has(head)) {
|
|
2319
|
+
if (!name.includes(".") && anchors?.has(head)) {
|
|
2320
|
+
findings.push({
|
|
2321
|
+
severity: "warning",
|
|
2322
|
+
rule: SORT_FIELD_UNPROVISIONED,
|
|
2323
|
+
where,
|
|
2324
|
+
path: `${path}${key.at}`,
|
|
2325
|
+
message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
|
|
2326
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2327
|
+
});
|
|
2328
|
+
}
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
if (!known.has(head)) {
|
|
2332
|
+
const dotted = name.includes(".");
|
|
2333
|
+
findings.push({
|
|
2334
|
+
severity: "error",
|
|
2335
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
2336
|
+
where,
|
|
2337
|
+
path: `${path}${key.at}`,
|
|
2338
|
+
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)),
|
|
2339
|
+
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(", ")}.` : "")
|
|
2340
|
+
});
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
const meta = target.fields[name];
|
|
2344
|
+
if (isVirtualSearchField2(meta)) {
|
|
2345
|
+
const vtype = meta?.type;
|
|
2346
|
+
findings.push({
|
|
2347
|
+
severity: "error",
|
|
2348
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
2349
|
+
where,
|
|
2350
|
+
path: `${path}${key.at}`,
|
|
2351
|
+
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.`,
|
|
2352
|
+
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.`
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return findings;
|
|
2357
|
+
}
|
|
2358
|
+
function validateSortableFields(stack) {
|
|
2359
|
+
const findings = [];
|
|
2360
|
+
if (!isRec4(stack)) return findings;
|
|
2361
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec4(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
2362
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2363
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2364
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
2365
|
+
findings.push(
|
|
2366
|
+
...checkSortDeclaration(
|
|
2367
|
+
declared,
|
|
2368
|
+
objectName,
|
|
2369
|
+
fieldsByObject,
|
|
2370
|
+
where,
|
|
2371
|
+
path,
|
|
2372
|
+
subject,
|
|
2373
|
+
unprovisionedAnchors
|
|
2374
|
+
)
|
|
2375
|
+
);
|
|
2376
|
+
};
|
|
2377
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
2378
|
+
const obj = objects[oi];
|
|
2379
|
+
if (!isRec4(obj)) continue;
|
|
2380
|
+
const objName = strName4(obj.name);
|
|
2381
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
2382
|
+
if (isRec4(obj.listViews)) {
|
|
2383
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2384
|
+
if (!isRec4(lv)) continue;
|
|
2385
|
+
check(
|
|
2386
|
+
lv.sort,
|
|
2387
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
2388
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
2389
|
+
listViewObject2(lv) ?? objName,
|
|
2390
|
+
`${label2} \u203A listViews.${key}`,
|
|
2391
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
2392
|
+
"list-view sort"
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
2398
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
2399
|
+
const view = views[vi];
|
|
2400
|
+
if (!isRec4(view)) continue;
|
|
2401
|
+
const viewLabel2 = strName4(view.name) ?? strName4(view.objectName) ?? `#${vi}`;
|
|
2402
|
+
const viewObject = strName4(view.objectName) ?? strName4(view.object);
|
|
2403
|
+
if (view.viewKind === "list" && !isRec4(view.config)) {
|
|
2404
|
+
check(
|
|
2405
|
+
view.sort,
|
|
2406
|
+
listViewObject2(view) ?? viewObject,
|
|
2407
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
2408
|
+
`views[${vi}].sort`,
|
|
2409
|
+
"list-view sort"
|
|
2410
|
+
);
|
|
2411
|
+
}
|
|
2412
|
+
if (view.viewKind === "list" && isRec4(view.config)) {
|
|
2413
|
+
check(
|
|
2414
|
+
view.config.sort,
|
|
2415
|
+
listViewObject2(view.config) ?? viewObject,
|
|
2416
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
2417
|
+
`views[${vi}].config.sort`,
|
|
2418
|
+
"list-view sort"
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
if (isRec4(view.list)) {
|
|
2422
|
+
check(
|
|
2423
|
+
view.list.sort,
|
|
2424
|
+
listViewObject2(view.list) ?? viewObject,
|
|
2425
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2426
|
+
`views[${vi}].list.sort`,
|
|
2427
|
+
"list-view sort"
|
|
2428
|
+
);
|
|
2429
|
+
}
|
|
2430
|
+
if (isRec4(view.listViews)) {
|
|
2431
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2432
|
+
if (!isRec4(lv)) continue;
|
|
2433
|
+
check(
|
|
2434
|
+
lv.sort,
|
|
2435
|
+
listViewObject2(lv) ?? viewObject,
|
|
2436
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2437
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
2438
|
+
"list-view sort"
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return findings;
|
|
2444
|
+
}
|
|
2445
|
+
function listViewObject2(listView) {
|
|
2446
|
+
const data = listView.data;
|
|
2447
|
+
return isRec4(data) ? strName4(data.object) : void 0;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
// src/page-walk.ts
|
|
2451
|
+
function isRec5(v) {
|
|
2452
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2453
|
+
}
|
|
2454
|
+
function strName5(v) {
|
|
2455
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2456
|
+
}
|
|
2065
2457
|
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
2066
2458
|
function isSourceAuthoredPage(page) {
|
|
2067
|
-
const kind =
|
|
2459
|
+
const kind = strName5(page.kind);
|
|
2068
2460
|
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
2069
2461
|
}
|
|
2070
2462
|
function walkPageComponents(page, pagePath) {
|
|
2071
2463
|
const out = [];
|
|
2072
|
-
if (!
|
|
2073
|
-
const pageObject =
|
|
2464
|
+
if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
|
|
2465
|
+
const pageObject = strName5(page.object);
|
|
2074
2466
|
const visit = (node, path, inheritedObject) => {
|
|
2075
|
-
if (!
|
|
2076
|
-
const props =
|
|
2077
|
-
const dataSource =
|
|
2078
|
-
const objectName =
|
|
2467
|
+
if (!isRec5(node)) return;
|
|
2468
|
+
const props = isRec5(node.properties) ? node.properties : void 0;
|
|
2469
|
+
const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
|
|
2470
|
+
const objectName = strName5(dataSource?.object) ?? strName5(props?.object) ?? inheritedObject;
|
|
2079
2471
|
out.push({ component: node, path, objectName });
|
|
2080
2472
|
if (!props) return;
|
|
2081
2473
|
if (Array.isArray(props.items)) {
|
|
2082
2474
|
for (let i = 0; i < props.items.length; i++) {
|
|
2083
2475
|
const item = props.items[i];
|
|
2084
|
-
if (!
|
|
2476
|
+
if (!isRec5(item) || !Array.isArray(item.children)) continue;
|
|
2085
2477
|
for (let c = 0; c < item.children.length; c++) {
|
|
2086
2478
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
2087
2479
|
}
|
|
@@ -2103,12 +2495,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
2103
2495
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
2104
2496
|
for (let r = 0; r < regions.length; r++) {
|
|
2105
2497
|
const region = regions[r];
|
|
2106
|
-
if (!
|
|
2498
|
+
if (!isRec5(region) || !Array.isArray(region.components)) continue;
|
|
2107
2499
|
for (let c = 0; c < region.components.length; c++) {
|
|
2108
2500
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
2109
2501
|
}
|
|
2110
2502
|
}
|
|
2111
|
-
const slots =
|
|
2503
|
+
const slots = isRec5(page.slots) ? page.slots : void 0;
|
|
2112
2504
|
if (slots) {
|
|
2113
2505
|
for (const [slot, value] of Object.entries(slots)) {
|
|
2114
2506
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2130,13 +2522,13 @@ function asArray8(v) {
|
|
|
2130
2522
|
}
|
|
2131
2523
|
return [];
|
|
2132
2524
|
}
|
|
2133
|
-
function
|
|
2525
|
+
function strName6(v) {
|
|
2134
2526
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2135
2527
|
}
|
|
2136
2528
|
function strList(v) {
|
|
2137
2529
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2138
2530
|
}
|
|
2139
|
-
function
|
|
2531
|
+
function distance4(a, b) {
|
|
2140
2532
|
const m = a.length;
|
|
2141
2533
|
const n = b.length;
|
|
2142
2534
|
if (m === 0) return n;
|
|
@@ -2152,11 +2544,11 @@ function distance3(a, b) {
|
|
|
2152
2544
|
}
|
|
2153
2545
|
return prev[n];
|
|
2154
2546
|
}
|
|
2155
|
-
function
|
|
2547
|
+
function suggest5(target, known) {
|
|
2156
2548
|
let best;
|
|
2157
2549
|
let bestScore = Infinity;
|
|
2158
2550
|
for (const candidate of known) {
|
|
2159
|
-
const d =
|
|
2551
|
+
const d = distance4(target, candidate);
|
|
2160
2552
|
if (d < bestScore) {
|
|
2161
2553
|
bestScore = d;
|
|
2162
2554
|
best = candidate;
|
|
@@ -2168,13 +2560,13 @@ function suggest4(target, known) {
|
|
|
2168
2560
|
function collectActionNames(stack) {
|
|
2169
2561
|
const names = /* @__PURE__ */ new Set();
|
|
2170
2562
|
for (const action of asArray8(stack.actions)) {
|
|
2171
|
-
const n =
|
|
2563
|
+
const n = strName6(action?.name);
|
|
2172
2564
|
if (n) names.add(n);
|
|
2173
2565
|
}
|
|
2174
2566
|
for (const obj of asArray8(stack.objects)) {
|
|
2175
2567
|
if (!obj || typeof obj !== "object") continue;
|
|
2176
2568
|
for (const action of asArray8(obj.actions)) {
|
|
2177
|
-
const n =
|
|
2569
|
+
const n = strName6(action?.name);
|
|
2178
2570
|
if (n) names.add(n);
|
|
2179
2571
|
}
|
|
2180
2572
|
}
|
|
@@ -2191,7 +2583,7 @@ function validateActionNameRefs(stack) {
|
|
|
2191
2583
|
rule: ACTION_NAME_UNDEFINED,
|
|
2192
2584
|
where,
|
|
2193
2585
|
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.` +
|
|
2586
|
+
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
2587
|
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
2588
|
});
|
|
2197
2589
|
};
|
|
@@ -2217,7 +2609,7 @@ function validateActionNameRefs(stack) {
|
|
|
2217
2609
|
if (!def || typeof def !== "object") continue;
|
|
2218
2610
|
if (def.execution !== "aggregate") continue;
|
|
2219
2611
|
if (def.actionDef !== void 0) continue;
|
|
2220
|
-
const name =
|
|
2612
|
+
const name = strName6(def.name);
|
|
2221
2613
|
if (!name) continue;
|
|
2222
2614
|
check(
|
|
2223
2615
|
name,
|
|
@@ -2232,7 +2624,7 @@ function validateActionNameRefs(stack) {
|
|
|
2232
2624
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2233
2625
|
const view = views[vi];
|
|
2234
2626
|
if (!view || typeof view !== "object") continue;
|
|
2235
|
-
const viewName =
|
|
2627
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? `#${vi}`;
|
|
2236
2628
|
const owner = `view "${viewName}"`;
|
|
2237
2629
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
2238
2630
|
const listViews = view.listViews;
|
|
@@ -2248,7 +2640,7 @@ function validateActionNameRefs(stack) {
|
|
|
2248
2640
|
if (!obj || typeof obj !== "object") continue;
|
|
2249
2641
|
const objListViews = obj.listViews;
|
|
2250
2642
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
2251
|
-
const owner = `object "${
|
|
2643
|
+
const owner = `object "${strName6(obj.name) ?? `#${oi}`}"`;
|
|
2252
2644
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
2253
2645
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
2254
2646
|
}
|
|
@@ -2257,7 +2649,7 @@ function validateActionNameRefs(stack) {
|
|
|
2257
2649
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2258
2650
|
const page = pages[pi];
|
|
2259
2651
|
if (!page || typeof page !== "object") continue;
|
|
2260
|
-
const pageName =
|
|
2652
|
+
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2261
2653
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2262
2654
|
const props = component.properties;
|
|
2263
2655
|
if (!props || typeof props !== "object") continue;
|
|
@@ -2265,7 +2657,7 @@ function validateActionNameRefs(stack) {
|
|
|
2265
2657
|
for (let ai = 0; ai < names.length; ai++) {
|
|
2266
2658
|
check(
|
|
2267
2659
|
names[ai],
|
|
2268
|
-
`page "${pageName}" \xB7 component "${
|
|
2660
|
+
`page "${pageName}" \xB7 component "${strName6(component.type) ?? "?"}"`,
|
|
2269
2661
|
`${path}.properties.actionNames[${ai}]`,
|
|
2270
2662
|
"Quick-actions bar"
|
|
2271
2663
|
);
|
|
@@ -2276,7 +2668,7 @@ function validateActionNameRefs(stack) {
|
|
|
2276
2668
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2277
2669
|
const app = apps[ai];
|
|
2278
2670
|
if (!app || typeof app !== "object") continue;
|
|
2279
|
-
const appName =
|
|
2671
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
2280
2672
|
const walkNav = (items, basePath) => {
|
|
2281
2673
|
const navItems = asArray8(items);
|
|
2282
2674
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -2284,20 +2676,20 @@ function validateActionNameRefs(stack) {
|
|
|
2284
2676
|
if (!nav || typeof nav !== "object") continue;
|
|
2285
2677
|
const navPath = `${basePath}[${ni}]`;
|
|
2286
2678
|
const actionDef = nav.actionDef;
|
|
2287
|
-
const actionName =
|
|
2679
|
+
const actionName = strName6(actionDef?.actionName);
|
|
2288
2680
|
if (nav.type === "action" && actionName) {
|
|
2289
2681
|
check(
|
|
2290
2682
|
actionName,
|
|
2291
|
-
`app "${appName}" \xB7 nav "${
|
|
2683
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2292
2684
|
`${navPath}.actionDef.actionName`,
|
|
2293
2685
|
"Navigation action item"
|
|
2294
2686
|
);
|
|
2295
2687
|
}
|
|
2296
|
-
const runAction =
|
|
2688
|
+
const runAction = strName6(nav.runAction);
|
|
2297
2689
|
if (nav.type === "object" && runAction) {
|
|
2298
2690
|
check(
|
|
2299
2691
|
runAction,
|
|
2300
|
-
`app "${appName}" \xB7 nav "${
|
|
2692
|
+
`app "${appName}" \xB7 nav "${strName6(nav.id) ?? `#${ni}`}"`,
|
|
2301
2693
|
`${navPath}.runAction`,
|
|
2302
2694
|
"Navigation deep-link auto-run"
|
|
2303
2695
|
);
|
|
@@ -2324,23 +2716,23 @@ function asArray9(v) {
|
|
|
2324
2716
|
}
|
|
2325
2717
|
return [];
|
|
2326
2718
|
}
|
|
2327
|
-
function
|
|
2719
|
+
function strName7(v) {
|
|
2328
2720
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2329
2721
|
}
|
|
2330
|
-
function
|
|
2722
|
+
function isRec6(v) {
|
|
2331
2723
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2332
2724
|
}
|
|
2333
2725
|
function fieldRefsFrom(value, basePath) {
|
|
2334
2726
|
const out = [];
|
|
2335
2727
|
const one = (v, path) => {
|
|
2336
|
-
const bare =
|
|
2728
|
+
const bare = strName7(v);
|
|
2337
2729
|
if (bare) {
|
|
2338
2730
|
out.push({ name: bare, path });
|
|
2339
2731
|
return;
|
|
2340
2732
|
}
|
|
2341
|
-
if (!
|
|
2342
|
-
const named =
|
|
2343
|
-
if (named) out.push({ name: named, path: `${path}.${
|
|
2733
|
+
if (!isRec6(v)) return;
|
|
2734
|
+
const named = strName7(v.field) ?? strName7(v.name);
|
|
2735
|
+
if (named) out.push({ name: named, path: `${path}.${strName7(v.field) ? "field" : "name"}` });
|
|
2344
2736
|
};
|
|
2345
2737
|
if (Array.isArray(value)) {
|
|
2346
2738
|
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
@@ -2366,7 +2758,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2366
2758
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2367
2759
|
"record:path": { props: ["statusField"] },
|
|
2368
2760
|
"element:number": { props: ["field"] },
|
|
2369
|
-
|
|
2761
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
2762
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
2763
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
2764
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
2765
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
2766
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
2370
2767
|
"element:form": { props: ["fields"] },
|
|
2371
2768
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2372
2769
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -2396,18 +2793,18 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2396
2793
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2397
2794
|
for (let si = 0; si < sections.length; si++) {
|
|
2398
2795
|
const section = sections[si];
|
|
2399
|
-
if (!
|
|
2796
|
+
if (!isRec6(section)) continue;
|
|
2400
2797
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2401
2798
|
}
|
|
2402
2799
|
}
|
|
2403
2800
|
return refs;
|
|
2404
2801
|
}
|
|
2405
2802
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2406
|
-
const add =
|
|
2407
|
-
const picker = add &&
|
|
2803
|
+
const add = isRec6(props.add) ? props.add : void 0;
|
|
2804
|
+
const picker = add && isRec6(add.picker) ? add.picker : void 0;
|
|
2408
2805
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2409
2806
|
return {
|
|
2410
|
-
relatedObject:
|
|
2807
|
+
relatedObject: strName7(props.objectName),
|
|
2411
2808
|
related: [
|
|
2412
2809
|
...fieldRefsFrom(props.columns, at("columns")),
|
|
2413
2810
|
...sortFieldRefs(props.sort, at("sort")),
|
|
@@ -2416,7 +2813,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2416
2813
|
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
2417
2814
|
],
|
|
2418
2815
|
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
2419
|
-
pickerObject: picker ?
|
|
2816
|
+
pickerObject: picker ? strName7(picker.object) : void 0,
|
|
2420
2817
|
picker: picker ? [
|
|
2421
2818
|
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
2422
2819
|
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
@@ -2425,13 +2822,13 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2425
2822
|
}
|
|
2426
2823
|
function indexObjectFields(stack) {
|
|
2427
2824
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2428
|
-
if (!
|
|
2825
|
+
if (!isRec6(stack)) return objectFields;
|
|
2429
2826
|
for (const obj of asArray9(stack.objects)) {
|
|
2430
|
-
const name =
|
|
2827
|
+
const name = strName7(obj.name);
|
|
2431
2828
|
if (!name) continue;
|
|
2432
2829
|
const names = /* @__PURE__ */ new Set();
|
|
2433
2830
|
for (const f of asArray9(obj.fields)) {
|
|
2434
|
-
const fn =
|
|
2831
|
+
const fn = strName7(f.name);
|
|
2435
2832
|
if (fn) names.add(fn);
|
|
2436
2833
|
}
|
|
2437
2834
|
objectFields.set(name, names);
|
|
@@ -2479,15 +2876,15 @@ function validatePageFieldBindings(stack) {
|
|
|
2479
2876
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2480
2877
|
const page = pages[pi];
|
|
2481
2878
|
if (!page || typeof page !== "object") continue;
|
|
2482
|
-
const pageName =
|
|
2879
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2483
2880
|
const checkRefs = (refs, objectName, where) => {
|
|
2484
2881
|
findings.push(
|
|
2485
2882
|
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2486
2883
|
);
|
|
2487
2884
|
};
|
|
2488
2885
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2489
|
-
const type =
|
|
2490
|
-
const props =
|
|
2886
|
+
const type = strName7(component.type);
|
|
2887
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2491
2888
|
if (!type || !props) continue;
|
|
2492
2889
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2493
2890
|
const base = `${path}.properties`;
|
|
@@ -2502,16 +2899,16 @@ function validatePageFieldBindings(stack) {
|
|
|
2502
2899
|
if (!refs) continue;
|
|
2503
2900
|
checkRefs(refs, objectName, where);
|
|
2504
2901
|
}
|
|
2505
|
-
const cfg =
|
|
2902
|
+
const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2506
2903
|
if (cfg) {
|
|
2507
|
-
const cfgObject =
|
|
2904
|
+
const cfgObject = strName7(cfg.source) ?? strName7(page.object);
|
|
2508
2905
|
const base = `pages[${pi}].interfaceConfig`;
|
|
2509
2906
|
const refs = [
|
|
2510
2907
|
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
2511
2908
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2512
2909
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2513
2910
|
];
|
|
2514
|
-
const userFilters =
|
|
2911
|
+
const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2515
2912
|
if (userFilters) {
|
|
2516
2913
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2517
2914
|
}
|
|
@@ -2533,16 +2930,16 @@ function asArray10(v) {
|
|
|
2533
2930
|
}
|
|
2534
2931
|
return [];
|
|
2535
2932
|
}
|
|
2536
|
-
function
|
|
2933
|
+
function strName8(v) {
|
|
2537
2934
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2538
2935
|
}
|
|
2539
2936
|
function strList2(v) {
|
|
2540
2937
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2541
2938
|
}
|
|
2542
|
-
function
|
|
2939
|
+
function isRec7(v) {
|
|
2543
2940
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2544
2941
|
}
|
|
2545
|
-
function
|
|
2942
|
+
function distance5(a, b) {
|
|
2546
2943
|
const m = a.length;
|
|
2547
2944
|
const n = b.length;
|
|
2548
2945
|
if (m === 0) return n;
|
|
@@ -2558,11 +2955,11 @@ function distance4(a, b) {
|
|
|
2558
2955
|
}
|
|
2559
2956
|
return prev[n];
|
|
2560
2957
|
}
|
|
2561
|
-
function
|
|
2958
|
+
function suggest6(target, known) {
|
|
2562
2959
|
let best;
|
|
2563
2960
|
let bestScore = Infinity;
|
|
2564
2961
|
for (const c of known) {
|
|
2565
|
-
const d =
|
|
2962
|
+
const d = distance5(target, c);
|
|
2566
2963
|
if (d < bestScore) {
|
|
2567
2964
|
bestScore = d;
|
|
2568
2965
|
best = c;
|
|
@@ -2578,16 +2975,16 @@ function list2(names) {
|
|
|
2578
2975
|
function indexDatasets(stack) {
|
|
2579
2976
|
const out = /* @__PURE__ */ new Map();
|
|
2580
2977
|
for (const ds of asArray10(stack.datasets)) {
|
|
2581
|
-
const name =
|
|
2978
|
+
const name = strName8(ds.name);
|
|
2582
2979
|
if (!name) continue;
|
|
2583
2980
|
const dimensions = /* @__PURE__ */ new Set();
|
|
2584
2981
|
for (const d of asArray10(ds.dimensions)) {
|
|
2585
|
-
const n =
|
|
2982
|
+
const n = strName8(d.name);
|
|
2586
2983
|
if (n) dimensions.add(n);
|
|
2587
2984
|
}
|
|
2588
2985
|
const measures = /* @__PURE__ */ new Set();
|
|
2589
2986
|
for (const m of asArray10(ds.measures)) {
|
|
2590
|
-
const n =
|
|
2987
|
+
const n = strName8(m.name);
|
|
2591
2988
|
if (n) measures.add(n);
|
|
2592
2989
|
}
|
|
2593
2990
|
out.set(name, { dimensions, measures });
|
|
@@ -2610,7 +3007,7 @@ function validateChartBindings(stack) {
|
|
|
2610
3007
|
where: binding.where,
|
|
2611
3008
|
path: `${binding.path}.dataset`,
|
|
2612
3009
|
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())}.${
|
|
3010
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
2614
3011
|
});
|
|
2615
3012
|
return;
|
|
2616
3013
|
}
|
|
@@ -2622,7 +3019,7 @@ function validateChartBindings(stack) {
|
|
|
2622
3019
|
where: binding.where,
|
|
2623
3020
|
path,
|
|
2624
3021
|
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)}.${
|
|
3022
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
2626
3023
|
});
|
|
2627
3024
|
};
|
|
2628
3025
|
const measureRef = (name, path, selected2) => {
|
|
@@ -2633,7 +3030,7 @@ function validateChartBindings(stack) {
|
|
|
2633
3030
|
where: binding.where,
|
|
2634
3031
|
path,
|
|
2635
3032
|
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)}.${
|
|
3033
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
2637
3034
|
});
|
|
2638
3035
|
return;
|
|
2639
3036
|
}
|
|
@@ -2668,26 +3065,26 @@ function validateChartBindings(stack) {
|
|
|
2668
3065
|
const reports = asArray10(stack.reports);
|
|
2669
3066
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2670
3067
|
const report = reports[ri];
|
|
2671
|
-
if (!
|
|
2672
|
-
const reportName =
|
|
3068
|
+
if (!isRec7(report)) continue;
|
|
3069
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
2673
3070
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2674
|
-
if (!
|
|
3071
|
+
if (!isRec7(chart)) return;
|
|
2675
3072
|
check({
|
|
2676
3073
|
dataset,
|
|
2677
3074
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
2678
3075
|
// it in lets the yAxis "declared but not selected" check work without
|
|
2679
3076
|
// reporting the selection itself twice.
|
|
2680
3077
|
values: { names: values, path: `${path}.values` },
|
|
2681
|
-
xAxis:
|
|
2682
|
-
yAxis:
|
|
2683
|
-
series: asArray10(chart.series).map((s, si) => ({ name:
|
|
3078
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
3079
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
3080
|
+
series: asArray10(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
2684
3081
|
where,
|
|
2685
3082
|
path: `${path}.chart`
|
|
2686
3083
|
});
|
|
2687
3084
|
};
|
|
2688
3085
|
checkReportChart(
|
|
2689
3086
|
report.chart,
|
|
2690
|
-
|
|
3087
|
+
strName8(report.dataset),
|
|
2691
3088
|
strList2(report.values),
|
|
2692
3089
|
`report "${reportName}" \xB7 chart`,
|
|
2693
3090
|
`reports[${ri}]`
|
|
@@ -2695,22 +3092,22 @@ function validateChartBindings(stack) {
|
|
|
2695
3092
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2696
3093
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2697
3094
|
const block = blocks[bi];
|
|
2698
|
-
if (!
|
|
3095
|
+
if (!isRec7(block)) continue;
|
|
2699
3096
|
checkReportChart(
|
|
2700
3097
|
block.chart,
|
|
2701
|
-
|
|
3098
|
+
strName8(block.dataset),
|
|
2702
3099
|
strList2(block.values),
|
|
2703
|
-
`report "${reportName}" \xB7 block "${
|
|
3100
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
2704
3101
|
`reports[${ri}].blocks[${bi}]`
|
|
2705
3102
|
);
|
|
2706
3103
|
}
|
|
2707
3104
|
}
|
|
2708
3105
|
const checkListChart = (container, where, path) => {
|
|
2709
|
-
if (!
|
|
3106
|
+
if (!isRec7(container)) return;
|
|
2710
3107
|
const chart = container.chart;
|
|
2711
|
-
if (!
|
|
3108
|
+
if (!isRec7(chart)) return;
|
|
2712
3109
|
check({
|
|
2713
|
-
dataset:
|
|
3110
|
+
dataset: strName8(chart.dataset),
|
|
2714
3111
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
2715
3112
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
2716
3113
|
where,
|
|
@@ -2720,10 +3117,10 @@ function validateChartBindings(stack) {
|
|
|
2720
3117
|
const views = asArray10(stack.views);
|
|
2721
3118
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2722
3119
|
const view = views[vi];
|
|
2723
|
-
if (!
|
|
2724
|
-
const viewName =
|
|
3120
|
+
if (!isRec7(view)) continue;
|
|
3121
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
2725
3122
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2726
|
-
if (
|
|
3123
|
+
if (isRec7(view.listViews)) {
|
|
2727
3124
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2728
3125
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2729
3126
|
}
|
|
@@ -2732,8 +3129,8 @@ function validateChartBindings(stack) {
|
|
|
2732
3129
|
const objects = asArray10(stack.objects);
|
|
2733
3130
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2734
3131
|
const obj = objects[oi];
|
|
2735
|
-
if (!
|
|
2736
|
-
const objName =
|
|
3132
|
+
if (!isRec7(obj) || !isRec7(obj.listViews)) continue;
|
|
3133
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
2737
3134
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2738
3135
|
checkListChart(
|
|
2739
3136
|
lv,
|
|
@@ -2745,19 +3142,19 @@ function validateChartBindings(stack) {
|
|
|
2745
3142
|
const pages = asArray10(stack.pages);
|
|
2746
3143
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2747
3144
|
const page = pages[pi];
|
|
2748
|
-
if (!
|
|
2749
|
-
const pageName =
|
|
3145
|
+
if (!isRec7(page)) continue;
|
|
3146
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
2750
3147
|
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:
|
|
3148
|
+
const props = isRec7(component.properties) ? component.properties : void 0;
|
|
3149
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
3150
|
+
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
3151
|
+
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
2755
3152
|
check({
|
|
2756
|
-
dataset:
|
|
3153
|
+
dataset: strName8(props.dataset),
|
|
2757
3154
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
2758
3155
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
2759
3156
|
series: [...axisRefs, ...seriesRefs],
|
|
2760
|
-
where: `page "${pageName}" \xB7 ${
|
|
3157
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
2761
3158
|
path: `${path}.properties`
|
|
2762
3159
|
});
|
|
2763
3160
|
}
|
|
@@ -2824,7 +3221,7 @@ function asArray12(v) {
|
|
|
2824
3221
|
}
|
|
2825
3222
|
return [];
|
|
2826
3223
|
}
|
|
2827
|
-
function
|
|
3224
|
+
function strName9(v) {
|
|
2828
3225
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2829
3226
|
}
|
|
2830
3227
|
function collectNavExposures(stack) {
|
|
@@ -2833,18 +3230,18 @@ function collectNavExposures(stack) {
|
|
|
2833
3230
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
2834
3231
|
const app = apps[ai];
|
|
2835
3232
|
if (!app || typeof app !== "object") continue;
|
|
2836
|
-
const appName =
|
|
3233
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
2837
3234
|
const walk = (items, basePath) => {
|
|
2838
3235
|
const navItems = asArray12(items);
|
|
2839
3236
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
2840
3237
|
const nav = navItems[ni];
|
|
2841
3238
|
if (!nav || typeof nav !== "object") continue;
|
|
2842
3239
|
const navPath = `${basePath}[${ni}]`;
|
|
2843
|
-
const objectName =
|
|
3240
|
+
const objectName = strName9(nav.objectName);
|
|
2844
3241
|
if (nav.type === "object" && objectName) {
|
|
2845
3242
|
out.push({
|
|
2846
3243
|
objectName,
|
|
2847
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3244
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
2848
3245
|
path: `${navPath}.objectName`
|
|
2849
3246
|
});
|
|
2850
3247
|
}
|
|
@@ -2868,7 +3265,7 @@ function validateNavAccess(stack) {
|
|
|
2868
3265
|
if (exposures.length === 0) return findings;
|
|
2869
3266
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
2870
3267
|
for (const obj of asArray12(stack.objects)) {
|
|
2871
|
-
const n =
|
|
3268
|
+
const n = strName9(obj.name);
|
|
2872
3269
|
if (n) ownObjects.add(n);
|
|
2873
3270
|
}
|
|
2874
3271
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -2898,13 +3295,13 @@ function validateNavAccess(stack) {
|
|
|
2898
3295
|
|
|
2899
3296
|
// src/validate-nav-target-refs.ts
|
|
2900
3297
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2901
|
-
var
|
|
3298
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2902
3299
|
function asArray13(v) {
|
|
2903
|
-
if (Array.isArray(v)) return v.filter(
|
|
2904
|
-
if (
|
|
3300
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
3301
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2905
3302
|
return [];
|
|
2906
3303
|
}
|
|
2907
|
-
function
|
|
3304
|
+
function strName10(v) {
|
|
2908
3305
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2909
3306
|
}
|
|
2910
3307
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -2916,14 +3313,14 @@ var NAV_TARGETS = [
|
|
|
2916
3313
|
function namesOf(collection) {
|
|
2917
3314
|
const out = /* @__PURE__ */ new Set();
|
|
2918
3315
|
for (const entry of asArray13(collection)) {
|
|
2919
|
-
const n =
|
|
3316
|
+
const n = strName10(entry.name);
|
|
2920
3317
|
if (n) out.add(n);
|
|
2921
3318
|
}
|
|
2922
3319
|
return out;
|
|
2923
3320
|
}
|
|
2924
3321
|
function validateNavTargetRefs(stack) {
|
|
2925
3322
|
const findings = [];
|
|
2926
|
-
if (!
|
|
3323
|
+
if (!isRec8(stack)) return findings;
|
|
2927
3324
|
const apps = asArray13(stack.apps);
|
|
2928
3325
|
if (apps.length === 0) return findings;
|
|
2929
3326
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2931,16 +3328,16 @@ function validateNavTargetRefs(stack) {
|
|
|
2931
3328
|
declared.set(collection, namesOf(stack[collection]));
|
|
2932
3329
|
}
|
|
2933
3330
|
for (const [ai, app] of apps.entries()) {
|
|
2934
|
-
const appName =
|
|
3331
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2935
3332
|
const walk = (items, basePath) => {
|
|
2936
3333
|
if (!Array.isArray(items)) return;
|
|
2937
3334
|
for (const [ni, raw] of items.entries()) {
|
|
2938
|
-
if (!
|
|
3335
|
+
if (!isRec8(raw)) continue;
|
|
2939
3336
|
const nav = raw;
|
|
2940
3337
|
const navPath = `${basePath}[${ni}]`;
|
|
2941
3338
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
2942
3339
|
if (nav.type !== type) continue;
|
|
2943
|
-
const target =
|
|
3340
|
+
const target = strName10(nav[prop]);
|
|
2944
3341
|
if (!target || isInterpolated2(target)) continue;
|
|
2945
3342
|
const known = declared.get(collection);
|
|
2946
3343
|
if (known.has(target)) continue;
|
|
@@ -2948,7 +3345,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2948
3345
|
findings.push({
|
|
2949
3346
|
severity: "warning",
|
|
2950
3347
|
rule: NAV_TARGET_UNRESOLVED,
|
|
2951
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3348
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
2952
3349
|
path: `${navPath}.${prop}`,
|
|
2953
3350
|
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
3351
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -2969,42 +3366,42 @@ function validateNavTargetRefs(stack) {
|
|
|
2969
3366
|
// src/validate-nav-object-servability.ts
|
|
2970
3367
|
import { canServeApiOperation } from "@objectstack/spec/data";
|
|
2971
3368
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2972
|
-
var
|
|
3369
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2973
3370
|
function asArray14(v) {
|
|
2974
|
-
if (Array.isArray(v)) return v.filter(
|
|
2975
|
-
if (
|
|
3371
|
+
if (Array.isArray(v)) return v.filter(isRec9);
|
|
3372
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => isRec9(def) ? { name, ...def } : { name });
|
|
2976
3373
|
return [];
|
|
2977
3374
|
}
|
|
2978
|
-
function
|
|
3375
|
+
function strName11(v) {
|
|
2979
3376
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2980
3377
|
}
|
|
2981
3378
|
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2982
3379
|
function validateNavObjectServability(stack) {
|
|
2983
3380
|
const findings = [];
|
|
2984
|
-
if (!
|
|
3381
|
+
if (!isRec9(stack)) return findings;
|
|
2985
3382
|
const apps = asArray14(stack.apps);
|
|
2986
3383
|
if (apps.length === 0) return findings;
|
|
2987
3384
|
const ownEnable = /* @__PURE__ */ new Map();
|
|
2988
3385
|
const objects = asArray14(stack.objects);
|
|
2989
3386
|
for (const [oi, obj] of objects.entries()) {
|
|
2990
|
-
const n =
|
|
3387
|
+
const n = strName11(obj.name);
|
|
2991
3388
|
if (!n) continue;
|
|
2992
3389
|
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
2993
3390
|
}
|
|
2994
3391
|
if (ownEnable.size === 0) return findings;
|
|
2995
3392
|
for (const [ai, app] of apps.entries()) {
|
|
2996
|
-
const appName =
|
|
3393
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
2997
3394
|
const walk = (items, basePath) => {
|
|
2998
3395
|
if (!Array.isArray(items)) return;
|
|
2999
3396
|
for (const [ni, raw] of items.entries()) {
|
|
3000
|
-
if (!
|
|
3397
|
+
if (!isRec9(raw)) continue;
|
|
3001
3398
|
const nav = raw;
|
|
3002
3399
|
const navPath = `${basePath}[${ni}]`;
|
|
3003
3400
|
if (nav.type === "object") {
|
|
3004
|
-
const target =
|
|
3401
|
+
const target = strName11(nav.objectName);
|
|
3005
3402
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3006
3403
|
if (target && declared && !canServeApiOperation(declared.enable, "list")) {
|
|
3007
|
-
const enable =
|
|
3404
|
+
const enable = isRec9(declared.enable) ? declared.enable : {};
|
|
3008
3405
|
const apiDisabled = enable.apiEnabled === false;
|
|
3009
3406
|
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
3407
|
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
@@ -3012,7 +3409,7 @@ function validateNavObjectServability(stack) {
|
|
|
3012
3409
|
findings.push({
|
|
3013
3410
|
severity: "error",
|
|
3014
3411
|
rule: NAV_OBJECT_UNSERVABLE,
|
|
3015
|
-
where: `app "${appName}" \xB7 nav "${
|
|
3412
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? strName11(nav.label) ?? `#${ni}`}"`,
|
|
3016
3413
|
// The nav entry is where the dead row is authored; the `enable`
|
|
3017
3414
|
// key that condemns it is named in the message, because the fix
|
|
3018
3415
|
// may belong at either end.
|
|
@@ -3039,27 +3436,27 @@ import { expandViewContainer } from "@objectstack/spec";
|
|
|
3039
3436
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
3040
3437
|
|
|
3041
3438
|
// src/view-walk.ts
|
|
3042
|
-
function
|
|
3439
|
+
function isRec10(v) {
|
|
3043
3440
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3044
3441
|
}
|
|
3045
|
-
function
|
|
3442
|
+
function strName12(v) {
|
|
3046
3443
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3047
3444
|
}
|
|
3048
3445
|
function viewObjectName(view) {
|
|
3049
|
-
return
|
|
3446
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec10(view.data) ? strName12(view.data.object) : void 0);
|
|
3050
3447
|
}
|
|
3051
3448
|
function viewContainerSites(view, basePath) {
|
|
3052
|
-
if (!
|
|
3449
|
+
if (!isRec10(view)) return [];
|
|
3053
3450
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3054
|
-
if (
|
|
3451
|
+
if (isRec10(view.form)) {
|
|
3055
3452
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3056
3453
|
}
|
|
3057
3454
|
for (const key of ["listViews", "formViews"]) {
|
|
3058
3455
|
const container = view[key];
|
|
3059
|
-
if (!
|
|
3456
|
+
if (!isRec10(container)) continue;
|
|
3060
3457
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
3061
3458
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
3062
|
-
if (!
|
|
3459
|
+
if (!isRec10(sub)) continue;
|
|
3063
3460
|
sites.push({
|
|
3064
3461
|
view: sub,
|
|
3065
3462
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -3077,18 +3474,18 @@ function formViewSites(view, basePath) {
|
|
|
3077
3474
|
// src/validate-translation-references.ts
|
|
3078
3475
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3079
3476
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3080
|
-
function
|
|
3477
|
+
function isRec11(v) {
|
|
3081
3478
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3082
3479
|
}
|
|
3083
3480
|
function asArray15(v) {
|
|
3084
3481
|
if (Array.isArray(v)) return v;
|
|
3085
|
-
if (
|
|
3482
|
+
if (isRec11(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec11(def) ? def : {} }));
|
|
3086
3483
|
return [];
|
|
3087
3484
|
}
|
|
3088
|
-
function
|
|
3485
|
+
function strName13(v) {
|
|
3089
3486
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3090
3487
|
}
|
|
3091
|
-
function
|
|
3488
|
+
function distance6(a, b) {
|
|
3092
3489
|
const m = a.length;
|
|
3093
3490
|
const n = b.length;
|
|
3094
3491
|
if (m === 0) return n;
|
|
@@ -3104,7 +3501,7 @@ function distance5(a, b) {
|
|
|
3104
3501
|
}
|
|
3105
3502
|
return prev[n];
|
|
3106
3503
|
}
|
|
3107
|
-
function
|
|
3504
|
+
function suggest7(target, known) {
|
|
3108
3505
|
const names = [...known];
|
|
3109
3506
|
const segmentMatch = names.find(
|
|
3110
3507
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -3113,7 +3510,7 @@ function suggest6(target, known) {
|
|
|
3113
3510
|
let best;
|
|
3114
3511
|
let bestScore = Infinity;
|
|
3115
3512
|
for (const candidate of names) {
|
|
3116
|
-
const d =
|
|
3513
|
+
const d = distance6(target, candidate);
|
|
3117
3514
|
if (d < bestScore) {
|
|
3118
3515
|
bestScore = d;
|
|
3119
3516
|
best = candidate;
|
|
@@ -3146,33 +3543,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
3146
3543
|
const addSections = (container, binding) => {
|
|
3147
3544
|
if (!binding) return;
|
|
3148
3545
|
for (const section of asArray15(container.sections)) {
|
|
3149
|
-
const sectionName =
|
|
3546
|
+
const sectionName = strName13(section.name);
|
|
3150
3547
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
3151
3548
|
}
|
|
3152
3549
|
};
|
|
3153
|
-
const listBinding =
|
|
3154
|
-
if (
|
|
3155
|
-
addView(recordObject ?? listBinding,
|
|
3550
|
+
const listBinding = isRec11(view.list) ? bindingOf(view.list) : void 0;
|
|
3551
|
+
if (isRec11(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3552
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
3156
3553
|
const named = namedViewKeys(view);
|
|
3157
3554
|
for (const family of ["listViews", "formViews"]) {
|
|
3158
3555
|
const container = view[family];
|
|
3159
|
-
if (!
|
|
3556
|
+
if (!isRec11(container)) continue;
|
|
3160
3557
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3161
3558
|
let at = 0;
|
|
3162
3559
|
for (const sub of Object.values(container)) {
|
|
3163
3560
|
if (!sub || typeof sub !== "object") continue;
|
|
3164
3561
|
const registryKey = registryKeys[at++];
|
|
3165
|
-
if (!
|
|
3562
|
+
if (!isRec11(sub)) continue;
|
|
3166
3563
|
const binding = bindingOf(sub) ?? listBinding;
|
|
3167
3564
|
addView(binding, registryKey);
|
|
3168
3565
|
addSections(sub, binding);
|
|
3169
3566
|
}
|
|
3170
3567
|
}
|
|
3171
|
-
if (
|
|
3568
|
+
if (isRec11(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
3172
3569
|
addSections(view, recordObject ?? listBinding);
|
|
3173
3570
|
}
|
|
3174
3571
|
function defaultListViewKey(object, container) {
|
|
3175
|
-
if (!object || !
|
|
3572
|
+
if (!object || !isRec11(container.list)) return void 0;
|
|
3176
3573
|
const item = expandViewContainer(object, container).find(
|
|
3177
3574
|
(i) => i.viewKind === "list" && i.isDefault
|
|
3178
3575
|
);
|
|
@@ -3184,7 +3581,7 @@ function namedViewKeys(container) {
|
|
|
3184
3581
|
const object = "probe";
|
|
3185
3582
|
const prefix = `${object}.`;
|
|
3186
3583
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3187
|
-
const countEntries = (v) =>
|
|
3584
|
+
const countEntries = (v) => isRec11(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3188
3585
|
const listCount = countEntries(container.listViews);
|
|
3189
3586
|
const formCount = countEntries(container.formViews);
|
|
3190
3587
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -3202,14 +3599,14 @@ function readOptions(field) {
|
|
|
3202
3599
|
values.add(opt);
|
|
3203
3600
|
continue;
|
|
3204
3601
|
}
|
|
3205
|
-
if (!
|
|
3206
|
-
const value =
|
|
3602
|
+
if (!isRec11(opt)) continue;
|
|
3603
|
+
const value = strName13(opt.value);
|
|
3207
3604
|
if (!value) continue;
|
|
3208
3605
|
values.add(value);
|
|
3209
|
-
const label2 =
|
|
3606
|
+
const label2 = strName13(opt.label);
|
|
3210
3607
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
3211
3608
|
}
|
|
3212
|
-
} else if (
|
|
3609
|
+
} else if (isRec11(raw)) {
|
|
3213
3610
|
for (const [value, label2] of Object.entries(raw)) {
|
|
3214
3611
|
values.add(value);
|
|
3215
3612
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -3230,23 +3627,23 @@ function buildUniverse(stack) {
|
|
|
3230
3627
|
return facts;
|
|
3231
3628
|
};
|
|
3232
3629
|
for (const obj of asArray15(stack.objects)) {
|
|
3233
|
-
const objectName =
|
|
3630
|
+
const objectName = strName13(obj.name);
|
|
3234
3631
|
if (!objectName) continue;
|
|
3235
3632
|
const facts = factsFor(objectName);
|
|
3236
3633
|
for (const field of asArray15(obj.fields)) {
|
|
3237
|
-
const fieldName =
|
|
3634
|
+
const fieldName = strName13(field.name);
|
|
3238
3635
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
3239
3636
|
}
|
|
3240
3637
|
for (const action of asArray15(obj.actions)) {
|
|
3241
|
-
const actionName =
|
|
3638
|
+
const actionName = strName13(action.name);
|
|
3242
3639
|
if (actionName) facts.actions.set(actionName, action);
|
|
3243
3640
|
}
|
|
3244
3641
|
for (const view of asArray15(obj.views)) {
|
|
3245
|
-
collectViewRecord({ ...view, object:
|
|
3642
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
3246
3643
|
}
|
|
3247
3644
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3248
3645
|
for (const group of asArray15(obj.fieldGroups)) {
|
|
3249
|
-
const key =
|
|
3646
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
3250
3647
|
if (key) facts.sections.add(key);
|
|
3251
3648
|
}
|
|
3252
3649
|
}
|
|
@@ -3257,10 +3654,10 @@ function buildUniverse(stack) {
|
|
|
3257
3654
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3258
3655
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3259
3656
|
if (!walked.objectName) continue;
|
|
3260
|
-
const props =
|
|
3657
|
+
const props = isRec11(walked.component.properties) ? walked.component.properties : void 0;
|
|
3261
3658
|
if (!props) continue;
|
|
3262
3659
|
for (const section of asArray15(props.sections)) {
|
|
3263
|
-
const sectionName =
|
|
3660
|
+
const sectionName = strName13(section.name);
|
|
3264
3661
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3265
3662
|
}
|
|
3266
3663
|
}
|
|
@@ -3268,9 +3665,9 @@ function buildUniverse(stack) {
|
|
|
3268
3665
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3269
3666
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3270
3667
|
for (const action of asArray15(stack.actions)) {
|
|
3271
|
-
const actionName =
|
|
3668
|
+
const actionName = strName13(action.name);
|
|
3272
3669
|
if (!actionName) continue;
|
|
3273
|
-
const owner =
|
|
3670
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
3274
3671
|
if (owner) {
|
|
3275
3672
|
factsFor(owner).actions.set(actionName, action);
|
|
3276
3673
|
actionOwners.set(actionName, owner);
|
|
@@ -3285,19 +3682,19 @@ function buildUniverse(stack) {
|
|
|
3285
3682
|
}
|
|
3286
3683
|
const apps = /* @__PURE__ */ new Map();
|
|
3287
3684
|
for (const app of asArray15(stack.apps)) {
|
|
3288
|
-
const appName =
|
|
3685
|
+
const appName = strName13(app.name);
|
|
3289
3686
|
if (!appName) continue;
|
|
3290
3687
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3291
3688
|
const walkNav = (items) => {
|
|
3292
3689
|
for (const item of asArray15(items)) {
|
|
3293
|
-
const id =
|
|
3690
|
+
const id = strName13(item.id);
|
|
3294
3691
|
if (id) navIds.add(id);
|
|
3295
3692
|
if (item.children) walkNav(item.children);
|
|
3296
3693
|
}
|
|
3297
3694
|
};
|
|
3298
3695
|
walkNav(app.navigation);
|
|
3299
3696
|
for (const area of asArray15(app.areas)) {
|
|
3300
|
-
const areaId =
|
|
3697
|
+
const areaId = strName13(area.id);
|
|
3301
3698
|
if (areaId) navIds.add(areaId);
|
|
3302
3699
|
walkNav(area.navigation);
|
|
3303
3700
|
}
|
|
@@ -3305,20 +3702,20 @@ function buildUniverse(stack) {
|
|
|
3305
3702
|
}
|
|
3306
3703
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3307
3704
|
for (const dash of asArray15(stack.dashboards)) {
|
|
3308
|
-
const dashName =
|
|
3705
|
+
const dashName = strName13(dash.name);
|
|
3309
3706
|
if (!dashName) continue;
|
|
3310
3707
|
const widgets = /* @__PURE__ */ new Set();
|
|
3311
3708
|
for (const widget of asArray15(dash.widgets)) {
|
|
3312
|
-
const id =
|
|
3709
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
3313
3710
|
if (id) widgets.add(id);
|
|
3314
3711
|
}
|
|
3315
3712
|
const actions = /* @__PURE__ */ new Set();
|
|
3316
3713
|
const headerActions = [
|
|
3317
|
-
...asArray15(
|
|
3714
|
+
...asArray15(isRec11(dash.header) ? dash.header.actions : void 0),
|
|
3318
3715
|
...asArray15(dash.actions)
|
|
3319
3716
|
];
|
|
3320
3717
|
for (const action of headerActions) {
|
|
3321
|
-
const key =
|
|
3718
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
3322
3719
|
if (key) actions.add(key);
|
|
3323
3720
|
}
|
|
3324
3721
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3330,7 +3727,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3330
3727
|
}
|
|
3331
3728
|
function validateTranslationReferences(stack) {
|
|
3332
3729
|
const findings = [];
|
|
3333
|
-
if (!
|
|
3730
|
+
if (!isRec11(stack)) return findings;
|
|
3334
3731
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3335
3732
|
if (bundles.length === 0) return findings;
|
|
3336
3733
|
const universe = buildUniverse(stack);
|
|
@@ -3339,13 +3736,13 @@ function validateTranslationReferences(stack) {
|
|
|
3339
3736
|
};
|
|
3340
3737
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3341
3738
|
const bundle = bundles[bi];
|
|
3342
|
-
if (!
|
|
3739
|
+
if (!isRec11(bundle)) continue;
|
|
3343
3740
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3344
|
-
if (!
|
|
3741
|
+
if (!isRec11(rawData)) continue;
|
|
3345
3742
|
const base = localePath(bi, locale);
|
|
3346
3743
|
const inLocale = `locale "${locale}"`;
|
|
3347
3744
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3348
|
-
if (!
|
|
3745
|
+
if (!isRec11(rawNode)) continue;
|
|
3349
3746
|
const objPath = `${base}.objects.${objectName}`;
|
|
3350
3747
|
const facts = universe.objects.get(objectName);
|
|
3351
3748
|
if (!facts) {
|
|
@@ -3353,7 +3750,7 @@ function validateTranslationReferences(stack) {
|
|
|
3353
3750
|
orphan(
|
|
3354
3751
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
3355
3752
|
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.` +
|
|
3753
|
+
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
3754
|
`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
3755
|
);
|
|
3359
3756
|
continue;
|
|
@@ -3366,12 +3763,12 @@ function validateTranslationReferences(stack) {
|
|
|
3366
3763
|
orphan(
|
|
3367
3764
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
3368
3765
|
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.` +
|
|
3766
|
+
`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
3767
|
`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
3768
|
);
|
|
3372
3769
|
continue;
|
|
3373
3770
|
}
|
|
3374
|
-
if (!
|
|
3771
|
+
if (!isRec11(rawField)) continue;
|
|
3375
3772
|
checkOptionKeys(findings, {
|
|
3376
3773
|
optionMap: rawField.options,
|
|
3377
3774
|
field,
|
|
@@ -3386,7 +3783,7 @@ function validateTranslationReferences(stack) {
|
|
|
3386
3783
|
orphan(
|
|
3387
3784
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
3388
3785
|
`${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.` +
|
|
3786
|
+
`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
3787
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
3391
3788
|
);
|
|
3392
3789
|
}
|
|
@@ -3395,7 +3792,7 @@ function validateTranslationReferences(stack) {
|
|
|
3395
3792
|
orphan(
|
|
3396
3793
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
3397
3794
|
`${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.` +
|
|
3795
|
+
`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
3796
|
`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
3797
|
);
|
|
3401
3798
|
}
|
|
@@ -3406,7 +3803,7 @@ function validateTranslationReferences(stack) {
|
|
|
3406
3803
|
orphan(
|
|
3407
3804
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
3408
3805
|
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.` +
|
|
3806
|
+
`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
3807
|
`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
3808
|
);
|
|
3412
3809
|
continue;
|
|
@@ -3428,7 +3825,7 @@ function validateTranslationReferences(stack) {
|
|
|
3428
3825
|
orphan(
|
|
3429
3826
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
3430
3827
|
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.` +
|
|
3828
|
+
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
3829
|
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
3830
|
);
|
|
3434
3831
|
continue;
|
|
@@ -3448,18 +3845,18 @@ function validateTranslationReferences(stack) {
|
|
|
3448
3845
|
orphan(
|
|
3449
3846
|
`${inLocale} \xB7 app "${appName}"`,
|
|
3450
3847
|
appPath,
|
|
3451
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
3848
|
+
`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
3849
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
3453
3850
|
);
|
|
3454
3851
|
continue;
|
|
3455
3852
|
}
|
|
3456
|
-
if (!
|
|
3853
|
+
if (!isRec11(rawApp)) continue;
|
|
3457
3854
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3458
3855
|
if (navIds.has(navId)) continue;
|
|
3459
3856
|
orphan(
|
|
3460
3857
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
3461
3858
|
`${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.` +
|
|
3859
|
+
`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
3860
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
3464
3861
|
);
|
|
3465
3862
|
}
|
|
@@ -3471,18 +3868,18 @@ function validateTranslationReferences(stack) {
|
|
|
3471
3868
|
orphan(
|
|
3472
3869
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
3473
3870
|
dashPath,
|
|
3474
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
3871
|
+
`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
3872
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
3476
3873
|
);
|
|
3477
3874
|
continue;
|
|
3478
3875
|
}
|
|
3479
|
-
if (!
|
|
3876
|
+
if (!isRec11(rawDash)) continue;
|
|
3480
3877
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3481
3878
|
if (dash.widgets.has(widgetId)) continue;
|
|
3482
3879
|
orphan(
|
|
3483
3880
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
3484
3881
|
`${dashPath}.widgets.${widgetId}`,
|
|
3485
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
3882
|
+
`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
3883
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
3487
3884
|
);
|
|
3488
3885
|
}
|
|
@@ -3491,7 +3888,7 @@ function validateTranslationReferences(stack) {
|
|
|
3491
3888
|
orphan(
|
|
3492
3889
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
3493
3890
|
`${dashPath}.actions.${actionKey}`,
|
|
3494
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
3891
|
+
`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
3892
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
3496
3893
|
);
|
|
3497
3894
|
}
|
|
@@ -3501,7 +3898,7 @@ function validateTranslationReferences(stack) {
|
|
|
3501
3898
|
return findings;
|
|
3502
3899
|
}
|
|
3503
3900
|
function asRecord(v) {
|
|
3504
|
-
return
|
|
3901
|
+
return isRec11(v) ? v : {};
|
|
3505
3902
|
}
|
|
3506
3903
|
function checkOptionKeys(findings, ctx) {
|
|
3507
3904
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3513,7 +3910,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3513
3910
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3514
3911
|
where: ctx.where,
|
|
3515
3912
|
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 "${
|
|
3913
|
+
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
3914
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3518
3915
|
});
|
|
3519
3916
|
return;
|
|
@@ -3526,17 +3923,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3526
3923
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3527
3924
|
where: ctx.where,
|
|
3528
3925
|
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.` +
|
|
3926
|
+
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
3927
|
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
3928
|
});
|
|
3532
3929
|
}
|
|
3533
3930
|
}
|
|
3534
3931
|
function checkActionParams(findings, ctx) {
|
|
3535
|
-
const rawParams = Object.keys(asRecord(
|
|
3932
|
+
const rawParams = Object.keys(asRecord(isRec11(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3536
3933
|
if (rawParams.length === 0) return;
|
|
3537
3934
|
const declared = /* @__PURE__ */ new Set();
|
|
3538
3935
|
for (const param of asArray15(ctx.action.params)) {
|
|
3539
|
-
const name =
|
|
3936
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
3540
3937
|
if (name) declared.add(name);
|
|
3541
3938
|
}
|
|
3542
3939
|
for (const paramName of rawParams) {
|
|
@@ -3546,40 +3943,40 @@ function checkActionParams(findings, ctx) {
|
|
|
3546
3943
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
3547
3944
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
3548
3945
|
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.` +
|
|
3946
|
+
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
3947
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
3551
3948
|
});
|
|
3552
3949
|
}
|
|
3553
3950
|
}
|
|
3554
3951
|
|
|
3555
3952
|
// src/collection-entries.ts
|
|
3556
|
-
function
|
|
3953
|
+
function isRec12(v) {
|
|
3557
3954
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3558
3955
|
}
|
|
3559
3956
|
function collectionEntries(v, base) {
|
|
3560
3957
|
if (Array.isArray(v)) {
|
|
3561
3958
|
const out = [];
|
|
3562
3959
|
for (let i = 0; i < v.length; i++) {
|
|
3563
|
-
if (
|
|
3960
|
+
if (isRec12(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3564
3961
|
}
|
|
3565
3962
|
return out;
|
|
3566
3963
|
}
|
|
3567
|
-
if (
|
|
3568
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3964
|
+
if (isRec12(v)) {
|
|
3965
|
+
return Object.entries(v).filter(([, def]) => isRec12(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3569
3966
|
}
|
|
3570
3967
|
return [];
|
|
3571
3968
|
}
|
|
3572
3969
|
|
|
3573
3970
|
// src/validate-translatable-sections.ts
|
|
3574
3971
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3575
|
-
function
|
|
3972
|
+
function isRec13(v) {
|
|
3576
3973
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3577
3974
|
}
|
|
3578
|
-
function
|
|
3975
|
+
function strName14(v) {
|
|
3579
3976
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3580
3977
|
}
|
|
3581
3978
|
function viewLabel(view) {
|
|
3582
|
-
const name =
|
|
3979
|
+
const name = strName14(view.name);
|
|
3583
3980
|
return name ? `view "${name}"` : "";
|
|
3584
3981
|
}
|
|
3585
3982
|
function joinWhere(...parts) {
|
|
@@ -3587,7 +3984,7 @@ function joinWhere(...parts) {
|
|
|
3587
3984
|
}
|
|
3588
3985
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3589
3986
|
const recordObject = viewObjectName(view);
|
|
3590
|
-
const listBinding =
|
|
3987
|
+
const listBinding = isRec13(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3591
3988
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3592
3989
|
sites.push({
|
|
3593
3990
|
path: `${site.path}.sections`,
|
|
@@ -3601,11 +3998,11 @@ function translatedObjectNames(stack) {
|
|
|
3601
3998
|
const out = /* @__PURE__ */ new Set();
|
|
3602
3999
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3603
4000
|
for (const bundle of bundles) {
|
|
3604
|
-
if (!
|
|
4001
|
+
if (!isRec13(bundle)) continue;
|
|
3605
4002
|
for (const data of Object.values(bundle)) {
|
|
3606
|
-
if (!
|
|
4003
|
+
if (!isRec13(data) || !isRec13(data.objects)) continue;
|
|
3607
4004
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3608
|
-
if (
|
|
4005
|
+
if (isRec13(node)) out.add(objectName);
|
|
3609
4006
|
}
|
|
3610
4007
|
}
|
|
3611
4008
|
}
|
|
@@ -3617,22 +4014,22 @@ function suggestedName(label2) {
|
|
|
3617
4014
|
}
|
|
3618
4015
|
function validateTranslatableSections(stack) {
|
|
3619
4016
|
const findings = [];
|
|
3620
|
-
if (!
|
|
4017
|
+
if (!isRec13(stack)) return findings;
|
|
3621
4018
|
const translated = translatedObjectNames(stack);
|
|
3622
4019
|
if (translated.size === 0) return findings;
|
|
3623
4020
|
const sites = [];
|
|
3624
4021
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3625
|
-
const objectName =
|
|
4022
|
+
const objectName = strName14(obj.name);
|
|
3626
4023
|
if (!objectName) continue;
|
|
3627
4024
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3628
4025
|
collectViewSites(
|
|
3629
|
-
{ ...view, object:
|
|
4026
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
3630
4027
|
path,
|
|
3631
4028
|
viewLabel(view),
|
|
3632
4029
|
sites
|
|
3633
4030
|
);
|
|
3634
4031
|
}
|
|
3635
|
-
if (
|
|
4032
|
+
if (isRec13(obj.listViews)) {
|
|
3636
4033
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3637
4034
|
}
|
|
3638
4035
|
}
|
|
@@ -3640,13 +4037,13 @@ function validateTranslatableSections(stack) {
|
|
|
3640
4037
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3641
4038
|
}
|
|
3642
4039
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3643
|
-
const pageName =
|
|
4040
|
+
const pageName = strName14(page.name);
|
|
3644
4041
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3645
4042
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3646
4043
|
if (!walked.objectName) continue;
|
|
3647
|
-
const props =
|
|
4044
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
3648
4045
|
if (!props) continue;
|
|
3649
|
-
const type =
|
|
4046
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
3650
4047
|
sites.push({
|
|
3651
4048
|
path: `${walked.path}.properties.sections`,
|
|
3652
4049
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3661,9 +4058,9 @@ function validateTranslatableSections(stack) {
|
|
|
3661
4058
|
if (!Array.isArray(site.sections)) continue;
|
|
3662
4059
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3663
4060
|
const section = site.sections[i];
|
|
3664
|
-
if (!
|
|
3665
|
-
if (
|
|
3666
|
-
const heading =
|
|
4061
|
+
if (!isRec13(section)) continue;
|
|
4062
|
+
if (strName14(section.name)) continue;
|
|
4063
|
+
const heading = strName14(section.label);
|
|
3667
4064
|
if (!heading) continue;
|
|
3668
4065
|
const slug = suggestedName(heading);
|
|
3669
4066
|
findings.push({
|
|
@@ -3681,10 +4078,10 @@ function validateTranslatableSections(stack) {
|
|
|
3681
4078
|
|
|
3682
4079
|
// src/flow-walk.ts
|
|
3683
4080
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
3684
|
-
function
|
|
4081
|
+
function isRec14(v) {
|
|
3685
4082
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3686
4083
|
}
|
|
3687
|
-
function
|
|
4084
|
+
function strName15(v) {
|
|
3688
4085
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3689
4086
|
}
|
|
3690
4087
|
var REGION_SLOTS = new Map(
|
|
@@ -3693,10 +4090,10 @@ var REGION_SLOTS = new Map(
|
|
|
3693
4090
|
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
3694
4091
|
var MAX_REGION_DEPTH = 16;
|
|
3695
4092
|
function flowNodeLabel(node, index) {
|
|
3696
|
-
return
|
|
4093
|
+
return strName15(node.label) ?? strName15(node.id) ?? `#${index}`;
|
|
3697
4094
|
}
|
|
3698
4095
|
function stripRegions(config) {
|
|
3699
|
-
if (!
|
|
4096
|
+
if (!isRec14(config)) return void 0;
|
|
3700
4097
|
let out;
|
|
3701
4098
|
for (const key of Object.keys(config)) {
|
|
3702
4099
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3707,11 +4104,11 @@ function stripRegions(config) {
|
|
|
3707
4104
|
}
|
|
3708
4105
|
function walkFlowNodes(flow, flowPath) {
|
|
3709
4106
|
const out = [];
|
|
3710
|
-
if (!
|
|
4107
|
+
if (!isRec14(flow)) return out;
|
|
3711
4108
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3712
4109
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3713
4110
|
nodes.forEach((raw, index) => {
|
|
3714
|
-
if (!
|
|
4111
|
+
if (!isRec14(raw)) return;
|
|
3715
4112
|
const path = `${basePath}[${index}]`;
|
|
3716
4113
|
out.push({
|
|
3717
4114
|
node: raw,
|
|
@@ -3720,9 +4117,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3720
4117
|
regionTrail: trail,
|
|
3721
4118
|
depth
|
|
3722
4119
|
});
|
|
3723
|
-
const type =
|
|
4120
|
+
const type = strName15(raw.type);
|
|
3724
4121
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3725
|
-
if (!slots || !
|
|
4122
|
+
if (!slots || !isRec14(raw.config)) return;
|
|
3726
4123
|
const config = raw.config;
|
|
3727
4124
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3728
4125
|
for (const slot of slots) {
|
|
@@ -3730,8 +4127,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3730
4127
|
if (slot === "branches") {
|
|
3731
4128
|
if (!Array.isArray(value)) continue;
|
|
3732
4129
|
value.forEach((branch, b) => {
|
|
3733
|
-
if (!
|
|
3734
|
-
const branchName =
|
|
4130
|
+
if (!isRec14(branch)) return;
|
|
4131
|
+
const branchName = strName15(branch.name) ?? `#${b}`;
|
|
3735
4132
|
visitList(
|
|
3736
4133
|
branch.nodes,
|
|
3737
4134
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3741,7 +4138,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3741
4138
|
});
|
|
3742
4139
|
continue;
|
|
3743
4140
|
}
|
|
3744
|
-
if (!
|
|
4141
|
+
if (!isRec14(value)) continue;
|
|
3745
4142
|
visitList(
|
|
3746
4143
|
value.nodes,
|
|
3747
4144
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3982,7 +4379,7 @@ function asArray17(v) {
|
|
|
3982
4379
|
}
|
|
3983
4380
|
return [];
|
|
3984
4381
|
}
|
|
3985
|
-
function
|
|
4382
|
+
function strName16(v) {
|
|
3986
4383
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3987
4384
|
}
|
|
3988
4385
|
function surfaceOf(v) {
|
|
@@ -3993,17 +4390,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3993
4390
|
if (!stack || typeof stack !== "object") return findings;
|
|
3994
4391
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3995
4392
|
for (const skill of asArray17(stack.skills)) {
|
|
3996
|
-
const n =
|
|
4393
|
+
const n = strName16(skill.name);
|
|
3997
4394
|
if (n) skillsByName.set(n, skill);
|
|
3998
4395
|
}
|
|
3999
4396
|
const agents = asArray17(stack.agents);
|
|
4000
4397
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4001
4398
|
const agent = agents[ai];
|
|
4002
|
-
const agentName =
|
|
4399
|
+
const agentName = strName16(agent.name) ?? `#${ai}`;
|
|
4003
4400
|
const agentSurface = surfaceOf(agent.surface);
|
|
4004
4401
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4005
4402
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4006
|
-
const ref =
|
|
4403
|
+
const ref = strName16(skillRefs[si]);
|
|
4007
4404
|
if (!ref) continue;
|
|
4008
4405
|
const skill = skillsByName.get(ref);
|
|
4009
4406
|
if (!skill) continue;
|
|
@@ -4032,10 +4429,10 @@ function asArray18(v) {
|
|
|
4032
4429
|
}
|
|
4033
4430
|
return [];
|
|
4034
4431
|
}
|
|
4035
|
-
function
|
|
4432
|
+
function strName17(v) {
|
|
4036
4433
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4037
4434
|
}
|
|
4038
|
-
function
|
|
4435
|
+
function distance7(a, b) {
|
|
4039
4436
|
const m = a.length;
|
|
4040
4437
|
const n = b.length;
|
|
4041
4438
|
if (m === 0) return n;
|
|
@@ -4051,14 +4448,14 @@ function distance6(a, b) {
|
|
|
4051
4448
|
}
|
|
4052
4449
|
return prev[n];
|
|
4053
4450
|
}
|
|
4054
|
-
function
|
|
4451
|
+
function suggest8(target, known) {
|
|
4055
4452
|
for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4056
4453
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4057
4454
|
}
|
|
4058
4455
|
let best;
|
|
4059
4456
|
let bestScore = Infinity;
|
|
4060
4457
|
for (const candidate of known) {
|
|
4061
|
-
const d =
|
|
4458
|
+
const d = distance7(target, candidate);
|
|
4062
4459
|
if (d < bestScore) {
|
|
4063
4460
|
bestScore = d;
|
|
4064
4461
|
best = candidate;
|
|
@@ -4073,8 +4470,8 @@ function materialisesAsTool(action) {
|
|
|
4073
4470
|
if (!ai || typeof ai !== "object") return false;
|
|
4074
4471
|
const aiRec = ai;
|
|
4075
4472
|
if (aiRec.exposed !== true) return false;
|
|
4076
|
-
if (!
|
|
4077
|
-
const type =
|
|
4473
|
+
if (!strName17(aiRec.description)) return false;
|
|
4474
|
+
const type = strName17(action.type);
|
|
4078
4475
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4079
4476
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4080
4477
|
return Boolean(action.target);
|
|
@@ -4082,12 +4479,12 @@ function materialisesAsTool(action) {
|
|
|
4082
4479
|
function collectToolUniverse(stack) {
|
|
4083
4480
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
4084
4481
|
for (const tool of asArray18(stack.tools)) {
|
|
4085
|
-
const n =
|
|
4482
|
+
const n = strName17(tool.name);
|
|
4086
4483
|
if (n) universe.add(n);
|
|
4087
4484
|
}
|
|
4088
4485
|
const addActionFamily = (actions) => {
|
|
4089
4486
|
for (const action of asArray18(actions)) {
|
|
4090
|
-
const n =
|
|
4487
|
+
const n = strName17(action.name);
|
|
4091
4488
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4092
4489
|
}
|
|
4093
4490
|
};
|
|
@@ -4101,7 +4498,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
4101
4498
|
const names = /* @__PURE__ */ new Set();
|
|
4102
4499
|
const scan = (actions) => {
|
|
4103
4500
|
for (const action of asArray18(actions)) {
|
|
4104
|
-
const n =
|
|
4501
|
+
const n = strName17(action.name);
|
|
4105
4502
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4106
4503
|
}
|
|
4107
4504
|
};
|
|
@@ -4127,10 +4524,10 @@ function validateAiToolReferences(stack) {
|
|
|
4127
4524
|
const skills = asArray18(stack.skills);
|
|
4128
4525
|
for (let si = 0; si < skills.length; si++) {
|
|
4129
4526
|
const skill = skills[si];
|
|
4130
|
-
const skillName =
|
|
4527
|
+
const skillName = strName17(skill.name) ?? `#${si}`;
|
|
4131
4528
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4132
4529
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4133
|
-
const ref =
|
|
4530
|
+
const ref = strName17(refs[ti]);
|
|
4134
4531
|
if (!ref || resolves(ref)) continue;
|
|
4135
4532
|
const isPattern = ref.endsWith("*");
|
|
4136
4533
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4139,7 +4536,7 @@ function validateAiToolReferences(stack) {
|
|
|
4139
4536
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4140
4537
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4141
4538
|
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.` +
|
|
4539
|
+
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
4540
|
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
4541
|
});
|
|
4145
4542
|
}
|
|
@@ -4157,7 +4554,7 @@ function asArray19(v) {
|
|
|
4157
4554
|
}
|
|
4158
4555
|
return [];
|
|
4159
4556
|
}
|
|
4160
|
-
function
|
|
4557
|
+
function strName18(v) {
|
|
4161
4558
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4162
4559
|
}
|
|
4163
4560
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -4167,7 +4564,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4167
4564
|
const agents = asArray19(stack.agents);
|
|
4168
4565
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4169
4566
|
const agent = agents[ai];
|
|
4170
|
-
const name =
|
|
4567
|
+
const name = strName18(agent.name) ?? `#${ai}`;
|
|
4171
4568
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4172
4569
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4173
4570
|
findings.push({
|
|
@@ -4183,9 +4580,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4183
4580
|
const apps = asArray19(stack.apps);
|
|
4184
4581
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4185
4582
|
const app = apps[appIdx];
|
|
4186
|
-
const defaultAgent =
|
|
4583
|
+
const defaultAgent = strName18(app.defaultAgent);
|
|
4187
4584
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4188
|
-
const appName =
|
|
4585
|
+
const appName = strName18(app.name) ?? `#${appIdx}`;
|
|
4189
4586
|
findings.push({
|
|
4190
4587
|
severity: "warning",
|
|
4191
4588
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4201,6 +4598,38 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4201
4598
|
// src/validate-hook-body-writes.ts
|
|
4202
4599
|
import { createRequire } from "module";
|
|
4203
4600
|
import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
|
|
4601
|
+
|
|
4602
|
+
// src/checked-parse.ts
|
|
4603
|
+
function createSourceFileChecked(tsc, fileName, source, options) {
|
|
4604
|
+
const sourceFile = tsc.createSourceFile(
|
|
4605
|
+
fileName,
|
|
4606
|
+
source,
|
|
4607
|
+
options.target,
|
|
4608
|
+
options.setParentNodes,
|
|
4609
|
+
options.scriptKind
|
|
4610
|
+
);
|
|
4611
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
4612
|
+
if (!diagnostics || diagnostics.length === 0) return { sourceFile };
|
|
4613
|
+
const first = diagnostics[0];
|
|
4614
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
|
|
4615
|
+
const offset = options.synthesizedLinesBefore ?? 0;
|
|
4616
|
+
return {
|
|
4617
|
+
sourceFile,
|
|
4618
|
+
failure: {
|
|
4619
|
+
message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
|
|
4620
|
+
line: Math.max(1, line + 1 - offset),
|
|
4621
|
+
column: character + 1,
|
|
4622
|
+
count: diagnostics.length
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
}
|
|
4626
|
+
function describeParseFailure(failure) {
|
|
4627
|
+
const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
|
|
4628
|
+
return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
|
|
4629
|
+
}
|
|
4630
|
+
var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
|
|
4631
|
+
|
|
4632
|
+
// src/validate-hook-body-writes.ts
|
|
4204
4633
|
var cachedTs = null;
|
|
4205
4634
|
function loadTypeScript() {
|
|
4206
4635
|
if (cachedTs) return cachedTs;
|
|
@@ -4215,6 +4644,8 @@ function loadTypeScript() {
|
|
|
4215
4644
|
return cachedTs;
|
|
4216
4645
|
}
|
|
4217
4646
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
4647
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
4648
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
4218
4649
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
4219
4650
|
{
|
|
4220
4651
|
id: "input-property-assign",
|
|
@@ -4281,13 +4712,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4281
4712
|
"owner",
|
|
4282
4713
|
"record_type"
|
|
4283
4714
|
]);
|
|
4284
|
-
|
|
4715
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
4716
|
+
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).`;
|
|
4717
|
+
}
|
|
4718
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4285
4719
|
function asArray20(v) {
|
|
4286
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4287
|
-
if (
|
|
4720
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4721
|
+
if (isRec15(v)) {
|
|
4288
4722
|
return Object.entries(v).map(([name, def]) => ({
|
|
4289
4723
|
name,
|
|
4290
|
-
...
|
|
4724
|
+
...isRec15(def) ? def : {}
|
|
4291
4725
|
}));
|
|
4292
4726
|
}
|
|
4293
4727
|
return [];
|
|
@@ -4310,23 +4744,23 @@ function judgeableFieldsOf(index, objectName) {
|
|
|
4310
4744
|
if (!declared || declared.size === 0) return void 0;
|
|
4311
4745
|
return declared;
|
|
4312
4746
|
}
|
|
4313
|
-
function extractHookBodyWrites(source) {
|
|
4314
|
-
return extractHookBodyWriteSet(source).writes;
|
|
4315
|
-
}
|
|
4316
4747
|
function extractHookBodyWriteSet(source) {
|
|
4317
4748
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
4318
4749
|
return { writes: [], ctxRecordEscapes: false };
|
|
4319
4750
|
}
|
|
4320
4751
|
const tsc = loadTypeScript();
|
|
4321
|
-
const sf =
|
|
4752
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
4753
|
+
tsc,
|
|
4322
4754
|
"hook-body.ts",
|
|
4323
4755
|
`async function __body(ctx) {
|
|
4324
4756
|
${source}
|
|
4325
4757
|
}`,
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4758
|
+
{
|
|
4759
|
+
target: tsc.ScriptTarget.Latest,
|
|
4760
|
+
setParentNodes: false,
|
|
4761
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
4762
|
+
synthesizedLinesBefore: 1
|
|
4763
|
+
}
|
|
4330
4764
|
);
|
|
4331
4765
|
const writes = [];
|
|
4332
4766
|
const recordRefs = [];
|
|
@@ -4423,7 +4857,8 @@ ${source}
|
|
|
4423
4857
|
visit(sf);
|
|
4424
4858
|
return {
|
|
4425
4859
|
writes,
|
|
4426
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
4860
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
4861
|
+
...parseFailure ? { parseFailure } : {}
|
|
4427
4862
|
};
|
|
4428
4863
|
}
|
|
4429
4864
|
function validateHookBodyWrites(stack) {
|
|
@@ -4431,15 +4866,28 @@ function validateHookBodyWrites(stack) {
|
|
|
4431
4866
|
const hooks = asArray20(stack.hooks);
|
|
4432
4867
|
if (hooks.length === 0) return findings;
|
|
4433
4868
|
let objectFields = null;
|
|
4869
|
+
let anchors = null;
|
|
4434
4870
|
hooks.forEach((hook, hookIndex) => {
|
|
4435
4871
|
const body = hook.body;
|
|
4436
|
-
if (!
|
|
4872
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4437
4873
|
const source = body.source;
|
|
4438
4874
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4439
|
-
const
|
|
4875
|
+
const extracted = extractHookBodyWriteSet(source);
|
|
4876
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
4877
|
+
if (extracted.parseFailure) {
|
|
4878
|
+
findings.push({
|
|
4879
|
+
severity: "warning",
|
|
4880
|
+
rule: HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
4881
|
+
where: `hook "${hookName}" \u203A body`,
|
|
4882
|
+
path: `hooks[${hookIndex}].body.source`,
|
|
4883
|
+
message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
4884
|
+
hint: PARSE_FAILURE_HINT
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4887
|
+
const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
4440
4888
|
if (writes.length === 0) return;
|
|
4441
4889
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
4442
|
-
|
|
4890
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4443
4891
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
4444
4892
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
4445
4893
|
);
|
|
@@ -4453,7 +4901,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4453
4901
|
if (reported.has(dedupeKey)) continue;
|
|
4454
4902
|
if (w.object === void 0) {
|
|
4455
4903
|
if (!inputJudgeable) continue;
|
|
4456
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
4904
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4905
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
4906
|
+
reported.add(dedupeKey);
|
|
4907
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
4908
|
+
findings.push({
|
|
4909
|
+
severity: "warning",
|
|
4910
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4911
|
+
where,
|
|
4912
|
+
path,
|
|
4913
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
4914
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
4915
|
+
});
|
|
4916
|
+
continue;
|
|
4917
|
+
}
|
|
4457
4918
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
4458
4919
|
reported.add(dedupeKey);
|
|
4459
4920
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -4469,7 +4930,20 @@ function validateHookBodyWrites(stack) {
|
|
|
4469
4930
|
} else {
|
|
4470
4931
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4471
4932
|
if (!known) continue;
|
|
4472
|
-
if (
|
|
4933
|
+
if (known.has(w.field)) continue;
|
|
4934
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
4935
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
4936
|
+
reported.add(dedupeKey);
|
|
4937
|
+
findings.push({
|
|
4938
|
+
severity: "warning",
|
|
4939
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
4940
|
+
where,
|
|
4941
|
+
path,
|
|
4942
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
4943
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
4944
|
+
});
|
|
4945
|
+
continue;
|
|
4946
|
+
}
|
|
4473
4947
|
reported.add(dedupeKey);
|
|
4474
4948
|
findings.push({
|
|
4475
4949
|
severity: "warning",
|
|
@@ -4498,19 +4972,21 @@ function fixHint(field, declared) {
|
|
|
4498
4972
|
import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
|
|
4499
4973
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
4500
4974
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
4975
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
4976
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
4501
4977
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
4502
4978
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
4503
4979
|
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
4504
4980
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4505
4981
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4506
4982
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4507
|
-
var
|
|
4983
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4508
4984
|
function asArray21(v) {
|
|
4509
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4510
|
-
if (
|
|
4985
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4986
|
+
if (isRec16(v)) {
|
|
4511
4987
|
return Object.entries(v).map(([name, def]) => ({
|
|
4512
4988
|
name,
|
|
4513
|
-
...
|
|
4989
|
+
...isRec16(def) ? def : {}
|
|
4514
4990
|
}));
|
|
4515
4991
|
}
|
|
4516
4992
|
return [];
|
|
@@ -4528,7 +5004,7 @@ function collectActionBodies(stack) {
|
|
|
4528
5004
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4529
5005
|
if (type !== "script") return;
|
|
4530
5006
|
const body = action.body;
|
|
4531
|
-
if (!
|
|
5007
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
4532
5008
|
const source = body.source;
|
|
4533
5009
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4534
5010
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4547,17 +5023,28 @@ function collectActionBodies(stack) {
|
|
|
4547
5023
|
}
|
|
4548
5024
|
function validateActionBodyWrites(stack) {
|
|
4549
5025
|
const findings = [];
|
|
4550
|
-
if (!
|
|
5026
|
+
if (!isRec16(stack)) return findings;
|
|
4551
5027
|
const sites = collectActionBodies(stack);
|
|
4552
5028
|
if (sites.length === 0) return findings;
|
|
4553
5029
|
let objectFields = null;
|
|
5030
|
+
let anchors = null;
|
|
4554
5031
|
for (const site of sites) {
|
|
4555
5032
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
4556
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5033
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
4557
5034
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
4558
5035
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
4559
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4560
5036
|
const where = `action "${site.name}" \u203A body`;
|
|
5037
|
+
if (parseFailure) {
|
|
5038
|
+
findings.push({
|
|
5039
|
+
severity: "warning",
|
|
5040
|
+
rule: ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
5041
|
+
where,
|
|
5042
|
+
path: site.path,
|
|
5043
|
+
message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
5044
|
+
hint: PARSE_FAILURE_HINT
|
|
5045
|
+
});
|
|
5046
|
+
}
|
|
5047
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
4561
5048
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
4562
5049
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
4563
5050
|
for (const w of recordWrites) {
|
|
@@ -4575,6 +5062,7 @@ function validateActionBodyWrites(stack) {
|
|
|
4575
5062
|
}
|
|
4576
5063
|
if (writes.length === 0) continue;
|
|
4577
5064
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5065
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4578
5066
|
const reported = /* @__PURE__ */ new Set();
|
|
4579
5067
|
for (const w of writes) {
|
|
4580
5068
|
if (w.object === void 0) continue;
|
|
@@ -4582,7 +5070,20 @@ function validateActionBodyWrites(stack) {
|
|
|
4582
5070
|
if (reported.has(dedupeKey)) continue;
|
|
4583
5071
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
4584
5072
|
if (!known) continue;
|
|
4585
|
-
if (
|
|
5073
|
+
if (known.has(w.field)) continue;
|
|
5074
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
5075
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
5076
|
+
reported.add(dedupeKey);
|
|
5077
|
+
findings.push({
|
|
5078
|
+
severity: "warning",
|
|
5079
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
5080
|
+
where,
|
|
5081
|
+
path: site.path,
|
|
5082
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
5083
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
5084
|
+
});
|
|
5085
|
+
continue;
|
|
5086
|
+
}
|
|
4586
5087
|
reported.add(dedupeKey);
|
|
4587
5088
|
findings.push({
|
|
4588
5089
|
severity: "warning",
|
|
@@ -4604,14 +5105,15 @@ function fixHint2(field, declared) {
|
|
|
4604
5105
|
// src/validate-flow-node-writes.ts
|
|
4605
5106
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
4606
5107
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5108
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
4607
5109
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4608
|
-
var
|
|
5110
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4609
5111
|
function asArray22(v) {
|
|
4610
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4611
|
-
if (
|
|
5112
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
5113
|
+
if (isRec17(v)) {
|
|
4612
5114
|
return Object.entries(v).map(([name, def]) => ({
|
|
4613
5115
|
name,
|
|
4614
|
-
...
|
|
5116
|
+
...isRec17(def) ? def : {}
|
|
4615
5117
|
}));
|
|
4616
5118
|
}
|
|
4617
5119
|
return [];
|
|
@@ -4624,30 +5126,44 @@ function readLiteralObjectName(config) {
|
|
|
4624
5126
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4625
5127
|
function validateFlowNodeWrites(stack) {
|
|
4626
5128
|
const findings = [];
|
|
4627
|
-
if (!
|
|
5129
|
+
if (!isRec17(stack)) return findings;
|
|
4628
5130
|
const flows = asArray22(stack.flows);
|
|
4629
5131
|
if (flows.length === 0) return findings;
|
|
4630
5132
|
let objectFields = null;
|
|
5133
|
+
let anchors = null;
|
|
4631
5134
|
flows.forEach((flow, flowIndex) => {
|
|
4632
5135
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
4633
5136
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4634
5137
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4635
5138
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4636
|
-
const config =
|
|
5139
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
4637
5140
|
if (!config) return;
|
|
4638
5141
|
const fields = config.fields;
|
|
4639
|
-
if (!
|
|
5142
|
+
if (!isRec17(fields)) return;
|
|
4640
5143
|
const written = Object.keys(fields);
|
|
4641
5144
|
if (written.length === 0) return;
|
|
4642
5145
|
const objectName = readLiteralObjectName(config);
|
|
4643
5146
|
if (!objectName) return;
|
|
4644
5147
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5148
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
4645
5149
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
4646
5150
|
if (!known) return;
|
|
4647
5151
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
4648
5152
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
4649
5153
|
for (const fieldName of written) {
|
|
4650
|
-
if (known.has(fieldName)
|
|
5154
|
+
if (known.has(fieldName)) continue;
|
|
5155
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
5156
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
5157
|
+
findings.push({
|
|
5158
|
+
severity: "warning",
|
|
5159
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
5160
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5161
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5162
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
5163
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
5164
|
+
});
|
|
5165
|
+
continue;
|
|
5166
|
+
}
|
|
4651
5167
|
if (fieldName.includes(".")) continue;
|
|
4652
5168
|
findings.push({
|
|
4653
5169
|
severity: "error",
|
|
@@ -4769,14 +5285,14 @@ import {
|
|
|
4769
5285
|
chartAggregateResultKeys,
|
|
4770
5286
|
isRecordContextBlockType
|
|
4771
5287
|
} from "@objectstack/spec/ui";
|
|
4772
|
-
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
5288
|
+
import { VALID_AST_OPERATORS as VALID_AST_OPERATORS2 } from "@objectstack/spec/data";
|
|
4773
5289
|
|
|
4774
5290
|
// src/zod-issue-format.ts
|
|
4775
|
-
var
|
|
5291
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4776
5292
|
var valueAtPath = (root, path) => {
|
|
4777
5293
|
let cur = root;
|
|
4778
5294
|
for (const key of path) {
|
|
4779
|
-
if (!
|
|
5295
|
+
if (!isRec18(cur) && !Array.isArray(cur)) return void 0;
|
|
4780
5296
|
cur = cur[key];
|
|
4781
5297
|
}
|
|
4782
5298
|
return cur;
|
|
@@ -4908,6 +5424,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4908
5424
|
};
|
|
4909
5425
|
return perPosition(init.expression);
|
|
4910
5426
|
}
|
|
5427
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
4911
5428
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4912
5429
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4913
5430
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -4915,7 +5432,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4915
5432
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4916
5433
|
function checkChartDrillDown(raw, push2) {
|
|
4917
5434
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4918
|
-
if (!
|
|
5435
|
+
if (!isRec19(raw)) {
|
|
4919
5436
|
push2(
|
|
4920
5437
|
"error",
|
|
4921
5438
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4938,7 +5455,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4938
5455
|
}
|
|
4939
5456
|
function checkChartAggregate(raw, push2) {
|
|
4940
5457
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4941
|
-
if (!
|
|
5458
|
+
if (!isRec19(raw)) {
|
|
4942
5459
|
push2(
|
|
4943
5460
|
"error",
|
|
4944
5461
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4969,7 +5486,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4969
5486
|
);
|
|
4970
5487
|
}
|
|
4971
5488
|
}
|
|
4972
|
-
var
|
|
5489
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4973
5490
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4974
5491
|
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4975
5492
|
const { values, where, path } = attrs;
|
|
@@ -4979,11 +5496,11 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
4979
5496
|
const aggregate = values.get("aggregate");
|
|
4980
5497
|
checkChartAggregate(aggregate, push2);
|
|
4981
5498
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4982
|
-
if (!
|
|
5499
|
+
if (!isRec19(aggregate)) return;
|
|
4983
5500
|
const fn = strOf(aggregate.function);
|
|
4984
5501
|
const field = strOf(aggregate.field);
|
|
4985
5502
|
const groupBy = aggregate.groupBy;
|
|
4986
|
-
const groupByField = strOf(groupBy) ?? (
|
|
5503
|
+
const groupByField = strOf(groupBy) ?? (isRec19(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4987
5504
|
const objectName = strOf(values.get("objectName"));
|
|
4988
5505
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4989
5506
|
if (objectName && known) {
|
|
@@ -5027,18 +5544,18 @@ function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors =
|
|
|
5027
5544
|
);
|
|
5028
5545
|
};
|
|
5029
5546
|
const xAxisRaw = values.get("xAxis");
|
|
5030
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5547
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec19(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
5031
5548
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
5032
5549
|
axisRef(categoryAxis, categoryProp);
|
|
5033
5550
|
const yAxisRaw = values.get("yAxis");
|
|
5034
5551
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
5035
5552
|
for (const a of yAxisList) {
|
|
5036
|
-
axisRef(strOf(a) ?? (
|
|
5553
|
+
axisRef(strOf(a) ?? (isRec19(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
5037
5554
|
}
|
|
5038
5555
|
const series = values.get("series");
|
|
5039
5556
|
if (Array.isArray(series)) {
|
|
5040
5557
|
for (const s of series) {
|
|
5041
|
-
if (!
|
|
5558
|
+
if (!isRec19(s)) continue;
|
|
5042
5559
|
const dataKey = strOf(s.dataKey);
|
|
5043
5560
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
5044
5561
|
}
|
|
@@ -5094,7 +5611,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
5094
5611
|
if (!Array.isArray(value)) return { child, parent };
|
|
5095
5612
|
for (let i = 0; i < value.length; i++) {
|
|
5096
5613
|
const sub = value[i];
|
|
5097
|
-
if (!
|
|
5614
|
+
if (!isRec19(sub)) continue;
|
|
5098
5615
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
5099
5616
|
child.push({
|
|
5100
5617
|
objectName: strOf(sub.childObject),
|
|
@@ -5119,7 +5636,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
5119
5636
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
5120
5637
|
return;
|
|
5121
5638
|
}
|
|
5122
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5639
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS2.has(node[1].toLowerCase())) {
|
|
5123
5640
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
5124
5641
|
}
|
|
5125
5642
|
}
|
|
@@ -5139,20 +5656,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
5139
5656
|
}
|
|
5140
5657
|
for (const key of spec.nestedFields ?? []) {
|
|
5141
5658
|
const v = readable(key);
|
|
5142
|
-
if (
|
|
5659
|
+
if (isRec19(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
5143
5660
|
}
|
|
5144
5661
|
for (const key of spec.sections ?? []) {
|
|
5145
5662
|
const v = readable(key);
|
|
5146
5663
|
if (!Array.isArray(v)) continue;
|
|
5147
5664
|
for (let i = 0; i < v.length; i++) {
|
|
5148
5665
|
const section = v[i];
|
|
5149
|
-
if (!
|
|
5666
|
+
if (!isRec19(section)) continue;
|
|
5150
5667
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
5151
5668
|
}
|
|
5152
5669
|
}
|
|
5153
5670
|
for (const key of spec.keyedByField ?? []) {
|
|
5154
5671
|
const v = readable(key);
|
|
5155
|
-
if (!
|
|
5672
|
+
if (!isRec19(v)) continue;
|
|
5156
5673
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
5157
5674
|
}
|
|
5158
5675
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -5236,11 +5753,20 @@ function validateReactPageProps(stack) {
|
|
|
5236
5753
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
5237
5754
|
const name = String(page.name ?? `#${p}`);
|
|
5238
5755
|
const tsc = loadTypeScript2();
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5756
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
|
|
5757
|
+
target: tsc.ScriptTarget.Latest,
|
|
5758
|
+
setParentNodes: true,
|
|
5759
|
+
scriptKind: tsc.ScriptKind.TSX
|
|
5760
|
+
});
|
|
5761
|
+
if (failure) {
|
|
5762
|
+
findings.push({
|
|
5763
|
+
severity: "warning",
|
|
5764
|
+
rule: REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
5765
|
+
where: `page "${name}"`,
|
|
5766
|
+
path: `pages[${p}].source`,
|
|
5767
|
+
message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
|
|
5768
|
+
hint: PARSE_FAILURE_HINT
|
|
5769
|
+
});
|
|
5244
5770
|
}
|
|
5245
5771
|
const locals = localComponentNames(tsc, sf);
|
|
5246
5772
|
const visit = (node) => {
|
|
@@ -5319,7 +5845,12 @@ function validateReactPageProps(stack) {
|
|
|
5319
5845
|
searchTargets,
|
|
5320
5846
|
where,
|
|
5321
5847
|
`${path} \u203A searchableFields`,
|
|
5322
|
-
"searchableFields"
|
|
5848
|
+
"searchableFields",
|
|
5849
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
5850
|
+
// default, spelled out here because the #8404 provenance index
|
|
5851
|
+
// follows it positionally.
|
|
5852
|
+
"narrowing",
|
|
5853
|
+
unprovisionedAnchors
|
|
5323
5854
|
)
|
|
5324
5855
|
);
|
|
5325
5856
|
}
|
|
@@ -5338,9 +5869,26 @@ function validateReactPageProps(stack) {
|
|
|
5338
5869
|
}
|
|
5339
5870
|
|
|
5340
5871
|
// src/reference-integrity-suite.ts
|
|
5872
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
5341
5873
|
var REFERENCE_INTEGRITY_RULES = [
|
|
5342
5874
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5343
|
-
|
|
5875
|
+
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
|
|
5876
|
+
// both judge a LIST VIEW's field references, and a standalone list view is
|
|
5877
|
+
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
|
|
5878
|
+
// an MCP/AI author has. Their walks read the flattened overlay shape that
|
|
5879
|
+
// door carries (see each rule's `views[]` self rung), and they resolve only
|
|
5880
|
+
// against `stack.objects`, which the per-write snapshot DOES carry — so the
|
|
5881
|
+
// crossing has no missing-collection false-positive channel. Measured over
|
|
5882
|
+
// the shipped view corpus before crossing (0 refusals; population in the
|
|
5883
|
+
// #9313 PR).
|
|
5884
|
+
{ name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
|
|
5885
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
5886
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
5887
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
5888
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
5889
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
5890
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
5891
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
5344
5892
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
5345
5893
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5346
5894
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -5446,9 +5994,11 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5446
5994
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5447
5995
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
5448
5996
|
];
|
|
5449
|
-
function validateReferenceIntegrity(stack) {
|
|
5997
|
+
function validateReferenceIntegrity(stack, options) {
|
|
5450
5998
|
const findings = [];
|
|
5999
|
+
const writeType = options?.runtimeWriteType;
|
|
5451
6000
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
6001
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
5452
6002
|
findings.push(...rule.run(stack));
|
|
5453
6003
|
}
|
|
5454
6004
|
return findings;
|
|
@@ -5459,10 +6009,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
5459
6009
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
5460
6010
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5461
6011
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5462
|
-
function
|
|
6012
|
+
function isRec20(v) {
|
|
5463
6013
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5464
6014
|
}
|
|
5465
|
-
function
|
|
6015
|
+
function strName19(v) {
|
|
5466
6016
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5467
6017
|
}
|
|
5468
6018
|
function asArray25(v) {
|
|
@@ -5476,8 +6026,8 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
5476
6026
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5477
6027
|
function suppliedByDataSource(issue, component) {
|
|
5478
6028
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5479
|
-
const dataSource =
|
|
5480
|
-
return
|
|
6029
|
+
const dataSource = isRec20(component.dataSource) ? component.dataSource : void 0;
|
|
6030
|
+
return strName19(dataSource?.object) !== void 0;
|
|
5481
6031
|
}
|
|
5482
6032
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
5483
6033
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -5494,18 +6044,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
5494
6044
|
}
|
|
5495
6045
|
function validateComponentProps(stack) {
|
|
5496
6046
|
const findings = [];
|
|
5497
|
-
if (!
|
|
6047
|
+
if (!isRec20(stack)) return findings;
|
|
5498
6048
|
const pages = asArray25(stack.pages);
|
|
5499
6049
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5500
6050
|
const page = pages[pi];
|
|
5501
|
-
if (!
|
|
5502
|
-
const pageName =
|
|
6051
|
+
if (!isRec20(page)) continue;
|
|
6052
|
+
const pageName = strName19(page.name) ?? `#${pi}`;
|
|
5503
6053
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5504
|
-
const type =
|
|
6054
|
+
const type = strName19(component.type);
|
|
5505
6055
|
if (!type) continue;
|
|
5506
6056
|
const schema = PROPS_SCHEMAS[type];
|
|
5507
6057
|
if (!schema) continue;
|
|
5508
|
-
const props =
|
|
6058
|
+
const props = isRec20(component.properties) ? component.properties : void 0;
|
|
5509
6059
|
if (!props) continue;
|
|
5510
6060
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5511
6061
|
const base = `${path}.properties`;
|
|
@@ -6683,10 +7233,10 @@ function asArray35(v) {
|
|
|
6683
7233
|
}
|
|
6684
7234
|
return [];
|
|
6685
7235
|
}
|
|
6686
|
-
function
|
|
7236
|
+
function isRec21(v) {
|
|
6687
7237
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6688
7238
|
}
|
|
6689
|
-
function
|
|
7239
|
+
function strName20(v) {
|
|
6690
7240
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6691
7241
|
}
|
|
6692
7242
|
function fieldNameOf(entry) {
|
|
@@ -6707,7 +7257,7 @@ function validateFormLayout(stack) {
|
|
|
6707
7257
|
objectFields.set(name, new Set(fields));
|
|
6708
7258
|
}
|
|
6709
7259
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6710
|
-
const viewName =
|
|
7260
|
+
const viewName = strName20(view.name) ?? strName20(view.object) ?? viewPath;
|
|
6711
7261
|
const containerObject = viewObjectName(view);
|
|
6712
7262
|
for (const site of formViewSites(view, viewPath)) {
|
|
6713
7263
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6717,7 +7267,7 @@ function validateFormLayout(stack) {
|
|
|
6717
7267
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6718
7268
|
for (let s = 0; s < sections.length; s++) {
|
|
6719
7269
|
const sec = sections[s];
|
|
6720
|
-
const secFields =
|
|
7270
|
+
const secFields = isRec21(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6721
7271
|
for (let f = 0; f < secFields.length; f++) {
|
|
6722
7272
|
const entry = secFields[f];
|
|
6723
7273
|
const fname = fieldNameOf(entry);
|
|
@@ -6732,7 +7282,7 @@ function validateFormLayout(stack) {
|
|
|
6732
7282
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6733
7283
|
});
|
|
6734
7284
|
}
|
|
6735
|
-
const colSpan =
|
|
7285
|
+
const colSpan = isRec21(entry) ? entry.colSpan : void 0;
|
|
6736
7286
|
if (colSpan != null) {
|
|
6737
7287
|
findings.push({
|
|
6738
7288
|
severity: "warning",
|
|
@@ -6895,12 +7445,12 @@ function bareRhsOnlyIdentifiers(ast) {
|
|
|
6895
7445
|
for (const name of elsewhere) rhs.delete(name);
|
|
6896
7446
|
return rhs;
|
|
6897
7447
|
}
|
|
6898
|
-
function
|
|
7448
|
+
function isRec22(v) {
|
|
6899
7449
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6900
7450
|
}
|
|
6901
7451
|
function schemaIdOf(view) {
|
|
6902
7452
|
const data = view.data;
|
|
6903
|
-
if (!
|
|
7453
|
+
if (!isRec22(data)) return void 0;
|
|
6904
7454
|
if (data.provider !== "schema") return void 0;
|
|
6905
7455
|
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6906
7456
|
}
|
|
@@ -7272,7 +7822,7 @@ function predicateSource2(v) {
|
|
|
7272
7822
|
}
|
|
7273
7823
|
return void 0;
|
|
7274
7824
|
}
|
|
7275
|
-
function
|
|
7825
|
+
function isRec23(v) {
|
|
7276
7826
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7277
7827
|
}
|
|
7278
7828
|
function checkPredicate(source, scope, where, path, findings) {
|
|
@@ -7343,7 +7893,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
7343
7893
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
7344
7894
|
for (let i = 0; i < entries.length; i++) {
|
|
7345
7895
|
const entry = entries[i];
|
|
7346
|
-
if (!
|
|
7896
|
+
if (!isRec23(entry)) continue;
|
|
7347
7897
|
const path = `${base}[${i}]`;
|
|
7348
7898
|
for (const key of PREDICATE_KEYS) {
|
|
7349
7899
|
const source = predicateSource2(entry[key]);
|
|
@@ -7377,7 +7927,7 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
7377
7927
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7378
7928
|
for (let s = 0; s < sections.length; s++) {
|
|
7379
7929
|
const section = sections[s];
|
|
7380
|
-
if (!
|
|
7930
|
+
if (!isRec23(section)) continue;
|
|
7381
7931
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7382
7932
|
for (const key of PREDICATE_KEYS) {
|
|
7383
7933
|
const source = predicateSource2(section[key]);
|
|
@@ -7661,6 +8211,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7661
8211
|
}
|
|
7662
8212
|
}
|
|
7663
8213
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
8214
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
7664
8215
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7665
8216
|
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7666
8217
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -7684,7 +8235,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7684
8235
|
}
|
|
7685
8236
|
}
|
|
7686
8237
|
const delegatedFrom = rec.delegated_from;
|
|
7687
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
8238
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
7688
8239
|
const reason = rec.reason;
|
|
7689
8240
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
7690
8241
|
findings.push({
|
|
@@ -7850,6 +8401,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7850
8401
|
import { compileCelToFilter } from "@objectstack/formula";
|
|
7851
8402
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7852
8403
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
8404
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
8405
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
7853
8406
|
function asArray38(v) {
|
|
7854
8407
|
if (Array.isArray(v)) return v;
|
|
7855
8408
|
if (v && typeof v === "object") {
|
|
@@ -7874,10 +8427,67 @@ function sourceOf(condition) {
|
|
|
7874
8427
|
return str2(input?.source);
|
|
7875
8428
|
}
|
|
7876
8429
|
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).";
|
|
8430
|
+
function effectiveSharingModelOf(obj) {
|
|
8431
|
+
const m = obj.sharingModel;
|
|
8432
|
+
if (m === "private") return "private";
|
|
8433
|
+
if (m === "public_read") return "read";
|
|
8434
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
8435
|
+
if (m == null) {
|
|
8436
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
8437
|
+
return isSystem ? "public" : "private";
|
|
8438
|
+
}
|
|
8439
|
+
return "private";
|
|
8440
|
+
}
|
|
8441
|
+
function masterOf(obj) {
|
|
8442
|
+
for (const f of asArray38(obj.fields)) {
|
|
8443
|
+
if (f.type === "master_detail") {
|
|
8444
|
+
const ref = f.reference;
|
|
8445
|
+
if (typeof ref === "string" && ref) return ref;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
return void 0;
|
|
8449
|
+
}
|
|
8450
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
8451
|
+
const object = str2(rule.object);
|
|
8452
|
+
if (!object) return [];
|
|
8453
|
+
const target = objectsByName.get(object);
|
|
8454
|
+
if (!target) return [];
|
|
8455
|
+
const name = str2(rule.name) || String(index);
|
|
8456
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
8457
|
+
const path = `sharingRules[${index}].object`;
|
|
8458
|
+
const owd = target.sharingModel;
|
|
8459
|
+
if (owd === "controlled_by_parent") {
|
|
8460
|
+
const master = masterOf(target);
|
|
8461
|
+
return [{
|
|
8462
|
+
severity: "error",
|
|
8463
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
8464
|
+
where,
|
|
8465
|
+
path,
|
|
8466
|
+
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.`,
|
|
8467
|
+
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.`
|
|
8468
|
+
}];
|
|
8469
|
+
}
|
|
8470
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
8471
|
+
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`;
|
|
8472
|
+
return [{
|
|
8473
|
+
severity: "error",
|
|
8474
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
8475
|
+
where,
|
|
8476
|
+
path,
|
|
8477
|
+
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.`,
|
|
8478
|
+
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.`
|
|
8479
|
+
}];
|
|
8480
|
+
}
|
|
7877
8481
|
function validateSharingRuleEnforceability(stack) {
|
|
7878
8482
|
const findings = [];
|
|
7879
8483
|
const cfg = stack ?? {};
|
|
8484
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
8485
|
+
for (const obj of asArray38(cfg.objects)) {
|
|
8486
|
+
const name = str2(obj.name);
|
|
8487
|
+
if (name) objectsByName.set(name, obj);
|
|
8488
|
+
}
|
|
7880
8489
|
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
8490
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
7881
8491
|
const input = toCompilerInput(rule.condition);
|
|
7882
8492
|
if (input === null) return;
|
|
7883
8493
|
const result = compileCelToFilter(input, { variables: {} });
|
|
@@ -8007,11 +8617,11 @@ import { createRequire as createRequire4 } from "module";
|
|
|
8007
8617
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
8008
8618
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
8009
8619
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
8010
|
-
var
|
|
8620
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8011
8621
|
function asArray40(v) {
|
|
8012
|
-
if (Array.isArray(v)) return v.filter(
|
|
8013
|
-
if (
|
|
8014
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8622
|
+
if (Array.isArray(v)) return v.filter(isRec24);
|
|
8623
|
+
if (isRec24(v)) {
|
|
8624
|
+
return Object.entries(v).filter(([, def]) => isRec24(def)).map(([name, def]) => ({ name, ...def }));
|
|
8015
8625
|
}
|
|
8016
8626
|
return [];
|
|
8017
8627
|
}
|
|
@@ -8028,7 +8638,7 @@ function loadAjv() {
|
|
|
8028
8638
|
`@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
8639
|
);
|
|
8030
8640
|
}
|
|
8031
|
-
const ctor =
|
|
8641
|
+
const ctor = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8032
8642
|
cachedAjv = ctor;
|
|
8033
8643
|
return ctor;
|
|
8034
8644
|
}
|
|
@@ -8043,7 +8653,7 @@ function loadAddFormats() {
|
|
|
8043
8653
|
`@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
8654
|
);
|
|
8045
8655
|
}
|
|
8046
|
-
const plugin =
|
|
8656
|
+
const plugin = isRec24(mod) && "default" in mod ? mod.default : mod;
|
|
8047
8657
|
cachedAddFormats = plugin;
|
|
8048
8658
|
return plugin;
|
|
8049
8659
|
}
|
|
@@ -8073,13 +8683,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
8073
8683
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
8074
8684
|
for (const branch of ["then", "otherwise"]) {
|
|
8075
8685
|
const nested = rule[branch];
|
|
8076
|
-
if (
|
|
8686
|
+
if (isRec24(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
8077
8687
|
}
|
|
8078
8688
|
return out;
|
|
8079
8689
|
}
|
|
8080
8690
|
function walkObjectValidationRules(stack) {
|
|
8081
8691
|
const walked = [];
|
|
8082
|
-
if (!
|
|
8692
|
+
if (!isRec24(stack)) return walked;
|
|
8083
8693
|
for (const obj of asArray40(stack.objects)) {
|
|
8084
8694
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8085
8695
|
const validations = obj.validations;
|
|
@@ -8114,7 +8724,7 @@ function validateRuleCompilability(stack) {
|
|
|
8114
8724
|
});
|
|
8115
8725
|
}
|
|
8116
8726
|
}
|
|
8117
|
-
if (rule.type === "json_schema" &&
|
|
8727
|
+
if (rule.type === "json_schema" && isRec24(rule.schema)) {
|
|
8118
8728
|
try {
|
|
8119
8729
|
createRuntimeAjv().compile(rule.schema);
|
|
8120
8730
|
} catch (err) {
|
|
@@ -8134,7 +8744,7 @@ function validateRuleCompilability(stack) {
|
|
|
8134
8744
|
|
|
8135
8745
|
// src/validate-rule-schema-formats.ts
|
|
8136
8746
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
8137
|
-
var
|
|
8747
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8138
8748
|
var SUBSCHEMA_KEYS = [
|
|
8139
8749
|
"additionalItems",
|
|
8140
8750
|
"additionalProperties",
|
|
@@ -8158,7 +8768,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
8158
8768
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
8159
8769
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
8160
8770
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
8161
|
-
if (!
|
|
8771
|
+
if (!isRec25(schema)) return;
|
|
8162
8772
|
if (typeof schema.format === "string") {
|
|
8163
8773
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
8164
8774
|
}
|
|
@@ -8177,7 +8787,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8177
8787
|
}
|
|
8178
8788
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
8179
8789
|
const value = schema[key];
|
|
8180
|
-
if (!
|
|
8790
|
+
if (!isRec25(value)) continue;
|
|
8181
8791
|
for (const [name, entry] of Object.entries(value)) {
|
|
8182
8792
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8183
8793
|
}
|
|
@@ -8185,13 +8795,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
8185
8795
|
const items = schema.items;
|
|
8186
8796
|
if (Array.isArray(items)) {
|
|
8187
8797
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
8188
|
-
} else if (
|
|
8798
|
+
} else if (isRec25(items)) {
|
|
8189
8799
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
8190
8800
|
}
|
|
8191
8801
|
const dependencies = schema.dependencies;
|
|
8192
|
-
if (
|
|
8802
|
+
if (isRec25(dependencies)) {
|
|
8193
8803
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
8194
|
-
if (!
|
|
8804
|
+
if (!isRec25(entry)) continue;
|
|
8195
8805
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
8196
8806
|
}
|
|
8197
8807
|
}
|
|
@@ -8218,9 +8828,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
8218
8828
|
let best = null;
|
|
8219
8829
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
8220
8830
|
for (const candidate of [...registered].sort()) {
|
|
8221
|
-
const
|
|
8222
|
-
if (
|
|
8223
|
-
bestDistance =
|
|
8831
|
+
const distance8 = editDistance2(authored, candidate);
|
|
8832
|
+
if (distance8 < bestDistance) {
|
|
8833
|
+
bestDistance = distance8;
|
|
8224
8834
|
best = candidate;
|
|
8225
8835
|
}
|
|
8226
8836
|
}
|
|
@@ -8230,7 +8840,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
8230
8840
|
const findings = [];
|
|
8231
8841
|
const pending = [];
|
|
8232
8842
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
8233
|
-
if (rule.type !== "json_schema" || !
|
|
8843
|
+
if (rule.type !== "json_schema" || !isRec25(rule.schema)) continue;
|
|
8234
8844
|
const uses = [];
|
|
8235
8845
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
8236
8846
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -8263,7 +8873,7 @@ function asArray41(v) {
|
|
|
8263
8873
|
}
|
|
8264
8874
|
return [];
|
|
8265
8875
|
}
|
|
8266
|
-
function
|
|
8876
|
+
function strName21(v) {
|
|
8267
8877
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8268
8878
|
}
|
|
8269
8879
|
function strList3(v) {
|
|
@@ -8278,7 +8888,7 @@ function collectNamePlacedActions(stack) {
|
|
|
8278
8888
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
8279
8889
|
}
|
|
8280
8890
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8281
|
-
const n =
|
|
8891
|
+
const n = strName21(def?.name);
|
|
8282
8892
|
if (n) placed.add(n);
|
|
8283
8893
|
}
|
|
8284
8894
|
};
|
|
@@ -8304,7 +8914,7 @@ function validateActionLocations(stack) {
|
|
|
8304
8914
|
const check = (action, path) => {
|
|
8305
8915
|
if (!action || typeof action !== "object") return;
|
|
8306
8916
|
if ("locations" in action) return;
|
|
8307
|
-
const name =
|
|
8917
|
+
const name = strName21(action.name);
|
|
8308
8918
|
if (!name) return;
|
|
8309
8919
|
if (namePlaced.has(name)) return;
|
|
8310
8920
|
findings.push({
|
|
@@ -9157,6 +9767,17 @@ function lintViewRefs(stack) {
|
|
|
9157
9767
|
}
|
|
9158
9768
|
|
|
9159
9769
|
// src/data-model-rules.ts
|
|
9770
|
+
function objectWhere(obj) {
|
|
9771
|
+
return `object "${obj?.name}"`;
|
|
9772
|
+
}
|
|
9773
|
+
function indexWhere(obj, idx, j, cols) {
|
|
9774
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
9775
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
9776
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
9777
|
+
}
|
|
9778
|
+
function fieldWhere(obj, fieldName) {
|
|
9779
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
9780
|
+
}
|
|
9160
9781
|
function fieldEntries2(fields) {
|
|
9161
9782
|
if (!fields) return [];
|
|
9162
9783
|
if (Array.isArray(fields)) {
|
|
@@ -9190,11 +9811,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9190
9811
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9191
9812
|
const idx = declaredIndexes[j];
|
|
9192
9813
|
if (idx?.unique !== true) continue;
|
|
9193
|
-
const
|
|
9814
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
9815
|
+
const cols = colList.join(", ");
|
|
9194
9816
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9195
9817
|
issues.push({
|
|
9196
9818
|
severity: "warning",
|
|
9197
9819
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9820
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9198
9821
|
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
9822
|
path: `objects[${i}].indexes[${j}]`,
|
|
9200
9823
|
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 +9866,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9243
9866
|
issues.push({
|
|
9244
9867
|
severity: "warning",
|
|
9245
9868
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
9869
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
9870
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
9871
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
9872
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
9873
|
+
where: fieldWhere(obj, name),
|
|
9246
9874
|
message,
|
|
9247
9875
|
path: `objects[${i}]`,
|
|
9248
9876
|
fix
|
|
@@ -9271,6 +9899,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9271
9899
|
issues.push({
|
|
9272
9900
|
severity: "warning",
|
|
9273
9901
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9902
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9274
9903
|
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
9904
|
path: `objects[${i}].indexes[${j}]`,
|
|
9276
9905
|
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 +9916,7 @@ var CLI_ONLY = ["cli"];
|
|
|
9287
9916
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
9288
9917
|
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
9918
|
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
|
|
9919
|
+
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
9920
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
9292
9921
|
var AUTHORING_RULES = [
|
|
9293
9922
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -9348,8 +9977,14 @@ var AUTHORING_RULES = [
|
|
|
9348
9977
|
input: "normalized",
|
|
9349
9978
|
commands: ALL,
|
|
9350
9979
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
9351
|
-
|
|
9352
|
-
|
|
9980
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
9981
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
9982
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
9983
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
9984
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
9985
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
9986
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9987
|
+
runtimeTypes: ["object"],
|
|
9353
9988
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
9354
9989
|
},
|
|
9355
9990
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -9373,8 +10008,11 @@ var AUTHORING_RULES = [
|
|
|
9373
10008
|
input: "normalized",
|
|
9374
10009
|
commands: ALL,
|
|
9375
10010
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9376
|
-
|
|
9377
|
-
|
|
10011
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
10012
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
10013
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
10014
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10015
|
+
runtimeTypes: ["object"],
|
|
9378
10016
|
run: (stack) => validateManagedApiMethods(stack)
|
|
9379
10017
|
},
|
|
9380
10018
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -9396,19 +10034,38 @@ var AUTHORING_RULES = [
|
|
|
9396
10034
|
},
|
|
9397
10035
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
9398
10036
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
10037
|
+
//
|
|
10038
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
10039
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
10040
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
10041
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
10042
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
10043
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
10044
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
10045
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
10046
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
10047
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
10048
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
10049
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
10050
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
10051
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
10052
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
10053
|
+
// advisory channel and never block (#4463 P1).
|
|
9399
10054
|
{
|
|
9400
10055
|
name: "validateWidgetBindings",
|
|
9401
10056
|
tier: "gating",
|
|
9402
10057
|
input: "parsed",
|
|
9403
10058
|
commands: ALL,
|
|
9404
10059
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
9405
|
-
surfaces:
|
|
9406
|
-
|
|
10060
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10061
|
+
runtimeTypes: ["dashboard"],
|
|
9407
10062
|
run: (stack) => validateWidgetBindings(stack)
|
|
9408
10063
|
},
|
|
9409
|
-
// ADR-0049 / #3367 — a header
|
|
9410
|
-
//
|
|
9411
|
-
//
|
|
10064
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
10065
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
10066
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
10067
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
10068
|
+
// Unresolved `url` routes stay advisory.
|
|
9412
10069
|
{
|
|
9413
10070
|
name: "validateDashboardActionRefs",
|
|
9414
10071
|
tier: "gating",
|
|
@@ -9433,6 +10090,28 @@ var AUTHORING_RULES = [
|
|
|
9433
10090
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
9434
10091
|
run: (stack) => validateFilterTokens(stack)
|
|
9435
10092
|
},
|
|
10093
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
10094
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
10095
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
10096
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
10097
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
10098
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
10099
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
10100
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
10101
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
10102
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
10103
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
10104
|
+
// vocabulary is near enough to reach for.
|
|
10105
|
+
{
|
|
10106
|
+
name: "validatePresetComparands",
|
|
10107
|
+
tier: "gating",
|
|
10108
|
+
input: "parsed",
|
|
10109
|
+
commands: ALL,
|
|
10110
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
10111
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10112
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
10113
|
+
run: (stack) => validatePresetComparands(stack)
|
|
10114
|
+
},
|
|
9436
10115
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9437
10116
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9438
10117
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -9476,9 +10155,31 @@ var AUTHORING_RULES = [
|
|
|
9476
10155
|
// collection in the snapshot and return nothing, and the two that would
|
|
9477
10156
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
9478
10157
|
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
10158
|
+
//
|
|
10159
|
+
// [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
|
|
10160
|
+
// taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
|
|
10161
|
+
// dispatch the suite, and the suite's own per-member `runtimeTypes`
|
|
10162
|
+
// (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
|
|
10163
|
+
// that snapshot — the gate passes the written type through
|
|
10164
|
+
// `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
|
|
10165
|
+
// members that judge a list view's field references and resolve only
|
|
10166
|
+
// against the `objects` collection the snapshot carries
|
|
10167
|
+
// (`validateSearchableFields`, `validateSortableFields`). The whole suite
|
|
10168
|
+
// is NOT the right granularity for this door, measured not assumed, and
|
|
10169
|
+
// the crossing fails differently per body shape — both ways wrong:
|
|
10170
|
+
// `validateActionNameRefs` (error-tier) resolves `views[].list` /
|
|
10171
|
+
// `views[].listViews.*` action names against `stack.actions`, a collection
|
|
10172
|
+
// no per-write snapshot carries, so on a CONTAINER view write it would
|
|
10173
|
+
// refuse every stack-level action the body names (measured:
|
|
10174
|
+
// `action-name-undefined` on the snapshot shape, clean on the full stack)
|
|
10175
|
+
// — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
|
|
10176
|
+
// type the gate has. On a FLATTENED overlay it has no rung at all, so the
|
|
10177
|
+
// crossing would be a silent no-op that reads as coverage — the very shape
|
|
10178
|
+
// #9313 was filed about. Flow snapshots are unchanged: every member keeps
|
|
10179
|
+
// the default `flow` declaration.
|
|
9479
10180
|
surfaces: CLI_AND_RUNTIME,
|
|
9480
|
-
runtimeTypes: ["flow"],
|
|
9481
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
10181
|
+
runtimeTypes: ["flow", "view"],
|
|
10182
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
9482
10183
|
},
|
|
9483
10184
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
9484
10185
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -9651,7 +10352,7 @@ var AUTHORING_RULES = [
|
|
|
9651
10352
|
commands: ALL,
|
|
9652
10353
|
source: "packages/lint/src/validate-record-title.ts",
|
|
9653
10354
|
surfaces: CLI_ONLY,
|
|
9654
|
-
surfaceReason:
|
|
10355
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9655
10356
|
run: (stack) => validateRecordTitle(stack)
|
|
9656
10357
|
},
|
|
9657
10358
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -9664,7 +10365,7 @@ var AUTHORING_RULES = [
|
|
|
9664
10365
|
commands: ALL,
|
|
9665
10366
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
9666
10367
|
surfaces: CLI_ONLY,
|
|
9667
|
-
surfaceReason:
|
|
10368
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9668
10369
|
run: (stack) => validateSemanticRoles(stack)
|
|
9669
10370
|
},
|
|
9670
10371
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -9875,7 +10576,7 @@ var AUTHORING_RULES = [
|
|
|
9875
10576
|
commands: ALL,
|
|
9876
10577
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
9877
10578
|
surfaces: CLI_ONLY,
|
|
9878
|
-
surfaceReason:
|
|
10579
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9879
10580
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
9880
10581
|
severity: "warning",
|
|
9881
10582
|
rule: f.rule,
|
|
@@ -9894,8 +10595,12 @@ var AUTHORING_RULES = [
|
|
|
9894
10595
|
input: "parsed",
|
|
9895
10596
|
commands: ALL,
|
|
9896
10597
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
9897
|
-
|
|
9898
|
-
|
|
10598
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
10599
|
+
// object does not carry is broken from the first record; only the error
|
|
10600
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
10601
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
10602
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10603
|
+
runtimeTypes: ["object"],
|
|
9899
10604
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
9900
10605
|
severity: f.severity,
|
|
9901
10606
|
rule: f.rule,
|
|
@@ -9936,12 +10641,12 @@ var AUTHORING_RULES = [
|
|
|
9936
10641
|
commands: ["validate", "build"],
|
|
9937
10642
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9938
10643
|
surfaces: CLI_ONLY,
|
|
9939
|
-
surfaceReason:
|
|
10644
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9940
10645
|
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
10646
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9942
10647
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9943
10648
|
rule: f.rule,
|
|
9944
|
-
where: f.
|
|
10649
|
+
where: f.where,
|
|
9945
10650
|
path: f.path,
|
|
9946
10651
|
message: f.message,
|
|
9947
10652
|
hint: f.fix ?? ""
|
|
@@ -9957,12 +10662,12 @@ var AUTHORING_RULES = [
|
|
|
9957
10662
|
commands: ["validate", "build"],
|
|
9958
10663
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9959
10664
|
surfaces: CLI_ONLY,
|
|
9960
|
-
surfaceReason:
|
|
10665
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9961
10666
|
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
10667
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9963
10668
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9964
10669
|
rule: f.rule,
|
|
9965
|
-
where: f.
|
|
10670
|
+
where: f.where,
|
|
9966
10671
|
path: f.path,
|
|
9967
10672
|
message: f.message,
|
|
9968
10673
|
hint: f.fix ?? ""
|
|
@@ -9979,12 +10684,12 @@ var AUTHORING_RULES = [
|
|
|
9979
10684
|
commands: ["validate", "build"],
|
|
9980
10685
|
source: "packages/lint/src/data-model-rules.ts",
|
|
9981
10686
|
surfaces: CLI_ONLY,
|
|
9982
|
-
surfaceReason:
|
|
10687
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
9983
10688
|
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
10689
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9985
10690
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9986
10691
|
rule: f.rule,
|
|
9987
|
-
where: f.
|
|
10692
|
+
where: f.where,
|
|
9988
10693
|
path: f.path,
|
|
9989
10694
|
message: f.message,
|
|
9990
10695
|
hint: f.fix ?? ""
|
|
@@ -10112,16 +10817,26 @@ var AUTHORING_RULES = [
|
|
|
10112
10817
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10113
10818
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10114
10819
|
},
|
|
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
|
-
//
|
|
10820
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
10821
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
10822
|
+
//
|
|
10823
|
+
// - `condition` has a single runtime consumer
|
|
10824
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
10825
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
10826
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
10827
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
10828
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
10829
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
10830
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
10831
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
10832
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
10833
|
+
//
|
|
10834
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
10835
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
10836
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
10837
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
10838
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
10839
|
+
// yet true.
|
|
10125
10840
|
{
|
|
10126
10841
|
name: "validateSharingRuleEnforceability",
|
|
10127
10842
|
tier: "gating",
|
|
@@ -10129,7 +10844,7 @@ var AUTHORING_RULES = [
|
|
|
10129
10844
|
commands: ALL,
|
|
10130
10845
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10131
10846
|
surfaces: CLI_ONLY,
|
|
10132
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
10847
|
+
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
10848
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10134
10849
|
},
|
|
10135
10850
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10168,8 +10883,15 @@ var AUTHORING_RULES = [
|
|
|
10168
10883
|
input: "parsed",
|
|
10169
10884
|
commands: ALL,
|
|
10170
10885
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10171
|
-
|
|
10172
|
-
|
|
10886
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
10887
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
10888
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
10889
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
10890
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
10891
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
10892
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
10893
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10894
|
+
runtimeTypes: ["object"],
|
|
10173
10895
|
run: (stack) => validateRuleCompilability(stack)
|
|
10174
10896
|
},
|
|
10175
10897
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10190,8 +10912,12 @@ var AUTHORING_RULES = [
|
|
|
10190
10912
|
input: "parsed",
|
|
10191
10913
|
commands: ALL,
|
|
10192
10914
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10193
|
-
|
|
10194
|
-
|
|
10915
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
10916
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
10917
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
10918
|
+
// format set is only enumerated once a schema actually names a format.
|
|
10919
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10920
|
+
runtimeTypes: ["object"],
|
|
10195
10921
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10196
10922
|
}
|
|
10197
10923
|
];
|
|
@@ -10233,7 +10959,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
10233
10959
|
permission: "permissions",
|
|
10234
10960
|
book: "books"
|
|
10235
10961
|
};
|
|
10236
|
-
|
|
10962
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
10963
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
10964
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
10965
|
+
return objects.filter((entry) => {
|
|
10966
|
+
if (!entry || typeof entry !== "object") return true;
|
|
10967
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
10968
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
10969
|
+
if (reachable.has(owner)) return true;
|
|
10970
|
+
return isSystemObject(entry);
|
|
10971
|
+
});
|
|
10972
|
+
}
|
|
10973
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
10974
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
10975
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
10237
10976
|
function runtimeAuthoringRulesFor(type) {
|
|
10238
10977
|
return AUTHORING_RULES.filter(
|
|
10239
10978
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -10259,7 +10998,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10259
10998
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10260
10999
|
const baseline = {};
|
|
10261
11000
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
10262
|
-
const
|
|
11001
|
+
const raw = args.context?.[key] ?? [];
|
|
11002
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
10263
11003
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10264
11004
|
}
|
|
10265
11005
|
const candidate = {
|
|
@@ -10268,6 +11008,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
10268
11008
|
};
|
|
10269
11009
|
return { baseline, candidate };
|
|
10270
11010
|
}
|
|
11011
|
+
var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
|
|
11012
|
+
var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11013
|
+
var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
|
|
11014
|
+
function nameKeyFindingPath(path, candidate) {
|
|
11015
|
+
const m = TOP_LEVEL_INDEX.exec(path);
|
|
11016
|
+
if (!m) return path;
|
|
11017
|
+
const [, stackKey, index, rest] = m;
|
|
11018
|
+
if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
|
|
11019
|
+
const collection = candidate[stackKey];
|
|
11020
|
+
const entry = collection?.[Number(index)];
|
|
11021
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
11022
|
+
if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
|
|
11023
|
+
return `${stackKey}.${name}${rest}`;
|
|
11024
|
+
}
|
|
10271
11025
|
function runRules(rules, stack, ctx) {
|
|
10272
11026
|
const findings = [];
|
|
10273
11027
|
for (const rule of rules) {
|
|
@@ -10293,12 +11047,16 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10293
11047
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
10294
11048
|
type: args.type,
|
|
10295
11049
|
item: args.item,
|
|
10296
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
11050
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
11051
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
10297
11052
|
});
|
|
10298
11053
|
if (!snapshots) return empty;
|
|
10299
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11054
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
10300
11055
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10301
|
-
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)))
|
|
11056
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
|
|
11057
|
+
const path = nameKeyFindingPath(f.path, snapshots.candidate);
|
|
11058
|
+
return path === f.path ? f : { ...f, path };
|
|
11059
|
+
});
|
|
10302
11060
|
return {
|
|
10303
11061
|
errors: added.filter((f) => f.severity === "error"),
|
|
10304
11062
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -10307,6 +11065,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10307
11065
|
}
|
|
10308
11066
|
export {
|
|
10309
11067
|
buildRuntimeWriteSnapshots,
|
|
11068
|
+
narrowObjectsToPackageClosure,
|
|
10310
11069
|
runRuntimeAuthoringRules,
|
|
10311
11070
|
runtimeAuthoringRulesFor,
|
|
10312
11071
|
runtimeGatedTypes,
|