@objectstack/lint 17.0.0-rc.5 → 17.0.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 +8409 -0
- package/dist/index.cjs +2526 -681
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +453 -129
- package/dist/index.d.ts +453 -129
- package/dist/index.js +2492 -661
- package/dist/index.js.map +1 -1
- package/dist/{runtime-Cs64ShwN.d.cts → runtime-H-nDodRy.d.cts} +135 -10
- package/dist/{runtime-Cs64ShwN.d.ts → runtime-H-nDodRy.d.ts} +135 -10
- package/dist/runtime.cjs +2091 -647
- 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 +2050 -589
- package/dist/runtime.js.map +1 -1
- package/package.json +6 -5
package/dist/runtime.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/runtime.ts
|
|
21
21
|
var runtime_exports = {};
|
|
22
22
|
__export(runtime_exports, {
|
|
23
|
+
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
23
24
|
runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
|
|
24
25
|
runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
|
|
25
26
|
runtimeGatedTypes: () => runtimeGatedTypes,
|
|
@@ -41,6 +42,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
41
42
|
function injectedColumnsFor(objectDef) {
|
|
42
43
|
return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
|
|
43
44
|
}
|
|
45
|
+
function unprovisionedInjectedColumnsFor(objectDef) {
|
|
46
|
+
return new Set((0, import_data.unprovisionedInjectedColumns)(objectDef));
|
|
47
|
+
}
|
|
48
|
+
function objectDefsOf(stack) {
|
|
49
|
+
if (!stack || typeof stack !== "object") return [];
|
|
50
|
+
const objects = stack.objects;
|
|
51
|
+
if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
|
|
52
|
+
if (objects && typeof objects === "object") {
|
|
53
|
+
return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
function indexUnprovisionedAnchors(stack) {
|
|
58
|
+
const index = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const obj of objectDefsOf(stack)) {
|
|
60
|
+
const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
|
|
61
|
+
if (!name) continue;
|
|
62
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
63
|
+
if (anchors.size > 0) index.set(name, anchors);
|
|
64
|
+
}
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
67
|
+
function unprovisionedAnchorCause(objectName, field) {
|
|
68
|
+
return `'${field}' is an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column`;
|
|
69
|
+
}
|
|
70
|
+
function unprovisionedAnchorHint(objectName, field) {
|
|
71
|
+
return `If the remote table really carries '${field}', declare it in ${objectName}'s own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`;
|
|
72
|
+
}
|
|
44
73
|
|
|
45
74
|
// src/validate-null-guards.ts
|
|
46
75
|
var import_formula = require("@objectstack/formula");
|
|
@@ -274,6 +303,37 @@ function buildFieldIndex(objects) {
|
|
|
274
303
|
}
|
|
275
304
|
return idx;
|
|
276
305
|
}
|
|
306
|
+
var BOUND_RECORD_ROOTS = ["record", "previous"];
|
|
307
|
+
function isCelNode(v) {
|
|
308
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
309
|
+
}
|
|
310
|
+
function collectBoundRecordReads(source) {
|
|
311
|
+
const out = /* @__PURE__ */ new Map();
|
|
312
|
+
const ast = (0, import_formula2.parseCelToAst)(source);
|
|
313
|
+
if (!ast) return out;
|
|
314
|
+
const pending = [ast];
|
|
315
|
+
while (pending.length > 0) {
|
|
316
|
+
const celNode = pending.pop();
|
|
317
|
+
if (!isCelNode(celNode)) continue;
|
|
318
|
+
if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
|
|
319
|
+
const [celRecv, seg] = celNode.args;
|
|
320
|
+
if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
|
|
321
|
+
if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const celArgs = celNode.args;
|
|
325
|
+
if (isCelNode(celArgs)) pending.push(celArgs);
|
|
326
|
+
else if (Array.isArray(celArgs)) {
|
|
327
|
+
for (const a of celArgs) {
|
|
328
|
+
if (isCelNode(a)) pending.push(a);
|
|
329
|
+
else if (Array.isArray(a)) {
|
|
330
|
+
for (const b of a) if (isCelNode(b)) pending.push(b);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
277
337
|
function buildFieldTypeIndex(objects) {
|
|
278
338
|
const idx = /* @__PURE__ */ new Map();
|
|
279
339
|
for (const obj of objects) {
|
|
@@ -373,6 +433,29 @@ function validateStackExpressions(stack) {
|
|
|
373
433
|
const fieldIndex = buildFieldIndex(objects);
|
|
374
434
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
375
435
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
436
|
+
const unprovisionedIndex = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const obj of objects) {
|
|
438
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
439
|
+
if (!name) continue;
|
|
440
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
441
|
+
if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
|
|
442
|
+
}
|
|
443
|
+
const warnUnprovisionedAnchors = (where, raw, objectName) => {
|
|
444
|
+
if (!objectName) return;
|
|
445
|
+
const anchors = unprovisionedIndex.get(objectName);
|
|
446
|
+
if (!anchors) return;
|
|
447
|
+
const source = celSourceOf(raw);
|
|
448
|
+
if (!source) return;
|
|
449
|
+
for (const [field, operand] of collectBoundRecordReads(source)) {
|
|
450
|
+
if (!anchors.has(field)) continue;
|
|
451
|
+
issues.push({
|
|
452
|
+
where,
|
|
453
|
+
message: `\`${operand}\` reads '${field}', an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column. The predicate can never match a real value \u2014 on SQLite it silently degrades to constant-false (HTTP 200, zero rows, no error). If the remote table really carries this column, declare '${field}' in the object's own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`,
|
|
454
|
+
source,
|
|
455
|
+
severity: "warning"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
};
|
|
376
459
|
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
377
460
|
if (!objectName) return;
|
|
378
461
|
const nullableFields = nullableIndex.get(objectName);
|
|
@@ -399,6 +482,39 @@ function validateStackExpressions(stack) {
|
|
|
399
482
|
);
|
|
400
483
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
401
484
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
485
|
+
warnUnprovisionedAnchors(where, raw, objectName);
|
|
486
|
+
};
|
|
487
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
488
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
489
|
+
const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
|
|
490
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
491
|
+
visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
|
|
492
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
|
|
493
|
+
requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
|
|
494
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
495
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
496
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
497
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
498
|
+
// one, not a fabricated fourth cell (#6716).
|
|
499
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
500
|
+
};
|
|
501
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
502
|
+
const source = celSourceOf(raw);
|
|
503
|
+
if (!source) return;
|
|
504
|
+
const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
|
|
505
|
+
if (!roots.ok) return;
|
|
506
|
+
const kept = import_formula2.SCOPE_ROOTS.filter(
|
|
507
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
508
|
+
);
|
|
509
|
+
if (kept.length === 0) return;
|
|
510
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
511
|
+
const prescription = FIELD_RULE_USER_ROOTS.includes(root) ? `To gate the CHOICES of a select by user, move the predicate to the option's own \`visibleWhen\` (\`options: [{ \u2026, visibleWhen: \u2026 }]\`) \u2014 per-option is the one \`*When\` surface that binds \`current_user\` and its ADR-0068 aliases. To hide the FIELD by role, declare field-level security on a permission set (\`fields: { '<object>.<field>': { readable: false } }\`), which the server enforces. To gate on record state, rewrite the predicate against \`record\`.` : root === "data" ? `\`data\` is the root of a METADATA form (a \`*.form\` module \u2014 the metadata row being edited); this is an OBJECT field, whose runtime form binds the row as \`record\` \u2014 one key name, two form kinds, two roots. Rewrite \`data.<key>\` as \`record.<field>\`.` : `\`${root}\` is declared platform-wide and bound at OTHER evaluation sites (flow, automation, screen and action predicates), never at the field level. Rewrite the predicate against \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item), or move the decision to a surface that binds \`${root}\`.`;
|
|
512
|
+
issues.push({
|
|
513
|
+
where,
|
|
514
|
+
message: `\`${slot}\` reads \`${root}\`, but a field-level conditional rule binds only \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item) \u2014 \`${root}\` is unbound here, so ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}. ` + prescription,
|
|
515
|
+
source,
|
|
516
|
+
severity: "error"
|
|
517
|
+
});
|
|
402
518
|
};
|
|
403
519
|
const checkDeclaredPredicate = (where, raw) => {
|
|
404
520
|
if (raw == null) return;
|
|
@@ -432,7 +548,14 @@ function validateStackExpressions(stack) {
|
|
|
432
548
|
if (retired.length > 0) {
|
|
433
549
|
issues.push({
|
|
434
550
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
435
|
-
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. `) +
|
|
551
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
|
|
552
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
553
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
554
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
555
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
556
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
557
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
558
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
436
559
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
437
560
|
});
|
|
438
561
|
} else if (!fn) {
|
|
@@ -466,13 +589,27 @@ function validateStackExpressions(stack) {
|
|
|
466
589
|
for (const [fname, f] of fieldList) {
|
|
467
590
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
468
591
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
592
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
593
|
+
}
|
|
594
|
+
for (const [oi, opt] of asArray(f.options).entries()) {
|
|
595
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
596
|
+
check(
|
|
597
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
598
|
+
opt.visibleWhen,
|
|
599
|
+
objectName,
|
|
600
|
+
"record"
|
|
601
|
+
);
|
|
469
602
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
603
|
+
for (const [slot, raw, consequence2] of [
|
|
604
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
605
|
+
["requiredWhen", f.requiredWhen, `the requirement would never be enforced \u2014 the predicate faults, the server logs and skips it, and the field stays optional in the database`]
|
|
606
|
+
]) {
|
|
607
|
+
const source = celSourceOf(raw);
|
|
608
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
472
609
|
issues.push({
|
|
473
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
474
|
-
message:
|
|
475
|
-
source
|
|
610
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
611
|
+
message: `\`${slot}\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and ${consequence2}. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
|
|
612
|
+
source,
|
|
476
613
|
severity: "error"
|
|
477
614
|
});
|
|
478
615
|
}
|
|
@@ -492,6 +629,7 @@ function validateStackExpressions(stack) {
|
|
|
492
629
|
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
493
630
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
494
631
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
632
|
+
warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
|
|
495
633
|
}
|
|
496
634
|
}
|
|
497
635
|
}
|
|
@@ -697,6 +835,43 @@ function validateFunctionalCompleteness(stack) {
|
|
|
697
835
|
return out;
|
|
698
836
|
}
|
|
699
837
|
|
|
838
|
+
// src/validate-managed-api-methods.ts
|
|
839
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
840
|
+
var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
|
|
841
|
+
var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
842
|
+
function entriesOf2(v) {
|
|
843
|
+
if (Array.isArray(v)) {
|
|
844
|
+
return v.flatMap(
|
|
845
|
+
(def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (isRec2(v)) {
|
|
849
|
+
return Object.entries(v).flatMap(
|
|
850
|
+
([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
function validateManagedApiMethods(stack) {
|
|
856
|
+
const out = [];
|
|
857
|
+
if (!isRec2(stack)) return out;
|
|
858
|
+
for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
|
|
859
|
+
const conflicts = (0, import_data2.checkManagedApiMethodAffordances)(obj.def);
|
|
860
|
+
if (conflicts.length === 0) continue;
|
|
861
|
+
const verbs = conflicts.map((c) => c.verb).join(", ");
|
|
862
|
+
const flags = [...new Set(conflicts.map((c) => c.needs))];
|
|
863
|
+
out.push({
|
|
864
|
+
severity: "error",
|
|
865
|
+
rule: MANAGED_API_METHOD_UNAFFORDABLE,
|
|
866
|
+
where: `object "${obj.name}"`,
|
|
867
|
+
path: `objects[${oi}].enable.apiMethods`,
|
|
868
|
+
message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + (0, import_data2.describeManagedApiMethodConflicts)(conflicts) + ` The registry STRIPS [${verbs}] at registration, so this declaration and the API you actually get already disagree \u2014 today the only trace is a line in the boot log.`,
|
|
869
|
+
hint: `Either add \`userActions: { ${flags.map((f) => `${f}: true`).join(", ")} }\` to the object \u2014 only if the write is genuinely one a user context may perform, and only once the guard enforcing it exists (ADR-0092 D4: affordance never ships ahead of the guard) \u2014 or remove [${verbs}] from \`enable.apiMethods\`, which is what the runtime does for you today.`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
|
|
700
875
|
// src/validate-view-containers.ts
|
|
701
876
|
var VIEW_CONTAINER_SHAPE = "view-container-shape";
|
|
702
877
|
var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
|
|
@@ -714,10 +889,32 @@ function containerViewCount(rec) {
|
|
|
714
889
|
function validateViewContainers(stack) {
|
|
715
890
|
const out = [];
|
|
716
891
|
if (!stack || typeof stack !== "object") return out;
|
|
892
|
+
const viewItems = stack.viewItems;
|
|
893
|
+
if (viewItems != null && asEntries(viewItems).length > 0) {
|
|
894
|
+
out.push({
|
|
895
|
+
severity: "error",
|
|
896
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
897
|
+
where: "viewItems",
|
|
898
|
+
path: "viewItems",
|
|
899
|
+
message: "`viewItems` is the machine-assembled channel for non-container view artifacts in runtime-assembled manifests (package export, environment artifacts) \u2014 it is not an authoring surface.",
|
|
900
|
+
hint: "Author views as defineView containers in `views:`; author a standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`), not in stack source."
|
|
901
|
+
});
|
|
902
|
+
}
|
|
717
903
|
for (const { key, value } of asEntries(stack.views)) {
|
|
718
904
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
719
905
|
const rec = value;
|
|
720
|
-
if (rec.viewKind != null)
|
|
906
|
+
if (rec.viewKind != null) {
|
|
907
|
+
const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
908
|
+
out.push({
|
|
909
|
+
severity: "error",
|
|
910
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
911
|
+
where: `views${key}${label3}`,
|
|
912
|
+
path: `views${key}`,
|
|
913
|
+
message: "A ViewItem record is not a view container: the stack `views:` collection carries containers only \u2014 `viewKind` belongs to a single VIEW, not to the container. The registration loop refuses this entry (#5320).",
|
|
914
|
+
hint: "Wrap it in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... } }) \u2014 or author the standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`). Machine-assembled manifests carry it under `viewItems:`."
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
721
918
|
if (containerViewCount(rec) > 0) continue;
|
|
722
919
|
const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
723
920
|
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
@@ -735,7 +932,7 @@ function validateViewContainers(stack) {
|
|
|
735
932
|
}
|
|
736
933
|
|
|
737
934
|
// src/validate-widget-bindings.ts
|
|
738
|
-
var
|
|
935
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
739
936
|
var import_ui = require("@objectstack/spec/ui");
|
|
740
937
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
741
938
|
var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
|
|
@@ -747,6 +944,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
|
747
944
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
748
945
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
749
946
|
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
947
|
+
var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
|
|
750
948
|
var LEGACY_ANALYTICS_KEYS = [
|
|
751
949
|
"categoryField",
|
|
752
950
|
"valueField",
|
|
@@ -866,6 +1064,7 @@ function validateWidgetBindings(stack) {
|
|
|
866
1064
|
}
|
|
867
1065
|
objectFieldTypes.set(o.name, fm);
|
|
868
1066
|
}
|
|
1067
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
869
1068
|
const datasetList = asArray3(stack.datasets);
|
|
870
1069
|
for (let i = 0; i < datasetList.length; i++) {
|
|
871
1070
|
const ds = datasetList[i];
|
|
@@ -878,7 +1077,7 @@ function validateWidgetBindings(stack) {
|
|
|
878
1077
|
const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
|
|
879
1078
|
if (!field || !aggregate) continue;
|
|
880
1079
|
const ftype = fieldTypes.get(field);
|
|
881
|
-
if (ftype && (0,
|
|
1080
|
+
if (ftype && (0, import_data3.isIncoherentAggregate)(aggregate, ftype)) {
|
|
882
1081
|
findings.push({
|
|
883
1082
|
severity: "warning",
|
|
884
1083
|
rule: MEASURE_AGGREGATE_INCOHERENT,
|
|
@@ -952,13 +1151,24 @@ function validateWidgetBindings(stack) {
|
|
|
952
1151
|
if (dashFilterDefs.length > 0) {
|
|
953
1152
|
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
954
1153
|
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
955
|
-
|
|
1154
|
+
const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
|
|
1155
|
+
if (objectFields && datasetObject) {
|
|
956
1156
|
for (const def of dashFilterDefs) {
|
|
957
1157
|
const eff = effectiveFilterField(w, def);
|
|
958
1158
|
if (!eff) continue;
|
|
959
1159
|
const field = eff.field;
|
|
960
1160
|
if (field.includes(".")) continue;
|
|
961
|
-
if (objectFields.has(field) || SYSTEM_FIELDS.has(field))
|
|
1161
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
|
|
1162
|
+
if (anchors?.has(field)) {
|
|
1163
|
+
push2({
|
|
1164
|
+
severity: "warning",
|
|
1165
|
+
rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
1166
|
+
message: (eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but ` : `inherits dashboard filter \`${def.name}(${field})\`, but `) + `${unprovisionedAnchorCause(datasetObject, field)}. The filter is ANDed into this widget's analytics query (#2501), so it can never match a real value \u2014 on SQLite it silently degrades to constant-false and the widget renders empty (HTTP 200, zero rows, no error).`,
|
|
1167
|
+
hint: `${unprovisionedAnchorHint(datasetObject, field)} A widget can also opt out with filterBindings: { ${def.name}: false }. Suppress with suppressWarnings: ['${DASHBOARD_FILTER_FIELD_UNPROVISIONED}'] if the remote schema resolves it some other way.`
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
962
1172
|
push2({
|
|
963
1173
|
severity: "error",
|
|
964
1174
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
@@ -1195,8 +1405,9 @@ function validateDashboardActionRefs(stack) {
|
|
|
1195
1405
|
}
|
|
1196
1406
|
|
|
1197
1407
|
// src/validate-filter-tokens.ts
|
|
1198
|
-
var
|
|
1199
|
-
|
|
1408
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
1409
|
+
|
|
1410
|
+
// src/filter-walk.ts
|
|
1200
1411
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1201
1412
|
function asArray5(v) {
|
|
1202
1413
|
if (Array.isArray(v)) return v;
|
|
@@ -1208,11 +1419,66 @@ function asArray5(v) {
|
|
|
1208
1419
|
function label(v, fallback) {
|
|
1209
1420
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
1210
1421
|
}
|
|
1211
|
-
|
|
1422
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
1423
|
+
if (!node || typeof node !== "object") return;
|
|
1424
|
+
if (seen.has(node)) return;
|
|
1425
|
+
seen.add(node);
|
|
1426
|
+
if (Array.isArray(node)) {
|
|
1427
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1431
|
+
const childPath = `${path}.${k}`;
|
|
1432
|
+
if (FILTER_KEYS.has(k)) {
|
|
1433
|
+
visit({ value: v, path: childPath, where });
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1436
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
1440
|
+
if (!stack || typeof stack !== "object") return;
|
|
1441
|
+
for (const { key, kind } of surfaces) {
|
|
1442
|
+
const items = asArray5(stack[key]);
|
|
1443
|
+
items.forEach((item, i) => {
|
|
1444
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
1445
|
+
if (kind === "dashboard") {
|
|
1446
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1447
|
+
widgets.forEach((w, wi) => {
|
|
1448
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1449
|
+
scanForFilters(
|
|
1450
|
+
w,
|
|
1451
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
1452
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1453
|
+
visit,
|
|
1454
|
+
/* @__PURE__ */ new Set()
|
|
1455
|
+
);
|
|
1456
|
+
});
|
|
1457
|
+
const { widgets: _skip, ...rest } = item;
|
|
1458
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/validate-filter-tokens.ts
|
|
1467
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
1468
|
+
var KNOWN_LIST = import_data4.CONTEXT_TOKENS.join("}, {");
|
|
1469
|
+
var TOKEN_FILTER_SURFACES = [
|
|
1470
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1471
|
+
{ key: "objects", kind: "object" },
|
|
1472
|
+
{ key: "views", kind: "view" },
|
|
1473
|
+
{ key: "reports", kind: "report" },
|
|
1474
|
+
{ key: "datasets", kind: "dataset" },
|
|
1475
|
+
{ key: "pages", kind: "page" },
|
|
1476
|
+
{ key: "apps", kind: "app" }
|
|
1477
|
+
];
|
|
1212
1478
|
function walkFilterValues(node, path, where, out, seen) {
|
|
1213
1479
|
if (node === null || node === void 0) return;
|
|
1214
1480
|
if (typeof node === "string") {
|
|
1215
|
-
const cls = (0,
|
|
1481
|
+
const cls = (0, import_data4.classifyFilterToken)(node);
|
|
1216
1482
|
if (cls?.kind === "unknown") {
|
|
1217
1483
|
const suggestion = cls.suggestion;
|
|
1218
1484
|
out.push({
|
|
@@ -1237,58 +1503,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
1237
1503
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
1238
1504
|
}
|
|
1239
1505
|
}
|
|
1240
|
-
function
|
|
1241
|
-
if (!
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1506
|
+
function validateFilterTokens(stack) {
|
|
1507
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1508
|
+
const out = [];
|
|
1509
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
1510
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
1511
|
+
});
|
|
1512
|
+
return out;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
// src/validate-empty-combinators.ts
|
|
1516
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
1517
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1518
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1519
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
1520
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1521
|
+
{ key: "objects", kind: "object" },
|
|
1522
|
+
{ key: "views", kind: "view" },
|
|
1523
|
+
{ key: "reports", kind: "report" },
|
|
1524
|
+
{ key: "datasets", kind: "dataset" },
|
|
1525
|
+
{ key: "pages", kind: "page" },
|
|
1526
|
+
{ key: "apps", kind: "app" },
|
|
1527
|
+
{ key: "flows", kind: "flow" }
|
|
1528
|
+
];
|
|
1529
|
+
function isFilterNode(value) {
|
|
1530
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1531
|
+
const proto = Object.getPrototypeOf(value);
|
|
1532
|
+
return proto === Object.prototype || proto === null;
|
|
1533
|
+
}
|
|
1534
|
+
var VERDICT_OF = {
|
|
1535
|
+
$and: (0, import_data5.reduceFilterVerdict)({ $and: [] }),
|
|
1536
|
+
$or: (0, import_data5.reduceFilterVerdict)({ $or: [] }),
|
|
1537
|
+
$not: (0, import_data5.reduceFilterVerdict)({ $not: {} }),
|
|
1538
|
+
node: (0, import_data5.reduceFilterVerdict)({}),
|
|
1539
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1540
|
+
orWithEmptyBranch: (0, import_data5.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
1541
|
+
};
|
|
1542
|
+
function rows(verdict) {
|
|
1543
|
+
if (verdict === "true") return "matches EVERY row";
|
|
1544
|
+
if (verdict === "false") return "matches NO row";
|
|
1545
|
+
return "carries a real predicate";
|
|
1546
|
+
}
|
|
1547
|
+
var MATCH_NONE_SPELLING = "If you really do want a predicate that selects nothing, `{ <field>: { $in: [] } }` is the declared spelling for it (an empty `$in` list matches nothing, on every backend) \u2014 it says so where an empty combinator only implies it.";
|
|
1548
|
+
var OMIT_THE_KEY = 'To express "no filter", DELETE the key \u2014 an absent `filter` and a filter that reduces to TRUE run identically, and only the absent key says so to the next reader (and to the next AI author that copies this metadata).';
|
|
1549
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
1550
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
1551
|
+
const message = key === "$and" ? `\`$and: []\` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ${rows(VERDICT_OF.$and)} \u2014 the key is authored, and it constrains nothing, so this surface reads as filtered and is not.` : key === "$or" ? `\`$or: []\` is a disjunction of ZERO branches. Under the #5322 identity ruling it ${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides every row (fail-closed by design \u2014 #5134).` : `\`$not: {}\` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ${rows(VERDICT_OF.$not)} \u2014 the opposite of the "no filter" an empty operand looks like.`;
|
|
1552
|
+
const hint = key === "$and" ? `${OMIT_THE_KEY} To express a constraint, put the conditions in the array. ${MATCH_NONE_SPELLING}` : key === "$or" ? `If you meant "no filter", this is its OPPOSITE: emptying the array does not relax the filter, it closes it. ${OMIT_THE_KEY} If you meant to offer alternatives, put the branches in the array. ${MATCH_NONE_SPELLING}` : `Put the condition you are negating inside \`$not\` (\`{ $not: { status: 'closed' } }\`). ${OMIT_THE_KEY} ${MATCH_NONE_SPELLING}`;
|
|
1553
|
+
ctx.out.push({
|
|
1554
|
+
severity: "error",
|
|
1555
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
1556
|
+
where: ctx.where,
|
|
1557
|
+
path,
|
|
1558
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
1559
|
+
hint: `${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity unchanged \u2014 this rule judges only what is written in the metadata.`
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
function emitEmptyNode(position, path, ctx) {
|
|
1563
|
+
if (position === "root") {
|
|
1564
|
+
ctx.out.push({
|
|
1565
|
+
severity: "error",
|
|
1566
|
+
rule: FILTER_EMPTY_NODE,
|
|
1567
|
+
where: ctx.where,
|
|
1568
|
+
path,
|
|
1569
|
+
message: `An EMPTY filter node (\`{}\`) is authored here. Under the #5322 identity ruling an empty node is TRUE \u2014 it ${rows(VERDICT_OF.node)}, exactly as if the key were absent \u2014 so a filter is declared and enforces nothing.`,
|
|
1570
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
1571
|
+
});
|
|
1246
1572
|
return;
|
|
1247
1573
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1574
|
+
if (position === "or-branch") {
|
|
1575
|
+
ctx.out.push({
|
|
1576
|
+
severity: "error",
|
|
1577
|
+
rule: FILTER_EMPTY_NODE,
|
|
1578
|
+
where: ctx.where,
|
|
1579
|
+
path,
|
|
1580
|
+
message: `An EMPTY branch (\`{}\`) of a \`$or\`. An empty node is TRUE, and one TRUE disjunct ABSORBS the whole disjunction (\`{ $or: [{ status: 'open' }, {}] }\` ${rows(VERDICT_OF.orWithEmptyBranch)}), so every branch you wrote beside it is dead.`,
|
|
1581
|
+
hint: "Delete the empty branch \u2014 the `$or` then means what it looks like. If it was meant to carry a condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW the scope to the surviving branches, which is why the runtime absorbs rather than filters \u2014 #5297.)"
|
|
1582
|
+
});
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
ctx.out.push({
|
|
1586
|
+
severity: "error",
|
|
1587
|
+
rule: FILTER_EMPTY_NODE,
|
|
1588
|
+
where: ctx.where,
|
|
1589
|
+
path,
|
|
1590
|
+
message: "An EMPTY branch (`{}`) of a `$and`. An empty node is TRUE \u2014 the AND identity \u2014 so the branch contributes no condition and the conjunction means whatever its other branches mean.",
|
|
1591
|
+
hint: "Delete the empty branch, or write the condition it was meant to carry. A branch that constrains nothing is indistinguishable from one whose condition was lost in an edit."
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
function scanNodeKeys(node, path, ctx) {
|
|
1595
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1596
|
+
if (key === "$and" || key === "$or") {
|
|
1597
|
+
if (!Array.isArray(value)) continue;
|
|
1598
|
+
if (value.length === 0) {
|
|
1599
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
value.forEach((element, index) => {
|
|
1603
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
1604
|
+
});
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
if (key === "$not") {
|
|
1608
|
+
if (!isFilterNode(value)) continue;
|
|
1609
|
+
if (Object.keys(value).length === 0) {
|
|
1610
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
1252
1614
|
continue;
|
|
1253
1615
|
}
|
|
1254
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
1255
1616
|
}
|
|
1256
1617
|
}
|
|
1257
|
-
function
|
|
1618
|
+
function scanBranch(value, path, position, ctx) {
|
|
1619
|
+
if (!isFilterNode(value)) return;
|
|
1620
|
+
if (Object.keys(value).length === 0) {
|
|
1621
|
+
emitEmptyNode(position, path, ctx);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
scanNodeKeys(value, path, ctx);
|
|
1625
|
+
}
|
|
1626
|
+
function validateEmptyCombinators(stack) {
|
|
1258
1627
|
if (!stack || typeof stack !== "object") return [];
|
|
1259
1628
|
const out = [];
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
["views", "view"],
|
|
1264
|
-
["reports", "report"],
|
|
1265
|
-
["datasets", "dataset"],
|
|
1266
|
-
["pages", "page"],
|
|
1267
|
-
["apps", "app"]
|
|
1268
|
-
];
|
|
1269
|
-
for (const [key, kind] of surfaces) {
|
|
1270
|
-
const items = asArray5(stack[key]);
|
|
1271
|
-
items.forEach((item, i) => {
|
|
1272
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
1273
|
-
if (kind === "dashboard") {
|
|
1274
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1275
|
-
widgets.forEach((w, wi) => {
|
|
1276
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1277
|
-
scanForFilters(
|
|
1278
|
-
w,
|
|
1279
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
1280
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1281
|
-
out,
|
|
1282
|
-
/* @__PURE__ */ new Set()
|
|
1283
|
-
);
|
|
1284
|
-
});
|
|
1285
|
-
const { widgets: _skip, ...rest } = item;
|
|
1286
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1629
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
1630
|
+
scanBranch(value, path, "root", { where, out });
|
|
1631
|
+
});
|
|
1292
1632
|
return out;
|
|
1293
1633
|
}
|
|
1294
1634
|
|
|
@@ -1482,7 +1822,7 @@ function validateObjectReferences(stack) {
|
|
|
1482
1822
|
}
|
|
1483
1823
|
|
|
1484
1824
|
// src/validate-searchable-fields.ts
|
|
1485
|
-
var
|
|
1825
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
1486
1826
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1487
1827
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1488
1828
|
function asArray7(v) {
|
|
@@ -1492,7 +1832,7 @@ function asArray7(v) {
|
|
|
1492
1832
|
}
|
|
1493
1833
|
return [];
|
|
1494
1834
|
}
|
|
1495
|
-
function
|
|
1835
|
+
function isRec3(v) {
|
|
1496
1836
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1497
1837
|
}
|
|
1498
1838
|
function strName3(v) {
|
|
@@ -1530,7 +1870,7 @@ function resolveAllowedSet(target) {
|
|
|
1530
1870
|
fields = { ...fields };
|
|
1531
1871
|
for (const f of systemDeclared) fields[f] = {};
|
|
1532
1872
|
}
|
|
1533
|
-
const { allowed, source } = (0,
|
|
1873
|
+
const { allowed, source } = (0, import_data6.resolveSearchFieldResolution)({
|
|
1534
1874
|
fields,
|
|
1535
1875
|
searchableFields: target.searchableFields,
|
|
1536
1876
|
displayField: target.displayField
|
|
@@ -1568,7 +1908,7 @@ function distance2(a, b) {
|
|
|
1568
1908
|
}
|
|
1569
1909
|
function indexObjectSearchTargets(stack) {
|
|
1570
1910
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1571
|
-
if (!
|
|
1911
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1572
1912
|
for (const obj of asArray7(stack.objects)) {
|
|
1573
1913
|
const name = strName3(obj.name);
|
|
1574
1914
|
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
@@ -1596,7 +1936,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1596
1936
|
where,
|
|
1597
1937
|
path: `${path}[${i}]`,
|
|
1598
1938
|
message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest3(name, known)),
|
|
1599
|
-
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a
|
|
1939
|
+
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a stored text field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
1940
|
+
});
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
if ((0, import_data6.isVirtualSearchField)(target.fields[name])) {
|
|
1944
|
+
const vtype = target.fields[name]?.type;
|
|
1945
|
+
findings.push({
|
|
1946
|
+
severity: "error",
|
|
1947
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1948
|
+
where,
|
|
1949
|
+
path: `${path}[${i}]`,
|
|
1950
|
+
message: `${subject} entry "${name}" on object "${objectName}" is a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materializes a column for 'search' to scan and the entry can never match. It reads as search coverage and delivers none \u2014 the runtime used to admit it verbatim because the declaration named it (#6674).`,
|
|
1951
|
+
hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
|
|
1600
1952
|
});
|
|
1601
1953
|
continue;
|
|
1602
1954
|
}
|
|
@@ -1616,7 +1968,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1616
1968
|
}
|
|
1617
1969
|
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
1618
1970
|
let why;
|
|
1619
|
-
if (
|
|
1971
|
+
if (import_data6.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
1620
1972
|
why = "a system/audit column, which the auto-default set never includes";
|
|
1621
1973
|
} else if (meta?.hidden) {
|
|
1622
1974
|
why = "hidden";
|
|
@@ -1630,15 +1982,15 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1630
1982
|
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1631
1983
|
where,
|
|
1632
1984
|
path: `${path}[${i}]`,
|
|
1633
|
-
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...
|
|
1634
|
-
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text
|
|
1985
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data6.SEARCHABLE_TEXTUAL_TYPES, ...import_data6.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
1986
|
+
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
|
|
1635
1987
|
});
|
|
1636
1988
|
}
|
|
1637
1989
|
return findings;
|
|
1638
1990
|
}
|
|
1639
1991
|
function validateSearchableFields(stack) {
|
|
1640
1992
|
const findings = [];
|
|
1641
|
-
if (!
|
|
1993
|
+
if (!isRec3(stack)) return findings;
|
|
1642
1994
|
const objects = asArray7(stack.objects);
|
|
1643
1995
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1644
1996
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
@@ -1648,7 +2000,7 @@ function validateSearchableFields(stack) {
|
|
|
1648
2000
|
};
|
|
1649
2001
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1650
2002
|
const obj = objects[oi];
|
|
1651
|
-
if (!
|
|
2003
|
+
if (!isRec3(obj)) continue;
|
|
1652
2004
|
const objName = strName3(obj.name);
|
|
1653
2005
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1654
2006
|
check(
|
|
@@ -1659,9 +2011,9 @@ function validateSearchableFields(stack) {
|
|
|
1659
2011
|
"searchableFields",
|
|
1660
2012
|
"canonical"
|
|
1661
2013
|
);
|
|
1662
|
-
if (
|
|
2014
|
+
if (isRec3(obj.listViews)) {
|
|
1663
2015
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1664
|
-
if (!
|
|
2016
|
+
if (!isRec3(lv)) continue;
|
|
1665
2017
|
check(
|
|
1666
2018
|
lv.searchableFields,
|
|
1667
2019
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1678,10 +2030,10 @@ function validateSearchableFields(stack) {
|
|
|
1678
2030
|
const views = asArray7(stack.views);
|
|
1679
2031
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1680
2032
|
const view = views[vi];
|
|
1681
|
-
if (!
|
|
2033
|
+
if (!isRec3(view)) continue;
|
|
1682
2034
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
1683
2035
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
1684
|
-
if (
|
|
2036
|
+
if (isRec3(view.list)) {
|
|
1685
2037
|
check(
|
|
1686
2038
|
view.list.searchableFields,
|
|
1687
2039
|
listViewObject(view.list) ?? viewObject,
|
|
@@ -1691,9 +2043,9 @@ function validateSearchableFields(stack) {
|
|
|
1691
2043
|
"narrowing"
|
|
1692
2044
|
);
|
|
1693
2045
|
}
|
|
1694
|
-
if (
|
|
2046
|
+
if (isRec3(view.listViews)) {
|
|
1695
2047
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1696
|
-
if (!
|
|
2048
|
+
if (!isRec3(lv)) continue;
|
|
1697
2049
|
check(
|
|
1698
2050
|
lv.searchableFields,
|
|
1699
2051
|
listViewObject(lv) ?? viewObject,
|
|
@@ -1709,11 +2061,11 @@ function validateSearchableFields(stack) {
|
|
|
1709
2061
|
}
|
|
1710
2062
|
function listViewObject(listView) {
|
|
1711
2063
|
const data = listView.data;
|
|
1712
|
-
return
|
|
2064
|
+
return isRec3(data) ? strName3(data.object) : void 0;
|
|
1713
2065
|
}
|
|
1714
2066
|
|
|
1715
2067
|
// src/page-walk.ts
|
|
1716
|
-
function
|
|
2068
|
+
function isRec4(v) {
|
|
1717
2069
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1718
2070
|
}
|
|
1719
2071
|
function strName4(v) {
|
|
@@ -1726,19 +2078,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1726
2078
|
}
|
|
1727
2079
|
function walkPageComponents(page, pagePath) {
|
|
1728
2080
|
const out = [];
|
|
1729
|
-
if (!
|
|
2081
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1730
2082
|
const pageObject = strName4(page.object);
|
|
1731
2083
|
const visit = (node, path, inheritedObject) => {
|
|
1732
|
-
if (!
|
|
1733
|
-
const props =
|
|
1734
|
-
const dataSource =
|
|
2084
|
+
if (!isRec4(node)) return;
|
|
2085
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2086
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1735
2087
|
const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
|
|
1736
2088
|
out.push({ component: node, path, objectName });
|
|
1737
2089
|
if (!props) return;
|
|
1738
2090
|
if (Array.isArray(props.items)) {
|
|
1739
2091
|
for (let i = 0; i < props.items.length; i++) {
|
|
1740
2092
|
const item = props.items[i];
|
|
1741
|
-
if (!
|
|
2093
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1742
2094
|
for (let c = 0; c < item.children.length; c++) {
|
|
1743
2095
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1744
2096
|
}
|
|
@@ -1760,12 +2112,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1760
2112
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1761
2113
|
for (let r = 0; r < regions.length; r++) {
|
|
1762
2114
|
const region = regions[r];
|
|
1763
|
-
if (!
|
|
2115
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1764
2116
|
for (let c = 0; c < region.components.length; c++) {
|
|
1765
2117
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1766
2118
|
}
|
|
1767
2119
|
}
|
|
1768
|
-
const slots =
|
|
2120
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1769
2121
|
if (slots) {
|
|
1770
2122
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1771
2123
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -1950,6 +2302,15 @@ function validateActionNameRefs(stack) {
|
|
|
1950
2302
|
"Navigation action item"
|
|
1951
2303
|
);
|
|
1952
2304
|
}
|
|
2305
|
+
const runAction = strName5(nav.runAction);
|
|
2306
|
+
if (nav.type === "object" && runAction) {
|
|
2307
|
+
check(
|
|
2308
|
+
runAction,
|
|
2309
|
+
`app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
|
|
2310
|
+
`${navPath}.runAction`,
|
|
2311
|
+
"Navigation deep-link auto-run"
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
1953
2314
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
1954
2315
|
}
|
|
1955
2316
|
};
|
|
@@ -1964,6 +2325,7 @@ function validateActionNameRefs(stack) {
|
|
|
1964
2325
|
|
|
1965
2326
|
// src/validate-page-field-bindings.ts
|
|
1966
2327
|
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
2328
|
+
var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
|
|
1967
2329
|
function asArray9(v) {
|
|
1968
2330
|
if (Array.isArray(v)) return v;
|
|
1969
2331
|
if (v && typeof v === "object") {
|
|
@@ -1974,7 +2336,7 @@ function asArray9(v) {
|
|
|
1974
2336
|
function strName6(v) {
|
|
1975
2337
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1976
2338
|
}
|
|
1977
|
-
function
|
|
2339
|
+
function isRec5(v) {
|
|
1978
2340
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1979
2341
|
}
|
|
1980
2342
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -1985,7 +2347,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
1985
2347
|
out.push({ name: bare, path });
|
|
1986
2348
|
return;
|
|
1987
2349
|
}
|
|
1988
|
-
if (!
|
|
2350
|
+
if (!isRec5(v)) return;
|
|
1989
2351
|
const named = strName6(v.field) ?? strName6(v.name);
|
|
1990
2352
|
if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
|
|
1991
2353
|
};
|
|
@@ -2015,8 +2377,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2015
2377
|
"element:number": { props: ["field"] },
|
|
2016
2378
|
"element:filter": { props: ["fields"] },
|
|
2017
2379
|
"element:form": { props: ["fields"] },
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2380
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2381
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
2382
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
2383
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
2384
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
2385
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
2386
|
+
//
|
|
2387
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
2388
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
2389
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
2390
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
2391
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
2392
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
2393
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
2394
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2020
2395
|
};
|
|
2021
2396
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2022
2397
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2030,15 +2405,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2030
2405
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2031
2406
|
for (let si = 0; si < sections.length; si++) {
|
|
2032
2407
|
const section = sections[si];
|
|
2033
|
-
if (!
|
|
2408
|
+
if (!isRec5(section)) continue;
|
|
2034
2409
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2035
2410
|
}
|
|
2036
2411
|
}
|
|
2037
2412
|
return refs;
|
|
2038
2413
|
}
|
|
2039
2414
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2040
|
-
const add =
|
|
2041
|
-
const picker = add &&
|
|
2415
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2416
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
2042
2417
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2043
2418
|
return {
|
|
2044
2419
|
relatedObject: strName6(props.objectName),
|
|
@@ -2059,7 +2434,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2059
2434
|
}
|
|
2060
2435
|
function indexObjectFields(stack) {
|
|
2061
2436
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2062
|
-
if (!
|
|
2437
|
+
if (!isRec5(stack)) return objectFields;
|
|
2063
2438
|
for (const obj of asArray9(stack.objects)) {
|
|
2064
2439
|
const name = strName6(obj.name);
|
|
2065
2440
|
if (!name) continue;
|
|
@@ -2072,14 +2447,27 @@ function indexObjectFields(stack) {
|
|
|
2072
2447
|
}
|
|
2073
2448
|
return objectFields;
|
|
2074
2449
|
}
|
|
2075
|
-
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2450
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
|
|
2076
2451
|
const findings = [];
|
|
2077
2452
|
if (!objectName) return findings;
|
|
2078
2453
|
const known = objectFields.get(objectName);
|
|
2079
2454
|
if (!known) return findings;
|
|
2455
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2080
2456
|
for (const ref of refs) {
|
|
2081
2457
|
if (ref.name.includes(".")) continue;
|
|
2082
|
-
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name))
|
|
2458
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
|
|
2459
|
+
if (anchors?.has(ref.name)) {
|
|
2460
|
+
findings.push({
|
|
2461
|
+
severity: "warning",
|
|
2462
|
+
rule: PAGE_FIELD_UNPROVISIONED,
|
|
2463
|
+
where,
|
|
2464
|
+
path: ref.path,
|
|
2465
|
+
message: `field "${ref.name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, ref.name)}` + (consequence2 === "queried" ? ' \u2014 it is used in a QUERY, so the predicate can never match a real value: on SQLite it silently degrades to constant-false and the surface renders an empty result that looks exactly like "there is no data".' : " \u2014 the component renders it, blank, on every record."),
|
|
2466
|
+
hint: unprovisionedAnchorHint(objectName, ref.name)
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2083
2471
|
findings.push({
|
|
2084
2472
|
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2085
2473
|
rule: PAGE_FIELD_UNKNOWN,
|
|
@@ -2095,17 +2483,20 @@ function validatePageFieldBindings(stack) {
|
|
|
2095
2483
|
const findings = [];
|
|
2096
2484
|
if (!stack || typeof stack !== "object") return findings;
|
|
2097
2485
|
const objectFields = indexObjectFields(stack);
|
|
2486
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2098
2487
|
const pages = asArray9(stack.pages);
|
|
2099
2488
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2100
2489
|
const page = pages[pi];
|
|
2101
2490
|
if (!page || typeof page !== "object") continue;
|
|
2102
2491
|
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2103
2492
|
const checkRefs = (refs, objectName, where) => {
|
|
2104
|
-
findings.push(
|
|
2493
|
+
findings.push(
|
|
2494
|
+
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2495
|
+
);
|
|
2105
2496
|
};
|
|
2106
2497
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2107
2498
|
const type = strName6(component.type);
|
|
2108
|
-
const props =
|
|
2499
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
2109
2500
|
if (!type || !props) continue;
|
|
2110
2501
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2111
2502
|
const base = `${path}.properties`;
|
|
@@ -2120,7 +2511,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2120
2511
|
if (!refs) continue;
|
|
2121
2512
|
checkRefs(refs, objectName, where);
|
|
2122
2513
|
}
|
|
2123
|
-
const cfg =
|
|
2514
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2124
2515
|
if (cfg) {
|
|
2125
2516
|
const cfgObject = strName6(cfg.source) ?? strName6(page.object);
|
|
2126
2517
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -2129,7 +2520,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2129
2520
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2130
2521
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2131
2522
|
];
|
|
2132
|
-
const userFilters =
|
|
2523
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2133
2524
|
if (userFilters) {
|
|
2134
2525
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2135
2526
|
}
|
|
@@ -2157,7 +2548,7 @@ function strName7(v) {
|
|
|
2157
2548
|
function strList2(v) {
|
|
2158
2549
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2159
2550
|
}
|
|
2160
|
-
function
|
|
2551
|
+
function isRec6(v) {
|
|
2161
2552
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2162
2553
|
}
|
|
2163
2554
|
function distance4(a, b) {
|
|
@@ -2286,10 +2677,10 @@ function validateChartBindings(stack) {
|
|
|
2286
2677
|
const reports = asArray10(stack.reports);
|
|
2287
2678
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2288
2679
|
const report = reports[ri];
|
|
2289
|
-
if (!
|
|
2680
|
+
if (!isRec6(report)) continue;
|
|
2290
2681
|
const reportName = strName7(report.name) ?? `#${ri}`;
|
|
2291
2682
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2292
|
-
if (!
|
|
2683
|
+
if (!isRec6(chart)) return;
|
|
2293
2684
|
check({
|
|
2294
2685
|
dataset,
|
|
2295
2686
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
@@ -2313,7 +2704,7 @@ function validateChartBindings(stack) {
|
|
|
2313
2704
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2314
2705
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2315
2706
|
const block = blocks[bi];
|
|
2316
|
-
if (!
|
|
2707
|
+
if (!isRec6(block)) continue;
|
|
2317
2708
|
checkReportChart(
|
|
2318
2709
|
block.chart,
|
|
2319
2710
|
strName7(block.dataset),
|
|
@@ -2324,9 +2715,9 @@ function validateChartBindings(stack) {
|
|
|
2324
2715
|
}
|
|
2325
2716
|
}
|
|
2326
2717
|
const checkListChart = (container, where, path) => {
|
|
2327
|
-
if (!
|
|
2718
|
+
if (!isRec6(container)) return;
|
|
2328
2719
|
const chart = container.chart;
|
|
2329
|
-
if (!
|
|
2720
|
+
if (!isRec6(chart)) return;
|
|
2330
2721
|
check({
|
|
2331
2722
|
dataset: strName7(chart.dataset),
|
|
2332
2723
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
@@ -2338,10 +2729,10 @@ function validateChartBindings(stack) {
|
|
|
2338
2729
|
const views = asArray10(stack.views);
|
|
2339
2730
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2340
2731
|
const view = views[vi];
|
|
2341
|
-
if (!
|
|
2732
|
+
if (!isRec6(view)) continue;
|
|
2342
2733
|
const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
|
|
2343
2734
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2344
|
-
if (
|
|
2735
|
+
if (isRec6(view.listViews)) {
|
|
2345
2736
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2346
2737
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2347
2738
|
}
|
|
@@ -2350,7 +2741,7 @@ function validateChartBindings(stack) {
|
|
|
2350
2741
|
const objects = asArray10(stack.objects);
|
|
2351
2742
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2352
2743
|
const obj = objects[oi];
|
|
2353
|
-
if (!
|
|
2744
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
2354
2745
|
const objName = strName7(obj.name) ?? `#${oi}`;
|
|
2355
2746
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2356
2747
|
checkListChart(
|
|
@@ -2363,10 +2754,10 @@ function validateChartBindings(stack) {
|
|
|
2363
2754
|
const pages = asArray10(stack.pages);
|
|
2364
2755
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2365
2756
|
const page = pages[pi];
|
|
2366
|
-
if (!
|
|
2757
|
+
if (!isRec6(page)) continue;
|
|
2367
2758
|
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2368
2759
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2369
|
-
const props =
|
|
2760
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2370
2761
|
if (!props || !strName7(props.dataset)) continue;
|
|
2371
2762
|
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
2372
2763
|
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
@@ -2516,10 +2907,10 @@ function validateNavAccess(stack) {
|
|
|
2516
2907
|
|
|
2517
2908
|
// src/validate-nav-target-refs.ts
|
|
2518
2909
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2519
|
-
var
|
|
2910
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2520
2911
|
function asArray13(v) {
|
|
2521
|
-
if (Array.isArray(v)) return v.filter(
|
|
2522
|
-
if (
|
|
2912
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
2913
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
2523
2914
|
return [];
|
|
2524
2915
|
}
|
|
2525
2916
|
function strName9(v) {
|
|
@@ -2541,7 +2932,7 @@ function namesOf(collection) {
|
|
|
2541
2932
|
}
|
|
2542
2933
|
function validateNavTargetRefs(stack) {
|
|
2543
2934
|
const findings = [];
|
|
2544
|
-
if (!
|
|
2935
|
+
if (!isRec7(stack)) return findings;
|
|
2545
2936
|
const apps = asArray13(stack.apps);
|
|
2546
2937
|
if (apps.length === 0) return findings;
|
|
2547
2938
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2553,7 +2944,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2553
2944
|
const walk = (items, basePath) => {
|
|
2554
2945
|
if (!Array.isArray(items)) return;
|
|
2555
2946
|
for (const [ni, raw] of items.entries()) {
|
|
2556
|
-
if (!
|
|
2947
|
+
if (!isRec7(raw)) continue;
|
|
2557
2948
|
const nav = raw;
|
|
2558
2949
|
const navPath = `${basePath}[${ni}]`;
|
|
2559
2950
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -2584,47 +2975,154 @@ function validateNavTargetRefs(stack) {
|
|
|
2584
2975
|
return findings;
|
|
2585
2976
|
}
|
|
2586
2977
|
|
|
2587
|
-
// src/validate-
|
|
2588
|
-
var
|
|
2589
|
-
var
|
|
2590
|
-
var
|
|
2591
|
-
function isRec7(v) {
|
|
2592
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2593
|
-
}
|
|
2978
|
+
// src/validate-nav-object-servability.ts
|
|
2979
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
2980
|
+
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2981
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2594
2982
|
function asArray14(v) {
|
|
2595
|
-
if (Array.isArray(v)) return v;
|
|
2596
|
-
if (
|
|
2983
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
2984
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2597
2985
|
return [];
|
|
2598
2986
|
}
|
|
2599
2987
|
function strName10(v) {
|
|
2600
2988
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2601
2989
|
}
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
const
|
|
2605
|
-
if (
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
}
|
|
2614
|
-
prev = curr;
|
|
2990
|
+
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2991
|
+
function validateNavObjectServability(stack) {
|
|
2992
|
+
const findings = [];
|
|
2993
|
+
if (!isRec8(stack)) return findings;
|
|
2994
|
+
const apps = asArray14(stack.apps);
|
|
2995
|
+
if (apps.length === 0) return findings;
|
|
2996
|
+
const ownEnable = /* @__PURE__ */ new Map();
|
|
2997
|
+
const objects = asArray14(stack.objects);
|
|
2998
|
+
for (const [oi, obj] of objects.entries()) {
|
|
2999
|
+
const n = strName10(obj.name);
|
|
3000
|
+
if (!n) continue;
|
|
3001
|
+
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
2615
3002
|
}
|
|
2616
|
-
return
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
3003
|
+
if (ownEnable.size === 0) return findings;
|
|
3004
|
+
for (const [ai, app] of apps.entries()) {
|
|
3005
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
3006
|
+
const walk = (items, basePath) => {
|
|
3007
|
+
if (!Array.isArray(items)) return;
|
|
3008
|
+
for (const [ni, raw] of items.entries()) {
|
|
3009
|
+
if (!isRec8(raw)) continue;
|
|
3010
|
+
const nav = raw;
|
|
3011
|
+
const navPath = `${basePath}[${ni}]`;
|
|
3012
|
+
if (nav.type === "object") {
|
|
3013
|
+
const target = strName10(nav.objectName);
|
|
3014
|
+
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3015
|
+
if (target && declared && !(0, import_data7.canServeApiOperation)(declared.enable, "list")) {
|
|
3016
|
+
const enable = isRec8(declared.enable) ? declared.enable : {};
|
|
3017
|
+
const apiDisabled = enable.apiEnabled === false;
|
|
3018
|
+
const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
|
|
3019
|
+
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
3020
|
+
const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
|
|
3021
|
+
findings.push({
|
|
3022
|
+
severity: "error",
|
|
3023
|
+
rule: NAV_OBJECT_UNSERVABLE,
|
|
3024
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
3025
|
+
// The nav entry is where the dead row is authored; the `enable`
|
|
3026
|
+
// key that condemns it is named in the message, because the fix
|
|
3027
|
+
// may belong at either end.
|
|
3028
|
+
path: `${navPath}.objectName`,
|
|
3029
|
+
message: `Navigation targets object "${target}", which cannot serve a list: ${condition} (\`${offendingKey}\`), so the list request answers ${answer} for EVERY user \u2014 platform administrators included, since that gate reads only the object's \`enable\` block and never the caller. The entry cannot be rescued with \`requiredPermissions\`: they are independent conditions. The server prunes this entry from the served \`/meta\` payload (#7912), so publishing it ships a menu row that silently is not there.`,
|
|
3030
|
+
hint: `Remove the nav entry, or make "${target}" listable by setting \`enable.apiEnabled: true\` and granting \`list\` in \`enable.apiMethods\`. \u26D4 Do NOT open the API on an object that is disabled on purpose \u2014 several platform objects hold credential material and are API-disabled deliberately; for those the entry is the mistake, not the \`enable\` block.`
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
3038
|
+
for (const [ari, area] of asArray14(app.areas).entries()) {
|
|
3039
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
3040
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return findings;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/validate-translation-references.ts
|
|
3047
|
+
var import_spec = require("@objectstack/spec");
|
|
3048
|
+
var import_system4 = require("@objectstack/spec/system");
|
|
3049
|
+
|
|
3050
|
+
// src/view-walk.ts
|
|
3051
|
+
function isRec9(v) {
|
|
3052
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3053
|
+
}
|
|
3054
|
+
function strName11(v) {
|
|
3055
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3056
|
+
}
|
|
3057
|
+
function viewObjectName(view) {
|
|
3058
|
+
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
|
|
3059
|
+
}
|
|
3060
|
+
function viewContainerSites(view, basePath) {
|
|
3061
|
+
if (!isRec9(view)) return [];
|
|
3062
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3063
|
+
if (isRec9(view.form)) {
|
|
3064
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3065
|
+
}
|
|
3066
|
+
for (const key of ["listViews", "formViews"]) {
|
|
3067
|
+
const container = view[key];
|
|
3068
|
+
if (!isRec9(container)) continue;
|
|
3069
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
3070
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
3071
|
+
if (!isRec9(sub)) continue;
|
|
3072
|
+
sites.push({
|
|
3073
|
+
view: sub,
|
|
3074
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
3075
|
+
surface: `${key}.${subKey}`,
|
|
3076
|
+
kind
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
return sites;
|
|
3081
|
+
}
|
|
3082
|
+
function formViewSites(view, basePath) {
|
|
3083
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// src/validate-translation-references.ts
|
|
3087
|
+
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3088
|
+
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3089
|
+
function isRec10(v) {
|
|
3090
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3091
|
+
}
|
|
3092
|
+
function asArray15(v) {
|
|
3093
|
+
if (Array.isArray(v)) return v;
|
|
3094
|
+
if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
|
|
3095
|
+
return [];
|
|
3096
|
+
}
|
|
3097
|
+
function strName12(v) {
|
|
3098
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3099
|
+
}
|
|
3100
|
+
function distance5(a, b) {
|
|
3101
|
+
const m = a.length;
|
|
3102
|
+
const n = b.length;
|
|
3103
|
+
if (m === 0) return n;
|
|
3104
|
+
if (n === 0) return m;
|
|
3105
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
3106
|
+
for (let i = 1; i <= m; i++) {
|
|
3107
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
3108
|
+
for (let j = 1; j <= n; j++) {
|
|
3109
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3110
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
3111
|
+
}
|
|
3112
|
+
prev = curr;
|
|
3113
|
+
}
|
|
3114
|
+
return prev[n];
|
|
3115
|
+
}
|
|
3116
|
+
function suggest6(target, known) {
|
|
3117
|
+
const names = [...known];
|
|
3118
|
+
const segmentMatch = names.find(
|
|
3119
|
+
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
3120
|
+
);
|
|
3121
|
+
if (segmentMatch) return ` Did you mean "${segmentMatch}"?`;
|
|
3122
|
+
let best;
|
|
3123
|
+
let bestScore = Infinity;
|
|
3124
|
+
for (const candidate of names) {
|
|
3125
|
+
const d = distance5(target, candidate);
|
|
2628
3126
|
if (d < bestScore) {
|
|
2629
3127
|
bestScore = d;
|
|
2630
3128
|
best = candidate;
|
|
@@ -2656,30 +3154,52 @@ function collectViewRecord(view, factsFor) {
|
|
|
2656
3154
|
};
|
|
2657
3155
|
const addSections = (container, binding) => {
|
|
2658
3156
|
if (!binding) return;
|
|
2659
|
-
for (const section of
|
|
2660
|
-
const sectionName =
|
|
3157
|
+
for (const section of asArray15(container.sections)) {
|
|
3158
|
+
const sectionName = strName12(section.name);
|
|
2661
3159
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2662
3160
|
}
|
|
2663
3161
|
};
|
|
2664
|
-
const listBinding =
|
|
2665
|
-
if (
|
|
2666
|
-
addView(recordObject ?? listBinding,
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
3162
|
+
const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
|
|
3163
|
+
if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3164
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
3165
|
+
const named = namedViewKeys(view);
|
|
3166
|
+
for (const family of ["listViews", "formViews"]) {
|
|
3167
|
+
const container = view[family];
|
|
3168
|
+
if (!isRec10(container)) continue;
|
|
3169
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3170
|
+
let at = 0;
|
|
3171
|
+
for (const sub of Object.values(container)) {
|
|
3172
|
+
if (!sub || typeof sub !== "object") continue;
|
|
3173
|
+
const registryKey = registryKeys[at++];
|
|
3174
|
+
if (!isRec10(sub)) continue;
|
|
2672
3175
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2673
|
-
addView(binding,
|
|
2674
|
-
addView(binding, strName10(sub.name));
|
|
3176
|
+
addView(binding, registryKey);
|
|
2675
3177
|
addSections(sub, binding);
|
|
2676
3178
|
}
|
|
2677
3179
|
}
|
|
2678
|
-
if (
|
|
3180
|
+
if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2679
3181
|
addSections(view, recordObject ?? listBinding);
|
|
2680
3182
|
}
|
|
2681
|
-
function
|
|
2682
|
-
|
|
3183
|
+
function defaultListViewKey(object, container) {
|
|
3184
|
+
if (!object || !isRec10(container.list)) return void 0;
|
|
3185
|
+
const item = (0, import_spec.expandViewContainer)(object, container).find(
|
|
3186
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
3187
|
+
);
|
|
3188
|
+
if (!item) return void 0;
|
|
3189
|
+
const prefix = `${object}.`;
|
|
3190
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
3191
|
+
}
|
|
3192
|
+
function namedViewKeys(container) {
|
|
3193
|
+
const object = "probe";
|
|
3194
|
+
const prefix = `${object}.`;
|
|
3195
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3196
|
+
const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3197
|
+
const listCount = countEntries(container.listViews);
|
|
3198
|
+
const formCount = countEntries(container.formViews);
|
|
3199
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
3200
|
+
const items = (0, import_spec.expandViewContainer)(object, container);
|
|
3201
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
3202
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
2683
3203
|
}
|
|
2684
3204
|
function readOptions(field) {
|
|
2685
3205
|
const raw = field.options;
|
|
@@ -2691,14 +3211,14 @@ function readOptions(field) {
|
|
|
2691
3211
|
values.add(opt);
|
|
2692
3212
|
continue;
|
|
2693
3213
|
}
|
|
2694
|
-
if (!
|
|
2695
|
-
const value =
|
|
3214
|
+
if (!isRec10(opt)) continue;
|
|
3215
|
+
const value = strName12(opt.value);
|
|
2696
3216
|
if (!value) continue;
|
|
2697
3217
|
values.add(value);
|
|
2698
|
-
const label2 =
|
|
3218
|
+
const label2 = strName12(opt.label);
|
|
2699
3219
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2700
3220
|
}
|
|
2701
|
-
} else if (
|
|
3221
|
+
} else if (isRec10(raw)) {
|
|
2702
3222
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2703
3223
|
values.add(value);
|
|
2704
3224
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2718,48 +3238,48 @@ function buildUniverse(stack) {
|
|
|
2718
3238
|
}
|
|
2719
3239
|
return facts;
|
|
2720
3240
|
};
|
|
2721
|
-
for (const obj of
|
|
2722
|
-
const objectName =
|
|
3241
|
+
for (const obj of asArray15(stack.objects)) {
|
|
3242
|
+
const objectName = strName12(obj.name);
|
|
2723
3243
|
if (!objectName) continue;
|
|
2724
3244
|
const facts = factsFor(objectName);
|
|
2725
|
-
for (const field of
|
|
2726
|
-
const fieldName =
|
|
3245
|
+
for (const field of asArray15(obj.fields)) {
|
|
3246
|
+
const fieldName = strName12(field.name);
|
|
2727
3247
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
2728
3248
|
}
|
|
2729
|
-
for (const action of
|
|
2730
|
-
const actionName =
|
|
3249
|
+
for (const action of asArray15(obj.actions)) {
|
|
3250
|
+
const actionName = strName12(action.name);
|
|
2731
3251
|
if (actionName) facts.actions.set(actionName, action);
|
|
2732
3252
|
}
|
|
2733
|
-
for (const view of
|
|
2734
|
-
collectViewRecord({ ...view, object:
|
|
3253
|
+
for (const view of asArray15(obj.views)) {
|
|
3254
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
2735
3255
|
}
|
|
2736
3256
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
2737
|
-
for (const group of
|
|
2738
|
-
const key =
|
|
3257
|
+
for (const group of asArray15(obj.fieldGroups)) {
|
|
3258
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
2739
3259
|
if (key) facts.sections.add(key);
|
|
2740
3260
|
}
|
|
2741
3261
|
}
|
|
2742
|
-
for (const view of
|
|
3262
|
+
for (const view of asArray15(stack.views)) {
|
|
2743
3263
|
collectViewRecord(view, factsFor);
|
|
2744
3264
|
}
|
|
2745
|
-
const pages =
|
|
3265
|
+
const pages = asArray15(stack.pages);
|
|
2746
3266
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2747
3267
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
2748
3268
|
if (!walked.objectName) continue;
|
|
2749
|
-
const props =
|
|
3269
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
2750
3270
|
if (!props) continue;
|
|
2751
|
-
for (const section of
|
|
2752
|
-
const sectionName =
|
|
3271
|
+
for (const section of asArray15(props.sections)) {
|
|
3272
|
+
const sectionName = strName12(section.name);
|
|
2753
3273
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
2754
3274
|
}
|
|
2755
3275
|
}
|
|
2756
3276
|
}
|
|
2757
3277
|
const globalActions = /* @__PURE__ */ new Map();
|
|
2758
3278
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
2759
|
-
for (const action of
|
|
2760
|
-
const actionName =
|
|
3279
|
+
for (const action of asArray15(stack.actions)) {
|
|
3280
|
+
const actionName = strName12(action.name);
|
|
2761
3281
|
if (!actionName) continue;
|
|
2762
|
-
const owner =
|
|
3282
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
2763
3283
|
if (owner) {
|
|
2764
3284
|
factsFor(owner).actions.set(actionName, action);
|
|
2765
3285
|
actionOwners.set(actionName, owner);
|
|
@@ -2773,41 +3293,41 @@ function buildUniverse(stack) {
|
|
|
2773
3293
|
}
|
|
2774
3294
|
}
|
|
2775
3295
|
const apps = /* @__PURE__ */ new Map();
|
|
2776
|
-
for (const app of
|
|
2777
|
-
const appName =
|
|
3296
|
+
for (const app of asArray15(stack.apps)) {
|
|
3297
|
+
const appName = strName12(app.name);
|
|
2778
3298
|
if (!appName) continue;
|
|
2779
3299
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
2780
3300
|
const walkNav = (items) => {
|
|
2781
|
-
for (const item of
|
|
2782
|
-
const id =
|
|
3301
|
+
for (const item of asArray15(items)) {
|
|
3302
|
+
const id = strName12(item.id);
|
|
2783
3303
|
if (id) navIds.add(id);
|
|
2784
3304
|
if (item.children) walkNav(item.children);
|
|
2785
3305
|
}
|
|
2786
3306
|
};
|
|
2787
3307
|
walkNav(app.navigation);
|
|
2788
|
-
for (const area of
|
|
2789
|
-
const areaId =
|
|
3308
|
+
for (const area of asArray15(app.areas)) {
|
|
3309
|
+
const areaId = strName12(area.id);
|
|
2790
3310
|
if (areaId) navIds.add(areaId);
|
|
2791
3311
|
walkNav(area.navigation);
|
|
2792
3312
|
}
|
|
2793
3313
|
apps.set(appName, navIds);
|
|
2794
3314
|
}
|
|
2795
3315
|
const dashboards = /* @__PURE__ */ new Map();
|
|
2796
|
-
for (const dash of
|
|
2797
|
-
const dashName =
|
|
3316
|
+
for (const dash of asArray15(stack.dashboards)) {
|
|
3317
|
+
const dashName = strName12(dash.name);
|
|
2798
3318
|
if (!dashName) continue;
|
|
2799
3319
|
const widgets = /* @__PURE__ */ new Set();
|
|
2800
|
-
for (const widget of
|
|
2801
|
-
const id =
|
|
3320
|
+
for (const widget of asArray15(dash.widgets)) {
|
|
3321
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
2802
3322
|
if (id) widgets.add(id);
|
|
2803
3323
|
}
|
|
2804
3324
|
const actions = /* @__PURE__ */ new Set();
|
|
2805
3325
|
const headerActions = [
|
|
2806
|
-
...
|
|
2807
|
-
...
|
|
3326
|
+
...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
|
|
3327
|
+
...asArray15(dash.actions)
|
|
2808
3328
|
];
|
|
2809
3329
|
for (const action of headerActions) {
|
|
2810
|
-
const key =
|
|
3330
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
2811
3331
|
if (key) actions.add(key);
|
|
2812
3332
|
}
|
|
2813
3333
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -2819,7 +3339,7 @@ function localePath(bundleIndex, locale) {
|
|
|
2819
3339
|
}
|
|
2820
3340
|
function validateTranslationReferences(stack) {
|
|
2821
3341
|
const findings = [];
|
|
2822
|
-
if (!
|
|
3342
|
+
if (!isRec10(stack)) return findings;
|
|
2823
3343
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
2824
3344
|
if (bundles.length === 0) return findings;
|
|
2825
3345
|
const universe = buildUniverse(stack);
|
|
@@ -2828,13 +3348,13 @@ function validateTranslationReferences(stack) {
|
|
|
2828
3348
|
};
|
|
2829
3349
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
2830
3350
|
const bundle = bundles[bi];
|
|
2831
|
-
if (!
|
|
3351
|
+
if (!isRec10(bundle)) continue;
|
|
2832
3352
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
2833
|
-
if (!
|
|
3353
|
+
if (!isRec10(rawData)) continue;
|
|
2834
3354
|
const base = localePath(bi, locale);
|
|
2835
3355
|
const inLocale = `locale "${locale}"`;
|
|
2836
3356
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
2837
|
-
if (!
|
|
3357
|
+
if (!isRec10(rawNode)) continue;
|
|
2838
3358
|
const objPath = `${base}.objects.${objectName}`;
|
|
2839
3359
|
const facts = universe.objects.get(objectName);
|
|
2840
3360
|
if (!facts) {
|
|
@@ -2860,7 +3380,7 @@ function validateTranslationReferences(stack) {
|
|
|
2860
3380
|
);
|
|
2861
3381
|
continue;
|
|
2862
3382
|
}
|
|
2863
|
-
if (!
|
|
3383
|
+
if (!isRec10(rawField)) continue;
|
|
2864
3384
|
checkOptionKeys(findings, {
|
|
2865
3385
|
optionMap: rawField.options,
|
|
2866
3386
|
field,
|
|
@@ -2942,7 +3462,7 @@ function validateTranslationReferences(stack) {
|
|
|
2942
3462
|
);
|
|
2943
3463
|
continue;
|
|
2944
3464
|
}
|
|
2945
|
-
if (!
|
|
3465
|
+
if (!isRec10(rawApp)) continue;
|
|
2946
3466
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
2947
3467
|
if (navIds.has(navId)) continue;
|
|
2948
3468
|
orphan(
|
|
@@ -2965,7 +3485,7 @@ function validateTranslationReferences(stack) {
|
|
|
2965
3485
|
);
|
|
2966
3486
|
continue;
|
|
2967
3487
|
}
|
|
2968
|
-
if (!
|
|
3488
|
+
if (!isRec10(rawDash)) continue;
|
|
2969
3489
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
2970
3490
|
if (dash.widgets.has(widgetId)) continue;
|
|
2971
3491
|
orphan(
|
|
@@ -2990,7 +3510,7 @@ function validateTranslationReferences(stack) {
|
|
|
2990
3510
|
return findings;
|
|
2991
3511
|
}
|
|
2992
3512
|
function asRecord(v) {
|
|
2993
|
-
return
|
|
3513
|
+
return isRec10(v) ? v : {};
|
|
2994
3514
|
}
|
|
2995
3515
|
function checkOptionKeys(findings, ctx) {
|
|
2996
3516
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3002,7 +3522,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3002
3522
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3003
3523
|
where: ctx.where,
|
|
3004
3524
|
path: ctx.path,
|
|
3005
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3525
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
3006
3526
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3007
3527
|
});
|
|
3008
3528
|
return;
|
|
@@ -3021,11 +3541,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3021
3541
|
}
|
|
3022
3542
|
}
|
|
3023
3543
|
function checkActionParams(findings, ctx) {
|
|
3024
|
-
const rawParams = Object.keys(asRecord(
|
|
3544
|
+
const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3025
3545
|
if (rawParams.length === 0) return;
|
|
3026
3546
|
const declared = /* @__PURE__ */ new Set();
|
|
3027
|
-
for (const param of
|
|
3028
|
-
const name =
|
|
3547
|
+
for (const param of asArray15(ctx.action.params)) {
|
|
3548
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
3029
3549
|
if (name) declared.add(name);
|
|
3030
3550
|
}
|
|
3031
3551
|
for (const paramName of rawParams) {
|
|
@@ -3041,78 +3561,60 @@ function checkActionParams(findings, ctx) {
|
|
|
3041
3561
|
}
|
|
3042
3562
|
}
|
|
3043
3563
|
|
|
3044
|
-
// src/
|
|
3045
|
-
|
|
3046
|
-
function isRec8(v) {
|
|
3564
|
+
// src/collection-entries.ts
|
|
3565
|
+
function isRec11(v) {
|
|
3047
3566
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3048
3567
|
}
|
|
3049
|
-
function strName11(v) {
|
|
3050
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3051
|
-
}
|
|
3052
|
-
function viewObjectName2(view) {
|
|
3053
|
-
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
|
|
3054
|
-
}
|
|
3055
3568
|
function collectionEntries(v, base) {
|
|
3056
3569
|
if (Array.isArray(v)) {
|
|
3057
3570
|
const out = [];
|
|
3058
3571
|
for (let i = 0; i < v.length; i++) {
|
|
3059
|
-
if (
|
|
3572
|
+
if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3060
3573
|
}
|
|
3061
3574
|
return out;
|
|
3062
3575
|
}
|
|
3063
|
-
if (
|
|
3064
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3576
|
+
if (isRec11(v)) {
|
|
3577
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3065
3578
|
}
|
|
3066
3579
|
return [];
|
|
3067
3580
|
}
|
|
3581
|
+
|
|
3582
|
+
// src/validate-translatable-sections.ts
|
|
3583
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3584
|
+
function isRec12(v) {
|
|
3585
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3586
|
+
}
|
|
3587
|
+
function strName13(v) {
|
|
3588
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3589
|
+
}
|
|
3068
3590
|
function viewLabel(view) {
|
|
3069
|
-
const name =
|
|
3591
|
+
const name = strName13(view.name);
|
|
3070
3592
|
return name ? `view "${name}"` : "";
|
|
3071
3593
|
}
|
|
3072
3594
|
function joinWhere(...parts) {
|
|
3073
3595
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
3074
3596
|
}
|
|
3075
3597
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3076
|
-
const recordObject =
|
|
3077
|
-
const listBinding =
|
|
3078
|
-
const
|
|
3079
|
-
sites.push({
|
|
3080
|
-
path: `${basePath}.sections`,
|
|
3081
|
-
surface: label2,
|
|
3082
|
-
objectName: recordObject ?? listBinding,
|
|
3083
|
-
sections: view.sections
|
|
3084
|
-
});
|
|
3085
|
-
if (isRec8(view.form)) {
|
|
3598
|
+
const recordObject = viewObjectName(view);
|
|
3599
|
+
const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3600
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
3086
3601
|
sites.push({
|
|
3087
|
-
path: `${
|
|
3088
|
-
surface: joinWhere(label2,
|
|
3089
|
-
objectName:
|
|
3090
|
-
sections: view.
|
|
3602
|
+
path: `${site.path}.sections`,
|
|
3603
|
+
surface: joinWhere(label2, site.surface),
|
|
3604
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
3605
|
+
sections: site.view.sections
|
|
3091
3606
|
});
|
|
3092
3607
|
}
|
|
3093
|
-
for (const key of ["listViews", "formViews"]) {
|
|
3094
|
-
const container = view[key];
|
|
3095
|
-
if (!isRec8(container)) continue;
|
|
3096
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
3097
|
-
if (!isRec8(sub)) continue;
|
|
3098
|
-
sites.push({
|
|
3099
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
3100
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
3101
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
3102
|
-
sections: sub.sections
|
|
3103
|
-
});
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
3608
|
}
|
|
3107
3609
|
function translatedObjectNames(stack) {
|
|
3108
3610
|
const out = /* @__PURE__ */ new Set();
|
|
3109
3611
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3110
3612
|
for (const bundle of bundles) {
|
|
3111
|
-
if (!
|
|
3613
|
+
if (!isRec12(bundle)) continue;
|
|
3112
3614
|
for (const data of Object.values(bundle)) {
|
|
3113
|
-
if (!
|
|
3615
|
+
if (!isRec12(data) || !isRec12(data.objects)) continue;
|
|
3114
3616
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3115
|
-
if (
|
|
3617
|
+
if (isRec12(node)) out.add(objectName);
|
|
3116
3618
|
}
|
|
3117
3619
|
}
|
|
3118
3620
|
}
|
|
@@ -3124,22 +3626,22 @@ function suggestedName(label2) {
|
|
|
3124
3626
|
}
|
|
3125
3627
|
function validateTranslatableSections(stack) {
|
|
3126
3628
|
const findings = [];
|
|
3127
|
-
if (!
|
|
3629
|
+
if (!isRec12(stack)) return findings;
|
|
3128
3630
|
const translated = translatedObjectNames(stack);
|
|
3129
3631
|
if (translated.size === 0) return findings;
|
|
3130
3632
|
const sites = [];
|
|
3131
3633
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3132
|
-
const objectName =
|
|
3634
|
+
const objectName = strName13(obj.name);
|
|
3133
3635
|
if (!objectName) continue;
|
|
3134
3636
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3135
3637
|
collectViewSites(
|
|
3136
|
-
{ ...view, object:
|
|
3638
|
+
{ ...view, object: strName13(view.object) ?? objectName },
|
|
3137
3639
|
path,
|
|
3138
3640
|
viewLabel(view),
|
|
3139
3641
|
sites
|
|
3140
3642
|
);
|
|
3141
3643
|
}
|
|
3142
|
-
if (
|
|
3644
|
+
if (isRec12(obj.listViews)) {
|
|
3143
3645
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3144
3646
|
}
|
|
3145
3647
|
}
|
|
@@ -3147,13 +3649,13 @@ function validateTranslatableSections(stack) {
|
|
|
3147
3649
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3148
3650
|
}
|
|
3149
3651
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3150
|
-
const pageName =
|
|
3652
|
+
const pageName = strName13(page.name);
|
|
3151
3653
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3152
3654
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3153
3655
|
if (!walked.objectName) continue;
|
|
3154
|
-
const props =
|
|
3656
|
+
const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
|
|
3155
3657
|
if (!props) continue;
|
|
3156
|
-
const type =
|
|
3658
|
+
const type = strName13(walked.component.type) ?? "component";
|
|
3157
3659
|
sites.push({
|
|
3158
3660
|
path: `${walked.path}.properties.sections`,
|
|
3159
3661
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3168,9 +3670,9 @@ function validateTranslatableSections(stack) {
|
|
|
3168
3670
|
if (!Array.isArray(site.sections)) continue;
|
|
3169
3671
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3170
3672
|
const section = site.sections[i];
|
|
3171
|
-
if (!
|
|
3172
|
-
if (
|
|
3173
|
-
const heading =
|
|
3673
|
+
if (!isRec12(section)) continue;
|
|
3674
|
+
if (strName13(section.name)) continue;
|
|
3675
|
+
const heading = strName13(section.label);
|
|
3174
3676
|
if (!heading) continue;
|
|
3175
3677
|
const slug = suggestedName(heading);
|
|
3176
3678
|
findings.push({
|
|
@@ -3188,10 +3690,10 @@ function validateTranslatableSections(stack) {
|
|
|
3188
3690
|
|
|
3189
3691
|
// src/flow-walk.ts
|
|
3190
3692
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
3191
|
-
function
|
|
3693
|
+
function isRec13(v) {
|
|
3192
3694
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3193
3695
|
}
|
|
3194
|
-
function
|
|
3696
|
+
function strName14(v) {
|
|
3195
3697
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3196
3698
|
}
|
|
3197
3699
|
var REGION_SLOTS = new Map(
|
|
@@ -3200,10 +3702,10 @@ var REGION_SLOTS = new Map(
|
|
|
3200
3702
|
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
3201
3703
|
var MAX_REGION_DEPTH = 16;
|
|
3202
3704
|
function flowNodeLabel(node, index) {
|
|
3203
|
-
return
|
|
3705
|
+
return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
|
|
3204
3706
|
}
|
|
3205
3707
|
function stripRegions(config) {
|
|
3206
|
-
if (!
|
|
3708
|
+
if (!isRec13(config)) return void 0;
|
|
3207
3709
|
let out;
|
|
3208
3710
|
for (const key of Object.keys(config)) {
|
|
3209
3711
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3214,11 +3716,11 @@ function stripRegions(config) {
|
|
|
3214
3716
|
}
|
|
3215
3717
|
function walkFlowNodes(flow, flowPath) {
|
|
3216
3718
|
const out = [];
|
|
3217
|
-
if (!
|
|
3719
|
+
if (!isRec13(flow)) return out;
|
|
3218
3720
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3219
3721
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3220
3722
|
nodes.forEach((raw, index) => {
|
|
3221
|
-
if (!
|
|
3723
|
+
if (!isRec13(raw)) return;
|
|
3222
3724
|
const path = `${basePath}[${index}]`;
|
|
3223
3725
|
out.push({
|
|
3224
3726
|
node: raw,
|
|
@@ -3227,9 +3729,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3227
3729
|
regionTrail: trail,
|
|
3228
3730
|
depth
|
|
3229
3731
|
});
|
|
3230
|
-
const type =
|
|
3732
|
+
const type = strName14(raw.type);
|
|
3231
3733
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3232
|
-
if (!slots || !
|
|
3734
|
+
if (!slots || !isRec13(raw.config)) return;
|
|
3233
3735
|
const config = raw.config;
|
|
3234
3736
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3235
3737
|
for (const slot of slots) {
|
|
@@ -3237,8 +3739,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3237
3739
|
if (slot === "branches") {
|
|
3238
3740
|
if (!Array.isArray(value)) continue;
|
|
3239
3741
|
value.forEach((branch, b) => {
|
|
3240
|
-
if (!
|
|
3241
|
-
const branchName =
|
|
3742
|
+
if (!isRec13(branch)) return;
|
|
3743
|
+
const branchName = strName14(branch.name) ?? `#${b}`;
|
|
3242
3744
|
visitList(
|
|
3243
3745
|
branch.nodes,
|
|
3244
3746
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3248,7 +3750,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3248
3750
|
});
|
|
3249
3751
|
continue;
|
|
3250
3752
|
}
|
|
3251
|
-
if (!
|
|
3753
|
+
if (!isRec13(value)) continue;
|
|
3252
3754
|
visitList(
|
|
3253
3755
|
value.nodes,
|
|
3254
3756
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3268,7 +3770,8 @@ function joinTrail(trail, segment) {
|
|
|
3268
3770
|
// src/validate-flow-template-paths.ts
|
|
3269
3771
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
3270
3772
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
3271
|
-
|
|
3773
|
+
var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
|
|
3774
|
+
function asArray16(v) {
|
|
3272
3775
|
if (Array.isArray(v)) return v;
|
|
3273
3776
|
if (v && typeof v === "object") {
|
|
3274
3777
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -3297,7 +3800,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3297
3800
|
]);
|
|
3298
3801
|
function fieldTypesOf(obj) {
|
|
3299
3802
|
const types = /* @__PURE__ */ new Map();
|
|
3300
|
-
for (const f of
|
|
3803
|
+
for (const f of asArray16(obj.fields)) {
|
|
3301
3804
|
if (typeof f.name === "string") {
|
|
3302
3805
|
types.set(f.name, typeof f.type === "string" ? f.type : "");
|
|
3303
3806
|
}
|
|
@@ -3394,10 +3897,10 @@ function declaredExpandOf(flow) {
|
|
|
3394
3897
|
}
|
|
3395
3898
|
function validateFlowTemplatePaths(stack) {
|
|
3396
3899
|
const findings = [];
|
|
3397
|
-
const flows =
|
|
3900
|
+
const flows = asArray16(stack.flows);
|
|
3398
3901
|
if (flows.length === 0) return findings;
|
|
3399
3902
|
const objectsByName = /* @__PURE__ */ new Map();
|
|
3400
|
-
for (const obj of
|
|
3903
|
+
for (const obj of asArray16(stack.objects)) {
|
|
3401
3904
|
if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
|
|
3402
3905
|
}
|
|
3403
3906
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -3410,6 +3913,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3410
3913
|
const obj = objectsByName.get(objectName);
|
|
3411
3914
|
if (!obj) return;
|
|
3412
3915
|
const fieldTypes = fieldTypesOf(obj);
|
|
3916
|
+
const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
|
|
3413
3917
|
const expandSet = declaredExpandOf(flow);
|
|
3414
3918
|
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
3415
3919
|
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
@@ -3421,6 +3925,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3421
3925
|
if (leaves.length === 0) return;
|
|
3422
3926
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
3423
3927
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
3928
|
+
const seenUnprovisioned = /* @__PURE__ */ new Set();
|
|
3424
3929
|
for (const leaf of leaves) {
|
|
3425
3930
|
const inFilter = leaf.inFilter;
|
|
3426
3931
|
for (const rest of recordRefsIn(leaf.text)) {
|
|
@@ -3428,6 +3933,19 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3428
3933
|
const hasSubPath = rest.length > 1;
|
|
3429
3934
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
3430
3935
|
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
3936
|
+
if (unprovisionedAnchors.has(head)) {
|
|
3937
|
+
if (!seenUnprovisioned.has(head)) {
|
|
3938
|
+
seenUnprovisioned.add(head);
|
|
3939
|
+
findings.push({
|
|
3940
|
+
severity: "warning",
|
|
3941
|
+
rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
3942
|
+
where,
|
|
3943
|
+
path: nodePath,
|
|
3944
|
+
message: (inFilter ? `${nodeType} filter references ` : "template references ") + `'{record.${rest.join(".")}}', and ${unprovisionedAnchorCause(objectName, head)} \u2014 ` + (inFilter ? `the token resolves to nothing on every run, which DROPS the condition from the query instead of narrowing it; the node then refuses to run at execution time (#3810).` : `the token resolves to an empty string on every run (silently).`),
|
|
3945
|
+
hint: unprovisionedAnchorHint(objectName, head)
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3431
3949
|
if (!isKnown) {
|
|
3432
3950
|
if (seenUnknown.has(head)) continue;
|
|
3433
3951
|
seenUnknown.add(head);
|
|
@@ -3466,14 +3984,14 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3466
3984
|
|
|
3467
3985
|
// src/validate-ai-surface-affinity.ts
|
|
3468
3986
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
3469
|
-
function
|
|
3987
|
+
function asArray17(v) {
|
|
3470
3988
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3471
3989
|
if (v && typeof v === "object") {
|
|
3472
3990
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3473
3991
|
}
|
|
3474
3992
|
return [];
|
|
3475
3993
|
}
|
|
3476
|
-
function
|
|
3994
|
+
function strName15(v) {
|
|
3477
3995
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3478
3996
|
}
|
|
3479
3997
|
function surfaceOf(v) {
|
|
@@ -3483,18 +4001,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3483
4001
|
const findings = [];
|
|
3484
4002
|
if (!stack || typeof stack !== "object") return findings;
|
|
3485
4003
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3486
|
-
for (const skill of
|
|
3487
|
-
const n =
|
|
4004
|
+
for (const skill of asArray17(stack.skills)) {
|
|
4005
|
+
const n = strName15(skill.name);
|
|
3488
4006
|
if (n) skillsByName.set(n, skill);
|
|
3489
4007
|
}
|
|
3490
|
-
const agents =
|
|
4008
|
+
const agents = asArray17(stack.agents);
|
|
3491
4009
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3492
4010
|
const agent = agents[ai];
|
|
3493
|
-
const agentName =
|
|
4011
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
3494
4012
|
const agentSurface = surfaceOf(agent.surface);
|
|
3495
4013
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3496
4014
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3497
|
-
const ref =
|
|
4015
|
+
const ref = strName15(skillRefs[si]);
|
|
3498
4016
|
if (!ref) continue;
|
|
3499
4017
|
const skill = skillsByName.get(ref);
|
|
3500
4018
|
if (!skill) continue;
|
|
@@ -3516,14 +4034,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3516
4034
|
// src/validate-ai-tool-references.ts
|
|
3517
4035
|
var import_system5 = require("@objectstack/spec/system");
|
|
3518
4036
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
3519
|
-
function
|
|
4037
|
+
function asArray18(v) {
|
|
3520
4038
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3521
4039
|
if (v && typeof v === "object") {
|
|
3522
4040
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3523
4041
|
}
|
|
3524
4042
|
return [];
|
|
3525
4043
|
}
|
|
3526
|
-
function
|
|
4044
|
+
function strName16(v) {
|
|
3527
4045
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3528
4046
|
}
|
|
3529
4047
|
function distance6(a, b) {
|
|
@@ -3564,26 +4082,26 @@ function materialisesAsTool(action) {
|
|
|
3564
4082
|
if (!ai || typeof ai !== "object") return false;
|
|
3565
4083
|
const aiRec = ai;
|
|
3566
4084
|
if (aiRec.exposed !== true) return false;
|
|
3567
|
-
if (!
|
|
3568
|
-
const type =
|
|
4085
|
+
if (!strName16(aiRec.description)) return false;
|
|
4086
|
+
const type = strName16(action.type);
|
|
3569
4087
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3570
4088
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3571
4089
|
return Boolean(action.target);
|
|
3572
4090
|
}
|
|
3573
4091
|
function collectToolUniverse(stack) {
|
|
3574
4092
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3575
|
-
for (const tool of
|
|
3576
|
-
const n =
|
|
4093
|
+
for (const tool of asArray18(stack.tools)) {
|
|
4094
|
+
const n = strName16(tool.name);
|
|
3577
4095
|
if (n) universe.add(n);
|
|
3578
4096
|
}
|
|
3579
4097
|
const addActionFamily = (actions) => {
|
|
3580
|
-
for (const action of
|
|
3581
|
-
const n =
|
|
4098
|
+
for (const action of asArray18(actions)) {
|
|
4099
|
+
const n = strName16(action.name);
|
|
3582
4100
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3583
4101
|
}
|
|
3584
4102
|
};
|
|
3585
4103
|
addActionFamily(stack.actions);
|
|
3586
|
-
for (const obj of
|
|
4104
|
+
for (const obj of asArray18(stack.objects)) {
|
|
3587
4105
|
addActionFamily(obj.actions);
|
|
3588
4106
|
}
|
|
3589
4107
|
return universe;
|
|
@@ -3591,13 +4109,13 @@ function collectToolUniverse(stack) {
|
|
|
3591
4109
|
function collectUnexposedActionNames(stack) {
|
|
3592
4110
|
const names = /* @__PURE__ */ new Set();
|
|
3593
4111
|
const scan = (actions) => {
|
|
3594
|
-
for (const action of
|
|
3595
|
-
const n =
|
|
4112
|
+
for (const action of asArray18(actions)) {
|
|
4113
|
+
const n = strName16(action.name);
|
|
3596
4114
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3597
4115
|
}
|
|
3598
4116
|
};
|
|
3599
4117
|
scan(stack.actions);
|
|
3600
|
-
for (const obj of
|
|
4118
|
+
for (const obj of asArray18(stack.objects)) scan(obj.actions);
|
|
3601
4119
|
return names;
|
|
3602
4120
|
}
|
|
3603
4121
|
function validateAiToolReferences(stack) {
|
|
@@ -3615,13 +4133,13 @@ function validateAiToolReferences(stack) {
|
|
|
3615
4133
|
}
|
|
3616
4134
|
return universe.has(ref);
|
|
3617
4135
|
};
|
|
3618
|
-
const skills =
|
|
4136
|
+
const skills = asArray18(stack.skills);
|
|
3619
4137
|
for (let si = 0; si < skills.length; si++) {
|
|
3620
4138
|
const skill = skills[si];
|
|
3621
|
-
const skillName =
|
|
4139
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
3622
4140
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3623
4141
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3624
|
-
const ref =
|
|
4142
|
+
const ref = strName16(refs[ti]);
|
|
3625
4143
|
if (!ref || resolves(ref)) continue;
|
|
3626
4144
|
const isPattern = ref.endsWith("*");
|
|
3627
4145
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3640,24 +4158,25 @@ function validateAiToolReferences(stack) {
|
|
|
3640
4158
|
|
|
3641
4159
|
// src/validate-ai-agent-authoring.ts
|
|
3642
4160
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3643
|
-
|
|
4161
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
4162
|
+
function asArray19(v) {
|
|
3644
4163
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3645
4164
|
if (v && typeof v === "object") {
|
|
3646
4165
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3647
4166
|
}
|
|
3648
4167
|
return [];
|
|
3649
4168
|
}
|
|
3650
|
-
function
|
|
4169
|
+
function strName17(v) {
|
|
3651
4170
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3652
4171
|
}
|
|
3653
4172
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
3654
4173
|
function validateAiAgentAuthoring(stack) {
|
|
3655
4174
|
const findings = [];
|
|
3656
4175
|
if (!stack || typeof stack !== "object") return findings;
|
|
3657
|
-
const agents =
|
|
4176
|
+
const agents = asArray19(stack.agents);
|
|
3658
4177
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3659
4178
|
const agent = agents[ai];
|
|
3660
|
-
const name =
|
|
4179
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
3661
4180
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3662
4181
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3663
4182
|
findings.push({
|
|
@@ -3669,6 +4188,22 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3669
4188
|
hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
|
|
3670
4189
|
});
|
|
3671
4190
|
}
|
|
4191
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
4192
|
+
const apps = asArray19(stack.apps);
|
|
4193
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4194
|
+
const app = apps[appIdx];
|
|
4195
|
+
const defaultAgent = strName17(app.defaultAgent);
|
|
4196
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4197
|
+
const appName = strName17(app.name) ?? `#${appIdx}`;
|
|
4198
|
+
findings.push({
|
|
4199
|
+
severity: "warning",
|
|
4200
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
4201
|
+
where: `app "${appName}".defaultAgent`,
|
|
4202
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
4203
|
+
message: `app "${appName}" pins \`defaultAgent\` to "${defaultAgent}", which is not in the platform agent roster (${roster}). The kernel ships exactly two agents (ADR-0063 \xA72) and resolves this key against them and their legacy aliases only \u2014 an unrecognized name is not rejected, it silently falls back to the platform default at runtime, so the pin has no effect and the value drifts from what actually serves the app.`,
|
|
4204
|
+
hint: `Set \`defaultAgent\` to one of the platform agent names: ${roster}. If the goal is a dedicated persona or capability, express it as skills instead \u2014 they attach to "ask" / "build" by surface affinity, not as a custom \`defaultAgent\` value.`
|
|
4205
|
+
});
|
|
4206
|
+
}
|
|
3672
4207
|
return findings;
|
|
3673
4208
|
}
|
|
3674
4209
|
|
|
@@ -3756,24 +4291,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
3756
4291
|
"owner",
|
|
3757
4292
|
"record_type"
|
|
3758
4293
|
]);
|
|
3759
|
-
var
|
|
3760
|
-
function
|
|
3761
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3762
|
-
if (
|
|
4294
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4295
|
+
function asArray20(v) {
|
|
4296
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4297
|
+
if (isRec14(v)) {
|
|
3763
4298
|
return Object.entries(v).map(([name, def]) => ({
|
|
3764
4299
|
name,
|
|
3765
|
-
...
|
|
4300
|
+
...isRec14(def) ? def : {}
|
|
3766
4301
|
}));
|
|
3767
4302
|
}
|
|
3768
4303
|
return [];
|
|
3769
4304
|
}
|
|
3770
4305
|
function indexObjectFields2(stack) {
|
|
3771
4306
|
const out = /* @__PURE__ */ new Map();
|
|
3772
|
-
for (const obj of
|
|
4307
|
+
for (const obj of asArray20(stack.objects)) {
|
|
3773
4308
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
3774
4309
|
if (!name) continue;
|
|
3775
4310
|
const names = /* @__PURE__ */ new Set();
|
|
3776
|
-
for (const f of
|
|
4311
|
+
for (const f of asArray20(obj.fields)) {
|
|
3777
4312
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
3778
4313
|
}
|
|
3779
4314
|
out.set(name, names);
|
|
@@ -3903,12 +4438,12 @@ ${source}
|
|
|
3903
4438
|
}
|
|
3904
4439
|
function validateHookBodyWrites(stack) {
|
|
3905
4440
|
const findings = [];
|
|
3906
|
-
const hooks =
|
|
4441
|
+
const hooks = asArray20(stack.hooks);
|
|
3907
4442
|
if (hooks.length === 0) return findings;
|
|
3908
4443
|
let objectFields = null;
|
|
3909
4444
|
hooks.forEach((hook, hookIndex) => {
|
|
3910
4445
|
const body = hook.body;
|
|
3911
|
-
if (!
|
|
4446
|
+
if (!isRec14(body) || body.language !== "js") return;
|
|
3912
4447
|
const source = body.source;
|
|
3913
4448
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3914
4449
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -3979,13 +4514,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
3979
4514
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
3980
4515
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
3981
4516
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
3982
|
-
var
|
|
3983
|
-
function
|
|
3984
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3985
|
-
if (
|
|
4517
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4518
|
+
function asArray21(v) {
|
|
4519
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4520
|
+
if (isRec15(v)) {
|
|
3986
4521
|
return Object.entries(v).map(([name, def]) => ({
|
|
3987
4522
|
name,
|
|
3988
|
-
...
|
|
4523
|
+
...isRec15(def) ? def : {}
|
|
3989
4524
|
}));
|
|
3990
4525
|
}
|
|
3991
4526
|
return [];
|
|
@@ -3999,11 +4534,11 @@ function collectActionBodies(stack) {
|
|
|
3999
4534
|
const sites = [];
|
|
4000
4535
|
const seen = /* @__PURE__ */ new Set();
|
|
4001
4536
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
4002
|
-
|
|
4537
|
+
asArray21(actions).forEach((action, index) => {
|
|
4003
4538
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4004
4539
|
if (type !== "script") return;
|
|
4005
4540
|
const body = action.body;
|
|
4006
|
-
if (!
|
|
4541
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4007
4542
|
const source = body.source;
|
|
4008
4543
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4009
4544
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4014,7 +4549,7 @@ function collectActionBodies(stack) {
|
|
|
4014
4549
|
});
|
|
4015
4550
|
};
|
|
4016
4551
|
collect(stack.actions, "actions");
|
|
4017
|
-
|
|
4552
|
+
asArray21(stack.objects).forEach((obj, objIndex) => {
|
|
4018
4553
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
4019
4554
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
4020
4555
|
});
|
|
@@ -4022,7 +4557,7 @@ function collectActionBodies(stack) {
|
|
|
4022
4557
|
}
|
|
4023
4558
|
function validateActionBodyWrites(stack) {
|
|
4024
4559
|
const findings = [];
|
|
4025
|
-
if (!
|
|
4560
|
+
if (!isRec15(stack)) return findings;
|
|
4026
4561
|
const sites = collectActionBodies(stack);
|
|
4027
4562
|
if (sites.length === 0) return findings;
|
|
4028
4563
|
let objectFields = null;
|
|
@@ -4080,13 +4615,13 @@ function fixHint2(field, declared) {
|
|
|
4080
4615
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
4081
4616
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4082
4617
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4083
|
-
var
|
|
4084
|
-
function
|
|
4085
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4086
|
-
if (
|
|
4618
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4619
|
+
function asArray22(v) {
|
|
4620
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4621
|
+
if (isRec16(v)) {
|
|
4087
4622
|
return Object.entries(v).map(([name, def]) => ({
|
|
4088
4623
|
name,
|
|
4089
|
-
...
|
|
4624
|
+
...isRec16(def) ? def : {}
|
|
4090
4625
|
}));
|
|
4091
4626
|
}
|
|
4092
4627
|
return [];
|
|
@@ -4099,8 +4634,8 @@ function readLiteralObjectName(config) {
|
|
|
4099
4634
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4100
4635
|
function validateFlowNodeWrites(stack) {
|
|
4101
4636
|
const findings = [];
|
|
4102
|
-
if (!
|
|
4103
|
-
const flows =
|
|
4637
|
+
if (!isRec16(stack)) return findings;
|
|
4638
|
+
const flows = asArray22(stack.flows);
|
|
4104
4639
|
if (flows.length === 0) return findings;
|
|
4105
4640
|
let objectFields = null;
|
|
4106
4641
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -4108,10 +4643,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4108
4643
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4109
4644
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4110
4645
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4111
|
-
const config =
|
|
4646
|
+
const config = isRec16(node.config) ? node.config : void 0;
|
|
4112
4647
|
if (!config) return;
|
|
4113
4648
|
const fields = config.fields;
|
|
4114
|
-
if (!
|
|
4649
|
+
if (!isRec16(fields)) return;
|
|
4115
4650
|
const written = Object.keys(fields);
|
|
4116
4651
|
if (written.length === 0) return;
|
|
4117
4652
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4145,7 +4680,7 @@ function fixHint3(field, declared) {
|
|
|
4145
4680
|
// src/validate-readonly-flow-writes.ts
|
|
4146
4681
|
var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
|
|
4147
4682
|
var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
|
|
4148
|
-
function
|
|
4683
|
+
function asArray23(v) {
|
|
4149
4684
|
if (Array.isArray(v)) return v;
|
|
4150
4685
|
if (v && typeof v === "object") {
|
|
4151
4686
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -4186,9 +4721,9 @@ function readLiteralObjectName2(config) {
|
|
|
4186
4721
|
}
|
|
4187
4722
|
function validateReadonlyFlowWrites(stack) {
|
|
4188
4723
|
const findings = [];
|
|
4189
|
-
const flows =
|
|
4724
|
+
const flows = asArray23(stack.flows);
|
|
4190
4725
|
if (flows.length === 0) return findings;
|
|
4191
|
-
const roIndex = buildReadonlyIndex(
|
|
4726
|
+
const roIndex = buildReadonlyIndex(asArray23(stack.objects));
|
|
4192
4727
|
flows.forEach((flow, flowIndex) => {
|
|
4193
4728
|
if (flow.runAs === "system") return;
|
|
4194
4729
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
@@ -4236,14 +4771,14 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
4236
4771
|
// src/validate-react-page-props.ts
|
|
4237
4772
|
var import_node_module2 = require("module");
|
|
4238
4773
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
4239
|
-
var
|
|
4774
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
4240
4775
|
|
|
4241
4776
|
// src/zod-issue-format.ts
|
|
4242
|
-
var
|
|
4777
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4243
4778
|
var valueAtPath = (root, path) => {
|
|
4244
4779
|
let cur = root;
|
|
4245
4780
|
for (const key of path) {
|
|
4246
|
-
if (!
|
|
4781
|
+
if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
|
|
4247
4782
|
cur = cur[key];
|
|
4248
4783
|
}
|
|
4249
4784
|
return cur;
|
|
@@ -4289,7 +4824,7 @@ function loadTypeScript2() {
|
|
|
4289
4824
|
}
|
|
4290
4825
|
return cachedTs2;
|
|
4291
4826
|
}
|
|
4292
|
-
var
|
|
4827
|
+
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4293
4828
|
var BLOCKS = new Map(
|
|
4294
4829
|
import_ui2.REACT_BLOCKS.map((b) => [
|
|
4295
4830
|
b.tag,
|
|
@@ -4377,12 +4912,13 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4377
4912
|
return perPosition(init.expression);
|
|
4378
4913
|
}
|
|
4379
4914
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4915
|
+
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4380
4916
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
4381
4917
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
4382
4918
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4383
4919
|
function checkChartDrillDown(raw, push2) {
|
|
4384
4920
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4385
|
-
if (!
|
|
4921
|
+
if (!isRec18(raw)) {
|
|
4386
4922
|
push2(
|
|
4387
4923
|
"error",
|
|
4388
4924
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4405,7 +4941,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4405
4941
|
}
|
|
4406
4942
|
function checkChartAggregate(raw, push2) {
|
|
4407
4943
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4408
|
-
if (!
|
|
4944
|
+
if (!isRec18(raw)) {
|
|
4409
4945
|
push2(
|
|
4410
4946
|
"error",
|
|
4411
4947
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4420,7 +4956,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4420
4956
|
"warning",
|
|
4421
4957
|
REACT_CHART_AGGREGATE_INVALID,
|
|
4422
4958
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
4423
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
4959
|
+
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
|
|
4424
4960
|
);
|
|
4425
4961
|
}
|
|
4426
4962
|
const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
|
|
@@ -4436,9 +4972,9 @@ function checkChartAggregate(raw, push2) {
|
|
|
4436
4972
|
);
|
|
4437
4973
|
}
|
|
4438
4974
|
}
|
|
4439
|
-
var
|
|
4975
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4440
4976
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4441
|
-
function checkObjectChart(attrs, objectFields, findings) {
|
|
4977
|
+
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4442
4978
|
const { values, where, path } = attrs;
|
|
4443
4979
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
4444
4980
|
checkChartDrillDown(values.get("drillDown"), push2);
|
|
@@ -4446,18 +4982,29 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4446
4982
|
const aggregate = values.get("aggregate");
|
|
4447
4983
|
checkChartAggregate(aggregate, push2);
|
|
4448
4984
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4449
|
-
if (!
|
|
4985
|
+
if (!isRec18(aggregate)) return;
|
|
4450
4986
|
const fn = strOf(aggregate.function);
|
|
4451
4987
|
const field = strOf(aggregate.field);
|
|
4452
4988
|
const groupBy = aggregate.groupBy;
|
|
4453
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4989
|
+
const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4454
4990
|
const objectName = strOf(values.get("objectName"));
|
|
4455
4991
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4456
4992
|
if (objectName && known) {
|
|
4993
|
+
const anchors = unprovisionedAnchors.get(objectName);
|
|
4457
4994
|
const fieldRef = (name, prop) => {
|
|
4458
4995
|
if (!name) return;
|
|
4459
4996
|
if (name.includes(".")) return;
|
|
4460
|
-
if (known.has(name) || SYSTEM_FIELDS.has(name))
|
|
4997
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) {
|
|
4998
|
+
if (anchors?.has(name)) {
|
|
4999
|
+
push2(
|
|
5000
|
+
"warning",
|
|
5001
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
5002
|
+
`aggregate.${prop} "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 the aggregate query reads a column that is empty on every row, so the chart ${prop === "groupBy" ? "groups everything into one empty bucket" : "aggregates nothing"} instead of failing.`,
|
|
5003
|
+
unprovisionedAnchorHint(objectName, name)
|
|
5004
|
+
);
|
|
5005
|
+
}
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
4461
5008
|
push2(
|
|
4462
5009
|
"error",
|
|
4463
5010
|
REACT_CHART_FIELD_UNKNOWN,
|
|
@@ -4483,18 +5030,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4483
5030
|
);
|
|
4484
5031
|
};
|
|
4485
5032
|
const xAxisRaw = values.get("xAxis");
|
|
4486
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5033
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4487
5034
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4488
5035
|
axisRef(categoryAxis, categoryProp);
|
|
4489
5036
|
const yAxisRaw = values.get("yAxis");
|
|
4490
5037
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4491
5038
|
for (const a of yAxisList) {
|
|
4492
|
-
axisRef(strOf(a) ?? (
|
|
5039
|
+
axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4493
5040
|
}
|
|
4494
5041
|
const series = values.get("series");
|
|
4495
5042
|
if (Array.isArray(series)) {
|
|
4496
5043
|
for (const s of series) {
|
|
4497
|
-
if (!
|
|
5044
|
+
if (!isRec18(s)) continue;
|
|
4498
5045
|
const dataKey = strOf(s.dataKey);
|
|
4499
5046
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4500
5047
|
}
|
|
@@ -4550,7 +5097,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4550
5097
|
if (!Array.isArray(value)) return { child, parent };
|
|
4551
5098
|
for (let i = 0; i < value.length; i++) {
|
|
4552
5099
|
const sub = value[i];
|
|
4553
|
-
if (!
|
|
5100
|
+
if (!isRec18(sub)) continue;
|
|
4554
5101
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4555
5102
|
child.push({
|
|
4556
5103
|
objectName: strOf(sub.childObject),
|
|
@@ -4575,7 +5122,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
4575
5122
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
4576
5123
|
return;
|
|
4577
5124
|
}
|
|
4578
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5125
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
4579
5126
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
4580
5127
|
}
|
|
4581
5128
|
}
|
|
@@ -4595,20 +5142,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4595
5142
|
}
|
|
4596
5143
|
for (const key of spec.nestedFields ?? []) {
|
|
4597
5144
|
const v = readable(key);
|
|
4598
|
-
if (
|
|
5145
|
+
if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4599
5146
|
}
|
|
4600
5147
|
for (const key of spec.sections ?? []) {
|
|
4601
5148
|
const v = readable(key);
|
|
4602
5149
|
if (!Array.isArray(v)) continue;
|
|
4603
5150
|
for (let i = 0; i < v.length; i++) {
|
|
4604
5151
|
const section = v[i];
|
|
4605
|
-
if (!
|
|
5152
|
+
if (!isRec18(section)) continue;
|
|
4606
5153
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4607
5154
|
}
|
|
4608
5155
|
}
|
|
4609
5156
|
for (const key of spec.keyedByField ?? []) {
|
|
4610
5157
|
const v = readable(key);
|
|
4611
|
-
if (!
|
|
5158
|
+
if (!isRec18(v)) continue;
|
|
4612
5159
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4613
5160
|
}
|
|
4614
5161
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4616,22 +5163,30 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4616
5163
|
}
|
|
4617
5164
|
return { own, queried };
|
|
4618
5165
|
}
|
|
4619
|
-
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
5166
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
|
|
4620
5167
|
const objectName = strOf(values.get("objectName"));
|
|
4621
5168
|
const out = [];
|
|
4622
5169
|
const spec = REACT_FIELD_SPECS[tag];
|
|
4623
5170
|
if (spec) {
|
|
4624
5171
|
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
4625
|
-
out.push(
|
|
4626
|
-
|
|
5172
|
+
out.push(
|
|
5173
|
+
...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5174
|
+
);
|
|
5175
|
+
out.push(
|
|
5176
|
+
...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
|
|
5177
|
+
);
|
|
4627
5178
|
}
|
|
4628
5179
|
if (tag === "ObjectForm") {
|
|
4629
5180
|
const raw = values.get("subforms");
|
|
4630
5181
|
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
4631
5182
|
for (const sub of subs.child) {
|
|
4632
|
-
out.push(
|
|
5183
|
+
out.push(
|
|
5184
|
+
...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5185
|
+
);
|
|
4633
5186
|
}
|
|
4634
|
-
out.push(
|
|
5187
|
+
out.push(
|
|
5188
|
+
...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5189
|
+
);
|
|
4635
5190
|
}
|
|
4636
5191
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
4637
5192
|
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
@@ -4640,7 +5195,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
4640
5195
|
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
4641
5196
|
objectName,
|
|
4642
5197
|
objectFields,
|
|
4643
|
-
where
|
|
5198
|
+
where,
|
|
5199
|
+
"skipped",
|
|
5200
|
+
unprovisionedAnchors
|
|
4644
5201
|
)
|
|
4645
5202
|
);
|
|
4646
5203
|
}
|
|
@@ -4672,8 +5229,9 @@ function localComponentNames(tsc, sf) {
|
|
|
4672
5229
|
function validateReactPageProps(stack) {
|
|
4673
5230
|
const findings = [];
|
|
4674
5231
|
const objectFields = indexObjectFields(stack);
|
|
5232
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
4675
5233
|
const searchTargets = indexObjectSearchTargets(stack);
|
|
4676
|
-
const pages =
|
|
5234
|
+
const pages = asArray24(stack.pages);
|
|
4677
5235
|
for (let p = 0; p < pages.length; p++) {
|
|
4678
5236
|
const page = pages[p];
|
|
4679
5237
|
if (!page || page.kind !== "react") continue;
|
|
@@ -4754,7 +5312,7 @@ function validateReactPageProps(stack) {
|
|
|
4754
5312
|
}
|
|
4755
5313
|
}
|
|
4756
5314
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
4757
|
-
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
5315
|
+
checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
|
|
4758
5316
|
}
|
|
4759
5317
|
if (tag === "ListView" && !hasSpread) {
|
|
4760
5318
|
findings.push(
|
|
@@ -4770,7 +5328,7 @@ function validateReactPageProps(stack) {
|
|
|
4770
5328
|
}
|
|
4771
5329
|
if (!hasSpread) {
|
|
4772
5330
|
findings.push(
|
|
4773
|
-
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
5331
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
|
|
4774
5332
|
);
|
|
4775
5333
|
}
|
|
4776
5334
|
}
|
|
@@ -4797,6 +5355,15 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
4797
5355
|
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
4798
5356
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
4799
5357
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5358
|
+
// [#7912] The THIRD question about a nav entry, after "does the target
|
|
5359
|
+
// resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
|
|
5360
|
+
// destination serve at all? An object's own `enable` block can make its list
|
|
5361
|
+
// answer 404/405 for every persona, and no gate authorable on the entry
|
|
5362
|
+
// expresses that — which is how #7544's dead row survived review for a year.
|
|
5363
|
+
// The server now prunes such an entry from the `/meta` payload; the
|
|
5364
|
+
// maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
|
|
5365
|
+
// prune is never silent to the author who wrote the row.
|
|
5366
|
+
{ name: "validateNavObjectServability", run: validateNavObjectServability },
|
|
4800
5367
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
4801
5368
|
// The same family from the other end (#5417). Its sibling above asks "does
|
|
4802
5369
|
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
@@ -4892,16 +5459,16 @@ function validateReferenceIntegrity(stack) {
|
|
|
4892
5459
|
|
|
4893
5460
|
// src/validate-component-props.ts
|
|
4894
5461
|
var import_ui3 = require("@objectstack/spec/ui");
|
|
4895
|
-
var
|
|
5462
|
+
var import_spec2 = require("@objectstack/spec");
|
|
4896
5463
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
4897
5464
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
4898
|
-
function
|
|
5465
|
+
function isRec19(v) {
|
|
4899
5466
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4900
5467
|
}
|
|
4901
|
-
function
|
|
5468
|
+
function strName18(v) {
|
|
4902
5469
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4903
5470
|
}
|
|
4904
|
-
function
|
|
5471
|
+
function asArray25(v) {
|
|
4905
5472
|
if (Array.isArray(v)) return v;
|
|
4906
5473
|
if (v && typeof v === "object") {
|
|
4907
5474
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4912,27 +5479,40 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
|
4912
5479
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
4913
5480
|
function suppliedByDataSource(issue, component) {
|
|
4914
5481
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
4915
|
-
const dataSource =
|
|
4916
|
-
return
|
|
5482
|
+
const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
|
|
5483
|
+
return strName18(dataSource?.object) !== void 0;
|
|
5484
|
+
}
|
|
5485
|
+
function unrecognizedKeysFromUnionArm(issue) {
|
|
5486
|
+
if (issue.code !== "invalid_union") return void 0;
|
|
5487
|
+
const arms = issue.errors;
|
|
5488
|
+
if (!arms || arms.length === 0) return void 0;
|
|
5489
|
+
let found;
|
|
5490
|
+
for (const arm of arms) {
|
|
5491
|
+
const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
|
|
5492
|
+
if (keyIssues.length === 0) continue;
|
|
5493
|
+
if (keyIssues.length !== arm.length || found) return void 0;
|
|
5494
|
+
found = keyIssues[0];
|
|
5495
|
+
}
|
|
5496
|
+
return found;
|
|
4917
5497
|
}
|
|
4918
5498
|
function validateComponentProps(stack) {
|
|
4919
5499
|
const findings = [];
|
|
4920
|
-
if (!
|
|
4921
|
-
const pages =
|
|
5500
|
+
if (!isRec19(stack)) return findings;
|
|
5501
|
+
const pages = asArray25(stack.pages);
|
|
4922
5502
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4923
5503
|
const page = pages[pi];
|
|
4924
|
-
if (!
|
|
4925
|
-
const pageName =
|
|
5504
|
+
if (!isRec19(page)) continue;
|
|
5505
|
+
const pageName = strName18(page.name) ?? `#${pi}`;
|
|
4926
5506
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4927
|
-
const type =
|
|
5507
|
+
const type = strName18(component.type);
|
|
4928
5508
|
if (!type) continue;
|
|
4929
5509
|
const schema = PROPS_SCHEMAS[type];
|
|
4930
5510
|
if (!schema) continue;
|
|
4931
|
-
const props =
|
|
5511
|
+
const props = isRec19(component.properties) ? component.properties : void 0;
|
|
4932
5512
|
if (!props) continue;
|
|
4933
5513
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
4934
5514
|
const base = `${path}.properties`;
|
|
4935
|
-
for (const f of (0,
|
|
5515
|
+
for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
|
|
4936
5516
|
findings.push({
|
|
4937
5517
|
severity: "warning",
|
|
4938
5518
|
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
@@ -4947,6 +5527,20 @@ function validateComponentProps(stack) {
|
|
|
4947
5527
|
for (const issue of parsed.error?.issues ?? []) {
|
|
4948
5528
|
if (suppliedByDataSource(issue, component)) continue;
|
|
4949
5529
|
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5530
|
+
const armIssue = unrecognizedKeysFromUnionArm(issue);
|
|
5531
|
+
if (armIssue) {
|
|
5532
|
+
for (const key of armIssue.keys ?? []) {
|
|
5533
|
+
findings.push({
|
|
5534
|
+
severity: "warning",
|
|
5535
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5536
|
+
where,
|
|
5537
|
+
path: `${at}.${key}`,
|
|
5538
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
|
|
5539
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
4950
5544
|
if (issue.code === "unrecognized_keys") {
|
|
4951
5545
|
for (const key of issue.keys ?? []) {
|
|
4952
5546
|
findings.push({
|
|
@@ -5177,7 +5771,7 @@ function looksLikeTailwind(className) {
|
|
|
5177
5771
|
return false;
|
|
5178
5772
|
});
|
|
5179
5773
|
}
|
|
5180
|
-
function
|
|
5774
|
+
function asArray26(v) {
|
|
5181
5775
|
if (Array.isArray(v)) return v;
|
|
5182
5776
|
if (v && typeof v === "object") {
|
|
5183
5777
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5270,13 +5864,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
5270
5864
|
}
|
|
5271
5865
|
function validateResponsiveStyles(stack) {
|
|
5272
5866
|
const findings = [];
|
|
5273
|
-
const pages =
|
|
5867
|
+
const pages = asArray26(stack.pages);
|
|
5274
5868
|
for (let p = 0; p < pages.length; p++) {
|
|
5275
5869
|
const page = pages[p];
|
|
5276
5870
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
5277
|
-
const regions =
|
|
5871
|
+
const regions = asArray26(page.regions);
|
|
5278
5872
|
for (let r = 0; r < regions.length; r++) {
|
|
5279
|
-
const components =
|
|
5873
|
+
const components = asArray26(regions[r].components);
|
|
5280
5874
|
for (let c = 0; c < components.length; c++) {
|
|
5281
5875
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
5282
5876
|
}
|
|
@@ -5287,10 +5881,10 @@ function validateResponsiveStyles(stack) {
|
|
|
5287
5881
|
|
|
5288
5882
|
// src/validate-jsx-pages.ts
|
|
5289
5883
|
var import_sdui_parser = require("@objectstack/sdui-parser");
|
|
5290
|
-
var
|
|
5884
|
+
var asArray27 = (v) => Array.isArray(v) ? v : [];
|
|
5291
5885
|
function validateJsxPages(stack, opts = {}) {
|
|
5292
5886
|
const findings = [];
|
|
5293
|
-
const pages =
|
|
5887
|
+
const pages = asArray27(stack.pages);
|
|
5294
5888
|
for (let p = 0; p < pages.length; p++) {
|
|
5295
5889
|
const page = pages[p];
|
|
5296
5890
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -5338,10 +5932,10 @@ function loadSucraseTransform() {
|
|
|
5338
5932
|
}
|
|
5339
5933
|
return cachedTransform;
|
|
5340
5934
|
}
|
|
5341
|
-
var
|
|
5935
|
+
var asArray28 = (v) => Array.isArray(v) ? v : [];
|
|
5342
5936
|
function validateReactPages(stack) {
|
|
5343
5937
|
const findings = [];
|
|
5344
|
-
const pages =
|
|
5938
|
+
const pages = asArray28(stack.pages);
|
|
5345
5939
|
for (let p = 0; p < pages.length; p++) {
|
|
5346
5940
|
const page = pages[p];
|
|
5347
5941
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5378,11 +5972,11 @@ function validateReactPages(stack) {
|
|
|
5378
5972
|
|
|
5379
5973
|
// src/validate-page-source-styling.ts
|
|
5380
5974
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
5381
|
-
var
|
|
5975
|
+
var asArray29 = (v) => Array.isArray(v) ? v : [];
|
|
5382
5976
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
5383
5977
|
function validatePageSourceStyling(stack) {
|
|
5384
5978
|
const findings = [];
|
|
5385
|
-
const pages =
|
|
5979
|
+
const pages = asArray29(stack.pages);
|
|
5386
5980
|
for (let p = 0; p < pages.length; p++) {
|
|
5387
5981
|
const page = pages[p];
|
|
5388
5982
|
if (!page) continue;
|
|
@@ -5410,7 +6004,7 @@ function validatePageSourceStyling(stack) {
|
|
|
5410
6004
|
// src/validate-capability-references.ts
|
|
5411
6005
|
var import_security = require("@objectstack/spec/security");
|
|
5412
6006
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
5413
|
-
function
|
|
6007
|
+
function asArray30(v) {
|
|
5414
6008
|
if (Array.isArray(v)) return v;
|
|
5415
6009
|
if (v && typeof v === "object") {
|
|
5416
6010
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5435,13 +6029,13 @@ function validateCapabilityReferences(stack) {
|
|
|
5435
6029
|
const findings = [];
|
|
5436
6030
|
if (!stack || typeof stack !== "object") return findings;
|
|
5437
6031
|
const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
|
|
5438
|
-
for (const cap of
|
|
6032
|
+
for (const cap of asArray30(stack.capabilities)) {
|
|
5439
6033
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
5440
6034
|
}
|
|
5441
|
-
for (const ps of
|
|
6035
|
+
for (const ps of asArray30(stack.permissions)) {
|
|
5442
6036
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
5443
6037
|
}
|
|
5444
|
-
for (const seed of
|
|
6038
|
+
for (const seed of asArray30(stack.data)) {
|
|
5445
6039
|
if (seed.object !== "sys_capability") continue;
|
|
5446
6040
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
5447
6041
|
const name = rec?.name;
|
|
@@ -5460,7 +6054,7 @@ function validateCapabilityReferences(stack) {
|
|
|
5460
6054
|
hint
|
|
5461
6055
|
});
|
|
5462
6056
|
};
|
|
5463
|
-
const objects =
|
|
6057
|
+
const objects = asArray30(stack.objects);
|
|
5464
6058
|
for (let i = 0; i < objects.length; i++) {
|
|
5465
6059
|
const obj = objects[i];
|
|
5466
6060
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5469,27 +6063,27 @@ function validateCapabilityReferences(stack) {
|
|
|
5469
6063
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
5470
6064
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
5471
6065
|
}
|
|
5472
|
-
const fields =
|
|
6066
|
+
const fields = asArray30(obj.fields);
|
|
5473
6067
|
for (const f of fields) {
|
|
5474
6068
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
5475
6069
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
5476
6070
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
5477
6071
|
}
|
|
5478
6072
|
}
|
|
5479
|
-
for (const [ai, action] of
|
|
6073
|
+
for (const [ai, action] of asArray30(obj.actions).entries()) {
|
|
5480
6074
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
5481
6075
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5482
6076
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
5483
6077
|
}
|
|
5484
6078
|
}
|
|
5485
6079
|
}
|
|
5486
|
-
for (const [i, action] of
|
|
6080
|
+
for (const [i, action] of asArray30(stack.actions).entries()) {
|
|
5487
6081
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
5488
6082
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5489
6083
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
5490
6084
|
}
|
|
5491
6085
|
}
|
|
5492
|
-
const apps =
|
|
6086
|
+
const apps = asArray30(stack.apps);
|
|
5493
6087
|
for (let i = 0; i < apps.length; i++) {
|
|
5494
6088
|
const app = apps[i];
|
|
5495
6089
|
if (!app || typeof app !== "object") continue;
|
|
@@ -5522,8 +6116,9 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
5522
6116
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
5523
6117
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
5524
6118
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
6119
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5525
6120
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5526
|
-
function
|
|
6121
|
+
function asArray31(v) {
|
|
5527
6122
|
if (Array.isArray(v)) return v;
|
|
5528
6123
|
if (v && typeof v === "object") {
|
|
5529
6124
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -5539,6 +6134,11 @@ function renderNonObject(v) {
|
|
|
5539
6134
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
5540
6135
|
return `a ${t}`;
|
|
5541
6136
|
}
|
|
6137
|
+
function renderTriggerToken(v) {
|
|
6138
|
+
if (typeof v === "string") return `'${v}'`;
|
|
6139
|
+
const json = JSON.stringify(v);
|
|
6140
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
6141
|
+
}
|
|
5542
6142
|
function startNodeOf(flow) {
|
|
5543
6143
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
5544
6144
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -5546,10 +6146,10 @@ function startNodeOf(flow) {
|
|
|
5546
6146
|
}
|
|
5547
6147
|
function validateFlowTriggerReadiness(stack) {
|
|
5548
6148
|
const findings = [];
|
|
5549
|
-
const flows =
|
|
6149
|
+
const flows = asArray31(stack.flows);
|
|
5550
6150
|
if (flows.length === 0) return findings;
|
|
5551
6151
|
const objectNames = new Set(
|
|
5552
|
-
|
|
6152
|
+
asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
|
|
5553
6153
|
);
|
|
5554
6154
|
flows.forEach((flow, flowIndex) => {
|
|
5555
6155
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
@@ -5658,6 +6258,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5658
6258
|
hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
|
|
5659
6259
|
});
|
|
5660
6260
|
}
|
|
6261
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
6262
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
6263
|
+
const hasTriggerType = config.triggerType != null;
|
|
6264
|
+
findings.push({
|
|
6265
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
6266
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
6267
|
+
// with no registry lookup in them — so no installed package can make
|
|
6268
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
6269
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
6270
|
+
// record-change trigger itself, and it still would not help, because
|
|
6271
|
+
// the flow never reaches the point of asking for one.
|
|
6272
|
+
severity: "error",
|
|
6273
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
6274
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
6275
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
6276
|
+
message: `declares type: 'record_change' but ` + (hasTriggerType ? `its start node's triggerType is ${renderTriggerToken(config.triggerType)}, which the engine routes to NO trigger` : `its start node has no triggerType at all, so there is nothing for the engine to route`) + ` \u2014 it binds a record-change flow only for a token starting with 'record-', so this flow is demoted to a manual one and never fires. Nothing NAMES it: the unbound-flow audit resolves the same binding and skips the flow as "manual \u2014 nothing to bind", so neither the boot warning nor the startup summary lists it; the only trace is the banner's flow count being one higher than its bound count.`,
|
|
6277
|
+
hint: `Use record-{before,after}-{create,update,delete,write} ('write' is create OR update in one flow, #3427; create/insert are synonyms). If the flow really is launched by hand or from a screen, declare type: 'autolaunched' or 'screen' instead of 'record_change' \u2014 those types have no trigger to be missing.`
|
|
6278
|
+
});
|
|
6279
|
+
}
|
|
5661
6280
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
5662
6281
|
findings.push({
|
|
5663
6282
|
severity: "warning",
|
|
@@ -5674,7 +6293,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5674
6293
|
|
|
5675
6294
|
// src/validate-approval-approvers.ts
|
|
5676
6295
|
var import_automation4 = require("@objectstack/spec/automation");
|
|
5677
|
-
var
|
|
6296
|
+
var import_spec3 = require("@objectstack/spec");
|
|
5678
6297
|
var import_formula3 = require("@objectstack/formula");
|
|
5679
6298
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
5680
6299
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
@@ -5689,13 +6308,13 @@ var APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED = "approval-approver-cross-org-unsup
|
|
|
5689
6308
|
var EXPRESSION_ROOTS = /* @__PURE__ */ new Set(["current", "trigger", "vars"]);
|
|
5690
6309
|
var RESERVED_OUTPUT_KEYS = /* @__PURE__ */ new Set(["decision", "requestId"]);
|
|
5691
6310
|
var GROUP_ROUTED_TYPES = /* @__PURE__ */ new Set(["position", "team", "department"]);
|
|
5692
|
-
var MEMBERSHIP_TIERS = new Set(
|
|
5693
|
-
var MEMBERSHIP_TIER_LIST =
|
|
6311
|
+
var MEMBERSHIP_TIERS = new Set(import_spec3.BUILTIN_MEMBERSHIP_ROLES);
|
|
6312
|
+
var MEMBERSHIP_TIER_LIST = import_spec3.BUILTIN_MEMBERSHIP_ROLES.join("/");
|
|
5694
6313
|
var TYPE_FIX = {
|
|
5695
6314
|
business_unit: "department",
|
|
5696
6315
|
bu: "department"
|
|
5697
6316
|
};
|
|
5698
|
-
function
|
|
6317
|
+
function asArray32(v) {
|
|
5699
6318
|
if (Array.isArray(v)) return v;
|
|
5700
6319
|
if (v && typeof v === "object") {
|
|
5701
6320
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5705,7 +6324,7 @@ function asArray31(v) {
|
|
|
5705
6324
|
function validateApprovalApprovers(stack) {
|
|
5706
6325
|
const findings = [];
|
|
5707
6326
|
if (!stack || typeof stack !== "object") return findings;
|
|
5708
|
-
const flows =
|
|
6327
|
+
const flows = asArray32(stack.flows);
|
|
5709
6328
|
const validTypes = new Set(import_automation4.ApproverType.options);
|
|
5710
6329
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
5711
6330
|
const flow = flows[fi];
|
|
@@ -5885,10 +6504,10 @@ function validateApprovalApprovers(stack) {
|
|
|
5885
6504
|
}
|
|
5886
6505
|
|
|
5887
6506
|
// src/validate-record-title.ts
|
|
5888
|
-
var
|
|
6507
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
5889
6508
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
5890
6509
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
5891
|
-
function
|
|
6510
|
+
function asArray33(v) {
|
|
5892
6511
|
if (Array.isArray(v)) return v;
|
|
5893
6512
|
if (v && typeof v === "object") {
|
|
5894
6513
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5897,7 +6516,7 @@ function asArray32(v) {
|
|
|
5897
6516
|
}
|
|
5898
6517
|
function validateRecordTitle(stack) {
|
|
5899
6518
|
const findings = [];
|
|
5900
|
-
const objects =
|
|
6519
|
+
const objects = asArray33(stack.objects);
|
|
5901
6520
|
for (let i = 0; i < objects.length; i++) {
|
|
5902
6521
|
const obj = objects[i];
|
|
5903
6522
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -5913,7 +6532,7 @@ function validateRecordTitle(stack) {
|
|
|
5913
6532
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
5914
6533
|
});
|
|
5915
6534
|
}
|
|
5916
|
-
const completeness = (0,
|
|
6535
|
+
const completeness = (0, import_data9.objectTitleCompleteness)(obj);
|
|
5917
6536
|
if (completeness.status === "none") {
|
|
5918
6537
|
findings.push({
|
|
5919
6538
|
severity: "warning",
|
|
@@ -5933,7 +6552,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
5933
6552
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
5934
6553
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
5935
6554
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
5936
|
-
|
|
6555
|
+
var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
|
|
6556
|
+
function asArray34(v) {
|
|
5937
6557
|
if (Array.isArray(v)) return v;
|
|
5938
6558
|
if (v && typeof v === "object") {
|
|
5939
6559
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5942,7 +6562,7 @@ function asArray33(v) {
|
|
|
5942
6562
|
}
|
|
5943
6563
|
function validateSemanticRoles(stack) {
|
|
5944
6564
|
const findings = [];
|
|
5945
|
-
const objects =
|
|
6565
|
+
const objects = asArray34(stack.objects);
|
|
5946
6566
|
for (let i = 0; i < objects.length; i++) {
|
|
5947
6567
|
const obj = objects[i];
|
|
5948
6568
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5951,6 +6571,15 @@ function validateSemanticRoles(stack) {
|
|
|
5951
6571
|
const path = `objects[${i}]`;
|
|
5952
6572
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
5953
6573
|
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
6574
|
+
const unprovisioned = unprovisionedInjectedColumnsFor(obj);
|
|
6575
|
+
const unprovisionedPointer = (slot, entry) => ({
|
|
6576
|
+
severity: "warning",
|
|
6577
|
+
rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
6578
|
+
where,
|
|
6579
|
+
path: `${path}.${slot}`,
|
|
6580
|
+
message: `${objName}: ${slot} points at "${entry}", an injected system column with no storage behind it \u2014 this object is external (ADR-0015), so the platform registers the anchor but the remote schema owns the table and no column backs it. Every consumer renders it empty on every record.`,
|
|
6581
|
+
hint: `If the remote table really carries "${entry}", declare it in the object's own fields (mapped through the external binding's columnMap); otherwise point ${slot} at a real remote column.`
|
|
6582
|
+
});
|
|
5954
6583
|
const declaredGroups = new Set(
|
|
5955
6584
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
5956
6585
|
);
|
|
@@ -5992,10 +6621,16 @@ function validateSemanticRoles(stack) {
|
|
|
5992
6621
|
message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
|
|
5993
6622
|
hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
|
|
5994
6623
|
});
|
|
6624
|
+
} else if (typeof stage === "string" && unprovisioned.has(stage)) {
|
|
6625
|
+
findings.push(unprovisionedPointer("stageField", stage));
|
|
5995
6626
|
}
|
|
5996
6627
|
const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
|
|
5997
6628
|
for (const entry of highlights) {
|
|
5998
|
-
if (typeof entry !== "string" || entry.length === 0
|
|
6629
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
6630
|
+
if (fieldNames.has(entry)) {
|
|
6631
|
+
if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
5999
6634
|
findings.push({
|
|
6000
6635
|
severity: "warning",
|
|
6001
6636
|
rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -6009,7 +6644,7 @@ function validateSemanticRoles(stack) {
|
|
|
6009
6644
|
(h) => typeof h === "string" && h.length > 0
|
|
6010
6645
|
);
|
|
6011
6646
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
6012
|
-
const declaredTitle = [obj.nameField, obj.
|
|
6647
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
6013
6648
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
6014
6649
|
const stripSet = new Set(
|
|
6015
6650
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -6037,13 +6672,19 @@ function validateSemanticRoles(stack) {
|
|
|
6037
6672
|
// src/validate-form-layout.ts
|
|
6038
6673
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
6039
6674
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
6040
|
-
function
|
|
6675
|
+
function asArray35(v) {
|
|
6041
6676
|
if (Array.isArray(v)) return v;
|
|
6042
6677
|
if (v && typeof v === "object") {
|
|
6043
6678
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6044
6679
|
}
|
|
6045
6680
|
return [];
|
|
6046
6681
|
}
|
|
6682
|
+
function isRec20(v) {
|
|
6683
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6684
|
+
}
|
|
6685
|
+
function strName19(v) {
|
|
6686
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6687
|
+
}
|
|
6047
6688
|
function fieldNameOf(entry) {
|
|
6048
6689
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
6049
6690
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -6052,60 +6693,53 @@ function fieldNameOf(entry) {
|
|
|
6052
6693
|
}
|
|
6053
6694
|
return null;
|
|
6054
6695
|
}
|
|
6055
|
-
function boundObject(view) {
|
|
6056
|
-
const data = view.data;
|
|
6057
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
6058
|
-
return data.object;
|
|
6059
|
-
}
|
|
6060
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
6061
|
-
}
|
|
6062
6696
|
function validateFormLayout(stack) {
|
|
6063
6697
|
const findings = [];
|
|
6064
6698
|
const objectFields = /* @__PURE__ */ new Map();
|
|
6065
|
-
for (const obj of
|
|
6699
|
+
for (const obj of asArray35(stack.objects)) {
|
|
6066
6700
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6067
6701
|
if (!name) continue;
|
|
6068
6702
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6069
6703
|
objectFields.set(name, new Set(fields));
|
|
6070
6704
|
}
|
|
6071
|
-
const
|
|
6072
|
-
|
|
6073
|
-
const
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
}
|
|
6705
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6706
|
+
const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
|
|
6707
|
+
const containerObject = viewObjectName(view);
|
|
6708
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6709
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
6710
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
6711
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6712
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6713
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6714
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6715
|
+
const sec = sections[s];
|
|
6716
|
+
const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6717
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6718
|
+
const entry = secFields[f];
|
|
6719
|
+
const fname = fieldNameOf(entry);
|
|
6720
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
6721
|
+
if (fname && known && !known.has(fname)) {
|
|
6722
|
+
findings.push({
|
|
6723
|
+
severity: "warning",
|
|
6724
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
6725
|
+
where,
|
|
6726
|
+
path: fpath,
|
|
6727
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
6728
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6729
|
+
});
|
|
6730
|
+
}
|
|
6731
|
+
const colSpan = isRec20(entry) ? entry.colSpan : void 0;
|
|
6732
|
+
if (colSpan != null) {
|
|
6733
|
+
findings.push({
|
|
6734
|
+
severity: "warning",
|
|
6735
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
6736
|
+
where,
|
|
6737
|
+
path: `${fpath}.colSpan`,
|
|
6738
|
+
message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
|
|
6739
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
}
|
|
6109
6743
|
}
|
|
6110
6744
|
}
|
|
6111
6745
|
}
|
|
@@ -6204,109 +6838,546 @@ function validateSeedStateMachine(stack) {
|
|
|
6204
6838
|
}
|
|
6205
6839
|
|
|
6206
6840
|
// src/validate-visibility-predicates.ts
|
|
6207
|
-
var
|
|
6841
|
+
var import_formula4 = require("@objectstack/formula");
|
|
6842
|
+
|
|
6843
|
+
// src/predicate-rhs-position.ts
|
|
6844
|
+
function isNode2(v) {
|
|
6845
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6846
|
+
}
|
|
6847
|
+
var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
|
|
6848
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6849
|
+
function bareId(node) {
|
|
6850
|
+
if (!isNode2(node)) return null;
|
|
6851
|
+
return node.op === "id" && typeof node.args === "string" ? node.args : null;
|
|
6852
|
+
}
|
|
6853
|
+
function bareRhsOnlyIdentifiers(ast) {
|
|
6854
|
+
const rhs = /* @__PURE__ */ new Set();
|
|
6855
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
6856
|
+
const walk = (node, suppressible) => {
|
|
6857
|
+
if (Array.isArray(node)) {
|
|
6858
|
+
for (const child of node) walk(child, suppressible);
|
|
6859
|
+
return;
|
|
6860
|
+
}
|
|
6861
|
+
if (!isNode2(node)) return;
|
|
6862
|
+
const args = node.args;
|
|
6863
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6864
|
+
walk(args[1], suppressible);
|
|
6865
|
+
walk(args[2], false);
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
6869
|
+
const right = suppressible ? bareId(args[1]) : null;
|
|
6870
|
+
walk(args[0], suppressible);
|
|
6871
|
+
if (right !== null) {
|
|
6872
|
+
rhs.add(right);
|
|
6873
|
+
return;
|
|
6874
|
+
}
|
|
6875
|
+
walk(args[1], suppressible);
|
|
6876
|
+
return;
|
|
6877
|
+
}
|
|
6878
|
+
const name = bareId(node);
|
|
6879
|
+
if (name !== null) {
|
|
6880
|
+
elsewhere.add(name);
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6883
|
+
walk(args, suppressible);
|
|
6884
|
+
};
|
|
6885
|
+
walk(ast, true);
|
|
6886
|
+
for (const name of elsewhere) rhs.delete(name);
|
|
6887
|
+
return rhs;
|
|
6888
|
+
}
|
|
6889
|
+
function isRec21(v) {
|
|
6890
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6891
|
+
}
|
|
6892
|
+
function schemaIdOf(view) {
|
|
6893
|
+
const data = view.data;
|
|
6894
|
+
if (!isRec21(data)) return void 0;
|
|
6895
|
+
if (data.provider !== "schema") return void 0;
|
|
6896
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
// src/validate-visibility-predicates.ts
|
|
6208
6900
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6901
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6902
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
6903
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
6209
6904
|
var CANONICAL = "visibleWhen";
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
if (
|
|
6213
|
-
|
|
6214
|
-
|
|
6905
|
+
function predicateSource(v) {
|
|
6906
|
+
if (typeof v === "string") return v;
|
|
6907
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
6908
|
+
return v.source;
|
|
6909
|
+
}
|
|
6910
|
+
return void 0;
|
|
6911
|
+
}
|
|
6912
|
+
function usesRoot(source, root) {
|
|
6913
|
+
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
6914
|
+
}
|
|
6915
|
+
function withoutStringLiterals(source) {
|
|
6916
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
6917
|
+
}
|
|
6918
|
+
var NON_CEL_SPELLINGS = [
|
|
6919
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
6920
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
6921
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
6922
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
6923
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
6924
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
6925
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
6926
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
6927
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
6928
|
+
];
|
|
6929
|
+
function quoteSource(source) {
|
|
6930
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
6931
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
6932
|
+
}
|
|
6933
|
+
function celRefusal(source) {
|
|
6934
|
+
if (!source.trim()) return null;
|
|
6935
|
+
const parsed = (0, import_formula4.parseCelToAstWithReason)(source);
|
|
6936
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
6937
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
6938
|
+
const identifiers = (0, import_formula4.collectCelRootIdentifiers)(source);
|
|
6939
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
6940
|
+
const scannable = withoutStringLiterals(source);
|
|
6941
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
6942
|
+
}
|
|
6943
|
+
function boundName(overrun) {
|
|
6944
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6945
|
+
}
|
|
6946
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6947
|
+
function isNode3(v) {
|
|
6948
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6949
|
+
}
|
|
6950
|
+
function namespaceRoots(node, out) {
|
|
6951
|
+
if (Array.isArray(node)) {
|
|
6952
|
+
for (const child of node) namespaceRoots(child, out);
|
|
6953
|
+
return;
|
|
6954
|
+
}
|
|
6955
|
+
if (!isNode3(node)) return;
|
|
6956
|
+
const args = node.args;
|
|
6957
|
+
if (Array.isArray(args)) {
|
|
6958
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6959
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6960
|
+
out.add(receiver.args);
|
|
6961
|
+
}
|
|
6962
|
+
}
|
|
6963
|
+
namespaceRoots(args, out);
|
|
6964
|
+
}
|
|
6965
|
+
function firstBareIdentifier(source, literalRhs) {
|
|
6966
|
+
const ast = (0, import_formula4.parseCelToAst)(source);
|
|
6967
|
+
if (!ast) return null;
|
|
6968
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
6969
|
+
namespaceRoots(ast, rooted);
|
|
6970
|
+
const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
|
|
6971
|
+
return (0, import_formula4.firstUndeclaredReference)(source, [
|
|
6972
|
+
...VIEW_PAGE_EXTRA_ROOTS,
|
|
6973
|
+
...rooted,
|
|
6974
|
+
...literalSlot
|
|
6975
|
+
]);
|
|
6976
|
+
}
|
|
6977
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
6978
|
+
runtime: "record",
|
|
6979
|
+
metadata: "data"
|
|
6980
|
+
};
|
|
6981
|
+
var MISLAYER_BY_LAYER = {
|
|
6982
|
+
runtime: {
|
|
6983
|
+
forbiddenRoot: "data",
|
|
6984
|
+
message: "visibility predicate is rooted at `data.` \u2014 that is the root a metadata-editing form binds (the row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
|
|
6985
|
+
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
6986
|
+
},
|
|
6987
|
+
metadata: {
|
|
6988
|
+
forbiddenRoot: "record",
|
|
6989
|
+
message: "visibility predicate is rooted at `record.` \u2014 that is the root a runtime view/page surface binds (the live record), not the root a metadata-editing form binds. On a metadata-editing form \u2014 the row under edit \u2014 a `record.`-rooted predicate never matches and the element renders unconditionally (ADR-0089).",
|
|
6990
|
+
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
6991
|
+
}
|
|
6992
|
+
};
|
|
6993
|
+
function checkElement(el, where, path, layer, findings, literalRhs = false) {
|
|
6994
|
+
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6995
|
+
const source = predicateSource(raw);
|
|
6996
|
+
const rule = MISLAYER_BY_LAYER[layer];
|
|
6997
|
+
if (source && usesRoot(source, rule.forbiddenRoot)) {
|
|
6998
|
+
findings.push({
|
|
6999
|
+
severity: "warning",
|
|
7000
|
+
rule: VISIBILITY_ROOT_MISLAYERED,
|
|
7001
|
+
where,
|
|
7002
|
+
path,
|
|
7003
|
+
message: rule.message,
|
|
7004
|
+
hint: rule.hint
|
|
7005
|
+
});
|
|
7006
|
+
}
|
|
7007
|
+
const refusal = source ? celRefusal(source) : null;
|
|
7008
|
+
if (source && refusal?.kind === "bounds") {
|
|
7009
|
+
const bound = boundName(refusal.overrun);
|
|
7010
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
7011
|
+
findings.push({
|
|
7012
|
+
severity: "error",
|
|
7013
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
7014
|
+
where,
|
|
7015
|
+
path,
|
|
7016
|
+
message: `visibility predicate is syntactically valid CEL but overruns ${bound} (${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front end refuses it, so it can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
7017
|
+
hint: `There is no syntax or dialect error to correct here \u2014 this is a SIZE fault, not a dialect mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work off the predicate: (1) collapse a long \`${root}.f == 'a' || ${root}.f == 'b' || \u2026\` chain into a single \`${root}.f in ['a', 'b', \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2); (2) precompute the heavy part into a formula/rollup field on the object and test that one field instead. Logic genuinely this large is not element visibility \u2014 compute it once on the record rather than re-deriving it in every predicate that needs it.`
|
|
7018
|
+
});
|
|
7019
|
+
}
|
|
7020
|
+
if (source && refusal?.kind === "syntax") {
|
|
7021
|
+
findings.push({
|
|
7022
|
+
severity: "error",
|
|
7023
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
7024
|
+
where,
|
|
7025
|
+
path,
|
|
7026
|
+
message: `visibility predicate is not valid CEL \u2014 ${refusal.detail} (predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
7027
|
+
hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator \u2014 CEL spells it \`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with \`${refusal.token.cel}\`, e.g. \`${refusal.token.example}\`.` : `Visibility predicates are bare CEL, e.g. \`record.status == 'open'\`. Spellings from other languages do not parse: write \`==\` (not \`===\`), \`!=\` (not \`!==\` or \`<>\`), \`&&\` (not \`and\`), \`||\` (not \`or\`), \`!\` (not \`not\`).`
|
|
7028
|
+
});
|
|
7029
|
+
}
|
|
7030
|
+
if (source && !refusal) {
|
|
7031
|
+
const bare = firstBareIdentifier(source, literalRhs);
|
|
7032
|
+
if (bare) {
|
|
7033
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
7034
|
+
findings.push({
|
|
7035
|
+
severity: "error",
|
|
7036
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
7037
|
+
where,
|
|
7038
|
+
path,
|
|
7039
|
+
message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
7040
|
+
hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a metadata-editing form binds the row under edit as `data`).")
|
|
7041
|
+
});
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
}
|
|
7045
|
+
function isFieldObject(entry) {
|
|
7046
|
+
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
7047
|
+
}
|
|
7048
|
+
function validateVisibilityPredicates(stack, opts = {}) {
|
|
7049
|
+
const declaredLayer = opts.layer ?? "runtime";
|
|
7050
|
+
const findings = [];
|
|
7051
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
7052
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
7053
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
7054
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
7055
|
+
const schemaBound = schemaIdOf(site.view) !== void 0;
|
|
7056
|
+
const literalRhs = schemaBound;
|
|
7057
|
+
const layer = schemaBound ? "metadata" : declaredLayer;
|
|
7058
|
+
for (const bucket of ["sections", "groups"]) {
|
|
7059
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7060
|
+
for (let s = 0; s < sections.length; s++) {
|
|
7061
|
+
const sec = sections[s];
|
|
7062
|
+
if (!sec || typeof sec !== "object") continue;
|
|
7063
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
7064
|
+
checkElement(sec, where, secPath, layer, findings, literalRhs);
|
|
7065
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
7066
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
7067
|
+
const entry = secFields[f];
|
|
7068
|
+
if (isFieldObject(entry)) {
|
|
7069
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
|
|
7070
|
+
}
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
}
|
|
7074
|
+
}
|
|
7075
|
+
}
|
|
7076
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
7077
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
7078
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
7079
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
7080
|
+
checkElement(walked.component, where, walked.path, declaredLayer, findings);
|
|
7081
|
+
}
|
|
7082
|
+
}
|
|
7083
|
+
return findings;
|
|
7084
|
+
}
|
|
7085
|
+
|
|
7086
|
+
// src/validate-predicate-path-refs.ts
|
|
7087
|
+
var import_formula5 = require("@objectstack/formula");
|
|
7088
|
+
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
7089
|
+
var import_spec4 = require("@objectstack/spec");
|
|
7090
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
7091
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
7092
|
+
var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
|
|
7093
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
7094
|
+
var ROOT = "data";
|
|
7095
|
+
function defOf(schema) {
|
|
7096
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
7097
|
+
const s = schema;
|
|
7098
|
+
return s.def ?? s._def;
|
|
7099
|
+
}
|
|
7100
|
+
function peel(schema, depth = 0) {
|
|
7101
|
+
if (!schema || depth > 25) return schema;
|
|
7102
|
+
const d = defOf(schema);
|
|
7103
|
+
if (!d) return schema;
|
|
7104
|
+
switch (d.type) {
|
|
7105
|
+
case "optional":
|
|
7106
|
+
case "nullable":
|
|
7107
|
+
case "default":
|
|
7108
|
+
case "prefault":
|
|
7109
|
+
case "readonly":
|
|
7110
|
+
case "catch":
|
|
7111
|
+
case "nonoptional":
|
|
7112
|
+
return peel(d.innerType, depth + 1);
|
|
7113
|
+
case "lazy":
|
|
7114
|
+
return peel(d.getter(), depth + 1);
|
|
7115
|
+
case "pipe": {
|
|
7116
|
+
const inner = peel(d.in, depth + 1);
|
|
7117
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
7118
|
+
}
|
|
7119
|
+
default:
|
|
7120
|
+
return schema;
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
function optionsOf(d) {
|
|
7124
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
7125
|
+
}
|
|
7126
|
+
function keysOf(schema, depth = 0) {
|
|
7127
|
+
if (depth > 25) return null;
|
|
7128
|
+
const u = peel(schema);
|
|
7129
|
+
const d = defOf(u);
|
|
7130
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
7131
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
7132
|
+
const all = /* @__PURE__ */ new Set();
|
|
7133
|
+
let keyBearing = false;
|
|
7134
|
+
for (const option of optionsOf(d)) {
|
|
7135
|
+
const k = keysOf(option, depth + 1);
|
|
7136
|
+
if (!k) continue;
|
|
7137
|
+
keyBearing = true;
|
|
7138
|
+
for (const key of k) all.add(key);
|
|
7139
|
+
}
|
|
7140
|
+
return keyBearing ? [...all] : null;
|
|
7141
|
+
}
|
|
7142
|
+
if (d?.type === "intersection") {
|
|
7143
|
+
const left = keysOf(d.left, depth + 1);
|
|
7144
|
+
const right = keysOf(d.right, depth + 1);
|
|
7145
|
+
if (!left && !right) return null;
|
|
7146
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
7147
|
+
}
|
|
7148
|
+
return null;
|
|
7149
|
+
}
|
|
7150
|
+
function propertyOf(schema, key, depth = 0) {
|
|
7151
|
+
if (depth > 25) return void 0;
|
|
7152
|
+
const u = peel(schema);
|
|
7153
|
+
const d = defOf(u);
|
|
7154
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
7155
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
7156
|
+
for (const option of optionsOf(d)) {
|
|
7157
|
+
const found = propertyOf(option, key, depth + 1);
|
|
7158
|
+
if (found !== void 0) return found;
|
|
7159
|
+
}
|
|
7160
|
+
}
|
|
7161
|
+
if (d?.type === "intersection") {
|
|
7162
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
7163
|
+
}
|
|
7164
|
+
return void 0;
|
|
7165
|
+
}
|
|
7166
|
+
function rowScopeOf(scope, key) {
|
|
7167
|
+
const prop = propertyOf(scope, key);
|
|
7168
|
+
if (prop === void 0) return void 0;
|
|
7169
|
+
let node = peel(prop);
|
|
7170
|
+
for (let i = 0; i < 25; i++) {
|
|
7171
|
+
const d = defOf(node);
|
|
7172
|
+
if (d?.type === "array") node = peel(d.element);
|
|
7173
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
7174
|
+
else return node;
|
|
7175
|
+
}
|
|
7176
|
+
return node;
|
|
7177
|
+
}
|
|
7178
|
+
function stepInto(scope, segment) {
|
|
7179
|
+
const u = peel(scope);
|
|
7180
|
+
const d = defOf(u);
|
|
7181
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
7182
|
+
const declared = keysOf(u);
|
|
7183
|
+
if (declared === null) return { kind: "opaque" };
|
|
7184
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
7185
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
7186
|
+
}
|
|
7187
|
+
function isNode4(v) {
|
|
7188
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
7189
|
+
}
|
|
7190
|
+
function memberChain(node) {
|
|
7191
|
+
if (!isNode4(node)) return null;
|
|
7192
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
7193
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
7194
|
+
const head = memberChain(node.args[0]);
|
|
7195
|
+
return head ? [...head, node.args[1]] : null;
|
|
7196
|
+
}
|
|
7197
|
+
return null;
|
|
7198
|
+
}
|
|
7199
|
+
function rootedPaths(node, out) {
|
|
7200
|
+
if (Array.isArray(node)) {
|
|
7201
|
+
for (const child of node) rootedPaths(child, out);
|
|
7202
|
+
return;
|
|
7203
|
+
}
|
|
7204
|
+
if (!isNode4(node)) return;
|
|
7205
|
+
if (node.op === ".") {
|
|
7206
|
+
const chain = memberChain(node);
|
|
7207
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
7208
|
+
out.push(chain.slice(1));
|
|
7209
|
+
return;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
rootedPaths(node.args, out);
|
|
7213
|
+
}
|
|
7214
|
+
var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
|
|
7215
|
+
function equalitySites(node, out) {
|
|
7216
|
+
if (Array.isArray(node)) {
|
|
7217
|
+
for (const child of node) equalitySites(child, out);
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
if (!isNode4(node)) return;
|
|
7221
|
+
const args = node.args;
|
|
7222
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7223
|
+
equalitySites(args[1], out);
|
|
7224
|
+
return;
|
|
6215
7225
|
}
|
|
6216
|
-
|
|
7226
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
7227
|
+
out.push({ op: node.op, right: args[1] });
|
|
7228
|
+
}
|
|
7229
|
+
equalitySites(args, out);
|
|
6217
7230
|
}
|
|
6218
|
-
function
|
|
7231
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
7232
|
+
if (Array.isArray(node)) {
|
|
7233
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
7234
|
+
return;
|
|
7235
|
+
}
|
|
7236
|
+
if (!isNode4(node)) return;
|
|
7237
|
+
const args = node.args;
|
|
7238
|
+
if (Array.isArray(args)) {
|
|
7239
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
7240
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
7241
|
+
excluded.add(receiver.args);
|
|
7242
|
+
}
|
|
7243
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7244
|
+
const macroArgs = args[2];
|
|
7245
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
7246
|
+
const bound = macroArgs[0];
|
|
7247
|
+
if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
7248
|
+
excluded.add(bound.args);
|
|
7249
|
+
}
|
|
7250
|
+
}
|
|
7251
|
+
}
|
|
7252
|
+
}
|
|
7253
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
7254
|
+
values.add(node.args);
|
|
7255
|
+
return;
|
|
7256
|
+
}
|
|
7257
|
+
classifyIdentifiers(args, values, excluded);
|
|
7258
|
+
}
|
|
7259
|
+
function predicateSource2(v) {
|
|
6219
7260
|
if (typeof v === "string") return v;
|
|
6220
7261
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
6221
7262
|
return v.source;
|
|
6222
7263
|
}
|
|
6223
7264
|
return void 0;
|
|
6224
7265
|
}
|
|
6225
|
-
function
|
|
6226
|
-
return
|
|
7266
|
+
function isRec22(v) {
|
|
7267
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6227
7268
|
}
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
7269
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
7270
|
+
const ast = (0, import_formula5.parseCelToAst)(source);
|
|
7271
|
+
if (!ast) return;
|
|
7272
|
+
const paths = [];
|
|
7273
|
+
rootedPaths(ast, paths);
|
|
7274
|
+
for (const segments of paths) {
|
|
7275
|
+
let cursor = scope;
|
|
7276
|
+
const walked = [];
|
|
7277
|
+
for (const segment of segments) {
|
|
7278
|
+
const step = stepInto(cursor, segment);
|
|
7279
|
+
if (step.kind === "opaque") break;
|
|
7280
|
+
if (step.kind === "undeclared") {
|
|
7281
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
7282
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
7283
|
+
findings.push({
|
|
7284
|
+
severity: "error",
|
|
7285
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
7286
|
+
where,
|
|
7287
|
+
path,
|
|
7288
|
+
message: `predicate references \`${full}\`, which the target schema does not declare \u2014 \`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, so the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149).`,
|
|
7289
|
+
hint: `${(0, import_spec4.formatSuggestion)((0, import_spec4.findClosestMatches)(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
7290
|
+
});
|
|
7291
|
+
break;
|
|
7292
|
+
}
|
|
7293
|
+
walked.push(segment);
|
|
7294
|
+
cursor = step.next;
|
|
7295
|
+
}
|
|
7296
|
+
}
|
|
7297
|
+
const declaredHere = keysOf(scope);
|
|
7298
|
+
const rhsOnly = bareRhsOnlyIdentifiers(ast);
|
|
7299
|
+
if (declaredHere) {
|
|
7300
|
+
const values = /* @__PURE__ */ new Set();
|
|
7301
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
7302
|
+
classifyIdentifiers(ast, values, excluded);
|
|
7303
|
+
for (const id of values) {
|
|
7304
|
+
if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
|
|
6243
7305
|
findings.push({
|
|
6244
|
-
severity: "
|
|
6245
|
-
rule:
|
|
7306
|
+
severity: "error",
|
|
7307
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
6246
7308
|
where,
|
|
6247
|
-
path
|
|
6248
|
-
message:
|
|
6249
|
-
hint: `
|
|
7309
|
+
path,
|
|
7310
|
+
message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
|
|
7311
|
+
hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
|
|
6250
7312
|
});
|
|
6251
7313
|
}
|
|
6252
7314
|
}
|
|
6253
|
-
const
|
|
6254
|
-
|
|
6255
|
-
const
|
|
6256
|
-
|
|
7315
|
+
const sites = [];
|
|
7316
|
+
equalitySites(ast, sites);
|
|
7317
|
+
for (const { op, right } of sites) {
|
|
7318
|
+
const chain = memberChain(right);
|
|
7319
|
+
if (!chain) continue;
|
|
7320
|
+
const text = chain.join(".");
|
|
7321
|
+
if (!PATH_SHAPED_RHS.test(text)) continue;
|
|
7322
|
+
const dotted = chain.length > 1;
|
|
6257
7323
|
findings.push({
|
|
6258
|
-
severity: "warning",
|
|
6259
|
-
rule:
|
|
7324
|
+
severity: dotted ? "error" : "warning",
|
|
7325
|
+
rule: PREDICATE_RHS_PATH_SHAPED,
|
|
6260
7326
|
where,
|
|
6261
7327
|
path,
|
|
6262
|
-
message:
|
|
6263
|
-
hint: rule.
|
|
7328
|
+
message: dotted ? `predicate compares against \`${text}\` on the RIGHT of \`${op}\`, which is a path but is not evaluated as one. A metadata-editing form resolves paths on the LEFT of \`${op}\` only; the right-hand side goes to the literal parser, so \`${text}\` is compared as the literal string "${text}". The verdict therefore does not depend on the right-hand path at all: \`a == ${text}\` is FALSE even when both sides hold the same value, and \`a != ${text}\` is correspondingly TRUE. An \`==\` written this way hides the element on every row, and nothing in the console says why (objectui#4049).` : `predicate compares against the unquoted word \`${text}\` on the RIGHT of \`${op}\`. The right-hand side of \`${op}\` is a literal, never a reference, so this is read as the literal string "${text}" \u2014 which is probably what you meant, and is why it appears to work. It is outside the declared subset all the same (\`path == 'literal'\`), and it stops working when this surface moves to the real CEL evaluator, where a bare \`${text}\` resolves to nothing (objectui#4049). The token also reads as a \`${ROOT}.\` root someone dropped, so this one finding carries BOTH readings: which one you meant is the thing no linter can know, and it changes the fix (#7696).`,
|
|
7329
|
+
hint: dotted ? `Two sanctioned spellings. (1) If you meant the TEXT, quote it: \`${op} '${text}'\`. (2) If you meant the PATH, restructure so the path is on the LEFT and a literal is on the right \u2014 comparing one path against another is outside the subset this surface renders, which is \`path == 'literal'\` / \`path != 'literal'\` and nothing wider. There is no third spelling that compares two paths here.` : `Two sanctioned spellings, and you must pick \u2014 they are not the same predicate. (1) If you meant the TEXT \`${text}\`, quote it: \`${op} '${text}'\`. That is what this renders as today, so it changes no behaviour and is the fix unless you know otherwise. (2) If you meant the FIELD \`${ROOT}.${text}\`, move it to the LEFT and put a literal on the right, e.g. \`${ROOT}.${text} == 'yes'\`. \u26D4 Do NOT simply add the root in place: \`${op} ${ROOT}.${text}\` is a path on the RIGHT, which this surface parses as the literal string "${ROOT}.${text}" \u2014 it is refused by this same rule at \`error\`, and it is FALSE on every row. The subset here is \`path == 'literal'\` and nothing wider.`
|
|
6264
7330
|
});
|
|
6265
7331
|
}
|
|
6266
7332
|
}
|
|
6267
|
-
function
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
const where = `view "${viewName}"`;
|
|
6279
|
-
for (const bucket of ["sections", "groups"]) {
|
|
6280
|
-
const sections = Array.isArray(view[bucket]) ? view[bucket] : [];
|
|
6281
|
-
for (let s = 0; s < sections.length; s++) {
|
|
6282
|
-
const sec = sections[s];
|
|
6283
|
-
if (!sec || typeof sec !== "object") continue;
|
|
6284
|
-
const secPath = `views[${i}].${bucket}[${s}]`;
|
|
6285
|
-
checkElement(sec, where, secPath, layer, findings);
|
|
6286
|
-
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
6287
|
-
for (let f = 0; f < secFields.length; f++) {
|
|
6288
|
-
const entry = secFields[f];
|
|
6289
|
-
if (isFieldObject(entry)) {
|
|
6290
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
6291
|
-
}
|
|
6292
|
-
}
|
|
7333
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
7334
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
7335
|
+
for (let i = 0; i < entries.length; i++) {
|
|
7336
|
+
const entry = entries[i];
|
|
7337
|
+
if (!isRec22(entry)) continue;
|
|
7338
|
+
const path = `${base}[${i}]`;
|
|
7339
|
+
for (const key of PREDICATE_KEYS) {
|
|
7340
|
+
const source = predicateSource2(entry[key]);
|
|
7341
|
+
if (source !== void 0 && source.trim()) {
|
|
7342
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
7343
|
+
break;
|
|
6293
7344
|
}
|
|
6294
7345
|
}
|
|
7346
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
7347
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
7348
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
7349
|
+
}
|
|
6295
7350
|
}
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
const
|
|
6302
|
-
const
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
7351
|
+
}
|
|
7352
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
7353
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => (0, import_kernel2.getMetadataTypeSchema)(schemaId));
|
|
7354
|
+
const findings = [];
|
|
7355
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
7356
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
7357
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
7358
|
+
const schemaId = schemaIdOf(site.view);
|
|
7359
|
+
if (!schemaId) continue;
|
|
7360
|
+
let root;
|
|
7361
|
+
try {
|
|
7362
|
+
root = resolveSchema(schemaId);
|
|
7363
|
+
} catch {
|
|
7364
|
+
root = void 0;
|
|
7365
|
+
}
|
|
7366
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
7367
|
+
for (const bucket of ["sections", "groups"]) {
|
|
7368
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7369
|
+
for (let s = 0; s < sections.length; s++) {
|
|
7370
|
+
const section = sections[s];
|
|
7371
|
+
if (!isRec22(section)) continue;
|
|
7372
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7373
|
+
for (const key of PREDICATE_KEYS) {
|
|
7374
|
+
const source = predicateSource2(section[key]);
|
|
7375
|
+
if (source !== void 0 && source.trim()) {
|
|
7376
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
7377
|
+
break;
|
|
7378
|
+
}
|
|
7379
|
+
}
|
|
7380
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
6310
7381
|
}
|
|
6311
7382
|
}
|
|
6312
7383
|
}
|
|
@@ -6328,6 +7399,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
|
6328
7399
|
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
6329
7400
|
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
6330
7401
|
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
7402
|
+
var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
|
|
6331
7403
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
6332
7404
|
var OWD_ALIAS_FIX = {
|
|
6333
7405
|
read: "public_read",
|
|
@@ -6373,6 +7445,13 @@ function firstMasterDetailField(obj) {
|
|
|
6373
7445
|
}
|
|
6374
7446
|
return void 0;
|
|
6375
7447
|
}
|
|
7448
|
+
function resolveCbpRelation(obj) {
|
|
7449
|
+
const entries = asArray36(obj.fields);
|
|
7450
|
+
const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
|
|
7451
|
+
const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
|
|
7452
|
+
if (!found) return void 0;
|
|
7453
|
+
return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
|
|
7454
|
+
}
|
|
6376
7455
|
function grantsObjectAccess(p) {
|
|
6377
7456
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
6378
7457
|
}
|
|
@@ -6418,6 +7497,16 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6418
7497
|
});
|
|
6419
7498
|
}
|
|
6420
7499
|
}
|
|
7500
|
+
if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
|
|
7501
|
+
findings.push({
|
|
7502
|
+
severity: "error",
|
|
7503
|
+
rule: SECURITY_CBP_NO_RELATION,
|
|
7504
|
+
where: `object "${objName}"`,
|
|
7505
|
+
path: `${objPath}.sharingModel`,
|
|
7506
|
+
message: `"${objName}" declares sharingModel 'controlled_by_parent' but has no relation the platform can derive access from. ADR-0055 resolves the master through a required master_detail, then any master_detail, then a required lookup \u2014 each of which must also name a reference target \u2014 and this object matches none of the three. At runtime every read is DENIED and every write is refused with 422 INVALID_METADATA (#7474), so the object is unusable rather than merely locked down.`,
|
|
7507
|
+
hint: `Add the master relation this object is derived from, e.g. fields.parent: { type: 'master_detail', reference: '<master_object>', required: true }. If the object has no master, its baseline is its own decision \u2014 use sharingModel: 'private' (owner + shares), 'public_read', or 'public_read_write'.`
|
|
7508
|
+
});
|
|
7509
|
+
}
|
|
6421
7510
|
if (typeof external === "string") {
|
|
6422
7511
|
if (OWD_ALIAS_FIX[external]) {
|
|
6423
7512
|
findings.push({
|
|
@@ -6483,53 +7572,6 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6483
7572
|
}
|
|
6484
7573
|
}
|
|
6485
7574
|
}
|
|
6486
|
-
const flagRole = (kind, name, label2, where, path) => {
|
|
6487
|
-
if (identifierHasRoleToken(name)) {
|
|
6488
|
-
findings.push({
|
|
6489
|
-
severity: "error",
|
|
6490
|
-
rule: SECURITY_ROLE_WORD,
|
|
6491
|
-
where,
|
|
6492
|
-
path,
|
|
6493
|
-
message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
|
|
6494
|
-
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
6495
|
-
});
|
|
6496
|
-
} else if (labelHasRoleWord(label2)) {
|
|
6497
|
-
findings.push({
|
|
6498
|
-
severity: "error",
|
|
6499
|
-
rule: SECURITY_ROLE_WORD,
|
|
6500
|
-
where,
|
|
6501
|
-
path: `${path.replace(/\.name$/, "")}.label`,
|
|
6502
|
-
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
6503
|
-
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
6504
|
-
});
|
|
6505
|
-
}
|
|
6506
|
-
};
|
|
6507
|
-
for (let i = 0; i < objects.length; i++) {
|
|
6508
|
-
const obj = objects[i];
|
|
6509
|
-
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
6510
|
-
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
6511
|
-
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
6512
|
-
for (const f of asArray36(obj.fields)) {
|
|
6513
|
-
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
6514
|
-
}
|
|
6515
|
-
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
6516
|
-
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
6517
|
-
}
|
|
6518
|
-
}
|
|
6519
|
-
for (let i = 0; i < permissionSets.length; i++) {
|
|
6520
|
-
const ps = permissionSets[i];
|
|
6521
|
-
if (!ps || typeof ps !== "object") continue;
|
|
6522
|
-
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
6523
|
-
}
|
|
6524
|
-
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
6525
|
-
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
6526
|
-
}
|
|
6527
|
-
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
6528
|
-
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
6529
|
-
}
|
|
6530
|
-
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
6531
|
-
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
6532
|
-
}
|
|
6533
7575
|
const stackSetNames = new Set(
|
|
6534
7576
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
6535
7577
|
);
|
|
@@ -6650,6 +7692,60 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6650
7692
|
}
|
|
6651
7693
|
return findings;
|
|
6652
7694
|
}
|
|
7695
|
+
function validateSecurityRoleWord(stack) {
|
|
7696
|
+
const findings = [];
|
|
7697
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
7698
|
+
const objects = asArray36(stack.objects);
|
|
7699
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7700
|
+
const flagRole = (kind, name, label2, where, path) => {
|
|
7701
|
+
if (identifierHasRoleToken(name)) {
|
|
7702
|
+
findings.push({
|
|
7703
|
+
severity: "error",
|
|
7704
|
+
rule: SECURITY_ROLE_WORD,
|
|
7705
|
+
where,
|
|
7706
|
+
path,
|
|
7707
|
+
message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
|
|
7708
|
+
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7709
|
+
});
|
|
7710
|
+
} else if (labelHasRoleWord(label2)) {
|
|
7711
|
+
findings.push({
|
|
7712
|
+
severity: "error",
|
|
7713
|
+
rule: SECURITY_ROLE_WORD,
|
|
7714
|
+
where,
|
|
7715
|
+
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7716
|
+
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7717
|
+
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7718
|
+
});
|
|
7719
|
+
}
|
|
7720
|
+
};
|
|
7721
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7722
|
+
const obj = objects[i];
|
|
7723
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7724
|
+
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7725
|
+
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7726
|
+
for (const f of asArray36(obj.fields)) {
|
|
7727
|
+
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7728
|
+
}
|
|
7729
|
+
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
7730
|
+
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
for (let i = 0; i < permissionSets.length; i++) {
|
|
7734
|
+
const ps = permissionSets[i];
|
|
7735
|
+
if (!ps || typeof ps !== "object") continue;
|
|
7736
|
+
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7737
|
+
}
|
|
7738
|
+
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
7739
|
+
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7740
|
+
}
|
|
7741
|
+
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
7742
|
+
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7743
|
+
}
|
|
7744
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7745
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7746
|
+
}
|
|
7747
|
+
return findings;
|
|
7748
|
+
}
|
|
6653
7749
|
|
|
6654
7750
|
// src/validate-org-axis-red-lines.ts
|
|
6655
7751
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
@@ -6742,7 +7838,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6742
7838
|
}
|
|
6743
7839
|
|
|
6744
7840
|
// src/validate-sharing-rule-enforceability.ts
|
|
6745
|
-
var
|
|
7841
|
+
var import_formula6 = require("@objectstack/formula");
|
|
6746
7842
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
6747
7843
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
6748
7844
|
function asArray38(v) {
|
|
@@ -6775,7 +7871,7 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
6775
7871
|
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
6776
7872
|
const input = toCompilerInput(rule.condition);
|
|
6777
7873
|
if (input === null) return;
|
|
6778
|
-
const result = (0,
|
|
7874
|
+
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
6779
7875
|
if (result.ok) return;
|
|
6780
7876
|
if (result.reason === "parse-error") return;
|
|
6781
7877
|
const name = str2(rule.name) || String(index);
|
|
@@ -6808,9 +7904,10 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
6808
7904
|
}
|
|
6809
7905
|
|
|
6810
7906
|
// src/validate-rls-predicate-enforceability.ts
|
|
6811
|
-
var
|
|
7907
|
+
var import_formula7 = require("@objectstack/formula");
|
|
6812
7908
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
6813
7909
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
7910
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
6814
7911
|
function asArray39(v) {
|
|
6815
7912
|
if (Array.isArray(v)) return v;
|
|
6816
7913
|
if (v && typeof v === "object") {
|
|
@@ -6822,6 +7919,13 @@ function str3(v) {
|
|
|
6822
7919
|
return typeof v === "string" ? v : "";
|
|
6823
7920
|
}
|
|
6824
7921
|
var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
|
|
7922
|
+
function boundsOverrunOf(bridged) {
|
|
7923
|
+
const parsed = (0, import_formula7.parseCelToAstWithReason)(bridged);
|
|
7924
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
7925
|
+
}
|
|
7926
|
+
function quote(source) {
|
|
7927
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
7928
|
+
}
|
|
6825
7929
|
function consequence(clause) {
|
|
6826
7930
|
const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
|
|
6827
7931
|
return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
|
|
@@ -6834,15 +7938,31 @@ function validateRlsPredicateEnforceability(stack) {
|
|
|
6834
7938
|
for (const clause of ["using", "check"]) {
|
|
6835
7939
|
const source = str3(policy[clause]);
|
|
6836
7940
|
if (!source.trim()) continue;
|
|
6837
|
-
if ((0,
|
|
6838
|
-
const
|
|
7941
|
+
if ((0, import_formula7.isSupportedRlsExpression)(source)) continue;
|
|
7942
|
+
const bridged = (0, import_formula7.sqlPredicateToCel)(source);
|
|
7943
|
+
const why = (0, import_formula7.isPushdownableCel)(bridged);
|
|
6839
7944
|
const detail = why.ok ? "" : why.detail;
|
|
6840
7945
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
7946
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
6841
7947
|
const psName = str3(ps.name) || String(psIndex);
|
|
6842
7948
|
const policyName = str3(policy.name) || String(pIndex);
|
|
6843
7949
|
const object = str3(policy.object);
|
|
6844
7950
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
6845
7951
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
7952
|
+
if (overrun) {
|
|
7953
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
7954
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
7955
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
7956
|
+
findings.push({
|
|
7957
|
+
severity: "error",
|
|
7958
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
7959
|
+
where,
|
|
7960
|
+
path,
|
|
7961
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
7962
|
+
hint: `There is no syntax or dialect error to correct here \u2014 the predicate is well-formed CEL and is simply too large for ${bound}${budget}, so the fix is to make it smaller or to move the work off the predicate. (1) Collapse a long \`field == a || field == b || \u2026\` chain into a single \`field in [a, b, \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2). (2) Pre-resolve the set into a membership key the runtime exposes and test \`field in current_user.<key>\` (ADR-0105 D11) \u2014 one comparison whatever the set size. (3) Denormalise a repeated sub-expression onto this object as a formula/rollup field and test that single column. (4) Split a TOP-LEVEL \`||\` across several \`rowLevelSecurity\` policies: applicable policies are OR-ed, so that is equivalent \u2014 but never split a top-level \`&&\` this way, which would WIDEN access rather than preserve it. Logic genuinely this large is not a row filter: move it to a hook or action body (\`ScriptBody { language: 'js' }\`, the L2 sandboxed surface).`
|
|
7963
|
+
});
|
|
7964
|
+
continue;
|
|
7965
|
+
}
|
|
6846
7966
|
if (parseError) {
|
|
6847
7967
|
findings.push({
|
|
6848
7968
|
severity: "error",
|
|
@@ -6874,11 +7994,11 @@ var import_meta4 = {};
|
|
|
6874
7994
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
6875
7995
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
6876
7996
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
6877
|
-
var
|
|
7997
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6878
7998
|
function asArray40(v) {
|
|
6879
|
-
if (Array.isArray(v)) return v.filter(
|
|
6880
|
-
if (
|
|
6881
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7999
|
+
if (Array.isArray(v)) return v.filter(isRec23);
|
|
8000
|
+
if (isRec23(v)) {
|
|
8001
|
+
return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
|
|
6882
8002
|
}
|
|
6883
8003
|
return [];
|
|
6884
8004
|
}
|
|
@@ -6895,7 +8015,7 @@ function loadAjv() {
|
|
|
6895
8015
|
`@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.`
|
|
6896
8016
|
);
|
|
6897
8017
|
}
|
|
6898
|
-
const ctor =
|
|
8018
|
+
const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
6899
8019
|
cachedAjv = ctor;
|
|
6900
8020
|
return ctor;
|
|
6901
8021
|
}
|
|
@@ -6910,7 +8030,7 @@ function loadAddFormats() {
|
|
|
6910
8030
|
`@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.`
|
|
6911
8031
|
);
|
|
6912
8032
|
}
|
|
6913
|
-
const plugin =
|
|
8033
|
+
const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
6914
8034
|
cachedAddFormats = plugin;
|
|
6915
8035
|
return plugin;
|
|
6916
8036
|
}
|
|
@@ -6940,13 +8060,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
6940
8060
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
6941
8061
|
for (const branch of ["then", "otherwise"]) {
|
|
6942
8062
|
const nested = rule[branch];
|
|
6943
|
-
if (
|
|
8063
|
+
if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
6944
8064
|
}
|
|
6945
8065
|
return out;
|
|
6946
8066
|
}
|
|
6947
8067
|
function walkObjectValidationRules(stack) {
|
|
6948
8068
|
const walked = [];
|
|
6949
|
-
if (!
|
|
8069
|
+
if (!isRec23(stack)) return walked;
|
|
6950
8070
|
for (const obj of asArray40(stack.objects)) {
|
|
6951
8071
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6952
8072
|
const validations = obj.validations;
|
|
@@ -6981,7 +8101,7 @@ function validateRuleCompilability(stack) {
|
|
|
6981
8101
|
});
|
|
6982
8102
|
}
|
|
6983
8103
|
}
|
|
6984
|
-
if (rule.type === "json_schema" &&
|
|
8104
|
+
if (rule.type === "json_schema" && isRec23(rule.schema)) {
|
|
6985
8105
|
try {
|
|
6986
8106
|
createRuntimeAjv().compile(rule.schema);
|
|
6987
8107
|
} catch (err) {
|
|
@@ -7001,7 +8121,7 @@ function validateRuleCompilability(stack) {
|
|
|
7001
8121
|
|
|
7002
8122
|
// src/validate-rule-schema-formats.ts
|
|
7003
8123
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7004
|
-
var
|
|
8124
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7005
8125
|
var SUBSCHEMA_KEYS = [
|
|
7006
8126
|
"additionalItems",
|
|
7007
8127
|
"additionalProperties",
|
|
@@ -7025,7 +8145,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7025
8145
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7026
8146
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7027
8147
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7028
|
-
if (!
|
|
8148
|
+
if (!isRec24(schema)) return;
|
|
7029
8149
|
if (typeof schema.format === "string") {
|
|
7030
8150
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7031
8151
|
}
|
|
@@ -7044,7 +8164,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7044
8164
|
}
|
|
7045
8165
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7046
8166
|
const value = schema[key];
|
|
7047
|
-
if (!
|
|
8167
|
+
if (!isRec24(value)) continue;
|
|
7048
8168
|
for (const [name, entry] of Object.entries(value)) {
|
|
7049
8169
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7050
8170
|
}
|
|
@@ -7052,13 +8172,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7052
8172
|
const items = schema.items;
|
|
7053
8173
|
if (Array.isArray(items)) {
|
|
7054
8174
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7055
|
-
} else if (
|
|
8175
|
+
} else if (isRec24(items)) {
|
|
7056
8176
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7057
8177
|
}
|
|
7058
8178
|
const dependencies = schema.dependencies;
|
|
7059
|
-
if (
|
|
8179
|
+
if (isRec24(dependencies)) {
|
|
7060
8180
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7061
|
-
if (!
|
|
8181
|
+
if (!isRec24(entry)) continue;
|
|
7062
8182
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7063
8183
|
}
|
|
7064
8184
|
}
|
|
@@ -7097,7 +8217,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7097
8217
|
const findings = [];
|
|
7098
8218
|
const pending = [];
|
|
7099
8219
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7100
|
-
if (rule.type !== "json_schema" || !
|
|
8220
|
+
if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
|
|
7101
8221
|
const uses = [];
|
|
7102
8222
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7103
8223
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7130,7 +8250,7 @@ function asArray41(v) {
|
|
|
7130
8250
|
}
|
|
7131
8251
|
return [];
|
|
7132
8252
|
}
|
|
7133
|
-
function
|
|
8253
|
+
function strName20(v) {
|
|
7134
8254
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7135
8255
|
}
|
|
7136
8256
|
function strList3(v) {
|
|
@@ -7145,7 +8265,7 @@ function collectNamePlacedActions(stack) {
|
|
|
7145
8265
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7146
8266
|
}
|
|
7147
8267
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
7148
|
-
const n =
|
|
8268
|
+
const n = strName20(def?.name);
|
|
7149
8269
|
if (n) placed.add(n);
|
|
7150
8270
|
}
|
|
7151
8271
|
};
|
|
@@ -7171,7 +8291,7 @@ function validateActionLocations(stack) {
|
|
|
7171
8291
|
const check = (action, path) => {
|
|
7172
8292
|
if (!action || typeof action !== "object") return;
|
|
7173
8293
|
if ("locations" in action) return;
|
|
7174
|
-
const name =
|
|
8294
|
+
const name = strName20(action.name);
|
|
7175
8295
|
if (!name) return;
|
|
7176
8296
|
if (namePlaced.has(name)) return;
|
|
7177
8297
|
findings.push({
|
|
@@ -7180,7 +8300,7 @@ function validateActionLocations(stack) {
|
|
|
7180
8300
|
where: `action "${name}"`,
|
|
7181
8301
|
path,
|
|
7182
8302
|
message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
|
|
7183
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
8303
|
+
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
|
|
7184
8304
|
});
|
|
7185
8305
|
};
|
|
7186
8306
|
const actions = asArray41(stack.actions);
|
|
@@ -7197,6 +8317,7 @@ function validateActionLocations(stack) {
|
|
|
7197
8317
|
|
|
7198
8318
|
// src/lint-flow-patterns.ts
|
|
7199
8319
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8320
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
7200
8321
|
function asArray42(v) {
|
|
7201
8322
|
if (Array.isArray(v)) return v;
|
|
7202
8323
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7471,7 +8592,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7471
8592
|
function filterCarriesNoCondition(filter) {
|
|
7472
8593
|
if (filter === void 0 || filter === null) return true;
|
|
7473
8594
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7474
|
-
return
|
|
8595
|
+
return (0, import_data10.reduceFilterVerdict)(filter) === "true";
|
|
8596
|
+
}
|
|
8597
|
+
function describeUnboundedFilter(filter) {
|
|
8598
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8599
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8600
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8601
|
+
}
|
|
8602
|
+
function previewFilter(filter) {
|
|
8603
|
+
try {
|
|
8604
|
+
const json = JSON.stringify(filter);
|
|
8605
|
+
if (typeof json !== "string") return typeof filter;
|
|
8606
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8607
|
+
} catch {
|
|
8608
|
+
return typeof filter;
|
|
8609
|
+
}
|
|
7475
8610
|
}
|
|
7476
8611
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7477
8612
|
for (const node of nodes) {
|
|
@@ -7482,10 +8617,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7482
8617
|
if (cfg.multi !== true) continue;
|
|
7483
8618
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7484
8619
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7485
|
-
const filterState = cfg.filter
|
|
8620
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7486
8621
|
findings.push({
|
|
7487
8622
|
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
7488
|
-
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where
|
|
8623
|
+
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards the filter as \`where\` (an absent key becomes \`{}\`) plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` bounded by nothing \u2014 a filter that reduces to TRUE constrains no row. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
|
|
7489
8624
|
hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
|
|
7490
8625
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7491
8626
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7787,6 +8922,13 @@ var TYPE_COLLECTIONS = [
|
|
|
7787
8922
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
7788
8923
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7789
8924
|
// silent, which is the shape this lint exists to prevent.
|
|
8925
|
+
//
|
|
8926
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
8927
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
8928
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
8929
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
8930
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
8931
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
7790
8932
|
{ type: "dashboard", key: "dashboards" }
|
|
7791
8933
|
];
|
|
7792
8934
|
function lintLivenessProperties(stack) {
|
|
@@ -7817,7 +8959,7 @@ function lintLivenessProperties(stack) {
|
|
|
7817
8959
|
}
|
|
7818
8960
|
|
|
7819
8961
|
// src/lint-autonumber-formats.ts
|
|
7820
|
-
var
|
|
8962
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
7821
8963
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
7822
8964
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7823
8965
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
@@ -7843,8 +8985,8 @@ function lintAutonumberFormats(stack) {
|
|
|
7843
8985
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
7844
8986
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
7845
8987
|
if (!fmt) continue;
|
|
7846
|
-
const tokens = (0,
|
|
7847
|
-
const refs = (0,
|
|
8988
|
+
const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
|
|
8989
|
+
const refs = (0, import_data11.referencedFields)(tokens);
|
|
7848
8990
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
7849
8991
|
for (const t of tokens) {
|
|
7850
8992
|
if (t.kind !== "literal") continue;
|
|
@@ -7898,7 +9040,7 @@ function lintAutonumberFormats(stack) {
|
|
|
7898
9040
|
}
|
|
7899
9041
|
|
|
7900
9042
|
// src/lint-view-refs.ts
|
|
7901
|
-
var
|
|
9043
|
+
var import_spec5 = require("@objectstack/spec");
|
|
7902
9044
|
function asArray45(v) {
|
|
7903
9045
|
if (Array.isArray(v)) return v;
|
|
7904
9046
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7932,7 +9074,7 @@ function lintViewRefs(stack) {
|
|
|
7932
9074
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7933
9075
|
continue;
|
|
7934
9076
|
}
|
|
7935
|
-
if (!(0,
|
|
9077
|
+
if (!(0, import_spec5.isAggregatedViewContainer)(v)) continue;
|
|
7936
9078
|
const object = viewContainerObjectName(v);
|
|
7937
9079
|
if (object) containers.push({ object, container: v });
|
|
7938
9080
|
}
|
|
@@ -7944,7 +9086,7 @@ function lintViewRefs(stack) {
|
|
|
7944
9086
|
}
|
|
7945
9087
|
}
|
|
7946
9088
|
for (const { object, container } of containers) {
|
|
7947
|
-
const { items, collisions } = (0,
|
|
9089
|
+
const { items, collisions } = (0, import_spec5.expandViewContainerWithDiagnostics)(object, container);
|
|
7948
9090
|
for (const it of items) indexKind(it.name, it.viewKind);
|
|
7949
9091
|
for (const col of collisions) {
|
|
7950
9092
|
findings.push({
|
|
@@ -8158,8 +9300,12 @@ var AUTHORING_RULES = [
|
|
|
8158
9300
|
}))
|
|
8159
9301
|
},
|
|
8160
9302
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8161
|
-
// mode)
|
|
8162
|
-
//
|
|
9303
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
9304
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
9305
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
9306
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
9307
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
9308
|
+
// parse would have eaten".
|
|
8163
9309
|
{
|
|
8164
9310
|
name: "validateListViewMode",
|
|
8165
9311
|
tier: "gating",
|
|
@@ -8190,9 +9336,38 @@ var AUTHORING_RULES = [
|
|
|
8190
9336
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8191
9337
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8192
9338
|
},
|
|
8193
|
-
//
|
|
8194
|
-
//
|
|
8195
|
-
//
|
|
9339
|
+
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
9340
|
+
// in `enable.apiMethods` that its own resolved affordances refuse. Every key
|
|
9341
|
+
// is one we know and each is individually valid, so #4001's unknown-key
|
|
9342
|
+
// rejection and the Zod parse both pass it; the contradiction is only visible
|
|
9343
|
+
// when the two keys are read TOGETHER, which nothing did at authoring time.
|
|
9344
|
+
//
|
|
9345
|
+
// `gating` because the declaration is already false when it ships: objectql's
|
|
9346
|
+
// registry strips the verb at registration, so the metadata advertises an API
|
|
9347
|
+
// the product does not serve. That strip has been correct and silent — a
|
|
9348
|
+
// `console.warn` on every control-plane boot that went unread for the life of
|
|
9349
|
+
// a real divergence (`sys_environment`/`sys_package`). This entry is the
|
|
9350
|
+
// ruling's "close it where the author is"; boot stays warn-and-strip.
|
|
9351
|
+
//
|
|
9352
|
+
// Pre-parse: the predicate reads only authored keys, and the finding must
|
|
9353
|
+
// survive an unrelated schema error elsewhere in the stack.
|
|
9354
|
+
{
|
|
9355
|
+
name: "validateManagedApiMethods",
|
|
9356
|
+
tier: "gating",
|
|
9357
|
+
input: "normalized",
|
|
9358
|
+
commands: ALL,
|
|
9359
|
+
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9360
|
+
surfaces: CLI_ONLY,
|
|
9361
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9362
|
+
run: (stack) => validateManagedApiMethods(stack)
|
|
9363
|
+
},
|
|
9364
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
9365
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
9366
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
9367
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
9368
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
9369
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
9370
|
+
// declared and which survives the parse untouched.
|
|
8196
9371
|
{
|
|
8197
9372
|
name: "validateViewContainers",
|
|
8198
9373
|
tier: "gating",
|
|
@@ -8242,6 +9417,31 @@ var AUTHORING_RULES = [
|
|
|
8242
9417
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8243
9418
|
run: (stack) => validateFilterTokens(stack)
|
|
8244
9419
|
},
|
|
9420
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9421
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9422
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
9423
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
9424
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
9425
|
+
// same-direction precedent one shape over.
|
|
9426
|
+
{
|
|
9427
|
+
name: "validateEmptyCombinators",
|
|
9428
|
+
tier: "gating",
|
|
9429
|
+
input: "parsed",
|
|
9430
|
+
commands: ALL,
|
|
9431
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
9432
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
9433
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
9434
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
9435
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
9436
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
9437
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
9438
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
9439
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
9440
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
9441
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9442
|
+
runtimeTypes: ["flow"],
|
|
9443
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
9444
|
+
},
|
|
8245
9445
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8246
9446
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8247
9447
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8290,6 +9490,16 @@ var AUTHORING_RULES = [
|
|
|
8290
9490
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8291
9491
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8292
9492
|
// separate step, once the warning-period inventory is empty.
|
|
9493
|
+
//
|
|
9494
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9495
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9496
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9497
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9498
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9499
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9500
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9501
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9502
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8293
9503
|
{
|
|
8294
9504
|
name: "validateComponentProps",
|
|
8295
9505
|
tier: "advisory",
|
|
@@ -8368,10 +9578,12 @@ var AUTHORING_RULES = [
|
|
|
8368
9578
|
//
|
|
8369
9579
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8370
9580
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8371
|
-
// dead?
|
|
9581
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8372
9582
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8373
|
-
// predicate cannot route at all,
|
|
8374
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9583
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9584
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9585
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9586
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8375
9587
|
// can be changed by installing a package, so there is no reading under which
|
|
8376
9588
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8377
9589
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8498,19 +9710,117 @@ var AUTHORING_RULES = [
|
|
|
8498
9710
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8499
9711
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8500
9712
|
},
|
|
8501
|
-
// ADR-0089 D3b —
|
|
8502
|
-
//
|
|
8503
|
-
//
|
|
9713
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
9714
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
9715
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
9716
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
9717
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
9718
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
9719
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
9720
|
+
//
|
|
9721
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
9722
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
9723
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
9724
|
+
// a change to this package's external input contract, and the maintainer's
|
|
9725
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
9726
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
9727
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
9728
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
9729
|
+
//
|
|
9730
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
9731
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
9732
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
9733
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
9734
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
9735
|
+
// "pre-parse evidence" one.
|
|
9736
|
+
//
|
|
9737
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
9738
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
9739
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
9740
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9741
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9742
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
9743
|
+
//
|
|
9744
|
+
// ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
|
|
9745
|
+
//
|
|
9746
|
+
// This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
|
|
9747
|
+
// in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
|
|
9748
|
+
// `advisories` channel landed (PR #7435). Before that move a `view` written
|
|
9749
|
+
// through Studio / REST `/meta` / MCP — the only door most tenants have, and
|
|
9750
|
+
// the door AI authors use — was judged by NONE of the family's rule ids (six
|
|
9751
|
+
// at the time of the move; seven since #7659 added
|
|
9752
|
+
// `predicate-rhs-path-shaped` inside the second entry).
|
|
9753
|
+
//
|
|
9754
|
+
// They move together on purpose, and the two entries carry one comment because
|
|
9755
|
+
// they are one wall: #7214's implementer wired its own rule here alone and then
|
|
9756
|
+
// REVERTED it, because a `view` refused for an unresolvable predicate PATH
|
|
9757
|
+
// while a predicate that does not parse at all walks through the same door is
|
|
9758
|
+
// less predictable than refusing neither. A half-wired wall is worse than an
|
|
9759
|
+
// unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
|
|
9760
|
+
// directly: every id on this surface is gated at the runtime door, or none is.
|
|
9761
|
+
//
|
|
9762
|
+
// The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
|
|
9763
|
+
// and re-measuring it at move time found it false: both rule functions read
|
|
9764
|
+
// `stack.views` and `stack.pages` and NO other collection — never `objects` —
|
|
9765
|
+
// so the per-write snapshot the gate builds is not partial for them, it is
|
|
9766
|
+
// complete. (`pages` is simply absent on a `view` write, so the page half
|
|
9767
|
+
// contributes zero findings to both differential passes rather than inventing
|
|
9768
|
+
// any.) The reason was not describing this rule; it was the default a rule got
|
|
9769
|
+
// when nobody measured, which is the #4409/#4463 defect one layer in.
|
|
9770
|
+
//
|
|
9771
|
+
// Runtime input tier: the gate hands the rules the body as persisted, without
|
|
9772
|
+
// `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
|
|
9773
|
+
// door. That costs the family nothing — `validateVisibilityPredicates` reads
|
|
9774
|
+
// `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
|
|
9775
|
+
// so a caller handing it a raw authored object still gets a verdict.
|
|
8504
9776
|
{
|
|
8505
9777
|
name: "validateVisibilityPredicates",
|
|
8506
|
-
tier: "
|
|
9778
|
+
tier: "gating",
|
|
8507
9779
|
input: "normalized",
|
|
8508
9780
|
commands: ALL,
|
|
8509
9781
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
8510
|
-
surfaces:
|
|
8511
|
-
|
|
9782
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9783
|
+
runtimeTypes: ["view"],
|
|
8512
9784
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8513
9785
|
},
|
|
9786
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
9787
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
9788
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
9789
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
9790
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
9791
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
9792
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
9793
|
+
// #6254's 16 bare `type ==` predicates.
|
|
9794
|
+
//
|
|
9795
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
9796
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
9797
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9798
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9799
|
+
// false build errors. See the rule's module note.
|
|
9800
|
+
//
|
|
9801
|
+
// #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
|
|
9802
|
+
// resolution question at all: the metadata-admin renderer resolves paths only
|
|
9803
|
+
// on the LEFT of `==` / `!=` and hands the right side to its literal parser,
|
|
9804
|
+
// so `data.a == data.b` resolves both sides cleanly, passes the two rules
|
|
9805
|
+
// above, and still compares against the string "data.b" — a constant verdict.
|
|
9806
|
+
// It carries `error` on a dotted chain (no reading under which it worked) and
|
|
9807
|
+
// `warning` on a bare word (`status == active` compares as the text today, so
|
|
9808
|
+
// refusing it would fail a build over metadata that renders correctly). The
|
|
9809
|
+
// per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
|
|
9810
|
+
// since #3760; the entry's `gating` tier is unchanged because it already was.
|
|
9811
|
+
{
|
|
9812
|
+
name: "validatePredicatePathRefs",
|
|
9813
|
+
tier: "gating",
|
|
9814
|
+
input: "normalized",
|
|
9815
|
+
commands: ALL,
|
|
9816
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9817
|
+
// The second half of the #7220 family move — see the block above the
|
|
9818
|
+
// `validateVisibilityPredicates` entry. This is the rule whose solo wiring
|
|
9819
|
+
// was reverted; it is wired now because its siblings are.
|
|
9820
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9821
|
+
runtimeTypes: ["view"],
|
|
9822
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
9823
|
+
},
|
|
8514
9824
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8515
9825
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8516
9826
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
@@ -8668,16 +9978,110 @@ var AUTHORING_RULES = [
|
|
|
8668
9978
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
8669
9979
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
8670
9980
|
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
9981
|
+
//
|
|
9982
|
+
// [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
|
|
9983
|
+
// was false in both halves — it read: "Already gated at this surface by a
|
|
9984
|
+
// DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
|
|
9985
|
+
// `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
|
|
9986
|
+
// on every runtime write. Running the linter here as well would double-report
|
|
9987
|
+
// one refusal in two vocabularies."
|
|
9988
|
+
//
|
|
9989
|
+
// - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
|
|
9990
|
+
// `externalSharingModel` through a local `OWD_WIDTH`, and never touches
|
|
9991
|
+
// `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
|
|
9992
|
+
// block carries it covers ONE — `security-external-wider-than-internal`
|
|
9993
|
+
// (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
|
|
9994
|
+
// corresponds to no lint rule at all, so it is not coverage in the other
|
|
9995
|
+
// direction either. Twelve rules were enforced at no runtime door while
|
|
9996
|
+
// this field said they were.
|
|
9997
|
+
// - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
|
|
9998
|
+
// `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
|
|
9999
|
+
// `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
|
|
10000
|
+
// both refuse by THROWING. The first to fire ends the write, so an author
|
|
10001
|
+
// sees one refusal, never two. The stated cost of moving was imaginary; the
|
|
10002
|
+
// reason it has not moved is the measured one below.
|
|
10003
|
+
//
|
|
10004
|
+
// The move IS taken now — the #7891 programme's three slices, in order:
|
|
10005
|
+
//
|
|
10006
|
+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
|
|
10007
|
+
// the isolation proof that the differential cancels every finding this
|
|
10008
|
+
// function derives from the sibling collections.
|
|
10009
|
+
// - #8309: the snapshot repair. The gate used to carry `objects` and
|
|
10010
|
+
// nothing else, so the three cross-collection rules judged a universe
|
|
10011
|
+
// missing the collection they compare against (measured: 38 phantom
|
|
10012
|
+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
|
|
10013
|
+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
|
|
10014
|
+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
|
|
10015
|
+
// - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
|
|
10016
|
+
// `object` measured DIRTY on that tree and was escalated, not forced.
|
|
10017
|
+
// - #8310 slice 2 (this state): `object` crosses under the maintainer
|
|
10018
|
+
// ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
|
|
10019
|
+
// authored OWD is REQUIRED at the runtime object door — an object
|
|
10020
|
+
// publish with no authored `sharingModel` is refused with the 422 lint
|
|
10021
|
+
// envelope (`security-owd-unset`); absence is not a decision. The ~16
|
|
10022
|
+
// objectql/rest suite files that relied on OWD-less publishes were
|
|
10023
|
+
// repaired honestly (fixtures author their posture), and
|
|
10024
|
+
// `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
|
|
10025
|
+
// answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
|
|
10026
|
+
// ADR-0094-seam 403 doors answer for what passes lint. The same ruling
|
|
10027
|
+
// retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
|
|
10028
|
+
// of this door (R1 env-tighten-only STAYS — no lint rule covers it);
|
|
10029
|
+
// see `object-posture-gate.ts` and the ADR-0094 amendment.
|
|
10030
|
+
//
|
|
10031
|
+
// `security-role-word` is NOT in this entry any more — that is what the
|
|
10032
|
+
// `validateSecurityRoleWord` entry below records. It judges six collections
|
|
10033
|
+
// (objects, fields, actions, permission sets, positions, apps — plus books),
|
|
10034
|
+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
|
|
10035
|
+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
|
|
10036
|
+
// function that still contained it would have enforced ONE rule id for a
|
|
10037
|
+
// strict subset of its collections: a door where a permission set named
|
|
10038
|
+
// `role_manager` is refused and a position named `sales_role` walks through
|
|
10039
|
+
// — the #7220 failure this table refuses to build, in either direction. The
|
|
10040
|
+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
|
|
10041
|
+
// entry.
|
|
10042
|
+
//
|
|
10043
|
+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
|
|
10044
|
+
// registration, not a per-rule split: the baseline/candidate differential is
|
|
10045
|
+
// what keeps a write of one declared type from leaking the other rules'
|
|
10046
|
+
// whole-stack findings — every finding derived from a sibling collection is
|
|
10047
|
+
// produced byte-identically in both passes and cancels in the diff. Only
|
|
10048
|
+
// findings the written item itself adds are attributed to the write.
|
|
8671
10049
|
{
|
|
8672
10050
|
name: "validateSecurityPosture",
|
|
8673
10051
|
tier: "gating",
|
|
8674
10052
|
input: "parsed",
|
|
8675
10053
|
commands: ALL,
|
|
8676
10054
|
source: "packages/lint/src/validate-security-posture.ts",
|
|
8677
|
-
surfaces:
|
|
8678
|
-
|
|
10055
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10056
|
+
runtimeTypes: ["seed", "permission", "book", "object"],
|
|
8679
10057
|
run: (stack) => validateSecurityPosture(stack)
|
|
8680
10058
|
},
|
|
10059
|
+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
|
|
10060
|
+
// `validateSecurityPosture` the day the rest of that block crossed the
|
|
10061
|
+
// runtime wall — so that it could stay behind WHOLE rather than cross for
|
|
10062
|
+
// three of the six collections it judges (#7220: one rule id must sit on ONE
|
|
10063
|
+
// side of the wall). The split is a surface boundary, not taste: the rule's
|
|
10064
|
+
// verdict and findings are byte-identical to before on every CLI command
|
|
10065
|
+
// (both entries run on all three), and the runtime door does not run it for
|
|
10066
|
+
// ANY type.
|
|
10067
|
+
//
|
|
10068
|
+
// The road to crossing is concrete and short, recorded here so the next
|
|
10069
|
+
// seat prices it correctly: carry `positions`/`apps` in
|
|
10070
|
+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
|
|
10071
|
+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
|
|
10072
|
+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
|
|
10073
|
+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
|
|
10074
|
+
// #7220 discipline satisfied.
|
|
10075
|
+
{
|
|
10076
|
+
name: "validateSecurityRoleWord",
|
|
10077
|
+
tier: "gating",
|
|
10078
|
+
input: "parsed",
|
|
10079
|
+
commands: ALL,
|
|
10080
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
10081
|
+
surfaces: CLI_ONLY,
|
|
10082
|
+
surfaceReason: "P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, positions, apps \u2014 plus books), and the per-write snapshot neither carries nor maps positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of its six collections \u2014 the #7220 split (an object named sales_role refused while a position named sales_role walks through). It crosses whole \u2014 positions/apps carried, mapped and declared \u2014 or stays behind; it stays behind until that wiring exists.",
|
|
10083
|
+
run: (stack) => validateSecurityRoleWord(stack)
|
|
10084
|
+
},
|
|
8681
10085
|
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
8682
10086
|
// sharing rule that walks it builds a second permission hierarchy (the
|
|
8683
10087
|
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
@@ -8728,7 +10132,7 @@ var AUTHORING_RULES = [
|
|
|
8728
10132
|
commands: ALL,
|
|
8729
10133
|
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
8730
10134
|
surfaces: CLI_ONLY,
|
|
8731
|
-
surfaceReason: "
|
|
10135
|
+
surfaceReason: "The rule reads `stack.permissions[]`, which the per-write snapshot DOES carry since #8309 \u2014 the remaining gap is only the declaration: no `runtimeTypes` names `permission` here, and that flip is a rollout decision on #8310's axis, not a wiring fix. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
8732
10136
|
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
8733
10137
|
},
|
|
8734
10138
|
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
@@ -8786,8 +10190,34 @@ var TYPE_TO_STACK_KEY = {
|
|
|
8786
10190
|
dashboard: "dashboards",
|
|
8787
10191
|
agent: "agents",
|
|
8788
10192
|
hook: "hooks",
|
|
8789
|
-
seed
|
|
10193
|
+
// [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
|
|
10194
|
+
// holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
|
|
10195
|
+
// — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
|
|
10196
|
+
// mapping onto one either.
|
|
10197
|
+
//
|
|
10198
|
+
// The wrong spelling was INERT rather than harmless, and it is the #4449 shape
|
|
10199
|
+
// one surface over: the wiring guard asks only that a declared type HAS a
|
|
10200
|
+
// mapping, never that the mapping names a key some rule reads. So it would
|
|
10201
|
+
// have stayed green while the gate built `{ objects, seeds: [item] }` for
|
|
10202
|
+
// every seed write and every rule reading `stack.data` saw nothing — wired,
|
|
10203
|
+
// and running on nothing, with `rulesRun` reporting the rules as having run.
|
|
10204
|
+
// Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
|
|
10205
|
+
// changed no behaviour then; it was corrected here, with the measurement
|
|
10206
|
+
// that found it (#7576), rather than left for the rollout card to trip
|
|
10207
|
+
// over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
|
|
10208
|
+
// mapping is load-bearing today, not merely inert-and-correct.
|
|
10209
|
+
seed: "data",
|
|
10210
|
+
// [#8309] `permission`/`book` map ahead of their registration (#8310), the
|
|
10211
|
+
// same order `seed` arrived in: the mapping plus the enriched snapshot below
|
|
10212
|
+
// are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
|
|
10213
|
+
// a mapping without a declaring rule is inert by construction (the gate
|
|
10214
|
+
// filters by `runtimeTypes` before it ever consults this table), while a
|
|
10215
|
+
// declaration without the mapping is the wired-onto-nothing state the wiring
|
|
10216
|
+
// guard refuses. Landing the mapping first keeps #8310 a registry data edit.
|
|
10217
|
+
permission: "permissions",
|
|
10218
|
+
book: "books"
|
|
8790
10219
|
};
|
|
10220
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
|
|
8791
10221
|
function runtimeAuthoringRulesFor(type) {
|
|
8792
10222
|
return AUTHORING_RULES.filter(
|
|
8793
10223
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -8805,6 +10235,23 @@ function stackKeyForType(type) {
|
|
|
8805
10235
|
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
8806
10236
|
}
|
|
8807
10237
|
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
10238
|
+
function buildRuntimeWriteSnapshots(args) {
|
|
10239
|
+
const stackKey = stackKeyForType(args.type);
|
|
10240
|
+
if (!stackKey) return null;
|
|
10241
|
+
if (!args.item || typeof args.item !== "object") return null;
|
|
10242
|
+
const item = args.item;
|
|
10243
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10244
|
+
const baseline = {};
|
|
10245
|
+
for (const key of CONTEXT_STACK_KEYS) {
|
|
10246
|
+
const collection = args.context?.[key] ?? [];
|
|
10247
|
+
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10248
|
+
}
|
|
10249
|
+
const candidate = {
|
|
10250
|
+
...baseline,
|
|
10251
|
+
[stackKey]: [...baseline[stackKey] ?? [], item]
|
|
10252
|
+
};
|
|
10253
|
+
return { baseline, candidate };
|
|
10254
|
+
}
|
|
8808
10255
|
function runRules(rules, stack, ctx) {
|
|
8809
10256
|
const findings = [];
|
|
8810
10257
|
for (const rule of rules) {
|
|
@@ -8827,19 +10274,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8827
10274
|
const rules = runtimeAuthoringRulesFor(args.type);
|
|
8828
10275
|
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
8829
10276
|
if (rules.length === 0) return empty;
|
|
8830
|
-
const
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
8835
|
-
|
|
10277
|
+
const snapshots = buildRuntimeWriteSnapshots({
|
|
10278
|
+
type: args.type,
|
|
10279
|
+
item: args.item,
|
|
10280
|
+
...args.context !== void 0 ? { context: args.context } : {}
|
|
10281
|
+
});
|
|
10282
|
+
if (!snapshots) return empty;
|
|
8836
10283
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
8837
|
-
const
|
|
8838
|
-
const
|
|
8839
|
-
const baseline = { objects: baselineObjects };
|
|
8840
|
-
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
8841
|
-
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
8842
|
-
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
10284
|
+
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10285
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
8843
10286
|
return {
|
|
8844
10287
|
errors: added.filter((f) => f.severity === "error"),
|
|
8845
10288
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -8848,6 +10291,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8848
10291
|
}
|
|
8849
10292
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8850
10293
|
0 && (module.exports = {
|
|
10294
|
+
buildRuntimeWriteSnapshots,
|
|
8851
10295
|
runRuntimeAuthoringRules,
|
|
8852
10296
|
runtimeAuthoringRulesFor,
|
|
8853
10297
|
runtimeGatedTypes,
|