@aeptus/aexeo-emdash 0.8.19 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +112 -2
  2. package/INSTALL.md +286 -4
  3. package/README.md +20 -5
  4. package/dist/adapter.d.ts +62 -4
  5. package/dist/adapter.d.ts.map +1 -1
  6. package/dist/admin/Findings.d.ts.map +1 -1
  7. package/dist/admin.d.ts.map +1 -1
  8. package/dist/admin.js +73 -6
  9. package/dist/assessment-display.d.ts +10 -0
  10. package/dist/assessment-display.d.ts.map +1 -0
  11. package/dist/cms-coverage.d.ts +10 -0
  12. package/dist/cms-coverage.d.ts.map +1 -0
  13. package/dist/configured.d.ts +16 -1
  14. package/dist/configured.d.ts.map +1 -1
  15. package/dist/configured.js +1341 -172
  16. package/dist/content-evaluator.d.ts +34 -0
  17. package/dist/content-evaluator.d.ts.map +1 -0
  18. package/dist/content.d.ts +98 -0
  19. package/dist/content.d.ts.map +1 -0
  20. package/dist/content.js +445 -0
  21. package/dist/data-route.d.ts +12 -1
  22. package/dist/data-route.d.ts.map +1 -1
  23. package/dist/evaluator-sandbox.d.ts +1 -1
  24. package/dist/evaluator-sandbox.d.ts.map +1 -1
  25. package/dist/evaluator.d.ts +1 -1
  26. package/dist/evaluator.d.ts.map +1 -1
  27. package/dist/findings-view.d.ts +11 -5
  28. package/dist/findings-view.d.ts.map +1 -1
  29. package/dist/index.d.ts +17 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +163 -4
  32. package/dist/inventory.d.ts +59 -0
  33. package/dist/inventory.d.ts.map +1 -0
  34. package/dist/plugin.d.ts +59 -3
  35. package/dist/plugin.d.ts.map +1 -1
  36. package/dist/presence-check.d.ts +1 -0
  37. package/dist/presence-check.d.ts.map +1 -1
  38. package/dist/sandbox-entry.d.ts +2 -1
  39. package/dist/sandbox-entry.d.ts.map +1 -1
  40. package/dist/sandbox-entry.js +1077 -190
  41. package/dist/sandbox.d.ts +2 -2
  42. package/dist/sidecar-url.d.ts.map +1 -1
  43. package/dist/sidecar.d.ts +9 -1
  44. package/dist/sidecar.d.ts.map +1 -1
  45. package/dist/types.d.ts +26 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.d.ts.map +1 -1
  49. package/dist/wasm-init.d.ts +8 -0
  50. package/dist/wasm-init.d.ts.map +1 -1
  51. package/package.json +28 -8
  52. package/wasm/aexeo_emdash_bridge.d.ts +22 -0
  53. package/wasm/aexeo_emdash_bridge.js +1 -1
  54. package/wasm/aexeo_emdash_bridge_bg.d.ts +15 -0
  55. package/wasm/aexeo_emdash_bridge_bg.js +130 -0
  56. package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
  57. package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +4 -0
@@ -4,14 +4,39 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/cms-coverage.ts
8
+ var NATIVE_RULE_ID = /^(?:A11Y|AGT|CNT|CRW|DEP|EDT|FACTS|GEO|IMG|LLM|LNK|MAP|QLT|ROB|SCH|SEO|SOC|SRF)[0-9]{3}$/;
9
+ var OBSERVABLE_BODY_RULES = /* @__PURE__ */ new Set(["CNT006", "LNK003"]);
10
+ function isCmsObservableFinding(finding, bodyComplete = true) {
11
+ return finding.aexeo_origin === "content_preset" || isLegacyCmsObservableFinding(finding, bodyComplete);
12
+ }
13
+ function isLegacyCmsObservableFinding(finding, bodyComplete = true) {
14
+ if (!NATIVE_RULE_ID.test(finding.rule_id)) return true;
15
+ return bodyComplete && finding.scope === "page" && OBSERVABLE_BODY_RULES.has(finding.rule_id);
16
+ }
17
+ function hasCompleteBody(document) {
18
+ return document.body !== void 0 && (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every((capability) => document.body_capabilities?.includes(capability)));
19
+ }
20
+ function cmsBodyCoverage(document) {
21
+ if (document.body === void 0 || document.body_capabilities?.length === 0) return "unavailable";
22
+ return hasCompleteBody(document) ? "complete" : "partial";
23
+ }
24
+ function cmsBodyCoverageNotice(coverage) {
25
+ if (coverage === "complete") return void 0;
26
+ return coverage === void 0 ? "Body coverage is unknown for this evaluation; save or refresh to reassess it." : `Body coverage is ${coverage}; content checks depending on missing capabilities cannot be evaluated reliably.`;
27
+ }
28
+
7
29
  // src/adapter.ts
