@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
@@ -1,5 +1,27 @@
1
+ // src/cms-coverage.ts
2
+ 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}$/;
3
+ var OBSERVABLE_BODY_RULES = /* @__PURE__ */ new Set(["CNT006", "LNK003"]);
4
+ function isCmsObservableFinding(finding, bodyComplete = true) {
5
+ return finding.aexeo_origin === "content_preset" || isLegacyCmsObservableFinding(finding, bodyComplete);
6
+ }
7
+ function isLegacyCmsObservableFinding(finding, bodyComplete = true) {
8
+ if (!NATIVE_RULE_ID.test(finding.rule_id)) return true;
9
+ return bodyComplete && finding.scope === "page" && OBSERVABLE_BODY_RULES.has(finding.rule_id);
10
+ }
11
+ function hasCompleteBody(document) {
12
+ return document.body !== void 0 && (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every((capability) => document.body_capabilities?.includes(capability)));
13
+ }
14
+ function cmsBodyCoverage(document) {
15
+ if (document.body === void 0 || document.body_capabilities?.length === 0) return "unavailable";
16
+ return hasCompleteBody(document) ? "complete" : "partial";
17
+ }
18
+ function cmsBodyCoverageNotice(coverage) {
19
+ if (coverage === "complete") return void 0;
20
+ 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.`;
21
+ }
22
+
1
23
  // src/adapter.ts
2
- async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
24
+ async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields, descriptionFields) {
3
25
  const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
4
26
  const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
5
27
  const adapted = adaptContentItem(content, {
@@ -7,7 +29,8 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
7
29
  publicUrl,
8
30
  ...defaultLocale === void 0 ? {} : { defaultLocale },
9
31
  ...blockMappings === void 0 ? {} : { blockMappings },
10
- ...contentFields === void 0 ? {} : { contentFields }
32
+ ...contentFields === void 0 ? {} : { contentFields },
33
+ ...descriptionFields === void 0 ? {} : { descriptionFields }
11
34
  });
12
35
  const translationSet = await contentApi.getTranslations(
13
36
  content.type,
@@ -32,14 +55,32 @@ async function adaptContentItemWithContext(content, contentApi, defaultLocale, b
32
55
  return adapted;
33
56
  }
34
57
  function adaptContentItem(content, options = {}) {
58
+ return adaptContentItemInternal(content, options, true);
59
+ }
60
+ function adaptContentItemInternal(content, options, includeSourcePaths) {
35
61
  const route = options.route ?? deriveRoute(content);
36
62
  const title = stringOrEmpty(
37
63
  content.seo?.title ?? content.data["title"] ?? content.slug ?? content.id
38
64
  );
39
- const document = { route, title };
40
- const description = content.seo?.description ?? content.data["description"];
41
- if (typeof description === "string" && description.length > 0) {
42
- document.description = description;
65
+ const document = { route, title, description_source_observed: false };
66
+ const fields = options.descriptionFields;
67
+ if (fields === void 0) {
68
+ const description = content.seo?.description ?? content.data["description"];
69
+ if (typeof description === "string" && description.length > 0) document.description = description;
70
+ } else if (fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0)) {
71
+ let observed = true;
72
+ for (const field of fields) {
73
+ if (!Object.hasOwn(content.data, field) || content.data[field] === void 0 || content.data[field] !== null && typeof content.data[field] !== "string") {
74
+ observed = false;
75
+ break;
76
+ }
77
+ const value = content.data[field];
78
+ if (typeof value === "string" && value.length > 0) {
79
+ document.description = value;
80
+ break;
81
+ }
82
+ }
83
+ if (observed) document.description_source_observed = true;
43
84
  }
44
85
  const canonical = content.seo?.canonical;
45
86
  if (typeof canonical === "string" && canonical.length > 0) {
@@ -54,6 +95,8 @@ function adaptContentItem(content, options = {}) {
54
95
  document.body = body.blocks;
55
96
  if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
56
97
  }
98
+ const bodySourcePaths = body === null || !includeSourcePaths ? void 0 : portableTextSourcePaths(content.data, body.blocks);
99
+ const bodySourceSpanPaths = body === null || !includeSourcePaths ? void 0 : portableTextSourceSpanPaths(content.data, body.blocks, bodySourcePaths);
57
100
  return {
58
101
  document,
59
102
  meta: {
@@ -63,6 +106,8 @@ function adaptContentItem(content, options = {}) {
63
106
  status: content.status,
64
107
  title,
65
108
  publicUrl: options.publicUrl ?? null,
109
+ ...bodySourcePaths === void 0 ? {} : { bodySourcePaths },
110
+ ...bodySourceSpanPaths === void 0 ? {} : { bodySourceSpanPaths },
66
111
  ...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
67
112
  }
68
113
  };
@@ -82,6 +127,100 @@ function routeFromPublicUrl(publicUrl) {
82
127
  function stringOrEmpty(value) {
83
128
  return typeof value === "string" ? value : "";
84
129
  }
130
+ function portableTextSourcePaths(data, blocks) {
131
+ if (blocks.length === 0) return void 0;
132
+ const targets = new WeakSet(blocks);
133
+ const paths = /* @__PURE__ */ new WeakMap();
134
+ const seen = /* @__PURE__ */ new WeakSet();
135
+ const stack = [{ value: data, path: ["data"] }];
136
+ let count = 0;
137
+ let inspected = 0;
138
+ while (stack.length > 0) {
139
+ const current = stack.pop();
140
+ if (typeof current.value !== "object" || current.value === null) continue;
141
+ if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
142
+ seen.add(current.value);
143
+ if (targets.has(current.value)) {
144
+ paths.set(current.value, current.path);
145
+ continue;
146
+ }
147
+ if (Array.isArray(current.value)) {
148
+ for (let i = 0; i < current.value.length; i++) {
149
+ if (++inspected > 5e4) return void 0;
150
+ const value = current.value[i];
151
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i] });
152
+ }
153
+ } else {
154
+ for (const key in current.value) {
155
+ if (!Object.hasOwn(current.value, key)) continue;
156
+ if (++inspected > 5e4) return void 0;
157
+ const value = current.value[key];
158
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key] });
159
+ }
160
+ }
161
+ }
162
+ const result = blocks.map((block) => paths.get(block) ?? null);
163
+ return result.some((path) => path !== null) ? result : void 0;
164
+ }
165
+ var BODY_CAPABILITIES = ["text", "headings", "paragraphs", "links"];
166
+ function intersectCapabilities(current, other) {
167
+ return other === void 0 ? [...current] : current.filter((capability) => other.includes(capability));
168
+ }
169
+ function bodyWithCapabilities(blocks, capabilities) {
170
+ return capabilities.length === BODY_CAPABILITIES.length ? { blocks } : { blocks, capabilities };
171
+ }
172
+ function portableTextSourceSpanPaths(data, blocks, blockPaths) {
173
+ const targets = /* @__PURE__ */ new WeakSet();
174
+ let targetCount = 0;
175
+ for (let i = 0; i < blocks.length; i++) {
176
+ if (blockPaths?.[i]) continue;
177
+ for (const child of blocks[i].children) {
178
+ if (++targetCount > 5e4) return void 0;
179
+ targets.add(child);
180
+ }
181
+ }
182
+ if (targetCount === 0) return void 0;
183
+ const paths = /* @__PURE__ */ new WeakMap();
184
+ const seen = /* @__PURE__ */ new WeakSet();
185
+ const stack = [
186
+ { value: data, path: ["data"], portableChild: false }
187
+ ];
188
+ let count = 0;
189
+ let inspected = 0;
190
+ while (stack.length > 0) {
191
+ const current = stack.pop();
192
+ if (typeof current.value !== "object" || current.value === null) continue;
193
+ if (++count > 5e4 || current.path.length > 64 || seen.has(current.value)) return void 0;
194
+ seen.add(current.value);
195
+ if (current.portableChild && targets.has(current.value) && isPortableTextSpan(current.value)) {
196
+ paths.set(current.value, [...current.path, "text"]);
197
+ }
198
+ if (Array.isArray(current.value)) {
199
+ for (let i = 0; i < current.value.length; i++) {
200
+ if (++inspected > 5e4) return void 0;
201
+ const value = current.value[i];
202
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, i], portableChild: current.portableChild });
203
+ }
204
+ } else {
205
+ for (const key in current.value) {
206
+ if (!Object.hasOwn(current.value, key)) continue;
207
+ if (++inspected > 5e4) return void 0;
208
+ const value = current.value[key];
209
+ if (typeof value === "object" && value !== null) stack.push({ value, path: [...current.path, key], portableChild: key === "children" && isPortableTextBlockCandidate(current.value) });
210
+ }
211
+ }
212
+ }
213
+ let mapped = false;
214
+ const result = blocks.map((block, index) => {
215
+ if (blockPaths?.[index]) return null;
216
+ return block.children.map((child) => {
217
+ const path = paths.get(child) ?? null;
218
+ if (path !== null) mapped = true;
219
+ return path;
220
+ });
221
+ });
222
+ return mapped ? result : void 0;
223
+ }
85
224
  function extractSelectedContent(data, fields, mappings) {
86
225
  if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
87
226
  const all = ["text", "headings", "paragraphs", "links"];
@@ -90,10 +229,17 @@ function extractSelectedContent(data, fields, mappings) {
90
229
  let observed = false;
91
230
  for (const field of fields) {
92
231
  const value = data[field];
93
- if (value === void 0 || value === null) continue;
232
+ if (value === void 0) {
233
+ capabilities = [];
234
+ continue;
235
+ }
94
236
  observed = true;
237
+ if (value === null) continue;
95
238
  const part = extractFirstPortableText({ content: value }, mappings);
96
- if (part === null) return { blocks, capabilities: [] };
239
+ if (part === null) {
240
+ capabilities = [];
241
+ continue;
242
+ }
97
243
  blocks.push(...part.blocks);
98
244
  capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
99
245
  }
@@ -104,9 +250,14 @@ function extractPortableText(data, mappings) {
104
250
  const result = extractFirstPortableText(data, mappings);
105
251
  if (result === null) return null;
106
252
  const candidates = Object.entries(data).filter(
107
- ([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
253
+ ([field, value]) => isPortableTextArray(value) || isMixedPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
108
254
  );
109
- return candidates.length > 1 ? { blocks: result.blocks, capabilities: [] } : result;
255
+ const unrecognizedBody = ["body", "content", "blocks"].some((field) => {
256
+ const value = data[field];
257
+ if (value == null || Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) return false;
258
+ return !candidates.some(([candidate]) => candidate === field);
259
+ });
260
+ return candidates.length > 1 || unrecognizedBody ? { blocks: result.blocks, capabilities: [] } : result;
110
261
  }
111
262
  function extractFirstPortableText(data, mappings) {
112
263
  let foundEmptyBody = false;
@@ -114,7 +265,8 @@ function extractFirstPortableText(data, mappings) {
114
265
  const value = data[slug];
115
266
  if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
116
267
  if (isPortableTextArray(value)) return { blocks: value };
117
- if (isEmdashBlocksArray(value, slug)) return mapCustomBlocks(value, mappings);
268
+ if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
269
+ if (isEmdashBlocksArray(value, slug)) return mapMixedPortableText(value, mappings);
118
270
  if (typeof value === "string" && value.trim().length > 0) {
119
271
  return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
120
272
  }
@@ -122,32 +274,184 @@ function extractFirstPortableText(data, mappings) {
122
274
  const blocks = data["blocks"];
123
275
  if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
124
276
  if (isPortableTextArray(blocks)) return { blocks };
125
- if (isEmdashBlocksArray(blocks, "blocks")) return mapCustomBlocks(blocks, mappings);
277
+ if (isMixedPortableTextArray(blocks)) return mapMixedPortableText(blocks, mappings);
278
+ if (isEmdashBlocksArray(blocks, "blocks")) return mapMixedPortableText(blocks, mappings);
126
279
  for (const [field, value] of Object.entries(data)) {
127
280
  if (field === "body" || field === "content" || field === "blocks") continue;
128
281
  if (isPortableTextArray(value)) return { blocks: value };
129
- if (isEmdashBlocksArray(value, field)) return mapCustomBlocks(value, mappings);
282
+ if (isMixedPortableTextArray(value)) return mapMixedPortableText(value, mappings);
283
+ if (isEmdashBlocksArray(value, field)) return mapMixedPortableText(value, mappings);
130
284
  }
131
285
  return foundEmptyBody ? { blocks: [] } : null;
132
286
  }
133
287
  function isPortableTextArray(value) {
134
288
  return Array.isArray(value) && value.length > 0 && value.every(
135
- (item) => isRecord(item) && item["_type"] === "block" && Array.isArray(item["children"]) && item["children"].every(
136
- (child) => isRecord(child) && typeof child["text"] === "string"
137
- )
289
+ (item) => isPortableTextBlockCandidate(item) && isWireSafeBlock(item)
138
290
  );
139
291
  }
292
+ function isPortableTextSpan(value) {
293
+ return isRecord(value) && value["_type"] === "span" && typeof value["text"] === "string";
294
+ }
295
+ function isPortableTextBlockCandidate(value) {
296
+ return isRecord(value) && value["_type"] === "block" && Array.isArray(value["children"]);
297
+ }
298
+ function isWireSafeSpan(value) {
299
+ 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"));
300
+ }
301
+ function isWireSafeMarkDef(value) {
302
+ return isRecord(value) && typeof value["_key"] === "string" && typeof value["_type"] === "string" && (value["_type"] !== "link" || typeof value["href"] === "string");
303
+ }
304
+ function isWireSafeBlock(block) {
305
+ const level = block["level"];
306
+ 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);
307
+ }
308
+ function normalizePortableBlock(block) {
309
+ const normalized = { ...block };
310
+ let incomplete = false;
311
+ if (normalized["_key"] != null && typeof normalized["_key"] !== "string") {
312
+ delete normalized["_key"];
313
+ incomplete = true;
314
+ }
315
+ if (normalized["style"] !== void 0 && (typeof normalized["style"] !== "string" || !["normal", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote"].includes(normalized["style"]))) {
316
+ delete normalized["style"];
317
+ incomplete = true;
318
+ }
319
+ if (normalized["listItem"] != null && normalized["listItem"] !== "bullet" && normalized["listItem"] !== "number") {
320
+ delete normalized["listItem"];
321
+ incomplete = true;
322
+ }
323
+ const level = normalized["level"];
324
+ if (level != null && (typeof level !== "number" || !Number.isInteger(level) || level < 0 || level > 4294967295)) {
325
+ delete normalized["level"];
326
+ incomplete = true;
327
+ }
328
+ if (normalized["markDefs"] !== void 0) {
329
+ if (!Array.isArray(normalized["markDefs"])) {
330
+ delete normalized["markDefs"];
331
+ incomplete = true;
332
+ } else {
333
+ const defs = normalized["markDefs"].filter(isWireSafeMarkDef);
334
+ if (defs.length !== normalized["markDefs"].length) {
335
+ normalized["markDefs"] = defs;
336
+ incomplete = true;
337
+ }
338
+ }
339
+ }
340
+ normalized["children"] = block["children"].map((child) => {
341
+ if (!isPortableTextSpan(child) || !isRecord(child) || isWireSafeSpan(child)) return child;
342
+ const span = { ...child };
343
+ if (span["_key"] != null && typeof span["_key"] !== "string") delete span["_key"];
344
+ if (span["marks"] !== void 0 && (!Array.isArray(span["marks"]) || !span["marks"].every((mark) => typeof mark === "string"))) delete span["marks"];
345
+ incomplete = true;
346
+ return span;
347
+ });
348
+ return { block: normalized, incomplete };
349
+ }
350
+ function isMixedPortableTextArray(value) {
351
+ return Array.isArray(value) && value.some(isPortableTextBlockCandidate);
352
+ }
353
+ function mapMixedPortableText(blocks, mappings, depth = 0) {
354
+ if (depth > 8) return { blocks: [], capabilities: [] };
355
+ const result = [];
356
+ let capabilities = [...BODY_CAPABILITIES];
357
+ for (const block of blocks) {
358
+ if (isPortableTextArray([block])) {
359
+ result.push(block);
360
+ continue;
361
+ }
362
+ if (isPortableTextBlockCandidate(block)) {
363
+ const normalized = normalizePortableBlock(block);
364
+ const projected = projectInlineBlock(normalized.block, mappings);
365
+ result.push(projected.block);
366
+ capabilities = normalized.incomplete ? [] : intersectCapabilities(capabilities, projected.capabilities);
367
+ continue;
368
+ }
369
+ if (!isRecord(block) || typeof block["_type"] !== "string" || block["_type"] === "block") {
370
+ capabilities = [];
371
+ continue;
372
+ }
373
+ const mapped = mapCustomBlocks([block], mappings, depth + 1);
374
+ result.push(...mapped.blocks);
375
+ capabilities = intersectCapabilities(capabilities, mapped.capabilities);
376
+ }
377
+ return bodyWithCapabilities(result, capabilities);
378
+ }
379
+ function projectInlineBlock(block, mappings) {
380
+ const children = [];
381
+ const defs = Array.isArray(block["markDefs"]) ? [...block["markDefs"]] : [];
382
+ const keys = new Set(defs.flatMap((def) => isRecord(def) && typeof def["_key"] === "string" ? [def["_key"]] : []));
383
+ let sequence = 0;
384
+ let capabilities = block["markDefs"] === void 0 || Array.isArray(block["markDefs"]) ? [...BODY_CAPABILITIES] : [];
385
+ for (const child of block["children"]) {
386
+ if (isRecord(child) && Array.isArray(child["marks"])) {
387
+ for (const mark of child["marks"]) if (typeof mark === "string") keys.add(mark);
388
+ }
389
+ }
390
+ for (const child of block["children"]) {
391
+ if (isPortableTextSpan(child)) {
392
+ children.push(child);
393
+ continue;
394
+ }
395
+ const type = isRecord(child) ? child["_type"] : void 0;
396
+ const mapping = typeof type === "string" && mappings && Object.hasOwn(mappings, type) ? mappings[type] : void 0;
397
+ if (!isRecord(child) || !isRecord(mapping) || !Array.isArray(mapping["inline"])) {
398
+ capabilities = [];
399
+ continue;
400
+ }
401
+ const declared = mapping["capabilities"];
402
+ if (declared !== void 0 && (!Array.isArray(declared) || declared.some((value) => typeof value !== "string" || !BODY_CAPABILITIES.includes(value)) || new Set(declared).size !== declared.length)) {
403
+ capabilities = [];
404
+ continue;
405
+ }
406
+ const newChildren = [];
407
+ const newDefs = [];
408
+ let valid = true;
409
+ for (const field of mapping["inline"]) {
410
+ 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"])) {
411
+ valid = false;
412
+ break;
413
+ }
414
+ const value = child[field["field"]];
415
+ const href = field["kind"] === "link" && typeof field["hrefField"] === "string" && field["hrefField"] ? child[field["hrefField"]] : void 0;
416
+ if ((value == null || value === "") && field["optional"] === true && (field["kind"] !== "link" || href == null || href === "")) continue;
417
+ if (typeof value !== "string" || field["kind"] === "link" && (typeof href !== "string" || !href)) {
418
+ valid = false;
419
+ break;
420
+ }
421
+ if (field["kind"] === "text") newChildren.push({ _type: "span", text: value, marks: [] });
422
+ else {
423
+ let key;
424
+ do {
425
+ key = `aexeo-inline-${sequence++}`;
426
+ } while (keys.has(key));
427
+ keys.add(key);
428
+ newDefs.push({ _key: key, _type: "link", href });
429
+ newChildren.push({ _type: "span", text: value, marks: [key] });
430
+ }
431
+ }
432
+ if (!valid) {
433
+ capabilities = [];
434
+ continue;
435
+ }
436
+ children.push(...newChildren);
437
+ defs.push(...newDefs);
438
+ if (Array.isArray(declared)) capabilities = intersectCapabilities(capabilities, declared);
439
+ }
440
+ return { block: { ...block, children, markDefs: defs }, capabilities };
441
+ }
140
442
  function isEmdashBlocksArray(value, field) {
141
443
  if (!Array.isArray(value) || value.length === 0) return false;
142
444
  return value.every(
143
- (item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
445
+ (item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks" || field === "body" || field === "content")
144
446
  );
145
447
  }
146
- function mapCustomBlocks(blocks, mappings) {
448
+ function mapCustomBlocks(blocks, mappings, depth = 0) {
449
+ if (depth > 8) return { blocks: [], capabilities: [] };
147
450
  const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
148
451
  const result = [];
149
- const mapFields = (record, fields, depth) => {
150
- if (depth > 8 || fields.length === 0) return false;
452
+ let capabilities = [...BODY_CAPABILITIES];
453
+ const mapFields = (record, fields, depth2) => {
454
+ if (depth2 > 8 || fields.length === 0) return false;
151
455
  for (const entry of fields) {
152
456
  if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
153
457
  if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
@@ -159,8 +463,16 @@ function mapCustomBlocks(blocks, mappings) {
159
463
  continue;
160
464
  }
161
465
  if (entry.kind === "richText") {
162
- if (!Array.isArray(value) || value.length > 0 && !isPortableTextArray(value)) return false;
163
- result.push(...value);
466
+ if (!Array.isArray(value)) return false;
467
+ if (value.length === 0 || isPortableTextArray(value)) {
468
+ result.push(...value);
469
+ } else if (isMixedPortableTextArray(value) || isEmdashBlocksArray(value, "blocks")) {
470
+ const nested = mapMixedPortableText(value, mappings, depth2 + 1);
471
+ result.push(...nested.blocks);
472
+ capabilities = intersectCapabilities(capabilities, nested.capabilities);
473
+ } else {
474
+ return false;
475
+ }
164
476
  } else if (entry.kind === "heading" || entry.kind === "paragraph") {
165
477
  if (typeof value !== "string") return false;
166
478
  result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
@@ -175,7 +487,7 @@ function mapCustomBlocks(blocks, mappings) {
175
487
  } else if (entry.kind === "repeat") {
176
488
  if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
177
489
  for (const item of value) {
178
- if (!isRecord(item) || !mapFields(item, entry.fields, depth + 1)) return false;
490
+ if (!isRecord(item) || !mapFields(item, entry.fields, depth2 + 1)) return false;
179
491
  }
180
492
  } else {
181
493
  return false;
@@ -184,10 +496,20 @@ function mapCustomBlocks(blocks, mappings) {
184
496
  return true;
185
497
  };
186
498
  for (const block of blocks) {
187
- const fields = mappings?.[String(block["_type"])];
188
- if (!Array.isArray(fields) || !mapFields(block, fields, 0)) return unavailable();
189
- }
190
- return { blocks: result };
499
+ const mapping = mappings?.[String(block["_type"])];
500
+ let fields;
501
+ if (Array.isArray(mapping)) {
502
+ fields = mapping;
503
+ } else if (isRecord(mapping) && Array.isArray(mapping["fields"]) && Array.isArray(mapping["capabilities"])) {
504
+ const declared = mapping["capabilities"];
505
+ if (declared.some((capability) => typeof capability !== "string" || !BODY_CAPABILITIES.includes(capability)) || new Set(declared).size !== declared.length) return unavailable();
506
+ fields = mapping["fields"];
507
+ capabilities = intersectCapabilities(capabilities, declared);
508
+ } else return unavailable();
509
+ if (fields.length === 0) continue;
510
+ if (!mapFields(block, fields, depth)) return unavailable();
511
+ }
512
+ return bodyWithCapabilities(result, capabilities);
191
513
  }
192
514
  function blocksToPortableText(blocks) {
193
515
  const paragraphs = [];
@@ -201,20 +523,49 @@ function blocksToPortableText(blocks) {
201
523
  }
202
524
  return paragraphs;
203
525
  }
526
+ var FALLBACK_MAX_DEPTH = 64;
527
+ var FALLBACK_MAX_NODES = 5e4;
528
+ var FALLBACK_MAX_CHARS = 1048576;
204
529
  function collectText(value, key, out) {
205
- if (typeof value === "string") {
206
- if (!isStructuralField(key) && value.trim().length > 0) out.push(value);
207
- return;
208
- }
209
- if (Array.isArray(value)) {
210
- for (const item of value) collectText(item, key, out);
211
- return;
212
- }
213
- if (!isRecord(value)) return;
214
- for (const [childKey, childValue] of Object.entries(value)) {
215
- if (isStructuralField(childKey)) continue;
216
- collectText(childValue, childKey, out);
217
- }
530
+ const active = /* @__PURE__ */ new WeakSet();
531
+ let nodes = 0;
532
+ let remaining = FALLBACK_MAX_CHARS;
533
+ const visit = (current, field, depth) => {
534
+ if (depth > FALLBACK_MAX_DEPTH || nodes >= FALLBACK_MAX_NODES || remaining <= 0) return;
535
+ nodes++;
536
+ if (typeof current === "string") {
537
+ if (!isStructuralField(field)) {
538
+ const separator = out.length > 0 ? 1 : 0;
539
+ const limit = Math.max(0, remaining - separator);
540
+ let text = current.slice(0, limit);
541
+ if (limit < current.length && /[\uD800-\uDBFF]/.test(text.at(-1) ?? "") && /[\uDC00-\uDFFF]/.test(current[limit] ?? "")) text = text.slice(0, -1);
542
+ text = text.trim();
543
+ if (text.length > 0) {
544
+ out.push(text);
545
+ remaining -= text.length + separator;
546
+ }
547
+ }
548
+ return;
549
+ }
550
+ if (!Array.isArray(current) && !isRecord(current)) return;
551
+ if (active.has(current)) return;
552
+ active.add(current);
553
+ if (Array.isArray(current)) {
554
+ for (const item of current) {
555
+ if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
556
+ visit(item, field, depth + 1);
557
+ }
558
+ } else {
559
+ for (const childKey in current) {
560
+ if (nodes >= FALLBACK_MAX_NODES || remaining <= 0) break;
561
+ if (Object.hasOwn(current, childKey) && !isStructuralField(childKey)) {
562
+ visit(current[childKey], childKey, depth + 1);
563
+ }
564
+ }
565
+ }
566
+ active.delete(current);
567
+ };
568
+ visit(value, key, 0);
218
569
  }
219
570
  var STRUCTURAL_FIELDS = /* @__PURE__ */ new Set([
220
571
  "_key",
@@ -273,12 +624,13 @@ function isRecord(value) {
273
624
  }
274
625
 
275
626
  // src/version.ts
276
- var PACKAGE_VERSION = "0.9.0";
627
+ var PACKAGE_VERSION = "0.9.1";
277
628
 
278
629
  // src/sidecar.ts
279
630
  async function evaluateViaSidecar(http, config, documents, collections) {
280
631
  const body = {
281
- documents
632
+ documents,
633
+ responseFormat: 2
282
634
  };
283
635
  if (collections !== void 0) body.collections = collections;
284
636
  if (config.configJson !== void 0) {
@@ -327,20 +679,36 @@ async function evaluateViaSidecar(http, config, documents, collections) {
327
679
  return { ok: false, reason: "invalid_response", detail };
328
680
  }
329
681
  try {
330
- const findings = JSON.parse(raw);
331
- if (!Array.isArray(findings)) {
332
- return {
333
- ok: false,
334
- reason: "invalid_response",
335
- detail: "sidecar returned non-array body"
336
- };
682
+ const value = JSON.parse(raw);
683
+ if (Array.isArray(value)) return { ok: true, findings: value };
684
+ if (!value || typeof value !== "object" || !("findings" in value) || !Array.isArray(value.findings) || !("assessments" in value) || !value.assessments || typeof value.assessments !== "object" || Array.isArray(value.assessments)) {
685
+ return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid assessment envelope" };
686
+ }
687
+ for (const finding of value.findings) {
688
+ if (finding && typeof finding === "object" && "aexeo_source_location" in finding && !validCounts(finding.aexeo_source_location, ["block_index", "span_index", "utf16_offset"])) {
689
+ return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid source location" };
690
+ }
337
691
  }
338
- return { ok: true, findings };
692
+ for (const assessment of Object.values(value.assessments)) {
693
+ if (assessment && typeof assessment === "object" && "source_mapping" in assessment && (!validCounts(assessment.source_mapping, ["eligible", "mapped", "unmapped"]) || assessment.source_mapping.mapped + assessment.source_mapping.unmapped !== assessment.source_mapping.eligible)) {
694
+ return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid source mapping coverage" };
695
+ }
696
+ if (!assessment || typeof assessment !== "object" || !("status" in assessment) || assessment.status !== "evaluated" || !("preset" in assessment) || typeof assessment.preset !== "string" || !("gates" in assessment) || !Array.isArray(assessment.gates) || assessment.gates.some((gate) => !gate || typeof gate !== "object" || !("gate_id" in gate) || typeof gate.gate_id !== "string" || !("status" in gate) || !["passed", "failed", "not_checked"].includes(String(gate.status)) || !("blocking" in gate) || typeof gate.blocking !== "boolean" || !("scope" in gate) || !["document", "collection", "site"].includes(String(gate.scope)) || !("required_capability" in gate) || typeof gate.required_capability !== "string" || !("reason" in gate) || gate.reason !== null && typeof gate.reason !== "string" || "measurements" in gate && (gate.measurements === null || typeof gate.measurements !== "object" || Array.isArray(gate.measurements) || Object.values(gate.measurements).some((v) => typeof v !== "number" || !Number.isFinite(v))))) {
697
+ return { ok: false, reason: "invalid_response", detail: "sidecar returned invalid gate assessment" };
698
+ }
699
+ }
700
+ return { ok: true, findings: value.findings, assessments: value.assessments };
339
701
  } catch (err) {
340
702
  const detail = err instanceof Error ? err.message : String(err);
341
703
  return { ok: false, reason: "invalid_response", detail };
342
704
  }
343
705
  }
706
+ function validCounts(value, keys) {
707
+ return value !== null && typeof value === "object" && !Array.isArray(value) && keys.every((key) => {
708
+ const count = value[key];
709
+ return typeof count === "number" && Number.isSafeInteger(count) && count >= 0;
710
+ });
711
+ }
344
712
  function trimTrailingSlash(url) {
345
713
  return url.endsWith("/") ? url.slice(0, -1) : url;
346
714
  }
@@ -359,6 +727,7 @@ var CONFIG_TOKEN_KEY = "config:eval_token";
359
727
  var SETTINGS_TOKEN_KEY = "eval_token";
360
728
  var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
361
729
  var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
730
+ var SETTINGS_DESCRIPTION_FIELDS_KEY = "description_fields_by_collection";
362
731
  var PLUGIN_SETTINGS_SCHEMA = {
363
732
  [SETTINGS_CONTENT_FIELDS_KEY]: {
364
733
  type: "string",
@@ -366,6 +735,12 @@ var PLUGIN_SETTINGS_SCHEMA = {
366
735
  description: "Ordered rendered body fields per collection.",
367
736
  multiline: true
368
737
  },
738
+ [SETTINGS_DESCRIPTION_FIELDS_KEY]: {
739
+ type: "string",
740
+ label: "Description fields by collection (JSON)",
741
+ description: "Ordered renderer description fallback fields per collection.",
742
+ multiline: true
743
+ },
369
744
  [SETTINGS_BLOCK_MAPPINGS_KEY]: {
370
745
  type: "string",
371
746
  label: "Custom block mappings (JSON)",
@@ -445,14 +820,54 @@ function describeError(err, secret) {
445
820
  function recordContentCoverage(ctx, documents) {
446
821
  const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
447
822
  for (const document of documents) {
448
- if (document.body === void 0 || document.body_capabilities?.length === 0) counts.unavailable++;
449
- else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
450
- (capability) => document.body_capabilities?.includes(capability)
451
- )) counts.complete++;
452
- else counts.partial++;
823
+ counts[cmsBodyCoverage(document)]++;
453
824
  }
454
825
  try {
455
826
  ctx.log?.info?.("aexeo.content_coverage", counts);
827
+ ctx.log?.info?.("aexeo.page_coverage", { documents: documents.length, rendered_page: "not_observed", generated_metadata: "not_observed" });
828
+ const observed = documents.filter((document) => document.description_source_observed === true).length;
829
+ const withValue = documents.filter((document) => document.description_source_observed === true && !!document.description).length;
830
+ ctx.log?.info?.("aexeo.description_coverage", { documents: documents.length, observed, with_value: withValue, unverified: documents.length - observed });
831
+ } catch {
832
+ }
833
+ }
834
+ function recordAssessmentCoverage(ctx, result, documents) {
835
+ let passed = 0, failed = 0, notChecked = 0, mappingReports = 0, mappingEligible = 0, mappingMapped = 0, mappingUnmapped = 0;
836
+ if (result.ok) for (const assessment of Object.values(result.assessments ?? {})) {
837
+ if (assessment.status !== "evaluated") continue;
838
+ if (assessment.source_mapping) {
839
+ mappingReports++;
840
+ mappingEligible += assessment.source_mapping.eligible;
841
+ mappingMapped += assessment.source_mapping.mapped;
842
+ mappingUnmapped += assessment.source_mapping.unmapped;
843
+ }
844
+ for (const gate of assessment.gates) {
845
+ if (gate.status === "passed") passed++;
846
+ else if (gate.status === "failed") failed++;
847
+ else notChecked++;
848
+ }
849
+ }
850
+ let assessedDocuments = 0, missingDocuments = 0, errorDocuments = 0;
851
+ if (result.ok) for (const document of documents) {
852
+ const assessment = result.assessments?.[document.route];
853
+ if (assessment === void 0) missingDocuments++;
854
+ else if (assessment.status === "error") errorDocuments++;
855
+ else assessedDocuments++;
856
+ }
857
+ 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;
858
+ try {
859
+ 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 });
860
+ } catch {
861
+ }
862
+ const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
863
+ let withheld = 0, incompleteBodyWithheld = 0;
864
+ if (result.ok) for (const finding of result.findings) {
865
+ if (finding.aexeo_origin === "content_preset") continue;
866
+ if (!isLegacyCmsObservableFinding(finding)) withheld++;
867
+ else if (!isLegacyCmsObservableFinding(finding, completeByRoute.get(finding.path) ?? false)) incompleteBodyWithheld++;
868
+ }
869
+ try {
870
+ ctx.log?.info?.("aexeo.cms_finding_coverage", { status: result.ok ? "evaluated" : "error", rendered_findings_withheld: withheld, incomplete_body_findings_withheld: incompleteBodyWithheld });
456
871
  } catch {
457
872
  }
458
873
  }
@@ -479,20 +894,26 @@ async function readSandboxBlockMappings(ctx) {
479
894
  return void 0;
480
895
  }
481
896
  }
482
- async function readSandboxContentFields(ctx) {
483
- if (!ctx.settings) return void 0;
897
+ async function readSandboxFields(ctx, key, warning) {
898
+ if (!ctx.settings) return { fields: void 0, unavailable: false };
484
899
  try {
485
- const raw = await ctx.settings.get(SETTINGS_CONTENT_FIELDS_KEY);
486
- if (raw === null || raw === "") return void 0;
900
+ const raw = await ctx.settings.get(key);
901
+ if (raw === null || raw === "") return { fields: void 0, unavailable: false };
487
902
  if (typeof raw !== "string" || raw.length > 65536) throw new Error("invalid selection size or type");
488
903
  const value = JSON.parse(raw);
489
904
  if (value === null || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((fields) => Array.isArray(fields) && fields.length > 0 && fields.every((field) => typeof field === "string" && field.length > 0))) throw new Error("invalid selection");
490
- return value;
905
+ return { fields: value, unavailable: false };
491
906
  } catch {
492
- ctx.log?.warn?.("Aexeo content field selection unavailable; multiple fields remain unverified");
493
- return void 0;
907
+ ctx.log?.warn?.(warning);
908
+ return { fields: void 0, unavailable: true };
494
909
  }
495
910
  }
911
+ function readSandboxContentFieldState(ctx) {
912
+ return readSandboxFields(ctx, SETTINGS_CONTENT_FIELDS_KEY, "Aexeo content field selection unavailable; body coverage remains unverified");
913
+ }
914
+ async function readSandboxDescriptionFields(ctx) {
915
+ return (await readSandboxFields(ctx, SETTINGS_DESCRIPTION_FIELDS_KEY, "Aexeo description field selection unavailable; description remains unverified")).fields;
916
+ }
496
917
  async function handleAfterSave(event, ctx) {
497
918
  const runtime = await readSidecarConfig(ctx);
498
919
  if (runtime === null) {
@@ -501,7 +922,7 @@ async function handleAfterSave(event, ctx) {
501
922
  );
502
923
  return;
503
924
  }
504
- const [blockMappings, fields] = await Promise.all([readSandboxBlockMappings(ctx), readSandboxContentFields(ctx)]);
925
+ const [blockMappings, fields, descriptions] = await Promise.all([readSandboxBlockMappings(ctx), readSandboxContentFieldState(ctx), readSandboxDescriptionFields(ctx)]);
505
926
  await handleAfterSaveConfigured(
506
927
  event,
507
928
  ctx,
@@ -513,7 +934,9 @@ async function handleAfterSave(event, ctx) {
513
934
  ),
514
935
  void 0,
515
936
  blockMappings,
516
- fields?.[event.content.type]
937
+ fields.fields?.[event.content.type],
938
+ descriptions?.[event.content.type],
939
+ fields.unavailable
517
940
  );
518
941
  }
519
942
  var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
@@ -536,15 +959,63 @@ var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
536
959
  "CNT005"
537
960
  // visible text duplicated on another route
538
961
  ]);
539
- async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields) {
540
- const adapted = await adaptContentItemWithContext(
541
- event.content,
542
- ctx.content,
543
- ctx.site?.locale,
544
- blockMappings,
545
- contentFields
962
+ function isSiteGraphFinding(finding) {
963
+ return finding.aexeo_origin !== "content_preset" && SITE_GRAPH_RULE_IDS.has(finding.rule_id);
964
+ }
965
+ function withCmsSourceLocation(finding, stored) {
966
+ if (finding.aexeo_cms_location !== void 0) {
967
+ finding = { ...finding };
968
+ delete finding.aexeo_cms_location;
969
+ }
970
+ const location = finding.aexeo_source_location;
971
+ if (finding.aexeo_origin !== "content_preset" || !location || !stored) return finding;
972
+ const blockPath = stored.meta.bodySourcePaths?.[location.block_index];
973
+ const spanPath = stored.meta.bodySourceSpanPaths?.[location.block_index]?.[location.span_index];
974
+ const path = blockPath ? [...blockPath, "children", location.span_index, "text"] : spanPath;
975
+ const text = stored.document.body?.[location.block_index]?.children[location.span_index]?.text;
976
+ const offset = location.utf16_offset;
977
+ 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;
978
+ return { ...finding, aexeo_cms_location: { path, utf16_offset: offset } };
979
+ }
980
+ function countCmsLocations(counts, findings) {
981
+ for (const finding of findings) {
982
+ if (finding.aexeo_origin !== "content_preset" || finding.aexeo_source_location === void 0) continue;
983
+ counts.eligible++;
984
+ if (finding.aexeo_cms_location !== void 0) counts.mapped++;
985
+ }
986
+ }
987
+ function recordCmsLocationCoverage(ctx, counts) {
988
+ try {
989
+ ctx.log?.info?.("aexeo.cms_location_coverage", { ...counts, unmapped: counts.eligible - counts.mapped });
990
+ } catch {
991
+ }
992
+ }
993
+ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields, descriptionFields, contentFieldsUnavailable = false) {
994
+ const prior = (await readAllStoredDocuments(ctx.kv)).filter(
995
+ (stored2) => stored2.meta.id === event.content.id && stored2.meta.collection === event.content.type
546
996
  );
997
+ let adapted;
998
+ try {
999
+ adapted = await adaptContentItemWithContext(
1000
+ event.content,
1001
+ ctx.content,
1002
+ ctx.site?.locale,
1003
+ blockMappings,
1004
+ contentFields,
1005
+ descriptionFields
1006
+ );
1007
+ } catch {
1008
+ try {
1009
+ ctx.log?.info?.("aexeo.adaptation_coverage", { failed: 1 });
1010
+ } catch {
1011
+ }
1012
+ ctx.log?.error?.("Aexeo afterSave adaptation failed");
1013
+ const previous = prior.length === 1 ? prior[0] : void 0;
1014
+ if (previous !== void 0) await markAdaptationUnavailable(ctx, event.content, previous);
1015
+ return;
1016
+ }
547
1017
  const document = adapted.document;
1018
+ if (contentFieldsUnavailable && document.body !== void 0) document.body_capabilities = [];
548
1019
  recordContentCoverage(ctx, [document]);
549
1020
  const { kv, log } = ctx;
550
1021
  const stored = { document, meta: adapted.meta };
@@ -552,20 +1023,28 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
552
1023
  log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
553
1024
  return;
554
1025
  }
555
- const result = await evaluator([document], { collections: [adapted.meta.collection] });
1026
+ const result = await evaluateSafely(evaluator, [document], { collections: [adapted.meta.collection] });
1027
+ recordAssessmentCoverage(ctx, result, [document]);
556
1028
  if (!result.ok) {
557
1029
  log?.error?.(`afterSave evaluator failure (${result.reason})`, {
558
1030
  detail: result.detail
559
1031
  });
1032
+ if (!await isSuperseded(kv, stored)) {
1033
+ await kv.set(findingsKey(document.route), {
1034
+ route: document.route,
1035
+ findings: [],
1036
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
1037
+ });
1038
+ }
560
1039
  return;
561
1040
  }
562
1041
  const storedGraphFindings = (await readFindings(kv, document.route)).filter(
563
- (finding) => SITE_GRAPH_RULE_IDS.has(finding.rule_id)
1042
+ (finding) => isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
564
1043
  );
565
1044
  const pageFindings = [
566
1045
  ...result.findings.filter(
567
- (finding) => finding.scope === "page" && !SITE_GRAPH_RULE_IDS.has(finding.rule_id)
568
- ),
1046
+ (finding) => finding.scope === "page" && !isSiteGraphFinding(finding) && isCmsObservableFinding(finding, hasCompleteBody(document))
1047
+ ).map((finding) => withCmsSourceLocation(finding, stored)),
569
1048
  ...storedGraphFindings
570
1049
  ];
571
1050
  const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
@@ -576,16 +1055,51 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
576
1055
  },
577
1056
  pageFindings
578
1057
  );
579
- if (await isSuperseded(kv, document.route, adapted.meta.updatedAt)) {
1058
+ if (await isSuperseded(kv, stored)) {
580
1059
  log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
581
1060
  return;
582
1061
  }
1062
+ const cmsLocationCounts = { eligible: 0, mapped: 0 };
1063
+ countCmsLocations(cmsLocationCounts, filtered);
1064
+ recordCmsLocationCoverage(ctx, cmsLocationCounts);
583
1065
  await kv.set(findingsKey(document.route), {
584
1066
  route: document.route,
585
- findings: filtered
1067
+ findings: filtered,
1068
+ body_coverage: cmsBodyCoverage(document),
1069
+ ...result.assessments?.[document.route] === void 0 ? {} : { assessment: result.assessments[document.route] }
586
1070
  });
587
1071
  }
588
1072
  var DEFAULT_COLLECTIONS = ["posts", "pages"];
1073
+ async function markAdaptationUnavailable(ctx, item, previous) {
1074
+ if (isOlderRevision(item.updatedAt, previous.meta.updatedAt) || await isSuperseded(ctx.kv, previous)) return;
1075
+ const unavailable = {
1076
+ document: { route: previous.document.route, title: previous.document.title },
1077
+ meta: {
1078
+ id: item.id,
1079
+ collection: item.type,
1080
+ slug: item.slug,
1081
+ status: item.status,
1082
+ title: previous.meta.title,
1083
+ publicUrl: null,
1084
+ adaptationFailed: true,
1085
+ ...item.updatedAt === void 0 ? {} : { updatedAt: item.updatedAt }
1086
+ }
1087
+ };
1088
+ if (!await persistDocument(ctx, unavailable) || await isSuperseded(ctx.kv, unavailable)) return;
1089
+ recordContentCoverage(ctx, [unavailable.document]);
1090
+ await ctx.kv.set(findingsKey(unavailable.document.route), {
1091
+ route: unavailable.document.route,
1092
+ findings: [],
1093
+ assessment: { status: "error", reason: "adaptation_failed", detail: "The CMS record could not be adapted." }
1094
+ });
1095
+ }
1096
+ async function evaluateSafely(evaluator, documents, context) {
1097
+ try {
1098
+ return await evaluator(documents, context);
1099
+ } catch {
1100
+ return { ok: false, reason: "evaluator_exception", detail: "The evaluator failed unexpectedly." };
1101
+ }
1102
+ }
589
1103
  async function evaluateAndPersistAll(ctx, options) {
590
1104
  const collections = options.collections ?? DEFAULT_COLLECTIONS;
591
1105
  const suppressionFilter = options.suppressionFilter;
@@ -601,8 +1115,10 @@ async function evaluateAndPersistAll(ctx, options) {
601
1115
  const documentCollections = [];
602
1116
  const adaptedByRoute = /* @__PURE__ */ new Map();
603
1117
  const documentRoutes = /* @__PURE__ */ new Set();
1118
+ const adaptationFailures = [];
604
1119
  for (const collection of collections) {
605
1120
  let cursor;
1121
+ const seenCursors = /* @__PURE__ */ new Set();
606
1122
  do {
607
1123
  try {
608
1124
  const page = await ctx.content.list(collection, {
@@ -610,22 +1126,35 @@ async function evaluateAndPersistAll(ctx, options) {
610
1126
  ...cursor === void 0 ? {} : { cursor }
611
1127
  });
612
1128
  for (const item of page.items) {
613
- const adapted = await adaptContentItemWithContext(
614
- item,
615
- ctx.content,
616
- ctx.site?.locale,
617
- options.blockMappings,
618
- options.contentFieldsByCollection?.[collection]
619
- );
620
- documents.push(adapted.document);
621
- documentCollections.push(adapted.meta.collection);
622
- documentRoutes.add(adapted.document.route);
623
- adaptedByRoute.set(adapted.document.route, {
624
- document: adapted.document,
625
- meta: adapted.meta
626
- });
1129
+ try {
1130
+ const adapted = await adaptContentItemWithContext(
1131
+ item,
1132
+ ctx.content,
1133
+ ctx.site?.locale,
1134
+ options.blockMappings,
1135
+ options.contentFieldsByCollection?.[collection],
1136
+ options.descriptionFieldsByCollection?.[collection]
1137
+ );
1138
+ if (options.contentFieldsUnavailable && adapted.document.body !== void 0) adapted.document.body_capabilities = [];
1139
+ documents.push(adapted.document);
1140
+ documentCollections.push(adapted.meta.collection);
1141
+ documentRoutes.add(adapted.document.route);
1142
+ adaptedByRoute.set(adapted.document.route, {
1143
+ document: adapted.document,
1144
+ meta: adapted.meta
1145
+ });
1146
+ } catch {
1147
+ adaptationFailures.push({ item, collection });
1148
+ summary.errors.push(`${collection}: content adaptation failed`);
1149
+ }
1150
+ }
1151
+ if (page.hasMore) {
1152
+ if (!page.cursor || seenCursors.has(page.cursor)) throw new Error("incomplete pagination: missing or repeated cursor");
1153
+ seenCursors.add(page.cursor);
1154
+ cursor = page.cursor;
1155
+ } else {
1156
+ cursor = void 0;
627
1157
  }
628
- cursor = page.hasMore ? page.cursor : void 0;
629
1158
  } catch (err) {
630
1159
  const message = err instanceof Error ? err.message : String(err);
631
1160
  summary.errors.push(`${collection}: ${message}`);
@@ -633,36 +1162,100 @@ async function evaluateAndPersistAll(ctx, options) {
633
1162
  }
634
1163
  } while (cursor !== void 0);
635
1164
  }
1165
+ const inventoryComplete = summary.errors.length === 0;
636
1166
  summary.documentsScanned = documents.length;
637
1167
  recordContentCoverage(ctx, documents);
1168
+ if (adaptationFailures.length > 0) {
1169
+ try {
1170
+ ctx.log?.info?.("aexeo.adaptation_coverage", { failed: adaptationFailures.length });
1171
+ } catch {
1172
+ }
1173
+ const storedByCollection = /* @__PURE__ */ new Map();
1174
+ for (const stored of storedAtStart) {
1175
+ const byId = storedByCollection.get(stored.meta.collection) ?? /* @__PURE__ */ new Map();
1176
+ const entries = byId.get(stored.meta.id) ?? [];
1177
+ entries.push(stored);
1178
+ byId.set(stored.meta.id, entries);
1179
+ storedByCollection.set(stored.meta.collection, byId);
1180
+ }
1181
+ for (const { item, collection } of adaptationFailures) {
1182
+ for (const stored of storedByCollection.get(collection)?.get(item.id) ?? []) {
1183
+ if (documentRoutes.has(stored.document.route)) continue;
1184
+ await markAdaptationUnavailable(ctx, item, stored);
1185
+ }
1186
+ }
1187
+ }
638
1188
  for (const stored of adaptedByRoute.values()) {
639
1189
  const current = await readStoredDocument(kv, stored.document.route);
640
1190
  if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
641
1191
  await kv.set(documentKey(stored.document.route), stored);
642
1192
  }
643
1193
  }
644
- const result = await options.evaluator(documents, { collections: documentCollections });
1194
+ const result = await evaluateSafely(options.evaluator, documents, { collections: documentCollections });
1195
+ recordAssessmentCoverage(ctx, result, documents);
645
1196
  if (!result.ok) {
646
1197
  log?.error?.(`aexeo evaluator failure (${result.reason})`, {
647
1198
  detail: result.detail
648
1199
  });
649
1200
  summary.errors.push(`${result.reason}: ${result.detail}`);
1201
+ try {
1202
+ ctx.log?.info?.("aexeo.sitewide_coverage", { status: "evaluation_error" });
1203
+ } catch {
1204
+ }
1205
+ await kv.set(findingsKey("*"), {
1206
+ route: "*",
1207
+ findings: [],
1208
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
1209
+ });
1210
+ for (const [route, stored] of adaptedByRoute) {
1211
+ if (!await isSuperseded(kv, stored)) {
1212
+ await kv.set(findingsKey(route), {
1213
+ route,
1214
+ findings: [],
1215
+ assessment: { status: "error", reason: result.reason, detail: result.detail }
1216
+ });
1217
+ }
1218
+ }
650
1219
  return summary;
651
1220
  }
652
1221
  const findingsByRoute = /* @__PURE__ */ new Map();
1222
+ findingsByRoute.set("*", []);
653
1223
  for (const route of documentRoutes) {
654
1224
  findingsByRoute.set(route, []);
655
1225
  }
1226
+ const completeByRoute = new Map(documents.map((document) => [document.route, hasCompleteBody(document)]));
1227
+ const allBodiesComplete = documents.every(hasCompleteBody);
1228
+ const changed = await refreshInputChanged(kv, collections, adaptedByRoute, storedAtStart);
1229
+ const coverageStatus = !inventoryComplete ? "inventory_incomplete" : changed ? "changed_during_refresh" : !allBodiesComplete ? "body_incomplete" : "complete";
1230
+ const globalCoverageComplete = coverageStatus === "complete";
1231
+ try {
1232
+ ctx.log?.info?.("aexeo.sitewide_coverage", { status: coverageStatus });
1233
+ } catch {
1234
+ }
1235
+ const globalAssessment = globalCoverageComplete ? void 0 : {
1236
+ status: "error",
1237
+ reason: coverageStatus,
1238
+ 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."
1239
+ };
1240
+ if (changed && inventoryComplete) {
1241
+ summary.errors.push("Sitewide checks unavailable: content changed during refresh.");
1242
+ } else if (!allBodiesComplete && inventoryComplete) {
1243
+ summary.errors.push("Sitewide checks unavailable: one or more document bodies are incomplete.");
1244
+ }
656
1245
  for (const finding of result.findings) {
1246
+ if (!globalCoverageComplete && (finding.scope !== "page" || isSiteGraphFinding(finding))) continue;
1247
+ const complete = finding.scope === "page" ? completeByRoute.get(finding.path) ?? false : allBodiesComplete;
1248
+ if (!isCmsObservableFinding(finding, complete)) continue;
657
1249
  const bucket = finding.scope === "page" ? finding.path : "*";
658
1250
  const list = findingsByRoute.get(bucket) ?? [];
659
- list.push(finding);
1251
+ list.push(withCmsSourceLocation(finding, adaptedByRoute.get(bucket)));
660
1252
  findingsByRoute.set(bucket, list);
661
1253
  }
662
1254
  let totalAfterSuppression = 0;
1255
+ const cmsLocationCounts = { eligible: 0, mapped: 0 };
663
1256
  for (const [route, findings] of findingsByRoute) {
664
1257
  const stored = adaptedByRoute.get(route);
665
- if (stored !== void 0 && await isSuperseded(kv, route, stored.meta.updatedAt)) {
1258
+ if (stored !== void 0 && await isSuperseded(kv, stored)) {
666
1259
  continue;
667
1260
  }
668
1261
  const context = {
@@ -671,16 +1264,37 @@ async function evaluateAndPersistAll(ctx, options) {
671
1264
  ...stored?.meta.status === void 0 ? {} : { status: stored.meta.status }
672
1265
  };
673
1266
  const filtered = suppressionFilter === void 0 ? findings : suppressionFilter.apply(context, findings);
674
- await kv.set(findingsKey(route), { route, findings: filtered });
1267
+ countCmsLocations(cmsLocationCounts, filtered);
1268
+ await kv.set(findingsKey(route), {
1269
+ route,
1270
+ findings: filtered,
1271
+ ...stored === void 0 ? {} : { body_coverage: cmsBodyCoverage(stored.document) },
1272
+ ...route === "*" && globalAssessment !== void 0 ? { assessment: globalAssessment } : result.assessments?.[route] === void 0 ? {} : { assessment: result.assessments[route] }
1273
+ });
675
1274
  summary.routesUpdated += 1;
676
1275
  totalAfterSuppression += filtered.length;
677
1276
  }
1277
+ recordCmsLocationCoverage(ctx, cmsLocationCounts);
678
1278
  summary.totalFindings = totalAfterSuppression;
679
- if (summary.errors.length === 0) {
1279
+ if (inventoryComplete && !changed) {
680
1280
  await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
681
1281
  }
682
1282
  return summary;
683
1283
  }
1284
+ async function refreshInputChanged(kv, collections, evaluated, initial) {
1285
+ const current = await readAllStoredDocuments(kv);
1286
+ const currentByRoute = new Map(current.map((entry) => [entry.document.route, entry]));
1287
+ const initialByRoute = new Map(initial.map((entry) => [entry.document.route, entry]));
1288
+ for (const [route, entry] of evaluated) {
1289
+ if (!sameStoredDocument(entry, currentByRoute.get(route))) return true;
1290
+ }
1291
+ const selected = new Set(collections);
1292
+ for (const entry of current) {
1293
+ const route = entry.document.route;
1294
+ if (!evaluated.has(route) && (entry.meta.collection === "" || selected.has(entry.meta.collection)) && !sameStoredDocument(entry, initialByRoute.get(route))) return true;
1295
+ }
1296
+ return false;
1297
+ }
684
1298
  function isOlderRevision(revision, other) {
685
1299
  if (revision === void 0 || other === void 0) return false;
686
1300
  return Date.parse(revision) < Date.parse(other);
@@ -688,9 +1302,11 @@ function isOlderRevision(revision, other) {
688
1302
  async function readStoredDocument(kv, route) {
689
1303
  return toStoredDocument(await kv.get(documentKey(route)));
690
1304
  }
691
- async function isSuperseded(kv, route, revision) {
692
- const current = await readStoredDocument(kv, route);
693
- return current === null || isOlderRevision(revision, current.meta.updatedAt);
1305
+ function sameStoredDocument(a, b) {
1306
+ return a != null && b != null && JSON.stringify(a) === JSON.stringify(b);
1307
+ }
1308
+ async function isSuperseded(kv, evaluated) {
1309
+ return !sameStoredDocument(evaluated, await readStoredDocument(kv, evaluated.document.route));
694
1310
  }
695
1311
  async function persistDocument(ctx, stored) {
696
1312
  const route = stored.document.route;
@@ -758,9 +1374,6 @@ function documentKey(route) {
758
1374
  const normalized = route === "" || route === "/" ? "/" : route;
759
1375
  return `document:${normalized}`;
760
1376
  }
761
- async function readAllDocuments(kv) {
762
- return (await readAllStoredDocuments(kv)).map((s) => s.document);
763
- }
764
1377
  async function readAllStoredDocuments(kv) {
765
1378
  const entries = await kv.list("document:");
766
1379
  const out = [];
@@ -777,6 +1390,10 @@ async function readFindings(kv, route) {
777
1390
  }
778
1391
  return stored.findings;
779
1392
  }
1393
+ async function readAssessment(kv, route) {
1394
+ const stored = await kv.get(findingsKey(route));
1395
+ return stored?.assessment;
1396
+ }
780
1397
 
781
1398
  // src/sidecar-url.ts
782
1399
  function validateSidecarUrl(raw, allowedHosts = []) {
@@ -882,6 +1499,19 @@ async function scoreSite(_documents) {
882
1499
  return null;
883
1500
  }
884
1501
 
1502
+ // src/assessment-display.ts
1503
+ function describeGate(gate) {
1504
+ const status = gate.status === "not_checked" ? "not checked" : gate.status;
1505
+ const reason = gate.status === "not_checked" ? `: ${gate.reason ?? "required input unavailable"}` : "";
1506
+ const metrics = Object.entries(gate.measurements ?? {}).map(([key, value]) => `${key.replaceAll("_", " ")}: ${value}`).join(", ");
1507
+ return `${gate.gate_id} ${status}${reason} (${gate.blocking ? "blocking" : "advisory"})${metrics ? ` \u2014 ${metrics}` : ""}`;
1508
+ }
1509
+ function summarizeGates(gates) {
1510
+ const counts = { passed: 0, failed: 0, not_checked: 0 };
1511
+ for (const gate of gates) counts[gate.status]++;
1512
+ return `${gates.length} configured checks: ${counts.passed} passed, ${counts.failed} failed, ${counts.not_checked} not checked`;
1513
+ }
1514
+
885
1515
  // src/findings-view.ts
886
1516
  function normalizeInteraction(input) {
887
1517
  if (!input || typeof input !== "object") {
@@ -920,6 +1550,8 @@ async function handleRefresh(options) {
920
1550
  evaluator: options.evaluator,
921
1551
  blockMappings: options.blockMappings,
922
1552
  contentFieldsByCollection: options.contentFieldsByCollection,
1553
+ contentFieldsUnavailable: options.contentFieldsUnavailable,
1554
+ descriptionFieldsByCollection: options.descriptionFieldsByCollection,
923
1555
  // Always forwarded. See RefreshOptions.suppressionFilter.
924
1556
  suppressionFilter: options.suppressionFilter
925
1557
  });
@@ -955,21 +1587,38 @@ async function handleRefresh(options) {
955
1587
  }
956
1588
  };
957
1589
  }
958
- async function readAllFindings(kv) {
959
- const entries = await kv.list(
960
- "findings:"
961
- );
962
- const out = [];
1590
+ async function readFindingsSnapshot(kv) {
1591
+ const entries = await kv.list("findings:");
1592
+ const sitewideAssessment = entries.find((entry) => entry.key === "findings:*")?.value?.assessment;
1593
+ const incomplete = sitewideAssessment?.status === "error";
1594
+ const findings = [];
1595
+ const routes = /* @__PURE__ */ new Set();
1596
+ let failedAssessments = 0;
1597
+ let uncheckedGates = 0;
1598
+ let incompleteBodies = 0;
1599
+ let unknownBodies = 0;
1600
+ let missingAssessments = 0;
963
1601
  for (const entry of entries) {
964
- if (entry.value === null) {
965
- continue;
966
- }
1602
+ if (entry.value === null) continue;
967
1603
  const route = entry.key.replace(/^findings:/, "");
1604
+ if (route !== "*") {
1605
+ routes.add(route);
1606
+ if (entry.value.body_coverage === void 0) unknownBodies++;
1607
+ else if (entry.value.body_coverage !== "complete") incompleteBodies++;
1608
+ const assessment = entry.value.assessment;
1609
+ if (assessment === void 0) missingAssessments++;
1610
+ if (assessment?.status === "error") failedAssessments++;
1611
+ if (assessment?.status === "evaluated") {
1612
+ uncheckedGates += assessment.gates.filter((gate) => gate.status === "not_checked").length;
1613
+ }
1614
+ }
968
1615
  for (const finding of entry.value.findings) {
969
- out.push({ ...finding, document_route: route });
1616
+ if (!isCmsObservableFinding(finding, route === "*" || entry.value.body_coverage === "complete") || incomplete && isSiteGraphFinding(finding)) continue;
1617
+ findings.push({ ...finding, document_route: route });
1618
+ if (route === "*") routes.add(route);
970
1619
  }
971
1620
  }
972
- return out;
1621
+ return { findings, documentRoutes: [...routes].sort(), sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments };
973
1622
  }
974
1623
  function severityFirst(a, b) {
975
1624
  const rank = (severity) => severity === "error" ? 0 : 1;
@@ -987,7 +1636,7 @@ function uniqueRoutes(rows) {
987
1636
  return [...routes].sort();
988
1637
  }
989
1638
  async function renderFindingsPage(kv, tuning) {
990
- const findings = await readAllFindings(kv);
1639
+ const { findings, documentRoutes, sitewideAssessment, failedAssessments, uncheckedGates, incompleteBodies, unknownBodies, missingAssessments } = await readFindingsSnapshot(kv);
991
1640
  const errors = findings.filter((finding) => finding.severity === "error");
992
1641
  const warnings = findings.filter(
993
1642
  (finding) => finding.severity === "warning"
@@ -998,8 +1647,13 @@ async function renderFindingsPage(kv, tuning) {
998
1647
  { type: "header", text: "SEO findings" },
999
1648
  {
1000
1649
  type: "context",
1001
- 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.`
1650
+ 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.`
1002
1651
  },
1652
+ { 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." },
1653
+ ...incompleteBodies || unknownBodies ? [{ type: "context", text: `${incompleteBodies} document bodies were incomplete or unavailable; coverage is unknown for ${unknownBodies} older or failed evaluations. Inspect individual documents.` }] : [],
1654
+ ...missingAssessments ? [{ type: "context", text: `No content preset assessment was recorded for ${missingAssessments} document(s); absence of findings is not an editorial quality assessment.` }] : [],
1655
+ ...failedAssessments || uncheckedGates ? [{ type: "context", text: `${failedAssessments} document evaluations failed; ${uncheckedGates} preset checks were not checked. Inspect individual documents for details.` }] : [],
1656
+ ...sitewideAssessment?.status === "error" ? [{ type: "context", text: `Sitewide checks unavailable: ${sitewideAssessment.reason}. ${sitewideAssessment.detail}` }] : [],
1003
1657
  { type: "divider" },
1004
1658
  {
1005
1659
  type: "actions",
@@ -1024,10 +1678,10 @@ async function renderFindingsPage(kv, tuning) {
1024
1678
  },
1025
1679
  sorted.length === 0 ? {
1026
1680
  type: "context",
1027
- text: tuning.emptyFindingsText
1681
+ text: documentRoutes.length === 0 ? tuning.emptyFindingsText : "Select a document to inspect its assessment."
1028
1682
  } : findingsTable(sorted)
1029
1683
  ];
1030
- if (routes.length > 0) {
1684
+ if (documentRoutes.length > 0) {
1031
1685
  blocks.push({
1032
1686
  type: "form",
1033
1687
  fields: [
@@ -1035,7 +1689,7 @@ async function renderFindingsPage(kv, tuning) {
1035
1689
  type: "select",
1036
1690
  action_id: "route_picker",
1037
1691
  label: "Document to inspect",
1038
- options: routes.map((route) => ({ label: route, value: route }))
1692
+ options: documentRoutes.map((route) => ({ label: route, value: route }))
1039
1693
  }
1040
1694
  ],
1041
1695
  submit: { label: "View document SEO", action_id: "view_document" }
@@ -1060,75 +1714,13 @@ function findingsTable(rows) {
1060
1714
  }))
1061
1715
  };
1062
1716
  }
1063
- async function renderScoreWidget(kv, tuning, log) {
1064
- const documents = await readAllDocuments(kv);
1065
- if (documents.length === 0) {
1066
- return {
1067
- blocks: [
1068
- { type: "header", text: "SEO score" },
1069
- {
1070
- type: "context",
1071
- text: tuning.emptyScoreText
1072
- }
1073
- ]
1074
- };
1075
- }
1076
- let score;
1077
- try {
1078
- score = await tuning.score(documents, kv);
1079
- } catch (error) {
1080
- log?.error("Aexeo score widget failed to initialize", { error });
1081
- return {
1082
- blocks: [
1083
- { type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
1084
- { type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
1085
- ]
1086
- };
1087
- }
1088
- if (score === null) {
1089
- return {
1090
- blocks: [
1091
- { type: "header", text: "SEO score" },
1092
- {
1093
- type: "context",
1094
- text: "The site score is unavailable in sandboxed mode. Findings are not affected."
1095
- }
1096
- ]
1097
- };
1098
- }
1099
- const blocks = [
1100
- {
1101
- type: "stats",
1102
- items: [
1103
- {
1104
- label: "Citation",
1105
- value: score.citation_readiness_score?.toString() ?? "unknown"
1106
- },
1107
- {
1108
- label: tuning.truthLabel(score),
1109
- value: score.truth_consistency_score?.toString() ?? "unknown"
1110
- },
1111
- {
1112
- label: "Answers",
1113
- value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
1114
- }
1115
- ]
1116
- }
1117
- ];
1118
- if (score.blockers.length > 0) {
1119
- blocks.push({
1120
- type: "context",
1121
- text: topBlockersLine(score)
1122
- });
1123
- }
1124
- return { blocks };
1125
- }
1126
- function topBlockersLine(score) {
1127
- const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
1128
- if (top.length === 0) {
1129
- return "No blockers identified.";
1130
- }
1131
- return `Top blockers: ${top.join(" \u2022 ")}`;
1717
+ async function renderScoreWidget(_kv, _tuning, _log) {
1718
+ return {
1719
+ blocks: [
1720
+ { type: "header", text: "SEO score unavailable" },
1721
+ { 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." }
1722
+ ]
1723
+ };
1132
1724
  }
1133
1725
  async function renderDocumentPanel(kv, route) {
1134
1726
  if (route === void 0) {
@@ -1142,7 +1734,11 @@ async function renderDocumentPanel(kv, route) {
1142
1734
  ]
1143
1735
  };
1144
1736
  }
1145
- const findings = await readFindings(kv, route);
1737
+ const incomplete = (await readAssessment(kv, "*"))?.status === "error";
1738
+ const record = await kv.get(findingsKey(route));
1739
+ const findings = (record?.findings ?? []).filter((finding) => isCmsObservableFinding(finding, record?.body_coverage === "complete") && (!incomplete || !isSiteGraphFinding(finding)));
1740
+ const assessment = record?.assessment;
1741
+ const bodyNotice = record && assessment?.status !== "error" ? cmsBodyCoverageNotice(record.body_coverage) : void 0;
1146
1742
  const errors = findings.filter((finding) => finding.severity === "error");
1147
1743
  const warnings = findings.filter(
1148
1744
  (finding) => finding.severity === "warning"
@@ -1153,8 +1749,14 @@ async function renderDocumentPanel(kv, route) {
1153
1749
  { type: "header", text: `Document SEO \u2014 ${route}` },
1154
1750
  {
1155
1751
  type: "context",
1156
- text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
1752
+ 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.`
1157
1753
  },
