@aeptus/aexeo-emdash 0.9.0 → 0.9.1

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