@objectstack/lint 17.0.0 → 17.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +622 -0
- package/dist/index.cjs +894 -293
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +310 -66
- package/dist/index.d.ts +310 -66
- package/dist/index.js +871 -278
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-s3X9D9hm.d.cts} +89 -2
- package/dist/{runtime-H-nDodRy.d.ts → runtime-s3X9D9hm.d.ts} +89 -2
- package/dist/runtime.cjs +986 -409
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +959 -378
- package/dist/runtime.js.map +1 -1
- package/package.json +5 -5
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
ACTION_BODY_WRITE_PATTERNS: () => ACTION_BODY_WRITE_PATTERNS,
|
|
25
25
|
ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
|
|
26
26
|
ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
27
|
+
ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR: () => ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
27
28
|
ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
|
|
28
29
|
ACTION_NO_PLACEMENT: () => ACTION_NO_PLACEMENT,
|
|
29
30
|
ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
|
|
@@ -69,6 +70,7 @@ __export(index_exports, {
|
|
|
69
70
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
70
71
|
FILTER_EMPTY_COMBINATOR: () => FILTER_EMPTY_COMBINATOR,
|
|
71
72
|
FILTER_EMPTY_NODE: () => FILTER_EMPTY_NODE,
|
|
73
|
+
FILTER_PRESET_COMPARAND: () => FILTER_PRESET_COMPARAND,
|
|
72
74
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
73
75
|
FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
|
|
74
76
|
FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -86,6 +88,7 @@ __export(index_exports, {
|
|
|
86
88
|
FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
87
89
|
FLOW_MULTI_WRITE_UNFILTERED: () => FLOW_MULTI_WRITE_UNFILTERED,
|
|
88
90
|
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
91
|
+
FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR: () => FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
89
92
|
FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
|
|
90
93
|
FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
|
|
91
94
|
FLOW_TEMPLATE_FIELD_UNPROVISIONED: () => FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
@@ -107,6 +110,7 @@ __export(index_exports, {
|
|
|
107
110
|
HOOK_BODY_WRITE_PATTERNS: () => HOOK_BODY_WRITE_PATTERNS,
|
|
108
111
|
HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
|
|
109
112
|
HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
113
|
+
HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR: () => HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
110
114
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
111
115
|
LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
|
|
112
116
|
LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
@@ -142,6 +146,7 @@ __export(index_exports, {
|
|
|
142
146
|
RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
|
|
143
147
|
SEAL_MARKERS: () => SEAL_MARKERS,
|
|
144
148
|
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
149
|
+
SEARCHABLE_FIELD_UNPROVISIONED: () => SEARCHABLE_FIELD_UNPROVISIONED,
|
|
145
150
|
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
146
151
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
147
152
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
@@ -160,8 +165,12 @@ __export(index_exports, {
|
|
|
160
165
|
SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
161
166
|
SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
162
167
|
SEMANTIC_ROLE_FIELD_UNPROVISIONED: () => SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
168
|
+
SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT: () => SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
169
|
+
SHARING_RULE_OBJECT_NOT_SHAREABLE: () => SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
163
170
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
164
171
|
SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
172
|
+
SORT_FIELD_UNKNOWN: () => SORT_FIELD_UNKNOWN,
|
|
173
|
+
SORT_FIELD_UNSORTABLE: () => SORT_FIELD_UNSORTABLE,
|
|
165
174
|
STARTUP_OPEN_VOCABULARY_VERDICT: () => STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
166
175
|
STARTUP_VERDICT_ASSERTIVE_WORDING: () => STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
167
176
|
STARTUP_VERDICT_HINT: () => STARTUP_VERDICT_HINT,
|
|
@@ -198,6 +207,7 @@ __export(index_exports, {
|
|
|
198
207
|
authoringRulesFor: () => authoringRulesFor,
|
|
199
208
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
200
209
|
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
210
|
+
checkSortDeclaration: () => checkSortDeclaration,
|
|
201
211
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
202
212
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
203
213
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
@@ -212,6 +222,7 @@ __export(index_exports, {
|
|
|
212
222
|
lintUniqueDeclarations: () => lintUniqueDeclarations,
|
|
213
223
|
lintUnscopedDeclaredIndexes: () => lintUnscopedDeclaredIndexes,
|
|
214
224
|
lintViewRefs: () => lintViewRefs,
|
|
225
|
+
narrowObjectsToPackageClosure: () => narrowObjectsToPackageClosure,
|
|
215
226
|
nearestRegisteredFormat: () => nearestRegisteredFormat,
|
|
216
227
|
nullGuardMessage: () => nullGuardMessage,
|
|
217
228
|
runAuthoringRules: () => runAuthoringRules,
|
|
@@ -250,6 +261,7 @@ __export(index_exports, {
|
|
|
250
261
|
validatePageFieldBindings: () => validatePageFieldBindings,
|
|
251
262
|
validatePageSourceStyling: () => validatePageSourceStyling,
|
|
252
263
|
validatePredicatePathRefs: () => validatePredicatePathRefs,
|
|
264
|
+
validatePresetComparands: () => validatePresetComparands,
|
|
253
265
|
validateReactPageProps: () => validateReactPageProps,
|
|
254
266
|
validateReactPages: () => validateReactPages,
|
|
255
267
|
validateReadonlyFlowWrites: () => validateReadonlyFlowWrites,
|
|
@@ -266,6 +278,7 @@ __export(index_exports, {
|
|
|
266
278
|
validateSeedStateMachine: () => validateSeedStateMachine,
|
|
267
279
|
validateSemanticRoles: () => validateSemanticRoles,
|
|
268
280
|
validateSharingRuleEnforceability: () => validateSharingRuleEnforceability,
|
|
281
|
+
validateSortableFields: () => validateSortableFields,
|
|
269
282
|
validateStackExpressions: () => validateStackExpressions,
|
|
270
283
|
validateTranslatableSections: () => validateTranslatableSections,
|
|
271
284
|
validateTranslationReferences: () => validateTranslationReferences,
|
|
@@ -860,10 +873,10 @@ var OUTCOME_CLAUSE = {
|
|
|
860
873
|
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
861
874
|
"fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
|
|
862
875
|
};
|
|
863
|
-
function nullGuardMessage(subject, objectName,
|
|
876
|
+
function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
|
|
864
877
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
865
|
-
const hasNote =
|
|
866
|
-
return `${subject} applies \`${
|
|
878
|
+
const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
|
|
879
|
+
return `${subject} applies \`${finding2.operator}\` to \`${finding2.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding2.operator}\` overload for null, and the whole predicate aborts \u2014 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
|
|
867
880
|
}
|
|
868
881
|
|
|
869
882
|
// src/validate-expressions.ts
|
|
@@ -1046,10 +1059,10 @@ function validateStackExpressions(stack) {
|
|
|
1046
1059
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
1047
1060
|
const source = celSourceOf(raw);
|
|
1048
1061
|
if (!source) return;
|
|
1049
|
-
for (const
|
|
1062
|
+
for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
1050
1063
|
issues.push({
|
|
1051
1064
|
where,
|
|
1052
|
-
message: nullGuardMessage(subject, objectName,
|
|
1065
|
+
message: nullGuardMessage(subject, objectName, finding2, outcome),
|
|
1053
1066
|
source,
|
|
1054
1067
|
severity: "error"
|
|
1055
1068
|
});
|
|
@@ -1073,7 +1086,7 @@ function validateStackExpressions(stack) {
|
|
|
1073
1086
|
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";
|
|
1074
1087
|
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
1075
1088
|
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)",
|
|
1076
|
-
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`).
|
|
1089
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). The server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
|
|
1077
1090
|
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",
|
|
1078
1091
|
// Listed rather than left to the `??` below, so the map covers every slot
|
|
1079
1092
|
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
@@ -1132,14 +1145,15 @@ function validateStackExpressions(stack) {
|
|
|
1132
1145
|
if (retired.length > 0) {
|
|
1133
1146
|
issues.push({
|
|
1134
1147
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
1135
|
-
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)
|
|
1136
|
-
// behaviour, never the retired key's fate —
|
|
1137
|
-
// over a branch that DELETES the key
|
|
1138
|
-
//
|
|
1148
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled), reworded under #9529: the house
|
|
1149
|
+
// sentence names the TOOL's behaviour, never the retired key's fate —
|
|
1150
|
+
// "rewrite it" read two ways over a branch that DELETES the key
|
|
1151
|
+
// (template/recipients/variables/script), and the tool never rewrote a
|
|
1152
|
+
// source file at all. Plain-quoted (not a template literal)
|
|
1139
1153
|
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
1140
1154
|
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
1141
1155
|
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
1142
|
-
"Run `os migrate meta --from 16` to
|
|
1156
|
+
"Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
|
|
1143
1157
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
1144
1158
|
});
|
|
1145
1159
|
} else if (!fn) {
|
|
@@ -1210,10 +1224,10 @@ function validateStackExpressions(stack) {
|
|
|
1210
1224
|
f.expression,
|
|
1211
1225
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
1212
1226
|
);
|
|
1213
|
-
const
|
|
1214
|
-
for (const e of res.errors) issues.push({ where:
|
|
1215
|
-
for (const w of res.warnings) issues.push({ where:
|
|
1216
|
-
warnUnprovisionedAnchors(
|
|
1227
|
+
const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
1228
|
+
for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
|
|
1229
|
+
for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
|
|
1230
|
+
warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
|
|
1217
1231
|
}
|
|
1218
1232
|
}
|
|
1219
1233
|
}
|
|
@@ -2835,6 +2849,7 @@ var import_data5 = require("@objectstack/spec/data");
|
|
|
2835
2849
|
var import_data4 = require("@objectstack/spec/data");
|
|
2836
2850
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
2837
2851
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
2852
|
+
var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
|
|
2838
2853
|
function asArray10(v) {
|
|
2839
2854
|
if (Array.isArray(v)) return v;
|
|
2840
2855
|
if (v && typeof v === "object") {
|
|
@@ -2925,7 +2940,7 @@ function indexObjectSearchTargets(stack) {
|
|
|
2925
2940
|
}
|
|
2926
2941
|
return fieldsByObject;
|
|
2927
2942
|
}
|
|
2928
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
2943
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
|
|
2929
2944
|
const findings = [];
|
|
2930
2945
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
2931
2946
|
if (!objectName) return findings;
|
|
@@ -2933,6 +2948,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2933
2948
|
const target = fieldsByObject.get(objectName);
|
|
2934
2949
|
if (!target) return findings;
|
|
2935
2950
|
const known = target.names;
|
|
2951
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2936
2952
|
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
2937
2953
|
for (let i = 0; i < declared.length; i++) {
|
|
2938
2954
|
const entry = declared[i];
|
|
@@ -2950,6 +2966,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2950
2966
|
});
|
|
2951
2967
|
continue;
|
|
2952
2968
|
}
|
|
2969
|
+
if (anchors?.has(name)) {
|
|
2970
|
+
findings.push({
|
|
2971
|
+
severity: "warning",
|
|
2972
|
+
rule: SEARCHABLE_FIELD_UNPROVISIONED,
|
|
2973
|
+
where,
|
|
2974
|
+
path: `${path}[${i}]`,
|
|
2975
|
+
message: `${subject} entry "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)}` + (role === "narrowing" ? ` \u2014 clients echo this declaration verbatim as the '$searchFields' override, so every toolbar search on this list scans a column that is empty on every record: it reads as search coverage and matches nothing.` : ` \u2014 'search' scans it on every record and it can never match, so the object's searchable set is narrower than it declares. Should it be the ONLY entry that resolves, the set scans nothing at all.`),
|
|
2976
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2953
2979
|
if ((0, import_data4.isVirtualSearchField)(target.fields[name])) {
|
|
2954
2980
|
const vtype = target.fields[name]?.type;
|
|
2955
2981
|
findings.push({
|
|
@@ -3003,9 +3029,19 @@ function validateSearchableFields(stack) {
|
|
|
3003
3029
|
if (!isRec4(stack)) return findings;
|
|
3004
3030
|
const objects = asArray10(stack.objects);
|
|
3005
3031
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
3032
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
3006
3033
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
3007
3034
|
findings.push(
|
|
3008
|
-
...checkSearchableFieldList(
|
|
3035
|
+
...checkSearchableFieldList(
|
|
3036
|
+
declared,
|
|
3037
|
+
objectName,
|
|
3038
|
+
fieldsByObject,
|
|
3039
|
+
where,
|
|
3040
|
+
path,
|
|
3041
|
+
subject,
|
|
3042
|
+
role,
|
|
3043
|
+
unprovisionedAnchors
|
|
3044
|
+
)
|
|
3009
3045
|
);
|
|
3010
3046
|
};
|
|
3011
3047
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -3192,7 +3228,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
3192
3228
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
3193
3229
|
"record:path": { props: ["statusField"] },
|
|
3194
3230
|
"element:number": { props: ["field"] },
|
|
3195
|
-
|
|
3231
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
3232
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
3233
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
3234
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
3235
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
3236
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
3196
3237
|
"element:form": { props: ["fields"] },
|
|
3197
3238
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
3198
3239
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -3896,7 +3937,12 @@ function validateReactPageProps(stack) {
|
|
|
3896
3937
|
searchTargets,
|
|
3897
3938
|
where,
|
|
3898
3939
|
`${path} \u203A searchableFields`,
|
|
3899
|
-
"searchableFields"
|
|
3940
|
+
"searchableFields",
|
|
3941
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
3942
|
+
// default, spelled out here because the #8404 provenance index
|
|
3943
|
+
// follows it positionally.
|
|
3944
|
+
"narrowing",
|
|
3945
|
+
unprovisionedAnchors
|
|
3900
3946
|
)
|
|
3901
3947
|
);
|
|
3902
3948
|
}
|
|
@@ -5468,6 +5514,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5468
5514
|
}
|
|
5469
5515
|
}
|
|
5470
5516
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
5517
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
5471
5518
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
5472
5519
|
for (const [i, seed] of asArray19(stack.data).entries()) {
|
|
5473
5520
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -5491,7 +5538,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5491
5538
|
}
|
|
5492
5539
|
}
|
|
5493
5540
|
const delegatedFrom = rec.delegated_from;
|
|
5494
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
5541
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
5495
5542
|
const reason = rec.reason;
|
|
5496
5543
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
5497
5544
|
findings.push({
|
|
@@ -5657,6 +5704,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
5657
5704
|
var import_formula6 = require("@objectstack/formula");
|
|
5658
5705
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
5659
5706
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
5707
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
5708
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
5660
5709
|
function asArray21(v) {
|
|
5661
5710
|
if (Array.isArray(v)) return v;
|
|
5662
5711
|
if (v && typeof v === "object") {
|
|
@@ -5681,10 +5730,67 @@ function sourceOf(condition) {
|
|
|
5681
5730
|
return str2(input?.source);
|
|
5682
5731
|
}
|
|
5683
5732
|
var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
|
|
5733
|
+
function effectiveSharingModelOf(obj) {
|
|
5734
|
+
const m = obj.sharingModel;
|
|
5735
|
+
if (m === "private") return "private";
|
|
5736
|
+
if (m === "public_read") return "read";
|
|
5737
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
5738
|
+
if (m == null) {
|
|
5739
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
5740
|
+
return isSystem ? "public" : "private";
|
|
5741
|
+
}
|
|
5742
|
+
return "private";
|
|
5743
|
+
}
|
|
5744
|
+
function masterOf(obj) {
|
|
5745
|
+
for (const f of asArray21(obj.fields)) {
|
|
5746
|
+
if (f.type === "master_detail") {
|
|
5747
|
+
const ref = f.reference;
|
|
5748
|
+
if (typeof ref === "string" && ref) return ref;
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
return void 0;
|
|
5752
|
+
}
|
|
5753
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
5754
|
+
const object = str2(rule.object);
|
|
5755
|
+
if (!object) return [];
|
|
5756
|
+
const target = objectsByName.get(object);
|
|
5757
|
+
if (!target) return [];
|
|
5758
|
+
const name = str2(rule.name) || String(index);
|
|
5759
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
5760
|
+
const path = `sharingRules[${index}].object`;
|
|
5761
|
+
const owd = target.sharingModel;
|
|
5762
|
+
if (owd === "controlled_by_parent") {
|
|
5763
|
+
const master = masterOf(target);
|
|
5764
|
+
return [{
|
|
5765
|
+
severity: "error",
|
|
5766
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
5767
|
+
where,
|
|
5768
|
+
path,
|
|
5769
|
+
message: `Sharing rule "${name}" is anchored on object "${object}", which declares sharingModel 'controlled_by_parent'. A detail record has no record-level access of its own \u2014 its visibility is DERIVED from its master (ADR-0055), so it holds no shares to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is controlled by its parent (master-detail); share the master record instead"), so the rule's boot backfill fails, no \`sys_record_share\` row is ever written, and the recipients this rule names get whatever the MASTER grants them \u2014 which may be nothing. The grant is declared and does not exist.`,
|
|
5770
|
+
hint: `Move the rule onto the MASTER object` + (master ? ` \u2014 "${object}" derives from "${master}" through its master_detail field, so share "${master}" and the detail rows follow` : `, and share that instead; the detail rows follow`) + `. If "${object}" is meant to carry a record-level baseline of its own, that is a different decision: change its sharingModel to 'private' (owner + shares) or 'public_read', and this rule becomes enforceable where it stands.`
|
|
5771
|
+
}];
|
|
5772
|
+
}
|
|
5773
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
5774
|
+
const declared = owd === "public_read_write" ? `declares sharingModel 'public_read_write'` : `declares no sharingModel and is a system object (\`isSystem: true\` or a \`sys_\` name), which ADR-0090 D1 resolves to public`;
|
|
5775
|
+
return [{
|
|
5776
|
+
severity: "error",
|
|
5777
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
5778
|
+
where,
|
|
5779
|
+
path,
|
|
5780
|
+
message: `Sharing rule "${name}" is anchored on object "${object}", which ${declared}. Its effective sharing model is therefore \`public\`, and sharing only ever WIDENS an OWD baseline \u2014 on the widest baseline there is nothing left to widen. \`SharingService.assertNotInertGrant\` refuses the grant with SHARING_NOT_ENABLED ("'${object}' is not under record-sharing enforcement"), so the rule's boot backfill fails and no \`sys_record_share\` row is ever written. Measured: \`buildReadFilter\` returns \`null\` for this object, i.e. NO record-level filter at all \u2014 every principal already reads every row, so this rule advertises a restriction that does not exist.`,
|
|
5781
|
+
hint: `Decide which half is wrong. If the ACCESS is right \u2014 everyone should read and write these records \u2014 the rule is dead metadata: delete it (ADR-0049 enforce-or-remove). If the RULE is right \u2014 only the named audience should reach these records \u2014 then "${object}"'s OWD is the defect: set sharingModel: 'private' (owner + shares) or 'public_read', and this rule starts enforcing. Do NOT re-home the rule onto another public object; that moves the inertness instead of removing it.`
|
|
5782
|
+
}];
|
|
5783
|
+
}
|
|
5684
5784
|
function validateSharingRuleEnforceability(stack) {
|
|
5685
5785
|
const findings = [];
|
|
5686
5786
|
const cfg = stack ?? {};
|
|
5787
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
5788
|
+
for (const obj of asArray21(cfg.objects)) {
|
|
5789
|
+
const name = str2(obj.name);
|
|
5790
|
+
if (name) objectsByName.set(name, obj);
|
|
5791
|
+
}
|
|
5687
5792
|
asArray21(cfg.sharingRules).forEach((rule, index) => {
|
|
5793
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
5688
5794
|
const input = toCompilerInput(rule.condition);
|
|
5689
5795
|
if (input === null) return;
|
|
5690
5796
|
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
@@ -5817,7 +5923,6 @@ function asArray23(v) {
|
|
|
5817
5923
|
function strName7(v) {
|
|
5818
5924
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5819
5925
|
}
|
|
5820
|
-
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
5821
5926
|
var URL_COLLECTION_TO_STACK_KEY = {
|
|
5822
5927
|
object: "objects",
|
|
5823
5928
|
objects: "objects",
|
|
@@ -5862,13 +5967,10 @@ function collectKnownTargets(stack) {
|
|
|
5862
5967
|
return { actions, objects, reports, dashboards, pages, views };
|
|
5863
5968
|
}
|
|
5864
5969
|
function resolveActionTarget(actionType, target, known) {
|
|
5865
|
-
if (known.actions.has(target)) return true;
|
|
5866
5970
|
if (actionType === "modal") {
|
|
5867
|
-
|
|
5868
|
-
const m = MODAL_VERB_RE.exec(target);
|
|
5869
|
-
if (m && known.objects.has(m[1])) return true;
|
|
5971
|
+
return known.pages.has(target);
|
|
5870
5972
|
}
|
|
5871
|
-
return
|
|
5973
|
+
return known.actions.has(target);
|
|
5872
5974
|
}
|
|
5873
5975
|
function resolveUrlRoute(target, known) {
|
|
5874
5976
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
|
|
@@ -5898,14 +6000,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
5898
6000
|
const actionType = strName7(action.actionType) ?? "url";
|
|
5899
6001
|
if (actionType === "script" || actionType === "modal") {
|
|
5900
6002
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
5901
|
-
const kindWord = actionType === "script" ? "script" : "modal";
|
|
5902
6003
|
findings.push({
|
|
5903
6004
|
severity: "error",
|
|
5904
6005
|
rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
5905
6006
|
where,
|
|
5906
6007
|
path,
|
|
5907
|
-
message:
|
|
5908
|
-
hint: actionType === "modal" ? `
|
|
6008
|
+
message: actionType === "modal" ? `modal action target "${target}" names no declared page \u2014 a modal target names a PAGE, only (objectstack#6739). The button renders but the runtime refuses the dispatch when clicked \u2014 a dangling reference (ADR-0049: a declared reference must resolve).` : `script action target "${target}" resolves to no defined action. The button renders but does nothing when clicked \u2014 a dangling reference the runtime cannot dispatch (ADR-0049: a declared reference must resolve).`,
|
|
6009
|
+
hint: actionType === "modal" ? `Point actionUrl at a declared page (stack.pages), or use actionType: 'form' with an "<object>.<view>" form-view target to open an object's form, or remove the button.` : `Define a script action named "${target}" (stack.actions or the object's actions) with an inline body or a registered handler, or remove the button.`
|
|
5909
6010
|
});
|
|
5910
6011
|
return;
|
|
5911
6012
|
}
|
|
@@ -5947,7 +6048,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
5947
6048
|
var import_data7 = require("@objectstack/spec/data");
|
|
5948
6049
|
|
|
5949
6050
|
// src/filter-walk.ts
|
|
5950
|
-
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
6051
|
+
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
|
|
5951
6052
|
function asArray24(v) {
|
|
5952
6053
|
if (Array.isArray(v)) return v;
|
|
5953
6054
|
if (v && typeof v === "object") {
|
|
@@ -6051,8 +6152,146 @@ function validateFilterTokens(stack) {
|
|
|
6051
6152
|
return out;
|
|
6052
6153
|
}
|
|
6053
6154
|
|
|
6054
|
-
// src/validate-
|
|
6155
|
+
// src/validate-preset-comparands.ts
|
|
6055
6156
|
var import_data8 = require("@objectstack/spec/data");
|
|
6157
|
+
var import_ui3 = require("@objectstack/spec/ui");
|
|
6158
|
+
var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
|
|
6159
|
+
var PRESET_COMPARAND_SURFACES = [
|
|
6160
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
6161
|
+
{ key: "objects", kind: "object" },
|
|
6162
|
+
{ key: "views", kind: "view" },
|
|
6163
|
+
{ key: "reports", kind: "report" },
|
|
6164
|
+
{ key: "datasets", kind: "dataset" },
|
|
6165
|
+
{ key: "pages", kind: "page" },
|
|
6166
|
+
{ key: "apps", kind: "app" },
|
|
6167
|
+
{ key: "flows", kind: "flow" }
|
|
6168
|
+
];
|
|
6169
|
+
var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
|
|
6170
|
+
var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
6171
|
+
var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
|
|
6172
|
+
"greater_than",
|
|
6173
|
+
"greater_than_or_equal",
|
|
6174
|
+
"less_than",
|
|
6175
|
+
"less_than_or_equal",
|
|
6176
|
+
"before",
|
|
6177
|
+
"after"
|
|
6178
|
+
]);
|
|
6179
|
+
var MAX_DEPTH = 32;
|
|
6180
|
+
function isPlainObject(v) {
|
|
6181
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
6182
|
+
}
|
|
6183
|
+
function finding(where, path, preset, operator) {
|
|
6184
|
+
return {
|
|
6185
|
+
severity: "error",
|
|
6186
|
+
rule: FILTER_PRESET_COMPARAND,
|
|
6187
|
+
where,
|
|
6188
|
+
path,
|
|
6189
|
+
message: (0, import_data8.bareDateRangePresetComparandMessage)(preset, operator),
|
|
6190
|
+
hint: "Presets belong to the dashboard date-filter bar (dateRange.defaultRange, a date global filter's defaultValue). In a filter comparand, write the {date-macro} window the message names, or an ISO date."
|
|
6191
|
+
};
|
|
6192
|
+
}
|
|
6193
|
+
function judgeConditionNode(node, path, where, out, depth) {
|
|
6194
|
+
if (depth > MAX_DEPTH) return;
|
|
6195
|
+
for (const [key, value] of Object.entries(node)) {
|
|
6196
|
+
const here = `${path}.${key}`;
|
|
6197
|
+
if (key === "$and" || key === "$or") {
|
|
6198
|
+
if (Array.isArray(value)) {
|
|
6199
|
+
value.forEach((arm, i) => {
|
|
6200
|
+
if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
|
|
6201
|
+
});
|
|
6202
|
+
}
|
|
6203
|
+
continue;
|
|
6204
|
+
}
|
|
6205
|
+
if (key === "$not") {
|
|
6206
|
+
if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
|
|
6207
|
+
continue;
|
|
6208
|
+
}
|
|
6209
|
+
if (key.startsWith("$")) continue;
|
|
6210
|
+
if (!isPlainObject(value)) continue;
|
|
6211
|
+
const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
|
|
6212
|
+
if (!hasOps) {
|
|
6213
|
+
judgeConditionNode(value, here, where, out, depth + 1);
|
|
6214
|
+
continue;
|
|
6215
|
+
}
|
|
6216
|
+
for (const [op, comparand] of Object.entries(value)) {
|
|
6217
|
+
if (ORDERING_DOLLAR_OPS.has(op) && (0, import_data8.isDateRangePresetName)(comparand)) {
|
|
6218
|
+
out.push(finding(where, `${here}.${op}`, comparand, op));
|
|
6219
|
+
continue;
|
|
6220
|
+
}
|
|
6221
|
+
if (op === "$between" && Array.isArray(comparand)) {
|
|
6222
|
+
comparand.forEach((endpoint, i) => {
|
|
6223
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6224
|
+
out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
|
|
6225
|
+
}
|
|
6226
|
+
});
|
|
6227
|
+
}
|
|
6228
|
+
}
|
|
6229
|
+
}
|
|
6230
|
+
}
|
|
6231
|
+
function judgeFilterRule(rule, path, where, out) {
|
|
6232
|
+
const operator = (0, import_ui3.normalizeFilterOperator)(rule.operator);
|
|
6233
|
+
if (typeof operator !== "string") return;
|
|
6234
|
+
const value = rule.value;
|
|
6235
|
+
if (ORDERING_RULE_OPS.has(operator) && (0, import_data8.isDateRangePresetName)(value)) {
|
|
6236
|
+
out.push(finding(where, `${path}.value`, value, operator));
|
|
6237
|
+
return;
|
|
6238
|
+
}
|
|
6239
|
+
if (operator === "between" && Array.isArray(value)) {
|
|
6240
|
+
value.forEach((endpoint, i) => {
|
|
6241
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6242
|
+
out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
|
|
6243
|
+
}
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
}
|
|
6247
|
+
function judgeTriple(triple, path, where, out) {
|
|
6248
|
+
const op = triple[1];
|
|
6249
|
+
if (typeof op !== "string") return;
|
|
6250
|
+
const canonical = (0, import_data8.canonicalAstOperator)(op);
|
|
6251
|
+
const value = triple[2];
|
|
6252
|
+
if (ORDERING_INFIX_OPS.has(canonical) && (0, import_data8.isDateRangePresetName)(value)) {
|
|
6253
|
+
out.push(finding(where, `${path}[2]`, value, op));
|
|
6254
|
+
return;
|
|
6255
|
+
}
|
|
6256
|
+
if (canonical === "between" && Array.isArray(value)) {
|
|
6257
|
+
value.forEach((endpoint, i) => {
|
|
6258
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6259
|
+
out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
|
|
6260
|
+
}
|
|
6261
|
+
});
|
|
6262
|
+
}
|
|
6263
|
+
}
|
|
6264
|
+
function judgeFilterValue(node, path, where, out, depth) {
|
|
6265
|
+
if (depth > MAX_DEPTH) return;
|
|
6266
|
+
if (Array.isArray(node)) {
|
|
6267
|
+
if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
6268
|
+
judgeTriple(node, path, where, out);
|
|
6269
|
+
return;
|
|
6270
|
+
}
|
|
6271
|
+
node.forEach((member, i) => {
|
|
6272
|
+
if (typeof member === "string") return;
|
|
6273
|
+
judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
|
|
6274
|
+
});
|
|
6275
|
+
return;
|
|
6276
|
+
}
|
|
6277
|
+
if (!isPlainObject(node)) return;
|
|
6278
|
+
if (typeof node.field === "string" && typeof node.operator === "string") {
|
|
6279
|
+
judgeFilterRule(node, path, where, out);
|
|
6280
|
+
return;
|
|
6281
|
+
}
|
|
6282
|
+
judgeConditionNode(node, path, where, out, depth);
|
|
6283
|
+
}
|
|
6284
|
+
function validatePresetComparands(stack) {
|
|
6285
|
+
if (!stack || typeof stack !== "object") return [];
|
|
6286
|
+
const out = [];
|
|
6287
|
+
walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
|
|
6288
|
+
judgeFilterValue(value, path, where, out, 0);
|
|
6289
|
+
});
|
|
6290
|
+
return out;
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
// src/validate-empty-combinators.ts
|
|
6294
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
6056
6295
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
6057
6296
|
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
6058
6297
|
var EMPTY_COMBINATOR_SURFACES = [
|
|
@@ -6071,12 +6310,12 @@ function isFilterNode(value) {
|
|
|
6071
6310
|
return proto === Object.prototype || proto === null;
|
|
6072
6311
|
}
|
|
6073
6312
|
var VERDICT_OF = {
|
|
6074
|
-
$and: (0,
|
|
6075
|
-
$or: (0,
|
|
6076
|
-
$not: (0,
|
|
6077
|
-
node: (0,
|
|
6313
|
+
$and: (0, import_data9.reduceFilterVerdict)({ $and: [] }),
|
|
6314
|
+
$or: (0, import_data9.reduceFilterVerdict)({ $or: [] }),
|
|
6315
|
+
$not: (0, import_data9.reduceFilterVerdict)({ $not: {} }),
|
|
6316
|
+
node: (0, import_data9.reduceFilterVerdict)({}),
|
|
6078
6317
|
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
6079
|
-
orWithEmptyBranch: (0,
|
|
6318
|
+
orWithEmptyBranch: (0, import_data9.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
6080
6319
|
};
|
|
6081
6320
|
function rows(verdict) {
|
|
6082
6321
|
if (verdict === "true") return "matches EVERY row";
|
|
@@ -6431,7 +6670,7 @@ function validateNavTargetRefs(stack) {
|
|
|
6431
6670
|
}
|
|
6432
6671
|
|
|
6433
6672
|
// src/validate-nav-object-servability.ts
|
|
6434
|
-
var
|
|
6673
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
6435
6674
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
6436
6675
|
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6437
6676
|
function asArray27(v) {
|
|
@@ -6467,7 +6706,7 @@ function validateNavObjectServability(stack) {
|
|
|
6467
6706
|
if (nav.type === "object") {
|
|
6468
6707
|
const target = strName10(nav.objectName);
|
|
6469
6708
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
6470
|
-
if (target && declared && !(0,
|
|
6709
|
+
if (target && declared && !(0, import_data10.canServeApiOperation)(declared.enable, "list")) {
|
|
6471
6710
|
const enable = isRec15(declared.enable) ? declared.enable : {};
|
|
6472
6711
|
const apiDisabled = enable.apiEnabled === false;
|
|
6473
6712
|
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(", ")})` : "");
|
|
@@ -6498,6 +6737,176 @@ function validateNavObjectServability(stack) {
|
|
|
6498
6737
|
return findings;
|
|
6499
6738
|
}
|
|
6500
6739
|
|
|
6740
|
+
// src/validate-sortable-fields.ts
|
|
6741
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
6742
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
6743
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
6744
|
+
function isRec16(v) {
|
|
6745
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6746
|
+
}
|
|
6747
|
+
function strName11(v) {
|
|
6748
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6749
|
+
}
|
|
6750
|
+
function readSortKeys(declared) {
|
|
6751
|
+
const fromShorthand = (raw, at) => {
|
|
6752
|
+
const trimmed = raw.trim();
|
|
6753
|
+
if (!trimmed) return void 0;
|
|
6754
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
6755
|
+
return bare ? { field: bare, at } : void 0;
|
|
6756
|
+
};
|
|
6757
|
+
if (typeof declared === "string") {
|
|
6758
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
6759
|
+
}
|
|
6760
|
+
if (Array.isArray(declared)) {
|
|
6761
|
+
const keys = [];
|
|
6762
|
+
for (let i = 0; i < declared.length; i++) {
|
|
6763
|
+
const el = declared[i];
|
|
6764
|
+
if (typeof el === "string") {
|
|
6765
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
6766
|
+
if (k) keys.push(k);
|
|
6767
|
+
continue;
|
|
6768
|
+
}
|
|
6769
|
+
if (!isRec16(el)) continue;
|
|
6770
|
+
const field = strName11(el.field);
|
|
6771
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
6772
|
+
}
|
|
6773
|
+
return keys;
|
|
6774
|
+
}
|
|
6775
|
+
return [];
|
|
6776
|
+
}
|
|
6777
|
+
function suggest4(target, known) {
|
|
6778
|
+
let best;
|
|
6779
|
+
let bestScore = Infinity;
|
|
6780
|
+
for (const candidate of known) {
|
|
6781
|
+
const d = distance3(target, candidate);
|
|
6782
|
+
if (d < bestScore) {
|
|
6783
|
+
bestScore = d;
|
|
6784
|
+
best = candidate;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
6788
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
6789
|
+
}
|
|
6790
|
+
function distance3(a, b) {
|
|
6791
|
+
const m = a.length;
|
|
6792
|
+
const n = b.length;
|
|
6793
|
+
if (m === 0) return n;
|
|
6794
|
+
if (n === 0) return m;
|
|
6795
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
6796
|
+
for (let i = 1; i <= m; i++) {
|
|
6797
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
6798
|
+
for (let j = 1; j <= n; j++) {
|
|
6799
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
6800
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
6801
|
+
}
|
|
6802
|
+
prev = curr;
|
|
6803
|
+
}
|
|
6804
|
+
return prev[n];
|
|
6805
|
+
}
|
|
6806
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject) {
|
|
6807
|
+
const findings = [];
|
|
6808
|
+
if (declared === void 0 || declared === null) return findings;
|
|
6809
|
+
if (!objectName) return findings;
|
|
6810
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
6811
|
+
const target = fieldsByObject.get(objectName);
|
|
6812
|
+
if (!target) return findings;
|
|
6813
|
+
const known = target.names;
|
|
6814
|
+
for (const key of readSortKeys(declared)) {
|
|
6815
|
+
const name = key.field;
|
|
6816
|
+
const head = name.split(".")[0];
|
|
6817
|
+
if (SYSTEM_FIELDS.has(head)) continue;
|
|
6818
|
+
if (!known.has(head)) {
|
|
6819
|
+
const dotted = name.includes(".");
|
|
6820
|
+
findings.push({
|
|
6821
|
+
severity: "error",
|
|
6822
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
6823
|
+
where,
|
|
6824
|
+
path: `${path}${key.at}`,
|
|
6825
|
+
message: `${subject} orders by "${name}", which is not a field on object "${objectName}". The runtime refuses the sort rather than dropping it: every load of this view answers 400 INVALID_SORT (#6994), because a sort is the view's FIRST fetch and not an optional interaction.` + (dotted ? "" : suggest4(head, known)),
|
|
6826
|
+
hint: (dotted ? `'sort' reaches only whole columns of "${objectName}" itself, never a related record's column \u2014 denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + (known.size > 0 ? `Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
6827
|
+
});
|
|
6828
|
+
continue;
|
|
6829
|
+
}
|
|
6830
|
+
const meta = target.fields[name];
|
|
6831
|
+
if ((0, import_data11.isVirtualSearchField)(meta)) {
|
|
6832
|
+
const vtype = meta?.type;
|
|
6833
|
+
findings.push({
|
|
6834
|
+
severity: "error",
|
|
6835
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
6836
|
+
where,
|
|
6837
|
+
path: `${path}${key.at}`,
|
|
6838
|
+
message: `${subject} orders by "${name}" on object "${objectName}", a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materialises a column to ORDER BY. Measured, an unrefused sort on one returns 'asc' and 'desc' in byte-identical order \u2014 the rows carry the values they were asked to be ordered by, unordered, under a success.`,
|
|
6839
|
+
hint: `Denormalise the value onto "${objectName}" (a stored field, written when the source changes) and sort by that, or drop "${name}" from this sort. At runtime both doors now refuse it with 400 INVALID_SORT \u2014 the REST ingress (#6994) and the engine itself (#7095) \u2014 so the declaration breaks the view's first fetch, and every fetch after it.`
|
|
6840
|
+
});
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
return findings;
|
|
6844
|
+
}
|
|
6845
|
+
function validateSortableFields(stack) {
|
|
6846
|
+
const findings = [];
|
|
6847
|
+
if (!isRec16(stack)) return findings;
|
|
6848
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
6849
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
6850
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
6851
|
+
findings.push(
|
|
6852
|
+
...checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject)
|
|
6853
|
+
);
|
|
6854
|
+
};
|
|
6855
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
6856
|
+
const obj = objects[oi];
|
|
6857
|
+
if (!isRec16(obj)) continue;
|
|
6858
|
+
const objName = strName11(obj.name);
|
|
6859
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
6860
|
+
if (isRec16(obj.listViews)) {
|
|
6861
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
6862
|
+
if (!isRec16(lv)) continue;
|
|
6863
|
+
check(
|
|
6864
|
+
lv.sort,
|
|
6865
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
6866
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
6867
|
+
listViewObject2(lv) ?? objName,
|
|
6868
|
+
`${label2} \u203A listViews.${key}`,
|
|
6869
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
6870
|
+
"list-view sort"
|
|
6871
|
+
);
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6875
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
6876
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
6877
|
+
const view = views[vi];
|
|
6878
|
+
if (!isRec16(view)) continue;
|
|
6879
|
+
const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
|
|
6880
|
+
const viewObject = strName11(view.objectName) ?? strName11(view.object);
|
|
6881
|
+
if (isRec16(view.list)) {
|
|
6882
|
+
check(
|
|
6883
|
+
view.list.sort,
|
|
6884
|
+
listViewObject2(view.list) ?? viewObject,
|
|
6885
|
+
`view "${viewLabel2}" \u203A list`,
|
|
6886
|
+
`views[${vi}].list.sort`,
|
|
6887
|
+
"list-view sort"
|
|
6888
|
+
);
|
|
6889
|
+
}
|
|
6890
|
+
if (isRec16(view.listViews)) {
|
|
6891
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
6892
|
+
if (!isRec16(lv)) continue;
|
|
6893
|
+
check(
|
|
6894
|
+
lv.sort,
|
|
6895
|
+
listViewObject2(lv) ?? viewObject,
|
|
6896
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
6897
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
6898
|
+
"list-view sort"
|
|
6899
|
+
);
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
return findings;
|
|
6904
|
+
}
|
|
6905
|
+
function listViewObject2(listView) {
|
|
6906
|
+
const data = listView.data;
|
|
6907
|
+
return isRec16(data) ? strName11(data.object) : void 0;
|
|
6908
|
+
}
|
|
6909
|
+
|
|
6501
6910
|
// src/validate-action-name-refs.ts
|
|
6502
6911
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
6503
6912
|
function asArray28(v) {
|
|
@@ -6507,13 +6916,13 @@ function asArray28(v) {
|
|
|
6507
6916
|
}
|
|
6508
6917
|
return [];
|
|
6509
6918
|
}
|
|
6510
|
-
function
|
|
6919
|
+
function strName12(v) {
|
|
6511
6920
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6512
6921
|
}
|
|
6513
6922
|
function strList(v) {
|
|
6514
6923
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
6515
6924
|
}
|
|
6516
|
-
function
|
|
6925
|
+
function distance4(a, b) {
|
|
6517
6926
|
const m = a.length;
|
|
6518
6927
|
const n = b.length;
|
|
6519
6928
|
if (m === 0) return n;
|
|
@@ -6529,11 +6938,11 @@ function distance3(a, b) {
|
|
|
6529
6938
|
}
|
|
6530
6939
|
return prev[n];
|
|
6531
6940
|
}
|
|
6532
|
-
function
|
|
6941
|
+
function suggest5(target, known) {
|
|
6533
6942
|
let best;
|
|
6534
6943
|
let bestScore = Infinity;
|
|
6535
6944
|
for (const candidate of known) {
|
|
6536
|
-
const d =
|
|
6945
|
+
const d = distance4(target, candidate);
|
|
6537
6946
|
if (d < bestScore) {
|
|
6538
6947
|
bestScore = d;
|
|
6539
6948
|
best = candidate;
|
|
@@ -6545,13 +6954,13 @@ function suggest4(target, known) {
|
|
|
6545
6954
|
function collectActionNames(stack) {
|
|
6546
6955
|
const names = /* @__PURE__ */ new Set();
|
|
6547
6956
|
for (const action of asArray28(stack.actions)) {
|
|
6548
|
-
const n =
|
|
6957
|
+
const n = strName12(action?.name);
|
|
6549
6958
|
if (n) names.add(n);
|
|
6550
6959
|
}
|
|
6551
6960
|
for (const obj of asArray28(stack.objects)) {
|
|
6552
6961
|
if (!obj || typeof obj !== "object") continue;
|
|
6553
6962
|
for (const action of asArray28(obj.actions)) {
|
|
6554
|
-
const n =
|
|
6963
|
+
const n = strName12(action?.name);
|
|
6555
6964
|
if (n) names.add(n);
|
|
6556
6965
|
}
|
|
6557
6966
|
}
|
|
@@ -6568,7 +6977,7 @@ function validateActionNameRefs(stack) {
|
|
|
6568
6977
|
rule: ACTION_NAME_UNDEFINED,
|
|
6569
6978
|
where,
|
|
6570
6979
|
path,
|
|
6571
|
-
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` +
|
|
6980
|
+
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest5(name, known),
|
|
6572
6981
|
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
|
|
6573
6982
|
});
|
|
6574
6983
|
};
|
|
@@ -6594,7 +7003,7 @@ function validateActionNameRefs(stack) {
|
|
|
6594
7003
|
if (!def || typeof def !== "object") continue;
|
|
6595
7004
|
if (def.execution !== "aggregate") continue;
|
|
6596
7005
|
if (def.actionDef !== void 0) continue;
|
|
6597
|
-
const name =
|
|
7006
|
+
const name = strName12(def.name);
|
|
6598
7007
|
if (!name) continue;
|
|
6599
7008
|
check(
|
|
6600
7009
|
name,
|
|
@@ -6609,7 +7018,7 @@ function validateActionNameRefs(stack) {
|
|
|
6609
7018
|
for (let vi = 0; vi < views.length; vi++) {
|
|
6610
7019
|
const view = views[vi];
|
|
6611
7020
|
if (!view || typeof view !== "object") continue;
|
|
6612
|
-
const viewName =
|
|
7021
|
+
const viewName = strName12(view.name) ?? strName12(view.object) ?? `#${vi}`;
|
|
6613
7022
|
const owner = `view "${viewName}"`;
|
|
6614
7023
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
6615
7024
|
const listViews = view.listViews;
|
|
@@ -6625,7 +7034,7 @@ function validateActionNameRefs(stack) {
|
|
|
6625
7034
|
if (!obj || typeof obj !== "object") continue;
|
|
6626
7035
|
const objListViews = obj.listViews;
|
|
6627
7036
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
6628
|
-
const owner = `object "${
|
|
7037
|
+
const owner = `object "${strName12(obj.name) ?? `#${oi}`}"`;
|
|
6629
7038
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
6630
7039
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
6631
7040
|
}
|
|
@@ -6634,7 +7043,7 @@ function validateActionNameRefs(stack) {
|
|
|
6634
7043
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6635
7044
|
const page = pages[pi];
|
|
6636
7045
|
if (!page || typeof page !== "object") continue;
|
|
6637
|
-
const pageName =
|
|
7046
|
+
const pageName = strName12(page.name) ?? `#${pi}`;
|
|
6638
7047
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6639
7048
|
const props = component.properties;
|
|
6640
7049
|
if (!props || typeof props !== "object") continue;
|
|
@@ -6642,7 +7051,7 @@ function validateActionNameRefs(stack) {
|
|
|
6642
7051
|
for (let ai = 0; ai < names.length; ai++) {
|
|
6643
7052
|
check(
|
|
6644
7053
|
names[ai],
|
|
6645
|
-
`page "${pageName}" \xB7 component "${
|
|
7054
|
+
`page "${pageName}" \xB7 component "${strName12(component.type) ?? "?"}"`,
|
|
6646
7055
|
`${path}.properties.actionNames[${ai}]`,
|
|
6647
7056
|
"Quick-actions bar"
|
|
6648
7057
|
);
|
|
@@ -6653,7 +7062,7 @@ function validateActionNameRefs(stack) {
|
|
|
6653
7062
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
6654
7063
|
const app = apps[ai];
|
|
6655
7064
|
if (!app || typeof app !== "object") continue;
|
|
6656
|
-
const appName =
|
|
7065
|
+
const appName = strName12(app.name) ?? `#${ai}`;
|
|
6657
7066
|
const walkNav = (items, basePath) => {
|
|
6658
7067
|
const navItems = asArray28(items);
|
|
6659
7068
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -6661,20 +7070,20 @@ function validateActionNameRefs(stack) {
|
|
|
6661
7070
|
if (!nav || typeof nav !== "object") continue;
|
|
6662
7071
|
const navPath = `${basePath}[${ni}]`;
|
|
6663
7072
|
const actionDef = nav.actionDef;
|
|
6664
|
-
const actionName =
|
|
7073
|
+
const actionName = strName12(actionDef?.actionName);
|
|
6665
7074
|
if (nav.type === "action" && actionName) {
|
|
6666
7075
|
check(
|
|
6667
7076
|
actionName,
|
|
6668
|
-
`app "${appName}" \xB7 nav "${
|
|
7077
|
+
`app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
6669
7078
|
`${navPath}.actionDef.actionName`,
|
|
6670
7079
|
"Navigation action item"
|
|
6671
7080
|
);
|
|
6672
7081
|
}
|
|
6673
|
-
const runAction =
|
|
7082
|
+
const runAction = strName12(nav.runAction);
|
|
6674
7083
|
if (nav.type === "object" && runAction) {
|
|
6675
7084
|
check(
|
|
6676
7085
|
runAction,
|
|
6677
|
-
`app "${appName}" \xB7 nav "${
|
|
7086
|
+
`app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
6678
7087
|
`${navPath}.runAction`,
|
|
6679
7088
|
"Navigation deep-link auto-run"
|
|
6680
7089
|
);
|
|
@@ -6700,7 +7109,7 @@ function asArray29(v) {
|
|
|
6700
7109
|
}
|
|
6701
7110
|
return [];
|
|
6702
7111
|
}
|
|
6703
|
-
function
|
|
7112
|
+
function strName13(v) {
|
|
6704
7113
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6705
7114
|
}
|
|
6706
7115
|
function strList2(v) {
|
|
@@ -6715,7 +7124,7 @@ function collectNamePlacedActions(stack) {
|
|
|
6715
7124
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
6716
7125
|
}
|
|
6717
7126
|
for (const def of asArray29(list3.bulkActionDefs)) {
|
|
6718
|
-
const n =
|
|
7127
|
+
const n = strName13(def?.name);
|
|
6719
7128
|
if (n) placed.add(n);
|
|
6720
7129
|
}
|
|
6721
7130
|
};
|
|
@@ -6741,7 +7150,7 @@ function validateActionLocations(stack) {
|
|
|
6741
7150
|
const check = (action, path) => {
|
|
6742
7151
|
if (!action || typeof action !== "object") return;
|
|
6743
7152
|
if ("locations" in action) return;
|
|
6744
|
-
const name =
|
|
7153
|
+
const name = strName13(action.name);
|
|
6745
7154
|
if (!name) return;
|
|
6746
7155
|
if (namePlaced.has(name)) return;
|
|
6747
7156
|
findings.push({
|
|
@@ -6766,14 +7175,14 @@ function validateActionLocations(stack) {
|
|
|
6766
7175
|
}
|
|
6767
7176
|
|
|
6768
7177
|
// src/validate-component-props.ts
|
|
6769
|
-
var
|
|
7178
|
+
var import_ui4 = require("@objectstack/spec/ui");
|
|
6770
7179
|
var import_spec3 = require("@objectstack/spec");
|
|
6771
7180
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
6772
7181
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
6773
|
-
function
|
|
7182
|
+
function isRec17(v) {
|
|
6774
7183
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6775
7184
|
}
|
|
6776
|
-
function
|
|
7185
|
+
function strName14(v) {
|
|
6777
7186
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6778
7187
|
}
|
|
6779
7188
|
function asArray30(v) {
|
|
@@ -6783,12 +7192,12 @@ function asArray30(v) {
|
|
|
6783
7192
|
}
|
|
6784
7193
|
return [];
|
|
6785
7194
|
}
|
|
6786
|
-
var PROPS_SCHEMAS =
|
|
7195
|
+
var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
|
|
6787
7196
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
6788
7197
|
function suppliedByDataSource(issue, component) {
|
|
6789
7198
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
6790
|
-
const dataSource =
|
|
6791
|
-
return
|
|
7199
|
+
const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
|
|
7200
|
+
return strName14(dataSource?.object) !== void 0;
|
|
6792
7201
|
}
|
|
6793
7202
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
6794
7203
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -6805,18 +7214,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
6805
7214
|
}
|
|
6806
7215
|
function validateComponentProps(stack) {
|
|
6807
7216
|
const findings = [];
|
|
6808
|
-
if (!
|
|
7217
|
+
if (!isRec17(stack)) return findings;
|
|
6809
7218
|
const pages = asArray30(stack.pages);
|
|
6810
7219
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6811
7220
|
const page = pages[pi];
|
|
6812
|
-
if (!
|
|
6813
|
-
const pageName =
|
|
7221
|
+
if (!isRec17(page)) continue;
|
|
7222
|
+
const pageName = strName14(page.name) ?? `#${pi}`;
|
|
6814
7223
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6815
|
-
const type =
|
|
7224
|
+
const type = strName14(component.type);
|
|
6816
7225
|
if (!type) continue;
|
|
6817
7226
|
const schema = PROPS_SCHEMAS[type];
|
|
6818
7227
|
if (!schema) continue;
|
|
6819
|
-
const props =
|
|
7228
|
+
const props = isRec17(component.properties) ? component.properties : void 0;
|
|
6820
7229
|
if (!props) continue;
|
|
6821
7230
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
6822
7231
|
const base = `${path}.properties`;
|
|
@@ -6888,16 +7297,16 @@ function asArray31(v) {
|
|
|
6888
7297
|
}
|
|
6889
7298
|
return [];
|
|
6890
7299
|
}
|
|
6891
|
-
function
|
|
7300
|
+
function strName15(v) {
|
|
6892
7301
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6893
7302
|
}
|
|
6894
7303
|
function strList3(v) {
|
|
6895
7304
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
6896
7305
|
}
|
|
6897
|
-
function
|
|
7306
|
+
function isRec18(v) {
|
|
6898
7307
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6899
7308
|
}
|
|
6900
|
-
function
|
|
7309
|
+
function distance5(a, b) {
|
|
6901
7310
|
const m = a.length;
|
|
6902
7311
|
const n = b.length;
|
|
6903
7312
|
if (m === 0) return n;
|
|
@@ -6913,11 +7322,11 @@ function distance4(a, b) {
|
|
|
6913
7322
|
}
|
|
6914
7323
|
return prev[n];
|
|
6915
7324
|
}
|
|
6916
|
-
function
|
|
7325
|
+
function suggest6(target, known) {
|
|
6917
7326
|
let best;
|
|
6918
7327
|
let bestScore = Infinity;
|
|
6919
7328
|
for (const c of known) {
|
|
6920
|
-
const d =
|
|
7329
|
+
const d = distance5(target, c);
|
|
6921
7330
|
if (d < bestScore) {
|
|
6922
7331
|
bestScore = d;
|
|
6923
7332
|
best = c;
|
|
@@ -6933,16 +7342,16 @@ function list2(names) {
|
|
|
6933
7342
|
function indexDatasets(stack) {
|
|
6934
7343
|
const out = /* @__PURE__ */ new Map();
|
|
6935
7344
|
for (const ds of asArray31(stack.datasets)) {
|
|
6936
|
-
const name =
|
|
7345
|
+
const name = strName15(ds.name);
|
|
6937
7346
|
if (!name) continue;
|
|
6938
7347
|
const dimensions = /* @__PURE__ */ new Set();
|
|
6939
7348
|
for (const d of asArray31(ds.dimensions)) {
|
|
6940
|
-
const n =
|
|
7349
|
+
const n = strName15(d.name);
|
|
6941
7350
|
if (n) dimensions.add(n);
|
|
6942
7351
|
}
|
|
6943
7352
|
const measures = /* @__PURE__ */ new Set();
|
|
6944
7353
|
for (const m of asArray31(ds.measures)) {
|
|
6945
|
-
const n =
|
|
7354
|
+
const n = strName15(m.name);
|
|
6946
7355
|
if (n) measures.add(n);
|
|
6947
7356
|
}
|
|
6948
7357
|
out.set(name, { dimensions, measures });
|
|
@@ -6965,7 +7374,7 @@ function validateChartBindings(stack) {
|
|
|
6965
7374
|
where: binding.where,
|
|
6966
7375
|
path: `${binding.path}.dataset`,
|
|
6967
7376
|
message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
|
|
6968
|
-
hint: `Declared datasets: ${list2(datasets.keys())}.${
|
|
7377
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
6969
7378
|
});
|
|
6970
7379
|
return;
|
|
6971
7380
|
}
|
|
@@ -6977,7 +7386,7 @@ function validateChartBindings(stack) {
|
|
|
6977
7386
|
where: binding.where,
|
|
6978
7387
|
path,
|
|
6979
7388
|
message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
|
|
6980
|
-
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${
|
|
7389
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
6981
7390
|
});
|
|
6982
7391
|
};
|
|
6983
7392
|
const measureRef = (name, path, selected2) => {
|
|
@@ -6988,7 +7397,7 @@ function validateChartBindings(stack) {
|
|
|
6988
7397
|
where: binding.where,
|
|
6989
7398
|
path,
|
|
6990
7399
|
message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
|
|
6991
|
-
hint: `Dataset measures: ${list2(ds.measures)}.${
|
|
7400
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
6992
7401
|
});
|
|
6993
7402
|
return;
|
|
6994
7403
|
}
|
|
@@ -7023,26 +7432,26 @@ function validateChartBindings(stack) {
|
|
|
7023
7432
|
const reports = asArray31(stack.reports);
|
|
7024
7433
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
7025
7434
|
const report = reports[ri];
|
|
7026
|
-
if (!
|
|
7027
|
-
const reportName =
|
|
7435
|
+
if (!isRec18(report)) continue;
|
|
7436
|
+
const reportName = strName15(report.name) ?? `#${ri}`;
|
|
7028
7437
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
7029
|
-
if (!
|
|
7438
|
+
if (!isRec18(chart)) return;
|
|
7030
7439
|
check({
|
|
7031
7440
|
dataset,
|
|
7032
7441
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
7033
7442
|
// it in lets the yAxis "declared but not selected" check work without
|
|
7034
7443
|
// reporting the selection itself twice.
|
|
7035
7444
|
values: { names: values, path: `${path}.values` },
|
|
7036
|
-
xAxis:
|
|
7037
|
-
yAxis:
|
|
7038
|
-
series: asArray31(chart.series).map((s, si) => ({ name:
|
|
7445
|
+
xAxis: strName15(chart.xAxis) ? { name: strName15(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
7446
|
+
yAxis: strName15(chart.yAxis) ? { name: strName15(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
7447
|
+
series: asArray31(chart.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
7039
7448
|
where,
|
|
7040
7449
|
path: `${path}.chart`
|
|
7041
7450
|
});
|
|
7042
7451
|
};
|
|
7043
7452
|
checkReportChart(
|
|
7044
7453
|
report.chart,
|
|
7045
|
-
|
|
7454
|
+
strName15(report.dataset),
|
|
7046
7455
|
strList3(report.values),
|
|
7047
7456
|
`report "${reportName}" \xB7 chart`,
|
|
7048
7457
|
`reports[${ri}]`
|
|
@@ -7050,22 +7459,22 @@ function validateChartBindings(stack) {
|
|
|
7050
7459
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
7051
7460
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
7052
7461
|
const block = blocks[bi];
|
|
7053
|
-
if (!
|
|
7462
|
+
if (!isRec18(block)) continue;
|
|
7054
7463
|
checkReportChart(
|
|
7055
7464
|
block.chart,
|
|
7056
|
-
|
|
7465
|
+
strName15(block.dataset),
|
|
7057
7466
|
strList3(block.values),
|
|
7058
|
-
`report "${reportName}" \xB7 block "${
|
|
7467
|
+
`report "${reportName}" \xB7 block "${strName15(block.name) ?? `#${bi}`}" chart`,
|
|
7059
7468
|
`reports[${ri}].blocks[${bi}]`
|
|
7060
7469
|
);
|
|
7061
7470
|
}
|
|
7062
7471
|
}
|
|
7063
7472
|
const checkListChart = (container, where, path) => {
|
|
7064
|
-
if (!
|
|
7473
|
+
if (!isRec18(container)) return;
|
|
7065
7474
|
const chart = container.chart;
|
|
7066
|
-
if (!
|
|
7475
|
+
if (!isRec18(chart)) return;
|
|
7067
7476
|
check({
|
|
7068
|
-
dataset:
|
|
7477
|
+
dataset: strName15(chart.dataset),
|
|
7069
7478
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
7070
7479
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
7071
7480
|
where,
|
|
@@ -7075,10 +7484,10 @@ function validateChartBindings(stack) {
|
|
|
7075
7484
|
const views = asArray31(stack.views);
|
|
7076
7485
|
for (let vi = 0; vi < views.length; vi++) {
|
|
7077
7486
|
const view = views[vi];
|
|
7078
|
-
if (!
|
|
7079
|
-
const viewName =
|
|
7487
|
+
if (!isRec18(view)) continue;
|
|
7488
|
+
const viewName = strName15(view.name) ?? strName15(view.objectName) ?? `#${vi}`;
|
|
7080
7489
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
7081
|
-
if (
|
|
7490
|
+
if (isRec18(view.listViews)) {
|
|
7082
7491
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
7083
7492
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
7084
7493
|
}
|
|
@@ -7087,8 +7496,8 @@ function validateChartBindings(stack) {
|
|
|
7087
7496
|
const objects = asArray31(stack.objects);
|
|
7088
7497
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7089
7498
|
const obj = objects[oi];
|
|
7090
|
-
if (!
|
|
7091
|
-
const objName =
|
|
7499
|
+
if (!isRec18(obj) || !isRec18(obj.listViews)) continue;
|
|
7500
|
+
const objName = strName15(obj.name) ?? `#${oi}`;
|
|
7092
7501
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
7093
7502
|
checkListChart(
|
|
7094
7503
|
lv,
|
|
@@ -7100,19 +7509,19 @@ function validateChartBindings(stack) {
|
|
|
7100
7509
|
const pages = asArray31(stack.pages);
|
|
7101
7510
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
7102
7511
|
const page = pages[pi];
|
|
7103
|
-
if (!
|
|
7104
|
-
const pageName =
|
|
7512
|
+
if (!isRec18(page)) continue;
|
|
7513
|
+
const pageName = strName15(page.name) ?? `#${pi}`;
|
|
7105
7514
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
7106
|
-
const props =
|
|
7107
|
-
if (!props || !
|
|
7108
|
-
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name:
|
|
7109
|
-
const seriesRefs = asArray31(props.series).map((s, si) => ({ name:
|
|
7515
|
+
const props = isRec18(component.properties) ? component.properties : void 0;
|
|
7516
|
+
if (!props || !strName15(props.dataset)) continue;
|
|
7517
|
+
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName15(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
7518
|
+
const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
7110
7519
|
check({
|
|
7111
|
-
dataset:
|
|
7520
|
+
dataset: strName15(props.dataset),
|
|
7112
7521
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
7113
7522
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
7114
7523
|
series: [...axisRefs, ...seriesRefs],
|
|
7115
|
-
where: `page "${pageName}" \xB7 ${
|
|
7524
|
+
where: `page "${pageName}" \xB7 ${strName15(component.type) ?? "chart"}`,
|
|
7116
7525
|
path: `${path}.properties`
|
|
7117
7526
|
});
|
|
7118
7527
|
}
|
|
@@ -7126,11 +7535,11 @@ var import_meta4 = {};
|
|
|
7126
7535
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7127
7536
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7128
7537
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7129
|
-
var
|
|
7538
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7130
7539
|
function asArray32(v) {
|
|
7131
|
-
if (Array.isArray(v)) return v.filter(
|
|
7132
|
-
if (
|
|
7133
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7540
|
+
if (Array.isArray(v)) return v.filter(isRec19);
|
|
7541
|
+
if (isRec19(v)) {
|
|
7542
|
+
return Object.entries(v).filter(([, def]) => isRec19(def)).map(([name, def]) => ({ name, ...def }));
|
|
7134
7543
|
}
|
|
7135
7544
|
return [];
|
|
7136
7545
|
}
|
|
@@ -7147,7 +7556,7 @@ function loadAjv() {
|
|
|
7147
7556
|
`@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.`
|
|
7148
7557
|
);
|
|
7149
7558
|
}
|
|
7150
|
-
const ctor =
|
|
7559
|
+
const ctor = isRec19(mod) && "default" in mod ? mod.default : mod;
|
|
7151
7560
|
cachedAjv = ctor;
|
|
7152
7561
|
return ctor;
|
|
7153
7562
|
}
|
|
@@ -7162,7 +7571,7 @@ function loadAddFormats() {
|
|
|
7162
7571
|
`@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.`
|
|
7163
7572
|
);
|
|
7164
7573
|
}
|
|
7165
|
-
const plugin =
|
|
7574
|
+
const plugin = isRec19(mod) && "default" in mod ? mod.default : mod;
|
|
7166
7575
|
cachedAddFormats = plugin;
|
|
7167
7576
|
return plugin;
|
|
7168
7577
|
}
|
|
@@ -7192,13 +7601,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
7192
7601
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
7193
7602
|
for (const branch of ["then", "otherwise"]) {
|
|
7194
7603
|
const nested = rule[branch];
|
|
7195
|
-
if (
|
|
7604
|
+
if (isRec19(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
7196
7605
|
}
|
|
7197
7606
|
return out;
|
|
7198
7607
|
}
|
|
7199
7608
|
function walkObjectValidationRules(stack) {
|
|
7200
7609
|
const walked = [];
|
|
7201
|
-
if (!
|
|
7610
|
+
if (!isRec19(stack)) return walked;
|
|
7202
7611
|
for (const obj of asArray32(stack.objects)) {
|
|
7203
7612
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7204
7613
|
const validations = obj.validations;
|
|
@@ -7233,7 +7642,7 @@ function validateRuleCompilability(stack) {
|
|
|
7233
7642
|
});
|
|
7234
7643
|
}
|
|
7235
7644
|
}
|
|
7236
|
-
if (rule.type === "json_schema" &&
|
|
7645
|
+
if (rule.type === "json_schema" && isRec19(rule.schema)) {
|
|
7237
7646
|
try {
|
|
7238
7647
|
createRuntimeAjv().compile(rule.schema);
|
|
7239
7648
|
} catch (err) {
|
|
@@ -7253,7 +7662,7 @@ function validateRuleCompilability(stack) {
|
|
|
7253
7662
|
|
|
7254
7663
|
// src/validate-rule-schema-formats.ts
|
|
7255
7664
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7256
|
-
var
|
|
7665
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7257
7666
|
var SUBSCHEMA_KEYS = [
|
|
7258
7667
|
"additionalItems",
|
|
7259
7668
|
"additionalProperties",
|
|
@@ -7277,7 +7686,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7277
7686
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7278
7687
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7279
7688
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7280
|
-
if (!
|
|
7689
|
+
if (!isRec20(schema)) return;
|
|
7281
7690
|
if (typeof schema.format === "string") {
|
|
7282
7691
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7283
7692
|
}
|
|
@@ -7296,7 +7705,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7296
7705
|
}
|
|
7297
7706
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7298
7707
|
const value = schema[key];
|
|
7299
|
-
if (!
|
|
7708
|
+
if (!isRec20(value)) continue;
|
|
7300
7709
|
for (const [name, entry] of Object.entries(value)) {
|
|
7301
7710
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7302
7711
|
}
|
|
@@ -7304,13 +7713,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7304
7713
|
const items = schema.items;
|
|
7305
7714
|
if (Array.isArray(items)) {
|
|
7306
7715
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7307
|
-
} else if (
|
|
7716
|
+
} else if (isRec20(items)) {
|
|
7308
7717
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7309
7718
|
}
|
|
7310
7719
|
const dependencies = schema.dependencies;
|
|
7311
|
-
if (
|
|
7720
|
+
if (isRec20(dependencies)) {
|
|
7312
7721
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7313
|
-
if (!
|
|
7722
|
+
if (!isRec20(entry)) continue;
|
|
7314
7723
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7315
7724
|
}
|
|
7316
7725
|
}
|
|
@@ -7337,9 +7746,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
7337
7746
|
let best = null;
|
|
7338
7747
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
7339
7748
|
for (const candidate of [...registered].sort()) {
|
|
7340
|
-
const
|
|
7341
|
-
if (
|
|
7342
|
-
bestDistance =
|
|
7749
|
+
const distance8 = editDistance2(authored, candidate);
|
|
7750
|
+
if (distance8 < bestDistance) {
|
|
7751
|
+
bestDistance = distance8;
|
|
7343
7752
|
best = candidate;
|
|
7344
7753
|
}
|
|
7345
7754
|
}
|
|
@@ -7349,7 +7758,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7349
7758
|
const findings = [];
|
|
7350
7759
|
const pending = [];
|
|
7351
7760
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7352
|
-
if (rule.type !== "json_schema" || !
|
|
7761
|
+
if (rule.type !== "json_schema" || !isRec20(rule.schema)) continue;
|
|
7353
7762
|
const uses = [];
|
|
7354
7763
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7355
7764
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7474,7 +7883,7 @@ function asArray34(v) {
|
|
|
7474
7883
|
}
|
|
7475
7884
|
return [];
|
|
7476
7885
|
}
|
|
7477
|
-
function
|
|
7886
|
+
function strName16(v) {
|
|
7478
7887
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7479
7888
|
}
|
|
7480
7889
|
function collectNavExposures(stack) {
|
|
@@ -7483,18 +7892,18 @@ function collectNavExposures(stack) {
|
|
|
7483
7892
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
7484
7893
|
const app = apps[ai];
|
|
7485
7894
|
if (!app || typeof app !== "object") continue;
|
|
7486
|
-
const appName =
|
|
7895
|
+
const appName = strName16(app.name) ?? `#${ai}`;
|
|
7487
7896
|
const walk = (items, basePath) => {
|
|
7488
7897
|
const navItems = asArray34(items);
|
|
7489
7898
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
7490
7899
|
const nav = navItems[ni];
|
|
7491
7900
|
if (!nav || typeof nav !== "object") continue;
|
|
7492
7901
|
const navPath = `${basePath}[${ni}]`;
|
|
7493
|
-
const objectName =
|
|
7902
|
+
const objectName = strName16(nav.objectName);
|
|
7494
7903
|
if (nav.type === "object" && objectName) {
|
|
7495
7904
|
out.push({
|
|
7496
7905
|
objectName,
|
|
7497
|
-
where: `app "${appName}" \xB7 nav "${
|
|
7906
|
+
where: `app "${appName}" \xB7 nav "${strName16(nav.id) ?? `#${ni}`}"`,
|
|
7498
7907
|
path: `${navPath}.objectName`
|
|
7499
7908
|
});
|
|
7500
7909
|
}
|
|
@@ -7518,7 +7927,7 @@ function validateNavAccess(stack) {
|
|
|
7518
7927
|
if (exposures.length === 0) return findings;
|
|
7519
7928
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
7520
7929
|
for (const obj of asArray34(stack.objects)) {
|
|
7521
|
-
const n =
|
|
7930
|
+
const n = strName16(obj.name);
|
|
7522
7931
|
if (n) ownObjects.add(n);
|
|
7523
7932
|
}
|
|
7524
7933
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -7551,18 +7960,18 @@ var import_spec4 = require("@objectstack/spec");
|
|
|
7551
7960
|
var import_system4 = require("@objectstack/spec/system");
|
|
7552
7961
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
7553
7962
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
7554
|
-
function
|
|
7963
|
+
function isRec21(v) {
|
|
7555
7964
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7556
7965
|
}
|
|
7557
7966
|
function asArray35(v) {
|
|
7558
7967
|
if (Array.isArray(v)) return v;
|
|
7559
|
-
if (
|
|
7968
|
+
if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
|
|
7560
7969
|
return [];
|
|
7561
7970
|
}
|
|
7562
|
-
function
|
|
7971
|
+
function strName17(v) {
|
|
7563
7972
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7564
7973
|
}
|
|
7565
|
-
function
|
|
7974
|
+
function distance6(a, b) {
|
|
7566
7975
|
const m = a.length;
|
|
7567
7976
|
const n = b.length;
|
|
7568
7977
|
if (m === 0) return n;
|
|
@@ -7578,7 +7987,7 @@ function distance5(a, b) {
|
|
|
7578
7987
|
}
|
|
7579
7988
|
return prev[n];
|
|
7580
7989
|
}
|
|
7581
|
-
function
|
|
7990
|
+
function suggest7(target, known) {
|
|
7582
7991
|
const names = [...known];
|
|
7583
7992
|
const segmentMatch = names.find(
|
|
7584
7993
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -7587,7 +7996,7 @@ function suggest6(target, known) {
|
|
|
7587
7996
|
let best;
|
|
7588
7997
|
let bestScore = Infinity;
|
|
7589
7998
|
for (const candidate of names) {
|
|
7590
|
-
const d =
|
|
7999
|
+
const d = distance6(target, candidate);
|
|
7591
8000
|
if (d < bestScore) {
|
|
7592
8001
|
bestScore = d;
|
|
7593
8002
|
best = candidate;
|
|
@@ -7620,33 +8029,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
7620
8029
|
const addSections = (container, binding) => {
|
|
7621
8030
|
if (!binding) return;
|
|
7622
8031
|
for (const section of asArray35(container.sections)) {
|
|
7623
|
-
const sectionName =
|
|
8032
|
+
const sectionName = strName17(section.name);
|
|
7624
8033
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
7625
8034
|
}
|
|
7626
8035
|
};
|
|
7627
|
-
const listBinding =
|
|
7628
|
-
if (
|
|
7629
|
-
addView(recordObject ?? listBinding,
|
|
8036
|
+
const listBinding = isRec21(view.list) ? bindingOf(view.list) : void 0;
|
|
8037
|
+
if (isRec21(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
8038
|
+
addView(recordObject ?? listBinding, strName17(view.name));
|
|
7630
8039
|
const named = namedViewKeys(view);
|
|
7631
8040
|
for (const family of ["listViews", "formViews"]) {
|
|
7632
8041
|
const container = view[family];
|
|
7633
|
-
if (!
|
|
8042
|
+
if (!isRec21(container)) continue;
|
|
7634
8043
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
7635
8044
|
let at = 0;
|
|
7636
8045
|
for (const sub of Object.values(container)) {
|
|
7637
8046
|
if (!sub || typeof sub !== "object") continue;
|
|
7638
8047
|
const registryKey = registryKeys[at++];
|
|
7639
|
-
if (!
|
|
8048
|
+
if (!isRec21(sub)) continue;
|
|
7640
8049
|
const binding = bindingOf(sub) ?? listBinding;
|
|
7641
8050
|
addView(binding, registryKey);
|
|
7642
8051
|
addSections(sub, binding);
|
|
7643
8052
|
}
|
|
7644
8053
|
}
|
|
7645
|
-
if (
|
|
8054
|
+
if (isRec21(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
7646
8055
|
addSections(view, recordObject ?? listBinding);
|
|
7647
8056
|
}
|
|
7648
8057
|
function defaultListViewKey(object, container) {
|
|
7649
|
-
if (!object || !
|
|
8058
|
+
if (!object || !isRec21(container.list)) return void 0;
|
|
7650
8059
|
const item = (0, import_spec4.expandViewContainer)(object, container).find(
|
|
7651
8060
|
(i) => i.viewKind === "list" && i.isDefault
|
|
7652
8061
|
);
|
|
@@ -7658,7 +8067,7 @@ function namedViewKeys(container) {
|
|
|
7658
8067
|
const object = "probe";
|
|
7659
8068
|
const prefix = `${object}.`;
|
|
7660
8069
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
7661
|
-
const countEntries = (v) =>
|
|
8070
|
+
const countEntries = (v) => isRec21(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
7662
8071
|
const listCount = countEntries(container.listViews);
|
|
7663
8072
|
const formCount = countEntries(container.formViews);
|
|
7664
8073
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -7676,14 +8085,14 @@ function readOptions(field) {
|
|
|
7676
8085
|
values.add(opt);
|
|
7677
8086
|
continue;
|
|
7678
8087
|
}
|
|
7679
|
-
if (!
|
|
7680
|
-
const value =
|
|
8088
|
+
if (!isRec21(opt)) continue;
|
|
8089
|
+
const value = strName17(opt.value);
|
|
7681
8090
|
if (!value) continue;
|
|
7682
8091
|
values.add(value);
|
|
7683
|
-
const label2 =
|
|
8092
|
+
const label2 = strName17(opt.label);
|
|
7684
8093
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
7685
8094
|
}
|
|
7686
|
-
} else if (
|
|
8095
|
+
} else if (isRec21(raw)) {
|
|
7687
8096
|
for (const [value, label2] of Object.entries(raw)) {
|
|
7688
8097
|
values.add(value);
|
|
7689
8098
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -7704,23 +8113,23 @@ function buildUniverse(stack) {
|
|
|
7704
8113
|
return facts;
|
|
7705
8114
|
};
|
|
7706
8115
|
for (const obj of asArray35(stack.objects)) {
|
|
7707
|
-
const objectName =
|
|
8116
|
+
const objectName = strName17(obj.name);
|
|
7708
8117
|
if (!objectName) continue;
|
|
7709
8118
|
const facts = factsFor(objectName);
|
|
7710
8119
|
for (const field of asArray35(obj.fields)) {
|
|
7711
|
-
const fieldName =
|
|
8120
|
+
const fieldName = strName17(field.name);
|
|
7712
8121
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
7713
8122
|
}
|
|
7714
8123
|
for (const action of asArray35(obj.actions)) {
|
|
7715
|
-
const actionName =
|
|
8124
|
+
const actionName = strName17(action.name);
|
|
7716
8125
|
if (actionName) facts.actions.set(actionName, action);
|
|
7717
8126
|
}
|
|
7718
8127
|
for (const view of asArray35(obj.views)) {
|
|
7719
|
-
collectViewRecord({ ...view, object:
|
|
8128
|
+
collectViewRecord({ ...view, object: strName17(view.object) ?? objectName }, factsFor);
|
|
7720
8129
|
}
|
|
7721
8130
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
7722
8131
|
for (const group of asArray35(obj.fieldGroups)) {
|
|
7723
|
-
const key =
|
|
8132
|
+
const key = strName17(group.key) ?? strName17(group.name);
|
|
7724
8133
|
if (key) facts.sections.add(key);
|
|
7725
8134
|
}
|
|
7726
8135
|
}
|
|
@@ -7731,10 +8140,10 @@ function buildUniverse(stack) {
|
|
|
7731
8140
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
7732
8141
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
7733
8142
|
if (!walked.objectName) continue;
|
|
7734
|
-
const props =
|
|
8143
|
+
const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
|
|
7735
8144
|
if (!props) continue;
|
|
7736
8145
|
for (const section of asArray35(props.sections)) {
|
|
7737
|
-
const sectionName =
|
|
8146
|
+
const sectionName = strName17(section.name);
|
|
7738
8147
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
7739
8148
|
}
|
|
7740
8149
|
}
|
|
@@ -7742,9 +8151,9 @@ function buildUniverse(stack) {
|
|
|
7742
8151
|
const globalActions = /* @__PURE__ */ new Map();
|
|
7743
8152
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
7744
8153
|
for (const action of asArray35(stack.actions)) {
|
|
7745
|
-
const actionName =
|
|
8154
|
+
const actionName = strName17(action.name);
|
|
7746
8155
|
if (!actionName) continue;
|
|
7747
|
-
const owner =
|
|
8156
|
+
const owner = strName17(action.objectName) ?? strName17(action.object);
|
|
7748
8157
|
if (owner) {
|
|
7749
8158
|
factsFor(owner).actions.set(actionName, action);
|
|
7750
8159
|
actionOwners.set(actionName, owner);
|
|
@@ -7759,19 +8168,19 @@ function buildUniverse(stack) {
|
|
|
7759
8168
|
}
|
|
7760
8169
|
const apps = /* @__PURE__ */ new Map();
|
|
7761
8170
|
for (const app of asArray35(stack.apps)) {
|
|
7762
|
-
const appName =
|
|
8171
|
+
const appName = strName17(app.name);
|
|
7763
8172
|
if (!appName) continue;
|
|
7764
8173
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
7765
8174
|
const walkNav = (items) => {
|
|
7766
8175
|
for (const item of asArray35(items)) {
|
|
7767
|
-
const id =
|
|
8176
|
+
const id = strName17(item.id);
|
|
7768
8177
|
if (id) navIds.add(id);
|
|
7769
8178
|
if (item.children) walkNav(item.children);
|
|
7770
8179
|
}
|
|
7771
8180
|
};
|
|
7772
8181
|
walkNav(app.navigation);
|
|
7773
8182
|
for (const area of asArray35(app.areas)) {
|
|
7774
|
-
const areaId =
|
|
8183
|
+
const areaId = strName17(area.id);
|
|
7775
8184
|
if (areaId) navIds.add(areaId);
|
|
7776
8185
|
walkNav(area.navigation);
|
|
7777
8186
|
}
|
|
@@ -7779,20 +8188,20 @@ function buildUniverse(stack) {
|
|
|
7779
8188
|
}
|
|
7780
8189
|
const dashboards = /* @__PURE__ */ new Map();
|
|
7781
8190
|
for (const dash of asArray35(stack.dashboards)) {
|
|
7782
|
-
const dashName =
|
|
8191
|
+
const dashName = strName17(dash.name);
|
|
7783
8192
|
if (!dashName) continue;
|
|
7784
8193
|
const widgets = /* @__PURE__ */ new Set();
|
|
7785
8194
|
for (const widget of asArray35(dash.widgets)) {
|
|
7786
|
-
const id =
|
|
8195
|
+
const id = strName17(widget.id) ?? strName17(widget.name);
|
|
7787
8196
|
if (id) widgets.add(id);
|
|
7788
8197
|
}
|
|
7789
8198
|
const actions = /* @__PURE__ */ new Set();
|
|
7790
8199
|
const headerActions = [
|
|
7791
|
-
...asArray35(
|
|
8200
|
+
...asArray35(isRec21(dash.header) ? dash.header.actions : void 0),
|
|
7792
8201
|
...asArray35(dash.actions)
|
|
7793
8202
|
];
|
|
7794
8203
|
for (const action of headerActions) {
|
|
7795
|
-
const key =
|
|
8204
|
+
const key = strName17(action.actionUrl) ?? strName17(action.url) ?? strName17(action.name);
|
|
7796
8205
|
if (key) actions.add(key);
|
|
7797
8206
|
}
|
|
7798
8207
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -7804,7 +8213,7 @@ function localePath(bundleIndex, locale) {
|
|
|
7804
8213
|
}
|
|
7805
8214
|
function validateTranslationReferences(stack) {
|
|
7806
8215
|
const findings = [];
|
|
7807
|
-
if (!
|
|
8216
|
+
if (!isRec21(stack)) return findings;
|
|
7808
8217
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
7809
8218
|
if (bundles.length === 0) return findings;
|
|
7810
8219
|
const universe = buildUniverse(stack);
|
|
@@ -7813,13 +8222,13 @@ function validateTranslationReferences(stack) {
|
|
|
7813
8222
|
};
|
|
7814
8223
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
7815
8224
|
const bundle = bundles[bi];
|
|
7816
|
-
if (!
|
|
8225
|
+
if (!isRec21(bundle)) continue;
|
|
7817
8226
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
7818
|
-
if (!
|
|
8227
|
+
if (!isRec21(rawData)) continue;
|
|
7819
8228
|
const base = localePath(bi, locale);
|
|
7820
8229
|
const inLocale = `locale "${locale}"`;
|
|
7821
8230
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
7822
|
-
if (!
|
|
8231
|
+
if (!isRec21(rawNode)) continue;
|
|
7823
8232
|
const objPath = `${base}.objects.${objectName}`;
|
|
7824
8233
|
const facts = universe.objects.get(objectName);
|
|
7825
8234
|
if (!facts) {
|
|
@@ -7827,7 +8236,7 @@ function validateTranslationReferences(stack) {
|
|
|
7827
8236
|
orphan(
|
|
7828
8237
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
7829
8238
|
objPath,
|
|
7830
|
-
(0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` +
|
|
8239
|
+
(0, import_system4.hasPlatformObjectPrefix)(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest7(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest7(objectName, universe.objects.keys()),
|
|
7831
8240
|
`Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
|
|
7832
8241
|
);
|
|
7833
8242
|
continue;
|
|
@@ -7840,12 +8249,12 @@ function validateTranslationReferences(stack) {
|
|
|
7840
8249
|
orphan(
|
|
7841
8250
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
7842
8251
|
fieldPath,
|
|
7843
|
-
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` +
|
|
8252
|
+
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest7(fieldName, facts.fields.keys()),
|
|
7844
8253
|
`Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
|
|
7845
8254
|
);
|
|
7846
8255
|
continue;
|
|
7847
8256
|
}
|
|
7848
|
-
if (!
|
|
8257
|
+
if (!isRec21(rawField)) continue;
|
|
7849
8258
|
checkOptionKeys(findings, {
|
|
7850
8259
|
optionMap: rawField.options,
|
|
7851
8260
|
field,
|
|
@@ -7860,7 +8269,7 @@ function validateTranslationReferences(stack) {
|
|
|
7860
8269
|
orphan(
|
|
7861
8270
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
7862
8271
|
`${objPath}._views.${viewName}`,
|
|
7863
|
-
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` +
|
|
8272
|
+
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest7(viewName, facts.views),
|
|
7864
8273
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
7865
8274
|
);
|
|
7866
8275
|
}
|
|
@@ -7869,7 +8278,7 @@ function validateTranslationReferences(stack) {
|
|
|
7869
8278
|
orphan(
|
|
7870
8279
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
7871
8280
|
`${objPath}._sections.${sectionName}`,
|
|
7872
|
-
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` +
|
|
8281
|
+
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest7(sectionName, facts.sections),
|
|
7873
8282
|
`Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
|
|
7874
8283
|
);
|
|
7875
8284
|
}
|
|
@@ -7880,7 +8289,7 @@ function validateTranslationReferences(stack) {
|
|
|
7880
8289
|
orphan(
|
|
7881
8290
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
7882
8291
|
actionPath,
|
|
7883
|
-
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` +
|
|
8292
|
+
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest7(actionName, facts.actions.keys()),
|
|
7884
8293
|
`Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
|
|
7885
8294
|
);
|
|
7886
8295
|
continue;
|
|
@@ -7902,7 +8311,7 @@ function validateTranslationReferences(stack) {
|
|
|
7902
8311
|
orphan(
|
|
7903
8312
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
7904
8313
|
actionPath,
|
|
7905
|
-
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` +
|
|
8314
|
+
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest7(actionName, universe.globalActions.keys()),
|
|
7906
8315
|
owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
|
|
7907
8316
|
);
|
|
7908
8317
|
continue;
|
|
@@ -7922,18 +8331,18 @@ function validateTranslationReferences(stack) {
|
|
|
7922
8331
|
orphan(
|
|
7923
8332
|
`${inLocale} \xB7 app "${appName}"`,
|
|
7924
8333
|
appPath,
|
|
7925
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
8334
|
+
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest7(appName, universe.apps.keys()),
|
|
7926
8335
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
7927
8336
|
);
|
|
7928
8337
|
continue;
|
|
7929
8338
|
}
|
|
7930
|
-
if (!
|
|
8339
|
+
if (!isRec21(rawApp)) continue;
|
|
7931
8340
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
7932
8341
|
if (navIds.has(navId)) continue;
|
|
7933
8342
|
orphan(
|
|
7934
8343
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
7935
8344
|
`${appPath}.navigation.${navId}`,
|
|
7936
|
-
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` +
|
|
8345
|
+
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest7(navId, navIds),
|
|
7937
8346
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
7938
8347
|
);
|
|
7939
8348
|
}
|
|
@@ -7945,18 +8354,18 @@ function validateTranslationReferences(stack) {
|
|
|
7945
8354
|
orphan(
|
|
7946
8355
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
7947
8356
|
dashPath,
|
|
7948
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
8357
|
+
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest7(dashName, universe.dashboards.keys()),
|
|
7949
8358
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
7950
8359
|
);
|
|
7951
8360
|
continue;
|
|
7952
8361
|
}
|
|
7953
|
-
if (!
|
|
8362
|
+
if (!isRec21(rawDash)) continue;
|
|
7954
8363
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
7955
8364
|
if (dash.widgets.has(widgetId)) continue;
|
|
7956
8365
|
orphan(
|
|
7957
8366
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
7958
8367
|
`${dashPath}.widgets.${widgetId}`,
|
|
7959
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
8368
|
+
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest7(widgetId, dash.widgets),
|
|
7960
8369
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
7961
8370
|
);
|
|
7962
8371
|
}
|
|
@@ -7965,7 +8374,7 @@ function validateTranslationReferences(stack) {
|
|
|
7965
8374
|
orphan(
|
|
7966
8375
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
7967
8376
|
`${dashPath}.actions.${actionKey}`,
|
|
7968
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
8377
|
+
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest7(actionKey, dash.actions),
|
|
7969
8378
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
7970
8379
|
);
|
|
7971
8380
|
}
|
|
@@ -7975,7 +8384,7 @@ function validateTranslationReferences(stack) {
|
|
|
7975
8384
|
return findings;
|
|
7976
8385
|
}
|
|
7977
8386
|
function asRecord(v) {
|
|
7978
|
-
return
|
|
8387
|
+
return isRec21(v) ? v : {};
|
|
7979
8388
|
}
|
|
7980
8389
|
function checkOptionKeys(findings, ctx) {
|
|
7981
8390
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -7987,7 +8396,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
7987
8396
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
7988
8397
|
where: ctx.where,
|
|
7989
8398
|
path: ctx.path,
|
|
7990
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
8399
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName17(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
7991
8400
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
7992
8401
|
});
|
|
7993
8402
|
return;
|
|
@@ -8000,17 +8409,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
8000
8409
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
8001
8410
|
where: ctx.where,
|
|
8002
8411
|
path: `${ctx.path}.${key}`,
|
|
8003
|
-
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` +
|
|
8412
|
+
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest7(key, declared.values),
|
|
8004
8413
|
hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
|
|
8005
8414
|
});
|
|
8006
8415
|
}
|
|
8007
8416
|
}
|
|
8008
8417
|
function checkActionParams(findings, ctx) {
|
|
8009
|
-
const rawParams = Object.keys(asRecord(
|
|
8418
|
+
const rawParams = Object.keys(asRecord(isRec21(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
8010
8419
|
if (rawParams.length === 0) return;
|
|
8011
8420
|
const declared = /* @__PURE__ */ new Set();
|
|
8012
8421
|
for (const param of asArray35(ctx.action.params)) {
|
|
8013
|
-
const name =
|
|
8422
|
+
const name = strName17(param.name) ?? strName17(param.field);
|
|
8014
8423
|
if (name) declared.add(name);
|
|
8015
8424
|
}
|
|
8016
8425
|
for (const paramName of rawParams) {
|
|
@@ -8020,7 +8429,7 @@ function checkActionParams(findings, ctx) {
|
|
|
8020
8429
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
8021
8430
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
8022
8431
|
path: `${ctx.path}.params.${paramName}`,
|
|
8023
|
-
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` +
|
|
8432
|
+
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest7(paramName, declared),
|
|
8024
8433
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
8025
8434
|
});
|
|
8026
8435
|
}
|
|
@@ -8028,14 +8437,14 @@ function checkActionParams(findings, ctx) {
|
|
|
8028
8437
|
|
|
8029
8438
|
// src/validate-translatable-sections.ts
|
|
8030
8439
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
8031
|
-
function
|
|
8440
|
+
function isRec22(v) {
|
|
8032
8441
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
8033
8442
|
}
|
|
8034
|
-
function
|
|
8443
|
+
function strName18(v) {
|
|
8035
8444
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8036
8445
|
}
|
|
8037
8446
|
function viewLabel(view) {
|
|
8038
|
-
const name =
|
|
8447
|
+
const name = strName18(view.name);
|
|
8039
8448
|
return name ? `view "${name}"` : "";
|
|
8040
8449
|
}
|
|
8041
8450
|
function joinWhere(...parts) {
|
|
@@ -8043,7 +8452,7 @@ function joinWhere(...parts) {
|
|
|
8043
8452
|
}
|
|
8044
8453
|
function collectViewSites(view, basePath, label2, sites) {
|
|
8045
8454
|
const recordObject = viewObjectName(view);
|
|
8046
|
-
const listBinding =
|
|
8455
|
+
const listBinding = isRec22(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
8047
8456
|
for (const site of viewContainerSites(view, basePath)) {
|
|
8048
8457
|
sites.push({
|
|
8049
8458
|
path: `${site.path}.sections`,
|
|
@@ -8057,11 +8466,11 @@ function translatedObjectNames(stack) {
|
|
|
8057
8466
|
const out = /* @__PURE__ */ new Set();
|
|
8058
8467
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
8059
8468
|
for (const bundle of bundles) {
|
|
8060
|
-
if (!
|
|
8469
|
+
if (!isRec22(bundle)) continue;
|
|
8061
8470
|
for (const data of Object.values(bundle)) {
|
|
8062
|
-
if (!
|
|
8471
|
+
if (!isRec22(data) || !isRec22(data.objects)) continue;
|
|
8063
8472
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
8064
|
-
if (
|
|
8473
|
+
if (isRec22(node)) out.add(objectName);
|
|
8065
8474
|
}
|
|
8066
8475
|
}
|
|
8067
8476
|
}
|
|
@@ -8073,22 +8482,22 @@ function suggestedName(label2) {
|
|
|
8073
8482
|
}
|
|
8074
8483
|
function validateTranslatableSections(stack) {
|
|
8075
8484
|
const findings = [];
|
|
8076
|
-
if (!
|
|
8485
|
+
if (!isRec22(stack)) return findings;
|
|
8077
8486
|
const translated = translatedObjectNames(stack);
|
|
8078
8487
|
if (translated.size === 0) return findings;
|
|
8079
8488
|
const sites = [];
|
|
8080
8489
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
8081
|
-
const objectName =
|
|
8490
|
+
const objectName = strName18(obj.name);
|
|
8082
8491
|
if (!objectName) continue;
|
|
8083
8492
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
8084
8493
|
collectViewSites(
|
|
8085
|
-
{ ...view, object:
|
|
8494
|
+
{ ...view, object: strName18(view.object) ?? objectName },
|
|
8086
8495
|
path,
|
|
8087
8496
|
viewLabel(view),
|
|
8088
8497
|
sites
|
|
8089
8498
|
);
|
|
8090
8499
|
}
|
|
8091
|
-
if (
|
|
8500
|
+
if (isRec22(obj.listViews)) {
|
|
8092
8501
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
8093
8502
|
}
|
|
8094
8503
|
}
|
|
@@ -8096,13 +8505,13 @@ function validateTranslatableSections(stack) {
|
|
|
8096
8505
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
8097
8506
|
}
|
|
8098
8507
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
8099
|
-
const pageName =
|
|
8508
|
+
const pageName = strName18(page.name);
|
|
8100
8509
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
8101
8510
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
8102
8511
|
if (!walked.objectName) continue;
|
|
8103
|
-
const props =
|
|
8512
|
+
const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
|
|
8104
8513
|
if (!props) continue;
|
|
8105
|
-
const type =
|
|
8514
|
+
const type = strName18(walked.component.type) ?? "component";
|
|
8106
8515
|
sites.push({
|
|
8107
8516
|
path: `${walked.path}.properties.sections`,
|
|
8108
8517
|
surface: joinWhere(pageLabel, type),
|
|
@@ -8117,9 +8526,9 @@ function validateTranslatableSections(stack) {
|
|
|
8117
8526
|
if (!Array.isArray(site.sections)) continue;
|
|
8118
8527
|
for (let i = 0; i < site.sections.length; i++) {
|
|
8119
8528
|
const section = site.sections[i];
|
|
8120
|
-
if (!
|
|
8121
|
-
if (
|
|
8122
|
-
const heading =
|
|
8529
|
+
if (!isRec22(section)) continue;
|
|
8530
|
+
if (strName18(section.name)) continue;
|
|
8531
|
+
const heading = strName18(section.label);
|
|
8123
8532
|
if (!heading) continue;
|
|
8124
8533
|
const slug = suggestedName(heading);
|
|
8125
8534
|
findings.push({
|
|
@@ -8144,7 +8553,7 @@ function asArray36(v) {
|
|
|
8144
8553
|
}
|
|
8145
8554
|
return [];
|
|
8146
8555
|
}
|
|
8147
|
-
function
|
|
8556
|
+
function strName19(v) {
|
|
8148
8557
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8149
8558
|
}
|
|
8150
8559
|
function surfaceOf(v) {
|
|
@@ -8155,17 +8564,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
8155
8564
|
if (!stack || typeof stack !== "object") return findings;
|
|
8156
8565
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
8157
8566
|
for (const skill of asArray36(stack.skills)) {
|
|
8158
|
-
const n =
|
|
8567
|
+
const n = strName19(skill.name);
|
|
8159
8568
|
if (n) skillsByName.set(n, skill);
|
|
8160
8569
|
}
|
|
8161
8570
|
const agents = asArray36(stack.agents);
|
|
8162
8571
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
8163
8572
|
const agent = agents[ai];
|
|
8164
|
-
const agentName =
|
|
8573
|
+
const agentName = strName19(agent.name) ?? `#${ai}`;
|
|
8165
8574
|
const agentSurface = surfaceOf(agent.surface);
|
|
8166
8575
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
8167
8576
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
8168
|
-
const ref =
|
|
8577
|
+
const ref = strName19(skillRefs[si]);
|
|
8169
8578
|
if (!ref) continue;
|
|
8170
8579
|
const skill = skillsByName.get(ref);
|
|
8171
8580
|
if (!skill) continue;
|
|
@@ -8194,10 +8603,10 @@ function asArray37(v) {
|
|
|
8194
8603
|
}
|
|
8195
8604
|
return [];
|
|
8196
8605
|
}
|
|
8197
|
-
function
|
|
8606
|
+
function strName20(v) {
|
|
8198
8607
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8199
8608
|
}
|
|
8200
|
-
function
|
|
8609
|
+
function distance7(a, b) {
|
|
8201
8610
|
const m = a.length;
|
|
8202
8611
|
const n = b.length;
|
|
8203
8612
|
if (m === 0) return n;
|
|
@@ -8213,14 +8622,14 @@ function distance6(a, b) {
|
|
|
8213
8622
|
}
|
|
8214
8623
|
return prev[n];
|
|
8215
8624
|
}
|
|
8216
|
-
function
|
|
8625
|
+
function suggest8(target, known) {
|
|
8217
8626
|
for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
8218
8627
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
8219
8628
|
}
|
|
8220
8629
|
let best;
|
|
8221
8630
|
let bestScore = Infinity;
|
|
8222
8631
|
for (const candidate of known) {
|
|
8223
|
-
const d =
|
|
8632
|
+
const d = distance7(target, candidate);
|
|
8224
8633
|
if (d < bestScore) {
|
|
8225
8634
|
bestScore = d;
|
|
8226
8635
|
best = candidate;
|
|
@@ -8235,8 +8644,8 @@ function materialisesAsTool(action) {
|
|
|
8235
8644
|
if (!ai || typeof ai !== "object") return false;
|
|
8236
8645
|
const aiRec = ai;
|
|
8237
8646
|
if (aiRec.exposed !== true) return false;
|
|
8238
|
-
if (!
|
|
8239
|
-
const type =
|
|
8647
|
+
if (!strName20(aiRec.description)) return false;
|
|
8648
|
+
const type = strName20(action.type);
|
|
8240
8649
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
8241
8650
|
if (type === "script") return Boolean(action.target || action.body);
|
|
8242
8651
|
return Boolean(action.target);
|
|
@@ -8244,12 +8653,12 @@ function materialisesAsTool(action) {
|
|
|
8244
8653
|
function collectToolUniverse(stack) {
|
|
8245
8654
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
8246
8655
|
for (const tool of asArray37(stack.tools)) {
|
|
8247
|
-
const n =
|
|
8656
|
+
const n = strName20(tool.name);
|
|
8248
8657
|
if (n) universe.add(n);
|
|
8249
8658
|
}
|
|
8250
8659
|
const addActionFamily = (actions) => {
|
|
8251
8660
|
for (const action of asArray37(actions)) {
|
|
8252
|
-
const n =
|
|
8661
|
+
const n = strName20(action.name);
|
|
8253
8662
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
8254
8663
|
}
|
|
8255
8664
|
};
|
|
@@ -8263,7 +8672,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
8263
8672
|
const names = /* @__PURE__ */ new Set();
|
|
8264
8673
|
const scan = (actions) => {
|
|
8265
8674
|
for (const action of asArray37(actions)) {
|
|
8266
|
-
const n =
|
|
8675
|
+
const n = strName20(action.name);
|
|
8267
8676
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
8268
8677
|
}
|
|
8269
8678
|
};
|
|
@@ -8289,10 +8698,10 @@ function validateAiToolReferences(stack) {
|
|
|
8289
8698
|
const skills = asArray37(stack.skills);
|
|
8290
8699
|
for (let si = 0; si < skills.length; si++) {
|
|
8291
8700
|
const skill = skills[si];
|
|
8292
|
-
const skillName =
|
|
8701
|
+
const skillName = strName20(skill.name) ?? `#${si}`;
|
|
8293
8702
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
8294
8703
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
8295
|
-
const ref =
|
|
8704
|
+
const ref = strName20(refs[ti]);
|
|
8296
8705
|
if (!ref || resolves(ref)) continue;
|
|
8297
8706
|
const isPattern = ref.endsWith("*");
|
|
8298
8707
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -8301,7 +8710,7 @@ function validateAiToolReferences(stack) {
|
|
|
8301
8710
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
8302
8711
|
where: `skill "${skillName}" \xB7 tools`,
|
|
8303
8712
|
path: `skills[${si}].tools[${ti}]`,
|
|
8304
|
-
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` +
|
|
8713
|
+
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest8(ref, universe),
|
|
8305
8714
|
hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${import_system5.PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
|
|
8306
8715
|
});
|
|
8307
8716
|
}
|
|
@@ -8319,7 +8728,7 @@ function asArray38(v) {
|
|
|
8319
8728
|
}
|
|
8320
8729
|
return [];
|
|
8321
8730
|
}
|
|
8322
|
-
function
|
|
8731
|
+
function strName21(v) {
|
|
8323
8732
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8324
8733
|
}
|
|
8325
8734
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -8329,7 +8738,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
8329
8738
|
const agents = asArray38(stack.agents);
|
|
8330
8739
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
8331
8740
|
const agent = agents[ai];
|
|
8332
|
-
const name =
|
|
8741
|
+
const name = strName21(agent.name) ?? `#${ai}`;
|
|
8333
8742
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
8334
8743
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
8335
8744
|
findings.push({
|
|
@@ -8345,9 +8754,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
8345
8754
|
const apps = asArray38(stack.apps);
|
|
8346
8755
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
8347
8756
|
const app = apps[appIdx];
|
|
8348
|
-
const defaultAgent =
|
|
8757
|
+
const defaultAgent = strName21(app.defaultAgent);
|
|
8349
8758
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
8350
|
-
const appName =
|
|
8759
|
+
const appName = strName21(app.name) ?? `#${appIdx}`;
|
|
8351
8760
|
findings.push({
|
|
8352
8761
|
severity: "warning",
|
|
8353
8762
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -8378,6 +8787,7 @@ function loadTypeScript3() {
|
|
|
8378
8787
|
return cachedTs3;
|
|
8379
8788
|
}
|
|
8380
8789
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
8790
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
8381
8791
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
8382
8792
|
{
|
|
8383
8793
|
id: "input-property-assign",
|
|
@@ -8450,13 +8860,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
8450
8860
|
"owner",
|
|
8451
8861
|
"record_type"
|
|
8452
8862
|
]);
|
|
8453
|
-
|
|
8863
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
8864
|
+
return `so the value can never land: the anchor exists only in the registered schema, which is what carries it PAST the write-path validator that refuses an undeclared name outright (INVALID_FIELD). The remote database is what rejects it \u2014 on a SQL remote with an untyped driver error ('no such column') that aborts the whole statement, so the correctly named fields in the same payload never land either; on a schemaless remote the key is persisted into a column no read surface returns (#4271).`;
|
|
8865
|
+
}
|
|
8866
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8454
8867
|
function asArray39(v) {
|
|
8455
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8456
|
-
if (
|
|
8868
|
+
if (Array.isArray(v)) return v.filter((x) => isRec23(x));
|
|
8869
|
+
if (isRec23(v)) {
|
|
8457
8870
|
return Object.entries(v).map(([name, def]) => ({
|
|
8458
8871
|
name,
|
|
8459
|
-
...
|
|
8872
|
+
...isRec23(def) ? def : {}
|
|
8460
8873
|
}));
|
|
8461
8874
|
}
|
|
8462
8875
|
return [];
|
|
@@ -8600,14 +9013,16 @@ function validateHookBodyWrites(stack) {
|
|
|
8600
9013
|
const hooks = asArray39(stack.hooks);
|
|
8601
9014
|
if (hooks.length === 0) return findings;
|
|
8602
9015
|
let objectFields = null;
|
|
9016
|
+
let anchors = null;
|
|
8603
9017
|
hooks.forEach((hook, hookIndex) => {
|
|
8604
9018
|
const body = hook.body;
|
|
8605
|
-
if (!
|
|
9019
|
+
if (!isRec23(body) || body.language !== "js") return;
|
|
8606
9020
|
const source = body.source;
|
|
8607
9021
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8608
9022
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
8609
9023
|
if (writes.length === 0) return;
|
|
8610
9024
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
9025
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8611
9026
|
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
8612
9027
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
8613
9028
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
@@ -8622,7 +9037,20 @@ function validateHookBodyWrites(stack) {
|
|
|
8622
9037
|
if (reported.has(dedupeKey)) continue;
|
|
8623
9038
|
if (w.object === void 0) {
|
|
8624
9039
|
if (!inputJudgeable) continue;
|
|
8625
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
9040
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9041
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
9042
|
+
reported.add(dedupeKey);
|
|
9043
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
9044
|
+
findings.push({
|
|
9045
|
+
severity: "warning",
|
|
9046
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9047
|
+
where,
|
|
9048
|
+
path,
|
|
9049
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
9050
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
9051
|
+
});
|
|
9052
|
+
continue;
|
|
9053
|
+
}
|
|
8626
9054
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
8627
9055
|
reported.add(dedupeKey);
|
|
8628
9056
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -8638,7 +9066,20 @@ function validateHookBodyWrites(stack) {
|
|
|
8638
9066
|
} else {
|
|
8639
9067
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
8640
9068
|
if (!known) continue;
|
|
8641
|
-
if (
|
|
9069
|
+
if (known.has(w.field)) continue;
|
|
9070
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9071
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
9072
|
+
reported.add(dedupeKey);
|
|
9073
|
+
findings.push({
|
|
9074
|
+
severity: "warning",
|
|
9075
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9076
|
+
where,
|
|
9077
|
+
path,
|
|
9078
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
9079
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
9080
|
+
});
|
|
9081
|
+
continue;
|
|
9082
|
+
}
|
|
8642
9083
|
reported.add(dedupeKey);
|
|
8643
9084
|
findings.push({
|
|
8644
9085
|
severity: "warning",
|
|
@@ -8667,6 +9108,7 @@ function fixHint(field, declared) {
|
|
|
8667
9108
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
8668
9109
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
8669
9110
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
9111
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
8670
9112
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
8671
9113
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
8672
9114
|
var ACTION_BODY_WRITE_EXCLUSIONS = [
|
|
@@ -8683,13 +9125,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
8683
9125
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
8684
9126
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
8685
9127
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
8686
|
-
var
|
|
9128
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8687
9129
|
function asArray40(v) {
|
|
8688
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8689
|
-
if (
|
|
9130
|
+
if (Array.isArray(v)) return v.filter((x) => isRec24(x));
|
|
9131
|
+
if (isRec24(v)) {
|
|
8690
9132
|
return Object.entries(v).map(([name, def]) => ({
|
|
8691
9133
|
name,
|
|
8692
|
-
...
|
|
9134
|
+
...isRec24(def) ? def : {}
|
|
8693
9135
|
}));
|
|
8694
9136
|
}
|
|
8695
9137
|
return [];
|
|
@@ -8707,7 +9149,7 @@ function collectActionBodies(stack) {
|
|
|
8707
9149
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
8708
9150
|
if (type !== "script") return;
|
|
8709
9151
|
const body = action.body;
|
|
8710
|
-
if (!
|
|
9152
|
+
if (!isRec24(body) || body.language !== "js") return;
|
|
8711
9153
|
const source = body.source;
|
|
8712
9154
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8713
9155
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -8726,10 +9168,11 @@ function collectActionBodies(stack) {
|
|
|
8726
9168
|
}
|
|
8727
9169
|
function validateActionBodyWrites(stack) {
|
|
8728
9170
|
const findings = [];
|
|
8729
|
-
if (!
|
|
9171
|
+
if (!isRec24(stack)) return findings;
|
|
8730
9172
|
const sites = collectActionBodies(stack);
|
|
8731
9173
|
if (sites.length === 0) return findings;
|
|
8732
9174
|
let objectFields = null;
|
|
9175
|
+
let anchors = null;
|
|
8733
9176
|
for (const site of sites) {
|
|
8734
9177
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
8735
9178
|
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
@@ -8754,6 +9197,7 @@ function validateActionBodyWrites(stack) {
|
|
|
8754
9197
|
}
|
|
8755
9198
|
if (writes.length === 0) continue;
|
|
8756
9199
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
9200
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8757
9201
|
const reported = /* @__PURE__ */ new Set();
|
|
8758
9202
|
for (const w of writes) {
|
|
8759
9203
|
if (w.object === void 0) continue;
|
|
@@ -8761,7 +9205,20 @@ function validateActionBodyWrites(stack) {
|
|
|
8761
9205
|
if (reported.has(dedupeKey)) continue;
|
|
8762
9206
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
8763
9207
|
if (!known) continue;
|
|
8764
|
-
if (
|
|
9208
|
+
if (known.has(w.field)) continue;
|
|
9209
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9210
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
9211
|
+
reported.add(dedupeKey);
|
|
9212
|
+
findings.push({
|
|
9213
|
+
severity: "warning",
|
|
9214
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9215
|
+
where,
|
|
9216
|
+
path: site.path,
|
|
9217
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
9218
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
9219
|
+
});
|
|
9220
|
+
continue;
|
|
9221
|
+
}
|
|
8765
9222
|
reported.add(dedupeKey);
|
|
8766
9223
|
findings.push({
|
|
8767
9224
|
severity: "warning",
|
|
@@ -8783,15 +9240,16 @@ function fixHint2(field, declared) {
|
|
|
8783
9240
|
// src/validate-flow-node-writes.ts
|
|
8784
9241
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
8785
9242
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
9243
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
8786
9244
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
8787
9245
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
8788
|
-
var
|
|
9246
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8789
9247
|
function asArray41(v) {
|
|
8790
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8791
|
-
if (
|
|
9248
|
+
if (Array.isArray(v)) return v.filter((x) => isRec25(x));
|
|
9249
|
+
if (isRec25(v)) {
|
|
8792
9250
|
return Object.entries(v).map(([name, def]) => ({
|
|
8793
9251
|
name,
|
|
8794
|
-
...
|
|
9252
|
+
...isRec25(def) ? def : {}
|
|
8795
9253
|
}));
|
|
8796
9254
|
}
|
|
8797
9255
|
return [];
|
|
@@ -8804,30 +9262,44 @@ function readLiteralObjectName2(config) {
|
|
|
8804
9262
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
8805
9263
|
function validateFlowNodeWrites(stack) {
|
|
8806
9264
|
const findings = [];
|
|
8807
|
-
if (!
|
|
9265
|
+
if (!isRec25(stack)) return findings;
|
|
8808
9266
|
const flows = asArray41(stack.flows);
|
|
8809
9267
|
if (flows.length === 0) return findings;
|
|
8810
9268
|
let objectFields = null;
|
|
9269
|
+
let anchors = null;
|
|
8811
9270
|
flows.forEach((flow, flowIndex) => {
|
|
8812
9271
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
8813
9272
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
8814
9273
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
8815
9274
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
8816
|
-
const config =
|
|
9275
|
+
const config = isRec25(node.config) ? node.config : void 0;
|
|
8817
9276
|
if (!config) return;
|
|
8818
9277
|
const fields = config.fields;
|
|
8819
|
-
if (!
|
|
9278
|
+
if (!isRec25(fields)) return;
|
|
8820
9279
|
const written = Object.keys(fields);
|
|
8821
9280
|
if (written.length === 0) return;
|
|
8822
9281
|
const objectName = readLiteralObjectName2(config);
|
|
8823
9282
|
if (!objectName) return;
|
|
8824
9283
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
9284
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8825
9285
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
8826
9286
|
if (!known) return;
|
|
8827
9287
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
8828
9288
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
8829
9289
|
for (const fieldName of written) {
|
|
8830
|
-
if (known.has(fieldName)
|
|
9290
|
+
if (known.has(fieldName)) continue;
|
|
9291
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
9292
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
9293
|
+
findings.push({
|
|
9294
|
+
severity: "warning",
|
|
9295
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
9296
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
9297
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
9298
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
9299
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
9300
|
+
});
|
|
9301
|
+
continue;
|
|
9302
|
+
}
|
|
8831
9303
|
if (fieldName.includes(".")) continue;
|
|
8832
9304
|
findings.push({
|
|
8833
9305
|
severity: "error",
|
|
@@ -8851,6 +9323,13 @@ function fixHint3(field, declared) {
|
|
|
8851
9323
|
var REFERENCE_INTEGRITY_RULES = [
|
|
8852
9324
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
8853
9325
|
{ name: "validateSearchableFields", run: validateSearchableFields },
|
|
9326
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
9327
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
9328
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
9329
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
9330
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
9331
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
9332
|
+
{ name: "validateSortableFields", run: validateSortableFields },
|
|
8854
9333
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
8855
9334
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
8856
9335
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -8966,7 +9445,7 @@ function validateReferenceIntegrity(stack) {
|
|
|
8966
9445
|
|
|
8967
9446
|
// src/lint-flow-patterns.ts
|
|
8968
9447
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8969
|
-
var
|
|
9448
|
+
var import_data12 = require("@objectstack/spec/data");
|
|
8970
9449
|
function asArray42(v) {
|
|
8971
9450
|
if (Array.isArray(v)) return v;
|
|
8972
9451
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -9241,7 +9720,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
9241
9720
|
function filterCarriesNoCondition(filter) {
|
|
9242
9721
|
if (filter === void 0 || filter === null) return true;
|
|
9243
9722
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
9244
|
-
return (0,
|
|
9723
|
+
return (0, import_data12.reduceFilterVerdict)(filter) === "true";
|
|
9245
9724
|
}
|
|
9246
9725
|
function describeUnboundedFilter(filter) {
|
|
9247
9726
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -9608,7 +10087,7 @@ function lintLivenessProperties(stack) {
|
|
|
9608
10087
|
}
|
|
9609
10088
|
|
|
9610
10089
|
// src/lint-autonumber-formats.ts
|
|
9611
|
-
var
|
|
10090
|
+
var import_data13 = require("@objectstack/spec/data");
|
|
9612
10091
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
9613
10092
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
9614
10093
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
@@ -9634,8 +10113,8 @@ function lintAutonumberFormats(stack) {
|
|
|
9634
10113
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
9635
10114
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
9636
10115
|
if (!fmt) continue;
|
|
9637
|
-
const tokens = (0,
|
|
9638
|
-
const refs = (0,
|
|
10116
|
+
const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
|
|
10117
|
+
const refs = (0, import_data13.referencedFields)(tokens);
|
|
9639
10118
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
9640
10119
|
for (const t of tokens) {
|
|
9641
10120
|
if (t.kind !== "literal") continue;
|
|
@@ -9790,6 +10269,17 @@ function lintViewRefs(stack) {
|
|
|
9790
10269
|
}
|
|
9791
10270
|
|
|
9792
10271
|
// src/data-model-rules.ts
|
|
10272
|
+
function objectWhere(obj) {
|
|
10273
|
+
return `object "${obj?.name}"`;
|
|
10274
|
+
}
|
|
10275
|
+
function indexWhere(obj, idx, j, cols) {
|
|
10276
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
10277
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
10278
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
10279
|
+
}
|
|
10280
|
+
function fieldWhere(obj, fieldName) {
|
|
10281
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
10282
|
+
}
|
|
9793
10283
|
var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
|
|
9794
10284
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
9795
10285
|
"number",
|
|
@@ -9862,11 +10352,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9862
10352
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9863
10353
|
const idx = declaredIndexes[j];
|
|
9864
10354
|
if (idx?.unique !== true) continue;
|
|
9865
|
-
const
|
|
10355
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
10356
|
+
const cols = colList.join(", ");
|
|
9866
10357
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9867
10358
|
issues.push({
|
|
9868
10359
|
severity: "warning",
|
|
9869
10360
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
10361
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9870
10362
|
message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
|
|
9871
10363
|
path: `objects[${i}].indexes[${j}]`,
|
|
9872
10364
|
fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
|
|
@@ -9915,6 +10407,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9915
10407
|
issues.push({
|
|
9916
10408
|
severity: "warning",
|
|
9917
10409
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
10410
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
10411
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
10412
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
10413
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
10414
|
+
where: fieldWhere(obj, name),
|
|
9918
10415
|
message,
|
|
9919
10416
|
path: `objects[${i}]`,
|
|
9920
10417
|
fix
|
|
@@ -9943,6 +10440,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9943
10440
|
issues.push({
|
|
9944
10441
|
severity: "warning",
|
|
9945
10442
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
10443
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9946
10444
|
message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
|
|
9947
10445
|
path: `objects[${i}].indexes[${j}]`,
|
|
9948
10446
|
fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
|
|
@@ -10089,7 +10587,7 @@ var CLI_ONLY = ["cli"];
|
|
|
10089
10587
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
10090
10588
|
var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
|
|
10091
10589
|
var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
|
|
10092
|
-
var
|
|
10590
|
+
var RUNTIME_OBJECT_ADVISORY_VOLUME = "Advisory-tier object rule: it cannot refuse a write, and it is held off the runtime door for advisory VOLUME (~8 findings per object write measured on unswept metadata, rendered in Studio since #4717), not refusal risk. Crossing it is a UX decision with its own card (#4716).";
|
|
10093
10591
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
10094
10592
|
var AUTHORING_RULES = [
|
|
10095
10593
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -10150,8 +10648,14 @@ var AUTHORING_RULES = [
|
|
|
10150
10648
|
input: "normalized",
|
|
10151
10649
|
commands: ALL,
|
|
10152
10650
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
10153
|
-
|
|
10154
|
-
|
|
10651
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
10652
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
10653
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
10654
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
10655
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
10656
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
10657
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10658
|
+
runtimeTypes: ["object"],
|
|
10155
10659
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
10156
10660
|
},
|
|
10157
10661
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -10175,8 +10679,11 @@ var AUTHORING_RULES = [
|
|
|
10175
10679
|
input: "normalized",
|
|
10176
10680
|
commands: ALL,
|
|
10177
10681
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
10178
|
-
|
|
10179
|
-
|
|
10682
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
10683
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
10684
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
10685
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10686
|
+
runtimeTypes: ["object"],
|
|
10180
10687
|
run: (stack) => validateManagedApiMethods(stack)
|
|
10181
10688
|
},
|
|
10182
10689
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -10198,19 +10705,38 @@ var AUTHORING_RULES = [
|
|
|
10198
10705
|
},
|
|
10199
10706
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
10200
10707
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
10708
|
+
//
|
|
10709
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
10710
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
10711
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
10712
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
10713
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
10714
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
10715
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
10716
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
10717
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
10718
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
10719
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
10720
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
10721
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
10722
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
10723
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
10724
|
+
// advisory channel and never block (#4463 P1).
|
|
10201
10725
|
{
|
|
10202
10726
|
name: "validateWidgetBindings",
|
|
10203
10727
|
tier: "gating",
|
|
10204
10728
|
input: "parsed",
|
|
10205
10729
|
commands: ALL,
|
|
10206
10730
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
10207
|
-
surfaces:
|
|
10208
|
-
|
|
10731
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10732
|
+
runtimeTypes: ["dashboard"],
|
|
10209
10733
|
run: (stack) => validateWidgetBindings(stack)
|
|
10210
10734
|
},
|
|
10211
|
-
// ADR-0049 / #3367 — a header
|
|
10212
|
-
//
|
|
10213
|
-
//
|
|
10735
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
10736
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
10737
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
10738
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
10739
|
+
// Unresolved `url` routes stay advisory.
|
|
10214
10740
|
{
|
|
10215
10741
|
name: "validateDashboardActionRefs",
|
|
10216
10742
|
tier: "gating",
|
|
@@ -10235,6 +10761,28 @@ var AUTHORING_RULES = [
|
|
|
10235
10761
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10236
10762
|
run: (stack) => validateFilterTokens(stack)
|
|
10237
10763
|
},
|
|
10764
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
10765
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
10766
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
10767
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
10768
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
10769
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
10770
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
10771
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
10772
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
10773
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
10774
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
10775
|
+
// vocabulary is near enough to reach for.
|
|
10776
|
+
{
|
|
10777
|
+
name: "validatePresetComparands",
|
|
10778
|
+
tier: "gating",
|
|
10779
|
+
input: "parsed",
|
|
10780
|
+
commands: ALL,
|
|
10781
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
10782
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10783
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
10784
|
+
run: (stack) => validatePresetComparands(stack)
|
|
10785
|
+
},
|
|
10238
10786
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
10239
10787
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
10240
10788
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -10453,7 +11001,7 @@ var AUTHORING_RULES = [
|
|
|
10453
11001
|
commands: ALL,
|
|
10454
11002
|
source: "packages/lint/src/validate-record-title.ts",
|
|
10455
11003
|
surfaces: CLI_ONLY,
|
|
10456
|
-
surfaceReason:
|
|
11004
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10457
11005
|
run: (stack) => validateRecordTitle(stack)
|
|
10458
11006
|
},
|
|
10459
11007
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -10466,7 +11014,7 @@ var AUTHORING_RULES = [
|
|
|
10466
11014
|
commands: ALL,
|
|
10467
11015
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
10468
11016
|
surfaces: CLI_ONLY,
|
|
10469
|
-
surfaceReason:
|
|
11017
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10470
11018
|
run: (stack) => validateSemanticRoles(stack)
|
|
10471
11019
|
},
|
|
10472
11020
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -10677,7 +11225,7 @@ var AUTHORING_RULES = [
|
|
|
10677
11225
|
commands: ALL,
|
|
10678
11226
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
10679
11227
|
surfaces: CLI_ONLY,
|
|
10680
|
-
surfaceReason:
|
|
11228
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10681
11229
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
10682
11230
|
severity: "warning",
|
|
10683
11231
|
rule: f.rule,
|
|
@@ -10696,8 +11244,12 @@ var AUTHORING_RULES = [
|
|
|
10696
11244
|
input: "parsed",
|
|
10697
11245
|
commands: ALL,
|
|
10698
11246
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
10699
|
-
|
|
10700
|
-
|
|
11247
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
11248
|
+
// object does not carry is broken from the first record; only the error
|
|
11249
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
11250
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
11251
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11252
|
+
runtimeTypes: ["object"],
|
|
10701
11253
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
10702
11254
|
severity: f.severity,
|
|
10703
11255
|
rule: f.rule,
|
|
@@ -10738,12 +11290,12 @@ var AUTHORING_RULES = [
|
|
|
10738
11290
|
commands: ["validate", "build"],
|
|
10739
11291
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10740
11292
|
surfaces: CLI_ONLY,
|
|
10741
|
-
surfaceReason:
|
|
11293
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10742
11294
|
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
10743
11295
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10744
11296
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10745
11297
|
rule: f.rule,
|
|
10746
|
-
where: f.
|
|
11298
|
+
where: f.where,
|
|
10747
11299
|
path: f.path,
|
|
10748
11300
|
message: f.message,
|
|
10749
11301
|
hint: f.fix ?? ""
|
|
@@ -10759,12 +11311,12 @@ var AUTHORING_RULES = [
|
|
|
10759
11311
|
commands: ["validate", "build"],
|
|
10760
11312
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10761
11313
|
surfaces: CLI_ONLY,
|
|
10762
|
-
surfaceReason:
|
|
11314
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10763
11315
|
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
10764
11316
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10765
11317
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10766
11318
|
rule: f.rule,
|
|
10767
|
-
where: f.
|
|
11319
|
+
where: f.where,
|
|
10768
11320
|
path: f.path,
|
|
10769
11321
|
message: f.message,
|
|
10770
11322
|
hint: f.fix ?? ""
|
|
@@ -10781,12 +11333,12 @@ var AUTHORING_RULES = [
|
|
|
10781
11333
|
commands: ["validate", "build"],
|
|
10782
11334
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10783
11335
|
surfaces: CLI_ONLY,
|
|
10784
|
-
surfaceReason:
|
|
11336
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10785
11337
|
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
10786
11338
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10787
11339
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10788
11340
|
rule: f.rule,
|
|
10789
|
-
where: f.
|
|
11341
|
+
where: f.where,
|
|
10790
11342
|
path: f.path,
|
|
10791
11343
|
message: f.message,
|
|
10792
11344
|
hint: f.fix ?? ""
|
|
@@ -10914,16 +11466,26 @@ var AUTHORING_RULES = [
|
|
|
10914
11466
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10915
11467
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10916
11468
|
},
|
|
10917
|
-
// #4698 — the "declared but never read" gate, for the
|
|
10918
|
-
// predicate is EXACT rather than inferred.
|
|
10919
|
-
//
|
|
10920
|
-
//
|
|
10921
|
-
//
|
|
10922
|
-
//
|
|
10923
|
-
//
|
|
10924
|
-
//
|
|
10925
|
-
//
|
|
10926
|
-
//
|
|
11469
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
11470
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
11471
|
+
//
|
|
11472
|
+
// - `condition` has a single runtime consumer
|
|
11473
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
11474
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
11475
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
11476
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
11477
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
11478
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
11479
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
11480
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
11481
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
11482
|
+
//
|
|
11483
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
11484
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
11485
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
11486
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
11487
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
11488
|
+
// yet true.
|
|
10927
11489
|
{
|
|
10928
11490
|
name: "validateSharingRuleEnforceability",
|
|
10929
11491
|
tier: "gating",
|
|
@@ -10931,7 +11493,7 @@ var AUTHORING_RULES = [
|
|
|
10931
11493
|
commands: ALL,
|
|
10932
11494
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10933
11495
|
surfaces: CLI_ONLY,
|
|
10934
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
11496
|
+
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. This entry used to add that the rule reads ONLY `stack.sharingRules[].condition` and needs no other collection, so crossing was a lone `runtimeTypes` edit. #9698 FALSIFIED that: the anchor arm resolves `sharingRules[].object` against `stack.objects` to read the anchor's OWD, so the rule is now cross-collection. `objects` IS carried by the per-write snapshot (`CONTEXT_STACK_KEYS`, #8309), so the remaining gap is unchanged in SHAPE \u2014 the gate must accept a `sharing_rule` type and the snapshot must carry `sharingRules`, which it does not \u2014 but it is now TWO collections, not one. Crossing with `sharingRules` uncarried would enforce this id for zero of its inputs while the entry claimed the door (#7220). Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
10935
11497
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10936
11498
|
},
|
|
10937
11499
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10970,8 +11532,15 @@ var AUTHORING_RULES = [
|
|
|
10970
11532
|
input: "parsed",
|
|
10971
11533
|
commands: ALL,
|
|
10972
11534
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10973
|
-
|
|
10974
|
-
|
|
11535
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
11536
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
11537
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
11538
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
11539
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
11540
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
11541
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
11542
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11543
|
+
runtimeTypes: ["object"],
|
|
10975
11544
|
run: (stack) => validateRuleCompilability(stack)
|
|
10976
11545
|
},
|
|
10977
11546
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10992,8 +11561,12 @@ var AUTHORING_RULES = [
|
|
|
10992
11561
|
input: "parsed",
|
|
10993
11562
|
commands: ALL,
|
|
10994
11563
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10995
|
-
|
|
10996
|
-
|
|
11564
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
11565
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
11566
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
11567
|
+
// format set is only enumerated once a schema actually names a format.
|
|
11568
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11569
|
+
runtimeTypes: ["object"],
|
|
10997
11570
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10998
11571
|
}
|
|
10999
11572
|
];
|
|
@@ -11053,7 +11626,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
11053
11626
|
permission: "permissions",
|
|
11054
11627
|
book: "books"
|
|
11055
11628
|
};
|
|
11056
|
-
|
|
11629
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
11630
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
11631
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
11632
|
+
return objects.filter((entry) => {
|
|
11633
|
+
if (!entry || typeof entry !== "object") return true;
|
|
11634
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
11635
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
11636
|
+
if (reachable.has(owner)) return true;
|
|
11637
|
+
return isSystemObject(entry);
|
|
11638
|
+
});
|
|
11639
|
+
}
|
|
11640
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
11641
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
11642
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
11057
11643
|
function runtimeAuthoringRulesFor(type) {
|
|
11058
11644
|
return AUTHORING_RULES.filter(
|
|
11059
11645
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -11079,7 +11665,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
11079
11665
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
11080
11666
|
const baseline = {};
|
|
11081
11667
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
11082
|
-
const
|
|
11668
|
+
const raw = args.context?.[key] ?? [];
|
|
11669
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
11083
11670
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
11084
11671
|
}
|
|
11085
11672
|
const candidate = {
|
|
@@ -11113,7 +11700,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11113
11700
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
11114
11701
|
type: args.type,
|
|
11115
11702
|
item: args.item,
|
|
11116
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
11703
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
11704
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
11117
11705
|
});
|
|
11118
11706
|
if (!snapshots) return empty;
|
|
11119
11707
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
@@ -11131,6 +11719,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11131
11719
|
ACTION_BODY_WRITE_PATTERNS,
|
|
11132
11720
|
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
11133
11721
|
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
11722
|
+
ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
11134
11723
|
ACTION_NAME_UNDEFINED,
|
|
11135
11724
|
ACTION_NO_PLACEMENT,
|
|
11136
11725
|
ACTION_RECORD_WRITE_DISCARDED,
|
|
@@ -11176,6 +11765,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11176
11765
|
FIELD_GROUP_UNDECLARED,
|
|
11177
11766
|
FILTER_EMPTY_COMBINATOR,
|
|
11178
11767
|
FILTER_EMPTY_NODE,
|
|
11768
|
+
FILTER_PRESET_COMPARAND,
|
|
11179
11769
|
FILTER_TOKEN_UNKNOWN,
|
|
11180
11770
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
11181
11771
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -11193,6 +11783,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11193
11783
|
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
11194
11784
|
FLOW_MULTI_WRITE_UNFILTERED,
|
|
11195
11785
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
11786
|
+
FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
11196
11787
|
FLOW_PHANTOM_AGGREGATION,
|
|
11197
11788
|
FLOW_RUNAS_UNSCOPED,
|
|
11198
11789
|
FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
@@ -11214,6 +11805,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11214
11805
|
HOOK_BODY_WRITE_PATTERNS,
|
|
11215
11806
|
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
11216
11807
|
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
11808
|
+
HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
11217
11809
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
11218
11810
|
LIVENESS_DEAD_PROPERTY,
|
|
11219
11811
|
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
@@ -11249,6 +11841,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11249
11841
|
RUNTIME_AJV_OPTIONS,
|
|
11250
11842
|
SEAL_MARKERS,
|
|
11251
11843
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
11844
|
+
SEARCHABLE_FIELD_UNPROVISIONED,
|
|
11252
11845
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
11253
11846
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
11254
11847
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
@@ -11267,8 +11860,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11267
11860
|
SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
11268
11861
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
11269
11862
|
SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
11863
|
+
SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
11864
|
+
SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
11270
11865
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
11271
11866
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
11867
|
+
SORT_FIELD_UNKNOWN,
|
|
11868
|
+
SORT_FIELD_UNSORTABLE,
|
|
11272
11869
|
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
11273
11870
|
STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
11274
11871
|
STARTUP_VERDICT_HINT,
|
|
@@ -11305,6 +11902,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11305
11902
|
authoringRulesFor,
|
|
11306
11903
|
buildAccessMatrix,
|
|
11307
11904
|
buildRuntimeWriteSnapshots,
|
|
11905
|
+
checkSortDeclaration,
|
|
11308
11906
|
diffAccessMatrix,
|
|
11309
11907
|
extractHookBodyWriteSet,
|
|
11310
11908
|
extractHookBodyWrites,
|
|
@@ -11319,6 +11917,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11319
11917
|
lintUniqueDeclarations,
|
|
11320
11918
|
lintUnscopedDeclaredIndexes,
|
|
11321
11919
|
lintViewRefs,
|
|
11920
|
+
narrowObjectsToPackageClosure,
|
|
11322
11921
|
nearestRegisteredFormat,
|
|
11323
11922
|
nullGuardMessage,
|
|
11324
11923
|
runAuthoringRules,
|
|
@@ -11357,6 +11956,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11357
11956
|
validatePageFieldBindings,
|
|
11358
11957
|
validatePageSourceStyling,
|
|
11359
11958
|
validatePredicatePathRefs,
|
|
11959
|
+
validatePresetComparands,
|
|
11360
11960
|
validateReactPageProps,
|
|
11361
11961
|
validateReactPages,
|
|
11362
11962
|
validateReadonlyFlowWrites,
|
|
@@ -11373,6 +11973,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11373
11973
|
validateSeedStateMachine,
|
|
11374
11974
|
validateSemanticRoles,
|
|
11375
11975
|
validateSharingRuleEnforceability,
|
|
11976
|
+
validateSortableFields,
|
|
11376
11977
|
validateStackExpressions,
|
|
11377
11978
|
validateTranslatableSections,
|
|
11378
11979
|
validateTranslationReferences,
|