@aeptus/aexeo-emdash 0.8.18 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +196 -4
  2. package/INSTALL.md +126 -8
  3. package/LICENSE +0 -0
  4. package/README.md +12 -5
  5. package/dist/adapter.d.ts +30 -2
  6. package/dist/adapter.d.ts.map +1 -1
  7. package/dist/admin/Absorbability.d.ts +0 -0
  8. package/dist/admin/Absorbability.d.ts.map +0 -0
  9. package/dist/admin/Accessibility.d.ts +0 -0
  10. package/dist/admin/Accessibility.d.ts.map +0 -0
  11. package/dist/admin/Citability.d.ts +0 -0
  12. package/dist/admin/Citability.d.ts.map +0 -0
  13. package/dist/admin/EntityLegitimacy.d.ts +0 -0
  14. package/dist/admin/EntityLegitimacy.d.ts.map +0 -0
  15. package/dist/admin/EntityPresence.d.ts +0 -0
  16. package/dist/admin/EntityPresence.d.ts.map +0 -0
  17. package/dist/admin/Facts.d.ts +0 -0
  18. package/dist/admin/Facts.d.ts.map +0 -0
  19. package/dist/admin/Findings.d.ts +0 -0
  20. package/dist/admin/Findings.d.ts.map +0 -0
  21. package/dist/admin/PillarView.d.ts +0 -0
  22. package/dist/admin/PillarView.d.ts.map +0 -0
  23. package/dist/admin/Retrievability.d.ts +0 -0
  24. package/dist/admin/Retrievability.d.ts.map +0 -0
  25. package/dist/admin.d.ts +0 -0
  26. package/dist/admin.d.ts.map +1 -1
  27. package/dist/admin.js +2 -2
  28. package/dist/configured.d.ts +6 -0
  29. package/dist/configured.d.ts.map +1 -1
  30. package/dist/configured.js +817 -334
  31. package/dist/content-evaluator.d.ts +14 -0
  32. package/dist/content-evaluator.d.ts.map +1 -0
  33. package/dist/content.d.ts +51 -0
  34. package/dist/content.d.ts.map +1 -0
  35. package/dist/content.js +365 -0
  36. package/dist/data-route.d.ts +3 -0
  37. package/dist/data-route.d.ts.map +1 -1
  38. package/dist/evaluator-sandbox.d.ts +1 -1
  39. package/dist/evaluator-sandbox.d.ts.map +1 -1
  40. package/dist/evaluator.d.ts +1 -1
  41. package/dist/evaluator.d.ts.map +1 -1
  42. package/dist/facts-route.d.ts +0 -0
  43. package/dist/facts-route.d.ts.map +0 -0
  44. package/dist/findings-view.d.ts +141 -0
  45. package/dist/findings-view.d.ts.map +1 -0
  46. package/dist/index.d.ts +15 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +111 -5
  49. package/dist/inventory.d.ts +51 -0
  50. package/dist/inventory.d.ts.map +1 -0
  51. package/dist/plugin.d.ts +51 -4
  52. package/dist/plugin.d.ts.map +1 -1
  53. package/dist/presence-check.d.ts +1 -0
  54. package/dist/presence-check.d.ts.map +1 -1
  55. package/dist/presence-route.d.ts +0 -0
  56. package/dist/presence-route.d.ts.map +0 -0
  57. package/dist/sandbox-entry.d.ts +4 -23
  58. package/dist/sandbox-entry.d.ts.map +1 -1
  59. package/dist/sandbox-entry.js +738 -396
  60. package/dist/sandbox.d.ts +2 -0
  61. package/dist/sandbox.d.ts.map +1 -1
  62. package/dist/sidecar-url.d.ts +18 -0
  63. package/dist/sidecar-url.d.ts.map +1 -0
  64. package/dist/sidecar.d.ts +7 -1
  65. package/dist/sidecar.d.ts.map +1 -1
  66. package/dist/suppressions.d.ts +0 -0
  67. package/dist/suppressions.d.ts.map +0 -0
  68. package/dist/types.d.ts +11 -4
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/version.d.ts +1 -1
  71. package/dist/version.d.ts.map +1 -1
  72. package/dist/wasm-init.d.ts +4 -0
  73. package/dist/wasm-init.d.ts.map +1 -1
  74. package/package.json +21 -8
  75. package/wasm/aexeo_emdash_bridge.d.ts +11 -0
  76. package/wasm/aexeo_emdash_bridge.js +1 -1
  77. package/wasm/aexeo_emdash_bridge_bg.d.ts +11 -0
  78. package/wasm/aexeo_emdash_bridge_bg.js +68 -0
  79. package/wasm/aexeo_emdash_bridge_bg.wasm +0 -0
  80. package/wasm/aexeo_emdash_bridge_bg.wasm.d.ts +2 -0
  81. package/dist/indexnow.d.ts +0 -15
  82. package/dist/indexnow.d.ts.map +0 -1
  83. package/dist/mcp.d.ts +0 -28
  84. package/dist/mcp.d.ts.map +0 -1
