@aeptus/aexeo-emdash 0.8.19 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -2
- package/INSTALL.md +286 -4
- package/README.md +20 -5
- package/dist/adapter.d.ts +62 -4
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin/Findings.d.ts.map +1 -1
- package/dist/admin.d.ts.map +1 -1
- package/dist/admin.js +73 -6
- package/dist/assessment-display.d.ts +10 -0
- package/dist/assessment-display.d.ts.map +1 -0
- package/dist/cms-coverage.d.ts +10 -0
- package/dist/cms-coverage.d.ts.map +1 -0
- package/dist/configured.d.ts +16 -1
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +1341 -172
- package/dist/content-evaluator.d.ts +34 -0
- package/dist/content-evaluator.d.ts.map +1 -0
- package/dist/content.d.ts +98 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +445 -0
- package/dist/data-route.d.ts +12 -1
- package/dist/data-route.d.ts.map +1 -1
- package/dist/evaluator-sandbox.d.ts +1 -1
- package/dist/evaluator-sandbox.d.ts.map +1 -1
- package/dist/evaluator.d.ts +1 -1
- package/dist/evaluator.d.ts.map +1 -1
- package/dist/findings-view.d.ts +11 -5
- package/dist/findings-view.d.ts.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +163 -4
- package/dist/inventory.d.ts +59 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/plugin.d.ts +59 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/presence-check.d.ts +1 -0
- package/dist/presence-check.d.ts.map +1 -1
- package/dist/sandbox-entry.d.ts +2 -1
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +1077 -190
- package/dist/sandbox.d.ts +2 -2
- package/dist/sidecar-url.d.ts.map +1 -1
- package/dist/sidecar.d.ts +9 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/types.d.ts +26 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/wasm-init.d.ts +8 -0
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +28 -8
- package/wasm/aexeo_emdash_bridge.d.ts +22 -0
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +15 -0
- package/wasm/aexeo_emdash_bridge_bg.js +130 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +4 -0
package/dist/configured.js
CHANGED
|
@@ -4,14 +4,39 @@ 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) {
|
|
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, {
|
|
12
34
|
route,
|
|
13
35
|
publicUrl,
|
|
14
|
-
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
36
|
+
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
37
|
+
...blockMappings === void 0 ? {} : { blockMappings },
|
|
38
|
+
...contentFields === void 0 ? {} : { contentFields },
|
|
39
|
+
...descriptionFields === void 0 ? {} : { descriptionFields }
|
|
15
40
|
});
|
|
16
41
|
const translationSet = await contentApi.getTranslations(
|
|
17
42
|
content.type,
|
|
@@ -36,14 +61,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
|
36
61
|
return adapted;
|
|
37
62
|
}
|
|
38
63
|
function adaptContentItem(content, options = {}) {
|
|
64
|
+
return adaptContentItemInternal(content, options, true);
|
|
65
|
+
}
|
|
66
|
+
function adaptContentItemInternal(content, options, includeSourcePaths) {
|
|
39
67
|
const route = options.route ?? deriveRoute(content);
|
|
40
68
|
const title = stringOrEmpty(
|
|
41
69
|
content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
|
|
42
70
|
);
|
|
43
|
-
const document = { route, title };
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
|
|
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;
|
|
47
90
|
}
|
|
48
91
|
const canonical = content.seo?.canonical;
|
|
49
92
|
if (typeof canonical === "string" && canonical.length > 0) {
|
|
@@ -53,8 +96,13 @@ function adaptContentItem(content, options = {}) {
|
|
|
53
96
|
if (typeof locale === "string" && locale.length > 0) {
|
|
54
97
|
document.lang = locale;
|
|
55
98
|
}
|
|
56
|
-
const body = extractPortableText(content.data);
|
|
57
|
-
if (body !== null)
|
|
99
|
+
const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
|
|
100
|
+
if (body !== null) {
|
|
101
|
+
document.body = body.blocks;
|
|
102
|
+
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
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);
|
|
58
106
|
return {
|
|
59
107
|
document,
|
|
60
108
|
meta: {
|
|
@@ -63,7 +111,10 @@ function adaptContentItem(content, options = {}) {
|
|
|
63
111
|
slug: content.slug,
|
|
64
112
|
status: content.status,
|
|
65
113
|
title,
|
|
66
|
-
publicUrl: options.publicUrl ?? null
|
|
114
|
+
publicUrl: options.publicUrl ?? null,
|
|
115
|
+
...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
|
|
116
|
+
...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
|
|
117
|
+
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
67
118
|
}
|
|
68
119
|
};
|
|
69
120
|
}
|
|
@@ -82,38 +133,390 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
82
133
|
function stringOrEmpty(value) {
|
|
83
134
|
return typeof value === "string" ? value : "";
|
|
84
135
|
}
|
|
85
|
-
function
|
|
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
|
+
}
|
|
230
|
+
function extractSelectedContent(data, fields, mappings) {
|
|
231
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
232
|
+
const all = ["text", "headings", "paragraphs", "links"];
|
|
233
|
+
let capabilities = all;
|
|
234
|
+
const blocks = [];
|
|
235
|
+
let observed = false;
|
|
236
|
+
for (const field of fields) {
|
|
237
|
+
const value = data[field];
|
|
238
|
+
if (value === void 0) {
|
|
239
|
+
capabilities = [];
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
observed = true;
|
|
243
|
+
if (value === null) continue;
|
|
244
|
+
const part = extractFirstPortableText({ content: value }, mappings);
|
|
245
|
+
if (part === null) {
|
|
246
|
+
capabilities = [];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
blocks.push(...part.blocks);
|
|
250
|
+
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
251
|
+
}
|
|
252
|
+
if (!observed) return null;
|
|
253
|
+
return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
|
|
254
|
+
}
|
|
255
|
+
function extractPortableText(data, mappings) {
|
|
256
|
+
const result = extractFirstPortableText(data, mappings);
|
|
257
|
+
if (result === null) return null;
|
|
258
|
+
const candidates = Object.entries(data).filter(
|
|
259
|
+
([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
260
|
+
);
|
|
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;
|
|
267
|
+
}
|
|
268
|
+
function extractFirstPortableText(data, mappings) {
|
|
269
|
+
let foundEmptyBody = false;
|
|
86
270
|
for (const slug of ["body", "content"]) {
|
|
87
271
|
const value = data[slug];
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
272
|
+
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
273
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
274
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
275
|
+
if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
|
|
90
276
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
91
|
-
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
277
|
+
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
92
278
|
}
|
|
93
279
|
}
|
|
94
280
|
const blocks = data["blocks"];
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
281
|
+
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
282
|
+
if (isPortableTextArray(blocks)) return { blocks };
|
|
283
|
+
if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
|
|
284
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
|
|
97
285
|
for (const [field, value] of Object.entries(data)) {
|
|
98
286
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
99
|
-
if (isPortableTextArray(value)) return value;
|
|
100
|
-
if (
|
|
287
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
288
|
+
if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
|
|
289
|
+
if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
|
|
101
290
|
}
|
|
102
|
-
return null;
|
|
291
|
+
return foundEmptyBody ? { blocks: [] } : null;
|
|
103
292
|
}
|
|
104
293
|
function isPortableTextArray(value) {
|
|
105
294
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
106
|
-
(item) =>
|
|
107
|
-
(child) => isRecord(child) && typeof child["text"] === "string"
|
|
108
|
-
)
|
|
295
|
+
(item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
|
|
109
296
|
);
|
|
110
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
|
+
}
|
|
111
448
|
function isEmdashBlocksArray(value, field) {
|
|
112
449
|
if (!Array.isArray(value) || value.length === 0) return false;
|
|
113
450
|
return value.every(
|
|
114
|
-
(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")
|
|
115
452
|
);
|
|
116
453
|
}
|
|
454
|
+
function mapCustomBlocks(blocks, mappings, depth = 0) {
|
|
455
|
+
if (depth > 8) return { blocks: [], capabilities: [] };
|
|
456
|
+
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
457
|
+
const result = [];
|
|
458
|
+
let capabilities = [...BODY_CAPABILITIES];
|
|
459
|
+
const mapFields = (record, fields, depth2) => {
|
|
460
|
+
if (depth2 > 8 || fields.length === 0) return false;
|
|
461
|
+
for (const entry of fields) {
|
|
462
|
+
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
463
|
+
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
464
|
+
if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
|
|
465
|
+
if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
|
|
466
|
+
const value = record[entry.field];
|
|
467
|
+
if ((value === void 0 || value === null || value === "") && entry.optional) {
|
|
468
|
+
if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (entry.kind === "richText") {
|
|
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
|
+
}
|
|
482
|
+
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
483
|
+
if (typeof value !== "string") return false;
|
|
484
|
+
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
485
|
+
} else if (entry.kind === "link") {
|
|
486
|
+
const href = record[entry.hrefField];
|
|
487
|
+
if (typeof value !== "string" || typeof href !== "string" || !href) return false;
|
|
488
|
+
result.push({
|
|
489
|
+
_type: "block",
|
|
490
|
+
children: [{ _type: "span", text: value, marks: ["link"] }],
|
|
491
|
+
markDefs: [{ _key: "link", _type: "link", href }]
|
|
492
|
+
});
|
|
493
|
+
} else if (entry.kind === "repeat") {
|
|
494
|
+
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
495
|
+
for (const item of value) {
|
|
496
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
|
|
497
|
+
}
|
|
498
|
+
} else {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
};
|
|
504
|
+
for (const block of blocks) {
|
|
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);
|
|
519
|
+
}
|
|
117
520
|
function blocksToPortableText(blocks) {
|
|
118
521
|
const paragraphs = [];
|
|
119
522
|
for (const block of blocks) {
|
|
@@ -126,20 +529,49 @@ function blocksToPortableText(blocks) {
|
|
|
126
529
|
}
|
|
127
530
|
return paragraphs;
|
|
128
531
|
}
|
|
532
|
+
var FALLBACK_MAX_DEPTH = 64;
|
|
533
|
+
var FALLBACK_MAX_NODES = 5e4;
|
|
534
|
+
var FALLBACK_MAX_CHARS = 1048576;
|
|
129
535
|
function collectText(value, key, out) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
143
575
|
}
|
|
144
576
|
var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
|
|
145
577
|
"_key",
|
|
@@ -197,35 +629,210 @@ function isRecord(value) {
|
|
|
197
629
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
198
630
|
}
|
|
199
631
|
|
|
632
|
+
// src/version.ts
|
|
633
|
+
var PACKAGE_VERSION = "0.9.1";
|
|
634
|
+
|
|
200
635
|
// src/plugin.ts
|
|
201
636
|
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
637
|
+
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
638
|
+
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
639
|
+
var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
|
|
202
640
|
var PLUGIN_SETTINGS_SCHEMA = {
|
|
641
|
+
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
642
|
+
type: "string",
|
|
643
|
+
label: "Content fields by collection (JSON)",
|
|
644
|
+
description: "Ordered rendered body fields per collection.",
|
|
645
|
+
multiline: true
|
|
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
|
+
},
|
|
653
|
+
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
654
|
+
type: "string",
|
|
655
|
+
label: "Custom block mappings (JSON)",
|
|
656
|
+
description: "Ordered mappings for the rendered fields of each custom block type.",
|
|
657
|
+
multiline: true
|
|
658
|
+
},
|
|
203
659
|
[SETTINGS_TOKEN_KEY]: {
|
|
204
660
|
type: "secret",
|
|
205
661
|
label: "EVAL_TOKEN",
|
|
206
662
|
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
207
663
|
}
|
|
208
664
|
};
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
665
|
+
function recordContentCoverage(ctx, documents) {
|
|
666
|
+
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
667
|
+
for (const document of documents) {
|
|
668
|
+
counts[cmsBodyCoverage(document)]++;
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
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 });
|
|
716
|
+
} catch {
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async function handleAfterDelete(event, ctx) {
|
|
720
|
+
for (const entry of await ctx.kv.list("document:")) {
|
|
721
|
+
const stored = toStoredDocument(entry.value);
|
|
722
|
+
if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
|
|
723
|
+
await ctx.kv.delete(entry.key);
|
|
724
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
729
|
+
"LNK001",
|
|
730
|
+
// internal link target missing from the site
|
|
731
|
+
"LNK002",
|
|
732
|
+
// no inbound links
|
|
733
|
+
"LNK003",
|
|
734
|
+
// weak anchor, checked only when the target route exists
|
|
735
|
+
"LNK004",
|
|
736
|
+
// too few inbound links
|
|
737
|
+
"SEO009",
|
|
738
|
+
// hreflang alternate target missing from the site
|
|
739
|
+
"SEO012",
|
|
740
|
+
// hreflang alternate does not link back
|
|
741
|
+
"SEO016",
|
|
742
|
+
// canonical points at another crawlable route
|
|
743
|
+
"SEO017",
|
|
744
|
+
// title and description shared with another route
|
|
745
|
+
"CNT005"
|
|
746
|
+
// visible text duplicated on another route
|
|
747
|
+
]);
|
|
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
|
|
214
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
|
+
}
|
|
215
803
|
const document = adapted.document;
|
|
804
|
+
if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
|
|
805
|
+
recordContentCoverage(ctx, [document]);
|
|
216
806
|
const { kv, log } = ctx;
|
|
217
807
|
const stored = { document, meta: adapted.meta };
|
|
218
|
-
await persistDocument(ctx, stored)
|
|
219
|
-
|
|
808
|
+
if (!await persistDocument(ctx, stored)) {
|
|
809
|
+
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
|
|
813
|
+
recordAssessmentCoverage(ctx, result, [document]);
|
|
220
814
|
if (!result.ok) {
|
|
221
815
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
222
816
|
detail: result.detail
|
|
223
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
|
+
}
|
|
224
825
|
return;
|
|
225
826
|
}
|
|
226
|
-
const
|
|
227
|
-
(finding) => finding
|
|
827
|
+
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
828
|
+
(finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
228
829
|
);
|
|
830
|
+
const pageFindings = [
|
|
831
|
+
...result.findings.filter(
|
|
832
|
+
(finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
|
|
833
|
+
).map((finding) => withCmsSourceLocation(finding, stored)),
|
|
834
|
+
...storedGraphFindings
|
|
835
|
+
];
|
|
229
836
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
230
837
|
{
|
|
231
838
|
route: document.route,
|
|
@@ -234,12 +841,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
234
841
|
},
|
|
235
842
|
pageFindings
|
|
236
843
|
);
|
|
844
|
+
if (await isSuperseded(kv, stored)) {
|
|
845
|
+
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
849
|
+
countCmsLocations(cmsLocationCounts, filtered);
|
|
850
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
237
851
|
await kv.set(findingsKey(document.route), {
|
|
238
852
|
route: document.route,
|
|
239
|
-
findings: filtered
|
|
853
|
+
findings: filtered,
|
|
854
|
+
body_coverage: cmsBodyCoverage(document),
|
|
855
|
+
...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
|
|
240
856
|
});
|
|
241
857
|
}
|
|
242
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
|
+
}
|
|
243
889
|
async function evaluateAndPersistAll(ctx, options) {
|
|
244
890
|
const collections = options.collections ?? DEFAULT_COLLECTIONS;
|
|
245
891
|
const suppressionFilter = options.suppressionFilter;
|
|
@@ -250,11 +896,15 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
250
896
|
totalFindings: 0,
|
|
251
897
|
errors: []
|
|
252
898
|
};
|
|
899
|
+
const storedAtStart = await readAllStoredDocuments(kv);
|
|
253
900
|
const documents = [];
|
|
901
|
+
const documentCollections = [];
|
|
254
902
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
255
903
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
904
|
+
const adaptationFailures = [];
|
|
256
905
|
for (const collection of collections) {
|
|
257
906
|
let cursor;
|
|
907
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
258
908
|
do {
|
|
259
909
|
try {
|
|
260
910
|
const page = await ctx.content.list(collection, {
|
|
@@ -262,19 +912,35 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
262
912
|
...cursor === void 0 ? {} : { cursor }
|
|
263
913
|
});
|
|
264
914
|
for (const item of page.items) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
943
|
}
|
|
277
|
-
cursor = page.hasMore ? page.cursor : void 0;
|
|
278
944
|
} catch (err) {
|
|
279
945
|
const message = err instanceof Error ? err.message : String(err);
|
|
280
946
|
summary.errors.push(`${collection}: ${message}`);
|
|
@@ -282,66 +948,183 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
282
948
|
}
|
|
283
949
|
} while (cursor !== void 0);
|
|
284
950
|
}
|
|
951
|
+
const inventoryComplete = summary.errors.length === 0;
|
|
285
952
|
summary.documentsScanned = documents.length;
|
|
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
|
+
}
|
|
286
974
|
for (const stored of adaptedByRoute.values()) {
|
|
287
|
-
await kv
|
|
975
|
+
const current = await readStoredDocument(kv, stored.document.route);
|
|
976
|
+
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
977
|
+
await kv.set(documentKey(stored.document.route), stored);
|
|
978
|
+
}
|
|
288
979
|
}
|
|
289
|
-
const result = await options.evaluator
|
|
980
|
+
const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
|
|
981
|
+
recordAssessmentCoverage(ctx, result, documents);
|
|
290
982
|
if (!result.ok) {
|
|
291
983
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
292
984
|
detail: result.detail
|
|
293
985
|
});
|
|
294
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
|
+
}
|
|
295
1005
|
return summary;
|
|
296
1006
|
}
|
|
297
1007
|
const findingsByRoute = /* @__PURE__ */ new Map();
|
|
1008
|
+
findingsByRoute.set("*", []);
|
|
298
1009
|
for (const route of documentRoutes) {
|
|
299
1010
|
findingsByRoute.set(route, []);
|
|
300
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
|
+
}
|
|
301
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;
|
|
302
1035
|
const bucket = finding.scope === "page" ? finding.path : "*";
|
|
303
1036
|
const list = findingsByRoute.get(bucket) ?? [];
|
|
304
|
-
list.push(finding);
|
|
1037
|
+
list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
|
|
305
1038
|
findingsByRoute.set(bucket, list);
|
|
306
1039
|
}
|
|
307
1040
|
let totalAfterSuppression = 0;
|
|
1041
|
+
const cmsLocationCounts = { eligible: 0, mapped: 0 };
|
|
308
1042
|
for (const [route, findings] of findingsByRoute) {
|
|
309
1043
|
const stored = adaptedByRoute.get(route);
|
|
1044
|
+
if (stored !== void 0 && await isSuperseded(kv, stored)) {
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
310
1047
|
const context = {
|
|
311
1048
|
route,
|
|
312
1049
|
...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
|
|
313
1050
|
...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
|
|
314
1051
|
};
|
|
315
1052
|
const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
|
|
316
|
-
|
|
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
|
+
});
|
|
317
1060
|
summary.routesUpdated += 1;
|
|
318
1061
|
totalAfterSuppression += filtered.length;
|
|
319
1062
|
}
|
|
1063
|
+
recordCmsLocationCoverage(ctx, cmsLocationCounts);
|
|
320
1064
|
summary.totalFindings = totalAfterSuppression;
|
|
321
|
-
if (
|
|
322
|
-
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
1065
|
+
if (inventoryComplete && !changed) {
|
|
1066
|
+
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
323
1067
|
}
|
|
324
1068
|
return summary;
|
|
325
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
|
+
}
|
|
1084
|
+
function isOlderRevision(revision, other) {
|
|
1085
|
+
if (revision === void 0 || other === void 0) return false;
|
|
1086
|
+
return Date.parse(revision) < Date.parse(other);
|
|
1087
|
+
}
|
|
1088
|
+
async function readStoredDocument(kv, route) {
|
|
1089
|
+
return toStoredDocument(await kv.get(documentKey(route)));
|
|
1090
|
+
}
|
|
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));
|
|
1096
|
+
}
|
|
326
1097
|
async function persistDocument(ctx, stored) {
|
|
327
1098
|
const route = stored.document.route;
|
|
328
|
-
const
|
|
329
|
-
for (const entry of entries) {
|
|
1099
|
+
const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
|
|
330
1100
|
const previous = toStoredDocument(entry.value);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
1101
|
+
return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
|
|
1102
|
+
});
|
|
1103
|
+
if (sameEntry.some(
|
|
1104
|
+
({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
|
|
1105
|
+
)) {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
for (const { key, previous } of sameEntry) {
|
|
1109
|
+
if (previous.document.route === route) continue;
|
|
1110
|
+
await ctx.kv.delete(key);
|
|
335
1111
|
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
336
1112
|
}
|
|
337
1113
|
await ctx.kv.set(documentKey(route), stored);
|
|
1114
|
+
return true;
|
|
338
1115
|
}
|
|
339
|
-
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
1116
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
|
|
340
1117
|
const scannedCollections = new Set(collections);
|
|
1118
|
+
const revisionAtStart = new Map(
|
|
1119
|
+
storedAtStart.map((stored) => [
|
|
1120
|
+
stored.document.route,
|
|
1121
|
+
stored.meta.updatedAt
|
|
1122
|
+
])
|
|
1123
|
+
);
|
|
341
1124
|
const entries = await ctx.kv.list("document:");
|
|
342
1125
|
for (const entry of entries) {
|
|
343
1126
|
const stored = toStoredDocument(entry.value);
|
|
344
|
-
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
1127
|
+
if (stored === null || stored.meta.collection !== "" && !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route) || !revisionAtStart.has(stored.document.route) || revisionAtStart.get(stored.document.route) !== stored.meta.updatedAt) {
|
|
345
1128
|
continue;
|
|
346
1129
|
}
|
|
347
1130
|
await ctx.kv.delete(entry.key);
|
|
@@ -401,6 +1184,23 @@ async function readFindings(kv, route) {
|
|
|
401
1184
|
}
|
|
402
1185
|
return stored.findings;
|
|
403
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
|
+
}
|
|
404
1204
|
|
|
405
1205
|
// src/findings-view.ts
|
|
406
1206
|
function normalizeInteraction(input) {
|
|
@@ -438,6 +1238,10 @@ async function handleRefresh(options) {
|
|
|
438
1238
|
summary = await evaluateAndPersistAll(options.ctx, {
|
|
439
1239
|
collections: options.collections,
|
|
440
1240
|
evaluator: options.evaluator,
|
|
1241
|
+
blockMappings: options.blockMappings,
|
|
1242
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1243
|
+
contentFieldsUnavailable: options.contentFieldsUnavailable,
|
|
1244
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
441
1245
|
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
442
1246
|
suppressionFilter: options.suppressionFilter
|
|
443
1247
|
});
|
|
@@ -473,21 +1277,38 @@ async function handleRefresh(options) {
|
|
|
473
1277
|
}
|
|
474
1278
|
};
|
|
475
1279
|
}
|
|
476
|
-
async function
|
|
477
|
-
const entries = await kv.list(
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
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;
|
|
481
1291
|
for (const entry of entries) {
|
|
482
|
-
if (entry.value === null)
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
1292
|
+
if (entry.value === null) continue;
|
|
485
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
|
+
}
|
|
486
1305
|
for (const finding of entry.value.findings) {
|
|
487
|
-
|
|
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);
|
|
488
1309
|
}
|
|
489
1310
|
}
|
|
490
|
-
return
|
|
1311
|
+
return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
|
|
491
1312
|
}
|
|
492
1313
|
function severityFirst(a, b) {
|
|
493
1314
|
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
@@ -505,7 +1326,7 @@ function uniqueRoutes(rows) {
|
|
|
505
1326
|
return [...routes].sort();
|
|
506
1327
|
}
|
|
507
1328
|
async function renderFindingsPage(kv, tuning) {
|
|
508
|
-
const findings = await
|
|
1329
|
+
const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
|
|
509
1330
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
510
1331
|
const warnings = findings.filter(
|
|
511
1332
|
(finding) => finding.severity === "warning"
|
|
@@ -516,8 +1337,13 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
516
1337
|
{ type: "header", text: "SEO findings" },
|
|
517
1338
|
{
|
|
518
1339
|
type: "context",
|
|
519
|
-
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.`
|
|
520
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}` }] : [],
|
|
521
1347
|
{ type: "divider" },
|
|
522
1348
|
{
|
|
523
1349
|
type: "actions",
|
|
@@ -542,10 +1368,10 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
542
1368
|
},
|
|
543
1369
|
sorted.length === 0 ? {
|
|
544
1370
|
type: "context",
|
|
545
|
-
text: tuning.emptyFindingsText
|
|
1371
|
+
text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
|
|
546
1372
|
} : findingsTable(sorted)
|
|
547
1373
|
];
|
|
548
|
-
if (
|
|
1374
|
+
if (documentRoutes.length > 0) {
|
|
549
1375
|
blocks.push({
|
|
550
1376
|
type: "form",
|
|
551
1377
|
fields: [
|
|
@@ -553,7 +1379,7 @@ async function renderFindingsPage(kv, tuning) {
|
|
|
553
1379
|
type: "select",
|
|
554
1380
|
action_id: "route_picker",
|
|
555
1381
|
label: "Document to inspect",
|
|
556
|
-
options:
|
|
1382
|
+
options: documentRoutes.map((route) => ({ label: route, value: route }))
|
|
557
1383
|
}
|
|
558
1384
|
],
|
|
559
1385
|
submit: { label: "View document SEO", action_id: "view_document" }
|
|
@@ -578,55 +1404,13 @@ function findingsTable(rows) {
|
|
|
578
1404
|
}))
|
|
579
1405
|
};
|
|
580
1406
|
}
|
|
581
|
-
async function renderScoreWidget(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
type: "context",
|
|
589
|
-
text: tuning.emptyScoreText
|
|
590
|
-
}
|
|
591
|
-
]
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
const score = await tuning.score(documents, kv);
|
|
595
|
-
const blocks = [
|
|
596
|
-
{
|
|
597
|
-
type: "stats",
|
|
598
|
-
items: [
|
|
599
|
-
{ label: "Overall", value: `${score.overall_score}` },
|
|
600
|
-
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
601
|
-
{
|
|
602
|
-
label: tuning.truthLabel(score),
|
|
603
|
-
value: `${score.truth_consistency_score}`
|
|
604
|
-
},
|
|
605
|
-
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
606
|
-
]
|
|
607
|
-
}
|
|
608
|
-
];
|
|
609
|
-
if (score.overall_score < 60) {
|
|
610
|
-
blocks.unshift({
|
|
611
|
-
type: "banner",
|
|
612
|
-
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
613
|
-
variant: "alert"
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
if (score.blockers.length > 0) {
|
|
617
|
-
blocks.push({
|
|
618
|
-
type: "context",
|
|
619
|
-
text: topBlockersLine(score)
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
return { blocks };
|
|
623
|
-
}
|
|
624
|
-
function topBlockersLine(score) {
|
|
625
|
-
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
626
|
-
if (top.length === 0) {
|
|
627
|
-
return "No blockers identified.";
|
|
628
|
-
}
|
|
629
|
-
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
|
+
};
|
|
630
1414
|
}
|
|
631
1415
|
async function renderDocumentPanel(kv, route) {
|
|
632
1416
|
if (route === void 0) {
|
|
@@ -640,7 +1424,11 @@ async function renderDocumentPanel(kv, route) {
|
|
|
640
1424
|
]
|
|
641
1425
|
};
|
|
642
1426
|
}
|
|
643
|
-
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;
|
|
644
1432
|
const errors = findings.filter((finding) => finding.severity === "error");
|
|
645
1433
|
const warnings = findings.filter(
|
|
646
1434
|
(finding) => finding.severity === "warning"
|
|
@@ -651,8 +1439,14 @@ async function renderDocumentPanel(kv, route) {
|
|
|
651
1439
|
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
652
1440
|
{
|
|
653
1441
|
type: "context",
|
|
654
|
-
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.`
|
|
655
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) }))] : [],
|
|
656
1450
|
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
657
1451
|
]
|
|
658
1452
|
};
|
|
@@ -664,16 +1458,13 @@ function documentFindingsTable(findings) {
|
|
|
664
1458
|
{ key: "rule", label: "Rule", format: "code" },
|
|
665
1459
|
{ key: "severity", label: "Severity", format: "badge" },
|
|
666
1460
|
{ key: "message", label: "Message" },
|
|
667
|
-
{ key: "block", label: "
|
|
1461
|
+
{ key: "block", label: "Location", format: "code" }
|
|
668
1462
|
],
|
|
669
1463
|
rows: findings.map((finding) => ({
|
|
670
1464
|
rule: finding.rule_id,
|
|
671
1465
|
severity: finding.severity,
|
|
672
1466
|
message: finding.message,
|
|
673
|
-
|
|
674
|
-
// when rendering; surfacing the path:line locator here lets authors
|
|
675
|
-
// locate the failing block via the editor's ⌘-F.
|
|
676
|
-
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}`
|
|
677
1468
|
}))
|
|
678
1469
|
};
|
|
679
1470
|
}
|
|
@@ -695,11 +1486,71 @@ __export(aexeo_emdash_bridge_bg_exports, {
|
|
|
695
1486
|
__wbg_set_wasm: () => __wbg_set_wasm,
|
|
696
1487
|
__wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
|
|
697
1488
|
__wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
|
|
1489
|
+
evaluateContent: () => evaluateContent,
|
|
1490
|
+
evaluateContentCollection: () => evaluateContentCollection,
|
|
698
1491
|
evaluateDocuments: () => evaluateDocuments,
|
|
699
1492
|
generateFactsPrompt: () => generateFactsPrompt,
|
|
1493
|
+
projectContentInput: () => projectContentInput,
|
|
1494
|
+
projectLinkDestinations: () => projectLinkDestinations,
|
|
700
1495
|
scoreIntelligence: () => scoreIntelligence,
|
|
701
1496
|
validateFactsManifest: () => validateFactsManifest
|
|
702
1497
|
});
|
|
1498
|
+
function evaluateContent(document_json, preset_name, sources_json, evidence_json, extensions_json, link_observations_json) {
|
|
1499
|
+
let deferred8_0;
|
|
1500
|
+
let deferred8_1;
|
|
1501
|
+
try {
|
|
1502
|
+
const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1503
|
+
const len0 = WASM_VECTOR_LEN;
|
|
1504
|
+
const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1505
|
+
const len1 = WASM_VECTOR_LEN;
|
|
1506
|
+
var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1507
|
+
var len2 = WASM_VECTOR_LEN;
|
|
1508
|
+
var ptr3 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1509
|
+
var len3 = WASM_VECTOR_LEN;
|
|
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];
|
|
1517
|
+
if (ret[3]) {
|
|
1518
|
+
ptr7 = 0;
|
|
1519
|
+
len7 = 0;
|
|
1520
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
1521
|
+
}
|
|
1522
|
+
deferred8_0 = ptr7;
|
|
1523
|
+
deferred8_1 = len7;
|
|
1524
|
+
return getStringFromWasm0(ptr7, len7);
|
|
1525
|
+
} finally {
|
|
1526
|
+
wasm.__wbindgen_free(deferred8_0, deferred8_1, 1);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function evaluateContentCollection(documents_json, preset_name, sources_json) {
|
|
1530
|
+
let deferred5_0;
|
|
1531
|
+
let deferred5_1;
|
|
1532
|
+
try {
|
|
1533
|
+
const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1534
|
+
const len0 = WASM_VECTOR_LEN;
|
|
1535
|
+
const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1536
|
+
const len1 = WASM_VECTOR_LEN;
|
|
1537
|
+
var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
1538
|
+
var len2 = WASM_VECTOR_LEN;
|
|
1539
|
+
const ret = wasm.evaluateContentCollection(ptr0, len0, ptr1, len1, ptr2, len2);
|
|
1540
|
+
var ptr4 = ret[0];
|
|
1541
|
+
var len4 = ret[1];
|
|
1542
|
+
if (ret[3]) {
|
|
1543
|
+
ptr4 = 0;
|
|
1544
|
+
len4 = 0;
|
|
1545
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
1546
|
+
}
|
|
1547
|
+
deferred5_0 = ptr4;
|
|
1548
|
+
deferred5_1 = len4;
|
|
1549
|
+
return getStringFromWasm0(ptr4, len4);
|
|
1550
|
+
} finally {
|
|
1551
|
+
wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
703
1554
|
function evaluateDocuments(documents_json, config_json) {
|
|
704
1555
|
let deferred4_0;
|
|
705
1556
|
let deferred4_1;
|
|
@@ -744,6 +1595,50 @@ function generateFactsPrompt(documents_json) {
|
|
|
744
1595
|
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
745
1596
|
}
|
|
746
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
|
+
}
|
|
747
1642
|
function scoreIntelligence(documents_json, manifest_json) {
|
|
748
1643
|
let deferred4_0;
|
|
749
1644
|
let deferred4_1;
|
|
@@ -969,6 +1864,18 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
|
|
|
969
1864
|
await ensureInitialized();
|
|
970
1865
|
return validateFactsManifest(manifestJson, documentsJson);
|
|
971
1866
|
}
|
|
1867
|
+
async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson, extensionsJson, linkObservationsJson) {
|
|
1868
|
+
await ensureInitialized();
|
|
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);
|
|
1878
|
+
}
|
|
972
1879
|
|
|
973
1880
|
// src/data-route.ts
|
|
974
1881
|
async function handleDataRoute(ctx, options) {
|
|
@@ -977,6 +1884,9 @@ async function handleDataRoute(ctx, options) {
|
|
|
977
1884
|
summary = await evaluateAndPersistAll(ctx, {
|
|
978
1885
|
collections: options.collections,
|
|
979
1886
|
evaluator: options.evaluator,
|
|
1887
|
+
blockMappings: options.blockMappings,
|
|
1888
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
1889
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection,
|
|
980
1890
|
...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
|
|
981
1891
|
});
|
|
982
1892
|
await persistManifestStateFindings(ctx);
|
|
@@ -995,20 +1905,22 @@ async function buildFindingsPayload(ctx) {
|
|
|
995
1905
|
);
|
|
996
1906
|
const findings = [];
|
|
997
1907
|
const sitewide = [];
|
|
1908
|
+
const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
|
|
998
1909
|
const perRoute = /* @__PURE__ */ new Map();
|
|
999
1910
|
for (const entry of entries) {
|
|
1000
1911
|
if (entry.value === null) continue;
|
|
1001
1912
|
const route = entry.key.replace(/^findings:/, "");
|
|
1002
1913
|
if (route === "*") {
|
|
1003
|
-
sitewide.push(...entry.value.findings);
|
|
1914
|
+
sitewide.push(...entry.value.findings.filter((finding) => isCmsObservableFinding(finding) && !(sitewideAssessment?.status === "error" && isSiteGraphFinding(finding))));
|
|
1004
1915
|
continue;
|
|
1005
1916
|
}
|
|
1006
1917
|
let bucket = perRoute.get(route);
|
|
1007
1918
|
if (bucket === void 0) {
|
|
1008
|
-
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 } };
|
|
1009
1920
|
perRoute.set(route, bucket);
|
|
1010
1921
|
}
|
|
1011
1922
|
for (const finding of entry.value.findings) {
|
|
1923
|
+
if (!isCmsObservableFinding(finding, entry.value.body_coverage === "complete") || sitewideAssessment?.status === "error" && isSiteGraphFinding(finding)) continue;
|
|
1012
1924
|
bucket.findings.push(finding);
|
|
1013
1925
|
if (finding.severity === "error") bucket.errors += 1;
|
|
1014
1926
|
else if (finding.severity === "warning") bucket.warnings += 1;
|
|
@@ -1029,7 +1941,9 @@ async function buildFindingsPayload(ctx) {
|
|
|
1029
1941
|
publishedUrl: stored2?.meta.publicUrl ?? "",
|
|
1030
1942
|
findingCount: bucket.findings.length,
|
|
1031
1943
|
errorCount: bucket.errors,
|
|
1032
|
-
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 }
|
|
1033
1947
|
});
|
|
1034
1948
|
}
|
|
1035
1949
|
routes.sort((a, b) => b.errorCount - a.errorCount || a.route.localeCompare(b.route));
|
|
@@ -1051,7 +1965,8 @@ async function buildFindingsPayload(ctx) {
|
|
|
1051
1965
|
documentsIndexed: documents.size
|
|
1052
1966
|
},
|
|
1053
1967
|
layerBreakdown,
|
|
1054
|
-
sitewideFindings: sitewide
|
|
1968
|
+
sitewideFindings: sitewide,
|
|
1969
|
+
...sitewideAssessment === void 0 ? {} : { sitewideAssessment }
|
|
1055
1970
|
};
|
|
1056
1971
|
}
|
|
1057
1972
|
function computeLayerBreakdown(findings) {
|
|
@@ -1245,12 +2160,16 @@ async function handleSave(ctx, body) {
|
|
|
1245
2160
|
return { saved: true };
|
|
1246
2161
|
}
|
|
1247
2162
|
|
|
1248
|
-
// src/presence-route.ts
|
|
1249
|
-
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
1250
|
-
|
|
1251
2163
|
// src/presence-check.ts
|
|
1252
2164
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
1253
2165
|
var USER_AGENT = "aexeo-emdash-plugin/0.8 (+https://github.com/schiste/Aexeo; entity-presence diagnostic)";
|
|
2166
|
+
var PRESENCE_HOSTS = [
|
|
2167
|
+
"en.wikipedia.org",
|
|
2168
|
+
"www.wikidata.org",
|
|
2169
|
+
"api.github.com",
|
|
2170
|
+
"rdap.org",
|
|
2171
|
+
"index.commoncrawl.org"
|
|
2172
|
+
];
|
|
1254
2173
|
var SOURCE_NAMES = [
|
|
1255
2174
|
"wikipedia",
|
|
1256
2175
|
"wikidata",
|
|
@@ -1307,7 +2226,7 @@ async function checkWikipedia(input, fetcher) {
|
|
|
1307
2226
|
return notFound2("wikipedia");
|
|
1308
2227
|
}
|
|
1309
2228
|
const title = String(titles[0]);
|
|
1310
|
-
if (!
|
|
2229
|
+
if (!namesEntity(title, input)) {
|
|
1311
2230
|
return notFound2("wikipedia");
|
|
1312
2231
|
}
|
|
1313
2232
|
const articleUrl = String(urls[0] ?? "");
|
|
@@ -1337,7 +2256,7 @@ async function checkWikidata(input, fetcher) {
|
|
|
1337
2256
|
return notFound2("wikidata");
|
|
1338
2257
|
}
|
|
1339
2258
|
const label = top.label ?? input.name;
|
|
1340
|
-
if (!
|
|
2259
|
+
if (!namesEntity(label, input)) {
|
|
1341
2260
|
return notFound2("wikidata");
|
|
1342
2261
|
}
|
|
1343
2262
|
return found("wikidata", {
|
|
@@ -1426,9 +2345,27 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1426
2345
|
"no website URL in manifest, can't query crawl index"
|
|
1427
2346
|
);
|
|
1428
2347
|
}
|
|
1429
|
-
|
|
2348
|
+
let index;
|
|
2349
|
+
try {
|
|
2350
|
+
const res = await timedFetch(
|
|
2351
|
+
fetcher,
|
|
2352
|
+
"https://index.commoncrawl.org/collinfo.json"
|
|
2353
|
+
);
|
|
2354
|
+
if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
|
|
2355
|
+
const newest = newestCrawlId(await res.json());
|
|
2356
|
+
if (newest === null) {
|
|
2357
|
+
return unreachable(
|
|
2358
|
+
"common_crawl",
|
|
2359
|
+
"no crawl index listed in collinfo.json"
|
|
2360
|
+
);
|
|
2361
|
+
}
|
|
2362
|
+
index = newest;
|
|
2363
|
+
} catch (error) {
|
|
2364
|
+
return unreachable("common_crawl", errMsg(error));
|
|
2365
|
+
}
|
|
1430
2366
|
const url = new URL(`https://index.commoncrawl.org/${index}-index`);
|
|
1431
2367
|
url.searchParams.set("url", host);
|
|
2368
|
+
url.searchParams.set("matchType", "domain");
|
|
1432
2369
|
url.searchParams.set("output", "json");
|
|
1433
2370
|
url.searchParams.set("limit", "1");
|
|
1434
2371
|
try {
|
|
@@ -1455,6 +2392,16 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1455
2392
|
return unreachable("common_crawl", errMsg(error));
|
|
1456
2393
|
}
|
|
1457
2394
|
}
|
|
2395
|
+
function newestCrawlId(collinfo) {
|
|
2396
|
+
if (!Array.isArray(collinfo)) return null;
|
|
2397
|
+
let newest = null;
|
|
2398
|
+
for (const crawl of collinfo) {
|
|
2399
|
+
const id = crawl?.id;
|
|
2400
|
+
if (typeof id !== "string" || !/^CC-MAIN-[A-Za-z0-9-]+$/.test(id)) continue;
|
|
2401
|
+
if (newest === null || id > newest) newest = id;
|
|
2402
|
+
}
|
|
2403
|
+
return newest;
|
|
2404
|
+
}
|
|
1458
2405
|
async function timedFetch(fetcher, url, init) {
|
|
1459
2406
|
const controller = new AbortController();
|
|
1460
2407
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -1500,10 +2447,45 @@ function errMsg(error) {
|
|
|
1500
2447
|
}
|
|
1501
2448
|
return String(error);
|
|
1502
2449
|
}
|
|
1503
|
-
function
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
2450
|
+
function namesEntity(candidate, input) {
|
|
2451
|
+
const normalized = normalizeEntityName(candidate);
|
|
2452
|
+
return [input.name, ...input.aliases ?? []].some(
|
|
2453
|
+
(name) => normalizeEntityName(name) === normalized
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
var LEGAL_FORMS = /* @__PURE__ */ new Set([
|
|
2457
|
+
"ab",
|
|
2458
|
+
"ag",
|
|
2459
|
+
"bv",
|
|
2460
|
+
"co",
|
|
2461
|
+
"company",
|
|
2462
|
+
"corp",
|
|
2463
|
+
"corporation",
|
|
2464
|
+
"gmbh",
|
|
2465
|
+
"inc",
|
|
2466
|
+
"incorporated",
|
|
2467
|
+
"kk",
|
|
2468
|
+
"limited",
|
|
2469
|
+
"llc",
|
|
2470
|
+
"llp",
|
|
2471
|
+
"lp",
|
|
2472
|
+
"ltd",
|
|
2473
|
+
"nv",
|
|
2474
|
+
"oy",
|
|
2475
|
+
"plc",
|
|
2476
|
+
"pty",
|
|
2477
|
+
"sa",
|
|
2478
|
+
"sarl",
|
|
2479
|
+
"sas",
|
|
2480
|
+
"srl"
|
|
2481
|
+
]);
|
|
2482
|
+
function normalizeEntityName(name) {
|
|
2483
|
+
const words = name.replace(/\([^)]*\)/g, " ").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0);
|
|
2484
|
+
let end = words.length;
|
|
2485
|
+
while (end > 0 && LEGAL_FORMS.has(words[end - 1])) end -= 1;
|
|
2486
|
+
const start = words[0] === "the" ? 1 : 0;
|
|
2487
|
+
const core = words.slice(start, end);
|
|
2488
|
+
return (core.length > 0 ? core : words).join(" ");
|
|
1507
2489
|
}
|
|
1508
2490
|
function sanitizeGitHubHandle(name) {
|
|
1509
2491
|
const handle = name.replace(/[\s_]+/g, "-");
|
|
@@ -1542,6 +2524,7 @@ function formatCdxTimestamp(ts) {
|
|
|
1542
2524
|
}
|
|
1543
2525
|
|
|
1544
2526
|
// src/presence-route.ts
|
|
2527
|
+
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
1545
2528
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1546
2529
|
var PRESENCE_KEY = "presence:current";
|
|
1547
2530
|
async function handlePresenceRoute(ctx) {
|
|
@@ -1664,8 +2647,150 @@ function globToMatcher(pattern) {
|
|
|
1664
2647
|
return (route) => re.test(route);
|
|
1665
2648
|
}
|
|
1666
2649
|
|
|
1667
|
-
// src/
|
|
1668
|
-
|
|
2650
|
+
// src/content.ts
|
|
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) {
|
|
2658
|
+
const started = onTelemetry ? performance.now() : 0;
|
|
2659
|
+
const documentJson = JSON.stringify(document);
|
|
2660
|
+
const sourcesJson = JSON.stringify(sources);
|
|
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));
|
|
2662
|
+
const report = JSON.parse(result);
|
|
2663
|
+
if (onTelemetry) {
|
|
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
|
+
}
|
|
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
|
+
}
|
|
2679
|
+
const checked = gate.measurements?.checked_destinations;
|
|
2680
|
+
const invalid = gate.measurements?.invalid_destinations;
|
|
2681
|
+
if (checked !== void 0 && invalid !== void 0) {
|
|
2682
|
+
event.link_destinations ??= { checked: 0, invalid: 0 };
|
|
2683
|
+
event.link_destinations.checked += checked;
|
|
2684
|
+
event.link_destinations.invalid += invalid;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
onTelemetry(event);
|
|
2688
|
+
}
|
|
2689
|
+
return report;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// src/content-evaluator.ts
|
|
2693
|
+
function withContentPreset(base, preset, evaluate, provider, project, onProviderTelemetry, linkProvider, projectLinks, onLinkProviderTelemetry) {
|
|
2694
|
+
if (!preset) return base;
|
|
2695
|
+
return async (documents, context) => {
|
|
2696
|
+
const result = await base(documents, context);
|
|
2697
|
+
if (!result.ok) return result;
|
|
2698
|
+
const findings = [...result.findings];
|
|
2699
|
+
const assessments = { ...result.assessments };
|
|
2700
|
+
try {
|
|
2701
|
+
for (const [index, document] of documents.entries()) {
|
|
2702
|
+
const collection = context?.collections[index];
|
|
2703
|
+
if (preset.collections !== void 0) {
|
|
2704
|
+
if (collection === void 0) throw new Error("collection is required for a scoped content preset");
|
|
2705
|
+
if (!preset.collections.includes(collection)) continue;
|
|
2706
|
+
}
|
|
2707
|
+
const selected = collection !== void 0 && preset.byCollection !== void 0 && Object.hasOwn(preset.byCollection, collection) ? preset.byCollection[collection] : void 0;
|
|
2708
|
+
const name = selected ?? preset.name;
|
|
2709
|
+
if (!name) throw new Error(`empty preset name for collection ${collection}`);
|
|
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 } }))
|
|
2774
|
+
};
|
|
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
|
+
}));
|
|
2787
|
+
}
|
|
2788
|
+
return { ok: true, findings, assessments };
|
|
2789
|
+
} catch (error) {
|
|
2790
|
+
return { ok: false, reason: "content_preset_error", detail: error instanceof Error ? error.message : String(error) };
|
|
2791
|
+
}
|
|
2792
|
+
};
|
|
2793
|
+
}
|
|
1669
2794
|
|
|
1670
2795
|
// src/configured.ts
|
|
1671
2796
|
import { definePlugin } from "emdash";
|
|
@@ -1724,17 +2849,22 @@ var inProcessEvaluator = async (documents) => {
|
|
|
1724
2849
|
return { ok: false, reason: "wasm_error", detail };
|
|
1725
2850
|
}
|
|
1726
2851
|
};
|
|
1727
|
-
function createPlugin(options = {}) {
|
|
2852
|
+
function createPlugin(options = {}, runtime = {}) {
|
|
1728
2853
|
const collections = resolveCollections(
|
|
1729
2854
|
options.collections,
|
|
1730
2855
|
options.includeCollections,
|
|
1731
2856
|
options.excludeCollections
|
|
1732
2857
|
);
|
|
1733
2858
|
const suppressionFilter = compileSuppressions(options.suppressions);
|
|
2859
|
+
const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3, runtime.externalGates, projectContentInput3, runtime.onExternalGateTelemetry, runtime.linkObservations, projectLinkDestinations3, runtime.onLinkProviderTelemetry);
|
|
1734
2860
|
return definePlugin({
|
|
1735
2861
|
id: "aexeo-emdash",
|
|
1736
2862
|
version: PACKAGE_VERSION,
|
|
1737
|
-
|
|
2863
|
+
// The entity-presence route fetches its public sources directly (see
|
|
2864
|
+
// presence-check.ts). rdap.org then redirects to the registry for the
|
|
2865
|
+
// domain's TLD; those registries cannot be listed here.
|
|
2866
|
+
capabilities: ["content:read", "network:request"],
|
|
2867
|
+
allowedHosts: [...PRESENCE_HOSTS],
|
|
1738
2868
|
hooks: {
|
|
1739
2869
|
// afterSave processes one saved document at a time — collections
|
|
1740
2870
|
// list isn't needed here. The hook always runs regardless of
|
|
@@ -1742,16 +2872,27 @@ function createPlugin(options = {}) {
|
|
|
1742
2872
|
"content:afterSave": (event, ctx) => handleAfterSaveConfigured(
|
|
1743
2873
|
event,
|
|
1744
2874
|
ctx,
|
|
1745
|
-
|
|
1746
|
-
suppressionFilter
|
|
1747
|
-
|
|
2875
|
+
evaluator,
|
|
2876
|
+
suppressionFilter,
|
|
2877
|
+
options.blockMappings,
|
|
2878
|
+
options.contentFieldsByCollection?.[event.content.type],
|
|
2879
|
+
options.descriptionFieldsByCollection?.[event.content.type]
|
|
2880
|
+
),
|
|
2881
|
+
"content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
|
|
1748
2882
|
},
|
|
2883
|
+
// emdash requires plugins:manage (admin) on a route that declares no
|
|
2884
|
+
// permission. Reading findings only needs plugins:read (editor); anything
|
|
2885
|
+
// that writes stays admin, stated explicitly so the split is visible.
|
|
1749
2886
|
routes: {
|
|
1750
2887
|
// The Refresh sweep (handleAdminRoute → handleRefresh) is the
|
|
1751
2888
|
// one consumer of `collections`. Bind the resolved list here so
|
|
1752
2889
|
// every request handles the same set without re-reading options.
|
|
2890
|
+
//
|
|
2891
|
+
// This route serves the pages and the dashboard widget, and also the
|
|
2892
|
+
// refresh_findings action, which handleAdminRoute gates itself.
|
|
1753
2893
|
admin: {
|
|
1754
|
-
|
|
2894
|
+
permission: "plugins:read",
|
|
2895
|
+
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection, options.descriptionFieldsByCollection)
|
|
1755
2896
|
},
|
|
1756
2897
|
// JSON data endpoint for the React adminEntry. Two routes:
|
|
1757
2898
|
// POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
|
|
@@ -1761,19 +2902,27 @@ function createPlugin(options = {}) {
|
|
|
1761
2902
|
// emdash edit page and the public site, which Block Kit can't
|
|
1762
2903
|
// express in any element type as of emdash 0.8.0.
|
|
1763
2904
|
data: {
|
|
2905
|
+
permission: "plugins:read",
|
|
1764
2906
|
handler: async (ctx) => (await handleDataRoute(ctx, {
|
|
1765
2907
|
collections,
|
|
1766
|
-
evaluator
|
|
2908
|
+
evaluator,
|
|
1767
2909
|
refresh: false,
|
|
1768
|
-
suppressionFilter
|
|
2910
|
+
suppressionFilter,
|
|
2911
|
+
blockMappings: options.blockMappings,
|
|
2912
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
2913
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection
|
|
1769
2914
|
})).payload
|
|
1770
2915
|
},
|
|
1771
2916
|
refresh: {
|
|
2917
|
+
permission: "plugins:manage",
|
|
1772
2918
|
handler: (ctx) => handleDataRoute(ctx, {
|
|
1773
2919
|
collections,
|
|
1774
|
-
evaluator
|
|
2920
|
+
evaluator,
|
|
1775
2921
|
refresh: true,
|
|
1776
|
-
suppressionFilter
|
|
2922
|
+
suppressionFilter,
|
|
2923
|
+
blockMappings: options.blockMappings,
|
|
2924
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
2925
|
+
descriptionFieldsByCollection: options.descriptionFieldsByCollection
|
|
1777
2926
|
})
|
|
1778
2927
|
},
|
|
1779
2928
|
// /facts route serves the truth-manifest authoring UI. It
|
|
@@ -1781,6 +2930,7 @@ function createPlugin(options = {}) {
|
|
|
1781
2930
|
// onto one POST endpoint via a "kind" body field — see
|
|
1782
2931
|
// facts-route.ts for the full contract.
|
|
1783
2932
|
facts: {
|
|
2933
|
+
permission: "plugins:manage",
|
|
1784
2934
|
handler: (ctx) => handleFactsRoute(ctx)
|
|
1785
2935
|
},
|
|
1786
2936
|
// /presence route serves the layer-4 entity-presence
|
|
@@ -1790,6 +2940,7 @@ function createPlugin(options = {}) {
|
|
|
1790
2940
|
// results in KV for 24h. Multiplexes data / refresh kinds —
|
|
1791
2941
|
// see presence-route.ts.
|
|
1792
2942
|
presence: {
|
|
2943
|
+
permission: "plugins:manage",
|
|
1793
2944
|
handler: (ctx) => handlePresenceRoute(ctx)
|
|
1794
2945
|
}
|
|
1795
2946
|
},
|
|
@@ -1821,6 +2972,7 @@ function createPlugin(options = {}) {
|
|
|
1821
2972
|
});
|
|
1822
2973
|
}
|
|
1823
2974
|
var configured_default = createPlugin;
|
|
2975
|
+
var ADMIN_ROLE = 50;
|
|
1824
2976
|
var adminViewTuning = {
|
|
1825
2977
|
emptyFindingsText: "Once a Refresh runs, findings will list here.",
|
|
1826
2978
|
emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
|
|
@@ -1828,7 +2980,7 @@ var adminViewTuning = {
|
|
|
1828
2980
|
score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
|
|
1829
2981
|
truthLabel: formatTruthLabel
|
|
1830
2982
|
};
|
|
1831
|
-
async function handleAdminRoute(ctx, collections, suppressionFilter) {
|
|
2983
|
+
async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection, descriptionFieldsByCollection) {
|
|
1832
2984
|
const body = normalizeInteraction(ctx.input);
|
|
1833
2985
|
if (body.type === "page_load") {
|
|
1834
2986
|
return handlePageLoad(ctx, body.page);
|
|
@@ -1838,12 +2990,29 @@ async function handleAdminRoute(ctx, collections, suppressionFilter) {
|
|
|
1838
2990
|
return renderDocumentPanel(ctx.kv, body.value);
|
|
1839
2991
|
}
|
|
1840
2992
|
if (body.action_id === "refresh_findings") {
|
|
2993
|
+
if ((ctx.user?.role ?? 0) < ADMIN_ROLE) {
|
|
2994
|
+
const page = await renderFindingsPage(ctx.kv, adminViewTuning);
|
|
2995
|
+
return {
|
|
2996
|
+
...page,
|
|
2997
|
+
blocks: [
|
|
2998
|
+
{
|
|
2999
|
+
type: "banner",
|
|
3000
|
+
title: "Only administrators can run Refresh.",
|
|
3001
|
+
variant: "error"
|
|
3002
|
+
},
|
|
3003
|
+
...page.blocks
|
|
3004
|
+
]
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
1841
3007
|
return handleRefresh({
|
|
1842
3008
|
ctx,
|
|
1843
3009
|
collections,
|
|
1844
|
-
evaluator
|
|
3010
|
+
evaluator,
|
|
1845
3011
|
suppressionFilter,
|
|
1846
|
-
tuning: adminViewTuning
|
|
3012
|
+
tuning: adminViewTuning,
|
|
3013
|
+
blockMappings,
|
|
3014
|
+
contentFieldsByCollection,
|
|
3015
|
+
descriptionFieldsByCollection
|
|
1847
3016
|
});
|
|
1848
3017
|
}
|
|
1849
3018
|
if (body.action_id.startsWith("filter:")) {
|
|
@@ -1868,7 +3037,7 @@ async function handlePageLoad(ctx, page) {
|
|
|
1868
3037
|
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1869
3038
|
}
|
|
1870
3039
|
if (normalized === "widget:aexeo-score") {
|
|
1871
|
-
return renderScoreWidget(ctx.kv, adminViewTuning);
|
|
3040
|
+
return renderScoreWidget(ctx.kv, adminViewTuning, ctx.log);
|
|
1872
3041
|
}
|
|
1873
3042
|
if (normalized === "document") {
|
|
1874
3043
|
return renderDocumentPanel(ctx.kv);
|