@aeptus/aexeo-emdash 0.8.18 → 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 +196 -4
- package/INSTALL.md +126 -8
- package/LICENSE +0 -0
- package/README.md +12 -5
- package/dist/adapter.d.ts +30 -2
- package/dist/adapter.d.ts.map +1 -1
- package/dist/admin/Absorbability.d.ts +0 -0
- package/dist/admin/Absorbability.d.ts.map +0 -0
- package/dist/admin/Accessibility.d.ts +0 -0
- package/dist/admin/Accessibility.d.ts.map +0 -0
- package/dist/admin/Citability.d.ts +0 -0
- package/dist/admin/Citability.d.ts.map +0 -0
- package/dist/admin/EntityLegitimacy.d.ts +0 -0
- package/dist/admin/EntityLegitimacy.d.ts.map +0 -0
- package/dist/admin/EntityPresence.d.ts +0 -0
- package/dist/admin/EntityPresence.d.ts.map +0 -0
- package/dist/admin/Facts.d.ts +0 -0
- package/dist/admin/Facts.d.ts.map +0 -0
- package/dist/admin/Findings.d.ts +0 -0
- package/dist/admin/Findings.d.ts.map +0 -0
- package/dist/admin/PillarView.d.ts +0 -0
- package/dist/admin/PillarView.d.ts.map +0 -0
- package/dist/admin/Retrievability.d.ts +0 -0
- package/dist/admin/Retrievability.d.ts.map +0 -0
- package/dist/admin.d.ts +0 -0
- 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 +817 -334
- 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/facts-route.d.ts +0 -0
- package/dist/facts-route.d.ts.map +0 -0
- package/dist/findings-view.d.ts +141 -0
- package/dist/findings-view.d.ts.map +1 -0
- package/dist/index.d.ts +15 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -5
- package/dist/inventory.d.ts +51 -0
- package/dist/inventory.d.ts.map +1 -0
- package/dist/plugin.d.ts +51 -4
- 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/presence-route.d.ts +0 -0
- package/dist/presence-route.d.ts.map +0 -0
- package/dist/sandbox-entry.d.ts +4 -23
- package/dist/sandbox-entry.d.ts.map +1 -1
- package/dist/sandbox-entry.js +738 -396
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.d.ts.map +1 -1
- package/dist/sidecar-url.d.ts +18 -0
- package/dist/sidecar-url.d.ts.map +1 -0
- package/dist/sidecar.d.ts +7 -1
- package/dist/sidecar.d.ts.map +1 -1
- package/dist/suppressions.d.ts +0 -0
- package/dist/suppressions.d.ts.map +0 -0
- 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 +21 -8
- 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/indexnow.d.ts +0 -15
- package/dist/indexnow.d.ts.map +0 -1
- package/dist/mcp.d.ts +0 -28
- package/dist/mcp.d.ts.map +0 -1
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,27 +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";
|
|
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
|
+
},
|
|
301
|
+
[SETTINGS_TOKEN_KEY]: {
|
|
302
|
+
type: "secret",
|
|
303
|
+
label: "EVAL_TOKEN",
|
|
304
|
+
description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
|
|
305
|
+
}
|
|
306
|
+
};
|
|
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) {
|
|
202
351
|
const adapted = await adaptContentItemWithContext(
|
|
203
352
|
event.content,
|
|
204
353
|
ctx.content,
|
|
205
|
-
ctx.site?.locale
|
|
354
|
+
ctx.site?.locale,
|
|
355
|
+
blockMappings,
|
|
356
|
+
contentFields
|
|
206
357
|
);
|
|
207
358
|
const document = adapted.document;
|
|
359
|
+
recordContentCoverage(ctx, [document]);
|
|
208
360
|
const { kv, log } = ctx;
|
|
209
361
|
const stored = { document, meta: adapted.meta };
|
|
210
|
-
await persistDocument(ctx, stored)
|
|
211
|
-
|
|
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] });
|
|
212
367
|
if (!result.ok) {
|
|
213
368
|
log?.error?.(`afterSave evaluator failure (${result.reason})`, {
|
|
214
369
|
detail: result.detail
|
|
215
370
|
});
|
|
216
371
|
return;
|
|
217
372
|
}
|
|
218
|
-
const
|
|
219
|
-
(finding) => finding.
|
|
373
|
+
const storedGraphFindings = (await readFindings(kv, document.route)).filter(
|
|
374
|
+
(finding) => SITE_GRAPH_RULE_IDS.has(finding.rule_id)
|
|
220
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
|
+
];
|
|
221
382
|
const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
|
|
222
383
|
{
|
|
223
384
|
route: document.route,
|
|
@@ -226,6 +387,10 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
|
|
|
226
387
|
},
|
|
227
388
|
pageFindings
|
|
228
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
|
+
}
|
|
229
394
|
await kv.set(findingsKey(document.route), {
|
|
230
395
|
route: document.route,
|
|
231
396
|
findings: filtered
|
|
@@ -242,7 +407,9 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
242
407
|
totalFindings: 0,
|
|
243
408
|
errors: []
|
|
244
409
|
};
|
|
410
|
+
const storedAtStart = await readAllStoredDocuments(kv);
|
|
245
411
|
const documents = [];
|
|
412
|
+
const documentCollections = [];
|
|
246
413
|
const adaptedByRoute = /* @__PURE__ */ new Map();
|
|
247
414
|
const documentRoutes = /* @__PURE__ */ new Set();
|
|
248
415
|
for (const collection of collections) {
|
|
@@ -257,9 +424,12 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
257
424
|
const adapted = await adaptContentItemWithContext(
|
|
258
425
|
item,
|
|
259
426
|
ctx.content,
|
|
260
|
-
ctx.site?.locale
|
|
427
|
+
ctx.site?.locale,
|
|
428
|
+
options.blockMappings,
|
|
429
|
+
options.contentFieldsByCollection?.[collection]
|
|
261
430
|
);
|
|
262
431
|
documents.push(adapted.document);
|
|
432
|
+
documentCollections.push(adapted.meta.collection);
|
|
263
433
|
documentRoutes.add(adapted.document.route);
|
|
264
434
|
adaptedByRoute.set(adapted.document.route, {
|
|
265
435
|
document: adapted.document,
|
|
@@ -275,10 +445,14 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
275
445
|
} while (cursor !== void 0);
|
|
276
446
|
}
|
|
277
447
|
summary.documentsScanned = documents.length;
|
|
448
|
+
recordContentCoverage(ctx, documents);
|
|
278
449
|
for (const stored of adaptedByRoute.values()) {
|
|
279
|
-
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
|
+
}
|
|
280
454
|
}
|
|
281
|
-
const result = await options.evaluator(documents);
|
|
455
|
+
const result = await options.evaluator(documents, { collections: documentCollections });
|
|
282
456
|
if (!result.ok) {
|
|
283
457
|
log?.error?.(`aexeo evaluator failure (${result.reason})`, {
|
|
284
458
|
detail: result.detail
|
|
@@ -299,6 +473,9 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
299
473
|
let totalAfterSuppression = 0;
|
|
300
474
|
for (const [route, findings] of findingsByRoute) {
|
|
301
475
|
const stored = adaptedByRoute.get(route);
|
|
476
|
+
if (stored !== void 0 && await isSuperseded(kv, route, stored.meta.updatedAt)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
302
479
|
const context = {
|
|
303
480
|
route,
|
|
304
481
|
...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
|
|
@@ -311,29 +488,52 @@ async function evaluateAndPersistAll(ctx, options) {
|
|
|
311
488
|
}
|
|
312
489
|
summary.totalFindings = totalAfterSuppression;
|
|
313
490
|
if (summary.errors.length === 0) {
|
|
314
|
-
await removeStaleDocuments(ctx, collections, documentRoutes);
|
|
491
|
+
await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
|
|
315
492
|
}
|
|
316
493
|
return summary;
|
|
317
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
|
+
}
|
|
318
506
|
async function persistDocument(ctx, stored) {
|
|
319
507
|
const route = stored.document.route;
|
|
320
|
-
const
|
|
321
|
-
for (const entry of entries) {
|
|
508
|
+
const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
|
|
322
509
|
const previous = toStoredDocument(entry.value);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
327
520
|
await ctx.kv.delete(findingsKey(previous.document.route));
|
|
328
521
|
}
|
|
329
522
|
await ctx.kv.set(documentKey(route), stored);
|
|
523
|
+
return true;
|
|
330
524
|
}
|
|
331
|
-
async function removeStaleDocuments(ctx, collections, currentRoutes) {
|
|
525
|
+
async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
|
|
332
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
|
+
);
|
|
333
533
|
const entries = await ctx.kv.list("document:");
|
|
334
534
|
for (const entry of entries) {
|
|
335
535
|
const stored = toStoredDocument(entry.value);
|
|
336
|
-
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) {
|
|
337
537
|
continue;
|
|
338
538
|
}
|
|
339
539
|
await ctx.kv.delete(entry.key);
|
|
@@ -394,6 +594,312 @@ async function readFindings(kv, route) {
|
|
|
394
594
|
return stored.findings;
|
|
395
595
|
}
|
|
396
596
|
|
|
597
|
+
// src/findings-view.ts
|
|
598
|
+
function normalizeInteraction(input) {
|
|
599
|
+
if (!input || typeof input !== "object") {
|
|
600
|
+
return { type: "page_load", page: "/findings" };
|
|
601
|
+
}
|
|
602
|
+
const raw = input;
|
|
603
|
+
if (raw.type === "page_load") {
|
|
604
|
+
return {
|
|
605
|
+
type: "page_load",
|
|
606
|
+
page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
if (raw.type === "block_action") {
|
|
610
|
+
return {
|
|
611
|
+
type: "block_action",
|
|
612
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
613
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
614
|
+
value: raw.value
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
if (raw.type === "form_submit") {
|
|
618
|
+
return {
|
|
619
|
+
type: "form_submit",
|
|
620
|
+
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
621
|
+
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
622
|
+
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
return { type: "page_load", page: "/findings" };
|
|
626
|
+
}
|
|
627
|
+
async function handleRefresh(options) {
|
|
628
|
+
let summary;
|
|
629
|
+
try {
|
|
630
|
+
summary = await evaluateAndPersistAll(options.ctx, {
|
|
631
|
+
collections: options.collections,
|
|
632
|
+
evaluator: options.evaluator,
|
|
633
|
+
blockMappings: options.blockMappings,
|
|
634
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
635
|
+
// Always forwarded. See RefreshOptions.suppressionFilter.
|
|
636
|
+
suppressionFilter: options.suppressionFilter
|
|
637
|
+
});
|
|
638
|
+
} catch (err) {
|
|
639
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
640
|
+
const message = `Refresh failed: ${detail}`;
|
|
641
|
+
return {
|
|
642
|
+
blocks: [
|
|
643
|
+
{ type: "header", text: "SEO findings" },
|
|
644
|
+
{
|
|
645
|
+
type: "banner",
|
|
646
|
+
title: message,
|
|
647
|
+
variant: options.tuning.refreshFailureVariant
|
|
648
|
+
}
|
|
649
|
+
],
|
|
650
|
+
toast: { message, type: "error" }
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
|
|
654
|
+
const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
|
|
655
|
+
if (summary.errors.length > 0) {
|
|
656
|
+
refreshed.blocks.unshift({
|
|
657
|
+
type: "banner",
|
|
658
|
+
title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
|
|
659
|
+
variant: "alert"
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return {
|
|
663
|
+
...refreshed,
|
|
664
|
+
toast: {
|
|
665
|
+
message: toastMessage,
|
|
666
|
+
type: summary.errors.length === 0 ? "success" : "info"
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
async function readAllFindings(kv) {
|
|
671
|
+
const entries = await kv.list(
|
|
672
|
+
"findings:"
|
|
673
|
+
);
|
|
674
|
+
const out = [];
|
|
675
|
+
for (const entry of entries) {
|
|
676
|
+
if (entry.value === null) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
const route = entry.key.replace(/^findings:/, "");
|
|
680
|
+
for (const finding of entry.value.findings) {
|
|
681
|
+
out.push({ ...finding, document_route: route });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function severityFirst(a, b) {
|
|
687
|
+
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
688
|
+
const diff = rank(a.severity) - rank(b.severity);
|
|
689
|
+
if (diff !== 0) {
|
|
690
|
+
return diff;
|
|
691
|
+
}
|
|
692
|
+
return a.rule_id.localeCompare(b.rule_id);
|
|
693
|
+
}
|
|
694
|
+
function uniqueRoutes(rows) {
|
|
695
|
+
const routes = /* @__PURE__ */ new Set();
|
|
696
|
+
for (const row of rows) {
|
|
697
|
+
routes.add(row.document_route);
|
|
698
|
+
}
|
|
699
|
+
return [...routes].sort();
|
|
700
|
+
}
|
|
701
|
+
async function renderFindingsPage(kv, tuning) {
|
|
702
|
+
const findings = await readAllFindings(kv);
|
|
703
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
704
|
+
const warnings = findings.filter(
|
|
705
|
+
(finding) => finding.severity === "warning"
|
|
706
|
+
);
|
|
707
|
+
const sorted = [...findings].sort(severityFirst);
|
|
708
|
+
const routes = uniqueRoutes(findings);
|
|
709
|
+
const blocks = [
|
|
710
|
+
{ type: "header", text: "SEO findings" },
|
|
711
|
+
{
|
|
712
|
+
type: "context",
|
|
713
|
+
text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
714
|
+
},
|
|
715
|
+
{ type: "divider" },
|
|
716
|
+
{
|
|
717
|
+
type: "actions",
|
|
718
|
+
elements: [
|
|
719
|
+
// Refresh evaluates every configured entry. Pressing it lists
|
|
720
|
+
// content via the live in-request bridge, evaluates it, and
|
|
721
|
+
// writes findings back to KV (with suppressions applied).
|
|
722
|
+
{
|
|
723
|
+
type: "button",
|
|
724
|
+
label: "Refresh",
|
|
725
|
+
action_id: "refresh_findings",
|
|
726
|
+
style: "primary"
|
|
727
|
+
},
|
|
728
|
+
{ type: "button", label: "All", action_id: "filter:all" },
|
|
729
|
+
{ type: "button", label: "Errors only", action_id: "filter:errors" },
|
|
730
|
+
{
|
|
731
|
+
type: "button",
|
|
732
|
+
label: "Warnings only",
|
|
733
|
+
action_id: "filter:warnings"
|
|
734
|
+
}
|
|
735
|
+
]
|
|
736
|
+
},
|
|
737
|
+
sorted.length === 0 ? {
|
|
738
|
+
type: "context",
|
|
739
|
+
text: tuning.emptyFindingsText
|
|
740
|
+
} : findingsTable(sorted)
|
|
741
|
+
];
|
|
742
|
+
if (routes.length > 0) {
|
|
743
|
+
blocks.push({
|
|
744
|
+
type: "form",
|
|
745
|
+
fields: [
|
|
746
|
+
{
|
|
747
|
+
type: "select",
|
|
748
|
+
action_id: "route_picker",
|
|
749
|
+
label: "Document to inspect",
|
|
750
|
+
options: routes.map((route) => ({ label: route, value: route }))
|
|
751
|
+
}
|
|
752
|
+
],
|
|
753
|
+
submit: { label: "View document SEO", action_id: "view_document" }
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
return { blocks };
|
|
757
|
+
}
|
|
758
|
+
function findingsTable(rows) {
|
|
759
|
+
return {
|
|
760
|
+
type: "table",
|
|
761
|
+
columns: [
|
|
762
|
+
{ key: "route", label: "Route" },
|
|
763
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
764
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
765
|
+
{ key: "message", label: "Message" }
|
|
766
|
+
],
|
|
767
|
+
rows: rows.map((row) => ({
|
|
768
|
+
route: row.document_route,
|
|
769
|
+
rule: row.rule_id,
|
|
770
|
+
severity: row.severity,
|
|
771
|
+
message: row.message
|
|
772
|
+
}))
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function renderScoreWidget(kv, tuning, log) {
|
|
776
|
+
const documents = await readAllDocuments(kv);
|
|
777
|
+
if (documents.length === 0) {
|
|
778
|
+
return {
|
|
779
|
+
blocks: [
|
|
780
|
+
{ type: "header", text: "SEO score" },
|
|
781
|
+
{
|
|
782
|
+
type: "context",
|
|
783
|
+
text: tuning.emptyScoreText
|
|
784
|
+
}
|
|
785
|
+
]
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
let score;
|
|
789
|
+
try {
|
|
790
|
+
score = await tuning.score(documents, kv);
|
|
791
|
+
} catch (error) {
|
|
792
|
+
log?.error("Aexeo score widget failed to initialize", { error });
|
|
793
|
+
return {
|
|
794
|
+
blocks: [
|
|
795
|
+
{ type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
|
|
796
|
+
{ type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
|
|
797
|
+
]
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
if (score === null) {
|
|
801
|
+
return {
|
|
802
|
+
blocks: [
|
|
803
|
+
{ type: "header", text: "SEO score" },
|
|
804
|
+
{
|
|
805
|
+
type: "context",
|
|
806
|
+
text: "The site score is unavailable in sandboxed mode. Findings are not affected."
|
|
807
|
+
}
|
|
808
|
+
]
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
const blocks = [
|
|
812
|
+
{
|
|
813
|
+
type: "stats",
|
|
814
|
+
items: [
|
|
815
|
+
{
|
|
816
|
+
label: "Citation",
|
|
817
|
+
value: score.citation_readiness_score?.toString() ?? "unknown"
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
label: tuning.truthLabel(score),
|
|
821
|
+
value: score.truth_consistency_score?.toString() ?? "unknown"
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
label: "Answers",
|
|
825
|
+
value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
}
|
|
829
|
+
];
|
|
830
|
+
if (score.blockers.length > 0) {
|
|
831
|
+
blocks.push({
|
|
832
|
+
type: "context",
|
|
833
|
+
text: topBlockersLine(score)
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return { blocks };
|
|
837
|
+
}
|
|
838
|
+
function topBlockersLine(score) {
|
|
839
|
+
const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
|
|
840
|
+
if (top.length === 0) {
|
|
841
|
+
return "No blockers identified.";
|
|
842
|
+
}
|
|
843
|
+
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
844
|
+
}
|
|
845
|
+
async function renderDocumentPanel(kv, route) {
|
|
846
|
+
if (route === void 0) {
|
|
847
|
+
return {
|
|
848
|
+
blocks: [
|
|
849
|
+
{ type: "header", text: "Document SEO" },
|
|
850
|
+
{
|
|
851
|
+
type: "context",
|
|
852
|
+
text: "Pick a document from the SEO findings list to see its rule findings here."
|
|
853
|
+
}
|
|
854
|
+
]
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
const findings = await readFindings(kv, route);
|
|
858
|
+
const errors = findings.filter((finding) => finding.severity === "error");
|
|
859
|
+
const warnings = findings.filter(
|
|
860
|
+
(finding) => finding.severity === "warning"
|
|
861
|
+
);
|
|
862
|
+
const sorted = [...findings].sort(severityFirst);
|
|
863
|
+
return {
|
|
864
|
+
blocks: [
|
|
865
|
+
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
866
|
+
{
|
|
867
|
+
type: "context",
|
|
868
|
+
text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
869
|
+
},
|
|
870
|
+
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
871
|
+
]
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function documentFindingsTable(findings) {
|
|
875
|
+
return {
|
|
876
|
+
type: "table",
|
|
877
|
+
columns: [
|
|
878
|
+
{ key: "rule", label: "Rule", format: "code" },
|
|
879
|
+
{ key: "severity", label: "Severity", format: "badge" },
|
|
880
|
+
{ key: "message", label: "Message" },
|
|
881
|
+
{ key: "block", label: "Block", format: "code" }
|
|
882
|
+
],
|
|
883
|
+
rows: findings.map((finding) => ({
|
|
884
|
+
rule: finding.rule_id,
|
|
885
|
+
severity: finding.severity,
|
|
886
|
+
message: finding.message,
|
|
887
|
+
// The bridge stamps every Portable Text block with id + data-pt-key
|
|
888
|
+
// when rendering; surfacing the path:line locator here lets authors
|
|
889
|
+
// locate the failing block via the editor's ⌘-F.
|
|
890
|
+
block: `${finding.path}:${finding.line}`
|
|
891
|
+
}))
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function notFound(page) {
|
|
895
|
+
return {
|
|
896
|
+
blocks: [
|
|
897
|
+
{ type: "header", text: "Not found" },
|
|
898
|
+
{ type: "context", text: `unknown page: ${page}` }
|
|
899
|
+
]
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
397
903
|
// wasm/aexeo_emdash_bridge_bg.js
|
|
398
904
|
var aexeo_emdash_bridge_bg_exports = {};
|
|
399
905
|
__export(aexeo_emdash_bridge_bg_exports, {
|
|
@@ -403,11 +909,65 @@ __export(aexeo_emdash_bridge_bg_exports, {
|
|
|
403
909
|
__wbg_set_wasm: () => __wbg_set_wasm,
|
|
404
910
|
__wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
|
|
405
911
|
__wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
|
|
912
|
+
evaluateContent: () => evaluateContent,
|
|
913
|
+
evaluateContentCollection: () => evaluateContentCollection,
|
|
406
914
|
evaluateDocuments: () => evaluateDocuments,
|
|
407
915
|
generateFactsPrompt: () => generateFactsPrompt,
|
|
408
916
|
scoreIntelligence: () => scoreIntelligence,
|
|
409
917
|
validateFactsManifest: () => validateFactsManifest
|
|
410
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
|
+
}
|
|
411
971
|
function evaluateDocuments(documents_json, config_json) {
|
|
412
972
|
let deferred4_0;
|
|
413
973
|
let deferred4_1;
|
|
@@ -677,6 +1237,10 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
|
|
|
677
1237
|
await ensureInitialized();
|
|
678
1238
|
return validateFactsManifest(manifestJson, documentsJson);
|
|
679
1239
|
}
|
|
1240
|
+
async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson) {
|
|
1241
|
+
await ensureInitialized();
|
|
1242
|
+
return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson);
|
|
1243
|
+
}
|
|
680
1244
|
|
|
681
1245
|
// src/data-route.ts
|
|
682
1246
|
async function handleDataRoute(ctx, options) {
|
|
@@ -685,6 +1249,8 @@ async function handleDataRoute(ctx, options) {
|
|
|
685
1249
|
summary = await evaluateAndPersistAll(ctx, {
|
|
686
1250
|
collections: options.collections,
|
|
687
1251
|
evaluator: options.evaluator,
|
|
1252
|
+
blockMappings: options.blockMappings,
|
|
1253
|
+
contentFieldsByCollection: options.contentFieldsByCollection,
|
|
688
1254
|
...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
|
|
689
1255
|
});
|
|
690
1256
|
await persistManifestStateFindings(ctx);
|
|
@@ -953,12 +1519,16 @@ async function handleSave(ctx, body) {
|
|
|
953
1519
|
return { saved: true };
|
|
954
1520
|
}
|
|
955
1521
|
|
|
956
|
-
// src/presence-route.ts
|
|
957
|
-
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
958
|
-
|
|
959
1522
|
// src/presence-check.ts
|
|
960
1523
|
var FETCH_TIMEOUT_MS = 5e3;
|
|
961
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
|
+
];
|
|
962
1532
|
var SOURCE_NAMES = [
|
|
963
1533
|
"wikipedia",
|
|
964
1534
|
"wikidata",
|
|
@@ -1012,11 +1582,11 @@ async function checkWikipedia(input, fetcher) {
|
|
|
1012
1582
|
const descriptions = body[2];
|
|
1013
1583
|
const urls = body[3];
|
|
1014
1584
|
if (!Array.isArray(titles) || !Array.isArray(urls) || titles.length === 0) {
|
|
1015
|
-
return
|
|
1585
|
+
return notFound2("wikipedia");
|
|
1016
1586
|
}
|
|
1017
1587
|
const title = String(titles[0]);
|
|
1018
|
-
if (!
|
|
1019
|
-
return
|
|
1588
|
+
if (!namesEntity(title, input)) {
|
|
1589
|
+
return notFound2("wikipedia");
|
|
1020
1590
|
}
|
|
1021
1591
|
const articleUrl = String(urls[0] ?? "");
|
|
1022
1592
|
const description = Array.isArray(descriptions) ? String(descriptions[0] ?? "") : "";
|
|
@@ -1042,11 +1612,11 @@ async function checkWikidata(input, fetcher) {
|
|
|
1042
1612
|
const body = await res.json();
|
|
1043
1613
|
const top = body.search?.[0];
|
|
1044
1614
|
if (!top || typeof top.id !== "string") {
|
|
1045
|
-
return
|
|
1615
|
+
return notFound2("wikidata");
|
|
1046
1616
|
}
|
|
1047
1617
|
const label = top.label ?? input.name;
|
|
1048
|
-
if (!
|
|
1049
|
-
return
|
|
1618
|
+
if (!namesEntity(label, input)) {
|
|
1619
|
+
return notFound2("wikidata");
|
|
1050
1620
|
}
|
|
1051
1621
|
return found("wikidata", {
|
|
1052
1622
|
label: `${top.id}${top.label ? ` \u2014 ${top.label}` : ""}`,
|
|
@@ -1072,7 +1642,7 @@ async function checkGitHub(input, fetcher) {
|
|
|
1072
1642
|
// it the API can return either v3 or experimental shapes.
|
|
1073
1643
|
headers: { Accept: "application/vnd.github+json" }
|
|
1074
1644
|
});
|
|
1075
|
-
if (res.status === 404) return
|
|
1645
|
+
if (res.status === 404) return notFound2("github");
|
|
1076
1646
|
if (res.status === 403 || res.status === 429) {
|
|
1077
1647
|
return unreachable("github", "rate-limited (try Refresh later)");
|
|
1078
1648
|
}
|
|
@@ -1104,7 +1674,7 @@ async function checkRdap(input, fetcher) {
|
|
|
1104
1674
|
const url = `https://rdap.org/domain/${encodeURIComponent(host)}`;
|
|
1105
1675
|
try {
|
|
1106
1676
|
const res = await timedFetch(fetcher, url);
|
|
1107
|
-
if (res.status === 404) return
|
|
1677
|
+
if (res.status === 404) return notFound2("rdap");
|
|
1108
1678
|
if (!res.ok) return unreachable("rdap", `HTTP ${res.status}`);
|
|
1109
1679
|
const body = await res.json();
|
|
1110
1680
|
const reg = (body.events ?? []).find(
|
|
@@ -1134,20 +1704,38 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1134
1704
|
"no website URL in manifest, can't query crawl index"
|
|
1135
1705
|
);
|
|
1136
1706
|
}
|
|
1137
|
-
|
|
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
|
+
}
|
|
1138
1725
|
const url = new URL(`https://index.commoncrawl.org/${index}-index`);
|
|
1139
1726
|
url.searchParams.set("url", host);
|
|
1727
|
+
url.searchParams.set("matchType", "domain");
|
|
1140
1728
|
url.searchParams.set("output", "json");
|
|
1141
1729
|
url.searchParams.set("limit", "1");
|
|
1142
1730
|
try {
|
|
1143
1731
|
const res = await timedFetch(fetcher, url.toString());
|
|
1144
|
-
if (res.status === 404) return
|
|
1732
|
+
if (res.status === 404) return notFound2("common_crawl");
|
|
1145
1733
|
if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
|
|
1146
1734
|
const text = (await res.text()).trim();
|
|
1147
|
-
if (text.length === 0) return
|
|
1735
|
+
if (text.length === 0) return notFound2("common_crawl");
|
|
1148
1736
|
let firstLine = text.split("\n")[0];
|
|
1149
1737
|
if (firstLine === void 0 || firstLine.length === 0) {
|
|
1150
|
-
return
|
|
1738
|
+
return notFound2("common_crawl");
|
|
1151
1739
|
}
|
|
1152
1740
|
let entry;
|
|
1153
1741
|
try {
|
|
@@ -1163,6 +1751,16 @@ async function checkCommonCrawl(input, fetcher) {
|
|
|
1163
1751
|
return unreachable("common_crawl", errMsg(error));
|
|
1164
1752
|
}
|
|
1165
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
|
+
}
|
|
1166
1764
|
async function timedFetch(fetcher, url, init) {
|
|
1167
1765
|
const controller = new AbortController();
|
|
1168
1766
|
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
@@ -1189,7 +1787,7 @@ function found(source, details) {
|
|
|
1189
1787
|
if (details.extra !== void 0) result.extra = details.extra;
|
|
1190
1788
|
return result;
|
|
1191
1789
|
}
|
|
1192
|
-
function
|
|
1790
|
+
function notFound2(source) {
|
|
1193
1791
|
return { source, status: "not_found", checkedAt: nowIso() };
|
|
1194
1792
|
}
|
|
1195
1793
|
function unreachable(source, error) {
|
|
@@ -1208,10 +1806,45 @@ function errMsg(error) {
|
|
|
1208
1806
|
}
|
|
1209
1807
|
return String(error);
|
|
1210
1808
|
}
|
|
1211
|
-
function
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
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(" ");
|
|
1215
1848
|
}
|
|
1216
1849
|
function sanitizeGitHubHandle(name) {
|
|
1217
1850
|
const handle = name.replace(/[\s_]+/g, "-");
|
|
@@ -1250,6 +1883,7 @@ function formatCdxTimestamp(ts) {
|
|
|
1250
1883
|
}
|
|
1251
1884
|
|
|
1252
1885
|
// src/presence-route.ts
|
|
1886
|
+
import { PluginRouteError as PluginRouteError2 } from "emdash";
|
|
1253
1887
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1254
1888
|
var PRESENCE_KEY = "presence:current";
|
|
1255
1889
|
async function handlePresenceRoute(ctx) {
|
|
@@ -1258,7 +1892,7 @@ async function handlePresenceRoute(ctx) {
|
|
|
1258
1892
|
case "data":
|
|
1259
1893
|
return await handleData2(ctx);
|
|
1260
1894
|
case "refresh":
|
|
1261
|
-
return await
|
|
1895
|
+
return await handleRefresh2(ctx);
|
|
1262
1896
|
default:
|
|
1263
1897
|
throw PluginRouteError2.badRequest(
|
|
1264
1898
|
`unknown presence route kind: ${String(body.kind)}`
|
|
@@ -1285,7 +1919,7 @@ async function handleData2(ctx) {
|
|
|
1285
1919
|
const state = ageMs !== null && ageMs > CACHE_TTL_MS ? "stale" : "fresh";
|
|
1286
1920
|
return payload(state, cached.results, cached.generatedAt, entity.name);
|
|
1287
1921
|
}
|
|
1288
|
-
async function
|
|
1922
|
+
async function handleRefresh2(ctx) {
|
|
1289
1923
|
const manifest = await readStoredFacts(ctx.kv);
|
|
1290
1924
|
if (manifest === null) {
|
|
1291
1925
|
return payload("no_manifest", [], null, null);
|
|
@@ -1372,8 +2006,61 @@ function globToMatcher(pattern) {
|
|
|
1372
2006
|
return (route) => re.test(route);
|
|
1373
2007
|
}
|
|
1374
2008
|
|
|
1375
|
-
// src/
|
|
1376
|
-
|
|
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
|
+
}
|
|
1377
2064
|
|
|
1378
2065
|
// src/configured.ts
|
|
1379
2066
|
import { definePlugin } from "emdash";
|
|
@@ -1439,10 +2126,15 @@ function createPlugin(options = {}) {
|
|
|
1439
2126
|
options.excludeCollections
|
|
1440
2127
|
);
|
|
1441
2128
|
const suppressionFilter = compileSuppressions(options.suppressions);
|
|
2129
|
+
const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3);
|
|
1442
2130
|
return definePlugin({
|
|
1443
2131
|
id: "aexeo-emdash",
|
|
1444
2132
|
version: PACKAGE_VERSION,
|
|
1445
|
-
|
|
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],
|
|
1446
2138
|
hooks: {
|
|
1447
2139
|
// afterSave processes one saved document at a time — collections
|
|
1448
2140
|
// list isn't needed here. The hook always runs regardless of
|
|
@@ -1450,16 +2142,26 @@ function createPlugin(options = {}) {
|
|
|
1450
2142
|
"content:afterSave": (event, ctx) => handleAfterSaveConfigured(
|
|
1451
2143
|
event,
|
|
1452
2144
|
ctx,
|
|
1453
|
-
|
|
1454
|
-
suppressionFilter
|
|
1455
|
-
|
|
2145
|
+
evaluator,
|
|
2146
|
+
suppressionFilter,
|
|
2147
|
+
options.blockMappings,
|
|
2148
|
+
options.contentFieldsByCollection?.[event.content.type]
|
|
2149
|
+
),
|
|
2150
|
+
"content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
|
|
1456
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.
|
|
1457
2155
|
routes: {
|
|
1458
2156
|
// The Refresh sweep (handleAdminRoute → handleRefresh) is the
|
|
1459
2157
|
// one consumer of `collections`. Bind the resolved list here so
|
|
1460
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.
|
|
1461
2162
|
admin: {
|
|
1462
|
-
|
|
2163
|
+
permission: "plugins:read",
|
|
2164
|
+
handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection)
|
|
1463
2165
|
},
|
|
1464
2166
|
// JSON data endpoint for the React adminEntry. Two routes:
|
|
1465
2167
|
// POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
|
|
@@ -1469,19 +2171,25 @@ function createPlugin(options = {}) {
|
|
|
1469
2171
|
// emdash edit page and the public site, which Block Kit can't
|
|
1470
2172
|
// express in any element type as of emdash 0.8.0.
|
|
1471
2173
|
data: {
|
|
2174
|
+
permission: "plugins:read",
|
|
1472
2175
|
handler: async (ctx) => (await handleDataRoute(ctx, {
|
|
1473
2176
|
collections,
|
|
1474
|
-
evaluator
|
|
2177
|
+
evaluator,
|
|
1475
2178
|
refresh: false,
|
|
1476
|
-
suppressionFilter
|
|
2179
|
+
suppressionFilter,
|
|
2180
|
+
blockMappings: options.blockMappings,
|
|
2181
|
+
contentFieldsByCollection: options.contentFieldsByCollection
|
|
1477
2182
|
})).payload
|
|
1478
2183
|
},
|
|
1479
2184
|
refresh: {
|
|
2185
|
+
permission: "plugins:manage",
|
|
1480
2186
|
handler: (ctx) => handleDataRoute(ctx, {
|
|
1481
2187
|
collections,
|
|
1482
|
-
evaluator
|
|
2188
|
+
evaluator,
|
|
1483
2189
|
refresh: true,
|
|
1484
|
-
suppressionFilter
|
|
2190
|
+
suppressionFilter,
|
|
2191
|
+
blockMappings: options.blockMappings,
|
|
2192
|
+
contentFieldsByCollection: options.contentFieldsByCollection
|
|
1485
2193
|
})
|
|
1486
2194
|
},
|
|
1487
2195
|
// /facts route serves the truth-manifest authoring UI. It
|
|
@@ -1489,6 +2197,7 @@ function createPlugin(options = {}) {
|
|
|
1489
2197
|
// onto one POST endpoint via a "kind" body field — see
|
|
1490
2198
|
// facts-route.ts for the full contract.
|
|
1491
2199
|
facts: {
|
|
2200
|
+
permission: "plugins:manage",
|
|
1492
2201
|
handler: (ctx) => handleFactsRoute(ctx)
|
|
1493
2202
|
},
|
|
1494
2203
|
// /presence route serves the layer-4 entity-presence
|
|
@@ -1498,6 +2207,7 @@ function createPlugin(options = {}) {
|
|
|
1498
2207
|
// results in KV for 24h. Multiplexes data / refresh kinds —
|
|
1499
2208
|
// see presence-route.ts.
|
|
1500
2209
|
presence: {
|
|
2210
|
+
permission: "plugins:manage",
|
|
1501
2211
|
handler: (ctx) => handlePresenceRoute(ctx)
|
|
1502
2212
|
}
|
|
1503
2213
|
},
|
|
@@ -1529,246 +2239,76 @@ function createPlugin(options = {}) {
|
|
|
1529
2239
|
});
|
|
1530
2240
|
}
|
|
1531
2241
|
var configured_default = createPlugin;
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
};
|
|
1542
|
-
}
|
|
1543
|
-
if (raw.type === "block_action") {
|
|
1544
|
-
return {
|
|
1545
|
-
type: "block_action",
|
|
1546
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
1547
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
1548
|
-
value: raw.value
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
if (raw.type === "form_submit") {
|
|
1552
|
-
return {
|
|
1553
|
-
type: "form_submit",
|
|
1554
|
-
action_id: typeof raw.action_id === "string" ? raw.action_id : "",
|
|
1555
|
-
...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
|
|
1556
|
-
values: raw.values && typeof raw.values === "object" ? raw.values : {}
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
return { type: "page_load", page: "/findings" };
|
|
1560
|
-
}
|
|
1561
|
-
async function handleAdminRoute(ctx, collections) {
|
|
2242
|
+
var ADMIN_ROLE = 50;
|
|
2243
|
+
var adminViewTuning = {
|
|
2244
|
+
emptyFindingsText: "Once a Refresh runs, findings will list here.",
|
|
2245
|
+
emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
|
|
2246
|
+
refreshFailureVariant: "error",
|
|
2247
|
+
score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
|
|
2248
|
+
truthLabel: formatTruthLabel
|
|
2249
|
+
};
|
|
2250
|
+
async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection) {
|
|
1562
2251
|
const body = normalizeInteraction(ctx.input);
|
|
1563
2252
|
if (body.type === "page_load") {
|
|
1564
2253
|
return handlePageLoad(ctx, body.page);
|
|
1565
2254
|
}
|
|
1566
2255
|
if (body.type === "block_action") {
|
|
1567
2256
|
if (body.action_id === "view_document" && typeof body.value === "string") {
|
|
1568
|
-
return renderDocumentPanel(ctx, body.value);
|
|
2257
|
+
return renderDocumentPanel(ctx.kv, body.value);
|
|
1569
2258
|
}
|
|
1570
2259
|
if (body.action_id === "refresh_findings") {
|
|
1571
|
-
|
|
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
|
+
}
|
|
2274
|
+
return handleRefresh({
|
|
2275
|
+
ctx,
|
|
2276
|
+
collections,
|
|
2277
|
+
evaluator,
|
|
2278
|
+
suppressionFilter,
|
|
2279
|
+
tuning: adminViewTuning,
|
|
2280
|
+
blockMappings,
|
|
2281
|
+
contentFieldsByCollection
|
|
2282
|
+
});
|
|
1572
2283
|
}
|
|
1573
2284
|
if (body.action_id.startsWith("filter:")) {
|
|
1574
|
-
return renderFindingsPage(ctx);
|
|
2285
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1575
2286
|
}
|
|
1576
|
-
return
|
|
2287
|
+
return notFound(body.action_id);
|
|
1577
2288
|
}
|
|
1578
2289
|
if (body.type === "form_submit") {
|
|
1579
2290
|
if (body.action_id === "view_document") {
|
|
1580
2291
|
const picked = body.values["route_picker"];
|
|
1581
2292
|
if (typeof picked === "string" && picked.length > 0) {
|
|
1582
|
-
return renderDocumentPanel(ctx, picked);
|
|
2293
|
+
return renderDocumentPanel(ctx.kv, picked);
|
|
1583
2294
|
}
|
|
1584
2295
|
}
|
|
1585
|
-
return renderFindingsPage(ctx);
|
|
2296
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1586
2297
|
}
|
|
1587
|
-
return renderFindingsPage(ctx);
|
|
2298
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1588
2299
|
}
|
|
1589
2300
|
async function handlePageLoad(ctx, page) {
|
|
1590
2301
|
const normalized = page.startsWith("/") ? page.slice(1) : page;
|
|
1591
2302
|
if (normalized === "" || normalized === "findings") {
|
|
1592
|
-
return renderFindingsPage(ctx);
|
|
2303
|
+
return renderFindingsPage(ctx.kv, adminViewTuning);
|
|
1593
2304
|
}
|
|
1594
2305
|
if (normalized === "widget:aexeo-score") {
|
|
1595
|
-
return renderScoreWidget(ctx);
|
|
2306
|
+
return renderScoreWidget(ctx.kv, adminViewTuning, ctx.log);
|
|
1596
2307
|
}
|
|
1597
2308
|
if (normalized === "document") {
|
|
1598
|
-
return renderDocumentPanel(ctx);
|
|
1599
|
-
}
|
|
1600
|
-
return notFound2(page);
|
|
1601
|
-
}
|
|
1602
|
-
async function handleRefresh2(ctx, collections) {
|
|
1603
|
-
let summary;
|
|
1604
|
-
try {
|
|
1605
|
-
summary = await evaluateAndPersistAll(ctx, {
|
|
1606
|
-
evaluator: inProcessEvaluator,
|
|
1607
|
-
collections
|
|
1608
|
-
});
|
|
1609
|
-
} catch (err) {
|
|
1610
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1611
|
-
return {
|
|
1612
|
-
blocks: [
|
|
1613
|
-
{ type: "header", text: "SEO findings" },
|
|
1614
|
-
{
|
|
1615
|
-
type: "banner",
|
|
1616
|
-
title: `Refresh failed: ${detail}`,
|
|
1617
|
-
variant: "error"
|
|
1618
|
-
}
|
|
1619
|
-
],
|
|
1620
|
-
toast: { message: `Refresh failed: ${detail}`, type: "error" }
|
|
1621
|
-
};
|
|
1622
|
-
}
|
|
1623
|
-
const refreshed = await renderFindingsPage(ctx);
|
|
1624
|
-
const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
|
|
1625
|
-
if (summary.errors.length > 0) {
|
|
1626
|
-
refreshed.blocks.unshift({
|
|
1627
|
-
type: "banner",
|
|
1628
|
-
title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
|
|
1629
|
-
variant: "alert"
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
return {
|
|
1633
|
-
...refreshed,
|
|
1634
|
-
toast: {
|
|
1635
|
-
message: toastMessage,
|
|
1636
|
-
type: summary.errors.length === 0 ? "success" : "info"
|
|
1637
|
-
}
|
|
1638
|
-
};
|
|
1639
|
-
}
|
|
1640
|
-
async function readAllFindings(ctx) {
|
|
1641
|
-
const entries = await ctx.kv.list(
|
|
1642
|
-
"findings:"
|
|
1643
|
-
);
|
|
1644
|
-
const out = [];
|
|
1645
|
-
for (const entry of entries) {
|
|
1646
|
-
if (entry.value === null) continue;
|
|
1647
|
-
const route = entry.key.replace(/^findings:/, "");
|
|
1648
|
-
for (const finding of entry.value.findings) {
|
|
1649
|
-
out.push({ ...finding, document_route: route });
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
return out;
|
|
1653
|
-
}
|
|
1654
|
-
function severityFirst(a, b) {
|
|
1655
|
-
const rank = (severity) => severity === "error" ? 0 : 1;
|
|
1656
|
-
const diff = rank(a.severity) - rank(b.severity);
|
|
1657
|
-
if (diff !== 0) return diff;
|
|
1658
|
-
return a.rule_id.localeCompare(b.rule_id);
|
|
1659
|
-
}
|
|
1660
|
-
function uniqueRoutes(rows) {
|
|
1661
|
-
const set = /* @__PURE__ */ new Set();
|
|
1662
|
-
for (const row of rows) set.add(row.document_route);
|
|
1663
|
-
return [...set].sort();
|
|
1664
|
-
}
|
|
1665
|
-
async function renderFindingsPage(ctx) {
|
|
1666
|
-
const findings = await readAllFindings(ctx);
|
|
1667
|
-
const errors = findings.filter((f) => f.severity === "error");
|
|
1668
|
-
const warnings = findings.filter((f) => f.severity === "warning");
|
|
1669
|
-
const sorted = [...findings].sort(severityFirst);
|
|
1670
|
-
const routes = uniqueRoutes(findings);
|
|
1671
|
-
const blocks = [
|
|
1672
|
-
{ type: "header", text: "SEO findings" },
|
|
1673
|
-
{
|
|
1674
|
-
type: "context",
|
|
1675
|
-
text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1676
|
-
},
|
|
1677
|
-
{ type: "divider" },
|
|
1678
|
-
{
|
|
1679
|
-
type: "actions",
|
|
1680
|
-
elements: [
|
|
1681
|
-
{
|
|
1682
|
-
type: "button",
|
|
1683
|
-
label: "Refresh",
|
|
1684
|
-
action_id: "refresh_findings",
|
|
1685
|
-
style: "primary"
|
|
1686
|
-
},
|
|
1687
|
-
{ type: "button", label: "All", action_id: "filter:all" },
|
|
1688
|
-
{ type: "button", label: "Errors only", action_id: "filter:errors" },
|
|
1689
|
-
{
|
|
1690
|
-
type: "button",
|
|
1691
|
-
label: "Warnings only",
|
|
1692
|
-
action_id: "filter:warnings"
|
|
1693
|
-
}
|
|
1694
|
-
]
|
|
1695
|
-
},
|
|
1696
|
-
sorted.length === 0 ? {
|
|
1697
|
-
type: "context",
|
|
1698
|
-
text: "Once a Refresh runs, findings will list here."
|
|
1699
|
-
} : findingsTable(sorted)
|
|
1700
|
-
];
|
|
1701
|
-
if (routes.length > 0) {
|
|
1702
|
-
blocks.push({
|
|
1703
|
-
type: "form",
|
|
1704
|
-
fields: [
|
|
1705
|
-
{
|
|
1706
|
-
type: "select",
|
|
1707
|
-
action_id: "route_picker",
|
|
1708
|
-
label: "Document to inspect",
|
|
1709
|
-
options: routes.map((route) => ({ label: route, value: route }))
|
|
1710
|
-
}
|
|
1711
|
-
],
|
|
1712
|
-
submit: { label: "View document SEO", action_id: "view_document" }
|
|
1713
|
-
});
|
|
1714
|
-
}
|
|
1715
|
-
return { blocks };
|
|
1716
|
-
}
|
|
1717
|
-
function findingsTable(rows) {
|
|
1718
|
-
return {
|
|
1719
|
-
type: "table",
|
|
1720
|
-
columns: [
|
|
1721
|
-
{ key: "route", label: "Route" },
|
|
1722
|
-
{ key: "rule", label: "Rule", format: "code" },
|
|
1723
|
-
{ key: "severity", label: "Severity", format: "badge" },
|
|
1724
|
-
{ key: "message", label: "Message" }
|
|
1725
|
-
],
|
|
1726
|
-
rows: rows.map((row) => ({
|
|
1727
|
-
route: row.document_route,
|
|
1728
|
-
rule: row.rule_id,
|
|
1729
|
-
severity: row.severity,
|
|
1730
|
-
message: row.message
|
|
1731
|
-
}))
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
async function renderScoreWidget(ctx) {
|
|
1735
|
-
const documents = await readAllDocuments(ctx.kv);
|
|
1736
|
-
if (documents.length === 0) {
|
|
1737
|
-
return {
|
|
1738
|
-
blocks: [
|
|
1739
|
-
{ type: "header", text: "SEO score" },
|
|
1740
|
-
{
|
|
1741
|
-
type: "context",
|
|
1742
|
-
text: "No documents indexed \u2014 click Refresh on the Aexeo findings page."
|
|
1743
|
-
}
|
|
1744
|
-
]
|
|
1745
|
-
};
|
|
2309
|
+
return renderDocumentPanel(ctx.kv);
|
|
1746
2310
|
}
|
|
1747
|
-
|
|
1748
|
-
const score = await scoreLocally(documents, facts);
|
|
1749
|
-
const truthLabel = formatTruthLabel(score);
|
|
1750
|
-
const blocks = [
|
|
1751
|
-
{
|
|
1752
|
-
type: "stats",
|
|
1753
|
-
items: [
|
|
1754
|
-
{ label: "Overall", value: `${score.overall_score}` },
|
|
1755
|
-
{ label: "Citation", value: `${score.citation_readiness_score}` },
|
|
1756
|
-
{ label: truthLabel, value: `${score.truth_consistency_score}` },
|
|
1757
|
-
{ label: "Answers", value: `${score.answer_pack_score}` }
|
|
1758
|
-
]
|
|
1759
|
-
}
|
|
1760
|
-
];
|
|
1761
|
-
if (score.overall_score < 60) {
|
|
1762
|
-
blocks.unshift({
|
|
1763
|
-
type: "banner",
|
|
1764
|
-
title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
|
|
1765
|
-
variant: "alert"
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
if (score.blockers.length > 0) {
|
|
1769
|
-
blocks.push({ type: "context", text: topBlockersLine(score) });
|
|
1770
|
-
}
|
|
1771
|
-
return { blocks };
|
|
2311
|
+
return notFound(page);
|
|
1772
2312
|
}
|
|
1773
2313
|
async function scoreLocally(documents, manifest = null) {
|
|
1774
2314
|
const manifestJson = manifest === null ? null : JSON.stringify(manifest);
|
|
@@ -1789,63 +2329,6 @@ function formatTruthLabel(score) {
|
|
|
1789
2329
|
return "Truth";
|
|
1790
2330
|
}
|
|
1791
2331
|
}
|
|
1792
|
-
function topBlockersLine(score) {
|
|
1793
|
-
const top = score.blockers.slice(0, 3).map((b) => b.message);
|
|
1794
|
-
if (top.length === 0) return "No blockers identified.";
|
|
1795
|
-
return `Top blockers: ${top.join(" \u2022 ")}`;
|
|
1796
|
-
}
|
|
1797
|
-
async function renderDocumentPanel(ctx, route) {
|
|
1798
|
-
if (route === void 0) {
|
|
1799
|
-
return {
|
|
1800
|
-
blocks: [
|
|
1801
|
-
{ type: "header", text: "Document SEO" },
|
|
1802
|
-
{
|
|
1803
|
-
type: "context",
|
|
1804
|
-
text: "Pick a document from the SEO findings list to see its rule findings here."
|
|
1805
|
-
}
|
|
1806
|
-
]
|
|
1807
|
-
};
|
|
1808
|
-
}
|
|
1809
|
-
const findings = await readFindings(ctx.kv, route);
|
|
1810
|
-
const errors = findings.filter((f) => f.severity === "error");
|
|
1811
|
-
const warnings = findings.filter((f) => f.severity === "warning");
|
|
1812
|
-
const sorted = [...findings].sort(severityFirst);
|
|
1813
|
-
return {
|
|
1814
|
-
blocks: [
|
|
1815
|
-
{ type: "header", text: `Document SEO \u2014 ${route}` },
|
|
1816
|
-
{
|
|
1817
|
-
type: "context",
|
|
1818
|
-
text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
|
|
1819
|
-
},
|
|
1820
|
-
...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
|
|
1821
|
-
]
|
|
1822
|
-
};
|
|
1823
|
-
}
|
|
1824
|
-
function documentFindingsTable(findings) {
|
|
1825
|
-
return {
|
|
1826
|
-
type: "table",
|
|
1827
|
-
columns: [
|
|
1828
|
-
{ key: "rule", label: "Rule", format: "code" },
|
|
1829
|
-
{ key: "severity", label: "Severity", format: "badge" },
|
|
1830
|
-
{ key: "message", label: "Message" },
|
|
1831
|
-
{ key: "block", label: "Block", format: "code" }
|
|
1832
|
-
],
|
|
1833
|
-
rows: findings.map((finding) => ({
|
|
1834
|
-
rule: finding.rule_id,
|
|
1835
|
-
severity: finding.severity,
|
|
1836
|
-
message: finding.message,
|
|
1837
|
-
block: `${finding.path}:${finding.line}`
|
|
1838
|
-
}))
|
|
1839
|
-
};
|
|
1840
|
-
}
|
|
1841
|
-
function notFound2(page) {
|
|
1842
|
-
return {
|
|
1843
|
-
blocks: [
|
|
1844
|
-
{ type: "header", text: "Not found" },
|
|
1845
|
-
{ type: "context", text: `unknown page: ${page}` }
|
|
1846
|
-
]
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
2332
|
export {
|
|
1850
2333
|
createPlugin,
|
|
1851
2334
|
configured_default as default
|