@aeptus/aexeo-emdash 0.9.0 → 0.9.1
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 +16 -0
- package/INSTALL.md +181 -8
- package/README.md +8 -0
- package/dist/adapter.d.ts +34 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin/Findings.d.ts.map +1 -1
- package/dist/admin.js +71 -4
- package/dist/assessment-display.d.ts +10 -0
- package/dist/assessment-display.d.ts.map +1 -0
- package/dist/cms-coverage.d.ts +10 -0
- package/dist/cms-coverage.d.ts.map +1 -0
- package/dist/configured.d.ts +11 -2
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +944 -210
- package/dist/content-evaluator.d.ts +22 -2
- package/dist/content-evaluator.d.ts.map +1 -1
- package/dist/content.d.ts +51 -4
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +99 -19
- package/dist/data-route.d.ts +9 -1
- package/dist/data-route.d.ts.map +1 -1
- package/dist/findings-view.d.ts +5 -4
- package/dist/findings-view.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -15
- package/dist/inventory.d.ts +9 -1
- package/dist/inventory.d.ts.map +1 -1
- package/dist/plugin.d.ts +30 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +797 -195
- package/dist/sidecar.d.ts +2 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/wasm-init.d.ts +5 -1
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +12 -3
- package/wasm/aexeo_emdash_bridge.d.ts +12 -1
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +4 -0
- package/wasm/aexeo_emdash_bridge_bg.js +73 -11
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +3 -1
package/dist/sandbox-entry.js
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
|
+
// src/cms-coverage.ts
|
|
2
|
+
var NATIVE_RULE_ID = /^(?:A11Y|AGT|CNT|CRW|DEP|EDT|FACTS|GEO|IMG|LLM|LNK|MAP|QLT|ROB|SCH|SEO|SOC|SRF)[0-9]{3}$/;
|
|
3
|
+
var OBSERVABLE_BODY_RULES = /* @__PURE__ */ new Set(["CNT006", "LNK003"]);
|
|
4
|
+
function isCmsObservableFinding(finding, bodyComplete = true) {
|
|
5
|
+
return finding.aexeo_origin === "content_preset" || isLegacyCmsObservableFinding(finding, bodyComplete);
|
|
6
|
+
}
|
|
7
|
+
function isLegacyCmsObservableFinding(finding, bodyComplete = true) {
|
|
8
|
+
if (!NATIVE_RULE_ID.test(finding.rule_id)) return true;
|
|
9
|
+
return bodyComplete && finding.scope === "page" && OBSERVABLE_BODY_RULES.has(finding.rule_id);
|
|
10
|
+
}
|
|
11
|
+
function hasCompleteBody(document) {
|
|
12
|
+
return document.body !== void 0 && (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every((capability) => document.body_capabilities?.includes(capability)));
|
|
13
|
+
}
|
|
14
|
+
function cmsBodyCoverage(document) {
|
|
15
|
+
if (document.body === void 0 || document.body_capabilities?.length === 0) return "unavailable";
|
|
16
|
+
return hasCompleteBody(document) ? "complete" : "partial";
|
|
17
|
+
}
|
|
18
|
+
function cmsBodyCoverageNotice(coverage) {
|
|
19
|
+
if (coverage === "complete") return void 0;
|
|
20
|
+
return coverage === void 0 ? "Body coverage is unknown for this evaluation; save or refresh to reassess it." : `Body coverage is ${coverage}; content checks depending on missing capabilities cannot be evaluated reliably.`;
|
|
21
|
+
}
|
|
22
|
+
|
|
1
23
|
// src/adapter.ts
|
|
2
|
-
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
|
|
24
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields, descriptionFields) {
|
|
3
25
|
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
4
26
|
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
5
27
|
const adapted = adaptContentItem(content, {
|
|
@@ -7,7 +29,8 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
|
|
|
7
29
|
publicUrl,
|
|
8
30
|
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
9
31
|
...blockMappings === void 0 ? {} : { blockMappings },
|
|
10
|
-
...contentFields === void 0 ? {} : { contentFields }
|
|
32
|
+
...contentFields === void 0 ? {} : { contentFields },
|
|
33
|
+
...descriptionFields === void 0 ? {} : { descriptionFields }
|
|
11
34
|
});
|
|
12
35
|
const translationSet = await contentApi.getTranslations(
|
|
13
36
|
content.type,
|
|
@@ -32,14 +55,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
|
|
|
32
55
|
return adapted;
|
|
33
56
|
}
|
|
34
57
|
function adaptContentItem(content, options = {}) {
|
|
58
|
+
return adaptContentItemInternal(content, options, true);
|
|
59
|
+
}
|
|
60
|
+
function adaptContentItemInternal(content, options, includeSourcePaths) {
|
|
35
61
|
const route = options.route ?? deriveRoute(content);
|
|
36
62
|
const title = stringOrEmpty(
|
|
37
63
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
38
64
|
);
|
|
39
|
-
const document = { route, title };
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
65
|
+
const document = { route, title, description_source_observed: false };
|
|
66
|
+
const fields = options.descriptionFields;
|
|
67
|
+
if (fields === void 0) {
|
|
68
|
+
const description = content.seo?.description ?? content.data["description"];
|
|
69
|
+
if (typeof description === "string" && description.length > 0) document.description = description;
|
|
70
|
+
} else if (fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0)) {
|
|
71
|
+
let observed = true;
|
|
72
|
+
for (const field of fields) {
|
|
73
|
+
if (!Object.hasOwn(content.data, field) || content.data[field] === void 0 || content.data[field] !== null && typeof content.data[field] !== "string") {
|
|
74
|
+
observed = false;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
const value = content.data[field];
|
|
78
|
+
if (typeof value === "string" && value.length > 0) {
|
|
79
|
+
document.description = value;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (observed) document.description_source_observed = true;
|
|
43
84
|
}
|
|
44
85
|
const canonical = content.seo?.canonical;
|
|
45
86
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
@@ -54,6 +95,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
54
95
|
document.body = body.blocks;
|
|
55
96
|
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
56
97
|
}
|
|
98
|
+
const bodySourcePaths = body === null || !includeSourcePaths ? void 0 : portableTextSourcePaths(content.data, body.blocks);
|
|
99
|
+
const bodySourceSpanPaths = body === null || !includeSourcePaths ? void 0 : portableTextSourceSpanPaths(content.data, body.blocks, bodySourcePaths);
|
|
57
100
|
return {
|
|
58
101
|
document,
|
|
59
102
|
meta: {
|
|
@@ -63,6 +106,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
63
106
|
status: content.status,
|
|
64
107
|
title,
|
|
65
108
|
publicUrl: options.publicUrl ?? null,
|
|
109
|
+
...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
|
|
110
|
+
...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
|
|
66
111
|
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
67
112
|
}
|
|
68
113
|
};
|
|
@@ -82,6 +127,100 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
82
127
|
function stringOrEmpty(value) {
|
|
83
128
|
return typeof value === "string" ? value : "";
|
|
84
129
|
}
|
|
130
|
+
function portableTextSourcePaths(data, blocks) {
|
|
131
|
+
if (blocks.length === 0) return void 0;
|
|
132
|
+
const targets = new WeakSet(blocks);
|
|
133
|
+
const paths = /* @__PURE__ */ new WeakMap();
|
|
134
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
135
|
+
const stack = [{ value: data, path: ["data"] }];
|
|
136
|
+
let count = 0;
|
|
137
|
+
let inspected = 0;
|
|
138
|
+
while (stack.length > 0) {
|
|
139
|
+
const current = stack.pop();
|
|
140
|
+
if (typeof current.value !== "object" || current.value === null) continue;
|
|
141
|
+
if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
|
|
142
|
+
seen.add(current.value);
|
|
143
|
+
if (targets.has(current.value)) {
|
|
144
|
+
paths.set(current.value, current.path);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(current.value)) {
|
|
148
|
+
for (let i = 0; i < current.value.length; i++) {
|
|
149
|
+
if (++inspected > 5e4) return void 0;
|
|
150
|
+
const value = current.value[i];
|
|
151
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i] });
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
for (const key in current.value) {
|
|
155
|
+
if (!Object.hasOwn(current.value, key)) continue;
|
|
156
|
+
if (++inspected > 5e4) return void 0;
|
|
157
|
+
const value = current.value[key];
|
|
158
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key] });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const result = blocks.map((block) => paths.get(block) ?? null);
|
|
163
|
+
return result.some((path) => path !== null) ? result : void 0;
|
|
164
|
+
}
|
|
165
|
+
var BODY_CAPABILITIES = ["text", "headings", "paragraphs", "links"];
|
|
166
|
+
function intersectCapabilities(current, other) {
|
|
167
|
+
return other === void 0 ? [...current] : current.filter((capability) => other.includes(capability));
|
|
168
|
+
}
|
|
169
|
+
function bodyWithCapabilities(blocks, capabilities) {
|
|
170
|
+
return capabilities.length === BODY_CAPABILITIES.length ? { blocks } : { blocks, capabilities };
|
|
171
|
+
}
|
|
172
|
+
function portableTextSourceSpanPaths(data, blocks, blockPaths) {
|
|
173
|
+
const targets = /* @__PURE__ */ new WeakSet();
|
|
174
|
+
let targetCount = 0;
|
|
175
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
176
|
+
if (blockPaths?.[i]) continue;
|
|
177
|
+
for (const child of blocks[i].children) {
|
|
178
|
+
if (++targetCount > 5e4) return void 0;
|
|
179
|
+
targets.add(child);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (targetCount === 0) return void 0;
|
|
183
|
+
const paths = /* @__PURE__ */ new WeakMap();
|
|
184
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
185
|
+
const stack = [
|
|
186
|
+
{ value: data, path: ["data"], portableChild: false }
|
|
187
|
+
];
|
|
188
|
+
let count = 0;
|
|
189
|
+
let inspected = 0;
|
|
190
|
+
while (stack.length > 0) {
|
|
191
|
+
const current = stack.pop();
|
|
192
|
+
if (typeof current.value !== "object" || current.value === null) continue;
|
|
193
|
+
if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
|
|
194
|
+
seen.add(current.value);
|
|
195
|
+
if (current.portableChild && targets.has(current.value) && isPortableTextSpan(current.value)) {
|
|
196
|
+
paths.set(current.value, [...current.path, "text"]);
|
|
197
|
+
}
|
|
198
|
+
if (Array.isArray(current.value)) {
|
|
199
|
+
for (let i = 0; i < current.value.length; i++) {
|
|
200
|
+
if (++inspected > 5e4) return void 0;
|
|
201
|
+
const value = current.value[i];
|
|
202
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i], portableChild: current.portableChild });
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
for (const key in current.value) {
|
|
206
|
+
if (!Object.hasOwn(current.value, key)) continue;
|
|
207
|
+
if (++inspected > 5e4) return void 0;
|
|
208
|
+
const value = current.value[key];
|
|
209
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key], portableChild: key === "children" && isPortableTextBlockCandidate(current.value) });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let mapped = false;
|
|
214
|
+
const result = blocks.map((block, index) => {
|
|
215
|
+
if (blockPaths?.[index]) return null;
|
|
216
|
+
return block.children.map((child) => {
|
|
217
|
+
const path = paths.get(child) ?? null;
|
|
218
|
+
if (path !== null) mapped = true;
|
|
219
|
+
return path;
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
return mapped ? result : void 0;
|
|
223
|
+
}
|
|
85
224
|
function extractSelectedContent(data, fields, mappings) {
|
|
86
225
|
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
87
226
|
const all = ["text", "headings", "paragraphs", "links"];
|
|
@@ -90,10 +229,17 @@ function extractSelectedContent(data, fields, mappings) {
|
|
|
90
229
|
let observed = false;
|
|
91
230
|
for (const field of fields) {
|
|
92
231
|
const value = data[field];
|
|
93
|
-
if (value === void 0
|
|
232
|
+
if (value === void 0) {
|
|
233
|
+
capabilities = [];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
94
236
|
observed = true;
|
|
237
|
+
if (value === null) continue;
|
|
95
238
|
const part = extractFirstPortableText({ content: value }, mappings);
|
|
96
|
-
if (part === null)
|
|
239
|
+
if (part === null) {
|
|
240
|
+
capabilities = [];
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
97
243
|
blocks.push(...part.blocks);
|
|
98
244
|
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
99
245
|
}
|
|
@@ -104,9 +250,14 @@ function extractPortableText(data, mappings) {
|
|
|
104
250
|
const result = extractFirstPortableText(data, mappings);
|
|
105
251
|
if (result === null) return null;
|
|
106
252
|
const candidates = Object.entries(data).filter(
|
|
107
|
-
([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
253
|
+
([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
108
254
|
);
|
|
109
|
-
|
|
255
|
+
const unrecognizedBody = ["body", "content", "blocks"].some((field) => {
|
|
256
|
+
const value = data[field];
|
|
257
|
+
if (value == null || Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) return false;
|
|
258
|
+
return !candidates.some(([candidate]) => candidate === field);
|
|
259
|
+
});
|
|
260
|
+
return candidates.length > 1 || unrecognizedBody ? { blocks: result.blocks, capabilities: [] } : result;
|
|
110
261
|
}
|
|
111
262
|
function extractFirstPortableText(data, mappings) {
|
|
112
263
|
let foundEmptyBody = false;
|
|
@@ -114,7 +265,8 @@ function extractFirstPortableText(data, mappings) {
|
|
|
114
265
|
const value = data[slug];
|
|
115
266
|
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
116
267
|
if (isPortableTextArray(value)) return { blocks: value };
|
|
117
|
-
if (
|
|
268
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
269
|
+
if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
|
|
118
270
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
119
271
|
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
120
272
|
}
|
|
@@ -122,32 +274,184 @@ function extractFirstPortableText(data, mappings) {
|
|
|
122
274
|
const blocks = data["blocks"];
|
|
123
275
|
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
124
276
|
if (isPortableTextArray(blocks)) return { blocks };
|
|
125
|
-
if (
|
|
277
|
+
if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
|
|
278
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
|
|
126
279
|
for (const [field, value] of Object.entries(data)) {
|
|
127
280
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
128
281
|
if (isPortableTextArray(value)) return { blocks: value };
|
|
129
|
-
if (
|
|
282
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
283
|
+
if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
|
|
130
284
|
}
|
|
131
285
|
return foundEmptyBody ? { blocks: [] } : null;
|
|
132
286
|
}
|
|
133
287
|
function isPortableTextArray(value) {
|
|
134
288
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
135
|
-
(item) =>
|
|
136
|
-
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
137
|
-
)
|
|
289
|
+
(item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
|
|
138
290
|
);
|
|
139
291
|
}
|
|
292
|
+
function isPortableTextSpan(value) {
|
|
293
|
+
return isRecord(value) && value["_type"] === "span" && typeof value["text"] === "string";
|
|
294
|
+
}
|
|
295
|
+
function isPortableTextBlockCandidate(value) {
|
|
296
|
+
return isRecord(value) && value["_type"] === "block" && Array.isArray(value["children"]);
|
|
297
|
+
}
|
|
298
|
+
function isWireSafeSpan(value) {
|
|
299
|
+
return isPortableTextSpan(value) && isRecord(value) && (value["_key"] == null || typeof value["_key"] === "string") && (value["marks"] === void 0 || Array.isArray(value["marks"]) && value["marks"].every((mark) => typeof mark === "string"));
|
|
300
|
+
}
|
|
301
|
+
function isWireSafeMarkDef(value) {
|
|
302
|
+
return isRecord(value) && typeof value["_key"] === "string" && typeof value["_type"] === "string" && (value["_type"] !== "link" || typeof value["href"] === "string");
|
|
303
|
+
}
|
|
304
|
+
function isWireSafeBlock(block) {
|
|
305
|
+
const level = block["level"];
|
|
306
|
+
return (block["_key"] == null || typeof block["_key"] === "string") && (block["style"] === void 0 || typeof block["style"] === "string" && ["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(block["style"])) && (block["listItem"] == null || block["listItem"] === "bullet" || block["listItem"] === "number") && (level == null || typeof level === "number" && Number.isInteger(level) && level >= 0 && level <= 4294967295) && (block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) && block["markDefs"].every(isWireSafeMarkDef)) && block["children"].every(isWireSafeSpan);
|
|
307
|
+
}
|
|
308
|
+
function normalizePortableBlock(block) {
|
|
309
|
+
const normalized = { ...block };
|
|
310
|
+
let incomplete = false;
|
|
311
|
+
if (normalized["_key"] != null && typeof normalized["_key"] !== "string") {
|
|
312
|
+
delete normalized["_key"];
|
|
313
|
+
incomplete = true;
|
|
314
|
+
}
|
|
315
|
+
if (normalized["style"] !== void 0 && (typeof normalized["style"] !== "string" || !["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(normalized["style"]))) {
|
|
316
|
+
delete normalized["style"];
|
|
317
|
+
incomplete = true;
|
|
318
|
+
}
|
|
319
|
+
if (normalized["listItem"] != null && normalized["listItem"] !== "bullet" && normalized["listItem"] !== "number") {
|
|
320
|
+
delete normalized["listItem"];
|
|
321
|
+
incomplete = true;
|
|
322
|
+
}
|
|
323
|
+
const level = normalized["level"];
|
|
324
|
+
if (level != null && (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4294967295)) {
|
|
325
|
+
delete normalized["level"];
|
|
326
|
+
incomplete = true;
|
|
327
|
+
}
|
|
328
|
+
if (normalized["markDefs"] !== void 0) {
|
|
329
|
+
if (!Array.isArray(normalized["markDefs"])) {
|
|
330
|
+
delete normalized["markDefs"];
|
|
331
|
+
incomplete = true;
|
|
332
|
+
} else {
|
|
333
|
+
const defs = normalized["markDefs"].filter(isWireSafeMarkDef);
|
|
334
|
+
if (defs.length !== normalized["markDefs"].length) {
|
|
335
|
+
normalized["markDefs"] = defs;
|
|
336
|
+
incomplete = true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
normalized["children"] = block["children"].map((child) => {
|
|
341
|
+
if (!isPortableTextSpan(child) || !isRecord(child) || isWireSafeSpan(child)) return child;
|
|
342
|
+
const span = { ...child };
|
|
343
|
+
if (span["_key"] != null && typeof span["_key"] !== "string") delete span["_key"];
|
|
344
|
+
if (span["marks"] !== void 0 && (!Array.isArray(span["marks"]) || !span["marks"].every((mark) => typeof mark === "string"))) delete span["marks"];
|
|
345
|
+
incomplete = true;
|
|
346
|
+
return span;
|
|
347
|
+
});
|
|
348
|
+
return { block: normalized, incomplete };
|
|
349
|
+
}
|
|
350
|
+
function isMixedPortableTextArray(value) {
|
|
351
|
+
return Array.isArray(value) && value.some(isPortableTextBlockCandidate);
|
|
352
|
+
}
|
|
353
|
+
function mapMixedPortableText(blocks, mappings, depth = 0) {
|
|
354
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
355
|
+
const result = [];
|
|
356
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
357
|
+
for (const block of blocks) {
|
|
358
|
+
if (isPortableTextArray([block])) {
|
|
359
|
+
result.push(block);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (isPortableTextBlockCandidate(block)) {
|
|
363
|
+
const normalized = normalizePortableBlock(block);
|
|
364
|
+
const projected = projectInlineBlock(normalized.block, mappings);
|
|
365
|
+
result.push(projected.block);
|
|
366
|
+
capabilities = normalized.incomplete ? [] : intersectCapabilities(capabilities, projected.capabilities);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (!isRecord(block) || typeof block["_type"] !== "string" || block["_type"] === "block") {
|
|
370
|
+
capabilities = [];
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const mapped = mapCustomBlocks([block], mappings, depth + 1);
|
|
374
|
+
result.push(...mapped.blocks);
|
|
375
|
+
capabilities = intersectCapabilities(capabilities, mapped.capabilities);
|
|
376
|
+
}
|
|
377
|
+
return bodyWithCapabilities(result, capabilities);
|
|
378
|
+
}
|
|
379
|
+
function projectInlineBlock(block, mappings) {
|
|
380
|
+
const children = [];
|
|
381
|
+
const defs = Array.isArray(block["markDefs"]) ? [...block["markDefs"]] : [];
|
|
382
|
+
const keys = new Set(defs.flatMap((def) => isRecord(def) && typeof def["_key"] === "string" ? [def["_key"]] : []));
|
|
383
|
+
let sequence = 0;
|
|
384
|
+
let capabilities = block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) ? [...BODY_CAPABILITIES] : [];
|
|
385
|
+
for (const child of block["children"]) {
|
|
386
|
+
if (isRecord(child) && Array.isArray(child["marks"])) {
|
|
387
|
+
for (const mark of child["marks"]) if (typeof mark === "string") keys.add(mark);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
for (const child of block["children"]) {
|
|
391
|
+
if (isPortableTextSpan(child)) {
|
|
392
|
+
children.push(child);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const type = isRecord(child) ? child["_type"] : void 0;
|
|
396
|
+
const mapping = typeof type === "string" && mappings && Object.hasOwn(mappings, type) ? mappings[type] : void 0;
|
|
397
|
+
if (!isRecord(child) || !isRecord(mapping) || !Array.isArray(mapping["inline"])) {
|
|
398
|
+
capabilities = [];
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const declared = mapping["capabilities"];
|
|
402
|
+
if (declared !== void 0 && (!Array.isArray(declared) || declared.some((value) => typeof value !== "string" || !BODY_CAPABILITIES.includes(value)) || new Set(declared).size !== declared.length)) {
|
|
403
|
+
capabilities = [];
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const newChildren = [];
|
|
407
|
+
const newDefs = [];
|
|
408
|
+
let valid = true;
|
|
409
|
+
for (const field of mapping["inline"]) {
|
|
410
|
+
if (!isRecord(field) || field["kind"] !== "text" && field["kind"] !== "link" || typeof field["field"] !== "string" || !field["field"] || field["optional"] !== void 0 && typeof field["optional"] !== "boolean" || field["kind"] === "link" && (typeof field["hrefField"] !== "string" || !field["hrefField"])) {
|
|
411
|
+
valid = false;
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
const value = child[field["field"]];
|
|
415
|
+
const href = field["kind"] === "link" && typeof field["hrefField"] === "string" && field["hrefField"] ? child[field["hrefField"]] : void 0;
|
|
416
|
+
if ((value == null || value === "") && field["optional"] === true && (field["kind"] !== "link" || href == null || href === "")) continue;
|
|
417
|
+
if (typeof value !== "string" || field["kind"] === "link" && (typeof href !== "string" || !href)) {
|
|
418
|
+
valid = false;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
if (field["kind"] === "text") newChildren.push({ _type: "span", text: value, marks: [] });
|
|
422
|
+
else {
|
|
423
|
+
let key;
|
|
424
|
+
do {
|
|
425
|
+
key = `aexeo-inline-${sequence++}`;
|
|
426
|
+
} while (keys.has(key));
|
|
427
|
+
keys.add(key);
|
|
428
|
+
newDefs.push({ _key: key, _type: "link", href });
|
|
429
|
+
newChildren.push({ _type: "span", text: value, marks: [key] });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (!valid) {
|
|
433
|
+
capabilities = [];
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
children.push(...newChildren);
|
|
437
|
+
defs.push(...newDefs);
|
|
438
|
+
if (Array.isArray(declared)) capabilities = intersectCapabilities(capabilities, declared);
|
|
439
|
+
}
|
|
440
|
+
return { block: { ...block, children, markDefs: defs }, capabilities };
|
|
441
|
+
}
|
|
140
442
|
function isEmdashBlocksArray(value, field) {
|
|
141
443
|
if (!Array.isArray(value) || value.length === 0) return false;
|
|
142
444
|
return value.every(
|
|
143
|
-
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
445
|
+
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks" || field === "body" || field === "content")
|
|
144
446
|
);
|
|
145
447
|
}
|
|
146
|
-
function mapCustomBlocks(blocks, mappings) {
|
|
448
|
+
function mapCustomBlocks(blocks, mappings, depth = 0) {
|
|
449
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
147
450
|
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
148
451
|
const result = [];
|
|
149
|
-
|
|
150
|
-
|
|
452
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
453
|
+
const mapFields = (record, fields, depth2) => {
|
|
454
|
+
if (depth2 > 8 || fields.length === 0) return false;
|
|
151
455
|
for (const entry of fields) {
|
|
152
456
|
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
153
457
|
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
@@ -159,8 +463,16 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
159
463
|
continue;
|
|
160
464
|
}
|
|
161
465
|
if (entry.kind === "richText") {
|
|
162
|
-
if (!Array.isArray(value)
|
|
163
|
-
|
|
466
|
+
if (!Array.isArray(value)) return false;
|
|
467
|
+
if (value.length === 0 || isPortableTextArray(value)) {
|
|
468
|
+
result.push(...value);
|
|
469
|
+
} else if (isMixedPortableTextArray(value) || isEmdashBlocksArray(value, "blocks")) {
|
|
470
|
+
const nested = mapMixedPortableText(value, mappings, depth2 + 1);
|
|
471
|
+
result.push(...nested.blocks);
|
|
472
|
+
capabilities = intersectCapabilities(capabilities, nested.capabilities);
|
|
473
|
+
} else {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
164
476
|
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
165
477
|
if (typeof value !== "string") return false;
|
|
166
478
|
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
@@ -175,7 +487,7 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
175
487
|
} else if (entry.kind === "repeat") {
|
|
176
488
|
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
177
489
|
for (const item of value) {
|
|
178
|
-
if (!isRecord(item) || !mapFields(item, entry.fields,
|
|
490
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
|
|
179
491
|
}
|
|
180
492
|
} else {
|
|
181
493
|
return false;
|
|
@@ -184,10 +496,20 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
184
496
|
return true;
|
|
185
497
|
};
|
|
186
498
|
for (const block of blocks) {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
499
|
+
const mapping = mappings?.[String(block["_type"])];
|
|
500
|
+
let fields;
|
|
501
|
+
if (Array.isArray(mapping)) {
|
|
502
|
+
fields = mapping;
|
|
503
|
+
} else if (isRecord(mapping) && Array.isArray(mapping["fields"]) && Array.isArray(mapping["capabilities"])) {
|
|
504
|
+
const declared = mapping["capabilities"];
|
|
505
|
+
if (declared.some((capability) => typeof capability !== "string" || !BODY_CAPABILITIES.includes(capability)) || new Set(declared).size !== declared.length) return unavailable();
|
|
506
|
+
fields = mapping["fields"];
|
|
507
|
+
capabilities = intersectCapabilities(capabilities, declared);
|
|
508
|
+
} else return unavailable();
|
|
509
|
+
if (fields.length === 0) continue;
|
|
510
|
+
if (!mapFields(block, fields, depth)) return unavailable();
|
|
511
|
+
}
|
|
512
|
+
return bodyWithCapabilities(result, capabilities);
|
|
191
513
|
}
|
|
192
514
|
function blocksToPortableText(blocks) {
|
|
193
515
|
const paragraphs = [];
|
|
@@ -201,20 +523,49 @@ function blocksToPortableText(blocks) {
|
|
|
201
523
|
}
|
|
202
524
|
return paragraphs;
|
|
203
525
|
}
|
|
526
|
+
var FALLBACK_MAX_DEPTH = 64;
|
|
527
|
+
var FALLBACK_MAX_NODES = 5e4;
|
|
528
|
+
var FALLBACK_MAX_CHARS = 1048576;
|
|
204
529
|
function collectText(value, key, out) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
530
|
+
const active = /* @__PURE__ */ new WeakSet();
|
|
531
|
+
let nodes = 0;
|
|
532
|
+
let remaining = FALLBACK_MAX_CHARS;
|
|
533
|
+
const visit = (current, field, depth) => {
|
|
534
|
+
if (depth > FALLBACK_MAX_DEPTH || nodes >= FALLBACK_MAX_NODES || remaining <= 0) return;
|
|
535
|
+
nodes++;
|
|
536
|
+
if (typeof current === "string") {
|
|
537
|
+
if (!isStructuralField(field)) {
|
|
538
|
+
const separator = out.length > 0 ? 1 : 0;
|
|
539
|
+
const limit = Math.max(0, remaining - separator);
|
|
540
|
+
let text = current.slice(0, limit);
|
|
541
|
+
if (limit < current.length && /[\uD800-\uDBFF]/.test(text.at(-1) ?? "") && /[\uDC00-\uDFFF]/.test(current[limit] ?? "")) text = text.slice(0, -1);
|
|
542
|
+
text = text.trim();
|
|
543
|
+
if (text.length > 0) {
|
|
544
|
+
out.push(text);
|
|
545
|
+
remaining -= text.length + separator;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!Array.isArray(current) && !isRecord(current)) return;
|
|
551
|
+
if (active.has(current)) return;
|
|
552
|
+
active.add(current);
|
|
553
|
+
if (Array.isArray(current)) {
|
|
554
|
+
for (const item of current) {
|
|
555
|
+
if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
|
|
556
|
+
visit(item, field, depth + 1);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
for (const childKey in current) {
|
|
560
|
+
if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
|
|
561
|
+
if (Object.hasOwn(current, childKey) && !isStructuralField(childKey)) {
|
|
562
|
+
visit(current[childKey], childKey, depth + 1);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
active.delete(current);
|
|
567
|
+
};
|
|
568
|
+
visit(value, key, 0);
|
|
218
569
|
}
|
|
219
570
|
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
220
571
|
"_key",
|
|
@@ -273,12 +624,13 @@ function isRecord(value) {
|
|
|
273
624
|
}
|
|
274
625
|
|
|
275
626
|
// src/version.ts
|
|
276
|
-
var PACKAGE_VERSION = "0.9.
|
|
627
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
277
628
|
|
|
278
629
|
// src/sidecar.ts
|
|
279
630
|
async function evaluateViaSidecar(http, config, documents, collections) {
|
|
280
631
|
const body = {
|
|
281
|
-
documents
|
|
632
|
+
documents,
|
|
633
|
+
responseFormat: 2
|
|
282
634
|
};
|
|
283
635
|
if (collections !== void 0) body.collections = collections;
|
|
284
636
|
if (config.configJson !== void 0) {
|
|
@@ -327,20 +679,36 @@ async function evaluateViaSidecar(http, config, documents, collections) {
|
|
|
327
679
|
return { ok: false, reason: "invalid_response", detail };
|
|
328
680
|
}
|
|
329
681
|
try {
|
|
330
|
-
const
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
682
|
+
const value = JSON.parse(raw);
|
|
683
|
+
if (Array.isArray(value)) return { ok: true, findings: value };
|
|
684
|
+
if (!value || typeof value !== "object" || !("findings" in value) || !Array.isArray(value.findings) || !("assessments" in value) || !value.assessments || typeof value.assessments !== "object" || Array.isArray(value.assessments)) {
|
|
685
|
+
return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid assessment envelope" };
|
|
686
|
+
}
|
|
687
|
+
for (const finding of value.findings) {
|
|
688
|
+
if (finding && typeof finding === "object" && "aexeo_source_location" in finding && !validCounts(finding.aexeo_source_location, ["block_index", "span_index", "utf16_offset"])) {
|
|
689
|
+
return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid source location" };
|
|
690
|
+
}
|
|
337
691
|
}
|
|
338
|
-
|
|
692
|
+
for (const assessment of Object.values(value.assessments)) {
|
|
693
|
+
if (assessment && typeof assessment === "object" && "source_mapping" in assessment && (!validCounts(assessment.source_mapping, ["eligible", "mapped", "unmapped"]) || assessment.source_mapping.mapped + assessment.source_mapping.unmapped !== assessment.source_mapping.eligible)) {
|
|
694
|
+
return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid source mapping coverage" };
|
|
695
|
+
}
|
|
696
|
+
if (!assessment || typeof assessment !== "object" || !("status" in assessment) || assessment.status !== "evaluated" || !("preset" in assessment) || typeof assessment.preset !== "string" || !("gates" in assessment) || !Array.isArray(assessment.gates) || assessment.gates.some((gate) => !gate || typeof gate !== "object" || !("gate_id" in gate) || typeof gate.gate_id !== "string" || !("status" in gate) || !["passed", "failed", "not_checked"].includes(String(gate.status)) || !("blocking" in gate) || typeof gate.blocking !== "boolean" || !("scope" in gate) || !["document", "collection", "site"].includes(String(gate.scope)) || !("required_capability" in gate) || typeof gate.required_capability !== "string" || !("reason" in gate) || gate.reason !== null && typeof gate.reason !== "string" || "measurements" in gate && (gate.measurements === null || typeof gate.measurements !== "object" || Array.isArray(gate.measurements) || Object.values(gate.measurements).some((v) => typeof v !== "number" || !Number.isFinite(v))))) {
|
|
697
|
+
return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid gate assessment" };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { ok: true, findings: value.findings, assessments: value.assessments };
|
|
339
701
|
} catch (err) {
|
|
340
702
|
const detail = err instanceof Error ? err.message : String(err);
|
|
341
703
|
return { ok: false, reason: "invalid_response", detail };
|
|
342
704
|
}
|
|
343
705
|
}
|
|
706
|
+
function validCounts(value, keys) {
|
|
707
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && keys.every((key) => {
|
|
708
|
+
const count = value[key];
|
|
709
|
+
return typeof count === "number" && Number.isSafeInteger(count) && count >= 0;
|
|
710
|
+
});
|
|
711
|
+
}
|
|
344
712
|
function trimTrailingSlash(url) {
|
|
345
713
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
346
714
|
}
|
|
@@ -359,6 +727,7 @@ var CONFIG_TOKEN_KEY = "config:eval_token";
|
|
|
359
727
|
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
360
728
|
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
361
729
|
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
730
|
+
var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
|
|
362
731
|
var PLUGIN_SETTINGS_SCHEMA = {
|
|
363
732
|
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
364
733
|
type: "string",
|
|
@@ -366,6 +735,12 @@ var PLUGIN_SETTINGS_SCHEMA = {
|
|
|
366
735
|
description: "Ordered rendered body fields per collection.",
|
|
367
736
|
multiline: true
|
|
368
737
|
},
|
|
738
|
+
[SETTINGS_DESCRIPTION_FIELDS_KEY]: {
|
|
739
|
+
type: "string",
|
|
740
|
+
label: "Description fields by collection (JSON)",
|
|
741
|
+
description: "Ordered renderer description fallback fields per collection.",
|
|
742
|
+
multiline: true
|
|
743
|
+
},
|
|
369
744
|
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
370
745
|
type: "string",
|
|
371
746
|
label: "Custom block mappings (JSON)",
|
|
@@ -445,14 +820,54 @@ function describeError(err, secret) {
|
|
|
445
820
|
function recordContentCoverage(ctx, documents) {
|
|
446
821
|
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
447
822
|
for (const document of documents) {
|
|
448
|
-
|
|
449
|
-
else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
|
|
450
|
-
(capability) => document.body_capabilities?.includes(capability)
|
|
451
|
-
)) counts.complete++;
|
|
452
|
-
else counts.partial++;
|
|
823
|
+
counts[cmsBodyCoverage(document)]++;
|
|
453
824
|
}
|
|
454
825
|
try {
|
|
455
826
|
ctx.log?.info?.("aexeo.content_coverage", counts);
|
|
827
|
+
ctx.log?.info?.("aexeo.page_coverage", { documents: documents.length, rendered_page: "not_observed", generated_metadata: "not_observed" });
|
|
828
|
+
const observed = documents.filter((document) => document.description_source_observed === true).length;
|
|
829
|
+
const withValue = documents.filter((document) => document.description_source_observed === true && !!document.description).length;
|
|
830
|
+
ctx.log?.info?.("aexeo.description_coverage", { documents: documents.length, observed, with_value: withValue, unverified: documents.length - observed });
|
|
831
|
+
} catch {
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function recordAssessmentCoverage(ctx, result, documents) {
|
|
835
|
+
let passed = 0, failed = 0, notChecked = 0, mappingReports = 0, mappingEligible = 0, mappingMapped = 0, mappingUnmapped = 0;
|
|
836
|
+
if (result.ok) for (const assessment of Object.values(result.assessments ?? {})) {
|
|
837
|
+
if (assessment.status !== "evaluated") continue;
|
|
838
|
+
if (assessment.source_mapping) {
|
|
839
|
+
mappingReports++;
|
|
840
|
+
mappingEligible += assessment.source_mapping.eligible;
|
|
841
|
+
mappingMapped += assessment.source_mapping.mapped;
|
|
842
|
+
mappingUnmapped += assessment.source_mapping.unmapped;
|
|
843
|
+
}
|
|
844
|
+
for (const gate of assessment.gates) {
|
|
845
|
+
if (gate.status === "passed") passed++;
|
|
846
|
+
else if (gate.status === "failed") failed++;
|
|
847
|
+
else notChecked++;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
let assessedDocuments = 0, missingDocuments = 0, errorDocuments = 0;
|
|
851
|
+
if (result.ok) for (const document of documents) {
|
|
852
|
+
const assessment = result.assessments?.[document.route];
|
|
853
|
+
if (assessment === void 0) missingDocuments++;
|
|
854
|
+
else if (assessment.status === "error") errorDocuments++;
|
|
855
|
+
else assessedDocuments++;
|
|
856
|
+
}
|
|
857
|
+
const collidingFindings = result.ok ? result.findings.filter((finding) => finding.aexeo_origin === "content_preset" && (SITE_GRAPH_RULE_IDS.has(finding.rule_id) || !isLegacyCmsObservableFinding(finding))).length : 0;
|
|
858
|
+
try {
|
|
859
|
+
ctx.log?.info?.("aexeo.preset_coverage", { status: result.ok ? "evaluated" : "error", documents: documents.length, assessed_documents: assessedDocuments, missing_assessments: missingDocuments, assessment_errors: errorDocuments, passed, failed, not_checked: notChecked, colliding_findings: collidingFindings, mapping_reports: mappingReports, mapping_eligible: mappingEligible, mapping_mapped: mappingMapped, mapping_unmapped: mappingUnmapped });
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
|
|
863
|
+
let withheld = 0, incompleteBodyWithheld = 0;
|
|
864
|
+
if (result.ok) for (const finding of result.findings) {
|
|
865
|
+
if (finding.aexeo_origin === "content_preset") continue;
|
|
866
|
+
if (!isLegacyCmsObservableFinding(finding)) withheld++;
|
|
867
|
+
else if (!isLegacyCmsObservableFinding(finding, completeByRoute.get(finding.path) ?? false)) incompleteBodyWithheld++;
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
ctx.log?.info?.("aexeo.cms_finding_coverage", { status: result.ok ? "evaluated" : "error", rendered_findings_withheld: withheld, incomplete_body_findings_withheld: incompleteBodyWithheld });
|
|
456
871
|
} catch {
|
|
457
872
|
}
|
|
458
873
|
}
|
|
@@ -479,20 +894,26 @@ async function readSandboxBlockMappings(ctx) {
|
|
|
479
894
|
return void 0;
|
|
480
895
|
}
|
|
481
896
|
}
|
|
482
|
-
async function
|
|
483
|
-
if (!ctx.settings) return void 0;
|
|
897
|
+
async function readSandboxFields(ctx, key, warning) {
|
|
898
|
+
if (!ctx.settings) return { fields: void 0, unavailable: false };
|
|
484
899
|
try {
|
|
485
|
-
const raw = await ctx.settings.get(
|
|
486
|
-
if (raw === null || raw === "") return void 0;
|
|
900
|
+
const raw = await ctx.settings.get(key);
|
|
901
|
+
if (raw === null || raw === "") return { fields: void 0, unavailable: false };
|
|
487
902
|
if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid selection size or type");
|
|
488
903
|
const value = JSON.parse(raw);
|
|
489
904
|
if (value === null || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((fields) => Array.isArray(fields) && fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0))) throw new Error("invalid selection");
|
|
490
|
-
return value;
|
|
905
|
+
return { fields: value, unavailable: false };
|
|
491
906
|
} catch {
|
|
492
|
-
ctx.log?.warn?.(
|
|
493
|
-
return void 0;
|
|
907
|
+
ctx.log?.warn?.(warning);
|
|
908
|
+
return { fields: void 0, unavailable: true };
|
|
494
909
|
}
|
|
495
910
|
}
|
|
911
|
+
function readSandboxContentFieldState(ctx) {
|
|
912
|
+
return readSandboxFields(ctx, SETTINGS_CONTENT_FIELDS_KEY, "Aexeo content field selection unavailable; body coverage remains unverified");
|
|
913
|
+
}
|
|
914
|
+
async function readSandboxDescriptionFields(ctx) {
|
|
915
|
+
return (await readSandboxFields(ctx, SETTINGS_DESCRIPTION_FIELDS_KEY, "Aexeo description field selection unavailable; description remains unverified")).fields;
|
|
916
|
+
}
|
|
496
917
|
async function handleAfterSave(event, ctx) {
|
|
497
918
|
const runtime = await readSidecarConfig(ctx);
|
|
498
919
|
if (runtime === null) {
|
|
@@ -501,7 +922,7 @@ async function handleAfterSave(event, ctx) {
|
|
|
501
922
|
);
|
|
502
923
|
return;
|
|
503
924
|
}
|
|
504
|
-
const [blockMappings, fields] = await Promise.all([readSandboxBlockMappings(ctx),
|
|
925
|
+
const [blockMappings, fields, descriptions] = await Promise.all([readSandboxBlockMappings(ctx), readSandboxContentFieldState(ctx), readSandboxDescriptionFields(ctx)]);
|
|
505
926
|
await handleAfterSaveConfigured(
|
|
506
927
|
event,
|
|
507
928
|
ctx,
|
|
@@ -513,7 +934,9 @@ async function handleAfterSave(event, ctx) {
|
|
|
513
934
|
),
|
|
514
935
|
void 0,
|
|
515
936
|
blockMappings,
|
|
516
|
-
fields?.[event.content.type]
|
|
937
|
+
fields.fields?.[event.content.type],
|
|
938
|
+
descriptions?.[event.content.type],
|
|
939
|
+
fields.unavailable
|
|
517
940
|
);
|
|
518
941
|
}
|
|
519
942
|
var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
@@ -536,15 +959,63 @@ var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
|
536
959
|
"CNT005"
|
|
537
960
|
// visible text duplicated on another route
|
|
538
961
|
]);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
962
|
+
function isSiteGraphFinding(finding) {
|
|
963
|
+
return finding.aexeo_origin !== "content_preset" && SITE_GRAPH_RULE_IDS.has(finding.rule_id);
|
|
964
|
+
}
|
|
965
|
+
function withCmsSourceLocation(finding, stored) {
|
|
966
|
+
if (finding.aexeo_cms_location !== void 0) {
|
|
967
|
+
finding = { ...finding };
|
|
968
|
+
delete finding.aexeo_cms_location;
|
|
969
|
+
}
|
|
970
|
+
const location = finding.aexeo_source_location;
|
|
971
|
+
if (finding.aexeo_origin !== "content_preset" || !location || !stored) return finding;
|
|
972
|
+
const blockPath = stored.meta.bodySourcePaths?.[location.block_index];
|
|
973
|
+
const spanPath = stored.meta.bodySourceSpanPaths?.[location.block_index]?.[location.span_index];
|
|
974
|
+
const path = blockPath ? [...blockPath, "children", location.span_index, "text"] : spanPath;
|
|
975
|
+
const text = stored.document.body?.[location.block_index]?.children[location.span_index]?.text;
|
|
976
|
+
const offset = location.utf16_offset;
|
|
977
|
+
if (!path || typeof text !== "string" || !Number.isSafeInteger(offset) || offset < 0 || offset > text.length || offset > 0 && offset < text.length && /[\uD800-\uDBFF]/.test(text[offset - 1]) && /[\uDC00-\uDFFF]/.test(text[offset])) return finding;
|
|
978
|
+
return { ...finding, aexeo_cms_location: { path, utf16_offset: offset } };
|
|
979
|
+
}
|
|
980
|
+
function countCmsLocations(counts, findings) {
|
|
981
|
+
for (const finding of findings) {
|
|
982
|
+
if (finding.aexeo_origin !== "content_preset" || finding.aexeo_source_location === void 0) continue;
|
|
983
|
+
counts.eligible++;
|
|
984
|
+
if (finding.aexeo_cms_location !== void 0) counts.mapped++;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
function recordCmsLocationCoverage(ctx, counts) {
|
|
988
|
+
try {
|
|
989
|
+
ctx.log?.info?.("aexeo.cms_location_coverage", { ...counts, unmapped: counts.eligible - counts.mapped });
|
|
990
|
+
} catch {
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields, descriptionFields, contentFieldsUnavailable = false) {
|
|
994
|
+
const prior = (await readAllStoredDocuments(ctx.kv)).filter(
|
|
995
|
+
(stored2) => stored2.meta.id === event.content.id && stored2.meta.collection === event.content.type
|
|
546
996
|
);
|
|
997
|
+
let adapted;
|
|
998
|
+
try {
|
|
999
|
+
adapted = await adaptContentItemWithContext(
|
|
1000
|
+
event.content,
|
|
1001
|
+
ctx.content,
|
|
1002
|
+
ctx.site?.locale,
|
|
1003
|
+
blockMappings,
|
|
1004
|
+
contentFields,
|
|
1005
|
+
descriptionFields
|
|
1006
|
+
);
|
|
1007
|
+
} catch {
|
|
1008
|
+
try {
|
|
1009
|
+
ctx.log?.info?.("aexeo.adaptation_coverage", { failed: 1 });
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
ctx.log?.error?.("Aexeo afterSave adaptation failed");
|
|
1013
|
+
const previous = prior.length === 1 ? prior[0] : void 0;
|
|
1014
|
+
if (previous !== void 0) await markAdaptationUnavailable(ctx, event.content, previous);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
547
1017
|
const document = adapted.document;
|
|
1018
|
+
if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
|
|
548
1019
|
recordContentCoverage(ctx, [document]);
|
|
549
1020
|
const { kv, log } = ctx;
|
|
550
1021
|
const stored = { document, meta: adapted.meta };
|
|
@@ -552,20 +1023,28 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
552
1023
|
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
553
1024
|
return;
|
|
554
1025
|
}
|
|
555
|
-
const result = await evaluator
|
|
1026
|
+
const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
|
|
1027
|
+
recordAssessmentCoverage(ctx, result, [document]);
|
|
556
1028
|
if (!result.ok) {
|
|
557
1029
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
558
1030
|
detail: result.detail
|
|
559
1031
|
});
|
|
1032
|
+
if (!await isSuperseded(kv, stored)) {
|
|
1033
|
+
await kv.set(findingsKey(document.route), {
|
|
1034
|
+
route: document.route,
|
|
1035
|
+
findings: [],
|
|
1036
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
560
1039
|
return;
|
|
561
1040
|
}
|
|
562
1041
|
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
563
|
-
(finding) =>
|
|
1042
|
+
(finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
564
1043
|
);
|
|
565
1044
|
const pageFindings = [
|
|
566
1045
|
...result.findings.filter(
|
|
567
|
-
(finding) => finding.scope === "page" && !
|
|
568
|
-
),
|
|
1046
|
+
(finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
1047
|
+
).map((finding) => withCmsSourceLocation(finding, stored)),
|
|
569
1048
|
...storedGraphFindings
|
|
570
1049
|
];
|
|
571
1050
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
@@ -576,16 +1055,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
576
1055
|
},
|
|
577
1056
|
pageFindings
|
|
578
1057
|
);
|
|
579
|
-
if (await isSuperseded(kv,
|
|
1058
|
+
if (await isSuperseded(kv, stored)) {
|
|
580
1059
|
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
581
1060
|
return;
|
|
582
1061
|
}
|
|
1062
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
1063
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
1064
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
583
1065
|
await kv.set(findingsKey(document.route), {
|
|
584
1066
|
route: document.route,
|
|
585
|
-
findings: filtered
|
|
1067
|
+
findings: filtered,
|
|
1068
|
+
body_coverage: cmsBodyCoverage(document),
|
|
1069
|
+
...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
|
|
586
1070
|
});
|
|
587
1071
|
}
|
|
588
1072
|
var DEFAULT_COLLECTIONS = ["posts", "pages"];
|
|
1073
|
+
async function markAdaptationUnavailable(ctx, item, previous) {
|
|
1074
|
+
if (isOlderRevision(item.updatedAt, previous.meta.updatedAt) || await isSuperseded(ctx.kv, previous)) return;
|
|
1075
|
+
const unavailable = {
|
|
1076
|
+
document: { route: previous.document.route, title: previous.document.title },
|
|
1077
|
+
meta: {
|
|
1078
|
+
id: item.id,
|
|
1079
|
+
collection: item.type,
|
|
1080
|
+
slug: item.slug,
|
|
1081
|
+
status: item.status,
|
|
1082
|
+
title: previous.meta.title,
|
|
1083
|
+
publicUrl: null,
|
|
1084
|
+
adaptationFailed: true,
|
|
1085
|
+
...item.updatedAt === void 0 ? {} : { updatedAt: item.updatedAt }
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
if (!await persistDocument(ctx, unavailable) || await isSuperseded(ctx.kv, unavailable)) return;
|
|
1089
|
+
recordContentCoverage(ctx, [unavailable.document]);
|
|
1090
|
+
await ctx.kv.set(findingsKey(unavailable.document.route), {
|
|
1091
|
+
route: unavailable.document.route,
|
|
1092
|
+
findings: [],
|
|
1093
|
+
assessment: { status: "error", reason: "adaptation_failed", detail: "The CMS record could not be adapted." }
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
async function evaluateSafely(evaluator, documents, context) {
|
|
1097
|
+
try {
|
|
1098
|
+
return await evaluator(documents, context);
|
|
1099
|
+
} catch {
|
|
1100
|
+
return { ok: false, reason: "evaluator_exception", detail: "The evaluator failed unexpectedly." };
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
589
1103
|
async function evaluateAndPersistAll(ctx, options) {
|
|
590
1104
|
const collections = options.collections ?? DEFAULT_COLLECTIONS;
|
|
591
1105
|
const suppressionFilter = options.suppressionFilter;
|
|
@@ -601,8 +1115,10 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
601
1115
|
const documentCollections = [];
|
|
602
1116
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
603
1117
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
1118
|
+
const adaptationFailures = [];
|
|
604
1119
|
for (const collection of collections) {
|
|
605
1120
|
let cursor;
|
|
1121
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
606
1122
|
do {
|
|
607
1123
|
try {
|
|
608
1124
|
const page = await ctx.content.list(collection, {
|
|
@@ -610,22 +1126,35 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
610
1126
|
...cursor === void 0 ? {} : { cursor }
|
|
611
1127
|
});
|
|
612
1128
|
for (const item of page.items) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1129
|
+
try {
|
|
1130
|
+
const adapted = await adaptContentItemWithContext(
|
|
1131
|
+
item,
|
|
1132
|
+
ctx.content,
|
|
1133
|
+
ctx.site?.locale,
|
|
1134
|
+
options.blockMappings,
|
|
1135
|
+
options.contentFieldsByCollection?.[collection],
|
|
1136
|
+
options.descriptionFieldsByCollection?.[collection]
|
|
1137
|
+
);
|
|
1138
|
+
if (options.contentFieldsUnavailable && adapted.document.body !== void 0) adapted.document.body_capabilities = [];
|
|
1139
|
+
documents.push(adapted.document);
|
|
1140
|
+
documentCollections.push(adapted.meta.collection);
|
|
1141
|
+
documentRoutes.add(adapted.document.route);
|
|
1142
|
+
adaptedByRoute.set(adapted.document.route, {
|
|
1143
|
+
document: adapted.document,
|
|
1144
|
+
meta: adapted.meta
|
|
1145
|
+
});
|
|
1146
|
+
} catch {
|
|
1147
|
+
adaptationFailures.push({ item, collection });
|
|
1148
|
+
summary.errors.push(`${collection}: content adaptation failed`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (page.hasMore) {
|
|
1152
|
+
if (!page.cursor || seenCursors.has(page.cursor)) throw new Error("incomplete pagination: missing or repeated cursor");
|
|
1153
|
+
seenCursors.add(page.cursor);
|
|
1154
|
+
cursor = page.cursor;
|
|
1155
|
+
} else {
|
|
1156
|
+
cursor = void 0;
|
|
627
1157
|
}
|
|
628
|
-
cursor = page.hasMore ? page.cursor : void 0;
|
|
629
1158
|
} catch (err) {
|
|
630
1159
|
const message = err instanceof Error ? err.message : String(err);
|
|
631
1160
|
summary.errors.push(`${collection}: ${message}`);
|
|
@@ -633,36 +1162,100 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
633
1162
|
}
|
|
634
1163
|
} while (cursor !== void 0);
|
|
635
1164
|
}
|
|
1165
|
+
const inventoryComplete = summary.errors.length === 0;
|
|
636
1166
|
summary.documentsScanned = documents.length;
|
|
637
1167
|
recordContentCoverage(ctx, documents);
|
|
1168
|
+
if (adaptationFailures.length > 0) {
|
|
1169
|
+
try {
|
|
1170
|
+
ctx.log?.info?.("aexeo.adaptation_coverage", { failed: adaptationFailures.length });
|
|
1171
|
+
} catch {
|
|
1172
|
+
}
|
|
1173
|
+
const storedByCollection = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const stored of storedAtStart) {
|
|
1175
|
+
const byId = storedByCollection.get(stored.meta.collection) ?? /* @__PURE__ */ new Map();
|
|
1176
|
+
const entries = byId.get(stored.meta.id) ?? [];
|
|
1177
|
+
entries.push(stored);
|
|
1178
|
+
byId.set(stored.meta.id, entries);
|
|
1179
|
+
storedByCollection.set(stored.meta.collection, byId);
|
|
1180
|
+
}
|
|
1181
|
+
for (const { item, collection } of adaptationFailures) {
|
|
1182
|
+
for (const stored of storedByCollection.get(collection)?.get(item.id) ?? []) {
|
|
1183
|
+
if (documentRoutes.has(stored.document.route)) continue;
|
|
1184
|
+
await markAdaptationUnavailable(ctx, item, stored);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
638
1188
|
for (const stored of adaptedByRoute.values()) {
|
|
639
1189
|
const current = await readStoredDocument(kv, stored.document.route);
|
|
640
1190
|
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
641
1191
|
await kv.set(documentKey(stored.document.route), stored);
|
|
642
1192
|
}
|
|
643
1193
|
}
|
|
644
|
-
const result = await options.evaluator
|
|
1194
|
+
const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
|
|
1195
|
+
recordAssessmentCoverage(ctx, result, documents);
|
|
645
1196
|
if (!result.ok) {
|
|
646
1197
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
647
1198
|
detail: result.detail
|
|
648
1199
|
});
|
|
649
1200
|
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
1201
|
+
try {
|
|
1202
|
+
ctx.log?.info?.("aexeo.sitewide_coverage", { status: "evaluation_error" });
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
await kv.set(findingsKey("*"), {
|
|
1206
|
+
route: "*",
|
|
1207
|
+
findings: [],
|
|
1208
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
1209
|
+
});
|
|
1210
|
+
for (const [route, stored] of adaptedByRoute) {
|
|
1211
|
+
if (!await isSuperseded(kv, stored)) {
|
|
1212
|
+
await kv.set(findingsKey(route), {
|
|
1213
|
+
route,
|
|
1214
|
+
findings: [],
|
|
1215
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
650
1219
|
return summary;
|
|
651
1220
|
}
|
|
652
1221
|
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
1222
|
+
findingsByRoute.set("*", []);
|
|
653
1223
|
for (const route of documentRoutes) {
|
|
654
1224
|
findingsByRoute.set(route, []);
|
|
655
1225
|
}
|
|
1226
|
+
const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
|
|
1227
|
+
const allBodiesComplete = documents.every(hasCompleteBody);
|
|
1228
|
+
const changed = await refreshInputChanged(kv, collections, adaptedByRoute, storedAtStart);
|
|
1229
|
+
const coverageStatus = !inventoryComplete ? "inventory_incomplete" : changed ? "changed_during_refresh" : !allBodiesComplete ? "body_incomplete" : "complete";
|
|
1230
|
+
const globalCoverageComplete = coverageStatus === "complete";
|
|
1231
|
+
try {
|
|
1232
|
+
ctx.log?.info?.("aexeo.sitewide_coverage", { status: coverageStatus });
|
|
1233
|
+
} catch {
|
|
1234
|
+
}
|
|
1235
|
+
const globalAssessment = globalCoverageComplete ? void 0 : {
|
|
1236
|
+
status: "error",
|
|
1237
|
+
reason: coverageStatus,
|
|
1238
|
+
detail: coverageStatus === "changed_during_refresh" ? "Sitewide and cross-document checks are unavailable because content changed during refresh." : "Sitewide and cross-document checks are unavailable because the CMS input is incomplete."
|
|
1239
|
+
};
|
|
1240
|
+
if (changed && inventoryComplete) {
|
|
1241
|
+
summary.errors.push("Sitewide checks unavailable: content changed during refresh.");
|
|
1242
|
+
} else if (!allBodiesComplete && inventoryComplete) {
|
|
1243
|
+
summary.errors.push("Sitewide checks unavailable: one or more document bodies are incomplete.");
|
|
1244
|
+
}
|
|
656
1245
|
for (const finding of result.findings) {
|
|
1246
|
+
if (!globalCoverageComplete && (finding.scope !== "page" || isSiteGraphFinding(finding))) continue;
|
|
1247
|
+
const complete = finding.scope === "page" ? completeByRoute.get(finding.path) ?? false : allBodiesComplete;
|
|
1248
|
+
if (!isCmsObservableFinding(finding, complete)) continue;
|
|
657
1249
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
658
1250
|
const list = findingsByRoute.get(bucket) ?? [];
|
|
659
|
-
list.push(finding);
|
|
1251
|
+
list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
|
|
660
1252
|
findingsByRoute.set(bucket, list);
|
|
661
1253
|
}
|
|
662
1254
|
let totalAfterSuppression = 0;
|
|
1255
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
663
1256
|
for (const [route, findings] of findingsByRoute) {
|
|
664
1257
|
const stored = adaptedByRoute.get(route);
|
|
665
|
-
if (stored !== void 0 && await isSuperseded(kv,
|
|
1258
|
+
if (stored !== void 0 && await isSuperseded(kv, stored)) {
|
|
666
1259
|
continue;
|
|
667
1260
|
}
|
|
668
1261
|
const context = {
|
|
@@ -671,16 +1264,37 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
671
1264
|
...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
|
|
672
1265
|
};
|
|
673
1266
|
const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
|
|
674
|
-
|
|
1267
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
1268
|
+
await kv.set(findingsKey(route), {
|
|
1269
|
+
route,
|
|
1270
|
+
findings: filtered,
|
|
1271
|
+
...stored === void 0 ? {} : { body_coverage: cmsBodyCoverage(stored.document) },
|
|
1272
|
+
...route === "*" && globalAssessment !== void 0 ? { assessment: globalAssessment } : result.assessments?.[route] === void 0 ? {} : { assessment: result.assessments[route] }
|
|
1273
|
+
});
|
|
675
1274
|
summary.routesUpdated += 1;
|
|
676
1275
|
totalAfterSuppression += filtered.length;
|
|
677
1276
|
}
|
|
1277
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
678
1278
|
summary.totalFindings = totalAfterSuppression;
|
|
679
|
-
if (
|
|
1279
|
+
if (inventoryComplete && !changed) {
|
|
680
1280
|
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
681
1281
|
}
|
|
682
1282
|
return summary;
|
|
683
1283
|
}
|
|
1284
|
+
async function refreshInputChanged(kv, collections, evaluated, initial) {
|
|
1285
|
+
const current = await readAllStoredDocuments(kv);
|
|
1286
|
+
const currentByRoute = new Map(current.map((entry) => [entry.document.route, entry]));
|
|
1287
|
+
const initialByRoute = new Map(initial.map((entry) => [entry.document.route, entry]));
|
|
1288
|
+
for (const [route, entry] of evaluated) {
|
|
1289
|
+
if (!sameStoredDocument(entry, currentByRoute.get(route))) return true;
|
|
1290
|
+
}
|
|
1291
|
+
const selected = new Set(collections);
|
|
1292
|
+
for (const entry of current) {
|
|
1293
|
+
const route = entry.document.route;
|
|
1294
|
+
if (!evaluated.has(route) && (entry.meta.collection === "" || selected.has(entry.meta.collection)) && !sameStoredDocument(entry, initialByRoute.get(route))) return true;
|
|
1295
|
+
}
|
|
1296
|
+
return false;
|
|
1297
|
+
}
|
|
684
1298
|
function isOlderRevision(revision, other) {
|
|
685
1299
|
if (revision === void 0 || other === void 0) return false;
|
|
686
1300
|
return Date.parse(revision) < Date.parse(other);
|
|
@@ -688,9 +1302,11 @@ function isOlderRevision(revision, other) {
|
|
|
688
1302
|
async function readStoredDocument(kv, route) {
|
|
689
1303
|
return toStoredDocument(await kv.get(documentKey(route)));
|
|
690
1304
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1305
|
+
function sameStoredDocument(a, b) {
|
|
1306
|
+
return a != null && b != null && JSON.stringify(a) === JSON.stringify(b);
|
|
1307
|
+
}
|
|
1308
|
+
async function isSuperseded(kv, evaluated) {
|
|
1309
|
+
return !sameStoredDocument(evaluated, await readStoredDocument(kv, evaluated.document.route));
|
|
694
1310
|
}
|
|
695
1311
|
async function persistDocument(ctx, stored) {
|
|
696
1312
|
const route = stored.document.route;
|
|
@@ -758,9 +1374,6 @@ function documentKey(route) {
|
|
|
758
1374
|
const normalized = route === "" || route === "/" ? "/" : route;
|
|
759
1375
|
return `document:${normalized}`;
|
|
760
1376
|
}
|
|
761
|
-
async function readAllDocuments(kv) {
|
|
762
|
-
return (await readAllStoredDocuments(kv)).map((s) => s.document);
|
|
763
|
-
}
|
|
764
1377
|
async function readAllStoredDocuments(kv) {
|
|
765
1378
|
const entries = await kv.list("document:");
|
|
766
1379
|
const out = [];
|
|
@@ -777,6 +1390,10 @@ async function readFindings(kv, route) {
|
|
|
777
1390
|
}
|
|
778
1391
|
return stored.findings;
|
|
779
1392
|
}
|
|
1393
|
+
async function readAssessment(kv, route) {
|
|
1394
|
+
const stored = await kv.get(findingsKey(route));
|
|
1395
|
+
return stored?.assessment;
|
|
1396
|
+
}
|
|
780
1397
|
|
|
781
1398
|
// src/sidecar-url.ts
|
|
782
1399
|
function validateSidecarUrl(raw, allowedHosts = []) {
|
|
@@ -882,6 +1499,19 @@ async function scoreSite(_documents) {
|
|
|
882
1499
|
return null;
|
|
883
1500
|
}
|
|
884
1501
|
|
|
1502
|
+
// src/assessment-display.ts
|
|
1503
|
+
function describeGate(gate) {
|
|
1504
|
+
const status = gate.status === "not_checked" ? "not checked" : gate.status;
|
|
1505
|
+
const reason = gate.status === "not_checked" ? `: ${gate.reason ?? "required input unavailable"}` : "";
|
|
1506
|
+
const metrics = Object.entries(gate.measurements ?? {}).map(([key, value]) => `${key.replaceAll("_", " ")}: ${value}`).join(", ");
|
|
1507
|
+
return `${gate.gate_id} ${status}${reason} (${gate.blocking ? "blocking" : "advisory"})${metrics ? ` \u2014 ${metrics}` : ""}`;
|
|
1508
|
+
}
|
|
1509
|
+
function summarizeGates(gates) {
|
|
1510
|
+
const counts = { passed: 0, failed: 0, not_checked: 0 };
|
|
1511
|
+
for (const gate of gates) counts[gate.status]++;
|
|
1512
|
+
return `${gates.length} configured checks: ${counts.passed} passed, ${counts.failed} failed, ${counts.not_checked} not checked`;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
885
1515
|
// src/findings-view.ts
|
|
886
1516
|
function normalizeInteraction(input) {
|
|
887
1517
|
if (!input || typeof input !== "object") {
|
|
@@ -920,6 +1550,8 @@ async function handleRefresh(options) {
|
|
|
920
1550
|
evaluator: options.evaluator,
|
|
921
1551
|
blockMappings: options.blockMappings,
|
|
922
1552
|
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1553
|
+
contentFieldsUnavailable: options.contentFieldsUnavailable,
|
|
1554
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
923
1555
|
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
924
1556
|
suppressionFilter: options.suppressionFilter
|
|
925
1557
|
});
|
|
@@ -955,21 +1587,38 @@ async function handleRefresh(options) {
|
|
|
955
1587
|
}
|
|
956
1588
|
};
|
|
957
1589
|
}
|
|
958
|
-
async function
|
|
959
|
-
const entries = await kv.list(
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const
|
|
1590
|
+
async function readFindingsSnapshot(kv) {
|
|
1591
|
+
const entries = await kv.list("findings:");
|
|
1592
|
+
const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
|
|
1593
|
+
const incomplete = sitewideAssessment?.status === "error";
|
|
1594
|
+
const findings = [];
|
|
1595
|
+
const routes = /* @__PURE__ */ new Set();
|
|
1596
|
+
let failedAssessments = 0;
|
|
1597
|
+
let uncheckedGates = 0;
|
|
1598
|
+
let incompleteBodies = 0;
|
|
1599
|
+
let unknownBodies = 0;
|
|
1600
|
+
let missingAssessments = 0;
|
|
963
1601
|
for (const entry of entries) {
|
|
964
|
-
if (entry.value === null)
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
1602
|
+
if (entry.value === null) continue;
|
|
967
1603
|
const route = entry.key.replace(/^findings:/, "");
|
|
1604
|
+
if (route !== "*") {
|
|
1605
|
+
routes.add(route);
|
|
1606
|
+
if (entry.value.body_coverage === void 0) unknownBodies++;
|
|
1607
|
+
else if (entry.value.body_coverage !== "complete") incompleteBodies++;
|
|
1608
|
+
const assessment = entry.value.assessment;
|
|
1609
|
+
if (assessment === void 0) missingAssessments++;
|
|
1610
|
+
if (assessment?.status === "error") failedAssessments++;
|
|
1611
|
+
if (assessment?.status === "evaluated") {
|
|
1612
|
+
uncheckedGates += assessment.gates.filter((gate) => gate.status === "not_checked").length;
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
968
1615
|
for (const finding of entry.value.findings) {
|
|
969
|
-
|
|
1616
|
+
if (!isCmsObservableFinding(finding, route === "*" || entry.value.body_coverage === "complete") || incomplete && isSiteGraphFinding(finding)) continue;
|
|
1617
|
+
findings.push({ ...finding, document_route: route });
|
|
1618
|
+
if (route === "*") routes.add(route);
|
|
970
1619
|
}
|
|
971
1620
|
}
|
|
972
|
-
return
|
|
1621
|
+
return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
|
|
973
1622
|
}
|
|
974
1623
|
function severityFirst(a, b) {
|
|
975
1624
|
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
@@ -987,7 +1636,7 @@ function uniqueRoutes(rows) {
|
|
|
987
1636
|
return [...routes].sort();
|
|
988
1637
|
}
|
|
989
1638
|
async function renderFindingsPage(kv, tuning) {
|
|
990
|
-
const findings = await
|
|
1639
|
+
const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
|
|
991
1640
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
992
1641
|
const warnings = findings.filter(
|
|
993
1642
|
(finding) => finding.severity === "warning"
|
|
@@ -998,8 +1647,13 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
998
1647
|
{ type: "header", text: "SEO findings" },
|
|
999
1648
|
{
|
|
1000
1649
|
type: "context",
|
|
1001
|
-
text: findings.length === 0 ? "No
|
|
1650
|
+
text: findings.length === 0 ? documentRoutes.length === 0 ? "No CMS document results available \u2014 click Refresh to evaluate records." : "No findings reported for the available CMS checks. Inspect assessments for unchecked or failed evaluations." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1002
1651
|
},
|
|
1652
|
+
{ type: "context", text: "Rendered page markup, semantic sections, generated metadata, duplicate pages and sitewide links are not checked here. Audit the built site for page-level and link findings." },
|
|
1653
|
+
...incompleteBodies || unknownBodies ? [{ type: "context", text: `${incompleteBodies} document bodies were incomplete or unavailable; coverage is unknown for ${unknownBodies} older or failed evaluations. Inspect individual documents.` }] : [],
|
|
1654
|
+
...missingAssessments ? [{ type: "context", text: `No content preset assessment was recorded for ${missingAssessments} document(s); absence of findings is not an editorial quality assessment.` }] : [],
|
|
1655
|
+
...failedAssessments || uncheckedGates ? [{ type: "context", text: `${failedAssessments} document evaluations failed; ${uncheckedGates} preset checks were not checked. Inspect individual documents for details.` }] : [],
|
|
1656
|
+
...sitewideAssessment?.status === "error" ? [{ type: "context", text: `Sitewide checks unavailable: ${sitewideAssessment.reason}. ${sitewideAssessment.detail}` }] : [],
|
|
1003
1657
|
{ type: "divider" },
|
|
1004
1658
|
{
|
|
1005
1659
|
type: "actions",
|
|
@@ -1024,10 +1678,10 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
1024
1678
|
},
|
|
1025
1679
|
sorted.length === 0 ? {
|
|
1026
1680
|
type: "context",
|
|
1027
|
-
text: tuning.emptyFindingsText
|
|
1681
|
+
text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
|
|
1028
1682
|
} : findingsTable(sorted)
|
|
1029
1683
|
];
|
|
1030
|
-
if (
|
|
1684
|
+
if (documentRoutes.length > 0) {
|
|
1031
1685
|
blocks.push({
|
|
1032
1686
|
type: "form",
|
|
1033
1687
|
fields: [
|
|
@@ -1035,7 +1689,7 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
1035
1689
|
type: "select",
|
|
1036
1690
|
action_id: "route_picker",
|
|
1037
1691
|
label: "Document to inspect",
|
|
1038
|
-
options:
|
|
1692
|
+
options: documentRoutes.map((route) => ({ label: route, value: route }))
|
|
1039
1693
|
}
|
|
1040
1694
|
],
|
|
1041
1695
|
submit: { label: "View document SEO", action_id: "view_document" }
|
|
@@ -1060,75 +1714,13 @@ function findingsTable(rows) {
|
|
|
1060
1714
|
}))
|
|
1061
1715
|
};
|
|
1062
1716
|
}
|
|
1063
|
-
async function renderScoreWidget(
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
type: "context",
|
|
1071
|
-
text: tuning.emptyScoreText
|
|
1072
|
-
}
|
|
1073
|
-
]
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
let score;
|
|
1077
|
-
try {
|
|
1078
|
-
score = await tuning.score(documents, kv);
|
|
1079
|
-
} catch (error) {
|
|
1080
|
-
log?.error("Aexeo score widget failed to initialize", { error });
|
|
1081
|
-
return {
|
|
1082
|
-
blocks: [
|
|
1083
|
-
{ type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
|
|
1084
|
-
{ type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
|
|
1085
|
-
]
|
|
1086
|
-
};
|
|
1087
|
-
}
|
|
1088
|
-
if (score === null) {
|
|
1089
|
-
return {
|
|
1090
|
-
blocks: [
|
|
1091
|
-
{ type: "header", text: "SEO score" },
|
|
1092
|
-
{
|
|
1093
|
-
type: "context",
|
|
1094
|
-
text: "The site score is unavailable in sandboxed mode. Findings are not affected."
|
|
1095
|
-
}
|
|
1096
|
-
]
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
const blocks = [
|
|
1100
|
-
{
|
|
1101
|
-
type: "stats",
|
|
1102
|
-
items: [
|
|
1103
|
-
{
|
|
1104
|
-
label: "Citation",
|
|
1105
|
-
value: score.citation_readiness_score?.toString() ?? "unknown"
|
|
1106
|
-
},
|
|
1107
|
-
{
|
|
1108
|
-
label: tuning.truthLabel(score),
|
|
1109
|
-
value: score.truth_consistency_score?.toString() ?? "unknown"
|
|
1110
|
-
},
|
|
1111
|
-
{
|
|
1112
|
-
label: "Answers",
|
|
1113
|
-
value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
|
|
1114
|
-
}
|
|
1115
|
-
]
|
|
1116
|
-
}
|
|
1117
|
-
];
|
|
1118
|
-
if (score.blockers.length > 0) {
|
|
1119
|
-
blocks.push({
|
|
1120
|
-
type: "context",
|
|
1121
|
-
text: topBlockersLine(score)
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
return { blocks };
|
|
1125
|
-
}
|
|
1126
|
-
function topBlockersLine(score) {
|
|
1127
|
-
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
1128
|
-
if (top.length === 0) {
|
|
1129
|
-
return "No blockers identified.";
|
|
1130
|
-
}
|
|
1131
|
-
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
1717
|
+
async function renderScoreWidget(_kv, _tuning, _log) {
|
|
1718
|
+
return {
|
|
1719
|
+
blocks: [
|
|
1720
|
+
{ type: "header", text: "SEO score unavailable" },
|
|
1721
|
+
{ type: "context", text: "CMS records do not include the rendered site. Run an Aexeo site audit on the built site to assess SEO and page-level signals." }
|
|
1722
|
+
]
|
|
1723
|
+
};
|
|
1132
1724
|
}
|
|
1133
1725
|
async function renderDocumentPanel(kv, route) {
|
|
1134
1726
|
if (route === void 0) {
|
|
@@ -1142,7 +1734,11 @@ async function renderDocumentPanel(kv, route) {
|
|
|
1142
1734
|
]
|
|
1143
1735
|
};
|
|
1144
1736
|
}
|
|
1145
|
-
const
|
|
1737
|
+
const incomplete = (await readAssessment(kv, "*"))?.status === "error";
|
|
1738
|
+
const record = await kv.get(findingsKey(route));
|
|
1739
|
+
const findings = (record?.findings ?? []).filter((finding) => isCmsObservableFinding(finding, record?.body_coverage === "complete") && (!incomplete || !isSiteGraphFinding(finding)));
|
|
1740
|
+
const assessment = record?.assessment;
|
|
1741
|
+
const bodyNotice = record && assessment?.status !== "error" ? cmsBodyCoverageNotice(record.body_coverage) : void 0;
|
|
1146
1742
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
1147
1743
|
const warnings = findings.filter(
|
|
1148
1744
|
(finding) => finding.severity === "warning"
|
|
@@ -1153,8 +1749,14 @@ async function renderDocumentPanel(kv, route) {
|
|
|
1153
1749
|
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
1154
1750
|
{
|
|
1155
1751
|
type: "context",
|
|
1156
|
-
text: findings.length === 0 ? `No findings for ${route}
|
|
1752
|
+
text: !record ? `No evaluation is available for ${route}. Save the document or refresh to evaluate it.` : assessment?.status === "error" ? `Evaluation failed for ${route}; findings are unavailable for this revision.` : findings.length === 0 ? `No findings for ${route} from the available CMS checks.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1157
1753
|
},
|
|
1754
|
+
{ type: "context", text: "Rendered page markup, semantic sections, generated metadata, duplicate pages and sitewide links are not checked here. Audit the built site for page-level and link findings." },
|
|
1755
|
+
...bodyNotice ? [{ type: "context", text: bodyNotice }] : [],
|
|
1756
|
+
...incomplete ? [{ type: "context", text: "Sitewide and cross-document checks are unavailable; refresh after resolving the CMS input error." }] : [],
|
|
1757
|
+
...record && assessment === void 0 ? [{ type: "context", text: "No content preset assessment was recorded for this document; absence of findings is not an editorial quality assessment." }] : [],
|
|
1758
|
+
...assessment?.status === "error" ? [{ type: "context", text: `Evaluation failed: ${assessment.reason}. Findings are unavailable for this revision.` }] : [],
|
|
1759
|
+
...assessment?.status === "evaluated" ? [{ type: "context", text: `Content preset: ${assessment.preset}. ${summarizeGates(assessment.gates)}. Results cover only these configured checks.` }, ...assessment.gates.map((gate) => ({ type: "context", text: describeGate(gate) }))] : [],
|
|
1158
1760
|
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
1159
1761
|
]
|
|
1160
1762
|
};
|
|
@@ -1166,16 +1768,13 @@ function documentFindingsTable(findings) {
|
|
|
1166
1768
|
{ key: "rule", label: "Rule", format: "code" },
|
|
1167
1769
|
{ key: "severity", label: "Severity", format: "badge" },
|
|
1168
1770
|
{ key: "message", label: "Message" },
|
|
1169
|
-
{ key: "block", label: "
|
|
1771
|
+
{ key: "block", label: "Location", format: "code" }
|
|
1170
1772
|
],
|
|
1171
1773
|
rows: findings.map((finding) => ({
|
|
1172
1774
|
rule: finding.rule_id,
|
|
1173
1775
|
severity: finding.severity,
|
|
1174
1776
|
message: finding.message,
|
|
1175
|
-
|
|
1176
|
-
// when rendering; surfacing the path:line locator here lets authors
|
|
1177
|
-
// locate the failing block via the editor's ⌘-F.
|
|
1178
|
-
block: `${finding.path}:${finding.line}`
|
|
1777
|
+
block: finding.aexeo_cms_location ? `CMS path ${JSON.stringify(finding.aexeo_cms_location.path)}, UTF-16 offset ${finding.aexeo_cms_location.utf16_offset}` : finding.aexeo_source_location ? `Projected body: block ${finding.aexeo_source_location.block_index + 1}, span ${finding.aexeo_source_location.span_index + 1}, UTF-16 offset ${finding.aexeo_source_location.utf16_offset}` : finding.aexeo_origin === "content_preset" ? "Projected location unavailable" : `Reported line ${finding.line}`
|
|
1179
1778
|
}))
|
|
1180
1779
|
};
|
|
1181
1780
|
}
|
|
@@ -1342,6 +1941,7 @@ async function handleRefresh2(ctx) {
|
|
|
1342
1941
|
context?.collections
|
|
1343
1942
|
);
|
|
1344
1943
|
};
|
|
1944
|
+
const contentFields = await readSandboxContentFieldState(ctx.ctx);
|
|
1345
1945
|
return handleRefresh({
|
|
1346
1946
|
ctx: ctx.ctx,
|
|
1347
1947
|
// The sandboxed descriptor takes no collection knobs, so the sweep
|
|
@@ -1350,7 +1950,9 @@ async function handleRefresh2(ctx) {
|
|
|
1350
1950
|
evaluator: sandboxEvaluator,
|
|
1351
1951
|
suppressionFilter: sandboxSuppressionFilter,
|
|
1352
1952
|
blockMappings: await readSandboxBlockMappings(ctx.ctx),
|
|
1353
|
-
contentFieldsByCollection:
|
|
1953
|
+
contentFieldsByCollection: contentFields.fields,
|
|
1954
|
+
contentFieldsUnavailable: contentFields.unavailable,
|
|
1955
|
+
descriptionFieldsByCollection: await readSandboxDescriptionFields(ctx.ctx),
|
|
1354
1956
|
tuning: adminViewTuning
|
|
1355
1957
|
});
|
|
1356
1958
|
}
|