@aeptus/aexeo-emdash 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/INSTALL.md +181 -8
- package/README.md +8 -0
- package/dist/adapter.d.ts +34 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin/Findings.d.ts.map +1 -1
- package/dist/admin.js +71 -4
- package/dist/assessment-display.d.ts +10 -0
- package/dist/assessment-display.d.ts.map +1 -0
- package/dist/cms-coverage.d.ts +10 -0
- package/dist/cms-coverage.d.ts.map +1 -0
- package/dist/configured.d.ts +11 -2
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +944 -210
- package/dist/content-evaluator.d.ts +22 -2
- package/dist/content-evaluator.d.ts.map +1 -1
- package/dist/content.d.ts +51 -4
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +99 -19
- package/dist/data-route.d.ts +9 -1
- package/dist/data-route.d.ts.map +1 -1
- package/dist/findings-view.d.ts +5 -4
- package/dist/findings-view.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -15
- package/dist/inventory.d.ts +9 -1
- package/dist/inventory.d.ts.map +1 -1
- package/dist/plugin.d.ts +30 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +797 -195
- package/dist/sidecar.d.ts +2 -0
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/wasm-init.d.ts +5 -1
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +12 -3
- package/wasm/aexeo_emdash_bridge.d.ts +12 -1
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +4 -0
- package/wasm/aexeo_emdash_bridge_bg.js +73 -11
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +3 -1
package/dist/configured.js
CHANGED
|
@@ -4,8 +4,30 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
+
// src/cms-coverage.ts
|
|
8
|
+
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}$/;
|
|
9
|
+
var OBSERVABLE_BODY_RULES = /* @__PURE__ */ new Set(["CNT006", "LNK003"]);
|
|
10
|
+
function isCmsObservableFinding(finding, bodyComplete = true) {
|
|
11
|
+
return finding.aexeo_origin === "content_preset" || isLegacyCmsObservableFinding(finding, bodyComplete);
|
|
12
|
+
}
|
|
13
|
+
function isLegacyCmsObservableFinding(finding, bodyComplete = true) {
|
|
14
|
+
if (!NATIVE_RULE_ID.test(finding.rule_id)) return true;
|
|
15
|
+
return bodyComplete && finding.scope === "page" && OBSERVABLE_BODY_RULES.has(finding.rule_id);
|
|
16
|
+
}
|
|
17
|
+
function hasCompleteBody(document) {
|
|
18
|
+
return document.body !== void 0 && (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every((capability) => document.body_capabilities?.includes(capability)));
|
|
19
|
+
}
|
|
20
|
+
function cmsBodyCoverage(document) {
|
|
21
|
+
if (document.body === void 0 || document.body_capabilities?.length === 0) return "unavailable";
|
|
22
|
+
return hasCompleteBody(document) ? "complete" : "partial";
|
|
23
|
+
}
|
|
24
|
+
function cmsBodyCoverageNotice(coverage) {
|
|
25
|
+
if (coverage === "complete") return void 0;
|
|
26
|
+
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.`;
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
// src/adapter.ts
|
|
8
|
-
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
|
|
30
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields, descriptionFields) {
|
|
9
31
|
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
10
32
|
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
11
33
|
const adapted = adaptContentItem(content, {
|
|
@@ -13,7 +35,8 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
|
|
|
13
35
|
publicUrl,
|
|
14
36
|
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
15
37
|
...blockMappings === void 0 ? {} : { blockMappings },
|
|
16
|
-
...contentFields === void 0 ? {} : { contentFields }
|
|
38
|
+
...contentFields === void 0 ? {} : { contentFields },
|
|
39
|
+
...descriptionFields === void 0 ? {} : { descriptionFields }
|
|
17
40
|
});
|
|
18
41
|
const translationSet = await contentApi.getTranslations(
|
|
19
42
|
content.type,
|
|
@@ -38,14 +61,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
|
|
|
38
61
|
return adapted;
|
|
39
62
|
}
|
|
40
63
|
function adaptContentItem(content, options = {}) {
|
|
64
|
+
return adaptContentItemInternal(content, options, true);
|
|
65
|
+
}
|
|
66
|
+
function adaptContentItemInternal(content, options, includeSourcePaths) {
|
|
41
67
|
const route = options.route ?? deriveRoute(content);
|
|
42
68
|
const title = stringOrEmpty(
|
|
43
69
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
44
70
|
);
|
|
45
|
-
const document = { route, title };
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
|
|
71
|
+
const document = { route, title, description_source_observed: false };
|
|
72
|
+
const fields = options.descriptionFields;
|
|
73
|
+
if (fields === void 0) {
|
|
74
|
+
const description = content.seo?.description ?? content.data["description"];
|
|
75
|
+
if (typeof description === "string" && description.length > 0) document.description = description;
|
|
76
|
+
} else if (fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0)) {
|
|
77
|
+
let observed = true;
|
|
78
|
+
for (const field of fields) {
|
|
79
|
+
if (!Object.hasOwn(content.data, field) || content.data[field] === void 0 || content.data[field] !== null && typeof content.data[field] !== "string") {
|
|
80
|
+
observed = false;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
const value = content.data[field];
|
|
84
|
+
if (typeof value === "string" && value.length > 0) {
|
|
85
|
+
document.description = value;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (observed) document.description_source_observed = true;
|
|
49
90
|
}
|
|
50
91
|
const canonical = content.seo?.canonical;
|
|
51
92
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
@@ -60,6 +101,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
60
101
|
document.body = body.blocks;
|
|
61
102
|
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
62
103
|
}
|
|
104
|
+
const bodySourcePaths = body === null || !includeSourcePaths ? void 0 : portableTextSourcePaths(content.data, body.blocks);
|
|
105
|
+
const bodySourceSpanPaths = body === null || !includeSourcePaths ? void 0 : portableTextSourceSpanPaths(content.data, body.blocks, bodySourcePaths);
|
|
63
106
|
return {
|
|
64
107
|
document,
|
|
65
108
|
meta: {
|
|
@@ -69,6 +112,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
69
112
|
status: content.status,
|
|
70
113
|
title,
|
|
71
114
|
publicUrl: options.publicUrl ?? null,
|
|
115
|
+
...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
|
|
116
|
+
...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
|
|
72
117
|
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
73
118
|
}
|
|
74
119
|
};
|
|
@@ -88,6 +133,100 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
88
133
|
function stringOrEmpty(value) {
|
|
89
134
|
return typeof value === "string" ? value : "";
|
|
90
135
|
}
|
|
136
|
+
function portableTextSourcePaths(data, blocks) {
|
|
137
|
+
if (blocks.length === 0) return void 0;
|
|
138
|
+
const targets = new WeakSet(blocks);
|
|
139
|
+
const paths = /* @__PURE__ */ new WeakMap();
|
|
140
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
141
|
+
const stack = [{ value: data, path: ["data"] }];
|
|
142
|
+
let count = 0;
|
|
143
|
+
let inspected = 0;
|
|
144
|
+
while (stack.length > 0) {
|
|
145
|
+
const current = stack.pop();
|
|
146
|
+
if (typeof current.value !== "object" || current.value === null) continue;
|
|
147
|
+
if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
|
|
148
|
+
seen.add(current.value);
|
|
149
|
+
if (targets.has(current.value)) {
|
|
150
|
+
paths.set(current.value, current.path);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (Array.isArray(current.value)) {
|
|
154
|
+
for (let i = 0; i < current.value.length; i++) {
|
|
155
|
+
if (++inspected > 5e4) return void 0;
|
|
156
|
+
const value = current.value[i];
|
|
157
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i] });
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
for (const key in current.value) {
|
|
161
|
+
if (!Object.hasOwn(current.value, key)) continue;
|
|
162
|
+
if (++inspected > 5e4) return void 0;
|
|
163
|
+
const value = current.value[key];
|
|
164
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key] });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const result = blocks.map((block) => paths.get(block) ?? null);
|
|
169
|
+
return result.some((path) => path !== null) ? result : void 0;
|
|
170
|
+
}
|
|
171
|
+
var BODY_CAPABILITIES = ["text", "headings", "paragraphs", "links"];
|
|
172
|
+
function intersectCapabilities(current, other) {
|
|
173
|
+
return other === void 0 ? [...current] : current.filter((capability) => other.includes(capability));
|
|
174
|
+
}
|
|
175
|
+
function bodyWithCapabilities(blocks, capabilities) {
|
|
176
|
+
return capabilities.length === BODY_CAPABILITIES.length ? { blocks } : { blocks, capabilities };
|
|
177
|
+
}
|
|
178
|
+
function portableTextSourceSpanPaths(data, blocks, blockPaths) {
|
|
179
|
+
const targets = /* @__PURE__ */ new WeakSet();
|
|
180
|
+
let targetCount = 0;
|
|
181
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
182
|
+
if (blockPaths?.[i]) continue;
|
|
183
|
+
for (const child of blocks[i].children) {
|
|
184
|
+
if (++targetCount > 5e4) return void 0;
|
|
185
|
+
targets.add(child);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (targetCount === 0) return void 0;
|
|
189
|
+
const paths = /* @__PURE__ */ new WeakMap();
|
|
190
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
191
|
+
const stack = [
|
|
192
|
+
{ value: data, path: ["data"], portableChild: false }
|
|
193
|
+
];
|
|
194
|
+
let count = 0;
|
|
195
|
+
let inspected = 0;
|
|
196
|
+
while (stack.length > 0) {
|
|
197
|
+
const current = stack.pop();
|
|
198
|
+
if (typeof current.value !== "object" || current.value === null) continue;
|
|
199
|
+
if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
|
|
200
|
+
seen.add(current.value);
|
|
201
|
+
if (current.portableChild && targets.has(current.value) && isPortableTextSpan(current.value)) {
|
|
202
|
+
paths.set(current.value, [...current.path, "text"]);
|
|
203
|
+
}
|
|
204
|
+
if (Array.isArray(current.value)) {
|
|
205
|
+
for (let i = 0; i < current.value.length; i++) {
|
|
206
|
+
if (++inspected > 5e4) return void 0;
|
|
207
|
+
const value = current.value[i];
|
|
208
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i], portableChild: current.portableChild });
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
for (const key in current.value) {
|
|
212
|
+
if (!Object.hasOwn(current.value, key)) continue;
|
|
213
|
+
if (++inspected > 5e4) return void 0;
|
|
214
|
+
const value = current.value[key];
|
|
215
|
+
if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key], portableChild: key === "children" && isPortableTextBlockCandidate(current.value) });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let mapped = false;
|
|
220
|
+
const result = blocks.map((block, index) => {
|
|
221
|
+
if (blockPaths?.[index]) return null;
|
|
222
|
+
return block.children.map((child) => {
|
|
223
|
+
const path = paths.get(child) ?? null;
|
|
224
|
+
if (path !== null) mapped = true;
|
|
225
|
+
return path;
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
return mapped ? result : void 0;
|
|
229
|
+
}
|
|
91
230
|
function extractSelectedContent(data, fields, mappings) {
|
|
92
231
|
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
93
232
|
const all = ["text", "headings", "paragraphs", "links"];
|
|
@@ -96,10 +235,17 @@ function extractSelectedContent(data, fields, mappings) {
|
|
|
96
235
|
let observed = false;
|
|
97
236
|
for (const field of fields) {
|
|
98
237
|
const value = data[field];
|
|
99
|
-
if (value === void 0
|
|
238
|
+
if (value === void 0) {
|
|
239
|
+
capabilities = [];
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
100
242
|
observed = true;
|
|
243
|
+
if (value === null) continue;
|
|
101
244
|
const part = extractFirstPortableText({ content: value }, mappings);
|
|
102
|
-
if (part === null)
|
|
245
|
+
if (part === null) {
|
|
246
|
+
capabilities = [];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
103
249
|
blocks.push(...part.blocks);
|
|
104
250
|
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
105
251
|
}
|
|
@@ -110,9 +256,14 @@ function extractPortableText(data, mappings) {
|
|
|
110
256
|
const result = extractFirstPortableText(data, mappings);
|
|
111
257
|
if (result === null) return null;
|
|
112
258
|
const candidates = Object.entries(data).filter(
|
|
113
|
-
([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
259
|
+
([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
114
260
|
);
|
|
115
|
-
|
|
261
|
+
const unrecognizedBody = ["body", "content", "blocks"].some((field) => {
|
|
262
|
+
const value = data[field];
|
|
263
|
+
if (value == null || Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) return false;
|
|
264
|
+
return !candidates.some(([candidate]) => candidate === field);
|
|
265
|
+
});
|
|
266
|
+
return candidates.length > 1 || unrecognizedBody ? { blocks: result.blocks, capabilities: [] } : result;
|
|
116
267
|
}
|
|
117
268
|
function extractFirstPortableText(data, mappings) {
|
|
118
269
|
let foundEmptyBody = false;
|
|
@@ -120,7 +271,8 @@ function extractFirstPortableText(data, mappings) {
|
|
|
120
271
|
const value = data[slug];
|
|
121
272
|
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
122
273
|
if (isPortableTextArray(value)) return { blocks: value };
|
|
123
|
-
if (
|
|
274
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
275
|
+
if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
|
|
124
276
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
125
277
|
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
126
278
|
}
|
|
@@ -128,32 +280,184 @@ function extractFirstPortableText(data, mappings) {
|
|
|
128
280
|
const blocks = data["blocks"];
|
|
129
281
|
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
130
282
|
if (isPortableTextArray(blocks)) return { blocks };
|
|
131
|
-
if (
|
|
283
|
+
if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
|
|
284
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
|
|
132
285
|
for (const [field, value] of Object.entries(data)) {
|
|
133
286
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
134
287
|
if (isPortableTextArray(value)) return { blocks: value };
|
|
135
|
-
if (
|
|
288
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
289
|
+
if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
|
|
136
290
|
}
|
|
137
291
|
return foundEmptyBody ? { blocks: [] } : null;
|
|
138
292
|
}
|
|
139
293
|
function isPortableTextArray(value) {
|
|
140
294
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
141
|
-
(item) =>
|
|
142
|
-
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
143
|
-
)
|
|
295
|
+
(item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
|
|
144
296
|
);
|
|
145
297
|
}
|
|
298
|
+
function isPortableTextSpan(value) {
|
|
299
|
+
return isRecord(value) && value["_type"] === "span" && typeof value["text"] === "string";
|
|
300
|
+
}
|
|
301
|
+
function isPortableTextBlockCandidate(value) {
|
|
302
|
+
return isRecord(value) && value["_type"] === "block" && Array.isArray(value["children"]);
|
|
303
|
+
}
|
|
304
|
+
function isWireSafeSpan(value) {
|
|
305
|
+
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"));
|
|
306
|
+
}
|
|
307
|
+
function isWireSafeMarkDef(value) {
|
|
308
|
+
return isRecord(value) && typeof value["_key"] === "string" && typeof value["_type"] === "string" && (value["_type"] !== "link" || typeof value["href"] === "string");
|
|
309
|
+
}
|
|
310
|
+
function isWireSafeBlock(block) {
|
|
311
|
+
const level = block["level"];
|
|
312
|
+
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);
|
|
313
|
+
}
|
|
314
|
+
function normalizePortableBlock(block) {
|
|
315
|
+
const normalized = { ...block };
|
|
316
|
+
let incomplete = false;
|
|
317
|
+
if (normalized["_key"] != null && typeof normalized["_key"] !== "string") {
|
|
318
|
+
delete normalized["_key"];
|
|
319
|
+
incomplete = true;
|
|
320
|
+
}
|
|
321
|
+
if (normalized["style"] !== void 0 && (typeof normalized["style"] !== "string" || !["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(normalized["style"]))) {
|
|
322
|
+
delete normalized["style"];
|
|
323
|
+
incomplete = true;
|
|
324
|
+
}
|
|
325
|
+
if (normalized["listItem"] != null && normalized["listItem"] !== "bullet" && normalized["listItem"] !== "number") {
|
|
326
|
+
delete normalized["listItem"];
|
|
327
|
+
incomplete = true;
|
|
328
|
+
}
|
|
329
|
+
const level = normalized["level"];
|
|
330
|
+
if (level != null && (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4294967295)) {
|
|
331
|
+
delete normalized["level"];
|
|
332
|
+
incomplete = true;
|
|
333
|
+
}
|
|
334
|
+
if (normalized["markDefs"] !== void 0) {
|
|
335
|
+
if (!Array.isArray(normalized["markDefs"])) {
|
|
336
|
+
delete normalized["markDefs"];
|
|
337
|
+
incomplete = true;
|
|
338
|
+
} else {
|
|
339
|
+
const defs = normalized["markDefs"].filter(isWireSafeMarkDef);
|
|
340
|
+
if (defs.length !== normalized["markDefs"].length) {
|
|
341
|
+
normalized["markDefs"] = defs;
|
|
342
|
+
incomplete = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
normalized["children"] = block["children"].map((child) => {
|
|
347
|
+
if (!isPortableTextSpan(child) || !isRecord(child) || isWireSafeSpan(child)) return child;
|
|
348
|
+
const span = { ...child };
|
|
349
|
+
if (span["_key"] != null && typeof span["_key"] !== "string") delete span["_key"];
|
|
350
|
+
if (span["marks"] !== void 0 && (!Array.isArray(span["marks"]) || !span["marks"].every((mark) => typeof mark === "string"))) delete span["marks"];
|
|
351
|
+
incomplete = true;
|
|
352
|
+
return span;
|
|
353
|
+
});
|
|
354
|
+
return { block: normalized, incomplete };
|
|
355
|
+
}
|
|
356
|
+
function isMixedPortableTextArray(value) {
|
|
357
|
+
return Array.isArray(value) && value.some(isPortableTextBlockCandidate);
|
|
358
|
+
}
|
|
359
|
+
function mapMixedPortableText(blocks, mappings, depth = 0) {
|
|
360
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
361
|
+
const result = [];
|
|
362
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
363
|
+
for (const block of blocks) {
|
|
364
|
+
if (isPortableTextArray([block])) {
|
|
365
|
+
result.push(block);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (isPortableTextBlockCandidate(block)) {
|
|
369
|
+
const normalized = normalizePortableBlock(block);
|
|
370
|
+
const projected = projectInlineBlock(normalized.block, mappings);
|
|
371
|
+
result.push(projected.block);
|
|
372
|
+
capabilities = normalized.incomplete ? [] : intersectCapabilities(capabilities, projected.capabilities);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (!isRecord(block) || typeof block["_type"] !== "string" || block["_type"] === "block") {
|
|
376
|
+
capabilities = [];
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const mapped = mapCustomBlocks([block], mappings, depth + 1);
|
|
380
|
+
result.push(...mapped.blocks);
|
|
381
|
+
capabilities = intersectCapabilities(capabilities, mapped.capabilities);
|
|
382
|
+
}
|
|
383
|
+
return bodyWithCapabilities(result, capabilities);
|
|
384
|
+
}
|
|
385
|
+
function projectInlineBlock(block, mappings) {
|
|
386
|
+
const children = [];
|
|
387
|
+
const defs = Array.isArray(block["markDefs"]) ? [...block["markDefs"]] : [];
|
|
388
|
+
const keys = new Set(defs.flatMap((def) => isRecord(def) && typeof def["_key"] === "string" ? [def["_key"]] : []));
|
|
389
|
+
let sequence = 0;
|
|
390
|
+
let capabilities = block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) ? [...BODY_CAPABILITIES] : [];
|
|
391
|
+
for (const child of block["children"]) {
|
|
392
|
+
if (isRecord(child) && Array.isArray(child["marks"])) {
|
|
393
|
+
for (const mark of child["marks"]) if (typeof mark === "string") keys.add(mark);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const child of block["children"]) {
|
|
397
|
+
if (isPortableTextSpan(child)) {
|
|
398
|
+
children.push(child);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const type = isRecord(child) ? child["_type"] : void 0;
|
|
402
|
+
const mapping = typeof type === "string" && mappings && Object.hasOwn(mappings, type) ? mappings[type] : void 0;
|
|
403
|
+
if (!isRecord(child) || !isRecord(mapping) || !Array.isArray(mapping["inline"])) {
|
|
404
|
+
capabilities = [];
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const declared = mapping["capabilities"];
|
|
408
|
+
if (declared !== void 0 && (!Array.isArray(declared) || declared.some((value) => typeof value !== "string" || !BODY_CAPABILITIES.includes(value)) || new Set(declared).size !== declared.length)) {
|
|
409
|
+
capabilities = [];
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const newChildren = [];
|
|
413
|
+
const newDefs = [];
|
|
414
|
+
let valid = true;
|
|
415
|
+
for (const field of mapping["inline"]) {
|
|
416
|
+
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"])) {
|
|
417
|
+
valid = false;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
const value = child[field["field"]];
|
|
421
|
+
const href = field["kind"] === "link" && typeof field["hrefField"] === "string" && field["hrefField"] ? child[field["hrefField"]] : void 0;
|
|
422
|
+
if ((value == null || value === "") && field["optional"] === true && (field["kind"] !== "link" || href == null || href === "")) continue;
|
|
423
|
+
if (typeof value !== "string" || field["kind"] === "link" && (typeof href !== "string" || !href)) {
|
|
424
|
+
valid = false;
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
if (field["kind"] === "text") newChildren.push({ _type: "span", text: value, marks: [] });
|
|
428
|
+
else {
|
|
429
|
+
let key;
|
|
430
|
+
do {
|
|
431
|
+
key = `aexeo-inline-${sequence++}`;
|
|
432
|
+
} while (keys.has(key));
|
|
433
|
+
keys.add(key);
|
|
434
|
+
newDefs.push({ _key: key, _type: "link", href });
|
|
435
|
+
newChildren.push({ _type: "span", text: value, marks: [key] });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (!valid) {
|
|
439
|
+
capabilities = [];
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
children.push(...newChildren);
|
|
443
|
+
defs.push(...newDefs);
|
|
444
|
+
if (Array.isArray(declared)) capabilities = intersectCapabilities(capabilities, declared);
|
|
445
|
+
}
|
|
446
|
+
return { block: { ...block, children, markDefs: defs }, capabilities };
|
|
447
|
+
}
|
|
146
448
|
function isEmdashBlocksArray(value, field) {
|
|
147
449
|
if (!Array.isArray(value) || value.length === 0) return false;
|
|
148
450
|
return value.every(
|
|
149
|
-
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
451
|
+
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks" || field === "body" || field === "content")
|
|
150
452
|
);
|
|
151
453
|
}
|
|
152
|
-
function mapCustomBlocks(blocks, mappings) {
|
|
454
|
+
function mapCustomBlocks(blocks, mappings, depth = 0) {
|
|
455
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
153
456
|
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
154
457
|
const result = [];
|
|
155
|
-
|
|
156
|
-
|
|
458
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
459
|
+
const mapFields = (record, fields, depth2) => {
|
|
460
|
+
if (depth2 > 8 || fields.length === 0) return false;
|
|
157
461
|
for (const entry of fields) {
|
|
158
462
|
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
159
463
|
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
@@ -165,8 +469,16 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
165
469
|
continue;
|
|
166
470
|
}
|
|
167
471
|
if (entry.kind === "richText") {
|
|
168
|
-
if (!Array.isArray(value)
|
|
169
|
-
|
|
472
|
+
if (!Array.isArray(value)) return false;
|
|
473
|
+
if (value.length === 0 || isPortableTextArray(value)) {
|
|
474
|
+
result.push(...value);
|
|
475
|
+
} else if (isMixedPortableTextArray(value) || isEmdashBlocksArray(value, "blocks")) {
|
|
476
|
+
const nested = mapMixedPortableText(value, mappings, depth2 + 1);
|
|
477
|
+
result.push(...nested.blocks);
|
|
478
|
+
capabilities = intersectCapabilities(capabilities, nested.capabilities);
|
|
479
|
+
} else {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
170
482
|
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
171
483
|
if (typeof value !== "string") return false;
|
|
172
484
|
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
@@ -181,7 +493,7 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
181
493
|
} else if (entry.kind === "repeat") {
|
|
182
494
|
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
183
495
|
for (const item of value) {
|
|
184
|
-
if (!isRecord(item) || !mapFields(item, entry.fields,
|
|
496
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
|
|
185
497
|
}
|
|
186
498
|
} else {
|
|
187
499
|
return false;
|
|
@@ -190,10 +502,20 @@ function mapCustomBlocks(blocks, mappings) {
|
|
|
190
502
|
return true;
|
|
191
503
|
};
|
|
192
504
|
for (const block of blocks) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
505
|
+
const mapping = mappings?.[String(block["_type"])];
|
|
506
|
+
let fields;
|
|
507
|
+
if (Array.isArray(mapping)) {
|
|
508
|
+
fields = mapping;
|
|
509
|
+
} else if (isRecord(mapping) && Array.isArray(mapping["fields"]) && Array.isArray(mapping["capabilities"])) {
|
|
510
|
+
const declared = mapping["capabilities"];
|
|
511
|
+
if (declared.some((capability) => typeof capability !== "string" || !BODY_CAPABILITIES.includes(capability)) || new Set(declared).size !== declared.length) return unavailable();
|
|
512
|
+
fields = mapping["fields"];
|
|
513
|
+
capabilities = intersectCapabilities(capabilities, declared);
|
|
514
|
+
} else return unavailable();
|
|
515
|
+
if (fields.length === 0) continue;
|
|
516
|
+
if (!mapFields(block, fields, depth)) return unavailable();
|
|
517
|
+
}
|
|
518
|
+
return bodyWithCapabilities(result, capabilities);
|
|
197
519
|
}
|
|
198
520
|
function blocksToPortableText(blocks) {
|
|
199
521
|
const paragraphs = [];
|
|
@@ -207,20 +529,49 @@ function blocksToPortableText(blocks) {
|
|
|
207
529
|
}
|
|
208
530
|
return paragraphs;
|
|
209
531
|
}
|
|
532
|
+
var FALLBACK_MAX_DEPTH = 64;
|
|
533
|
+
var FALLBACK_MAX_NODES = 5e4;
|
|
534
|
+
var FALLBACK_MAX_CHARS = 1048576;
|
|
210
535
|
function collectText(value, key, out) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
536
|
+
const active = /* @__PURE__ */ new WeakSet();
|
|
537
|
+
let nodes = 0;
|
|
538
|
+
let remaining = FALLBACK_MAX_CHARS;
|
|
539
|
+
const visit = (current, field, depth) => {
|
|
540
|
+
if (depth > FALLBACK_MAX_DEPTH || nodes >= FALLBACK_MAX_NODES || remaining <= 0) return;
|
|
541
|
+
nodes++;
|
|
542
|
+
if (typeof current === "string") {
|
|
543
|
+
if (!isStructuralField(field)) {
|
|
544
|
+
const separator = out.length > 0 ? 1 : 0;
|
|
545
|
+
const limit = Math.max(0, remaining - separator);
|
|
546
|
+
let text = current.slice(0, limit);
|
|
547
|
+
if (limit < current.length && /[\uD800-\uDBFF]/.test(text.at(-1) ?? "") && /[\uDC00-\uDFFF]/.test(current[limit] ?? "")) text = text.slice(0, -1);
|
|
548
|
+
text = text.trim();
|
|
549
|
+
if (text.length > 0) {
|
|
550
|
+
out.push(text);
|
|
551
|
+
remaining -= text.length + separator;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (!Array.isArray(current) && !isRecord(current)) return;
|
|
557
|
+
if (active.has(current)) return;
|
|
558
|
+
active.add(current);
|
|
559
|
+
if (Array.isArray(current)) {
|
|
560
|
+
for (const item of current) {
|
|
561
|
+
if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
|
|
562
|
+
visit(item, field, depth + 1);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
for (const childKey in current) {
|
|
566
|
+
if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
|
|
567
|
+
if (Object.hasOwn(current, childKey) && !isStructuralField(childKey)) {
|
|
568
|
+
visit(current[childKey], childKey, depth + 1);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
active.delete(current);
|
|
573
|
+
};
|
|
574
|
+
visit(value, key, 0);
|
|
224
575
|
}
|
|
225
576
|
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
226
577
|
"_key",
|
|
@@ -279,12 +630,13 @@ function isRecord(value) {
|
|
|
279
630
|
}
|
|
280
631
|
|
|
281
632
|
// src/version.ts
|
|
282
|
-
var PACKAGE_VERSION = "0.9.
|
|
633
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
283
634
|
|
|
284
635
|
// src/plugin.ts
|
|
285
636
|
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
286
637
|
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
287
638
|
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
639
|
+
var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
|
|
288
640
|
var PLUGIN_SETTINGS_SCHEMA = {
|
|
289
641
|
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
290
642
|
type: "string",
|
|
@@ -292,6 +644,12 @@ var PLUGIN_SETTINGS_SCHEMA = {
|
|
|
292
644
|
description: "Ordered rendered body fields per collection.",
|
|
293
645
|
multiline: true
|
|
294
646
|
},
|
|
647
|
+
[SETTINGS_DESCRIPTION_FIELDS_KEY]: {
|
|
648
|
+
type: "string",
|
|
649
|
+
label: "Description fields by collection (JSON)",
|
|
650
|
+
description: "Ordered renderer description fallback fields per collection.",
|
|
651
|
+
multiline: true
|
|
652
|
+
},
|
|
295
653
|
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
296
654
|
type: "string",
|
|
297
655
|
label: "Custom block mappings (JSON)",
|
|
@@ -307,14 +665,54 @@ var PLUGIN_SETTINGS_SCHEMA = {
|
|
|
307
665
|
function recordContentCoverage(ctx, documents) {
|
|
308
666
|
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
309
667
|
for (const document of documents) {
|
|
310
|
-
|
|
311
|
-
else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
|
|
312
|
-
(capability) => document.body_capabilities?.includes(capability)
|
|
313
|
-
)) counts.complete++;
|
|
314
|
-
else counts.partial++;
|
|
668
|
+
counts[cmsBodyCoverage(document)]++;
|
|
315
669
|
}
|
|
316
670
|
try {
|
|
317
671
|
ctx.log?.info?.("aexeo.content_coverage", counts);
|
|
672
|
+
ctx.log?.info?.("aexeo.page_coverage", { documents: documents.length, rendered_page: "not_observed", generated_metadata: "not_observed" });
|
|
673
|
+
const observed = documents.filter((document) => document.description_source_observed === true).length;
|
|
674
|
+
const withValue = documents.filter((document) => document.description_source_observed === true && !!document.description).length;
|
|
675
|
+
ctx.log?.info?.("aexeo.description_coverage", { documents: documents.length, observed, with_value: withValue, unverified: documents.length - observed });
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function recordAssessmentCoverage(ctx, result, documents) {
|
|
680
|
+
let passed = 0, failed = 0, notChecked = 0, mappingReports = 0, mappingEligible = 0, mappingMapped = 0, mappingUnmapped = 0;
|
|
681
|
+
if (result.ok) for (const assessment of Object.values(result.assessments ?? {})) {
|
|
682
|
+
if (assessment.status !== "evaluated") continue;
|
|
683
|
+
if (assessment.source_mapping) {
|
|
684
|
+
mappingReports++;
|
|
685
|
+
mappingEligible += assessment.source_mapping.eligible;
|
|
686
|
+
mappingMapped += assessment.source_mapping.mapped;
|
|
687
|
+
mappingUnmapped += assessment.source_mapping.unmapped;
|
|
688
|
+
}
|
|
689
|
+
for (const gate of assessment.gates) {
|
|
690
|
+
if (gate.status === "passed") passed++;
|
|
691
|
+
else if (gate.status === "failed") failed++;
|
|
692
|
+
else notChecked++;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
let assessedDocuments = 0, missingDocuments = 0, errorDocuments = 0;
|
|
696
|
+
if (result.ok) for (const document of documents) {
|
|
697
|
+
const assessment = result.assessments?.[document.route];
|
|
698
|
+
if (assessment === void 0) missingDocuments++;
|
|
699
|
+
else if (assessment.status === "error") errorDocuments++;
|
|
700
|
+
else assessedDocuments++;
|
|
701
|
+
}
|
|
702
|
+
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;
|
|
703
|
+
try {
|
|
704
|
+
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 });
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
|
|
708
|
+
let withheld = 0, incompleteBodyWithheld = 0;
|
|
709
|
+
if (result.ok) for (const finding of result.findings) {
|
|
710
|
+
if (finding.aexeo_origin === "content_preset") continue;
|
|
711
|
+
if (!isLegacyCmsObservableFinding(finding)) withheld++;
|
|
712
|
+
else if (!isLegacyCmsObservableFinding(finding, completeByRoute.get(finding.path) ?? false)) incompleteBodyWithheld++;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
ctx.log?.info?.("aexeo.cms_finding_coverage", { status: result.ok ? "evaluated" : "error", rendered_findings_withheld: withheld, incomplete_body_findings_withheld: incompleteBodyWithheld });
|
|
318
716
|
} catch {
|
|
319
717
|
}
|
|
320
718
|
}
|
|
@@ -347,15 +745,63 @@ var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
|
347
745
|
"CNT005"
|
|
348
746
|
// visible text duplicated on another route
|
|
349
747
|
]);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
748
|
+
function isSiteGraphFinding(finding) {
|
|
749
|
+
return finding.aexeo_origin !== "content_preset" && SITE_GRAPH_RULE_IDS.has(finding.rule_id);
|
|
750
|
+
}
|
|
751
|
+
function withCmsSourceLocation(finding, stored) {
|
|
752
|
+
if (finding.aexeo_cms_location !== void 0) {
|
|
753
|
+
finding = { ...finding };
|
|
754
|
+
delete finding.aexeo_cms_location;
|
|
755
|
+
}
|
|
756
|
+
const location = finding.aexeo_source_location;
|
|
757
|
+
if (finding.aexeo_origin !== "content_preset" || !location || !stored) return finding;
|
|
758
|
+
const blockPath = stored.meta.bodySourcePaths?.[location.block_index];
|
|
759
|
+
const spanPath = stored.meta.bodySourceSpanPaths?.[location.block_index]?.[location.span_index];
|
|
760
|
+
const path = blockPath ? [...blockPath, "children", location.span_index, "text"] : spanPath;
|
|
761
|
+
const text = stored.document.body?.[location.block_index]?.children[location.span_index]?.text;
|
|
762
|
+
const offset = location.utf16_offset;
|
|
763
|
+
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;
|
|
764
|
+
return { ...finding, aexeo_cms_location: { path, utf16_offset: offset } };
|
|
765
|
+
}
|
|
766
|
+
function countCmsLocations(counts, findings) {
|
|
767
|
+
for (const finding of findings) {
|
|
768
|
+
if (finding.aexeo_origin !== "content_preset" || finding.aexeo_source_location === void 0) continue;
|
|
769
|
+
counts.eligible++;
|
|
770
|
+
if (finding.aexeo_cms_location !== void 0) counts.mapped++;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function recordCmsLocationCoverage(ctx, counts) {
|
|
774
|
+
try {
|
|
775
|
+
ctx.log?.info?.("aexeo.cms_location_coverage", { ...counts, unmapped: counts.eligible - counts.mapped });
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields, descriptionFields, contentFieldsUnavailable = false) {
|
|
780
|
+
const prior = (await readAllStoredDocuments(ctx.kv)).filter(
|
|
781
|
+
(stored2) => stored2.meta.id === event.content.id && stored2.meta.collection === event.content.type
|
|
357
782
|
);
|
|
783
|
+
let adapted;
|
|
784
|
+
try {
|
|
785
|
+
adapted = await adaptContentItemWithContext(
|
|
786
|
+
event.content,
|
|
787
|
+
ctx.content,
|
|
788
|
+
ctx.site?.locale,
|
|
789
|
+
blockMappings,
|
|
790
|
+
contentFields,
|
|
791
|
+
descriptionFields
|
|
792
|
+
);
|
|
793
|
+
} catch {
|
|
794
|
+
try {
|
|
795
|
+
ctx.log?.info?.("aexeo.adaptation_coverage", { failed: 1 });
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
ctx.log?.error?.("Aexeo afterSave adaptation failed");
|
|
799
|
+
const previous = prior.length === 1 ? prior[0] : void 0;
|
|
800
|
+
if (previous !== void 0) await markAdaptationUnavailable(ctx, event.content, previous);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
358
803
|
const document = adapted.document;
|
|
804
|
+
if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
|
|
359
805
|
recordContentCoverage(ctx, [document]);
|
|
360
806
|
const { kv, log } = ctx;
|
|
361
807
|
const stored = { document, meta: adapted.meta };
|
|
@@ -363,20 +809,28 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
363
809
|
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
364
810
|
return;
|
|
365
811
|
}
|
|
366
|
-
const result = await evaluator
|
|
812
|
+
const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
|
|
813
|
+
recordAssessmentCoverage(ctx, result, [document]);
|
|
367
814
|
if (!result.ok) {
|
|
368
815
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
369
816
|
detail: result.detail
|
|
370
817
|
});
|
|
818
|
+
if (!await isSuperseded(kv, stored)) {
|
|
819
|
+
await kv.set(findingsKey(document.route), {
|
|
820
|
+
route: document.route,
|
|
821
|
+
findings: [],
|
|
822
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
823
|
+
});
|
|
824
|
+
}
|
|
371
825
|
return;
|
|
372
826
|
}
|
|
373
827
|
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
374
|
-
(finding) =>
|
|
828
|
+
(finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
375
829
|
);
|
|
376
830
|
const pageFindings = [
|
|
377
831
|
...result.findings.filter(
|
|
378
|
-
(finding) => finding.scope === "page" && !
|
|
379
|
-
),
|
|
832
|
+
(finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
833
|
+
).map((finding) => withCmsSourceLocation(finding, stored)),
|
|
380
834
|
...storedGraphFindings
|
|
381
835
|
];
|
|
382
836
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
@@ -387,16 +841,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
387
841
|
},
|
|
388
842
|
pageFindings
|
|
389
843
|
);
|
|
390
|
-
if (await isSuperseded(kv,
|
|
844
|
+
if (await isSuperseded(kv, stored)) {
|
|
391
845
|
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
392
846
|
return;
|
|
393
847
|
}
|
|
848
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
849
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
850
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
394
851
|
await kv.set(findingsKey(document.route), {
|
|
395
852
|
route: document.route,
|
|
396
|
-
findings: filtered
|
|
853
|
+
findings: filtered,
|
|
854
|
+
body_coverage: cmsBodyCoverage(document),
|
|
855
|
+
...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
|
|
397
856
|
});
|
|
398
857
|
}
|
|
399
858
|
var DEFAULT_COLLECTIONS = ["posts", "pages"];
|
|
859
|
+
async function markAdaptationUnavailable(ctx, item, previous) {
|
|
860
|
+
if (isOlderRevision(item.updatedAt, previous.meta.updatedAt) || await isSuperseded(ctx.kv, previous)) return;
|
|
861
|
+
const unavailable = {
|
|
862
|
+
document: { route: previous.document.route, title: previous.document.title },
|
|
863
|
+
meta: {
|
|
864
|
+
id: item.id,
|
|
865
|
+
collection: item.type,
|
|
866
|
+
slug: item.slug,
|
|
867
|
+
status: item.status,
|
|
868
|
+
title: previous.meta.title,
|
|
869
|
+
publicUrl: null,
|
|
870
|
+
adaptationFailed: true,
|
|
871
|
+
...item.updatedAt === void 0 ? {} : { updatedAt: item.updatedAt }
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
if (!await persistDocument(ctx, unavailable) || await isSuperseded(ctx.kv, unavailable)) return;
|
|
875
|
+
recordContentCoverage(ctx, [unavailable.document]);
|
|
876
|
+
await ctx.kv.set(findingsKey(unavailable.document.route), {
|
|
877
|
+
route: unavailable.document.route,
|
|
878
|
+
findings: [],
|
|
879
|
+
assessment: { status: "error", reason: "adaptation_failed", detail: "The CMS record could not be adapted." }
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async function evaluateSafely(evaluator, documents, context) {
|
|
883
|
+
try {
|
|
884
|
+
return await evaluator(documents, context);
|
|
885
|
+
} catch {
|
|
886
|
+
return { ok: false, reason: "evaluator_exception", detail: "The evaluator failed unexpectedly." };
|
|
887
|
+
}
|
|
888
|
+
}
|
|
400
889
|
async function evaluateAndPersistAll(ctx, options) {
|
|
401
890
|
const collections = options.collections ?? DEFAULT_COLLECTIONS;
|
|
402
891
|
const suppressionFilter = options.suppressionFilter;
|
|
@@ -412,8 +901,10 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
412
901
|
const documentCollections = [];
|
|
413
902
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
414
903
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
904
|
+
const adaptationFailures = [];
|
|
415
905
|
for (const collection of collections) {
|
|
416
906
|
let cursor;
|
|
907
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
417
908
|
do {
|
|
418
909
|
try {
|
|
419
910
|
const page = await ctx.content.list(collection, {
|
|
@@ -421,22 +912,35 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
421
912
|
...cursor === void 0 ? {} : { cursor }
|
|
422
913
|
});
|
|
423
914
|
for (const item of page.items) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
915
|
+
try {
|
|
916
|
+
const adapted = await adaptContentItemWithContext(
|
|
917
|
+
item,
|
|
918
|
+
ctx.content,
|
|
919
|
+
ctx.site?.locale,
|
|
920
|
+
options.blockMappings,
|
|
921
|
+
options.contentFieldsByCollection?.[collection],
|
|
922
|
+
options.descriptionFieldsByCollection?.[collection]
|
|
923
|
+
);
|
|
924
|
+
if (options.contentFieldsUnavailable && adapted.document.body !== void 0) adapted.document.body_capabilities = [];
|
|
925
|
+
documents.push(adapted.document);
|
|
926
|
+
documentCollections.push(adapted.meta.collection);
|
|
927
|
+
documentRoutes.add(adapted.document.route);
|
|
928
|
+
adaptedByRoute.set(adapted.document.route, {
|
|
929
|
+
document: adapted.document,
|
|
930
|
+
meta: adapted.meta
|
|
931
|
+
});
|
|
932
|
+
} catch {
|
|
933
|
+
adaptationFailures.push({ item, collection });
|
|
934
|
+
summary.errors.push(`${collection}: content adaptation failed`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (page.hasMore) {
|
|
938
|
+
if (!page.cursor || seenCursors.has(page.cursor)) throw new Error("incomplete pagination: missing or repeated cursor");
|
|
939
|
+
seenCursors.add(page.cursor);
|
|
940
|
+
cursor = page.cursor;
|
|
941
|
+
} else {
|
|
942
|
+
cursor = void 0;
|
|
438
943
|
}
|
|
439
|
-
cursor = page.hasMore ? page.cursor : void 0;
|
|
440
944
|
} catch (err) {
|
|
441
945
|
const message = err instanceof Error ? err.message : String(err);
|
|
442
946
|
summary.errors.push(`${collection}: ${message}`);
|
|
@@ -444,36 +948,100 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
444
948
|
}
|
|
445
949
|
} while (cursor !== void 0);
|
|
446
950
|
}
|
|
951
|
+
const inventoryComplete = summary.errors.length === 0;
|
|
447
952
|
summary.documentsScanned = documents.length;
|
|
448
953
|
recordContentCoverage(ctx, documents);
|
|
954
|
+
if (adaptationFailures.length > 0) {
|
|
955
|
+
try {
|
|
956
|
+
ctx.log?.info?.("aexeo.adaptation_coverage", { failed: adaptationFailures.length });
|
|
957
|
+
} catch {
|
|
958
|
+
}
|
|
959
|
+
const storedByCollection = /* @__PURE__ */ new Map();
|
|
960
|
+
for (const stored of storedAtStart) {
|
|
961
|
+
const byId = storedByCollection.get(stored.meta.collection) ?? /* @__PURE__ */ new Map();
|
|
962
|
+
const entries = byId.get(stored.meta.id) ?? [];
|
|
963
|
+
entries.push(stored);
|
|
964
|
+
byId.set(stored.meta.id, entries);
|
|
965
|
+
storedByCollection.set(stored.meta.collection, byId);
|
|
966
|
+
}
|
|
967
|
+
for (const { item, collection } of adaptationFailures) {
|
|
968
|
+
for (const stored of storedByCollection.get(collection)?.get(item.id) ?? []) {
|
|
969
|
+
if (documentRoutes.has(stored.document.route)) continue;
|
|
970
|
+
await markAdaptationUnavailable(ctx, item, stored);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
449
974
|
for (const stored of adaptedByRoute.values()) {
|
|
450
975
|
const current = await readStoredDocument(kv, stored.document.route);
|
|
451
976
|
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
452
977
|
await kv.set(documentKey(stored.document.route), stored);
|
|
453
978
|
}
|
|
454
979
|
}
|
|
455
|
-
const result = await options.evaluator
|
|
980
|
+
const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
|
|
981
|
+
recordAssessmentCoverage(ctx, result, documents);
|
|
456
982
|
if (!result.ok) {
|
|
457
983
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
458
984
|
detail: result.detail
|
|
459
985
|
});
|
|
460
986
|
summary.errors.push(`${result.reason}: ${result.detail}`);
|
|
987
|
+
try {
|
|
988
|
+
ctx.log?.info?.("aexeo.sitewide_coverage", { status: "evaluation_error" });
|
|
989
|
+
} catch {
|
|
990
|
+
}
|
|
991
|
+
await kv.set(findingsKey("*"), {
|
|
992
|
+
route: "*",
|
|
993
|
+
findings: [],
|
|
994
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
995
|
+
});
|
|
996
|
+
for (const [route, stored] of adaptedByRoute) {
|
|
997
|
+
if (!await isSuperseded(kv, stored)) {
|
|
998
|
+
await kv.set(findingsKey(route), {
|
|
999
|
+
route,
|
|
1000
|
+
findings: [],
|
|
1001
|
+
assessment: { status: "error", reason: result.reason, detail: result.detail }
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
461
1005
|
return summary;
|
|
462
1006
|
}
|
|
463
1007
|
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
1008
|
+
findingsByRoute.set("*", []);
|
|
464
1009
|
for (const route of documentRoutes) {
|
|
465
1010
|
findingsByRoute.set(route, []);
|
|
466
1011
|
}
|
|
1012
|
+
const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
|
|
1013
|
+
const allBodiesComplete = documents.every(hasCompleteBody);
|
|
1014
|
+
const changed = await refreshInputChanged(kv, collections, adaptedByRoute, storedAtStart);
|
|
1015
|
+
const coverageStatus = !inventoryComplete ? "inventory_incomplete" : changed ? "changed_during_refresh" : !allBodiesComplete ? "body_incomplete" : "complete";
|
|
1016
|
+
const globalCoverageComplete = coverageStatus === "complete";
|
|
1017
|
+
try {
|
|
1018
|
+
ctx.log?.info?.("aexeo.sitewide_coverage", { status: coverageStatus });
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
const globalAssessment = globalCoverageComplete ? void 0 : {
|
|
1022
|
+
status: "error",
|
|
1023
|
+
reason: coverageStatus,
|
|
1024
|
+
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."
|
|
1025
|
+
};
|
|
1026
|
+
if (changed && inventoryComplete) {
|
|
1027
|
+
summary.errors.push("Sitewide checks unavailable: content changed during refresh.");
|
|
1028
|
+
} else if (!allBodiesComplete && inventoryComplete) {
|
|
1029
|
+
summary.errors.push("Sitewide checks unavailable: one or more document bodies are incomplete.");
|
|
1030
|
+
}
|
|
467
1031
|
for (const finding of result.findings) {
|
|
1032
|
+
if (!globalCoverageComplete && (finding.scope !== "page" || isSiteGraphFinding(finding))) continue;
|
|
1033
|
+
const complete = finding.scope === "page" ? completeByRoute.get(finding.path) ?? false : allBodiesComplete;
|
|
1034
|
+
if (!isCmsObservableFinding(finding, complete)) continue;
|
|
468
1035
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
469
1036
|
const list = findingsByRoute.get(bucket) ?? [];
|
|
470
|
-
list.push(finding);
|
|
1037
|
+
list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
|
|
471
1038
|
findingsByRoute.set(bucket, list);
|
|
472
1039
|
}
|
|
473
1040
|
let totalAfterSuppression = 0;
|
|
1041
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
474
1042
|
for (const [route, findings] of findingsByRoute) {
|
|
475
1043
|
const stored = adaptedByRoute.get(route);
|
|
476
|
-
if (stored !== void 0 && await isSuperseded(kv,
|
|
1044
|
+
if (stored !== void 0 && await isSuperseded(kv, stored)) {
|
|
477
1045
|
continue;
|
|
478
1046
|
}
|
|
479
1047
|
const context = {
|
|
@@ -482,16 +1050,37 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
482
1050
|
...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
|
|
483
1051
|
};
|
|
484
1052
|
const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
|
|
485
|
-
|
|
1053
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
1054
|
+
await kv.set(findingsKey(route), {
|
|
1055
|
+
route,
|
|
1056
|
+
findings: filtered,
|
|
1057
|
+
...stored === void 0 ? {} : { body_coverage: cmsBodyCoverage(stored.document) },
|
|
1058
|
+
...route === "*" && globalAssessment !== void 0 ? { assessment: globalAssessment } : result.assessments?.[route] === void 0 ? {} : { assessment: result.assessments[route] }
|
|
1059
|
+
});
|
|
486
1060
|
summary.routesUpdated += 1;
|
|
487
1061
|
totalAfterSuppression += filtered.length;
|
|
488
1062
|
}
|
|
1063
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
489
1064
|
summary.totalFindings = totalAfterSuppression;
|
|
490
|
-
if (
|
|
1065
|
+
if (inventoryComplete && !changed) {
|
|
491
1066
|
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
492
1067
|
}
|
|
493
1068
|
return summary;
|
|
494
1069
|
}
|
|
1070
|
+
async function refreshInputChanged(kv, collections, evaluated, initial) {
|
|
1071
|
+
const current = await readAllStoredDocuments(kv);
|
|
1072
|
+
const currentByRoute = new Map(current.map((entry) => [entry.document.route, entry]));
|
|
1073
|
+
const initialByRoute = new Map(initial.map((entry) => [entry.document.route, entry]));
|
|
1074
|
+
for (const [route, entry] of evaluated) {
|
|
1075
|
+
if (!sameStoredDocument(entry, currentByRoute.get(route))) return true;
|
|
1076
|
+
}
|
|
1077
|
+
const selected = new Set(collections);
|
|
1078
|
+
for (const entry of current) {
|
|
1079
|
+
const route = entry.document.route;
|
|
1080
|
+
if (!evaluated.has(route) && (entry.meta.collection === "" || selected.has(entry.meta.collection)) && !sameStoredDocument(entry, initialByRoute.get(route))) return true;
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
495
1084
|
function isOlderRevision(revision, other) {
|
|
496
1085
|
if (revision === void 0 || other === void 0) return false;
|
|
497
1086
|
return Date.parse(revision) < Date.parse(other);
|
|
@@ -499,9 +1088,11 @@ function isOlderRevision(revision, other) {
|
|
|
499
1088
|
async function readStoredDocument(kv, route) {
|
|
500
1089
|
return toStoredDocument(await kv.get(documentKey(route)));
|
|
501
1090
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
1091
|
+
function sameStoredDocument(a, b) {
|
|
1092
|
+
return a != null && b != null && JSON.stringify(a) === JSON.stringify(b);
|
|
1093
|
+
}
|
|
1094
|
+
async function isSuperseded(kv, evaluated) {
|
|
1095
|
+
return !sameStoredDocument(evaluated, await readStoredDocument(kv, evaluated.document.route));
|
|
505
1096
|
}
|
|
506
1097
|
async function persistDocument(ctx, stored) {
|
|
507
1098
|
const route = stored.document.route;
|
|
@@ -593,6 +1184,23 @@ async function readFindings(kv, route) {
|
|
|
593
1184
|
}
|
|
594
1185
|
return stored.findings;
|
|
595
1186
|
}
|
|
1187
|
+
async function readAssessment(kv, route) {
|
|
1188
|
+
const stored = await kv.get(findingsKey(route));
|
|
1189
|
+
return stored?.assessment;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/assessment-display.ts
|
|
1193
|
+
function describeGate(gate) {
|
|
1194
|
+
const status = gate.status === "not_checked" ? "not checked" : gate.status;
|
|
1195
|
+
const reason = gate.status === "not_checked" ? `: ${gate.reason ?? "required input unavailable"}` : "";
|
|
1196
|
+
const metrics = Object.entries(gate.measurements ?? {}).map(([key, value]) => `${key.replaceAll("_", " ")}: ${value}`).join(", ");
|
|
1197
|
+
return `${gate.gate_id} ${status}${reason} (${gate.blocking ? "blocking" : "advisory"})${metrics ? ` \u2014 ${metrics}` : ""}`;
|
|
1198
|
+
}
|
|
1199
|
+
function summarizeGates(gates) {
|
|
1200
|
+
const counts = { passed: 0, failed: 0, not_checked: 0 };
|
|
1201
|
+
for (const gate of gates) counts[gate.status]++;
|
|
1202
|
+
return `${gates.length} configured checks: ${counts.passed} passed, ${counts.failed} failed, ${counts.not_checked} not checked`;
|
|
1203
|
+
}
|
|
596
1204
|
|
|
597
1205
|
// src/findings-view.ts
|
|
598
1206
|
function normalizeInteraction(input) {
|
|
@@ -632,6 +1240,8 @@ async function handleRefresh(options) {
|
|
|
632
1240
|
evaluator: options.evaluator,
|
|
633
1241
|
blockMappings: options.blockMappings,
|
|
634
1242
|
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1243
|
+
contentFieldsUnavailable: options.contentFieldsUnavailable,
|
|
1244
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
635
1245
|
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
636
1246
|
suppressionFilter: options.suppressionFilter
|
|
637
1247
|
});
|
|
@@ -667,21 +1277,38 @@ async function handleRefresh(options) {
|
|
|
667
1277
|
}
|
|
668
1278
|
};
|
|
669
1279
|
}
|
|
670
|
-
async function
|
|
671
|
-
const entries = await kv.list(
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
const
|
|
1280
|
+
async function readFindingsSnapshot(kv) {
|
|
1281
|
+
const entries = await kv.list("findings:");
|
|
1282
|
+
const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
|
|
1283
|
+
const incomplete = sitewideAssessment?.status === "error";
|
|
1284
|
+
const findings = [];
|
|
1285
|
+
const routes = /* @__PURE__ */ new Set();
|
|
1286
|
+
let failedAssessments = 0;
|
|
1287
|
+
let uncheckedGates = 0;
|
|
1288
|
+
let incompleteBodies = 0;
|
|
1289
|
+
let unknownBodies = 0;
|
|
1290
|
+
let missingAssessments = 0;
|
|
675
1291
|
for (const entry of entries) {
|
|
676
|
-
if (entry.value === null)
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
1292
|
+
if (entry.value === null) continue;
|
|
679
1293
|
const route = entry.key.replace(/^findings:/, "");
|
|
1294
|
+
if (route !== "*") {
|
|
1295
|
+
routes.add(route);
|
|
1296
|
+
if (entry.value.body_coverage === void 0) unknownBodies++;
|
|
1297
|
+
else if (entry.value.body_coverage !== "complete") incompleteBodies++;
|
|
1298
|
+
const assessment = entry.value.assessment;
|
|
1299
|
+
if (assessment === void 0) missingAssessments++;
|
|
1300
|
+
if (assessment?.status === "error") failedAssessments++;
|
|
1301
|
+
if (assessment?.status === "evaluated") {
|
|
1302
|
+
uncheckedGates += assessment.gates.filter((gate) => gate.status === "not_checked").length;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
680
1305
|
for (const finding of entry.value.findings) {
|
|
681
|
-
|
|
1306
|
+
if (!isCmsObservableFinding(finding, route === "*" || entry.value.body_coverage === "complete") || incomplete && isSiteGraphFinding(finding)) continue;
|
|
1307
|
+
findings.push({ ...finding, document_route: route });
|
|
1308
|
+
if (route === "*") routes.add(route);
|
|
682
1309
|
}
|
|
683
1310
|
}
|
|
684
|
-
return
|
|
1311
|
+
return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
|
|
685
1312
|
}
|
|
686
1313
|
function severityFirst(a, b) {
|
|
687
1314
|
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
@@ -699,7 +1326,7 @@ function uniqueRoutes(rows) {
|
|
|
699
1326
|
return [...routes].sort();
|
|
700
1327
|
}
|
|
701
1328
|
async function renderFindingsPage(kv, tuning) {
|
|
702
|
-
const findings = await
|
|
1329
|
+
const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
|
|
703
1330
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
704
1331
|
const warnings = findings.filter(
|
|
705
1332
|
(finding) => finding.severity === "warning"
|
|
@@ -710,8 +1337,13 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
710
1337
|
{ type: "header", text: "SEO findings" },
|
|
711
1338
|
{
|
|
712
1339
|
type: "context",
|
|
713
|
-
text: findings.length === 0 ? "No
|
|
1340
|
+
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.`
|
|
714
1341
|
},
|
|
1342
|
+
{ 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." },
|
|
1343
|
+
...incompleteBodies || unknownBodies ? [{ type: "context", text: `${incompleteBodies} document bodies were incomplete or unavailable; coverage is unknown for ${unknownBodies} older or failed evaluations. Inspect individual documents.` }] : [],
|
|
1344
|
+
...missingAssessments ? [{ type: "context", text: `No content preset assessment was recorded for ${missingAssessments} document(s); absence of findings is not an editorial quality assessment.` }] : [],
|
|
1345
|
+
...failedAssessments || uncheckedGates ? [{ type: "context", text: `${failedAssessments} document evaluations failed; ${uncheckedGates} preset checks were not checked. Inspect individual documents for details.` }] : [],
|
|
1346
|
+
...sitewideAssessment?.status === "error" ? [{ type: "context", text: `Sitewide checks unavailable: ${sitewideAssessment.reason}. ${sitewideAssessment.detail}` }] : [],
|
|
715
1347
|
{ type: "divider" },
|
|
716
1348
|
{
|
|
717
1349
|
type: "actions",
|
|
@@ -736,10 +1368,10 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
736
1368
|
},
|
|
737
1369
|
sorted.length === 0 ? {
|
|
738
1370
|
type: "context",
|
|
739
|
-
text: tuning.emptyFindingsText
|
|
1371
|
+
text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
|
|
740
1372
|
} : findingsTable(sorted)
|
|
741
1373
|
];
|
|
742
|
-
if (
|
|
1374
|
+
if (documentRoutes.length > 0) {
|
|
743
1375
|
blocks.push({
|
|
744
1376
|
type: "form",
|
|
745
1377
|
fields: [
|
|
@@ -747,7 +1379,7 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
747
1379
|
type: "select",
|
|
748
1380
|
action_id: "route_picker",
|
|
749
1381
|
label: "Document to inspect",
|
|
750
|
-
options:
|
|
1382
|
+
options: documentRoutes.map((route) => ({ label: route, value: route }))
|
|
751
1383
|
}
|
|
752
1384
|
],
|
|
753
1385
|
submit: { label: "View document SEO", action_id: "view_document" }
|
|
@@ -772,75 +1404,13 @@ function findingsTable(rows) {
|
|
|
772
1404
|
}))
|
|
773
1405
|
};
|
|
774
1406
|
}
|
|
775
|
-
async function renderScoreWidget(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
type: "context",
|
|
783
|
-
text: tuning.emptyScoreText
|
|
784
|
-
}
|
|
785
|
-
]
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
let score;
|
|
789
|
-
try {
|
|
790
|
-
score = await tuning.score(documents, kv);
|
|
791
|
-
} catch (error) {
|
|
792
|
-
log?.error("Aexeo score widget failed to initialize", { error });
|
|
793
|
-
return {
|
|
794
|
-
blocks: [
|
|
795
|
-
{ type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
|
|
796
|
-
{ type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
|
|
797
|
-
]
|
|
798
|
-
};
|
|
799
|
-
}
|
|
800
|
-
if (score === null) {
|
|
801
|
-
return {
|
|
802
|
-
blocks: [
|
|
803
|
-
{ type: "header", text: "SEO score" },
|
|
804
|
-
{
|
|
805
|
-
type: "context",
|
|
806
|
-
text: "The site score is unavailable in sandboxed mode. Findings are not affected."
|
|
807
|
-
}
|
|
808
|
-
]
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
const blocks = [
|
|
812
|
-
{
|
|
813
|
-
type: "stats",
|
|
814
|
-
items: [
|
|
815
|
-
{
|
|
816
|
-
label: "Citation",
|
|
817
|
-
value: score.citation_readiness_score?.toString() ?? "unknown"
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
label: tuning.truthLabel(score),
|
|
821
|
-
value: score.truth_consistency_score?.toString() ?? "unknown"
|
|
822
|
-
},
|
|
823
|
-
{
|
|
824
|
-
label: "Answers",
|
|
825
|
-
value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
|
|
826
|
-
}
|
|
827
|
-
]
|
|
828
|
-
}
|
|
829
|
-
];
|
|
830
|
-
if (score.blockers.length > 0) {
|
|
831
|
-
blocks.push({
|
|
832
|
-
type: "context",
|
|
833
|
-
text: topBlockersLine(score)
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
return { blocks };
|
|
837
|
-
}
|
|
838
|
-
function topBlockersLine(score) {
|
|
839
|
-
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
840
|
-
if (top.length === 0) {
|
|
841
|
-
return "No blockers identified.";
|
|
842
|
-
}
|
|
843
|
-
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
1407
|
+
async function renderScoreWidget(_kv, _tuning, _log) {
|
|
1408
|
+
return {
|
|
1409
|
+
blocks: [
|
|
1410
|
+
{ type: "header", text: "SEO score unavailable" },
|
|
1411
|
+
{ 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." }
|
|
1412
|
+
]
|
|
1413
|
+
};
|
|
844
1414
|
}
|
|
845
1415
|
async function renderDocumentPanel(kv, route) {
|
|
846
1416
|
if (route === void 0) {
|
|
@@ -854,7 +1424,11 @@ async function renderDocumentPanel(kv, route) {
|
|
|
854
1424
|
]
|
|
855
1425
|
};
|
|
856
1426
|
}
|
|
857
|
-
const
|
|
1427
|
+
const incomplete = (await readAssessment(kv, "*"))?.status === "error";
|
|
1428
|
+
const record = await kv.get(findingsKey(route));
|
|
1429
|
+
const findings = (record?.findings ?? []).filter((finding) => isCmsObservableFinding(finding, record?.body_coverage === "complete") && (!incomplete || !isSiteGraphFinding(finding)));
|
|
1430
|
+
const assessment = record?.assessment;
|
|
1431
|
+
const bodyNotice = record && assessment?.status !== "error" ? cmsBodyCoverageNotice(record.body_coverage) : void 0;
|
|
858
1432
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
859
1433
|
const warnings = findings.filter(
|
|
860
1434
|
(finding) => finding.severity === "warning"
|
|
@@ -865,8 +1439,14 @@ async function renderDocumentPanel(kv, route) {
|
|
|
865
1439
|
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
866
1440
|
{
|
|
867
1441
|
type: "context",
|
|
868
|
-
text: findings.length === 0 ? `No findings for ${route}
|
|
1442
|
+
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.`
|
|
869
1443
|
},
|
|
1444
|
+
{ 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." },
|
|
1445
|
+
...bodyNotice ? [{ type: "context", text: bodyNotice }] : [],
|
|
1446
|
+
...incomplete ? [{ type: "context", text: "Sitewide and cross-document checks are unavailable; refresh after resolving the CMS input error." }] : [],
|
|
1447
|
+
...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." }] : [],
|
|
1448
|
+
...assessment?.status === "error" ? [{ type: "context", text: `Evaluation failed: ${assessment.reason}. Findings are unavailable for this revision.` }] : [],
|
|
1449
|
+
...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) }))] : [],
|
|
870
1450
|
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
871
1451
|
]
|
|
872
1452
|
};
|
|
@@ -878,16 +1458,13 @@ function documentFindingsTable(findings) {
|
|
|
878
1458
|
{ key: "rule", label: "Rule", format: "code" },
|
|
879
1459
|
{ key: "severity", label: "Severity", format: "badge" },
|
|
880
1460
|
{ key: "message", label: "Message" },
|
|
881
|
-
{ key: "block", label: "
|
|
1461
|
+
{ key: "block", label: "Location", format: "code" }
|
|
882
1462
|
],
|
|
883
1463
|
rows: findings.map((finding) => ({
|
|
884
1464
|
rule: finding.rule_id,
|
|
885
1465
|
severity: finding.severity,
|
|
886
1466
|
message: finding.message,
|
|
887
|
-
|
|
888
|
-
// when rendering; surfacing the path:line locator here lets authors
|
|
889
|
-
// locate the failing block via the editor's ⌘-F.
|
|
890
|
-
block: `${finding.path}:${finding.line}`
|
|
1467
|
+
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}`
|
|
891
1468
|
}))
|
|
892
1469
|
};
|
|
893
1470
|
}
|
|
@@ -913,12 +1490,14 @@ __export(aexeo_emdash_bridge_bg_exports, {
|
|
|
913
1490
|
evaluateContentCollection: () => evaluateContentCollection,
|
|
914
1491
|
evaluateDocuments: () => evaluateDocuments,
|
|
915
1492
|
generateFactsPrompt: () => generateFactsPrompt,
|
|
1493
|
+
projectContentInput: () => projectContentInput,
|
|
1494
|
+
projectLinkDestinations: () => projectLinkDestinations,
|
|
916
1495
|
scoreIntelligence: () => scoreIntelligence,
|
|
917
1496
|
validateFactsManifest: () => validateFactsManifest
|
|
918
1497
|
});
|
|
919
|
-
function evaluateContent(document_json, preset_name, sources_json, evidence_json) {
|
|
920
|
-
let
|
|
921
|
-
let
|
|
1498
|
+
function evaluateContent(document_json, preset_name, sources_json, evidence_json, extensions_json, link_observations_json) {
|
|
1499
|
+
let deferred8_0;
|
|
1500
|
+
let deferred8_1;
|
|
922
1501
|
try {
|
|
923
1502
|
const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
924
1503
|
const len0 = WASM_VECTOR_LEN;
|
|
@@ -928,19 +1507,23 @@ function evaluateContent(document_json, preset_name, sources_json, evidence_json
|
|
|
928
1507
|
var len2 = WASM_VECTOR_LEN;
|
|
929
1508
|
var ptr3 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
930
1509
|
var len3 = WASM_VECTOR_LEN;
|
|
931
|
-
|
|
932
|
-
var
|
|
933
|
-
var
|
|
1510
|
+
var ptr4 = isLikeNone(extensions_json) ? 0 : passStringToWasm0(extensions_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1511
|
+
var len4 = WASM_VECTOR_LEN;
|
|
1512
|
+
var ptr5 = isLikeNone(link_observations_json) ? 0 : passStringToWasm0(link_observations_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1513
|
+
var len5 = WASM_VECTOR_LEN;
|
|
1514
|
+
const ret = wasm.evaluateContent(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5);
|
|
1515
|
+
var ptr7 = ret[0];
|
|
1516
|
+
var len7 = ret[1];
|
|
934
1517
|
if (ret[3]) {
|
|
935
|
-
|
|
936
|
-
|
|
1518
|
+
ptr7 = 0;
|
|
1519
|
+
len7 = 0;
|
|
937
1520
|
throw takeFromExternrefTable0(ret[2]);
|
|
938
1521
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
return getStringFromWasm0(
|
|
1522
|
+
deferred8_0 = ptr7;
|
|
1523
|
+
deferred8_1 = len7;
|
|
1524
|
+
return getStringFromWasm0(ptr7, len7);
|
|
942
1525
|
} finally {
|
|
943
|
-
wasm.__wbindgen_free(
|
|
1526
|
+
wasm.__wbindgen_free(deferred8_0, deferred8_1, 1);
|
|
944
1527
|
}
|
|
945
1528
|
}
|
|
946
1529
|
function evaluateContentCollection(documents_json, preset_name, sources_json) {
|
|
@@ -1012,6 +1595,50 @@ function generateFactsPrompt(documents_json) {
|
|
|
1012
1595
|
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
1013
1596
|
}
|
|
1014
1597
|
}
|
|
1598
|
+
function projectContentInput(document_json, evidence_json) {
|
|
1599
|
+
let deferred4_0;
|
|
1600
|
+
let deferred4_1;
|
|
1601
|
+
try {
|
|
1602
|
+
const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1603
|
+
const len0 = WASM_VECTOR_LEN;
|
|
1604
|
+
var ptr1 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1605
|
+
var len1 = WASM_VECTOR_LEN;
|
|
1606
|
+
const ret = wasm.projectContentInput(ptr0, len0, ptr1, len1);
|
|
1607
|
+
var ptr3 = ret[0];
|
|
1608
|
+
var len3 = ret[1];
|
|
1609
|
+
if (ret[3]) {
|
|
1610
|
+
ptr3 = 0;
|
|
1611
|
+
len3 = 0;
|
|
1612
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
1613
|
+
}
|
|
1614
|
+
deferred4_0 = ptr3;
|
|
1615
|
+
deferred4_1 = len3;
|
|
1616
|
+
return getStringFromWasm0(ptr3, len3);
|
|
1617
|
+
} finally {
|
|
1618
|
+
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function projectLinkDestinations(document_json) {
|
|
1622
|
+
let deferred3_0;
|
|
1623
|
+
let deferred3_1;
|
|
1624
|
+
try {
|
|
1625
|
+
const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1626
|
+
const len0 = WASM_VECTOR_LEN;
|
|
1627
|
+
const ret = wasm.projectLinkDestinations(ptr0, len0);
|
|
1628
|
+
var ptr2 = ret[0];
|
|
1629
|
+
var len2 = ret[1];
|
|
1630
|
+
if (ret[3]) {
|
|
1631
|
+
ptr2 = 0;
|
|
1632
|
+
len2 = 0;
|
|
1633
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
1634
|
+
}
|
|
1635
|
+
deferred3_0 = ptr2;
|
|
1636
|
+
deferred3_1 = len2;
|
|
1637
|
+
return getStringFromWasm0(ptr2, len2);
|
|
1638
|
+
} finally {
|
|
1639
|
+
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1015
1642
|
function scoreIntelligence(documents_json, manifest_json) {
|
|
1016
1643
|
let deferred4_0;
|
|
1017
1644
|
let deferred4_1;
|
|
@@ -1237,9 +1864,17 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
|
|
|
1237
1864
|
await ensureInitialized();
|
|
1238
1865
|
return validateFactsManifest(manifestJson, documentsJson);
|
|
1239
1866
|
}
|
|
1240
|
-
async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson) {
|
|
1867
|
+
async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson, extensionsJson, linkObservationsJson) {
|
|
1241
1868
|
await ensureInitialized();
|
|
1242
|
-
return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson);
|
|
1869
|
+
return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson, extensionsJson, linkObservationsJson);
|
|
1870
|
+
}
|
|
1871
|
+
async function projectLinkDestinations2(documentJson) {
|
|
1872
|
+
await ensureInitialized();
|
|
1873
|
+
return projectLinkDestinations(documentJson);
|
|
1874
|
+
}
|
|
1875
|
+
async function projectContentInput2(documentJson, evidenceJson) {
|
|
1876
|
+
await ensureInitialized();
|
|
1877
|
+
return projectContentInput(documentJson, evidenceJson);
|
|
1243
1878
|
}
|
|
1244
1879
|
|
|
1245
1880
|
// src/data-route.ts
|
|
@@ -1251,6 +1886,7 @@ async function handleDataRoute(ctx, options) {
|
|
|
1251
1886
|
evaluator: options.evaluator,
|
|
1252
1887
|
blockMappings: options.blockMappings,
|
|
1253
1888
|
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1889
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
1254
1890
|
...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
|
|
1255
1891
|
});
|
|
1256
1892
|
await persistManifestStateFindings(ctx);
|
|
@@ -1269,20 +1905,22 @@ async function buildFindingsPayload(ctx) {
|
|
|
1269
1905
|
);
|
|
1270
1906
|
const findings = [];
|
|
1271
1907
|
const sitewide = [];
|
|
1908
|
+
const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
|
|
1272
1909
|
const perRoute = /* @__PURE__ */ new Map();
|
|
1273
1910
|
for (const entry of entries) {
|
|
1274
1911
|
if (entry.value === null) continue;
|
|
1275
1912
|
const route = entry.key.replace(/^findings:/, "");
|
|
1276
1913
|
if (route === "*") {
|
|
1277
|
-
sitewide.push(...entry.value.findings);
|
|
1914
|
+
sitewide.push(...entry.value.findings.filter((finding) => isCmsObservableFinding(finding) && !(sitewideAssessment?.status === "error" && isSiteGraphFinding(finding))));
|
|
1278
1915
|
continue;
|
|
1279
1916
|
}
|
|
1280
1917
|
let bucket = perRoute.get(route);
|
|
1281
1918
|
if (bucket === void 0) {
|
|
1282
|
-
bucket = { findings: [], errors: 0, warnings: 0 };
|
|
1919
|
+
bucket = { findings: [], errors: 0, warnings: 0, ...entry.value.body_coverage === void 0 ? {} : { body_coverage: entry.value.body_coverage }, ...entry.value.assessment === void 0 ? {} : { assessment: entry.value.assessment } };
|
|
1283
1920
|
perRoute.set(route, bucket);
|
|
1284
1921
|
}
|
|
1285
1922
|
for (const finding of entry.value.findings) {
|
|
1923
|
+
if (!isCmsObservableFinding(finding, entry.value.body_coverage === "complete") || sitewideAssessment?.status === "error" && isSiteGraphFinding(finding)) continue;
|
|
1286
1924
|
bucket.findings.push(finding);
|
|
1287
1925
|
if (finding.severity === "error") bucket.errors += 1;
|
|
1288
1926
|
else if (finding.severity === "warning") bucket.warnings += 1;
|
|
@@ -1303,7 +1941,9 @@ async function buildFindingsPayload(ctx) {
|
|
|
1303
1941
|
publishedUrl: stored2?.meta.publicUrl ?? "",
|
|
1304
1942
|
findingCount: bucket.findings.length,
|
|
1305
1943
|
errorCount: bucket.errors,
|
|
1306
|
-
warningCount: bucket.warnings
|
|
1944
|
+
warningCount: bucket.warnings,
|
|
1945
|
+
...bucket.assessment === void 0 ? {} : { assessment: bucket.assessment },
|
|
1946
|
+
...bucket.body_coverage === void 0 ? {} : { body_coverage: bucket.body_coverage }
|
|
1307
1947
|
});
|
|
1308
1948
|
}
|
|
1309
1949
|
routes.sort((a, b) => b.errorCount - a.errorCount || a.route.localeCompare(b.route));
|
|
@@ -1325,7 +1965,8 @@ async function buildFindingsPayload(ctx) {
|
|
|
1325
1965
|
documentsIndexed: documents.size
|
|
1326
1966
|
},
|
|
1327
1967
|
layerBreakdown,
|
|
1328
|
-
sitewideFindings: sitewide
|
|
1968
|
+
sitewideFindings: sitewide,
|
|
1969
|
+
...sitewideAssessment === void 0 ? {} : { sitewideAssessment }
|
|
1329
1970
|
};
|
|
1330
1971
|
}
|
|
1331
1972
|
function computeLayerBreakdown(findings) {
|
|
@@ -2007,15 +2648,34 @@ function globToMatcher(pattern) {
|
|
|
2007
2648
|
}
|
|
2008
2649
|
|
|
2009
2650
|
// src/content.ts
|
|
2010
|
-
async function
|
|
2651
|
+
async function projectContentInput3(document, evidenceSources) {
|
|
2652
|
+
return JSON.parse(await projectContentInput2(JSON.stringify(document), evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources)));
|
|
2653
|
+
}
|
|
2654
|
+
async function projectLinkDestinations3(document) {
|
|
2655
|
+
return JSON.parse(await projectLinkDestinations2(JSON.stringify(document)));
|
|
2656
|
+
}
|
|
2657
|
+
async function evaluateContent3(document, preset, sources = {}, evidenceSources, onTelemetry, externalResults, linkObservations) {
|
|
2011
2658
|
const started = onTelemetry ? performance.now() : 0;
|
|
2012
2659
|
const documentJson = JSON.stringify(document);
|
|
2013
2660
|
const sourcesJson = JSON.stringify(sources);
|
|
2014
|
-
const result = evidenceSources === void 0 ? await evaluateContent2(documentJson, preset, sourcesJson) : await evaluateContent2(documentJson, preset, sourcesJson, JSON.stringify(evidenceSources));
|
|
2661
|
+
const result = linkObservations !== void 0 ? await evaluateContent2(documentJson, preset, sourcesJson, evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources), externalResults === void 0 ? void 0 : JSON.stringify(externalResults), JSON.stringify(linkObservations)) : externalResults !== void 0 ? await evaluateContent2(documentJson, preset, sourcesJson, evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources), JSON.stringify(externalResults)) : evidenceSources === void 0 ? await evaluateContent2(documentJson, preset, sourcesJson) : await evaluateContent2(documentJson, preset, sourcesJson, JSON.stringify(evidenceSources));
|
|
2015
2662
|
const report = JSON.parse(result);
|
|
2016
2663
|
if (onTelemetry) {
|
|
2017
2664
|
const event = { evaluation_ms: performance.now() - started, source_locations_mapped: report.source_locations?.length ?? 0 };
|
|
2665
|
+
if (report.source_mapping) {
|
|
2666
|
+
event.source_locations_eligible = report.source_mapping.eligible;
|
|
2667
|
+
event.source_locations_unmapped = report.source_mapping.unmapped;
|
|
2668
|
+
}
|
|
2018
2669
|
for (const gate of report.gates) {
|
|
2670
|
+
if (gate.required_capability === "link_reachability" && gate.measurements) {
|
|
2671
|
+
const { checked_destinations, not_found_destinations, unchecked_destinations } = gate.measurements;
|
|
2672
|
+
if (checked_destinations !== void 0 && not_found_destinations !== void 0 && unchecked_destinations !== void 0) {
|
|
2673
|
+
event.link_reachability ??= { checked: 0, not_found: 0, unchecked: 0 };
|
|
2674
|
+
event.link_reachability.checked += checked_destinations;
|
|
2675
|
+
event.link_reachability.not_found += not_found_destinations;
|
|
2676
|
+
event.link_reachability.unchecked += unchecked_destinations;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2019
2679
|
const checked = gate.measurements?.checked_destinations;
|
|
2020
2680
|
const invalid = gate.measurements?.invalid_destinations;
|
|
2021
2681
|
if (checked !== void 0 && invalid !== void 0) {
|
|
@@ -2030,12 +2690,13 @@ async function evaluateContent3(document, preset, sources = {}, evidenceSources,
|
|
|
2030
2690
|
}
|
|
2031
2691
|
|
|
2032
2692
|
// src/content-evaluator.ts
|
|
2033
|
-
function withContentPreset(base, preset, evaluate) {
|
|
2693
|
+
function withContentPreset(base, preset, evaluate, provider, project, onProviderTelemetry, linkProvider, projectLinks, onLinkProviderTelemetry) {
|
|
2034
2694
|
if (!preset) return base;
|
|
2035
2695
|
return async (documents, context) => {
|
|
2036
2696
|
const result = await base(documents, context);
|
|
2037
2697
|
if (!result.ok) return result;
|
|
2038
2698
|
const findings = [...result.findings];
|
|
2699
|
+
const assessments = { ...result.assessments };
|
|
2039
2700
|
try {
|
|
2040
2701
|
for (const [index, document] of documents.entries()) {
|
|
2041
2702
|
const collection = context?.collections[index];
|
|
@@ -2046,16 +2707,85 @@ function withContentPreset(base, preset, evaluate) {
|
|
|
2046
2707
|
const selected = collection !== void 0 && preset.byCollection !== void 0 && Object.hasOwn(preset.byCollection, collection) ? preset.byCollection[collection] : void 0;
|
|
2047
2708
|
const name = selected ?? preset.name;
|
|
2048
2709
|
if (!name) throw new Error(`empty preset name for collection ${collection}`);
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2710
|
+
let report = preset.evidenceSources === void 0 ? await evaluate(document, name, preset.sources) : await evaluate(document, name, preset.sources, preset.evidenceSources);
|
|
2711
|
+
let externalResults;
|
|
2712
|
+
let externalFailure = false;
|
|
2713
|
+
if (provider !== void 0 && report.gates.some((gate) => gate.required_capability === "extension")) {
|
|
2714
|
+
const started = onProviderTelemetry ? performance.now() : 0;
|
|
2715
|
+
let status = "error";
|
|
2716
|
+
try {
|
|
2717
|
+
if (!project) throw new Error("content gate projection is unavailable");
|
|
2718
|
+
externalResults = await provider({ input: await project(document, preset.evidenceSources), preset: name, collection });
|
|
2719
|
+
if (!Array.isArray(externalResults)) throw new Error("external gate provider must return an array");
|
|
2720
|
+
report = await evaluate(document, name, preset.sources, preset.evidenceSources, void 0, externalResults);
|
|
2721
|
+
status = "success";
|
|
2722
|
+
} catch {
|
|
2723
|
+
externalResults = void 0;
|
|
2724
|
+
externalFailure = true;
|
|
2725
|
+
} finally {
|
|
2726
|
+
if (onProviderTelemetry) {
|
|
2727
|
+
try {
|
|
2728
|
+
onProviderTelemetry({ duration_ms: performance.now() - started, status });
|
|
2729
|
+
} catch {
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (linkProvider && report.gates.some((gate) => gate.required_capability === "link_reachability")) {
|
|
2735
|
+
const started = onLinkProviderTelemetry ? performance.now() : 0;
|
|
2736
|
+
let status = "error";
|
|
2737
|
+
try {
|
|
2738
|
+
const projection = projectLinks ? await projectLinks(document) : null;
|
|
2739
|
+
if (projection === null) {
|
|
2740
|
+
status = "unavailable";
|
|
2741
|
+
} else {
|
|
2742
|
+
const observations = await linkProvider({ destinations: [...projection.destinations], preset: name, collection });
|
|
2743
|
+
const valid = /* @__PURE__ */ new Set(["reachable", "not_found", "not_checked", "not_applicable"]);
|
|
2744
|
+
if (!Array.isArray(observations) || observations.length !== projection.destinations.length || observations.some((value) => !valid.has(value))) throw new Error("invalid link observations");
|
|
2745
|
+
report = await evaluate(
|
|
2746
|
+
document,
|
|
2747
|
+
name,
|
|
2748
|
+
preset.sources,
|
|
2749
|
+
preset.evidenceSources,
|
|
2750
|
+
void 0,
|
|
2751
|
+
externalResults,
|
|
2752
|
+
{ ...projection, observations }
|
|
2753
|
+
);
|
|
2754
|
+
status = "success";
|
|
2755
|
+
}
|
|
2756
|
+
} catch {
|
|
2757
|
+
} finally {
|
|
2758
|
+
if (onLinkProviderTelemetry) {
|
|
2759
|
+
try {
|
|
2760
|
+
onLinkProviderTelemetry({ duration_ms: performance.now() - started, status });
|
|
2761
|
+
} catch {
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
if (externalFailure) {
|
|
2767
|
+
report = { ...report, gates: report.gates.map((gate) => gate.required_capability === "extension" && gate.status === "not_checked" ? { ...gate, reason: "external gate provider failed" } : gate) };
|
|
2768
|
+
}
|
|
2769
|
+
assessments[document.route] = {
|
|
2770
|
+
status: "evaluated",
|
|
2771
|
+
preset: name,
|
|
2772
|
+
...report.source_mapping === void 0 ? {} : { source_mapping: report.source_mapping },
|
|
2773
|
+
gates: report.gates.map(({ gate_id, status, blocking, scope, required_capability, reason, measurements }) => ({ gate_id, status, blocking, scope, required_capability, reason, ...measurements === void 0 ? {} : { measurements } }))
|
|
2055
2774
|
};
|
|
2056
|
-
|
|
2775
|
+
const locations = new Map((report.source_locations ?? []).map((location) => [
|
|
2776
|
+
`${location.gate_id}:${location.finding_index}`,
|
|
2777
|
+
location
|
|
2778
|
+
]));
|
|
2779
|
+
for (const gate of report.gates) findings.push(...gate.findings.map((finding, index2) => {
|
|
2780
|
+
const location = locations.get(`${gate.gate_id}:${index2}`);
|
|
2781
|
+
return {
|
|
2782
|
+
...finding,
|
|
2783
|
+
aexeo_origin: "content_preset",
|
|
2784
|
+
...location === void 0 ? {} : { aexeo_source_location: { block_index: location.block_index, span_index: location.span_index, utf16_offset: location.utf16_offset } }
|
|
2785
|
+
};
|
|
2786
|
+
}));
|
|
2057
2787
|
}
|
|
2058
|
-
return { ok: true, findings };
|
|
2788
|
+
return { ok: true, findings, assessments };
|
|
2059
2789
|
} catch (error) {
|
|
2060
2790
|
return { ok: false, reason: "content_preset_error", detail: error instanceof Error ? error.message : String(error) };
|
|
2061
2791
|
}
|
|
@@ -2119,14 +2849,14 @@ var inProcessEvaluator = async (documents) => {
|
|
|
2119
2849
|
return { ok: false, reason: "wasm_error", detail };
|
|
2120
2850
|
}
|
|
2121
2851
|
};
|
|
2122
|
-
function createPlugin(options = {}) {
|
|
2852
|
+
function createPlugin(options = {}, runtime = {}) {
|
|
2123
2853
|
const collections = resolveCollections(
|
|
2124
2854
|
options.collections,
|
|
2125
2855
|
options.includeCollections,
|
|
2126
2856
|
options.excludeCollections
|
|
2127
2857
|
);
|
|
2128
2858
|
const suppressionFilter = compileSuppressions(options.suppressions);
|
|
2129
|
-
const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3);
|
|
2859
|
+
const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3, runtime.externalGates, projectContentInput3, runtime.onExternalGateTelemetry, runtime.linkObservations, projectLinkDestinations3, runtime.onLinkProviderTelemetry);
|
|
2130
2860
|
return definePlugin({
|
|
2131
2861
|
id: "aexeo-emdash",
|
|
2132
2862
|
version: PACKAGE_VERSION,
|
|
@@ -2145,7 +2875,8 @@ function createPlugin(options = {}) {
|
|
|
2145
2875
|
evaluator,
|
|
2146
2876
|
suppressionFilter,
|
|
2147
2877
|
options.blockMappings,
|
|
2148
|
-
options.contentFieldsByCollection?.[event.content.type]
|
|
2878
|
+
options.contentFieldsByCollection?.[event.content.type],
|
|
2879
|
+
options.descriptionFieldsByCollection?.[event.content.type]
|
|
2149
2880
|
),
|
|
2150
2881
|
"content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
|
|
2151
2882
|
},
|
|
@@ -2161,7 +2892,7 @@ function createPlugin(options = {}) {
|
|
|
2161
2892
|
// refresh_findings action, which handleAdminRoute gates itself.
|
|
2162
2893
|
admin: {
|
|
2163
2894
|
permission: "plugins:read",
|
|
2164
|
-
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection)
|
|
2895
|
+
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection, options.descriptionFieldsByCollection)
|
|
2165
2896
|
},
|
|
2166
2897
|
// JSON data endpoint for the React adminEntry. Two routes:
|
|
2167
2898
|
// POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
|
|
@@ -2178,7 +2909,8 @@ function createPlugin(options = {}) {
|
|
|
2178
2909
|
refresh: false,
|
|
2179
2910
|
suppressionFilter,
|
|
2180
2911
|
blockMappings: options.blockMappings,
|
|
2181
|
-
contentFieldsByCollection: options.contentFieldsByCollection
|
|
2912
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
2913
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection
|
|
2182
2914
|
})).payload
|
|
2183
2915
|
},
|
|
2184
2916
|
refresh: {
|
|
@@ -2189,7 +2921,8 @@ function createPlugin(options = {}) {
|
|
|
2189
2921
|
refresh: true,
|
|
2190
2922
|
suppressionFilter,
|
|
2191
2923
|
blockMappings: options.blockMappings,
|
|
2192
|
-
contentFieldsByCollection: options.contentFieldsByCollection
|
|
2924
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
2925
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection
|
|
2193
2926
|
})
|
|
2194
2927
|
},
|
|
2195
2928
|
// /facts route serves the truth-manifest authoring UI. It
|
|
@@ -2247,7 +2980,7 @@ var adminViewTuning = {
|
|
|
2247
2980
|
score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
|
|
2248
2981
|
truthLabel: formatTruthLabel
|
|
2249
2982
|
};
|
|
2250
|
-
async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection) {
|
|
2983
|
+
async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection, descriptionFieldsByCollection) {
|
|
2251
2984
|
const body = normalizeInteraction(ctx.input);
|
|
2252
2985
|
if (body.type === "page_load") {
|
|
2253
2986
|
return handlePageLoad(ctx, body.page);
|
|
@@ -2278,7 +3011,8 @@ async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator,
|
|
|
2278
3011
|
suppressionFilter,
|
|
2279
3012
|
tuning: adminViewTuning,
|
|
2280
3013
|
blockMappings,
|
|
2281
|
-
contentFieldsByCollection
|
|
3014
|
+
contentFieldsByCollection,
|
|
3015
|
+
descriptionFieldsByCollection
|
|
2282
3016
|
});
|
|
2283
3017
|
}
|
|
2284
3018
|
if (body.action_id.startsWith("filter:")) {
|