@aeptus/aexeo-emdash 0.8.19 → 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 +112 -2
- package/INSTALL.md +286 -4
- package/README.md +20 -5
- package/dist/adapter.d.ts +62 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin/Findings.d.ts.map +1 -1
- package/dist/admin.d.ts.map +1 -1
- package/dist/admin.js +73 -6
- 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 +16 -1
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +1341 -172
- package/dist/content-evaluator.d.ts +34 -0
- package/dist/content-evaluator.d.ts.map +1 -0
- package/dist/content.d.ts +98 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +445 -0
- package/dist/data-route.d.ts +12 -1
- package/dist/data-route.d.ts.map +1 -1
- package/dist/evaluator-sandbox.d.ts +1 -1
- package/dist/evaluator-sandbox.d.ts.map +1 -1
- package/dist/evaluator.d.ts +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/findings-view.d.ts +11 -5
- package/dist/findings-view.d.ts.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +163 -4
- package/dist/inventory.d.ts +59 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/plugin.d.ts +59 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/presence-check.d.ts +1 -0
- package/dist/presence-check.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts +2 -1
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +1077 -190
- package/dist/sandbox.d.ts +2 -2
- package/dist/sidecar-url.d.ts.map +1 -1
- package/dist/sidecar.d.ts +9 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/types.d.ts +26 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/wasm-init.d.ts +8 -0
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +28 -8
- package/wasm/aexeo_emdash_bridge.d.ts +22 -0
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +15 -0
- package/wasm/aexeo_emdash_bridge_bg.js +130 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +4 -0
package/dist/sandbox-entry.js
CHANGED
|
@@ -1,11 +1,36 @@
|
|
|
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) {
|
|
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, {
|
|
6
28
|
route,
|
|
7
29
|
publicUrl,
|
|
8
|
-
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
30
|
+
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
31
|
+
...blockMappings === void 0 ? {} : { blockMappings },
|
|
32
|
+
...contentFields === void 0 ? {} : { contentFields },
|
|
33
|
+
...descriptionFields === void 0 ? {} : { descriptionFields }
|
|
9
34
|
});
|
|
10
35
|
const translationSet = await contentApi.getTranslations(
|
|
11
36
|
content.type,
|
|
@@ -30,14 +55,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
|
30
55
|
return adapted;
|
|
31
56
|
}
|
|
32
57
|
function adaptContentItem(content, options = {}) {
|
|
58
|
+
return adaptContentItemInternal(content, options, true);
|
|
59
|
+
}
|
|
60
|
+
function adaptContentItemInternal(content, options, includeSourcePaths) {
|
|
33
61
|
const route = options.route ?? deriveRoute(content);
|
|
34
62
|
const title = stringOrEmpty(
|
|
35
63
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
36
64
|
);
|
|
37
|
-
const document = { route, title };
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
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;
|
|
41
84
|
}
|
|
42
85
|
const canonical = content.seo?.canonical;
|
|
43
86
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
@@ -47,8 +90,13 @@ function adaptContentItem(content, options = {}) {
|
|
|
47
90
|
if (typeof locale === "string" && locale.length > 0) {
|
|
48
91
|
document.lang = locale;
|
|
49
92
|
}
|
|
50
|
-
const body = extractPortableText(content.data);
|
|
51
|
-
if (body !== null)
|
|
93
|
+
const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
|
|
94
|
+
if (body !== null) {
|
|
95
|
+
document.body = body.blocks;
|
|
96
|
+
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
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);
|
|
52
100
|
return {
|
|
53
101
|
document,
|
|
54
102
|
meta: {
|
|
@@ -57,7 +105,10 @@ function adaptContentItem(content, options = {}) {
|
|
|
57
105
|
slug: content.slug,
|
|
58
106
|
status: content.status,
|
|
59
107
|
title,
|
|
60
|
-
publicUrl: options.publicUrl ?? null
|
|
108
|
+
publicUrl: options.publicUrl ?? null,
|
|
109
|
+
...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
|
|
110
|
+
...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
|
|
111
|
+
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
61
112
|
}
|
|
62
113
|
};
|
|
63
114
|
}
|
|
@@ -76,38 +127,390 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
76
127
|
function stringOrEmpty(value) {
|
|
77
128
|
return typeof value === "string" ? value : "";
|
|
78
129
|
}
|
|
79
|
-
function
|
|
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
|
+
}
|
|
224
|
+
function extractSelectedContent(data, fields, mappings) {
|
|
225
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
226
|
+
const all = ["text", "headings", "paragraphs", "links"];
|
|
227
|
+
let capabilities = all;
|
|
228
|
+
const blocks = [];
|
|
229
|
+
let observed = false;
|
|
230
|
+
for (const field of fields) {
|
|
231
|
+
const value = data[field];
|
|
232
|
+
if (value === void 0) {
|
|
233
|
+
capabilities = [];
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
observed = true;
|
|
237
|
+
if (value === null) continue;
|
|
238
|
+
const part = extractFirstPortableText({ content: value }, mappings);
|
|
239
|
+
if (part === null) {
|
|
240
|
+
capabilities = [];
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
blocks.push(...part.blocks);
|
|
244
|
+
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
245
|
+
}
|
|
246
|
+
if (!observed) return null;
|
|
247
|
+
return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
|
|
248
|
+
}
|
|
249
|
+
function extractPortableText(data, mappings) {
|
|
250
|
+
const result = extractFirstPortableText(data, mappings);
|
|
251
|
+
if (result === null) return null;
|
|
252
|
+
const candidates = Object.entries(data).filter(
|
|
253
|
+
([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
254
|
+
);
|
|
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;
|
|
261
|
+
}
|
|
262
|
+
function extractFirstPortableText(data, mappings) {
|
|
263
|
+
let foundEmptyBody = false;
|
|
80
264
|
for (const slug of ["body", "content"]) {
|
|
81
265
|
const value = data[slug];
|
|
82
|
-
if (
|
|
83
|
-
if (
|
|
266
|
+
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
267
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
268
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
269
|
+
if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
|
|
84
270
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
85
|
-
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
271
|
+
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
86
272
|
}
|
|
87
273
|
}
|
|
88
274
|
const blocks = data["blocks"];
|
|
89
|
-
if (
|
|
90
|
-
if (
|
|
275
|
+
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
276
|
+
if (isPortableTextArray(blocks)) return { blocks };
|
|
277
|
+
if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
|
|
278
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
|
|
91
279
|
for (const [field, value] of Object.entries(data)) {
|
|
92
280
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
93
|
-
if (isPortableTextArray(value)) return value;
|
|
94
|
-
if (
|
|
281
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
282
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
283
|
+
if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
|
|
95
284
|
}
|
|
96
|
-
return null;
|
|
285
|
+
return foundEmptyBody ? { blocks: [] } : null;
|
|
97
286
|
}
|
|
98
287
|
function isPortableTextArray(value) {
|
|
99
288
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
100
|
-
(item) =>
|
|
101
|
-
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
102
|
-
)
|
|
289
|
+
(item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
|
|
103
290
|
);
|
|
104
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
|
+
}
|
|
105
442
|
function isEmdashBlocksArray(value, field) {
|
|
106
443
|
if (!Array.isArray(value) || value.length === 0) return false;
|
|
107
444
|
return value.every(
|
|
108
|
-
(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")
|
|
109
446
|
);
|
|
110
447
|
}
|
|
448
|
+
function mapCustomBlocks(blocks, mappings, depth = 0) {
|
|
449
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
450
|
+
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
451
|
+
const result = [];
|
|
452
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
453
|
+
const mapFields = (record, fields, depth2) => {
|
|
454
|
+
if (depth2 > 8 || fields.length === 0) return false;
|
|
455
|
+
for (const entry of fields) {
|
|
456
|
+
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
457
|
+
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
458
|
+
if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
|
|
459
|
+
if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
|
|
460
|
+
const value = record[entry.field];
|
|
461
|
+
if ((value === void 0 || value === null || value === "") && entry.optional) {
|
|
462
|
+
if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (entry.kind === "richText") {
|
|
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
|
+
}
|
|
476
|
+
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
477
|
+
if (typeof value !== "string") return false;
|
|
478
|
+
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
479
|
+
} else if (entry.kind === "link") {
|
|
480
|
+
const href = record[entry.hrefField];
|
|
481
|
+
if (typeof value !== "string" || typeof href !== "string" || !href) return false;
|
|
482
|
+
result.push({
|
|
483
|
+
_type: "block",
|
|
484
|
+
children: [{ _type: "span", text: value, marks: ["link"] }],
|
|
485
|
+
markDefs: [{ _key: "link", _type: "link", href }]
|
|
486
|
+
});
|
|
487
|
+
} else if (entry.kind === "repeat") {
|
|
488
|
+
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
489
|
+
for (const item of value) {
|
|
490
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
|
|
491
|
+
}
|
|
492
|
+
} else {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
};
|
|
498
|
+
for (const block of blocks) {
|
|
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);
|
|
513
|
+
}
|
|
111
514
|
function blocksToPortableText(blocks) {
|
|
112
515
|
const paragraphs = [];
|
|
113
516
|
for (const block of blocks) {
|
|
@@ -120,20 +523,49 @@ function blocksToPortableText(blocks) {
|
|
|
120
523
|
}
|
|
121
524
|
return paragraphs;
|
|
122
525
|
}
|
|
526
|
+
var FALLBACK_MAX_DEPTH = 64;
|
|
527
|
+
var FALLBACK_MAX_NODES = 5e4;
|
|
528
|
+
var FALLBACK_MAX_CHARS = 1048576;
|
|
123
529
|
function collectText(value, key, out) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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);
|
|
137
569
|
}
|
|
138
570
|
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
139
571
|
"_key",
|
|
@@ -191,11 +623,16 @@ function isRecord(value) {
|
|
|
191
623
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
624
|
}
|
|
193
625
|
|
|
626
|
+
// src/version.ts
|
|
627
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
628
|
+
|
|
194
629
|
// src/sidecar.ts
|
|
195
|
-
async function evaluateViaSidecar(http, config, documents) {
|
|
630
|
+
async function evaluateViaSidecar(http, config, documents, collections) {
|
|
196
631
|
const body = {
|
|
197
|
-
documents
|
|
632
|
+
documents,
|
|
633
|
+
responseFormat: 2
|
|
198
634
|
};
|
|
635
|
+
if (collections !== void 0) body.collections = collections;
|
|
199
636
|
if (config.configJson !== void 0) {
|
|
200
637
|
body.configJson = config.configJson;
|
|
201
638
|
}
|
|
@@ -228,6 +665,12 @@ async function evaluateViaSidecar(http, config, documents) {
|
|
|
228
665
|
}
|
|
229
666
|
return { ok: false, reason: "server_error", detail };
|
|
230
667
|
}
|
|
668
|
+
const workerVersion = response.headers?.get("x-aexeo-version") ?? "unknown";
|
|
669
|
+
if (workerVersion !== PACKAGE_VERSION) {
|
|
670
|
+
config.log?.warn(
|
|
671
|
+
`Aexeo sidecar is version ${workerVersion} but the plugin is ${PACKAGE_VERSION}; redeploy the Worker so both run the same rules`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
231
674
|
let raw;
|
|
232
675
|
try {
|
|
233
676
|
raw = await response.text();
|
|
@@ -236,20 +679,36 @@ async function evaluateViaSidecar(http, config, documents) {
|
|
|
236
679
|
return { ok: false, reason: "invalid_response", detail };
|
|
237
680
|
}
|
|
238
681
|
try {
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
reason: "invalid_response",
|
|
244
|
-
detail: "sidecar returned non-array body"
|
|
245
|
-
};
|
|
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" };
|
|
246
686
|
}
|
|
247
|
-
|
|
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
|
+
}
|
|
691
|
+
}
|
|
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 };
|
|
248
701
|
} catch (err) {
|
|
249
702
|
const detail = err instanceof Error ? err.message : String(err);
|
|
250
703
|
return { ok: false, reason: "invalid_response", detail };
|
|
251
704
|
}
|
|
252
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
|
+
}
|
|
253
712
|
function trimTrailingSlash(url) {
|
|
254
713
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
255
714
|
}
|
|
@@ -266,7 +725,28 @@ function normalizeHost(evaluatorHost) {
|
|
|
266
725
|
var CONFIG_URL_KEY = "config:evaluator_url";
|
|
267
726
|
var CONFIG_TOKEN_KEY = "config:eval_token";
|
|
268
727
|
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
728
|
+
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
729
|
+
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
730
|
+
var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
|
|
269
731
|
var PLUGIN_SETTINGS_SCHEMA = {
|
|
732
|
+
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
733
|
+
type: "string",
|
|
734
|
+
label: "Content fields by collection (JSON)",
|
|
735
|
+
description: "Ordered rendered body fields per collection.",
|
|
736
|
+
multiline: true
|
|
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
|
+
},
|
|
744
|
+
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
745
|
+
type: "string",
|
|
746
|
+
label: "Custom block mappings (JSON)",
|
|
747
|
+
description: "Ordered mappings for the rendered fields of each custom block type.",
|
|
748
|
+
multiline: true
|
|
749
|
+
},
|
|
270
750
|
[SETTINGS_TOKEN_KEY]: {
|
|
271
751
|
type: "secret",
|
|
272
752
|
label: "EVAL_TOKEN",
|
|
@@ -285,10 +765,21 @@ async function readSidecarConfig(store) {
|
|
|
285
765
|
return { url, token };
|
|
286
766
|
}
|
|
287
767
|
async function writeSidecarConfig(store, config) {
|
|
288
|
-
await store.kv.set(CONFIG_URL_KEY, config.url);
|
|
289
768
|
await writeStoredToken(store, config.token);
|
|
769
|
+
await store.kv.set(CONFIG_URL_KEY, config.url);
|
|
770
|
+
}
|
|
771
|
+
async function isTokenStoredInPlaintext(store) {
|
|
772
|
+
return await readLegacyToken(store) !== null;
|
|
773
|
+
}
|
|
774
|
+
async function readLegacyToken(store) {
|
|
775
|
+
const legacy = await store.kv.get(CONFIG_TOKEN_KEY);
|
|
776
|
+
return typeof legacy === "string" && legacy.length > 0 ? legacy : null;
|
|
290
777
|
}
|
|
291
778
|
async function readStoredToken(store) {
|
|
779
|
+
const legacy = await readLegacyToken(store);
|
|
780
|
+
if (legacy !== null) {
|
|
781
|
+
return legacy;
|
|
782
|
+
}
|
|
292
783
|
const { settings } = store;
|
|
293
784
|
if (settings !== void 0) {
|
|
294
785
|
try {
|
|
@@ -298,32 +789,131 @@ async function readStoredToken(store) {
|
|
|
298
789
|
}
|
|
299
790
|
} catch (err) {
|
|
300
791
|
store.log?.warn?.(
|
|
301
|
-
`aexeo: could not read the encrypted EVAL_TOKEN from plugin settings
|
|
792
|
+
`aexeo: could not read the encrypted EVAL_TOKEN from plugin settings: ${describeError(err, "")}`
|
|
302
793
|
);
|
|
303
794
|
}
|
|
304
795
|
}
|
|
305
|
-
|
|
306
|
-
return typeof legacy === "string" && legacy.length > 0 ? legacy : null;
|
|
796
|
+
return null;
|
|
307
797
|
}
|
|
798
|
+
var TokenStorageError = class extends Error {
|
|
799
|
+
};
|
|
308
800
|
async function writeStoredToken(store, token) {
|
|
309
801
|
const { settings } = store;
|
|
310
|
-
if (settings
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
802
|
+
if (settings === void 0) {
|
|
803
|
+
throw new TokenStorageError(
|
|
804
|
+
"This emdash host does not provide encrypted plugin settings, so EVAL_TOKEN was not saved. Upgrade emdash."
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
try {
|
|
808
|
+
await settings.set(SETTINGS_TOKEN_KEY, token);
|
|
809
|
+
await store.kv.delete(CONFIG_TOKEN_KEY);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
throw new TokenStorageError(
|
|
812
|
+
`EVAL_TOKEN was not saved because the host could not store it encrypted. Set EMDASH_ENCRYPTION_KEY on the emdash host and try again. Detail: ${describeError(err, token)}`
|
|
813
|
+
);
|
|
320
814
|
}
|
|
321
|
-
await store.kv.set(CONFIG_TOKEN_KEY, token);
|
|
322
815
|
}
|
|
323
816
|
function describeError(err, secret) {
|
|
324
817
|
const raw = err instanceof Error ? err.message : String(err);
|
|
325
818
|
return secret.length === 0 ? raw : raw.split(secret).join("[redacted]");
|
|
326
819
|
}
|
|
820
|
+
function recordContentCoverage(ctx, documents) {
|
|
821
|
+
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
822
|
+
for (const document of documents) {
|
|
823
|
+
counts[cmsBodyCoverage(document)]++;
|
|
824
|
+
}
|
|
825
|
+
try {
|
|
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 });
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
async function handleAfterDelete(event, ctx) {
|
|
875
|
+
for (const entry of await ctx.kv.list("document:")) {
|
|
876
|
+
const stored = toStoredDocument(entry.value);
|
|
877
|
+
if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
|
|
878
|
+
await ctx.kv.delete(entry.key);
|
|
879
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async function readSandboxBlockMappings(ctx) {
|
|
884
|
+
if (!ctx.settings) return void 0;
|
|
885
|
+
try {
|
|
886
|
+
const raw = await ctx.settings.get(SETTINGS_BLOCK_MAPPINGS_KEY);
|
|
887
|
+
if (raw === null || raw === "") return void 0;
|
|
888
|
+
if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid mapping size or type");
|
|
889
|
+
const value = JSON.parse(raw);
|
|
890
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("mapping must be an object");
|
|
891
|
+
return value;
|
|
892
|
+
} catch {
|
|
893
|
+
ctx.log?.warn?.("Aexeo custom block mappings unavailable; custom content remains unverified");
|
|
894
|
+
return void 0;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function readSandboxFields(ctx, key, warning) {
|
|
898
|
+
if (!ctx.settings) return { fields: void 0, unavailable: false };
|
|
899
|
+
try {
|
|
900
|
+
const raw = await ctx.settings.get(key);
|
|
901
|
+
if (raw === null || raw === "") return { fields: void 0, unavailable: false };
|
|
902
|
+
if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid selection size or type");
|
|
903
|
+
const value = JSON.parse(raw);
|
|
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");
|
|
905
|
+
return { fields: value, unavailable: false };
|
|
906
|
+
} catch {
|
|
907
|
+
ctx.log?.warn?.(warning);
|
|
908
|
+
return { fields: void 0, unavailable: true };
|
|
909
|
+
}
|
|
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
|
+
}
|
|
327
917
|
async function handleAfterSave(event, ctx) {
|
|
328
918
|
const runtime = await readSidecarConfig(ctx);
|
|
329
919
|
if (runtime === null) {
|
|
@@ -332,36 +922,131 @@ async function handleAfterSave(event, ctx) {
|
|
|
332
922
|
);
|
|
333
923
|
return;
|
|
334
924
|
}
|
|
925
|
+
const [blockMappings, fields, descriptions] = await Promise.all([readSandboxBlockMappings(ctx), readSandboxContentFieldState(ctx), readSandboxDescriptionFields(ctx)]);
|
|
335
926
|
await handleAfterSaveConfigured(
|
|
336
927
|
event,
|
|
337
928
|
ctx,
|
|
338
|
-
(documents) => evaluateViaSidecar(
|
|
929
|
+
(documents, context) => evaluateViaSidecar(
|
|
339
930
|
ctx.http,
|
|
340
|
-
{ url: runtime.url, authToken: runtime.token },
|
|
341
|
-
documents
|
|
342
|
-
|
|
931
|
+
{ url: runtime.url, authToken: runtime.token, log: ctx.log },
|
|
932
|
+
documents,
|
|
933
|
+
context?.collections
|
|
934
|
+
),
|
|
935
|
+
void 0,
|
|
936
|
+
blockMappings,
|
|
937
|
+
fields.fields?.[event.content.type],
|
|
938
|
+
descriptions?.[event.content.type],
|
|
939
|
+
fields.unavailable
|
|
343
940
|
);
|
|
344
941
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
942
|
+
var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
943
|
+
"LNK001",
|
|
944
|
+
// internal link target missing from the site
|
|
945
|
+
"LNK002",
|
|
946
|
+
// no inbound links
|
|
947
|
+
"LNK003",
|
|
948
|
+
// weak anchor, checked only when the target route exists
|
|
949
|
+
"LNK004",
|
|
950
|
+
// too few inbound links
|
|
951
|
+
"SEO009",
|
|
952
|
+
// hreflang alternate target missing from the site
|
|
953
|
+
"SEO012",
|
|
954
|
+
// hreflang alternate does not link back
|
|
955
|
+
"SEO016",
|
|
956
|
+
// canonical points at another crawlable route
|
|
957
|
+
"SEO017",
|
|
958
|
+
// title and description shared with another route
|
|
959
|
+
"CNT005"
|
|
960
|
+
// visible text duplicated on another route
|
|
961
|
+
]);
|
|
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
|
|
350
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
|
+
}
|
|
351
1017
|
const document = adapted.document;
|
|
1018
|
+
if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
|
|
1019
|
+
recordContentCoverage(ctx, [document]);
|
|
352
1020
|
const { kv, log } = ctx;
|
|
353
1021
|
const stored = { document, meta: adapted.meta };
|
|
354
|
-
await persistDocument(ctx, stored)
|
|
355
|
-
|
|
1022
|
+
if (!await persistDocument(ctx, stored)) {
|
|
1023
|
+
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
|
|
1027
|
+
recordAssessmentCoverage(ctx, result, [document]);
|
|
356
1028
|
if (!result.ok) {
|
|
357
1029
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
358
1030
|
detail: result.detail
|
|
359
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
|
+
}
|
|
360
1039
|
return;
|
|
361
1040
|
}
|
|
362
|
-
const
|
|
363
|
-
(finding) => finding
|
|
1041
|
+
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
1042
|
+
(finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
364
1043
|
);
|
|
1044
|
+
const pageFindings = [
|
|
1045
|
+
...result.findings.filter(
|
|
1046
|
+
(finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
1047
|
+
).map((finding) => withCmsSourceLocation(finding, stored)),
|
|
1048
|
+
...storedGraphFindings
|
|
1049
|
+
];
|
|
365
1050
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
366
1051
|
{
|
|
367
1052
|
route: document.route,
|
|
@@ -370,12 +1055,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
370
1055
|
},
|
|
371
1056
|
pageFindings
|
|
372
1057
|
);
|
|
1058
|
+
if (await isSuperseded(kv, stored)) {
|
|
1059
|
+
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
1063
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
1064
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
373
1065
|
await kv.set(findingsKey(document.route), {
|
|
374
1066
|
route: document.route,
|
|
375
|
-
findings: filtered
|
|
1067
|
+
findings: filtered,
|
|
1068
|
+
body_coverage: cmsBodyCoverage(document),
|
|
1069
|
+
...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
|
|
376
1070
|
});
|
|
377
1071
|
}
|
|
378
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
|
+
}
|
|
379
1103
|
async function evaluateAndPersistAll(ctx, options) {
|
|
380
1104
|
const collections = options.collections ?? DEFAULT_COLLECTIONS;
|
|
381
1105
|
const suppressionFilter = options.suppressionFilter;
|
|
@@ -386,11 +1110,15 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
386
1110
|
totalFindings: 0,
|
|
387
1111
|
errors: []
|
|
388
1112
|
};
|
|
1113
|
+
const storedAtStart = await readAllStoredDocuments(kv);
|
|
389
1114
|
const documents = [];
|
|
1115
|
+
const documentCollections = [];
|
|
390
1116
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
391
1117
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
1118
|
+
const adaptationFailures = [];
|
|
392
1119
|
for (const collection of collections) {
|
|
393
1120
|
let cursor;
|
|
1121
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
394
1122
|
do {
|
|
395
1123
|
try {
|
|
396
1124
|
const page = await ctx.content.list(collection, {
|
|
@@ -398,19 +1126,35 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
398
1126
|
...cursor === void 0 ? {} : { cursor }
|
|
399
1127
|
});
|
|
400
1128
|
for (const item of page.items) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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;
|
|
412
1157
|
}
|
|
413
|
-
cursor = page.hasMore ? page.cursor : void 0;
|
|
414
1158
|
} catch (err) {
|
|
415
1159
|
const message = err instanceof Error ? err.message : String(err);
|
|
416
1160
|
summary.errors.push(`${collection}: ${message}`);
|
|
@@ -418,66 +1162,183 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
418
1162
|
}
|
|
419
1163
|
} while (cursor !== void 0);
|
|
420
1164
|
}
|
|
1165
|
+
const inventoryComplete = summary.errors.length === 0;
|
|
421
1166
|
summary.documentsScanned = documents.length;
|
|
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
|
+
}
|
|
422
1188
|
for (const stored of adaptedByRoute.values()) {
|
|
423
|
-
await kv
|
|
1189
|
+
const current = await readStoredDocument(kv, stored.document.route);
|
|
1190
|
+
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
1191
|
+
await kv.set(documentKey(stored.document.route), stored);
|
|
1192
|
+
}
|
|
424
1193
|
}
|
|
425
|
-
const result = await options.evaluator
|
|
1194
|
+
const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
|
|
1195
|
+
recordAssessmentCoverage(ctx, result, documents);
|
|
426
1196
|
if (!result.ok) {
|
|
427
1197
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
428
1198
|
detail: result.detail
|
|
429
1199
|
});
|
|
430
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
|
+
}
|
|
431
1219
|
return summary;
|
|
432
1220
|
}
|
|
433
1221
|
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
1222
|
+
findingsByRoute.set("*", []);
|
|
434
1223
|
for (const route of documentRoutes) {
|
|
435
1224
|
findingsByRoute.set(route, []);
|
|
436
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
|
+
}
|
|
437
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;
|
|
438
1249
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
439
1250
|
const list = findingsByRoute.get(bucket) ?? [];
|
|
440
|
-
list.push(finding);
|
|
1251
|
+
list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
|
|
441
1252
|
findingsByRoute.set(bucket, list);
|
|
442
1253
|
}
|
|
443
1254
|
let totalAfterSuppression = 0;
|
|
1255
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
444
1256
|
for (const [route, findings] of findingsByRoute) {
|
|
445
1257
|
const stored = adaptedByRoute.get(route);
|
|
1258
|
+
if (stored !== void 0 && await isSuperseded(kv, stored)) {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
446
1261
|
const context = {
|
|
447
1262
|
route,
|
|
448
1263
|
...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
|
|
449
1264
|
...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
|
|
450
1265
|
};
|
|
451
1266
|
const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
|
|
452
|
-
|
|
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
|
+
});
|
|
453
1274
|
summary.routesUpdated += 1;
|
|
454
1275
|
totalAfterSuppression += filtered.length;
|
|
455
1276
|
}
|
|
1277
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
456
1278
|
summary.totalFindings = totalAfterSuppression;
|
|
457
|
-
if (
|
|
458
|
-
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
1279
|
+
if (inventoryComplete && !changed) {
|
|
1280
|
+
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
459
1281
|
}
|
|
460
1282
|
return summary;
|
|
461
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
|
+
}
|
|
1298
|
+
function isOlderRevision(revision, other) {
|
|
1299
|
+
if (revision === void 0 || other === void 0) return false;
|
|
1300
|
+
return Date.parse(revision) < Date.parse(other);
|
|
1301
|
+
}
|
|
1302
|
+
async function readStoredDocument(kv, route) {
|
|
1303
|
+
return toStoredDocument(await kv.get(documentKey(route)));
|
|
1304
|
+
}
|
|
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));
|
|
1310
|
+
}
|
|
462
1311
|
async function persistDocument(ctx, stored) {
|
|
463
1312
|
const route = stored.document.route;
|
|
464
|
-
const
|
|
465
|
-
for (const entry of entries) {
|
|
1313
|
+
const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
|
|
466
1314
|
const previous = toStoredDocument(entry.value);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1315
|
+
return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
|
|
1316
|
+
});
|
|
1317
|
+
if (sameEntry.some(
|
|
1318
|
+
({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
|
|
1319
|
+
)) {
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
for (const { key, previous } of sameEntry) {
|
|
1323
|
+
if (previous.document.route === route) continue;
|
|
1324
|
+
await ctx.kv.delete(key);
|
|
471
1325
|
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
472
1326
|
}
|
|
473
1327
|
await ctx.kv.set(documentKey(route), stored);
|
|
1328
|
+
return true;
|
|
474
1329
|
}
|
|
475
|
-
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
1330
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
|
|
476
1331
|
const scannedCollections = new Set(collections);
|
|
1332
|
+
const revisionAtStart = new Map(
|
|
1333
|
+
storedAtStart.map((stored) => [
|
|
1334
|
+
stored.document.route,
|
|
1335
|
+
stored.meta.updatedAt
|
|
1336
|
+
])
|
|
1337
|
+
);
|
|
477
1338
|
const entries = await ctx.kv.list("document:");
|
|
478
1339
|
for (const entry of entries) {
|
|
479
1340
|
const stored = toStoredDocument(entry.value);
|
|
480
|
-
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
1341
|
+
if (stored === null || stored.meta.collection !== "" && !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route) || !revisionAtStart.has(stored.document.route) || revisionAtStart.get(stored.document.route) !== stored.meta.updatedAt) {
|
|
481
1342
|
continue;
|
|
482
1343
|
}
|
|
483
1344
|
await ctx.kv.delete(entry.key);
|
|
@@ -513,9 +1374,6 @@ function documentKey(route) {
|
|
|
513
1374
|
const normalized = route === "" || route === "/" ? "/" : route;
|
|
514
1375
|
return `document:${normalized}`;
|
|
515
1376
|
}
|
|
516
|
-
async function readAllDocuments(kv) {
|
|
517
|
-
return (await readAllStoredDocuments(kv)).map((s) => s.document);
|
|
518
|
-
}
|
|
519
1377
|
async function readAllStoredDocuments(kv) {
|
|
520
1378
|
const entries = await kv.list("document:");
|
|
521
1379
|
const out = [];
|
|
@@ -532,6 +1390,10 @@ async function readFindings(kv, route) {
|
|
|
532
1390
|
}
|
|
533
1391
|
return stored.findings;
|
|
534
1392
|
}
|
|
1393
|
+
async function readAssessment(kv, route) {
|
|
1394
|
+
const stored = await kv.get(findingsKey(route));
|
|
1395
|
+
return stored?.assessment;
|
|
1396
|
+
}
|
|
535
1397
|
|
|
536
1398
|
// src/sidecar-url.ts
|
|
537
1399
|
function validateSidecarUrl(raw, allowedHosts = []) {
|
|
@@ -552,6 +1414,12 @@ function validateSidecarUrl(raw, allowedHosts = []) {
|
|
|
552
1414
|
};
|
|
553
1415
|
}
|
|
554
1416
|
const host = parsed.hostname.toLowerCase();
|
|
1417
|
+
if (host.endsWith(".")) {
|
|
1418
|
+
return {
|
|
1419
|
+
ok: false,
|
|
1420
|
+
error: `Sidecar URL host must not end with a dot: ${host}`
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
555
1423
|
if (isBlockedHost(host)) {
|
|
556
1424
|
return {
|
|
557
1425
|
ok: false,
|
|
@@ -628,15 +1496,20 @@ function hostMatchesAllowlist(host, allowedHosts) {
|
|
|
628
1496
|
|
|
629
1497
|
// src/evaluator-sandbox.ts
|
|
630
1498
|
async function scoreSite(_documents) {
|
|
631
|
-
return
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
}
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
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`;
|
|
640
1513
|
}
|
|
641
1514
|
|
|
642
1515
|
// src/findings-view.ts
|
|
@@ -675,6 +1548,10 @@ async function handleRefresh(options) {
|
|
|
675
1548
|
summary = await evaluateAndPersistAll(options.ctx, {
|
|
676
1549
|
collections: options.collections,
|
|
677
1550
|
evaluator: options.evaluator,
|
|
1551
|
+
blockMappings: options.blockMappings,
|
|
1552
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1553
|
+
contentFieldsUnavailable: options.contentFieldsUnavailable,
|
|
1554
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
678
1555
|
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
679
1556
|
suppressionFilter: options.suppressionFilter
|
|
680
1557
|
});
|
|
@@ -710,21 +1587,38 @@ async function handleRefresh(options) {
|
|
|
710
1587
|
}
|
|
711
1588
|
};
|
|
712
1589
|
}
|
|
713
|
-
async function
|
|
714
|
-
const entries = await kv.list(
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
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;
|
|
718
1601
|
for (const entry of entries) {
|
|
719
|
-
if (entry.value === null)
|
|
720
|
-
continue;
|
|
721
|
-
}
|
|
1602
|
+
if (entry.value === null) continue;
|
|
722
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
|
+
}
|
|
723
1615
|
for (const finding of entry.value.findings) {
|
|
724
|
-
|
|
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);
|
|
725
1619
|
}
|
|
726
1620
|
}
|
|
727
|
-
return
|
|
1621
|
+
return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
|
|
728
1622
|
}
|
|
729
1623
|
function severityFirst(a, b) {
|
|
730
1624
|
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
@@ -742,7 +1636,7 @@ function uniqueRoutes(rows) {
|
|
|
742
1636
|
return [...routes].sort();
|
|
743
1637
|
}
|
|
744
1638
|
async function renderFindingsPage(kv, tuning) {
|
|
745
|
-
const findings = await
|
|
1639
|
+
const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
|
|
746
1640
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
747
1641
|
const warnings = findings.filter(
|
|
748
1642
|
(finding) => finding.severity === "warning"
|
|
@@ -753,8 +1647,13 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
753
1647
|
{ type: "header", text: "SEO findings" },
|
|
754
1648
|
{
|
|
755
1649
|
type: "context",
|
|
756
|
-
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.`
|
|
757
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}` }] : [],
|
|
758
1657
|
{ type: "divider" },
|
|
759
1658
|
{
|
|
760
1659
|
type: "actions",
|
|
@@ -779,10 +1678,10 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
779
1678
|
},
|
|
780
1679
|
sorted.length === 0 ? {
|
|
781
1680
|
type: "context",
|
|
782
|
-
text: tuning.emptyFindingsText
|
|
1681
|
+
text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
|
|
783
1682
|
} : findingsTable(sorted)
|
|
784
1683
|
];
|
|
785
|
-
if (
|
|
1684
|
+
if (documentRoutes.length > 0) {
|
|
786
1685
|
blocks.push({
|
|
787
1686
|
type: "form",
|
|
788
1687
|
fields: [
|
|
@@ -790,7 +1689,7 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
790
1689
|
type: "select",
|
|
791
1690
|
action_id: "route_picker",
|
|
792
1691
|
label: "Document to inspect",
|
|
793
|
-
options:
|
|
1692
|
+
options: documentRoutes.map((route) => ({ label: route, value: route }))
|
|
794
1693
|
}
|
|
795
1694
|
],
|
|
796
1695
|
submit: { label: "View document SEO", action_id: "view_document" }
|
|
@@ -815,55 +1714,13 @@ function findingsTable(rows) {
|
|
|
815
1714
|
}))
|
|
816
1715
|
};
|
|
817
1716
|
}
|
|
818
|
-
async function renderScoreWidget(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
type: "context",
|
|
826
|
-
text: tuning.emptyScoreText
|
|
827
|
-
}
|
|
828
|
-
]
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
const score = await tuning.score(documents, kv);
|
|
832
|
-
const blocks = [
|
|
833
|
-
{
|
|
834
|
-
type: "stats",
|
|
835
|
-
items: [
|
|
836
|
-
{ label: "Overall", value: `${score.overall_score}` },
|
|
837
|
-
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
838
|
-
{
|
|
839
|
-
label: tuning.truthLabel(score),
|
|
840
|
-
value: `${score.truth_consistency_score}`
|
|
841
|
-
},
|
|
842
|
-
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
843
|
-
]
|
|
844
|
-
}
|
|
845
|
-
];
|
|
846
|
-
if (score.overall_score < 60) {
|
|
847
|
-
blocks.unshift({
|
|
848
|
-
type: "banner",
|
|
849
|
-
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
850
|
-
variant: "alert"
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
if (score.blockers.length > 0) {
|
|
854
|
-
blocks.push({
|
|
855
|
-
type: "context",
|
|
856
|
-
text: topBlockersLine(score)
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
return { blocks };
|
|
860
|
-
}
|
|
861
|
-
function topBlockersLine(score) {
|
|
862
|
-
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
863
|
-
if (top.length === 0) {
|
|
864
|
-
return "No blockers identified.";
|
|
865
|
-
}
|
|
866
|
-
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
|
+
};
|
|
867
1724
|
}
|
|
868
1725
|
async function renderDocumentPanel(kv, route) {
|
|
869
1726
|
if (route === void 0) {
|
|
@@ -877,7 +1734,11 @@ async function renderDocumentPanel(kv, route) {
|
|
|
877
1734
|
]
|
|
878
1735
|
};
|
|
879
1736
|
}
|
|
880
|
-
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;
|
|
881
1742
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
882
1743
|
const warnings = findings.filter(
|
|
883
1744
|
(finding) => finding.severity === "warning"
|
|
@@ -888,8 +1749,14 @@ async function renderDocumentPanel(kv, route) {
|
|
|
888
1749
|
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
889
1750
|
{
|
|
890
1751
|
type: "context",
|
|
891
|
-
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.`
|
|
892
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) }))] : [],
|
|
893
1760
|
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
894
1761
|
]
|
|
895
1762
|
};
|
|
@@ -901,16 +1768,13 @@ function documentFindingsTable(findings) {
|
|
|
901
1768
|
{ key: "rule", label: "Rule", format: "code" },
|
|
902
1769
|
{ key: "severity", label: "Severity", format: "badge" },
|
|
903
1770
|
{ key: "message", label: "Message" },
|
|
904
|
-
{ key: "block", label: "
|
|
1771
|
+
{ key: "block", label: "Location", format: "code" }
|
|
905
1772
|
],
|
|
906
1773
|
rows: findings.map((finding) => ({
|
|
907
1774
|
rule: finding.rule_id,
|
|
908
1775
|
severity: finding.severity,
|
|
909
1776
|
message: finding.message,
|
|
910
|
-
|
|
911
|
-
// when rendering; surfacing the path:line locator here lets authors
|
|
912
|
-
// locate the failing block via the editor's ⌘-F.
|
|
913
|
-
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}`
|
|
914
1778
|
}))
|
|
915
1779
|
};
|
|
916
1780
|
}
|
|
@@ -1061,7 +1925,7 @@ async function handleBlockAction(ctx, actionId, value) {
|
|
|
1061
1925
|
return notFound(actionId);
|
|
1062
1926
|
}
|
|
1063
1927
|
async function handleRefresh2(ctx) {
|
|
1064
|
-
const sandboxEvaluator = async (documents) => {
|
|
1928
|
+
const sandboxEvaluator = async (documents, context) => {
|
|
1065
1929
|
const runtime = await readSidecarConfig(ctx.ctx);
|
|
1066
1930
|
if (runtime === null) {
|
|
1067
1931
|
return {
|
|
@@ -1072,10 +1936,12 @@ async function handleRefresh2(ctx) {
|
|
|
1072
1936
|
}
|
|
1073
1937
|
return evaluateViaSidecar(
|
|
1074
1938
|
ctx.ctx.http,
|
|
1075
|
-
{ url: runtime.url, authToken: runtime.token },
|
|
1076
|
-
documents
|
|
1939
|
+
{ url: runtime.url, authToken: runtime.token, log: ctx.ctx.log },
|
|
1940
|
+
documents,
|
|
1941
|
+
context?.collections
|
|
1077
1942
|
);
|
|
1078
1943
|
};
|
|
1944
|
+
const contentFields = await readSandboxContentFieldState(ctx.ctx);
|
|
1079
1945
|
return handleRefresh({
|
|
1080
1946
|
ctx: ctx.ctx,
|
|
1081
1947
|
// The sandboxed descriptor takes no collection knobs, so the sweep
|
|
@@ -1083,18 +1949,30 @@ async function handleRefresh2(ctx) {
|
|
|
1083
1949
|
collections: DEFAULT_COLLECTIONS,
|
|
1084
1950
|
evaluator: sandboxEvaluator,
|
|
1085
1951
|
suppressionFilter: sandboxSuppressionFilter,
|
|
1952
|
+
blockMappings: await readSandboxBlockMappings(ctx.ctx),
|
|
1953
|
+
contentFieldsByCollection: contentFields.fields,
|
|
1954
|
+
contentFieldsUnavailable: contentFields.unavailable,
|
|
1955
|
+
descriptionFieldsByCollection: await readSandboxDescriptionFields(ctx.ctx),
|
|
1086
1956
|
tuning: adminViewTuning
|
|
1087
1957
|
});
|
|
1088
1958
|
}
|
|
1089
1959
|
async function renderSetupPage(ctx, options = {}) {
|
|
1090
1960
|
const existing = await readSidecarConfig(ctx.ctx);
|
|
1961
|
+
const tokenInPlaintext = await isTokenStoredInPlaintext(ctx.ctx);
|
|
1091
1962
|
const blocks = [
|
|
1092
1963
|
{ type: "header", text: "Aexeo setup" },
|
|
1093
1964
|
{
|
|
1094
1965
|
type: "context",
|
|
1095
|
-
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker.
|
|
1966
|
+
text: "Paste the URL and auth token of your deployed aexeo-crawl-worker. Saving stores the token encrypted by the emdash host, and fails if the host cannot encrypt it. Both values are read at runtime, so no rebuild is required after a change. Rotate the token here whenever you redeploy the sidecar with a new secret."
|
|
1096
1967
|
}
|
|
1097
1968
|
];
|
|
1969
|
+
if (tokenInPlaintext) {
|
|
1970
|
+
blocks.push({
|
|
1971
|
+
type: "banner",
|
|
1972
|
+
title: "EVAL_TOKEN is currently stored unencrypted in plugin KV. Save this form to encrypt it.",
|
|
1973
|
+
variant: "alert"
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1098
1976
|
if (options.bannerError !== void 0) {
|
|
1099
1977
|
blocks.push({
|
|
1100
1978
|
type: "banner",
|
|
@@ -1135,7 +2013,7 @@ async function renderSetupPage(ctx, options = {}) {
|
|
|
1135
2013
|
{ type: "divider" },
|
|
1136
2014
|
{
|
|
1137
2015
|
type: "context",
|
|
1138
|
-
text: existing === null ? "Status: not configured. Refresh on the findings page will fail until both fields are saved." : `Status: configured. Sidecar at ${existing.url}. Token stored \u2014 paste a new one above to rotate.`
|
|
2016
|
+
text: existing === null ? "Status: not configured. Refresh on the findings page will fail until both fields are saved." : `Status: configured. Sidecar at ${existing.url}. Token stored ${tokenInPlaintext ? "unencrypted" : "encrypted"} \u2014 paste a new one above to rotate.`
|
|
1139
2017
|
}
|
|
1140
2018
|
);
|
|
1141
2019
|
return { blocks };
|
|
@@ -1172,14 +2050,23 @@ async function handleSetupSubmit(ctx, values) {
|
|
|
1172
2050
|
url: validated.url,
|
|
1173
2051
|
token: resolvedToken
|
|
1174
2052
|
};
|
|
1175
|
-
|
|
2053
|
+
try {
|
|
2054
|
+
await writeSidecarConfig(ctx.ctx, next);
|
|
2055
|
+
} catch (err) {
|
|
2056
|
+
if (!(err instanceof TokenStorageError)) throw err;
|
|
2057
|
+
return renderSetupPage(ctx, {
|
|
2058
|
+
bannerError: err.message,
|
|
2059
|
+
initialUrl: validated.url
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
1176
2062
|
return renderSetupPage(ctx, {
|
|
1177
2063
|
bannerSuccess: "Configuration saved. Click Refresh on the findings page to evaluate."
|
|
1178
2064
|
});
|
|
1179
2065
|
}
|
|
1180
2066
|
var sandbox_entry_default = definePlugin({
|
|
1181
2067
|
hooks: {
|
|
1182
|
-
"content:afterSave": handleAfterSave
|
|
2068
|
+
"content:afterSave": handleAfterSave,
|
|
2069
|
+
"content:afterDelete": handleAfterDelete
|
|
1183
2070
|
},
|
|
1184
2071
|
routes: {
|
|
1185
2072
|
admin: { handler: handleAdminRoute }
|