8
- async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
30
+ async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields, descriptionFields) {
9
31
  const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
10
32
  const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
11
33
  const adapted = adaptContentItem(content, {
12
34
  route,
13
35
  publicUrl,
14
- ...defaultLocale === void 0 ? {} : { defaultLocale }
36
+ ...defaultLocale === void 0 ? {} : { defaultLocale },
37
+ ...blockMappings === void 0 ? {} : { blockMappings },
38
+ ...contentFields === void 0 ? {} : { contentFields },
39
+ ...descriptionFields === void 0 ? {} : { descriptionFields }
15
40
  });
16
41
  const translationSet = await contentApi.getTranslations(
17
42
  content.type,
@@ -36,14 +61,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
36
61
  return adapted;
37
62
  }
38
63
  function adaptContentItem(content, options = {}) {
64
+ return adaptContentItemInternal(content, options, true);
65
+ }
66
+ function adaptContentItemInternal(content, options, includeSourcePaths) {
39
67
  const route = options.route ?? deriveRoute(content);
40
68
  const title = stringOrEmpty(
41
69
  content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
42
70
  );
43
- const document = { route, title };
44
- const description = content.seo?.description ?? content.data["description"];
45
- if (typeof description === "string" && description.length > 0) {
46
- document.description = description;
71
+ const document = { route, title, description_source_observed: false };
72
+ const fields = options.descriptionFields;
73
+ if (fields === void 0) {
74
+ const description = content.seo?.description ?? content.data["description"];
75
+ if (typeof description === "string" && description.length > 0) document.description = description;
76
+ } else if (fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0)) {
77
+ let observed = true;
78
+ for (const field of fields) {
79
+ if (!Object.hasOwn(content.data, field) || content.data[field] === void 0 || content.data[field] !== null && typeof content.data[field] !== "string") {
80
+ observed = false;
81
+ break;
82
+ }
83
+ const value = content.data[field];
84
+ if (typeof value === "string" && value.length > 0) {
85
+ document.description = value;
86
+ break;
87
+ }
88
+ }
89
+ if (observed) document.description_source_observed = true;
47
90
  }
48
91
  const canonical = content.seo?.canonical;
49
92
  if (typeof canonical === "string" && canonical.length > 0) {
@@ -53,8 +96,13 @@ function adaptContentItem(content, options = {}) {
53
96
  if (typeof locale === "string" && locale.length > 0) {
54
97
  document.lang = locale;
55
98
  }
56
- const body = extractPortableText(content.data);
57
- if (body !== null) document.body = body;
99
+ const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
100
+ if (body !== null) {
101
+ document.body = body.blocks;
102
+ if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
103
+ }
104
+ const bodySourcePaths = body === null || !includeSourcePaths ? void 0 : portableTextSourcePaths(content.data, body.blocks);
105
+ const bodySourceSpanPaths = body === null || !includeSourcePaths ? void 0 : portableTextSourceSpanPaths(content.data, body.blocks, bodySourcePaths);
58
106
  return {
59
107
  document,
60
108
  meta: {
@@ -63,7 +111,10 @@ function adaptContentItem(content, options = {}) {
63
111
  slug: content.slug,
64
112
  status: content.status,
65
113
  title,
66
- publicUrl: options.publicUrl ?? null
114
+ publicUrl: options.publicUrl ?? null,
115
+ ...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
116
+ ...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
117
+ ...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
67
118
  }
68
119
  };
69
120
  }
@@ -82,38 +133,390 @@ function routeFromPublicUrl(publicUrl) {
82
133
  function stringOrEmpty(value) {
83
134
  return typeof value === "string" ? value : "";
84
135
  }
85
- function extractPortableText(data) {
136
+ function portableTextSourcePaths(data, blocks) {
137
+ if (blocks.length === 0) return void 0;
138
+ const targets = new WeakSet(blocks);
139
+ const paths = /* @__PURE__ */ new WeakMap();
140
+ const seen = /* @__PURE__ */ new WeakSet();
141
+ const stack = [{ value: data, path: ["data"] }];
142
+ let count = 0;
143
+ let inspected = 0;
144
+ while (stack.length > 0) {
145
+ const current = stack.pop();
146
+ if (typeof current.value !== "object" || current.value === null) continue;
147
+ if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
148
+ seen.add(current.value);
149
+ if (targets.has(current.value)) {
150
+ paths.set(current.value, current.path);
151
+ continue;
152
+ }
153
+ if (Array.isArray(current.value)) {
154
+ for (let i = 0; i < current.value.length; i++) {
155
+ if (++inspected > 5e4) return void 0;
156
+ const value = current.value[i];
157
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i] });
158
+ }
159
+ } else {
160
+ for (const key in current.value) {
161
+ if (!Object.hasOwn(current.value, key)) continue;
162
+ if (++inspected > 5e4) return void 0;
163
+ const value = current.value[key];
164
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key] });
165
+ }
166
+ }
167
+ }
168
+ const result = blocks.map((block) => paths.get(block) ?? null);
169
+ return result.some((path) => path !== null) ? result : void 0;
170
+ }
171
+ var BODY_CAPABILITIES = ["text", "headings", "paragraphs", "links"];
172
+ function intersectCapabilities(current, other) {
173
+ return other === void 0 ? [...current] : current.filter((capability) => other.includes(capability));
174
+ }
175
+ function bodyWithCapabilities(blocks, capabilities) {
176
+ return capabilities.length === BODY_CAPABILITIES.length ? { blocks } : { blocks, capabilities };
177
+ }
178
+ function portableTextSourceSpanPaths(data, blocks, blockPaths) {
179
+ const targets = /* @__PURE__ */ new WeakSet();
180
+ let targetCount = 0;
181
+ for (let i = 0; i < blocks.length; i++) {
182
+ if (blockPaths?.[i]) continue;
183
+ for (const child of blocks[i].children) {
184
+ if (++targetCount > 5e4) return void 0;
185
+ targets.add(child);
186
+ }
187
+ }
188
+ if (targetCount === 0) return void 0;
189
+ const paths = /* @__PURE__ */ new WeakMap();
190
+ const seen = /* @__PURE__ */ new WeakSet();
191
+ const stack = [
192
+ { value: data, path: ["data"], portableChild: false }
193
+ ];
194
+ let count = 0;
195
+ let inspected = 0;
196
+ while (stack.length > 0) {
197
+ const current = stack.pop();
198
+ if (typeof current.value !== "object" || current.value === null) continue;
199
+ if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
200
+ seen.add(current.value);
201
+ if (current.portableChild && targets.has(current.value) && isPortableTextSpan(current.value)) {
202
+ paths.set(current.value, [...current.path, "text"]);
203
+ }
204
+ if (Array.isArray(current.value)) {
205
+ for (let i = 0; i < current.value.length; i++) {
206
+ if (++inspected > 5e4) return void 0;
207
+ const value = current.value[i];
208
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i], portableChild: current.portableChild });
209
+ }
210
+ } else {
211
+ for (const key in current.value) {
212
+ if (!Object.hasOwn(current.value, key)) continue;
213
+ if (++inspected > 5e4) return void 0;
214
+ const value = current.value[key];
215
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key], portableChild: key === "children" && isPortableTextBlockCandidate(current.value) });
216
+ }
217
+ }
218
+ }
219
+ let mapped = false;
220
+ const result = blocks.map((block, index) => {
221
+ if (blockPaths?.[index]) return null;
222
+ return block.children.map((child) => {
223
+ const path = paths.get(child) ?? null;
224
+ if (path !== null) mapped = true;
225
+ return path;
226
+ });
227
+ });
228
+ return mapped ? result : void 0;
229
+ }
230
+ function extractSelectedContent(data, fields, mappings) {
231
+ if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
232
+ const all = ["text", "headings", "paragraphs", "links"];
233
+ let capabilities = all;
234
+ const blocks = [];
235
+ let observed = false;
236
+ for (const field of fields) {
237
+ const value = data[field];
238
+ if (value === void 0) {
239
+ capabilities = [];
240
+ continue;
241
+ }
242
+ observed = true;
243
+ if (value === null) continue;
244
+ const part = extractFirstPortableText({ content: value }, mappings);
245
+ if (part === null) {
246
+ capabilities = [];
247
+ continue;
248
+ }
249
+ blocks.push(...part.blocks);
250
+ capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
251
+ }
252
+ if (!observed) return null;
253
+ return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
254
+ }
255
+ function extractPortableText(data, mappings) {
256
+ const result = extractFirstPortableText(data, mappings);
257
+ if (result === null) return null;
258
+ const candidates = Object.entries(data).filter(
259
+ ([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
260
+ );
261
+ const unrecognizedBody = ["body", "content", "blocks"].some((field) => {
262
+ const value = data[field];
263
+ if (value == null || Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) return false;
264
+ return !candidates.some(([candidate]) => candidate === field);
265
+ });
266
+ return candidates.length > 1 || unrecognizedBody ? { blocks: result.blocks, capabilities: [] } : result;
267
+ }
268
+ function extractFirstPortableText(data, mappings) {
269
+ let foundEmptyBody = false;
86
270
  for (const slug of ["body", "content"]) {
87
271
  const value = data[slug];
88
- if (isPortableTextArray(value)) return value;
89
- if (isEmdashBlocksArray(value, slug)) return blocksToPortableText(value);
272
+ if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
273
+ if (isPortableTextArray(value)) return { blocks: value };
274
+ if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
275
+ if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
90
276
  if (typeof value === "string" && value.trim().length > 0) {
91
- return [portableTextBlock(value.trim(), `${slug}-text`)];
277
+ return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
92
278
  }
93
279
  }
94
280
  const blocks = data["blocks"];
95
- if (isPortableTextArray(blocks)) return blocks;
96
- if (isEmdashBlocksArray(blocks, "blocks")) return blocksToPortableText(blocks);
281
+ if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
282
+ if (isPortableTextArray(blocks)) return { blocks };
283
+ if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
284
+ if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
97
285
  for (const [field, value] of Object.entries(data)) {
98
286
  if (field === "body" || field === "content" || field === "blocks") continue;
99
- if (isPortableTextArray(value)) return value;
100
- if (isEmdashBlocksArray(value, field)) return blocksToPortableText(value);
287
+ if (isPortableTextArray(value)) return { blocks: value };
288
+ if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
289
+ if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
101
290
  }
102
- return null;
291
+ return foundEmptyBody ? { blocks: [] } : null;
103
292
  }
104
293
  function isPortableTextArray(value) {
105
294
  return Array.isArray(value) && value.length > 0 && value.every(
106
- (item) => isRecord(item) && item["_type"] === "block" && Array.isArray(item["children"]) && item["children"].every(
107
- (child) => isRecord(child) && typeof child["text"] === "string"
108
- )
295
+ (item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
109
296
  );
110
297
  }
298
+ function isPortableTextSpan(value) {
299
+ return isRecord(value) && value["_type"] === "span" && typeof value["text"] === "string";
300
+ }
301
+ function isPortableTextBlockCandidate(value) {
302
+ return isRecord(value) && value["_type"] === "block" && Array.isArray(value["children"]);
303
+ }
304
+ function isWireSafeSpan(value) {
305
+ return isPortableTextSpan(value) && isRecord(value) && (value["_key"] == null || typeof value["_key"] === "string") && (value["marks"] === void 0 || Array.isArray(value["marks"]) && value["marks"].every((mark) => typeof mark === "string"));
306
+ }
307
+ function isWireSafeMarkDef(value) {
308
+ return isRecord(value) && typeof value["_key"] === "string" && typeof value["_type"] === "string" && (value["_type"] !== "link" || typeof value["href"] === "string");
309
+ }
310
+ function isWireSafeBlock(block) {
311
+ const level = block["level"];
312
+ return (block["_key"] == null || typeof block["_key"] === "string") && (block["style"] === void 0 || typeof block["style"] === "string" && ["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(block["style"])) && (block["listItem"] == null || block["listItem"] === "bullet" || block["listItem"] === "number") && (level == null || typeof level === "number" && Number.isInteger(level) && level >= 0 && level <= 4294967295) && (block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) && block["markDefs"].every(isWireSafeMarkDef)) && block["children"].every(isWireSafeSpan);
313
+ }
314
+ function normalizePortableBlock(block) {
315
+ const normalized = { ...block };
316
+ let incomplete = false;
317
+ if (normalized["_key"] != null && typeof normalized["_key"] !== "string") {
318
+ delete normalized["_key"];
319
+ incomplete = true;
320
+ }
321
+ if (normalized["style"] !== void 0 && (typeof normalized["style"] !== "string" || !["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(normalized["style"]))) {
322
+ delete normalized["style"];
323
+ incomplete = true;
324
+ }
325
+ if (normalized["listItem"] != null && normalized["listItem"] !== "bullet" && normalized["listItem"] !== "number") {
326
+ delete normalized["listItem"];
327
+ incomplete = true;
328
+ }
329
+ const level = normalized["level"];
330
+ if (level != null && (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4294967295)) {
331
+ delete normalized["level"];
332
+ incomplete = true;
333
+ }
334
+ if (normalized["markDefs"] !== void 0) {
335
+ if (!Array.isArray(normalized["markDefs"])) {
336
+ delete normalized["markDefs"];
337
+ incomplete = true;
338
+ } else {
339
+ const defs = normalized["markDefs"].filter(isWireSafeMarkDef);
340
+ if (defs.length !== normalized["markDefs"].length) {
341
+ normalized["markDefs"] = defs;
342
+ incomplete = true;
343
+ }
344
+ }
345
+ }
346
+ normalized["children"] = block["children"].map((child) => {
347
+ if (!isPortableTextSpan(child) || !isRecord(child) || isWireSafeSpan(child)) return child;
348
+ const span = { ...child };
349
+ if (span["_key"] != null && typeof span["_key"] !== "string") delete span["_key"];
350
+ if (span["marks"] !== void 0 && (!Array.isArray(span["marks"]) || !span["marks"].every((mark) => typeof mark === "string"))) delete span["marks"];
351
+ incomplete = true;
352
+ return span;
353
+ });
354
+ return { block: normalized, incomplete };
355
+ }
356
+ function isMixedPortableTextArray(value) {
357
+ return Array.isArray(value) && value.some(isPortableTextBlockCandidate);
358
+ }
359
+ function mapMixedPortableText(blocks, mappings, depth = 0) {
360
+ if (depth > 8) return { blocks: [], capabilities: [] };
361
+ const result = [];
362
+ let capabilities = [...BODY_CAPABILITIES];
363
+ for (const block of blocks) {
364
+ if (isPortableTextArray([block])) {
365
+ result.push(block);
366
+ continue;
367
+ }
368
+ if (isPortableTextBlockCandidate(block)) {
369
+ const normalized = normalizePortableBlock(block);
370
+ const projected = projectInlineBlock(normalized.block, mappings);
371
+ result.push(projected.block);
372
+ capabilities = normalized.incomplete ? [] : intersectCapabilities(capabilities, projected.capabilities);
373
+ continue;
374
+ }
375
+ if (!isRecord(block) || typeof block["_type"] !== "string" || block["_type"] === "block") {
376
+ capabilities = [];
377
+ continue;
378
+ }
379
+ const mapped = mapCustomBlocks([block], mappings, depth + 1);
380
+ result.push(...mapped.blocks);
381
+ capabilities = intersectCapabilities(capabilities, mapped.capabilities);
382
+ }
383
+ return bodyWithCapabilities(result, capabilities);
384
+ }
385
+ function projectInlineBlock(block, mappings) {
386
+ const children = [];
387
+ const defs = Array.isArray(block["markDefs"]) ? [...block["markDefs"]] : [];
388
+ const keys = new Set(defs.flatMap((def) => isRecord(def) && typeof def["_key"] === "string" ? [def["_key"]] : []));
389
+ let sequence = 0;
390
+ let capabilities = block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) ? [...BODY_CAPABILITIES] : [];
391
+ for (const child of block["children"]) {
392
+ if (isRecord(child) && Array.isArray(child["marks"])) {
393
+ for (const mark of child["marks"]) if (typeof mark === "string") keys.add(mark);
394
+ }
395
+ }
396
+ for (const child of block["children"]) {
397
+ if (isPortableTextSpan(child)) {
398
+ children.push(child);
399
+ continue;
400
+ }
401
+ const type = isRecord(child) ? child["_type"] : void 0;
402
+ const mapping = typeof type === "string" && mappings && Object.hasOwn(mappings, type) ? mappings[type] : void 0;
403
+ if (!isRecord(child) || !isRecord(mapping) || !Array.isArray(mapping["inline"])) {
404
+ capabilities = [];
405
+ continue;
406
+ }
407
+ const declared = mapping["capabilities"];
408
+ if (declared !== void 0 && (!Array.isArray(declared) || declared.some((value) => typeof value !== "string" || !BODY_CAPABILITIES.includes(value)) || new Set(declared).size !== declared.length)) {
409
+ capabilities = [];
410
+ continue;
411
+ }
412
+ const newChildren = [];
413
+ const newDefs = [];
414
+ let valid = true;
415
+ for (const field of mapping["inline"]) {
416
+ if (!isRecord(field) || field["kind"] !== "text" && field["kind"] !== "link" || typeof field["field"] !== "string" || !field["field"] || field["optional"] !== void 0 && typeof field["optional"] !== "boolean" || field["kind"] === "link" && (typeof field["hrefField"] !== "string" || !field["hrefField"])) {
417
+ valid = false;
418
+ break;
419
+ }
420
+ const value = child[field["field"]];
421
+ const href = field["kind"] === "link" && typeof field["hrefField"] === "string" && field["hrefField"] ? child[field["hrefField"]] : void 0;
422
+ if ((value == null || value === "") && field["optional"] === true && (field["kind"] !== "link" || href == null || href === "")) continue;
423
+ if (typeof value !== "string" || field["kind"] === "link" && (typeof href !== "string" || !href)) {
424
+ valid = false;
425
+ break;
426
+ }
427
+ if (field["kind"] === "text") newChildren.push({ _type: "span", text: value, marks: [] });
428
+ else {
429
+ let key;
430
+ do {
431
+ key = `aexeo-inline-${sequence++}`;
432
+ } while (keys.has(key));
433
+ keys.add(key);
434
+ newDefs.push({ _key: key, _type: "link", href });
435
+ newChildren.push({ _type: "span", text: value, marks: [key] });
436
+ }
437
+ }
438
+ if (!valid) {
439
+ capabilities = [];
440
+ continue;
441
+ }
442
+ children.push(...newChildren);
443
+ defs.push(...newDefs);
444
+ if (Array.isArray(declared)) capabilities = intersectCapabilities(capabilities, declared);
445
+ }
446
+ return { block: { ...block, children, markDefs: defs }, capabilities };
447
+ }
111
448
  function isEmdashBlocksArray(value, field) {
112
449
  if (!Array.isArray(value) || value.length === 0) return false;
113
450
  return value.every(
114
- (item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
451
+ (item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks" || field === "body" || field === "content")
115
452
  );
116
453
  }
454
+ function mapCustomBlocks(blocks, mappings, depth = 0) {
455
+ if (depth > 8) return { blocks: [], capabilities: [] };
456
+ const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
457
+ const result = [];
458
+ let capabilities = [...BODY_CAPABILITIES];
459
+ const mapFields = (record, fields, depth2) => {
460
+ if (depth2 > 8 || fields.length === 0) return false;
461
+ for (const entry of fields) {
462
+ if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
463
+ if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
464
+ if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
465
+ if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
466
+ const value = record[entry.field];
467
+ if ((value === void 0 || value === null || value === "") && entry.optional) {
468
+ if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
469
+ continue;
470
+ }
471
+ if (entry.kind === "richText") {
472
+ if (!Array.isArray(value)) return false;
473
+ if (value.length === 0 || isPortableTextArray(value)) {
474
+ result.push(...value);
475
+ } else if (isMixedPortableTextArray(value) || isEmdashBlocksArray(value, "blocks")) {
476
+ const nested = mapMixedPortableText(value, mappings, depth2 + 1);
477
+ result.push(...nested.blocks);
478
+ capabilities = intersectCapabilities(capabilities, nested.capabilities);
479
+ } else {
480
+ return false;
481
+ }
482
+ } else if (entry.kind === "heading" || entry.kind === "paragraph") {
483
+ if (typeof value !== "string") return false;
484
+ result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
485
+ } else if (entry.kind === "link") {
486
+ const href = record[entry.hrefField];
487
+ if (typeof value !== "string" || typeof href !== "string" || !href) return false;
488
+ result.push({
489
+ _type: "block",
490
+ children: [{ _type: "span", text: value, marks: ["link"] }],
491
+ markDefs: [{ _key: "link", _type: "link", href }]
492
+ });
493
+ } else if (entry.kind === "repeat") {
494
+ if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
495
+ for (const item of value) {
496
+ if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
497
+ }
498
+ } else {
499
+ return false;
500
+ }
501
+ }
502
+ return true;
503
+ };
504
+ for (const block of blocks) {
505
+ const mapping = mappings?.[String(block["_type"])];
506
+ let fields;
507
+ if (Array.isArray(mapping)) {
508
+ fields = mapping;
509
+ } else if (isRecord(mapping) && Array.isArray(mapping["fields"]) && Array.isArray(mapping["capabilities"])) {
510
+ const declared = mapping["capabilities"];
511
+ if (declared.some((capability) => typeof capability !== "string" || !BODY_CAPABILITIES.includes(capability)) || new Set(declared).size !== declared.length) return unavailable();
512
+ fields = mapping["fields"];
513
+ capabilities = intersectCapabilities(capabilities, declared);
514
+ } else return unavailable();
515
+ if (fields.length === 0) continue;
516
+ if (!mapFields(block, fields, depth)) return unavailable();
517
+ }
518
+ return bodyWithCapabilities(result, capabilities);
519
+ }
117
520
  function blocksToPortableText(blocks) {
118
521
  const paragraphs = [];
119
522
  for (const block of blocks) {
@@ -126,20 +529,49 @@ function blocksToPortableText(blocks) {
126
529
  }
127
530
  return paragraphs;
128
531
  }
532
+ var FALLBACK_MAX_DEPTH = 64;
533
+ var FALLBACK_MAX_NODES = 5e4;
534
+ var FALLBACK_MAX_CHARS = 1048576;
129
535
  function collectText(value, key, out) {
130
- if (typeof value === "string") {
131
- if (!isStructuralField(key) && value.trim().length > 0) out.push(value);
132
- return;
133
- }
134
- if (Array.isArray(value)) {
135
- for (const item of value) collectText(item, key, out);
136
- return;
137
- }
138
- if (!isRecord(value)) return;
139
- for (const [childKey, childValue] of Object.entries(value)) {
140
- if (isStructuralField(childKey)) continue;
141
- collectText(childValue, childKey, out);
142
- }
536
+ const active = /* @__PURE__ */ new WeakSet();
537
+ let nodes = 0;
538
+ let remaining = FALLBACK_MAX_CHARS;
539
+ const visit = (current, field, depth) => {
540
+ if (depth > FALLBACK_MAX_DEPTH || nodes >= FALLBACK_MAX_NODES || remaining <= 0) return;
541
+ nodes++;
542
+ if (typeof current === "string") {
543
+ if (!isStructuralField(field)) {
544
+ const separator = out.length > 0 ? 1 : 0;
545
+ const limit = Math.max(0, remaining - separator);
546
+ let text = current.slice(0, limit);
547
+ if (limit < current.length && /[\uD800-\uDBFF]/.test(text.at(-1) ?? "") && /[\uDC00-\uDFFF]/.test(current[limit] ?? "")) text = text.slice(0, -1);
548
+ text = text.trim();
549
+ if (text.length > 0) {
550
+ out.push(text);
551
+ remaining -= text.length + separator;
552
+ }
553
+ }
554
+ return;
555
+ }
556
+ if (!Array.isArray(current) && !isRecord(current)) return;
557
+ if (active.has(current)) return;
558
+ active.add(current);
559
+ if (Array.isArray(current)) {
560
+ for (const item of current) {
561
+ if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
562
+ visit(item, field, depth + 1);
563
+ }
564
+ } else {
565
+ for (const childKey in current) {
566
+ if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
567
+ if (Object.hasOwn(current, childKey) && !isStructuralField(childKey)) {
568
+ visit(current[childKey], childKey, depth + 1);
569
+ }
570
+ }
571
+ }
572
+ active.delete(current);
573
+ };
574
+ visit(value, key, 0);
143
575
  }
144
576
  var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
145
577
  "_key",
@@ -197,35 +629,210 @@ function isRecord(value) {
197
629
  return typeof value === "object" && value !== null && !Array.isArray(value);
198
630
  }
199
631
 
632
+ // src/version.ts
633
+ var PACKAGE_VERSION = "0.9.1";
634
+
200
635
  // src/plugin.ts
201
636
  var SETTINGS_TOKEN_KEY = "eval_token";
637
+ var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
638
+ var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
639
+ var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
202
640
  var PLUGIN_SETTINGS_SCHEMA = {
641
+ [SETTINGS_CONTENT_FIELDS_KEY]: {
642
+ type: "string",
643
+ label: "Content fields by collection (JSON)",
644
+ description: "Ordered rendered body fields per collection.",
645
+ multiline: true
646
+ },
647
+ [SETTINGS_DESCRIPTION_FIELDS_KEY]: {
648
+ type: "string",
649
+ label: "Description fields by collection (JSON)",
650
+ description: "Ordered renderer description fallback fields per collection.",
651
+ multiline: true
652
+ },
653
+ [SETTINGS_BLOCK_MAPPINGS_KEY]: {
654
+ type: "string",
655
+ label: "Custom block mappings (JSON)",
656
+ description: "Ordered mappings for the rendered fields of each custom block type.",
657
+ multiline: true
658
+ },
203
659
  [SETTINGS_TOKEN_KEY]: {
204
660
  type: "secret",
205
661
  label: "EVAL_TOKEN",
206
662
  description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
207
663
  }
208
664
  };
209
- async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter) {
210
- const adapted = await adaptContentItemWithContext(
211
- event.content,
212
- ctx.content,
213
- ctx.site?.locale
665
+ function recordContentCoverage(ctx, documents) {
666
+ const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
667
+ for (const document of documents) {
668
+ counts[cmsBodyCoverage(document)]++;
669
+ }
670
+ try {
671
+ ctx.log?.info?.("aexeo.content_coverage", counts);
672
+ ctx.log?.info?.("aexeo.page_coverage", { documents: documents.length, rendered_page: "not_observed", generated_metadata: "not_observed" });
673
+ const observed = documents.filter((document) => document.description_source_observed === true).length;
674
+ const withValue = documents.filter((document) => document.description_source_observed === true && !!document.description).length;
675
+ ctx.log?.info?.("aexeo.description_coverage", { documents: documents.length, observed, with_value: withValue, unverified: documents.length - observed });
676
+ } catch {
677
+ }
678
+ }
679
+ function recordAssessmentCoverage(ctx, result, documents) {
680
+ let passed = 0, failed = 0, notChecked = 0, mappingReports = 0, mappingEligible = 0, mappingMapped = 0, mappingUnmapped = 0;
681
+ if (result.ok) for (const assessment of Object.values(result.assessments ?? {})) {
682
+ if (assessment.status !== "evaluated") continue;
683
+ if (assessment.source_mapping) {
684
+ mappingReports++;
685
+ mappingEligible += assessment.source_mapping.eligible;
686
+ mappingMapped += assessment.source_mapping.mapped;
687
+ mappingUnmapped += assessment.source_mapping.unmapped;
688
+ }
689
+ for (const gate of assessment.gates) {
690
+ if (gate.status === "passed") passed++;
691
+ else if (gate.status === "failed") failed++;
692
+ else notChecked++;
693
+ }
694
+ }
695
+ let assessedDocuments = 0, missingDocuments = 0, errorDocuments = 0;
696
+ if (result.ok) for (const document of documents) {
697
+ const assessment = result.assessments?.[document.route];
698
+ if (assessment === void 0) missingDocuments++;
699
+ else if (assessment.status === "error") errorDocuments++;
700
+ else assessedDocuments++;
701
+ }
702
+ const collidingFindings = result.ok ? result.findings.filter((finding) => finding.aexeo_origin === "content_preset" && (SITE_GRAPH_RULE_IDS.has(finding.rule_id) || !isLegacyCmsObservableFinding(finding))).length : 0;
703
+ try {
704
+ ctx.log?.info?.("aexeo.preset_coverage", { status: result.ok ? "evaluated" : "error", documents: documents.length, assessed_documents: assessedDocuments, missing_assessments: missingDocuments, assessment_errors: errorDocuments, passed, failed, not_checked: notChecked, colliding_findings: collidingFindings, mapping_reports: mappingReports, mapping_eligible: mappingEligible, mapping_mapped: mappingMapped, mapping_unmapped: mappingUnmapped });
705
+ } catch {
706
+ }
707
+ const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
708
+ let withheld = 0, incompleteBodyWithheld = 0;
709
+ if (result.ok) for (const finding of result.findings) {
710
+ if (finding.aexeo_origin === "content_preset") continue;
711
+ if (!isLegacyCmsObservableFinding(finding)) withheld++;
712
+ else if (!isLegacyCmsObservableFinding(finding, completeByRoute.get(finding.path) ?? false)) incompleteBodyWithheld++;
713
+ }
714
+ try {
715
+ ctx.log?.info?.("aexeo.cms_finding_coverage", { status: result.ok ? "evaluated" : "error", rendered_findings_withheld: withheld, incomplete_body_findings_withheld: incompleteBodyWithheld });
716
+ } catch {
717
+ }
718
+ }
719
+ async function handleAfterDelete(event, ctx) {
720
+ for (const entry of await ctx.kv.list("document:")) {
721
+ const stored = toStoredDocument(entry.value);
722
+ if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
723
+ await ctx.kv.delete(entry.key);
724
+ await ctx.kv.delete(findingsKey(stored.document.route));
725
+ }
726
+ }
727
+ }
728
+ var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
729
+ "LNK001",
730
+ // internal link target missing from the site
731
+ "LNK002",
732
+ // no inbound links
733
+ "LNK003",
734
+ // weak anchor, checked only when the target route exists
735
+ "LNK004",
736
+ // too few inbound links
737
+ "SEO009",
738
+ // hreflang alternate target missing from the site
739
+ "SEO012",
740
+ // hreflang alternate does not link back
741
+ "SEO016",
742
+ // canonical points at another crawlable route
743
+ "SEO017",
744
+ // title and description shared with another route
745
+ "CNT005"
746
+ // visible text duplicated on another route
747
+ ]);
748
+ function isSiteGraphFinding(finding) {
749
+ return finding.aexeo_origin !== "content_preset" && SITE_GRAPH_RULE_IDS.has(finding.rule_id);
750
+ }
751
+ function withCmsSourceLocation(finding, stored) {
752
+ if (finding.aexeo_cms_location !== void 0) {
753
+ finding = { ...finding };
754
+ delete finding.aexeo_cms_location;
755
+ }
756
+ const location = finding.aexeo_source_location;
757
+ if (finding.aexeo_origin !== "content_preset" || !location || !stored) return finding;
758
+ const blockPath = stored.meta.bodySourcePaths?.[location.block_index];
759
+ const spanPath = stored.meta.bodySourceSpanPaths?.[location.block_index]?.[location.span_index];
760
+ const path = blockPath ? [...blockPath, "children", location.span_index, "text"] : spanPath;
761
+ const text = stored.document.body?.[location.block_index]?.children[location.span_index]?.text;
762
+ const offset = location.utf16_offset;
763
+ if (!path || typeof text !== "string" || !Number.isSafeInteger(offset) || offset < 0 || offset > text.length || offset > 0 && offset < text.length && /[\uD800-\uDBFF]/.test(text[offset - 1]) && /[\uDC00-\uDFFF]/.test(text[offset])) return finding;
764
+ return { ...finding, aexeo_cms_location: { path, utf16_offset: offset } };
765
+ }
766
+ function countCmsLocations(counts, findings) {
767
+ for (const finding of findings) {
768
+ if (finding.aexeo_origin !== "content_preset" || finding.aexeo_source_location === void 0) continue;
769
+ counts.eligible++;
770
+ if (finding.aexeo_cms_location !== void 0) counts.mapped++;
771
+ }
772
+ }
773
+ function recordCmsLocationCoverage(ctx, counts) {
774
+ try {
775
+ ctx.log?.info?.("aexeo.cms_location_coverage", { ...counts, unmapped: counts.eligible - counts.mapped });
776
+ } catch {
777
+ }
778
+ }
779
+ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields, descriptionFields, contentFieldsUnavailable = false) {
780
+ const prior = (await readAllStoredDocuments(ctx.kv)).filter(
781
+ (stored2) => stored2.meta.id === event.content.id && stored2.meta.collection === event.content.type
214
782
  );
783
+ let adapted;
784
+ try {
785
+ adapted = await adaptContentItemWithContext(
786
+ event.content,
787
+ ctx.content,
788
+ ctx.site?.locale,
789
+ blockMappings,
790
+ contentFields,
791
+ descriptionFields
792
+ );
793
+ } catch {
794
+ try {
795
+ ctx.log?.info?.("aexeo.adaptation_coverage", { failed: 1 });
796
+ } catch {
797
+ }
798
+ ctx.log?.error?.("Aexeo afterSave adaptation failed");
799
+ const previous = prior.length === 1 ? prior[0] : void 0;
800
+ if (previous !== void 0) await markAdaptationUnavailable(ctx, event.content, previous);
801
+ return;
802
+ }
215
803
  const document = adapted.document;
804
+ if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
805
+ recordContentCoverage(ctx, [document]);
216
806
  const { kv, log } = ctx;
217
807
  const stored = { document, meta: adapted.meta };
218
- await persistDocument(ctx, stored);
219
- const result = await evaluator([document]);
808
+ if (!await persistDocument(ctx, stored)) {
809
+ log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
810
+ return;
811
+ }
812
+ const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
813
+ recordAssessmentCoverage(ctx, result, [document]);
220
814
  if (!result.ok) {
221
815
  log?.error?.(`afterSave evaluator failure (${result.reason})`, {
222
816
  detail: result.detail
223
817
  });
818
+ if (!await isSuperseded(kv, stored)) {
819
+ await kv.set(findingsKey(document.route), {
820
+ route: document.route,
821
+ findings: [],
822
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
823
+ });
824
+ }
224
825
  return;
225
826
  }
226
- const pageFindings = result.findings.filter(
227
- (finding) => finding.scope === "page"
827
+ const storedGraphFindings = (await readFindings(kv, document.route)).filter(
828
+ (finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
228
829
  );
830
+ const pageFindings = [
831
+ ...result.findings.filter(
832
+ (finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
833
+ ).map((finding) => withCmsSourceLocation(finding, stored)),
834
+ ...storedGraphFindings
835
+ ];
229
836
  const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
230
837
  {
231
838
  route: document.route,
@@ -234,12 +841,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
234
841
  },
235
842
  pageFindings
236
843
  );
844
+ if (await isSuperseded(kv, stored)) {
845
+ log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
846
+ return;
847
+ }
848
+ const cmsLocationCounts = { eligible: 0, mapped: 0 };
849
+ countCmsLocations(cmsLocationCounts, filtered);
850
+ recordCmsLocationCoverage(ctx, cmsLocationCounts);
237
851
  await kv.set(findingsKey(document.route), {
238
852
  route: document.route,
239
- findings: filtered
853
+ findings: filtered,
854
+ body_coverage: cmsBodyCoverage(document),
855
+ ...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
240
856
  });
241
857
  }
242
858
  var DEFAULT_COLLECTIONS = ["posts", "pages"];
859
+ async function markAdaptationUnavailable(ctx, item, previous) {
860
+ if (isOlderRevision(item.updatedAt, previous.meta.updatedAt) || await isSuperseded(ctx.kv, previous)) return;
861
+ const unavailable = {
862
+ document: { route: previous.document.route, title: previous.document.title },
863
+ meta: {
864
+ id: item.id,
865
+ collection: item.type,
866
+ slug: item.slug,
867
+ status: item.status,
868
+ title: previous.meta.title,
869
+ publicUrl: null,
870
+ adaptationFailed: true,
871
+ ...item.updatedAt === void 0 ? {} : { updatedAt: item.updatedAt }
872
+ }
873
+ };
874
+ if (!await persistDocument(ctx, unavailable) || await isSuperseded(ctx.kv, unavailable)) return;
875
+ recordContentCoverage(ctx, [unavailable.document]);
876
+ await ctx.kv.set(findingsKey(unavailable.document.route), {
877
+ route: unavailable.document.route,
878
+ findings: [],
879
+ assessment: { status: "error", reason: "adaptation_failed", detail: "The CMS record could not be adapted." }
880
+ });
881
+ }
882
+ async function evaluateSafely(evaluator, documents, context) {
883
+ try {
884
+ return await evaluator(documents, context);
885
+ } catch {
886
+ return { ok: false, reason: "evaluator_exception", detail: "The evaluator failed unexpectedly." };
887
+ }
888
+ }
243
889
  async function evaluateAndPersistAll(ctx, options) {
244
890
  const collections = options.collections ?? DEFAULT_COLLECTIONS;
245
891
  const suppressionFilter = options.suppressionFilter;
@@ -250,11 +896,15 @@ async function evaluateAndPersistAll(ctx, options) {
250
896
  totalFindings: 0,
251
897
  errors: []
252
898
  };
899
+ const storedAtStart = await readAllStoredDocuments(kv);
253
900
  const documents = [];
901
+ const documentCollections = [];
254
902
  const adaptedByRoute = /* @__PURE__ */ new Map();
255
903
  const documentRoutes = /* @__PURE__ */ new Set();
904
+ const adaptationFailures = [];
256
905
  for (const collection of collections) {
257
906
  let cursor;
907
+ const seenCursors = /* @__PURE__ */ new Set();
258
908
  do {
259
909
  try {
260
910
  const page = await ctx.content.list(collection, {
@@ -262,19 +912,35 @@ async function evaluateAndPersistAll(ctx, options) {
262
912
  ...cursor === void 0 ? {} : { cursor }
263
913
  });
264
914
  for (const item of page.items) {
265
- const adapted = await adaptContentItemWithContext(
266
- item,
267
- ctx.content,
268
- ctx.site?.locale
269
- );
270
- documents.push(adapted.document);
271
- documentRoutes.add(adapted.document.route);
272
- adaptedByRoute.set(adapted.document.route, {
273
- document: adapted.document,
274
- meta: adapted.meta
275
- });
915
+ try {
916
+ const adapted = await adaptContentItemWithContext(
917
+ item,
918
+ ctx.content,
919
+ ctx.site?.locale,
920
+ options.blockMappings,
921
+ options.contentFieldsByCollection?.[collection],
922
+ options.descriptionFieldsByCollection?.[collection]
923
+ );
924
+ if (options.contentFieldsUnavailable && adapted.document.body !== void 0) adapted.document.body_capabilities = [];
925
+ documents.push(adapted.document);
926
+ documentCollections.push(adapted.meta.collection);
927
+ documentRoutes.add(adapted.document.route);
928
+ adaptedByRoute.set(adapted.document.route, {
929
+ document: adapted.document,
930
+ meta: adapted.meta
931
+ });
932
+ } catch {
933
+ adaptationFailures.push({ item, collection });
934
+ summary.errors.push(`${collection}: content adaptation failed`);
935
+ }
936
+ }
937
+ if (page.hasMore) {
938
+ if (!page.cursor || seenCursors.has(page.cursor)) throw new Error("incomplete pagination: missing or repeated cursor");
939
+ seenCursors.add(page.cursor);
940
+ cursor = page.cursor;
941
+ } else {
942
+ cursor = void 0;
276
943
  }
277
- cursor = page.hasMore ? page.cursor : void 0;
278
944
  } catch (err) {
279
945
  const message = err instanceof Error ? err.message : String(err);
280
946
  summary.errors.push(`${collection}: ${message}`);
@@ -282,66 +948,183 @@ async function evaluateAndPersistAll(ctx, options) {
282
948
  }
283
949
  } while (cursor !== void 0);
284
950
  }
951
+ const inventoryComplete = summary.errors.length === 0;
285
952
  summary.documentsScanned = documents.length;
953
+ recordContentCoverage(ctx, documents);
954
+ if (adaptationFailures.length > 0) {
955
+ try {
956
+ ctx.log?.info?.("aexeo.adaptation_coverage", { failed: adaptationFailures.length });
957
+ } catch {
958
+ }
959
+ const storedByCollection = /* @__PURE__ */ new Map();
960
+ for (const stored of storedAtStart) {
961
+ const byId = storedByCollection.get(stored.meta.collection) ?? /* @__PURE__ */ new Map();
962
+ const entries = byId.get(stored.meta.id) ?? [];
963
+ entries.push(stored);
964
+ byId.set(stored.meta.id, entries);
965
+ storedByCollection.set(stored.meta.collection, byId);
966
+ }
967
+ for (const { item, collection } of adaptationFailures) {
968
+ for (const stored of storedByCollection.get(collection)?.get(item.id) ?? []) {
969
+ if (documentRoutes.has(stored.document.route)) continue;
970
+ await markAdaptationUnavailable(ctx, item, stored);
971
+ }
972
+ }
973
+ }
286
974
  for (const stored of adaptedByRoute.values()) {
287
- await kv.set(documentKey(stored.document.route), stored);
975
+ const current = await readStoredDocument(kv, stored.document.route);
976
+ if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
977
+ await kv.set(documentKey(stored.document.route), stored);
978
+ }
288
979
  }
289
- const result = await options.evaluator(documents);
980
+ const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
981
+ recordAssessmentCoverage(ctx, result, documents);
290
982
  if (!result.ok) {
291
983
  log?.error?.(`aexeo evaluator failure (${result.reason})`, {
292
984
  detail: result.detail
293
985
  });
294
986
  summary.errors.push(`${result.reason}: ${result.detail}`);
987
+ try {
988
+ ctx.log?.info?.("aexeo.sitewide_coverage", { status: "evaluation_error" });
989
+ } catch {
990
+ }
991
+ await kv.set(findingsKey("*"), {
992
+ route: "*",
993
+ findings: [],
994
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
995
+ });
996
+ for (const [route, stored] of adaptedByRoute) {
997
+ if (!await isSuperseded(kv, stored)) {
998
+ await kv.set(findingsKey(route), {
999
+ route,
1000
+ findings: [],
1001
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
1002
+ });
1003
+ }
1004
+ }
295
1005
  return summary;
296
1006
  }
297
1007
  const findingsByRoute = /* @__PURE__ */ new Map();
1008
+ findingsByRoute.set("*", []);
298
1009
  for (const route of documentRoutes) {
299
1010
  findingsByRoute.set(route, []);
300
1011
  }
1012
+ const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
1013
+ const allBodiesComplete = documents.every(hasCompleteBody);
1014
+ const changed = await refreshInputChanged(kv, collections, adaptedByRoute, storedAtStart);
1015
+ const coverageStatus = !inventoryComplete ? "inventory_incomplete" : changed ? "changed_during_refresh" : !allBodiesComplete ? "body_incomplete" : "complete";
1016
+ const globalCoverageComplete = coverageStatus === "complete";
1017
+ try {
1018
+ ctx.log?.info?.("aexeo.sitewide_coverage", { status: coverageStatus });
1019
+ } catch {
1020
+ }
1021
+ const globalAssessment = globalCoverageComplete ? void 0 : {
1022
+ status: "error",
1023
+ reason: coverageStatus,
1024
+ detail: coverageStatus === "changed_during_refresh" ? "Sitewide and cross-document checks are unavailable because content changed during refresh." : "Sitewide and cross-document checks are unavailable because the CMS input is incomplete."
1025
+ };
1026
+ if (changed && inventoryComplete) {
1027
+ summary.errors.push("Sitewide checks unavailable: content changed during refresh.");
1028
+ } else if (!allBodiesComplete && inventoryComplete) {
1029
+ summary.errors.push("Sitewide checks unavailable: one or more document bodies are incomplete.");
1030
+ }
301
1031
  for (const finding of result.findings) {
1032
+ if (!globalCoverageComplete && (finding.scope !== "page" || isSiteGraphFinding(finding))) continue;
1033
+ const complete = finding.scope === "page" ? completeByRoute.get(finding.path) ?? false : allBodiesComplete;
1034
+ if (!isCmsObservableFinding(finding, complete)) continue;
302
1035
  const bucket = finding.scope === "page" ? finding.path : "*";
303
1036
  const list = findingsByRoute.get(bucket) ?? [];
304
- list.push(finding);
1037
+ list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
305
1038
  findingsByRoute.set(bucket, list);
306
1039
  }
307
1040
  let totalAfterSuppression = 0;
1041
+ const cmsLocationCounts = { eligible: 0, mapped: 0 };
308
1042
  for (const [route, findings] of findingsByRoute) {
309
1043
  const stored = adaptedByRoute.get(route);
1044
+ if (stored !== void 0 && await isSuperseded(kv, stored)) {
1045
+ continue;
1046
+ }
310
1047
  const context = {
311
1048
  route,
312
1049
  ...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
313
1050
  ...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
314
1051
  };
315
1052
  const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
316
- await kv.set(findingsKey(route), { route, findings: filtered });
1053
+ countCmsLocations(cmsLocationCounts, filtered);
1054
+ await kv.set(findingsKey(route), {
1055
+ route,
1056
+ findings: filtered,
1057
+ ...stored === void 0 ? {} : { body_coverage: cmsBodyCoverage(stored.document) },
1058
+ ...route === "*" && globalAssessment !== void 0 ? { assessment: globalAssessment } : result.assessments?.[route] === void 0 ? {} : { assessment: result.assessments[route] }
1059
+ });
317
1060
  summary.routesUpdated += 1;
318
1061
  totalAfterSuppression += filtered.length;
319
1062
  }
1063
+ recordCmsLocationCoverage(ctx, cmsLocationCounts);
320
1064
  summary.totalFindings = totalAfterSuppression;
321
- if (summary.errors.length === 0) {
322
- await removeStaleDocuments(ctx, collections, documentRoutes);
1065
+ if (inventoryComplete && !changed) {
1066
+ await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
323
1067
  }
324
1068
  return summary;
325
1069
  }
1070
+ async function refreshInputChanged(kv, collections, evaluated, initial) {
1071
+ const current = await readAllStoredDocuments(kv);
1072
+ const currentByRoute = new Map(current.map((entry) => [entry.document.route, entry]));
1073
+ const initialByRoute = new Map(initial.map((entry) => [entry.document.route, entry]));
1074
+ for (const [route, entry] of evaluated) {
1075
+ if (!sameStoredDocument(entry, currentByRoute.get(route))) return true;
1076
+ }
1077
+ const selected = new Set(collections);
1078
+ for (const entry of current) {
1079
+ const route = entry.document.route;
1080
+ if (!evaluated.has(route) && (entry.meta.collection === "" || selected.has(entry.meta.collection)) && !sameStoredDocument(entry, initialByRoute.get(route))) return true;
1081
+ }
1082
+ return false;
1083
+ }
1084
+ function isOlderRevision(revision, other) {
1085
+ if (revision === void 0 || other === void 0) return false;
1086
+ return Date.parse(revision) < Date.parse(other);
1087
+ }
1088
+ async function readStoredDocument(kv, route) {
1089
+ return toStoredDocument(await kv.get(documentKey(route)));
1090
+ }
1091
+ function sameStoredDocument(a, b) {
1092
+ return a != null && b != null && JSON.stringify(a) === JSON.stringify(b);
1093
+ }
1094
+ async function isSuperseded(kv, evaluated) {
1095
+ return !sameStoredDocument(evaluated, await readStoredDocument(kv, evaluated.document.route));
1096
+ }
326
1097
  async function persistDocument(ctx, stored) {
327
1098
  const route = stored.document.route;
328
- const entries = await ctx.kv.list("document:");
329
- for (const entry of entries) {
1099
+ const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
330
1100
  const previous = toStoredDocument(entry.value);
331
- if (previous === null || previous.document.route === route || previous.meta.id !== stored.meta.id || previous.meta.collection !== stored.meta.collection) {
332
- continue;
333
- }
334
- await ctx.kv.delete(entry.key);
1101
+ return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
1102
+ });
1103
+ if (sameEntry.some(
1104
+ ({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
1105
+ )) {
1106
+ return false;
1107
+ }
1108
+ for (const { key, previous } of sameEntry) {
1109
+ if (previous.document.route === route) continue;
1110
+ await ctx.kv.delete(key);
335
1111
  await ctx.kv.delete(findingsKey(previous.document.route));
336
1112
  }
337
1113
  await ctx.kv.set(documentKey(route), stored);
1114
+ return true;
338
1115
  }
339
- async function removeStaleDocuments(ctx, collections, currentRoutes) {
1116
+ async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
340
1117
  const scannedCollections = new Set(collections);
1118
+ const revisionAtStart = new Map(
1119
+ storedAtStart.map((stored) => [
1120
+ stored.document.route,
1121
+ stored.meta.updatedAt
1122
+ ])
1123
+ );
341
1124
  const entries = await ctx.kv.list("document:");
342
1125
  for (const entry of entries) {
343
1126
  const stored = toStoredDocument(entry.value);
344
- if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
1127
+ if (stored === null || stored.meta.collection !== "" && !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route) || !revisionAtStart.has(stored.document.route) || revisionAtStart.get(stored.document.route) !== stored.meta.updatedAt) {
345
1128
  continue;
346
1129
  }
347
1130
  await ctx.kv.delete(entry.key);
@@ -401,6 +1184,23 @@ async function readFindings(kv, route) {
401
1184
  }
402
1185
  return stored.findings;
403
1186
  }
1187
+ async function readAssessment(kv, route) {
1188
+ const stored = await kv.get(findingsKey(route));
1189
+ return stored?.assessment;
1190
+ }
1191
+
1192
+ // src/assessment-display.ts
1193
+ function describeGate(gate) {
1194
+ const status = gate.status === "not_checked" ? "not checked" : gate.status;
1195
+ const reason = gate.status === "not_checked" ? `: ${gate.reason ?? "required input unavailable"}` : "";
1196
+ const metrics = Object.entries(gate.measurements ?? {}).map(([key, value]) => `${key.replaceAll("_", " ")}: ${value}`).join(", ");
1197
+ return `${gate.gate_id} ${status}${reason} (${gate.blocking ? "blocking" : "advisory"})${metrics ? ` \u2014 ${metrics}` : ""}`;
1198
+ }
1199
+ function summarizeGates(gates) {
1200
+ const counts = { passed: 0, failed: 0, not_checked: 0 };
1201
+ for (const gate of gates) counts[gate.status]++;
1202
+ return `${gates.length} configured checks: ${counts.passed} passed, ${counts.failed} failed, ${counts.not_checked} not checked`;
1203
+ }
404
1204
 
405
1205
  // src/findings-view.ts
406
1206
  function normalizeInteraction(input) {
@@ -438,6 +1238,10 @@ async function handleRefresh(options) {
438
1238
  summary = await evaluateAndPersistAll(options.ctx, {
439
1239
  collections: options.collections,
440
1240
  evaluator: options.evaluator,
1241
+ blockMappings: options.blockMappings,
1242
+ contentFieldsByCollection: options.contentFieldsByCollection,
1243
+ contentFieldsUnavailable: options.contentFieldsUnavailable,
1244
+ descriptionFieldsByCollection: options.descriptionFieldsByCollection,
441
1245
  // Always forwarded. See RefreshOptions.suppressionFilter.
442
1246
  suppressionFilter: options.suppressionFilter
443
1247
  });
@@ -473,21 +1277,38 @@ async function handleRefresh(options) {
473
1277
  }
474
1278
  };
475
1279
  }
476
- async function readAllFindings(kv) {
477
- const entries = await kv.list(
478
- "findings:"
479
- );
480
- const out = [];
1280
+ async function readFindingsSnapshot(kv) {
1281
+ const entries = await kv.list("findings:");
1282
+ const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
1283
+ const incomplete = sitewideAssessment?.status === "error";
1284
+ const findings = [];
1285
+ const routes = /* @__PURE__ */ new Set();
1286
+ let failedAssessments = 0;
1287
+ let uncheckedGates = 0;
1288
+ let incompleteBodies = 0;
1289
+ let unknownBodies = 0;
1290
+ let missingAssessments = 0;
481
1291
  for (const entry of entries) {
482
- if (entry.value === null) {
483
- continue;
484
- }
1292
+ if (entry.value === null) continue;
485
1293
  const route = entry.key.replace(/^findings:/, "");
1294
+ if (route !== "*") {
1295
+ routes.add(route);
1296
+ if (entry.value.body_coverage === void 0) unknownBodies++;
1297
+ else if (entry.value.body_coverage !== "complete") incompleteBodies++;
1298
+ const assessment = entry.value.assessment;
1299
+ if (assessment === void 0) missingAssessments++;
1300
+ if (assessment?.status === "error") failedAssessments++;
1301
+ if (assessment?.status === "evaluated") {
1302
+ uncheckedGates += assessment.gates.filter((gate) => gate.status === "not_checked").length;
1303
+ }
1304
+ }
486
1305
  for (const finding of entry.value.findings) {
487
- out.push({ ...finding, document_route: route });
1306
+ if (!isCmsObservableFinding(finding, route === "*" || entry.value.body_coverage === "complete") || incomplete && isSiteGraphFinding(finding)) continue;
1307
+ findings.push({ ...finding, document_route: route });
1308
+ if (route === "*") routes.add(route);
488
1309
  }
489
1310
  }
490
- return out;
1311
+ return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
491
1312
  }
492
1313
  function severityFirst(a, b) {
493
1314
  const rank = (severity) => severity === "error" ? 0 : 1;
@@ -505,7 +1326,7 @@ function uniqueRoutes(rows) {
505
1326
  return [...routes].sort();
506
1327
  }
507
1328
  async function renderFindingsPage(kv, tuning) {
508
- const findings = await readAllFindings(kv);
1329
+ const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
509
1330
  const errors = findings.filter((finding) => finding.severity === "error");
510
1331
  const warnings = findings.filter(
511
1332
  (finding) => finding.severity === "warning"
@@ -516,8 +1337,13 @@ async function renderFindingsPage(kv, tuning) {
516
1337
  { type: "header", text: "SEO findings" },
517
1338
  {
518
1339
  type: "context",
519
- text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
1340
+ text: findings.length === 0 ? documentRoutes.length === 0 ? "No CMS document results available \u2014 click Refresh to evaluate records." : "No findings reported for the available CMS checks. Inspect assessments for unchecked or failed evaluations." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
520
1341
  },
1342
+ { type: "context", text: "Rendered page markup, semantic sections, generated metadata, duplicate pages and sitewide links are not checked here. Audit the built site for page-level and link findings." },
1343
+ ...incompleteBodies || unknownBodies ? [{ type: "context", text: `${incompleteBodies} document bodies were incomplete or unavailable; coverage is unknown for ${unknownBodies} older or failed evaluations. Inspect individual documents.` }] : [],
1344
+ ...missingAssessments ? [{ type: "context", text: `No content preset assessment was recorded for ${missingAssessments} document(s); absence of findings is not an editorial quality assessment.` }] : [],
1345
+ ...failedAssessments || uncheckedGates ? [{ type: "context", text: `${failedAssessments} document evaluations failed; ${uncheckedGates} preset checks were not checked. Inspect individual documents for details.` }] : [],
1346
+ ...sitewideAssessment?.status === "error" ? [{ type: "context", text: `Sitewide checks unavailable: ${sitewideAssessment.reason}. ${sitewideAssessment.detail}` }] : [],
521
1347
  { type: "divider" },
522
1348
  {
523
1349
  type: "actions",
@@ -542,10 +1368,10 @@ async function renderFindingsPage(kv, tuning) {
542
1368
  },
543
1369
  sorted.length === 0 ? {
544
1370
  type: "context",
545
- text: tuning.emptyFindingsText
1371
+ text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
546
1372
  } : findingsTable(sorted)
547
1373
  ];
548
- if (routes.length > 0) {
1374
+ if (documentRoutes.length > 0) {
549
1375
  blocks.push({
550
1376
  type: "form",
551
1377
  fields: [
@@ -553,7 +1379,7 @@ async function renderFindingsPage(kv, tuning) {
553
1379
  type: "select",
554
1380
  action_id: "route_picker",
555
1381
  label: "Document to inspect",
556
- options: routes.map((route) => ({ label: route, value: route }))
1382
+ options: documentRoutes.map((route) => ({ label: route, value: route }))
557
1383
  }
558
1384
  ],
559
1385
  submit: { label: "View document SEO", action_id: "view_document" }
@@ -578,55 +1404,13 @@ function findingsTable(rows) {
578
1404
  }))
579
1405
  };
580
1406
  }
581
- async function renderScoreWidget(kv, tuning) {
582
- const documents = await readAllDocuments(kv);
583
- if (documents.length === 0) {
584
- return {
585
- blocks: [
586
- { type: "header", text: "SEO score" },
587
- {
588
- type: "context",
589
- text: tuning.emptyScoreText
590
- }
591
- ]
592
- };
593
- }
594
- const score = await tuning.score(documents, kv);
595
- const blocks = [
596
- {
597
- type: "stats",
598
- items: [
599
- { label: "Overall", value: `${score.overall_score}` },
600
- { label: "Citation", value: `${score.citation_readiness_score}` },
601
- {
602
- label: tuning.truthLabel(score),
603
- value: `${score.truth_consistency_score}`
604
- },
605
- { label: "Answers", value: `${score.answer_pack_score}` }
606
- ]
607
- }
608
- ];
609
- if (score.overall_score < 60) {
610
- blocks.unshift({
611
- type: "banner",
612
- title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
613
- variant: "alert"
614
- });
615
- }
616
- if (score.blockers.length > 0) {
617
- blocks.push({
618
- type: "context",
619
- text: topBlockersLine(score)
620
- });
621
- }
622
- return { blocks };
623
- }
624
- function topBlockersLine(score) {
625
- const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
626
- if (top.length === 0) {
627
- return "No blockers identified.";
628
- }
629
- return `Top blockers: ${top.join(" \u2022 ")}`;
1407
+ async function renderScoreWidget(_kv, _tuning, _log) {
1408
+ return {
1409
+ blocks: [
1410
+ { type: "header", text: "SEO score unavailable" },
1411
+ { type: "context", text: "CMS records do not include the rendered site. Run an Aexeo site audit on the built site to assess SEO and page-level signals." }
1412
+ ]
1413
+ };
630
1414
  }
631
1415
  async function renderDocumentPanel(kv, route) {
632
1416
  if (route === void 0) {
@@ -640,7 +1424,11 @@ async function renderDocumentPanel(kv, route) {
640
1424
  ]
641
1425
  };
642
1426
  }
643
- const findings = await readFindings(kv, route);
1427
+ const incomplete = (await readAssessment(kv, "*"))?.status === "error";
1428
+ const record = await kv.get(findingsKey(route));
1429
+ const findings = (record?.findings ?? []).filter((finding) => isCmsObservableFinding(finding, record?.body_coverage === "complete") && (!incomplete || !isSiteGraphFinding(finding)));
1430
+ const assessment = record?.assessment;
1431
+ const bodyNotice = record && assessment?.status !== "error" ? cmsBodyCoverageNotice(record.body_coverage) : void 0;
644
1432
  const errors = findings.filter((finding) => finding.severity === "error");
645
1433
  const warnings = findings.filter(
646
1434
  (finding) => finding.severity === "warning"
@@ -651,8 +1439,14 @@ async function renderDocumentPanel(kv, route) {
651
1439
  { type: "header", text: `Document SEO \u2014 ${route}` },
652
1440
  {
653
1441
  type: "context",
654
- text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
1442
+ text: !record ? `No evaluation is available for ${route}. Save the document or refresh to evaluate it.` : assessment?.status === "error" ? `Evaluation failed for ${route}; findings are unavailable for this revision.` : findings.length === 0 ? `No findings for ${route} from the available CMS checks.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
655
1443
  },
1444
+ { type: "context", text: "Rendered page markup, semantic sections, generated metadata, duplicate pages and sitewide links are not checked here. Audit the built site for page-level and link findings." },
1445
+ ...bodyNotice ? [{ type: "context", text: bodyNotice }] : [],
1446
+ ...incomplete ? [{ type: "context", text: "Sitewide and cross-document checks are unavailable; refresh after resolving the CMS input error." }] : [],
1447
+ ...record && assessment === void 0 ? [{ type: "context", text: "No content preset assessment was recorded for this document; absence of findings is not an editorial quality assessment." }] : [],
1448
+ ...assessment?.status === "error" ? [{ type: "context", text: `Evaluation failed: ${assessment.reason}. Findings are unavailable for this revision.` }] : [],
1449
+ ...assessment?.status === "evaluated" ? [{ type: "context", text: `Content preset: ${assessment.preset}. ${summarizeGates(assessment.gates)}. Results cover only these configured checks.` }, ...assessment.gates.map((gate) => ({ type: "context", text: describeGate(gate) }))] : [],
656
1450
  ...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
657
1451
  ]
658
1452
  };
@@ -664,16 +1458,13 @@ function documentFindingsTable(findings) {
664
1458
  { key: "rule", label: "Rule", format: "code" },
665
1459
  { key: "severity", label: "Severity", format: "badge" },
666
1460
  { key: "message", label: "Message" },
667
- { key: "block", label: "Block", format: "code" }
1461
+ { key: "block", label: "Location", format: "code" }
668
1462
  ],
669
1463
  rows: findings.map((finding) => ({
670
1464
  rule: finding.rule_id,
671
1465
  severity: finding.severity,
672
1466
  message: finding.message,
673
- // The bridge stamps every Portable Text block with id + data-pt-key
674
- // when rendering; surfacing the path:line locator here lets authors
675
- // locate the failing block via the editor's ⌘-F.
676
- block: `${finding.path}:${finding.line}`
1467
+ block: finding.aexeo_cms_location ? `CMS path ${JSON.stringify(finding.aexeo_cms_location.path)}, UTF-16 offset ${finding.aexeo_cms_location.utf16_offset}` : finding.aexeo_source_location ? `Projected body: block ${finding.aexeo_source_location.block_index + 1}, span ${finding.aexeo_source_location.span_index + 1}, UTF-16 offset ${finding.aexeo_source_location.utf16_offset}` : finding.aexeo_origin === "content_preset" ? "Projected location unavailable" : `Reported line ${finding.line}`
677
1468
  }))
678
1469
  };
679
1470
  }
@@ -695,11 +1486,71 @@ __export(aexeo_emdash_bridge_bg_exports, {
695
1486
  __wbg_set_wasm: () => __wbg_set_wasm,
696
1487
  __wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
697
1488
  __wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
1489
+ evaluateContent: () => evaluateContent,
1490
+ evaluateContentCollection: () => evaluateContentCollection,
698
1491
  evaluateDocuments: () => evaluateDocuments,
699
1492
  generateFactsPrompt: () => generateFactsPrompt,
1493
+ projectContentInput: () => projectContentInput,
1494
+ projectLinkDestinations: () => projectLinkDestinations,
700
1495
  scoreIntelligence: () => scoreIntelligence,
701
1496
  validateFactsManifest: () => validateFactsManifest
702
1497
  });
1498
+ function evaluateContent(document_json, preset_name, sources_json, evidence_json, extensions_json, link_observations_json) {
1499
+ let deferred8_0;
1500
+ let deferred8_1;
1501
+ try {
1502
+ const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1503
+ const len0 = WASM_VECTOR_LEN;
1504
+ const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1505
+ const len1 = WASM_VECTOR_LEN;
1506
+ var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1507
+ var len2 = WASM_VECTOR_LEN;
1508
+ var ptr3 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1509
+ var len3 = WASM_VECTOR_LEN;
1510
+ var ptr4 = isLikeNone(extensions_json) ? 0 : passStringToWasm0(extensions_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1511
+ var len4 = WASM_VECTOR_LEN;
1512
+ var ptr5 = isLikeNone(link_observations_json) ? 0 : passStringToWasm0(link_observations_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1513
+ var len5 = WASM_VECTOR_LEN;
1514
+ const ret = wasm.evaluateContent(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4, ptr5, len5);
1515
+ var ptr7 = ret[0];
1516
+ var len7 = ret[1];
1517
+ if (ret[3]) {
1518
+ ptr7 = 0;
1519
+ len7 = 0;
1520
+ throw takeFromExternrefTable0(ret[2]);
1521
+ }
1522
+ deferred8_0 = ptr7;
1523
+ deferred8_1 = len7;
1524
+ return getStringFromWasm0(ptr7, len7);
1525
+ } finally {
1526
+ wasm.__wbindgen_free(deferred8_0, deferred8_1, 1);
1527
+ }
1528
+ }
1529
+ function evaluateContentCollection(documents_json, preset_name, sources_json) {
1530
+ let deferred5_0;
1531
+ let deferred5_1;
1532
+ try {
1533
+ const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1534
+ const len0 = WASM_VECTOR_LEN;
1535
+ const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1536
+ const len1 = WASM_VECTOR_LEN;
1537
+ var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1538
+ var len2 = WASM_VECTOR_LEN;
1539
+ const ret = wasm.evaluateContentCollection(ptr0, len0, ptr1, len1, ptr2, len2);
1540
+ var ptr4 = ret[0];
1541
+ var len4 = ret[1];
1542
+ if (ret[3]) {
1543
+ ptr4 = 0;
1544
+ len4 = 0;
1545
+ throw takeFromExternrefTable0(ret[2]);
1546
+ }
1547
+ deferred5_0 = ptr4;
1548
+ deferred5_1 = len4;
1549
+ return getStringFromWasm0(ptr4, len4);
1550
+ } finally {
1551
+ wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
1552
+ }
1553
+ }
703
1554
  function evaluateDocuments(documents_json, config_json) {
704
1555
  let deferred4_0;
705
1556
  let deferred4_1;
@@ -744,6 +1595,50 @@ function generateFactsPrompt(documents_json) {
744
1595
  wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
745
1596
  }
746
1597
  }
1598
+ function projectContentInput(document_json, evidence_json) {
1599
+ let deferred4_0;
1600
+ let deferred4_1;
1601
+ try {
1602
+ const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1603
+ const len0 = WASM_VECTOR_LEN;
1604
+ var ptr1 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1605
+ var len1 = WASM_VECTOR_LEN;
1606
+ const ret = wasm.projectContentInput(ptr0, len0, ptr1, len1);
1607
+ var ptr3 = ret[0];
1608
+ var len3 = ret[1];
1609
+ if (ret[3]) {
1610
+ ptr3 = 0;
1611
+ len3 = 0;
1612
+ throw takeFromExternrefTable0(ret[2]);
1613
+ }
1614
+ deferred4_0 = ptr3;
1615
+ deferred4_1 = len3;
1616
+ return getStringFromWasm0(ptr3, len3);
1617
+ } finally {
1618
+ wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
1619
+ }
1620
+ }
1621
+ function projectLinkDestinations(document_json) {
1622
+ let deferred3_0;
1623
+ let deferred3_1;
1624
+ try {
1625
+ const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
1626
+ const len0 = WASM_VECTOR_LEN;
1627
+ const ret = wasm.projectLinkDestinations(ptr0, len0);
1628
+ var ptr2 = ret[0];
1629
+ var len2 = ret[1];
1630
+ if (ret[3]) {
1631
+ ptr2 = 0;
1632
+ len2 = 0;
1633
+ throw takeFromExternrefTable0(ret[2]);
1634
+ }
1635
+ deferred3_0 = ptr2;
1636
+ deferred3_1 = len2;
1637
+ return getStringFromWasm0(ptr2, len2);
1638
+ } finally {
1639
+ wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
1640
+ }
1641
+ }
747
1642
  function scoreIntelligence(documents_json, manifest_json) {
748
1643
  let deferred4_0;
749
1644
  let deferred4_1;
@@ -969,6 +1864,18 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
969
1864
  await ensureInitialized();
970
1865
  return validateFactsManifest(manifestJson, documentsJson);
971
1866
  }
1867
+ async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson, extensionsJson, linkObservationsJson) {
1868
+ await ensureInitialized();
1869
+ return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson, extensionsJson, linkObservationsJson);
1870
+ }
1871
+ async function projectLinkDestinations2(documentJson) {
1872
+ await ensureInitialized();
1873
+ return projectLinkDestinations(documentJson);
1874
+ }
1875
+ async function projectContentInput2(documentJson, evidenceJson) {
1876
+ await ensureInitialized();
1877
+ return projectContentInput(documentJson, evidenceJson);
1878
+ }
972
1879
 
973
1880
  // src/data-route.ts
974
1881
  async function handleDataRoute(ctx, options) {
@@ -977,6 +1884,9 @@ async function handleDataRoute(ctx, options) {
977
1884
  summary = await evaluateAndPersistAll(ctx, {
978
1885
  collections: options.collections,
979
1886
  evaluator: options.evaluator,
1887
+ blockMappings: options.blockMappings,
1888
+ contentFieldsByCollection: options.contentFieldsByCollection,
1889
+ descriptionFieldsByCollection: options.descriptionFieldsByCollection,
980
1890
  ...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
981
1891
  });
982
1892
  await persistManifestStateFindings(ctx);
@@ -995,20 +1905,22 @@ async function buildFindingsPayload(ctx) {
995
1905
  );
996
1906
  const findings = [];
997
1907
  const sitewide = [];
1908
+ const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
998
1909
  const perRoute = /* @__PURE__ */ new Map();
999
1910
  for (const entry of entries) {
1000
1911
  if (entry.value === null) continue;
1001
1912
  const route = entry.key.replace(/^findings:/, "");
1002
1913
  if (route === "*") {
1003
- sitewide.push(...entry.value.findings);
1914
+ sitewide.push(...entry.value.findings.filter((finding) => isCmsObservableFinding(finding) && !(sitewideAssessment?.status === "error" && isSiteGraphFinding(finding))));
1004
1915
  continue;
1005
1916
  }
1006
1917
  let bucket = perRoute.get(route);
1007
1918
  if (bucket === void 0) {
1008
- bucket = { findings: [], errors: 0, warnings: 0 };
1919
+ bucket = { findings: [], errors: 0, warnings: 0, ...entry.value.body_coverage === void 0 ? {} : { body_coverage: entry.value.body_coverage }, ...entry.value.assessment === void 0 ? {} : { assessment: entry.value.assessment } };
1009
1920
  perRoute.set(route, bucket);
1010
1921
  }
1011
1922
  for (const finding of entry.value.findings) {
1923
+ if (!isCmsObservableFinding(finding, entry.value.body_coverage === "complete") || sitewideAssessment?.status === "error" && isSiteGraphFinding(finding)) continue;
1012
1924
  bucket.findings.push(finding);
1013
1925
  if (finding.severity === "error") bucket.errors += 1;
1014
1926
  else if (finding.severity === "warning") bucket.warnings += 1;
@@ -1029,7 +1941,9 @@ async function buildFindingsPayload(ctx) {
1029
1941
  publishedUrl: stored2?.meta.publicUrl ?? "",
1030
1942
  findingCount: bucket.findings.length,
1031
1943
  errorCount: bucket.errors,
1032
- warningCount: bucket.warnings
1944
+ warningCount: bucket.warnings,
1945
+ ...bucket.assessment === void 0 ? {} : { assessment: bucket.assessment },
1946
+ ...bucket.body_coverage === void 0 ? {} : { body_coverage: bucket.body_coverage }
1033
1947
  });
1034
1948
  }
1035
1949
  routes.sort((a, b) => b.errorCount - a.errorCount || a.route.localeCompare(b.route));
@@ -1051,7 +1965,8 @@ async function buildFindingsPayload(ctx) {
1051
1965
  documentsIndexed: documents.size
1052
1966
  },
1053
1967
  layerBreakdown,
1054
- sitewideFindings: sitewide
1968
+ sitewideFindings: sitewide,
1969
+ ...sitewideAssessment === void 0 ? {} : { sitewideAssessment }
1055
1970
  };
1056
1971
  }
1057
1972
  function computeLayerBreakdown(findings) {
@@ -1245,12 +2160,16 @@ async function handleSave(ctx, body) {
1245
2160
  return { saved: true };
1246
2161
  }
1247
2162
 
1248
- // src/presence-route.ts
1249
- import { PluginRouteError as PluginRouteError2 } from "emdash";
1250
-
1251
2163
  // src/presence-check.ts
1252
2164
  var FETCH_TIMEOUT_MS = 5e3;
1253
2165
  var USER_AGENT = "aexeo-emdash-plugin/0.8 (+https://github.com/schiste/Aexeo; entity-presence diagnostic)";
2166
+ var PRESENCE_HOSTS = [
2167
+ "en.wikipedia.org",
2168
+ "www.wikidata.org",
2169
+ "api.github.com",
2170
+ "rdap.org",
2171
+ "index.commoncrawl.org"
2172
+ ];
1254
2173
  var SOURCE_NAMES = [
1255
2174
  "wikipedia",
1256
2175
  "wikidata",
@@ -1307,7 +2226,7 @@ async function checkWikipedia(input, fetcher) {
1307
2226
  return notFound2("wikipedia");
1308
2227
  }
1309
2228
  const title = String(titles[0]);
1310
- if (!fuzzyMatch(title, input.name)) {
2229
+ if (!namesEntity(title, input)) {
1311
2230
  return notFound2("wikipedia");
1312
2231
  }
1313
2232
  const articleUrl = String(urls[0] ?? "");
@@ -1337,7 +2256,7 @@ async function checkWikidata(input, fetcher) {
1337
2256
  return notFound2("wikidata");
1338
2257
  }
1339
2258
  const label = top.label ?? input.name;
1340
- if (!fuzzyMatch(label, input.name)) {
2259
+ if (!namesEntity(label, input)) {
1341
2260
  return notFound2("wikidata");
1342
2261
  }
1343
2262
  return found("wikidata", {
@@ -1426,9 +2345,27 @@ async function checkCommonCrawl(input, fetcher) {
1426
2345
  "no website URL in manifest, can't query crawl index"
1427
2346
  );
1428
2347
  }
1429
- const index = "CC-MAIN-2026-15";
2348
+ let index;
2349
+ try {
2350
+ const res = await timedFetch(
2351
+ fetcher,
2352
+ "https://index.commoncrawl.org/collinfo.json"
2353
+ );
2354
+ if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
2355
+ const newest = newestCrawlId(await res.json());
2356
+ if (newest === null) {
2357
+ return unreachable(
2358
+ "common_crawl",
2359
+ "no crawl index listed in collinfo.json"
2360
+ );
2361
+ }
2362
+ index = newest;
2363
+ } catch (error) {
2364
+ return unreachable("common_crawl", errMsg(error));
2365
+ }
1430
2366
  const url = new URL(`https://index.commoncrawl.org/${index}-index`);
1431
2367
  url.searchParams.set("url", host);
2368
+ url.searchParams.set("matchType", "domain");
1432
2369
  url.searchParams.set("output", "json");
1433
2370
  url.searchParams.set("limit", "1");
1434
2371
  try {
@@ -1455,6 +2392,16 @@ async function checkCommonCrawl(input, fetcher) {
1455
2392
  return unreachable("common_crawl", errMsg(error));
1456
2393
  }
1457
2394
  }
2395
+ function newestCrawlId(collinfo) {
2396
+ if (!Array.isArray(collinfo)) return null;
2397
+ let newest = null;
2398
+ for (const crawl of collinfo) {
2399
+ const id = crawl?.id;
2400
+ if (typeof id !== "string" || !/^CC-MAIN-[A-Za-z0-9-]+$/.test(id)) continue;
2401
+ if (newest === null || id > newest) newest = id;
2402
+ }
2403
+ return newest;
2404
+ }
1458
2405
  async function timedFetch(fetcher, url, init) {
1459
2406
  const controller = new AbortController();
1460
2407
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
@@ -1500,10 +2447,45 @@ function errMsg(error) {
1500
2447
  }
1501
2448
  return String(error);
1502
2449
  }
1503
- function fuzzyMatch(a, b) {
1504
- const aL = a.toLowerCase().trim();
1505
- const bL = b.toLowerCase().trim();
1506
- return aL.includes(bL) || bL.includes(aL);
2450
+ function namesEntity(candidate, input) {
2451
+ const normalized = normalizeEntityName(candidate);
2452
+ return [input.name, ...input.aliases ?? []].some(
2453
+ (name) => normalizeEntityName(name) === normalized
2454
+ );
2455
+ }
2456
+ var LEGAL_FORMS = /* @__PURE__ */ new Set([
2457
+ "ab",
2458
+ "ag",
2459
+ "bv",
2460
+ "co",
2461
+ "company",
2462
+ "corp",
2463
+ "corporation",
2464
+ "gmbh",
2465
+ "inc",
2466
+ "incorporated",
2467
+ "kk",
2468
+ "limited",
2469
+ "llc",
2470
+ "llp",
2471
+ "lp",
2472
+ "ltd",
2473
+ "nv",
2474
+ "oy",
2475
+ "plc",
2476
+ "pty",
2477
+ "sa",
2478
+ "sarl",
2479
+ "sas",
2480
+ "srl"
2481
+ ]);
2482
+ function normalizeEntityName(name) {
2483
+ const words = name.replace(/\([^)]*\)/g, " ").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0);
2484
+ let end = words.length;
2485
+ while (end > 0 && LEGAL_FORMS.has(words[end - 1])) end -= 1;
2486
+ const start = words[0] === "the" ? 1 : 0;
2487
+ const core = words.slice(start, end);
2488
+ return (core.length > 0 ? core : words).join(" ");
1507
2489
  }
1508
2490
  function sanitizeGitHubHandle(name) {
1509
2491
  const handle = name.replace(/[\s_]+/g, "-");
@@ -1542,6 +2524,7 @@ function formatCdxTimestamp(ts) {
1542
2524
  }
1543
2525
 
1544
2526
  // src/presence-route.ts
2527
+ import { PluginRouteError as PluginRouteError2 } from "emdash";
1545
2528
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
1546
2529
  var PRESENCE_KEY = "presence:current";
1547
2530
  async function handlePresenceRoute(ctx) {
@@ -1664,8 +2647,150 @@ function globToMatcher(pattern) {
1664
2647
  return (route) => re.test(route);
1665
2648
  }
1666
2649
 
1667
- // src/version.ts
1668
- var PACKAGE_VERSION = "0.8.19";
2650
+ // src/content.ts
2651
+ async function projectContentInput3(document, evidenceSources) {
2652
+ return JSON.parse(await projectContentInput2(JSON.stringify(document), evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources)));
2653
+ }
2654
+ async function projectLinkDestinations3(document) {
2655
+ return JSON.parse(await projectLinkDestinations2(JSON.stringify(document)));
2656
+ }
2657
+ async function evaluateContent3(document, preset, sources = {}, evidenceSources, onTelemetry, externalResults, linkObservations) {
2658
+ const started = onTelemetry ? performance.now() : 0;
2659
+ const documentJson = JSON.stringify(document);
2660
+ const sourcesJson = JSON.stringify(sources);
2661
+ const result = linkObservations !== void 0 ? await evaluateContent2(documentJson, preset, sourcesJson, evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources), externalResults === void 0 ? void 0 : JSON.stringify(externalResults), JSON.stringify(linkObservations)) : externalResults !== void 0 ? await evaluateContent2(documentJson, preset, sourcesJson, evidenceSources === void 0 ? void 0 : JSON.stringify(evidenceSources), JSON.stringify(externalResults)) : evidenceSources === void 0 ? await evaluateContent2(documentJson, preset, sourcesJson) : await evaluateContent2(documentJson, preset, sourcesJson, JSON.stringify(evidenceSources));
2662
+ const report = JSON.parse(result);
2663
+ if (onTelemetry) {
2664
+ const event = { evaluation_ms: performance.now() - started, source_locations_mapped: report.source_locations?.length ?? 0 };
2665
+ if (report.source_mapping) {
2666
+ event.source_locations_eligible = report.source_mapping.eligible;
2667
+ event.source_locations_unmapped = report.source_mapping.unmapped;
2668
+ }
2669
+ for (const gate of report.gates) {
2670
+ if (gate.required_capability === "link_reachability" && gate.measurements) {
2671
+ const { checked_destinations, not_found_destinations, unchecked_destinations } = gate.measurements;
2672
+ if (checked_destinations !== void 0 && not_found_destinations !== void 0 && unchecked_destinations !== void 0) {
2673
+ event.link_reachability ??= { checked: 0, not_found: 0, unchecked: 0 };
2674
+ event.link_reachability.checked += checked_destinations;
2675
+ event.link_reachability.not_found += not_found_destinations;
2676
+ event.link_reachability.unchecked += unchecked_destinations;
2677
+ }
2678
+ }
2679
+ const checked = gate.measurements?.checked_destinations;
2680
+ const invalid = gate.measurements?.invalid_destinations;
2681
+ if (checked !== void 0 && invalid !== void 0) {
2682
+ event.link_destinations ??= { checked: 0, invalid: 0 };
2683
+ event.link_destinations.checked += checked;
2684
+ event.link_destinations.invalid += invalid;
2685
+ }
2686
+ }
2687
+ onTelemetry(event);
2688
+ }
2689
+ return report;
2690
+ }
2691
+
2692
+ // src/content-evaluator.ts
2693
+ function withContentPreset(base, preset, evaluate, provider, project, onProviderTelemetry, linkProvider, projectLinks, onLinkProviderTelemetry) {
2694
+ if (!preset) return base;
2695
+ return async (documents, context) => {
2696
+ const result = await base(documents, context);
2697
+ if (!result.ok) return result;
2698
+ const findings = [...result.findings];
2699
+ const assessments = { ...result.assessments };
2700
+ try {
2701
+ for (const [index, document] of documents.entries()) {
2702
+ const collection = context?.collections[index];
2703
+ if (preset.collections !== void 0) {
2704
+ if (collection === void 0) throw new Error("collection is required for a scoped content preset");
2705
+ if (!preset.collections.includes(collection)) continue;
2706
+ }
2707
+ const selected = collection !== void 0 && preset.byCollection !== void 0 && Object.hasOwn(preset.byCollection, collection) ? preset.byCollection[collection] : void 0;
2708
+ const name = selected ?? preset.name;
2709
+ if (!name) throw new Error(`empty preset name for collection ${collection}`);
2710
+ let report = preset.evidenceSources === void 0 ? await evaluate(document, name, preset.sources) : await evaluate(document, name, preset.sources, preset.evidenceSources);
2711
+ let externalResults;
2712
+ let externalFailure = false;
2713
+ if (provider !== void 0 && report.gates.some((gate) => gate.required_capability === "extension")) {
2714
+ const started = onProviderTelemetry ? performance.now() : 0;
2715
+ let status = "error";
2716
+ try {
2717
+ if (!project) throw new Error("content gate projection is unavailable");
2718
+ externalResults = await provider({ input: await project(document, preset.evidenceSources), preset: name, collection });
2719
+ if (!Array.isArray(externalResults)) throw new Error("external gate provider must return an array");
2720
+ report = await evaluate(document, name, preset.sources, preset.evidenceSources, void 0, externalResults);
2721
+ status = "success";
2722
+ } catch {
2723
+ externalResults = void 0;
2724
+ externalFailure = true;
2725
+ } finally {
2726
+ if (onProviderTelemetry) {
2727
+ try {
2728
+ onProviderTelemetry({ duration_ms: performance.now() - started, status });
2729
+ } catch {
2730
+ }
2731
+ }
2732
+ }
2733
+ }
2734
+ if (linkProvider && report.gates.some((gate) => gate.required_capability === "link_reachability")) {
2735
+ const started = onLinkProviderTelemetry ? performance.now() : 0;
2736
+ let status = "error";
2737
+ try {
2738
+ const projection = projectLinks ? await projectLinks(document) : null;
2739
+ if (projection === null) {
2740
+ status = "unavailable";
2741
+ } else {
2742
+ const observations = await linkProvider({ destinations: [...projection.destinations], preset: name, collection });
2743
+ const valid = /* @__PURE__ */ new Set(["reachable", "not_found", "not_checked", "not_applicable"]);
2744
+ if (!Array.isArray(observations) || observations.length !== projection.destinations.length || observations.some((value) => !valid.has(value))) throw new Error("invalid link observations");
2745
+ report = await evaluate(
2746
+ document,
2747
+ name,
2748
+ preset.sources,
2749
+ preset.evidenceSources,
2750
+ void 0,
2751
+ externalResults,
2752
+ { ...projection, observations }
2753
+ );
2754
+ status = "success";
2755
+ }
2756
+ } catch {
2757
+ } finally {
2758
+ if (onLinkProviderTelemetry) {
2759
+ try {
2760
+ onLinkProviderTelemetry({ duration_ms: performance.now() - started, status });
2761
+ } catch {
2762
+ }
2763
+ }
2764
+ }
2765
+ }
2766
+ if (externalFailure) {
2767
+ report = { ...report, gates: report.gates.map((gate) => gate.required_capability === "extension" && gate.status === "not_checked" ? { ...gate, reason: "external gate provider failed" } : gate) };
2768
+ }
2769
+ assessments[document.route] = {
2770
+ status: "evaluated",
2771
+ preset: name,
2772
+ ...report.source_mapping === void 0 ? {} : { source_mapping: report.source_mapping },
2773
+ gates: report.gates.map(({ gate_id, status, blocking, scope, required_capability, reason, measurements }) => ({ gate_id, status, blocking, scope, required_capability, reason, ...measurements === void 0 ? {} : { measurements } }))
2774
+ };
2775
+ const locations = new Map((report.source_locations ?? []).map((location) => [
2776
+ `${location.gate_id}:${location.finding_index}`,
2777
+ location
2778
+ ]));
2779
+ for (const gate of report.gates) findings.push(...gate.findings.map((finding, index2) => {
2780
+ const location = locations.get(`${gate.gate_id}:${index2}`);
2781
+ return {
2782
+ ...finding,
2783
+ aexeo_origin: "content_preset",
2784
+ ...location === void 0 ? {} : { aexeo_source_location: { block_index: location.block_index, span_index: location.span_index, utf16_offset: location.utf16_offset } }
2785
+ };
2786
+ }));
2787
+ }
2788
+ return { ok: true, findings, assessments };
2789
+ } catch (error) {
2790
+ return { ok: false, reason: "content_preset_error", detail: error instanceof Error ? error.message : String(error) };
2791
+ }
2792
+ };
2793
+ }
1669
2794
 
1670
2795
  // src/configured.ts
1671
2796
  import { definePlugin } from "emdash";
@@ -1724,17 +2849,22 @@ var inProcessEvaluator = async (documents) => {
1724
2849
  return { ok: false, reason: "wasm_error", detail };
1725
2850
  }
1726
2851
  };
1727
- function createPlugin(options = {}) {
2852
+ function createPlugin(options = {}, runtime = {}) {
1728
2853
  const collections = resolveCollections(
1729
2854
  options.collections,
1730
2855
  options.includeCollections,
1731
2856
  options.excludeCollections
1732
2857
  );
1733
2858
  const suppressionFilter = compileSuppressions(options.suppressions);
2859
+ const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3, runtime.externalGates, projectContentInput3, runtime.onExternalGateTelemetry, runtime.linkObservations, projectLinkDestinations3, runtime.onLinkProviderTelemetry);
1734
2860
  return definePlugin({
1735
2861
  id: "aexeo-emdash",
1736
2862
  version: PACKAGE_VERSION,
1737
- capabilities: ["content:read"],
2863
+ // The entity-presence route fetches its public sources directly (see
2864
+ // presence-check.ts). rdap.org then redirects to the registry for the
2865
+ // domain's TLD; those registries cannot be listed here.
2866
+ capabilities: ["content:read", "network:request"],
2867
+ allowedHosts: [...PRESENCE_HOSTS],
1738
2868
  hooks: {
1739
2869
  // afterSave processes one saved document at a time — collections
1740
2870
  // list isn't needed here. The hook always runs regardless of
@@ -1742,16 +2872,27 @@ function createPlugin(options = {}) {
1742
2872
  "content:afterSave": (event, ctx) => handleAfterSaveConfigured(
1743
2873
  event,
1744
2874
  ctx,
1745
- inProcessEvaluator,
1746
- suppressionFilter
1747
- )
2875
+ evaluator,
2876
+ suppressionFilter,
2877
+ options.blockMappings,
2878
+ options.contentFieldsByCollection?.[event.content.type],
2879
+ options.descriptionFieldsByCollection?.[event.content.type]
2880
+ ),
2881
+ "content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
1748
2882
  },
2883
+ // emdash requires plugins:manage (admin) on a route that declares no
2884
+ // permission. Reading findings only needs plugins:read (editor); anything
2885
+ // that writes stays admin, stated explicitly so the split is visible.
1749
2886
  routes: {
1750
2887
  // The Refresh sweep (handleAdminRoute → handleRefresh) is the
1751
2888
  // one consumer of `collections`. Bind the resolved list here so
1752
2889
  // every request handles the same set without re-reading options.
2890
+ //
2891
+ // This route serves the pages and the dashboard widget, and also the
2892
+ // refresh_findings action, which handleAdminRoute gates itself.
1753
2893
  admin: {
1754
- handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter)
2894
+ permission: "plugins:read",
2895
+ handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection, options.descriptionFieldsByCollection)
1755
2896
  },
1756
2897
  // JSON data endpoint for the React adminEntry. Two routes:
1757
2898
  // POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
@@ -1761,19 +2902,27 @@ function createPlugin(options = {}) {
1761
2902
  // emdash edit page and the public site, which Block Kit can't
1762
2903
  // express in any element type as of emdash 0.8.0.
1763
2904
  data: {
2905
+ permission: "plugins:read",
1764
2906
  handler: async (ctx) => (await handleDataRoute(ctx, {
1765
2907
  collections,
1766
- evaluator: inProcessEvaluator,
2908
+ evaluator,
1767
2909
  refresh: false,
1768
- suppressionFilter
2910
+ suppressionFilter,
2911
+ blockMappings: options.blockMappings,
2912
+ contentFieldsByCollection: options.contentFieldsByCollection,
2913
+ descriptionFieldsByCollection: options.descriptionFieldsByCollection
1769
2914
  })).payload
1770
2915
  },
1771
2916
  refresh: {
2917
+ permission: "plugins:manage",
1772
2918
  handler: (ctx) => handleDataRoute(ctx, {
1773
2919
  collections,
1774
- evaluator: inProcessEvaluator,
2920
+ evaluator,
1775
2921
  refresh: true,
1776
- suppressionFilter
2922
+ suppressionFilter,
2923
+ blockMappings: options.blockMappings,
2924
+ contentFieldsByCollection: options.contentFieldsByCollection,
2925
+ descriptionFieldsByCollection: options.descriptionFieldsByCollection
1777
2926
  })
1778
2927
  },
1779
2928
  // /facts route serves the truth-manifest authoring UI. It
@@ -1781,6 +2930,7 @@ function createPlugin(options = {}) {
1781
2930
  // onto one POST endpoint via a "kind" body field — see
1782
2931
  // facts-route.ts for the full contract.
1783
2932
  facts: {
2933
+ permission: "plugins:manage",
1784
2934
  handler: (ctx) => handleFactsRoute(ctx)
1785
2935
  },
1786
2936
  // /presence route serves the layer-4 entity-presence
@@ -1790,6 +2940,7 @@ function createPlugin(options = {}) {
1790
2940
  // results in KV for 24h. Multiplexes data / refresh kinds —
1791
2941
  // see presence-route.ts.
1792
2942
  presence: {
2943
+ permission: "plugins:manage",
1793
2944
  handler: (ctx) => handlePresenceRoute(ctx)
1794
2945
  }
1795
2946
  },
@@ -1821,6 +2972,7 @@ function createPlugin(options = {}) {
1821
2972
  });
1822
2973
  }
1823
2974
  var configured_default = createPlugin;
2975
+ var ADMIN_ROLE = 50;
1824
2976
  var adminViewTuning = {
1825
2977
  emptyFindingsText: "Once a Refresh runs, findings will list here.",
1826
2978
  emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
@@ -1828,7 +2980,7 @@ var adminViewTuning = {
1828
2980
  score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
1829
2981
  truthLabel: formatTruthLabel
1830
2982
  };
1831
- async function handleAdminRoute(ctx, collections, suppressionFilter) {
2983
+ async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection, descriptionFieldsByCollection) {
1832
2984
  const body = normalizeInteraction(ctx.input);
1833
2985
  if (body.type === "page_load") {
1834
2986
  return handlePageLoad(ctx, body.page);
@@ -1838,12 +2990,29 @@ async function handleAdminRoute(ctx, collections, suppressionFilter) {
1838
2990
  return renderDocumentPanel(ctx.kv, body.value);
1839
2991
  }
1840
2992
  if (body.action_id === "refresh_findings") {
2993
+ if ((ctx.user?.role ?? 0) < ADMIN_ROLE) {
2994
+ const page = await renderFindingsPage(ctx.kv, adminViewTuning);
2995
+ return {
2996
+ ...page,
2997
+ blocks: [
2998
+ {
2999
+ type: "banner",
3000
+ title: "Only administrators can run Refresh.",
3001
+ variant: "error"
3002
+ },
3003
+ ...page.blocks
3004
+ ]
3005
+ };
3006
+ }
1841
3007
  return handleRefresh({
1842
3008
  ctx,
1843
3009
  collections,
1844
- evaluator: inProcessEvaluator,
3010
+ evaluator,
1845
3011
  suppressionFilter,
1846
- tuning: adminViewTuning
3012
+ tuning: adminViewTuning,
3013
+ blockMappings,
3014
+ contentFieldsByCollection,
3015
+ descriptionFieldsByCollection
1847
3016
  });
1848
3017
  }
1849
3018
  if (body.action_id.startsWith("filter:")) {
@@ -1868,7 +3037,7 @@ async function handlePageLoad(ctx, page) {
1868
3037
  return renderFindingsPage(ctx.kv, adminViewTuning);
1869
3038
  }
1870
3039
  if (normalized === "widget:aexeo-score") {
1871
- return renderScoreWidget(ctx.kv, adminViewTuning);
3040
+ return renderScoreWidget(ctx.kv, adminViewTuning, ctx.log);
1872
3041
  }
1873
3042
  if (normalized === "document") {
1874
3043
  return renderDocumentPanel(ctx.kv);