@objectstack/lint 17.0.0 → 17.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +851 -0
- package/dist/index.cjs +1126 -320
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +520 -72
- package/dist/index.d.ts +520 -72
- package/dist/index.js +1096 -305
- package/dist/index.js.map +1 -1
- package/dist/{runtime-H-nDodRy.d.cts → runtime-cV-l_vtC.d.cts} +109 -3
- package/dist/{runtime-H-nDodRy.d.ts → runtime-cV-l_vtC.d.ts} +109 -3
- package/dist/runtime.cjs +1188 -433
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1161 -402
- package/dist/runtime.js.map +1 -1
- package/package.json +7 -6
package/dist/index.cjs
CHANGED
|
@@ -20,10 +20,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ACTION_BODY_SOURCE_UNPARSEABLE: () => ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
23
24
|
ACTION_BODY_WRITE_EXCLUSIONS: () => ACTION_BODY_WRITE_EXCLUSIONS,
|
|
24
25
|
ACTION_BODY_WRITE_PATTERNS: () => ACTION_BODY_WRITE_PATTERNS,
|
|
25
26
|
ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
|
|
26
27
|
ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
28
|
+
ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR: () => ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
27
29
|
ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
|
|
28
30
|
ACTION_NO_PLACEMENT: () => ACTION_NO_PLACEMENT,
|
|
29
31
|
ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
|
|
@@ -69,6 +71,7 @@ __export(index_exports, {
|
|
|
69
71
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
70
72
|
FILTER_EMPTY_COMBINATOR: () => FILTER_EMPTY_COMBINATOR,
|
|
71
73
|
FILTER_EMPTY_NODE: () => FILTER_EMPTY_NODE,
|
|
74
|
+
FILTER_PRESET_COMPARAND: () => FILTER_PRESET_COMPARAND,
|
|
72
75
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
73
76
|
FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
|
|
74
77
|
FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -86,6 +89,7 @@ __export(index_exports, {
|
|
|
86
89
|
FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
87
90
|
FLOW_MULTI_WRITE_UNFILTERED: () => FLOW_MULTI_WRITE_UNFILTERED,
|
|
88
91
|
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
92
|
+
FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR: () => FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
89
93
|
FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
|
|
90
94
|
FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
|
|
91
95
|
FLOW_TEMPLATE_FIELD_UNPROVISIONED: () => FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
@@ -103,10 +107,12 @@ __export(index_exports, {
|
|
|
103
107
|
FLOW_WRITE_NODE_TYPES_DEFERRED: () => FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
104
108
|
FORM_COLSPAN_ABSOLUTE: () => FORM_COLSPAN_ABSOLUTE,
|
|
105
109
|
FORM_FIELD_UNKNOWN: () => FORM_FIELD_UNKNOWN,
|
|
110
|
+
HOOK_BODY_SOURCE_UNPARSEABLE: () => HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
106
111
|
HOOK_BODY_WRITE_EXCLUSIONS: () => HOOK_BODY_WRITE_EXCLUSIONS,
|
|
107
112
|
HOOK_BODY_WRITE_PATTERNS: () => HOOK_BODY_WRITE_PATTERNS,
|
|
108
113
|
HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
|
|
109
114
|
HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
115
|
+
HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR: () => HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
110
116
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
111
117
|
LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
|
|
112
118
|
LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
@@ -125,6 +131,7 @@ __export(index_exports, {
|
|
|
125
131
|
PAGE_FIELD_UNKNOWN: () => PAGE_FIELD_UNKNOWN,
|
|
126
132
|
PAGE_FIELD_UNPROVISIONED: () => PAGE_FIELD_UNPROVISIONED,
|
|
127
133
|
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
134
|
+
PARSE_FAILURE_HINT: () => PARSE_FAILURE_HINT,
|
|
128
135
|
PREDICATE_PATH_UNRESOLVED: () => PREDICATE_PATH_UNRESOLVED,
|
|
129
136
|
PREDICATE_PATH_UNROOTED: () => PREDICATE_PATH_UNROOTED,
|
|
130
137
|
PREDICATE_RHS_PATH_SHAPED: () => PREDICATE_RHS_PATH_SHAPED,
|
|
@@ -135,6 +142,7 @@ __export(index_exports, {
|
|
|
135
142
|
REACT_CHART_DRILLDOWN_INVALID: () => REACT_CHART_DRILLDOWN_INVALID,
|
|
136
143
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
137
144
|
REACT_CHART_FIELD_UNPROVISIONED: () => REACT_CHART_FIELD_UNPROVISIONED,
|
|
145
|
+
REACT_PAGE_SOURCE_UNPARSEABLE: () => REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
138
146
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
139
147
|
RLS_PREDICATE_OVER_BUDGET: () => RLS_PREDICATE_OVER_BUDGET,
|
|
140
148
|
RLS_PREDICATE_UNENFORCEABLE: () => RLS_PREDICATE_UNENFORCEABLE,
|
|
@@ -142,6 +150,7 @@ __export(index_exports, {
|
|
|
142
150
|
RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
|
|
143
151
|
SEAL_MARKERS: () => SEAL_MARKERS,
|
|
144
152
|
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
153
|
+
SEARCHABLE_FIELD_UNPROVISIONED: () => SEARCHABLE_FIELD_UNPROVISIONED,
|
|
145
154
|
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
146
155
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
147
156
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
@@ -160,9 +169,15 @@ __export(index_exports, {
|
|
|
160
169
|
SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
161
170
|
SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
162
171
|
SEMANTIC_ROLE_FIELD_UNPROVISIONED: () => SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
172
|
+
SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT: () => SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
173
|
+
SHARING_RULE_OBJECT_NOT_SHAREABLE: () => SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
163
174
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
164
175
|
SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
176
|
+
SORT_FIELD_UNKNOWN: () => SORT_FIELD_UNKNOWN,
|
|
177
|
+
SORT_FIELD_UNPROVISIONED: () => SORT_FIELD_UNPROVISIONED,
|
|
178
|
+
SORT_FIELD_UNSORTABLE: () => SORT_FIELD_UNSORTABLE,
|
|
165
179
|
STARTUP_OPEN_VOCABULARY_VERDICT: () => STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
180
|
+
STARTUP_SOURCE_UNPARSEABLE: () => STARTUP_SOURCE_UNPARSEABLE,
|
|
166
181
|
STARTUP_VERDICT_ASSERTIVE_WORDING: () => STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
167
182
|
STARTUP_VERDICT_HINT: () => STARTUP_VERDICT_HINT,
|
|
168
183
|
STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
|
|
@@ -198,6 +213,8 @@ __export(index_exports, {
|
|
|
198
213
|
authoringRulesFor: () => authoringRulesFor,
|
|
199
214
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
200
215
|
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
216
|
+
checkSortDeclaration: () => checkSortDeclaration,
|
|
217
|
+
describeParseFailure: () => describeParseFailure,
|
|
201
218
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
202
219
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
203
220
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
@@ -212,6 +229,7 @@ __export(index_exports, {
|
|
|
212
229
|
lintUniqueDeclarations: () => lintUniqueDeclarations,
|
|
213
230
|
lintUnscopedDeclaredIndexes: () => lintUnscopedDeclaredIndexes,
|
|
214
231
|
lintViewRefs: () => lintViewRefs,
|
|
232
|
+
narrowObjectsToPackageClosure: () => narrowObjectsToPackageClosure,
|
|
215
233
|
nearestRegisteredFormat: () => nearestRegisteredFormat,
|
|
216
234
|
nullGuardMessage: () => nullGuardMessage,
|
|
217
235
|
runAuthoringRules: () => runAuthoringRules,
|
|
@@ -250,6 +268,7 @@ __export(index_exports, {
|
|
|
250
268
|
validatePageFieldBindings: () => validatePageFieldBindings,
|
|
251
269
|
validatePageSourceStyling: () => validatePageSourceStyling,
|
|
252
270
|
validatePredicatePathRefs: () => validatePredicatePathRefs,
|
|
271
|
+
validatePresetComparands: () => validatePresetComparands,
|
|
253
272
|
validateReactPageProps: () => validateReactPageProps,
|
|
254
273
|
validateReactPages: () => validateReactPages,
|
|
255
274
|
validateReadonlyFlowWrites: () => validateReadonlyFlowWrites,
|
|
@@ -266,6 +285,7 @@ __export(index_exports, {
|
|
|
266
285
|
validateSeedStateMachine: () => validateSeedStateMachine,
|
|
267
286
|
validateSemanticRoles: () => validateSemanticRoles,
|
|
268
287
|
validateSharingRuleEnforceability: () => validateSharingRuleEnforceability,
|
|
288
|
+
validateSortableFields: () => validateSortableFields,
|
|
269
289
|
validateStackExpressions: () => validateStackExpressions,
|
|
270
290
|
validateTranslatableSections: () => validateTranslatableSections,
|
|
271
291
|
validateTranslationReferences: () => validateTranslationReferences,
|
|
@@ -860,10 +880,10 @@ var OUTCOME_CLAUSE = {
|
|
|
860
880
|
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
861
881
|
"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
882
|
};
|
|
863
|
-
function nullGuardMessage(subject, objectName,
|
|
883
|
+
function nullGuardMessage(subject, objectName, finding2, outcome = "fail-closed") {
|
|
864
884
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
865
|
-
const hasNote =
|
|
866
|
-
return `${subject} applies \`${
|
|
885
|
+
const hasNote = finding2.hasOnlyGuard ? ` \`has(${finding2.operand})\` does not guard it.` : "";
|
|
886
|
+
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
887
|
}
|
|
868
888
|
|
|
869
889
|
// src/validate-expressions.ts
|
|
@@ -1046,10 +1066,10 @@ function validateStackExpressions(stack) {
|
|
|
1046
1066
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
1047
1067
|
const source = celSourceOf(raw);
|
|
1048
1068
|
if (!source) return;
|
|
1049
|
-
for (const
|
|
1069
|
+
for (const finding2 of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
1050
1070
|
issues.push({
|
|
1051
1071
|
where,
|
|
1052
|
-
message: nullGuardMessage(subject, objectName,
|
|
1072
|
+
message: nullGuardMessage(subject, objectName, finding2, outcome),
|
|
1053
1073
|
source,
|
|
1054
1074
|
severity: "error"
|
|
1055
1075
|
});
|
|
@@ -1073,7 +1093,7 @@ function validateStackExpressions(stack) {
|
|
|
1073
1093
|
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
1094
|
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
1075
1095
|
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`).
|
|
1096
|
+
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
1097
|
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
1098
|
// Listed rather than left to the `??` below, so the map covers every slot
|
|
1079
1099
|
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
@@ -1132,14 +1152,15 @@ function validateStackExpressions(stack) {
|
|
|
1132
1152
|
if (retired.length > 0) {
|
|
1133
1153
|
issues.push({
|
|
1134
1154
|
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
|
-
//
|
|
1155
|
+
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
|
|
1156
|
+
// sentence names the TOOL's behaviour, never the retired key's fate —
|
|
1157
|
+
// "rewrite it" read two ways over a branch that DELETES the key
|
|
1158
|
+
// (template/recipients/variables/script), and the tool never rewrote a
|
|
1159
|
+
// source file at all. Plain-quoted (not a template literal)
|
|
1139
1160
|
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
1140
1161
|
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
1141
1162
|
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
1142
|
-
"Run `os migrate meta --from 16` to
|
|
1163
|
+
"Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.",
|
|
1143
1164
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
1144
1165
|
});
|
|
1145
1166
|
} else if (!fn) {
|
|
@@ -1210,10 +1231,10 @@ function validateStackExpressions(stack) {
|
|
|
1210
1231
|
f.expression,
|
|
1211
1232
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
1212
1233
|
);
|
|
1213
|
-
const
|
|
1214
|
-
for (const e of res.errors) issues.push({ where:
|
|
1215
|
-
for (const w of res.warnings) issues.push({ where:
|
|
1216
|
-
warnUnprovisionedAnchors(
|
|
1234
|
+
const fieldWhere2 = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
1235
|
+
for (const e of res.errors) issues.push({ where: fieldWhere2, message: e.message, source: e.source, severity: "error" });
|
|
1236
|
+
for (const w of res.warnings) issues.push({ where: fieldWhere2, message: w.message, source: w.source, severity: "warning" });
|
|
1237
|
+
warnUnprovisionedAnchors(fieldWhere2, f.expression, objectName);
|
|
1217
1238
|
}
|
|
1218
1239
|
}
|
|
1219
1240
|
}
|
|
@@ -1289,6 +1310,38 @@ function validateStackExpressions(stack) {
|
|
|
1289
1310
|
|
|
1290
1311
|
// src/lint-startup-registry-verdict.ts
|
|
1291
1312
|
var import_node_module = require("module");
|
|
1313
|
+
|
|
1314
|
+
// src/checked-parse.ts
|
|
1315
|
+
function createSourceFileChecked(tsc, fileName, source, options) {
|
|
1316
|
+
const sourceFile = tsc.createSourceFile(
|
|
1317
|
+
fileName,
|
|
1318
|
+
source,
|
|
1319
|
+
options.target,
|
|
1320
|
+
options.setParentNodes,
|
|
1321
|
+
options.scriptKind
|
|
1322
|
+
);
|
|
1323
|
+
const diagnostics = sourceFile.parseDiagnostics;
|
|
1324
|
+
if (!diagnostics || diagnostics.length === 0) return { sourceFile };
|
|
1325
|
+
const first = diagnostics[0];
|
|
1326
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(first.start ?? 0);
|
|
1327
|
+
const offset = options.synthesizedLinesBefore ?? 0;
|
|
1328
|
+
return {
|
|
1329
|
+
sourceFile,
|
|
1330
|
+
failure: {
|
|
1331
|
+
message: tsc.flattenDiagnosticMessageText(first.messageText, " "),
|
|
1332
|
+
line: Math.max(1, line + 1 - offset),
|
|
1333
|
+
column: character + 1,
|
|
1334
|
+
count: diagnostics.length
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function describeParseFailure(failure) {
|
|
1339
|
+
const more = failure.count > 1 ? `; ${failure.count} syntax errors in total` : "";
|
|
1340
|
+
return `line ${failure.line}, column ${failure.column}: ${failure.message}${more}`;
|
|
1341
|
+
}
|
|
1342
|
+
var PARSE_FAILURE_HINT = "The source is parsed (never executed) so it can be checked. A source with syntax errors is only partially recovered, so the checks that follow may have skipped real problems \u2014 fix the syntax error and re-run to get a verdict that covers the whole source.";
|
|
1343
|
+
|
|
1344
|
+
// src/lint-startup-registry-verdict.ts
|
|
1292
1345
|
var import_meta = {};
|
|
1293
1346
|
var cachedTs = null;
|
|
1294
1347
|
function loadTypeScript() {
|
|
@@ -1305,6 +1358,7 @@ function loadTypeScript() {
|
|
|
1305
1358
|
}
|
|
1306
1359
|
var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
|
|
1307
1360
|
var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
|
|
1361
|
+
var STARTUP_SOURCE_UNPARSEABLE = "startup-source-unparseable";
|
|
1308
1362
|
var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
|
|
1309
1363
|
[
|
|
1310
1364
|
"getRegisteredNodeTypes",
|
|
@@ -1533,13 +1587,22 @@ function findStartupRegistryVerdicts(source, options = {}) {
|
|
|
1533
1587
|
if (!mentionsAny) return [];
|
|
1534
1588
|
const t = loadTypeScript();
|
|
1535
1589
|
const fileLabel = options.file ?? "source";
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
}
|
|
1590
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(t, fileLabel, source, {
|
|
1591
|
+
target: t.ScriptTarget.Latest,
|
|
1592
|
+
setParentNodes: true,
|
|
1593
|
+
scriptKind: t.ScriptKind.TS
|
|
1594
|
+
});
|
|
1542
1595
|
const findings = [];
|
|
1596
|
+
if (failure) {
|
|
1597
|
+
findings.push({
|
|
1598
|
+
severity: "warning",
|
|
1599
|
+
rule: STARTUP_SOURCE_UNPARSEABLE,
|
|
1600
|
+
where: fileLabel,
|
|
1601
|
+
path: `${fileLabel}:${failure.line}`,
|
|
1602
|
+
message: `source did not parse (${describeParseFailure(failure)}), so this rule read a partially recovered tree \u2014 a startup verdict in the unread part is not reported.`,
|
|
1603
|
+
hint: PARSE_FAILURE_HINT
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1543
1606
|
const moduleBindings = moduleLevelMutableBindings(t, sf);
|
|
1544
1607
|
const functionBodies = indexFunctionBodies(t, sf);
|
|
1545
1608
|
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
@@ -2835,6 +2898,7 @@ var import_data5 = require("@objectstack/spec/data");
|
|
|
2835
2898
|
var import_data4 = require("@objectstack/spec/data");
|
|
2836
2899
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
2837
2900
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
2901
|
+
var SEARCHABLE_FIELD_UNPROVISIONED = "searchable-field-unprovisioned";
|
|
2838
2902
|
function asArray10(v) {
|
|
2839
2903
|
if (Array.isArray(v)) return v;
|
|
2840
2904
|
if (v && typeof v === "object") {
|
|
@@ -2925,7 +2989,7 @@ function indexObjectSearchTargets(stack) {
|
|
|
2925
2989
|
}
|
|
2926
2990
|
return fieldsByObject;
|
|
2927
2991
|
}
|
|
2928
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
2992
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing", unprovisionedAnchors) {
|
|
2929
2993
|
const findings = [];
|
|
2930
2994
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
2931
2995
|
if (!objectName) return findings;
|
|
@@ -2933,6 +2997,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2933
2997
|
const target = fieldsByObject.get(objectName);
|
|
2934
2998
|
if (!target) return findings;
|
|
2935
2999
|
const known = target.names;
|
|
3000
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2936
3001
|
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
2937
3002
|
for (let i = 0; i < declared.length; i++) {
|
|
2938
3003
|
const entry = declared[i];
|
|
@@ -2950,6 +3015,16 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2950
3015
|
});
|
|
2951
3016
|
continue;
|
|
2952
3017
|
}
|
|
3018
|
+
if (anchors?.has(name)) {
|
|
3019
|
+
findings.push({
|
|
3020
|
+
severity: "warning",
|
|
3021
|
+
rule: SEARCHABLE_FIELD_UNPROVISIONED,
|
|
3022
|
+
where,
|
|
3023
|
+
path: `${path}[${i}]`,
|
|
3024
|
+
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.`),
|
|
3025
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
2953
3028
|
if ((0, import_data4.isVirtualSearchField)(target.fields[name])) {
|
|
2954
3029
|
const vtype = target.fields[name]?.type;
|
|
2955
3030
|
findings.push({
|
|
@@ -3003,9 +3078,19 @@ function validateSearchableFields(stack) {
|
|
|
3003
3078
|
if (!isRec4(stack)) return findings;
|
|
3004
3079
|
const objects = asArray10(stack.objects);
|
|
3005
3080
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
3081
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
3006
3082
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
3007
3083
|
findings.push(
|
|
3008
|
-
...checkSearchableFieldList(
|
|
3084
|
+
...checkSearchableFieldList(
|
|
3085
|
+
declared,
|
|
3086
|
+
objectName,
|
|
3087
|
+
fieldsByObject,
|
|
3088
|
+
where,
|
|
3089
|
+
path,
|
|
3090
|
+
subject,
|
|
3091
|
+
role,
|
|
3092
|
+
unprovisionedAnchors
|
|
3093
|
+
)
|
|
3009
3094
|
);
|
|
3010
3095
|
};
|
|
3011
3096
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
@@ -3043,6 +3128,26 @@ function validateSearchableFields(stack) {
|
|
|
3043
3128
|
if (!isRec4(view)) continue;
|
|
3044
3129
|
const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
3045
3130
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
3131
|
+
if (view.viewKind === "list" && !isRec4(view.config)) {
|
|
3132
|
+
check(
|
|
3133
|
+
view.searchableFields,
|
|
3134
|
+
listViewObject(view) ?? viewObject,
|
|
3135
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
3136
|
+
`views[${vi}].searchableFields`,
|
|
3137
|
+
"list-view searchableFields",
|
|
3138
|
+
"narrowing"
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
if (view.viewKind === "list" && isRec4(view.config)) {
|
|
3142
|
+
check(
|
|
3143
|
+
view.config.searchableFields,
|
|
3144
|
+
listViewObject(view.config) ?? viewObject,
|
|
3145
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
3146
|
+
`views[${vi}].config.searchableFields`,
|
|
3147
|
+
"list-view searchableFields",
|
|
3148
|
+
"narrowing"
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3046
3151
|
if (isRec4(view.list)) {
|
|
3047
3152
|
check(
|
|
3048
3153
|
view.list.searchableFields,
|
|
@@ -3192,7 +3297,12 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
3192
3297
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
3193
3298
|
"record:path": { props: ["statusField"] },
|
|
3194
3299
|
"element:number": { props: ["field"] },
|
|
3195
|
-
|
|
3300
|
+
// `element:filter` had a `{ props: ['fields'] }` entry until #9220 retired the
|
|
3301
|
+
// whole element at element grain (ADR-0049 — no renderer ever shipped for it).
|
|
3302
|
+
// Every `ElementFilterProps` key is a `retiredKey()` tombstone now, so no
|
|
3303
|
+
// spec-conformant page carries `fields` on it, and the #5068 props gate
|
|
3304
|
+
// reports an authored one by name with the element-retirement prescription —
|
|
3305
|
+
// the same #5775/#6629 residue class as the record-picker entries below.
|
|
3196
3306
|
"element:form": { props: ["fields"] },
|
|
3197
3307
|
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
3198
3308
|
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
@@ -3485,6 +3595,7 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
3485
3595
|
};
|
|
3486
3596
|
return perPosition(init.expression);
|
|
3487
3597
|
}
|
|
3598
|
+
var REACT_PAGE_SOURCE_UNPARSEABLE = "react-page-source-unparseable";
|
|
3488
3599
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
3489
3600
|
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
3490
3601
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
@@ -3813,11 +3924,20 @@ function validateReactPageProps(stack) {
|
|
|
3813
3924
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
3814
3925
|
const name = String(page.name ?? `#${p}`);
|
|
3815
3926
|
const tsc = loadTypeScript2();
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3927
|
+
const { sourceFile: sf, failure } = createSourceFileChecked(tsc, "page.tsx", source, {
|
|
3928
|
+
target: tsc.ScriptTarget.Latest,
|
|
3929
|
+
setParentNodes: true,
|
|
3930
|
+
scriptKind: tsc.ScriptKind.TSX
|
|
3931
|
+
});
|
|
3932
|
+
if (failure) {
|
|
3933
|
+
findings.push({
|
|
3934
|
+
severity: "warning",
|
|
3935
|
+
rule: REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
3936
|
+
where: `page "${name}"`,
|
|
3937
|
+
path: `pages[${p}].source`,
|
|
3938
|
+
message: `kind:'react' source did not parse (${describeParseFailure(failure)}), so the component-contract checks read a partially recovered tree and may have missed real problems.`,
|
|
3939
|
+
hint: PARSE_FAILURE_HINT
|
|
3940
|
+
});
|
|
3821
3941
|
}
|
|
3822
3942
|
const locals = localComponentNames(tsc, sf);
|
|
3823
3943
|
const visit = (node) => {
|
|
@@ -3896,7 +4016,12 @@ function validateReactPageProps(stack) {
|
|
|
3896
4016
|
searchTargets,
|
|
3897
4017
|
where,
|
|
3898
4018
|
`${path} \u203A searchableFields`,
|
|
3899
|
-
"searchableFields"
|
|
4019
|
+
"searchableFields",
|
|
4020
|
+
// A `<ListView>` prop is a view-level narrowing — the checker's
|
|
4021
|
+
// default, spelled out here because the #8404 provenance index
|
|
4022
|
+
// follows it positionally.
|
|
4023
|
+
"narrowing",
|
|
4024
|
+
unprovisionedAnchors
|
|
3900
4025
|
)
|
|
3901
4026
|
);
|
|
3902
4027
|
}
|
|
@@ -5468,6 +5593,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5468
5593
|
}
|
|
5469
5594
|
}
|
|
5470
5595
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
5596
|
+
const DELEGATION_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position"]);
|
|
5471
5597
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
5472
5598
|
for (const [i, seed] of asArray19(stack.data).entries()) {
|
|
5473
5599
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
@@ -5491,7 +5617,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5491
5617
|
}
|
|
5492
5618
|
}
|
|
5493
5619
|
const delegatedFrom = rec.delegated_from;
|
|
5494
|
-
if (delegatedFrom != null && delegatedFrom !== "") {
|
|
5620
|
+
if (DELEGATION_SEED_OBJECTS.has(seedObject) && delegatedFrom != null && delegatedFrom !== "") {
|
|
5495
5621
|
const reason = rec.reason;
|
|
5496
5622
|
if (typeof reason !== "string" || reason.trim().length === 0) {
|
|
5497
5623
|
findings.push({
|
|
@@ -5657,6 +5783,8 @@ function validateOrgAxisRedLines(stack) {
|
|
|
5657
5783
|
var import_formula6 = require("@objectstack/formula");
|
|
5658
5784
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
5659
5785
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
5786
|
+
var SHARING_RULE_OBJECT_NOT_SHAREABLE = "sharing-rule-object-not-shareable";
|
|
5787
|
+
var SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT = "sharing-rule-object-controlled-by-parent";
|
|
5660
5788
|
function asArray21(v) {
|
|
5661
5789
|
if (Array.isArray(v)) return v;
|
|
5662
5790
|
if (v && typeof v === "object") {
|
|
@@ -5681,10 +5809,67 @@ function sourceOf(condition) {
|
|
|
5681
5809
|
return str2(input?.source);
|
|
5682
5810
|
}
|
|
5683
5811
|
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).";
|
|
5812
|
+
function effectiveSharingModelOf(obj) {
|
|
5813
|
+
const m = obj.sharingModel;
|
|
5814
|
+
if (m === "private") return "private";
|
|
5815
|
+
if (m === "public_read") return "read";
|
|
5816
|
+
if (m === "public_read_write" || m === "controlled_by_parent") return "public";
|
|
5817
|
+
if (m == null) {
|
|
5818
|
+
const isSystem = obj.isSystem === true || str2(obj.name).startsWith("sys_");
|
|
5819
|
+
return isSystem ? "public" : "private";
|
|
5820
|
+
}
|
|
5821
|
+
return "private";
|
|
5822
|
+
}
|
|
5823
|
+
function masterOf(obj) {
|
|
5824
|
+
for (const f of asArray21(obj.fields)) {
|
|
5825
|
+
if (f.type === "master_detail") {
|
|
5826
|
+
const ref = f.reference;
|
|
5827
|
+
if (typeof ref === "string" && ref) return ref;
|
|
5828
|
+
}
|
|
5829
|
+
}
|
|
5830
|
+
return void 0;
|
|
5831
|
+
}
|
|
5832
|
+
function anchorFindings(rule, index, objectsByName) {
|
|
5833
|
+
const object = str2(rule.object);
|
|
5834
|
+
if (!object) return [];
|
|
5835
|
+
const target = objectsByName.get(object);
|
|
5836
|
+
if (!target) return [];
|
|
5837
|
+
const name = str2(rule.name) || String(index);
|
|
5838
|
+
const where = `sharing rule "${name}" on object "${object}"`;
|
|
5839
|
+
const path = `sharingRules[${index}].object`;
|
|
5840
|
+
const owd = target.sharingModel;
|
|
5841
|
+
if (owd === "controlled_by_parent") {
|
|
5842
|
+
const master = masterOf(target);
|
|
5843
|
+
return [{
|
|
5844
|
+
severity: "error",
|
|
5845
|
+
rule: SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
5846
|
+
where,
|
|
5847
|
+
path,
|
|
5848
|
+
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.`,
|
|
5849
|
+
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.`
|
|
5850
|
+
}];
|
|
5851
|
+
}
|
|
5852
|
+
if (effectiveSharingModelOf(target) !== "public") return [];
|
|
5853
|
+
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`;
|
|
5854
|
+
return [{
|
|
5855
|
+
severity: "error",
|
|
5856
|
+
rule: SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
5857
|
+
where,
|
|
5858
|
+
path,
|
|
5859
|
+
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.`,
|
|
5860
|
+
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.`
|
|
5861
|
+
}];
|
|
5862
|
+
}
|
|
5684
5863
|
function validateSharingRuleEnforceability(stack) {
|
|
5685
5864
|
const findings = [];
|
|
5686
5865
|
const cfg = stack ?? {};
|
|
5866
|
+
const objectsByName = /* @__PURE__ */ new Map();
|
|
5867
|
+
for (const obj of asArray21(cfg.objects)) {
|
|
5868
|
+
const name = str2(obj.name);
|
|
5869
|
+
if (name) objectsByName.set(name, obj);
|
|
5870
|
+
}
|
|
5687
5871
|
asArray21(cfg.sharingRules).forEach((rule, index) => {
|
|
5872
|
+
anchorFindings(rule, index, objectsByName).forEach((f) => findings.push(f));
|
|
5688
5873
|
const input = toCompilerInput(rule.condition);
|
|
5689
5874
|
if (input === null) return;
|
|
5690
5875
|
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
@@ -5817,7 +6002,6 @@ function asArray23(v) {
|
|
|
5817
6002
|
function strName7(v) {
|
|
5818
6003
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5819
6004
|
}
|
|
5820
|
-
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
5821
6005
|
var URL_COLLECTION_TO_STACK_KEY = {
|
|
5822
6006
|
object: "objects",
|
|
5823
6007
|
objects: "objects",
|
|
@@ -5862,13 +6046,10 @@ function collectKnownTargets(stack) {
|
|
|
5862
6046
|
return { actions, objects, reports, dashboards, pages, views };
|
|
5863
6047
|
}
|
|
5864
6048
|
function resolveActionTarget(actionType, target, known) {
|
|
5865
|
-
if (known.actions.has(target)) return true;
|
|
5866
6049
|
if (actionType === "modal") {
|
|
5867
|
-
|
|
5868
|
-
const m = MODAL_VERB_RE.exec(target);
|
|
5869
|
-
if (m && known.objects.has(m[1])) return true;
|
|
6050
|
+
return known.pages.has(target);
|
|
5870
6051
|
}
|
|
5871
|
-
return
|
|
6052
|
+
return known.actions.has(target);
|
|
5872
6053
|
}
|
|
5873
6054
|
function resolveUrlRoute(target, known) {
|
|
5874
6055
|
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(target) || target.startsWith("//")) return null;
|
|
@@ -5898,14 +6079,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
5898
6079
|
const actionType = strName7(action.actionType) ?? "url";
|
|
5899
6080
|
if (actionType === "script" || actionType === "modal") {
|
|
5900
6081
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
5901
|
-
const kindWord = actionType === "script" ? "script" : "modal";
|
|
5902
6082
|
findings.push({
|
|
5903
6083
|
severity: "error",
|
|
5904
6084
|
rule: DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
5905
6085
|
where,
|
|
5906
6086
|
path,
|
|
5907
|
-
message:
|
|
5908
|
-
hint: actionType === "modal" ? `
|
|
6087
|
+
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).`,
|
|
6088
|
+
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
6089
|
});
|
|
5910
6090
|
return;
|
|
5911
6091
|
}
|
|
@@ -5947,7 +6127,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
5947
6127
|
var import_data7 = require("@objectstack/spec/data");
|
|
5948
6128
|
|
|
5949
6129
|
// src/filter-walk.ts
|
|
5950
|
-
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
6130
|
+
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter", "relatedListFilter"]);
|
|
5951
6131
|
function asArray24(v) {
|
|
5952
6132
|
if (Array.isArray(v)) return v;
|
|
5953
6133
|
if (v && typeof v === "object") {
|
|
@@ -6051,8 +6231,146 @@ function validateFilterTokens(stack) {
|
|
|
6051
6231
|
return out;
|
|
6052
6232
|
}
|
|
6053
6233
|
|
|
6054
|
-
// src/validate-
|
|
6234
|
+
// src/validate-preset-comparands.ts
|
|
6055
6235
|
var import_data8 = require("@objectstack/spec/data");
|
|
6236
|
+
var import_ui3 = require("@objectstack/spec/ui");
|
|
6237
|
+
var FILTER_PRESET_COMPARAND = "filter-preset-comparand";
|
|
6238
|
+
var PRESET_COMPARAND_SURFACES = [
|
|
6239
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
6240
|
+
{ key: "objects", kind: "object" },
|
|
6241
|
+
{ key: "views", kind: "view" },
|
|
6242
|
+
{ key: "reports", kind: "report" },
|
|
6243
|
+
{ key: "datasets", kind: "dataset" },
|
|
6244
|
+
{ key: "pages", kind: "page" },
|
|
6245
|
+
{ key: "apps", kind: "app" },
|
|
6246
|
+
{ key: "flows", kind: "flow" }
|
|
6247
|
+
];
|
|
6248
|
+
var ORDERING_DOLLAR_OPS = /* @__PURE__ */ new Set(["$gt", "$gte", "$lt", "$lte"]);
|
|
6249
|
+
var ORDERING_INFIX_OPS = /* @__PURE__ */ new Set([">", ">=", "<", "<="]);
|
|
6250
|
+
var ORDERING_RULE_OPS = /* @__PURE__ */ new Set([
|
|
6251
|
+
"greater_than",
|
|
6252
|
+
"greater_than_or_equal",
|
|
6253
|
+
"less_than",
|
|
6254
|
+
"less_than_or_equal",
|
|
6255
|
+
"before",
|
|
6256
|
+
"after"
|
|
6257
|
+
]);
|
|
6258
|
+
var MAX_DEPTH = 32;
|
|
6259
|
+
function isPlainObject(v) {
|
|
6260
|
+
return !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
6261
|
+
}
|
|
6262
|
+
function finding(where, path, preset, operator) {
|
|
6263
|
+
return {
|
|
6264
|
+
severity: "error",
|
|
6265
|
+
rule: FILTER_PRESET_COMPARAND,
|
|
6266
|
+
where,
|
|
6267
|
+
path,
|
|
6268
|
+
message: (0, import_data8.bareDateRangePresetComparandMessage)(preset, operator),
|
|
6269
|
+
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."
|
|
6270
|
+
};
|
|
6271
|
+
}
|
|
6272
|
+
function judgeConditionNode(node, path, where, out, depth) {
|
|
6273
|
+
if (depth > MAX_DEPTH) return;
|
|
6274
|
+
for (const [key, value] of Object.entries(node)) {
|
|
6275
|
+
const here = `${path}.${key}`;
|
|
6276
|
+
if (key === "$and" || key === "$or") {
|
|
6277
|
+
if (Array.isArray(value)) {
|
|
6278
|
+
value.forEach((arm, i) => {
|
|
6279
|
+
if (isPlainObject(arm)) judgeConditionNode(arm, `${here}[${i}]`, where, out, depth + 1);
|
|
6280
|
+
});
|
|
6281
|
+
}
|
|
6282
|
+
continue;
|
|
6283
|
+
}
|
|
6284
|
+
if (key === "$not") {
|
|
6285
|
+
if (isPlainObject(value)) judgeConditionNode(value, here, where, out, depth + 1);
|
|
6286
|
+
continue;
|
|
6287
|
+
}
|
|
6288
|
+
if (key.startsWith("$")) continue;
|
|
6289
|
+
if (!isPlainObject(value)) continue;
|
|
6290
|
+
const hasOps = Object.keys(value).some((k) => k.startsWith("$"));
|
|
6291
|
+
if (!hasOps) {
|
|
6292
|
+
judgeConditionNode(value, here, where, out, depth + 1);
|
|
6293
|
+
continue;
|
|
6294
|
+
}
|
|
6295
|
+
for (const [op, comparand] of Object.entries(value)) {
|
|
6296
|
+
if (ORDERING_DOLLAR_OPS.has(op) && (0, import_data8.isDateRangePresetName)(comparand)) {
|
|
6297
|
+
out.push(finding(where, `${here}.${op}`, comparand, op));
|
|
6298
|
+
continue;
|
|
6299
|
+
}
|
|
6300
|
+
if (op === "$between" && Array.isArray(comparand)) {
|
|
6301
|
+
comparand.forEach((endpoint, i) => {
|
|
6302
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6303
|
+
out.push(finding(where, `${here}.${op}[${i}]`, endpoint, op));
|
|
6304
|
+
}
|
|
6305
|
+
});
|
|
6306
|
+
}
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
6310
|
+
function judgeFilterRule(rule, path, where, out) {
|
|
6311
|
+
const operator = (0, import_ui3.normalizeFilterOperator)(rule.operator);
|
|
6312
|
+
if (typeof operator !== "string") return;
|
|
6313
|
+
const value = rule.value;
|
|
6314
|
+
if (ORDERING_RULE_OPS.has(operator) && (0, import_data8.isDateRangePresetName)(value)) {
|
|
6315
|
+
out.push(finding(where, `${path}.value`, value, operator));
|
|
6316
|
+
return;
|
|
6317
|
+
}
|
|
6318
|
+
if (operator === "between" && Array.isArray(value)) {
|
|
6319
|
+
value.forEach((endpoint, i) => {
|
|
6320
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6321
|
+
out.push(finding(where, `${path}.value[${i}]`, endpoint, operator));
|
|
6322
|
+
}
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
function judgeTriple(triple, path, where, out) {
|
|
6327
|
+
const op = triple[1];
|
|
6328
|
+
if (typeof op !== "string") return;
|
|
6329
|
+
const canonical = (0, import_data8.canonicalAstOperator)(op);
|
|
6330
|
+
const value = triple[2];
|
|
6331
|
+
if (ORDERING_INFIX_OPS.has(canonical) && (0, import_data8.isDateRangePresetName)(value)) {
|
|
6332
|
+
out.push(finding(where, `${path}[2]`, value, op));
|
|
6333
|
+
return;
|
|
6334
|
+
}
|
|
6335
|
+
if (canonical === "between" && Array.isArray(value)) {
|
|
6336
|
+
value.forEach((endpoint, i) => {
|
|
6337
|
+
if ((0, import_data8.isDateRangePresetName)(endpoint)) {
|
|
6338
|
+
out.push(finding(where, `${path}[2][${i}]`, endpoint, op));
|
|
6339
|
+
}
|
|
6340
|
+
});
|
|
6341
|
+
}
|
|
6342
|
+
}
|
|
6343
|
+
function judgeFilterValue(node, path, where, out, depth) {
|
|
6344
|
+
if (depth > MAX_DEPTH) return;
|
|
6345
|
+
if (Array.isArray(node)) {
|
|
6346
|
+
if (typeof node[0] === "string" && typeof node[1] === "string" && !["and", "or"].includes(node[0].toLowerCase()) && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
6347
|
+
judgeTriple(node, path, where, out);
|
|
6348
|
+
return;
|
|
6349
|
+
}
|
|
6350
|
+
node.forEach((member, i) => {
|
|
6351
|
+
if (typeof member === "string") return;
|
|
6352
|
+
judgeFilterValue(member, `${path}[${i}]`, where, out, depth + 1);
|
|
6353
|
+
});
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
6356
|
+
if (!isPlainObject(node)) return;
|
|
6357
|
+
if (typeof node.field === "string" && typeof node.operator === "string") {
|
|
6358
|
+
judgeFilterRule(node, path, where, out);
|
|
6359
|
+
return;
|
|
6360
|
+
}
|
|
6361
|
+
judgeConditionNode(node, path, where, out, depth);
|
|
6362
|
+
}
|
|
6363
|
+
function validatePresetComparands(stack) {
|
|
6364
|
+
if (!stack || typeof stack !== "object") return [];
|
|
6365
|
+
const out = [];
|
|
6366
|
+
walkAuthoredFilters(stack, PRESET_COMPARAND_SURFACES, ({ value, path, where }) => {
|
|
6367
|
+
judgeFilterValue(value, path, where, out, 0);
|
|
6368
|
+
});
|
|
6369
|
+
return out;
|
|
6370
|
+
}
|
|
6371
|
+
|
|
6372
|
+
// src/validate-empty-combinators.ts
|
|
6373
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
6056
6374
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
6057
6375
|
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
6058
6376
|
var EMPTY_COMBINATOR_SURFACES = [
|
|
@@ -6071,12 +6389,12 @@ function isFilterNode(value) {
|
|
|
6071
6389
|
return proto === Object.prototype || proto === null;
|
|
6072
6390
|
}
|
|
6073
6391
|
var VERDICT_OF = {
|
|
6074
|
-
$and: (0,
|
|
6075
|
-
$or: (0,
|
|
6076
|
-
$not: (0,
|
|
6077
|
-
node: (0,
|
|
6392
|
+
$and: (0, import_data9.reduceFilterVerdict)({ $and: [] }),
|
|
6393
|
+
$or: (0, import_data9.reduceFilterVerdict)({ $or: [] }),
|
|
6394
|
+
$not: (0, import_data9.reduceFilterVerdict)({ $not: {} }),
|
|
6395
|
+
node: (0, import_data9.reduceFilterVerdict)({}),
|
|
6078
6396
|
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
6079
|
-
orWithEmptyBranch: (0,
|
|
6397
|
+
orWithEmptyBranch: (0, import_data9.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
6080
6398
|
};
|
|
6081
6399
|
function rows(verdict) {
|
|
6082
6400
|
if (verdict === "true") return "matches EVERY row";
|
|
@@ -6431,7 +6749,7 @@ function validateNavTargetRefs(stack) {
|
|
|
6431
6749
|
}
|
|
6432
6750
|
|
|
6433
6751
|
// src/validate-nav-object-servability.ts
|
|
6434
|
-
var
|
|
6752
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
6435
6753
|
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
6436
6754
|
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6437
6755
|
function asArray27(v) {
|
|
@@ -6467,7 +6785,7 @@ function validateNavObjectServability(stack) {
|
|
|
6467
6785
|
if (nav.type === "object") {
|
|
6468
6786
|
const target = strName10(nav.objectName);
|
|
6469
6787
|
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
6470
|
-
if (target && declared && !(0,
|
|
6788
|
+
if (target && declared && !(0, import_data10.canServeApiOperation)(declared.enable, "list")) {
|
|
6471
6789
|
const enable = isRec15(declared.enable) ? declared.enable : {};
|
|
6472
6790
|
const apiDisabled = enable.apiEnabled === false;
|
|
6473
6791
|
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 +6816,217 @@ function validateNavObjectServability(stack) {
|
|
|
6498
6816
|
return findings;
|
|
6499
6817
|
}
|
|
6500
6818
|
|
|
6819
|
+
// src/validate-sortable-fields.ts
|
|
6820
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
6821
|
+
var SORT_FIELD_UNKNOWN = "sort-field-unknown";
|
|
6822
|
+
var SORT_FIELD_UNSORTABLE = "sort-field-unsortable";
|
|
6823
|
+
var SORT_FIELD_UNPROVISIONED = "sort-field-unprovisioned";
|
|
6824
|
+
function isRec16(v) {
|
|
6825
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6826
|
+
}
|
|
6827
|
+
function strName11(v) {
|
|
6828
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6829
|
+
}
|
|
6830
|
+
function readSortKeys(declared) {
|
|
6831
|
+
const fromShorthand = (raw, at) => {
|
|
6832
|
+
const trimmed = raw.trim();
|
|
6833
|
+
if (!trimmed) return void 0;
|
|
6834
|
+
const bare = trimmed.startsWith("-") ? trimmed.slice(1).trim() : trimmed.split(/\s+/)[0];
|
|
6835
|
+
return bare ? { field: bare, at } : void 0;
|
|
6836
|
+
};
|
|
6837
|
+
if (typeof declared === "string") {
|
|
6838
|
+
return declared.split(",").map((part, i) => fromShorthand(part, declared.includes(",") ? `[${i}]` : "")).filter((k) => !!k);
|
|
6839
|
+
}
|
|
6840
|
+
if (Array.isArray(declared)) {
|
|
6841
|
+
const keys = [];
|
|
6842
|
+
for (let i = 0; i < declared.length; i++) {
|
|
6843
|
+
const el = declared[i];
|
|
6844
|
+
if (typeof el === "string") {
|
|
6845
|
+
const k = fromShorthand(el, `[${i}]`);
|
|
6846
|
+
if (k) keys.push(k);
|
|
6847
|
+
continue;
|
|
6848
|
+
}
|
|
6849
|
+
if (!isRec16(el)) continue;
|
|
6850
|
+
const field = strName11(el.field);
|
|
6851
|
+
if (field) keys.push({ field: field.trim(), at: `[${i}]` });
|
|
6852
|
+
}
|
|
6853
|
+
return keys;
|
|
6854
|
+
}
|
|
6855
|
+
return [];
|
|
6856
|
+
}
|
|
6857
|
+
function suggest4(target, known) {
|
|
6858
|
+
let best;
|
|
6859
|
+
let bestScore = Infinity;
|
|
6860
|
+
for (const candidate of known) {
|
|
6861
|
+
const d = distance3(target, candidate);
|
|
6862
|
+
if (d < bestScore) {
|
|
6863
|
+
bestScore = d;
|
|
6864
|
+
best = candidate;
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
6868
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
6869
|
+
}
|
|
6870
|
+
function distance3(a, b) {
|
|
6871
|
+
const m = a.length;
|
|
6872
|
+
const n = b.length;
|
|
6873
|
+
if (m === 0) return n;
|
|
6874
|
+
if (n === 0) return m;
|
|
6875
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
6876
|
+
for (let i = 1; i <= m; i++) {
|
|
6877
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
6878
|
+
for (let j = 1; j <= n; j++) {
|
|
6879
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
6880
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
6881
|
+
}
|
|
6882
|
+
prev = curr;
|
|
6883
|
+
}
|
|
6884
|
+
return prev[n];
|
|
6885
|
+
}
|
|
6886
|
+
function checkSortDeclaration(declared, objectName, fieldsByObject, where, path, subject, unprovisionedAnchors) {
|
|
6887
|
+
const findings = [];
|
|
6888
|
+
if (declared === void 0 || declared === null) return findings;
|
|
6889
|
+
if (!objectName) return findings;
|
|
6890
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
6891
|
+
const target = fieldsByObject.get(objectName);
|
|
6892
|
+
if (!target) return findings;
|
|
6893
|
+
const known = target.names;
|
|
6894
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
6895
|
+
for (const key of readSortKeys(declared)) {
|
|
6896
|
+
const name = key.field;
|
|
6897
|
+
const head = name.split(".")[0];
|
|
6898
|
+
if (SYSTEM_FIELDS.has(head)) {
|
|
6899
|
+
if (!name.includes(".") && anchors?.has(head)) {
|
|
6900
|
+
findings.push({
|
|
6901
|
+
severity: "warning",
|
|
6902
|
+
rule: SORT_FIELD_UNPROVISIONED,
|
|
6903
|
+
where,
|
|
6904
|
+
path: `${path}${key.at}`,
|
|
6905
|
+
message: `${subject} orders by "${name}", which resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 so the ORDER BY reaches the driver, finds no column, and is dropped. Measured on a federated object over a real remote table, 'asc' and 'desc' return BYTE-IDENTICAL row order under a 200 while the same query on a real column reverses. Unlike this rule's other two verdicts nothing refuses it: the REST ingress (#6994) and the engine (#7095) both judge only 'formula', so the view's FIRST fetch \u2014 and every fetch after it \u2014 silently answers in an arbitrary order, which 'limit'/'offset' then slice into an arbitrary page.`,
|
|
6906
|
+
hint: unprovisionedAnchorHint(objectName, name)
|
|
6907
|
+
});
|
|
6908
|
+
}
|
|
6909
|
+
continue;
|
|
6910
|
+
}
|
|
6911
|
+
if (!known.has(head)) {
|
|
6912
|
+
const dotted = name.includes(".");
|
|
6913
|
+
findings.push({
|
|
6914
|
+
severity: "error",
|
|
6915
|
+
rule: SORT_FIELD_UNKNOWN,
|
|
6916
|
+
where,
|
|
6917
|
+
path: `${path}${key.at}`,
|
|
6918
|
+
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)),
|
|
6919
|
+
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(", ")}.` : "")
|
|
6920
|
+
});
|
|
6921
|
+
continue;
|
|
6922
|
+
}
|
|
6923
|
+
const meta = target.fields[name];
|
|
6924
|
+
if ((0, import_data11.isVirtualSearchField)(meta)) {
|
|
6925
|
+
const vtype = meta?.type;
|
|
6926
|
+
findings.push({
|
|
6927
|
+
severity: "error",
|
|
6928
|
+
rule: SORT_FIELD_UNSORTABLE,
|
|
6929
|
+
where,
|
|
6930
|
+
path: `${path}${key.at}`,
|
|
6931
|
+
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.`,
|
|
6932
|
+
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.`
|
|
6933
|
+
});
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
return findings;
|
|
6937
|
+
}
|
|
6938
|
+
function validateSortableFields(stack) {
|
|
6939
|
+
const findings = [];
|
|
6940
|
+
if (!isRec16(stack)) return findings;
|
|
6941
|
+
const objects = Array.isArray(stack.objects) ? stack.objects : isRec16(stack.objects) ? Object.entries(stack.objects).map(([name, def]) => ({ name, ...def })) : [];
|
|
6942
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
6943
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
6944
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
6945
|
+
findings.push(
|
|
6946
|
+
...checkSortDeclaration(
|
|
6947
|
+
declared,
|
|
6948
|
+
objectName,
|
|
6949
|
+
fieldsByObject,
|
|
6950
|
+
where,
|
|
6951
|
+
path,
|
|
6952
|
+
subject,
|
|
6953
|
+
unprovisionedAnchors
|
|
6954
|
+
)
|
|
6955
|
+
);
|
|
6956
|
+
};
|
|
6957
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
6958
|
+
const obj = objects[oi];
|
|
6959
|
+
if (!isRec16(obj)) continue;
|
|
6960
|
+
const objName = strName11(obj.name);
|
|
6961
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
6962
|
+
if (isRec16(obj.listViews)) {
|
|
6963
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
6964
|
+
if (!isRec16(lv)) continue;
|
|
6965
|
+
check(
|
|
6966
|
+
lv.sort,
|
|
6967
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
6968
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
6969
|
+
listViewObject2(lv) ?? objName,
|
|
6970
|
+
`${label2} \u203A listViews.${key}`,
|
|
6971
|
+
`objects[${oi}].listViews.${key}.sort`,
|
|
6972
|
+
"list-view sort"
|
|
6973
|
+
);
|
|
6974
|
+
}
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
const views = Array.isArray(stack.views) ? stack.views : [];
|
|
6978
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
6979
|
+
const view = views[vi];
|
|
6980
|
+
if (!isRec16(view)) continue;
|
|
6981
|
+
const viewLabel2 = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
|
|
6982
|
+
const viewObject = strName11(view.objectName) ?? strName11(view.object);
|
|
6983
|
+
if (view.viewKind === "list" && !isRec16(view.config)) {
|
|
6984
|
+
check(
|
|
6985
|
+
view.sort,
|
|
6986
|
+
listViewObject2(view) ?? viewObject,
|
|
6987
|
+
`view "${viewLabel2}" (flattened list overlay)`,
|
|
6988
|
+
`views[${vi}].sort`,
|
|
6989
|
+
"list-view sort"
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
if (view.viewKind === "list" && isRec16(view.config)) {
|
|
6993
|
+
check(
|
|
6994
|
+
view.config.sort,
|
|
6995
|
+
listViewObject2(view.config) ?? viewObject,
|
|
6996
|
+
`view "${viewLabel2}" (ViewItem record)`,
|
|
6997
|
+
`views[${vi}].config.sort`,
|
|
6998
|
+
"list-view sort"
|
|
6999
|
+
);
|
|
7000
|
+
}
|
|
7001
|
+
if (isRec16(view.list)) {
|
|
7002
|
+
check(
|
|
7003
|
+
view.list.sort,
|
|
7004
|
+
listViewObject2(view.list) ?? viewObject,
|
|
7005
|
+
`view "${viewLabel2}" \u203A list`,
|
|
7006
|
+
`views[${vi}].list.sort`,
|
|
7007
|
+
"list-view sort"
|
|
7008
|
+
);
|
|
7009
|
+
}
|
|
7010
|
+
if (isRec16(view.listViews)) {
|
|
7011
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
7012
|
+
if (!isRec16(lv)) continue;
|
|
7013
|
+
check(
|
|
7014
|
+
lv.sort,
|
|
7015
|
+
listViewObject2(lv) ?? viewObject,
|
|
7016
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
7017
|
+
`views[${vi}].listViews.${key}.sort`,
|
|
7018
|
+
"list-view sort"
|
|
7019
|
+
);
|
|
7020
|
+
}
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
return findings;
|
|
7024
|
+
}
|
|
7025
|
+
function listViewObject2(listView) {
|
|
7026
|
+
const data = listView.data;
|
|
7027
|
+
return isRec16(data) ? strName11(data.object) : void 0;
|
|
7028
|
+
}
|
|
7029
|
+
|
|
6501
7030
|
// src/validate-action-name-refs.ts
|
|
6502
7031
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
6503
7032
|
function asArray28(v) {
|
|
@@ -6507,13 +7036,13 @@ function asArray28(v) {
|
|
|
6507
7036
|
}
|
|
6508
7037
|
return [];
|
|
6509
7038
|
}
|
|
6510
|
-
function
|
|
7039
|
+
function strName12(v) {
|
|
6511
7040
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6512
7041
|
}
|
|
6513
7042
|
function strList(v) {
|
|
6514
7043
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
6515
7044
|
}
|
|
6516
|
-
function
|
|
7045
|
+
function distance4(a, b) {
|
|
6517
7046
|
const m = a.length;
|
|
6518
7047
|
const n = b.length;
|
|
6519
7048
|
if (m === 0) return n;
|
|
@@ -6529,11 +7058,11 @@ function distance3(a, b) {
|
|
|
6529
7058
|
}
|
|
6530
7059
|
return prev[n];
|
|
6531
7060
|
}
|
|
6532
|
-
function
|
|
7061
|
+
function suggest5(target, known) {
|
|
6533
7062
|
let best;
|
|
6534
7063
|
let bestScore = Infinity;
|
|
6535
7064
|
for (const candidate of known) {
|
|
6536
|
-
const d =
|
|
7065
|
+
const d = distance4(target, candidate);
|
|
6537
7066
|
if (d < bestScore) {
|
|
6538
7067
|
bestScore = d;
|
|
6539
7068
|
best = candidate;
|
|
@@ -6545,13 +7074,13 @@ function suggest4(target, known) {
|
|
|
6545
7074
|
function collectActionNames(stack) {
|
|
6546
7075
|
const names = /* @__PURE__ */ new Set();
|
|
6547
7076
|
for (const action of asArray28(stack.actions)) {
|
|
6548
|
-
const n =
|
|
7077
|
+
const n = strName12(action?.name);
|
|
6549
7078
|
if (n) names.add(n);
|
|
6550
7079
|
}
|
|
6551
7080
|
for (const obj of asArray28(stack.objects)) {
|
|
6552
7081
|
if (!obj || typeof obj !== "object") continue;
|
|
6553
7082
|
for (const action of asArray28(obj.actions)) {
|
|
6554
|
-
const n =
|
|
7083
|
+
const n = strName12(action?.name);
|
|
6555
7084
|
if (n) names.add(n);
|
|
6556
7085
|
}
|
|
6557
7086
|
}
|
|
@@ -6568,7 +7097,7 @@ function validateActionNameRefs(stack) {
|
|
|
6568
7097
|
rule: ACTION_NAME_UNDEFINED,
|
|
6569
7098
|
where,
|
|
6570
7099
|
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.` +
|
|
7100
|
+
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
7101
|
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
7102
|
});
|
|
6574
7103
|
};
|
|
@@ -6594,7 +7123,7 @@ function validateActionNameRefs(stack) {
|
|
|
6594
7123
|
if (!def || typeof def !== "object") continue;
|
|
6595
7124
|
if (def.execution !== "aggregate") continue;
|
|
6596
7125
|
if (def.actionDef !== void 0) continue;
|
|
6597
|
-
const name =
|
|
7126
|
+
const name = strName12(def.name);
|
|
6598
7127
|
if (!name) continue;
|
|
6599
7128
|
check(
|
|
6600
7129
|
name,
|
|
@@ -6609,7 +7138,7 @@ function validateActionNameRefs(stack) {
|
|
|
6609
7138
|
for (let vi = 0; vi < views.length; vi++) {
|
|
6610
7139
|
const view = views[vi];
|
|
6611
7140
|
if (!view || typeof view !== "object") continue;
|
|
6612
|
-
const viewName =
|
|
7141
|
+
const viewName = strName12(view.name) ?? strName12(view.object) ?? `#${vi}`;
|
|
6613
7142
|
const owner = `view "${viewName}"`;
|
|
6614
7143
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
6615
7144
|
const listViews = view.listViews;
|
|
@@ -6625,7 +7154,7 @@ function validateActionNameRefs(stack) {
|
|
|
6625
7154
|
if (!obj || typeof obj !== "object") continue;
|
|
6626
7155
|
const objListViews = obj.listViews;
|
|
6627
7156
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
6628
|
-
const owner = `object "${
|
|
7157
|
+
const owner = `object "${strName12(obj.name) ?? `#${oi}`}"`;
|
|
6629
7158
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
6630
7159
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
6631
7160
|
}
|
|
@@ -6634,7 +7163,7 @@ function validateActionNameRefs(stack) {
|
|
|
6634
7163
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6635
7164
|
const page = pages[pi];
|
|
6636
7165
|
if (!page || typeof page !== "object") continue;
|
|
6637
|
-
const pageName =
|
|
7166
|
+
const pageName = strName12(page.name) ?? `#${pi}`;
|
|
6638
7167
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6639
7168
|
const props = component.properties;
|
|
6640
7169
|
if (!props || typeof props !== "object") continue;
|
|
@@ -6642,7 +7171,7 @@ function validateActionNameRefs(stack) {
|
|
|
6642
7171
|
for (let ai = 0; ai < names.length; ai++) {
|
|
6643
7172
|
check(
|
|
6644
7173
|
names[ai],
|
|
6645
|
-
`page "${pageName}" \xB7 component "${
|
|
7174
|
+
`page "${pageName}" \xB7 component "${strName12(component.type) ?? "?"}"`,
|
|
6646
7175
|
`${path}.properties.actionNames[${ai}]`,
|
|
6647
7176
|
"Quick-actions bar"
|
|
6648
7177
|
);
|
|
@@ -6653,7 +7182,7 @@ function validateActionNameRefs(stack) {
|
|
|
6653
7182
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
6654
7183
|
const app = apps[ai];
|
|
6655
7184
|
if (!app || typeof app !== "object") continue;
|
|
6656
|
-
const appName =
|
|
7185
|
+
const appName = strName12(app.name) ?? `#${ai}`;
|
|
6657
7186
|
const walkNav = (items, basePath) => {
|
|
6658
7187
|
const navItems = asArray28(items);
|
|
6659
7188
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
@@ -6661,20 +7190,20 @@ function validateActionNameRefs(stack) {
|
|
|
6661
7190
|
if (!nav || typeof nav !== "object") continue;
|
|
6662
7191
|
const navPath = `${basePath}[${ni}]`;
|
|
6663
7192
|
const actionDef = nav.actionDef;
|
|
6664
|
-
const actionName =
|
|
7193
|
+
const actionName = strName12(actionDef?.actionName);
|
|
6665
7194
|
if (nav.type === "action" && actionName) {
|
|
6666
7195
|
check(
|
|
6667
7196
|
actionName,
|
|
6668
|
-
`app "${appName}" \xB7 nav "${
|
|
7197
|
+
`app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
6669
7198
|
`${navPath}.actionDef.actionName`,
|
|
6670
7199
|
"Navigation action item"
|
|
6671
7200
|
);
|
|
6672
7201
|
}
|
|
6673
|
-
const runAction =
|
|
7202
|
+
const runAction = strName12(nav.runAction);
|
|
6674
7203
|
if (nav.type === "object" && runAction) {
|
|
6675
7204
|
check(
|
|
6676
7205
|
runAction,
|
|
6677
|
-
`app "${appName}" \xB7 nav "${
|
|
7206
|
+
`app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
6678
7207
|
`${navPath}.runAction`,
|
|
6679
7208
|
"Navigation deep-link auto-run"
|
|
6680
7209
|
);
|
|
@@ -6700,7 +7229,7 @@ function asArray29(v) {
|
|
|
6700
7229
|
}
|
|
6701
7230
|
return [];
|
|
6702
7231
|
}
|
|
6703
|
-
function
|
|
7232
|
+
function strName13(v) {
|
|
6704
7233
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6705
7234
|
}
|
|
6706
7235
|
function strList2(v) {
|
|
@@ -6715,7 +7244,7 @@ function collectNamePlacedActions(stack) {
|
|
|
6715
7244
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
6716
7245
|
}
|
|
6717
7246
|
for (const def of asArray29(list3.bulkActionDefs)) {
|
|
6718
|
-
const n =
|
|
7247
|
+
const n = strName13(def?.name);
|
|
6719
7248
|
if (n) placed.add(n);
|
|
6720
7249
|
}
|
|
6721
7250
|
};
|
|
@@ -6741,7 +7270,7 @@ function validateActionLocations(stack) {
|
|
|
6741
7270
|
const check = (action, path) => {
|
|
6742
7271
|
if (!action || typeof action !== "object") return;
|
|
6743
7272
|
if ("locations" in action) return;
|
|
6744
|
-
const name =
|
|
7273
|
+
const name = strName13(action.name);
|
|
6745
7274
|
if (!name) return;
|
|
6746
7275
|
if (namePlaced.has(name)) return;
|
|
6747
7276
|
findings.push({
|
|
@@ -6766,14 +7295,14 @@ function validateActionLocations(stack) {
|
|
|
6766
7295
|
}
|
|
6767
7296
|
|
|
6768
7297
|
// src/validate-component-props.ts
|
|
6769
|
-
var
|
|
7298
|
+
var import_ui4 = require("@objectstack/spec/ui");
|
|
6770
7299
|
var import_spec3 = require("@objectstack/spec");
|
|
6771
7300
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
6772
7301
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
6773
|
-
function
|
|
7302
|
+
function isRec17(v) {
|
|
6774
7303
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6775
7304
|
}
|
|
6776
|
-
function
|
|
7305
|
+
function strName14(v) {
|
|
6777
7306
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6778
7307
|
}
|
|
6779
7308
|
function asArray30(v) {
|
|
@@ -6783,12 +7312,12 @@ function asArray30(v) {
|
|
|
6783
7312
|
}
|
|
6784
7313
|
return [];
|
|
6785
7314
|
}
|
|
6786
|
-
var PROPS_SCHEMAS =
|
|
7315
|
+
var PROPS_SCHEMAS = import_ui4.ComponentPropsMap;
|
|
6787
7316
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
6788
7317
|
function suppliedByDataSource(issue, component) {
|
|
6789
7318
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
6790
|
-
const dataSource =
|
|
6791
|
-
return
|
|
7319
|
+
const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
|
|
7320
|
+
return strName14(dataSource?.object) !== void 0;
|
|
6792
7321
|
}
|
|
6793
7322
|
function unrecognizedKeysFromUnionArm(issue) {
|
|
6794
7323
|
if (issue.code !== "invalid_union") return void 0;
|
|
@@ -6805,18 +7334,18 @@ function unrecognizedKeysFromUnionArm(issue) {
|
|
|
6805
7334
|
}
|
|
6806
7335
|
function validateComponentProps(stack) {
|
|
6807
7336
|
const findings = [];
|
|
6808
|
-
if (!
|
|
7337
|
+
if (!isRec17(stack)) return findings;
|
|
6809
7338
|
const pages = asArray30(stack.pages);
|
|
6810
7339
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6811
7340
|
const page = pages[pi];
|
|
6812
|
-
if (!
|
|
6813
|
-
const pageName =
|
|
7341
|
+
if (!isRec17(page)) continue;
|
|
7342
|
+
const pageName = strName14(page.name) ?? `#${pi}`;
|
|
6814
7343
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6815
|
-
const type =
|
|
7344
|
+
const type = strName14(component.type);
|
|
6816
7345
|
if (!type) continue;
|
|
6817
7346
|
const schema = PROPS_SCHEMAS[type];
|
|
6818
7347
|
if (!schema) continue;
|
|
6819
|
-
const props =
|
|
7348
|
+
const props = isRec17(component.properties) ? component.properties : void 0;
|
|
6820
7349
|
if (!props) continue;
|
|
6821
7350
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
6822
7351
|
const base = `${path}.properties`;
|
|
@@ -6888,16 +7417,16 @@ function asArray31(v) {
|
|
|
6888
7417
|
}
|
|
6889
7418
|
return [];
|
|
6890
7419
|
}
|
|
6891
|
-
function
|
|
7420
|
+
function strName15(v) {
|
|
6892
7421
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6893
7422
|
}
|
|
6894
7423
|
function strList3(v) {
|
|
6895
7424
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
6896
7425
|
}
|
|
6897
|
-
function
|
|
7426
|
+
function isRec18(v) {
|
|
6898
7427
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6899
7428
|
}
|
|
6900
|
-
function
|
|
7429
|
+
function distance5(a, b) {
|
|
6901
7430
|
const m = a.length;
|
|
6902
7431
|
const n = b.length;
|
|
6903
7432
|
if (m === 0) return n;
|
|
@@ -6913,11 +7442,11 @@ function distance4(a, b) {
|
|
|
6913
7442
|
}
|
|
6914
7443
|
return prev[n];
|
|
6915
7444
|
}
|
|
6916
|
-
function
|
|
7445
|
+
function suggest6(target, known) {
|
|
6917
7446
|
let best;
|
|
6918
7447
|
let bestScore = Infinity;
|
|
6919
7448
|
for (const c of known) {
|
|
6920
|
-
const d =
|
|
7449
|
+
const d = distance5(target, c);
|
|
6921
7450
|
if (d < bestScore) {
|
|
6922
7451
|
bestScore = d;
|
|
6923
7452
|
best = c;
|
|
@@ -6933,16 +7462,16 @@ function list2(names) {
|
|
|
6933
7462
|
function indexDatasets(stack) {
|
|
6934
7463
|
const out = /* @__PURE__ */ new Map();
|
|
6935
7464
|
for (const ds of asArray31(stack.datasets)) {
|
|
6936
|
-
const name =
|
|
7465
|
+
const name = strName15(ds.name);
|
|
6937
7466
|
if (!name) continue;
|
|
6938
7467
|
const dimensions = /* @__PURE__ */ new Set();
|
|
6939
7468
|
for (const d of asArray31(ds.dimensions)) {
|
|
6940
|
-
const n =
|
|
7469
|
+
const n = strName15(d.name);
|
|
6941
7470
|
if (n) dimensions.add(n);
|
|
6942
7471
|
}
|
|
6943
7472
|
const measures = /* @__PURE__ */ new Set();
|
|
6944
7473
|
for (const m of asArray31(ds.measures)) {
|
|
6945
|
-
const n =
|
|
7474
|
+
const n = strName15(m.name);
|
|
6946
7475
|
if (n) measures.add(n);
|
|
6947
7476
|
}
|
|
6948
7477
|
out.set(name, { dimensions, measures });
|
|
@@ -6965,7 +7494,7 @@ function validateChartBindings(stack) {
|
|
|
6965
7494
|
where: binding.where,
|
|
6966
7495
|
path: `${binding.path}.dataset`,
|
|
6967
7496
|
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())}.${
|
|
7497
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest6(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
6969
7498
|
});
|
|
6970
7499
|
return;
|
|
6971
7500
|
}
|
|
@@ -6977,7 +7506,7 @@ function validateChartBindings(stack) {
|
|
|
6977
7506
|
where: binding.where,
|
|
6978
7507
|
path,
|
|
6979
7508
|
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)}.${
|
|
7509
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest6(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
6981
7510
|
});
|
|
6982
7511
|
};
|
|
6983
7512
|
const measureRef = (name, path, selected2) => {
|
|
@@ -6988,7 +7517,7 @@ function validateChartBindings(stack) {
|
|
|
6988
7517
|
where: binding.where,
|
|
6989
7518
|
path,
|
|
6990
7519
|
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)}.${
|
|
7520
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest6(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
6992
7521
|
});
|
|
6993
7522
|
return;
|
|
6994
7523
|
}
|
|
@@ -7023,26 +7552,26 @@ function validateChartBindings(stack) {
|
|
|
7023
7552
|
const reports = asArray31(stack.reports);
|
|
7024
7553
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
7025
7554
|
const report = reports[ri];
|
|
7026
|
-
if (!
|
|
7027
|
-
const reportName =
|
|
7555
|
+
if (!isRec18(report)) continue;
|
|
7556
|
+
const reportName = strName15(report.name) ?? `#${ri}`;
|
|
7028
7557
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
7029
|
-
if (!
|
|
7558
|
+
if (!isRec18(chart)) return;
|
|
7030
7559
|
check({
|
|
7031
7560
|
dataset,
|
|
7032
7561
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
7033
7562
|
// it in lets the yAxis "declared but not selected" check work without
|
|
7034
7563
|
// reporting the selection itself twice.
|
|
7035
7564
|
values: { names: values, path: `${path}.values` },
|
|
7036
|
-
xAxis:
|
|
7037
|
-
yAxis:
|
|
7038
|
-
series: asArray31(chart.series).map((s, si) => ({ name:
|
|
7565
|
+
xAxis: strName15(chart.xAxis) ? { name: strName15(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
7566
|
+
yAxis: strName15(chart.yAxis) ? { name: strName15(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
7567
|
+
series: asArray31(chart.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
7039
7568
|
where,
|
|
7040
7569
|
path: `${path}.chart`
|
|
7041
7570
|
});
|
|
7042
7571
|
};
|
|
7043
7572
|
checkReportChart(
|
|
7044
7573
|
report.chart,
|
|
7045
|
-
|
|
7574
|
+
strName15(report.dataset),
|
|
7046
7575
|
strList3(report.values),
|
|
7047
7576
|
`report "${reportName}" \xB7 chart`,
|
|
7048
7577
|
`reports[${ri}]`
|
|
@@ -7050,22 +7579,22 @@ function validateChartBindings(stack) {
|
|
|
7050
7579
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
7051
7580
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
7052
7581
|
const block = blocks[bi];
|
|
7053
|
-
if (!
|
|
7582
|
+
if (!isRec18(block)) continue;
|
|
7054
7583
|
checkReportChart(
|
|
7055
7584
|
block.chart,
|
|
7056
|
-
|
|
7585
|
+
strName15(block.dataset),
|
|
7057
7586
|
strList3(block.values),
|
|
7058
|
-
`report "${reportName}" \xB7 block "${
|
|
7587
|
+
`report "${reportName}" \xB7 block "${strName15(block.name) ?? `#${bi}`}" chart`,
|
|
7059
7588
|
`reports[${ri}].blocks[${bi}]`
|
|
7060
7589
|
);
|
|
7061
7590
|
}
|
|
7062
7591
|
}
|
|
7063
7592
|
const checkListChart = (container, where, path) => {
|
|
7064
|
-
if (!
|
|
7593
|
+
if (!isRec18(container)) return;
|
|
7065
7594
|
const chart = container.chart;
|
|
7066
|
-
if (!
|
|
7595
|
+
if (!isRec18(chart)) return;
|
|
7067
7596
|
check({
|
|
7068
|
-
dataset:
|
|
7597
|
+
dataset: strName15(chart.dataset),
|
|
7069
7598
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
7070
7599
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
7071
7600
|
where,
|
|
@@ -7075,10 +7604,10 @@ function validateChartBindings(stack) {
|
|
|
7075
7604
|
const views = asArray31(stack.views);
|
|
7076
7605
|
for (let vi = 0; vi < views.length; vi++) {
|
|
7077
7606
|
const view = views[vi];
|
|
7078
|
-
if (!
|
|
7079
|
-
const viewName =
|
|
7607
|
+
if (!isRec18(view)) continue;
|
|
7608
|
+
const viewName = strName15(view.name) ?? strName15(view.objectName) ?? `#${vi}`;
|
|
7080
7609
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
7081
|
-
if (
|
|
7610
|
+
if (isRec18(view.listViews)) {
|
|
7082
7611
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
7083
7612
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
7084
7613
|
}
|
|
@@ -7087,8 +7616,8 @@ function validateChartBindings(stack) {
|
|
|
7087
7616
|
const objects = asArray31(stack.objects);
|
|
7088
7617
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7089
7618
|
const obj = objects[oi];
|
|
7090
|
-
if (!
|
|
7091
|
-
const objName =
|
|
7619
|
+
if (!isRec18(obj) || !isRec18(obj.listViews)) continue;
|
|
7620
|
+
const objName = strName15(obj.name) ?? `#${oi}`;
|
|
7092
7621
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
7093
7622
|
checkListChart(
|
|
7094
7623
|
lv,
|
|
@@ -7100,19 +7629,19 @@ function validateChartBindings(stack) {
|
|
|
7100
7629
|
const pages = asArray31(stack.pages);
|
|
7101
7630
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
7102
7631
|
const page = pages[pi];
|
|
7103
|
-
if (!
|
|
7104
|
-
const pageName =
|
|
7632
|
+
if (!isRec18(page)) continue;
|
|
7633
|
+
const pageName = strName15(page.name) ?? `#${pi}`;
|
|
7105
7634
|
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:
|
|
7635
|
+
const props = isRec18(component.properties) ? component.properties : void 0;
|
|
7636
|
+
if (!props || !strName15(props.dataset)) continue;
|
|
7637
|
+
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName15(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
7638
|
+
const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName15(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
7110
7639
|
check({
|
|
7111
|
-
dataset:
|
|
7640
|
+
dataset: strName15(props.dataset),
|
|
7112
7641
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
7113
7642
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
7114
7643
|
series: [...axisRefs, ...seriesRefs],
|
|
7115
|
-
where: `page "${pageName}" \xB7 ${
|
|
7644
|
+
where: `page "${pageName}" \xB7 ${strName15(component.type) ?? "chart"}`,
|
|
7116
7645
|
path: `${path}.properties`
|
|
7117
7646
|
});
|
|
7118
7647
|
}
|
|
@@ -7126,11 +7655,11 @@ var import_meta4 = {};
|
|
|
7126
7655
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7127
7656
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7128
7657
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7129
|
-
var
|
|
7658
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7130
7659
|
function asArray32(v) {
|
|
7131
|
-
if (Array.isArray(v)) return v.filter(
|
|
7132
|
-
if (
|
|
7133
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7660
|
+
if (Array.isArray(v)) return v.filter(isRec19);
|
|
7661
|
+
if (isRec19(v)) {
|
|
7662
|
+
return Object.entries(v).filter(([, def]) => isRec19(def)).map(([name, def]) => ({ name, ...def }));
|
|
7134
7663
|
}
|
|
7135
7664
|
return [];
|
|
7136
7665
|
}
|
|
@@ -7147,7 +7676,7 @@ function loadAjv() {
|
|
|
7147
7676
|
`@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
7677
|
);
|
|
7149
7678
|
}
|
|
7150
|
-
const ctor =
|
|
7679
|
+
const ctor = isRec19(mod) && "default" in mod ? mod.default : mod;
|
|
7151
7680
|
cachedAjv = ctor;
|
|
7152
7681
|
return ctor;
|
|
7153
7682
|
}
|
|
@@ -7162,7 +7691,7 @@ function loadAddFormats() {
|
|
|
7162
7691
|
`@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
7692
|
);
|
|
7164
7693
|
}
|
|
7165
|
-
const plugin =
|
|
7694
|
+
const plugin = isRec19(mod) && "default" in mod ? mod.default : mod;
|
|
7166
7695
|
cachedAddFormats = plugin;
|
|
7167
7696
|
return plugin;
|
|
7168
7697
|
}
|
|
@@ -7192,13 +7721,13 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
7192
7721
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
7193
7722
|
for (const branch of ["then", "otherwise"]) {
|
|
7194
7723
|
const nested = rule[branch];
|
|
7195
|
-
if (
|
|
7724
|
+
if (isRec19(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
7196
7725
|
}
|
|
7197
7726
|
return out;
|
|
7198
7727
|
}
|
|
7199
7728
|
function walkObjectValidationRules(stack) {
|
|
7200
7729
|
const walked = [];
|
|
7201
|
-
if (!
|
|
7730
|
+
if (!isRec19(stack)) return walked;
|
|
7202
7731
|
for (const obj of asArray32(stack.objects)) {
|
|
7203
7732
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7204
7733
|
const validations = obj.validations;
|
|
@@ -7233,7 +7762,7 @@ function validateRuleCompilability(stack) {
|
|
|
7233
7762
|
});
|
|
7234
7763
|
}
|
|
7235
7764
|
}
|
|
7236
|
-
if (rule.type === "json_schema" &&
|
|
7765
|
+
if (rule.type === "json_schema" && isRec19(rule.schema)) {
|
|
7237
7766
|
try {
|
|
7238
7767
|
createRuntimeAjv().compile(rule.schema);
|
|
7239
7768
|
} catch (err) {
|
|
@@ -7253,7 +7782,7 @@ function validateRuleCompilability(stack) {
|
|
|
7253
7782
|
|
|
7254
7783
|
// src/validate-rule-schema-formats.ts
|
|
7255
7784
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7256
|
-
var
|
|
7785
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7257
7786
|
var SUBSCHEMA_KEYS = [
|
|
7258
7787
|
"additionalItems",
|
|
7259
7788
|
"additionalProperties",
|
|
@@ -7277,7 +7806,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7277
7806
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7278
7807
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7279
7808
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7280
|
-
if (!
|
|
7809
|
+
if (!isRec20(schema)) return;
|
|
7281
7810
|
if (typeof schema.format === "string") {
|
|
7282
7811
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7283
7812
|
}
|
|
@@ -7296,7 +7825,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7296
7825
|
}
|
|
7297
7826
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7298
7827
|
const value = schema[key];
|
|
7299
|
-
if (!
|
|
7828
|
+
if (!isRec20(value)) continue;
|
|
7300
7829
|
for (const [name, entry] of Object.entries(value)) {
|
|
7301
7830
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7302
7831
|
}
|
|
@@ -7304,13 +7833,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7304
7833
|
const items = schema.items;
|
|
7305
7834
|
if (Array.isArray(items)) {
|
|
7306
7835
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7307
|
-
} else if (
|
|
7836
|
+
} else if (isRec20(items)) {
|
|
7308
7837
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7309
7838
|
}
|
|
7310
7839
|
const dependencies = schema.dependencies;
|
|
7311
|
-
if (
|
|
7840
|
+
if (isRec20(dependencies)) {
|
|
7312
7841
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7313
|
-
if (!
|
|
7842
|
+
if (!isRec20(entry)) continue;
|
|
7314
7843
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7315
7844
|
}
|
|
7316
7845
|
}
|
|
@@ -7337,9 +7866,9 @@ function nearestRegisteredFormat(name, registered) {
|
|
|
7337
7866
|
let best = null;
|
|
7338
7867
|
let bestDistance = Number.POSITIVE_INFINITY;
|
|
7339
7868
|
for (const candidate of [...registered].sort()) {
|
|
7340
|
-
const
|
|
7341
|
-
if (
|
|
7342
|
-
bestDistance =
|
|
7869
|
+
const distance8 = editDistance2(authored, candidate);
|
|
7870
|
+
if (distance8 < bestDistance) {
|
|
7871
|
+
bestDistance = distance8;
|
|
7343
7872
|
best = candidate;
|
|
7344
7873
|
}
|
|
7345
7874
|
}
|
|
@@ -7349,7 +7878,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7349
7878
|
const findings = [];
|
|
7350
7879
|
const pending = [];
|
|
7351
7880
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7352
|
-
if (rule.type !== "json_schema" || !
|
|
7881
|
+
if (rule.type !== "json_schema" || !isRec20(rule.schema)) continue;
|
|
7353
7882
|
const uses = [];
|
|
7354
7883
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7355
7884
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7474,7 +8003,7 @@ function asArray34(v) {
|
|
|
7474
8003
|
}
|
|
7475
8004
|
return [];
|
|
7476
8005
|
}
|
|
7477
|
-
function
|
|
8006
|
+
function strName16(v) {
|
|
7478
8007
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7479
8008
|
}
|
|
7480
8009
|
function collectNavExposures(stack) {
|
|
@@ -7483,18 +8012,18 @@ function collectNavExposures(stack) {
|
|
|
7483
8012
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
7484
8013
|
const app = apps[ai];
|
|
7485
8014
|
if (!app || typeof app !== "object") continue;
|
|
7486
|
-
const appName =
|
|
8015
|
+
const appName = strName16(app.name) ?? `#${ai}`;
|
|
7487
8016
|
const walk = (items, basePath) => {
|
|
7488
8017
|
const navItems = asArray34(items);
|
|
7489
8018
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
7490
8019
|
const nav = navItems[ni];
|
|
7491
8020
|
if (!nav || typeof nav !== "object") continue;
|
|
7492
8021
|
const navPath = `${basePath}[${ni}]`;
|
|
7493
|
-
const objectName =
|
|
8022
|
+
const objectName = strName16(nav.objectName);
|
|
7494
8023
|
if (nav.type === "object" && objectName) {
|
|
7495
8024
|
out.push({
|
|
7496
8025
|
objectName,
|
|
7497
|
-
where: `app "${appName}" \xB7 nav "${
|
|
8026
|
+
where: `app "${appName}" \xB7 nav "${strName16(nav.id) ?? `#${ni}`}"`,
|
|
7498
8027
|
path: `${navPath}.objectName`
|
|
7499
8028
|
});
|
|
7500
8029
|
}
|
|
@@ -7518,7 +8047,7 @@ function validateNavAccess(stack) {
|
|
|
7518
8047
|
if (exposures.length === 0) return findings;
|
|
7519
8048
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
7520
8049
|
for (const obj of asArray34(stack.objects)) {
|
|
7521
|
-
const n =
|
|
8050
|
+
const n = strName16(obj.name);
|
|
7522
8051
|
if (n) ownObjects.add(n);
|
|
7523
8052
|
}
|
|
7524
8053
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -7551,18 +8080,18 @@ var import_spec4 = require("@objectstack/spec");
|
|
|
7551
8080
|
var import_system4 = require("@objectstack/spec/system");
|
|
7552
8081
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
7553
8082
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
7554
|
-
function
|
|
8083
|
+
function isRec21(v) {
|
|
7555
8084
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7556
8085
|
}
|
|
7557
8086
|
function asArray35(v) {
|
|
7558
8087
|
if (Array.isArray(v)) return v;
|
|
7559
|
-
if (
|
|
8088
|
+
if (isRec21(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec21(def) ? def : {} }));
|
|
7560
8089
|
return [];
|
|
7561
8090
|
}
|
|
7562
|
-
function
|
|
8091
|
+
function strName17(v) {
|
|
7563
8092
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7564
8093
|
}
|
|
7565
|
-
function
|
|
8094
|
+
function distance6(a, b) {
|
|
7566
8095
|
const m = a.length;
|
|
7567
8096
|
const n = b.length;
|
|
7568
8097
|
if (m === 0) return n;
|
|
@@ -7578,7 +8107,7 @@ function distance5(a, b) {
|
|
|
7578
8107
|
}
|
|
7579
8108
|
return prev[n];
|
|
7580
8109
|
}
|
|
7581
|
-
function
|
|
8110
|
+
function suggest7(target, known) {
|
|
7582
8111
|
const names = [...known];
|
|
7583
8112
|
const segmentMatch = names.find(
|
|
7584
8113
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -7587,7 +8116,7 @@ function suggest6(target, known) {
|
|
|
7587
8116
|
let best;
|
|
7588
8117
|
let bestScore = Infinity;
|
|
7589
8118
|
for (const candidate of names) {
|
|
7590
|
-
const d =
|
|
8119
|
+
const d = distance6(target, candidate);
|
|
7591
8120
|
if (d < bestScore) {
|
|
7592
8121
|
bestScore = d;
|
|
7593
8122
|
best = candidate;
|
|
@@ -7620,33 +8149,33 @@ function collectViewRecord(view, factsFor) {
|
|
|
7620
8149
|
const addSections = (container, binding) => {
|
|
7621
8150
|
if (!binding) return;
|
|
7622
8151
|
for (const section of asArray35(container.sections)) {
|
|
7623
|
-
const sectionName =
|
|
8152
|
+
const sectionName = strName17(section.name);
|
|
7624
8153
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
7625
8154
|
}
|
|
7626
8155
|
};
|
|
7627
|
-
const listBinding =
|
|
7628
|
-
if (
|
|
7629
|
-
addView(recordObject ?? listBinding,
|
|
8156
|
+
const listBinding = isRec21(view.list) ? bindingOf(view.list) : void 0;
|
|
8157
|
+
if (isRec21(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
8158
|
+
addView(recordObject ?? listBinding, strName17(view.name));
|
|
7630
8159
|
const named = namedViewKeys(view);
|
|
7631
8160
|
for (const family of ["listViews", "formViews"]) {
|
|
7632
8161
|
const container = view[family];
|
|
7633
|
-
if (!
|
|
8162
|
+
if (!isRec21(container)) continue;
|
|
7634
8163
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
7635
8164
|
let at = 0;
|
|
7636
8165
|
for (const sub of Object.values(container)) {
|
|
7637
8166
|
if (!sub || typeof sub !== "object") continue;
|
|
7638
8167
|
const registryKey = registryKeys[at++];
|
|
7639
|
-
if (!
|
|
8168
|
+
if (!isRec21(sub)) continue;
|
|
7640
8169
|
const binding = bindingOf(sub) ?? listBinding;
|
|
7641
8170
|
addView(binding, registryKey);
|
|
7642
8171
|
addSections(sub, binding);
|
|
7643
8172
|
}
|
|
7644
8173
|
}
|
|
7645
|
-
if (
|
|
8174
|
+
if (isRec21(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
7646
8175
|
addSections(view, recordObject ?? listBinding);
|
|
7647
8176
|
}
|
|
7648
8177
|
function defaultListViewKey(object, container) {
|
|
7649
|
-
if (!object || !
|
|
8178
|
+
if (!object || !isRec21(container.list)) return void 0;
|
|
7650
8179
|
const item = (0, import_spec4.expandViewContainer)(object, container).find(
|
|
7651
8180
|
(i) => i.viewKind === "list" && i.isDefault
|
|
7652
8181
|
);
|
|
@@ -7658,7 +8187,7 @@ function namedViewKeys(container) {
|
|
|
7658
8187
|
const object = "probe";
|
|
7659
8188
|
const prefix = `${object}.`;
|
|
7660
8189
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
7661
|
-
const countEntries = (v) =>
|
|
8190
|
+
const countEntries = (v) => isRec21(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
7662
8191
|
const listCount = countEntries(container.listViews);
|
|
7663
8192
|
const formCount = countEntries(container.formViews);
|
|
7664
8193
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -7676,14 +8205,14 @@ function readOptions(field) {
|
|
|
7676
8205
|
values.add(opt);
|
|
7677
8206
|
continue;
|
|
7678
8207
|
}
|
|
7679
|
-
if (!
|
|
7680
|
-
const value =
|
|
8208
|
+
if (!isRec21(opt)) continue;
|
|
8209
|
+
const value = strName17(opt.value);
|
|
7681
8210
|
if (!value) continue;
|
|
7682
8211
|
values.add(value);
|
|
7683
|
-
const label2 =
|
|
8212
|
+
const label2 = strName17(opt.label);
|
|
7684
8213
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
7685
8214
|
}
|
|
7686
|
-
} else if (
|
|
8215
|
+
} else if (isRec21(raw)) {
|
|
7687
8216
|
for (const [value, label2] of Object.entries(raw)) {
|
|
7688
8217
|
values.add(value);
|
|
7689
8218
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -7704,23 +8233,23 @@ function buildUniverse(stack) {
|
|
|
7704
8233
|
return facts;
|
|
7705
8234
|
};
|
|
7706
8235
|
for (const obj of asArray35(stack.objects)) {
|
|
7707
|
-
const objectName =
|
|
8236
|
+
const objectName = strName17(obj.name);
|
|
7708
8237
|
if (!objectName) continue;
|
|
7709
8238
|
const facts = factsFor(objectName);
|
|
7710
8239
|
for (const field of asArray35(obj.fields)) {
|
|
7711
|
-
const fieldName =
|
|
8240
|
+
const fieldName = strName17(field.name);
|
|
7712
8241
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
7713
8242
|
}
|
|
7714
8243
|
for (const action of asArray35(obj.actions)) {
|
|
7715
|
-
const actionName =
|
|
8244
|
+
const actionName = strName17(action.name);
|
|
7716
8245
|
if (actionName) facts.actions.set(actionName, action);
|
|
7717
8246
|
}
|
|
7718
8247
|
for (const view of asArray35(obj.views)) {
|
|
7719
|
-
collectViewRecord({ ...view, object:
|
|
8248
|
+
collectViewRecord({ ...view, object: strName17(view.object) ?? objectName }, factsFor);
|
|
7720
8249
|
}
|
|
7721
8250
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
7722
8251
|
for (const group of asArray35(obj.fieldGroups)) {
|
|
7723
|
-
const key =
|
|
8252
|
+
const key = strName17(group.key) ?? strName17(group.name);
|
|
7724
8253
|
if (key) facts.sections.add(key);
|
|
7725
8254
|
}
|
|
7726
8255
|
}
|
|
@@ -7731,10 +8260,10 @@ function buildUniverse(stack) {
|
|
|
7731
8260
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
7732
8261
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
7733
8262
|
if (!walked.objectName) continue;
|
|
7734
|
-
const props =
|
|
8263
|
+
const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
|
|
7735
8264
|
if (!props) continue;
|
|
7736
8265
|
for (const section of asArray35(props.sections)) {
|
|
7737
|
-
const sectionName =
|
|
8266
|
+
const sectionName = strName17(section.name);
|
|
7738
8267
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
7739
8268
|
}
|
|
7740
8269
|
}
|
|
@@ -7742,9 +8271,9 @@ function buildUniverse(stack) {
|
|
|
7742
8271
|
const globalActions = /* @__PURE__ */ new Map();
|
|
7743
8272
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
7744
8273
|
for (const action of asArray35(stack.actions)) {
|
|
7745
|
-
const actionName =
|
|
8274
|
+
const actionName = strName17(action.name);
|
|
7746
8275
|
if (!actionName) continue;
|
|
7747
|
-
const owner =
|
|
8276
|
+
const owner = strName17(action.objectName) ?? strName17(action.object);
|
|
7748
8277
|
if (owner) {
|
|
7749
8278
|
factsFor(owner).actions.set(actionName, action);
|
|
7750
8279
|
actionOwners.set(actionName, owner);
|
|
@@ -7759,19 +8288,19 @@ function buildUniverse(stack) {
|
|
|
7759
8288
|
}
|
|
7760
8289
|
const apps = /* @__PURE__ */ new Map();
|
|
7761
8290
|
for (const app of asArray35(stack.apps)) {
|
|
7762
|
-
const appName =
|
|
8291
|
+
const appName = strName17(app.name);
|
|
7763
8292
|
if (!appName) continue;
|
|
7764
8293
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
7765
8294
|
const walkNav = (items) => {
|
|
7766
8295
|
for (const item of asArray35(items)) {
|
|
7767
|
-
const id =
|
|
8296
|
+
const id = strName17(item.id);
|
|
7768
8297
|
if (id) navIds.add(id);
|
|
7769
8298
|
if (item.children) walkNav(item.children);
|
|
7770
8299
|
}
|
|
7771
8300
|
};
|
|
7772
8301
|
walkNav(app.navigation);
|
|
7773
8302
|
for (const area of asArray35(app.areas)) {
|
|
7774
|
-
const areaId =
|
|
8303
|
+
const areaId = strName17(area.id);
|
|
7775
8304
|
if (areaId) navIds.add(areaId);
|
|
7776
8305
|
walkNav(area.navigation);
|
|
7777
8306
|
}
|
|
@@ -7779,20 +8308,20 @@ function buildUniverse(stack) {
|
|
|
7779
8308
|
}
|
|
7780
8309
|
const dashboards = /* @__PURE__ */ new Map();
|
|
7781
8310
|
for (const dash of asArray35(stack.dashboards)) {
|
|
7782
|
-
const dashName =
|
|
8311
|
+
const dashName = strName17(dash.name);
|
|
7783
8312
|
if (!dashName) continue;
|
|
7784
8313
|
const widgets = /* @__PURE__ */ new Set();
|
|
7785
8314
|
for (const widget of asArray35(dash.widgets)) {
|
|
7786
|
-
const id =
|
|
8315
|
+
const id = strName17(widget.id) ?? strName17(widget.name);
|
|
7787
8316
|
if (id) widgets.add(id);
|
|
7788
8317
|
}
|
|
7789
8318
|
const actions = /* @__PURE__ */ new Set();
|
|
7790
8319
|
const headerActions = [
|
|
7791
|
-
...asArray35(
|
|
8320
|
+
...asArray35(isRec21(dash.header) ? dash.header.actions : void 0),
|
|
7792
8321
|
...asArray35(dash.actions)
|
|
7793
8322
|
];
|
|
7794
8323
|
for (const action of headerActions) {
|
|
7795
|
-
const key =
|
|
8324
|
+
const key = strName17(action.actionUrl) ?? strName17(action.url) ?? strName17(action.name);
|
|
7796
8325
|
if (key) actions.add(key);
|
|
7797
8326
|
}
|
|
7798
8327
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -7804,7 +8333,7 @@ function localePath(bundleIndex, locale) {
|
|
|
7804
8333
|
}
|
|
7805
8334
|
function validateTranslationReferences(stack) {
|
|
7806
8335
|
const findings = [];
|
|
7807
|
-
if (!
|
|
8336
|
+
if (!isRec21(stack)) return findings;
|
|
7808
8337
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
7809
8338
|
if (bundles.length === 0) return findings;
|
|
7810
8339
|
const universe = buildUniverse(stack);
|
|
@@ -7813,13 +8342,13 @@ function validateTranslationReferences(stack) {
|
|
|
7813
8342
|
};
|
|
7814
8343
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
7815
8344
|
const bundle = bundles[bi];
|
|
7816
|
-
if (!
|
|
8345
|
+
if (!isRec21(bundle)) continue;
|
|
7817
8346
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
7818
|
-
if (!
|
|
8347
|
+
if (!isRec21(rawData)) continue;
|
|
7819
8348
|
const base = localePath(bi, locale);
|
|
7820
8349
|
const inLocale = `locale "${locale}"`;
|
|
7821
8350
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
7822
|
-
if (!
|
|
8351
|
+
if (!isRec21(rawNode)) continue;
|
|
7823
8352
|
const objPath = `${base}.objects.${objectName}`;
|
|
7824
8353
|
const facts = universe.objects.get(objectName);
|
|
7825
8354
|
if (!facts) {
|
|
@@ -7827,7 +8356,7 @@ function validateTranslationReferences(stack) {
|
|
|
7827
8356
|
orphan(
|
|
7828
8357
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
7829
8358
|
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.` +
|
|
8359
|
+
(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
8360
|
`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
8361
|
);
|
|
7833
8362
|
continue;
|
|
@@ -7840,12 +8369,12 @@ function validateTranslationReferences(stack) {
|
|
|
7840
8369
|
orphan(
|
|
7841
8370
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
7842
8371
|
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.` +
|
|
8372
|
+
`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
8373
|
`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
8374
|
);
|
|
7846
8375
|
continue;
|
|
7847
8376
|
}
|
|
7848
|
-
if (!
|
|
8377
|
+
if (!isRec21(rawField)) continue;
|
|
7849
8378
|
checkOptionKeys(findings, {
|
|
7850
8379
|
optionMap: rawField.options,
|
|
7851
8380
|
field,
|
|
@@ -7860,7 +8389,7 @@ function validateTranslationReferences(stack) {
|
|
|
7860
8389
|
orphan(
|
|
7861
8390
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
7862
8391
|
`${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.` +
|
|
8392
|
+
`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
8393
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
7865
8394
|
);
|
|
7866
8395
|
}
|
|
@@ -7869,7 +8398,7 @@ function validateTranslationReferences(stack) {
|
|
|
7869
8398
|
orphan(
|
|
7870
8399
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
7871
8400
|
`${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.` +
|
|
8401
|
+
`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
8402
|
`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
8403
|
);
|
|
7875
8404
|
}
|
|
@@ -7880,7 +8409,7 @@ function validateTranslationReferences(stack) {
|
|
|
7880
8409
|
orphan(
|
|
7881
8410
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
7882
8411
|
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.` +
|
|
8412
|
+
`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
8413
|
`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
8414
|
);
|
|
7886
8415
|
continue;
|
|
@@ -7902,7 +8431,7 @@ function validateTranslationReferences(stack) {
|
|
|
7902
8431
|
orphan(
|
|
7903
8432
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
7904
8433
|
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.` +
|
|
8434
|
+
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
8435
|
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
8436
|
);
|
|
7908
8437
|
continue;
|
|
@@ -7922,18 +8451,18 @@ function validateTranslationReferences(stack) {
|
|
|
7922
8451
|
orphan(
|
|
7923
8452
|
`${inLocale} \xB7 app "${appName}"`,
|
|
7924
8453
|
appPath,
|
|
7925
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
8454
|
+
`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
8455
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
7927
8456
|
);
|
|
7928
8457
|
continue;
|
|
7929
8458
|
}
|
|
7930
|
-
if (!
|
|
8459
|
+
if (!isRec21(rawApp)) continue;
|
|
7931
8460
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
7932
8461
|
if (navIds.has(navId)) continue;
|
|
7933
8462
|
orphan(
|
|
7934
8463
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
7935
8464
|
`${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.` +
|
|
8465
|
+
`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
8466
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
7938
8467
|
);
|
|
7939
8468
|
}
|
|
@@ -7945,18 +8474,18 @@ function validateTranslationReferences(stack) {
|
|
|
7945
8474
|
orphan(
|
|
7946
8475
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
7947
8476
|
dashPath,
|
|
7948
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
8477
|
+
`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
8478
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
7950
8479
|
);
|
|
7951
8480
|
continue;
|
|
7952
8481
|
}
|
|
7953
|
-
if (!
|
|
8482
|
+
if (!isRec21(rawDash)) continue;
|
|
7954
8483
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
7955
8484
|
if (dash.widgets.has(widgetId)) continue;
|
|
7956
8485
|
orphan(
|
|
7957
8486
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
7958
8487
|
`${dashPath}.widgets.${widgetId}`,
|
|
7959
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
8488
|
+
`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
8489
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
7961
8490
|
);
|
|
7962
8491
|
}
|
|
@@ -7965,7 +8494,7 @@ function validateTranslationReferences(stack) {
|
|
|
7965
8494
|
orphan(
|
|
7966
8495
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
7967
8496
|
`${dashPath}.actions.${actionKey}`,
|
|
7968
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
8497
|
+
`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
8498
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
7970
8499
|
);
|
|
7971
8500
|
}
|
|
@@ -7975,7 +8504,7 @@ function validateTranslationReferences(stack) {
|
|
|
7975
8504
|
return findings;
|
|
7976
8505
|
}
|
|
7977
8506
|
function asRecord(v) {
|
|
7978
|
-
return
|
|
8507
|
+
return isRec21(v) ? v : {};
|
|
7979
8508
|
}
|
|
7980
8509
|
function checkOptionKeys(findings, ctx) {
|
|
7981
8510
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -7987,7 +8516,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
7987
8516
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
7988
8517
|
where: ctx.where,
|
|
7989
8518
|
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 "${
|
|
8519
|
+
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
8520
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
7992
8521
|
});
|
|
7993
8522
|
return;
|
|
@@ -8000,17 +8529,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
8000
8529
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
8001
8530
|
where: ctx.where,
|
|
8002
8531
|
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.` +
|
|
8532
|
+
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
8533
|
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
8534
|
});
|
|
8006
8535
|
}
|
|
8007
8536
|
}
|
|
8008
8537
|
function checkActionParams(findings, ctx) {
|
|
8009
|
-
const rawParams = Object.keys(asRecord(
|
|
8538
|
+
const rawParams = Object.keys(asRecord(isRec21(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
8010
8539
|
if (rawParams.length === 0) return;
|
|
8011
8540
|
const declared = /* @__PURE__ */ new Set();
|
|
8012
8541
|
for (const param of asArray35(ctx.action.params)) {
|
|
8013
|
-
const name =
|
|
8542
|
+
const name = strName17(param.name) ?? strName17(param.field);
|
|
8014
8543
|
if (name) declared.add(name);
|
|
8015
8544
|
}
|
|
8016
8545
|
for (const paramName of rawParams) {
|
|
@@ -8020,7 +8549,7 @@ function checkActionParams(findings, ctx) {
|
|
|
8020
8549
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
8021
8550
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
8022
8551
|
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.` +
|
|
8552
|
+
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
8553
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
8025
8554
|
});
|
|
8026
8555
|
}
|
|
@@ -8028,14 +8557,14 @@ function checkActionParams(findings, ctx) {
|
|
|
8028
8557
|
|
|
8029
8558
|
// src/validate-translatable-sections.ts
|
|
8030
8559
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
8031
|
-
function
|
|
8560
|
+
function isRec22(v) {
|
|
8032
8561
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
8033
8562
|
}
|
|
8034
|
-
function
|
|
8563
|
+
function strName18(v) {
|
|
8035
8564
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8036
8565
|
}
|
|
8037
8566
|
function viewLabel(view) {
|
|
8038
|
-
const name =
|
|
8567
|
+
const name = strName18(view.name);
|
|
8039
8568
|
return name ? `view "${name}"` : "";
|
|
8040
8569
|
}
|
|
8041
8570
|
function joinWhere(...parts) {
|
|
@@ -8043,7 +8572,7 @@ function joinWhere(...parts) {
|
|
|
8043
8572
|
}
|
|
8044
8573
|
function collectViewSites(view, basePath, label2, sites) {
|
|
8045
8574
|
const recordObject = viewObjectName(view);
|
|
8046
|
-
const listBinding =
|
|
8575
|
+
const listBinding = isRec22(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
8047
8576
|
for (const site of viewContainerSites(view, basePath)) {
|
|
8048
8577
|
sites.push({
|
|
8049
8578
|
path: `${site.path}.sections`,
|
|
@@ -8057,11 +8586,11 @@ function translatedObjectNames(stack) {
|
|
|
8057
8586
|
const out = /* @__PURE__ */ new Set();
|
|
8058
8587
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
8059
8588
|
for (const bundle of bundles) {
|
|
8060
|
-
if (!
|
|
8589
|
+
if (!isRec22(bundle)) continue;
|
|
8061
8590
|
for (const data of Object.values(bundle)) {
|
|
8062
|
-
if (!
|
|
8591
|
+
if (!isRec22(data) || !isRec22(data.objects)) continue;
|
|
8063
8592
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
8064
|
-
if (
|
|
8593
|
+
if (isRec22(node)) out.add(objectName);
|
|
8065
8594
|
}
|
|
8066
8595
|
}
|
|
8067
8596
|
}
|
|
@@ -8073,22 +8602,22 @@ function suggestedName(label2) {
|
|
|
8073
8602
|
}
|
|
8074
8603
|
function validateTranslatableSections(stack) {
|
|
8075
8604
|
const findings = [];
|
|
8076
|
-
if (!
|
|
8605
|
+
if (!isRec22(stack)) return findings;
|
|
8077
8606
|
const translated = translatedObjectNames(stack);
|
|
8078
8607
|
if (translated.size === 0) return findings;
|
|
8079
8608
|
const sites = [];
|
|
8080
8609
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
8081
|
-
const objectName =
|
|
8610
|
+
const objectName = strName18(obj.name);
|
|
8082
8611
|
if (!objectName) continue;
|
|
8083
8612
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
8084
8613
|
collectViewSites(
|
|
8085
|
-
{ ...view, object:
|
|
8614
|
+
{ ...view, object: strName18(view.object) ?? objectName },
|
|
8086
8615
|
path,
|
|
8087
8616
|
viewLabel(view),
|
|
8088
8617
|
sites
|
|
8089
8618
|
);
|
|
8090
8619
|
}
|
|
8091
|
-
if (
|
|
8620
|
+
if (isRec22(obj.listViews)) {
|
|
8092
8621
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
8093
8622
|
}
|
|
8094
8623
|
}
|
|
@@ -8096,13 +8625,13 @@ function validateTranslatableSections(stack) {
|
|
|
8096
8625
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
8097
8626
|
}
|
|
8098
8627
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
8099
|
-
const pageName =
|
|
8628
|
+
const pageName = strName18(page.name);
|
|
8100
8629
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
8101
8630
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
8102
8631
|
if (!walked.objectName) continue;
|
|
8103
|
-
const props =
|
|
8632
|
+
const props = isRec22(walked.component.properties) ? walked.component.properties : void 0;
|
|
8104
8633
|
if (!props) continue;
|
|
8105
|
-
const type =
|
|
8634
|
+
const type = strName18(walked.component.type) ?? "component";
|
|
8106
8635
|
sites.push({
|
|
8107
8636
|
path: `${walked.path}.properties.sections`,
|
|
8108
8637
|
surface: joinWhere(pageLabel, type),
|
|
@@ -8117,9 +8646,9 @@ function validateTranslatableSections(stack) {
|
|
|
8117
8646
|
if (!Array.isArray(site.sections)) continue;
|
|
8118
8647
|
for (let i = 0; i < site.sections.length; i++) {
|
|
8119
8648
|
const section = site.sections[i];
|
|
8120
|
-
if (!
|
|
8121
|
-
if (
|
|
8122
|
-
const heading =
|
|
8649
|
+
if (!isRec22(section)) continue;
|
|
8650
|
+
if (strName18(section.name)) continue;
|
|
8651
|
+
const heading = strName18(section.label);
|
|
8123
8652
|
if (!heading) continue;
|
|
8124
8653
|
const slug = suggestedName(heading);
|
|
8125
8654
|
findings.push({
|
|
@@ -8144,7 +8673,7 @@ function asArray36(v) {
|
|
|
8144
8673
|
}
|
|
8145
8674
|
return [];
|
|
8146
8675
|
}
|
|
8147
|
-
function
|
|
8676
|
+
function strName19(v) {
|
|
8148
8677
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8149
8678
|
}
|
|
8150
8679
|
function surfaceOf(v) {
|
|
@@ -8155,17 +8684,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
8155
8684
|
if (!stack || typeof stack !== "object") return findings;
|
|
8156
8685
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
8157
8686
|
for (const skill of asArray36(stack.skills)) {
|
|
8158
|
-
const n =
|
|
8687
|
+
const n = strName19(skill.name);
|
|
8159
8688
|
if (n) skillsByName.set(n, skill);
|
|
8160
8689
|
}
|
|
8161
8690
|
const agents = asArray36(stack.agents);
|
|
8162
8691
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
8163
8692
|
const agent = agents[ai];
|
|
8164
|
-
const agentName =
|
|
8693
|
+
const agentName = strName19(agent.name) ?? `#${ai}`;
|
|
8165
8694
|
const agentSurface = surfaceOf(agent.surface);
|
|
8166
8695
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
8167
8696
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
8168
|
-
const ref =
|
|
8697
|
+
const ref = strName19(skillRefs[si]);
|
|
8169
8698
|
if (!ref) continue;
|
|
8170
8699
|
const skill = skillsByName.get(ref);
|
|
8171
8700
|
if (!skill) continue;
|
|
@@ -8194,10 +8723,10 @@ function asArray37(v) {
|
|
|
8194
8723
|
}
|
|
8195
8724
|
return [];
|
|
8196
8725
|
}
|
|
8197
|
-
function
|
|
8726
|
+
function strName20(v) {
|
|
8198
8727
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8199
8728
|
}
|
|
8200
|
-
function
|
|
8729
|
+
function distance7(a, b) {
|
|
8201
8730
|
const m = a.length;
|
|
8202
8731
|
const n = b.length;
|
|
8203
8732
|
if (m === 0) return n;
|
|
@@ -8213,14 +8742,14 @@ function distance6(a, b) {
|
|
|
8213
8742
|
}
|
|
8214
8743
|
return prev[n];
|
|
8215
8744
|
}
|
|
8216
|
-
function
|
|
8745
|
+
function suggest8(target, known) {
|
|
8217
8746
|
for (const prefix of import_system5.PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
8218
8747
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
8219
8748
|
}
|
|
8220
8749
|
let best;
|
|
8221
8750
|
let bestScore = Infinity;
|
|
8222
8751
|
for (const candidate of known) {
|
|
8223
|
-
const d =
|
|
8752
|
+
const d = distance7(target, candidate);
|
|
8224
8753
|
if (d < bestScore) {
|
|
8225
8754
|
bestScore = d;
|
|
8226
8755
|
best = candidate;
|
|
@@ -8235,8 +8764,8 @@ function materialisesAsTool(action) {
|
|
|
8235
8764
|
if (!ai || typeof ai !== "object") return false;
|
|
8236
8765
|
const aiRec = ai;
|
|
8237
8766
|
if (aiRec.exposed !== true) return false;
|
|
8238
|
-
if (!
|
|
8239
|
-
const type =
|
|
8767
|
+
if (!strName20(aiRec.description)) return false;
|
|
8768
|
+
const type = strName20(action.type);
|
|
8240
8769
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
8241
8770
|
if (type === "script") return Boolean(action.target || action.body);
|
|
8242
8771
|
return Boolean(action.target);
|
|
@@ -8244,12 +8773,12 @@ function materialisesAsTool(action) {
|
|
|
8244
8773
|
function collectToolUniverse(stack) {
|
|
8245
8774
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
8246
8775
|
for (const tool of asArray37(stack.tools)) {
|
|
8247
|
-
const n =
|
|
8776
|
+
const n = strName20(tool.name);
|
|
8248
8777
|
if (n) universe.add(n);
|
|
8249
8778
|
}
|
|
8250
8779
|
const addActionFamily = (actions) => {
|
|
8251
8780
|
for (const action of asArray37(actions)) {
|
|
8252
|
-
const n =
|
|
8781
|
+
const n = strName20(action.name);
|
|
8253
8782
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
8254
8783
|
}
|
|
8255
8784
|
};
|
|
@@ -8263,7 +8792,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
8263
8792
|
const names = /* @__PURE__ */ new Set();
|
|
8264
8793
|
const scan = (actions) => {
|
|
8265
8794
|
for (const action of asArray37(actions)) {
|
|
8266
|
-
const n =
|
|
8795
|
+
const n = strName20(action.name);
|
|
8267
8796
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
8268
8797
|
}
|
|
8269
8798
|
};
|
|
@@ -8289,10 +8818,10 @@ function validateAiToolReferences(stack) {
|
|
|
8289
8818
|
const skills = asArray37(stack.skills);
|
|
8290
8819
|
for (let si = 0; si < skills.length; si++) {
|
|
8291
8820
|
const skill = skills[si];
|
|
8292
|
-
const skillName =
|
|
8821
|
+
const skillName = strName20(skill.name) ?? `#${si}`;
|
|
8293
8822
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
8294
8823
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
8295
|
-
const ref =
|
|
8824
|
+
const ref = strName20(refs[ti]);
|
|
8296
8825
|
if (!ref || resolves(ref)) continue;
|
|
8297
8826
|
const isPattern = ref.endsWith("*");
|
|
8298
8827
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -8301,7 +8830,7 @@ function validateAiToolReferences(stack) {
|
|
|
8301
8830
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
8302
8831
|
where: `skill "${skillName}" \xB7 tools`,
|
|
8303
8832
|
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.` +
|
|
8833
|
+
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
8834
|
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
8835
|
});
|
|
8307
8836
|
}
|
|
@@ -8319,7 +8848,7 @@ function asArray38(v) {
|
|
|
8319
8848
|
}
|
|
8320
8849
|
return [];
|
|
8321
8850
|
}
|
|
8322
|
-
function
|
|
8851
|
+
function strName21(v) {
|
|
8323
8852
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
8324
8853
|
}
|
|
8325
8854
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -8329,7 +8858,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
8329
8858
|
const agents = asArray38(stack.agents);
|
|
8330
8859
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
8331
8860
|
const agent = agents[ai];
|
|
8332
|
-
const name =
|
|
8861
|
+
const name = strName21(agent.name) ?? `#${ai}`;
|
|
8333
8862
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
8334
8863
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
8335
8864
|
findings.push({
|
|
@@ -8345,9 +8874,9 @@ function validateAiAgentAuthoring(stack) {
|
|
|
8345
8874
|
const apps = asArray38(stack.apps);
|
|
8346
8875
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
8347
8876
|
const app = apps[appIdx];
|
|
8348
|
-
const defaultAgent =
|
|
8877
|
+
const defaultAgent = strName21(app.defaultAgent);
|
|
8349
8878
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
8350
|
-
const appName =
|
|
8879
|
+
const appName = strName21(app.name) ?? `#${appIdx}`;
|
|
8351
8880
|
findings.push({
|
|
8352
8881
|
severity: "warning",
|
|
8353
8882
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -8378,6 +8907,8 @@ function loadTypeScript3() {
|
|
|
8378
8907
|
return cachedTs3;
|
|
8379
8908
|
}
|
|
8380
8909
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
8910
|
+
var HOOK_BODY_SOURCE_UNPARSEABLE = "hook-body-source-unparseable";
|
|
8911
|
+
var HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR = "hook-body-write-unprovisioned-anchor";
|
|
8381
8912
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
8382
8913
|
{
|
|
8383
8914
|
id: "input-property-assign",
|
|
@@ -8450,13 +8981,16 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
8450
8981
|
"owner",
|
|
8451
8982
|
"record_type"
|
|
8452
8983
|
]);
|
|
8453
|
-
|
|
8984
|
+
function unprovisionedAnchorWriteConsequence() {
|
|
8985
|
+
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).`;
|
|
8986
|
+
}
|
|
8987
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8454
8988
|
function asArray39(v) {
|
|
8455
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8456
|
-
if (
|
|
8989
|
+
if (Array.isArray(v)) return v.filter((x) => isRec23(x));
|
|
8990
|
+
if (isRec23(v)) {
|
|
8457
8991
|
return Object.entries(v).map(([name, def]) => ({
|
|
8458
8992
|
name,
|
|
8459
|
-
...
|
|
8993
|
+
...isRec23(def) ? def : {}
|
|
8460
8994
|
}));
|
|
8461
8995
|
}
|
|
8462
8996
|
return [];
|
|
@@ -8487,15 +9021,18 @@ function extractHookBodyWriteSet(source) {
|
|
|
8487
9021
|
return { writes: [], ctxRecordEscapes: false };
|
|
8488
9022
|
}
|
|
8489
9023
|
const tsc = loadTypeScript3();
|
|
8490
|
-
const sf =
|
|
9024
|
+
const { sourceFile: sf, failure: parseFailure } = createSourceFileChecked(
|
|
9025
|
+
tsc,
|
|
8491
9026
|
"hook-body.ts",
|
|
8492
9027
|
`async function __body(ctx) {
|
|
8493
9028
|
${source}
|
|
8494
9029
|
}`,
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
9030
|
+
{
|
|
9031
|
+
target: tsc.ScriptTarget.Latest,
|
|
9032
|
+
setParentNodes: false,
|
|
9033
|
+
scriptKind: tsc.ScriptKind.TS,
|
|
9034
|
+
synthesizedLinesBefore: 1
|
|
9035
|
+
}
|
|
8499
9036
|
);
|
|
8500
9037
|
const writes = [];
|
|
8501
9038
|
const recordRefs = [];
|
|
@@ -8592,7 +9129,8 @@ ${source}
|
|
|
8592
9129
|
visit(sf);
|
|
8593
9130
|
return {
|
|
8594
9131
|
writes,
|
|
8595
|
-
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
9132
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref)),
|
|
9133
|
+
...parseFailure ? { parseFailure } : {}
|
|
8596
9134
|
};
|
|
8597
9135
|
}
|
|
8598
9136
|
function validateHookBodyWrites(stack) {
|
|
@@ -8600,15 +9138,28 @@ function validateHookBodyWrites(stack) {
|
|
|
8600
9138
|
const hooks = asArray39(stack.hooks);
|
|
8601
9139
|
if (hooks.length === 0) return findings;
|
|
8602
9140
|
let objectFields = null;
|
|
9141
|
+
let anchors = null;
|
|
8603
9142
|
hooks.forEach((hook, hookIndex) => {
|
|
8604
9143
|
const body = hook.body;
|
|
8605
|
-
if (!
|
|
9144
|
+
if (!isRec23(body) || body.language !== "js") return;
|
|
8606
9145
|
const source = body.source;
|
|
8607
9146
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8608
|
-
const
|
|
9147
|
+
const extracted = extractHookBodyWriteSet(source);
|
|
9148
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
9149
|
+
if (extracted.parseFailure) {
|
|
9150
|
+
findings.push({
|
|
9151
|
+
severity: "warning",
|
|
9152
|
+
rule: HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
9153
|
+
where: `hook "${hookName}" \u203A body`,
|
|
9154
|
+
path: `hooks[${hookIndex}].body.source`,
|
|
9155
|
+
message: `L2 body did not parse (${describeParseFailure(extracted.parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
9156
|
+
hint: PARSE_FAILURE_HINT
|
|
9157
|
+
});
|
|
9158
|
+
}
|
|
9159
|
+
const writes = extracted.writes.filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
8609
9160
|
if (writes.length === 0) return;
|
|
8610
9161
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
8611
|
-
|
|
9162
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8612
9163
|
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
8613
9164
|
(o) => typeof o === "string" && o.trim() !== ""
|
|
8614
9165
|
);
|
|
@@ -8622,7 +9173,20 @@ function validateHookBodyWrites(stack) {
|
|
|
8622
9173
|
if (reported.has(dedupeKey)) continue;
|
|
8623
9174
|
if (w.object === void 0) {
|
|
8624
9175
|
if (!inputJudgeable) continue;
|
|
8625
|
-
if (IMPLICIT_FIELDS2.has(w.field))
|
|
9176
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9177
|
+
if (!targets.every((t) => anchors.get(t)?.has(w.field))) continue;
|
|
9178
|
+
reported.add(dedupeKey);
|
|
9179
|
+
const anchorObj = targets.length === 1 ? targets[0] : targets.join(", ");
|
|
9180
|
+
findings.push({
|
|
9181
|
+
severity: "warning",
|
|
9182
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9183
|
+
where,
|
|
9184
|
+
path,
|
|
9185
|
+
message: `body writes '${w.field}' to its input, and ${unprovisionedAnchorCause(anchorObj, w.field)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
9186
|
+
hint: unprovisionedAnchorHint(anchorObj, w.field)
|
|
9187
|
+
});
|
|
9188
|
+
continue;
|
|
9189
|
+
}
|
|
8626
9190
|
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
8627
9191
|
reported.add(dedupeKey);
|
|
8628
9192
|
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
@@ -8638,7 +9202,20 @@ function validateHookBodyWrites(stack) {
|
|
|
8638
9202
|
} else {
|
|
8639
9203
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
8640
9204
|
if (!known) continue;
|
|
8641
|
-
if (
|
|
9205
|
+
if (known.has(w.field)) continue;
|
|
9206
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9207
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
9208
|
+
reported.add(dedupeKey);
|
|
9209
|
+
findings.push({
|
|
9210
|
+
severity: "warning",
|
|
9211
|
+
rule: HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9212
|
+
where,
|
|
9213
|
+
path,
|
|
9214
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
9215
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
9216
|
+
});
|
|
9217
|
+
continue;
|
|
9218
|
+
}
|
|
8642
9219
|
reported.add(dedupeKey);
|
|
8643
9220
|
findings.push({
|
|
8644
9221
|
severity: "warning",
|
|
@@ -8667,6 +9244,8 @@ function fixHint(field, declared) {
|
|
|
8667
9244
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
8668
9245
|
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
8669
9246
|
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
9247
|
+
var ACTION_BODY_SOURCE_UNPARSEABLE = "action-body-source-unparseable";
|
|
9248
|
+
var ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR = "action-body-write-unprovisioned-anchor";
|
|
8670
9249
|
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
8671
9250
|
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
8672
9251
|
var ACTION_BODY_WRITE_EXCLUSIONS = [
|
|
@@ -8683,13 +9262,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
8683
9262
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
8684
9263
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
8685
9264
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
8686
|
-
var
|
|
9265
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8687
9266
|
function asArray40(v) {
|
|
8688
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8689
|
-
if (
|
|
9267
|
+
if (Array.isArray(v)) return v.filter((x) => isRec24(x));
|
|
9268
|
+
if (isRec24(v)) {
|
|
8690
9269
|
return Object.entries(v).map(([name, def]) => ({
|
|
8691
9270
|
name,
|
|
8692
|
-
...
|
|
9271
|
+
...isRec24(def) ? def : {}
|
|
8693
9272
|
}));
|
|
8694
9273
|
}
|
|
8695
9274
|
return [];
|
|
@@ -8707,7 +9286,7 @@ function collectActionBodies(stack) {
|
|
|
8707
9286
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
8708
9287
|
if (type !== "script") return;
|
|
8709
9288
|
const body = action.body;
|
|
8710
|
-
if (!
|
|
9289
|
+
if (!isRec24(body) || body.language !== "js") return;
|
|
8711
9290
|
const source = body.source;
|
|
8712
9291
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8713
9292
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -8726,17 +9305,28 @@ function collectActionBodies(stack) {
|
|
|
8726
9305
|
}
|
|
8727
9306
|
function validateActionBodyWrites(stack) {
|
|
8728
9307
|
const findings = [];
|
|
8729
|
-
if (!
|
|
9308
|
+
if (!isRec24(stack)) return findings;
|
|
8730
9309
|
const sites = collectActionBodies(stack);
|
|
8731
9310
|
if (sites.length === 0) return findings;
|
|
8732
9311
|
let objectFields = null;
|
|
9312
|
+
let anchors = null;
|
|
8733
9313
|
for (const site of sites) {
|
|
8734
9314
|
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
8735
|
-
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
9315
|
+
const { writes: allWrites, ctxRecordEscapes, parseFailure } = extractHookBodyWriteSet(site.source);
|
|
8736
9316
|
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
8737
9317
|
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
8738
|
-
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
8739
9318
|
const where = `action "${site.name}" \u203A body`;
|
|
9319
|
+
if (parseFailure) {
|
|
9320
|
+
findings.push({
|
|
9321
|
+
severity: "warning",
|
|
9322
|
+
rule: ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
9323
|
+
where,
|
|
9324
|
+
path: site.path,
|
|
9325
|
+
message: `L2 body did not parse (${describeParseFailure(parseFailure)}), so its write set was read from a partially recovered tree \u2014 an undeclared field write in the unread part is not reported.`,
|
|
9326
|
+
hint: PARSE_FAILURE_HINT
|
|
9327
|
+
});
|
|
9328
|
+
}
|
|
9329
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
8740
9330
|
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
8741
9331
|
const reportedFields = /* @__PURE__ */ new Set();
|
|
8742
9332
|
for (const w of recordWrites) {
|
|
@@ -8754,6 +9344,7 @@ function validateActionBodyWrites(stack) {
|
|
|
8754
9344
|
}
|
|
8755
9345
|
if (writes.length === 0) continue;
|
|
8756
9346
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
9347
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8757
9348
|
const reported = /* @__PURE__ */ new Set();
|
|
8758
9349
|
for (const w of writes) {
|
|
8759
9350
|
if (w.object === void 0) continue;
|
|
@@ -8761,7 +9352,20 @@ function validateActionBodyWrites(stack) {
|
|
|
8761
9352
|
if (reported.has(dedupeKey)) continue;
|
|
8762
9353
|
const known = judgeableFieldsOf(objectFields, w.object);
|
|
8763
9354
|
if (!known) continue;
|
|
8764
|
-
if (
|
|
9355
|
+
if (known.has(w.field)) continue;
|
|
9356
|
+
if (IMPLICIT_FIELDS2.has(w.field)) {
|
|
9357
|
+
if (!anchors.get(w.object)?.has(w.field)) continue;
|
|
9358
|
+
reported.add(dedupeKey);
|
|
9359
|
+
findings.push({
|
|
9360
|
+
severity: "warning",
|
|
9361
|
+
rule: ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
9362
|
+
where,
|
|
9363
|
+
path: site.path,
|
|
9364
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', and ${unprovisionedAnchorCause(w.object, w.field)} \u2014 ${unprovisionedAnchorWriteConsequence()}`,
|
|
9365
|
+
hint: unprovisionedAnchorHint(w.object, w.field)
|
|
9366
|
+
});
|
|
9367
|
+
continue;
|
|
9368
|
+
}
|
|
8765
9369
|
reported.add(dedupeKey);
|
|
8766
9370
|
findings.push({
|
|
8767
9371
|
severity: "warning",
|
|
@@ -8783,15 +9387,16 @@ function fixHint2(field, declared) {
|
|
|
8783
9387
|
// src/validate-flow-node-writes.ts
|
|
8784
9388
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
8785
9389
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
9390
|
+
var FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR = "flow-node-write-unprovisioned-anchor";
|
|
8786
9391
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
8787
9392
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
8788
|
-
var
|
|
9393
|
+
var isRec25 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8789
9394
|
function asArray41(v) {
|
|
8790
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8791
|
-
if (
|
|
9395
|
+
if (Array.isArray(v)) return v.filter((x) => isRec25(x));
|
|
9396
|
+
if (isRec25(v)) {
|
|
8792
9397
|
return Object.entries(v).map(([name, def]) => ({
|
|
8793
9398
|
name,
|
|
8794
|
-
...
|
|
9399
|
+
...isRec25(def) ? def : {}
|
|
8795
9400
|
}));
|
|
8796
9401
|
}
|
|
8797
9402
|
return [];
|
|
@@ -8804,30 +9409,44 @@ function readLiteralObjectName2(config) {
|
|
|
8804
9409
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
8805
9410
|
function validateFlowNodeWrites(stack) {
|
|
8806
9411
|
const findings = [];
|
|
8807
|
-
if (!
|
|
9412
|
+
if (!isRec25(stack)) return findings;
|
|
8808
9413
|
const flows = asArray41(stack.flows);
|
|
8809
9414
|
if (flows.length === 0) return findings;
|
|
8810
9415
|
let objectFields = null;
|
|
9416
|
+
let anchors = null;
|
|
8811
9417
|
flows.forEach((flow, flowIndex) => {
|
|
8812
9418
|
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
8813
9419
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
8814
9420
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
8815
9421
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
8816
|
-
const config =
|
|
9422
|
+
const config = isRec25(node.config) ? node.config : void 0;
|
|
8817
9423
|
if (!config) return;
|
|
8818
9424
|
const fields = config.fields;
|
|
8819
|
-
if (!
|
|
9425
|
+
if (!isRec25(fields)) return;
|
|
8820
9426
|
const written = Object.keys(fields);
|
|
8821
9427
|
if (written.length === 0) return;
|
|
8822
9428
|
const objectName = readLiteralObjectName2(config);
|
|
8823
9429
|
if (!objectName) return;
|
|
8824
9430
|
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
9431
|
+
anchors ?? (anchors = indexUnprovisionedAnchors(stack));
|
|
8825
9432
|
const known = judgeableFieldsOf(objectFields, objectName);
|
|
8826
9433
|
if (!known) return;
|
|
8827
9434
|
const nodeName = flowNodeLabel(node, walkIndex);
|
|
8828
9435
|
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
8829
9436
|
for (const fieldName of written) {
|
|
8830
|
-
if (known.has(fieldName)
|
|
9437
|
+
if (known.has(fieldName)) continue;
|
|
9438
|
+
if (IMPLICIT_FIELDS2.has(fieldName)) {
|
|
9439
|
+
if (!anchors.get(objectName)?.has(fieldName)) continue;
|
|
9440
|
+
findings.push({
|
|
9441
|
+
severity: "warning",
|
|
9442
|
+
rule: FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
9443
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
9444
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
9445
|
+
message: `${node.type} writes '${fieldName}', and ${unprovisionedAnchorCause(objectName, fieldName)} \u2014 ` + unprovisionedAnchorWriteConsequence(),
|
|
9446
|
+
hint: unprovisionedAnchorHint(objectName, fieldName)
|
|
9447
|
+
});
|
|
9448
|
+
continue;
|
|
9449
|
+
}
|
|
8831
9450
|
if (fieldName.includes(".")) continue;
|
|
8832
9451
|
findings.push({
|
|
8833
9452
|
severity: "error",
|
|
@@ -8848,9 +9467,26 @@ function fixHint3(field, declared) {
|
|
|
8848
9467
|
}
|
|
8849
9468
|
|
|
8850
9469
|
// src/reference-integrity-suite.ts
|
|
9470
|
+
var DEFAULT_MEMBER_RUNTIME_TYPES = ["flow"];
|
|
8851
9471
|
var REFERENCE_INTEGRITY_RULES = [
|
|
8852
9472
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
8853
|
-
|
|
9473
|
+
// [#9313] `runtimeTypes` gains `view` on this member and its sort sibling:
|
|
9474
|
+
// both judge a LIST VIEW's field references, and a standalone list view is
|
|
9475
|
+
// written through `PUT /api/v1/meta/view` — the only door a Studio tenant or
|
|
9476
|
+
// an MCP/AI author has. Their walks read the flattened overlay shape that
|
|
9477
|
+
// door carries (see each rule's `views[]` self rung), and they resolve only
|
|
9478
|
+
// against `stack.objects`, which the per-write snapshot DOES carry — so the
|
|
9479
|
+
// crossing has no missing-collection false-positive channel. Measured over
|
|
9480
|
+
// the shipped view corpus before crossing (0 refusals; population in the
|
|
9481
|
+
// #9313 PR).
|
|
9482
|
+
{ name: "validateSearchableFields", runtimeTypes: ["flow", "view"], run: validateSearchableFields },
|
|
9483
|
+
// [#9257] The same reading, one axis over: a list view's `sort` is a field
|
|
9484
|
+
// name written in metadata, resolved against the object's declared fields. It
|
|
9485
|
+
// gates (`error`) because the runtime does not tolerate a bad one at all —
|
|
9486
|
+
// `assertSortFieldsExist` (#6994) and `assertOrderByIsMaterializable` (#7095)
|
|
9487
|
+
// both answer `400 INVALID_SORT` — and a view's sort is its FIRST fetch, so
|
|
9488
|
+
// the refusal is the whole view, on every load, traced to nothing.
|
|
9489
|
+
{ name: "validateSortableFields", runtimeTypes: ["flow", "view"], run: validateSortableFields },
|
|
8854
9490
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
8855
9491
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
8856
9492
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -8956,9 +9592,11 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
8956
9592
|
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
8957
9593
|
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
8958
9594
|
];
|
|
8959
|
-
function validateReferenceIntegrity(stack) {
|
|
9595
|
+
function validateReferenceIntegrity(stack, options) {
|
|
8960
9596
|
const findings = [];
|
|
9597
|
+
const writeType = options?.runtimeWriteType;
|
|
8961
9598
|
for (const rule of REFERENCE_INTEGRITY_RULES) {
|
|
9599
|
+
if (writeType !== void 0 && !(rule.runtimeTypes ?? DEFAULT_MEMBER_RUNTIME_TYPES).includes(writeType)) continue;
|
|
8962
9600
|
findings.push(...rule.run(stack));
|
|
8963
9601
|
}
|
|
8964
9602
|
return findings;
|
|
@@ -8966,7 +9604,7 @@ function validateReferenceIntegrity(stack) {
|
|
|
8966
9604
|
|
|
8967
9605
|
// src/lint-flow-patterns.ts
|
|
8968
9606
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8969
|
-
var
|
|
9607
|
+
var import_data12 = require("@objectstack/spec/data");
|
|
8970
9608
|
function asArray42(v) {
|
|
8971
9609
|
if (Array.isArray(v)) return v;
|
|
8972
9610
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -9241,7 +9879,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
9241
9879
|
function filterCarriesNoCondition(filter) {
|
|
9242
9880
|
if (filter === void 0 || filter === null) return true;
|
|
9243
9881
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
9244
|
-
return (0,
|
|
9882
|
+
return (0, import_data12.reduceFilterVerdict)(filter) === "true";
|
|
9245
9883
|
}
|
|
9246
9884
|
function describeUnboundedFilter(filter) {
|
|
9247
9885
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -9608,7 +10246,7 @@ function lintLivenessProperties(stack) {
|
|
|
9608
10246
|
}
|
|
9609
10247
|
|
|
9610
10248
|
// src/lint-autonumber-formats.ts
|
|
9611
|
-
var
|
|
10249
|
+
var import_data13 = require("@objectstack/spec/data");
|
|
9612
10250
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
9613
10251
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
9614
10252
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
@@ -9634,8 +10272,8 @@ function lintAutonumberFormats(stack) {
|
|
|
9634
10272
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
9635
10273
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
9636
10274
|
if (!fmt) continue;
|
|
9637
|
-
const tokens = (0,
|
|
9638
|
-
const refs = (0,
|
|
10275
|
+
const tokens = (0, import_data13.parseAutonumberFormat)(fmt);
|
|
10276
|
+
const refs = (0, import_data13.referencedFields)(tokens);
|
|
9639
10277
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
9640
10278
|
for (const t of tokens) {
|
|
9641
10279
|
if (t.kind !== "literal") continue;
|
|
@@ -9790,6 +10428,17 @@ function lintViewRefs(stack) {
|
|
|
9790
10428
|
}
|
|
9791
10429
|
|
|
9792
10430
|
// src/data-model-rules.ts
|
|
10431
|
+
function objectWhere(obj) {
|
|
10432
|
+
return `object "${obj?.name}"`;
|
|
10433
|
+
}
|
|
10434
|
+
function indexWhere(obj, idx, j, cols) {
|
|
10435
|
+
const named = typeof idx?.name === "string" && idx.name.trim() ? `'${idx.name.trim()}'` : "";
|
|
10436
|
+
const label2 = named || (cols.length > 0 ? `[${cols.join(", ")}]` : `#${j}`);
|
|
10437
|
+
return `${objectWhere(obj)} \xB7 index ${label2}`;
|
|
10438
|
+
}
|
|
10439
|
+
function fieldWhere(obj, fieldName) {
|
|
10440
|
+
return `${objectWhere(obj)} \xB7 field '${fieldName}'`;
|
|
10441
|
+
}
|
|
9793
10442
|
var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
|
|
9794
10443
|
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
9795
10444
|
"number",
|
|
@@ -9862,11 +10511,13 @@ function lintUnscopedDeclaredIndexes(objects) {
|
|
|
9862
10511
|
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
9863
10512
|
const idx = declaredIndexes[j];
|
|
9864
10513
|
if (idx?.unique !== true) continue;
|
|
9865
|
-
const
|
|
10514
|
+
const colList = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
10515
|
+
const cols = colList.join(", ");
|
|
9866
10516
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
9867
10517
|
issues.push({
|
|
9868
10518
|
severity: "warning",
|
|
9869
10519
|
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
10520
|
+
where: indexWhere(obj, idx, j, colList),
|
|
9870
10521
|
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
10522
|
path: `objects[${i}].indexes[${j}]`,
|
|
9872
10523
|
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 +10566,11 @@ function lintUniqueDeclarations(objects) {
|
|
|
9915
10566
|
issues.push({
|
|
9916
10567
|
severity: "warning",
|
|
9917
10568
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
10569
|
+
// More specific than `path` on purpose: this rule's finding is about ONE
|
|
10570
|
+
// column, but its `path` has always been the whole object (`objects[i]`)
|
|
10571
|
+
// because the defect straddles `fields.<name>.unique` and an entry of
|
|
10572
|
+
// `indexes`. `where` can name the column without picking one of the two.
|
|
10573
|
+
where: fieldWhere(obj, name),
|
|
9918
10574
|
message,
|
|
9919
10575
|
path: `objects[${i}]`,
|
|
9920
10576
|
fix
|
|
@@ -9943,6 +10599,7 @@ function lintLegacyOrganizationComposites(objects) {
|
|
|
9943
10599
|
issues.push({
|
|
9944
10600
|
severity: "warning",
|
|
9945
10601
|
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
10602
|
+
where: indexWhere(obj, idx, j, cols),
|
|
9946
10603
|
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
10604
|
path: `objects[${i}].indexes[${j}]`,
|
|
9948
10605
|
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 +10746,7 @@ var CLI_ONLY = ["cli"];
|
|
|
10089
10746
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
10090
10747
|
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
10748
|
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
|
|
10749
|
+
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
10750
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
10094
10751
|
var AUTHORING_RULES = [
|
|
10095
10752
|
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
@@ -10150,8 +10807,14 @@ var AUTHORING_RULES = [
|
|
|
10150
10807
|
input: "normalized",
|
|
10151
10808
|
commands: ALL,
|
|
10152
10809
|
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
10153
|
-
|
|
10154
|
-
|
|
10810
|
+
// Runtime publish gate (#4716): the OBJECT write door — the five gating
|
|
10811
|
+
// object rules cross together under the 2026-08-18 adjudication. The
|
|
10812
|
+
// false-positive budget the crossing owed was exempted on a measured
|
|
10813
|
+
// 0 refusals / 75 real object declarations (authored config-file metadata,
|
|
10814
|
+
// so a lower bound — see RUNTIME_OBJECT_ADVISORY_VOLUME's note); the six
|
|
10815
|
+
// advisory-tier object rules deliberately do NOT ride.
|
|
10816
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10817
|
+
runtimeTypes: ["object"],
|
|
10155
10818
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
10156
10819
|
},
|
|
10157
10820
|
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
@@ -10175,8 +10838,11 @@ var AUTHORING_RULES = [
|
|
|
10175
10838
|
input: "normalized",
|
|
10176
10839
|
commands: ALL,
|
|
10177
10840
|
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
10178
|
-
|
|
10179
|
-
|
|
10841
|
+
// Runtime publish gate (#4716): a managed object advertising a verb its
|
|
10842
|
+
// own affordances refuse is exactly the contradiction a Studio/MCP author
|
|
10843
|
+
// can save today — the CLI sweep (#7934) never sees an overlay row.
|
|
10844
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10845
|
+
runtimeTypes: ["object"],
|
|
10180
10846
|
run: (stack) => validateManagedApiMethods(stack)
|
|
10181
10847
|
},
|
|
10182
10848
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
@@ -10198,19 +10864,38 @@ var AUTHORING_RULES = [
|
|
|
10198
10864
|
},
|
|
10199
10865
|
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
10200
10866
|
// chartConfig axis/series must resolve against the declared datasets.
|
|
10867
|
+
//
|
|
10868
|
+
// Runtime publish gate (#7529, maintainer-ruled 2026-08-12): a dashboard
|
|
10869
|
+
// widget bound to a dataset that resolves to nothing sailed through save AND
|
|
10870
|
+
// publish — this rule provably caught the body and was simply never invoked,
|
|
10871
|
+
// because `dashboard` was not a runtime-gated type. Option B: a DRAFT may
|
|
10872
|
+
// hold a forward reference; publishing refuses it with the key path named.
|
|
10873
|
+
// The snapshot carries `datasets` for exactly this rule (`RuntimeStackContext`
|
|
10874
|
+
// — without it every legitimate board reads as dangling, the 3-phantom
|
|
10875
|
+
// measurement). `surfaces` is per-RULE, so this flip puts all SIX of the
|
|
10876
|
+
// rule's error ids on the publish gate, not just `widget-dataset-unknown` —
|
|
10877
|
+
// ruled 2026-08-15: they are one coherent "this board cannot render"
|
|
10878
|
+
// reference-integrity class, and the ~6× wider accept-set narrowing was
|
|
10879
|
+
// accepted knowingly rather than splitting the dataset limb into its own
|
|
10880
|
+
// rule (traversal-duplication drift) or adding a per-finding-id surface
|
|
10881
|
+
// filter (registry machinery that weakens "delete a rule from the table and
|
|
10882
|
+
// enforcement stops in the same commit"). Warning-tier ids ride along on the
|
|
10883
|
+
// advisory channel and never block (#4463 P1).
|
|
10201
10884
|
{
|
|
10202
10885
|
name: "validateWidgetBindings",
|
|
10203
10886
|
tier: "gating",
|
|
10204
10887
|
input: "parsed",
|
|
10205
10888
|
commands: ALL,
|
|
10206
10889
|
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
10207
|
-
surfaces:
|
|
10208
|
-
|
|
10890
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10891
|
+
runtimeTypes: ["dashboard"],
|
|
10209
10892
|
run: (stack) => validateWidgetBindings(stack)
|
|
10210
10893
|
},
|
|
10211
|
-
// ADR-0049 / #3367 — a header
|
|
10212
|
-
//
|
|
10213
|
-
//
|
|
10894
|
+
// ADR-0049 / #3367 — a dashboard header action naming a dead target ships a
|
|
10895
|
+
// button that renders and refuses (or does nothing) on click: a `script`
|
|
10896
|
+
// target must name a defined action, a `modal` target must name a declared
|
|
10897
|
+
// page (objectstack#6739-A — a modal string target names a PAGE, only).
|
|
10898
|
+
// Unresolved `url` routes stay advisory.
|
|
10214
10899
|
{
|
|
10215
10900
|
name: "validateDashboardActionRefs",
|
|
10216
10901
|
tier: "gating",
|
|
@@ -10235,6 +10920,28 @@ var AUTHORING_RULES = [
|
|
|
10235
10920
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10236
10921
|
run: (stack) => validateFilterTokens(stack)
|
|
10237
10922
|
},
|
|
10923
|
+
// #8793 (the ruled C half of #8690) — a declared dashboard date-range preset
|
|
10924
|
+
// name (`last_30_days`, …) authored as a bare ORDERING comparand resolves in
|
|
10925
|
+
// no layer: the engine refuses it on a declared temporal field at query time
|
|
10926
|
+
// (INVALID_FILTER / 400, PR #8808), and anywhere else it compares as a
|
|
10927
|
+
// literal string. This is the authoring-time refusal the ruling shipped
|
|
10928
|
+
// alongside the engine door, judging the filter literal in isolation —
|
|
10929
|
+
// ordering positions only, all three authored filter shapes. Like
|
|
10930
|
+
// `validateEmptyCombinators` it needs NO resolution context, so
|
|
10931
|
+
// RUNTIME_NEEDS_FULL_SNAPSHOT does not apply and the runtime gate runs it
|
|
10932
|
+
// for every filter-carrying type the gate already maps: the write path is
|
|
10933
|
+
// the one door an AI author uses, and dashboards/views are where the preset
|
|
10934
|
+
// vocabulary is near enough to reach for.
|
|
10935
|
+
{
|
|
10936
|
+
name: "validatePresetComparands",
|
|
10937
|
+
tier: "gating",
|
|
10938
|
+
input: "parsed",
|
|
10939
|
+
commands: ALL,
|
|
10940
|
+
source: "packages/lint/src/validate-preset-comparands.ts",
|
|
10941
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10942
|
+
runtimeTypes: ["dashboard", "view", "object", "page", "flow"],
|
|
10943
|
+
run: (stack) => validatePresetComparands(stack)
|
|
10944
|
+
},
|
|
10238
10945
|
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
10239
10946
|
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
10240
10947
|
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
@@ -10278,9 +10985,31 @@ var AUTHORING_RULES = [
|
|
|
10278
10985
|
// collection in the snapshot and return nothing, and the two that would
|
|
10279
10986
|
// load `typescript` need a hook/action/react body the snapshot never
|
|
10280
10987
|
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
10988
|
+
//
|
|
10989
|
+
// [#9313] `view` joins, and the granularity decision #4463 P2 reserved is
|
|
10990
|
+
// taken HERE, in writing: the entry-level `runtimeTypes` says which WRITES
|
|
10991
|
+
// dispatch the suite, and the suite's own per-member `runtimeTypes`
|
|
10992
|
+
// (`ReferenceIntegrityRule`, default `['flow']`) says which MEMBERS judge
|
|
10993
|
+
// that snapshot — the gate passes the written type through
|
|
10994
|
+
// `ctx.runtimeWriteType`. A `view` write therefore reaches exactly the two
|
|
10995
|
+
// members that judge a list view's field references and resolve only
|
|
10996
|
+
// against the `objects` collection the snapshot carries
|
|
10997
|
+
// (`validateSearchableFields`, `validateSortableFields`). The whole suite
|
|
10998
|
+
// is NOT the right granularity for this door, measured not assumed, and
|
|
10999
|
+
// the crossing fails differently per body shape — both ways wrong:
|
|
11000
|
+
// `validateActionNameRefs` (error-tier) resolves `views[].list` /
|
|
11001
|
+
// `views[].listViews.*` action names against `stack.actions`, a collection
|
|
11002
|
+
// no per-write snapshot carries, so on a CONTAINER view write it would
|
|
11003
|
+
// refuse every stack-level action the body names (measured:
|
|
11004
|
+
// `action-name-undefined` on the snapshot shape, clean on the full stack)
|
|
11005
|
+
// — RUNTIME_NEEDS_FULL_SNAPSHOT's exact sentence, on the hottest write
|
|
11006
|
+
// type the gate has. On a FLATTENED overlay it has no rung at all, so the
|
|
11007
|
+
// crossing would be a silent no-op that reads as coverage — the very shape
|
|
11008
|
+
// #9313 was filed about. Flow snapshots are unchanged: every member keeps
|
|
11009
|
+
// the default `flow` declaration.
|
|
10281
11010
|
surfaces: CLI_AND_RUNTIME,
|
|
10282
|
-
runtimeTypes: ["flow"],
|
|
10283
|
-
run: (stack) => validateReferenceIntegrity(stack)
|
|
11011
|
+
runtimeTypes: ["flow", "view"],
|
|
11012
|
+
run: (stack, ctx) => validateReferenceIntegrity(stack, ctx)
|
|
10284
11013
|
},
|
|
10285
11014
|
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
10286
11015
|
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
@@ -10453,7 +11182,7 @@ var AUTHORING_RULES = [
|
|
|
10453
11182
|
commands: ALL,
|
|
10454
11183
|
source: "packages/lint/src/validate-record-title.ts",
|
|
10455
11184
|
surfaces: CLI_ONLY,
|
|
10456
|
-
surfaceReason:
|
|
11185
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10457
11186
|
run: (stack) => validateRecordTitle(stack)
|
|
10458
11187
|
},
|
|
10459
11188
|
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
@@ -10466,7 +11195,7 @@ var AUTHORING_RULES = [
|
|
|
10466
11195
|
commands: ALL,
|
|
10467
11196
|
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
10468
11197
|
surfaces: CLI_ONLY,
|
|
10469
|
-
surfaceReason:
|
|
11198
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10470
11199
|
run: (stack) => validateSemanticRoles(stack)
|
|
10471
11200
|
},
|
|
10472
11201
|
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
@@ -10677,7 +11406,7 @@ var AUTHORING_RULES = [
|
|
|
10677
11406
|
commands: ALL,
|
|
10678
11407
|
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
10679
11408
|
surfaces: CLI_ONLY,
|
|
10680
|
-
surfaceReason:
|
|
11409
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10681
11410
|
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
10682
11411
|
severity: "warning",
|
|
10683
11412
|
rule: f.rule,
|
|
@@ -10696,8 +11425,12 @@ var AUTHORING_RULES = [
|
|
|
10696
11425
|
input: "parsed",
|
|
10697
11426
|
commands: ALL,
|
|
10698
11427
|
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
10699
|
-
|
|
10700
|
-
|
|
11428
|
+
// Runtime publish gate (#4716): an autonumber referencing a field the
|
|
11429
|
+
// object does not carry is broken from the first record; only the error
|
|
11430
|
+
// arm blocks — the optional-field arm is `warning` and rides the
|
|
11431
|
+
// advisory channel like every other non-error finding (#4463 P1).
|
|
11432
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11433
|
+
runtimeTypes: ["object"],
|
|
10701
11434
|
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
10702
11435
|
severity: f.severity,
|
|
10703
11436
|
rule: f.rule,
|
|
@@ -10738,12 +11471,12 @@ var AUTHORING_RULES = [
|
|
|
10738
11471
|
commands: ["validate", "build"],
|
|
10739
11472
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10740
11473
|
surfaces: CLI_ONLY,
|
|
10741
|
-
surfaceReason:
|
|
11474
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10742
11475
|
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
11476
|
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10744
11477
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10745
11478
|
rule: f.rule,
|
|
10746
|
-
where: f.
|
|
11479
|
+
where: f.where,
|
|
10747
11480
|
path: f.path,
|
|
10748
11481
|
message: f.message,
|
|
10749
11482
|
hint: f.fix ?? ""
|
|
@@ -10759,12 +11492,12 @@ var AUTHORING_RULES = [
|
|
|
10759
11492
|
commands: ["validate", "build"],
|
|
10760
11493
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10761
11494
|
surfaces: CLI_ONLY,
|
|
10762
|
-
surfaceReason:
|
|
11495
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10763
11496
|
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
11497
|
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10765
11498
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10766
11499
|
rule: f.rule,
|
|
10767
|
-
where: f.
|
|
11500
|
+
where: f.where,
|
|
10768
11501
|
path: f.path,
|
|
10769
11502
|
message: f.message,
|
|
10770
11503
|
hint: f.fix ?? ""
|
|
@@ -10781,12 +11514,12 @@ var AUTHORING_RULES = [
|
|
|
10781
11514
|
commands: ["validate", "build"],
|
|
10782
11515
|
source: "packages/lint/src/data-model-rules.ts",
|
|
10783
11516
|
surfaces: CLI_ONLY,
|
|
10784
|
-
surfaceReason:
|
|
11517
|
+
surfaceReason: RUNTIME_OBJECT_ADVISORY_VOLUME,
|
|
10785
11518
|
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
11519
|
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
10787
11520
|
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
10788
11521
|
rule: f.rule,
|
|
10789
|
-
where: f.
|
|
11522
|
+
where: f.where,
|
|
10790
11523
|
path: f.path,
|
|
10791
11524
|
message: f.message,
|
|
10792
11525
|
hint: f.fix ?? ""
|
|
@@ -10914,16 +11647,26 @@ var AUTHORING_RULES = [
|
|
|
10914
11647
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
10915
11648
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
10916
11649
|
},
|
|
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
|
-
//
|
|
11650
|
+
// #4698 / #9698 — the "declared but never read" gate, for the two fields of a
|
|
11651
|
+
// sharing rule where the predicate is EXACT rather than inferred.
|
|
11652
|
+
//
|
|
11653
|
+
// - `condition` has a single runtime consumer
|
|
11654
|
+
// (`bootstrapDeclaredSharingRules`) whose only use of the key is
|
|
11655
|
+
// `compileCelToFilter(condition, { variables: {} })`; a condition that
|
|
11656
|
+
// does not lower means the rule is SKIPPED at boot.
|
|
11657
|
+
// - `object` decides whether the grant is refused outright: reconcile hands
|
|
11658
|
+
// each row to `SharingService.grant`, whose ADR-0111 D7 pre-flight THROWS
|
|
11659
|
+
// `SHARING_NOT_ENABLED` when the anchor's effective sharing model is
|
|
11660
|
+
// `public` or it is a `controlled_by_parent` detail. Both are decidable
|
|
11661
|
+
// from authored metadata; the other arms of that verdict (`owner_id`, the
|
|
11662
|
+
// bypass set, federated anchors) are not, and are excluded by name.
|
|
11663
|
+
//
|
|
11664
|
+
// Either way the grant is declared and does not exist. The lint calls the
|
|
11665
|
+
// same compiler and mirrors the same verdict function, from the same inputs
|
|
11666
|
+
// — the verdict cannot drift from the consumers'. Gating for the ADR-0078
|
|
11667
|
+
// reason `SharingRuleSchema`'s own docblock states: the whole authorable
|
|
11668
|
+
// surface is enforced, and these were the fields where that sentence was not
|
|
11669
|
+
// yet true.
|
|
10927
11670
|
{
|
|
10928
11671
|
name: "validateSharingRuleEnforceability",
|
|
10929
11672
|
tier: "gating",
|
|
@@ -10931,7 +11674,7 @@ var AUTHORING_RULES = [
|
|
|
10931
11674
|
commands: ALL,
|
|
10932
11675
|
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
10933
11676
|
surfaces: CLI_ONLY,
|
|
10934
|
-
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone.
|
|
11677
|
+
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
11678
|
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
10936
11679
|
},
|
|
10937
11680
|
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
@@ -10970,8 +11713,15 @@ var AUTHORING_RULES = [
|
|
|
10970
11713
|
input: "parsed",
|
|
10971
11714
|
commands: ALL,
|
|
10972
11715
|
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
10973
|
-
|
|
10974
|
-
|
|
11716
|
+
// Runtime publish gate (#4716): the rule loads ajv LAZILY, only when the
|
|
11717
|
+
// judged snapshot actually carries a `json_schema` validation — so an
|
|
11718
|
+
// ordinary object write (no `json_schema` anywhere) still loads no
|
|
11719
|
+
// compiler, which `runtime-lazy-deps.test.ts` pins in both directions.
|
|
11720
|
+
// The load it does take (~64 ms cold once, ~15 ms warm per publish
|
|
11721
|
+
// carrying such a rule) is the measured, adjudicated price of refusing a
|
|
11722
|
+
// validation rule that would otherwise ship compiled-by-nothing.
|
|
11723
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11724
|
+
runtimeTypes: ["object"],
|
|
10975
11725
|
run: (stack) => validateRuleCompilability(stack)
|
|
10976
11726
|
},
|
|
10977
11727
|
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
@@ -10992,8 +11742,12 @@ var AUTHORING_RULES = [
|
|
|
10992
11742
|
input: "parsed",
|
|
10993
11743
|
commands: ALL,
|
|
10994
11744
|
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
10995
|
-
|
|
10996
|
-
|
|
11745
|
+
// Runtime publish gate (#4716): crosses with its compile sibling above —
|
|
11746
|
+
// the two judgements over one artifact stay on one side of the wall
|
|
11747
|
+
// (#7220's family discipline). Same lazy-ajv contract: the registered
|
|
11748
|
+
// format set is only enumerated once a schema actually names a format.
|
|
11749
|
+
surfaces: CLI_AND_RUNTIME,
|
|
11750
|
+
runtimeTypes: ["object"],
|
|
10997
11751
|
run: (stack) => validateRuleSchemaFormats(stack)
|
|
10998
11752
|
}
|
|
10999
11753
|
];
|
|
@@ -11053,7 +11807,20 @@ var TYPE_TO_STACK_KEY = {
|
|
|
11053
11807
|
permission: "permissions",
|
|
11054
11808
|
book: "books"
|
|
11055
11809
|
};
|
|
11056
|
-
|
|
11810
|
+
function narrowObjectsToPackageClosure(objects, scope) {
|
|
11811
|
+
if (!scope || typeof scope.packageId !== "string" || scope.packageId === "") return objects;
|
|
11812
|
+
const reachable = /* @__PURE__ */ new Set([scope.packageId, ...scope.dependencies]);
|
|
11813
|
+
return objects.filter((entry) => {
|
|
11814
|
+
if (!entry || typeof entry !== "object") return true;
|
|
11815
|
+
const owner = entry[PACKAGE_PROVENANCE_KEY];
|
|
11816
|
+
if (typeof owner !== "string" || owner === "" || owner === OVERLAY_PROVENANCE_SENTINEL) return true;
|
|
11817
|
+
if (reachable.has(owner)) return true;
|
|
11818
|
+
return isSystemObject(entry);
|
|
11819
|
+
});
|
|
11820
|
+
}
|
|
11821
|
+
var PACKAGE_PROVENANCE_KEY = "_packageId";
|
|
11822
|
+
var OVERLAY_PROVENANCE_SENTINEL = "sys_metadata";
|
|
11823
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books", "datasets"];
|
|
11057
11824
|
function runtimeAuthoringRulesFor(type) {
|
|
11058
11825
|
return AUTHORING_RULES.filter(
|
|
11059
11826
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -11079,7 +11846,8 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
11079
11846
|
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
11080
11847
|
const baseline = {};
|
|
11081
11848
|
for (const key of CONTEXT_STACK_KEYS) {
|
|
11082
|
-
const
|
|
11849
|
+
const raw = args.context?.[key] ?? [];
|
|
11850
|
+
const collection = key === "objects" ? narrowObjectsToPackageClosure(raw, args.packageScope) : raw;
|
|
11083
11851
|
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
11084
11852
|
}
|
|
11085
11853
|
const candidate = {
|
|
@@ -11088,6 +11856,20 @@ function buildRuntimeWriteSnapshots(args) {
|
|
|
11088
11856
|
};
|
|
11089
11857
|
return { baseline, candidate };
|
|
11090
11858
|
}
|
|
11859
|
+
var NAME_KEYED_STACK_KEYS = ["objects", "permissions", "books"];
|
|
11860
|
+
var PATH_SAFE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
11861
|
+
var TOP_LEVEL_INDEX = /^(objects|permissions|books)\[(\d+)\](.*)$/;
|
|
11862
|
+
function nameKeyFindingPath(path, candidate) {
|
|
11863
|
+
const m = TOP_LEVEL_INDEX.exec(path);
|
|
11864
|
+
if (!m) return path;
|
|
11865
|
+
const [, stackKey, index, rest] = m;
|
|
11866
|
+
if (!NAME_KEYED_STACK_KEYS.includes(stackKey)) return path;
|
|
11867
|
+
const collection = candidate[stackKey];
|
|
11868
|
+
const entry = collection?.[Number(index)];
|
|
11869
|
+
const name = entry && typeof entry === "object" ? entry.name : void 0;
|
|
11870
|
+
if (typeof name !== "string" || !PATH_SAFE_NAME.test(name)) return path;
|
|
11871
|
+
return `${stackKey}.${name}${rest}`;
|
|
11872
|
+
}
|
|
11091
11873
|
function runRules(rules, stack, ctx) {
|
|
11092
11874
|
const findings = [];
|
|
11093
11875
|
for (const rule of rules) {
|
|
@@ -11113,12 +11895,16 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11113
11895
|
const snapshots = buildRuntimeWriteSnapshots({
|
|
11114
11896
|
type: args.type,
|
|
11115
11897
|
item: args.item,
|
|
11116
|
-
...args.context !== void 0 ? { context: args.context } : {}
|
|
11898
|
+
...args.context !== void 0 ? { context: args.context } : {},
|
|
11899
|
+
...args.packageScope !== void 0 ? { packageScope: args.packageScope } : {}
|
|
11117
11900
|
});
|
|
11118
11901
|
if (!snapshots) return empty;
|
|
11119
|
-
const ctx = { sduiManifest: args.sduiManifest };
|
|
11902
|
+
const ctx = { sduiManifest: args.sduiManifest, runtimeWriteType: args.type };
|
|
11120
11903
|
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
11121
|
-
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)))
|
|
11904
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f))).map((f) => {
|
|
11905
|
+
const path = nameKeyFindingPath(f.path, snapshots.candidate);
|
|
11906
|
+
return path === f.path ? f : { ...f, path };
|
|
11907
|
+
});
|
|
11122
11908
|
return {
|
|
11123
11909
|
errors: added.filter((f) => f.severity === "error"),
|
|
11124
11910
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -11127,10 +11913,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11127
11913
|
}
|
|
11128
11914
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11129
11915
|
0 && (module.exports = {
|
|
11916
|
+
ACTION_BODY_SOURCE_UNPARSEABLE,
|
|
11130
11917
|
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
11131
11918
|
ACTION_BODY_WRITE_PATTERNS,
|
|
11132
11919
|
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
11133
11920
|
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
11921
|
+
ACTION_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
11134
11922
|
ACTION_NAME_UNDEFINED,
|
|
11135
11923
|
ACTION_NO_PLACEMENT,
|
|
11136
11924
|
ACTION_RECORD_WRITE_DISCARDED,
|
|
@@ -11176,6 +11964,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11176
11964
|
FIELD_GROUP_UNDECLARED,
|
|
11177
11965
|
FILTER_EMPTY_COMBINATOR,
|
|
11178
11966
|
FILTER_EMPTY_NODE,
|
|
11967
|
+
FILTER_PRESET_COMPARAND,
|
|
11179
11968
|
FILTER_TOKEN_UNKNOWN,
|
|
11180
11969
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
11181
11970
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -11193,6 +11982,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11193
11982
|
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
11194
11983
|
FLOW_MULTI_WRITE_UNFILTERED,
|
|
11195
11984
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
11985
|
+
FLOW_NODE_WRITE_UNPROVISIONED_ANCHOR,
|
|
11196
11986
|
FLOW_PHANTOM_AGGREGATION,
|
|
11197
11987
|
FLOW_RUNAS_UNSCOPED,
|
|
11198
11988
|
FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
@@ -11210,10 +12000,12 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11210
12000
|
FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
11211
12001
|
FORM_COLSPAN_ABSOLUTE,
|
|
11212
12002
|
FORM_FIELD_UNKNOWN,
|
|
12003
|
+
HOOK_BODY_SOURCE_UNPARSEABLE,
|
|
11213
12004
|
HOOK_BODY_WRITE_EXCLUSIONS,
|
|
11214
12005
|
HOOK_BODY_WRITE_PATTERNS,
|
|
11215
12006
|
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
11216
12007
|
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
12008
|
+
HOOK_BODY_WRITE_UNPROVISIONED_ANCHOR,
|
|
11217
12009
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
11218
12010
|
LIVENESS_DEAD_PROPERTY,
|
|
11219
12011
|
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
@@ -11232,6 +12024,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11232
12024
|
PAGE_FIELD_UNKNOWN,
|
|
11233
12025
|
PAGE_FIELD_UNPROVISIONED,
|
|
11234
12026
|
PAGE_SOURCE_CLASSNAME,
|
|
12027
|
+
PARSE_FAILURE_HINT,
|
|
11235
12028
|
PREDICATE_PATH_UNRESOLVED,
|
|
11236
12029
|
PREDICATE_PATH_UNROOTED,
|
|
11237
12030
|
PREDICATE_RHS_PATH_SHAPED,
|
|
@@ -11242,6 +12035,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11242
12035
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
11243
12036
|
REACT_CHART_FIELD_UNKNOWN,
|
|
11244
12037
|
REACT_CHART_FIELD_UNPROVISIONED,
|
|
12038
|
+
REACT_PAGE_SOURCE_UNPARSEABLE,
|
|
11245
12039
|
REFERENCE_INTEGRITY_RULES,
|
|
11246
12040
|
RLS_PREDICATE_OVER_BUDGET,
|
|
11247
12041
|
RLS_PREDICATE_UNENFORCEABLE,
|
|
@@ -11249,6 +12043,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11249
12043
|
RUNTIME_AJV_OPTIONS,
|
|
11250
12044
|
SEAL_MARKERS,
|
|
11251
12045
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
12046
|
+
SEARCHABLE_FIELD_UNPROVISIONED,
|
|
11252
12047
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
11253
12048
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
11254
12049
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
@@ -11267,9 +12062,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11267
12062
|
SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
11268
12063
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
11269
12064
|
SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
12065
|
+
SHARING_RULE_OBJECT_CONTROLLED_BY_PARENT,
|
|
12066
|
+
SHARING_RULE_OBJECT_NOT_SHAREABLE,
|
|
11270
12067
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
11271
12068
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
12069
|
+
SORT_FIELD_UNKNOWN,
|
|
12070
|
+
SORT_FIELD_UNPROVISIONED,
|
|
12071
|
+
SORT_FIELD_UNSORTABLE,
|
|
11272
12072
|
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
12073
|
+
STARTUP_SOURCE_UNPARSEABLE,
|
|
11273
12074
|
STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
11274
12075
|
STARTUP_VERDICT_HINT,
|
|
11275
12076
|
STYLE_CLASSNAME_TAILWIND,
|
|
@@ -11305,6 +12106,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11305
12106
|
authoringRulesFor,
|
|
11306
12107
|
buildAccessMatrix,
|
|
11307
12108
|
buildRuntimeWriteSnapshots,
|
|
12109
|
+
checkSortDeclaration,
|
|
12110
|
+
describeParseFailure,
|
|
11308
12111
|
diffAccessMatrix,
|
|
11309
12112
|
extractHookBodyWriteSet,
|
|
11310
12113
|
extractHookBodyWrites,
|
|
@@ -11319,6 +12122,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11319
12122
|
lintUniqueDeclarations,
|
|
11320
12123
|
lintUnscopedDeclaredIndexes,
|
|
11321
12124
|
lintViewRefs,
|
|
12125
|
+
narrowObjectsToPackageClosure,
|
|
11322
12126
|
nearestRegisteredFormat,
|
|
11323
12127
|
nullGuardMessage,
|
|
11324
12128
|
runAuthoringRules,
|
|
@@ -11357,6 +12161,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11357
12161
|
validatePageFieldBindings,
|
|
11358
12162
|
validatePageSourceStyling,
|
|
11359
12163
|
validatePredicatePathRefs,
|
|
12164
|
+
validatePresetComparands,
|
|
11360
12165
|
validateReactPageProps,
|
|
11361
12166
|
validateReactPages,
|
|
11362
12167
|
validateReadonlyFlowWrites,
|
|
@@ -11373,6 +12178,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
11373
12178
|
validateSeedStateMachine,
|
|
11374
12179
|
validateSemanticRoles,
|
|
11375
12180
|
validateSharingRuleEnforceability,
|
|
12181
|
+
validateSortableFields,
|
|
11376
12182
|
validateStackExpressions,
|
|
11377
12183
|
validateTranslatableSections,
|
|
11378
12184
|
validateTranslationReferences,
|