From d1aef91c72d6a3bf670826dd059b7bf3b7b2ffae Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 5 Aug 2026 11:58:27 +0100 Subject: [PATCH 01/53] feat: build-generate AI retrieval layer (knowledge graph + index) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Docusaurus postBuild plugin that turns the rendered docs into an AI-consumable layer, generated at build time (nothing committed): /assets/knowledge-retrieval-index.json (fast lookup) /assets/knowledge-graph.jsonld (concept graph) Parses the final rendered HTML (so imported partials/MDX are captured in full), splits each current-version page into ~1400-char knowledge modules, and derives per-chunk metadata: summary (from the frontmatter description), rule-based intents/audiences, framework, product, and the real URL. The graph mines real edges — BelongsToProduct, CitesApi, SeeAlso, and per-product AvailableOn / NotAvailableOn. Current docs only (frozen versions, the external API reference, and *.html redirect stubs excluded). Extraction failures are non-fatal so they never block a deploy. Co-Authored-By: Claude Opus 4.8 (1M context) --- docusaurus.config.ts | 4 + src/plugins/knowledge-extractor/index.ts | 612 +++++++++++++++++++++++ 2 files changed, 616 insertions(+) create mode 100644 src/plugins/knowledge-extractor/index.ts diff --git a/docusaurus.config.ts b/docusaurus.config.ts index db796f49b5..b37e60ba80 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -4,6 +4,7 @@ import type * as Preset from "@docusaurus/preset-classic"; import * as dotenv from 'dotenv'; import { version } from "react"; import remarkHideComments from "./src/plugins/remark-hide-comments"; +import knowledgeExtractor from "./src/plugins/knowledge-extractor"; dotenv.config(); // Load environment variables from .env file /** @@ -410,6 +411,9 @@ const config: Config = { ignoreFiles: llmsIgnoreFiles, }, ], + // Build-generate AI layer: emits /assets/knowledge-retrieval-index.json and + // /assets/knowledge-graph.jsonld from the rendered HTML (see src/plugins/knowledge-extractor). + knowledgeExtractor, ], presets: [ diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts new file mode 100644 index 0000000000..53795c7dfa --- /dev/null +++ b/src/plugins/knowledge-extractor/index.ts @@ -0,0 +1,612 @@ +/** + * knowledge-extractor — build-generate AI layer. + * + * Runs in Docusaurus `postBuild`, so it reads the FINAL rendered HTML (which + * already has all partials/MDX components inlined) rather than raw Markdown. + * That means pages whose prose lives in imported partials are captured in full, + * and every chunk gets the real, user-facing URL and the frontmatter-derived + * `` as its summary. + * + * It splits each CURRENT-version page into small self-contained knowledge + * modules (chunks, ~1400 chars) with rule-based metadata, preserving link URLs + * inside the prose (so citations — including external API-reference links — + * survive), then emits the two artifacts an assistant / in-docs search consume: + * - /assets/knowledge-retrieval-index.json (fast lookup) + * - /assets/knowledge-graph.jsonld (enriched concept graph) + * + * The graph is not just faceting: alongside intent/audience/channel/framework + * it mines real edges from the content — product membership, cites-API, + * see-also (internal links), and per-product availability (from "not available" + * stubs). Per-module intermediates are held in memory only, never committed. + * + * Faithful port of the bundle's Python pipeline, adapted to the Scandit repo. + */ + +import fs from "node:fs"; +import path from "node:path"; +import * as cheerio from "cheerio"; + +const CHUNK_TARGET_CHARS = 1400; +const OWNER = "docsops-auto"; + +type Chunk = { heading: string; content: string }; + +// --------------------------------------------------------------------------- +// small helpers (ported) +// --------------------------------------------------------------------------- +function slug(value: string): string { + const clean = String(value) + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, ""); + return clean.replace(/-{2,}/g, "-") || "module"; +} + +function firstHeading(text: string): string { + for (const line of text.split(/\r?\n/)) { + const m = line.match(/^\s*#{2,6}\s+(.+?)\s*$/); + if (m) return m[1].trim(); + } + return ""; +} + +/** Split body into ~chunkTarget-sized chunks at H2/H3 boundaries (ported). */ +function chunkBody(body: string, target: number): Chunk[] { + const parts = body.split(/\n(?=##\s|###\s)/); + const chunks: Chunk[] = []; + let current = ""; + let currentHeading = ""; + for (let part of parts) { + part = part.trim(); + if (!part) continue; + const partHeading = firstHeading(part); + const candidate = current ? `${current}\n\n${part}`.trim() : part; + if (candidate.length <= target) { + current = candidate; + if (!currentHeading) currentHeading = partHeading; + continue; + } + if (current) chunks.push({ heading: currentHeading, content: current }); + if (part.length <= target) { + current = part; + currentHeading = partHeading; + continue; + } + let para = ""; + let paraHeading = partHeading; + for (let p of part.split("\n\n")) { + p = p.trim(); + if (!p) continue; + const cand = para ? `${para}\n\n${p}`.trim() : p; + if (cand.length <= target) { + para = cand; + if (!paraHeading) paraHeading = firstHeading(p); + } else { + if (para) chunks.push({ heading: paraHeading, content: para }); + para = p; + paraHeading = firstHeading(p); + } + } + current = para; + currentHeading = paraHeading; + } + if (current) chunks.push({ heading: currentHeading, content: current }); + return chunks; +} + +function pickIntents(contentType: string, title: string, body: string): string[] { + const text = `${title} ${body}`.toLowerCase(); + const intents: string[] = []; + if (contentType === "tutorial" || contentType === "how-to" || text.includes("configure")) intents.push("configure"); + if (contentType === "troubleshooting" || text.includes("error") || text.includes("fix")) intents.push("troubleshoot"); + if (contentType === "reference" || contentType === "concept" || text.includes("integrat")) intents.push("integrate"); + if (text.includes("secure") || text.includes(" auth")) intents.push("secure"); + if (intents.length === 0) intents.push("configure"); + return Array.from(new Set(intents)).sort(); +} + +function pickAudiences(contentType: string): string[] { + if (contentType === "tutorial") return ["beginner", "practitioner"]; + if (contentType === "reference" || contentType === "concept") return ["developer", "operator"]; + if (contentType === "troubleshooting") return ["support", "operator"]; + return ["practitioner", "developer"]; +} + +function extractSummary(description: string, bodyChunk: string): string { + const desc = (description || "").trim(); + if (desc) return desc.slice(0, 240); + let text = bodyChunk.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\s+/g, " ").trim(); + if (text.length < 30) text = `${text} This module is auto-generated from docs content for retrieval and assistant context.`; + return text.slice(0, 240); +} + +// --------------------------------------------------------------------------- +// HTML -> markdown-ish text (link URLs preserved so citations survive) +// --------------------------------------------------------------------------- +const HEADING_LEVEL: Record = { h1: 1, h2: 2, h3: 3, h4: 4, h5: 5, h6: 6 }; + +/** Serialize inline content, keeping `[label](href)` for links and `code` spans. */ +function serializeInline($: cheerio.CheerioAPI, node: any): string { + let out = ""; + $(node) + .contents() + .each((_i, n: any) => { + if (n.type === "text") { + out += n.data || ""; + } else if (n.type === "tag") { + const tag = String(n.name || "").toLowerCase(); + if (tag === "a") { + const href = String($(n).attr("href") || ""); + const label = serializeInline($, n).replace(/\s+/g, " ").trim(); + if (!label) return; + out += href && !href.startsWith("#") ? `[${label}](${href})` : label; + } else if (tag === "code") { + out += "`" + $(n).text() + "`"; + } else if (tag === "br") { + out += " "; + } else { + out += serializeInline($, n); + } + } + }); + return out; +} + +function inlineText($: cheerio.CheerioAPI, el: any): string { + return serializeInline($, el).replace(/​/g, "").replace(/\s+/g, " ").trim(); +} + +function tableToMd($: cheerio.CheerioAPI, el: any): string { + const rows: string[] = []; + $(el) + .find("tr") + .each((_i, tr) => { + const cells: string[] = []; + $(tr) + .children("th,td") + .each((_j, c) => cells.push(inlineText($, c))); + if (cells.length) rows.push(`| ${cells.join(" | ")} |`); + }); + return rows.join("\n"); +} + +function blockToMd($: cheerio.CheerioAPI, el: any): string { + const tag = String(el.tagName || el.name || "").toLowerCase(); + if (HEADING_LEVEL[tag]) { + const t = inlineText($, el).replace(/^#+\s*/, ""); + return t ? `${"#".repeat(HEADING_LEVEL[tag])} ${t}` : ""; + } + if (tag === "p") return inlineText($, el); + if (tag === "ul" || tag === "ol") { + const items: string[] = []; + $(el) + .children("li") + .each((_i, li) => { + const t = inlineText($, li); + if (t) items.push(`- ${t}`); + }); + return items.join("\n"); + } + if (tag === "pre") { + const code = $(el).text().replace(/\s+$/g, ""); + return code ? "```\n" + code + "\n```" : ""; + } + if (tag === "table") return tableToMd($, el); + if (tag === "blockquote") return inlineText($, el); + if (tag === "div" || tag === "section" || tag === "details" || tag === "article" || tag === "aside") { + const parts: string[] = []; + $(el) + .children() + .each((_i, c) => { + const t = blockToMd($, c); + if (t && t.trim()) parts.push(t.trim()); + }); + return parts.join("\n\n"); + } + return inlineText($, el); +} + +function extractMarkdownish($: cheerio.CheerioAPI, root: any): string { + const parts: string[] = []; + $(root) + .children() + .each((_i, el) => { + const t = blockToMd($, el); + if (t && t.trim()) parts.push(t.trim()); + }); + return parts.join("\n\n").replace(/\n{3,}/g, "\n\n").trim(); +} + +// --------------------------------------------------------------------------- +// repo-specific derivations +// --------------------------------------------------------------------------- +function pathSegments(pathname: string): string[] { + return pathname.split("/").filter(Boolean); +} + +/** /sdks/ios/... -> "ios"; /sdks/net/ios/... -> "net-ios"; else "". */ +function detectFramework(pathname: string): string { + const p = pathSegments(pathname); + if (p[0] !== "sdks") return ""; + if (p[1] === "net" && (p[2] === "ios" || p[2] === "android")) return `net-${p[2]}`; + return p[1] || ""; +} + +/** Product a page belongs to (sparkscan, matrixscan, id-capture, ...) or "core". */ +function detectProduct(pathname: string): string { + const p = pathSegments(pathname); + if (p[0] === "sdks") { + const i = p[1] === "net" ? 3 : 2; // first segment after the framework + const rest = p.slice(i); + return rest.length >= 2 ? slug(rest[0]) : "core"; // product dir vs framework-level page + } + return p[0] ? slug(p[0]) : "general"; +} + +function detectContentType(pathname: string, title: string): string { + const p = pathname.toLowerCase(); + const t = title.toLowerCase(); + if (p.includes("/api/") || p.endsWith("/api/") || p.includes("api-reference")) return "reference"; + if (p.includes("get-started") || p.includes("installation") || t.startsWith("get started")) return "tutorial"; + if (p.includes("troubleshoot") || t.includes("troubleshoot")) return "troubleshooting"; + if (p.includes("/intro") || p.includes("/concepts/") || p.includes("overview") || t.startsWith("about ")) return "concept"; + return "docs"; +} + +function isAvailabilityStub(title: string, body: string): boolean { + return /\bnot available\b/i.test(title) || /is not available (on|for) the/i.test(body); +} + +/** Classify links found in chunk prose into internal doc paths and API-ref URLs. */ +function classifyLinks(chunkMarkdown: string, site: string): { internal: string[]; api: string[] } { + const internal = new Set(); + const api = new Set(); + const re = /\[[^\]]*\]\(([^)]+)\)/g; + let m: RegExpExecArray | null; + while ((m = re.exec(chunkMarkdown))) { + let href = m[1].trim(); + if (!href) continue; + if (href.includes("/data-capture-sdk/")) { + api.add(href.split("#")[0]); + continue; + } + // normalize same-host absolute URLs to a site path + if (site && href.startsWith(site)) href = href.slice(site.length) || "/"; + if (href.startsWith("/")) { + const p = href.split(/[?#]/)[0]; + if (p.startsWith("/img") || p.startsWith("/assets") || /\.(png|jpe?g|gif|svg|mp4|pdf|zip)$/i.test(p)) continue; + internal.add(p.endsWith("/") ? p : `${p}/`); + } + } + return { internal: Array.from(internal), api: Array.from(api) }; +} + +function buildModule(args: { + pathname: string; + url: string; + sourceSite: string; + site: string; + title: string; + description: string; + chunk: Chunk; + idx: number; + framework: string; + product: string; + contentType: string; + version: string; + updatedAt: string; + notAvailable: boolean; +}) { + const { pathname, url, sourceSite, site, title: rawTitle, description, chunk, idx, framework, product, contentType, version, updatedAt, notAvailable } = args; + const chunkClean = chunk.content.trim(); + const title = (rawTitle || pathname).trim(); + const displayTitle = idx === 1 ? title : `${title} (Part ${idx})`; + const summary = extractSummary(description, chunkClean); + const resolvedHeading = (chunk.heading || displayTitle).trim(); + const links = classifyLinks(chunkClean, site); + const assistantContext = + `Use this module when answering questions related to: ${displayTitle}. ` + + `Source path: ${pathname}. ` + + `Summary: ${summary}\n\n${chunkClean}`; + const moduleId = slug(`auto-${pathname}-${idx}`); + const intents = pickIntents(contentType, displayTitle, chunkClean); + const audiences = pickAudiences(contentType); + const stemSlug = slug(pathSegments(pathname).pop() || "module"); + const tags = Array.from( + new Set(["auto-extracted", contentType || "docs", framework, product, stemSlug].filter(Boolean)), + ).sort(); + const keywords = Array.from( + new Set([stemSlug.replace(/-/g, " "), product.replace(/-/g, " "), contentType || "docs", framework.replace(/-/g, " ")].filter(Boolean)), + ).sort(); + return { + id: moduleId, + title: displayTitle.slice(0, 90), + summary: summary.slice(0, 240), + intents, + audiences, + channels: ["docs", "assistant", "automation"], + priority: 60, + status: "active", + owner: OWNER, + last_verified: updatedAt.slice(0, 10), + dependencies: [] as string[], + tags, + metadata: { + url, + title: displayTitle.slice(0, 90), + heading: resolvedHeading.slice(0, 180), + framework, + product, + version, + updated_at: updatedAt, + source_site: sourceSite, + source_path: pathname, + not_available: notAvailable, + }, + semantic: { + topic: resolvedHeading.slice(0, 120), + intent: intents[0], + audience: audiences[0], + keywords, + status: "rule_based", + }, + references: links.internal.slice(0, 20), + api_refs: links.api.slice(0, 20), + content: { + docs_markdown: chunkClean, + assistant_context: assistantContext, + }, + }; +} + +type KModule = ReturnType; + +// --------------------------------------------------------------------------- +// consumable artifacts +// --------------------------------------------------------------------------- +function toIndexRecord(m: KModule) { + return { + objectID: m.id, + id: m.id, + title: m.title, + summary: m.summary, + status: m.status, + priority: m.priority, + owner: m.owner, + last_verified: m.last_verified, + intents: m.intents, + audiences: m.audiences, + channels: m.channels, + dependencies: m.dependencies, + tags: m.tags, + docs_excerpt: m.content.docs_markdown.slice(0, 400), + assistant_excerpt: m.content.assistant_context.slice(0, 300), + url: m.metadata.url, + heading: m.metadata.heading, + framework: m.metadata.framework, + product: m.metadata.product, + version: m.metadata.version, + updated_at: m.metadata.updated_at, + source_site: m.metadata.source_site, + not_available: m.metadata.not_available, + references: m.references, + api_refs: m.api_refs, + topic: m.semantic.topic, + semantic_intent: m.semantic.intent, + semantic_audience: m.semantic.audience, + keywords: m.semantic.keywords, + semantic_status: m.semantic.status, + }; +} + +/** Enriched JSON-LD graph: facets + mined product / api / see-also / availability edges. */ +function buildGraph(modules: KModule[], site: string) { + const indexedPaths = new Set(modules.map((m) => m.metadata.source_path)); + const uniq = (vals: string[]) => Array.from(new Set(vals.filter((v) => v && v.trim()))).sort(); + + const moduleNodes = modules.map((m) => ({ + "@id": `urn:module:${m.id}`, + "@type": "KnowledgeModule", + name: m.title || m.id, + description: m.summary || "", + status: m.status || "active", + priority: Number(m.priority || 0), + intents: m.intents, + audiences: m.audiences, + channels: m.channels, + framework: m.metadata.framework, + product: m.metadata.product, + version: m.metadata.version, + url: m.metadata.url, + lastVerified: m.last_verified || "", + })); + + const intents = uniq(modules.flatMap((m) => m.intents)); + const audiences = uniq(modules.flatMap((m) => m.audiences)); + const channels = uniq(modules.flatMap((m) => m.channels)); + const frameworks = uniq(modules.map((m) => m.metadata.framework)); + const products = uniq(modules.map((m) => m.metadata.product)); + const apiRefs = uniq(modules.flatMap((m) => m.api_refs)); + const docPaths = uniq(modules.map((m) => m.metadata.source_path)); + + const conceptNodes: any[] = [ + ...intents.map((v) => ({ "@id": `urn:intent:${v}`, "@type": "Intent", name: v })), + ...audiences.map((v) => ({ "@id": `urn:audience:${v}`, "@type": "Audience", name: v })), + ...channels.map((v) => ({ "@id": `urn:channel:${v}`, "@type": "Channel", name: v })), + ...frameworks.map((v) => ({ "@id": `urn:framework:${v}`, "@type": "Framework", name: v })), + ...products.map((v) => ({ "@id": `urn:product:${v}`, "@type": "Product", name: v })), + ...apiRefs.map((v) => ({ "@id": `urn:api:${v}`, "@type": "ApiReference", url: v })), + ...docPaths.map((v) => ({ "@id": `urn:doc:${v}`, "@type": "Doc", url: `${site}${v}` })), + ]; + + const edges: any[] = []; + const edgeSeen = new Set(); + const addEdge = (id: string, type: string, src: string, tgt: string) => { + if (edgeSeen.has(id)) return; + edgeSeen.add(id); + edges.push({ "@id": id, "@type": type, source: { "@id": src }, target: { "@id": tgt } }); + }; + + // product-level availability, aggregated across a product's modules + const available = new Map>(); // product -> frameworks present + const unavailable = new Map>(); // product -> frameworks with a "not available" stub + + for (const m of modules) { + const src = `urn:module:${m.id}`; + for (const v of m.intents) addEdge(`${src}#intent:${v}`, "HasIntent", src, `urn:intent:${v}`); + for (const v of m.audiences) addEdge(`${src}#audience:${v}`, "HasAudience", src, `urn:audience:${v}`); + for (const v of m.channels) addEdge(`${src}#channel:${v}`, "HasChannel", src, `urn:channel:${v}`); + if (m.metadata.framework) addEdge(`${src}#framework:${m.metadata.framework}`, "HasFramework", src, `urn:framework:${m.metadata.framework}`); + if (m.metadata.product) addEdge(`${src}#product:${m.metadata.product}`, "BelongsToProduct", src, `urn:product:${m.metadata.product}`); + for (const a of m.api_refs) addEdge(`${src}#api:${a}`, "CitesApi", src, `urn:api:${a}`); + for (const ref of m.references) { + if (indexedPaths.has(ref)) addEdge(`${src}#see:${ref}`, "SeeAlso", src, `urn:doc:${ref}`); + } + // record availability + const prod = m.metadata.product; + const fw = m.metadata.framework; + if (prod && fw) { + if (m.metadata.not_available) { + if (!unavailable.has(prod)) unavailable.set(prod, new Set()); + unavailable.get(prod)!.add(fw); + } else { + if (!available.has(prod)) available.set(prod, new Set()); + available.get(prod)!.add(fw); + } + } + } + + // product <-> framework availability edges (directly answers "what's available where") + for (const [prod, fws] of available) { + for (const fw of fws) addEdge(`urn:product:${prod}#avail:${fw}`, "AvailableOn", `urn:product:${prod}`, `urn:framework:${fw}`); + } + for (const [prod, fws] of unavailable) { + for (const fw of fws) addEdge(`urn:product:${prod}#navail:${fw}`, "NotAvailableOn", `urn:product:${prod}`, `urn:framework:${fw}`); + } + + return { + "@context": { + "@vocab": "https://docsops.scandit.com/schema#", + name: "http://schema.org/name", + description: "http://schema.org/description", + url: "http://schema.org/url", + status: "https://docsops.scandit.com/schema#status", + source: { "@id": "https://docsops.scandit.com/schema#source", "@type": "@id" }, + target: { "@id": "https://docsops.scandit.com/schema#target", "@type": "@id" }, + }, + "@graph": [...moduleNodes, ...conceptNodes, ...edges], + }; +} + +// --------------------------------------------------------------------------- +// filesystem walk +// --------------------------------------------------------------------------- +function walkHtml(dir: string, skipDir: (name: string) => boolean): string[] { + const out: string[] = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (skipDir(entry.name)) continue; + out.push(...walkHtml(full, skipDir)); + } else if (entry.isFile() && entry.name === "index.html") { + out.push(full); + } + } + return out; +} + +// --------------------------------------------------------------------------- +// plugin +// --------------------------------------------------------------------------- +export default function knowledgeExtractor(context: any, _options: any) { + const siteDir: string = context?.siteDir || process.cwd(); + return { + name: "knowledge-extractor", + async postBuild({ siteConfig, outDir }: { siteConfig: any; outDir: string }) { + try { + const site = String(siteConfig?.url || "").replace(/\/+$/, ""); + const sourceSite = site ? new URL(site).hostname.toLowerCase() : ""; + const updatedAt = new Date().toISOString(); + const version = "current"; + + // Index the CURRENT docs version only. Frozen versions (versions.json) + // are archived duplicates; the external API reference (data-capture-sdk) + // is a separate tool; *.html dirs are client-redirect stubs. + let frozenVersions: string[] = []; + try { + const parsed = JSON.parse(fs.readFileSync(path.join(siteDir, "versions.json"), "utf8")); + if (Array.isArray(parsed)) frozenVersions = parsed.map(String); + } catch { + /* no versions.json */ + } + const excluded = new Set([...frozenVersions, "data-capture-sdk", "assets", "img", "fonts", "search"]); + const skipDir = (name: string) => excluded.has(name) || name.endsWith(".html"); + + const files = walkHtml(outDir, skipDir); + const modules: KModule[] = []; + let pagesProcessed = 0; + + for (const file of files) { + let html: string; + try { + html = fs.readFileSync(file, "utf8"); + } catch { + continue; + } + const $ = cheerio.load(html); + const root = $("article .markdown").first().length + ? $("article .markdown").first() + : $(".theme-doc-markdown").first().length + ? $(".theme-doc-markdown").first() + : $("article").first(); + if (!root.length) continue; // not a doc page + + const relDir = path.relative(outDir, path.dirname(file)).split(path.sep).join("/"); + const pathname = relDir ? `/${relDir}/` : "/"; + const url = `${site}${pathname}`; + const title = ($("h1").first().text() || $("title").text() || "").replace(/​/g, "").trim(); + const description = ($('meta[name="description"]').attr("content") || "").trim(); + const bodyMd = extractMarkdownish($, root); + if (!bodyMd.trim()) continue; + + const chunks = chunkBody(bodyMd, CHUNK_TARGET_CHARS); + if (!chunks.length) continue; + pagesProcessed += 1; + + const framework = detectFramework(pathname); + const product = detectProduct(pathname); + const contentType = detectContentType(pathname, title); + const notAvailable = isAvailabilityStub(title, bodyMd); + chunks.forEach((chunk, i) => { + modules.push( + buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, contentType, version, updatedAt, notAvailable }), + ); + }); + } + + const active = modules.filter((m) => m.status === "active"); + const index = active.map(toIndexRecord); + const graph = buildGraph(active, site); + + const assetsDir = path.join(outDir, "assets"); + fs.mkdirSync(assetsDir, { recursive: true }); + fs.writeFileSync(path.join(assetsDir, "knowledge-retrieval-index.json"), JSON.stringify(index, null, 2) + "\n", "utf8"); + fs.writeFileSync(path.join(assetsDir, "knowledge-graph.jsonld"), JSON.stringify(graph, null, 2) + "\n", "utf8"); + + const edgeTypes: Record = {}; + for (const n of graph["@graph"] as any[]) { + if ("source" in n && "target" in n) edgeTypes[n["@type"]] = (edgeTypes[n["@type"]] || 0) + 1; + } + const edgeSummary = Object.entries(edgeTypes) + .map(([k, v]) => `${k}=${v}`) + .join(" "); + console.log( + `[knowledge-extractor] ${index.length} modules from ${pagesProcessed} pages | ` + + `graph: ${graph["@graph"].length} nodes | edges: ${edgeSummary} -> /assets/`, + ); + } catch (err) { + console.warn(`[knowledge-extractor] skipped (non-fatal): ${(err as Error)?.message || err}`); + } + }, + }; +} From 65676e09b3136796a1d103edfbb7f805d3a789b3 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 5 Aug 2026 12:50:05 +0100 Subject: [PATCH 02/53] ci: add docs retrieval-quality evals (success@k / precision@k / MRR) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Score the build-generated knowledge retrieval index against a gold set of real user queries (seeded from Algolia top-queries + the product taxonomy). A module is relevant when its URL matches the query's expected path-class, so the gold set stays stable even though module ids are auto-generated. - scripts/retrieval-evals/run.cjs — pure-Node token-mode scorer + thresholds - scripts/retrieval-evals/gold-set.json — 20 seed queries - .github/workflows/docs-retrieval-evals.yml — builds docs, runs the eval on PRs touching docs/extractor/gold-set; uploads the report - npm run docs:retrieval-evals Verified locally against 4523 modules: success@3=0.90, precision@3=0.87, MRR=0.86 (thresholds 0.8 / 0.6). The eval already surfaces real token-mode weaknesses (e.g. "data matrix" matching "matrixscan") — which is the point. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs-retrieval-evals.yml | 47 +++++++ package.json | 3 +- scripts/retrieval-evals/gold-set.json | 25 ++++ scripts/retrieval-evals/run.cjs | 149 +++++++++++++++++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs-retrieval-evals.yml create mode 100644 scripts/retrieval-evals/gold-set.json create mode 100644 scripts/retrieval-evals/run.cjs diff --git a/.github/workflows/docs-retrieval-evals.yml b/.github/workflows/docs-retrieval-evals.yml new file mode 100644 index 0000000000..e05d62b571 --- /dev/null +++ b/.github/workflows/docs-retrieval-evals.yml @@ -0,0 +1,47 @@ +name: Docs Retrieval Evals + +# Measures retrieval quality of the build-generated knowledge index against a +# gold set of real queries (success@k / precision@k / MRR). Runs on PRs that +# touch docs, the extractor, or the gold set — a regression in descriptions or +# partials that hurts retrieval fails here. + +on: + pull_request: + branches: + - main + paths: + - 'docs/**' + - 'src/plugins/knowledge-extractor/**' + - 'scripts/retrieval-evals/**' + - '.github/workflows/docs-retrieval-evals.yml' + +jobs: + retrieval-evals: + name: Docs Retrieval Evals + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'yarn' + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Build docs (generates the knowledge retrieval index) + run: yarn build + + - name: Run retrieval evals + run: node scripts/retrieval-evals/run.cjs --report reports/retrieval-evals.json + + - name: Upload eval report + if: always() + uses: actions/upload-artifact@v4 + with: + name: retrieval-evals-report + path: reports/retrieval-evals.json + if-no-files-found: ignore diff --git a/package.json b/package.json index 8baf78ff4e..ef2f714c03 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc" + "typecheck": "tsc", + "docs:retrieval-evals": "node scripts/retrieval-evals/run.cjs" }, "dependencies": { "@docusaurus/core": "^3.4.0", diff --git a/scripts/retrieval-evals/gold-set.json b/scripts/retrieval-evals/gold-set.json new file mode 100644 index 0000000000..7743f6cda2 --- /dev/null +++ b/scripts/retrieval-evals/gold-set.json @@ -0,0 +1,25 @@ +{ + "_comment": "Retrieval-eval gold set. Each entry is a real user query (seeded from Algolia top-queries + the product/intent taxonomy) mapped to the URL/path substrings a correct answer should live under. A retrieved knowledge module counts as relevant if its `url` contains ANY of `expect`. Add entries as new products/pages land; keep queries in the user's words.", + "queries": [ + { "query": "barcode capture get started", "expect": ["/barcode-capture/get-started"] }, + { "query": "sparkscan", "expect": ["/sparkscan/"] }, + { "query": "spark scan get started", "expect": ["/sparkscan/get-started", "/sparkscan/intro"] }, + { "query": "matrixscan count", "expect": ["/matrixscan-count/"] }, + { "query": "matrixscan find", "expect": ["/matrixscan-find/", "/matrixscan-pick/"] }, + { "query": "label capture", "expect": ["/label-capture/"] }, + { "query": "smart label capture", "expect": ["/label-capture/"] }, + { "query": "id capture supported documents", "expect": ["/id-capture/supported-documents", "/id-capture/", "id-documents"] }, + { "query": "id bolt", "expect": ["/id-bolt/"] }, + { "query": "parser", "expect": ["/parser/"] }, + { "query": "barcode selection", "expect": ["/barcode-selection/"] }, + { "query": "add the sdk", "expect": ["/add-sdk"] }, + { "query": "how to initialize data capture context", "expect": ["/add-sdk", "/get-started"] }, + { "query": "supported symbologies", "expect": ["barcode-symbologies", "symbology-properties"] }, + { "query": "qr code scanning", "expect": ["barcode-symbologies", "/barcode-capture/"] }, + { "query": "data matrix", "expect": ["barcode-symbologies"] }, + { "query": "ai powered barcode scanning", "expect": ["ai-powered-barcode-scanning"] }, + { "query": "scandit express inventory count", "expect": ["/express/", "inventory-count"] }, + { "query": "release notes", "expect": ["/release-notes"] }, + { "query": "agent skills", "expect": ["/agent-skills"] } + ] +} diff --git a/scripts/retrieval-evals/run.cjs b/scripts/retrieval-evals/run.cjs new file mode 100644 index 0000000000..c2d87834da --- /dev/null +++ b/scripts/retrieval-evals/run.cjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +"use strict"; +/** + * Retrieval-quality evals for the build-generated knowledge retrieval index + * (docs/assets/knowledge-retrieval-index.json, emitted by the knowledge-extractor + * plugin at build time). + * + * Scores each gold-set query with the same token-overlap ranking the index is + * designed for, then reports: + * - success@k : fraction of queries with a relevant module in the top k + * - precision@k : mean fraction of the top k that are relevant + * - MRR : mean reciprocal rank of the first relevant module + * + * A retrieved module is "relevant" when its `url` contains ANY of the query's + * `expect` substrings (a path-class, so the gold set stays stable even though + * module ids are auto-generated). Fails (exit 1) if success@k or MRR drops below + * the thresholds — so a regression in descriptions/partials is caught in CI. + * + * Adapted from the bundle's run_retrieval_evals.py (token mode), pure Node stdlib. + * + * Usage: node scripts/retrieval-evals/run.cjs [--index ] [--k 3] + * [--min-success 0.8] [--min-mrr 0.6] [--report ] + */ +const fs = require("fs"); +const path = require("path"); + +function arg(name, def) { + const i = process.argv.indexOf(`--${name}`); + return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : def; +} + +const INDEX = arg("index", "build/assets/knowledge-retrieval-index.json"); +const GOLD = arg("gold", path.join(__dirname, "gold-set.json")); +const K = parseInt(arg("k", "3"), 10); +const MIN_SUCCESS = parseFloat(arg("min-success", "0.8")); +const MIN_MRR = parseFloat(arg("min-mrr", "0.6")); +const REPORT = arg("report", ""); + +const TOKEN = /[a-z0-9]{2,}/gi; +const tokenize = (s) => new Set((String(s || "").toLowerCase().match(TOKEN) || [])); + +function docText(d) { + return [ + d.title, + d.summary, + d.docs_excerpt, + d.assistant_excerpt, + (d.keywords || []).join(" "), + (d.intents || []).join(" "), + ].join(" "); +} + +function score(queryTokens, doc) { + const dt = tokenize(docText(doc)); + if (!queryTokens.size || !dt.size) return 0; + let overlap = 0; + for (const t of queryTokens) if (dt.has(t)) overlap++; + return overlap / Math.sqrt(queryTokens.size * dt.size); +} + +function search(index, query, k) { + const qt = tokenize(query); + return index + .map((d) => ({ id: d.id, url: String(d.url || d.source_site || ""), s: score(qt, d) })) + .sort((a, b) => b.s - a.s || a.id.localeCompare(b.id)) + .slice(0, k); +} + +function relevant(hit, expect) { + const u = hit.url.toLowerCase(); + return expect.some((e) => u.includes(e.toLowerCase())); +} + +function main() { + if (!fs.existsSync(INDEX)) { + console.error(`retrieval-evals: index not found at ${INDEX} — run \`yarn build\` first.`); + process.exit(1); + } + const index = JSON.parse(fs.readFileSync(INDEX, "utf8")); + const gold = JSON.parse(fs.readFileSync(GOLD, "utf8")).queries || []; + if (!Array.isArray(index) || !index.length) { + console.error("retrieval-evals: empty or invalid index."); + process.exit(1); + } + if (!gold.length) { + console.error("retrieval-evals: empty gold set."); + process.exit(1); + } + + let successSum = 0, + precisionSum = 0, + rrSum = 0; + const rows = []; + for (const g of gold) { + const hits = search(index, g.query, K); + const rel = hits.map((h) => relevant(h, g.expect)); + const firstRel = rel.indexOf(true); + const success = firstRel !== -1 ? 1 : 0; + const precision = rel.filter(Boolean).length / Math.max(hits.length, 1); + const rr = firstRel !== -1 ? 1 / (firstRel + 1) : 0; + successSum += success; + precisionSum += precision; + rrSum += rr; + rows.push({ + query: g.query, + success, + precision: +precision.toFixed(3), + rr: +rr.toFixed(3), + top: hits.map((h) => h.url.replace(/^https?:\/\/[^/]+/, "")), + }); + } + + const n = gold.length; + const metrics = { + success_at_k: +(successSum / n).toFixed(4), + precision_at_k: +(precisionSum / n).toFixed(4), + mrr: +(rrSum / n).toFixed(4), + k: K, + query_count: n, + index_size: index.length, + }; + + console.log(`\nRetrieval evals (token mode, k=${K}, ${n} queries over ${index.length} modules)`); + console.log(` success@${K} = ${metrics.success_at_k} (min ${MIN_SUCCESS})`); + console.log(` precision@${K}= ${metrics.precision_at_k}`); + console.log(` MRR = ${metrics.mrr} (min ${MIN_MRR})\n`); + for (const r of rows) { + if (!r.success) console.log(` ✗ MISS "${r.query}" → top: ${r.top.join(" , ") || "(none)"}`); + } + + const breaches = []; + if (metrics.success_at_k < MIN_SUCCESS) breaches.push(`success@${K}=${metrics.success_at_k} < ${MIN_SUCCESS}`); + if (metrics.mrr < MIN_MRR) breaches.push(`MRR=${metrics.mrr} < ${MIN_MRR}`); + + if (REPORT) { + fs.mkdirSync(path.dirname(REPORT), { recursive: true }); + fs.writeFileSync(REPORT, JSON.stringify({ status: breaches.length ? "breach" : "ok", metrics, breaches, rows }, null, 2) + "\n"); + } + + if (breaches.length) { + console.log("Retrieval quality below threshold:"); + for (const b of breaches) console.log(` breach: ${b}`); + process.exit(1); + } + console.log("Retrieval quality OK."); + process.exit(0); +} + +main(); From d948727be1cf031866c89cc4870f4317c60c8777 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 5 Aug 2026 12:58:22 +0100 Subject: [PATCH 03/53] ci(retrieval-evals): add capped recall@k metric + threshold Report recall@k = relevant-in-top-k / min(total-relevant-in-corpus, k), with a --min-recall gate (default 0.6), alongside success@k / precision@k / MRR. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/retrieval-evals/run.cjs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/scripts/retrieval-evals/run.cjs b/scripts/retrieval-evals/run.cjs index c2d87834da..beea5e55d5 100644 --- a/scripts/retrieval-evals/run.cjs +++ b/scripts/retrieval-evals/run.cjs @@ -33,6 +33,7 @@ const INDEX = arg("index", "build/assets/knowledge-retrieval-index.json"); const GOLD = arg("gold", path.join(__dirname, "gold-set.json")); const K = parseInt(arg("k", "3"), 10); const MIN_SUCCESS = parseFloat(arg("min-success", "0.8")); +const MIN_RECALL = parseFloat(arg("min-recall", "0.6")); const MIN_MRR = parseFloat(arg("min-mrr", "0.6")); const REPORT = arg("report", ""); @@ -87,24 +88,37 @@ function main() { process.exit(1); } + // Total relevant modules per query in the whole corpus (denominator for + // capped recall). Path-class gold sets have many relevant docs, so recall@k + // is capped at k: hits / min(total_relevant, k) — 1.0 means every one of the + // top-k slots that could be relevant is. + const totalRelevant = (expect) => + index.reduce((c, d) => c + (relevant({ url: String(d.url || "") }, expect) ? 1 : 0), 0); + let successSum = 0, precisionSum = 0, + recallSum = 0, rrSum = 0; const rows = []; for (const g of gold) { const hits = search(index, g.query, K); const rel = hits.map((h) => relevant(h, g.expect)); const firstRel = rel.indexOf(true); + const relCount = rel.filter(Boolean).length; const success = firstRel !== -1 ? 1 : 0; - const precision = rel.filter(Boolean).length / Math.max(hits.length, 1); + const precision = relCount / Math.max(hits.length, 1); + const denom = Math.min(totalRelevant(g.expect), K) || 1; + const recall = relCount / denom; const rr = firstRel !== -1 ? 1 / (firstRel + 1) : 0; successSum += success; precisionSum += precision; + recallSum += recall; rrSum += rr; rows.push({ query: g.query, success, precision: +precision.toFixed(3), + recall: +recall.toFixed(3), rr: +rr.toFixed(3), top: hits.map((h) => h.url.replace(/^https?:\/\/[^/]+/, "")), }); @@ -114,6 +128,7 @@ function main() { const metrics = { success_at_k: +(successSum / n).toFixed(4), precision_at_k: +(precisionSum / n).toFixed(4), + recall_at_k: +(recallSum / n).toFixed(4), mrr: +(rrSum / n).toFixed(4), k: K, query_count: n, @@ -121,15 +136,17 @@ function main() { }; console.log(`\nRetrieval evals (token mode, k=${K}, ${n} queries over ${index.length} modules)`); - console.log(` success@${K} = ${metrics.success_at_k} (min ${MIN_SUCCESS})`); - console.log(` precision@${K}= ${metrics.precision_at_k}`); - console.log(` MRR = ${metrics.mrr} (min ${MIN_MRR})\n`); + console.log(` success@${K} = ${metrics.success_at_k} (min ${MIN_SUCCESS})`); + console.log(` precision@${K} = ${metrics.precision_at_k}`); + console.log(` recall@${K} = ${metrics.recall_at_k} (min ${MIN_RECALL})`); + console.log(` MRR = ${metrics.mrr} (min ${MIN_MRR})\n`); for (const r of rows) { if (!r.success) console.log(` ✗ MISS "${r.query}" → top: ${r.top.join(" , ") || "(none)"}`); } const breaches = []; if (metrics.success_at_k < MIN_SUCCESS) breaches.push(`success@${K}=${metrics.success_at_k} < ${MIN_SUCCESS}`); + if (metrics.recall_at_k < MIN_RECALL) breaches.push(`recall@${K}=${metrics.recall_at_k} < ${MIN_RECALL}`); if (metrics.mrr < MIN_MRR) breaches.push(`MRR=${metrics.mrr} < ${MIN_MRR}`); if (REPORT) { From 1748eaf7dbff5f1df636c57cc6161da4f8e1f5ca Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 5 Aug 2026 13:19:49 +0100 Subject: [PATCH 04/53] fix(ai-layer): declare cheerio, fail loud on empty extraction, atomic writes + all-docs eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review of PR #418: 1. cheerio was imported but undeclared — resolved only via npm hoisting from a transitive Docusaurus dep. A dedupe / lockfile regen / Docusaurus 3.x bump could drop it, failing config load before any lifecycle (so postBuild's try/catch can't help) and breaking the whole build. Declare cheerio@1.0.0-rc.12 and update both lockfiles. 2. Empty extraction could publish empty artifacts on a green build. postBuild now keeps per-page failures non-fatal but THROWS when 0 modules are extracted (matches onBrokenLinks:"throw"), so selector drift fails the build. 3. Both artifacts are written atomically (temp + rename) so a failure between the two writes can't ship a mismatched index/graph. Also: `--auto` eval mode does corpus-wide, page-level self-retrieval over ALL docs (page-success@3=0.82 over 572 pages), not just the 20-query gold set; recall@k added earlier. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 12 +- package.json | 1 + scripts/retrieval-evals/run.cjs | 78 +++++ src/plugins/knowledge-extractor/index.ts | 116 ++++--- yarn.lock | 384 +++++++++++++++-------- 5 files changed, 403 insertions(+), 188 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5bfeaf51a6..e77b07319d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@docusaurus/plugin-client-redirects": "3.4.0", "@docusaurus/preset-classic": "^3.4.0", "@mdx-js/react": "^3.0.0", + "cheerio": "1.0.0-rc.12", "classnames": "^2.5.1", "clsx": "^1.2.1", "docusaurus-plugin-sass": "^0.2.5", @@ -20,7 +21,8 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-player": "^2.16.0", - "sass": "^1.77.2" + "sass": "^1.77.2", + "search-insights": "^2.17.3" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.4.0", @@ -12918,10 +12920,10 @@ } }, "node_modules/search-insights": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.14.0.tgz", - "integrity": "sha512-OLN6MsPMCghDOqlCtsIsYgtsC0pnwVTyT9Mu6A3ewOj1DxvzZF6COrn2g86E/c05xbktB0XN04m/t1Z+n+fTGw==", - "peer": true + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "license": "MIT" }, "node_modules/section-matter": { "version": "1.0.0", diff --git a/package.json b/package.json index ef2f714c03..f4e1e5d8ac 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@docusaurus/plugin-client-redirects": "3.4.0", "@docusaurus/preset-classic": "^3.4.0", "@mdx-js/react": "^3.0.0", + "cheerio": "1.0.0-rc.12", "classnames": "^2.5.1", "clsx": "^1.2.1", "docusaurus-plugin-sass": "^0.2.5", diff --git a/scripts/retrieval-evals/run.cjs b/scripts/retrieval-evals/run.cjs index beea5e55d5..549f2ea98a 100644 --- a/scripts/retrieval-evals/run.cjs +++ b/scripts/retrieval-evals/run.cjs @@ -35,6 +35,13 @@ const K = parseInt(arg("k", "3"), 10); const MIN_SUCCESS = parseFloat(arg("min-success", "0.8")); const MIN_RECALL = parseFloat(arg("min-recall", "0.6")); const MIN_MRR = parseFloat(arg("min-mrr", "0.6")); +// --auto: corpus-wide self-retrieval over EVERY module (not just the 20-query +// gold set) — each module becomes a query built from its own title+summary and +// must retrieve itself in the top k. Measures coverage across all docs we +// create/edit. --auto-limit caps it; --min-auto-success gates it. +const AUTO = process.argv.includes("--auto"); +const AUTO_LIMIT = parseInt(arg("auto-limit", "0"), 10); +const MIN_AUTO_SUCCESS = parseFloat(arg("min-auto-success", "0")); const REPORT = arg("report", ""); const TOKEN = /[a-z0-9]{2,}/gi; @@ -83,6 +90,77 @@ function main() { console.error("retrieval-evals: empty or invalid index."); process.exit(1); } + if (AUTO) { + // Corpus-wide, PAGE-LEVEL self-retrieval over every doc: for each page, + // query with a chunk's own title+summary and check that a chunk from the + // SAME page (same url) lands in the top k. Page-level (not exact-chunk) is + // the meaningful coverage metric here, because the index holds many + // near-duplicate chunks per page (shared prose across frameworks, "Part N" + // splits) — so "did we surface the right page for this doc's content?" is + // what matters, not "did this exact chunk outrank its own siblings". + const docs = index.map((m) => ({ + id: String(m.id || ""), + url: String(m.url || ""), + tokens: tokenize(docText(m)), + query: [m.title, m.summary].filter(Boolean).join(" ").trim() || String(m.id || ""), + })); + // One representative query per unique page (first chunk seen). + const seen = new Set(); + let rows = docs.filter((d) => d.url && !seen.has(d.url) && seen.add(d.url)); + if (AUTO_LIMIT > 0) rows = rows.slice(0, AUTO_LIMIT); + const fastScore = (qt, dt) => { + if (!qt.size || !dt.size) return 0; + let o = 0; + for (const t of qt) if (dt.has(t)) o++; + return o / Math.sqrt(qt.size * dt.size); + }; + let ok = 0, + rrSum = 0; + const misses = []; + for (const r of rows) { + const qt = tokenize(r.query); + // best score among chunks of the SAME page + let bestSame = 0; + for (const d of docs) if (d.url === r.url) bestSame = Math.max(bestSame, fastScore(qt, d.tokens)); + // rank of that best same-page chunk = # of OTHER-page chunks scoring higher + let better = 0; + for (const d of docs) { + if (d.url === r.url) continue; + if (fastScore(qt, d.tokens) > bestSame) { + better++; + if (better >= K) break; + } + } + if (better < K) { + ok++; + rrSum += 1 / (better + 1); + } else { + misses.push(r.url.replace(/^https?:\/\/[^/]+/, "")); + } + } + const metrics = { + mode: "auto-page-self-retrieval", + pages: rows.length, + modules: docs.length, + k: K, + page_success_at_k: +(ok / rows.length).toFixed(4), + page_mrr: +(rrSum / rows.length).toFixed(4), + }; + console.log(`\nRetrieval self-eval (AUTO, page-level over all docs): ${rows.length} pages / ${docs.length} modules, k=${K}`); + console.log(` page-success@${K} = ${metrics.page_success_at_k} (min ${MIN_AUTO_SUCCESS})`); + console.log(` page-MRR = ${metrics.page_mrr}`); + console.log(` ${misses.length} page(s) not surfaced in top ${K} by their own content.`); + misses.slice(0, 10).forEach((m) => console.log(` ✗ ${m}`)); + if (REPORT) { + fs.mkdirSync(path.dirname(REPORT), { recursive: true }); + fs.writeFileSync( + REPORT, + JSON.stringify({ status: metrics.page_success_at_k < MIN_AUTO_SUCCESS ? "breach" : "ok", metrics, misses: misses.slice(0, 200) }, null, 2) + "\n", + ); + } + process.exit(metrics.page_success_at_k < MIN_AUTO_SUCCESS ? 1 : 0); + } + if (!gold.length) { console.error("retrieval-evals: empty gold set."); process.exit(1); diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index 53795c7dfa..90999fa143 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -523,36 +523,33 @@ export default function knowledgeExtractor(context: any, _options: any) { return { name: "knowledge-extractor", async postBuild({ siteConfig, outDir }: { siteConfig: any; outDir: string }) { + const site = String(siteConfig?.url || "").replace(/\/+$/, ""); + const sourceSite = site ? new URL(site).hostname.toLowerCase() : ""; + const updatedAt = new Date().toISOString(); + const version = "current"; + + // Index the CURRENT docs version only. Frozen versions (versions.json) + // are archived duplicates; the external API reference (data-capture-sdk) + // is a separate tool; *.html dirs are client-redirect stubs. + let frozenVersions: string[] = []; try { - const site = String(siteConfig?.url || "").replace(/\/+$/, ""); - const sourceSite = site ? new URL(site).hostname.toLowerCase() : ""; - const updatedAt = new Date().toISOString(); - const version = "current"; - - // Index the CURRENT docs version only. Frozen versions (versions.json) - // are archived duplicates; the external API reference (data-capture-sdk) - // is a separate tool; *.html dirs are client-redirect stubs. - let frozenVersions: string[] = []; + const parsed = JSON.parse(fs.readFileSync(path.join(siteDir, "versions.json"), "utf8")); + if (Array.isArray(parsed)) frozenVersions = parsed.map(String); + } catch { + /* no versions.json */ + } + const excluded = new Set([...frozenVersions, "data-capture-sdk", "assets", "img", "fonts", "search"]); + const skipDir = (name: string) => excluded.has(name) || name.endsWith(".html"); + + const files = walkHtml(outDir, skipDir); + const modules: KModule[] = []; + let pagesProcessed = 0; + let pageErrors = 0; + + for (const file of files) { + // Per-page failures are non-fatal — skip the one bad page, keep going. try { - const parsed = JSON.parse(fs.readFileSync(path.join(siteDir, "versions.json"), "utf8")); - if (Array.isArray(parsed)) frozenVersions = parsed.map(String); - } catch { - /* no versions.json */ - } - const excluded = new Set([...frozenVersions, "data-capture-sdk", "assets", "img", "fonts", "search"]); - const skipDir = (name: string) => excluded.has(name) || name.endsWith(".html"); - - const files = walkHtml(outDir, skipDir); - const modules: KModule[] = []; - let pagesProcessed = 0; - - for (const file of files) { - let html: string; - try { - html = fs.readFileSync(file, "utf8"); - } catch { - continue; - } + const html = fs.readFileSync(file, "utf8"); const $ = cheerio.load(html); const root = $("article .markdown").first().length ? $("article .markdown").first() @@ -582,31 +579,52 @@ export default function knowledgeExtractor(context: any, _options: any) { buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, contentType, version, updatedAt, notAvailable }), ); }); + } catch (err) { + pageErrors += 1; + console.warn(`[knowledge-extractor] skipped page ${path.relative(outDir, file)}: ${(err as Error)?.message || err}`); } + } - const active = modules.filter((m) => m.status === "active"); - const index = active.map(toIndexRecord); - const graph = buildGraph(active, site); - - const assetsDir = path.join(outDir, "assets"); - fs.mkdirSync(assetsDir, { recursive: true }); - fs.writeFileSync(path.join(assetsDir, "knowledge-retrieval-index.json"), JSON.stringify(index, null, 2) + "\n", "utf8"); - fs.writeFileSync(path.join(assetsDir, "knowledge-graph.jsonld"), JSON.stringify(graph, null, 2) + "\n", "utf8"); - - const edgeTypes: Record = {}; - for (const n of graph["@graph"] as any[]) { - if ("source" in n && "target" in n) edgeTypes[n["@type"]] = (edgeTypes[n["@type"]] || 0) + 1; - } - const edgeSummary = Object.entries(edgeTypes) - .map(([k, v]) => `${k}=${v}`) - .join(" "); - console.log( - `[knowledge-extractor] ${index.length} modules from ${pagesProcessed} pages | ` + - `graph: ${graph["@graph"].length} nodes | edges: ${edgeSummary} -> /assets/`, + const active = modules.filter((m) => m.status === "active"); + const index = active.map(toIndexRecord); + const graph = buildGraph(active, site); + + // Fail LOUD on empty extraction — matches the config's onBrokenLinks:"throw" + // convention. "Non-fatal" covers one bad page, not "extracted nothing at + // all": selector drift (theme upgrade renames .markdown/.theme-doc-markdown) + // must not silently publish an empty index + node-less graph over a green + // build. Throwing here fails `docusaurus build`, so the regression is seen. + if (pagesProcessed === 0 || index.length === 0) { + throw new Error( + `[knowledge-extractor] extracted 0 modules from ${files.length} HTML file(s) ` + + `(${pageErrors} page error(s)). Page selectors likely drifted — refusing to ` + + `overwrite the AI-layer artifacts with empty output.`, ); - } catch (err) { - console.warn(`[knowledge-extractor] skipped (non-fatal): ${(err as Error)?.message || err}`); } + + // Write both artifacts atomically: emit to temp files, then rename, so a + // failure between the two writes can never ship an index without a matching + // graph (or vice versa). + const assetsDir = path.join(outDir, "assets"); + fs.mkdirSync(assetsDir, { recursive: true }); + const idxPath = path.join(assetsDir, "knowledge-retrieval-index.json"); + const graphPath = path.join(assetsDir, "knowledge-graph.jsonld"); + fs.writeFileSync(idxPath + ".tmp", JSON.stringify(index, null, 2) + "\n", "utf8"); + fs.writeFileSync(graphPath + ".tmp", JSON.stringify(graph, null, 2) + "\n", "utf8"); + fs.renameSync(idxPath + ".tmp", idxPath); + fs.renameSync(graphPath + ".tmp", graphPath); + + const edgeTypes: Record = {}; + for (const n of graph["@graph"] as any[]) { + if ("source" in n && "target" in n) edgeTypes[n["@type"]] = (edgeTypes[n["@type"]] || 0) + 1; + } + const edgeSummary = Object.entries(edgeTypes) + .map(([k, v]) => `${k}=${v}`) + .join(" "); + console.log( + `[knowledge-extractor] ${index.length} modules from ${pagesProcessed} pages ` + + `(${pageErrors} page error(s)) | graph: ${graph["@graph"].length} nodes | edges: ${edgeSummary} -> /assets/`, + ); }, }; } diff --git a/yarn.lock b/yarn.lock index 24f98e4da0..01689fe6d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,6 +41,11 @@ resolved "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.20.0.tgz" integrity sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ== +"@algolia/cache-common@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz" + integrity sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g== + "@algolia/cache-in-memory@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.20.0.tgz" @@ -75,6 +80,14 @@ "@algolia/requester-common" "4.20.0" "@algolia/transporter" "4.20.0" +"@algolia/client-common@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz" + integrity sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA== + dependencies: + "@algolia/requester-common" "4.24.0" + "@algolia/transporter" "4.24.0" + "@algolia/client-personalization@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.20.0.tgz" @@ -84,6 +97,15 @@ "@algolia/requester-common" "4.20.0" "@algolia/transporter" "4.20.0" +"@algolia/client-search@>= 4.9.1 < 6": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz" + integrity sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA== + dependencies: + "@algolia/client-common" "4.24.0" + "@algolia/requester-common" "4.24.0" + "@algolia/transporter" "4.24.0" + "@algolia/client-search@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.20.0.tgz" @@ -103,6 +125,11 @@ resolved "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.20.0.tgz" integrity sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ== +"@algolia/logger-common@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz" + integrity sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA== + "@algolia/logger-console@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.20.0.tgz" @@ -122,6 +149,11 @@ resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.20.0.tgz" integrity sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng== +"@algolia/requester-common@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz" + integrity sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA== + "@algolia/requester-node-http@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.20.0.tgz" @@ -138,6 +170,15 @@ "@algolia/logger-common" "4.20.0" "@algolia/requester-common" "4.20.0" +"@algolia/transporter@4.24.0": + version "4.24.0" + resolved "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz" + integrity sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA== + dependencies: + "@algolia/cache-common" "4.24.0" + "@algolia/logger-common" "4.24.0" + "@algolia/requester-common" "4.24.0" + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" @@ -159,7 +200,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz" integrity sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA== -"@babel/core@^7.21.3", "@babel/core@^7.23.3": +"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.21.3", "@babel/core@^7.23.3", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0": version "7.24.3" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz" integrity sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ== @@ -1211,7 +1252,7 @@ "@docsearch/css" "3.5.2" algoliasearch "^4.19.1" -"@docusaurus/core@3.4.0", "@docusaurus/core@^3.4.0": +"@docusaurus/core@^2.0.0-beta || ^3.0.0-alpha", "@docusaurus/core@^3.0.0", "@docusaurus/core@^3.4.0", "@docusaurus/core@3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz" integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== @@ -1333,7 +1374,7 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@3.4.0", "@docusaurus/module-type-aliases@^3.4.0": +"@docusaurus/module-type-aliases@^3.4.0", "@docusaurus/module-type-aliases@3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz" integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== @@ -1584,7 +1625,7 @@ resolved "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.4.0.tgz" integrity sha512-0qENiJ+TRaeTzcg4olrnh0BQ7eCxTgbYWBnWUeQDc84UYkt/T3pDNnm3SiQkqPb+YQ1qtYFlC0RriAElclo8Dg== -"@docusaurus/types@3.4.0", "@docusaurus/types@^3.4.0": +"@docusaurus/types@*", "@docusaurus/types@^3.4.0", "@docusaurus/types@3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz" integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== @@ -1766,7 +1807,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -1900,7 +1941,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" "@svgr/babel-plugin-transform-svg-component" "8.0.0" -"@svgr/core@8.1.0": +"@svgr/core@*", "@svgr/core@8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== @@ -2211,7 +2252,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*": +"@types/react@*", "@types/react@>= 16.8.0 < 19.0.0", "@types/react@>=16": version "18.3.3" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz" integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== @@ -2296,7 +2337,7 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": +"@webassemblyjs/ast@^1.11.5", "@webassemblyjs/ast@1.11.6": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz" integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== @@ -2397,7 +2438,7 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" -"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": +"@webassemblyjs/wasm-parser@^1.11.5", "@webassemblyjs/wasm-parser@1.11.6": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz" integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== @@ -2450,7 +2491,7 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz" integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== -acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: version "8.11.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz" integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== @@ -2475,7 +2516,12 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: +ajv-keywords@^3.4.1: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2487,7 +2533,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.2, ajv@^6.12.5: +ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2497,7 +2543,7 @@ ajv@^6.12.2, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -2514,7 +2560,7 @@ algoliasearch-helper@^3.13.3: dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1: +algoliasearch@^4.18.0, algoliasearch@^4.19.1, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6": version "4.20.0" resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.20.0.tgz" integrity sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g== @@ -2600,16 +2646,16 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - array-flatten@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -2784,7 +2830,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.23.0: +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0": version "4.23.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -2938,7 +2984,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12: +cheerio@^1.0.0-rc.12, cheerio@1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -2951,7 +2997,7 @@ cheerio@^1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: +chokidar@^3.4.2, chokidar@^3.5.3, "chokidar@>=3.0.0 <4.0.0": version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3045,16 +3091,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - color-name@~1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + colord@^2.9.3: version "2.9.3" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" @@ -3425,20 +3471,27 @@ debounce@^1.2.1: resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@2.6.9, debug@^2.6.0: +debug@^2.6.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" @@ -3512,16 +3565,16 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -depd@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + dequal@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" @@ -4023,7 +4076,7 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" -file-loader@^6.2.0: +file-loader@*, file-loader@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== @@ -4170,11 +4223,6 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -4316,16 +4364,16 @@ got@^12.1.0: p-cancelable "^3.0.0" responselike "^3.0.0" -graceful-fs@4.2.10: - version "4.2.10" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graceful-fs@4.2.10: + version "4.2.10" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz" @@ -4616,6 +4664,16 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -4627,16 +4685,6 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -4745,7 +4793,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4755,16 +4803,16 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: version "1.3.8" resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== +ini@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" @@ -4782,16 +4830,16 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - ipaddr.js@^2.0.1: version "2.1.0" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" @@ -4967,16 +5015,16 @@ is-yarn-global@^0.4.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz" integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== - isarray@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -5951,7 +5999,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": +"mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -5961,14 +6009,40 @@ mime-db@~1.33.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz" integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== -mime-types@2.1.18, mime-types@~2.1.17: +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^2.1.31: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@~2.1.17, mime-types@2.1.18: version "2.1.18" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz" integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== dependencies: mime-db "~1.33.0" -mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@~2.1.24: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -6007,7 +6081,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6385,6 +6459,13 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" @@ -6395,13 +6476,6 @@ path-to-regexp@2.2.1: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -6720,7 +6794,7 @@ postcss-zindex@^6.0.2: resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz" integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== -postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38: +"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.2, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.31, postcss@^8.4.38: version "8.4.38" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -6843,16 +6917,21 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" - integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== +range-parser@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -range-parser@^1.2.1, range-parser@~1.2.1: +range-parser@~1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== +range-parser@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" + integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== + raw-body@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" @@ -6903,7 +6982,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@^18.0.0: +react-dom@*, "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", react-dom@^18.0.0, "react-dom@>= 16.8.0 < 19.0.0": version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -6949,7 +7028,7 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -"react-loadable@npm:@docusaurus/react-loadable@6.0.0": +react-loadable@*, "react-loadable@npm:@docusaurus/react-loadable@6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz" integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== @@ -6987,7 +7066,7 @@ react-router-dom@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.3.4, react-router@^5.3.4: +react-router@^5.3.4, react-router@>=5, react-router@5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== @@ -7002,7 +7081,7 @@ react-router@5.3.4, react-router@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@^18.0.0: +react@*, "react@^16.13.1 || ^17.0.0 || ^18.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, react@^18.2.0, "react@>= 16.8.0 < 19.0.0", react@>=15, react@>=16, react@>=16.0.0, react@>=16.6.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -7305,15 +7384,20 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" @@ -7331,7 +7415,7 @@ sass-loader@^10.1.1: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.77.2: +sass@^1.3.0, sass@^1.30.0, sass@^1.77.2: version "1.77.2" resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz" integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== @@ -7352,16 +7436,25 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== +schema-utils@^3.0.0: + version "3.3.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^3.1.1: + version "3.3.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" -schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: +schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -7380,9 +7473,18 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -search-insights@^2.17.3: +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +search-insights@^2.17.3, "search-insights@>= 1 < 3": version "2.17.3" - resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.3.tgz#8faea5d20507bf348caba0724e5386862847b661" + resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz" integrity sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== section-matter@^1.0.0: @@ -7623,7 +7725,7 @@ sort-css-media-queries@2.2.0: resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz" integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: +source-map-js@^1.0.1, source-map-js@^1.2.0, "source-map-js@>=0.6.2 <2.0.0": version "1.2.0" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== @@ -7636,7 +7738,7 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@~0.6.0: +source-map@^0.6.0: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -7646,6 +7748,11 @@ source-map@^0.7.0: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== +source-map@~0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" @@ -7684,22 +7791,45 @@ srcset@^4.0.0: resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz" integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - "statuses@>= 1.4.0 < 2": version "1.5.0" resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + std-env@^3.0.1: version "3.5.0" resolved "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz" integrity sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA== -string-width@^4.1.0, string-width@^4.2.0: +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7717,20 +7847,6 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - stringify-entities@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" @@ -7954,7 +8070,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@~5.2.2: +"typescript@>= 2.7", typescript@>=4.9.5, typescript@~5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== @@ -8070,7 +8186,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@1.0.0, unpipe@~1.0.0: +unpipe@~1.0.0, unpipe@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -8279,7 +8395,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -webpack@^5.88.1: +"webpack@^4.0.0 || ^5.0.0", "webpack@^4.36.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.88.1, "webpack@>= 4", "webpack@>=4.41.1 || 5.x", webpack@>=5, "webpack@3 || 4 || 5": version "5.89.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz" integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== @@ -8319,7 +8435,7 @@ webpackbar@^5.0.2: pretty-time "^1.1.0" std-env "^3.0.1" -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: +websocket-driver@^0.7.4, websocket-driver@>=0.5.1: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== From 972e6cbc937103a7e0496902391cb46303646ca4 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Wed, 5 Aug 2026 13:53:30 +0100 Subject: [PATCH 05/53] fix(retrieval-evals): drop redundant recall (= precision here), add push trigger Follow-up to review of PR #418: - recall@k was algebraically identical to precision@k for this gold set: it uses path-classes (>K relevant modules each), so capped recall (relCount/min(total,k)) reduces to precision (relCount/k), query by query. Reporting both was one signal twice, and --min-recall could only fail when precision already had. Removed the recall metric/gate; report precision@k with an honestly-named MIN_PRECISION floor. A real recall needs single-page gold entries (noted in code). - Run the eval on push to main too, so the deploy branch's index is checked (the plugin's own fail-loud floor already fails every build on empty extraction; this adds the quality check on main). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs-retrieval-evals.yml | 8 +++++- scripts/retrieval-evals/run.cjs | 31 ++++++++++------------ 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docs-retrieval-evals.yml b/.github/workflows/docs-retrieval-evals.yml index e05d62b571..8e8b26ef34 100644 --- a/.github/workflows/docs-retrieval-evals.yml +++ b/.github/workflows/docs-retrieval-evals.yml @@ -3,7 +3,10 @@ name: Docs Retrieval Evals # Measures retrieval quality of the build-generated knowledge index against a # gold set of real queries (success@k / precision@k / MRR). Runs on PRs that # touch docs, the extractor, or the gold set — a regression in descriptions or -# partials that hurts retrieval fails here. +# partials that hurts retrieval fails here. Also runs on push to main, so the +# deploy branch is checked too (an empty index from a cause outside the PR +# paths — e.g. a Docusaurus upgrade renaming article selectors — is caught here +# as well as by the plugin's own fail-loud floor check during the build). on: pull_request: @@ -14,6 +17,9 @@ on: - 'src/plugins/knowledge-extractor/**' - 'scripts/retrieval-evals/**' - '.github/workflows/docs-retrieval-evals.yml' + push: + branches: + - main jobs: retrieval-evals: diff --git a/scripts/retrieval-evals/run.cjs b/scripts/retrieval-evals/run.cjs index 549f2ea98a..635ffb2c31 100644 --- a/scripts/retrieval-evals/run.cjs +++ b/scripts/retrieval-evals/run.cjs @@ -33,7 +33,7 @@ const INDEX = arg("index", "build/assets/knowledge-retrieval-index.json"); const GOLD = arg("gold", path.join(__dirname, "gold-set.json")); const K = parseInt(arg("k", "3"), 10); const MIN_SUCCESS = parseFloat(arg("min-success", "0.8")); -const MIN_RECALL = parseFloat(arg("min-recall", "0.6")); +const MIN_PRECISION = parseFloat(arg("min-precision", "0.6")); const MIN_MRR = parseFloat(arg("min-mrr", "0.6")); // --auto: corpus-wide self-retrieval over EVERY module (not just the 20-query // gold set) — each module becomes a query built from its own title+summary and @@ -166,16 +166,19 @@ function main() { process.exit(1); } - // Total relevant modules per query in the whole corpus (denominator for - // capped recall). Path-class gold sets have many relevant docs, so recall@k - // is capped at k: hits / min(total_relevant, k) — 1.0 means every one of the - // top-k slots that could be relevant is. - const totalRelevant = (expect) => - index.reduce((c, d) => c + (relevant({ url: String(d.url || "") }, expect) ? 1 : 0), 0); - + // NOTE on recall: this gold set uses path-classes (a query maps to "any page + // under /sparkscan/", not one specific page), and every such class has more + // than K relevant modules in the corpus. A capped recall@k + // (relCount / min(totalRelevant, k)) therefore reduces algebraically to + // precision@k (relCount / k) — the same number query by query, not just on + // average. Reporting both would be one signal printed twice, and a + // "min-recall" gate could only ever fail when precision already had. So we + // report precision@k with an honestly-named MIN_PRECISION floor and DO NOT + // report a redundant recall. A real recall metric needs single-page gold + // entries (small, exact totalRelevant) or an uncapped denominator at a large + // k — a future gold-set change, not a rename. let successSum = 0, precisionSum = 0, - recallSum = 0, rrSum = 0; const rows = []; for (const g of gold) { @@ -185,18 +188,14 @@ function main() { const relCount = rel.filter(Boolean).length; const success = firstRel !== -1 ? 1 : 0; const precision = relCount / Math.max(hits.length, 1); - const denom = Math.min(totalRelevant(g.expect), K) || 1; - const recall = relCount / denom; const rr = firstRel !== -1 ? 1 / (firstRel + 1) : 0; successSum += success; precisionSum += precision; - recallSum += recall; rrSum += rr; rows.push({ query: g.query, success, precision: +precision.toFixed(3), - recall: +recall.toFixed(3), rr: +rr.toFixed(3), top: hits.map((h) => h.url.replace(/^https?:\/\/[^/]+/, "")), }); @@ -206,7 +205,6 @@ function main() { const metrics = { success_at_k: +(successSum / n).toFixed(4), precision_at_k: +(precisionSum / n).toFixed(4), - recall_at_k: +(recallSum / n).toFixed(4), mrr: +(rrSum / n).toFixed(4), k: K, query_count: n, @@ -215,8 +213,7 @@ function main() { console.log(`\nRetrieval evals (token mode, k=${K}, ${n} queries over ${index.length} modules)`); console.log(` success@${K} = ${metrics.success_at_k} (min ${MIN_SUCCESS})`); - console.log(` precision@${K} = ${metrics.precision_at_k}`); - console.log(` recall@${K} = ${metrics.recall_at_k} (min ${MIN_RECALL})`); + console.log(` precision@${K} = ${metrics.precision_at_k} (min ${MIN_PRECISION})`); console.log(` MRR = ${metrics.mrr} (min ${MIN_MRR})\n`); for (const r of rows) { if (!r.success) console.log(` ✗ MISS "${r.query}" → top: ${r.top.join(" , ") || "(none)"}`); @@ -224,7 +221,7 @@ function main() { const breaches = []; if (metrics.success_at_k < MIN_SUCCESS) breaches.push(`success@${K}=${metrics.success_at_k} < ${MIN_SUCCESS}`); - if (metrics.recall_at_k < MIN_RECALL) breaches.push(`recall@${K}=${metrics.recall_at_k} < ${MIN_RECALL}`); + if (metrics.precision_at_k < MIN_PRECISION) breaches.push(`precision@${K}=${metrics.precision_at_k} < ${MIN_PRECISION}`); if (metrics.mrr < MIN_MRR) breaches.push(`MRR=${metrics.mrr} < ${MIN_MRR}`); if (REPORT) { From 84ae98cc5b39ed2357d85b10dbf258788e3bed36 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Thu, 6 Aug 2026 11:08:01 +0100 Subject: [PATCH 06/53] feat: ingest curated frontmatter into the AI retrieval index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read each page's source frontmatter (gray-matter) and flow user_intents, not_for, product, topic_type, canonical_id into every chunk — product/topic_type now authoritative over path heuristics, user_intents injected into assistant_context, semantic_status flips rule_based->frontmatter. Falls back to today's heuristics when a page has no mapped source or no fields, so extraction never breaks (verified: 4523 modules/572 pages/identical edge counts). Declares gray-matter (already a Docusaurus transitive dep) explicitly. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 1 + src/plugins/knowledge-extractor/index.ts | 100 +++++- yarn.lock | 384 ++++++++--------------- 3 files changed, 229 insertions(+), 256 deletions(-) diff --git a/package.json b/package.json index f4e1e5d8ac..685014773f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "classnames": "^2.5.1", "clsx": "^1.2.1", "docusaurus-plugin-sass": "^0.2.5", + "gray-matter": "4.0.3", "prism-react-renderer": "^2.1.0", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index 90999fa143..209a83ae57 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -25,6 +25,7 @@ import fs from "node:fs"; import path from "node:path"; import * as cheerio from "cheerio"; +import matter from "gray-matter"; const CHUNK_TARGET_CHARS = 1400; const OWNER = "docsops-auto"; @@ -281,6 +282,63 @@ function classifyLinks(chunkMarkdown: string, site: string): { internal: string[ return { internal: Array.from(internal), api: Array.from(api) }; } +// --------------------------------------------------------------------------- +// frontmatter ingestion — the CURATED signal, read straight from source .md +// (the rendered HTML only carries description/keywords/title, so the rich +// extended-schema fields must be read from the source frontmatter itself) +// --------------------------------------------------------------------------- +const TOPIC_TYPE_TO_CONTENT: Record = { + "get-started": "tutorial", + tutorial: "tutorial", + "how-to": "how-to", + howto: "how-to", + reference: "reference", + concept: "concept", + about: "concept", + overview: "concept", + troubleshooting: "troubleshooting", +}; + +function fmStringArray(v: unknown): string[] { + if (Array.isArray(v)) return v.map((x) => String(x).trim()).filter(Boolean); + if (typeof v === "string" && v.trim()) return [v.trim()]; + return []; +} + +function fmFirst(v: unknown): string { + if (Array.isArray(v)) return v.length ? String(v[0]).trim() : ""; + return v == null ? "" : String(v).trim(); +} + +/** + * Best-effort: map a built page pathname back to its CURRENT-version source + * markdown and return the parsed frontmatter. Returns {} when there is no 1:1 + * source file (custom `slug:`, generated category page, redirect stub) — callers + * then fall back to path/heuristic derivation, so this NEVER breaks extraction. + */ +function readFrontMatter(siteDir: string, pathname: string): Record { + const rel = pathname.replace(/^\/+|\/+$/g, ""); + const base = path.join(siteDir, "docs"); + const candidates = rel + ? [ + path.join(base, `${rel}.md`), + path.join(base, `${rel}.mdx`), + path.join(base, rel, "index.md"), + path.join(base, rel, "index.mdx"), + path.join(base, rel, "README.md"), + ] + : [path.join(base, "index.md"), path.join(base, "intro.md"), path.join(base, "index.mdx")]; + for (const file of candidates) { + try { + if (!fs.existsSync(file)) continue; + return (matter(fs.readFileSync(file, "utf8")).data || {}) as Record; + } catch { + /* unreadable / malformed frontmatter — try next candidate, else heuristics */ + } + } + return {}; +} + function buildModule(args: { pathname: string; url: string; @@ -296,16 +354,28 @@ function buildModule(args: { version: string; updatedAt: string; notAvailable: boolean; + fm: Record; }) { - const { pathname, url, sourceSite, site, title: rawTitle, description, chunk, idx, framework, product, contentType, version, updatedAt, notAvailable } = args; + const { pathname, url, sourceSite, site, title: rawTitle, description, chunk, idx, framework, product, contentType, version, updatedAt, notAvailable, fm } = args; const chunkClean = chunk.content.trim(); const title = (rawTitle || pathname).trim(); const displayTitle = idx === 1 ? title : `${title} (Part ${idx})`; const summary = extractSummary(description, chunkClean); const resolvedHeading = (chunk.heading || displayTitle).trim(); const links = classifyLinks(chunkClean, site); + // Curated signal read from the page's own frontmatter (empty when absent). + const userIntents = fmStringArray(fm.user_intents); + const notFor = fmStringArray(fm.not_for); + const canonicalId = fmFirst(fm.canonical_id); + const topicType = fmFirst(fm.topic_type); + const fmKeywords = fmStringArray(fm.keywords); + const curatedApplied = Boolean(userIntents.length || fmFirst(fm.product) || topicType); + + const intentLine = userIntents.length ? `User intents: ${userIntents.join("; ")}. ` : ""; + const notForLine = notFor.length ? `Not for: ${notFor.join("; ")}. ` : ""; const assistantContext = `Use this module when answering questions related to: ${displayTitle}. ` + + `${intentLine}${notForLine}` + `Source path: ${pathname}. ` + `Summary: ${summary}\n\n${chunkClean}`; const moduleId = slug(`auto-${pathname}-${idx}`); @@ -316,7 +386,9 @@ function buildModule(args: { new Set(["auto-extracted", contentType || "docs", framework, product, stemSlug].filter(Boolean)), ).sort(); const keywords = Array.from( - new Set([stemSlug.replace(/-/g, " "), product.replace(/-/g, " "), contentType || "docs", framework.replace(/-/g, " ")].filter(Boolean)), + new Set( + [stemSlug.replace(/-/g, " "), product.replace(/-/g, " "), contentType || "docs", framework.replace(/-/g, " "), ...fmKeywords].filter(Boolean), + ), ).sort(); return { id: moduleId, @@ -331,6 +403,8 @@ function buildModule(args: { last_verified: updatedAt.slice(0, 10), dependencies: [] as string[], tags, + user_intents: userIntents, + not_for: notFor, metadata: { url, title: displayTitle.slice(0, 90), @@ -341,6 +415,8 @@ function buildModule(args: { updated_at: updatedAt, source_site: sourceSite, source_path: pathname, + canonical_id: canonicalId, + topic_type: topicType, not_available: notAvailable, }, semantic: { @@ -348,7 +424,7 @@ function buildModule(args: { intent: intents[0], audience: audiences[0], keywords, - status: "rule_based", + status: curatedApplied ? "frontmatter" : "rule_based", }, references: links.internal.slice(0, 20), api_refs: links.api.slice(0, 20), @@ -395,6 +471,10 @@ function toIndexRecord(m: KModule) { semantic_intent: m.semantic.intent, semantic_audience: m.semantic.audience, keywords: m.semantic.keywords, + user_intents: m.user_intents, + not_for: m.not_for, + canonical_id: m.metadata.canonical_id, + topic_type: m.metadata.topic_type, semantic_status: m.semantic.status, }; } @@ -418,6 +498,10 @@ function buildGraph(modules: KModule[], site: string) { product: m.metadata.product, version: m.metadata.version, url: m.metadata.url, + userIntents: m.user_intents, + notFor: m.not_for, + canonicalId: m.metadata.canonical_id, + topicType: m.metadata.topic_type, lastVerified: m.last_verified || "", })); @@ -570,13 +654,17 @@ export default function knowledgeExtractor(context: any, _options: any) { if (!chunks.length) continue; pagesProcessed += 1; + const fm = readFrontMatter(siteDir, pathname); const framework = detectFramework(pathname); - const product = detectProduct(pathname); - const contentType = detectContentType(pathname, title); + // Frontmatter is authoritative when present; fall back to path/heuristics. + const fmProduct = fmFirst(fm.product); + const product = fmProduct ? slug(fmProduct) : detectProduct(pathname); + const fmTopic = fmFirst(fm.topic_type).toLowerCase(); + const contentType = TOPIC_TYPE_TO_CONTENT[fmTopic] || detectContentType(pathname, title); const notAvailable = isAvailabilityStub(title, bodyMd); chunks.forEach((chunk, i) => { modules.push( - buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, contentType, version, updatedAt, notAvailable }), + buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, contentType, version, updatedAt, notAvailable, fm }), ); }); } catch (err) { diff --git a/yarn.lock b/yarn.lock index 01689fe6d7..0f1b189158 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,11 +41,6 @@ resolved "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.20.0.tgz" integrity sha512-vCfxauaZutL3NImzB2G9LjLt36vKAckc6DhMp05An14kVo8F1Yofb6SIl6U3SaEz8pG2QOB9ptwM5c+zGevwIQ== -"@algolia/cache-common@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz" - integrity sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g== - "@algolia/cache-in-memory@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.20.0.tgz" @@ -80,14 +75,6 @@ "@algolia/requester-common" "4.20.0" "@algolia/transporter" "4.20.0" -"@algolia/client-common@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz" - integrity sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA== - dependencies: - "@algolia/requester-common" "4.24.0" - "@algolia/transporter" "4.24.0" - "@algolia/client-personalization@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.20.0.tgz" @@ -97,15 +84,6 @@ "@algolia/requester-common" "4.20.0" "@algolia/transporter" "4.20.0" -"@algolia/client-search@>= 4.9.1 < 6": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz" - integrity sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA== - dependencies: - "@algolia/client-common" "4.24.0" - "@algolia/requester-common" "4.24.0" - "@algolia/transporter" "4.24.0" - "@algolia/client-search@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.20.0.tgz" @@ -125,11 +103,6 @@ resolved "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.20.0.tgz" integrity sha512-xouigCMB5WJYEwvoWW5XDv7Z9f0A8VoXJc3VKwlHJw/je+3p2RcDXfksLI4G4lIVncFUYMZx30tP/rsdlvvzHQ== -"@algolia/logger-common@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz" - integrity sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA== - "@algolia/logger-console@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.20.0.tgz" @@ -149,11 +122,6 @@ resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.20.0.tgz" integrity sha512-9h6ye6RY/BkfmeJp7Z8gyyeMrmmWsMOCRBXQDs4mZKKsyVlfIVICpcSibbeYcuUdurLhIlrOUkH3rQEgZzonng== -"@algolia/requester-common@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz" - integrity sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA== - "@algolia/requester-node-http@4.20.0": version "4.20.0" resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.20.0.tgz" @@ -170,15 +138,6 @@ "@algolia/logger-common" "4.20.0" "@algolia/requester-common" "4.20.0" -"@algolia/transporter@4.24.0": - version "4.24.0" - resolved "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz" - integrity sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA== - dependencies: - "@algolia/cache-common" "4.24.0" - "@algolia/logger-common" "4.24.0" - "@algolia/requester-common" "4.24.0" - "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" @@ -200,7 +159,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.1.tgz" integrity sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.21.3", "@babel/core@^7.23.3", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0": +"@babel/core@^7.21.3", "@babel/core@^7.23.3": version "7.24.3" resolved "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz" integrity sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ== @@ -1252,7 +1211,7 @@ "@docsearch/css" "3.5.2" algoliasearch "^4.19.1" -"@docusaurus/core@^2.0.0-beta || ^3.0.0-alpha", "@docusaurus/core@^3.0.0", "@docusaurus/core@^3.4.0", "@docusaurus/core@3.4.0": +"@docusaurus/core@3.4.0", "@docusaurus/core@^3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz" integrity sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w== @@ -1374,7 +1333,7 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@^3.4.0", "@docusaurus/module-type-aliases@3.4.0": +"@docusaurus/module-type-aliases@3.4.0", "@docusaurus/module-type-aliases@^3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz" integrity sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw== @@ -1625,7 +1584,7 @@ resolved "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.4.0.tgz" integrity sha512-0qENiJ+TRaeTzcg4olrnh0BQ7eCxTgbYWBnWUeQDc84UYkt/T3pDNnm3SiQkqPb+YQ1qtYFlC0RriAElclo8Dg== -"@docusaurus/types@*", "@docusaurus/types@^3.4.0", "@docusaurus/types@3.4.0": +"@docusaurus/types@3.4.0", "@docusaurus/types@^3.4.0": version "3.4.0" resolved "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz" integrity sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A== @@ -1807,7 +1766,7 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== @@ -1941,7 +1900,7 @@ "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" "@svgr/babel-plugin-transform-svg-component" "8.0.0" -"@svgr/core@*", "@svgr/core@8.1.0": +"@svgr/core@8.1.0": version "8.1.0" resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz" integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== @@ -2252,7 +2211,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@>= 16.8.0 < 19.0.0", "@types/react@>=16": +"@types/react@*": version "18.3.3" resolved "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz" integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== @@ -2337,7 +2296,7 @@ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@webassemblyjs/ast@^1.11.5", "@webassemblyjs/ast@1.11.6": +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz" integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== @@ -2438,7 +2397,7 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" -"@webassemblyjs/wasm-parser@^1.11.5", "@webassemblyjs/wasm-parser@1.11.6": +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": version "1.11.6" resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz" integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== @@ -2491,7 +2450,7 @@ acorn-walk@^8.0.0: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz" integrity sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA== -"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8, acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: +acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: version "8.11.2" resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz" integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== @@ -2516,12 +2475,7 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" -ajv-keywords@^3.4.1: - version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^3.5.2: +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== @@ -2533,7 +2487,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1: +ajv@^6.12.2, ajv@^6.12.5: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2543,7 +2497,7 @@ ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.9.0: version "8.12.0" resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -2560,7 +2514,7 @@ algoliasearch-helper@^3.13.3: dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6": +algoliasearch@^4.18.0, algoliasearch@^4.19.1: version "4.20.0" resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.20.0.tgz" integrity sha512-y+UHEjnOItoNy0bYO+WWmLWBlPwDjKHW6mNHrPi0NkuhpQOOEbrkwQH/wgKFDLh7qlKjzoKeiRtlpewDPDG23g== @@ -2646,16 +2600,16 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - array-flatten@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" @@ -2830,7 +2784,7 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.23.0, "browserslist@>= 4.21.0": +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4.22.1, browserslist@^4.22.2, browserslist@^4.23.0: version "4.23.0" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz" integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== @@ -2984,7 +2938,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12, cheerio@1.0.0-rc.12: +cheerio@1.0.0-rc.12, cheerio@^1.0.0-rc.12: version "1.0.0-rc.12" resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -2997,7 +2951,7 @@ cheerio@^1.0.0-rc.12, cheerio@1.0.0-rc.12: parse5 "^7.0.0" parse5-htmlparser2-tree-adapter "^7.0.0" -chokidar@^3.4.2, chokidar@^3.5.3, "chokidar@>=3.0.0 <4.0.0": +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3: version "3.5.3" resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== @@ -3091,16 +3045,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colord@^2.9.3: version "2.9.3" resolved "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz" @@ -3471,27 +3425,20 @@ debounce@^1.2.1: resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@^2.6.0: +debug@2.6.9, debug@^2.6.0: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@4: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@2.6.9: - version "2.6.9" - resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" @@ -3565,16 +3512,16 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - depd@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + dequal@^2.0.0: version "2.0.3" resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz" @@ -4076,7 +4023,7 @@ feed@^4.2.2: dependencies: xml-js "^1.6.11" -file-loader@*, file-loader@^6.2.0: +file-loader@^6.2.0: version "6.2.0" resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== @@ -4223,6 +4170,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -4364,17 +4316,17 @@ got@^12.1.0: p-cancelable "^3.0.0" responselike "^3.0.0" -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - version "4.2.11" - resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - graceful-fs@4.2.10: version "4.2.10" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -gray-matter@^4.0.3: +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +gray-matter@4.0.3, gray-matter@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz" integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== @@ -4664,16 +4616,6 @@ http-deceiver@^1.2.7: resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - http-errors@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" @@ -4685,6 +4627,16 @@ http-errors@2.0.0: statuses "2.0.1" toidentifier "1.0.1" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" @@ -4793,7 +4745,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4803,16 +4755,16 @@ inherits@2.0.3: resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== -ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.8" - resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - ini@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + inline-style-parser@0.1.1: version "0.1.1" resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" @@ -4830,16 +4782,16 @@ invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -ipaddr.js@^2.0.1: - version "2.1.0" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" - integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== - ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz" @@ -5015,16 +4967,16 @@ is-yarn-global@^0.4.0: resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz" integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isarray@0.0.1: version "0.0.1" resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" @@ -5999,7 +5951,7 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" -"mime-db@>= 1.43.0 < 2": +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== @@ -6009,40 +5961,14 @@ mime-db@~1.33.0: resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz" integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ== -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.27: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@^2.1.31: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@~2.1.17, mime-types@2.1.18: +mime-types@2.1.18, mime-types@~2.1.17: version "2.1.18" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz" integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ== dependencies: mime-db "~1.33.0" -mime-types@~2.1.24: - version "2.1.35" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime-types@~2.1.34: +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -6081,7 +6007,7 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@3.1.2: +minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6459,13 +6385,6 @@ path-parse@^1.0.7: resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" @@ -6476,6 +6395,13 @@ path-to-regexp@2.2.1: resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -6794,7 +6720,7 @@ postcss-zindex@^6.0.2: resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz" integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== -"postcss@^7.0.0 || ^8.0.1", postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.2, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.31, postcss@^8.4.38: +postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.38: version "8.4.38" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz" integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== @@ -6917,21 +6843,16 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" -range-parser@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - range-parser@1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz" integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A== +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + raw-body@2.5.1: version "2.5.1" resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" @@ -6982,7 +6903,7 @@ react-dev-utils@^12.0.1: strip-ansi "^6.0.1" text-table "^0.2.0" -react-dom@*, "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", react-dom@^18.0.0, "react-dom@>= 16.8.0 < 19.0.0": +react-dom@^18.0.0: version "18.2.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz" integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== @@ -7028,7 +6949,7 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" -react-loadable@*, "react-loadable@npm:@docusaurus/react-loadable@6.0.0": +"react-loadable@npm:@docusaurus/react-loadable@6.0.0": version "6.0.0" resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz" integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== @@ -7066,7 +6987,7 @@ react-router-dom@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@^5.3.4, react-router@>=5, react-router@5.3.4: +react-router@5.3.4, react-router@^5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== @@ -7081,7 +7002,7 @@ react-router@^5.3.4, react-router@>=5, react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@*, "react@^16.13.1 || ^17.0.0 || ^18.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0", react@^18.0.0, react@^18.2.0, "react@>= 16.8.0 < 19.0.0", react@>=15, react@>=16, react@>=16.0.0, react@>=16.6.0: +react@^18.0.0: version "18.2.0" resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz" integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== @@ -7384,20 +7305,15 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== "safer-buffer@>= 2.1.2 < 3": version "2.1.2" @@ -7415,7 +7331,7 @@ sass-loader@^10.1.1: schema-utils "^3.0.0" semver "^7.3.2" -sass@^1.3.0, sass@^1.30.0, sass@^1.77.2: +sass@^1.77.2: version "1.77.2" resolved "https://registry.npmjs.org/sass/-/sass-1.77.2.tgz" integrity sha512-eb4GZt1C3avsX3heBNlrc7I09nyT00IUuo4eFhAbeXWU2fvA7oXI53SxODVAA+zgZCk9aunAZgO+losjR3fAwA== @@ -7436,25 +7352,16 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -schema-utils@^3.0.0: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^3.1.1: - version "3.3.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" - integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" -schema-utils@^3.2.0: +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: version "3.3.0" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz" integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== @@ -7473,16 +7380,7 @@ schema-utils@^4.0.0, schema-utils@^4.0.1: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" -schema-utils@2.7.0: - version "2.7.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" - integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== - dependencies: - "@types/json-schema" "^7.0.4" - ajv "^6.12.2" - ajv-keywords "^3.4.1" - -search-insights@^2.17.3, "search-insights@>= 1 < 3": +search-insights@^2.17.3: version "2.17.3" resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz" integrity sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== @@ -7725,7 +7623,7 @@ sort-css-media-queries@2.2.0: resolved "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz" integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== -source-map-js@^1.0.1, source-map-js@^1.2.0, "source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== @@ -7738,7 +7636,7 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0: +source-map@^0.6.0, source-map@~0.6.0: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -7748,11 +7646,6 @@ source-map@^0.7.0: resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== -source-map@~0.6.0: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" @@ -7791,45 +7684,22 @@ srcset@^4.0.0: resolved "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz" integrity sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw== -"statuses@>= 1.4.0 < 2": - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + std-env@^3.0.1: version "3.5.0" resolved "https://registry.npmjs.org/std-env/-/std-env-3.5.0.tgz" integrity sha512-JGUEaALvL0Mf6JCfYnJOTcobY+Nc7sG/TemDRBqCA0wEr4DER7zDchaaixTlmOxAjG1uRJmX82EQcxwTQTkqVA== -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -string-width@^4.1.0: - version "4.2.3" - resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.2.0: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -7847,6 +7717,20 @@ string-width@^5.0.1, string-width@^5.1.2: emoji-regex "^9.2.2" strip-ansi "^7.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + stringify-entities@^4.0.0: version "4.0.3" resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" @@ -8070,7 +7954,7 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -"typescript@>= 2.7", typescript@>=4.9.5, typescript@~5.2.2: +typescript@~5.2.2: version "5.2.2" resolved "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== @@ -8186,7 +8070,7 @@ universalify@^2.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz" integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== -unpipe@~1.0.0, unpipe@1.0.0: +unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== @@ -8395,7 +8279,7 @@ webpack-sources@^3.2.3: resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== -"webpack@^4.0.0 || ^5.0.0", "webpack@^4.36.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.88.1, "webpack@>= 4", "webpack@>=4.41.1 || 5.x", webpack@>=5, "webpack@3 || 4 || 5": +webpack@^5.88.1: version "5.89.0" resolved "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz" integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== @@ -8435,7 +8319,7 @@ webpackbar@^5.0.2: pretty-time "^1.1.0" std-env "^3.0.1" -websocket-driver@^0.7.4, websocket-driver@>=0.5.1: +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== From fa3bb320d3603153ab8626b6168c9794d4b6cf2a Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Thu, 6 Aug 2026 11:21:34 +0100 Subject: [PATCH 07/53] fix: keep curated intents in dedicated fields, not the length-capped excerpt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assistant_excerpt is sliced to 300 chars; inlining user_intents/not_for pushed the summary out of the preview. user_intents/not_for/canonical_id/topic_type are already emitted as full, un-truncated fields, so the signal is preserved — the excerpt now stays a clean title+summary preview. llms.txt is unaffected (docusaurus-plugin-llms never reads these fields). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/plugins/knowledge-extractor/index.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index 209a83ae57..d1be0c4004 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -371,11 +371,13 @@ function buildModule(args: { const fmKeywords = fmStringArray(fm.keywords); const curatedApplied = Boolean(userIntents.length || fmFirst(fm.product) || topicType); - const intentLine = userIntents.length ? `User intents: ${userIntents.join("; ")}. ` : ""; - const notForLine = notFor.length ? `Not for: ${notFor.join("; ")}. ` : ""; + // NB: user_intents / not_for are emitted as dedicated, UN-truncated fields + // (see the return + toIndexRecord). We deliberately do NOT inline them into + // assistant_context, because the index only ships a 300-char assistant_excerpt + // — inlining them would crowd out the summary. The curated signal lives in the + // structured fields; the excerpt stays a clean title+summary preview. const assistantContext = `Use this module when answering questions related to: ${displayTitle}. ` + - `${intentLine}${notForLine}` + `Source path: ${pathname}. ` + `Summary: ${summary}\n\n${chunkClean}`; const moduleId = slug(`auto-${pathname}-${idx}`); From 1be9fbefba89015b4c9484596b910646035c24f0 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Thu, 6 Aug 2026 11:33:38 +0100 Subject: [PATCH 08/53] feat: fold user_intents into searchable keywords (not_for excluded) Makes curated intent retrievable by any consumer that searches keywords, not only one configured to read the dedicated user_intents field. not_for is excluded from keywords to avoid false-matching the products it steers away from; it stays a structured field for reranker demotion. Verified: intent phrase present in keywords, not_for absent from keywords, dedicated fields full. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/plugins/knowledge-extractor/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index d1be0c4004..11a5ca35a1 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -387,9 +387,14 @@ function buildModule(args: { const tags = Array.from( new Set(["auto-extracted", contentType || "docs", framework, product, stemSlug].filter(Boolean)), ).sort(); + // user_intents ARE folded into keywords (untruncated + conventionally searched), + // so the curated intent is retrievable even by a consumer that only searches + // keywords. not_for is deliberately EXCLUDED here — putting "…use MatrixScan + // Count" into searchable text would make this page falsely match that product; + // not_for stays a structured field for a reranker to demote against. const keywords = Array.from( new Set( - [stemSlug.replace(/-/g, " "), product.replace(/-/g, " "), contentType || "docs", framework.replace(/-/g, " "), ...fmKeywords].filter(Boolean), + [stemSlug.replace(/-/g, " "), product.replace(/-/g, " "), contentType || "docs", framework.replace(/-/g, " "), ...fmKeywords, ...userIntents].filter(Boolean), ), ).sort(); return { From 2a6edd58050e72b3c7496b2c5202b0dc77d4259e Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Fri, 7 Aug 2026 11:05:06 +0100 Subject: [PATCH 09/53] feat: emit all products for multi-product pages; rename status to frontmatter_augmented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-product: frontmatter product may be a list (e.g. troubleshooting: [sparkscan, barcode-capture, id-capture]); now every product flows into the record (products[]), graph nodes, and BelongsToProduct edges — not just the first — so LLMs see all relevant products. Rename: semantic.status 'frontmatter'->'frontmatter_augmented' since intent/audience remain heuristic and only keywords/product are curated; the old label overstated. Verified: products=[sparkscan,barcode-capture] on a two-product page, status renamed, 0 page errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/plugins/knowledge-extractor/index.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index 11a5ca35a1..4ef00708e6 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -350,13 +350,14 @@ function buildModule(args: { idx: number; framework: string; product: string; + products: string[]; contentType: string; version: string; updatedAt: string; notAvailable: boolean; fm: Record; }) { - const { pathname, url, sourceSite, site, title: rawTitle, description, chunk, idx, framework, product, contentType, version, updatedAt, notAvailable, fm } = args; + const { pathname, url, sourceSite, site, title: rawTitle, description, chunk, idx, framework, product, products, contentType, version, updatedAt, notAvailable, fm } = args; const chunkClean = chunk.content.trim(); const title = (rawTitle || pathname).trim(); const displayTitle = idx === 1 ? title : `${title} (Part ${idx})`; @@ -418,6 +419,7 @@ function buildModule(args: { heading: resolvedHeading.slice(0, 180), framework, product, + products, version, updated_at: updatedAt, source_site: sourceSite, @@ -431,7 +433,7 @@ function buildModule(args: { intent: intents[0], audience: audiences[0], keywords, - status: curatedApplied ? "frontmatter" : "rule_based", + status: curatedApplied ? "frontmatter_augmented" : "rule_based", }, references: links.internal.slice(0, 20), api_refs: links.api.slice(0, 20), @@ -468,6 +470,7 @@ function toIndexRecord(m: KModule) { heading: m.metadata.heading, framework: m.metadata.framework, product: m.metadata.product, + products: m.metadata.products, version: m.metadata.version, updated_at: m.metadata.updated_at, source_site: m.metadata.source_site, @@ -503,6 +506,7 @@ function buildGraph(modules: KModule[], site: string) { channels: m.channels, framework: m.metadata.framework, product: m.metadata.product, + products: m.metadata.products, version: m.metadata.version, url: m.metadata.url, userIntents: m.user_intents, @@ -516,7 +520,7 @@ function buildGraph(modules: KModule[], site: string) { const audiences = uniq(modules.flatMap((m) => m.audiences)); const channels = uniq(modules.flatMap((m) => m.channels)); const frameworks = uniq(modules.map((m) => m.metadata.framework)); - const products = uniq(modules.map((m) => m.metadata.product)); + const products = uniq(modules.flatMap((m) => (m.metadata.products && m.metadata.products.length ? m.metadata.products : [m.metadata.product]))); const apiRefs = uniq(modules.flatMap((m) => m.api_refs)); const docPaths = uniq(modules.map((m) => m.metadata.source_path)); @@ -548,7 +552,7 @@ function buildGraph(modules: KModule[], site: string) { for (const v of m.audiences) addEdge(`${src}#audience:${v}`, "HasAudience", src, `urn:audience:${v}`); for (const v of m.channels) addEdge(`${src}#channel:${v}`, "HasChannel", src, `urn:channel:${v}`); if (m.metadata.framework) addEdge(`${src}#framework:${m.metadata.framework}`, "HasFramework", src, `urn:framework:${m.metadata.framework}`); - if (m.metadata.product) addEdge(`${src}#product:${m.metadata.product}`, "BelongsToProduct", src, `urn:product:${m.metadata.product}`); + for (const p of (m.metadata.products && m.metadata.products.length ? m.metadata.products : [m.metadata.product]).filter(Boolean)) addEdge(`${src}#product:${p}`, "BelongsToProduct", src, `urn:product:${p}`); for (const a of m.api_refs) addEdge(`${src}#api:${a}`, "CitesApi", src, `urn:api:${a}`); for (const ref of m.references) { if (indexedPaths.has(ref)) addEdge(`${src}#see:${ref}`, "SeeAlso", src, `urn:doc:${ref}`); @@ -664,14 +668,15 @@ export default function knowledgeExtractor(context: any, _options: any) { const fm = readFrontMatter(siteDir, pathname); const framework = detectFramework(pathname); // Frontmatter is authoritative when present; fall back to path/heuristics. - const fmProduct = fmFirst(fm.product); - const product = fmProduct ? slug(fmProduct) : detectProduct(pathname); + const fmProducts = fmStringArray(fm.product).map(slug).filter(Boolean); + const products = fmProducts.length ? Array.from(new Set(fmProducts)) : [detectProduct(pathname)]; + const product = products[0]; const fmTopic = fmFirst(fm.topic_type).toLowerCase(); const contentType = TOPIC_TYPE_TO_CONTENT[fmTopic] || detectContentType(pathname, title); const notAvailable = isAvailabilityStub(title, bodyMd); chunks.forEach((chunk, i) => { modules.push( - buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, contentType, version, updatedAt, notAvailable, fm }), + buildModule({ pathname, url, sourceSite, site, title, description, chunk, idx: i + 1, framework, product, products, contentType, version, updatedAt, notAvailable, fm }), ); }); } catch (err) { From 0110d026ab6f99ac305fc475119ee2f8dbf733d2 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Fri, 4 Sep 2026 12:26:59 +0100 Subject: [PATCH 10/53] fix(knowledge-extractor): address the review findings on the retrieval layer Every fix below was reproduced against the repo's real build/ output first, and re-measured after. Numbers are before -> after on 616 current-version pages. HIGH - code samples were shipped broken. Prism renders one per source line and emits no newline characters at all, so $(el).text() returned the whole sample on one line. Not cosmetic: a `//` or `#` comment then commented out the rest of the sample. Join the token lines, falling back to .text() for non-Prism blocks (the SkillsCallout command blocks). Multi-line samples in the index: 0 -> 275. HIGH - heading anchors leaked into content and poisoned the graph. Docusaurus renders them as whose label is a single U+200B and whose href is the FULL page path plus a fragment - so neither the empty-label check (U+200B is not \s, so trim() keeps it) nor the startsWith("#") guard caught them. They ended up in heading, topic and the excerpt, and classifyLinks then read the href as a real reference. Skip hash-link anchors, strip zero-width characters from labels, and drop self-references when building SeeAlso. "## Key Benefits[](/hosted/.../#key-benefits)" -> "## Key Benefits"; SeeAlso edges 4292 -> 944, of which self-referential 3248 -> 0. MEDIUM - the graph contradicted itself about availability. detectProduct filed any single-FILE product under "core", so /sdks/web/barcode-generator/ became product core on framework web; since every other web page also contributed AvailableOn(core, web), the graph asserted both AvailableOn and NotAvailableOn for the same pair. Read the product keys from src/data/products.json - the registry the rest of the site already uses - and exclude the synthetic core / general buckets from availability edges entirely. Contradicted edges 5 -> 0, and the facts that were being lost now appear: barcode-generator unavailable on web, .NET iOS, .NET Android and Titanium; matrixscan-count and matrixscan-pick unavailable on web. MEDIUM - text directly inside a wrapper element was dropped. The div / section / details / article / aside branch only recursed children(), so a text node that was a direct child vanished - which is how an admonition's severity word ("danger") disappeared, leaving a :::danger block indistinguishable from prose. Walk contents() instead. MEDIUM - list structure was destroyed. ul and ol both emitted "- ", and inlineText() flattened each
  • including nested lists and extra

    into the parent bullet with no separator, so ordered steps lost their numbers, sub-steps merged upward, and where the HTML had no whitespace between

  • the words glued together. Emit real ordered markers (honouring the start attribute), recurse into nested lists and indent them. Modules with numbered steps: 0 -> 63. MEDIUM - 29 MB of public assets with no budget. HasChannel was 3x every module (14904 edges) carrying no information, because channels is a hardcoded constant; the edges are gone and the field stays for consumers. Added a size budget that fails the build on runaway growth - the existing guard only caught EMPTY output. Index 11.2 -> 9.5 MB, graph 18.1 -> 11.4 MB. LOW - chunk splitting could break a fence. Splitting on /\n(?=##\s|###\s)/ with no fence awareness would cut inside a code block whose line starts with "## ", leaving an unterminated fence and making the code comment the chunk heading. No such line exists in docs/ today, so this is a guard, not a live fix. LOW - the "atomically" comment was false. Two renameSync calls are not atomic as a pair, so a crash between them shipped a new index with the old graph - exactly what the comment claimed to prevent. Keep the previous index and roll the first rename back if the second throws. LOW - last_verified was a constant. It was the build clock, so every module claimed it was verified today. My first attempt used the source file's mtime, which measured as STILL one distinct value: git does not preserve mtimes, so a fresh clone or worktree stamps every file with the checkout time. And actions/checkout@v4 is used with no fetch-depth, i.e. depth 1, so a git-log lookup would collapse to one commit date too. So: take the real per-file commit date from one batched git log, and emit "" when the answer is genuinely unknowable (shallow clone, or no git) rather than a value that looks like data. updated_at keeps the build stamp, which is honestly named and true. Distinct last_verified values: 1 -> 19. Verified: tsc reports no errors in this file; the plugin runs clean over the real build (4452 modules, 616 pages, 0 page errors); the PR's own eval harness passes (success@3 0.90, precision@3 0.8333, MRR 0.90 against floors of 0.8/0.6/0.6). Co-Authored-By: Claude Opus 5 (1M context) --- src/plugins/knowledge-extractor/index.ts | 300 +++++++++++++++++++++-- 1 file changed, 276 insertions(+), 24 deletions(-) diff --git a/src/plugins/knowledge-extractor/index.ts b/src/plugins/knowledge-extractor/index.ts index 4ef00708e6..30640ebafe 100644 --- a/src/plugins/knowledge-extractor/index.ts +++ b/src/plugins/knowledge-extractor/index.ts @@ -24,10 +24,15 @@ import fs from "node:fs"; import path from "node:path"; +import { execFileSync } from "node:child_process"; import * as cheerio from "cheerio"; import matter from "gray-matter"; const CHUNK_TARGET_CHARS = 1400; +// Budgets for the published artifacts. Deliberately close to today's real sizes +// so growth has to be an explicit decision rather than a silent one. +const MAX_INDEX_MB = 14; +const MAX_GRAPH_MB = 16; const OWNER = "docsops-auto"; type Chunk = { heading: string; content: string }; @@ -52,8 +57,31 @@ function firstHeading(text: string): string { } /** Split body into ~chunkTarget-sized chunks at H2/H3 boundaries (ported). */ +/** + * Split on headings, but never inside a fenced code block: a sample line that + * begins with "## " or "### " (a shell or Python comment, or a Markdown example) + * would otherwise start a new chunk, leaving an unterminated fence behind and + * making the code comment the chunk's heading. No such line exists in docs/ + * today, so this is a guard against a future sample rather than a live bug. + */ +function splitOnHeadings(body: string): string[] { + const out: string[] = []; + let buf: string[] = []; + let inFence = false; + for (const line of body.split("\n")) { + if (/^\s*```/.test(line)) inFence = !inFence; + if (!inFence && /^(##\s|###\s)/.test(line) && buf.length) { + out.push(buf.join("\n")); + buf = []; + } + buf.push(line); + } + if (buf.length) out.push(buf.join("\n")); + return out; +} + function chunkBody(body: string, target: number): Chunk[] { - const parts = body.split(/\n(?=##\s|###\s)/); + const parts = splitOnHeadings(body); const chunks: Chunk[] = []; let current = ""; let currentHeading = ""; @@ -137,8 +165,21 @@ function serializeInline($: cheerio.CheerioAPI, node: any): string { } else if (n.type === "tag") { const tag = String(n.name || "").toLowerCase(); if (tag === "a") { + // Docusaurus heading anchors are whose label is a + // single U+200B and whose href is the FULL page path plus a fragment - + // not a bare "#anchor". So neither the empty-label check nor the + // startsWith("#") guard caught them: they leaked into the heading, the + // topic and the excerpt, and classifyLinks then read the stripped href + // as a real reference, which is what made most SeeAlso edges point a + // module at its own page. + const cls = String($(n).attr("class") || ""); + if (/\bhash-link\b/.test(cls)) return; const href = String($(n).attr("href") || ""); - const label = serializeInline($, n).replace(/\s+/g, " ").trim(); + // U+200B is not matched by \s, so trim() leaves it and !label is false. + const label = serializeInline($, n) + .replace(/[\u200b\u200c\u200d\ufeff]/g, "") + .replace(/\s+/g, " ") + .trim(); if (!label) return; out += href && !href.startsWith("#") ? `[${label}](${href})` : label; } else if (tag === "code") { @@ -179,26 +220,88 @@ function blockToMd($: cheerio.CheerioAPI, el: any): string { } if (tag === "p") return inlineText($, el); if (tag === "ul" || tag === "ol") { + // Previously every list emitted "- " and inlineText() flattened each
  • + // including any nested