@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.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/validate-expressions.ts
|
|
2
|
-
import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
|
|
2
|
+
import { validateExpression, collectCelRootIdentifiers, parseCelToAst as parseCelToAst2, SCOPE_ROOTS } from "@objectstack/formula";
|
|
3
3
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
4
4
|
|
|
5
5
|
// src/system-fields.ts
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FIELD_GROUP_SYSTEM_FIELDS,
|
|
8
|
+
resolveInjectedSystemColumns,
|
|
9
|
+
unprovisionedInjectedColumns
|
|
10
|
+
} from "@objectstack/spec/data";
|
|
7
11
|
import { SystemFieldName } from "@objectstack/spec/system";
|
|
8
12
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
9
13
|
...FIELD_GROUP_SYSTEM_FIELDS,
|
|
@@ -12,6 +16,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
12
16
|
function injectedColumnsFor(objectDef) {
|
|
13
17
|
return resolveInjectedSystemColumns(objectDef).names;
|
|
14
18
|
}
|
|
19
|
+
function unprovisionedInjectedColumnsFor(objectDef) {
|
|
20
|
+
return new Set(unprovisionedInjectedColumns(objectDef));
|
|
21
|
+
}
|
|
22
|
+
function objectDefsOf(stack) {
|
|
23
|
+
if (!stack || typeof stack !== "object") return [];
|
|
24
|
+
const objects = stack.objects;
|
|
25
|
+
if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
|
|
26
|
+
if (objects && typeof objects === "object") {
|
|
27
|
+
return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
function indexUnprovisionedAnchors(stack) {
|
|
32
|
+
const index = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const obj of objectDefsOf(stack)) {
|
|
34
|
+
const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
|
|
35
|
+
if (!name) continue;
|
|
36
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
37
|
+
if (anchors.size > 0) index.set(name, anchors);
|
|
38
|
+
}
|
|
39
|
+
return index;
|
|
40
|
+
}
|
|
41
|
+
function unprovisionedAnchorCause(objectName, field) {
|
|
42
|
+
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`;
|
|
43
|
+
}
|
|
44
|
+
function unprovisionedAnchorHint(objectName, field) {
|
|
45
|
+
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).`;
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
// src/validate-null-guards.ts
|
|
17
49
|
import { parseCelToAst } from "@objectstack/formula";
|
|
@@ -245,6 +277,37 @@ function buildFieldIndex(objects) {
|
|
|
245
277
|
}
|
|
246
278
|
return idx;
|
|
247
279
|
}
|
|
280
|
+
var BOUND_RECORD_ROOTS = ["record", "previous"];
|
|
281
|
+
function isCelNode(v) {
|
|
282
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
283
|
+
}
|
|
284
|
+
function collectBoundRecordReads(source) {
|
|
285
|
+
const out = /* @__PURE__ */ new Map();
|
|
286
|
+
const ast = parseCelToAst2(source);
|
|
287
|
+
if (!ast) return out;
|
|
288
|
+
const pending = [ast];
|
|
289
|
+
while (pending.length > 0) {
|
|
290
|
+
const celNode = pending.pop();
|
|
291
|
+
if (!isCelNode(celNode)) continue;
|
|
292
|
+
if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
|
|
293
|
+
const [celRecv, seg] = celNode.args;
|
|
294
|
+
if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
|
|
295
|
+
if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const celArgs = celNode.args;
|
|
299
|
+
if (isCelNode(celArgs)) pending.push(celArgs);
|
|
300
|
+
else if (Array.isArray(celArgs)) {
|
|
301
|
+
for (const a of celArgs) {
|
|
302
|
+
if (isCelNode(a)) pending.push(a);
|
|
303
|
+
else if (Array.isArray(a)) {
|
|
304
|
+
for (const b of a) if (isCelNode(b)) pending.push(b);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
248
311
|
function buildFieldTypeIndex(objects) {
|
|
249
312
|
const idx = /* @__PURE__ */ new Map();
|
|
250
313
|
for (const obj of objects) {
|
|
@@ -344,6 +407,29 @@ function validateStackExpressions(stack) {
|
|
|
344
407
|
const fieldIndex = buildFieldIndex(objects);
|
|
345
408
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
346
409
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
410
|
+
const unprovisionedIndex = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const obj of objects) {
|
|
412
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
413
|
+
if (!name) continue;
|
|
414
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
415
|
+
if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
|
|
416
|
+
}
|
|
417
|
+
const warnUnprovisionedAnchors = (where, raw, objectName) => {
|
|
418
|
+
if (!objectName) return;
|
|
419
|
+
const anchors = unprovisionedIndex.get(objectName);
|
|
420
|
+
if (!anchors) return;
|
|
421
|
+
const source = celSourceOf(raw);
|
|
422
|
+
if (!source) return;
|
|
423
|
+
for (const [field, operand] of collectBoundRecordReads(source)) {
|
|
424
|
+
if (!anchors.has(field)) continue;
|
|
425
|
+
issues.push({
|
|
426
|
+
where,
|
|
427
|
+
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).`,
|
|
428
|
+
source,
|
|
429
|
+
severity: "warning"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
};
|
|
347
433
|
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
348
434
|
if (!objectName) return;
|
|
349
435
|
const nullableFields = nullableIndex.get(objectName);
|
|
@@ -370,6 +456,39 @@ function validateStackExpressions(stack) {
|
|
|
370
456
|
);
|
|
371
457
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
372
458
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
459
|
+
warnUnprovisionedAnchors(where, raw, objectName);
|
|
460
|
+
};
|
|
461
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
462
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
463
|
+
const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
|
|
464
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
465
|
+
visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
|
|
466
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). 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",
|
|
467
|
+
requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
|
|
468
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
469
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
470
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
471
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
472
|
+
// one, not a fabricated fourth cell (#6716).
|
|
473
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
474
|
+
};
|
|
475
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
476
|
+
const source = celSourceOf(raw);
|
|
477
|
+
if (!source) return;
|
|
478
|
+
const roots = collectCelRootIdentifiers(source);
|
|
479
|
+
if (!roots.ok) return;
|
|
480
|
+
const kept = SCOPE_ROOTS.filter(
|
|
481
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
482
|
+
);
|
|
483
|
+
if (kept.length === 0) return;
|
|
484
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
485
|
+
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}\`.`;
|
|
486
|
+
issues.push({
|
|
487
|
+
where,
|
|
488
|
+
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,
|
|
489
|
+
source,
|
|
490
|
+
severity: "error"
|
|
491
|
+
});
|
|
373
492
|
};
|
|
374
493
|
const checkDeclaredPredicate = (where, raw) => {
|
|
375
494
|
if (raw == null) return;
|
|
@@ -403,7 +522,14 @@ function validateStackExpressions(stack) {
|
|
|
403
522
|
if (retired.length > 0) {
|
|
404
523
|
issues.push({
|
|
405
524
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
406
|
-
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. `) +
|
|
525
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
|
|
526
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
527
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
528
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
529
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
530
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
531
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
532
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
407
533
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
408
534
|
});
|
|
409
535
|
} else if (!fn) {
|
|
@@ -437,13 +563,27 @@ function validateStackExpressions(stack) {
|
|
|
437
563
|
for (const [fname, f] of fieldList) {
|
|
438
564
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
439
565
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
566
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
567
|
+
}
|
|
568
|
+
for (const [oi, opt] of asArray(f.options).entries()) {
|
|
569
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
570
|
+
check(
|
|
571
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
572
|
+
opt.visibleWhen,
|
|
573
|
+
objectName,
|
|
574
|
+
"record"
|
|
575
|
+
);
|
|
440
576
|
}
|
|
441
|
-
const
|
|
442
|
-
|
|
577
|
+
for (const [slot, raw, consequence2] of [
|
|
578
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
579
|
+
["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`]
|
|
580
|
+
]) {
|
|
581
|
+
const source = celSourceOf(raw);
|
|
582
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
443
583
|
issues.push({
|
|
444
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
445
|
-
message:
|
|
446
|
-
source
|
|
584
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
585
|
+
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\`.`),
|
|
586
|
+
source,
|
|
447
587
|
severity: "error"
|
|
448
588
|
});
|
|
449
589
|
}
|
|
@@ -463,6 +603,7 @@ function validateStackExpressions(stack) {
|
|
|
463
603
|
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
464
604
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
465
605
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
606
|
+
warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
|
|
466
607
|
}
|
|
467
608
|
}
|
|
468
609
|
}
|
|
@@ -672,6 +813,46 @@ function validateFunctionalCompleteness(stack) {
|
|
|
672
813
|
return out;
|
|
673
814
|
}
|
|
674
815
|
|
|
816
|
+
// src/validate-managed-api-methods.ts
|
|
817
|
+
import {
|
|
818
|
+
checkManagedApiMethodAffordances,
|
|
819
|
+
describeManagedApiMethodConflicts
|
|
820
|
+
} from "@objectstack/spec/data";
|
|
821
|
+
var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
|
|
822
|
+
var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
823
|
+
function entriesOf2(v) {
|
|
824
|
+
if (Array.isArray(v)) {
|
|
825
|
+
return v.flatMap(
|
|
826
|
+
(def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (isRec2(v)) {
|
|
830
|
+
return Object.entries(v).flatMap(
|
|
831
|
+
([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
function validateManagedApiMethods(stack) {
|
|
837
|
+
const out = [];
|
|
838
|
+
if (!isRec2(stack)) return out;
|
|
839
|
+
for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
|
|
840
|
+
const conflicts = checkManagedApiMethodAffordances(obj.def);
|
|
841
|
+
if (conflicts.length === 0) continue;
|
|
842
|
+
const verbs = conflicts.map((c) => c.verb).join(", ");
|
|
843
|
+
const flags = [...new Set(conflicts.map((c) => c.needs))];
|
|
844
|
+
out.push({
|
|
845
|
+
severity: "error",
|
|
846
|
+
rule: MANAGED_API_METHOD_UNAFFORDABLE,
|
|
847
|
+
where: `object "${obj.name}"`,
|
|
848
|
+
path: `objects[${oi}].enable.apiMethods`,
|
|
849
|
+
message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + 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.`,
|
|
850
|
+
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.`
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
|
|
675
856
|
// src/validate-view-containers.ts
|
|
676
857
|
var VIEW_CONTAINER_SHAPE = "view-container-shape";
|
|
677
858
|
var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
|
|
@@ -689,10 +870,32 @@ function containerViewCount(rec) {
|
|
|
689
870
|
function validateViewContainers(stack) {
|
|
690
871
|
const out = [];
|
|
691
872
|
if (!stack || typeof stack !== "object") return out;
|
|
873
|
+
const viewItems = stack.viewItems;
|
|
874
|
+
if (viewItems != null && asEntries(viewItems).length > 0) {
|
|
875
|
+
out.push({
|
|
876
|
+
severity: "error",
|
|
877
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
878
|
+
where: "viewItems",
|
|
879
|
+
path: "viewItems",
|
|
880
|
+
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.",
|
|
881
|
+
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."
|
|
882
|
+
});
|
|
883
|
+
}
|
|
692
884
|
for (const { key, value } of asEntries(stack.views)) {
|
|
693
885
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
694
886
|
const rec = value;
|
|
695
|
-
if (rec.viewKind != null)
|
|
887
|
+
if (rec.viewKind != null) {
|
|
888
|
+
const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
889
|
+
out.push({
|
|
890
|
+
severity: "error",
|
|
891
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
892
|
+
where: `views${key}${label3}`,
|
|
893
|
+
path: `views${key}`,
|
|
894
|
+
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).",
|
|
895
|
+
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:`."
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
696
899
|
if (containerViewCount(rec) > 0) continue;
|
|
697
900
|
const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
698
901
|
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
@@ -722,6 +925,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
|
722
925
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
723
926
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
724
927
|
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
928
|
+
var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
|
|
725
929
|
var LEGACY_ANALYTICS_KEYS = [
|
|
726
930
|
"categoryField",
|
|
727
931
|
"valueField",
|
|
@@ -841,6 +1045,7 @@ function validateWidgetBindings(stack) {
|
|
|
841
1045
|
}
|
|
842
1046
|
objectFieldTypes.set(o.name, fm);
|
|
843
1047
|
}
|
|
1048
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
844
1049
|
const datasetList = asArray3(stack.datasets);
|
|
845
1050
|
for (let i = 0; i < datasetList.length; i++) {
|
|
846
1051
|
const ds = datasetList[i];
|
|
@@ -927,13 +1132,24 @@ function validateWidgetBindings(stack) {
|
|
|
927
1132
|
if (dashFilterDefs.length > 0) {
|
|
928
1133
|
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
929
1134
|
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
930
|
-
|
|
1135
|
+
const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
|
|
1136
|
+
if (objectFields && datasetObject) {
|
|
931
1137
|
for (const def of dashFilterDefs) {
|
|
932
1138
|
const eff = effectiveFilterField(w, def);
|
|
933
1139
|
if (!eff) continue;
|
|
934
1140
|
const field = eff.field;
|
|
935
1141
|
if (field.includes(".")) continue;
|
|
936
|
-
if (objectFields.has(field) || SYSTEM_FIELDS.has(field))
|
|
1142
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
|
|
1143
|
+
if (anchors?.has(field)) {
|
|
1144
|
+
push2({
|
|
1145
|
+
severity: "warning",
|
|
1146
|
+
rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
1147
|
+
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).`,
|
|
1148
|
+
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.`
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
937
1153
|
push2({
|
|
938
1154
|
severity: "error",
|
|
939
1155
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
@@ -1171,7 +1387,8 @@ function validateDashboardActionRefs(stack) {
|
|
|
1171
1387
|
|
|
1172
1388
|
// src/validate-filter-tokens.ts
|
|
1173
1389
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
1174
|
-
|
|
1390
|
+
|
|
1391
|
+
// src/filter-walk.ts
|
|
1175
1392
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1176
1393
|
function asArray5(v) {
|
|
1177
1394
|
if (Array.isArray(v)) return v;
|
|
@@ -1183,7 +1400,62 @@ function asArray5(v) {
|
|
|
1183
1400
|
function label(v, fallback) {
|
|
1184
1401
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
1185
1402
|
}
|
|
1403
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
1404
|
+
if (!node || typeof node !== "object") return;
|
|
1405
|
+
if (seen.has(node)) return;
|
|
1406
|
+
seen.add(node);
|
|
1407
|
+
if (Array.isArray(node)) {
|
|
1408
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1412
|
+
const childPath = `${path}.${k}`;
|
|
1413
|
+
if (FILTER_KEYS.has(k)) {
|
|
1414
|
+
visit({ value: v, path: childPath, where });
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
1421
|
+
if (!stack || typeof stack !== "object") return;
|
|
1422
|
+
for (const { key, kind } of surfaces) {
|
|
1423
|
+
const items = asArray5(stack[key]);
|
|
1424
|
+
items.forEach((item, i) => {
|
|
1425
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
1426
|
+
if (kind === "dashboard") {
|
|
1427
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1428
|
+
widgets.forEach((w, wi) => {
|
|
1429
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1430
|
+
scanForFilters(
|
|
1431
|
+
w,
|
|
1432
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
1433
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1434
|
+
visit,
|
|
1435
|
+
/* @__PURE__ */ new Set()
|
|
1436
|
+
);
|
|
1437
|
+
});
|
|
1438
|
+
const { widgets: _skip, ...rest } = item;
|
|
1439
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/validate-filter-tokens.ts
|
|
1448
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
1186
1449
|
var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
|
|
1450
|
+
var TOKEN_FILTER_SURFACES = [
|
|
1451
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1452
|
+
{ key: "objects", kind: "object" },
|
|
1453
|
+
{ key: "views", kind: "view" },
|
|
1454
|
+
{ key: "reports", kind: "report" },
|
|
1455
|
+
{ key: "datasets", kind: "dataset" },
|
|
1456
|
+
{ key: "pages", kind: "page" },
|
|
1457
|
+
{ key: "apps", kind: "app" }
|
|
1458
|
+
];
|
|
1187
1459
|
function walkFilterValues(node, path, where, out, seen) {
|
|
1188
1460
|
if (node === null || node === void 0) return;
|
|
1189
1461
|
if (typeof node === "string") {
|
|
@@ -1212,58 +1484,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
1212
1484
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
1213
1485
|
}
|
|
1214
1486
|
}
|
|
1215
|
-
function
|
|
1216
|
-
if (!
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1487
|
+
function validateFilterTokens(stack) {
|
|
1488
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1489
|
+
const out = [];
|
|
1490
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
1491
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
1492
|
+
});
|
|
1493
|
+
return out;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/validate-empty-combinators.ts
|
|
1497
|
+
import { reduceFilterVerdict } from "@objectstack/spec/data";
|
|
1498
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1499
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1500
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
1501
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1502
|
+
{ key: "objects", kind: "object" },
|
|
1503
|
+
{ key: "views", kind: "view" },
|
|
1504
|
+
{ key: "reports", kind: "report" },
|
|
1505
|
+
{ key: "datasets", kind: "dataset" },
|
|
1506
|
+
{ key: "pages", kind: "page" },
|
|
1507
|
+
{ key: "apps", kind: "app" },
|
|
1508
|
+
{ key: "flows", kind: "flow" }
|
|
1509
|
+
];
|
|
1510
|
+
function isFilterNode(value) {
|
|
1511
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1512
|
+
const proto = Object.getPrototypeOf(value);
|
|
1513
|
+
return proto === Object.prototype || proto === null;
|
|
1514
|
+
}
|
|
1515
|
+
var VERDICT_OF = {
|
|
1516
|
+
$and: reduceFilterVerdict({ $and: [] }),
|
|
1517
|
+
$or: reduceFilterVerdict({ $or: [] }),
|
|
1518
|
+
$not: reduceFilterVerdict({ $not: {} }),
|
|
1519
|
+
node: reduceFilterVerdict({}),
|
|
1520
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1521
|
+
orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
|
|
1522
|
+
};
|
|
1523
|
+
function rows(verdict) {
|
|
1524
|
+
if (verdict === "true") return "matches EVERY row";
|
|
1525
|
+
if (verdict === "false") return "matches NO row";
|
|
1526
|
+
return "carries a real predicate";
|
|
1527
|
+
}
|
|
1528
|
+
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.";
|
|
1529
|
+
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).';
|
|
1530
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
1531
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
1532
|
+
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.`;
|
|
1533
|
+
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}`;
|
|
1534
|
+
ctx.out.push({
|
|
1535
|
+
severity: "error",
|
|
1536
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
1537
|
+
where: ctx.where,
|
|
1538
|
+
path,
|
|
1539
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
1540
|
+
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.`
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
function emitEmptyNode(position, path, ctx) {
|
|
1544
|
+
if (position === "root") {
|
|
1545
|
+
ctx.out.push({
|
|
1546
|
+
severity: "error",
|
|
1547
|
+
rule: FILTER_EMPTY_NODE,
|
|
1548
|
+
where: ctx.where,
|
|
1549
|
+
path,
|
|
1550
|
+
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.`,
|
|
1551
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
1552
|
+
});
|
|
1221
1553
|
return;
|
|
1222
1554
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1555
|
+
if (position === "or-branch") {
|
|
1556
|
+
ctx.out.push({
|
|
1557
|
+
severity: "error",
|
|
1558
|
+
rule: FILTER_EMPTY_NODE,
|
|
1559
|
+
where: ctx.where,
|
|
1560
|
+
path,
|
|
1561
|
+
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.`,
|
|
1562
|
+
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.)"
|
|
1563
|
+
});
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
ctx.out.push({
|
|
1567
|
+
severity: "error",
|
|
1568
|
+
rule: FILTER_EMPTY_NODE,
|
|
1569
|
+
where: ctx.where,
|
|
1570
|
+
path,
|
|
1571
|
+
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.",
|
|
1572
|
+
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."
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
function scanNodeKeys(node, path, ctx) {
|
|
1576
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1577
|
+
if (key === "$and" || key === "$or") {
|
|
1578
|
+
if (!Array.isArray(value)) continue;
|
|
1579
|
+
if (value.length === 0) {
|
|
1580
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
value.forEach((element, index) => {
|
|
1584
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
1585
|
+
});
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (key === "$not") {
|
|
1589
|
+
if (!isFilterNode(value)) continue;
|
|
1590
|
+
if (Object.keys(value).length === 0) {
|
|
1591
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
1227
1595
|
continue;
|
|
1228
1596
|
}
|
|
1229
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
1230
1597
|
}
|
|
1231
1598
|
}
|
|
1232
|
-
function
|
|
1599
|
+
function scanBranch(value, path, position, ctx) {
|
|
1600
|
+
if (!isFilterNode(value)) return;
|
|
1601
|
+
if (Object.keys(value).length === 0) {
|
|
1602
|
+
emitEmptyNode(position, path, ctx);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
scanNodeKeys(value, path, ctx);
|
|
1606
|
+
}
|
|
1607
|
+
function validateEmptyCombinators(stack) {
|
|
1233
1608
|
if (!stack || typeof stack !== "object") return [];
|
|
1234
1609
|
const out = [];
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
["views", "view"],
|
|
1239
|
-
["reports", "report"],
|
|
1240
|
-
["datasets", "dataset"],
|
|
1241
|
-
["pages", "page"],
|
|
1242
|
-
["apps", "app"]
|
|
1243
|
-
];
|
|
1244
|
-
for (const [key, kind] of surfaces) {
|
|
1245
|
-
const items = asArray5(stack[key]);
|
|
1246
|
-
items.forEach((item, i) => {
|
|
1247
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
1248
|
-
if (kind === "dashboard") {
|
|
1249
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1250
|
-
widgets.forEach((w, wi) => {
|
|
1251
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1252
|
-
scanForFilters(
|
|
1253
|
-
w,
|
|
1254
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
1255
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1256
|
-
out,
|
|
1257
|
-
/* @__PURE__ */ new Set()
|
|
1258
|
-
);
|
|
1259
|
-
});
|
|
1260
|
-
const { widgets: _skip, ...rest } = item;
|
|
1261
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1610
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
1611
|
+
scanBranch(value, path, "root", { where, out });
|
|
1612
|
+
});
|
|
1267
1613
|
return out;
|
|
1268
1614
|
}
|
|
1269
1615
|
|
|
@@ -1463,6 +1809,7 @@ function validateObjectReferences(stack) {
|
|
|
1463
1809
|
// src/validate-searchable-fields.ts
|
|
1464
1810
|
import {
|
|
1465
1811
|
resolveSearchFieldResolution,
|
|
1812
|
+
isVirtualSearchField,
|
|
1466
1813
|
SEARCHABLE_TEXTUAL_TYPES,
|
|
1467
1814
|
SEARCHABLE_ENUM_TYPES,
|
|
1468
1815
|
SEARCH_AUTO_EXCLUDED_FIELDS
|
|
@@ -1476,7 +1823,7 @@ function asArray7(v) {
|
|
|
1476
1823
|
}
|
|
1477
1824
|
return [];
|
|
1478
1825
|
}
|
|
1479
|
-
function
|
|
1826
|
+
function isRec3(v) {
|
|
1480
1827
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1481
1828
|
}
|
|
1482
1829
|
function strName3(v) {
|
|
@@ -1552,7 +1899,7 @@ function distance2(a, b) {
|
|
|
1552
1899
|
}
|
|
1553
1900
|
function indexObjectSearchTargets(stack) {
|
|
1554
1901
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1555
|
-
if (!
|
|
1902
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1556
1903
|
for (const obj of asArray7(stack.objects)) {
|
|
1557
1904
|
const name = strName3(obj.name);
|
|
1558
1905
|
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
@@ -1580,7 +1927,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1580
1927
|
where,
|
|
1581
1928
|
path: `${path}[${i}]`,
|
|
1582
1929
|
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)),
|
|
1583
|
-
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
|
|
1930
|
+
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(", ")}.` : "")
|
|
1931
|
+
});
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (isVirtualSearchField(target.fields[name])) {
|
|
1935
|
+
const vtype = target.fields[name]?.type;
|
|
1936
|
+
findings.push({
|
|
1937
|
+
severity: "error",
|
|
1938
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1939
|
+
where,
|
|
1940
|
+
path: `${path}[${i}]`,
|
|
1941
|
+
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).`,
|
|
1942
|
+
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).`
|
|
1584
1943
|
});
|
|
1585
1944
|
continue;
|
|
1586
1945
|
}
|
|
@@ -1615,14 +1974,14 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1615
1974
|
where,
|
|
1616
1975
|
path: `${path}[${i}]`,
|
|
1617
1976
|
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...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).`,
|
|
1618
|
-
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
|
|
1977
|
+
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.`
|
|
1619
1978
|
});
|
|
1620
1979
|
}
|
|
1621
1980
|
return findings;
|
|
1622
1981
|
}
|
|
1623
1982
|
function validateSearchableFields(stack) {
|
|
1624
1983
|
const findings = [];
|
|
1625
|
-
if (!
|
|
1984
|
+
if (!isRec3(stack)) return findings;
|
|
1626
1985
|
const objects = asArray7(stack.objects);
|
|
1627
1986
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1628
1987
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
@@ -1632,7 +1991,7 @@ function validateSearchableFields(stack) {
|
|
|
1632
1991
|
};
|
|
1633
1992
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1634
1993
|
const obj = objects[oi];
|
|
1635
|
-
if (!
|
|
1994
|
+
if (!isRec3(obj)) continue;
|
|
1636
1995
|
const objName = strName3(obj.name);
|
|
1637
1996
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1638
1997
|
check(
|
|
@@ -1643,9 +2002,9 @@ function validateSearchableFields(stack) {
|
|
|
1643
2002
|
"searchableFields",
|
|
1644
2003
|
"canonical"
|
|
1645
2004
|
);
|
|
1646
|
-
if (
|
|
2005
|
+
if (isRec3(obj.listViews)) {
|
|
1647
2006
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1648
|
-
if (!
|
|
2007
|
+
if (!isRec3(lv)) continue;
|
|
1649
2008
|
check(
|
|
1650
2009
|
lv.searchableFields,
|
|
1651
2010
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1662,10 +2021,10 @@ function validateSearchableFields(stack) {
|
|
|
1662
2021
|
const views = asArray7(stack.views);
|
|
1663
2022
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1664
2023
|
const view = views[vi];
|
|
1665
|
-
if (!
|
|
2024
|
+
if (!isRec3(view)) continue;
|
|
1666
2025
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
1667
2026
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
1668
|
-
if (
|
|
2027
|
+
if (isRec3(view.list)) {
|
|
1669
2028
|
check(
|
|
1670
2029
|
view.list.searchableFields,
|
|
1671
2030
|
listViewObject(view.list) ?? viewObject,
|
|
@@ -1675,9 +2034,9 @@ function validateSearchableFields(stack) {
|
|
|
1675
2034
|
"narrowing"
|
|
1676
2035
|
);
|
|
1677
2036
|
}
|
|
1678
|
-
if (
|
|
2037
|
+
if (isRec3(view.listViews)) {
|
|
1679
2038
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1680
|
-
if (!
|
|
2039
|
+
if (!isRec3(lv)) continue;
|
|
1681
2040
|
check(
|
|
1682
2041
|
lv.searchableFields,
|
|
1683
2042
|
listViewObject(lv) ?? viewObject,
|
|
@@ -1693,11 +2052,11 @@ function validateSearchableFields(stack) {
|
|
|
1693
2052
|
}
|
|
1694
2053
|
function listViewObject(listView) {
|
|
1695
2054
|
const data = listView.data;
|
|
1696
|
-
return
|
|
2055
|
+
return isRec3(data) ? strName3(data.object) : void 0;
|
|
1697
2056
|
}
|
|
1698
2057
|
|
|
1699
2058
|
// src/page-walk.ts
|
|
1700
|
-
function
|
|
2059
|
+
function isRec4(v) {
|
|
1701
2060
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1702
2061
|
}
|
|
1703
2062
|
function strName4(v) {
|
|
@@ -1710,19 +2069,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1710
2069
|
}
|
|
1711
2070
|
function walkPageComponents(page, pagePath) {
|
|
1712
2071
|
const out = [];
|
|
1713
|
-
if (!
|
|
2072
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1714
2073
|
const pageObject = strName4(page.object);
|
|
1715
2074
|
const visit = (node, path, inheritedObject) => {
|
|
1716
|
-
if (!
|
|
1717
|
-
const props =
|
|
1718
|
-
const dataSource =
|
|
2075
|
+
if (!isRec4(node)) return;
|
|
2076
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2077
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1719
2078
|
const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
|
|
1720
2079
|
out.push({ component: node, path, objectName });
|
|
1721
2080
|
if (!props) return;
|
|
1722
2081
|
if (Array.isArray(props.items)) {
|
|
1723
2082
|
for (let i = 0; i < props.items.length; i++) {
|
|
1724
2083
|
const item = props.items[i];
|
|
1725
|
-
if (!
|
|
2084
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1726
2085
|
for (let c = 0; c < item.children.length; c++) {
|
|
1727
2086
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1728
2087
|
}
|
|
@@ -1744,12 +2103,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1744
2103
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1745
2104
|
for (let r = 0; r < regions.length; r++) {
|
|
1746
2105
|
const region = regions[r];
|
|
1747
|
-
if (!
|
|
2106
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1748
2107
|
for (let c = 0; c < region.components.length; c++) {
|
|
1749
2108
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1750
2109
|
}
|
|
1751
2110
|
}
|
|
1752
|
-
const slots =
|
|
2111
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1753
2112
|
if (slots) {
|
|
1754
2113
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1755
2114
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -1934,6 +2293,15 @@ function validateActionNameRefs(stack) {
|
|
|
1934
2293
|
"Navigation action item"
|
|
1935
2294
|
);
|
|
1936
2295
|
}
|
|
2296
|
+
const runAction = strName5(nav.runAction);
|
|
2297
|
+
if (nav.type === "object" && runAction) {
|
|
2298
|
+
check(
|
|
2299
|
+
runAction,
|
|
2300
|
+
`app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
|
|
2301
|
+
`${navPath}.runAction`,
|
|
2302
|
+
"Navigation deep-link auto-run"
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
1937
2305
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
1938
2306
|
}
|
|
1939
2307
|
};
|
|
@@ -1948,6 +2316,7 @@ function validateActionNameRefs(stack) {
|
|
|
1948
2316
|
|
|
1949
2317
|
// src/validate-page-field-bindings.ts
|
|
1950
2318
|
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
2319
|
+
var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
|
|
1951
2320
|
function asArray9(v) {
|
|
1952
2321
|
if (Array.isArray(v)) return v;
|
|
1953
2322
|
if (v && typeof v === "object") {
|
|
@@ -1958,7 +2327,7 @@ function asArray9(v) {
|
|
|
1958
2327
|
function strName6(v) {
|
|
1959
2328
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1960
2329
|
}
|
|
1961
|
-
function
|
|
2330
|
+
function isRec5(v) {
|
|
1962
2331
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1963
2332
|
}
|
|
1964
2333
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -1969,7 +2338,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
1969
2338
|
out.push({ name: bare, path });
|
|
1970
2339
|
return;
|
|
1971
2340
|
}
|
|
1972
|
-
if (!
|
|
2341
|
+
if (!isRec5(v)) return;
|
|
1973
2342
|
const named = strName6(v.field) ?? strName6(v.name);
|
|
1974
2343
|
if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
|
|
1975
2344
|
};
|
|
@@ -1999,8 +2368,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
1999
2368
|
"element:number": { props: ["field"] },
|
|
2000
2369
|
"element:filter": { props: ["fields"] },
|
|
2001
2370
|
"element:form": { props: ["fields"] },
|
|
2002
|
-
//
|
|
2003
|
-
|
|
2371
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2372
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
2373
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
2374
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
2375
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
2376
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
2377
|
+
//
|
|
2378
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
2379
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
2380
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
2381
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
2382
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
2383
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
2384
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
2385
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2004
2386
|
};
|
|
2005
2387
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2006
2388
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2014,15 +2396,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2014
2396
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2015
2397
|
for (let si = 0; si < sections.length; si++) {
|
|
2016
2398
|
const section = sections[si];
|
|
2017
|
-
if (!
|
|
2399
|
+
if (!isRec5(section)) continue;
|
|
2018
2400
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2019
2401
|
}
|
|
2020
2402
|
}
|
|
2021
2403
|
return refs;
|
|
2022
2404
|
}
|
|
2023
2405
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2024
|
-
const add =
|
|
2025
|
-
const picker = add &&
|
|
2406
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2407
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
2026
2408
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2027
2409
|
return {
|
|
2028
2410
|
relatedObject: strName6(props.objectName),
|
|
@@ -2043,7 +2425,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2043
2425
|
}
|
|
2044
2426
|
function indexObjectFields(stack) {
|
|
2045
2427
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2046
|
-
if (!
|
|
2428
|
+
if (!isRec5(stack)) return objectFields;
|
|
2047
2429
|
for (const obj of asArray9(stack.objects)) {
|
|
2048
2430
|
const name = strName6(obj.name);
|
|
2049
2431
|
if (!name) continue;
|
|
@@ -2056,14 +2438,27 @@ function indexObjectFields(stack) {
|
|
|
2056
2438
|
}
|
|
2057
2439
|
return objectFields;
|
|
2058
2440
|
}
|
|
2059
|
-
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2441
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
|
|
2060
2442
|
const findings = [];
|
|
2061
2443
|
if (!objectName) return findings;
|
|
2062
2444
|
const known = objectFields.get(objectName);
|
|
2063
2445
|
if (!known) return findings;
|
|
2446
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2064
2447
|
for (const ref of refs) {
|
|
2065
2448
|
if (ref.name.includes(".")) continue;
|
|
2066
|
-
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name))
|
|
2449
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
|
|
2450
|
+
if (anchors?.has(ref.name)) {
|
|
2451
|
+
findings.push({
|
|
2452
|
+
severity: "warning",
|
|
2453
|
+
rule: PAGE_FIELD_UNPROVISIONED,
|
|
2454
|
+
where,
|
|
2455
|
+
path: ref.path,
|
|
2456
|
+
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."),
|
|
2457
|
+
hint: unprovisionedAnchorHint(objectName, ref.name)
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2067
2462
|
findings.push({
|
|
2068
2463
|
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2069
2464
|
rule: PAGE_FIELD_UNKNOWN,
|
|
@@ -2079,17 +2474,20 @@ function validatePageFieldBindings(stack) {
|
|
|
2079
2474
|
const findings = [];
|
|
2080
2475
|
if (!stack || typeof stack !== "object") return findings;
|
|
2081
2476
|
const objectFields = indexObjectFields(stack);
|
|
2477
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2082
2478
|
const pages = asArray9(stack.pages);
|
|
2083
2479
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2084
2480
|
const page = pages[pi];
|
|
2085
2481
|
if (!page || typeof page !== "object") continue;
|
|
2086
2482
|
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2087
2483
|
const checkRefs = (refs, objectName, where) => {
|
|
2088
|
-
findings.push(
|
|
2484
|
+
findings.push(
|
|
2485
|
+
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2486
|
+
);
|
|
2089
2487
|
};
|
|
2090
2488
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2091
2489
|
const type = strName6(component.type);
|
|
2092
|
-
const props =
|
|
2490
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
2093
2491
|
if (!type || !props) continue;
|
|
2094
2492
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2095
2493
|
const base = `${path}.properties`;
|
|
@@ -2104,7 +2502,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2104
2502
|
if (!refs) continue;
|
|
2105
2503
|
checkRefs(refs, objectName, where);
|
|
2106
2504
|
}
|
|
2107
|
-
const cfg =
|
|
2505
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2108
2506
|
if (cfg) {
|
|
2109
2507
|
const cfgObject = strName6(cfg.source) ?? strName6(page.object);
|
|
2110
2508
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -2113,7 +2511,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2113
2511
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2114
2512
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2115
2513
|
];
|
|
2116
|
-
const userFilters =
|
|
2514
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2117
2515
|
if (userFilters) {
|
|
2118
2516
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2119
2517
|
}
|
|
@@ -2141,7 +2539,7 @@ function strName7(v) {
|
|
|
2141
2539
|
function strList2(v) {
|
|
2142
2540
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2143
2541
|
}
|
|
2144
|
-
function
|
|
2542
|
+
function isRec6(v) {
|
|
2145
2543
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2146
2544
|
}
|
|
2147
2545
|
function distance4(a, b) {
|
|
@@ -2270,10 +2668,10 @@ function validateChartBindings(stack) {
|
|
|
2270
2668
|
const reports = asArray10(stack.reports);
|
|
2271
2669
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2272
2670
|
const report = reports[ri];
|
|
2273
|
-
if (!
|
|
2671
|
+
if (!isRec6(report)) continue;
|
|
2274
2672
|
const reportName = strName7(report.name) ?? `#${ri}`;
|
|
2275
2673
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2276
|
-
if (!
|
|
2674
|
+
if (!isRec6(chart)) return;
|
|
2277
2675
|
check({
|
|
2278
2676
|
dataset,
|
|
2279
2677
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
@@ -2297,7 +2695,7 @@ function validateChartBindings(stack) {
|
|
|
2297
2695
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2298
2696
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2299
2697
|
const block = blocks[bi];
|
|
2300
|
-
if (!
|
|
2698
|
+
if (!isRec6(block)) continue;
|
|
2301
2699
|
checkReportChart(
|
|
2302
2700
|
block.chart,
|
|
2303
2701
|
strName7(block.dataset),
|
|
@@ -2308,9 +2706,9 @@ function validateChartBindings(stack) {
|
|
|
2308
2706
|
}
|
|
2309
2707
|
}
|
|
2310
2708
|
const checkListChart = (container, where, path) => {
|
|
2311
|
-
if (!
|
|
2709
|
+
if (!isRec6(container)) return;
|
|
2312
2710
|
const chart = container.chart;
|
|
2313
|
-
if (!
|
|
2711
|
+
if (!isRec6(chart)) return;
|
|
2314
2712
|
check({
|
|
2315
2713
|
dataset: strName7(chart.dataset),
|
|
2316
2714
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
@@ -2322,10 +2720,10 @@ function validateChartBindings(stack) {
|
|
|
2322
2720
|
const views = asArray10(stack.views);
|
|
2323
2721
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2324
2722
|
const view = views[vi];
|
|
2325
|
-
if (!
|
|
2723
|
+
if (!isRec6(view)) continue;
|
|
2326
2724
|
const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
|
|
2327
2725
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2328
|
-
if (
|
|
2726
|
+
if (isRec6(view.listViews)) {
|
|
2329
2727
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2330
2728
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2331
2729
|
}
|
|
@@ -2334,7 +2732,7 @@ function validateChartBindings(stack) {
|
|
|
2334
2732
|
const objects = asArray10(stack.objects);
|
|
2335
2733
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2336
2734
|
const obj = objects[oi];
|
|
2337
|
-
if (!
|
|
2735
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
2338
2736
|
const objName = strName7(obj.name) ?? `#${oi}`;
|
|
2339
2737
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2340
2738
|
checkListChart(
|
|
@@ -2347,10 +2745,10 @@ function validateChartBindings(stack) {
|
|
|
2347
2745
|
const pages = asArray10(stack.pages);
|
|
2348
2746
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2349
2747
|
const page = pages[pi];
|
|
2350
|
-
if (!
|
|
2748
|
+
if (!isRec6(page)) continue;
|
|
2351
2749
|
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2352
2750
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2353
|
-
const props =
|
|
2751
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2354
2752
|
if (!props || !strName7(props.dataset)) continue;
|
|
2355
2753
|
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
2356
2754
|
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
@@ -2500,10 +2898,10 @@ function validateNavAccess(stack) {
|
|
|
2500
2898
|
|
|
2501
2899
|
// src/validate-nav-target-refs.ts
|
|
2502
2900
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2503
|
-
var
|
|
2901
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2504
2902
|
function asArray13(v) {
|
|
2505
|
-
if (Array.isArray(v)) return v.filter(
|
|
2506
|
-
if (
|
|
2903
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
2904
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
2507
2905
|
return [];
|
|
2508
2906
|
}
|
|
2509
2907
|
function strName9(v) {
|
|
@@ -2525,7 +2923,7 @@ function namesOf(collection) {
|
|
|
2525
2923
|
}
|
|
2526
2924
|
function validateNavTargetRefs(stack) {
|
|
2527
2925
|
const findings = [];
|
|
2528
|
-
if (!
|
|
2926
|
+
if (!isRec7(stack)) return findings;
|
|
2529
2927
|
const apps = asArray13(stack.apps);
|
|
2530
2928
|
if (apps.length === 0) return findings;
|
|
2531
2929
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2537,7 +2935,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2537
2935
|
const walk = (items, basePath) => {
|
|
2538
2936
|
if (!Array.isArray(items)) return;
|
|
2539
2937
|
for (const [ni, raw] of items.entries()) {
|
|
2540
|
-
if (!
|
|
2938
|
+
if (!isRec7(raw)) continue;
|
|
2541
2939
|
const nav = raw;
|
|
2542
2940
|
const navPath = `${basePath}[${ni}]`;
|
|
2543
2941
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -2568,34 +2966,141 @@ function validateNavTargetRefs(stack) {
|
|
|
2568
2966
|
return findings;
|
|
2569
2967
|
}
|
|
2570
2968
|
|
|
2571
|
-
// src/validate-
|
|
2572
|
-
import {
|
|
2573
|
-
var
|
|
2574
|
-
var
|
|
2575
|
-
function isRec7(v) {
|
|
2576
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2577
|
-
}
|
|
2969
|
+
// src/validate-nav-object-servability.ts
|
|
2970
|
+
import { canServeApiOperation } from "@objectstack/spec/data";
|
|
2971
|
+
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2972
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2578
2973
|
function asArray14(v) {
|
|
2579
|
-
if (Array.isArray(v)) return v;
|
|
2580
|
-
if (
|
|
2974
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
2975
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2581
2976
|
return [];
|
|
2582
2977
|
}
|
|
2583
2978
|
function strName10(v) {
|
|
2584
2979
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2585
2980
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const
|
|
2589
|
-
if (
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2981
|
+
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2982
|
+
function validateNavObjectServability(stack) {
|
|
2983
|
+
const findings = [];
|
|
2984
|
+
if (!isRec8(stack)) return findings;
|
|
2985
|
+
const apps = asArray14(stack.apps);
|
|
2986
|
+
if (apps.length === 0) return findings;
|
|
2987
|
+
const ownEnable = /* @__PURE__ */ new Map();
|
|
2988
|
+
const objects = asArray14(stack.objects);
|
|
2989
|
+
for (const [oi, obj] of objects.entries()) {
|
|
2990
|
+
const n = strName10(obj.name);
|
|
2991
|
+
if (!n) continue;
|
|
2992
|
+
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
2993
|
+
}
|
|
2994
|
+
if (ownEnable.size === 0) return findings;
|
|
2995
|
+
for (const [ai, app] of apps.entries()) {
|
|
2996
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2997
|
+
const walk = (items, basePath) => {
|
|
2998
|
+
if (!Array.isArray(items)) return;
|
|
2999
|
+
for (const [ni, raw] of items.entries()) {
|
|
3000
|
+
if (!isRec8(raw)) continue;
|
|
3001
|
+
const nav = raw;
|
|
3002
|
+
const navPath = `${basePath}[${ni}]`;
|
|
3003
|
+
if (nav.type === "object") {
|
|
3004
|
+
const target = strName10(nav.objectName);
|
|
3005
|
+
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3006
|
+
if (target && declared && !canServeApiOperation(declared.enable, "list")) {
|
|
3007
|
+
const enable = isRec8(declared.enable) ? declared.enable : {};
|
|
3008
|
+
const apiDisabled = enable.apiEnabled === false;
|
|
3009
|
+
const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
|
|
3010
|
+
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
3011
|
+
const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
|
|
3012
|
+
findings.push({
|
|
3013
|
+
severity: "error",
|
|
3014
|
+
rule: NAV_OBJECT_UNSERVABLE,
|
|
3015
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
3016
|
+
// The nav entry is where the dead row is authored; the `enable`
|
|
3017
|
+
// key that condemns it is named in the message, because the fix
|
|
3018
|
+
// may belong at either end.
|
|
3019
|
+
path: `${navPath}.objectName`,
|
|
3020
|
+
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.`,
|
|
3021
|
+
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.`
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
3029
|
+
for (const [ari, area] of asArray14(app.areas).entries()) {
|
|
3030
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
3031
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
2597
3032
|
}
|
|
2598
|
-
|
|
3033
|
+
}
|
|
3034
|
+
return findings;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
// src/validate-translation-references.ts
|
|
3038
|
+
import { expandViewContainer } from "@objectstack/spec";
|
|
3039
|
+
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
3040
|
+
|
|
3041
|
+
// src/view-walk.ts
|
|
3042
|
+
function isRec9(v) {
|
|
3043
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3044
|
+
}
|
|
3045
|
+
function strName11(v) {
|
|
3046
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3047
|
+
}
|
|
3048
|
+
function viewObjectName(view) {
|
|
3049
|
+
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
|
|
3050
|
+
}
|
|
3051
|
+
function viewContainerSites(view, basePath) {
|
|
3052
|
+
if (!isRec9(view)) return [];
|
|
3053
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3054
|
+
if (isRec9(view.form)) {
|
|
3055
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3056
|
+
}
|
|
3057
|
+
for (const key of ["listViews", "formViews"]) {
|
|
3058
|
+
const container = view[key];
|
|
3059
|
+
if (!isRec9(container)) continue;
|
|
3060
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
3061
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
3062
|
+
if (!isRec9(sub)) continue;
|
|
3063
|
+
sites.push({
|
|
3064
|
+
view: sub,
|
|
3065
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
3066
|
+
surface: `${key}.${subKey}`,
|
|
3067
|
+
kind
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return sites;
|
|
3072
|
+
}
|
|
3073
|
+
function formViewSites(view, basePath) {
|
|
3074
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// src/validate-translation-references.ts
|
|
3078
|
+
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
3079
|
+
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
3080
|
+
function isRec10(v) {
|
|
3081
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3082
|
+
}
|
|
3083
|
+
function asArray15(v) {
|
|
3084
|
+
if (Array.isArray(v)) return v;
|
|
3085
|
+
if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
|
|
3086
|
+
return [];
|
|
3087
|
+
}
|
|
3088
|
+
function strName12(v) {
|
|
3089
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3090
|
+
}
|
|
3091
|
+
function distance5(a, b) {
|
|
3092
|
+
const m = a.length;
|
|
3093
|
+
const n = b.length;
|
|
3094
|
+
if (m === 0) return n;
|
|
3095
|
+
if (n === 0) return m;
|
|
3096
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
3097
|
+
for (let i = 1; i <= m; i++) {
|
|
3098
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
3099
|
+
for (let j = 1; j <= n; j++) {
|
|
3100
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3101
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
3102
|
+
}
|
|
3103
|
+
prev = curr;
|
|
2599
3104
|
}
|
|
2600
3105
|
return prev[n];
|
|
2601
3106
|
}
|
|
@@ -2640,30 +3145,52 @@ function collectViewRecord(view, factsFor) {
|
|
|
2640
3145
|
};
|
|
2641
3146
|
const addSections = (container, binding) => {
|
|
2642
3147
|
if (!binding) return;
|
|
2643
|
-
for (const section of
|
|
2644
|
-
const sectionName =
|
|
3148
|
+
for (const section of asArray15(container.sections)) {
|
|
3149
|
+
const sectionName = strName12(section.name);
|
|
2645
3150
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2646
3151
|
}
|
|
2647
3152
|
};
|
|
2648
|
-
const listBinding =
|
|
2649
|
-
if (
|
|
2650
|
-
addView(recordObject ?? listBinding,
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3153
|
+
const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
|
|
3154
|
+
if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3155
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
3156
|
+
const named = namedViewKeys(view);
|
|
3157
|
+
for (const family of ["listViews", "formViews"]) {
|
|
3158
|
+
const container = view[family];
|
|
3159
|
+
if (!isRec10(container)) continue;
|
|
3160
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
3161
|
+
let at = 0;
|
|
3162
|
+
for (const sub of Object.values(container)) {
|
|
3163
|
+
if (!sub || typeof sub !== "object") continue;
|
|
3164
|
+
const registryKey = registryKeys[at++];
|
|
3165
|
+
if (!isRec10(sub)) continue;
|
|
2656
3166
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2657
|
-
addView(binding,
|
|
2658
|
-
addView(binding, strName10(sub.name));
|
|
3167
|
+
addView(binding, registryKey);
|
|
2659
3168
|
addSections(sub, binding);
|
|
2660
3169
|
}
|
|
2661
3170
|
}
|
|
2662
|
-
if (
|
|
3171
|
+
if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2663
3172
|
addSections(view, recordObject ?? listBinding);
|
|
2664
3173
|
}
|
|
2665
|
-
function
|
|
2666
|
-
|
|
3174
|
+
function defaultListViewKey(object, container) {
|
|
3175
|
+
if (!object || !isRec10(container.list)) return void 0;
|
|
3176
|
+
const item = expandViewContainer(object, container).find(
|
|
3177
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
3178
|
+
);
|
|
3179
|
+
if (!item) return void 0;
|
|
3180
|
+
const prefix = `${object}.`;
|
|
3181
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
3182
|
+
}
|
|
3183
|
+
function namedViewKeys(container) {
|
|
3184
|
+
const object = "probe";
|
|
3185
|
+
const prefix = `${object}.`;
|
|
3186
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
3187
|
+
const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
3188
|
+
const listCount = countEntries(container.listViews);
|
|
3189
|
+
const formCount = countEntries(container.formViews);
|
|
3190
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
3191
|
+
const items = expandViewContainer(object, container);
|
|
3192
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
3193
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
2667
3194
|
}
|
|
2668
3195
|
function readOptions(field) {
|
|
2669
3196
|
const raw = field.options;
|
|
@@ -2675,14 +3202,14 @@ function readOptions(field) {
|
|
|
2675
3202
|
values.add(opt);
|
|
2676
3203
|
continue;
|
|
2677
3204
|
}
|
|
2678
|
-
if (!
|
|
2679
|
-
const value =
|
|
3205
|
+
if (!isRec10(opt)) continue;
|
|
3206
|
+
const value = strName12(opt.value);
|
|
2680
3207
|
if (!value) continue;
|
|
2681
3208
|
values.add(value);
|
|
2682
|
-
const label2 =
|
|
3209
|
+
const label2 = strName12(opt.label);
|
|
2683
3210
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2684
3211
|
}
|
|
2685
|
-
} else if (
|
|
3212
|
+
} else if (isRec10(raw)) {
|
|
2686
3213
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2687
3214
|
values.add(value);
|
|
2688
3215
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2702,48 +3229,48 @@ function buildUniverse(stack) {
|
|
|
2702
3229
|
}
|
|
2703
3230
|
return facts;
|
|
2704
3231
|
};
|
|
2705
|
-
for (const obj of
|
|
2706
|
-
const objectName =
|
|
3232
|
+
for (const obj of asArray15(stack.objects)) {
|
|
3233
|
+
const objectName = strName12(obj.name);
|
|
2707
3234
|
if (!objectName) continue;
|
|
2708
3235
|
const facts = factsFor(objectName);
|
|
2709
|
-
for (const field of
|
|
2710
|
-
const fieldName =
|
|
3236
|
+
for (const field of asArray15(obj.fields)) {
|
|
3237
|
+
const fieldName = strName12(field.name);
|
|
2711
3238
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
2712
3239
|
}
|
|
2713
|
-
for (const action of
|
|
2714
|
-
const actionName =
|
|
3240
|
+
for (const action of asArray15(obj.actions)) {
|
|
3241
|
+
const actionName = strName12(action.name);
|
|
2715
3242
|
if (actionName) facts.actions.set(actionName, action);
|
|
2716
3243
|
}
|
|
2717
|
-
for (const view of
|
|
2718
|
-
collectViewRecord({ ...view, object:
|
|
3244
|
+
for (const view of asArray15(obj.views)) {
|
|
3245
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
2719
3246
|
}
|
|
2720
3247
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
2721
|
-
for (const group of
|
|
2722
|
-
const key =
|
|
3248
|
+
for (const group of asArray15(obj.fieldGroups)) {
|
|
3249
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
2723
3250
|
if (key) facts.sections.add(key);
|
|
2724
3251
|
}
|
|
2725
3252
|
}
|
|
2726
|
-
for (const view of
|
|
3253
|
+
for (const view of asArray15(stack.views)) {
|
|
2727
3254
|
collectViewRecord(view, factsFor);
|
|
2728
3255
|
}
|
|
2729
|
-
const pages =
|
|
3256
|
+
const pages = asArray15(stack.pages);
|
|
2730
3257
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2731
3258
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
2732
3259
|
if (!walked.objectName) continue;
|
|
2733
|
-
const props =
|
|
3260
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
2734
3261
|
if (!props) continue;
|
|
2735
|
-
for (const section of
|
|
2736
|
-
const sectionName =
|
|
3262
|
+
for (const section of asArray15(props.sections)) {
|
|
3263
|
+
const sectionName = strName12(section.name);
|
|
2737
3264
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
2738
3265
|
}
|
|
2739
3266
|
}
|
|
2740
3267
|
}
|
|
2741
3268
|
const globalActions = /* @__PURE__ */ new Map();
|
|
2742
3269
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
2743
|
-
for (const action of
|
|
2744
|
-
const actionName =
|
|
3270
|
+
for (const action of asArray15(stack.actions)) {
|
|
3271
|
+
const actionName = strName12(action.name);
|
|
2745
3272
|
if (!actionName) continue;
|
|
2746
|
-
const owner =
|
|
3273
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
2747
3274
|
if (owner) {
|
|
2748
3275
|
factsFor(owner).actions.set(actionName, action);
|
|
2749
3276
|
actionOwners.set(actionName, owner);
|
|
@@ -2757,41 +3284,41 @@ function buildUniverse(stack) {
|
|
|
2757
3284
|
}
|
|
2758
3285
|
}
|
|
2759
3286
|
const apps = /* @__PURE__ */ new Map();
|
|
2760
|
-
for (const app of
|
|
2761
|
-
const appName =
|
|
3287
|
+
for (const app of asArray15(stack.apps)) {
|
|
3288
|
+
const appName = strName12(app.name);
|
|
2762
3289
|
if (!appName) continue;
|
|
2763
3290
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
2764
3291
|
const walkNav = (items) => {
|
|
2765
|
-
for (const item of
|
|
2766
|
-
const id =
|
|
3292
|
+
for (const item of asArray15(items)) {
|
|
3293
|
+
const id = strName12(item.id);
|
|
2767
3294
|
if (id) navIds.add(id);
|
|
2768
3295
|
if (item.children) walkNav(item.children);
|
|
2769
3296
|
}
|
|
2770
3297
|
};
|
|
2771
3298
|
walkNav(app.navigation);
|
|
2772
|
-
for (const area of
|
|
2773
|
-
const areaId =
|
|
3299
|
+
for (const area of asArray15(app.areas)) {
|
|
3300
|
+
const areaId = strName12(area.id);
|
|
2774
3301
|
if (areaId) navIds.add(areaId);
|
|
2775
3302
|
walkNav(area.navigation);
|
|
2776
3303
|
}
|
|
2777
3304
|
apps.set(appName, navIds);
|
|
2778
3305
|
}
|
|
2779
3306
|
const dashboards = /* @__PURE__ */ new Map();
|
|
2780
|
-
for (const dash of
|
|
2781
|
-
const dashName =
|
|
3307
|
+
for (const dash of asArray15(stack.dashboards)) {
|
|
3308
|
+
const dashName = strName12(dash.name);
|
|
2782
3309
|
if (!dashName) continue;
|
|
2783
3310
|
const widgets = /* @__PURE__ */ new Set();
|
|
2784
|
-
for (const widget of
|
|
2785
|
-
const id =
|
|
3311
|
+
for (const widget of asArray15(dash.widgets)) {
|
|
3312
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
2786
3313
|
if (id) widgets.add(id);
|
|
2787
3314
|
}
|
|
2788
3315
|
const actions = /* @__PURE__ */ new Set();
|
|
2789
3316
|
const headerActions = [
|
|
2790
|
-
...
|
|
2791
|
-
...
|
|
3317
|
+
...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
|
|
3318
|
+
...asArray15(dash.actions)
|
|
2792
3319
|
];
|
|
2793
3320
|
for (const action of headerActions) {
|
|
2794
|
-
const key =
|
|
3321
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
2795
3322
|
if (key) actions.add(key);
|
|
2796
3323
|
}
|
|
2797
3324
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -2803,7 +3330,7 @@ function localePath(bundleIndex, locale) {
|
|
|
2803
3330
|
}
|
|
2804
3331
|
function validateTranslationReferences(stack) {
|
|
2805
3332
|
const findings = [];
|
|
2806
|
-
if (!
|
|
3333
|
+
if (!isRec10(stack)) return findings;
|
|
2807
3334
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
2808
3335
|
if (bundles.length === 0) return findings;
|
|
2809
3336
|
const universe = buildUniverse(stack);
|
|
@@ -2812,13 +3339,13 @@ function validateTranslationReferences(stack) {
|
|
|
2812
3339
|
};
|
|
2813
3340
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
2814
3341
|
const bundle = bundles[bi];
|
|
2815
|
-
if (!
|
|
3342
|
+
if (!isRec10(bundle)) continue;
|
|
2816
3343
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
2817
|
-
if (!
|
|
3344
|
+
if (!isRec10(rawData)) continue;
|
|
2818
3345
|
const base = localePath(bi, locale);
|
|
2819
3346
|
const inLocale = `locale "${locale}"`;
|
|
2820
3347
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
2821
|
-
if (!
|
|
3348
|
+
if (!isRec10(rawNode)) continue;
|
|
2822
3349
|
const objPath = `${base}.objects.${objectName}`;
|
|
2823
3350
|
const facts = universe.objects.get(objectName);
|
|
2824
3351
|
if (!facts) {
|
|
@@ -2844,7 +3371,7 @@ function validateTranslationReferences(stack) {
|
|
|
2844
3371
|
);
|
|
2845
3372
|
continue;
|
|
2846
3373
|
}
|
|
2847
|
-
if (!
|
|
3374
|
+
if (!isRec10(rawField)) continue;
|
|
2848
3375
|
checkOptionKeys(findings, {
|
|
2849
3376
|
optionMap: rawField.options,
|
|
2850
3377
|
field,
|
|
@@ -2926,7 +3453,7 @@ function validateTranslationReferences(stack) {
|
|
|
2926
3453
|
);
|
|
2927
3454
|
continue;
|
|
2928
3455
|
}
|
|
2929
|
-
if (!
|
|
3456
|
+
if (!isRec10(rawApp)) continue;
|
|
2930
3457
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
2931
3458
|
if (navIds.has(navId)) continue;
|
|
2932
3459
|
orphan(
|
|
@@ -2949,7 +3476,7 @@ function validateTranslationReferences(stack) {
|
|
|
2949
3476
|
);
|
|
2950
3477
|
continue;
|
|
2951
3478
|
}
|
|
2952
|
-
if (!
|
|
3479
|
+
if (!isRec10(rawDash)) continue;
|
|
2953
3480
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
2954
3481
|
if (dash.widgets.has(widgetId)) continue;
|
|
2955
3482
|
orphan(
|
|
@@ -2974,7 +3501,7 @@ function validateTranslationReferences(stack) {
|
|
|
2974
3501
|
return findings;
|
|
2975
3502
|
}
|
|
2976
3503
|
function asRecord(v) {
|
|
2977
|
-
return
|
|
3504
|
+
return isRec10(v) ? v : {};
|
|
2978
3505
|
}
|
|
2979
3506
|
function checkOptionKeys(findings, ctx) {
|
|
2980
3507
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -2986,7 +3513,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
2986
3513
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
2987
3514
|
where: ctx.where,
|
|
2988
3515
|
path: ctx.path,
|
|
2989
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3516
|
+
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.`,
|
|
2990
3517
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
2991
3518
|
});
|
|
2992
3519
|
return;
|
|
@@ -3005,11 +3532,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3005
3532
|
}
|
|
3006
3533
|
}
|
|
3007
3534
|
function checkActionParams(findings, ctx) {
|
|
3008
|
-
const rawParams = Object.keys(asRecord(
|
|
3535
|
+
const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3009
3536
|
if (rawParams.length === 0) return;
|
|
3010
3537
|
const declared = /* @__PURE__ */ new Set();
|
|
3011
|
-
for (const param of
|
|
3012
|
-
const name =
|
|
3538
|
+
for (const param of asArray15(ctx.action.params)) {
|
|
3539
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
3013
3540
|
if (name) declared.add(name);
|
|
3014
3541
|
}
|
|
3015
3542
|
for (const paramName of rawParams) {
|
|
@@ -3025,78 +3552,60 @@ function checkActionParams(findings, ctx) {
|
|
|
3025
3552
|
}
|
|
3026
3553
|
}
|
|
3027
3554
|
|
|
3028
|
-
// src/
|
|
3029
|
-
|
|
3030
|
-
function isRec8(v) {
|
|
3555
|
+
// src/collection-entries.ts
|
|
3556
|
+
function isRec11(v) {
|
|
3031
3557
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3032
3558
|
}
|
|
3033
|
-
function strName11(v) {
|
|
3034
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3035
|
-
}
|
|
3036
|
-
function viewObjectName2(view) {
|
|
3037
|
-
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
|
|
3038
|
-
}
|
|
3039
3559
|
function collectionEntries(v, base) {
|
|
3040
3560
|
if (Array.isArray(v)) {
|
|
3041
3561
|
const out = [];
|
|
3042
3562
|
for (let i = 0; i < v.length; i++) {
|
|
3043
|
-
if (
|
|
3563
|
+
if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3044
3564
|
}
|
|
3045
3565
|
return out;
|
|
3046
3566
|
}
|
|
3047
|
-
if (
|
|
3048
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3567
|
+
if (isRec11(v)) {
|
|
3568
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3049
3569
|
}
|
|
3050
3570
|
return [];
|
|
3051
3571
|
}
|
|
3572
|
+
|
|
3573
|
+
// src/validate-translatable-sections.ts
|
|
3574
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3575
|
+
function isRec12(v) {
|
|
3576
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3577
|
+
}
|
|
3578
|
+
function strName13(v) {
|
|
3579
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3580
|
+
}
|
|
3052
3581
|
function viewLabel(view) {
|
|
3053
|
-
const name =
|
|
3582
|
+
const name = strName13(view.name);
|
|
3054
3583
|
return name ? `view "${name}"` : "";
|
|
3055
3584
|
}
|
|
3056
3585
|
function joinWhere(...parts) {
|
|
3057
3586
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
3058
3587
|
}
|
|
3059
3588
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3060
|
-
const recordObject =
|
|
3061
|
-
const listBinding =
|
|
3062
|
-
const
|
|
3063
|
-
sites.push({
|
|
3064
|
-
path: `${basePath}.sections`,
|
|
3065
|
-
surface: label2,
|
|
3066
|
-
objectName: recordObject ?? listBinding,
|
|
3067
|
-
sections: view.sections
|
|
3068
|
-
});
|
|
3069
|
-
if (isRec8(view.form)) {
|
|
3589
|
+
const recordObject = viewObjectName(view);
|
|
3590
|
+
const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3591
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
3070
3592
|
sites.push({
|
|
3071
|
-
path: `${
|
|
3072
|
-
surface: joinWhere(label2,
|
|
3073
|
-
objectName:
|
|
3074
|
-
sections: view.
|
|
3593
|
+
path: `${site.path}.sections`,
|
|
3594
|
+
surface: joinWhere(label2, site.surface),
|
|
3595
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
3596
|
+
sections: site.view.sections
|
|
3075
3597
|
});
|
|
3076
3598
|
}
|
|
3077
|
-
for (const key of ["listViews", "formViews"]) {
|
|
3078
|
-
const container = view[key];
|
|
3079
|
-
if (!isRec8(container)) continue;
|
|
3080
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
3081
|
-
if (!isRec8(sub)) continue;
|
|
3082
|
-
sites.push({
|
|
3083
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
3084
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
3085
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
3086
|
-
sections: sub.sections
|
|
3087
|
-
});
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
3599
|
}
|
|
3091
3600
|
function translatedObjectNames(stack) {
|
|
3092
3601
|
const out = /* @__PURE__ */ new Set();
|
|
3093
3602
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3094
3603
|
for (const bundle of bundles) {
|
|
3095
|
-
if (!
|
|
3604
|
+
if (!isRec12(bundle)) continue;
|
|
3096
3605
|
for (const data of Object.values(bundle)) {
|
|
3097
|
-
if (!
|
|
3606
|
+
if (!isRec12(data) || !isRec12(data.objects)) continue;
|
|
3098
3607
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3099
|
-
if (
|
|
3608
|
+
if (isRec12(node)) out.add(objectName);
|
|
3100
3609
|
}
|
|
3101
3610
|
}
|
|
3102
3611
|
}
|
|
@@ -3108,22 +3617,22 @@ function suggestedName(label2) {
|
|
|
3108
3617
|
}
|
|
3109
3618
|
function validateTranslatableSections(stack) {
|
|
3110
3619
|
const findings = [];
|
|
3111
|
-
if (!
|
|
3620
|
+
if (!isRec12(stack)) return findings;
|
|
3112
3621
|
const translated = translatedObjectNames(stack);
|
|
3113
3622
|
if (translated.size === 0) return findings;
|
|
3114
3623
|
const sites = [];
|
|
3115
3624
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3116
|
-
const objectName =
|
|
3625
|
+
const objectName = strName13(obj.name);
|
|
3117
3626
|
if (!objectName) continue;
|
|
3118
3627
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3119
3628
|
collectViewSites(
|
|
3120
|
-
{ ...view, object:
|
|
3629
|
+
{ ...view, object: strName13(view.object) ?? objectName },
|
|
3121
3630
|
path,
|
|
3122
3631
|
viewLabel(view),
|
|
3123
3632
|
sites
|
|
3124
3633
|
);
|
|
3125
3634
|
}
|
|
3126
|
-
if (
|
|
3635
|
+
if (isRec12(obj.listViews)) {
|
|
3127
3636
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3128
3637
|
}
|
|
3129
3638
|
}
|
|
@@ -3131,13 +3640,13 @@ function validateTranslatableSections(stack) {
|
|
|
3131
3640
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3132
3641
|
}
|
|
3133
3642
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3134
|
-
const pageName =
|
|
3643
|
+
const pageName = strName13(page.name);
|
|
3135
3644
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3136
3645
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3137
3646
|
if (!walked.objectName) continue;
|
|
3138
|
-
const props =
|
|
3647
|
+
const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
|
|
3139
3648
|
if (!props) continue;
|
|
3140
|
-
const type =
|
|
3649
|
+
const type = strName13(walked.component.type) ?? "component";
|
|
3141
3650
|
sites.push({
|
|
3142
3651
|
path: `${walked.path}.properties.sections`,
|
|
3143
3652
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3152,9 +3661,9 @@ function validateTranslatableSections(stack) {
|
|
|
3152
3661
|
if (!Array.isArray(site.sections)) continue;
|
|
3153
3662
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3154
3663
|
const section = site.sections[i];
|
|
3155
|
-
if (!
|
|
3156
|
-
if (
|
|
3157
|
-
const heading =
|
|
3664
|
+
if (!isRec12(section)) continue;
|
|
3665
|
+
if (strName13(section.name)) continue;
|
|
3666
|
+
const heading = strName13(section.label);
|
|
3158
3667
|
if (!heading) continue;
|
|
3159
3668
|
const slug = suggestedName(heading);
|
|
3160
3669
|
findings.push({
|
|
@@ -3172,10 +3681,10 @@ function validateTranslatableSections(stack) {
|
|
|
3172
3681
|
|
|
3173
3682
|
// src/flow-walk.ts
|
|
3174
3683
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
3175
|
-
function
|
|
3684
|
+
function isRec13(v) {
|
|
3176
3685
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3177
3686
|
}
|
|
3178
|
-
function
|
|
3687
|
+
function strName14(v) {
|
|
3179
3688
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3180
3689
|
}
|
|
3181
3690
|
var REGION_SLOTS = new Map(
|
|
@@ -3184,10 +3693,10 @@ var REGION_SLOTS = new Map(
|
|
|
3184
3693
|
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
3185
3694
|
var MAX_REGION_DEPTH = 16;
|
|
3186
3695
|
function flowNodeLabel(node, index) {
|
|
3187
|
-
return
|
|
3696
|
+
return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
|
|
3188
3697
|
}
|
|
3189
3698
|
function stripRegions(config) {
|
|
3190
|
-
if (!
|
|
3699
|
+
if (!isRec13(config)) return void 0;
|
|
3191
3700
|
let out;
|
|
3192
3701
|
for (const key of Object.keys(config)) {
|
|
3193
3702
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3198,11 +3707,11 @@ function stripRegions(config) {
|
|
|
3198
3707
|
}
|
|
3199
3708
|
function walkFlowNodes(flow, flowPath) {
|
|
3200
3709
|
const out = [];
|
|
3201
|
-
if (!
|
|
3710
|
+
if (!isRec13(flow)) return out;
|
|
3202
3711
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3203
3712
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3204
3713
|
nodes.forEach((raw, index) => {
|
|
3205
|
-
if (!
|
|
3714
|
+
if (!isRec13(raw)) return;
|
|
3206
3715
|
const path = `${basePath}[${index}]`;
|
|
3207
3716
|
out.push({
|
|
3208
3717
|
node: raw,
|
|
@@ -3211,9 +3720,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3211
3720
|
regionTrail: trail,
|
|
3212
3721
|
depth
|
|
3213
3722
|
});
|
|
3214
|
-
const type =
|
|
3723
|
+
const type = strName14(raw.type);
|
|
3215
3724
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3216
|
-
if (!slots || !
|
|
3725
|
+
if (!slots || !isRec13(raw.config)) return;
|
|
3217
3726
|
const config = raw.config;
|
|
3218
3727
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3219
3728
|
for (const slot of slots) {
|
|
@@ -3221,8 +3730,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3221
3730
|
if (slot === "branches") {
|
|
3222
3731
|
if (!Array.isArray(value)) continue;
|
|
3223
3732
|
value.forEach((branch, b) => {
|
|
3224
|
-
if (!
|
|
3225
|
-
const branchName =
|
|
3733
|
+
if (!isRec13(branch)) return;
|
|
3734
|
+
const branchName = strName14(branch.name) ?? `#${b}`;
|
|
3226
3735
|
visitList(
|
|
3227
3736
|
branch.nodes,
|
|
3228
3737
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3232,7 +3741,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3232
3741
|
});
|
|
3233
3742
|
continue;
|
|
3234
3743
|
}
|
|
3235
|
-
if (!
|
|
3744
|
+
if (!isRec13(value)) continue;
|
|
3236
3745
|
visitList(
|
|
3237
3746
|
value.nodes,
|
|
3238
3747
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3252,7 +3761,8 @@ function joinTrail(trail, segment) {
|
|
|
3252
3761
|
// src/validate-flow-template-paths.ts
|
|
3253
3762
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
3254
3763
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
3255
|
-
|
|
3764
|
+
var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
|
|
3765
|
+
function asArray16(v) {
|
|
3256
3766
|
if (Array.isArray(v)) return v;
|
|
3257
3767
|
if (v && typeof v === "object") {
|
|
3258
3768
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -3281,7 +3791,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3281
3791
|
]);
|
|
3282
3792
|
function fieldTypesOf(obj) {
|
|
3283
3793
|
const types = /* @__PURE__ */ new Map();
|
|
3284
|
-
for (const f of
|
|
3794
|
+
for (const f of asArray16(obj.fields)) {
|
|
3285
3795
|
if (typeof f.name === "string") {
|
|
3286
3796
|
types.set(f.name, typeof f.type === "string" ? f.type : "");
|
|
3287
3797
|
}
|
|
@@ -3378,10 +3888,10 @@ function declaredExpandOf(flow) {
|
|
|
3378
3888
|
}
|
|
3379
3889
|
function validateFlowTemplatePaths(stack) {
|
|
3380
3890
|
const findings = [];
|
|
3381
|
-
const flows =
|
|
3891
|
+
const flows = asArray16(stack.flows);
|
|
3382
3892
|
if (flows.length === 0) return findings;
|
|
3383
3893
|
const objectsByName = /* @__PURE__ */ new Map();
|
|
3384
|
-
for (const obj of
|
|
3894
|
+
for (const obj of asArray16(stack.objects)) {
|
|
3385
3895
|
if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
|
|
3386
3896
|
}
|
|
3387
3897
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -3394,6 +3904,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3394
3904
|
const obj = objectsByName.get(objectName);
|
|
3395
3905
|
if (!obj) return;
|
|
3396
3906
|
const fieldTypes = fieldTypesOf(obj);
|
|
3907
|
+
const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
|
|
3397
3908
|
const expandSet = declaredExpandOf(flow);
|
|
3398
3909
|
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
3399
3910
|
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
@@ -3405,6 +3916,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3405
3916
|
if (leaves.length === 0) return;
|
|
3406
3917
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
3407
3918
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
3919
|
+
const seenUnprovisioned = /* @__PURE__ */ new Set();
|
|
3408
3920
|
for (const leaf of leaves) {
|
|
3409
3921
|
const inFilter = leaf.inFilter;
|
|
3410
3922
|
for (const rest of recordRefsIn(leaf.text)) {
|
|
@@ -3412,6 +3924,19 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3412
3924
|
const hasSubPath = rest.length > 1;
|
|
3413
3925
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
3414
3926
|
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
3927
|
+
if (unprovisionedAnchors.has(head)) {
|
|
3928
|
+
if (!seenUnprovisioned.has(head)) {
|
|
3929
|
+
seenUnprovisioned.add(head);
|
|
3930
|
+
findings.push({
|
|
3931
|
+
severity: "warning",
|
|
3932
|
+
rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
3933
|
+
where,
|
|
3934
|
+
path: nodePath,
|
|
3935
|
+
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).`),
|
|
3936
|
+
hint: unprovisionedAnchorHint(objectName, head)
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3415
3940
|
if (!isKnown) {
|
|
3416
3941
|
if (seenUnknown.has(head)) continue;
|
|
3417
3942
|
seenUnknown.add(head);
|
|
@@ -3450,14 +3975,14 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3450
3975
|
|
|
3451
3976
|
// src/validate-ai-surface-affinity.ts
|
|
3452
3977
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
3453
|
-
function
|
|
3978
|
+
function asArray17(v) {
|
|
3454
3979
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3455
3980
|
if (v && typeof v === "object") {
|
|
3456
3981
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3457
3982
|
}
|
|
3458
3983
|
return [];
|
|
3459
3984
|
}
|
|
3460
|
-
function
|
|
3985
|
+
function strName15(v) {
|
|
3461
3986
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3462
3987
|
}
|
|
3463
3988
|
function surfaceOf(v) {
|
|
@@ -3467,18 +3992,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3467
3992
|
const findings = [];
|
|
3468
3993
|
if (!stack || typeof stack !== "object") return findings;
|
|
3469
3994
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3470
|
-
for (const skill of
|
|
3471
|
-
const n =
|
|
3995
|
+
for (const skill of asArray17(stack.skills)) {
|
|
3996
|
+
const n = strName15(skill.name);
|
|
3472
3997
|
if (n) skillsByName.set(n, skill);
|
|
3473
3998
|
}
|
|
3474
|
-
const agents =
|
|
3999
|
+
const agents = asArray17(stack.agents);
|
|
3475
4000
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3476
4001
|
const agent = agents[ai];
|
|
3477
|
-
const agentName =
|
|
4002
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
3478
4003
|
const agentSurface = surfaceOf(agent.surface);
|
|
3479
4004
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3480
4005
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3481
|
-
const ref =
|
|
4006
|
+
const ref = strName15(skillRefs[si]);
|
|
3482
4007
|
if (!ref) continue;
|
|
3483
4008
|
const skill = skillsByName.get(ref);
|
|
3484
4009
|
if (!skill) continue;
|
|
@@ -3500,14 +4025,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3500
4025
|
// src/validate-ai-tool-references.ts
|
|
3501
4026
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
3502
4027
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
3503
|
-
function
|
|
4028
|
+
function asArray18(v) {
|
|
3504
4029
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3505
4030
|
if (v && typeof v === "object") {
|
|
3506
4031
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3507
4032
|
}
|
|
3508
4033
|
return [];
|
|
3509
4034
|
}
|
|
3510
|
-
function
|
|
4035
|
+
function strName16(v) {
|
|
3511
4036
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3512
4037
|
}
|
|
3513
4038
|
function distance6(a, b) {
|
|
@@ -3548,26 +4073,26 @@ function materialisesAsTool(action) {
|
|
|
3548
4073
|
if (!ai || typeof ai !== "object") return false;
|
|
3549
4074
|
const aiRec = ai;
|
|
3550
4075
|
if (aiRec.exposed !== true) return false;
|
|
3551
|
-
if (!
|
|
3552
|
-
const type =
|
|
4076
|
+
if (!strName16(aiRec.description)) return false;
|
|
4077
|
+
const type = strName16(action.type);
|
|
3553
4078
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3554
4079
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3555
4080
|
return Boolean(action.target);
|
|
3556
4081
|
}
|
|
3557
4082
|
function collectToolUniverse(stack) {
|
|
3558
4083
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3559
|
-
for (const tool of
|
|
3560
|
-
const n =
|
|
4084
|
+
for (const tool of asArray18(stack.tools)) {
|
|
4085
|
+
const n = strName16(tool.name);
|
|
3561
4086
|
if (n) universe.add(n);
|
|
3562
4087
|
}
|
|
3563
4088
|
const addActionFamily = (actions) => {
|
|
3564
|
-
for (const action of
|
|
3565
|
-
const n =
|
|
4089
|
+
for (const action of asArray18(actions)) {
|
|
4090
|
+
const n = strName16(action.name);
|
|
3566
4091
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3567
4092
|
}
|
|
3568
4093
|
};
|
|
3569
4094
|
addActionFamily(stack.actions);
|
|
3570
|
-
for (const obj of
|
|
4095
|
+
for (const obj of asArray18(stack.objects)) {
|
|
3571
4096
|
addActionFamily(obj.actions);
|
|
3572
4097
|
}
|
|
3573
4098
|
return universe;
|
|
@@ -3575,13 +4100,13 @@ function collectToolUniverse(stack) {
|
|
|
3575
4100
|
function collectUnexposedActionNames(stack) {
|
|
3576
4101
|
const names = /* @__PURE__ */ new Set();
|
|
3577
4102
|
const scan = (actions) => {
|
|
3578
|
-
for (const action of
|
|
3579
|
-
const n =
|
|
4103
|
+
for (const action of asArray18(actions)) {
|
|
4104
|
+
const n = strName16(action.name);
|
|
3580
4105
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3581
4106
|
}
|
|
3582
4107
|
};
|
|
3583
4108
|
scan(stack.actions);
|
|
3584
|
-
for (const obj of
|
|
4109
|
+
for (const obj of asArray18(stack.objects)) scan(obj.actions);
|
|
3585
4110
|
return names;
|
|
3586
4111
|
}
|
|
3587
4112
|
function validateAiToolReferences(stack) {
|
|
@@ -3599,13 +4124,13 @@ function validateAiToolReferences(stack) {
|
|
|
3599
4124
|
}
|
|
3600
4125
|
return universe.has(ref);
|
|
3601
4126
|
};
|
|
3602
|
-
const skills =
|
|
4127
|
+
const skills = asArray18(stack.skills);
|
|
3603
4128
|
for (let si = 0; si < skills.length; si++) {
|
|
3604
4129
|
const skill = skills[si];
|
|
3605
|
-
const skillName =
|
|
4130
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
3606
4131
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3607
4132
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3608
|
-
const ref =
|
|
4133
|
+
const ref = strName16(refs[ti]);
|
|
3609
4134
|
if (!ref || resolves(ref)) continue;
|
|
3610
4135
|
const isPattern = ref.endsWith("*");
|
|
3611
4136
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3624,24 +4149,25 @@ function validateAiToolReferences(stack) {
|
|
|
3624
4149
|
|
|
3625
4150
|
// src/validate-ai-agent-authoring.ts
|
|
3626
4151
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3627
|
-
|
|
4152
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
4153
|
+
function asArray19(v) {
|
|
3628
4154
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3629
4155
|
if (v && typeof v === "object") {
|
|
3630
4156
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3631
4157
|
}
|
|
3632
4158
|
return [];
|
|
3633
4159
|
}
|
|
3634
|
-
function
|
|
4160
|
+
function strName17(v) {
|
|
3635
4161
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3636
4162
|
}
|
|
3637
4163
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
3638
4164
|
function validateAiAgentAuthoring(stack) {
|
|
3639
4165
|
const findings = [];
|
|
3640
4166
|
if (!stack || typeof stack !== "object") return findings;
|
|
3641
|
-
const agents =
|
|
4167
|
+
const agents = asArray19(stack.agents);
|
|
3642
4168
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3643
4169
|
const agent = agents[ai];
|
|
3644
|
-
const name =
|
|
4170
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
3645
4171
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3646
4172
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3647
4173
|
findings.push({
|
|
@@ -3653,6 +4179,22 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3653
4179
|
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.` : ``)
|
|
3654
4180
|
});
|
|
3655
4181
|
}
|
|
4182
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
4183
|
+
const apps = asArray19(stack.apps);
|
|
4184
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
4185
|
+
const app = apps[appIdx];
|
|
4186
|
+
const defaultAgent = strName17(app.defaultAgent);
|
|
4187
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
4188
|
+
const appName = strName17(app.name) ?? `#${appIdx}`;
|
|
4189
|
+
findings.push({
|
|
4190
|
+
severity: "warning",
|
|
4191
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
4192
|
+
where: `app "${appName}".defaultAgent`,
|
|
4193
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
4194
|
+
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.`,
|
|
4195
|
+
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.`
|
|
4196
|
+
});
|
|
4197
|
+
}
|
|
3656
4198
|
return findings;
|
|
3657
4199
|
}
|
|
3658
4200
|
|
|
@@ -3739,24 +4281,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
3739
4281
|
"owner",
|
|
3740
4282
|
"record_type"
|
|
3741
4283
|
]);
|
|
3742
|
-
var
|
|
3743
|
-
function
|
|
3744
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3745
|
-
if (
|
|
4284
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4285
|
+
function asArray20(v) {
|
|
4286
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4287
|
+
if (isRec14(v)) {
|
|
3746
4288
|
return Object.entries(v).map(([name, def]) => ({
|
|
3747
4289
|
name,
|
|
3748
|
-
...
|
|
4290
|
+
...isRec14(def) ? def : {}
|
|
3749
4291
|
}));
|
|
3750
4292
|
}
|
|
3751
4293
|
return [];
|
|
3752
4294
|
}
|
|
3753
4295
|
function indexObjectFields2(stack) {
|
|
3754
4296
|
const out = /* @__PURE__ */ new Map();
|
|
3755
|
-
for (const obj of
|
|
4297
|
+
for (const obj of asArray20(stack.objects)) {
|
|
3756
4298
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
3757
4299
|
if (!name) continue;
|
|
3758
4300
|
const names = /* @__PURE__ */ new Set();
|
|
3759
|
-
for (const f of
|
|
4301
|
+
for (const f of asArray20(obj.fields)) {
|
|
3760
4302
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
3761
4303
|
}
|
|
3762
4304
|
out.set(name, names);
|
|
@@ -3886,12 +4428,12 @@ ${source}
|
|
|
3886
4428
|
}
|
|
3887
4429
|
function validateHookBodyWrites(stack) {
|
|
3888
4430
|
const findings = [];
|
|
3889
|
-
const hooks =
|
|
4431
|
+
const hooks = asArray20(stack.hooks);
|
|
3890
4432
|
if (hooks.length === 0) return findings;
|
|
3891
4433
|
let objectFields = null;
|
|
3892
4434
|
hooks.forEach((hook, hookIndex) => {
|
|
3893
4435
|
const body = hook.body;
|
|
3894
|
-
if (!
|
|
4436
|
+
if (!isRec14(body) || body.language !== "js") return;
|
|
3895
4437
|
const source = body.source;
|
|
3896
4438
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3897
4439
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -3962,13 +4504,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
3962
4504
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
3963
4505
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
3964
4506
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
3965
|
-
var
|
|
3966
|
-
function
|
|
3967
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3968
|
-
if (
|
|
4507
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4508
|
+
function asArray21(v) {
|
|
4509
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4510
|
+
if (isRec15(v)) {
|
|
3969
4511
|
return Object.entries(v).map(([name, def]) => ({
|
|
3970
4512
|
name,
|
|
3971
|
-
...
|
|
4513
|
+
...isRec15(def) ? def : {}
|
|
3972
4514
|
}));
|
|
3973
4515
|
}
|
|
3974
4516
|
return [];
|
|
@@ -3982,11 +4524,11 @@ function collectActionBodies(stack) {
|
|
|
3982
4524
|
const sites = [];
|
|
3983
4525
|
const seen = /* @__PURE__ */ new Set();
|
|
3984
4526
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
3985
|
-
|
|
4527
|
+
asArray21(actions).forEach((action, index) => {
|
|
3986
4528
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
3987
4529
|
if (type !== "script") return;
|
|
3988
4530
|
const body = action.body;
|
|
3989
|
-
if (!
|
|
4531
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
3990
4532
|
const source = body.source;
|
|
3991
4533
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3992
4534
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -3997,7 +4539,7 @@ function collectActionBodies(stack) {
|
|
|
3997
4539
|
});
|
|
3998
4540
|
};
|
|
3999
4541
|
collect(stack.actions, "actions");
|
|
4000
|
-
|
|
4542
|
+
asArray21(stack.objects).forEach((obj, objIndex) => {
|
|
4001
4543
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
4002
4544
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
4003
4545
|
});
|
|
@@ -4005,7 +4547,7 @@ function collectActionBodies(stack) {
|
|
|
4005
4547
|
}
|
|
4006
4548
|
function validateActionBodyWrites(stack) {
|
|
4007
4549
|
const findings = [];
|
|
4008
|
-
if (!
|
|
4550
|
+
if (!isRec15(stack)) return findings;
|
|
4009
4551
|
const sites = collectActionBodies(stack);
|
|
4010
4552
|
if (sites.length === 0) return findings;
|
|
4011
4553
|
let objectFields = null;
|
|
@@ -4063,13 +4605,13 @@ function fixHint2(field, declared) {
|
|
|
4063
4605
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
4064
4606
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4065
4607
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4066
|
-
var
|
|
4067
|
-
function
|
|
4068
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4069
|
-
if (
|
|
4608
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4609
|
+
function asArray22(v) {
|
|
4610
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4611
|
+
if (isRec16(v)) {
|
|
4070
4612
|
return Object.entries(v).map(([name, def]) => ({
|
|
4071
4613
|
name,
|
|
4072
|
-
...
|
|
4614
|
+
...isRec16(def) ? def : {}
|
|
4073
4615
|
}));
|
|
4074
4616
|
}
|
|
4075
4617
|
return [];
|
|
@@ -4082,8 +4624,8 @@ function readLiteralObjectName(config) {
|
|
|
4082
4624
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4083
4625
|
function validateFlowNodeWrites(stack) {
|
|
4084
4626
|
const findings = [];
|
|
4085
|
-
if (!
|
|
4086
|
-
const flows =
|
|
4627
|
+
if (!isRec16(stack)) return findings;
|
|
4628
|
+
const flows = asArray22(stack.flows);
|
|
4087
4629
|
if (flows.length === 0) return findings;
|
|
4088
4630
|
let objectFields = null;
|
|
4089
4631
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -4091,10 +4633,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4091
4633
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4092
4634
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4093
4635
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4094
|
-
const config =
|
|
4636
|
+
const config = isRec16(node.config) ? node.config : void 0;
|
|
4095
4637
|
if (!config) return;
|
|
4096
4638
|
const fields = config.fields;
|
|
4097
|
-
if (!
|
|
4639
|
+
if (!isRec16(fields)) return;
|
|
4098
4640
|
const written = Object.keys(fields);
|
|
4099
4641
|
if (written.length === 0) return;
|
|
4100
4642
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4128,7 +4670,7 @@ function fixHint3(field, declared) {
|
|
|
4128
4670
|
// src/validate-readonly-flow-writes.ts
|
|
4129
4671
|
var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
|
|
4130
4672
|
var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
|
|
4131
|
-
function
|
|
4673
|
+
function asArray23(v) {
|
|
4132
4674
|
if (Array.isArray(v)) return v;
|
|
4133
4675
|
if (v && typeof v === "object") {
|
|
4134
4676
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -4169,9 +4711,9 @@ function readLiteralObjectName2(config) {
|
|
|
4169
4711
|
}
|
|
4170
4712
|
function validateReadonlyFlowWrites(stack) {
|
|
4171
4713
|
const findings = [];
|
|
4172
|
-
const flows =
|
|
4714
|
+
const flows = asArray23(stack.flows);
|
|
4173
4715
|
if (flows.length === 0) return findings;
|
|
4174
|
-
const roIndex = buildReadonlyIndex(
|
|
4716
|
+
const roIndex = buildReadonlyIndex(asArray23(stack.objects));
|
|
4175
4717
|
flows.forEach((flow, flowIndex) => {
|
|
4176
4718
|
if (flow.runAs === "system") return;
|
|
4177
4719
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
@@ -4230,11 +4772,11 @@ import {
|
|
|
4230
4772
|
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
4231
4773
|
|
|
4232
4774
|
// src/zod-issue-format.ts
|
|
4233
|
-
var
|
|
4775
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4234
4776
|
var valueAtPath = (root, path) => {
|
|
4235
4777
|
let cur = root;
|
|
4236
4778
|
for (const key of path) {
|
|
4237
|
-
if (!
|
|
4779
|
+
if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
|
|
4238
4780
|
cur = cur[key];
|
|
4239
4781
|
}
|
|
4240
4782
|
return cur;
|
|
@@ -4279,7 +4821,7 @@ function loadTypeScript2() {
|
|
|
4279
4821
|
}
|
|
4280
4822
|
return cachedTs2;
|
|
4281
4823
|
}
|
|
4282
|
-
var
|
|
4824
|
+
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4283
4825
|
var BLOCKS = new Map(
|
|
4284
4826
|
REACT_BLOCKS.map((b) => [
|
|
4285
4827
|
b.tag,
|
|
@@ -4367,12 +4909,13 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4367
4909
|
return perPosition(init.expression);
|
|
4368
4910
|
}
|
|
4369
4911
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4912
|
+
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4370
4913
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
4371
4914
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
4372
4915
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4373
4916
|
function checkChartDrillDown(raw, push2) {
|
|
4374
4917
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4375
|
-
if (!
|
|
4918
|
+
if (!isRec18(raw)) {
|
|
4376
4919
|
push2(
|
|
4377
4920
|
"error",
|
|
4378
4921
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4395,7 +4938,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4395
4938
|
}
|
|
4396
4939
|
function checkChartAggregate(raw, push2) {
|
|
4397
4940
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4398
|
-
if (!
|
|
4941
|
+
if (!isRec18(raw)) {
|
|
4399
4942
|
push2(
|
|
4400
4943
|
"error",
|
|
4401
4944
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4410,7 +4953,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4410
4953
|
"warning",
|
|
4411
4954
|
REACT_CHART_AGGREGATE_INVALID,
|
|
4412
4955
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
4413
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
4956
|
+
"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."
|
|
4414
4957
|
);
|
|
4415
4958
|
}
|
|
4416
4959
|
const parsed = ChartAggregateSchema.safeParse(raw);
|
|
@@ -4426,9 +4969,9 @@ function checkChartAggregate(raw, push2) {
|
|
|
4426
4969
|
);
|
|
4427
4970
|
}
|
|
4428
4971
|
}
|
|
4429
|
-
var
|
|
4972
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4430
4973
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4431
|
-
function checkObjectChart(attrs, objectFields, findings) {
|
|
4974
|
+
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4432
4975
|
const { values, where, path } = attrs;
|
|
4433
4976
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
4434
4977
|
checkChartDrillDown(values.get("drillDown"), push2);
|
|
@@ -4436,18 +4979,29 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4436
4979
|
const aggregate = values.get("aggregate");
|
|
4437
4980
|
checkChartAggregate(aggregate, push2);
|
|
4438
4981
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4439
|
-
if (!
|
|
4982
|
+
if (!isRec18(aggregate)) return;
|
|
4440
4983
|
const fn = strOf(aggregate.function);
|
|
4441
4984
|
const field = strOf(aggregate.field);
|
|
4442
4985
|
const groupBy = aggregate.groupBy;
|
|
4443
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4986
|
+
const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4444
4987
|
const objectName = strOf(values.get("objectName"));
|
|
4445
4988
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4446
4989
|
if (objectName && known) {
|
|
4990
|
+
const anchors = unprovisionedAnchors.get(objectName);
|
|
4447
4991
|
const fieldRef = (name, prop) => {
|
|
4448
4992
|
if (!name) return;
|
|
4449
4993
|
if (name.includes(".")) return;
|
|
4450
|
-
if (known.has(name) || SYSTEM_FIELDS.has(name))
|
|
4994
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) {
|
|
4995
|
+
if (anchors?.has(name)) {
|
|
4996
|
+
push2(
|
|
4997
|
+
"warning",
|
|
4998
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
4999
|
+
`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.`,
|
|
5000
|
+
unprovisionedAnchorHint(objectName, name)
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
4451
5005
|
push2(
|
|
4452
5006
|
"error",
|
|
4453
5007
|
REACT_CHART_FIELD_UNKNOWN,
|
|
@@ -4473,18 +5027,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4473
5027
|
);
|
|
4474
5028
|
};
|
|
4475
5029
|
const xAxisRaw = values.get("xAxis");
|
|
4476
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5030
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4477
5031
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4478
5032
|
axisRef(categoryAxis, categoryProp);
|
|
4479
5033
|
const yAxisRaw = values.get("yAxis");
|
|
4480
5034
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4481
5035
|
for (const a of yAxisList) {
|
|
4482
|
-
axisRef(strOf(a) ?? (
|
|
5036
|
+
axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4483
5037
|
}
|
|
4484
5038
|
const series = values.get("series");
|
|
4485
5039
|
if (Array.isArray(series)) {
|
|
4486
5040
|
for (const s of series) {
|
|
4487
|
-
if (!
|
|
5041
|
+
if (!isRec18(s)) continue;
|
|
4488
5042
|
const dataKey = strOf(s.dataKey);
|
|
4489
5043
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4490
5044
|
}
|
|
@@ -4540,7 +5094,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4540
5094
|
if (!Array.isArray(value)) return { child, parent };
|
|
4541
5095
|
for (let i = 0; i < value.length; i++) {
|
|
4542
5096
|
const sub = value[i];
|
|
4543
|
-
if (!
|
|
5097
|
+
if (!isRec18(sub)) continue;
|
|
4544
5098
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4545
5099
|
child.push({
|
|
4546
5100
|
objectName: strOf(sub.childObject),
|
|
@@ -4585,20 +5139,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4585
5139
|
}
|
|
4586
5140
|
for (const key of spec.nestedFields ?? []) {
|
|
4587
5141
|
const v = readable(key);
|
|
4588
|
-
if (
|
|
5142
|
+
if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4589
5143
|
}
|
|
4590
5144
|
for (const key of spec.sections ?? []) {
|
|
4591
5145
|
const v = readable(key);
|
|
4592
5146
|
if (!Array.isArray(v)) continue;
|
|
4593
5147
|
for (let i = 0; i < v.length; i++) {
|
|
4594
5148
|
const section = v[i];
|
|
4595
|
-
if (!
|
|
5149
|
+
if (!isRec18(section)) continue;
|
|
4596
5150
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4597
5151
|
}
|
|
4598
5152
|
}
|
|
4599
5153
|
for (const key of spec.keyedByField ?? []) {
|
|
4600
5154
|
const v = readable(key);
|
|
4601
|
-
if (!
|
|
5155
|
+
if (!isRec18(v)) continue;
|
|
4602
5156
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4603
5157
|
}
|
|
4604
5158
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4606,22 +5160,30 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4606
5160
|
}
|
|
4607
5161
|
return { own, queried };
|
|
4608
5162
|
}
|
|
4609
|
-
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
5163
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
|
|
4610
5164
|
const objectName = strOf(values.get("objectName"));
|
|
4611
5165
|
const out = [];
|
|
4612
5166
|
const spec = REACT_FIELD_SPECS[tag];
|
|
4613
5167
|
if (spec) {
|
|
4614
5168
|
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
4615
|
-
out.push(
|
|
4616
|
-
|
|
5169
|
+
out.push(
|
|
5170
|
+
...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5171
|
+
);
|
|
5172
|
+
out.push(
|
|
5173
|
+
...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
|
|
5174
|
+
);
|
|
4617
5175
|
}
|
|
4618
5176
|
if (tag === "ObjectForm") {
|
|
4619
5177
|
const raw = values.get("subforms");
|
|
4620
5178
|
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
4621
5179
|
for (const sub of subs.child) {
|
|
4622
|
-
out.push(
|
|
5180
|
+
out.push(
|
|
5181
|
+
...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5182
|
+
);
|
|
4623
5183
|
}
|
|
4624
|
-
out.push(
|
|
5184
|
+
out.push(
|
|
5185
|
+
...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5186
|
+
);
|
|
4625
5187
|
}
|
|
4626
5188
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
4627
5189
|
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
@@ -4630,7 +5192,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
4630
5192
|
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
4631
5193
|
objectName,
|
|
4632
5194
|
objectFields,
|
|
4633
|
-
where
|
|
5195
|
+
where,
|
|
5196
|
+
"skipped",
|
|
5197
|
+
unprovisionedAnchors
|
|
4634
5198
|
)
|
|
4635
5199
|
);
|
|
4636
5200
|
}
|
|
@@ -4662,8 +5226,9 @@ function localComponentNames(tsc, sf) {
|
|
|
4662
5226
|
function validateReactPageProps(stack) {
|
|
4663
5227
|
const findings = [];
|
|
4664
5228
|
const objectFields = indexObjectFields(stack);
|
|
5229
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
4665
5230
|
const searchTargets = indexObjectSearchTargets(stack);
|
|
4666
|
-
const pages =
|
|
5231
|
+
const pages = asArray24(stack.pages);
|
|
4667
5232
|
for (let p = 0; p < pages.length; p++) {
|
|
4668
5233
|
const page = pages[p];
|
|
4669
5234
|
if (!page || page.kind !== "react") continue;
|
|
@@ -4744,7 +5309,7 @@ function validateReactPageProps(stack) {
|
|
|
4744
5309
|
}
|
|
4745
5310
|
}
|
|
4746
5311
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
4747
|
-
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
5312
|
+
checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
|
|
4748
5313
|
}
|
|
4749
5314
|
if (tag === "ListView" && !hasSpread) {
|
|
4750
5315
|
findings.push(
|
|
@@ -4760,7 +5325,7 @@ function validateReactPageProps(stack) {
|
|
|
4760
5325
|
}
|
|
4761
5326
|
if (!hasSpread) {
|
|
4762
5327
|
findings.push(
|
|
4763
|
-
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
5328
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
|
|
4764
5329
|
);
|
|
4765
5330
|
}
|
|
4766
5331
|
}
|
|
@@ -4787,6 +5352,15 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
4787
5352
|
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
4788
5353
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
4789
5354
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5355
|
+
// [#7912] The THIRD question about a nav entry, after "does the target
|
|
5356
|
+
// resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
|
|
5357
|
+
// destination serve at all? An object's own `enable` block can make its list
|
|
5358
|
+
// answer 404/405 for every persona, and no gate authorable on the entry
|
|
5359
|
+
// expresses that — which is how #7544's dead row survived review for a year.
|
|
5360
|
+
// The server now prunes such an entry from the `/meta` payload; the
|
|
5361
|
+
// maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
|
|
5362
|
+
// prune is never silent to the author who wrote the row.
|
|
5363
|
+
{ name: "validateNavObjectServability", run: validateNavObjectServability },
|
|
4790
5364
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
4791
5365
|
// The same family from the other end (#5417). Its sibling above asks "does
|
|
4792
5366
|
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
@@ -4885,13 +5459,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
4885
5459
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
4886
5460
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
4887
5461
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
4888
|
-
function
|
|
5462
|
+
function isRec19(v) {
|
|
4889
5463
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4890
5464
|
}
|
|
4891
|
-
function
|
|
5465
|
+
function strName18(v) {
|
|
4892
5466
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4893
5467
|
}
|
|
4894
|
-
function
|
|
5468
|
+
function asArray25(v) {
|
|
4895
5469
|
if (Array.isArray(v)) return v;
|
|
4896
5470
|
if (v && typeof v === "object") {
|
|
4897
5471
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4902,23 +5476,36 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
4902
5476
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
4903
5477
|
function suppliedByDataSource(issue, component) {
|
|
4904
5478
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
4905
|
-
const dataSource =
|
|
4906
|
-
return
|
|
5479
|
+
const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
|
|
5480
|
+
return strName18(dataSource?.object) !== void 0;
|
|
5481
|
+
}
|
|
5482
|
+
function unrecognizedKeysFromUnionArm(issue) {
|
|
5483
|
+
if (issue.code !== "invalid_union") return void 0;
|
|
5484
|
+
const arms = issue.errors;
|
|
5485
|
+
if (!arms || arms.length === 0) return void 0;
|
|
5486
|
+
let found;
|
|
5487
|
+
for (const arm of arms) {
|
|
5488
|
+
const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
|
|
5489
|
+
if (keyIssues.length === 0) continue;
|
|
5490
|
+
if (keyIssues.length !== arm.length || found) return void 0;
|
|
5491
|
+
found = keyIssues[0];
|
|
5492
|
+
}
|
|
5493
|
+
return found;
|
|
4907
5494
|
}
|
|
4908
5495
|
function validateComponentProps(stack) {
|
|
4909
5496
|
const findings = [];
|
|
4910
|
-
if (!
|
|
4911
|
-
const pages =
|
|
5497
|
+
if (!isRec19(stack)) return findings;
|
|
5498
|
+
const pages = asArray25(stack.pages);
|
|
4912
5499
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4913
5500
|
const page = pages[pi];
|
|
4914
|
-
if (!
|
|
4915
|
-
const pageName =
|
|
5501
|
+
if (!isRec19(page)) continue;
|
|
5502
|
+
const pageName = strName18(page.name) ?? `#${pi}`;
|
|
4916
5503
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4917
|
-
const type =
|
|
5504
|
+
const type = strName18(component.type);
|
|
4918
5505
|
if (!type) continue;
|
|
4919
5506
|
const schema = PROPS_SCHEMAS[type];
|
|
4920
5507
|
if (!schema) continue;
|
|
4921
|
-
const props =
|
|
5508
|
+
const props = isRec19(component.properties) ? component.properties : void 0;
|
|
4922
5509
|
if (!props) continue;
|
|
4923
5510
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
4924
5511
|
const base = `${path}.properties`;
|
|
@@ -4937,6 +5524,20 @@ function validateComponentProps(stack) {
|
|
|
4937
5524
|
for (const issue of parsed.error?.issues ?? []) {
|
|
4938
5525
|
if (suppliedByDataSource(issue, component)) continue;
|
|
4939
5526
|
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5527
|
+
const armIssue = unrecognizedKeysFromUnionArm(issue);
|
|
5528
|
+
if (armIssue) {
|
|
5529
|
+
for (const key of armIssue.keys ?? []) {
|
|
5530
|
+
findings.push({
|
|
5531
|
+
severity: "warning",
|
|
5532
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5533
|
+
where,
|
|
5534
|
+
path: `${at}.${key}`,
|
|
5535
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
|
|
5536
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
continue;
|
|
5540
|
+
}
|
|
4940
5541
|
if (issue.code === "unrecognized_keys") {
|
|
4941
5542
|
for (const key of issue.keys ?? []) {
|
|
4942
5543
|
findings.push({
|
|
@@ -5167,7 +5768,7 @@ function looksLikeTailwind(className) {
|
|
|
5167
5768
|
return false;
|
|
5168
5769
|
});
|
|
5169
5770
|
}
|
|
5170
|
-
function
|
|
5771
|
+
function asArray26(v) {
|
|
5171
5772
|
if (Array.isArray(v)) return v;
|
|
5172
5773
|
if (v && typeof v === "object") {
|
|
5173
5774
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5260,13 +5861,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
5260
5861
|
}
|
|
5261
5862
|
function validateResponsiveStyles(stack) {
|
|
5262
5863
|
const findings = [];
|
|
5263
|
-
const pages =
|
|
5864
|
+
const pages = asArray26(stack.pages);
|
|
5264
5865
|
for (let p = 0; p < pages.length; p++) {
|
|
5265
5866
|
const page = pages[p];
|
|
5266
5867
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
5267
|
-
const regions =
|
|
5868
|
+
const regions = asArray26(page.regions);
|
|
5268
5869
|
for (let r = 0; r < regions.length; r++) {
|
|
5269
|
-
const components =
|
|
5870
|
+
const components = asArray26(regions[r].components);
|
|
5270
5871
|
for (let c = 0; c < components.length; c++) {
|
|
5271
5872
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
5272
5873
|
}
|
|
@@ -5277,10 +5878,10 @@ function validateResponsiveStyles(stack) {
|
|
|
5277
5878
|
|
|
5278
5879
|
// src/validate-jsx-pages.ts
|
|
5279
5880
|
import { parseJsx, compile } from "@objectstack/sdui-parser";
|
|
5280
|
-
var
|
|
5881
|
+
var asArray27 = (v) => Array.isArray(v) ? v : [];
|
|
5281
5882
|
function validateJsxPages(stack, opts = {}) {
|
|
5282
5883
|
const findings = [];
|
|
5283
|
-
const pages =
|
|
5884
|
+
const pages = asArray27(stack.pages);
|
|
5284
5885
|
for (let p = 0; p < pages.length; p++) {
|
|
5285
5886
|
const page = pages[p];
|
|
5286
5887
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -5327,10 +5928,10 @@ function loadSucraseTransform() {
|
|
|
5327
5928
|
}
|
|
5328
5929
|
return cachedTransform;
|
|
5329
5930
|
}
|
|
5330
|
-
var
|
|
5931
|
+
var asArray28 = (v) => Array.isArray(v) ? v : [];
|
|
5331
5932
|
function validateReactPages(stack) {
|
|
5332
5933
|
const findings = [];
|
|
5333
|
-
const pages =
|
|
5934
|
+
const pages = asArray28(stack.pages);
|
|
5334
5935
|
for (let p = 0; p < pages.length; p++) {
|
|
5335
5936
|
const page = pages[p];
|
|
5336
5937
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5367,11 +5968,11 @@ function validateReactPages(stack) {
|
|
|
5367
5968
|
|
|
5368
5969
|
// src/validate-page-source-styling.ts
|
|
5369
5970
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
5370
|
-
var
|
|
5971
|
+
var asArray29 = (v) => Array.isArray(v) ? v : [];
|
|
5371
5972
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
5372
5973
|
function validatePageSourceStyling(stack) {
|
|
5373
5974
|
const findings = [];
|
|
5374
|
-
const pages =
|
|
5975
|
+
const pages = asArray29(stack.pages);
|
|
5375
5976
|
for (let p = 0; p < pages.length; p++) {
|
|
5376
5977
|
const page = pages[p];
|
|
5377
5978
|
if (!page) continue;
|
|
@@ -5399,7 +6000,7 @@ function validatePageSourceStyling(stack) {
|
|
|
5399
6000
|
// src/validate-capability-references.ts
|
|
5400
6001
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
5401
6002
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
5402
|
-
function
|
|
6003
|
+
function asArray30(v) {
|
|
5403
6004
|
if (Array.isArray(v)) return v;
|
|
5404
6005
|
if (v && typeof v === "object") {
|
|
5405
6006
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5424,13 +6025,13 @@ function validateCapabilityReferences(stack) {
|
|
|
5424
6025
|
const findings = [];
|
|
5425
6026
|
if (!stack || typeof stack !== "object") return findings;
|
|
5426
6027
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
5427
|
-
for (const cap of
|
|
6028
|
+
for (const cap of asArray30(stack.capabilities)) {
|
|
5428
6029
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
5429
6030
|
}
|
|
5430
|
-
for (const ps of
|
|
6031
|
+
for (const ps of asArray30(stack.permissions)) {
|
|
5431
6032
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
5432
6033
|
}
|
|
5433
|
-
for (const seed of
|
|
6034
|
+
for (const seed of asArray30(stack.data)) {
|
|
5434
6035
|
if (seed.object !== "sys_capability") continue;
|
|
5435
6036
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
5436
6037
|
const name = rec?.name;
|
|
@@ -5449,7 +6050,7 @@ function validateCapabilityReferences(stack) {
|
|
|
5449
6050
|
hint
|
|
5450
6051
|
});
|
|
5451
6052
|
};
|
|
5452
|
-
const objects =
|
|
6053
|
+
const objects = asArray30(stack.objects);
|
|
5453
6054
|
for (let i = 0; i < objects.length; i++) {
|
|
5454
6055
|
const obj = objects[i];
|
|
5455
6056
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5458,27 +6059,27 @@ function validateCapabilityReferences(stack) {
|
|
|
5458
6059
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
5459
6060
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
5460
6061
|
}
|
|
5461
|
-
const fields =
|
|
6062
|
+
const fields = asArray30(obj.fields);
|
|
5462
6063
|
for (const f of fields) {
|
|
5463
6064
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
5464
6065
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
5465
6066
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
5466
6067
|
}
|
|
5467
6068
|
}
|
|
5468
|
-
for (const [ai, action] of
|
|
6069
|
+
for (const [ai, action] of asArray30(obj.actions).entries()) {
|
|
5469
6070
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
5470
6071
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5471
6072
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
5472
6073
|
}
|
|
5473
6074
|
}
|
|
5474
6075
|
}
|
|
5475
|
-
for (const [i, action] of
|
|
6076
|
+
for (const [i, action] of asArray30(stack.actions).entries()) {
|
|
5476
6077
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
5477
6078
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5478
6079
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
5479
6080
|
}
|
|
5480
6081
|
}
|
|
5481
|
-
const apps =
|
|
6082
|
+
const apps = asArray30(stack.apps);
|
|
5482
6083
|
for (let i = 0; i < apps.length; i++) {
|
|
5483
6084
|
const app = apps[i];
|
|
5484
6085
|
if (!app || typeof app !== "object") continue;
|
|
@@ -5511,8 +6112,9 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
5511
6112
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
5512
6113
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
5513
6114
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
6115
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5514
6116
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5515
|
-
function
|
|
6117
|
+
function asArray31(v) {
|
|
5516
6118
|
if (Array.isArray(v)) return v;
|
|
5517
6119
|
if (v && typeof v === "object") {
|
|
5518
6120
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -5528,6 +6130,11 @@ function renderNonObject(v) {
|
|
|
5528
6130
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
5529
6131
|
return `a ${t}`;
|
|
5530
6132
|
}
|
|
6133
|
+
function renderTriggerToken(v) {
|
|
6134
|
+
if (typeof v === "string") return `'${v}'`;
|
|
6135
|
+
const json = JSON.stringify(v);
|
|
6136
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
6137
|
+
}
|
|
5531
6138
|
function startNodeOf(flow) {
|
|
5532
6139
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
5533
6140
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -5535,10 +6142,10 @@ function startNodeOf(flow) {
|
|
|
5535
6142
|
}
|
|
5536
6143
|
function validateFlowTriggerReadiness(stack) {
|
|
5537
6144
|
const findings = [];
|
|
5538
|
-
const flows =
|
|
6145
|
+
const flows = asArray31(stack.flows);
|
|
5539
6146
|
if (flows.length === 0) return findings;
|
|
5540
6147
|
const objectNames = new Set(
|
|
5541
|
-
|
|
6148
|
+
asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
|
|
5542
6149
|
);
|
|
5543
6150
|
flows.forEach((flow, flowIndex) => {
|
|
5544
6151
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
@@ -5647,6 +6254,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5647
6254
|
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.`
|
|
5648
6255
|
});
|
|
5649
6256
|
}
|
|
6257
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
6258
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
6259
|
+
const hasTriggerType = config.triggerType != null;
|
|
6260
|
+
findings.push({
|
|
6261
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
6262
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
6263
|
+
// with no registry lookup in them — so no installed package can make
|
|
6264
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
6265
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
6266
|
+
// record-change trigger itself, and it still would not help, because
|
|
6267
|
+
// the flow never reaches the point of asking for one.
|
|
6268
|
+
severity: "error",
|
|
6269
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
6270
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
6271
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
6272
|
+
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.`,
|
|
6273
|
+
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.`
|
|
6274
|
+
});
|
|
6275
|
+
}
|
|
5650
6276
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
5651
6277
|
findings.push({
|
|
5652
6278
|
severity: "warning",
|
|
@@ -5692,7 +6318,7 @@ var TYPE_FIX = {
|
|
|
5692
6318
|
business_unit: "department",
|
|
5693
6319
|
bu: "department"
|
|
5694
6320
|
};
|
|
5695
|
-
function
|
|
6321
|
+
function asArray32(v) {
|
|
5696
6322
|
if (Array.isArray(v)) return v;
|
|
5697
6323
|
if (v && typeof v === "object") {
|
|
5698
6324
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5702,7 +6328,7 @@ function asArray31(v) {
|
|
|
5702
6328
|
function validateApprovalApprovers(stack) {
|
|
5703
6329
|
const findings = [];
|
|
5704
6330
|
if (!stack || typeof stack !== "object") return findings;
|
|
5705
|
-
const flows =
|
|
6331
|
+
const flows = asArray32(stack.flows);
|
|
5706
6332
|
const validTypes = new Set(ApproverType.options);
|
|
5707
6333
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
5708
6334
|
const flow = flows[fi];
|
|
@@ -5885,7 +6511,7 @@ function validateApprovalApprovers(stack) {
|
|
|
5885
6511
|
import { objectTitleCompleteness } from "@objectstack/spec/data";
|
|
5886
6512
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
5887
6513
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
5888
|
-
function
|
|
6514
|
+
function asArray33(v) {
|
|
5889
6515
|
if (Array.isArray(v)) return v;
|
|
5890
6516
|
if (v && typeof v === "object") {
|
|
5891
6517
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5894,7 +6520,7 @@ function asArray32(v) {
|
|
|
5894
6520
|
}
|
|
5895
6521
|
function validateRecordTitle(stack) {
|
|
5896
6522
|
const findings = [];
|
|
5897
|
-
const objects =
|
|
6523
|
+
const objects = asArray33(stack.objects);
|
|
5898
6524
|
for (let i = 0; i < objects.length; i++) {
|
|
5899
6525
|
const obj = objects[i];
|
|
5900
6526
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -5930,7 +6556,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
5930
6556
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
5931
6557
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
5932
6558
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
5933
|
-
|
|
6559
|
+
var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
|
|
6560
|
+
function asArray34(v) {
|
|
5934
6561
|
if (Array.isArray(v)) return v;
|
|
5935
6562
|
if (v && typeof v === "object") {
|
|
5936
6563
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5939,7 +6566,7 @@ function asArray33(v) {
|
|
|
5939
6566
|
}
|
|
5940
6567
|
function validateSemanticRoles(stack) {
|
|
5941
6568
|
const findings = [];
|
|
5942
|
-
const objects =
|
|
6569
|
+
const objects = asArray34(stack.objects);
|
|
5943
6570
|
for (let i = 0; i < objects.length; i++) {
|
|
5944
6571
|
const obj = objects[i];
|
|
5945
6572
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5948,6 +6575,15 @@ function validateSemanticRoles(stack) {
|
|
|
5948
6575
|
const path = `objects[${i}]`;
|
|
5949
6576
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
5950
6577
|
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
6578
|
+
const unprovisioned = unprovisionedInjectedColumnsFor(obj);
|
|
6579
|
+
const unprovisionedPointer = (slot, entry) => ({
|
|
6580
|
+
severity: "warning",
|
|
6581
|
+
rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
6582
|
+
where,
|
|
6583
|
+
path: `${path}.${slot}`,
|
|
6584
|
+
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.`,
|
|
6585
|
+
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.`
|
|
6586
|
+
});
|
|
5951
6587
|
const declaredGroups = new Set(
|
|
5952
6588
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
5953
6589
|
);
|
|
@@ -5989,10 +6625,16 @@ function validateSemanticRoles(stack) {
|
|
|
5989
6625
|
message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
|
|
5990
6626
|
hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
|
|
5991
6627
|
});
|
|
6628
|
+
} else if (typeof stage === "string" && unprovisioned.has(stage)) {
|
|
6629
|
+
findings.push(unprovisionedPointer("stageField", stage));
|
|
5992
6630
|
}
|
|
5993
6631
|
const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
|
|
5994
6632
|
for (const entry of highlights) {
|
|
5995
|
-
if (typeof entry !== "string" || entry.length === 0
|
|
6633
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
6634
|
+
if (fieldNames.has(entry)) {
|
|
6635
|
+
if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
|
|
6636
|
+
continue;
|
|
6637
|
+
}
|
|
5996
6638
|
findings.push({
|
|
5997
6639
|
severity: "warning",
|
|
5998
6640
|
rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -6006,7 +6648,7 @@ function validateSemanticRoles(stack) {
|
|
|
6006
6648
|
(h) => typeof h === "string" && h.length > 0
|
|
6007
6649
|
);
|
|
6008
6650
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
6009
|
-
const declaredTitle = [obj.nameField, obj.
|
|
6651
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
6010
6652
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
6011
6653
|
const stripSet = new Set(
|
|
6012
6654
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -6034,13 +6676,19 @@ function validateSemanticRoles(stack) {
|
|
|
6034
6676
|
// src/validate-form-layout.ts
|
|
6035
6677
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
6036
6678
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
6037
|
-
function
|
|
6679
|
+
function asArray35(v) {
|
|
6038
6680
|
if (Array.isArray(v)) return v;
|
|
6039
6681
|
if (v && typeof v === "object") {
|
|
6040
6682
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6041
6683
|
}
|
|
6042
6684
|
return [];
|
|
6043
6685
|
}
|
|
6686
|
+
function isRec20(v) {
|
|
6687
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6688
|
+
}
|
|
6689
|
+
function strName19(v) {
|
|
6690
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6691
|
+
}
|
|
6044
6692
|
function fieldNameOf(entry) {
|
|
6045
6693
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
6046
6694
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -6049,60 +6697,53 @@ function fieldNameOf(entry) {
|
|
|
6049
6697
|
}
|
|
6050
6698
|
return null;
|
|
6051
6699
|
}
|
|
6052
|
-
function boundObject(view) {
|
|
6053
|
-
const data = view.data;
|
|
6054
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
6055
|
-
return data.object;
|
|
6056
|
-
}
|
|
6057
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
6058
|
-
}
|
|
6059
6700
|
function validateFormLayout(stack) {
|
|
6060
6701
|
const findings = [];
|
|
6061
6702
|
const objectFields = /* @__PURE__ */ new Map();
|
|
6062
|
-
for (const obj of
|
|
6703
|
+
for (const obj of asArray35(stack.objects)) {
|
|
6063
6704
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6064
6705
|
if (!name) continue;
|
|
6065
6706
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6066
6707
|
objectFields.set(name, new Set(fields));
|
|
6067
6708
|
}
|
|
6068
|
-
const
|
|
6069
|
-
|
|
6070
|
-
const
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
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
|
-
}
|
|
6709
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6710
|
+
const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
|
|
6711
|
+
const containerObject = viewObjectName(view);
|
|
6712
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6713
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
6714
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
6715
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6716
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6717
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6718
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6719
|
+
const sec = sections[s];
|
|
6720
|
+
const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6721
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6722
|
+
const entry = secFields[f];
|
|
6723
|
+
const fname = fieldNameOf(entry);
|
|
6724
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
6725
|
+
if (fname && known && !known.has(fname)) {
|
|
6726
|
+
findings.push({
|
|
6727
|
+
severity: "warning",
|
|
6728
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
6729
|
+
where,
|
|
6730
|
+
path: fpath,
|
|
6731
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
6732
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6733
|
+
});
|
|
6734
|
+
}
|
|
6735
|
+
const colSpan = isRec20(entry) ? entry.colSpan : void 0;
|
|
6736
|
+
if (colSpan != null) {
|
|
6737
|
+
findings.push({
|
|
6738
|
+
severity: "warning",
|
|
6739
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
6740
|
+
where,
|
|
6741
|
+
path: `${fpath}.colSpan`,
|
|
6742
|
+
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`,
|
|
6743
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
6744
|
+
});
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6106
6747
|
}
|
|
6107
6748
|
}
|
|
6108
6749
|
}
|
|
@@ -6201,17 +6842,75 @@ function validateSeedStateMachine(stack) {
|
|
|
6201
6842
|
}
|
|
6202
6843
|
|
|
6203
6844
|
// src/validate-visibility-predicates.ts
|
|
6204
|
-
|
|
6845
|
+
import {
|
|
6846
|
+
collectCelRootIdentifiers as collectCelRootIdentifiers3,
|
|
6847
|
+
firstUndeclaredReference,
|
|
6848
|
+
parseCelToAst as parseCelToAst3,
|
|
6849
|
+
parseCelToAstWithReason
|
|
6850
|
+
} from "@objectstack/formula";
|
|
6851
|
+
|
|
6852
|
+
// src/predicate-rhs-position.ts
|
|
6853
|
+
function isNode2(v) {
|
|
6854
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6855
|
+
}
|
|
6856
|
+
var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
|
|
6857
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6858
|
+
function bareId(node) {
|
|
6859
|
+
if (!isNode2(node)) return null;
|
|
6860
|
+
return node.op === "id" && typeof node.args === "string" ? node.args : null;
|
|
6861
|
+
}
|
|
6862
|
+
function bareRhsOnlyIdentifiers(ast) {
|
|
6863
|
+
const rhs = /* @__PURE__ */ new Set();
|
|
6864
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
6865
|
+
const walk = (node, suppressible) => {
|
|
6866
|
+
if (Array.isArray(node)) {
|
|
6867
|
+
for (const child of node) walk(child, suppressible);
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
6870
|
+
if (!isNode2(node)) return;
|
|
6871
|
+
const args = node.args;
|
|
6872
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6873
|
+
walk(args[1], suppressible);
|
|
6874
|
+
walk(args[2], false);
|
|
6875
|
+
return;
|
|
6876
|
+
}
|
|
6877
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
6878
|
+
const right = suppressible ? bareId(args[1]) : null;
|
|
6879
|
+
walk(args[0], suppressible);
|
|
6880
|
+
if (right !== null) {
|
|
6881
|
+
rhs.add(right);
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6884
|
+
walk(args[1], suppressible);
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
const name = bareId(node);
|
|
6888
|
+
if (name !== null) {
|
|
6889
|
+
elsewhere.add(name);
|
|
6890
|
+
return;
|
|
6891
|
+
}
|
|
6892
|
+
walk(args, suppressible);
|
|
6893
|
+
};
|
|
6894
|
+
walk(ast, true);
|
|
6895
|
+
for (const name of elsewhere) rhs.delete(name);
|
|
6896
|
+
return rhs;
|
|
6897
|
+
}
|
|
6898
|
+
function isRec21(v) {
|
|
6899
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6900
|
+
}
|
|
6901
|
+
function schemaIdOf(view) {
|
|
6902
|
+
const data = view.data;
|
|
6903
|
+
if (!isRec21(data)) return void 0;
|
|
6904
|
+
if (data.provider !== "schema") return void 0;
|
|
6905
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// src/validate-visibility-predicates.ts
|
|
6205
6909
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6910
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6911
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
6912
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
6206
6913
|
var CANONICAL = "visibleWhen";
|
|
6207
|
-
var ALIASES = ["visibleOn", "visibility"];
|
|
6208
|
-
function asArray35(v) {
|
|
6209
|
-
if (Array.isArray(v)) return v;
|
|
6210
|
-
if (v && typeof v === "object") {
|
|
6211
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6212
|
-
}
|
|
6213
|
-
return [];
|
|
6214
|
-
}
|
|
6215
6914
|
function predicateSource(v) {
|
|
6216
6915
|
if (typeof v === "string") return v;
|
|
6217
6916
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
@@ -6222,31 +6921,85 @@ function predicateSource(v) {
|
|
|
6222
6921
|
function usesRoot(source, root) {
|
|
6223
6922
|
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
6224
6923
|
}
|
|
6225
|
-
|
|
6924
|
+
function withoutStringLiterals(source) {
|
|
6925
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
6926
|
+
}
|
|
6927
|
+
var NON_CEL_SPELLINGS = [
|
|
6928
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
6929
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
6930
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
6931
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
6932
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
6933
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
6934
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
6935
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
6936
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
6937
|
+
];
|
|
6938
|
+
function quoteSource(source) {
|
|
6939
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
6940
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
6941
|
+
}
|
|
6942
|
+
function celRefusal(source) {
|
|
6943
|
+
if (!source.trim()) return null;
|
|
6944
|
+
const parsed = parseCelToAstWithReason(source);
|
|
6945
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
6946
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
6947
|
+
const identifiers = collectCelRootIdentifiers3(source);
|
|
6948
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
6949
|
+
const scannable = withoutStringLiterals(source);
|
|
6950
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
6951
|
+
}
|
|
6952
|
+
function boundName(overrun) {
|
|
6953
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6954
|
+
}
|
|
6955
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6956
|
+
function isNode3(v) {
|
|
6957
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6958
|
+
}
|
|
6959
|
+
function namespaceRoots(node, out) {
|
|
6960
|
+
if (Array.isArray(node)) {
|
|
6961
|
+
for (const child of node) namespaceRoots(child, out);
|
|
6962
|
+
return;
|
|
6963
|
+
}
|
|
6964
|
+
if (!isNode3(node)) return;
|
|
6965
|
+
const args = node.args;
|
|
6966
|
+
if (Array.isArray(args)) {
|
|
6967
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6968
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6969
|
+
out.add(receiver.args);
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
namespaceRoots(args, out);
|
|
6973
|
+
}
|
|
6974
|
+
function firstBareIdentifier(source, literalRhs) {
|
|
6975
|
+
const ast = parseCelToAst3(source);
|
|
6976
|
+
if (!ast) return null;
|
|
6977
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
6978
|
+
namespaceRoots(ast, rooted);
|
|
6979
|
+
const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
|
|
6980
|
+
return firstUndeclaredReference(source, [
|
|
6981
|
+
...VIEW_PAGE_EXTRA_ROOTS,
|
|
6982
|
+
...rooted,
|
|
6983
|
+
...literalSlot
|
|
6984
|
+
]);
|
|
6985
|
+
}
|
|
6986
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
6987
|
+
runtime: "record",
|
|
6988
|
+
metadata: "data"
|
|
6989
|
+
};
|
|
6990
|
+
var MISLAYER_BY_LAYER = {
|
|
6226
6991
|
runtime: {
|
|
6227
6992
|
forbiddenRoot: "data",
|
|
6228
|
-
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing
|
|
6993
|
+
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).",
|
|
6229
6994
|
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
6230
6995
|
},
|
|
6231
6996
|
metadata: {
|
|
6232
6997
|
forbiddenRoot: "record",
|
|
6233
|
-
message: "visibility predicate is rooted at `record.` \u2014 that is the
|
|
6998
|
+
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).",
|
|
6234
6999
|
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
6235
7000
|
}
|
|
6236
7001
|
};
|
|
6237
|
-
function checkElement(el, where, path, layer, findings) {
|
|
6238
|
-
for (const alias of ALIASES) {
|
|
6239
|
-
if (el[alias] !== void 0) {
|
|
6240
|
-
findings.push({
|
|
6241
|
-
severity: "warning",
|
|
6242
|
-
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
6243
|
-
where,
|
|
6244
|
-
path: `${path}.${alias}`,
|
|
6245
|
-
message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
|
|
6246
|
-
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
6247
|
-
});
|
|
6248
|
-
}
|
|
6249
|
-
}
|
|
7002
|
+
function checkElement(el, where, path, layer, findings, literalRhs = false) {
|
|
6250
7003
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6251
7004
|
const source = predicateSource(raw);
|
|
6252
7005
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -6260,50 +7013,380 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6260
7013
|
hint: rule.hint
|
|
6261
7014
|
});
|
|
6262
7015
|
}
|
|
7016
|
+
const refusal = source ? celRefusal(source) : null;
|
|
7017
|
+
if (source && refusal?.kind === "bounds") {
|
|
7018
|
+
const bound = boundName(refusal.overrun);
|
|
7019
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
7020
|
+
findings.push({
|
|
7021
|
+
severity: "error",
|
|
7022
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
7023
|
+
where,
|
|
7024
|
+
path,
|
|
7025
|
+
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).`,
|
|
7026
|
+
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.`
|
|
7027
|
+
});
|
|
7028
|
+
}
|
|
7029
|
+
if (source && refusal?.kind === "syntax") {
|
|
7030
|
+
findings.push({
|
|
7031
|
+
severity: "error",
|
|
7032
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
7033
|
+
where,
|
|
7034
|
+
path,
|
|
7035
|
+
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).`,
|
|
7036
|
+
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\`).`
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
if (source && !refusal) {
|
|
7040
|
+
const bare = firstBareIdentifier(source, literalRhs);
|
|
7041
|
+
if (bare) {
|
|
7042
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
7043
|
+
findings.push({
|
|
7044
|
+
severity: "error",
|
|
7045
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
7046
|
+
where,
|
|
7047
|
+
path,
|
|
7048
|
+
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).`,
|
|
7049
|
+
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`).")
|
|
7050
|
+
});
|
|
7051
|
+
}
|
|
7052
|
+
}
|
|
6263
7053
|
}
|
|
6264
7054
|
function isFieldObject(entry) {
|
|
6265
7055
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
6266
7056
|
}
|
|
6267
7057
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
6268
|
-
const
|
|
7058
|
+
const declaredLayer = opts.layer ?? "runtime";
|
|
6269
7059
|
const findings = [];
|
|
6270
|
-
const
|
|
6271
|
-
|
|
6272
|
-
const view
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
const
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
7060
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
7061
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
7062
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
7063
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
7064
|
+
const schemaBound = schemaIdOf(site.view) !== void 0;
|
|
7065
|
+
const literalRhs = schemaBound;
|
|
7066
|
+
const layer = schemaBound ? "metadata" : declaredLayer;
|
|
7067
|
+
for (const bucket of ["sections", "groups"]) {
|
|
7068
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7069
|
+
for (let s = 0; s < sections.length; s++) {
|
|
7070
|
+
const sec = sections[s];
|
|
7071
|
+
if (!sec || typeof sec !== "object") continue;
|
|
7072
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
7073
|
+
checkElement(sec, where, secPath, layer, findings, literalRhs);
|
|
7074
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
7075
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
7076
|
+
const entry = secFields[f];
|
|
7077
|
+
if (isFieldObject(entry)) {
|
|
7078
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
|
|
7079
|
+
}
|
|
6288
7080
|
}
|
|
6289
7081
|
}
|
|
6290
7082
|
}
|
|
6291
7083
|
}
|
|
6292
7084
|
}
|
|
6293
|
-
const
|
|
6294
|
-
|
|
6295
|
-
const
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
7085
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
7086
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
7087
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
7088
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
7089
|
+
checkElement(walked.component, where, walked.path, declaredLayer, findings);
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
return findings;
|
|
7093
|
+
}
|
|
7094
|
+
|
|
7095
|
+
// src/validate-predicate-path-refs.ts
|
|
7096
|
+
import { parseCelToAst as parseCelToAst4 } from "@objectstack/formula";
|
|
7097
|
+
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
7098
|
+
import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec";
|
|
7099
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
7100
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
7101
|
+
var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
|
|
7102
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
7103
|
+
var ROOT = "data";
|
|
7104
|
+
function defOf(schema) {
|
|
7105
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
7106
|
+
const s = schema;
|
|
7107
|
+
return s.def ?? s._def;
|
|
7108
|
+
}
|
|
7109
|
+
function peel(schema, depth = 0) {
|
|
7110
|
+
if (!schema || depth > 25) return schema;
|
|
7111
|
+
const d = defOf(schema);
|
|
7112
|
+
if (!d) return schema;
|
|
7113
|
+
switch (d.type) {
|
|
7114
|
+
case "optional":
|
|
7115
|
+
case "nullable":
|
|
7116
|
+
case "default":
|
|
7117
|
+
case "prefault":
|
|
7118
|
+
case "readonly":
|
|
7119
|
+
case "catch":
|
|
7120
|
+
case "nonoptional":
|
|
7121
|
+
return peel(d.innerType, depth + 1);
|
|
7122
|
+
case "lazy":
|
|
7123
|
+
return peel(d.getter(), depth + 1);
|
|
7124
|
+
case "pipe": {
|
|
7125
|
+
const inner = peel(d.in, depth + 1);
|
|
7126
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
7127
|
+
}
|
|
7128
|
+
default:
|
|
7129
|
+
return schema;
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
function optionsOf(d) {
|
|
7133
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
7134
|
+
}
|
|
7135
|
+
function keysOf(schema, depth = 0) {
|
|
7136
|
+
if (depth > 25) return null;
|
|
7137
|
+
const u = peel(schema);
|
|
7138
|
+
const d = defOf(u);
|
|
7139
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
7140
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
7141
|
+
const all = /* @__PURE__ */ new Set();
|
|
7142
|
+
let keyBearing = false;
|
|
7143
|
+
for (const option of optionsOf(d)) {
|
|
7144
|
+
const k = keysOf(option, depth + 1);
|
|
7145
|
+
if (!k) continue;
|
|
7146
|
+
keyBearing = true;
|
|
7147
|
+
for (const key of k) all.add(key);
|
|
7148
|
+
}
|
|
7149
|
+
return keyBearing ? [...all] : null;
|
|
7150
|
+
}
|
|
7151
|
+
if (d?.type === "intersection") {
|
|
7152
|
+
const left = keysOf(d.left, depth + 1);
|
|
7153
|
+
const right = keysOf(d.right, depth + 1);
|
|
7154
|
+
if (!left && !right) return null;
|
|
7155
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
7156
|
+
}
|
|
7157
|
+
return null;
|
|
7158
|
+
}
|
|
7159
|
+
function propertyOf(schema, key, depth = 0) {
|
|
7160
|
+
if (depth > 25) return void 0;
|
|
7161
|
+
const u = peel(schema);
|
|
7162
|
+
const d = defOf(u);
|
|
7163
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
7164
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
7165
|
+
for (const option of optionsOf(d)) {
|
|
7166
|
+
const found = propertyOf(option, key, depth + 1);
|
|
7167
|
+
if (found !== void 0) return found;
|
|
7168
|
+
}
|
|
7169
|
+
}
|
|
7170
|
+
if (d?.type === "intersection") {
|
|
7171
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
7172
|
+
}
|
|
7173
|
+
return void 0;
|
|
7174
|
+
}
|
|
7175
|
+
function rowScopeOf(scope, key) {
|
|
7176
|
+
const prop = propertyOf(scope, key);
|
|
7177
|
+
if (prop === void 0) return void 0;
|
|
7178
|
+
let node = peel(prop);
|
|
7179
|
+
for (let i = 0; i < 25; i++) {
|
|
7180
|
+
const d = defOf(node);
|
|
7181
|
+
if (d?.type === "array") node = peel(d.element);
|
|
7182
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
7183
|
+
else return node;
|
|
7184
|
+
}
|
|
7185
|
+
return node;
|
|
7186
|
+
}
|
|
7187
|
+
function stepInto(scope, segment) {
|
|
7188
|
+
const u = peel(scope);
|
|
7189
|
+
const d = defOf(u);
|
|
7190
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
7191
|
+
const declared = keysOf(u);
|
|
7192
|
+
if (declared === null) return { kind: "opaque" };
|
|
7193
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
7194
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
7195
|
+
}
|
|
7196
|
+
function isNode4(v) {
|
|
7197
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
7198
|
+
}
|
|
7199
|
+
function memberChain(node) {
|
|
7200
|
+
if (!isNode4(node)) return null;
|
|
7201
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
7202
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
7203
|
+
const head = memberChain(node.args[0]);
|
|
7204
|
+
return head ? [...head, node.args[1]] : null;
|
|
7205
|
+
}
|
|
7206
|
+
return null;
|
|
7207
|
+
}
|
|
7208
|
+
function rootedPaths(node, out) {
|
|
7209
|
+
if (Array.isArray(node)) {
|
|
7210
|
+
for (const child of node) rootedPaths(child, out);
|
|
7211
|
+
return;
|
|
7212
|
+
}
|
|
7213
|
+
if (!isNode4(node)) return;
|
|
7214
|
+
if (node.op === ".") {
|
|
7215
|
+
const chain = memberChain(node);
|
|
7216
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
7217
|
+
out.push(chain.slice(1));
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
}
|
|
7221
|
+
rootedPaths(node.args, out);
|
|
7222
|
+
}
|
|
7223
|
+
var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
|
|
7224
|
+
function equalitySites(node, out) {
|
|
7225
|
+
if (Array.isArray(node)) {
|
|
7226
|
+
for (const child of node) equalitySites(child, out);
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
7229
|
+
if (!isNode4(node)) return;
|
|
7230
|
+
const args = node.args;
|
|
7231
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7232
|
+
equalitySites(args[1], out);
|
|
7233
|
+
return;
|
|
7234
|
+
}
|
|
7235
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
7236
|
+
out.push({ op: node.op, right: args[1] });
|
|
7237
|
+
}
|
|
7238
|
+
equalitySites(args, out);
|
|
7239
|
+
}
|
|
7240
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
7241
|
+
if (Array.isArray(node)) {
|
|
7242
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
7243
|
+
return;
|
|
7244
|
+
}
|
|
7245
|
+
if (!isNode4(node)) return;
|
|
7246
|
+
const args = node.args;
|
|
7247
|
+
if (Array.isArray(args)) {
|
|
7248
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
7249
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
7250
|
+
excluded.add(receiver.args);
|
|
7251
|
+
}
|
|
7252
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7253
|
+
const macroArgs = args[2];
|
|
7254
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
7255
|
+
const bound = macroArgs[0];
|
|
7256
|
+
if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
7257
|
+
excluded.add(bound.args);
|
|
7258
|
+
}
|
|
7259
|
+
}
|
|
7260
|
+
}
|
|
7261
|
+
}
|
|
7262
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
7263
|
+
values.add(node.args);
|
|
7264
|
+
return;
|
|
7265
|
+
}
|
|
7266
|
+
classifyIdentifiers(args, values, excluded);
|
|
7267
|
+
}
|
|
7268
|
+
function predicateSource2(v) {
|
|
7269
|
+
if (typeof v === "string") return v;
|
|
7270
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
7271
|
+
return v.source;
|
|
7272
|
+
}
|
|
7273
|
+
return void 0;
|
|
7274
|
+
}
|
|
7275
|
+
function isRec22(v) {
|
|
7276
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7277
|
+
}
|
|
7278
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
7279
|
+
const ast = parseCelToAst4(source);
|
|
7280
|
+
if (!ast) return;
|
|
7281
|
+
const paths = [];
|
|
7282
|
+
rootedPaths(ast, paths);
|
|
7283
|
+
for (const segments of paths) {
|
|
7284
|
+
let cursor = scope;
|
|
7285
|
+
const walked = [];
|
|
7286
|
+
for (const segment of segments) {
|
|
7287
|
+
const step = stepInto(cursor, segment);
|
|
7288
|
+
if (step.kind === "opaque") break;
|
|
7289
|
+
if (step.kind === "undeclared") {
|
|
7290
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
7291
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
7292
|
+
findings.push({
|
|
7293
|
+
severity: "error",
|
|
7294
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
7295
|
+
where,
|
|
7296
|
+
path,
|
|
7297
|
+
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).`,
|
|
7298
|
+
hint: `${formatSuggestion4(findClosestMatches4(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
7299
|
+
});
|
|
7300
|
+
break;
|
|
7301
|
+
}
|
|
7302
|
+
walked.push(segment);
|
|
7303
|
+
cursor = step.next;
|
|
7304
|
+
}
|
|
7305
|
+
}
|
|
7306
|
+
const declaredHere = keysOf(scope);
|
|
7307
|
+
const rhsOnly = bareRhsOnlyIdentifiers(ast);
|
|
7308
|
+
if (declaredHere) {
|
|
7309
|
+
const values = /* @__PURE__ */ new Set();
|
|
7310
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
7311
|
+
classifyIdentifiers(ast, values, excluded);
|
|
7312
|
+
for (const id of values) {
|
|
7313
|
+
if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
|
|
7314
|
+
findings.push({
|
|
7315
|
+
severity: "error",
|
|
7316
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
7317
|
+
where,
|
|
7318
|
+
path,
|
|
7319
|
+
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).`,
|
|
7320
|
+
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).`
|
|
7321
|
+
});
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
const sites = [];
|
|
7325
|
+
equalitySites(ast, sites);
|
|
7326
|
+
for (const { op, right } of sites) {
|
|
7327
|
+
const chain = memberChain(right);
|
|
7328
|
+
if (!chain) continue;
|
|
7329
|
+
const text = chain.join(".");
|
|
7330
|
+
if (!PATH_SHAPED_RHS.test(text)) continue;
|
|
7331
|
+
const dotted = chain.length > 1;
|
|
7332
|
+
findings.push({
|
|
7333
|
+
severity: dotted ? "error" : "warning",
|
|
7334
|
+
rule: PREDICATE_RHS_PATH_SHAPED,
|
|
7335
|
+
where,
|
|
7336
|
+
path,
|
|
7337
|
+
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).`,
|
|
7338
|
+
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.`
|
|
7339
|
+
});
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
7343
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
7344
|
+
for (let i = 0; i < entries.length; i++) {
|
|
7345
|
+
const entry = entries[i];
|
|
7346
|
+
if (!isRec22(entry)) continue;
|
|
7347
|
+
const path = `${base}[${i}]`;
|
|
7348
|
+
for (const key of PREDICATE_KEYS) {
|
|
7349
|
+
const source = predicateSource2(entry[key]);
|
|
7350
|
+
if (source !== void 0 && source.trim()) {
|
|
7351
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
7352
|
+
break;
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
7355
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
7356
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
7357
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
7362
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
|
|
7363
|
+
const findings = [];
|
|
7364
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
7365
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
7366
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
7367
|
+
const schemaId = schemaIdOf(site.view);
|
|
7368
|
+
if (!schemaId) continue;
|
|
7369
|
+
let root;
|
|
7370
|
+
try {
|
|
7371
|
+
root = resolveSchema(schemaId);
|
|
7372
|
+
} catch {
|
|
7373
|
+
root = void 0;
|
|
7374
|
+
}
|
|
7375
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
7376
|
+
for (const bucket of ["sections", "groups"]) {
|
|
7377
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
7378
|
+
for (let s = 0; s < sections.length; s++) {
|
|
7379
|
+
const section = sections[s];
|
|
7380
|
+
if (!isRec22(section)) continue;
|
|
7381
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
7382
|
+
for (const key of PREDICATE_KEYS) {
|
|
7383
|
+
const source = predicateSource2(section[key]);
|
|
7384
|
+
if (source !== void 0 && source.trim()) {
|
|
7385
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
7386
|
+
break;
|
|
7387
|
+
}
|
|
7388
|
+
}
|
|
7389
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
6307
7390
|
}
|
|
6308
7391
|
}
|
|
6309
7392
|
}
|
|
@@ -6325,6 +7408,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
|
6325
7408
|
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
6326
7409
|
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
6327
7410
|
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
7411
|
+
var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
|
|
6328
7412
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
6329
7413
|
var OWD_ALIAS_FIX = {
|
|
6330
7414
|
read: "public_read",
|
|
@@ -6370,6 +7454,13 @@ function firstMasterDetailField(obj) {
|
|
|
6370
7454
|
}
|
|
6371
7455
|
return void 0;
|
|
6372
7456
|
}
|
|
7457
|
+
function resolveCbpRelation(obj) {
|
|
7458
|
+
const entries = asArray36(obj.fields);
|
|
7459
|
+
const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
|
|
7460
|
+
const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
|
|
7461
|
+
if (!found) return void 0;
|
|
7462
|
+
return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
|
|
7463
|
+
}
|
|
6373
7464
|
function grantsObjectAccess(p) {
|
|
6374
7465
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
6375
7466
|
}
|
|
@@ -6415,6 +7506,16 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6415
7506
|
});
|
|
6416
7507
|
}
|
|
6417
7508
|
}
|
|
7509
|
+
if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
|
|
7510
|
+
findings.push({
|
|
7511
|
+
severity: "error",
|
|
7512
|
+
rule: SECURITY_CBP_NO_RELATION,
|
|
7513
|
+
where: `object "${objName}"`,
|
|
7514
|
+
path: `${objPath}.sharingModel`,
|
|
7515
|
+
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.`,
|
|
7516
|
+
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'.`
|
|
7517
|
+
});
|
|
7518
|
+
}
|
|
6418
7519
|
if (typeof external === "string") {
|
|
6419
7520
|
if (OWD_ALIAS_FIX[external]) {
|
|
6420
7521
|
findings.push({
|
|
@@ -6480,53 +7581,6 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6480
7581
|
}
|
|
6481
7582
|
}
|
|
6482
7583
|
}
|
|
6483
|
-
const flagRole = (kind, name, label2, where, path) => {
|
|
6484
|
-
if (identifierHasRoleToken(name)) {
|
|
6485
|
-
findings.push({
|
|
6486
|
-
severity: "error",
|
|
6487
|
-
rule: SECURITY_ROLE_WORD,
|
|
6488
|
-
where,
|
|
6489
|
-
path,
|
|
6490
|
-
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).`,
|
|
6491
|
-
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
6492
|
-
});
|
|
6493
|
-
} else if (labelHasRoleWord(label2)) {
|
|
6494
|
-
findings.push({
|
|
6495
|
-
severity: "error",
|
|
6496
|
-
rule: SECURITY_ROLE_WORD,
|
|
6497
|
-
where,
|
|
6498
|
-
path: `${path.replace(/\.name$/, "")}.label`,
|
|
6499
|
-
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
6500
|
-
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
6501
|
-
});
|
|
6502
|
-
}
|
|
6503
|
-
};
|
|
6504
|
-
for (let i = 0; i < objects.length; i++) {
|
|
6505
|
-
const obj = objects[i];
|
|
6506
|
-
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
6507
|
-
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
6508
|
-
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
6509
|
-
for (const f of asArray36(obj.fields)) {
|
|
6510
|
-
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
6511
|
-
}
|
|
6512
|
-
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
6513
|
-
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
6514
|
-
}
|
|
6515
|
-
}
|
|
6516
|
-
for (let i = 0; i < permissionSets.length; i++) {
|
|
6517
|
-
const ps = permissionSets[i];
|
|
6518
|
-
if (!ps || typeof ps !== "object") continue;
|
|
6519
|
-
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
6520
|
-
}
|
|
6521
|
-
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
6522
|
-
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
6523
|
-
}
|
|
6524
|
-
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
6525
|
-
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
6526
|
-
}
|
|
6527
|
-
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
6528
|
-
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
6529
|
-
}
|
|
6530
7584
|
const stackSetNames = new Set(
|
|
6531
7585
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
6532
7586
|
);
|
|
@@ -6647,6 +7701,60 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6647
7701
|
}
|
|
6648
7702
|
return findings;
|
|
6649
7703
|
}
|
|
7704
|
+
function validateSecurityRoleWord(stack) {
|
|
7705
|
+
const findings = [];
|
|
7706
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
7707
|
+
const objects = asArray36(stack.objects);
|
|
7708
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7709
|
+
const flagRole = (kind, name, label2, where, path) => {
|
|
7710
|
+
if (identifierHasRoleToken(name)) {
|
|
7711
|
+
findings.push({
|
|
7712
|
+
severity: "error",
|
|
7713
|
+
rule: SECURITY_ROLE_WORD,
|
|
7714
|
+
where,
|
|
7715
|
+
path,
|
|
7716
|
+
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).`,
|
|
7717
|
+
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7718
|
+
});
|
|
7719
|
+
} else if (labelHasRoleWord(label2)) {
|
|
7720
|
+
findings.push({
|
|
7721
|
+
severity: "error",
|
|
7722
|
+
rule: SECURITY_ROLE_WORD,
|
|
7723
|
+
where,
|
|
7724
|
+
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7725
|
+
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7726
|
+
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
};
|
|
7730
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7731
|
+
const obj = objects[i];
|
|
7732
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7733
|
+
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7734
|
+
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7735
|
+
for (const f of asArray36(obj.fields)) {
|
|
7736
|
+
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7737
|
+
}
|
|
7738
|
+
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
7739
|
+
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
for (let i = 0; i < permissionSets.length; i++) {
|
|
7743
|
+
const ps = permissionSets[i];
|
|
7744
|
+
if (!ps || typeof ps !== "object") continue;
|
|
7745
|
+
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7746
|
+
}
|
|
7747
|
+
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
7748
|
+
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7749
|
+
}
|
|
7750
|
+
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
7751
|
+
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7752
|
+
}
|
|
7753
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7754
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7755
|
+
}
|
|
7756
|
+
return findings;
|
|
7757
|
+
}
|
|
6650
7758
|
|
|
6651
7759
|
// src/validate-org-axis-red-lines.ts
|
|
6652
7760
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
@@ -6805,9 +7913,15 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
6805
7913
|
}
|
|
6806
7914
|
|
|
6807
7915
|
// src/validate-rls-predicate-enforceability.ts
|
|
6808
|
-
import {
|
|
7916
|
+
import {
|
|
7917
|
+
isPushdownableCel,
|
|
7918
|
+
isSupportedRlsExpression,
|
|
7919
|
+
parseCelToAstWithReason as parseCelToAstWithReason2,
|
|
7920
|
+
sqlPredicateToCel
|
|
7921
|
+
} from "@objectstack/formula";
|
|
6809
7922
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
6810
7923
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
7924
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
6811
7925
|
function asArray39(v) {
|
|
6812
7926
|
if (Array.isArray(v)) return v;
|
|
6813
7927
|
if (v && typeof v === "object") {
|
|
@@ -6819,6 +7933,13 @@ function str3(v) {
|
|
|
6819
7933
|
return typeof v === "string" ? v : "";
|
|
6820
7934
|
}
|
|
6821
7935
|
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.";
|
|
7936
|
+
function boundsOverrunOf(bridged) {
|
|
7937
|
+
const parsed = parseCelToAstWithReason2(bridged);
|
|
7938
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
7939
|
+
}
|
|
7940
|
+
function quote(source) {
|
|
7941
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
7942
|
+
}
|
|
6822
7943
|
function consequence(clause) {
|
|
6823
7944
|
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). ';
|
|
6824
7945
|
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.";
|
|
@@ -6832,14 +7953,30 @@ function validateRlsPredicateEnforceability(stack) {
|
|
|
6832
7953
|
const source = str3(policy[clause]);
|
|
6833
7954
|
if (!source.trim()) continue;
|
|
6834
7955
|
if (isSupportedRlsExpression(source)) continue;
|
|
6835
|
-
const
|
|
7956
|
+
const bridged = sqlPredicateToCel(source);
|
|
7957
|
+
const why = isPushdownableCel(bridged);
|
|
6836
7958
|
const detail = why.ok ? "" : why.detail;
|
|
6837
7959
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
7960
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
6838
7961
|
const psName = str3(ps.name) || String(psIndex);
|
|
6839
7962
|
const policyName = str3(policy.name) || String(pIndex);
|
|
6840
7963
|
const object = str3(policy.object);
|
|
6841
7964
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
6842
7965
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
7966
|
+
if (overrun) {
|
|
7967
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
7968
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
7969
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
7970
|
+
findings.push({
|
|
7971
|
+
severity: "error",
|
|
7972
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
7973
|
+
where,
|
|
7974
|
+
path,
|
|
7975
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
7976
|
+
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).`
|
|
7977
|
+
});
|
|
7978
|
+
continue;
|
|
7979
|
+
}
|
|
6843
7980
|
if (parseError) {
|
|
6844
7981
|
findings.push({
|
|
6845
7982
|
severity: "error",
|
|
@@ -6870,11 +8007,11 @@ import { createRequire as createRequire4 } from "module";
|
|
|
6870
8007
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
6871
8008
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
6872
8009
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
6873
|
-
var
|
|
8010
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6874
8011
|
function asArray40(v) {
|
|
6875
|
-
if (Array.isArray(v)) return v.filter(
|
|
6876
|
-
if (
|
|
6877
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8012
|
+
if (Array.isArray(v)) return v.filter(isRec23);
|
|
8013
|
+
if (isRec23(v)) {
|
|
8014
|
+
return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
|
|
6878
8015
|
}
|
|
6879
8016
|
return [];
|
|
6880
8017
|
}
|
|
@@ -6891,7 +8028,7 @@ function loadAjv() {
|
|
|
6891
8028
|
`@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.`
|
|
6892
8029
|
);
|
|
6893
8030
|
}
|
|
6894
|
-
const ctor =
|
|
8031
|
+
const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
6895
8032
|
cachedAjv = ctor;
|
|
6896
8033
|
return ctor;
|
|
6897
8034
|
}
|
|
@@ -6906,7 +8043,7 @@ function loadAddFormats() {
|
|
|
6906
8043
|
`@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.`
|
|
6907
8044
|
);
|
|
6908
8045
|
}
|
|
6909
|
-
const plugin =
|
|
8046
|
+
const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
6910
8047
|
cachedAddFormats = plugin;
|
|
6911
8048
|
return plugin;
|
|
6912
8049
|
}
|
|
@@ -6936,13 +8073,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
6936
8073
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
6937
8074
|
for (const branch of ["then", "otherwise"]) {
|
|
6938
8075
|
const nested = rule[branch];
|
|
6939
|
-
if (
|
|
8076
|
+
if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
6940
8077
|
}
|
|
6941
8078
|
return out;
|
|
6942
8079
|
}
|
|
6943
8080
|
function walkObjectValidationRules(stack) {
|
|
6944
8081
|
const walked = [];
|
|
6945
|
-
if (!
|
|
8082
|
+
if (!isRec23(stack)) return walked;
|
|
6946
8083
|
for (const obj of asArray40(stack.objects)) {
|
|
6947
8084
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6948
8085
|
const validations = obj.validations;
|
|
@@ -6977,7 +8114,7 @@ function validateRuleCompilability(stack) {
|
|
|
6977
8114
|
});
|
|
6978
8115
|
}
|
|
6979
8116
|
}
|
|
6980
|
-
if (rule.type === "json_schema" &&
|
|
8117
|
+
if (rule.type === "json_schema" && isRec23(rule.schema)) {
|
|
6981
8118
|
try {
|
|
6982
8119
|
createRuntimeAjv().compile(rule.schema);
|
|
6983
8120
|
} catch (err) {
|
|
@@ -6997,7 +8134,7 @@ function validateRuleCompilability(stack) {
|
|
|
6997
8134
|
|
|
6998
8135
|
// src/validate-rule-schema-formats.ts
|
|
6999
8136
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7000
|
-
var
|
|
8137
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7001
8138
|
var SUBSCHEMA_KEYS = [
|
|
7002
8139
|
"additionalItems",
|
|
7003
8140
|
"additionalProperties",
|
|
@@ -7021,7 +8158,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7021
8158
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7022
8159
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7023
8160
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7024
|
-
if (!
|
|
8161
|
+
if (!isRec24(schema)) return;
|
|
7025
8162
|
if (typeof schema.format === "string") {
|
|
7026
8163
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7027
8164
|
}
|
|
@@ -7040,7 +8177,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7040
8177
|
}
|
|
7041
8178
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7042
8179
|
const value = schema[key];
|
|
7043
|
-
if (!
|
|
8180
|
+
if (!isRec24(value)) continue;
|
|
7044
8181
|
for (const [name, entry] of Object.entries(value)) {
|
|
7045
8182
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7046
8183
|
}
|
|
@@ -7048,13 +8185,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7048
8185
|
const items = schema.items;
|
|
7049
8186
|
if (Array.isArray(items)) {
|
|
7050
8187
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7051
|
-
} else if (
|
|
8188
|
+
} else if (isRec24(items)) {
|
|
7052
8189
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7053
8190
|
}
|
|
7054
8191
|
const dependencies = schema.dependencies;
|
|
7055
|
-
if (
|
|
8192
|
+
if (isRec24(dependencies)) {
|
|
7056
8193
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7057
|
-
if (!
|
|
8194
|
+
if (!isRec24(entry)) continue;
|
|
7058
8195
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7059
8196
|
}
|
|
7060
8197
|
}
|
|
@@ -7093,7 +8230,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7093
8230
|
const findings = [];
|
|
7094
8231
|
const pending = [];
|
|
7095
8232
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7096
|
-
if (rule.type !== "json_schema" || !
|
|
8233
|
+
if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
|
|
7097
8234
|
const uses = [];
|
|
7098
8235
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7099
8236
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7126,7 +8263,7 @@ function asArray41(v) {
|
|
|
7126
8263
|
}
|
|
7127
8264
|
return [];
|
|
7128
8265
|
}
|
|
7129
|
-
function
|
|
8266
|
+
function strName20(v) {
|
|
7130
8267
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7131
8268
|
}
|
|
7132
8269
|
function strList3(v) {
|
|
@@ -7141,7 +8278,7 @@ function collectNamePlacedActions(stack) {
|
|
|
7141
8278
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7142
8279
|
}
|
|
7143
8280
|
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
7144
|
-
const n =
|
|
8281
|
+
const n = strName20(def?.name);
|
|
7145
8282
|
if (n) placed.add(n);
|
|
7146
8283
|
}
|
|
7147
8284
|
};
|
|
@@ -7167,7 +8304,7 @@ function validateActionLocations(stack) {
|
|
|
7167
8304
|
const check = (action, path) => {
|
|
7168
8305
|
if (!action || typeof action !== "object") return;
|
|
7169
8306
|
if ("locations" in action) return;
|
|
7170
|
-
const name =
|
|
8307
|
+
const name = strName20(action.name);
|
|
7171
8308
|
if (!name) return;
|
|
7172
8309
|
if (namePlaced.has(name)) return;
|
|
7173
8310
|
findings.push({
|
|
@@ -7176,7 +8313,7 @@ function validateActionLocations(stack) {
|
|
|
7176
8313
|
where: `action "${name}"`,
|
|
7177
8314
|
path,
|
|
7178
8315
|
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.`,
|
|
7179
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
8316
|
+
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."
|
|
7180
8317
|
});
|
|
7181
8318
|
};
|
|
7182
8319
|
const actions = asArray41(stack.actions);
|
|
@@ -7197,6 +8334,7 @@ import {
|
|
|
7197
8334
|
APPROVAL_REVISE_NODE_TYPE,
|
|
7198
8335
|
collectFlowGraphs as collectFlowGraphs2
|
|
7199
8336
|
} from "@objectstack/spec/automation";
|
|
8337
|
+
import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
|
|
7200
8338
|
function asArray42(v) {
|
|
7201
8339
|
if (Array.isArray(v)) return v;
|
|
7202
8340
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7471,7 +8609,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7471
8609
|
function filterCarriesNoCondition(filter) {
|
|
7472
8610
|
if (filter === void 0 || filter === null) return true;
|
|
7473
8611
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7474
|
-
return
|
|
8612
|
+
return reduceFilterVerdict2(filter) === "true";
|
|
8613
|
+
}
|
|
8614
|
+
function describeUnboundedFilter(filter) {
|
|
8615
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8616
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8617
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8618
|
+
}
|
|
8619
|
+
function previewFilter(filter) {
|
|
8620
|
+
try {
|
|
8621
|
+
const json = JSON.stringify(filter);
|
|
8622
|
+
if (typeof json !== "string") return typeof filter;
|
|
8623
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8624
|
+
} catch {
|
|
8625
|
+
return typeof filter;
|
|
8626
|
+
}
|
|
7475
8627
|
}
|
|
7476
8628
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7477
8629
|
for (const node of nodes) {
|
|
@@ -7482,10 +8634,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7482
8634
|
if (cfg.multi !== true) continue;
|
|
7483
8635
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7484
8636
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7485
|
-
const filterState = cfg.filter
|
|
8637
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7486
8638
|
findings.push({
|
|
7487
8639
|
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
|
|
8640
|
+
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
8641
|
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
8642
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7491
8643
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7786,6 +8938,13 @@ var TYPE_COLLECTIONS = [
|
|
|
7786
8938
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
7787
8939
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7788
8940
|
// silent, which is the shape this lint exists to prevent.
|
|
8941
|
+
//
|
|
8942
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
8943
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
8944
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
8945
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
8946
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
8947
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
7789
8948
|
{ type: "dashboard", key: "dashboards" }
|
|
7790
8949
|
];
|
|
7791
8950
|
function lintLivenessProperties(stack) {
|
|
@@ -8157,8 +9316,12 @@ var AUTHORING_RULES = [
|
|
|
8157
9316
|
}))
|
|
8158
9317
|
},
|
|
8159
9318
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8160
|
-
// mode)
|
|
8161
|
-
//
|
|
9319
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
9320
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
9321
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
9322
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
9323
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
9324
|
+
// parse would have eaten".
|
|
8162
9325
|
{
|
|
8163
9326
|
name: "validateListViewMode",
|
|
8164
9327
|
tier: "gating",
|
|
@@ -8189,9 +9352,38 @@ var AUTHORING_RULES = [
|
|
|
8189
9352
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8190
9353
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8191
9354
|
},
|
|
8192
|
-
//
|
|
8193
|
-
//
|
|
8194
|
-
//
|
|
9355
|
+
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
9356
|
+
// in `enable.apiMethods` that its own resolved affordances refuse. Every key
|
|
9357
|
+
// is one we know and each is individually valid, so #4001's unknown-key
|
|
9358
|
+
// rejection and the Zod parse both pass it; the contradiction is only visible
|
|
9359
|
+
// when the two keys are read TOGETHER, which nothing did at authoring time.
|
|
9360
|
+
//
|
|
9361
|
+
// `gating` because the declaration is already false when it ships: objectql's
|
|
9362
|
+
// registry strips the verb at registration, so the metadata advertises an API
|
|
9363
|
+
// the product does not serve. That strip has been correct and silent — a
|
|
9364
|
+
// `console.warn` on every control-plane boot that went unread for the life of
|
|
9365
|
+
// a real divergence (`sys_environment`/`sys_package`). This entry is the
|
|
9366
|
+
// ruling's "close it where the author is"; boot stays warn-and-strip.
|
|
9367
|
+
//
|
|
9368
|
+
// Pre-parse: the predicate reads only authored keys, and the finding must
|
|
9369
|
+
// survive an unrelated schema error elsewhere in the stack.
|
|
9370
|
+
{
|
|
9371
|
+
name: "validateManagedApiMethods",
|
|
9372
|
+
tier: "gating",
|
|
9373
|
+
input: "normalized",
|
|
9374
|
+
commands: ALL,
|
|
9375
|
+
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9376
|
+
surfaces: CLI_ONLY,
|
|
9377
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9378
|
+
run: (stack) => validateManagedApiMethods(stack)
|
|
9379
|
+
},
|
|
9380
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
9381
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
9382
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
9383
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
9384
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
9385
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
9386
|
+
// declared and which survives the parse untouched.
|
|
8195
9387
|
{
|
|
8196
9388
|
name: "validateViewContainers",
|
|
8197
9389
|
tier: "gating",
|
|
@@ -8241,6 +9433,31 @@ var AUTHORING_RULES = [
|
|
|
8241
9433
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8242
9434
|
run: (stack) => validateFilterTokens(stack)
|
|
8243
9435
|
},
|
|
9436
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9437
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9438
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
9439
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
9440
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
9441
|
+
// same-direction precedent one shape over.
|
|
9442
|
+
{
|
|
9443
|
+
name: "validateEmptyCombinators",
|
|
9444
|
+
tier: "gating",
|
|
9445
|
+
input: "parsed",
|
|
9446
|
+
commands: ALL,
|
|
9447
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
9448
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
9449
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
9450
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
9451
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
9452
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
9453
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
9454
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
9455
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
9456
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
9457
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9458
|
+
runtimeTypes: ["flow"],
|
|
9459
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
9460
|
+
},
|
|
8244
9461
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8245
9462
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8246
9463
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8289,6 +9506,16 @@ var AUTHORING_RULES = [
|
|
|
8289
9506
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8290
9507
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8291
9508
|
// separate step, once the warning-period inventory is empty.
|
|
9509
|
+
//
|
|
9510
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9511
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9512
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9513
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9514
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9515
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9516
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9517
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9518
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8292
9519
|
{
|
|
8293
9520
|
name: "validateComponentProps",
|
|
8294
9521
|
tier: "advisory",
|
|
@@ -8367,10 +9594,12 @@ var AUTHORING_RULES = [
|
|
|
8367
9594
|
//
|
|
8368
9595
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8369
9596
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8370
|
-
// dead?
|
|
9597
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8371
9598
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8372
|
-
// predicate cannot route at all,
|
|
8373
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9599
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9600
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9601
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9602
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8374
9603
|
// can be changed by installing a package, so there is no reading under which
|
|
8375
9604
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8376
9605
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8497,19 +9726,117 @@ var AUTHORING_RULES = [
|
|
|
8497
9726
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8498
9727
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8499
9728
|
},
|
|
8500
|
-
// ADR-0089 D3b —
|
|
8501
|
-
//
|
|
8502
|
-
//
|
|
9729
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
9730
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
9731
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
9732
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
9733
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
9734
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
9735
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
9736
|
+
//
|
|
9737
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
9738
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
9739
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
9740
|
+
// a change to this package's external input contract, and the maintainer's
|
|
9741
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
9742
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
9743
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
9744
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
9745
|
+
//
|
|
9746
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
9747
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
9748
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
9749
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
9750
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
9751
|
+
// "pre-parse evidence" one.
|
|
9752
|
+
//
|
|
9753
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
9754
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
9755
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
9756
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9757
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9758
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
9759
|
+
//
|
|
9760
|
+
// ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
|
|
9761
|
+
//
|
|
9762
|
+
// This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
|
|
9763
|
+
// in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
|
|
9764
|
+
// `advisories` channel landed (PR #7435). Before that move a `view` written
|
|
9765
|
+
// through Studio / REST `/meta` / MCP — the only door most tenants have, and
|
|
9766
|
+
// the door AI authors use — was judged by NONE of the family's rule ids (six
|
|
9767
|
+
// at the time of the move; seven since #7659 added
|
|
9768
|
+
// `predicate-rhs-path-shaped` inside the second entry).
|
|
9769
|
+
//
|
|
9770
|
+
// They move together on purpose, and the two entries carry one comment because
|
|
9771
|
+
// they are one wall: #7214's implementer wired its own rule here alone and then
|
|
9772
|
+
// REVERTED it, because a `view` refused for an unresolvable predicate PATH
|
|
9773
|
+
// while a predicate that does not parse at all walks through the same door is
|
|
9774
|
+
// less predictable than refusing neither. A half-wired wall is worse than an
|
|
9775
|
+
// unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
|
|
9776
|
+
// directly: every id on this surface is gated at the runtime door, or none is.
|
|
9777
|
+
//
|
|
9778
|
+
// The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
|
|
9779
|
+
// and re-measuring it at move time found it false: both rule functions read
|
|
9780
|
+
// `stack.views` and `stack.pages` and NO other collection — never `objects` —
|
|
9781
|
+
// so the per-write snapshot the gate builds is not partial for them, it is
|
|
9782
|
+
// complete. (`pages` is simply absent on a `view` write, so the page half
|
|
9783
|
+
// contributes zero findings to both differential passes rather than inventing
|
|
9784
|
+
// any.) The reason was not describing this rule; it was the default a rule got
|
|
9785
|
+
// when nobody measured, which is the #4409/#4463 defect one layer in.
|
|
9786
|
+
//
|
|
9787
|
+
// Runtime input tier: the gate hands the rules the body as persisted, without
|
|
9788
|
+
// `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
|
|
9789
|
+
// door. That costs the family nothing — `validateVisibilityPredicates` reads
|
|
9790
|
+
// `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
|
|
9791
|
+
// so a caller handing it a raw authored object still gets a verdict.
|
|
8503
9792
|
{
|
|
8504
9793
|
name: "validateVisibilityPredicates",
|
|
8505
|
-
tier: "
|
|
9794
|
+
tier: "gating",
|
|
8506
9795
|
input: "normalized",
|
|
8507
9796
|
commands: ALL,
|
|
8508
9797
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
8509
|
-
surfaces:
|
|
8510
|
-
|
|
9798
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9799
|
+
runtimeTypes: ["view"],
|
|
8511
9800
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8512
9801
|
},
|
|
9802
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
9803
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
9804
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
9805
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
9806
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
9807
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
9808
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
9809
|
+
// #6254's 16 bare `type ==` predicates.
|
|
9810
|
+
//
|
|
9811
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
9812
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
9813
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9814
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9815
|
+
// false build errors. See the rule's module note.
|
|
9816
|
+
//
|
|
9817
|
+
// #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
|
|
9818
|
+
// resolution question at all: the metadata-admin renderer resolves paths only
|
|
9819
|
+
// on the LEFT of `==` / `!=` and hands the right side to its literal parser,
|
|
9820
|
+
// so `data.a == data.b` resolves both sides cleanly, passes the two rules
|
|
9821
|
+
// above, and still compares against the string "data.b" — a constant verdict.
|
|
9822
|
+
// It carries `error` on a dotted chain (no reading under which it worked) and
|
|
9823
|
+
// `warning` on a bare word (`status == active` compares as the text today, so
|
|
9824
|
+
// refusing it would fail a build over metadata that renders correctly). The
|
|
9825
|
+
// per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
|
|
9826
|
+
// since #3760; the entry's `gating` tier is unchanged because it already was.
|
|
9827
|
+
{
|
|
9828
|
+
name: "validatePredicatePathRefs",
|
|
9829
|
+
tier: "gating",
|
|
9830
|
+
input: "normalized",
|
|
9831
|
+
commands: ALL,
|
|
9832
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9833
|
+
// The second half of the #7220 family move — see the block above the
|
|
9834
|
+
// `validateVisibilityPredicates` entry. This is the rule whose solo wiring
|
|
9835
|
+
// was reverted; it is wired now because its siblings are.
|
|
9836
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9837
|
+
runtimeTypes: ["view"],
|
|
9838
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
9839
|
+
},
|
|
8513
9840
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8514
9841
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8515
9842
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
@@ -8667,16 +9994,110 @@ var AUTHORING_RULES = [
|
|
|
8667
9994
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
8668
9995
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
8669
9996
|
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
9997
|
+
//
|
|
9998
|
+
// [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
|
|
9999
|
+
// was false in both halves — it read: "Already gated at this surface by a
|
|
10000
|
+
// DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
|
|
10001
|
+
// `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
|
|
10002
|
+
// on every runtime write. Running the linter here as well would double-report
|
|
10003
|
+
// one refusal in two vocabularies."
|
|
10004
|
+
//
|
|
10005
|
+
// - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
|
|
10006
|
+
// `externalSharingModel` through a local `OWD_WIDTH`, and never touches
|
|
10007
|
+
// `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
|
|
10008
|
+
// block carries it covers ONE — `security-external-wider-than-internal`
|
|
10009
|
+
// (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
|
|
10010
|
+
// corresponds to no lint rule at all, so it is not coverage in the other
|
|
10011
|
+
// direction either. Twelve rules were enforced at no runtime door while
|
|
10012
|
+
// this field said they were.
|
|
10013
|
+
// - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
|
|
10014
|
+
// `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
|
|
10015
|
+
// `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
|
|
10016
|
+
// both refuse by THROWING. The first to fire ends the write, so an author
|
|
10017
|
+
// sees one refusal, never two. The stated cost of moving was imaginary; the
|
|
10018
|
+
// reason it has not moved is the measured one below.
|
|
10019
|
+
//
|
|
10020
|
+
// The move IS taken now — the #7891 programme's three slices, in order:
|
|
10021
|
+
//
|
|
10022
|
+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
|
|
10023
|
+
// the isolation proof that the differential cancels every finding this
|
|
10024
|
+
// function derives from the sibling collections.
|
|
10025
|
+
// - #8309: the snapshot repair. The gate used to carry `objects` and
|
|
10026
|
+
// nothing else, so the three cross-collection rules judged a universe
|
|
10027
|
+
// missing the collection they compare against (measured: 38 phantom
|
|
10028
|
+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
|
|
10029
|
+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
|
|
10030
|
+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
|
|
10031
|
+
// - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
|
|
10032
|
+
// `object` measured DIRTY on that tree and was escalated, not forced.
|
|
10033
|
+
// - #8310 slice 2 (this state): `object` crosses under the maintainer
|
|
10034
|
+
// ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
|
|
10035
|
+
// authored OWD is REQUIRED at the runtime object door — an object
|
|
10036
|
+
// publish with no authored `sharingModel` is refused with the 422 lint
|
|
10037
|
+
// envelope (`security-owd-unset`); absence is not a decision. The ~16
|
|
10038
|
+
// objectql/rest suite files that relied on OWD-less publishes were
|
|
10039
|
+
// repaired honestly (fixtures author their posture), and
|
|
10040
|
+
// `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
|
|
10041
|
+
// answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
|
|
10042
|
+
// ADR-0094-seam 403 doors answer for what passes lint. The same ruling
|
|
10043
|
+
// retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
|
|
10044
|
+
// of this door (R1 env-tighten-only STAYS — no lint rule covers it);
|
|
10045
|
+
// see `object-posture-gate.ts` and the ADR-0094 amendment.
|
|
10046
|
+
//
|
|
10047
|
+
// `security-role-word` is NOT in this entry any more — that is what the
|
|
10048
|
+
// `validateSecurityRoleWord` entry below records. It judges six collections
|
|
10049
|
+
// (objects, fields, actions, permission sets, positions, apps — plus books),
|
|
10050
|
+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
|
|
10051
|
+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
|
|
10052
|
+
// function that still contained it would have enforced ONE rule id for a
|
|
10053
|
+
// strict subset of its collections: a door where a permission set named
|
|
10054
|
+
// `role_manager` is refused and a position named `sales_role` walks through
|
|
10055
|
+
// — the #7220 failure this table refuses to build, in either direction. The
|
|
10056
|
+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
|
|
10057
|
+
// entry.
|
|
10058
|
+
//
|
|
10059
|
+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
|
|
10060
|
+
// registration, not a per-rule split: the baseline/candidate differential is
|
|
10061
|
+
// what keeps a write of one declared type from leaking the other rules'
|
|
10062
|
+
// whole-stack findings — every finding derived from a sibling collection is
|
|
10063
|
+
// produced byte-identically in both passes and cancels in the diff. Only
|
|
10064
|
+
// findings the written item itself adds are attributed to the write.
|
|
8670
10065
|
{
|
|
8671
10066
|
name: "validateSecurityPosture",
|
|
8672
10067
|
tier: "gating",
|
|
8673
10068
|
input: "parsed",
|
|
8674
10069
|
commands: ALL,
|
|
8675
10070
|
source: "packages/lint/src/validate-security-posture.ts",
|
|
8676
|
-
surfaces:
|
|
8677
|
-
|
|
10071
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10072
|
+
runtimeTypes: ["seed", "permission", "book", "object"],
|
|
8678
10073
|
run: (stack) => validateSecurityPosture(stack)
|
|
8679
10074
|
},
|
|
10075
|
+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
|
|
10076
|
+
// `validateSecurityPosture` the day the rest of that block crossed the
|
|
10077
|
+
// runtime wall — so that it could stay behind WHOLE rather than cross for
|
|
10078
|
+
// three of the six collections it judges (#7220: one rule id must sit on ONE
|
|
10079
|
+
// side of the wall). The split is a surface boundary, not taste: the rule's
|
|
10080
|
+
// verdict and findings are byte-identical to before on every CLI command
|
|
10081
|
+
// (both entries run on all three), and the runtime door does not run it for
|
|
10082
|
+
// ANY type.
|
|
10083
|
+
//
|
|
10084
|
+
// The road to crossing is concrete and short, recorded here so the next
|
|
10085
|
+
// seat prices it correctly: carry `positions`/`apps` in
|
|
10086
|
+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
|
|
10087
|
+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
|
|
10088
|
+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
|
|
10089
|
+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
|
|
10090
|
+
// #7220 discipline satisfied.
|
|
10091
|
+
{
|
|
10092
|
+
name: "validateSecurityRoleWord",
|
|
10093
|
+
tier: "gating",
|
|
10094
|
+
input: "parsed",
|
|
10095
|
+
commands: ALL,
|
|
10096
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
10097
|
+
surfaces: CLI_ONLY,
|
|
10098
|
+
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.",
|
|
10099
|
+
run: (stack) => validateSecurityRoleWord(stack)
|
|
10100
|
+
},
|
|
8680
10101
|
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
8681
10102
|
// sharing rule that walks it builds a second permission hierarchy (the
|
|
8682
10103
|
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
@@ -8727,7 +10148,7 @@ var AUTHORING_RULES = [
|
|
|
8727
10148
|
commands: ALL,
|
|
8728
10149
|
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
8729
10150
|
surfaces: CLI_ONLY,
|
|
8730
|
-
surfaceReason: "
|
|
10151
|
+
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.",
|
|
8731
10152
|
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
8732
10153
|
},
|
|
8733
10154
|
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
@@ -8785,8 +10206,34 @@ var TYPE_TO_STACK_KEY = {
|
|
|
8785
10206
|
dashboard: "dashboards",
|
|
8786
10207
|
agent: "agents",
|
|
8787
10208
|
hook: "hooks",
|
|
8788
|
-
seed
|
|
10209
|
+
// [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
|
|
10210
|
+
// holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
|
|
10211
|
+
// — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
|
|
10212
|
+
// mapping onto one either.
|
|
10213
|
+
//
|
|
10214
|
+
// The wrong spelling was INERT rather than harmless, and it is the #4449 shape
|
|
10215
|
+
// one surface over: the wiring guard asks only that a declared type HAS a
|
|
10216
|
+
// mapping, never that the mapping names a key some rule reads. So it would
|
|
10217
|
+
// have stayed green while the gate built `{ objects, seeds: [item] }` for
|
|
10218
|
+
// every seed write and every rule reading `stack.data` saw nothing — wired,
|
|
10219
|
+
// and running on nothing, with `rulesRun` reporting the rules as having run.
|
|
10220
|
+
// Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
|
|
10221
|
+
// changed no behaviour then; it was corrected here, with the measurement
|
|
10222
|
+
// that found it (#7576), rather than left for the rollout card to trip
|
|
10223
|
+
// over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
|
|
10224
|
+
// mapping is load-bearing today, not merely inert-and-correct.
|
|
10225
|
+
seed: "data",
|
|
10226
|
+
// [#8309] `permission`/`book` map ahead of their registration (#8310), the
|
|
10227
|
+
// same order `seed` arrived in: the mapping plus the enriched snapshot below
|
|
10228
|
+
// are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
|
|
10229
|
+
// a mapping without a declaring rule is inert by construction (the gate
|
|
10230
|
+
// filters by `runtimeTypes` before it ever consults this table), while a
|
|
10231
|
+
// declaration without the mapping is the wired-onto-nothing state the wiring
|
|
10232
|
+
// guard refuses. Landing the mapping first keeps #8310 a registry data edit.
|
|
10233
|
+
permission: "permissions",
|
|
10234
|
+
book: "books"
|
|
8789
10235
|
};
|
|
10236
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
|
|
8790
10237
|
function runtimeAuthoringRulesFor(type) {
|
|
8791
10238
|
return AUTHORING_RULES.filter(
|
|
8792
10239
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -8804,6 +10251,23 @@ function stackKeyForType(type) {
|
|
|
8804
10251
|
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
8805
10252
|
}
|
|
8806
10253
|
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
10254
|
+
function buildRuntimeWriteSnapshots(args) {
|
|
10255
|
+
const stackKey = stackKeyForType(args.type);
|
|
10256
|
+
if (!stackKey) return null;
|
|
10257
|
+
if (!args.item || typeof args.item !== "object") return null;
|
|
10258
|
+
const item = args.item;
|
|
10259
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10260
|
+
const baseline = {};
|
|
10261
|
+
for (const key of CONTEXT_STACK_KEYS) {
|
|
10262
|
+
const collection = args.context?.[key] ?? [];
|
|
10263
|
+
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10264
|
+
}
|
|
10265
|
+
const candidate = {
|
|
10266
|
+
...baseline,
|
|
10267
|
+
[stackKey]: [...baseline[stackKey] ?? [], item]
|
|
10268
|
+
};
|
|
10269
|
+
return { baseline, candidate };
|
|
10270
|
+
}
|
|
8807
10271
|
function runRules(rules, stack, ctx) {
|
|
8808
10272
|
const findings = [];
|
|
8809
10273
|
for (const rule of rules) {
|
|
@@ -8826,19 +10290,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8826
10290
|
const rules = runtimeAuthoringRulesFor(args.type);
|
|
8827
10291
|
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
8828
10292
|
if (rules.length === 0) return empty;
|
|
8829
|
-
const
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
|
|
8834
|
-
|
|
10293
|
+
const snapshots = buildRuntimeWriteSnapshots({
|
|
10294
|
+
type: args.type,
|
|
10295
|
+
item: args.item,
|
|
10296
|
+
...args.context !== void 0 ? { context: args.context } : {}
|
|
10297
|
+
});
|
|
10298
|
+
if (!snapshots) return empty;
|
|
8835
10299
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
8836
|
-
const
|
|
8837
|
-
const
|
|
8838
|
-
const baseline = { objects: baselineObjects };
|
|
8839
|
-
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
8840
|
-
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
8841
|
-
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
10300
|
+
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10301
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
8842
10302
|
return {
|
|
8843
10303
|
errors: added.filter((f) => f.severity === "error"),
|
|
8844
10304
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -8846,6 +10306,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8846
10306
|
};
|
|
8847
10307
|
}
|
|
8848
10308
|
export {
|
|
10309
|
+
buildRuntimeWriteSnapshots,
|
|
8849
10310
|
runRuntimeAuthoringRules,
|
|
8850
10311
|
runtimeAuthoringRulesFor,
|
|
8851
10312
|
runtimeGatedTypes,
|