@aeptus/aexeo-emdash 0.8.19 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -2
- package/INSTALL.md +113 -4
- package/README.md +12 -5
- package/dist/adapter.d.ts +30 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin.d.ts.map +1 -1
- package/dist/admin.js +2 -2
- package/dist/configured.d.ts +6 -0
- package/dist/configured.d.ts.map +1 -1
- package/dist/configured.js +505 -70
- package/dist/content-evaluator.d.ts +14 -0
- package/dist/content-evaluator.d.ts.map +1 -0
- package/dist/content.d.ts +51 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +365 -0
- package/dist/data-route.d.ts +3 -0
- 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 +7 -2
- package/dist/findings-view.d.ts.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -4
- package/dist/inventory.d.ts +51 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/plugin.d.ts +30 -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 +366 -81
- package/dist/sandbox.d.ts +2 -2
- package/dist/sidecar-url.d.ts.map +1 -1
- package/dist/sidecar.d.ts +7 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/types.d.ts +11 -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 +4 -0
- package/dist/wasm-init.d.ts.map +1 -1
- package/package.json +18 -7
- package/wasm/aexeo_emdash_bridge.d.ts +11 -0
- package/wasm/aexeo_emdash_bridge.js +1 -1
- package/wasm/aexeo_emdash_bridge_bg.d.ts +11 -0
- package/wasm/aexeo_emdash_bridge_bg.js +68 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
- package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +2 -0
package/dist/configured.js
CHANGED
|
@@ -5,13 +5,15 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/adapter.ts
|
|
8
|
-
async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
|
|
8
|
+
async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
|
|
9
9
|
const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
|
|
10
10
|
const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
|
|
11
11
|
const adapted = adaptContentItem(content, {
|
|
12
12
|
route,
|
|
13
13
|
publicUrl,
|
|
14
|
-
...defaultLocale === void 0 ? {} : { defaultLocale }
|
|
14
|
+
...defaultLocale === void 0 ? {} : { defaultLocale },
|
|
15
|
+
...blockMappings === void 0 ? {} : { blockMappings },
|
|
16
|
+
...contentFields === void 0 ? {} : { contentFields }
|
|
15
17
|
});
|
|
16
18
|
const translationSet = await contentApi.getTranslations(
|
|
17
19
|
content.type,
|
|
@@ -53,8 +55,11 @@ function adaptContentItem(content, options = {}) {
|
|
|
53
55
|
if (typeof locale === "string" && locale.length > 0) {
|
|
54
56
|
document.lang = locale;
|
|
55
57
|
}
|
|
56
|
-
const body = extractPortableText(content.data);
|
|
57
|
-
if (body !== null)
|
|
58
|
+
const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
|
|
59
|
+
if (body !== null) {
|
|
60
|
+
document.body = body.blocks;
|
|
61
|
+
if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
|
|
62
|
+
}
|
|
58
63
|
return {
|
|
59
64
|
document,
|
|
60
65
|
meta: {
|
|
@@ -63,7 +68,8 @@ function adaptContentItem(content, options = {}) {
|
|
|
63
68
|
slug: content.slug,
|
|
64
69
|
status: content.status,
|
|
65
70
|
title,
|
|
66
|
-
publicUrl: options.publicUrl ?? null
|
|
71
|
+
publicUrl: options.publicUrl ?? null,
|
|
72
|
+
...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
}
|
|
@@ -82,24 +88,53 @@ function routeFromPublicUrl(publicUrl) {
|
|
|
82
88
|
function stringOrEmpty(value) {
|
|
83
89
|
return typeof value === "string" ? value : "";
|
|
84
90
|
}
|
|
85
|
-
function
|
|
91
|
+
function extractSelectedContent(data, fields, mappings) {
|
|
92
|
+
if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
|
|
93
|
+
const all = ["text", "headings", "paragraphs", "links"];
|
|
94
|
+
let capabilities = all;
|
|
95
|
+
const blocks = [];
|
|
96
|
+
let observed = false;
|
|
97
|
+
for (const field of fields) {
|
|
98
|
+
const value = data[field];
|
|
99
|
+
if (value === void 0 || value === null) continue;
|
|
100
|
+
observed = true;
|
|
101
|
+
const part = extractFirstPortableText({ content: value }, mappings);
|
|
102
|
+
if (part === null) return { blocks, capabilities: [] };
|
|
103
|
+
blocks.push(...part.blocks);
|
|
104
|
+
capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
|
|
105
|
+
}
|
|
106
|
+
if (!observed) return null;
|
|
107
|
+
return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
|
|
108
|
+
}
|
|
109
|
+
function extractPortableText(data, mappings) {
|
|
110
|
+
const result = extractFirstPortableText(data, mappings);
|
|
111
|
+
if (result === null) return null;
|
|
112
|
+
const candidates = Object.entries(data).filter(
|
|
113
|
+
([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
|
|
114
|
+
);
|
|
115
|
+
return candidates.length > 1 ? { blocks: result.blocks, capabilities: [] } : result;
|
|
116
|
+
}
|
|
117
|
+
function extractFirstPortableText(data, mappings) {
|
|
118
|
+
let foundEmptyBody = false;
|
|
86
119
|
for (const slug of ["body", "content"]) {
|
|
87
120
|
const value = data[slug];
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
121
|
+
if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
|
|
122
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
123
|
+
if (isEmdashBlocksArray(value, slug)) return mapCustomBlocks(value, mappings);
|
|
90
124
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
91
|
-
return [portableTextBlock(value.trim(), `${slug}-text`)];
|
|
125
|
+
return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
|
|
92
126
|
}
|
|
93
127
|
}
|
|
94
128
|
const blocks = data["blocks"];
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
129
|
+
if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
|
|
130
|
+
if (isPortableTextArray(blocks)) return { blocks };
|
|
131
|
+
if (isEmdashBlocksArray(blocks, "blocks")) return mapCustomBlocks(blocks, mappings);
|
|
97
132
|
for (const [field, value] of Object.entries(data)) {
|
|
98
133
|
if (field === "body" || field === "content" || field === "blocks") continue;
|
|
99
|
-
if (isPortableTextArray(value)) return value;
|
|
100
|
-
if (isEmdashBlocksArray(value, field)) return
|
|
134
|
+
if (isPortableTextArray(value)) return { blocks: value };
|
|
135
|
+
if (isEmdashBlocksArray(value, field)) return mapCustomBlocks(value, mappings);
|
|
101
136
|
}
|
|
102
|
-
return null;
|
|
137
|
+
return foundEmptyBody ? { blocks: [] } : null;
|
|
103
138
|
}
|
|
104
139
|
function isPortableTextArray(value) {
|
|
105
140
|
return Array.isArray(value) && value.length > 0 && value.every(
|
|
@@ -114,6 +149,52 @@ function isEmdashBlocksArray(value, field) {
|
|
|
114
149
|
(item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
|
|
115
150
|
);
|
|
116
151
|
}
|
|
152
|
+
function mapCustomBlocks(blocks, mappings) {
|
|
153
|
+
const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
|
|
154
|
+
const result = [];
|
|
155
|
+
const mapFields = (record, fields, depth) => {
|
|
156
|
+
if (depth > 8 || fields.length === 0) return false;
|
|
157
|
+
for (const entry of fields) {
|
|
158
|
+
if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
|
|
159
|
+
if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
|
|
160
|
+
if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
|
|
161
|
+
if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
|
|
162
|
+
const value = record[entry.field];
|
|
163
|
+
if ((value === void 0 || value === null || value === "") && entry.optional) {
|
|
164
|
+
if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (entry.kind === "richText") {
|
|
168
|
+
if (!Array.isArray(value) || value.length > 0 && !isPortableTextArray(value)) return false;
|
|
169
|
+
result.push(...value);
|
|
170
|
+
} else if (entry.kind === "heading" || entry.kind === "paragraph") {
|
|
171
|
+
if (typeof value !== "string") return false;
|
|
172
|
+
result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
|
|
173
|
+
} else if (entry.kind === "link") {
|
|
174
|
+
const href = record[entry.hrefField];
|
|
175
|
+
if (typeof value !== "string" || typeof href !== "string" || !href) return false;
|
|
176
|
+
result.push({
|
|
177
|
+
_type: "block",
|
|
178
|
+
children: [{ _type: "span", text: value, marks: ["link"] }],
|
|
179
|
+
markDefs: [{ _key: "link", _type: "link", href }]
|
|
180
|
+
});
|
|
181
|
+
} else if (entry.kind === "repeat") {
|
|
182
|
+
if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
|
|
183
|
+
for (const item of value) {
|
|
184
|
+
if (!isRecord(item) || !mapFields(item, entry.fields, depth + 1)) return false;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
};
|
|
192
|
+
for (const block of blocks) {
|
|
193
|
+
const fields = mappings?.[String(block["_type"])];
|
|
194
|
+
if (!Array.isArray(fields) || !mapFields(block, fields, 0)) return unavailable();
|
|
195
|
+
}
|
|
196
|
+
return { blocks: result };
|
|
197
|
+
}
|
|
117
198
|
function blocksToPortableText(blocks) {
|
|
118
199
|
const paragraphs = [];
|
|
119
200
|
for (const block of blocks) {
|
|
@@ -197,35 +278,107 @@ function isRecord(value) {
|
|
|
197
278
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
198
279
|
}
|
|
199
280
|
|
|
281
|
+
// src/version.ts
|
|
282
|
+
var PACKAGE_VERSION = "0.9.0";
|
|
283
|
+
|
|
200
284
|
// src/plugin.ts
|
|
201
285
|
var SETTINGS_TOKEN_KEY = "eval_token";
|
|
286
|
+
var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
|
|
287
|
+
var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
|
|
202
288
|
var PLUGIN_SETTINGS_SCHEMA = {
|
|
289
|
+
[SETTINGS_CONTENT_FIELDS_KEY]: {
|
|
290
|
+
type: "string",
|
|
291
|
+
label: "Content fields by collection (JSON)",
|
|
292
|
+
description: "Ordered rendered body fields per collection.",
|
|
293
|
+
multiline: true
|
|
294
|
+
},
|
|
295
|
+
[SETTINGS_BLOCK_MAPPINGS_KEY]: {
|
|
296
|
+
type: "string",
|
|
297
|
+
label: "Custom block mappings (JSON)",
|
|
298
|
+
description: "Ordered mappings for the rendered fields of each custom block type.",
|
|
299
|
+
multiline: true
|
|
300
|
+
},
|
|
203
301
|
[SETTINGS_TOKEN_KEY]: {
|
|
204
302
|
type: "secret",
|
|
205
303
|
label: "EVAL_TOKEN",
|
|
206
304
|
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
207
305
|
}
|
|
208
306
|
};
|
|
209
|
-
|
|
307
|
+
function recordContentCoverage(ctx, documents) {
|
|
308
|
+
const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
|
|
309
|
+
for (const document of documents) {
|
|
310
|
+
if (document.body === void 0 || document.body_capabilities?.length === 0) counts.unavailable++;
|
|
311
|
+
else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
|
|
312
|
+
(capability) => document.body_capabilities?.includes(capability)
|
|
313
|
+
)) counts.complete++;
|
|
314
|
+
else counts.partial++;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
ctx.log?.info?.("aexeo.content_coverage", counts);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function handleAfterDelete(event, ctx) {
|
|
322
|
+
for (const entry of await ctx.kv.list("document:")) {
|
|
323
|
+
const stored = toStoredDocument(entry.value);
|
|
324
|
+
if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
|
|
325
|
+
await ctx.kv.delete(entry.key);
|
|
326
|
+
await ctx.kv.delete(findingsKey(stored.document.route));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
|
|
331
|
+
"LNK001",
|
|
332
|
+
// internal link target missing from the site
|
|
333
|
+
"LNK002",
|
|
334
|
+
// no inbound links
|
|
335
|
+
"LNK003",
|
|
336
|
+
// weak anchor, checked only when the target route exists
|
|
337
|
+
"LNK004",
|
|
338
|
+
// too few inbound links
|
|
339
|
+
"SEO009",
|
|
340
|
+
// hreflang alternate target missing from the site
|
|
341
|
+
"SEO012",
|
|
342
|
+
// hreflang alternate does not link back
|
|
343
|
+
"SEO016",
|
|
344
|
+
// canonical points at another crawlable route
|
|
345
|
+
"SEO017",
|
|
346
|
+
// title and description shared with another route
|
|
347
|
+
"CNT005"
|
|
348
|
+
// visible text duplicated on another route
|
|
349
|
+
]);
|
|
350
|
+
async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields) {
|
|
210
351
|
const adapted = await adaptContentItemWithContext(
|
|
211
352
|
event.content,
|
|
212
353
|
ctx.content,
|
|
213
|
-
ctx.site?.locale
|
|
354
|
+
ctx.site?.locale,
|
|
355
|
+
blockMappings,
|
|
356
|
+
contentFields
|
|
214
357
|
);
|
|
215
358
|
const document = adapted.document;
|
|
359
|
+
recordContentCoverage(ctx, [document]);
|
|
216
360
|
const { kv, log } = ctx;
|
|
217
361
|
const stored = { document, meta: adapted.meta };
|
|
218
|
-
await persistDocument(ctx, stored)
|
|
219
|
-
|
|
362
|
+
if (!await persistDocument(ctx, stored)) {
|
|
363
|
+
log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const result = await evaluator([document], { collections: [adapted.meta.collection] });
|
|
220
367
|
if (!result.ok) {
|
|
221
368
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
222
369
|
detail: result.detail
|
|
223
370
|
});
|
|
224
371
|
return;
|
|
225
372
|
}
|
|
226
|
-
const
|
|
227
|
-
(finding) => finding.
|
|
373
|
+
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
374
|
+
(finding) => SITE_GRAPH_RULE_IDS.has(finding.rule_id)
|
|
228
375
|
);
|
|
376
|
+
const pageFindings = [
|
|
377
|
+
...result.findings.filter(
|
|
378
|
+
(finding) => finding.scope === "page" && !SITE_GRAPH_RULE_IDS.has(finding.rule_id)
|
|
379
|
+
),
|
|
380
|
+
...storedGraphFindings
|
|
381
|
+
];
|
|
229
382
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
230
383
|
{
|
|
231
384
|
route: document.route,
|
|
@@ -234,6 +387,10 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
234
387
|
},
|
|
235
388
|
pageFindings
|
|
236
389
|
);
|
|
390
|
+
if (await isSuperseded(kv, document.route, adapted.meta.updatedAt)) {
|
|
391
|
+
log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
237
394
|
await kv.set(findingsKey(document.route), {
|
|
238
395
|
route: document.route,
|
|
239
396
|
findings: filtered
|
|
@@ -250,7 +407,9 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
250
407
|
totalFindings: 0,
|
|
251
408
|
errors: []
|
|
252
409
|
};
|
|
410
|
+
const storedAtStart = await readAllStoredDocuments(kv);
|
|
253
411
|
const documents = [];
|
|
412
|
+
const documentCollections = [];
|
|
254
413
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
255
414
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
256
415
|
for (const collection of collections) {
|
|
@@ -265,9 +424,12 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
265
424
|
const adapted = await adaptContentItemWithContext(
|
|
266
425
|
item,
|
|
267
426
|
ctx.content,
|
|
268
|
-
ctx.site?.locale
|
|
427
|
+
ctx.site?.locale,
|
|
428
|
+
options.blockMappings,
|
|
429
|
+
options.contentFieldsByCollection?.[collection]
|
|
269
430
|
);
|
|
270
431
|
documents.push(adapted.document);
|
|
432
|
+
documentCollections.push(adapted.meta.collection);
|
|
271
433
|
documentRoutes.add(adapted.document.route);
|
|
272
434
|
adaptedByRoute.set(adapted.document.route, {
|
|
273
435
|
document: adapted.document,
|
|
@@ -283,10 +445,14 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
283
445
|
} while (cursor !== void 0);
|
|
284
446
|
}
|
|
285
447
|
summary.documentsScanned = documents.length;
|
|
448
|
+
recordContentCoverage(ctx, documents);
|
|
286
449
|
for (const stored of adaptedByRoute.values()) {
|
|
287
|
-
await kv
|
|
450
|
+
const current = await readStoredDocument(kv, stored.document.route);
|
|
451
|
+
if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
|
|
452
|
+
await kv.set(documentKey(stored.document.route), stored);
|
|
453
|
+
}
|
|
288
454
|
}
|
|
289
|
-
const result = await options.evaluator(documents);
|
|
455
|
+
const result = await options.evaluator(documents, { collections: documentCollections });
|
|
290
456
|
if (!result.ok) {
|
|
291
457
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
292
458
|
detail: result.detail
|
|
@@ -307,6 +473,9 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
307
473
|
let totalAfterSuppression = 0;
|
|
308
474
|
for (const [route, findings] of findingsByRoute) {
|
|
309
475
|
const stored = adaptedByRoute.get(route);
|
|
476
|
+
if (stored !== void 0 && await isSuperseded(kv, route, stored.meta.updatedAt)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
310
479
|
const context = {
|
|
311
480
|
route,
|
|
312
481
|
...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
|
|
@@ -319,29 +488,52 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
319
488
|
}
|
|
320
489
|
summary.totalFindings = totalAfterSuppression;
|
|
321
490
|
if (summary.errors.length === 0) {
|
|
322
|
-
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
491
|
+
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
323
492
|
}
|
|
324
493
|
return summary;
|
|
325
494
|
}
|
|
495
|
+
function isOlderRevision(revision, other) {
|
|
496
|
+
if (revision === void 0 || other === void 0) return false;
|
|
497
|
+
return Date.parse(revision) < Date.parse(other);
|
|
498
|
+
}
|
|
499
|
+
async function readStoredDocument(kv, route) {
|
|
500
|
+
return toStoredDocument(await kv.get(documentKey(route)));
|
|
501
|
+
}
|
|
502
|
+
async function isSuperseded(kv, route, revision) {
|
|
503
|
+
const current = await readStoredDocument(kv, route);
|
|
504
|
+
return current === null || isOlderRevision(revision, current.meta.updatedAt);
|
|
505
|
+
}
|
|
326
506
|
async function persistDocument(ctx, stored) {
|
|
327
507
|
const route = stored.document.route;
|
|
328
|
-
const
|
|
329
|
-
for (const entry of entries) {
|
|
508
|
+
const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
|
|
330
509
|
const previous = toStoredDocument(entry.value);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
510
|
+
return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
|
|
511
|
+
});
|
|
512
|
+
if (sameEntry.some(
|
|
513
|
+
({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
|
|
514
|
+
)) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
for (const { key, previous } of sameEntry) {
|
|
518
|
+
if (previous.document.route === route) continue;
|
|
519
|
+
await ctx.kv.delete(key);
|
|
335
520
|
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
336
521
|
}
|
|
337
522
|
await ctx.kv.set(documentKey(route), stored);
|
|
523
|
+
return true;
|
|
338
524
|
}
|
|
339
|
-
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
525
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
|
|
340
526
|
const scannedCollections = new Set(collections);
|
|
527
|
+
const revisionAtStart = new Map(
|
|
528
|
+
storedAtStart.map((stored) => [
|
|
529
|
+
stored.document.route,
|
|
530
|
+
stored.meta.updatedAt
|
|
531
|
+
])
|
|
532
|
+
);
|
|
341
533
|
const entries = await ctx.kv.list("document:");
|
|
342
534
|
for (const entry of entries) {
|
|
343
535
|
const stored = toStoredDocument(entry.value);
|
|
344
|
-
if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
|
|
536
|
+
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
537
|
continue;
|
|
346
538
|
}
|
|
347
539
|
await ctx.kv.delete(entry.key);
|
|
@@ -438,6 +630,8 @@ async function handleRefresh(options) {
|
|
|
438
630
|
summary = await evaluateAndPersistAll(options.ctx, {
|
|
439
631
|
collections: options.collections,
|
|
440
632
|
evaluator: options.evaluator,
|
|
633
|
+
blockMappings: options.blockMappings,
|
|
634
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
441
635
|
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
442
636
|
suppressionFilter: options.suppressionFilter
|
|
443
637
|
});
|
|
@@ -578,7 +772,7 @@ function findingsTable(rows) {
|
|
|
578
772
|
}))
|
|
579
773
|
};
|
|
580
774
|
}
|
|
581
|
-
async function renderScoreWidget(kv, tuning) {
|
|
775
|
+
async function renderScoreWidget(kv, tuning, log) {
|
|
582
776
|
const documents = await readAllDocuments(kv);
|
|
583
777
|
if (documents.length === 0) {
|
|
584
778
|
return {
|
|
@@ -591,28 +785,48 @@ async function renderScoreWidget(kv, tuning) {
|
|
|
591
785
|
]
|
|
592
786
|
};
|
|
593
787
|
}
|
|
594
|
-
|
|
788
|
+
let score;
|
|
789
|
+
try {
|
|
790
|
+
score = await tuning.score(documents, kv);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
log?.error("Aexeo score widget failed to initialize", { error });
|
|
793
|
+
return {
|
|
794
|
+
blocks: [
|
|
795
|
+
{ type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
|
|
796
|
+
{ type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
|
|
797
|
+
]
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (score === null) {
|
|
801
|
+
return {
|
|
802
|
+
blocks: [
|
|
803
|
+
{ type: "header", text: "SEO score" },
|
|
804
|
+
{
|
|
805
|
+
type: "context",
|
|
806
|
+
text: "The site score is unavailable in sandboxed mode. Findings are not affected."
|
|
807
|
+
}
|
|
808
|
+
]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
595
811
|
const blocks = [
|
|
596
812
|
{
|
|
597
813
|
type: "stats",
|
|
598
814
|
items: [
|
|
599
|
-
{
|
|
600
|
-
|
|
815
|
+
{
|
|
816
|
+
label: "Citation",
|
|
817
|
+
value: score.citation_readiness_score?.toString() ?? "unknown"
|
|
818
|
+
},
|
|
601
819
|
{
|
|
602
820
|
label: tuning.truthLabel(score),
|
|
603
|
-
value:
|
|
821
|
+
value: score.truth_consistency_score?.toString() ?? "unknown"
|
|
604
822
|
},
|
|
605
|
-
{
|
|
823
|
+
{
|
|
824
|
+
label: "Answers",
|
|
825
|
+
value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
|
|
826
|
+
}
|
|
606
827
|
]
|
|
607
828
|
}
|
|
608
829
|
];
|
|
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
830
|
if (score.blockers.length > 0) {
|
|
617
831
|
blocks.push({
|
|
618
832
|
type: "context",
|
|
@@ -695,11 +909,65 @@ __export(aexeo_emdash_bridge_bg_exports, {
|
|
|
695
909
|
__wbg_set_wasm: () => __wbg_set_wasm,
|
|
696
910
|
__wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
|
|
697
911
|
__wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
|
|
912
|
+
evaluateContent: () => evaluateContent,
|
|
913
|
+
evaluateContentCollection: () => evaluateContentCollection,
|
|
698
914
|
evaluateDocuments: () => evaluateDocuments,
|
|
699
915
|
generateFactsPrompt: () => generateFactsPrompt,
|
|
700
916
|
scoreIntelligence: () => scoreIntelligence,
|
|
701
917
|
validateFactsManifest: () => validateFactsManifest
|
|
702
918
|
});
|
|
919
|
+
function evaluateContent(document_json, preset_name, sources_json, evidence_json) {
|
|
920
|
+
let deferred6_0;
|
|
921
|
+
let deferred6_1;
|
|
922
|
+
try {
|
|
923
|
+
const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
924
|
+
const len0 = WASM_VECTOR_LEN;
|
|
925
|
+
const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
926
|
+
const len1 = WASM_VECTOR_LEN;
|
|
927
|
+
var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
928
|
+
var len2 = WASM_VECTOR_LEN;
|
|
929
|
+
var ptr3 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
930
|
+
var len3 = WASM_VECTOR_LEN;
|
|
931
|
+
const ret = wasm.evaluateContent(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
|
|
932
|
+
var ptr5 = ret[0];
|
|
933
|
+
var len5 = ret[1];
|
|
934
|
+
if (ret[3]) {
|
|
935
|
+
ptr5 = 0;
|
|
936
|
+
len5 = 0;
|
|
937
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
938
|
+
}
|
|
939
|
+
deferred6_0 = ptr5;
|
|
940
|
+
deferred6_1 = len5;
|
|
941
|
+
return getStringFromWasm0(ptr5, len5);
|
|
942
|
+
} finally {
|
|
943
|
+
wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
function evaluateContentCollection(documents_json, preset_name, sources_json) {
|
|
947
|
+
let deferred5_0;
|
|
948
|
+
let deferred5_1;
|
|
949
|
+
try {
|
|
950
|
+
const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
951
|
+
const len0 = WASM_VECTOR_LEN;
|
|
952
|
+
const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
953
|
+
const len1 = WASM_VECTOR_LEN;
|
|
954
|
+
var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
955
|
+
var len2 = WASM_VECTOR_LEN;
|
|
956
|
+
const ret = wasm.evaluateContentCollection(ptr0, len0, ptr1, len1, ptr2, len2);
|
|
957
|
+
var ptr4 = ret[0];
|
|
958
|
+
var len4 = ret[1];
|
|
959
|
+
if (ret[3]) {
|
|
960
|
+
ptr4 = 0;
|
|
961
|
+
len4 = 0;
|
|
962
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
963
|
+
}
|
|
964
|
+
deferred5_0 = ptr4;
|
|
965
|
+
deferred5_1 = len4;
|
|
966
|
+
return getStringFromWasm0(ptr4, len4);
|
|
967
|
+
} finally {
|
|
968
|
+
wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
703
971
|
function evaluateDocuments(documents_json, config_json) {
|
|
704
972
|
let deferred4_0;
|
|
705
973
|
let deferred4_1;
|
|
@@ -969,6 +1237,10 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
|
|
|
969
1237
|
await ensureInitialized();
|
|
970
1238
|
return validateFactsManifest(manifestJson, documentsJson);
|
|
971
1239
|
}
|
|
1240
|
+
async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson) {
|
|
1241
|
+
await ensureInitialized();
|
|
1242
|
+
return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson);
|
|
1243
|
+
}
|
|
972
1244
|
|
|
973
1245
|
// src/data-route.ts
|
|
974
1246
|
async function handleDataRoute(ctx, options) {
|
|
@@ -977,6 +1249,8 @@ async function handleDataRoute(ctx, options) {
|
|
|
977
1249
|
summary = await evaluateAndPersistAll(ctx, {
|
|
978
1250
|
collections: options.collections,
|
|
979
1251
|
evaluator: options.evaluator,
|
|
1252
|
+
blockMappings: options.blockMappings,
|
|
1253
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
980
1254
|
...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
|
|
981
1255
|
});
|
|
982
1256
|
await persistManifestStateFindings(ctx);
|
|
@@ -1245,12 +1519,16 @@ async function handleSave(ctx, body) {
|
|
|
1245
1519
|
return { saved: true };
|
|
1246
1520
|
}
|
|
1247
1521
|
|
|
1248
|
-
// src/presence-route.ts
|
|
1249
|
-
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
1250
|
-
|
|
1251
1522
|
// src/presence-check.ts
|
|
1252
1523
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
1253
1524
|
var USER_AGENT = "aexeo-emdash-plugin/0.8 (+https://github.com/schiste/Aexeo; entity-presence diagnostic)";
|
|
1525
|
+
var PRESENCE_HOSTS = [
|
|
1526
|
+
"en.wikipedia.org",
|
|
1527
|
+
"www.wikidata.org",
|
|
1528
|
+
"api.github.com",
|
|
1529
|
+
"rdap.org",
|
|
1530
|
+
"index.commoncrawl.org"
|
|
1531
|
+
];
|
|
1254
1532
|
var SOURCE_NAMES = [
|
|
1255
1533
|
"wikipedia",
|
|
1256
1534
|
"wikidata",
|
|
@@ -1307,7 +1585,7 @@ async function checkWikipedia(input, fetcher) {
|
|
|
1307
1585
|
return notFound2("wikipedia");
|
|
1308
1586
|
}
|
|
1309
1587
|
const title = String(titles[0]);
|
|
1310
|
-
if (!
|
|
1588
|
+
if (!namesEntity(title, input)) {
|
|
1311
1589
|
return notFound2("wikipedia");
|
|
1312
1590
|
}
|
|
1313
1591
|
const articleUrl = String(urls[0] ?? "");
|
|
@@ -1337,7 +1615,7 @@ async function checkWikidata(input, fetcher) {
|
|
|
1337
1615
|
return notFound2("wikidata");
|
|
1338
1616
|
}
|
|
1339
1617
|
const label = top.label ?? input.name;
|
|
1340
|
-
if (!
|
|
1618
|
+
if (!namesEntity(label, input)) {
|
|
1341
1619
|
return notFound2("wikidata");
|
|
1342
1620
|
}
|
|
1343
1621
|
return found("wikidata", {
|
|
@@ -1426,9 +1704,27 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1426
1704
|
"no website URL in manifest, can't query crawl index"
|
|
1427
1705
|
);
|
|
1428
1706
|
}
|
|
1429
|
-
|
|
1707
|
+
let index;
|
|
1708
|
+
try {
|
|
1709
|
+
const res = await timedFetch(
|
|
1710
|
+
fetcher,
|
|
1711
|
+
"https://index.commoncrawl.org/collinfo.json"
|
|
1712
|
+
);
|
|
1713
|
+
if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
|
|
1714
|
+
const newest = newestCrawlId(await res.json());
|
|
1715
|
+
if (newest === null) {
|
|
1716
|
+
return unreachable(
|
|
1717
|
+
"common_crawl",
|
|
1718
|
+
"no crawl index listed in collinfo.json"
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
index = newest;
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
return unreachable("common_crawl", errMsg(error));
|
|
1724
|
+
}
|
|
1430
1725
|
const url = new URL(`https://index.commoncrawl.org/${index}-index`);
|
|
1431
1726
|
url.searchParams.set("url", host);
|
|
1727
|
+
url.searchParams.set("matchType", "domain");
|
|
1432
1728
|
url.searchParams.set("output", "json");
|
|
1433
1729
|
url.searchParams.set("limit", "1");
|
|
1434
1730
|
try {
|
|
@@ -1455,6 +1751,16 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1455
1751
|
return unreachable("common_crawl", errMsg(error));
|
|
1456
1752
|
}
|
|
1457
1753
|
}
|
|
1754
|
+
function newestCrawlId(collinfo) {
|
|
1755
|
+
if (!Array.isArray(collinfo)) return null;
|
|
1756
|
+
let newest = null;
|
|
1757
|
+
for (const crawl of collinfo) {
|
|
1758
|
+
const id = crawl?.id;
|
|
1759
|
+
if (typeof id !== "string" || !/^CC-MAIN-[A-Za-z0-9-]+$/.test(id)) continue;
|
|
1760
|
+
if (newest === null || id > newest) newest = id;
|
|
1761
|
+
}
|
|
1762
|
+
return newest;
|
|
1763
|
+
}
|
|
1458
1764
|
async function timedFetch(fetcher, url, init) {
|
|
1459
1765
|
const controller = new AbortController();
|
|
1460
1766
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -1500,10 +1806,45 @@ function errMsg(error) {
|
|
|
1500
1806
|
}
|
|
1501
1807
|
return String(error);
|
|
1502
1808
|
}
|
|
1503
|
-
function
|
|
1504
|
-
const
|
|
1505
|
-
|
|
1506
|
-
|
|
1809
|
+
function namesEntity(candidate, input) {
|
|
1810
|
+
const normalized = normalizeEntityName(candidate);
|
|
1811
|
+
return [input.name, ...input.aliases ?? []].some(
|
|
1812
|
+
(name) => normalizeEntityName(name) === normalized
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
var LEGAL_FORMS = /* @__PURE__ */ new Set([
|
|
1816
|
+
"ab",
|
|
1817
|
+
"ag",
|
|
1818
|
+
"bv",
|
|
1819
|
+
"co",
|
|
1820
|
+
"company",
|
|
1821
|
+
"corp",
|
|
1822
|
+
"corporation",
|
|
1823
|
+
"gmbh",
|
|
1824
|
+
"inc",
|
|
1825
|
+
"incorporated",
|
|
1826
|
+
"kk",
|
|
1827
|
+
"limited",
|
|
1828
|
+
"llc",
|
|
1829
|
+
"llp",
|
|
1830
|
+
"lp",
|
|
1831
|
+
"ltd",
|
|
1832
|
+
"nv",
|
|
1833
|
+
"oy",
|
|
1834
|
+
"plc",
|
|
1835
|
+
"pty",
|
|
1836
|
+
"sa",
|
|
1837
|
+
"sarl",
|
|
1838
|
+
"sas",
|
|
1839
|
+
"srl"
|
|
1840
|
+
]);
|
|
1841
|
+
function normalizeEntityName(name) {
|
|
1842
|
+
const words = name.replace(/\([^)]*\)/g, " ").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0);
|
|
1843
|
+
let end = words.length;
|
|
1844
|
+
while (end > 0 && LEGAL_FORMS.has(words[end - 1])) end -= 1;
|
|
1845
|
+
const start = words[0] === "the" ? 1 : 0;
|
|
1846
|
+
const core = words.slice(start, end);
|
|
1847
|
+
return (core.length > 0 ? core : words).join(" ");
|
|
1507
1848
|
}
|
|
1508
1849
|
function sanitizeGitHubHandle(name) {
|
|
1509
1850
|
const handle = name.replace(/[\s_]+/g, "-");
|
|
@@ -1542,6 +1883,7 @@ function formatCdxTimestamp(ts) {
|
|
|
1542
1883
|
}
|
|
1543
1884
|
|
|
1544
1885
|
// src/presence-route.ts
|
|
1886
|
+
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
1545
1887
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1546
1888
|
var PRESENCE_KEY = "presence:current";
|
|
1547
1889
|
async function handlePresenceRoute(ctx) {
|
|
@@ -1664,8 +2006,61 @@ function globToMatcher(pattern) {
|
|
|
1664
2006
|
return (route) => re.test(route);
|
|
1665
2007
|
}
|
|
1666
2008
|
|
|
1667
|
-
// src/
|
|
1668
|
-
|
|
2009
|
+
// src/content.ts
|
|
2010
|
+
async function evaluateContent3(document, preset, sources = {}, evidenceSources, onTelemetry) {
|
|
2011
|
+
const started = onTelemetry ? performance.now() : 0;
|
|
2012
|
+
const documentJson = JSON.stringify(document);
|
|
2013
|
+
const sourcesJson = JSON.stringify(sources);
|
|
2014
|
+
const result = evidenceSources === void 0 ? await evaluateContent2(documentJson, preset, sourcesJson) : await evaluateContent2(documentJson, preset, sourcesJson, JSON.stringify(evidenceSources));
|
|
2015
|
+
const report = JSON.parse(result);
|
|
2016
|
+
if (onTelemetry) {
|
|
2017
|
+
const event = { evaluation_ms: performance.now() - started, source_locations_mapped: report.source_locations?.length ?? 0 };
|
|
2018
|
+
for (const gate of report.gates) {
|
|
2019
|
+
const checked = gate.measurements?.checked_destinations;
|
|
2020
|
+
const invalid = gate.measurements?.invalid_destinations;
|
|
2021
|
+
if (checked !== void 0 && invalid !== void 0) {
|
|
2022
|
+
event.link_destinations ??= { checked: 0, invalid: 0 };
|
|
2023
|
+
event.link_destinations.checked += checked;
|
|
2024
|
+
event.link_destinations.invalid += invalid;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
onTelemetry(event);
|
|
2028
|
+
}
|
|
2029
|
+
return report;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// src/content-evaluator.ts
|
|
2033
|
+
function withContentPreset(base, preset, evaluate) {
|
|
2034
|
+
if (!preset) return base;
|
|
2035
|
+
return async (documents, context) => {
|
|
2036
|
+
const result = await base(documents, context);
|
|
2037
|
+
if (!result.ok) return result;
|
|
2038
|
+
const findings = [...result.findings];
|
|
2039
|
+
try {
|
|
2040
|
+
for (const [index, document] of documents.entries()) {
|
|
2041
|
+
const collection = context?.collections[index];
|
|
2042
|
+
if (preset.collections !== void 0) {
|
|
2043
|
+
if (collection === void 0) throw new Error("collection is required for a scoped content preset");
|
|
2044
|
+
if (!preset.collections.includes(collection)) continue;
|
|
2045
|
+
}
|
|
2046
|
+
const selected = collection !== void 0 && preset.byCollection !== void 0 && Object.hasOwn(preset.byCollection, collection) ? preset.byCollection[collection] : void 0;
|
|
2047
|
+
const name = selected ?? preset.name;
|
|
2048
|
+
if (!name) throw new Error(`empty preset name for collection ${collection}`);
|
|
2049
|
+
const report = preset.evidenceSources === void 0 ? await evaluate(document, name, preset.sources) : await evaluate(document, name, preset.sources, preset.evidenceSources);
|
|
2050
|
+
const incomplete = report.gates.filter((gate) => gate.status === "not_checked");
|
|
2051
|
+
if (incomplete.length) return {
|
|
2052
|
+
ok: false,
|
|
2053
|
+
reason: "content_preset_incomplete",
|
|
2054
|
+
detail: `${document.route}: ${incomplete.map((gate) => `${gate.gate_id}: ${gate.reason}`).join(", ")}`
|
|
2055
|
+
};
|
|
2056
|
+
for (const gate of report.gates) findings.push(...gate.findings);
|
|
2057
|
+
}
|
|
2058
|
+
return { ok: true, findings };
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
return { ok: false, reason: "content_preset_error", detail: error instanceof Error ? error.message : String(error) };
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
1669
2064
|
|
|
1670
2065
|
// src/configured.ts
|
|
1671
2066
|
import { definePlugin } from "emdash";
|
|
@@ -1731,10 +2126,15 @@ function createPlugin(options = {}) {
|
|
|
1731
2126
|
options.excludeCollections
|
|
1732
2127
|
);
|
|
1733
2128
|
const suppressionFilter = compileSuppressions(options.suppressions);
|
|
2129
|
+
const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3);
|
|
1734
2130
|
return definePlugin({
|
|
1735
2131
|
id: "aexeo-emdash",
|
|
1736
2132
|
version: PACKAGE_VERSION,
|
|
1737
|
-
|
|
2133
|
+
// The entity-presence route fetches its public sources directly (see
|
|
2134
|
+
// presence-check.ts). rdap.org then redirects to the registry for the
|
|
2135
|
+
// domain's TLD; those registries cannot be listed here.
|
|
2136
|
+
capabilities: ["content:read", "network:request"],
|
|
2137
|
+
allowedHosts: [...PRESENCE_HOSTS],
|
|
1738
2138
|
hooks: {
|
|
1739
2139
|
// afterSave processes one saved document at a time — collections
|
|
1740
2140
|
// list isn't needed here. The hook always runs regardless of
|
|
@@ -1742,16 +2142,26 @@ function createPlugin(options = {}) {
|
|
|
1742
2142
|
"content:afterSave": (event, ctx) => handleAfterSaveConfigured(
|
|
1743
2143
|
event,
|
|
1744
2144
|
ctx,
|
|
1745
|
-
|
|
1746
|
-
suppressionFilter
|
|
1747
|
-
|
|
2145
|
+
evaluator,
|
|
2146
|
+
suppressionFilter,
|
|
2147
|
+
options.blockMappings,
|
|
2148
|
+
options.contentFieldsByCollection?.[event.content.type]
|
|
2149
|
+
),
|
|
2150
|
+
"content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
|
|
1748
2151
|
},
|
|
2152
|
+
// emdash requires plugins:manage (admin) on a route that declares no
|
|
2153
|
+
// permission. Reading findings only needs plugins:read (editor); anything
|
|
2154
|
+
// that writes stays admin, stated explicitly so the split is visible.
|
|
1749
2155
|
routes: {
|
|
1750
2156
|
// The Refresh sweep (handleAdminRoute → handleRefresh) is the
|
|
1751
2157
|
// one consumer of `collections`. Bind the resolved list here so
|
|
1752
2158
|
// every request handles the same set without re-reading options.
|
|
2159
|
+
//
|
|
2160
|
+
// This route serves the pages and the dashboard widget, and also the
|
|
2161
|
+
// refresh_findings action, which handleAdminRoute gates itself.
|
|
1753
2162
|
admin: {
|
|
1754
|
-
|
|
2163
|
+
permission: "plugins:read",
|
|
2164
|
+
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection)
|
|
1755
2165
|
},
|
|
1756
2166
|
// JSON data endpoint for the React adminEntry. Two routes:
|
|
1757
2167
|
// POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
|
|
@@ -1761,19 +2171,25 @@ function createPlugin(options = {}) {
|
|
|
1761
2171
|
// emdash edit page and the public site, which Block Kit can't
|
|
1762
2172
|
// express in any element type as of emdash 0.8.0.
|
|
1763
2173
|
data: {
|
|
2174
|
+
permission: "plugins:read",
|
|
1764
2175
|
handler: async (ctx) => (await handleDataRoute(ctx, {
|
|
1765
2176
|
collections,
|
|
1766
|
-
evaluator
|
|
2177
|
+
evaluator,
|
|
1767
2178
|
refresh: false,
|
|
1768
|
-
suppressionFilter
|
|
2179
|
+
suppressionFilter,
|
|
2180
|
+
blockMappings: options.blockMappings,
|
|
2181
|
+
contentFieldsByCollection: options.contentFieldsByCollection
|
|
1769
2182
|
})).payload
|
|
1770
2183
|
},
|
|
1771
2184
|
refresh: {
|
|
2185
|
+
permission: "plugins:manage",
|
|
1772
2186
|
handler: (ctx) => handleDataRoute(ctx, {
|
|
1773
2187
|
collections,
|
|
1774
|
-
evaluator
|
|
2188
|
+
evaluator,
|
|
1775
2189
|
refresh: true,
|
|
1776
|
-
suppressionFilter
|
|
2190
|
+
suppressionFilter,
|
|
2191
|
+
blockMappings: options.blockMappings,
|
|
2192
|
+
contentFieldsByCollection: options.contentFieldsByCollection
|
|
1777
2193
|
})
|
|
1778
2194
|
},
|
|
1779
2195
|
// /facts route serves the truth-manifest authoring UI. It
|
|
@@ -1781,6 +2197,7 @@ function createPlugin(options = {}) {
|
|
|
1781
2197
|
// onto one POST endpoint via a "kind" body field — see
|
|
1782
2198
|
// facts-route.ts for the full contract.
|
|
1783
2199
|
facts: {
|
|
2200
|
+
permission: "plugins:manage",
|
|
1784
2201
|
handler: (ctx) => handleFactsRoute(ctx)
|
|
1785
2202
|
},
|
|
1786
2203
|
// /presence route serves the layer-4 entity-presence
|
|
@@ -1790,6 +2207,7 @@ function createPlugin(options = {}) {
|
|
|
1790
2207
|
// results in KV for 24h. Multiplexes data / refresh kinds —
|
|
1791
2208
|
// see presence-route.ts.
|
|
1792
2209
|
presence: {
|
|
2210
|
+
permission: "plugins:manage",
|
|
1793
2211
|
handler: (ctx) => handlePresenceRoute(ctx)
|
|
1794
2212
|
}
|
|
1795
2213
|
},
|
|
@@ -1821,6 +2239,7 @@ function createPlugin(options = {}) {
|
|
|
1821
2239
|
});
|
|
1822
2240
|
}
|
|
1823
2241
|
var configured_default = createPlugin;
|
|
2242
|
+
var ADMIN_ROLE = 50;
|
|
1824
2243
|
var adminViewTuning = {
|
|
1825
2244
|
emptyFindingsText: "Once a Refresh runs, findings will list here.",
|
|
1826
2245
|
emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
|
|
@@ -1828,7 +2247,7 @@ var adminViewTuning = {
|
|
|
1828
2247
|
score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
|
|
1829
2248
|
truthLabel: formatTruthLabel
|
|
1830
2249
|
};
|
|
1831
|
-
async function handleAdminRoute(ctx, collections, suppressionFilter) {
|
|
2250
|
+
async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection) {
|
|
1832
2251
|
const body = normalizeInteraction(ctx.input);
|
|
1833
2252
|
if (body.type === "page_load") {
|
|
1834
2253
|
return handlePageLoad(ctx, body.page);
|
|
@@ -1838,12 +2257,28 @@ async function handleAdminRoute(ctx, collections, suppressionFilter) {
|
|
|
1838
2257
|
return renderDocumentPanel(ctx.kv, body.value);
|
|
1839
2258
|
}
|
|
1840
2259
|
if (body.action_id === "refresh_findings") {
|
|
2260
|
+
if ((ctx.user?.role ?? 0) < ADMIN_ROLE) {
|
|
2261
|
+
const page = await renderFindingsPage(ctx.kv, adminViewTuning);
|
|
2262
|
+
return {
|
|
2263
|
+
...page,
|
|
2264
|
+
blocks: [
|
|
2265
|
+
{
|
|
2266
|
+
type: "banner",
|
|
2267
|
+
title: "Only administrators can run Refresh.",
|
|
2268
|
+
variant: "error"
|
|
2269
|
+
},
|
|
2270
|
+
...page.blocks
|
|
2271
|
+
]
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
1841
2274
|
return handleRefresh({
|
|
1842
2275
|
ctx,
|
|
1843
2276
|
collections,
|
|
1844
|
-
evaluator
|
|
2277
|
+
evaluator,
|
|
1845
2278
|
suppressionFilter,
|
|
1846
|
-
tuning: adminViewTuning
|
|
2279
|
+
tuning: adminViewTuning,
|
|
2280
|
+
blockMappings,
|
|
2281
|
+
contentFieldsByCollection
|
|
1847
2282
|
});
|
|
1848
2283
|
}
|
|
1849
2284
|
if (body.action_id.startsWith("filter:")) {
|
|
@@ -1868,7 +2303,7 @@ async function handlePageLoad(ctx, page) {
|
|
|
1868
2303
|
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1869
2304
|
}
|
|
1870
2305
|
if (normalized === "widget:aexeo-score") {
|
|
1871
|
-
return renderScoreWidget(ctx.kv, adminViewTuning);
|
|
2306
|
+
return renderScoreWidget(ctx.kv, adminViewTuning, ctx.log);
|
|
1872
2307
|
}
|
|
1873
2308
|
if (normalized === "document") {
|
|
1874
2309
|
return renderDocumentPanel(ctx.kv);
|