@@ -5,13 +5,15 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/adapter.ts
8
- async function adaptContentItemWithContext(content, contentApi, defaultLocale) {
8
+ async function adaptContentItemWithContext(content, contentApi, defaultLocale, blockMappings, contentFields) {
9
9
  const publicUrl = content.status === "published" ? await contentApi.getPublicUrl(content.type, content.id) : null;
10
10
  const route = publicUrl === null ? deriveRoute(content) : routeFromPublicUrl(publicUrl);
11
11
  const adapted = adaptContentItem(content, {
12
12
  route,
13
13
  publicUrl,
14
- ...defaultLocale === void 0 ? {} : { defaultLocale }
14
+ ...defaultLocale === void 0 ? {} : { defaultLocale },
15
+ ...blockMappings === void 0 ? {} : { blockMappings },
16
+ ...contentFields === void 0 ? {} : { contentFields }
15
17
  });
16
18
  const translationSet = await contentApi.getTranslations(
17
19
  content.type,
@@ -53,8 +55,11 @@ function adaptContentItem(content, options = {}) {
53
55
  if (typeof locale === "string" && locale.length > 0) {
54
56
  document.lang = locale;
55
57
  }
56
- const body = extractPortableText(content.data);
57
- if (body !== null) document.body = body;
58
+ const body = options.contentFields === void 0 ? extractPortableText(content.data, options.blockMappings) : extractSelectedContent(content.data, options.contentFields, options.blockMappings);
59
+ if (body !== null) {
60
+ document.body = body.blocks;
61
+ if (body.capabilities !== void 0) document.body_capabilities = body.capabilities;
62
+ }
58
63
  return {
59
64
  document,
60
65
  meta: {
@@ -63,7 +68,8 @@ function adaptContentItem(content, options = {}) {
63
68
  slug: content.slug,
64
69
  status: content.status,
65
70
  title,
66
- publicUrl: options.publicUrl ?? null
71
+ publicUrl: options.publicUrl ?? null,
72
+ ...content.updatedAt === void 0 ? {} : { updatedAt: content.updatedAt }
67
73
  }
68
74
  };
69
75
  }
@@ -82,24 +88,53 @@ function routeFromPublicUrl(publicUrl) {
82
88
  function stringOrEmpty(value) {
83
89
  return typeof value === "string" ? value : "";
84
90
  }
85
- function extractPortableText(data) {
91
+ function extractSelectedContent(data, fields, mappings) {
92
+ if (fields.length === 0 || new Set(fields).size !== fields.length || fields.some((field) => typeof field !== "string" || field.length === 0)) return null;
93
+ const all = ["text", "headings", "paragraphs", "links"];
94
+ let capabilities = all;
95
+ const blocks = [];
96
+ let observed = false;
97
+ for (const field of fields) {
98
+ const value = data[field];
99
+ if (value === void 0 || value === null) continue;
100
+ observed = true;
101
+ const part = extractFirstPortableText({ content: value }, mappings);
102
+ if (part === null) return { blocks, capabilities: [] };
103
+ blocks.push(...part.blocks);
104
+ capabilities = capabilities.filter((capability) => part.capabilities === void 0 || part.capabilities.includes(capability));
105
+ }
106
+ if (!observed) return null;
107
+ return capabilities.length === all.length ? { blocks } : { blocks, capabilities: [...capabilities] };
108
+ }
109
+ function extractPortableText(data, mappings) {
110
+ const result = extractFirstPortableText(data, mappings);
111
+ if (result === null) return null;
112
+ const candidates = Object.entries(data).filter(
113
+ ([field, value]) => isPortableTextArray(value) || isEmdashBlocksArray(value, field) || (field === "body" || field === "content") && typeof value === "string" && value.trim().length > 0
114
+ );
115
+ return candidates.length > 1 ? { blocks: result.blocks, capabilities: [] } : result;
116
+ }
117
+ function extractFirstPortableText(data, mappings) {
118
+ let foundEmptyBody = false;
86
119
  for (const slug of ["body", "content"]) {
87
120
  const value = data[slug];
88
- if (isPortableTextArray(value)) return value;
89
- if (isEmdashBlocksArray(value, slug)) return blocksToPortableText(value);
121
+ if (Array.isArray(value) && value.length === 0 || typeof value === "string" && value.trim().length === 0) foundEmptyBody = true;
122
+ if (isPortableTextArray(value)) return { blocks: value };
123
+ if (isEmdashBlocksArray(value, slug)) return mapCustomBlocks(value, mappings);
90
124
  if (typeof value === "string" && value.trim().length > 0) {
91
- return [portableTextBlock(value.trim(), `${slug}-text`)];
125
+ return { blocks: [portableTextBlock(value.trim(), `${slug}-text`)], capabilities: ["text"] };
92
126
  }
93
127
  }
94
128
  const blocks = data["blocks"];
95
- if (isPortableTextArray(blocks)) return blocks;
96
- if (isEmdashBlocksArray(blocks, "blocks")) return blocksToPortableText(blocks);
129
+ if (Array.isArray(blocks) && blocks.length === 0) foundEmptyBody = true;
130
+ if (isPortableTextArray(blocks)) return { blocks };
131
+ if (isEmdashBlocksArray(blocks, "blocks")) return mapCustomBlocks(blocks, mappings);
97
132
  for (const [field, value] of Object.entries(data)) {
98
133
  if (field === "body" || field === "content" || field === "blocks") continue;
99
- if (isPortableTextArray(value)) return value;
100
- if (isEmdashBlocksArray(value, field)) return blocksToPortableText(value);
134
+ if (isPortableTextArray(value)) return { blocks: value };
135
+ if (isEmdashBlocksArray(value, field)) return mapCustomBlocks(value, mappings);
101
136
  }
102
- return null;
137
+ return foundEmptyBody ? { blocks: [] } : null;
103
138
  }
104
139
  function isPortableTextArray(value) {
105
140
  return Array.isArray(value) && value.length > 0 && value.every(
@@ -114,6 +149,52 @@ function isEmdashBlocksArray(value, field) {
114
149
  (item) => isRecord(item) && typeof item["_type"] === "string" && typeof item["_key"] === "string" && (typeof item["_version"] === "number" || field === "blocks")
115
150
  );
116
151
  }
152
+ function mapCustomBlocks(blocks, mappings) {
153
+ const unavailable = () => ({ blocks: blocksToPortableText(blocks), capabilities: [] });
154
+ const result = [];
155
+ const mapFields = (record, fields, depth) => {
156
+ if (depth > 8 || fields.length === 0) return false;
157
+ for (const entry of fields) {
158
+ if (!entry || typeof entry.field !== "string" || !entry.field || !["heading", "paragraph", "richText", "link", "repeat"].includes(entry.kind)) return false;
159
+ if (entry.kind === "heading" && entry.level !== void 0 && ![1, 2, 3, 4, 5, 6].includes(entry.level)) return false;
160
+ if (entry.kind === "link" && (typeof entry.hrefField !== "string" || !entry.hrefField)) return false;
161
+ if (entry.kind === "repeat" && (!Array.isArray(entry.fields) || entry.fields.length === 0)) return false;
162
+ const value = record[entry.field];
163
+ if ((value === void 0 || value === null || value === "") && entry.optional) {
164
+ if (entry.kind === "link" && record[entry.hrefField] != null && record[entry.hrefField] !== "") return false;
165
+ continue;
166
+ }
167
+ if (entry.kind === "richText") {
168
+ if (!Array.isArray(value) || value.length > 0 && !isPortableTextArray(value)) return false;
169
+ result.push(...value);
170
+ } else if (entry.kind === "heading" || entry.kind === "paragraph") {
171
+ if (typeof value !== "string") return false;
172
+ result.push({ ...portableTextBlock(value, ""), style: entry.kind === "heading" ? `h${entry.level ?? 2}` : "normal" });
173
+ } else if (entry.kind === "link") {
174
+ const href = record[entry.hrefField];
175
+ if (typeof value !== "string" || typeof href !== "string" || !href) return false;
176
+ result.push({
177
+ _type: "block",
178
+ children: [{ _type: "span", text: value, marks: ["link"] }],
179
+ markDefs: [{ _key: "link", _type: "link", href }]
180
+ });
181
+ } else if (entry.kind === "repeat") {
182
+ if (!Array.isArray(value) || !Array.isArray(entry.fields)) return false;
183
+ for (const item of value) {
184
+ if (!isRecord(item) || !mapFields(item, entry.fields, depth + 1)) return false;
185
+ }
186
+ } else {
187
+ return false;
188
+ }
189
+ }
190
+ return true;
191
+ };
192
+ for (const block of blocks) {
193
+ const fields = mappings?.[String(block["_type"])];
194
+ if (!Array.isArray(fields) || !mapFields(block, fields, 0)) return unavailable();
195
+ }
196
+ return { blocks: result };
197
+ }
117
198
  function blocksToPortableText(blocks) {
118
199
  const paragraphs = [];
119
200
  for (const block of blocks) {
@@ -197,27 +278,107 @@ function isRecord(value) {
197
278
  return typeof value === "object" && value !== null && !Array.isArray(value);
198
279
  }
199
280
 
281
+ // src/version.ts
282
+ var PACKAGE_VERSION = "0.9.0";
283
+
200
284
  // src/plugin.ts
201
- async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter) {
285
+ var SETTINGS_TOKEN_KEY = "eval_token";
286
+ var SETTINGS_BLOCK_MAPPINGS_KEY = "block_mappings";
287
+ var SETTINGS_CONTENT_FIELDS_KEY = "content_fields_by_collection";
288
+ var PLUGIN_SETTINGS_SCHEMA = {
289
+ [SETTINGS_CONTENT_FIELDS_KEY]: {
290
+ type: "string",
291
+ label: "Content fields by collection (JSON)",
292
+ description: "Ordered rendered body fields per collection.",
293
+ multiline: true
294
+ },
295
+ [SETTINGS_BLOCK_MAPPINGS_KEY]: {
296
+ type: "string",
297
+ label: "Custom block mappings (JSON)",
298
+ description: "Ordered mappings for the rendered fields of each custom block type.",
299
+ multiline: true
300
+ },
301
+ [SETTINGS_TOKEN_KEY]: {
302
+ type: "secret",
303
+ label: "EVAL_TOKEN",
304
+ description: "Bearer token for your aexeo-crawl-worker Worker. Encrypted at rest by the emdash host."
305
+ }
306
+ };
307
+ function recordContentCoverage(ctx, documents) {
308
+ const counts = { documents: documents.length, complete: 0, partial: 0, unavailable: 0 };
309
+ for (const document of documents) {
310
+ if (document.body === void 0 || document.body_capabilities?.length === 0) counts.unavailable++;
311
+ else if (document.body_capabilities === void 0 || ["text", "headings", "paragraphs", "links"].every(
312
+ (capability) => document.body_capabilities?.includes(capability)
313
+ )) counts.complete++;
314
+ else counts.partial++;
315
+ }
316
+ try {
317
+ ctx.log?.info?.("aexeo.content_coverage", counts);
318
+ } catch {
319
+ }
320
+ }
321
+ async function handleAfterDelete(event, ctx) {
322
+ for (const entry of await ctx.kv.list("document:")) {
323
+ const stored = toStoredDocument(entry.value);
324
+ if (stored !== null && stored.meta.id === event.id && stored.meta.collection === event.collection) {
325
+ await ctx.kv.delete(entry.key);
326
+ await ctx.kv.delete(findingsKey(stored.document.route));
327
+ }
328
+ }
329
+ }
330
+ var SITE_GRAPH_RULE_IDS = /* @__PURE__ */ new Set([
331
+ "LNK001",
332
+ // internal link target missing from the site
333
+ "LNK002",
334
+ // no inbound links
335
+ "LNK003",
336
+ // weak anchor, checked only when the target route exists
337
+ "LNK004",
338
+ // too few inbound links
339
+ "SEO009",
340
+ // hreflang alternate target missing from the site
341
+ "SEO012",
342
+ // hreflang alternate does not link back
343
+ "SEO016",
344
+ // canonical points at another crawlable route
345
+ "SEO017",
346
+ // title and description shared with another route
347
+ "CNT005"
348
+ // visible text duplicated on another route
349
+ ]);
350
+ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilter, blockMappings, contentFields) {
202
351
  const adapted = await adaptContentItemWithContext(
203
352
  event.content,
204
353
  ctx.content,
205
- ctx.site?.locale
354
+ ctx.site?.locale,
355
+ blockMappings,
356
+ contentFields
206
357
  );
207
358
  const document = adapted.document;
359
+ recordContentCoverage(ctx, [document]);
208
360
  const { kv, log } = ctx;
209
361
  const stored = { document, meta: adapted.meta };
210
- await persistDocument(ctx, stored);
211
- const result = await evaluator([document]);
362
+ if (!await persistDocument(ctx, stored)) {
363
+ log?.info?.(`Aexeo afterSave skipped: ${document.route} has a newer save`);
364
+ return;
365
+ }
366
+ const result = await evaluator([document], { collections: [adapted.meta.collection] });
212
367
  if (!result.ok) {
213
368
  log?.error?.(`afterSave evaluator failure (${result.reason})`, {
214
369
  detail: result.detail
215
370
  });
216
371
  return;
217
372
  }
218
- const pageFindings = result.findings.filter(
219
- (finding) => finding.scope === "page"
373
+ const storedGraphFindings = (await readFindings(kv, document.route)).filter(
374
+ (finding) => SITE_GRAPH_RULE_IDS.has(finding.rule_id)
220
375
  );
376
+ const pageFindings = [
377
+ ...result.findings.filter(
378
+ (finding) => finding.scope === "page" && !SITE_GRAPH_RULE_IDS.has(finding.rule_id)
379
+ ),
380
+ ...storedGraphFindings
381
+ ];
221
382
  const filtered = suppressionFilter === void 0 ? pageFindings : suppressionFilter.apply(
222
383
  {
223
384
  route: document.route,
@@ -226,6 +387,10 @@ async function handleAfterSaveConfigured(event, ctx, evaluator, suppressionFilte
226
387
  },
227
388
  pageFindings
228
389
  );
390
+ if (await isSuperseded(kv, document.route, adapted.meta.updatedAt)) {
391
+ log?.info?.(`Aexeo afterSave discarded: ${document.route} changed while evaluating`);
392
+ return;
393
+ }
229
394
  await kv.set(findingsKey(document.route), {
230
395
  route: document.route,
231
396
  findings: filtered
@@ -242,7 +407,9 @@ async function evaluateAndPersistAll(ctx, options) {
242
407
  totalFindings: 0,
243
408
  errors: []
244
409
  };
410
+ const storedAtStart = await readAllStoredDocuments(kv);
245
411
  const documents = [];
412
+ const documentCollections = [];
246
413
  const adaptedByRoute = /* @__PURE__ */ new Map();
247
414
  const documentRoutes = /* @__PURE__ */ new Set();
248
415
  for (const collection of collections) {
@@ -257,9 +424,12 @@ async function evaluateAndPersistAll(ctx, options) {
257
424
  const adapted = await adaptContentItemWithContext(
258
425
  item,
259
426
  ctx.content,
260
- ctx.site?.locale
427
+ ctx.site?.locale,
428
+ options.blockMappings,
429
+ options.contentFieldsByCollection?.[collection]
261
430
  );
262
431
  documents.push(adapted.document);
432
+ documentCollections.push(adapted.meta.collection);
263
433
  documentRoutes.add(adapted.document.route);
264
434
  adaptedByRoute.set(adapted.document.route, {
265
435
  document: adapted.document,
@@ -275,10 +445,14 @@ async function evaluateAndPersistAll(ctx, options) {
275
445
  } while (cursor !== void 0);
276
446
  }
277
447
  summary.documentsScanned = documents.length;
448
+ recordContentCoverage(ctx, documents);
278
449
  for (const stored of adaptedByRoute.values()) {
279
- await kv.set(documentKey(stored.document.route), stored);
450
+ const current = await readStoredDocument(kv, stored.document.route);
451
+ if (!isOlderRevision(stored.meta.updatedAt, current?.meta.updatedAt)) {
452
+ await kv.set(documentKey(stored.document.route), stored);
453
+ }
280
454
  }
281
- const result = await options.evaluator(documents);
455
+ const result = await options.evaluator(documents, { collections: documentCollections });
282
456
  if (!result.ok) {
283
457
  log?.error?.(`aexeo evaluator failure (${result.reason})`, {
284
458
  detail: result.detail
@@ -299,6 +473,9 @@ async function evaluateAndPersistAll(ctx, options) {
299
473
  let totalAfterSuppression = 0;
300
474
  for (const [route, findings] of findingsByRoute) {
301
475
  const stored = adaptedByRoute.get(route);
476
+ if (stored !== void 0 && await isSuperseded(kv, route, stored.meta.updatedAt)) {
477
+ continue;
478
+ }
302
479
  const context = {
303
480
  route,
304
481
  ...stored?.meta.collection === void 0 ? {} : { collection: stored.meta.collection },
@@ -311,29 +488,52 @@ async function evaluateAndPersistAll(ctx, options) {
311
488
  }
312
489
  summary.totalFindings = totalAfterSuppression;
313
490
  if (summary.errors.length === 0) {
314
- await removeStaleDocuments(ctx, collections, documentRoutes);
491
+ await removeStaleDocuments(ctx, collections, documentRoutes, storedAtStart);
315
492
  }
316
493
  return summary;
317
494
  }
495
+ function isOlderRevision(revision, other) {
496
+ if (revision === void 0 || other === void 0) return false;
497
+ return Date.parse(revision) < Date.parse(other);
498
+ }
499
+ async function readStoredDocument(kv, route) {
500
+ return toStoredDocument(await kv.get(documentKey(route)));
501
+ }
502
+ async function isSuperseded(kv, route, revision) {
503
+ const current = await readStoredDocument(kv, route);
504
+ return current === null || isOlderRevision(revision, current.meta.updatedAt);
505
+ }
318
506
  async function persistDocument(ctx, stored) {
319
507
  const route = stored.document.route;
320
- const entries = await ctx.kv.list("document:");
321
- for (const entry of entries) {
508
+ const sameEntry = (await ctx.kv.list("document:")).flatMap((entry) => {
322
509
  const previous = toStoredDocument(entry.value);
323
- if (previous === null || previous.document.route === route || previous.meta.id !== stored.meta.id || previous.meta.collection !== stored.meta.collection) {
324
- continue;
325
- }
326
- await ctx.kv.delete(entry.key);
510
+ return previous !== null && previous.meta.id === stored.meta.id && previous.meta.collection === stored.meta.collection ? [{ key: entry.key, previous }] : [];
511
+ });
512
+ if (sameEntry.some(
513
+ ({ previous }) => isOlderRevision(stored.meta.updatedAt, previous.meta.updatedAt)
514
+ )) {
515
+ return false;
516
+ }
517
+ for (const { key, previous } of sameEntry) {
518
+ if (previous.document.route === route) continue;
519
+ await ctx.kv.delete(key);
327
520
  await ctx.kv.delete(findingsKey(previous.document.route));
328
521
  }
329
522
  await ctx.kv.set(documentKey(route), stored);
523
+ return true;
330
524
  }
331
- async function removeStaleDocuments(ctx, collections, currentRoutes) {
525
+ async function removeStaleDocuments(ctx, collections, currentRoutes, storedAtStart) {
332
526
  const scannedCollections = new Set(collections);
527
+ const revisionAtStart = new Map(
528
+ storedAtStart.map((stored) => [
529
+ stored.document.route,
530
+ stored.meta.updatedAt
531
+ ])
532
+ );
333
533
  const entries = await ctx.kv.list("document:");
334
534
  for (const entry of entries) {
335
535
  const stored = toStoredDocument(entry.value);
336
- if (stored === null || !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route)) {
536
+ if (stored === null || stored.meta.collection !== "" && !scannedCollections.has(stored.meta.collection) || currentRoutes.has(stored.document.route) || !revisionAtStart.has(stored.document.route) || revisionAtStart.get(stored.document.route) !== stored.meta.updatedAt) {
337
537
  continue;
338
538
  }
339
539
  await ctx.kv.delete(entry.key);
@@ -394,6 +594,312 @@ async function readFindings(kv, route) {
394
594
  return stored.findings;
395
595
  }
396
596
 
597
+ // src/findings-view.ts
598
+ function normalizeInteraction(input) {
599
+ if (!input || typeof input !== "object") {
600
+ return { type: "page_load", page: "/findings" };
601
+ }
602
+ const raw = input;
603
+ if (raw.type === "page_load") {
604
+ return {
605
+ type: "page_load",
606
+ page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
607
+ };
608
+ }
609
+ if (raw.type === "block_action") {
610
+ return {
611
+ type: "block_action",
612
+ action_id: typeof raw.action_id === "string" ? raw.action_id : "",
613
+ ...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
614
+ value: raw.value
615
+ };
616
+ }
617
+ if (raw.type === "form_submit") {
618
+ return {
619
+ type: "form_submit",
620
+ action_id: typeof raw.action_id === "string" ? raw.action_id : "",
621
+ ...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
622
+ values: raw.values && typeof raw.values === "object" ? raw.values : {}
623
+ };
624
+ }
625
+ return { type: "page_load", page: "/findings" };
626
+ }
627
+ async function handleRefresh(options) {
628
+ let summary;
629
+ try {
630
+ summary = await evaluateAndPersistAll(options.ctx, {
631
+ collections: options.collections,
632
+ evaluator: options.evaluator,
633
+ blockMappings: options.blockMappings,
634
+ contentFieldsByCollection: options.contentFieldsByCollection,
635
+ // Always forwarded. See RefreshOptions.suppressionFilter.
636
+ suppressionFilter: options.suppressionFilter
637
+ });
638
+ } catch (err) {
639
+ const detail = err instanceof Error ? err.message : String(err);
640
+ const message = `Refresh failed: ${detail}`;
641
+ return {
642
+ blocks: [
643
+ { type: "header", text: "SEO findings" },
644
+ {
645
+ type: "banner",
646
+ title: message,
647
+ variant: options.tuning.refreshFailureVariant
648
+ }
649
+ ],
650
+ toast: { message, type: "error" }
651
+ };
652
+ }
653
+ const refreshed = await renderFindingsPage(options.ctx.kv, options.tuning);
654
+ const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
655
+ if (summary.errors.length > 0) {
656
+ refreshed.blocks.unshift({
657
+ type: "banner",
658
+ title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
659
+ variant: "alert"
660
+ });
661
+ }
662
+ return {
663
+ ...refreshed,
664
+ toast: {
665
+ message: toastMessage,
666
+ type: summary.errors.length === 0 ? "success" : "info"
667
+ }
668
+ };
669
+ }
670
+ async function readAllFindings(kv) {
671
+ const entries = await kv.list(
672
+ "findings:"
673
+ );
674
+ const out = [];
675
+ for (const entry of entries) {
676
+ if (entry.value === null) {
677
+ continue;
678
+ }
679
+ const route = entry.key.replace(/^findings:/, "");
680
+ for (const finding of entry.value.findings) {
681
+ out.push({ ...finding, document_route: route });
682
+ }
683
+ }
684
+ return out;
685
+ }
686
+ function severityFirst(a, b) {
687
+ const rank = (severity) => severity === "error" ? 0 : 1;
688
+ const diff = rank(a.severity) - rank(b.severity);
689
+ if (diff !== 0) {
690
+ return diff;
691
+ }
692
+ return a.rule_id.localeCompare(b.rule_id);
693
+ }
694
+ function uniqueRoutes(rows) {
695
+ const routes = /* @__PURE__ */ new Set();
696
+ for (const row of rows) {
697
+ routes.add(row.document_route);
698
+ }
699
+ return [...routes].sort();
700
+ }
701
+ async function renderFindingsPage(kv, tuning) {
702
+ const findings = await readAllFindings(kv);
703
+ const errors = findings.filter((finding) => finding.severity === "error");
704
+ const warnings = findings.filter(
705
+ (finding) => finding.severity === "warning"
706
+ );
707
+ const sorted = [...findings].sort(severityFirst);
708
+ const routes = uniqueRoutes(findings);
709
+ const blocks = [
710
+ { type: "header", text: "SEO findings" },
711
+ {
712
+ type: "context",
713
+ text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
714
+ },
715
+ { type: "divider" },
716
+ {
717
+ type: "actions",
718
+ elements: [
719
+ // Refresh evaluates every configured entry. Pressing it lists
720
+ // content via the live in-request bridge, evaluates it, and
721
+ // writes findings back to KV (with suppressions applied).
722
+ {
723
+ type: "button",
724
+ label: "Refresh",
725
+ action_id: "refresh_findings",
726
+ style: "primary"
727
+ },
728
+ { type: "button", label: "All", action_id: "filter:all" },
729
+ { type: "button", label: "Errors only", action_id: "filter:errors" },
730
+ {
731
+ type: "button",
732
+ label: "Warnings only",
733
+ action_id: "filter:warnings"
734
+ }
735
+ ]
736
+ },
737
+ sorted.length === 0 ? {
738
+ type: "context",
739
+ text: tuning.emptyFindingsText
740
+ } : findingsTable(sorted)
741
+ ];
742
+ if (routes.length > 0) {
743
+ blocks.push({
744
+ type: "form",
745
+ fields: [
746
+ {
747
+ type: "select",
748
+ action_id: "route_picker",
749
+ label: "Document to inspect",
750
+ options: routes.map((route) => ({ label: route, value: route }))
751
+ }
752
+ ],
753
+ submit: { label: "View document SEO", action_id: "view_document" }
754
+ });
755
+ }
756
+ return { blocks };
757
+ }
758
+ function findingsTable(rows) {
759
+ return {
760
+ type: "table",
761
+ columns: [
762
+ { key: "route", label: "Route" },
763
+ { key: "rule", label: "Rule", format: "code" },
764
+ { key: "severity", label: "Severity", format: "badge" },
765
+ { key: "message", label: "Message" }
766
+ ],
767
+ rows: rows.map((row) => ({
768
+ route: row.document_route,
769
+ rule: row.rule_id,
770
+ severity: row.severity,
771
+ message: row.message
772
+ }))
773
+ };
774
+ }
775
+ async function renderScoreWidget(kv, tuning, log) {
776
+ const documents = await readAllDocuments(kv);
777
+ if (documents.length === 0) {
778
+ return {
779
+ blocks: [
780
+ { type: "header", text: "SEO score" },
781
+ {
782
+ type: "context",
783
+ text: tuning.emptyScoreText
784
+ }
785
+ ]
786
+ };
787
+ }
788
+ let score;
789
+ try {
790
+ score = await tuning.score(documents, kv);
791
+ } catch (error) {
792
+ log?.error("Aexeo score widget failed to initialize", { error });
793
+ return {
794
+ blocks: [
795
+ { type: "banner", title: "SEO score is temporarily unavailable", variant: "alert" },
796
+ { type: "context", text: "The authoring interface remains available. Retry after the evaluator runtime has been checked." }
797
+ ]
798
+ };
799
+ }
800
+ if (score === null) {
801
+ return {
802
+ blocks: [
803
+ { type: "header", text: "SEO score" },
804
+ {
805
+ type: "context",
806
+ text: "The site score is unavailable in sandboxed mode. Findings are not affected."
807
+ }
808
+ ]
809
+ };
810
+ }
811
+ const blocks = [
812
+ {
813
+ type: "stats",
814
+ items: [
815
+ {
816
+ label: "Citation",
817
+ value: score.citation_readiness_score?.toString() ?? "unknown"
818
+ },
819
+ {
820
+ label: tuning.truthLabel(score),
821
+ value: score.truth_consistency_score?.toString() ?? "unknown"
822
+ },
823
+ {
824
+ label: "Answers",
825
+ value: `${score.answer_pack.routes_with_answer_blocks}/${score.answer_pack.routes_analyzed} routes`
826
+ }
827
+ ]
828
+ }
829
+ ];
830
+ if (score.blockers.length > 0) {
831
+ blocks.push({
832
+ type: "context",
833
+ text: topBlockersLine(score)
834
+ });
835
+ }
836
+ return { blocks };
837
+ }
838
+ function topBlockersLine(score) {
839
+ const top = score.blockers.slice(0, 3).map((blocker) => blocker.message);
840
+ if (top.length === 0) {
841
+ return "No blockers identified.";
842
+ }
843
+ return `Top blockers: ${top.join(" \u2022 ")}`;
844
+ }
845
+ async function renderDocumentPanel(kv, route) {
846
+ if (route === void 0) {
847
+ return {
848
+ blocks: [
849
+ { type: "header", text: "Document SEO" },
850
+ {
851
+ type: "context",
852
+ text: "Pick a document from the SEO findings list to see its rule findings here."
853
+ }
854
+ ]
855
+ };
856
+ }
857
+ const findings = await readFindings(kv, route);
858
+ const errors = findings.filter((finding) => finding.severity === "error");
859
+ const warnings = findings.filter(
860
+ (finding) => finding.severity === "warning"
861
+ );
862
+ const sorted = [...findings].sort(severityFirst);
863
+ return {
864
+ blocks: [
865
+ { type: "header", text: `Document SEO \u2014 ${route}` },
866
+ {
867
+ type: "context",
868
+ text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
869
+ },
870
+ ...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
871
+ ]
872
+ };
873
+ }
874
+ function documentFindingsTable(findings) {
875
+ return {
876
+ type: "table",
877
+ columns: [
878
+ { key: "rule", label: "Rule", format: "code" },
879
+ { key: "severity", label: "Severity", format: "badge" },
880
+ { key: "message", label: "Message" },
881
+ { key: "block", label: "Block", format: "code" }
882
+ ],
883
+ rows: findings.map((finding) => ({
884
+ rule: finding.rule_id,
885
+ severity: finding.severity,
886
+ message: finding.message,
887
+ // The bridge stamps every Portable Text block with id + data-pt-key
888
+ // when rendering; surfacing the path:line locator here lets authors
889
+ // locate the failing block via the editor's ⌘-F.
890
+ block: `${finding.path}:${finding.line}`
891
+ }))
892
+ };
893
+ }
894
+ function notFound(page) {
895
+ return {
896
+ blocks: [
897
+ { type: "header", text: "Not found" },
898
+ { type: "context", text: `unknown page: ${page}` }
899
+ ]
900
+ };
901
+ }
902
+
397
903
  // wasm/aexeo_emdash_bridge_bg.js
398
904
  var aexeo_emdash_bridge_bg_exports = {};
399
905
  __export(aexeo_emdash_bridge_bg_exports, {
@@ -403,11 +909,65 @@ __export(aexeo_emdash_bridge_bg_exports, {
403
909
  __wbg_set_wasm: () => __wbg_set_wasm,
404
910
  __wbg_stack_3b0d974bbf31e44f: () => __wbg_stack_3b0d974bbf31e44f,
405
911
  __wbindgen_init_externref_table: () => __wbindgen_init_externref_table,
912
+ evaluateContent: () => evaluateContent,
913
+ evaluateContentCollection: () => evaluateContentCollection,
406
914
  evaluateDocuments: () => evaluateDocuments,
407
915
  generateFactsPrompt: () => generateFactsPrompt,
408
916
  scoreIntelligence: () => scoreIntelligence,
409
917
  validateFactsManifest: () => validateFactsManifest
410
918
  });
919
+ function evaluateContent(document_json, preset_name, sources_json, evidence_json) {
920
+ let deferred6_0;
921
+ let deferred6_1;
922
+ try {
923
+ const ptr0 = passStringToWasm0(document_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
924
+ const len0 = WASM_VECTOR_LEN;
925
+ const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
926
+ const len1 = WASM_VECTOR_LEN;
927
+ var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
928
+ var len2 = WASM_VECTOR_LEN;
929
+ var ptr3 = isLikeNone(evidence_json) ? 0 : passStringToWasm0(evidence_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
930
+ var len3 = WASM_VECTOR_LEN;
931
+ const ret = wasm.evaluateContent(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
932
+ var ptr5 = ret[0];
933
+ var len5 = ret[1];
934
+ if (ret[3]) {
935
+ ptr5 = 0;
936
+ len5 = 0;
937
+ throw takeFromExternrefTable0(ret[2]);
938
+ }
939
+ deferred6_0 = ptr5;
940
+ deferred6_1 = len5;
941
+ return getStringFromWasm0(ptr5, len5);
942
+ } finally {
943
+ wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
944
+ }
945
+ }
946
+ function evaluateContentCollection(documents_json, preset_name, sources_json) {
947
+ let deferred5_0;
948
+ let deferred5_1;
949
+ try {
950
+ const ptr0 = passStringToWasm0(documents_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
951
+ const len0 = WASM_VECTOR_LEN;
952
+ const ptr1 = passStringToWasm0(preset_name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
953
+ const len1 = WASM_VECTOR_LEN;
954
+ var ptr2 = isLikeNone(sources_json) ? 0 : passStringToWasm0(sources_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
955
+ var len2 = WASM_VECTOR_LEN;
956
+ const ret = wasm.evaluateContentCollection(ptr0, len0, ptr1, len1, ptr2, len2);
957
+ var ptr4 = ret[0];
958
+ var len4 = ret[1];
959
+ if (ret[3]) {
960
+ ptr4 = 0;
961
+ len4 = 0;
962
+ throw takeFromExternrefTable0(ret[2]);
963
+ }
964
+ deferred5_0 = ptr4;
965
+ deferred5_1 = len4;
966
+ return getStringFromWasm0(ptr4, len4);
967
+ } finally {
968
+ wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
969
+ }
970
+ }
411
971
  function evaluateDocuments(documents_json, config_json) {
412
972
  let deferred4_0;
413
973
  let deferred4_1;
@@ -677,6 +1237,10 @@ async function validateFactsManifest2(manifestJson, documentsJson) {
677
1237
  await ensureInitialized();
678
1238
  return validateFactsManifest(manifestJson, documentsJson);
679
1239
  }
1240
+ async function evaluateContent2(documentJson, presetName, sourcesJson, evidenceJson) {
1241
+ await ensureInitialized();
1242
+ return evaluateContent(documentJson, presetName, sourcesJson, evidenceJson);
1243
+ }
680
1244
 
681
1245
  // src/data-route.ts
682
1246
  async function handleDataRoute(ctx, options) {
@@ -685,6 +1249,8 @@ async function handleDataRoute(ctx, options) {
685
1249
  summary = await evaluateAndPersistAll(ctx, {
686
1250
  collections: options.collections,
687
1251
  evaluator: options.evaluator,
1252
+ blockMappings: options.blockMappings,
1253
+ contentFieldsByCollection: options.contentFieldsByCollection,
688
1254
  ...options.suppressionFilter === void 0 ? {} : { suppressionFilter: options.suppressionFilter }
689
1255
  });
690
1256
  await persistManifestStateFindings(ctx);
@@ -953,12 +1519,16 @@ async function handleSave(ctx, body) {
953
1519
  return { saved: true };
954
1520
  }
955
1521
 
956
- // src/presence-route.ts
957
- import { PluginRouteError as PluginRouteError2 } from "emdash";
958
-
959
1522
  // src/presence-check.ts
960
1523
  var FETCH_TIMEOUT_MS = 5e3;
961
1524
  var USER_AGENT = "aexeo-emdash-plugin/0.8 (+https://github.com/schiste/Aexeo; entity-presence diagnostic)";
1525
+ var PRESENCE_HOSTS = [
1526
+ "en.wikipedia.org",
1527
+ "www.wikidata.org",
1528
+ "api.github.com",
1529
+ "rdap.org",
1530
+ "index.commoncrawl.org"
1531
+ ];
962
1532
  var SOURCE_NAMES = [
963
1533
  "wikipedia",
964
1534
  "wikidata",
@@ -1012,11 +1582,11 @@ async function checkWikipedia(input, fetcher) {
1012
1582
  const descriptions = body[2];
1013
1583
  const urls = body[3];
1014
1584
  if (!Array.isArray(titles) || !Array.isArray(urls) || titles.length === 0) {
1015
- return notFound("wikipedia");
1585
+ return notFound2("wikipedia");
1016
1586
  }
1017
1587
  const title = String(titles[0]);
1018
- if (!fuzzyMatch(title, input.name)) {
1019
- return notFound("wikipedia");
1588
+ if (!namesEntity(title, input)) {
1589
+ return notFound2("wikipedia");
1020
1590
  }
1021
1591
  const articleUrl = String(urls[0] ?? "");
1022
1592
  const description = Array.isArray(descriptions) ? String(descriptions[0] ?? "") : "";
@@ -1042,11 +1612,11 @@ async function checkWikidata(input, fetcher) {
1042
1612
  const body = await res.json();
1043
1613
  const top = body.search?.[0];
1044
1614
  if (!top || typeof top.id !== "string") {
1045
- return notFound("wikidata");
1615
+ return notFound2("wikidata");
1046
1616
  }
1047
1617
  const label = top.label ?? input.name;
1048
- if (!fuzzyMatch(label, input.name)) {
1049
- return notFound("wikidata");
1618
+ if (!namesEntity(label, input)) {
1619
+ return notFound2("wikidata");
1050
1620
  }
1051
1621
  return found("wikidata", {
1052
1622
  label: `${top.id}${top.label ? ` \u2014 ${top.label}` : ""}`,
@@ -1072,7 +1642,7 @@ async function checkGitHub(input, fetcher) {
1072
1642
  // it the API can return either v3 or experimental shapes.
1073
1643
  headers: { Accept: "application/vnd.github+json" }
1074
1644
  });
1075
- if (res.status === 404) return notFound("github");
1645
+ if (res.status === 404) return notFound2("github");
1076
1646
  if (res.status === 403 || res.status === 429) {
1077
1647
  return unreachable("github", "rate-limited (try Refresh later)");
1078
1648
  }
@@ -1104,7 +1674,7 @@ async function checkRdap(input, fetcher) {
1104
1674
  const url = `https://rdap.org/domain/${encodeURIComponent(host)}`;
1105
1675
  try {
1106
1676
  const res = await timedFetch(fetcher, url);
1107
- if (res.status === 404) return notFound("rdap");
1677
+ if (res.status === 404) return notFound2("rdap");
1108
1678
  if (!res.ok) return unreachable("rdap", `HTTP ${res.status}`);
1109
1679
  const body = await res.json();
1110
1680
  const reg = (body.events ?? []).find(
@@ -1134,20 +1704,38 @@ async function checkCommonCrawl(input, fetcher) {
1134
1704
  "no website URL in manifest, can't query crawl index"
1135
1705
  );
1136
1706
  }
1137
- const index = "CC-MAIN-2026-15";
1707
+ let index;
1708
+ try {
1709
+ const res = await timedFetch(
1710
+ fetcher,
1711
+ "https://index.commoncrawl.org/collinfo.json"
1712
+ );
1713
+ if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
1714
+ const newest = newestCrawlId(await res.json());
1715
+ if (newest === null) {
1716
+ return unreachable(
1717
+ "common_crawl",
1718
+ "no crawl index listed in collinfo.json"
1719
+ );
1720
+ }
1721
+ index = newest;
1722
+ } catch (error) {
1723
+ return unreachable("common_crawl", errMsg(error));
1724
+ }
1138
1725
  const url = new URL(`https://index.commoncrawl.org/${index}-index`);
1139
1726
  url.searchParams.set("url", host);
1727
+ url.searchParams.set("matchType", "domain");
1140
1728
  url.searchParams.set("output", "json");
1141
1729
  url.searchParams.set("limit", "1");
1142
1730
  try {
1143
1731
  const res = await timedFetch(fetcher, url.toString());
1144
- if (res.status === 404) return notFound("common_crawl");
1732
+ if (res.status === 404) return notFound2("common_crawl");
1145
1733
  if (!res.ok) return unreachable("common_crawl", `HTTP ${res.status}`);
1146
1734
  const text = (await res.text()).trim();
1147
- if (text.length === 0) return notFound("common_crawl");
1735
+ if (text.length === 0) return notFound2("common_crawl");
1148
1736
  let firstLine = text.split("\n")[0];
1149
1737
  if (firstLine === void 0 || firstLine.length === 0) {
1150
- return notFound("common_crawl");
1738
+ return notFound2("common_crawl");
1151
1739
  }
1152
1740
  let entry;
1153
1741
  try {
@@ -1163,6 +1751,16 @@ async function checkCommonCrawl(input, fetcher) {
1163
1751
  return unreachable("common_crawl", errMsg(error));
1164
1752
  }
1165
1753
  }
1754
+ function newestCrawlId(collinfo) {
1755
+ if (!Array.isArray(collinfo)) return null;
1756
+ let newest = null;
1757
+ for (const crawl of collinfo) {
1758
+ const id = crawl?.id;
1759
+ if (typeof id !== "string" || !/^CC-MAIN-[A-Za-z0-9-]+$/.test(id)) continue;
1760
+ if (newest === null || id > newest) newest = id;
1761
+ }
1762
+ return newest;
1763
+ }
1166
1764
  async function timedFetch(fetcher, url, init) {
1167
1765
  const controller = new AbortController();
1168
1766
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
@@ -1189,7 +1787,7 @@ function found(source, details) {
1189
1787
  if (details.extra !== void 0) result.extra = details.extra;
1190
1788
  return result;
1191
1789
  }
1192
- function notFound(source) {
1790
+ function notFound2(source) {
1193
1791
  return { source, status: "not_found", checkedAt: nowIso() };
1194
1792
  }
1195
1793
  function unreachable(source, error) {
@@ -1208,10 +1806,45 @@ function errMsg(error) {
1208
1806
  }
1209
1807
  return String(error);
1210
1808
  }
1211
- function fuzzyMatch(a, b) {
1212
- const aL = a.toLowerCase().trim();
1213
- const bL = b.toLowerCase().trim();
1214
- return aL.includes(bL) || bL.includes(aL);
1809
+ function namesEntity(candidate, input) {
1810
+ const normalized = normalizeEntityName(candidate);
1811
+ return [input.name, ...input.aliases ?? []].some(
1812
+ (name) => normalizeEntityName(name) === normalized
1813
+ );
1814
+ }
1815
+ var LEGAL_FORMS = /* @__PURE__ */ new Set([
1816
+ "ab",
1817
+ "ag",
1818
+ "bv",
1819
+ "co",
1820
+ "company",
1821
+ "corp",
1822
+ "corporation",
1823
+ "gmbh",
1824
+ "inc",
1825
+ "incorporated",
1826
+ "kk",
1827
+ "limited",
1828
+ "llc",
1829
+ "llp",
1830
+ "lp",
1831
+ "ltd",
1832
+ "nv",
1833
+ "oy",
1834
+ "plc",
1835
+ "pty",
1836
+ "sa",
1837
+ "sarl",
1838
+ "sas",
1839
+ "srl"
1840
+ ]);
1841
+ function normalizeEntityName(name) {
1842
+ const words = name.replace(/\([^)]*\)/g, " ").toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((word) => word.length > 0);
1843
+ let end = words.length;
1844
+ while (end > 0 && LEGAL_FORMS.has(words[end - 1])) end -= 1;
1845
+ const start = words[0] === "the" ? 1 : 0;
1846
+ const core = words.slice(start, end);
1847
+ return (core.length > 0 ? core : words).join(" ");
1215
1848
  }
1216
1849
  function sanitizeGitHubHandle(name) {
1217
1850
  const handle = name.replace(/[\s_]+/g, "-");
@@ -1250,6 +1883,7 @@ function formatCdxTimestamp(ts) {
1250
1883
  }
1251
1884
 
1252
1885
  // src/presence-route.ts
1886
+ import { PluginRouteError as PluginRouteError2 } from "emdash";
1253
1887
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
1254
1888
  var PRESENCE_KEY = "presence:current";
1255
1889
  async function handlePresenceRoute(ctx) {
@@ -1258,7 +1892,7 @@ async function handlePresenceRoute(ctx) {
1258
1892
  case "data":
1259
1893
  return await handleData2(ctx);
1260
1894
  case "refresh":
1261
- return await handleRefresh(ctx);
1895
+ return await handleRefresh2(ctx);
1262
1896
  default:
1263
1897
  throw PluginRouteError2.badRequest(
1264
1898
  `unknown presence route kind: ${String(body.kind)}`
@@ -1285,7 +1919,7 @@ async function handleData2(ctx) {
1285
1919
  const state = ageMs !== null && ageMs > CACHE_TTL_MS ? "stale" : "fresh";
1286
1920
  return payload(state, cached.results, cached.generatedAt, entity.name);
1287
1921
  }
1288
- async function handleRefresh(ctx) {
1922
+ async function handleRefresh2(ctx) {
1289
1923
  const manifest = await readStoredFacts(ctx.kv);
1290
1924
  if (manifest === null) {
1291
1925
  return payload("no_manifest", [], null, null);
@@ -1372,8 +2006,61 @@ function globToMatcher(pattern) {
1372
2006
  return (route) => re.test(route);
1373
2007
  }
1374
2008
 
1375
- // src/version.ts
1376
- var PACKAGE_VERSION = "0.8.18";
2009
+ // src/content.ts
2010
+ async function evaluateContent3(document, preset, sources = {}, evidenceSources, onTelemetry) {
2011
+ const started = onTelemetry ? performance.now() : 0;
2012
+ const documentJson = JSON.stringify(document);
2013
+ const sourcesJson = JSON.stringify(sources);
2014
+ const result = evidenceSources === void 0 ? await evaluateContent2(documentJson, preset, sourcesJson) : await evaluateContent2(documentJson, preset, sourcesJson, JSON.stringify(evidenceSources));
2015
+ const report = JSON.parse(result);
2016
+ if (onTelemetry) {
2017
+ const event = { evaluation_ms: performance.now() - started, source_locations_mapped: report.source_locations?.length ?? 0 };
2018
+ for (const gate of report.gates) {
2019
+ const checked = gate.measurements?.checked_destinations;
2020
+ const invalid = gate.measurements?.invalid_destinations;
2021
+ if (checked !== void 0 && invalid !== void 0) {
2022
+ event.link_destinations ??= { checked: 0, invalid: 0 };
2023
+ event.link_destinations.checked += checked;
2024
+ event.link_destinations.invalid += invalid;
2025
+ }
2026
+ }
2027
+ onTelemetry(event);
2028
+ }
2029
+ return report;
2030
+ }
2031
+
2032
+ // src/content-evaluator.ts
2033
+ function withContentPreset(base, preset, evaluate) {
2034
+ if (!preset) return base;
2035
+ return async (documents, context) => {
2036
+ const result = await base(documents, context);
2037
+ if (!result.ok) return result;
2038
+ const findings = [...result.findings];
2039
+ try {
2040
+ for (const [index, document] of documents.entries()) {
2041
+ const collection = context?.collections[index];
2042
+ if (preset.collections !== void 0) {
2043
+ if (collection === void 0) throw new Error("collection is required for a scoped content preset");
2044
+ if (!preset.collections.includes(collection)) continue;
2045
+ }
2046
+ const selected = collection !== void 0 && preset.byCollection !== void 0 && Object.hasOwn(preset.byCollection, collection) ? preset.byCollection[collection] : void 0;
2047
+ const name = selected ?? preset.name;
2048
+ if (!name) throw new Error(`empty preset name for collection ${collection}`);
2049
+ const report = preset.evidenceSources === void 0 ? await evaluate(document, name, preset.sources) : await evaluate(document, name, preset.sources, preset.evidenceSources);
2050
+ const incomplete = report.gates.filter((gate) => gate.status === "not_checked");
2051
+ if (incomplete.length) return {
2052
+ ok: false,
2053
+ reason: "content_preset_incomplete",
2054
+ detail: `${document.route}: ${incomplete.map((gate) => `${gate.gate_id}: ${gate.reason}`).join(", ")}`
2055
+ };
2056
+ for (const gate of report.gates) findings.push(...gate.findings);
2057
+ }
2058
+ return { ok: true, findings };
2059
+ } catch (error) {
2060
+ return { ok: false, reason: "content_preset_error", detail: error instanceof Error ? error.message : String(error) };
2061
+ }
2062
+ };
2063
+ }
1377
2064
 
1378
2065
  // src/configured.ts
1379
2066
  import { definePlugin } from "emdash";
@@ -1439,10 +2126,15 @@ function createPlugin(options = {}) {
1439
2126
  options.excludeCollections
1440
2127
  );
1441
2128
  const suppressionFilter = compileSuppressions(options.suppressions);
2129
+ const evaluator = withContentPreset(inProcessEvaluator, options.contentPreset, evaluateContent3);
1442
2130
  return definePlugin({
1443
2131
  id: "aexeo-emdash",
1444
2132
  version: PACKAGE_VERSION,
1445
- capabilities: ["content:read"],
2133
+ // The entity-presence route fetches its public sources directly (see
2134
+ // presence-check.ts). rdap.org then redirects to the registry for the
2135
+ // domain's TLD; those registries cannot be listed here.
2136
+ capabilities: ["content:read", "network:request"],
2137
+ allowedHosts: [...PRESENCE_HOSTS],
1446
2138
  hooks: {
1447
2139
  // afterSave processes one saved document at a time — collections
1448
2140
  // list isn't needed here. The hook always runs regardless of
@@ -1450,16 +2142,26 @@ function createPlugin(options = {}) {
1450
2142
  "content:afterSave": (event, ctx) => handleAfterSaveConfigured(
1451
2143
  event,
1452
2144
  ctx,
1453
- inProcessEvaluator,
1454
- suppressionFilter
1455
- )
2145
+ evaluator,
2146
+ suppressionFilter,
2147
+ options.blockMappings,
2148
+ options.contentFieldsByCollection?.[event.content.type]
2149
+ ),
2150
+ "content:afterDelete": (event, ctx) => handleAfterDelete(event, ctx)
1456
2151
  },
2152
+ // emdash requires plugins:manage (admin) on a route that declares no
2153
+ // permission. Reading findings only needs plugins:read (editor); anything
2154
+ // that writes stays admin, stated explicitly so the split is visible.
1457
2155
  routes: {
1458
2156
  // The Refresh sweep (handleAdminRoute → handleRefresh) is the
1459
2157
  // one consumer of `collections`. Bind the resolved list here so
1460
2158
  // every request handles the same set without re-reading options.
2159
+ //
2160
+ // This route serves the pages and the dashboard widget, and also the
2161
+ // refresh_findings action, which handleAdminRoute gates itself.
1461
2162
  admin: {
1462
- handler: (ctx) => handleAdminRoute(ctx, collections)
2163
+ permission: "plugins:read",
2164
+ handler: (ctx) => handleAdminRoute(ctx, collections, suppressionFilter, evaluator, options.blockMappings, options.contentFieldsByCollection)
1463
2165
  },
1464
2166
  // JSON data endpoint for the React adminEntry. Two routes:
1465
2167
  // POST /_emdash/api/plugins/aexeo-emdash/data — read current findings
@@ -1469,19 +2171,25 @@ function createPlugin(options = {}) {
1469
2171
  // emdash edit page and the public site, which Block Kit can't
1470
2172
  // express in any element type as of emdash 0.8.0.
1471
2173
  data: {
2174
+ permission: "plugins:read",
1472
2175
  handler: async (ctx) => (await handleDataRoute(ctx, {
1473
2176
  collections,
1474
- evaluator: inProcessEvaluator,
2177
+ evaluator,
1475
2178
  refresh: false,
1476
- suppressionFilter
2179
+ suppressionFilter,
2180
+ blockMappings: options.blockMappings,
2181
+ contentFieldsByCollection: options.contentFieldsByCollection
1477
2182
  })).payload
1478
2183
  },
1479
2184
  refresh: {
2185
+ permission: "plugins:manage",
1480
2186
  handler: (ctx) => handleDataRoute(ctx, {
1481
2187
  collections,
1482
- evaluator: inProcessEvaluator,
2188
+ evaluator,
1483
2189
  refresh: true,
1484
- suppressionFilter
2190
+ suppressionFilter,
2191
+ blockMappings: options.blockMappings,
2192
+ contentFieldsByCollection: options.contentFieldsByCollection
1485
2193
  })
1486
2194
  },
1487
2195
  // /facts route serves the truth-manifest authoring UI. It
@@ -1489,6 +2197,7 @@ function createPlugin(options = {}) {
1489
2197
  // onto one POST endpoint via a "kind" body field — see
1490
2198
  // facts-route.ts for the full contract.
1491
2199
  facts: {
2200
+ permission: "plugins:manage",
1492
2201
  handler: (ctx) => handleFactsRoute(ctx)
1493
2202
  },
1494
2203
  // /presence route serves the layer-4 entity-presence
@@ -1498,6 +2207,7 @@ function createPlugin(options = {}) {
1498
2207
  // results in KV for 24h. Multiplexes data / refresh kinds —
1499
2208
  // see presence-route.ts.
1500
2209
  presence: {
2210
+ permission: "plugins:manage",
1501
2211
  handler: (ctx) => handlePresenceRoute(ctx)
1502
2212
  }
1503
2213
  },
@@ -1529,246 +2239,76 @@ function createPlugin(options = {}) {
1529
2239
  });
1530
2240
  }
1531
2241
  var configured_default = createPlugin;
1532
- function normalizeInteraction(input) {
1533
- if (!input || typeof input !== "object") {
1534
- return { type: "page_load", page: "/findings" };
1535
- }
1536
- const raw = input;
1537
- if (raw.type === "page_load") {
1538
- return {
1539
- type: "page_load",
1540
- page: typeof raw.page === "string" && raw.page.length > 0 ? raw.page : "/findings"
1541
- };
1542
- }
1543
- if (raw.type === "block_action") {
1544
- return {
1545
- type: "block_action",
1546
- action_id: typeof raw.action_id === "string" ? raw.action_id : "",
1547
- ...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
1548
- value: raw.value
1549
- };
1550
- }
1551
- if (raw.type === "form_submit") {
1552
- return {
1553
- type: "form_submit",
1554
- action_id: typeof raw.action_id === "string" ? raw.action_id : "",
1555
- ...typeof raw.block_id === "string" ? { block_id: raw.block_id } : {},
1556
- values: raw.values && typeof raw.values === "object" ? raw.values : {}
1557
- };
1558
- }
1559
- return { type: "page_load", page: "/findings" };
1560
- }
1561
- async function handleAdminRoute(ctx, collections) {
2242
+ var ADMIN_ROLE = 50;
2243
+ var adminViewTuning = {
2244
+ emptyFindingsText: "Once a Refresh runs, findings will list here.",
2245
+ emptyScoreText: "No documents indexed \u2014 click Refresh on the Aexeo findings page.",
2246
+ refreshFailureVariant: "error",
2247
+ score: async (documents, kv) => scoreLocally(documents, await readStoredFacts(kv)),
2248
+ truthLabel: formatTruthLabel
2249
+ };
2250
+ async function handleAdminRoute(ctx, collections, suppressionFilter, evaluator, blockMappings, contentFieldsByCollection) {
1562
2251
  const body = normalizeInteraction(ctx.input);
1563
2252
  if (body.type === "page_load") {
1564
2253
  return handlePageLoad(ctx, body.page);
1565
2254
  }
1566
2255
  if (body.type === "block_action") {
1567
2256
  if (body.action_id === "view_document" && typeof body.value === "string") {
1568
- return renderDocumentPanel(ctx, body.value);
2257
+ return renderDocumentPanel(ctx.kv, body.value);
1569
2258
  }
1570
2259
  if (body.action_id === "refresh_findings") {
1571
- return handleRefresh2(ctx, collections);
2260
+ if ((ctx.user?.role ?? 0) < ADMIN_ROLE) {
2261
+ const page = await renderFindingsPage(ctx.kv, adminViewTuning);
2262
+ return {
2263
+ ...page,
2264
+ blocks: [
2265
+ {
2266
+ type: "banner",
2267
+ title: "Only administrators can run Refresh.",
2268
+ variant: "error"
2269
+ },
2270
+ ...page.blocks
2271
+ ]
2272
+ };
2273
+ }
2274
+ return handleRefresh({
2275
+ ctx,
2276
+ collections,
2277
+ evaluator,
2278
+ suppressionFilter,
2279
+ tuning: adminViewTuning,
2280
+ blockMappings,
2281
+ contentFieldsByCollection
2282
+ });
1572
2283
  }
1573
2284
  if (body.action_id.startsWith("filter:")) {
1574
- return renderFindingsPage(ctx);
2285
+ return renderFindingsPage(ctx.kv, adminViewTuning);
1575
2286
  }
1576
- return notFound2(body.action_id);
2287
+ return notFound(body.action_id);
1577
2288
  }
1578
2289
  if (body.type === "form_submit") {
1579
2290
  if (body.action_id === "view_document") {
1580
2291
  const picked = body.values["route_picker"];
1581
2292
  if (typeof picked === "string" && picked.length > 0) {
1582
- return renderDocumentPanel(ctx, picked);
2293
+ return renderDocumentPanel(ctx.kv, picked);
1583
2294
  }
1584
2295
  }
1585
- return renderFindingsPage(ctx);
2296
+ return renderFindingsPage(ctx.kv, adminViewTuning);
1586
2297
  }
1587
- return renderFindingsPage(ctx);
2298
+ return renderFindingsPage(ctx.kv, adminViewTuning);
1588
2299
  }
1589
2300
  async function handlePageLoad(ctx, page) {
1590
2301
  const normalized = page.startsWith("/") ? page.slice(1) : page;
1591
2302
  if (normalized === "" || normalized === "findings") {
1592
- return renderFindingsPage(ctx);
2303
+ return renderFindingsPage(ctx.kv, adminViewTuning);
1593
2304
  }
1594
2305
  if (normalized === "widget:aexeo-score") {
1595
- return renderScoreWidget(ctx);
2306
+ return renderScoreWidget(ctx.kv, adminViewTuning, ctx.log);
1596
2307
  }
1597
2308
  if (normalized === "document") {
1598
- return renderDocumentPanel(ctx);
1599
- }
1600
- return notFound2(page);
1601
- }
1602
- async function handleRefresh2(ctx, collections) {
1603
- let summary;
1604
- try {
1605
- summary = await evaluateAndPersistAll(ctx, {
1606
- evaluator: inProcessEvaluator,
1607
- collections
1608
- });
1609
- } catch (err) {
1610
- const detail = err instanceof Error ? err.message : String(err);
1611
- return {
1612
- blocks: [
1613
- { type: "header", text: "SEO findings" },
1614
- {
1615
- type: "banner",
1616
- title: `Refresh failed: ${detail}`,
1617
- variant: "error"
1618
- }
1619
- ],
1620
- toast: { message: `Refresh failed: ${detail}`, type: "error" }
1621
- };
1622
- }
1623
- const refreshed = await renderFindingsPage(ctx);
1624
- const toastMessage = summary.errors.length === 0 ? `Refreshed ${summary.routesUpdated} routes (${summary.totalFindings} findings across ${summary.documentsScanned} documents)` : `Refresh completed with ${summary.errors.length} errors \u2014 see banner`;
1625
- if (summary.errors.length > 0) {
1626
- refreshed.blocks.unshift({
1627
- type: "banner",
1628
- title: `Refresh issues: ${summary.errors.join(" \u2022 ")}`,
1629
- variant: "alert"
1630
- });
1631
- }
1632
- return {
1633
- ...refreshed,
1634
- toast: {
1635
- message: toastMessage,
1636
- type: summary.errors.length === 0 ? "success" : "info"
1637
- }
1638
- };
1639
- }
1640
- async function readAllFindings(ctx) {
1641
- const entries = await ctx.kv.list(
1642
- "findings:"
1643
- );
1644
- const out = [];
1645
- for (const entry of entries) {
1646
- if (entry.value === null) continue;
1647
- const route = entry.key.replace(/^findings:/, "");
1648
- for (const finding of entry.value.findings) {
1649
- out.push({ ...finding, document_route: route });
1650
- }
1651
- }
1652
- return out;
1653
- }
1654
- function severityFirst(a, b) {
1655
- const rank = (severity) => severity === "error" ? 0 : 1;
1656
- const diff = rank(a.severity) - rank(b.severity);
1657
- if (diff !== 0) return diff;
1658
- return a.rule_id.localeCompare(b.rule_id);
1659
- }
1660
- function uniqueRoutes(rows) {
1661
- const set = /* @__PURE__ */ new Set();
1662
- for (const row of rows) set.add(row.document_route);
1663
- return [...set].sort();
1664
- }
1665
- async function renderFindingsPage(ctx) {
1666
- const findings = await readAllFindings(ctx);
1667
- const errors = findings.filter((f) => f.severity === "error");
1668
- const warnings = findings.filter((f) => f.severity === "warning");
1669
- const sorted = [...findings].sort(severityFirst);
1670
- const routes = uniqueRoutes(findings);
1671
- const blocks = [
1672
- { type: "header", text: "SEO findings" },
1673
- {
1674
- type: "context",
1675
- text: findings.length === 0 ? "No findings yet \u2014 click Refresh to evaluate the site." : `${findings.length} findings across ${routes.length} routes \u2014 ${errors.length} errors, ${warnings.length} warnings.`
1676
- },
1677
- { type: "divider" },
1678
- {
1679
- type: "actions",
1680
- elements: [
1681
- {
1682
- type: "button",
1683
- label: "Refresh",
1684
- action_id: "refresh_findings",
1685
- style: "primary"
1686
- },
1687
- { type: "button", label: "All", action_id: "filter:all" },
1688
- { type: "button", label: "Errors only", action_id: "filter:errors" },
1689
- {
1690
- type: "button",
1691
- label: "Warnings only",
1692
- action_id: "filter:warnings"
1693
- }
1694
- ]
1695
- },
1696
- sorted.length === 0 ? {
1697
- type: "context",
1698
- text: "Once a Refresh runs, findings will list here."
1699
- } : findingsTable(sorted)
1700
- ];
1701
- if (routes.length > 0) {
1702
- blocks.push({
1703
- type: "form",
1704
- fields: [
1705
- {
1706
- type: "select",
1707
- action_id: "route_picker",
1708
- label: "Document to inspect",
1709
- options: routes.map((route) => ({ label: route, value: route }))
1710
- }
1711
- ],
1712
- submit: { label: "View document SEO", action_id: "view_document" }
1713
- });
1714
- }
1715
- return { blocks };
1716
- }
1717
- function findingsTable(rows) {
1718
- return {
1719
- type: "table",
1720
- columns: [
1721
- { key: "route", label: "Route" },
1722
- { key: "rule", label: "Rule", format: "code" },
1723
- { key: "severity", label: "Severity", format: "badge" },
1724
- { key: "message", label: "Message" }
1725
- ],
1726
- rows: rows.map((row) => ({
1727
- route: row.document_route,
1728
- rule: row.rule_id,
1729
- severity: row.severity,
1730
- message: row.message
1731
- }))
1732
- };
1733
- }
1734
- async function renderScoreWidget(ctx) {
1735
- const documents = await readAllDocuments(ctx.kv);
1736
- if (documents.length === 0) {
1737
- return {
1738
- blocks: [
1739
- { type: "header", text: "SEO score" },
1740
- {
1741
- type: "context",
1742
- text: "No documents indexed \u2014 click Refresh on the Aexeo findings page."
1743
- }
1744
- ]
1745
- };
2309
+ return renderDocumentPanel(ctx.kv);
1746
2310
  }
1747
- const facts = await readStoredFacts(ctx.kv);
1748
- const score = await scoreLocally(documents, facts);
1749
- const truthLabel = formatTruthLabel(score);
1750
- const blocks = [
1751
- {
1752
- type: "stats",
1753
- items: [
1754
- { label: "Overall", value: `${score.overall_score}` },
1755
- { label: "Citation", value: `${score.citation_readiness_score}` },
1756
- { label: truthLabel, value: `${score.truth_consistency_score}` },
1757
- { label: "Answers", value: `${score.answer_pack_score}` }
1758
- ]
1759
- }
1760
- ];
1761
- if (score.overall_score < 60) {
1762
- blocks.unshift({
1763
- type: "banner",
1764
- title: `Site score is ${score.overall_score} \u2014 below the 60 quality threshold`,
1765
- variant: "alert"
1766
- });
1767
- }
1768
- if (score.blockers.length > 0) {
1769
- blocks.push({ type: "context", text: topBlockersLine(score) });
1770
- }
1771
- return { blocks };
2311
+ return notFound(page);
1772
2312
  }
1773
2313
  async function scoreLocally(documents, manifest = null) {
1774
2314
  const manifestJson = manifest === null ? null : JSON.stringify(manifest);
@@ -1789,63 +2329,6 @@ function formatTruthLabel(score) {
1789
2329
  return "Truth";
1790
2330
  }
1791
2331
  }
1792
- function topBlockersLine(score) {
1793
- const top = score.blockers.slice(0, 3).map((b) => b.message);
1794
- if (top.length === 0) return "No blockers identified.";
1795
- return `Top blockers: ${top.join(" \u2022 ")}`;
1796
- }
1797
- async function renderDocumentPanel(ctx, route) {
1798
- if (route === void 0) {
1799
- return {
1800
- blocks: [
1801
- { type: "header", text: "Document SEO" },
1802
- {
1803
- type: "context",
1804
- text: "Pick a document from the SEO findings list to see its rule findings here."
1805
- }
1806
- ]
1807
- };
1808
- }
1809
- const findings = await readFindings(ctx.kv, route);
1810
- const errors = findings.filter((f) => f.severity === "error");
1811
- const warnings = findings.filter((f) => f.severity === "warning");
1812
- const sorted = [...findings].sort(severityFirst);
1813
- return {
1814
- blocks: [
1815
- { type: "header", text: `Document SEO \u2014 ${route}` },
1816
- {
1817
- type: "context",
1818
- text: findings.length === 0 ? `No findings for ${route} \u2014 the document is currently clean.` : `${findings.length} findings \u2014 ${errors.length} errors, ${warnings.length} warnings.`
1819
- },
1820
- ...findings.length === 0 ? [] : [{ type: "divider" }, documentFindingsTable(sorted)]
1821
- ]
1822
- };
1823
- }
1824
- function documentFindingsTable(findings) {
1825
- return {
1826
- type: "table",
1827
- columns: [
1828
- { key: "rule", label: "Rule", format: "code" },
1829
- { key: "severity", label: "Severity", format: "badge" },
1830
- { key: "message", label: "Message" },
1831
- { key: "block", label: "Block", format: "code" }
1832
- ],
1833
- rows: findings.map((finding) => ({
1834
- rule: finding.rule_id,
1835
- severity: finding.severity,
1836
- message: finding.message,
1837
- block: `${finding.path}:${finding.line}`
1838
- }))
1839
- };
1840
- }
1841
- function notFound2(page) {
1842
- return {
1843
- blocks: [
1844
- { type: "header", text: "Not found" },
1845
- { type: "context", text: `unknown page: ${page}` }
1846
- ]
1847
- };
1848
- }
1849
2332
  export {
1850
2333
  createPlugin,
1851
2334
  configured_default as default