1754
+ { 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." },
1755
+ ...bodyNotice ? [{ type: "context", text: bodyNotice }] : [],
1756
+ ...incomplete ? [{ type: "context", text: "Sitewide and cross-document checks are unavailable; refresh after resolving the CMS input error." }] : [],
1757
+ ...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." }] : [],
1758
+ ...assessment?.status === "error" ? [{ type: "context", text: `Evaluation failed: ${assessment.reason}. Findings are unavailable for this revision.` }] : [],
1759
+ ...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) }))] : [],
1158
1760
  ...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
1159
1761
  ]
1160
1762
  };
@@ -1166,16 +1768,13 @@ function documentFindingsTable(findings) {
1166
1768
  { key: "rule", label: "Rule", format: "code" },
1167
1769
  { key: "severity", label: "Severity", format: "badge" },
1168
1770
  { key: "message", label: "Message" },
1169
- { key: "block", label: "Block", format: "code" }
1771
+ { key: "block", label: "Location", format: "code" }
1170
1772
  ],
1171
1773
  rows: findings.map((finding) => ({
1172
1774
  rule: finding.rule_id,
1173
1775
  severity: finding.severity,
1174
1776
  message: finding.message,
1175
- // The bridge stamps every Portable Text block with id + data-pt-key
1176
- // when rendering; surfacing the path:line locator here lets authors
1177
- // locate the failing block via the editor's ⌘-F.
1178
- block: `${finding.path}:${finding.line}`
1777
+ 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}`
1179
1778
  }))
1180
1779
  };
1181
1780
  }
@@ -1342,6 +1941,7 @@ async function handleRefresh2(ctx) {
1342
1941
  context?.collections
1343
1942
  );
1344
1943
  };
1944
+ const contentFields = await readSandboxContentFieldState(ctx.ctx);
1345
1945
  return handleRefresh({
1346
1946
  ctx: ctx.ctx,
1347
1947
  // The sandboxed descriptor takes no collection knobs, so the sweep
@@ -1350,7 +1950,9 @@ async function handleRefresh2(ctx) {
1350
1950
  evaluator: sandboxEvaluator,
1351
1951
  suppressionFilter: sandboxSuppressionFilter,
1352
1952
  blockMappings: await readSandboxBlockMappings(ctx.ctx),
1353
- contentFieldsByCollection: await readSandboxContentFields(ctx.ctx),
1953
+ contentFieldsByCollection: contentFields.fields,
1954
+ contentFieldsUnavailable: contentFields.unavailable,
1955
+ descriptionFieldsByCollection: await readSandboxDescriptionFields(ctx.ctx),
1354
1956
  tuning: adminViewTuning
1355
1957
  });
1356
1958
  }