@coywolf/emdash 0.0.0-stage → 0.35.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +201 -2
- package/package.json +70 -5
- package/src/admin/admin-css.ts +57 -0
- package/src/admin/ai.tsx +915 -0
- package/src/admin/backups.tsx +613 -0
- package/src/admin/breadcrumbs.tsx +542 -0
- package/src/admin/codeBlocks.tsx +204 -0
- package/src/admin/customBlocks.tsx +321 -0
- package/src/admin/dirty.ts +20 -0
- package/src/admin/discovery.tsx +463 -0
- package/src/admin/download.ts +28 -0
- package/src/admin/feature-switches.tsx +124 -0
- package/src/admin/features.tsx +142 -0
- package/src/admin/files.tsx +791 -0
- package/src/admin/formUploads.tsx +445 -0
- package/src/admin/guides.tsx +18 -0
- package/src/admin/headings.tsx +262 -0
- package/src/admin/images.tsx +825 -0
- package/src/admin/index.tsx +62 -0
- package/src/admin/links.tsx +980 -0
- package/src/admin/performance.tsx +330 -0
- package/src/admin/preview.tsx +63 -0
- package/src/admin/redirects-removed.tsx +266 -0
- package/src/admin/redirects.tsx +470 -0
- package/src/admin/reviews.tsx +232 -0
- package/src/admin/robots-bots.tsx +647 -0
- package/src/admin/robots-shared.tsx +109 -0
- package/src/admin/robots-wizard.tsx +748 -0
- package/src/admin/robots.tsx +911 -0
- package/src/admin/save-bar.tsx +134 -0
- package/src/admin/schema.tsx +1187 -0
- package/src/admin/search.tsx +355 -0
- package/src/admin/settings-ui.tsx +87 -0
- package/src/admin/trash-prompt-core.ts +135 -0
- package/src/admin/trash-prompt.tsx +156 -0
- package/src/admin/usage.tsx +301 -0
- package/src/admin/videos-display.tsx +248 -0
- package/src/admin/videos.tsx +1202 -0
- package/src/admin/wpImport.tsx +1516 -0
- package/src/ai/entities.ts +59 -0
- package/src/ai/export.ts +61 -0
- package/src/ai/logic.test.ts +187 -0
- package/src/ai/logic.ts +606 -0
- package/src/ai/pack.ts +121 -0
- package/src/ai/prompts.ts +87 -0
- package/src/ai/providers.ts +276 -0
- package/src/ai/queue.test.ts +97 -0
- package/src/ai/routes.ts +359 -0
- package/src/ai/store.ts +301 -0
- package/src/ai/wikidata.test.ts +184 -0
- package/src/ai/wikidata.ts +87 -0
- package/src/ai/worker.ts +530 -0
- package/src/astro/astro-modules.d.ts +6 -0
- package/src/astro/breadcrumbs/Breadcrumbs.astro +129 -0
- package/src/astro/breadcrumbs/BreadcrumbsBlock.astro +12 -0
- package/src/astro/codeBlocks/CodeBlock.astro +62 -0
- package/src/astro/codeBlocks/EmDashCode.astro +61 -0
- package/src/astro/customBlocks/CustomBlock.astro +64 -0
- package/src/astro/customBlocks/Details.astro +8 -0
- package/src/astro/customBlocks/Disclosure.astro +8 -0
- package/src/astro/customBlocks/Note.astro +8 -0
- package/src/astro/customBlocks/Podcast.astro +8 -0
- package/src/astro/customBlocks/Quote.astro +8 -0
- package/src/astro/customBlocks/Testimonial.astro +8 -0
- package/src/astro/files/FileCard.astro +103 -0
- package/src/astro/headings/HeadingBlock.astro +129 -0
- package/src/astro/headings/TableOfContents.astro +45 -0
- package/src/astro/headings/request.ts +19 -0
- package/src/astro/images/ResponsiveImage.astro +60 -0
- package/src/astro/index.ts +84 -0
- package/src/astro/reviews/CoywolfReview.astro +27 -0
- package/src/astro/reviews/Review.astro +49 -0
- package/src/astro/search/SearchBox.astro +61 -0
- package/src/astro/search/SearchBoxForm.astro +509 -0
- package/src/astro/tables/CaptionedTable.astro +72 -0
- package/src/astro/videos/CoywolfVideo.astro +448 -0
- package/src/backups/download.ts +112 -0
- package/src/backups/dump.test.mjs +287 -0
- package/src/backups/dump.ts +288 -0
- package/src/backups/module.ts +276 -0
- package/src/backups/pack.ts +33 -0
- package/src/backups/restore.test.mjs +103 -0
- package/src/backups/restore.ts +209 -0
- package/src/backups/store.ts +409 -0
- package/src/breadcrumbs/pack.ts +113 -0
- package/src/breadcrumbs/settings.ts +102 -0
- package/src/breadcrumbs/stamp.ts +40 -0
- package/src/breadcrumbs/trail.ts +123 -0
- package/src/codeBlocks/contrast.ts +179 -0
- package/src/codeBlocks/hljs.d.ts +5 -0
- package/src/codeBlocks/pack.ts +110 -0
- package/src/codeBlocks/render.test.ts +152 -0
- package/src/codeBlocks/render.ts +304 -0
- package/src/codeBlocks/settings.ts +18 -0
- package/src/codeBlocks/themes.generated.ts +304 -0
- package/src/codeBlocks/themes.ts +114 -0
- package/src/core/compose.ts +244 -0
- package/src/core/content-url.ts +888 -0
- package/src/core/csv.ts +119 -0
- package/src/core/d1-batch.ts +65 -0
- package/src/core/entry-docs.ts +175 -0
- package/src/core/features-module.ts +48 -0
- package/src/core/features.ts +392 -0
- package/src/core/generation.ts +88 -0
- package/src/core/guide-content.ts +166 -0
- package/src/core/module.ts +104 -0
- package/src/core/read-failures.ts +57 -0
- package/src/core/request-memo.ts +71 -0
- package/src/core/secrets.ts +83 -0
- package/src/core/settings-enhance.ts +74 -0
- package/src/core/site.ts +36 -0
- package/src/core/storage.ts +27 -0
- package/src/customBlocks/pack.ts +260 -0
- package/src/customBlocks/render.ts +424 -0
- package/src/customBlocks/rich.ts +313 -0
- package/src/customBlocks/settings.ts +46 -0
- package/src/discovery/build.ts +252 -0
- package/src/discovery/feed-build.ts +217 -0
- package/src/discovery/feed.ts +172 -0
- package/src/discovery/indexnow.ts +114 -0
- package/src/discovery/llms.ts +79 -0
- package/src/discovery/markdown.ts +294 -0
- package/src/discovery/memory-cache.ts +97 -0
- package/src/discovery/middleware.ts +186 -0
- package/src/discovery/module.ts +353 -0
- package/src/discovery/news.ts +99 -0
- package/src/discovery/pack.ts +58 -0
- package/src/discovery/settings.ts +91 -0
- package/src/discovery/urls.ts +35 -0
- package/src/env.d.ts +10 -0
- package/src/files/card.ts +231 -0
- package/src/files/counts.test.ts +114 -0
- package/src/files/counts.ts +60 -0
- package/src/files/export.ts +44 -0
- package/src/files/format.test.ts +93 -0
- package/src/files/format.ts +163 -0
- package/src/files/module.ts +596 -0
- package/src/files/pack.ts +92 -0
- package/src/files/r2-rules.test.ts +30 -0
- package/src/files/r2-rules.ts +41 -0
- package/src/files/range.ts +50 -0
- package/src/files/s3.ts +114 -0
- package/src/files/serve.ts +141 -0
- package/src/files/sigv4.test.ts +64 -0
- package/src/files/sigv4.ts +160 -0
- package/src/files/site.ts +137 -0
- package/src/files/walker.ts +58 -0
- package/src/formUploads/lib.ts +266 -0
- package/src/formUploads/pack.ts +55 -0
- package/src/formUploads/wrap.ts +592 -0
- package/src/forms.ts +16 -0
- package/src/headings/headings.test.ts +159 -0
- package/src/headings/pack.ts +154 -0
- package/src/headings/render.ts +139 -0
- package/src/headings/settings.ts +117 -0
- package/src/headings/slug.ts +68 -0
- package/src/headings/stamp.ts +142 -0
- package/src/headings/test-hooks.mjs +14 -0
- package/src/headings/toc.ts +48 -0
- package/src/images/cloudflare.ts +329 -0
- package/src/images/lib.ts +221 -0
- package/src/images/module.ts +372 -0
- package/src/images/pack.ts +804 -0
- package/src/images/pending.ts +37 -0
- package/src/images/settings.ts +48 -0
- package/src/images/setup.ts +192 -0
- package/src/images/variants-estimate.ts +162 -0
- package/src/images/variants-store.ts +475 -0
- package/src/images/variants.ts +311 -0
- package/src/index.ts +143 -0
- package/src/links/check.ts +101 -0
- package/src/links/classify.ts +270 -0
- package/src/links/export.ts +59 -0
- package/src/links/module.ts +652 -0
- package/src/links/pack.ts +43 -0
- package/src/links/pt.ts +325 -0
- package/src/links/store.ts +704 -0
- package/src/middleware.ts +287 -0
- package/src/modules.ts +53 -0
- package/src/options.ts +66 -0
- package/src/pageCache/lib.test.mjs +224 -0
- package/src/pageCache/lib.ts +274 -0
- package/src/pageCache/pack.ts +240 -0
- package/src/pageCache/usage-core.ts +152 -0
- package/src/pageCache/usage.ts +262 -0
- package/src/pageCache/warm.test.mjs +670 -0
- package/src/pageCache/warm.ts +722 -0
- package/src/redirects/middleware.ts +169 -0
- package/src/redirects/module.ts +124 -0
- package/src/redirects/pack.ts +89 -0
- package/src/redirects/removed-core.ts +151 -0
- package/src/redirects/removed.ts +293 -0
- package/src/redirects/rules.ts +355 -0
- package/src/redirects/slug-changes.ts +207 -0
- package/src/redirects/slug-core.test.ts +50 -0
- package/src/redirects/slug-core.ts +49 -0
- package/src/redirects/transfer.ts +92 -0
- package/src/reviews/lib.ts +624 -0
- package/src/reviews/pack.ts +135 -0
- package/src/reviews/schema.ts +128 -0
- package/src/reviews/settings.ts +26 -0
- package/src/robots/bots.ts +321 -0
- package/src/robots/data/bots.json +696 -0
- package/src/robots/data/verified.json +582 -0
- package/src/robots/directory.ts +26 -0
- package/src/robots/explain.ts +191 -0
- package/src/robots/guided.ts +300 -0
- package/src/robots/importer.ts +263 -0
- package/src/robots/middleware.ts +151 -0
- package/src/robots/module.ts +446 -0
- package/src/robots/pack.ts +69 -0
- package/src/robots/radar.ts +129 -0
- package/src/robots/rep.test.ts +335 -0
- package/src/robots/rep.ts +362 -0
- package/src/robots/rules.test.ts +251 -0
- package/src/robots/rules.ts +883 -0
- package/src/robots/transfer.ts +45 -0
- package/src/robots/validate.ts +524 -0
- package/src/schema/catalog.ts +146 -0
- package/src/schema/graph.test.mjs +522 -0
- package/src/schema/graph.ts +791 -0
- package/src/schema/module.ts +1138 -0
- package/src/schema/og-image.ts +100 -0
- package/src/schema/pack.ts +58 -0
- package/src/search/engine.ts +578 -0
- package/src/search/fallback.ts +80 -0
- package/src/search/live-cache.ts +116 -0
- package/src/search/live-client.ts +770 -0
- package/src/search/live-serve.ts +282 -0
- package/src/search/module.ts +261 -0
- package/src/search/pack.ts +57 -0
- package/src/search/query.ts +132 -0
- package/src/search/ratelimit-core.ts +118 -0
- package/src/search/ratelimit.ts +79 -0
- package/src/search/snippet.ts +135 -0
- package/src/search/title-index.ts +139 -0
- package/src/search/title-match.ts +192 -0
- package/src/shared.ts +71 -0
- package/src/version.ts +2 -0
- package/src/videos/date.ts +40 -0
- package/src/videos/engagement.ts +75 -0
- package/src/videos/lib.ts +679 -0
- package/src/videos/module.ts +1160 -0
- package/src/videos/pack.ts +187 -0
- package/src/videos/pause.ts +42 -0
- package/src/videos/poster.ts +324 -0
- package/src/videos/render.ts +336 -0
- package/src/videos/store.ts +551 -0
- package/src/videos/stream.ts +134 -0
- package/src/wpImport/convert.ts +702 -0
- package/src/wpImport/guests.ts +329 -0
- package/src/wpImport/gutenberg.ts +157 -0
- package/src/wpImport/markers.ts +66 -0
- package/src/wpImport/pack.ts +451 -0
- package/src/wpImport/parents.ts +202 -0
- package/src/wpImport/prepare.ts +592 -0
- package/src/wpImport/redirects.ts +358 -0
- package/src/wpImport/sources.ts +67 -0
- package/src/wpImport/stream.ts +160 -0
- package/src/wpImport/urls.ts +354 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read the grounded entities for an entry, for Schema.org output.
|
|
3
|
+
*
|
|
4
|
+
* The Schema module calls `getEntryEntities(ctx, collection, id)` (inside the
|
|
5
|
+
* plugin context) or `getEntryEntities(db, collection, id)` with the site's
|
|
6
|
+
* D1 database (outside it, e.g. an Astro component), and merges `about` and
|
|
7
|
+
* `mentions` into its Article/WebPage node. Returns empty lists when the
|
|
8
|
+
* "ai.entities" feature is off or nothing has been analyzed yet.
|
|
9
|
+
*/
|
|
10
|
+
import type { PluginContext, StorageCollection } from "emdash";
|
|
11
|
+
|
|
12
|
+
import { PLUGIN_ID, cachedCtxFeatures, isOn, siteFeatures } from "../core/features.js";
|
|
13
|
+
import { noteReadFailure } from "../core/read-failures.js";
|
|
14
|
+
import { type Entity, entityNodes } from "./logic.js";
|
|
15
|
+
import type { EntryRecord } from "./store.js";
|
|
16
|
+
|
|
17
|
+
export interface EntryEntities {
|
|
18
|
+
entities: Entity[];
|
|
19
|
+
about: Record<string, unknown>[];
|
|
20
|
+
mentions: Record<string, unknown>[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EMPTY: EntryEntities = { entities: [], about: [], mentions: [] };
|
|
24
|
+
|
|
25
|
+
type Source = Pick<PluginContext, "storage" | "settings"> | D1Database;
|
|
26
|
+
|
|
27
|
+
function isD1(source: Source): source is D1Database {
|
|
28
|
+
return typeof (source as D1Database).prepare === "function";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `preloaded.record`: the entry's aiEntries doc, already read by the caller
|
|
33
|
+
* (Schema & Social reads it with the entry's other docs), so nothing is read
|
|
34
|
+
* here; the caller has checked the "ai.entities" switch.
|
|
35
|
+
*/
|
|
36
|
+
export async function getEntryEntities(source: Source, collection: string, id: string, preloaded?: { record: EntryRecord | null }): Promise<EntryEntities> {
|
|
37
|
+
try {
|
|
38
|
+
let record: EntryRecord | null = null;
|
|
39
|
+
if (preloaded) {
|
|
40
|
+
record = preloaded.record ?? null;
|
|
41
|
+
} else if (isD1(source)) {
|
|
42
|
+
if (!isOn(await siteFeatures(), "ai.entities")) return EMPTY;
|
|
43
|
+
const row = await source
|
|
44
|
+
.prepare("SELECT data FROM _plugin_storage WHERE plugin_id = ? AND collection = ? AND id = ?")
|
|
45
|
+
.bind(PLUGIN_ID, "aiEntries", `${collection}:${id}`)
|
|
46
|
+
.first<{ data: string }>();
|
|
47
|
+
record = row?.data ? (JSON.parse(row.data) as EntryRecord) : null;
|
|
48
|
+
} else {
|
|
49
|
+
if (!isOn(await cachedCtxFeatures(source), "ai.entities")) return EMPTY;
|
|
50
|
+
const entries = (source.storage as Record<string, StorageCollection>).aiEntries as StorageCollection<EntryRecord> | undefined;
|
|
51
|
+
record = (await entries?.get(`${collection}:${id}`)) ?? null;
|
|
52
|
+
}
|
|
53
|
+
const entities = Array.isArray(record?.entities) ? record.entities : [];
|
|
54
|
+
return { entities, ...entityNodes(entities) };
|
|
55
|
+
} catch {
|
|
56
|
+
noteReadFailure("AI entities"); // The page goes out without its entities: not into the edge cache.
|
|
57
|
+
return EMPTY; // Schema output must never fail because of enrichment.
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/ai/export.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Enrichment CSV export: every analyzed entry with the entities found in
|
|
3
|
+
* it (one row per entity) and its AI-written description. Pure, so the admin
|
|
4
|
+
* page and the tests share it.
|
|
5
|
+
*/
|
|
6
|
+
import { type CsvCell, toCsv } from "../core/csv.js";
|
|
7
|
+
|
|
8
|
+
export interface ExportEntry {
|
|
9
|
+
collection: string;
|
|
10
|
+
entryId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
status: string;
|
|
13
|
+
error: string;
|
|
14
|
+
entities: Array<{ name: string; type: string; description: string; qid: string; wikipedia: string; website: string; primary: boolean }>;
|
|
15
|
+
description: string;
|
|
16
|
+
descriptionStatus: string;
|
|
17
|
+
updated: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ENTITY_CSV_COLUMNS = [
|
|
21
|
+
"collection",
|
|
22
|
+
"entry_id",
|
|
23
|
+
"entry_title",
|
|
24
|
+
"analysis",
|
|
25
|
+
"entity",
|
|
26
|
+
"entity_type",
|
|
27
|
+
"role",
|
|
28
|
+
"wikidata_id",
|
|
29
|
+
"wikidata_url",
|
|
30
|
+
"wikipedia",
|
|
31
|
+
"website",
|
|
32
|
+
"entity_description",
|
|
33
|
+
"ai_description",
|
|
34
|
+
"description_status",
|
|
35
|
+
"updated",
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
/** Spreadsheet-safe CSV. An entry with no entities still gets one row, so its description and errors are kept. */
|
|
39
|
+
export function entitiesToCsv(entries: ExportEntry[]): string {
|
|
40
|
+
const rows: CsvCell[][] = [[...ENTITY_CSV_COLUMNS]];
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const base = [e.collection, e.entryId, e.title, e.status === "error" ? `error: ${e.error}` : "ok"];
|
|
43
|
+
const tail = [e.description, e.descriptionStatus, e.updated];
|
|
44
|
+
if (!e.entities.length) rows.push([...base, "", "", "", "", "", "", "", "", ...tail]);
|
|
45
|
+
for (const x of e.entities) {
|
|
46
|
+
rows.push([
|
|
47
|
+
...base,
|
|
48
|
+
x.name,
|
|
49
|
+
x.type,
|
|
50
|
+
x.primary ? "about" : "mentions",
|
|
51
|
+
x.qid,
|
|
52
|
+
x.qid ? `https://www.wikidata.org/wiki/${x.qid}` : "",
|
|
53
|
+
x.wikipedia,
|
|
54
|
+
x.website,
|
|
55
|
+
x.description,
|
|
56
|
+
...tail,
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return toCsv(rows, { guard: true });
|
|
61
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for AI Enrichment's pure logic.
|
|
3
|
+
* Run: node --test test/*.test.mjs (or node --test src/ai/*.test.ts)
|
|
4
|
+
*/
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
|
|
8
|
+
// @ts-ignore -- Node runs the .ts source directly; tsc doesn't need to resolve it.
|
|
9
|
+
import * as L from "./logic.ts";
|
|
10
|
+
|
|
11
|
+
const logic = L as typeof import("./logic.js");
|
|
12
|
+
|
|
13
|
+
test("decodeJson tolerates fences, prose, and trailing text", () => {
|
|
14
|
+
assert.deepEqual(logic.decodeJson('```json\n[{"a":1}]\n```'), [{ a: 1 }]);
|
|
15
|
+
assert.deepEqual(logic.decodeJson('Here you go: {"a":2} hope that helps'), { a: 2 });
|
|
16
|
+
assert.equal(logic.decodeJson("no json here"), null);
|
|
17
|
+
assert.deepEqual(logic.decodeJson({ already: true }), { already: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("parseMentions validates shape, types, duplicates, and the 12 cap", () => {
|
|
21
|
+
const rows = [
|
|
22
|
+
{ surface: "NYC", name: "New York City", type: "Place", description: "A city", primary: true },
|
|
23
|
+
{ name: "new york city", type: "Place" }, // duplicate (case-insensitive)
|
|
24
|
+
{ name: "Widget", type: "Gadget", description: "<b>thing</b>" }, // unknown type → Thing, tags stripped
|
|
25
|
+
{ type: "Person" }, // no name
|
|
26
|
+
"junk",
|
|
27
|
+
...Array.from({ length: 20 }, (_, i) => ({ name: `E${i}`, type: "Organization" })),
|
|
28
|
+
];
|
|
29
|
+
const out = logic.parseMentions(JSON.stringify(rows));
|
|
30
|
+
assert.equal(out.length, 12);
|
|
31
|
+
assert.deepEqual(out[0], { surface: "NYC", name: "New York City", type: "Place", description: "A city", primary: true });
|
|
32
|
+
assert.equal(out[1].type, "Thing");
|
|
33
|
+
assert.equal(out[1].description, "thing");
|
|
34
|
+
assert.equal(out[1].primary, false);
|
|
35
|
+
assert.deepEqual(logic.parseMentions("not json"), []);
|
|
36
|
+
assert.equal(logic.parseMentions('{"entities":[{"name":"X","type":"Thing"}]}').length, 1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("parseChoices keeps only QID strings", () => {
|
|
40
|
+
assert.deepEqual(logic.parseChoices('{"A":"q42","B":null,"C":"https://wikidata.org/Q1","D":7}'), { A: "Q42", B: null, C: null, D: null });
|
|
41
|
+
assert.deepEqual(logic.parseChoices("[1,2]"), {});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("cleanDescription strips quotes and labels and caps at 155 on a word", () => {
|
|
45
|
+
assert.equal(logic.cleanDescription('"Meta description: A short summary."'), "A short summary.");
|
|
46
|
+
const long = "word ".repeat(60);
|
|
47
|
+
const out = logic.cleanDescription(long);
|
|
48
|
+
assert.ok([...out].length <= 155, `length ${[...out].length}`);
|
|
49
|
+
assert.ok(out.endsWith("…"));
|
|
50
|
+
assert.ok(!out.includes("wor…"));
|
|
51
|
+
assert.equal(logic.cleanDescription(""), "");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("parseImageText validates and normalizes", () => {
|
|
55
|
+
const r = logic.parseImageText('{"alt_text":"Image of a dog running.","title":"Dog Running","caption":"A dog.","description":"x"}');
|
|
56
|
+
assert.deepEqual(r, { alt: "A dog running.", caption: "A dog.", title: "Dog Running" });
|
|
57
|
+
assert.equal(logic.parseImageText('{"alt_text":"","title":"Divider","caption":""}').alt, "");
|
|
58
|
+
assert.throws(() => logic.parseImageText(""), /empty response/);
|
|
59
|
+
assert.throws(() => logic.parseImageText("nope"), /could not be parsed/);
|
|
60
|
+
assert.throws(() => logic.parseImageText('{"caption":"only"}'), /usable/);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const human = { p31: ["Q5"], wikipedia: "", website: "" };
|
|
64
|
+
const city = { p31: ["Q515"], wikipedia: "https://en.wikipedia.org/wiki/Paris", website: "https://paris.fr" };
|
|
65
|
+
const disamb = { p31: ["Q4167410"], wikipedia: "", website: "" };
|
|
66
|
+
|
|
67
|
+
test("grounding: candidates, choices restricted to real candidates", () => {
|
|
68
|
+
const mentions = [
|
|
69
|
+
{ surface: "", name: "Paris", type: "Place", description: "", primary: true },
|
|
70
|
+
{ surface: "", name: "Mercury", type: "Thing", description: "planet", primary: false },
|
|
71
|
+
{ surface: "", name: "Nobody", type: "Person", description: "", primary: false },
|
|
72
|
+
] as const;
|
|
73
|
+
const { mentions: grounded, ambiguous } = logic.attachCandidates([...mentions], [
|
|
74
|
+
[{ id: "Q90", label: "Paris", description: "capital of France" }],
|
|
75
|
+
[
|
|
76
|
+
{ id: "Q308", label: "Mercury", description: "planet" },
|
|
77
|
+
{ id: "Q925", label: "mercury", description: "element" },
|
|
78
|
+
],
|
|
79
|
+
[],
|
|
80
|
+
]);
|
|
81
|
+
assert.equal(grounded[0].qid, "Q90");
|
|
82
|
+
assert.deepEqual(ambiguous, [1]);
|
|
83
|
+
assert.equal(grounded[2].qid, "");
|
|
84
|
+
// A hallucinated QID is ignored; a listed one is accepted.
|
|
85
|
+
assert.equal(logic.applyChoices(grounded, ambiguous, { Mercury: "Q999" })[1].qid, "");
|
|
86
|
+
assert.equal(logic.applyChoices(grounded, ambiguous, { Mercury: "Q308" })[1].qid, "Q308");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("verifyEntities drops disambiguation pages and Person/human mismatches", () => {
|
|
90
|
+
const m = (name: string, type: string, qid: string) => ({ surface: "", name, type, description: "", primary: false, qid, candidates: [] }) as never;
|
|
91
|
+
const out = logic.verifyEntities(
|
|
92
|
+
[m("Ada Lovelace", "Person", "Q7259"), m("Paris", "Place", "Q90"), m("Paris Hilton", "Place", "Q47899"), m("A Band", "Person", "Q1"), m("Mercury", "Thing", "Q2"), m("Unresolved", "Thing", ""), m("Dup", "Place", "Q90")],
|
|
93
|
+
{ Q7259: human, Q90: city, Q47899: human, Q1: { p31: ["Q215380"], wikipedia: "", website: "" }, Q2: disamb },
|
|
94
|
+
);
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
out.map((e) => e.qid),
|
|
97
|
+
["Q7259", "Q90"],
|
|
98
|
+
);
|
|
99
|
+
assert.equal(out[1].website, "https://paris.fr");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("parseSearch reads a wbsearchentities response", () => {
|
|
103
|
+
assert.deepEqual(logic.parseSearch({ search: [{ id: "Q1", label: "Universe", description: "all" }, { id: "P31" }, {}] }), [{ id: "Q1", label: "Universe", description: "all" }]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("entityNodes builds about/mentions with sameAs", () => {
|
|
107
|
+
const nodes = logic.entityNodes([
|
|
108
|
+
{ name: "Paris", type: "Place", description: "City", qid: "Q90", wikipedia: "https://en.wikipedia.org/wiki/Paris", website: "", primary: true },
|
|
109
|
+
{ name: "X", type: "Thing", description: "", qid: "Q1", wikipedia: "", website: "", primary: false },
|
|
110
|
+
{ name: "Bad", type: "Thing", description: "", qid: "nope", wikipedia: "", website: "", primary: false },
|
|
111
|
+
]);
|
|
112
|
+
assert.deepEqual(nodes.about, [{ "@type": "Place", name: "Paris", sameAs: ["https://www.wikidata.org/wiki/Q90", "https://en.wikipedia.org/wiki/Paris"], description: "City" }]);
|
|
113
|
+
assert.deepEqual(nodes.mentions, [{ "@type": "Thing", name: "X", sameAs: "https://www.wikidata.org/wiki/Q1" }]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("Portable Text → plain text", () => {
|
|
117
|
+
const pt = [
|
|
118
|
+
{ _type: "block", children: [{ _type: "span", text: "Hello " }, { _type: "span", text: "world." }] },
|
|
119
|
+
{ _type: "image", asset: { _ref: "x" }, caption: "A caption" },
|
|
120
|
+
{ _type: "columns", columns: [{ content: [{ _type: "block", children: [{ text: "Nested." }] }] }] },
|
|
121
|
+
];
|
|
122
|
+
assert.equal(logic.portableTextToPlain(pt), "Hello world.\n\nA caption\n\nNested.");
|
|
123
|
+
const text = logic.entryPlainText({ body: pt, excerpt: "<p>Intro</p>", count: 3 }, [
|
|
124
|
+
{ slug: "excerpt", type: "text" },
|
|
125
|
+
{ slug: "body", type: "portableText" },
|
|
126
|
+
{ slug: "count", type: "number" },
|
|
127
|
+
]);
|
|
128
|
+
assert.ok(text.startsWith(" Intro") || text.startsWith("Intro"));
|
|
129
|
+
assert.ok(text.includes("Hello world."));
|
|
130
|
+
assert.equal(logic.entryPlainText({ body: pt }, [{ slug: "body", type: "portableText" }], 5), "Hello");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("normalizeName and nameMatches ignore case, diacritics, and punctuation", () => {
|
|
134
|
+
assert.equal(logic.normalizeName(" Café-Müller, Inc. "), "cafe muller inc");
|
|
135
|
+
const c = { id: "Q1", label: "Beyoncé", description: "", aliases: ["Beyoncé Knowles"] };
|
|
136
|
+
assert.equal(logic.nameMatches({ name: "beyonce", surface: "" }, c), true);
|
|
137
|
+
assert.equal(logic.nameMatches({ name: "Queen B", surface: "Beyoncé Knowles" }, c), true);
|
|
138
|
+
assert.equal(logic.nameMatches({ name: "Beyoncé tour", surface: "" }, c), false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("attachCandidates drops search hits whose label/alias doesn't match, even a single one", () => {
|
|
142
|
+
const m = [
|
|
143
|
+
{ surface: "", name: "Acme Widgets", type: "Organization", description: "", primary: true },
|
|
144
|
+
{ surface: "the Met", name: "Metropolitan Museum of Art", type: "Organization", description: "", primary: false },
|
|
145
|
+
] as never;
|
|
146
|
+
const { mentions, ambiguous } = logic.attachCandidates(m, [
|
|
147
|
+
[{ id: "Q5", label: "Acme Corporation", description: "fictional company" }],
|
|
148
|
+
[
|
|
149
|
+
{ id: "Q160236", label: "Metropolitan Museum of Art", description: "art museum" },
|
|
150
|
+
{ id: "Q99", label: "Metropolitan Opera", description: "opera company", aliases: ["the Met"] },
|
|
151
|
+
],
|
|
152
|
+
]);
|
|
153
|
+
assert.equal(mentions[0].qid, "");
|
|
154
|
+
assert.equal(mentions[0].candidates.length, 0);
|
|
155
|
+
assert.deepEqual(ambiguous, [1]); // both match (label and alias): the model must choose
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("parseSearch keeps aliases and the matched text", () => {
|
|
159
|
+
const out = logic.parseSearch({ search: [{ id: "Q1", label: "A", aliases: ["Alpha"], match: { type: "alias", text: "Alef" } }] });
|
|
160
|
+
assert.deepEqual(out[0].aliases, ["Alpha", "Alef"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("verifyEntities uses the Wikidata label as the name and rejects unverifiable items", () => {
|
|
164
|
+
const g = (name: string, type: string, qid: string, label: string) =>
|
|
165
|
+
({ surface: "", name, type, description: "", primary: true, qid, candidates: [{ id: qid, label, description: "" }] }) as never;
|
|
166
|
+
const out = logic.verifyEntities([g("nyc", "Place", "Q60", "New York City"), g("Ghost", "Thing", "Q404", "Ghost")], {
|
|
167
|
+
Q60: { p31: ["Q515"], wikipedia: "", website: "" },
|
|
168
|
+
});
|
|
169
|
+
assert.deepEqual(
|
|
170
|
+
out.map((e) => e.name),
|
|
171
|
+
["New York City"],
|
|
172
|
+
); // Q404 wasn't returned by Wikidata: dropped
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("typeCompatible: Person needs Q5; Organization/Place reject creative works; Organization rejects places", () => {
|
|
176
|
+
assert.equal(logic.typeCompatible("Person", []), false);
|
|
177
|
+
assert.equal(logic.typeCompatible("Person", ["Q5"]), true);
|
|
178
|
+
assert.equal(logic.typeCompatible("Organization", ["Q482994"]), false); // album, not the band
|
|
179
|
+
assert.equal(logic.typeCompatible("Organization", ["Q515"]), false); // city
|
|
180
|
+
assert.equal(logic.typeCompatible("Organization", ["Q4830453"]), true); // business
|
|
181
|
+
assert.equal(logic.typeCompatible("Organization", []), false);
|
|
182
|
+
assert.equal(logic.typeCompatible("Place", ["Q11424"]), false); // film
|
|
183
|
+
assert.equal(logic.typeCompatible("Place", ["Q515"]), true);
|
|
184
|
+
assert.equal(logic.typeCompatible("Thing", ["Q11424"]), true); // a film is a fine Thing
|
|
185
|
+
assert.equal(logic.typeCompatible("Thing", ["Q5"]), false);
|
|
186
|
+
assert.equal(logic.typeCompatible("Thing", ["Q4167410"]), false);
|
|
187
|
+
});
|