diff --git a/apps/www/.gitignore b/apps/www/.gitignore index f24d48d91c..6fc722e865 100644 --- a/apps/www/.gitignore +++ b/apps/www/.gitignore @@ -1,3 +1,6 @@ # Next.js build output and the type shim it generates on first build. .next/ next-env.d.ts + +# @affordances/md writes its route table here on each dev and build (next.config.ts). +.affordances/ diff --git a/apps/www/next.config.ts b/apps/www/next.config.ts index 21c7683922..d5c3fc196d 100644 --- a/apps/www/next.config.ts +++ b/apps/www/next.config.ts @@ -1,6 +1,7 @@ import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import createMDX from "@next/mdx"; +import { withMd } from "@affordances/md/next"; import type { NextConfig } from "next"; /** @@ -15,20 +16,6 @@ import type { NextConfig } from "next"; */ const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); -const acceptsMarkdown = { - type: "header", - key: "accept", - value: ".*text/markdown.*", -} as const; - -/* - * Extensionless pages, as the docs site has them. Skips the markdown route - * itself, the API, MCP and social-card endpoints, Next's own assets, and anything with a - * file extension (`.md` urls, llms.txt, openapi.json), which has no markdown - * twin. - */ -const pages = "/:path((?!llms\\.mdx/|(?:api|_next|mcp|og)(?:/|$))(?!.*\\.[a-z0-9]+$).+)"; - const nextConfig: NextConfig = { // Pages prerender a shell around cached data (lib/data.ts), and links prefetch that shell. cacheComponents: true, @@ -42,24 +29,6 @@ const nextConfig: NextConfig = { // takumi's native renderer (app/og) is loaded by Node, not bundled -- as vlt.io has it. serverExternalPackages: ["@takumi-rs/image-response"], turbopack: { root: workspaceRoot }, - // The docs' rewrites (vltpkg/www/docs/next.config.ts). The query survives a - // rewrite, so `/x.md?fixture=astro` is the markdown of that filtered view. - rewrites: async () => ({ - // Agents that ask for markdown get it at the page's own url; runs before - // the filesystem, which would otherwise match the html page first. - beforeFiles: [ - { source: "/", has: [acceptsMarkdown], destination: "/llms.mdx" }, - { source: pages, has: [acceptsMarkdown], destination: "/llms.mdx/:path" }, - ], - // `/package-managers/clean.md` serves that page as plain markdown. - afterFiles: [{ source: "/:path+.md", destination: "/llms.mdx/:path+" }], - }), - // Those urls answer with html or markdown depending on `Accept`, so caches must key on it. - headers: async () => - ["/", pages].map((source) => ({ - source, - headers: [{ key: "Vary", value: "Accept" }], - })), // Compare was removed; its links land on the same section's report. redirects: async () => [ /* The front door is the default benchmark; what used to be here is /about. @@ -76,4 +45,11 @@ const nextConfig: NextConfig = { ], }; -export default createMDX()(nextConfig); +/* + * Every page's Markdown twin (`/x.md`, or the page's url with `Accept: + * text/markdown`) is @affordances/md's: its rewrites send both to + * app/api/affordances/md, which converts the page's own HTML, query and + * redirects included, and fills the figures' ``s from each route's + * markdown.ts. + */ +export default withMd(createMDX()(nextConfig)); diff --git a/apps/www/package.json b/apps/www/package.json index f3cee8b766..263c10b56d 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -15,6 +15,7 @@ "test": "vitest run" }, "dependencies": { + "@affordances/md": "0.5.0", "@base-ui/react": "1.9.0", "@benchmarks/data": "workspace:*", "@benchmarks/types": "workspace:*", diff --git a/apps/www/src/app/[section]/[[...params]]/markdown.ts b/apps/www/src/app/[section]/[[...params]]/markdown.ts new file mode 100644 index 0000000000..e45fd5d3c6 --- /dev/null +++ b/apps/www/src/app/[section]/[[...params]]/markdown.ts @@ -0,0 +1,134 @@ +import type { MarkdownArgs, Slots } from "@affordances/md/next"; +import { + figureMarkdown, + meanOf, + referenceMarkdown, + runMarkdown, + TWIN_CACHE, + type FigureMarkdown, +} from "@/lib/markdown"; +import { siteOrigin } from "@/lib/metadata"; +import { loadReportParams } from "@/lib/params"; +import { ignoredSelection, loadReport, type ReportResult } from "@/lib/report"; +import { SECTION_DEFS } from "@/lib/sections"; + +/** + * The report's Markdown twin (@affordances/md): the page's own HTML, + * converted, with each figure's `` (components/report.tsx `Figure`) + * filled with the numbers it draws -- the same builders as the MCP tools' and + * the API's Markdown (lib/markdown.ts). A chart is canvas, and a table on the + * page is formatted for reading; a twin carries the values. + * + * A 404 an agent can act on: an unknown section is the page's own + * `notFound()`, and `notFoundMarkdown` gives it our body. The page answers the + * other two with something other than a 404 (a redirect to the section's own + * report, the scenario with the extra segment ignored), so the default export + * answers those itself. + */ +type Route = "/[section]/[[...params]]"; +type Args = MarkdownArgs; + +const decode = (segment: string | undefined) => + segment === undefined ? undefined : decodeURIComponent(segment); + +/* One report a twin request, however many of its slots fill. */ +const loaded = new WeakMap>(); +const report = (args: Args) => { + let found = loaded.get(args.request); + if (found === undefined) { + const query = loadReportParams(args.searchParams); + found = loadReport({ + section: decode(args.params.section) ?? "", + scenario: decode(args.params.params?.[0]), + query, + }).then((result) => ({ + result, + ...(result.status === "ok" + ? { figures: figureMarkdown(result.report, siteOrigin(args.request)) } + : {}), + })); + loaded.set(args.request, found); + } + return found; +}; + +const pathOf = (args: Args) => { + const segments = [args.params.section, ...(args.params.params ?? [])].map((s) => decode(s) ?? ""); + return { segments, path: `/${segments.map(encodeURIComponent).join("/")}` }; +}; + +const notFoundBody = (args: Args, path: string, detail = "") => { + const abs = (to: string) => new URL(to, siteOrigin(args.request)).href; + return `# 404: Page not found\n\nThere is no page at \`${path}\` on vlt benchmarks.${detail}\n\n- [llms.txt](${abs("/llms.txt")}) lists every page, each available as Markdown\n- [openapi.json](${abs("/openapi.json")}) describes the JSON API\n- [Home](${abs("/")})\n`; +}; + +const notFound = (args: Args, path: string, detail?: string) => + new Response(notFoundBody(args, path, detail), { + status: 404, + headers: { "content-type": "text/markdown; charset=utf-8" }, + }); + +/* The page's own 404: an unknown section. */ +export const notFoundMarkdown = (args: Args) => notFoundBody(args, pathOf(args).path); + +export const publicCache = TWIN_CACHE; + +export default async function markdown(args: Args) { + const { segments, path } = pathOf(args); + /* The page renders `/{section}/{scenario}/extra` as the scenario, with a 200. */ + if (segments.length > 2) return notFound(args, path); + const { result } = await report(args); + /* The page redirects an unknown scenario to the section's report, a 307 and no 404. */ + if (result.status === "unknown-scenario" && args.params.params !== undefined) { + return notFound( + args, + path, + ` Scenarios ${SECTION_DEFS[result.section].label} published in this run: ${result.scenarios.map((s) => `\`${s}\``).join(", ")}.`, + ); + } + return undefined; +} + +const figure = + (id: string) => + async (args: Args): Promise => { + const { figures } = await report(args); + const lines = figures?.[id]; + return lines === undefined ? undefined : lines.join("\n"); + }; + +export const slots = { + /* + * The run and filters, as the menus and chips show them, and before them a + * selection the report fell back from: what a strict reader is told and the + * page leaves silent. + */ + context: async (args) => { + const { result } = await report(args); + if (result.status !== "ok") return undefined; + const { averageOf } = result.report.scenario; + const ignored = ignoredSelection(loadReportParams(args.searchParams), result.report); + return [ + ...(ignored === null ? [] : [`> ${ignored} This is the report without that selection.`, ""]), + ...(averageOf === null ? [] : [meanOf(averageOf), ""]), + ...runMarkdown(result.report), + ].join("\n"); + }, + reference: async (args) => { + const { result } = await report(args); + return result.status === "ok" + ? referenceMarkdown(result.report, siteOrigin(args.request)).join("\n") + : undefined; + }, + "leaderboard-table": figure("leaderboard-table"), + "trend-by-tool": figure("trend-by-tool"), + "time-trend": figure("time-trend"), + "time-bars": figure("time-bars"), + "time-by-fixture": figure("time-by-fixture"), + "time-table": figure("time-table"), + "per-package-chart": figure("per-package-chart"), + "per-package-by-fixture": figure("per-package-by-fixture"), + "per-package-table": figure("per-package-table"), + "package-count": figure("package-count"), + "process-count": figure("process-count"), +} satisfies Slots; diff --git a/apps/www/src/app/[section]/[[...params]]/page.tsx b/apps/www/src/app/[section]/[[...params]]/page.tsx index 46100c0233..da81d7ed21 100644 --- a/apps/www/src/app/[section]/[[...params]]/page.tsx +++ b/apps/www/src/app/[section]/[[...params]]/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Fragment, Suspense } from "react"; import { notFound, redirect } from "next/navigation"; import { ArrowRight, ChartLine } from "lucide-react"; +import { AgentOnly, AgentSlot } from "@affordances/md/react"; import { InlineCode, Marked } from "@/components/inline-code"; import { Button } from "@/components/ui/button"; import { TimeSeriesChart } from "@/components/charts-echarts"; @@ -41,6 +42,7 @@ import { toTrendColumns } from "@/lib/chart-model"; import { loadReportParams, rangeLabel, reportHref, reportParams } from "@/lib/params"; import { loadCatalog } from "@/lib/catalog"; import { dataClient } from "@/lib/data"; +import { PER_PACKAGE_DERIVED } from "@/lib/markdown"; import { loadReport } from "@/lib/report"; import { withAverage } from "@/lib/scenarios"; import { isSectionId, SECTION_DEFS, SECTIONS } from "@/lib/sections"; @@ -665,6 +667,8 @@ const Report = async ({ } /> + {/* Empty for people; the twin's run and filters, as the menus and chips show them (./markdown.ts). */} + {caveats.qualifier === null ? null : (
@@ -716,12 +720,21 @@ const Report = async ({ id={part.id} title={part.items.every((item) => solo.has(item.id)) ? undefined : part.title} > + {/* For agents only: on the page, the guides and axis labels say it (lib/markdown.ts). */} + {part.id === "per-package" ? ( + +

{PER_PACKAGE_DERIVED}

+
+ ) : null} {part.items.map((item) => ( {bodies[item.id]} ))} ))} - + {/* The raw files are a menu here; the twin lists them, with the method and a citation. */} + + + {/* Fixed to the viewport's right edge, in the page margin: no column. */} ) { + return (await llmsTxt(request)).text(); +} + +export const publicCache = TWIN_CACHE; diff --git a/apps/www/src/app/about/page.tsx b/apps/www/src/app/about/page.tsx index f2e46c63f1..c53978ddd9 100644 --- a/apps/www/src/app/about/page.tsx +++ b/apps/www/src/app/about/page.tsx @@ -8,7 +8,7 @@ import Content from "./content.mdx"; /* The site's front page: what the benchmarks are, how agents read them, and the latest headline of each. `/` itself opens on the default benchmark. Its - Markdown twin is llms.txt. */ + Markdown twin is llms.txt (./markdown.ts). */ export const metadata = pageMetadata({ title: "About", description: SITE_DESCRIPTION, diff --git a/apps/www/src/app/api/affordances/md/[[...path]]/route.ts b/apps/www/src/app/api/affordances/md/[[...path]]/route.ts new file mode 100644 index 0000000000..136b5dda33 --- /dev/null +++ b/apps/www/src/app/api/affordances/md/[[...path]]/route.ts @@ -0,0 +1,6 @@ +/* + * Every page's Markdown twin, reached through withMd's rewrites (next.config.ts). + * The All-time report (`/package-managers/clean?range=all`) fits the default + * `maxBytes`: it counts the HTML kept once the Flight data is stripped. + */ +export { GET } from "@affordances/md/next"; diff --git a/apps/www/src/app/llms-full.txt/route.ts b/apps/www/src/app/llms-full.txt/route.ts index 9a510907c3..14c40e65b1 100644 --- a/apps/www/src/app/llms-full.txt/route.ts +++ b/apps/www/src/app/llms-full.txt/route.ts @@ -1,27 +1,32 @@ -import { connection } from "next/server"; +import { createLlmsFullHandler } from "@affordances/md/next"; import { loadCatalog } from "@/lib/catalog"; import { dataClient } from "@/lib/data"; -import { reportMarkdown } from "@/lib/markdown"; +import { reportMarkdown, TWIN_CACHE } from "@/lib/markdown"; import { siteOrigin } from "@/lib/metadata"; import { loadReportParams } from "@/lib/params"; import { loadReport } from "@/lib/report"; -import { withAverage } from "@/lib/scenarios"; +import { scenarioPath, withAverage } from "@/lib/scenarios"; import { SECTIONS } from "@/lib/sections"; -export const GET = async (request: Request) => { - await connection(); - const client = dataClient(); - const { scenarios } = await loadCatalog(client); - const query = loadReportParams(new URLSearchParams()); - const texts = await Promise.all( - SECTIONS.flatMap((section) => - withAverage(scenarios[section]).map(async ({ variation }) => { - const result = await loadReport({ section, scenario: variation, query, client }); - return result.status === "ok" ? reportMarkdown(result.report, siteOrigin(request)) : ""; - }), - ), - ); - return new Response(texts.filter(Boolean).join("\n\n"), { - headers: { "Content-Type": "text/plain; charset=utf-8" }, - }); -}; +/* + * Every scenario of the latest run at the default view, from the Markdown + * builder rather than rendered: a page whose report throws makes the file a + * 503 that's never cached, not a cached file without it. + */ +export const GET = createLlmsFullHandler({ + publicCache: TWIN_CACHE, + pages: async (request) => { + const client = dataClient(); + const { scenarios } = await loadCatalog(client); + const query = loadReportParams(new URLSearchParams()); + return SECTIONS.flatMap((section) => + withAverage(scenarios[section]).map(({ variation }) => ({ + url: new URL(scenarioPath(section, variation), siteOrigin(request)).href, + markdown: async () => { + const result = await loadReport({ section, scenario: variation, query, client }); + return result.status === "ok" ? reportMarkdown(result.report, siteOrigin(request)) : ""; + }, + })), + ); + }, +}); diff --git a/apps/www/src/app/llms.mdx/[[...slug]]/route.ts b/apps/www/src/app/llms.mdx/[[...slug]]/route.ts deleted file mode 100644 index f25c82e2ef..0000000000 --- a/apps/www/src/app/llms.mdx/[[...slug]]/route.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { GET as llmsTxt } from "@/app/llms.txt/route"; -import { loadCatalog } from "@/lib/catalog"; -import { dataClient } from "@/lib/data"; -import { methodologyMarkdown } from "@/app/methodology/markdown"; -import { reportMarkdown } from "@/lib/markdown"; -import { siteOrigin } from "@/lib/metadata"; -import { loadReportParams } from "@/lib/params"; -import { ignoredSelection, loadReport } from "@/lib/report"; -import { SECTION_DEFS } from "@/lib/sections"; - -// `Vary` because the same url serves html to browsers -const markdown = (body: string, status = 200) => - new Response(body, { - status, - headers: { "Content-Type": "text/markdown; charset=utf-8", Vary: "Accept" }, - }); - -// reached through the `Accept: text/markdown` and `/:path+.md` rewrites in next.config.ts -export const GET = async ( - request: Request, - { params }: { params: Promise<{ slug?: string[] }> }, -) => { - const { slug = [] } = await params; - const origin = siteOrigin(request); - const abs = (path: string) => new URL(path, origin).href; - // the About page (once the landing page) is cards and links, so its markdown is the llms.txt index - // `/about` is that page now; `/` redirects to the default benchmark - if (slug.length === 0 || slug.join("/") === "about") { - return markdown(await (await llmsTxt(request)).text()); - } - if (slug.join("/") === "methodology") { - return markdown(methodologyMarkdown((await loadCatalog(dataClient())).scenarios, origin)); - } - - const path = `/${slug.join("/")}`; - // a body agents can act on; `notFound()` would send an empty one - const missing = (detail = "") => - markdown( - `# 404: Page not found\n\nThere is no page at \`${path}\` on vlt benchmarks.${detail}\n\n- [llms.txt](${abs("/llms.txt")}) lists every page, each available as Markdown\n- [openapi.json](${abs("/openapi.json")}) describes the JSON API\n- [Home](${abs("/")})\n`, - 404, - ); - - const [section = "", scenario, ...extra] = slug.map(decodeURIComponent); - if (extra.length > 0) return missing(); - const query = loadReportParams(new URL(request.url).searchParams); - let result = await loadReport({ section, scenario, query }); - // `/{section}` opens on its average (or first scenario), as the page does - if (result.status === "unknown-scenario" && scenario === undefined) { - result = await loadReport({ section, scenario: result.opens, query }); - } - - switch (result.status) { - case "unknown-section": - return missing(); - case "unknown-scenario": - return missing( - ` Scenarios ${SECTION_DEFS[result.section].label} published in this run: ${result.scenarios.map((s) => `\`${s}\``).join(", ")}.`, - ); - case "no-dates": - return markdown( - `# ${SECTION_DEFS[result.section].label}\n\nThe served tree reports no dates. There is nothing to show.\n`, - ); - case "no-results": - return markdown( - `# ${SECTION_DEFS[result.section].label}\n\nNo ${SECTION_DEFS[result.section].label.toLowerCase()} results in the run of ${result.date}.${result.availableDates.length === 0 ? "" : ` Recent runs that did publish them: ${result.availableDates.map((d) => `[${d}](${abs(`/${result.section}.md?date=${d}`)})`).join(", ")}.`}\n`, - ); - case "ok": { - const ignored = ignoredSelection(query, result.report); - const body = reportMarkdown(result.report, origin); - return markdown( - ignored === null - ? body - : `> ${ignored} This is the report without that selection.\n\n${body}`, - ); - } - } -}; diff --git a/apps/www/src/app/llms.txt/route.ts b/apps/www/src/app/llms.txt/route.ts index 5629f25738..16cdb3fb58 100644 --- a/apps/www/src/app/llms.txt/route.ts +++ b/apps/www/src/app/llms.txt/route.ts @@ -1,43 +1,54 @@ +import { createLlmsTxtHandler } from "@affordances/md/next"; import { loadCatalog } from "@/lib/catalog"; import { dataClient } from "@/lib/data"; +import { TWIN_CACHE } from "@/lib/markdown"; import { READING, SITE_DESCRIPTION, SITE_NAME, siteOrigin } from "@/lib/metadata"; import { DESCRIPTIONS, scenarioPath, withAverage } from "@/lib/scenarios"; import { SECTION_DEFS, SECTIONS } from "@/lib/sections"; -/** The llms.txt index, in the docs' shape: a title, a summary, how to read the results, then every page. */ -export const GET = async (request: Request) => { +/** + * The llms.txt index, in the docs' shape: a title, a summary, how to read the + * results, then every page. Its links are objects, so no page is rendered to + * name it; built per request for the site's origin in the note. + */ +export const GET = (request: Request) => { const abs = (path: string) => new URL(path, siteOrigin(request)).href; - const { scenarios } = await loadCatalog(dataClient()); - const text = `# ${SITE_NAME} - -> ${SITE_DESCRIPTION} - -Every page is available as Markdown: append \`.md\` to its URL, or request the page with an \`Accept: text/markdown\` header. The links below point at the Markdown versions. Query parameters apply to the Markdown as they do to the page: ${abs("/package-managers/clean.md?fixture=babylon")} is that scenario on one fixture. [llms-full.txt](${abs("/llms-full.txt")}) holds every scenario of the latest run, at the default view, in one file. [Methodology](${abs("/methodology.md")}) holds the method, the runner environment and what each scenario measures. [About](${abs("/about.md")}) is this index as a page; \`/\` itself opens on the registries average, and each benchmark's own URL on its average: the derived mean of its scenarios. - -## Reading the results - -${READING.map((line) => `- ${line}`).join("\n")} -- Page parameters: \`date\`, \`fixture\`, \`range\` (today, 1w, 1m, 1y or all: rolling days of trend ending at the run; default 1m), \`from\` and \`to\` (an exact trend window), \`hide\` (comma-separated tools), \`metric\` (total, change, baseline or per-package), \`exclude\` (\`YYYY-MM-DD\` or \`tool@YYYY-MM-DD\`, comma-separated). - -## API and MCP - -- JSON API, described at [openapi.json](${abs("/openapi.json")}): \`/api/v1/runs\`, \`/api/v1/catalog\`, \`/api/v1/{section}/{scenario}\` (the full report, same parameters as the page) and \`/api/v1/{section}/{scenario}/history\` (the trend, failures and coverage). -- MCP server (streamable HTTP, read-only, no auth): ${abs("/mcp")}. Tools: \`list_benchmarks\`, \`get_results\`, \`get_history\`, \`compare_scenarios\`. Add it to Claude Code with \`claude mcp add --transport http vlt-benchmarks ${abs("/mcp")}\`. - -## Benchmarks -${SECTIONS.map( - (section) => ` -### ${SECTION_DEFS[section].label} - -${SECTION_DEFS[section].about} - -${withAverage(scenarios[section]) - .map( - (s) => - `- [${s.variation}](${abs(`${scenarioPath(section, s.variation)}.md`)}): ${DESCRIPTIONS[s.variation] ?? "Published, with no description yet."}`, - ) - .join("\n")}`, -).join("\n")} -`; - return new Response(text, { headers: { "Content-Type": "text/plain; charset=utf-8" } }); + return createLlmsTxtHandler({ + title: SITE_NAME, + summary: SITE_DESCRIPTION, + note: `Every page is available as Markdown: append \`.md\` to its URL, or request the page with an \`Accept: text/markdown\` header. The links below point at the Markdown versions. Query parameters apply to the Markdown as they do to the page: ${abs("/package-managers/clean.md?fixture=babylon")} is that scenario on one fixture. [llms-full.txt](${abs("/llms-full.txt")}) holds every scenario of the latest run, at the default view, in one file. [Methodology](${abs("/methodology.md")}) holds the method, the runner environment and what each scenario measures. [About](${abs("/about.md")}) is this index as a page; \`/\` itself opens on the registries average, and each benchmark's own URL on its average: the derived mean of its scenarios.`, + full: false, + publicCache: TWIN_CACHE, + sections: async () => { + const { scenarios } = await loadCatalog(dataClient()); + return [ + { + title: "Reading the results", + intro: [ + ...READING.map((line) => `- ${line}`), + "- Page parameters: `date`, `fixture`, `range` (today, 1w, 1m, 1y or all: rolling days of trend ending at the run; default 1m), `from` and `to` (an exact trend window), `hide` (comma-separated tools), `metric` (total, change, baseline or per-package), `exclude` (`YYYY-MM-DD` or `tool@YYYY-MM-DD`, comma-separated).", + ].join("\n"), + }, + { + title: "API and MCP", + intro: [ + `- JSON API, described at [openapi.json](${abs("/openapi.json")}): \`/api/v1/runs\`, \`/api/v1/catalog\`, \`/api/v1/{section}/{scenario}\` (the full report, same parameters as the page) and \`/api/v1/{section}/{scenario}/history\` (the trend, failures and coverage).`, + `- MCP server (streamable HTTP, read-only, no auth): ${abs("/mcp")}. Tools: \`list_benchmarks\`, \`get_results\`, \`get_history\`, \`compare_scenarios\`. Add it to Claude Code with \`claude mcp add --transport http vlt-benchmarks ${abs("/mcp")}\`.`, + ].join("\n"), + }, + { + title: "Benchmarks", + sections: SECTIONS.map((section) => ({ + title: SECTION_DEFS[section].label, + intro: SECTION_DEFS[section].about, + pages: withAverage(scenarios[section]).map((s) => ({ + url: abs(scenarioPath(section, s.variation)), + title: s.variation, + description: DESCRIPTIONS[s.variation] ?? "Published, with no description yet.", + })), + })), + }, + ]; + }, + })(request); }; diff --git a/apps/www/src/app/methodology/markdown.ts b/apps/www/src/app/methodology/markdown.ts index 57dfc413ac..78a1c373ee 100644 --- a/apps/www/src/app/methodology/markdown.ts +++ b/apps/www/src/app/methodology/markdown.ts @@ -1,41 +1,19 @@ -import { mdxMarkdown, mdxSource } from "@/lib/mdx"; -import { METHOD, READING, RESOURCES, RUNNER, RUNNER_NOTE, TRADEMARK } from "@/lib/metadata"; -import type { SectionScenarios } from "@/lib/page-tree"; -import { DESCRIPTIONS, scenarioPath, stateParts } from "@/lib/scenarios"; -import { SECTION_DEFS, SECTIONS } from "@/lib/sections"; +import type { MarkdownArgs } from "@affordances/md/next"; +import { loadCatalog } from "@/lib/catalog"; +import { dataClient } from "@/lib/data"; +import { TWIN_CACHE } from "@/lib/markdown"; +import { siteOrigin } from "@/lib/metadata"; +import { methodologyMarkdown } from "@/lib/methodology-markdown"; -/** The Methodology page as Markdown: content.mdx, each block written out as ./components.tsx draws it. */ -export const methodologyMarkdown = (scenarios: SectionScenarios, origin: string): string => { - const abs = (path: string) => new URL(path, origin).href; - const lines = (...lines: string[]) => lines.join("\n"); - const body = mdxMarkdown(mdxSource("app/methodology/content.mdx"), { - Method: METHOD, - Environment: lines( - ...RUNNER.map(([term, value]) => `- ${term}: \`${value}\``), - "", - RUNNER_NOTE, - ), - Reading: lines(...READING.map((line) => `- ${line}`)), - Scenarios: SECTIONS.map((section) => - lines( - `### ${SECTION_DEFS[section].label}`, - "", - SECTION_DEFS[section].about, - "", - ...scenarios[section].map( - (s) => - `- [${s.variation}](${abs(`${scenarioPath(section, s.variation)}.md`)}): ${DESCRIPTIONS[s.variation] ?? "Published, with no description yet."}${ - s.kind === "other" - ? "" - : ` Before each run: ${stateParts(s.state) - .map((p) => `${p.name} ${p.word}`) - .join(", ")}.` - }`, - ), - ), - ).join("\n\n"), - Resources: lines(...RESOURCES.map(([term, , href]) => `- ${term}: ${href}`)), - Trademark: TRADEMARK, - }); - return `# Methodology (${abs("/methodology")})\n\n${body}`; -}; +/* + * The Methodology twin is written, not converted: the page's description + * lists convert to bold paragraphs, and each scenario's state list runs into + * its description. This is content.mdx with each block as Markdown, the + * fact-checked wording word for word. + */ +export default async function markdown({ request }: MarkdownArgs<"/methodology">) { + const { scenarios } = await loadCatalog(dataClient()); + return methodologyMarkdown(scenarios, siteOrigin(request)); +} + +export const publicCache = TWIN_CACHE; diff --git a/apps/www/src/app/methodology/page.tsx b/apps/www/src/app/methodology/page.tsx index d5e520b9f1..34aa802a1e 100644 --- a/apps/www/src/app/methodology/page.tsx +++ b/apps/www/src/app/methodology/page.tsx @@ -7,7 +7,7 @@ import { tocOf } from "@/lib/mdx"; import { pageMetadata, siteOrigin } from "@/lib/metadata"; import { Environment, Method, Reading, Resources, Scenarios, Trademark } from "./components"; import Content from "./content.mdx"; -import { methodologyMarkdown } from "./markdown"; +import { methodologyMarkdown } from "@/lib/methodology-markdown"; const description = "How the benchmarks are measured, where they run, and how to read the results."; diff --git a/apps/www/src/components/report.tsx b/apps/www/src/components/report.tsx index 79d6cf92b1..85e99dc6d9 100644 --- a/apps/www/src/components/report.tsx +++ b/apps/www/src/components/report.tsx @@ -1,5 +1,6 @@ import { BarChart3, Info, LineChart, ScatterChart, Table2 } from "lucide-react"; import type { ReactNode } from "react"; +import { AgentSlot } from "@affordances/md/react"; import { HeadingAnchor, HeadingLink } from "@/components/heading-anchor"; /** @@ -231,7 +232,8 @@ export const Figure = ({ kind === "table" || !firstTabActive ? "" : "rounded-tl-none" }`} > - {children} + {/* Its Markdown twin's numbers: app/[section]/[[...params]]/markdown.ts fills this by id. */} + {children}
{footer === undefined ? null : ( /* Inset as the panel is (the shell's 4px), so a control in a corner is @@ -241,8 +243,10 @@ export const Figure = ({ {guide === undefined ? null : ( + /* How to read the marks; the twin's slot says what its own numbers are. */

{guide} diff --git a/apps/www/src/lib/markdown.ts b/apps/www/src/lib/markdown.ts index 27c7bbbd8b..96edbebd44 100644 --- a/apps/www/src/lib/markdown.ts +++ b/apps/www/src/lib/markdown.ts @@ -41,6 +41,15 @@ const METRIC_TEXT: { readonly [M in Metric]: { readonly name: string; readonly u }, }; +/* + * How long a CDN keeps a page's Markdown twin (`publicCache` in each route's + * markdown.ts): an hour fresh, and at most an hour more served stale while it + * revalidates. The data changes nightly (and on a reprocess), so a twin trails + * its page by an hour or two after a run, never the day `'hours'` would allow. + * A deploy purges the CDN. + */ +export const TWIN_CACHE = { revalidate: 3600, expire: 7200 }; + /** The view the report describes, with only the selections it honoured. */ export const viewHref = (report: ReportModel): string => { const { filters } = report; @@ -218,113 +227,25 @@ const trendLines = ({ return lines; }; -export const reportMarkdown = (report: ReportModel, origin: string): string => { +/** Each figure of a report as Markdown lines, by its id on the page. */ +export type FigureMarkdown = { readonly [figure: string]: readonly string[] }; + +/** + * Each figure of the report as Markdown, keyed by its id on the page + * (`outline`), without the figure's own heading: each chart as the numbers it + * draws, each table as a table. `reportMarkdown` sets them under the page's + * headings; the page's Markdown twin (app/[section]/[[...params]]/markdown.ts) + * fills each figure's `` with its own, under the heading the page + * renders. + */ +export const figureMarkdown = (report: ReportModel, origin: string): FigureMarkdown => { const abs = (path: string) => new URL(path, origin).href; - const { section, scenario, run, filters, finding, caveats, trend } = report; - /* What the trend shows: the reader's metric, as the chart and the history - endpoint apply it. Per package falls back to total where it cannot be drawn. */ + const { section, scenario, filters, trend } = report; const metric: Metric = filters.metric === "per-package" ? trend.plotted : filters.metric; - const query = viewHref(report).slice(report.path.length); const fixtureCount = report.fixtures.length; - const perPackage = report.outline.some((s) => s.id === "per-package"); - const footprint = report.outline.find((s) => s.id === "footprint"); - const lines: string[] = []; - const add = (...more: string[]) => lines.push(...more); - - add( - `# ${section.label} — ${scenario.variation} (${abs(viewHref(report))})`, - "", - `Benchmarks / ${section.label} / ${scenario.variation}`, - "", - `- Run: ${run.date}${run.isLatest ? " (the latest run)" : ` (not the latest run; the latest is ${run.latest})`}. Results regenerate nightly.`, - `- Measured: mean ${report.measure} time ${filters.fixture === null ? `across ${fixtureCount} ${fixtureCount === 1 ? "fixture" : "fixtures"}` : `on the ${fixtureLabel(filters.fixture)} fixture`}${scenario.averageOf === null ? "" : `, averaged over ${scenario.averageOf.scenarios.length} scenarios (derived, not measured)`}, on GitHub-hosted ubuntu-24.04-arm runners.${section.id === "registries" ? " Every registry is installed from with the same npm client." : ""}`, - `- Filters in effect:`, - ` - fixture: ${filters.fixture === null ? `all (${report.fixtures.join(", ")})` : filters.fixture}`, - ` - trend window: ${filters.window.custom ? "custom" : rangeLabel(filters.range).toLowerCase()}, ${filters.window.from} to ${filters.window.to}`, - ` - trend metric: ${METRIC_TEXT[metric].name}`, - ` - hidden tools: ${filters.hide.length === 0 ? "none" : filters.hide.join(", ")}`, - ` - excluded runs: ${filters.exclude.length === 0 ? "none" : filters.exclude.join(", ")}`, - "", - "## Headline", - "", - finding.sentence, - "", - ); - if (finding.qualifier !== null) add(`Not ranked: ${finding.qualifier}`, ""); - if (report.stats !== null) { - add(...report.stats.map((s) => `- ${s.label}: ${s.value} (${s.detail})`), ""); - } - - add("## Scenario", "", scenario.description); - if (scenario.uncurated) add("", "Published in this run, and not yet in any curated ordering."); - if (scenario.averageOf !== null) { - const { scenarios, byFixture } = scenario.averageOf; - add( - "", - Object.values(byFixture).every((v) => v.length === scenarios.length) - ? `Mean of: ${scenarios.join(", ")}.` - : `Mean of, per fixture: ${Object.entries(byFixture) - .map(([f, v]) => `${fixtureLabel(f)}: ${v.join(", ")}`) - .join("; ")}.`, - ); - } - if (scenario.stateParts !== null) { - add( - "", - `Before each run: ${scenario.stateParts.map((p) => `${p.name} ${p.word}`).join(", ")}.`, - ); - } - add(""); - const against = report.baseline === null ? "the baseline" : label(report, report.baseline); - add( - "## Leaderboard", - "", - `Ranked by mean ${report.measure} time; lower is faster. ±RSD is the pooled relative standard deviation. Since last run compares with the previous run in the trend window.`, - "", - table( - [ - "#", - "Tool", - "Key", - "Mean (s)", - "±RSD", - `Against ${against}`, - "Since last run", - "Fixtures", - "Version", - ], - report.leaderboard - .filter((r) => !filters.hide.includes(r.tool)) - .map((r) => [ - String(r.rank), - r.label, - r.tool, - num(r.mean), - `${(r.rsd * 100).toFixed(1)}%`, - r.isBaseline ? "baseline" : r.ratio === null ? "—" : formatVersus(r.ratio), - r.sinceLastRun === null ? "—" : percent(r.sinceLastRun), - `${r.fixtures}/${fixtureCount}${r.failed ? ", DNF" : ""}`, - r.version ?? "—", - ]), - ), - "", - ); - if (report.unranked.length > 0) { - add( - `Not ranked (completed no fixture): ${report.unranked.map((t) => label(report, t)).join(", ")}.`, - "", - ); - } - - /* - * The rest follows the page, section by section and figure by figure, in - * its order and under its titles (`outline`): each chart as the numbers it - * draws, each table as a table. - */ const shown = (tool: string) => !filters.hide.includes(tool); const tools = [...report.ranked, ...report.unranked].filter(shown); - const items = new Set(report.outline.flatMap((s) => s.items.map((i) => i.id))); const fmt = (unit: "s" | "ms", v: number) => unit === "s" ? formatDuration(v) : `${formatMs(v)}ms`; const versus = (tool: string, value: number, reference: number | undefined) => @@ -345,13 +266,49 @@ export const reportMarkdown = (report: ReportModel, origin: string): string => { versus(b.tool, b.value, reference), ]), ); - const measureTitle = report.isScript ? "Execution" : "Install"; + const grid = layout(trend.series.dense); + const timeReference = report.timeBars.bars.find((b) => b.tool === report.baseline)?.value; + const ppReference = report.perPackageBars.bars.find((b) => b.tool === report.baseline)?.value; - add(`## ${measureTitle} time`, ""); - if (items.has("trend-by-tool")) { - add( - "### Trend by tool", + return { + "leaderboard-table": [ + `Ranked by mean ${report.measure} time; lower is faster. ±RSD is the pooled relative standard deviation. Since last run compares with the previous run in the trend window.`, + "", + table( + [ + "#", + "Tool", + "Key", + "Mean (s)", + "±RSD", + `Against ${against}`, + "Since last run", + "Fixtures", + "Version", + ], + report.leaderboard + .filter((r) => !filters.hide.includes(r.tool)) + .map((r) => [ + String(r.rank), + r.label, + r.tool, + num(r.mean), + `${(r.rsd * 100).toFixed(1)}%`, + r.isBaseline ? "baseline" : r.ratio === null ? "—" : formatVersus(r.ratio), + r.sinceLastRun === null ? "—" : percent(r.sinceLastRun), + `${r.fixtures}/${fixtureCount}${r.failed ? ", DNF" : ""}`, + r.version ?? "—", + ]), + ), "", + ...(report.unranked.length === 0 + ? [] + : [ + `Not ranked (completed no fixture): ${report.unranked.map((t) => label(report, t)).join(", ")}.`, + "", + ]), + ], + "trend-by-tool": [ `Each tool's mean on this run, and how it moved from its first to its last run in the trend window${trend.focus === null ? "" : ` (focused on ${trend.focus.from} to ${trend.focus.to})`}; lower is faster.`, "", table( @@ -371,86 +328,67 @@ export const reportMarkdown = (report: ReportModel, origin: string): string => { }), ), "", - ); - } - const grid = layout(trend.series.dense); - add( - "### Performance over time", - "", - `${metric === "total" ? `Mean ${report.measure} time in seconds` : METRIC_TEXT[metric].unit}, ${grid.rows} from ${filters.window.from} to ${filters.window.to}.${trend.cut === null ? "" : ` ${trend.cut}`} An empty cell is a ${grid.row} with no published value for that tool: no run, a DNF, a partial run or an exclusion. The chart bridges those with a dotted line by default; nothing is interpolated here. For other windows use the history endpoint: ${abs(`/api/v1/${section.id}/${encodeURIComponent(scenario.variation)}/history`)}`, - "", - ...trendLines({ - rows: metricRows( - rowsOf(trend.series), - [...report.ranked, ...report.unranked], - metric, - report.baseline, - ), - runs: trend.points, - markers: trend.markers, - outliers: trend.outliers, - tools, - fixtureCount, - label: (tool) => label(report, tool), - level: "####", - }), - ); - const timeReference = report.timeBars.bars.find((b) => b.tool === report.baseline)?.value; - add( - `### Total ${report.measure} time by tool`, - "", - `Mean across ${fixtureCount === 1 ? "the fixture" : `all ${fixtureCount} fixtures`}, fastest first; only tools that completed every fixture.`, - "", - barTable(report.timeBars.bars, "s", timeReference), - "", - ); - if (items.has("time-by-fixture")) { - add( - `### Total ${report.measure} time by fixture`, + ], + "time-trend": [ + `${metric === "total" ? `Mean ${report.measure} time in seconds` : METRIC_TEXT[metric].unit}, ${grid.rows} from ${filters.window.from} to ${filters.window.to}.${trend.cut === null ? "" : ` ${trend.cut}`} An empty cell is a ${grid.row} with no published value for that tool: no run, a DNF, a partial run or an exclusion. The chart bridges those with a dotted line by default; nothing is interpolated here. For other windows use the history endpoint: ${abs(`/api/v1/${section.id}/${encodeURIComponent(scenario.variation)}/history`)}`, "", - "Each fixture's tools, fastest first. A tool is listed on every fixture it finished; a failed run is named, not timed.", + ...trendLines({ + rows: metricRows( + rowsOf(trend.series), + [...report.ranked, ...report.unranked], + metric, + report.baseline, + ), + runs: trend.points, + markers: trend.markers, + outliers: trend.outliers, + tools, + fixtureCount, + label: (tool) => label(report, tool), + level: "####", + }), + ], + "time-bars": [ + `Mean across ${fixtureCount === 1 ? "the fixture" : `all ${fixtureCount} fixtures`}, fastest first; only tools that completed every fixture.`, "", - ); - for (const f of fixtureBars(report.timeRows)) { - const reference = f.bars.find((b) => b.tool === report.baseline)?.value; - add(`#### ${fixtureLabel(f.fixture)}`, "", barTable(f.bars, "s", reference), ""); - const failed = f.failed.filter(shown); - if (failed.length > 0) - add(`Did not finish: ${failed.map((t) => label(report, t)).join(", ")}.`, ""); - } - } - add( - `### Total ${report.measure} time data`, - "", - "Mean seconds per fixture. DNF: the run exited non-zero and is excluded from every figure. —: not run on that fixture.", - "", - perFixture(report, report.timeRows), - "", - ); - if (perPackage) { - const ppReference = report.perPackageBars.bars.find((b) => b.tool === report.baseline)?.value; - add( - "## Per-package install time", + barTable(report.timeBars.bars, "s", timeReference), "", - "Derived: install time divided by the number of packages the tool installed. A tool with a time but no published package count is left out rather than divided by a guess.", + ], + "time-by-fixture": [ + "Each fixture's tools, fastest first. A tool is listed on every fixture it finished; a failed run is named, not timed.", "", - "### Per-package time by tool", + ...fixtureBars(report.timeRows).flatMap((f) => { + const reference = f.bars.find((b) => b.tool === report.baseline)?.value; + const failed = f.failed.filter(shown); + return [ + `#### ${fixtureLabel(f.fixture)}`, + "", + barTable(f.bars, "s", reference), + "", + ...(failed.length === 0 + ? [] + : [`Did not finish: ${failed.map((t) => label(report, t)).join(", ")}.`, ""]), + ]; + }), + ], + "time-table": [ + "Mean seconds per fixture. DNF: the run exited non-zero and is excluded from every figure. —: not run on that fixture.", + "", + perFixture(report, report.timeRows), "", + ], + "per-package-chart": [ "Milliseconds per installed package, mean across fixtures, fastest first; only tools with a value on every fixture.", "", barTable(report.perPackageBars.bars, "ms", ppReference), "", - ); - if (items.has("per-package-by-fixture")) { - add( - "### Per-package time by fixture (ms per installed package, derived)", - "", - `Each fixture's per-package time, run by run, ${grid.rows}; an empty cell is no value for that tool on that ${grid.row}.`, - "", - ); - for (const { fixture, series } of trend.perPackageByFixture) { + ], + "per-package-by-fixture": [ + `Each fixture's per-package time, run by run, ${grid.rows}; an empty cell is no value for that tool on that ${grid.row}.`, + "", + ...trend.perPackageByFixture.flatMap(({ fixture, series }) => { const rows = rowsOf(series); - add( + return [ `#### ${fixtureLabel(fixture)}`, "", ...trendLines({ @@ -477,35 +415,76 @@ export const reportMarkdown = (report: ReportModel, origin: string): string => { label: (tool) => label(report, tool), level: "#####", }), - ); - } - } - add("### Per-package time data", "", perFixture(report, report.perPackageRows), ""); - } - if (footprint !== undefined) { - add("## Install footprint", ""); - if (footprint.items.some((i) => i.id === "package-count")) { - add( - "### Package count", - "", - "Packages installed. A range after a count means that tool's installs did not all produce the same number.", - "", - counts(report, report.packageCounts), - "", - ); - } - if (footprint.items.some((i) => i.id === "process-count")) { - add("### Spawned processes", "", counts(report, report.processCounts), ""); - } - } + ]; + }), + ], + "per-package-table": [perFixture(report, report.perPackageRows), ""], + "package-count": [ + "Packages installed. A range after a count means that tool's installs did not all produce the same number.", + "", + counts(report, report.packageCounts), + "", + ], + "process-count": [counts(report, report.processCounts), ""], + }; +}; + +/** + * What the per-package section says before its figures: that its numbers are + * derived, and why a tool can be missing. The report's Markdown opens the + * section with it, and so does the page's twin (an `` in page.tsx); + * on the page it is the figures' guides and axis labels. + */ +export const PER_PACKAGE_DERIVED = + "Derived: install time divided by the number of packages the tool installed. A tool with a time but no published package count is left out rather than divided by a guess."; + +/** What a derived average is the mean of: its scenarios, per fixture where they differ. */ +export const meanOf = ({ + scenarios, + byFixture, +}: NonNullable): string => + Object.values(byFixture).every((v) => v.length === scenarios.length) + ? `Mean of: ${scenarios.join(", ")}.` + : `Mean of, per fixture: ${Object.entries(byFixture) + .map(([f, v]) => `${fixtureLabel(f)}: ${v.join(", ")}`) + .join("; ")}.`; + +/** The run and the filters a report is for: what the page's menus, picker and chips show. */ +export const runMarkdown = (report: ReportModel): string[] => { + const { section, scenario, run, filters, trend } = report; + /* What the trend shows: the reader's metric, as the chart and the history + endpoint apply it. Per package falls back to total where it cannot be drawn. */ + const metric: Metric = filters.metric === "per-package" ? trend.plotted : filters.metric; + const fixtureCount = report.fixtures.length; + return [ + `- Run: ${run.date}${run.isLatest ? " (the latest run)" : ` (not the latest run; the latest is ${run.latest})`}. Results regenerate nightly.`, + `- Measured: mean ${report.measure} time ${filters.fixture === null ? `across ${fixtureCount} ${fixtureCount === 1 ? "fixture" : "fixtures"}` : `on the ${fixtureLabel(filters.fixture)} fixture`}${scenario.averageOf === null ? "" : `, averaged over ${scenario.averageOf.scenarios.length} scenarios (derived, not measured)`}, on GitHub-hosted ubuntu-24.04-arm runners.${section.id === "registries" ? " Every registry is installed from with the same npm client." : ""}`, + `- Filters in effect:`, + ` - fixture: ${filters.fixture === null ? `all (${report.fixtures.join(", ")})` : filters.fixture}`, + ` - trend window: ${filters.window.custom ? "custom" : rangeLabel(filters.range).toLowerCase()}, ${filters.window.from} to ${filters.window.to}`, + ` - trend metric: ${METRIC_TEXT[metric].name}`, + ` - hidden tools: ${filters.hide.length === 0 ? "none" : filters.hide.join(", ")}`, + ` - excluded runs: ${filters.exclude.length === 0 ? "none" : filters.exclude.join(", ")}`, + "", + ]; +}; +/** + * What a report is read against: its caveats, method, environment, data and + * citation. The page keeps the raw files in a menu and the method on its own + * page; the twin says them here. + */ +export const referenceMarkdown = (report: ReportModel, origin: string): string[] => { + const abs = (path: string) => new URL(path, origin).href; + const { section, scenario, filters, finding, caveats } = report; + const query = viewHref(report).slice(report.path.length); const notes = [ ...caveats.leaderboard.map((n) => `${n.title}. ${n.body}`), ...caveats.timeBars.map((n) => `Total ${report.measure} time by tool — ${n.title}. ${n.body}`), ...caveats.perPackageBars.map((n) => `Per-package time by tool — ${n.title}. ${n.body}`), ...(caveats.hidden === null ? [] : [`${caveats.hidden.title}. ${caveats.hidden.body}`]), ]; - add( + return [ "## Caveats", "", "- Total time is the measurement. Per-package time, change since start and relative-to-baseline are derived from it and labelled derived wherever they appear.", @@ -543,7 +522,94 @@ export const reportMarkdown = (report: ReportModel, origin: string): string => { citation("bibtex", report.citation, abs(report.citation.path)), "```", "", + ]; +}; + +export const reportMarkdown = (report: ReportModel, origin: string): string => { + const abs = (path: string) => new URL(path, origin).href; + const { section, scenario, finding } = report; + const perPackage = report.outline.some((s) => s.id === "per-package"); + const footprint = report.outline.find((s) => s.id === "footprint"); + const lines: string[] = []; + const add = (...more: string[]) => lines.push(...more); + + add( + `# ${section.label} — ${scenario.variation} (${abs(viewHref(report))})`, + "", + `Benchmarks / ${section.label} / ${scenario.variation}`, + "", + ...runMarkdown(report), + "## Headline", + "", + finding.sentence, + "", ); + if (finding.qualifier !== null) add(`Not ranked: ${finding.qualifier}`, ""); + if (report.stats !== null) { + add(...report.stats.map((s) => `- ${s.label}: ${s.value} (${s.detail})`), ""); + } + + add("## Scenario", "", scenario.description); + if (scenario.uncurated) add("", "Published in this run, and not yet in any curated ordering."); + if (scenario.averageOf !== null) add("", meanOf(scenario.averageOf)); + if (scenario.stateParts !== null) { + add( + "", + `Before each run: ${scenario.stateParts.map((p) => `${p.name} ${p.word}`).join(", ")}.`, + ); + } + add(""); + + const figures = figureMarkdown(report, origin); + const body = (figure: string): readonly string[] => figures[figure] ?? []; + const items = new Set(report.outline.flatMap((s) => s.items.map((i) => i.id))); + const measureTitle = report.isScript ? "Execution" : "Install"; + + add("## Leaderboard", "", ...body("leaderboard-table")); + + /* + * The rest follows the page, section by section and figure by figure, in + * its order and under its titles (`outline`): each chart as the numbers it + * draws, each table as a table. + */ + add(`## ${measureTitle} time`, ""); + if (items.has("trend-by-tool")) add("### Trend by tool", "", ...body("trend-by-tool")); + add("### Performance over time", "", ...body("time-trend")); + add(`### Total ${report.measure} time by tool`, "", ...body("time-bars")); + if (items.has("time-by-fixture")) { + add(`### Total ${report.measure} time by fixture`, "", ...body("time-by-fixture")); + } + add(`### Total ${report.measure} time data`, "", ...body("time-table")); + if (perPackage) { + add( + "## Per-package install time", + "", + PER_PACKAGE_DERIVED, + "", + "### Per-package time by tool", + "", + ...body("per-package-chart"), + ); + if (items.has("per-package-by-fixture")) { + add( + "### Per-package time by fixture (ms per installed package, derived)", + "", + ...body("per-package-by-fixture"), + ); + } + add("### Per-package time data", "", ...body("per-package-table")); + } + if (footprint !== undefined) { + add("## Install footprint", ""); + if (footprint.items.some((i) => i.id === "package-count")) { + add("### Package count", "", ...body("package-count")); + } + if (footprint.items.some((i) => i.id === "process-count")) { + add("### Spawned processes", "", ...body("process-count")); + } + } + + add(...referenceMarkdown(report, origin)); return lines.join("\n"); }; diff --git a/apps/www/src/lib/methodology-markdown.ts b/apps/www/src/lib/methodology-markdown.ts new file mode 100644 index 0000000000..57dfc413ac --- /dev/null +++ b/apps/www/src/lib/methodology-markdown.ts @@ -0,0 +1,41 @@ +import { mdxMarkdown, mdxSource } from "@/lib/mdx"; +import { METHOD, READING, RESOURCES, RUNNER, RUNNER_NOTE, TRADEMARK } from "@/lib/metadata"; +import type { SectionScenarios } from "@/lib/page-tree"; +import { DESCRIPTIONS, scenarioPath, stateParts } from "@/lib/scenarios"; +import { SECTION_DEFS, SECTIONS } from "@/lib/sections"; + +/** The Methodology page as Markdown: content.mdx, each block written out as ./components.tsx draws it. */ +export const methodologyMarkdown = (scenarios: SectionScenarios, origin: string): string => { + const abs = (path: string) => new URL(path, origin).href; + const lines = (...lines: string[]) => lines.join("\n"); + const body = mdxMarkdown(mdxSource("app/methodology/content.mdx"), { + Method: METHOD, + Environment: lines( + ...RUNNER.map(([term, value]) => `- ${term}: \`${value}\``), + "", + RUNNER_NOTE, + ), + Reading: lines(...READING.map((line) => `- ${line}`)), + Scenarios: SECTIONS.map((section) => + lines( + `### ${SECTION_DEFS[section].label}`, + "", + SECTION_DEFS[section].about, + "", + ...scenarios[section].map( + (s) => + `- [${s.variation}](${abs(`${scenarioPath(section, s.variation)}.md`)}): ${DESCRIPTIONS[s.variation] ?? "Published, with no description yet."}${ + s.kind === "other" + ? "" + : ` Before each run: ${stateParts(s.state) + .map((p) => `${p.name} ${p.word}`) + .join(", ")}.` + }`, + ), + ), + ).join("\n\n"), + Resources: lines(...RESOURCES.map(([term, , href]) => `- ${term}: ${href}`)), + Trademark: TRADEMARK, + }); + return `# Methodology (${abs("/methodology")})\n\n${body}`; +}; diff --git a/apps/www/test/average.test.ts b/apps/www/test/average.test.ts index 085b81fb48..b205c000ec 100644 --- a/apps/www/test/average.test.ts +++ b/apps/www/test/average.test.ts @@ -3,8 +3,8 @@ import { DataClient } from "@benchmarks/data"; import nextConfig from "../next.config.ts"; import type { BenchmarkFile } from "@benchmarks/types"; import { GET as api } from "../src/app/api/v1/[section]/[scenario]/route.ts"; +import reportTwin, { slots } from "../src/app/[section]/[[...params]]/markdown.ts"; import { GET as llms } from "../src/app/llms.txt/route.ts"; -import { GET as markdown } from "../src/app/llms.mdx/[[...slug]]/route.ts"; import { TREND_BUDGET, trendDates } from "../src/lib/load.ts"; import { averageFiles } from "../src/lib/measurements.ts"; import { treeFor } from "../src/lib/page-tree.ts"; @@ -202,24 +202,25 @@ describe("the average report's twins", () => { vi.unstubAllGlobals(); }); - it("Markdown says it is derived, and of what", async () => { - const res = await markdown(new Request(`${ORIGIN}/llms.mdx/package-managers/average`), { - params: Promise.resolve({ slug: ["package-managers", "average"] }), - }); - const text = await res.text(); - expect(text.split("\n")[0]).toBe( - `# Package managers — average (${ORIGIN}/package-managers/average)`, - ); - expect(text).toContain("averaged over 2 scenarios (derived, not measured)"); - expect(text).toContain("Mean of: clean, cache."); - expect(text).not.toContain("## Footprint"); + /* The handler's args for `/package-managers/average`, as twins.test.ts builds them. */ + const twin = (path: string) => { + const [section = "", ...rest] = path.split("/").slice(1); + return { + route: "/[section]/[[...params]]" as const, + params: rest.length === 0 ? { section } : { section, params: rest }, + searchParams: new URLSearchParams(), + request: new Request(`${ORIGIN}/api/affordances/md${path}`), + }; + }; + + it("its Markdown twin says it is derived, and of what", async () => { + const context = await slots.context(twin("/package-managers/average")); + expect(context).toContain("averaged over 2 scenarios (derived, not measured)"); + expect(context).toContain("Mean of: clean, cache."); }); - it("the section's own Markdown is the average", async () => { - const res = await markdown(new Request(`${ORIGIN}/llms.mdx/package-managers`), { - params: Promise.resolve({ slug: ["package-managers"] }), - }); - expect(await res.text()).toMatch(/^# Package managers — average /); + it("the section's own twin is the page's redirect to the average", async () => { + expect(await reportTwin(twin("/package-managers"))).toBeUndefined(); }); it("the JSON API returns it", async () => { diff --git a/apps/www/test/honesty-inventory.test.ts b/apps/www/test/honesty-inventory.test.ts index 51802bc768..c91e0fad6a 100644 --- a/apps/www/test/honesty-inventory.test.ts +++ b/apps/www/test/honesty-inventory.test.ts @@ -1041,3 +1041,48 @@ describe("a partial day is a gap on the trend, never a dip", () => { expect(p?.failed.zpm?.[0]?.fixture).toBe("babylon"); }); }); + +/* + * The Markdown twin was the Markdown builder's alone (lib/markdown.ts); it is + * now the page's HTML, converted by @affordances/md, with the builder's + * numbers in each figure's slot. What moved, and where it is checked: + * + * - The numbers (leaderboard, every chart's values, the trend CSV with its + * gaps as empty cells, DNFs, version changes, outliers, every table): + * still the builder's, through `slots` -- twins.test.ts checks each fill is + * the builder's lines for that figure, and these entries check the builder. + * - The run and filters in effect, a fallen-back selection, and an average's + * scenarios: the `context` slot -- twins.test.ts, average.test.ts. + * - Caveats, method, environment, data and citation: the `reference` slot -- + * twins.test.ts. + * - That per-package numbers are derived, and why a tool without a count is + * missing (PER_PACKAGE_DERIVED): an `` heading the section, since + * the figures' guides, which say it on the page, are not the twin's -- + * twins.e2e.mjs, and below. + * - The headline, the lede, the stats and each figure's notes: the page's own + * text, converted -- twins.e2e.mjs against a server, since only a render + * has them. + * - A 404 an agent can act on: the default export, since the page streams its + * `notFound()` as a 200 -- twins.test.ts and twins.e2e.mjs. + */ +describe("inventory 16 · the twin's numbers are the builder's, never a chart's markup", () => { + const read = (p: string) => readFileSync(new URL(p, import.meta.url), "utf8"); + + it("every figure's body is a slot named for the figure", () => { + expect(read("../src/components/report.tsx")).toMatch( + /\{children\}<\/AgentSlot>/, + ); + }); + + it("the per-package section still says its numbers are derived, in the builder's words", () => { + expect(read("../src/app/[section]/[[...params]]/page.tsx")).toMatch( + /part\.id === "per-package"[\s\S]{0,80}\s*

\{PER_PACKAGE_DERIVED\}<\/p>/, + ); + }); + + it("a figure's guide, which describes the drawn marks, is not the twin's", () => { + expect(read("../src/components/report.tsx")).toMatch( + /data-narrative\n\s+data-affordances-md="skip"/, + ); + }); +}); diff --git a/apps/www/test/routes.test.ts b/apps/www/test/routes.test.ts index fc69528d6b..18cf775c40 100644 --- a/apps/www/test/routes.test.ts +++ b/apps/www/test/routes.test.ts @@ -5,9 +5,7 @@ import { GET as report } from "../src/app/api/v1/[section]/[scenario]/route.ts"; import { GET as history } from "../src/app/api/v1/[section]/[scenario]/history/route.ts"; import { GET as llmsFull } from "../src/app/llms-full.txt/route.ts"; import { GET as llms } from "../src/app/llms.txt/route.ts"; -import { GET as markdown } from "../src/app/llms.mdx/[[...slug]]/route.ts"; import { GET as openapi } from "../src/app/openapi.json/route.ts"; -import { METHOD, READING, RUNNER_NOTE, TRADEMARK } from "../src/lib/metadata.ts"; import { DATA_URL, fakeFetch } from "./fixture-tree.ts"; const ORIGIN = "https://benchmarks.vlt.io"; @@ -122,58 +120,7 @@ describe("JSON API", () => { }); }); -describe("Markdown twins", () => { - const md = (slug: string[] | undefined, search = "") => - markdown(get(`/llms.mdx/${(slug ?? []).join("/")}${search}`), { - params: Promise.resolve({ slug }), - }); - - it("a scenario page, for exactly the filtered view", async () => { - const res = await md(["package-managers", "clean"], "?fixture=astro"); - expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); - expect(res.headers.get("vary")).toBe("Accept"); - const text = await res.text(); - expect(text).toContain(" - fixture: astro"); - expect(text).toContain("On Astro, "); - }); - - it("a section with nothing to average opens on its first scenario", async () => { - expect(await (await md(["package-managers"])).text()).toMatch(/^# Package managers — clean /); - }); - - it("an unknown page is a Markdown 404 with somewhere to go", async () => { - const res = await md(["nope"]); - expect(res.status).toBe(404); - const text = await res.text(); - expect(text).toMatch(/^# 404: Page not found/); - expect(text).toContain(`[llms.txt](${ORIGIN}/llms.txt)`); - }); - - it("methodology: the fact-checked wording word for word, and every published scenario", async () => { - const res = await md(["methodology"]); - expect(res.status).toBe(200); - const text = await res.text(); - expect(text.split("\n")[0]).toBe(`# Methodology (${ORIGIN}/methodology)`); - expect(text).toContain(`## Method\n\n${METHOD}\n`); - expect(text).toContain("- Workflow label: `ubuntu-24.04-arm`"); - expect(text).toContain(RUNNER_NOTE); - for (const line of READING) expect(text).toContain(`- ${line}`); - expect(text).toContain( - `- [clean](${ORIGIN}/package-managers/clean.md): A first install on a fresh machine.`, - ); - expect(text).toContain("Before each run: binary preinstalled, cache empty, lockfile absent"); - expect(text).toContain(`- [registry-clean](${ORIGIN}/registries/registry-clean.md)`); - expect(text).toContain( - "- Methodology: https://github.com/vltpkg/benchmarks/blob/main/docs/METHODOLOGY.md", - ); - expect(text.trimEnd().endsWith(TRADEMARK)).toBe(true); - expect(text).not.toMatch(/Ampere|Graviton|Neoverse/); - }); - - it("the home page's twin is llms.txt", async () => { - expect(await (await md(undefined)).text()).toBe(await (await llms(get("/llms.txt"))).text()); - }); - +describe("llms.txt and llms-full.txt", () => { it("llms.txt indexes every page as Markdown, and documents the API and MCP", async () => { const text = await (await llms(get("/llms.txt"))).text(); expect(text).toContain(`- [clean](${ORIGIN}/package-managers/clean.md)`); diff --git a/apps/www/test/twins.e2e.mjs b/apps/www/test/twins.e2e.mjs new file mode 100644 index 0000000000..d01b08c8f1 --- /dev/null +++ b/apps/www/test/twins.e2e.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +/** + * The Markdown twins against a running site: what the @affordances/md handler + * makes of each page's HTML, its slots filled. Not a vitest test: it needs a + * server (`next start`, data from DATA_URL). + * + * node apps/www/test/twins.e2e.mjs [http://localhost:3000] + * + * Exits non-zero, naming each failed check, if any fails. + */ +const base = process.argv[2] ?? process.env.BASE_URL ?? "http://localhost:3000"; +const failures = []; +const check = (name, ok, detail = "") => { + console.log(`${ok ? "ok " : "FAIL"} ${name}${ok || !detail ? "" : `: ${detail}`}`); + if (!ok) failures.push(name); +}; +const get = (path, headers = {}) => fetch(new URL(path, base), { headers, redirect: "manual" }); +const markdown = (res) => res.headers.get("content-type")?.startsWith("text/markdown") ?? false; + +{ + const res = await get("/package-managers/clean.md"); + const text = await res.text(); + check("a report's twin is 200 Markdown", res.status === 200 && markdown(res), String(res.status)); + check( + "a CDN may keep it an hour, and serve it stale an hour more", + res.headers.get("cache-control") === "s-maxage=3600, stale-while-revalidate=3600", + String(res.headers.get("cache-control")), + ); + for (const heading of [ + "## Leaderboard", + "### Performance over time", + "### Total install time data", + "## Cite", + ]) { + check(`it has "${heading}"`, text.includes(`\n${heading}\n`)); + } + check("its trend is a CSV with a date column", /```csv\ndate,/.test(text)); + check("it states the filters in effect", text.includes("- Filters in effect:")); + const perPackage = text.slice(text.indexOf("\n## Per-package install time\n")); + check( + "its per-package section says it is derived, and why a tool can be missing", + perPackage.includes("Derived: install time divided by") && + perPackage.includes("divided by a guess"), + ); + check("no figure is left as its chart's markup", !/^\*$/m.test(text)); + const negotiated = await get("/package-managers/clean?fixture=astro", { + accept: "text/markdown", + }); + check( + "Accept: text/markdown at the page's URL redirects to its twin, query kept", + negotiated.status === 307 && + negotiated.headers.get("location") === "/package-managers/clean.md?fixture=astro", + `${negotiated.status} ${negotiated.headers.get("location")}`, + ); + await negotiated.body?.cancel(); + const followed = await fetch(new URL("/package-managers/clean", base), { + headers: { accept: "text/markdown" }, + }); + check("...which is the same twin", (await followed.text()) === text); +} +{ + const text = await (await get("/package-managers/clean.md?fixture=astro")).text(); + check( + "the query is the filtered view", + text.includes(" - fixture: astro") && /^# On Astro, /m.test(text), + ); +} +const notFound = async (path, ...lines) => { + const res = await get(path); + const text = await res.text(); + check( + `${path} is a 404 no one caches`, + res.status === 404 && res.headers.get("cache-control") === "private, no-store", + `${res.status} ${res.headers.get("cache-control")}`, + ); + check( + `...that says where to go`, + /^# 404: Page not found$/m.test(text) && + text.includes("/llms.txt)") && + lines.every((line) => text.includes(line)), + ); +}; +await notFound("/nope.md", "There is no page at `/nope`"); +await notFound("/package-managers/clean/extra.md", "`/package-managers/clean/extra`"); +await notFound("/package-managers/nope.md", "published in this run: `clean`"); +{ + const res = await get("/registries.md"); + check( + "a section's twin redirects to its average's", + res.status === 307 && res.headers.get("location")?.endsWith("/registries/average.md"), + `${res.status} ${res.headers.get("location")}`, + ); + const root = await get("/", { accept: "text/markdown" }); + check( + "/ redirects (next.config.ts) before any twin", + root.status === 307 && root.headers.get("location")?.endsWith("/registries/average"), + `${root.status} ${root.headers.get("location")}`, + ); +} +{ + const text = await (await get("/methodology.md")).text(); + check("the Methodology twin is its written Markdown", /^# Methodology \(/m.test(text)); + const about = await get("/about.md"); + const index = await (await get("/llms.txt")).text(); + check( + "the About twin is the llms.txt index", + (await about.text()).endsWith(index) && index.includes("## Reading the results"), + ); + check( + "...cached like every twin", + about.headers.get("cache-control") === "s-maxage=3600, stale-while-revalidate=3600", + ); +} +{ + const listed = ((await (await get("/llms.txt")).text()).match(/^- \[.+\]\(.+\.md\)/gm) ?? []) + .length; + const res = await get("/llms-full.txt"); + const pages = ((await res.text()).match(/^# .+ — .+ \(https?:\/\/[^)]+\)$/gm) ?? []).length; + check( + "llms-full.txt holds every page llms.txt lists, and a CDN may keep it", + listed > 0 && + pages === listed && + res.headers.get("cache-control") === "s-maxage=3600, stale-while-revalidate=3600", + `${pages} of ${listed} pages, ${res.headers.get("cache-control")}`, + ); +} +{ + const res = await get("/package-managers/clean"); + check( + "the page links to its twin", + (res.headers.get("link") ?? "").includes( + '; rel="alternate"; type="text/markdown"', + ), + ); + await res.body?.cancel(); +} + +if (failures.length > 0) { + console.error(`\n${failures.length} failed`); + process.exit(1); +} +console.log("\nall passed"); diff --git a/apps/www/test/twins.test.ts b/apps/www/test/twins.test.ts new file mode 100644 index 0000000000..990f249f37 --- /dev/null +++ b/apps/www/test/twins.test.ts @@ -0,0 +1,171 @@ +/** + * The Markdown twins' own code (@affordances/md): each route's `markdown.ts`, + * called as the handler calls it. What the handler does with the page's HTML + * is checked against a running server by twins.e2e.mjs. + */ +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import reportTwin, { + notFoundMarkdown, + publicCache, + slots, +} from "../src/app/[section]/[[...params]]/markdown.ts"; +import aboutTwin from "../src/app/about/markdown.ts"; +import methodologyTwin from "../src/app/methodology/markdown.ts"; +import { GET as llms } from "../src/app/llms.txt/route.ts"; +import { figureMarkdown, reportMarkdown, TWIN_CACHE } from "../src/lib/markdown.ts"; +import { METHOD, READING, RUNNER_NOTE, TRADEMARK } from "../src/lib/metadata.ts"; +import { loadReportParams } from "../src/lib/params.ts"; +import { loadReport, type ReportModel } from "../src/lib/report.ts"; +import { DATA_URL, fakeFetch } from "./fixture-tree.ts"; + +const ORIGIN = "https://benchmarks.vlt.io"; + +beforeAll(() => { + vi.stubEnv("DATA_URL", DATA_URL); + vi.stubGlobal("fetch", fakeFetch); +}); +afterAll(() => { + vi.unstubAllEnvs(); + vi.unstubAllGlobals(); +}); + +/* `MarkdownArgs` as the handler builds them for a page path: params URI-encoded, the query as given. */ +const args = (path: string, search = "") => { + const [section = "", ...rest] = path.split("/").slice(1).map(encodeURIComponent); + return { + route: "/[section]/[[...params]]" as const, + params: rest.length === 0 ? { section } : { section, params: rest }, + searchParams: new URLSearchParams(search), + request: new Request(`${ORIGIN}/api/affordances/md${path}${search}`), + }; +}; + +const reportOf = async (section: string, scenario: string, search = ""): Promise => { + const result = await loadReport({ + section, + scenario, + query: loadReportParams(new URLSearchParams(search)), + }); + if (result.status !== "ok") throw new Error(result.status); + return result.report; +}; + +const fill = async (name: keyof typeof slots, path: string, search = "") => + slots[name](args(path, search)); + +describe("a report's twin", () => { + it("is its page: the default leaves a report to the HTML", async () => { + expect(await reportTwin(args("/package-managers/clean"))).toBeUndefined(); + expect(await reportTwin(args("/package-managers/clean", "?fixture=astro"))).toBeUndefined(); + }); + + it("leaves `/{section}` to the page's redirect to its average", async () => { + expect(await reportTwin(args("/package-managers"))).toBeUndefined(); + }); + + it("fills every figure on the page with the numbers the Markdown builder gives it", async () => { + const report = await reportOf("package-managers", "clean"); + const figures = figureMarkdown(report, ORIGIN); + const whole = reportMarkdown(report, ORIGIN); + for (const item of report.outline.flatMap((s) => s.items)) { + expect(Object.keys(slots)).toContain(item.id); + const filled = await fill(item.id as keyof typeof slots, "/package-managers/clean"); + expect(filled).toBe(figures[item.id]?.join("\n")); + // ...the same lines the MCP tools and the API's Markdown carry. + expect(whole).toContain(filled); + } + }); + + it("is the filtered view: the query reaches every fill", async () => { + const context = await fill("context", "/package-managers/clean", "?fixture=astro"); + expect(context).toContain(" - fixture: astro"); + const trend = await fill("time-trend", "/package-managers/clean", "?fixture=astro&range=all"); + expect(trend).toMatch(/^Mean install time in seconds, one row per calendar day from /); + expect(trend).toContain("```csv\ndate,"); + const astro = await reportOf("package-managers", "clean", "?fixture=astro&range=all"); + expect(trend).toBe(figureMarkdown(astro, ORIGIN)["time-trend"]?.join("\n")); + }); + + it("says what a strict reader is told when the report falls back from a selection", async () => { + const context = await fill("context", "/package-managers/clean", "?fixture=nope"); + expect(context).toMatch( + /^> The clean scenario published no "nope" fixture on .+ This is the report without that selection\.\n/, + ); + }); + + it("closes with the caveats, method, data and a citation", async () => { + const reference = await fill("reference", "/package-managers/clean"); + expect(reference).toMatch(/^## Caveats\n/); + expect(reference).toContain(`## Method\n\n${METHOD}\n`); + expect(reference).toContain(`- This report as JSON: ${ORIGIN}/api/v1/package-managers/clean`); + expect(reference).toContain("```bibtex"); + }); + + it("an unknown section is the page's own 404, with our body and somewhere to go", async () => { + // The page calls notFound(), so the twin is the handler's 404 with notFoundMarkdown's body. + expect(await reportTwin(args("/nope"))).toBeUndefined(); + const text = notFoundMarkdown(args("/nope")); + expect(text).toMatch( + /^# 404: Page not found\n\nThere is no page at `\/nope` on vlt benchmarks\.\n/, + ); + expect(text).toContain(`[llms.txt](${ORIGIN}/llms.txt)`); + expect(notFoundMarkdown(args("/nope/clean"))).toContain("`/nope/clean`"); + }); + + it("a path past the scenario is a Markdown 404, where the page renders the scenario", async () => { + const res = await reportTwin(args("/package-managers/clean/extra")); + if (!(res instanceof Response)) throw new Error("no response"); + expect(res.status).toBe(404); + const text = await res.text(); + expect(text).toMatch(/^# 404: Page not found/); + expect(text).toContain("`/package-managers/clean/extra`"); + expect(text).toContain(`[llms.txt](${ORIGIN}/llms.txt)`); + }); + + it("is cached as long as every twin", () => { + expect(publicCache).toBe(TWIN_CACHE); + }); + + it("an unknown scenario is a 404 that names the published ones, where the page redirects", async () => { + const res = await reportTwin(args("/package-managers/nope")); + if (!(res instanceof Response)) throw new Error("no response"); + expect(res.status).toBe(404); + expect(await res.text()).toContain("published in this run: `clean`"); + }); +}); + +describe("the Methodology twin", () => { + it("is the fact-checked wording word for word, and every published scenario", async () => { + const text = String( + await methodologyTwin({ + params: {}, + searchParams: new URLSearchParams(), + request: new Request(`${ORIGIN}/methodology.md`), + }), + ); + expect(text.split("\n")[0]).toBe(`# Methodology (${ORIGIN}/methodology)`); + expect(text).toContain(`## Method\n\n${METHOD}\n`); + expect(text).toContain("- Workflow label: `ubuntu-24.04-arm`"); + expect(text).toContain(RUNNER_NOTE); + for (const line of READING) expect(text).toContain(`- ${line}`); + expect(text).toContain( + `- [clean](${ORIGIN}/package-managers/clean.md): A first install on a fresh machine.`, + ); + expect(text).toContain("Before each run: binary preinstalled, cache empty, lockfile absent"); + expect(text).toContain(`- [registry-clean](${ORIGIN}/registries/registry-clean.md)`); + expect(text).toContain( + "- Methodology: https://github.com/vltpkg/benchmarks/blob/main/docs/METHODOLOGY.md", + ); + expect(text.trimEnd().endsWith(TRADEMARK)).toBe(true); + expect(text).not.toMatch(/Ampere|Graviton|Neoverse/); + }); +}); + +describe("the About twin", () => { + it("is the llms.txt index", async () => { + const request = new Request(`${ORIGIN}/about.md`); + expect(await aboutTwin({ params: {}, searchParams: new URLSearchParams(), request })).toBe( + await (await llms(request)).text(), + ); + }); +}); diff --git a/apps/www/tsconfig.json b/apps/www/tsconfig.json index 94c0d06048..071341ce04 100644 --- a/apps/www/tsconfig.json +++ b/apps/www/tsconfig.json @@ -11,6 +11,6 @@ "plugins": [{ "name": "next" }], "paths": { "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "src", "test", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "src", "test", ".next/types/**/*.ts", ".affordances/**/*.ts"], "exclude": ["node_modules"] } diff --git a/vlt-lock.json b/vlt-lock.json index 887c1314b8..277745726c 100644 --- a/vlt-lock.json +++ b/vlt-lock.json @@ -18,6 +18,7 @@ } }, "nodes": { + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5": [0,"@affordances/md","sha512-08O4Og1jWy0SlYWx8rTexD3Ji2omoFxLUPa1fLvJee2BPq0yra2ssSEk/3ikmMgiatNdwghfYGUoDkm6M0kDwA==",null,null,null,null,null,{ "affordances-md": "dist/cli.js"}], "~npm~@alloc+quick-lru@5.3.0": [2,"@alloc/quick-lru","sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="], "~npm~@babel+runtime@8.0.7": [0,"@babel/runtime","sha512-k0hagDDq/6w4v+0Sp4wFvIz/yswH5O7Va6aRTnD7N62Tnborp4X+YB+H3XUcWVSR4aE47WPEXiIIZchBs0GmYg=="], "~npm~@base-ui+react@1.9.0~peer.489ab62e90f7fd2c": [0,"@base-ui/react","sha512-S8CSYosASBKtpL8SWUfS78rFElfLijiCgA7oHOK22jCJ95FULsYyKwBNFOgudt0RA+/7yNc7F81bOvQnRgbBiQ=="], @@ -244,6 +245,8 @@ "~npm~astring@1.9.0": [0,"astring","sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==",null,null,null,null,null,{ "astring": "bin/astring"}], "~npm~bail@2.0.2": [0,"bail","sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], "~npm~baseline-browser-mapping@2.11.30": [0,"baseline-browser-mapping","sha512-YJs6Pz17RMNv8HoyKREtCY38E3BV1nPkWVmIb+XN1uHC1Vqt3KZCH/uJH9ZKRoiKb/icZ6tvehyee6RZNbFfRA==",null,null,null,null,null,{ "baseline-browser-mapping": "dist/cli.cjs"}], + "~npm~bcp-47-match@2.0.3": [0,"bcp-47-match","sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ=="], + "~npm~boolbase@1.0.0": [0,"boolbase","sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "~npm~caniuse-lite@1.0.30001818": [0,"caniuse-lite","sha512-XJbgo4adscw7R4MZU4/qVFkoMyPaNUAWmdNY4o/k9x7fFwmv4frIlndI9lWuaxviW9ObiZel9LuncnQPn6hQ5A=="], "~npm~ccount@2.0.1": [0,"ccount","sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], "~npm~chai@6.3.0": [2,"chai","sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="], @@ -259,6 +262,7 @@ "~npm~collapse-white-space@2.1.0": [0,"collapse-white-space","sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], "~npm~comma-separated-tokens@2.0.3": [0,"comma-separated-tokens","sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "~npm~commander@11.1.0": [0,"commander","sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + "~npm~css-selector-parser@3.3.0": [0,"css-selector-parser","sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g=="], "~npm~csstype@3.2.3": [0,"csstype","sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "~npm~date-fns@4.4.0": [0,"date-fns","sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], "~npm~debug@4.4.3": [0,"debug","sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -267,11 +271,14 @@ "~npm~detect-libc@2.1.2": [0,"detect-libc","sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "~npm~detect-node-es@1.1.0": [0,"detect-node-es","sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "~npm~devlop@1.1.0": [0,"devlop","sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "~npm~direction@2.0.1": [0,"direction","sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==",null,null,null,null,null,{ "direction": "cli.js"}], "~npm~echarts@6.1.0": [0,"echarts","sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA=="], "~npm~enhanced-resolve@5.26.0": [2,"enhanced-resolve","sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="], + "~npm~entities@9.0.0": [0,"entities","sha512-oJFNDJg3kTPwAD1cNn2+WmUq4wFoo09npc3Ydf1D7ED4smRiWrsKnaLxykrTWgyGEcusc87P5dxlAleol9kpkg=="], "~npm~es-module-lexer@2.3.2": [2,"es-module-lexer","sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="], "~npm~esast-util-from-estree@2.0.0": [0,"esast-util-from-estree","sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="], "~npm~esast-util-from-js@2.0.1": [0,"esast-util-from-js","sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="], + "~npm~escape-string-regexp@5.0.0": [0,"escape-string-regexp","sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "~npm~estree-util-attach-comments@3.0.0": [0,"estree-util-attach-comments","sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], "~npm~estree-util-build-jsx@3.0.1": [0,"estree-util-build-jsx","sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="], "~npm~estree-util-is-identifier-name@3.0.0": [0,"estree-util-is-identifier-name","sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], @@ -286,9 +293,25 @@ "~npm~fsevents@2.3.3": [3,"fsevents","sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",null,null,null,null,{ "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" }, "os": [ "darwin" ]}], "~npm~get-nonce@1.0.1": [0,"get-nonce","sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "~npm~graceful-fs@4.2.11": [2,"graceful-fs","sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "~npm~hast-util-embedded@3.0.0": [0,"hast-util-embedded","sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA=="], + "~npm~hast-util-from-html@2.1.0": [0,"hast-util-from-html","sha512-873p9AuH93gBnATbH8rU3YvjTKxyrIeWd/M9tiLMzn3G82Os0xhQurctfnbYUSrDXoyjbJGpJ2qmTpefMw7ZaQ=="], + "~npm~hast-util-from-parse5@8.0.4": [0,"hast-util-from-parse5","sha512-Yq7XlXor0Cpb71sI2E0zpv8QxQ5a6rfhDEETk4bcgrFX2SvJAY2PHocWwFtL6U9qyfiyByW3wZ6sZEfP2kW81g=="], + "~npm~hast-util-has-property@3.0.0": [0,"hast-util-has-property","sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA=="], + "~npm~hast-util-is-body-ok-link@3.0.1": [0,"hast-util-is-body-ok-link","sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ=="], + "~npm~hast-util-is-element@3.0.0": [0,"hast-util-is-element","sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "~npm~hast-util-minify-whitespace@1.0.1": [0,"hast-util-minify-whitespace","sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw=="], + "~npm~hast-util-parse-selector@4.0.0": [0,"hast-util-parse-selector","sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "~npm~hast-util-phrasing@3.0.1": [0,"hast-util-phrasing","sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ=="], + "~npm~hast-util-select@6.0.4": [0,"hast-util-select","sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw=="], "~npm~hast-util-to-estree@3.1.3": [0,"hast-util-to-estree","sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="], + "~npm~hast-util-to-html@9.0.6": [0,"hast-util-to-html","sha512-1SZUxCN3TtJAE7e2BQN2MBHsoZ1gnmi9GoCP5Di59CSWptIezSAW09yDZyVbanb78V9eCwrKqdQsjtfVTxYHWA=="], "~npm~hast-util-to-jsx-runtime@2.3.6": [0,"hast-util-to-jsx-runtime","sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], + "~npm~hast-util-to-mdast@10.1.2": [0,"hast-util-to-mdast","sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ=="], + "~npm~hast-util-to-string@3.0.1": [0,"hast-util-to-string","sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="], + "~npm~hast-util-to-text@4.0.3": [0,"hast-util-to-text","sha512-5yrlWOvGFs6CqHriUpSsNrjzT/Tdo+IOQudbznxOn8K5qblp18MJyzOg8IgDOKQ8MYgQcJA7SusuCjSpDr8mSA=="], "~npm~hast-util-whitespace@3.0.0": [0,"hast-util-whitespace","sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "~npm~hastscript@9.0.3": [0,"hastscript","sha512-1ALZXSmCGmW3h8AMz4R4+QKSKFi7sNgXLVwmwB8FYVtl1CwQ3WDkSAEY/d5tp1g/vbQgq2s1WlHgfO3jbw1TsA=="], + "~npm~html-void-elements@3.0.0": [0,"html-void-elements","sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], "~npm~inline-style-parser@0.2.7": [0,"inline-style-parser","sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], "~npm~is-alphabetical@2.0.1": [0,"is-alphabetical","sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "~npm~is-alphanumerical@2.0.1": [0,"is-alphanumerical","sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], @@ -325,8 +348,16 @@ "~npm~magic-string@0.30.21": [2,"magic-string","sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "~npm~magic-string@1.4.3": [2,"magic-string","sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ=="], "~npm~markdown-extensions@2.0.0": [0,"markdown-extensions","sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="], + "~npm~markdown-table@3.1.0": [0,"markdown-table","sha512-xpeSCdM+B+6P3ncfgaB7FoDbtJ30xAe6Z18TGT7AgXJsHqCElCICZovQqdRMm7kHgpTyYv36EupqewoFFSB8vw=="], "~npm~mcp-handler@2.3.0~peer.8acc8965eabfcd93": [0,"mcp-handler","sha512-mvs+fJTLTa6kFv71CKPr+2ey+yXeuooM6WHz96pHhSvXe9m83qPizswU5NJ0yrUnJ2T+70VFPjsdstZtdB3evQ==",null,null,null,null,null,{ "mcp-adapter": "dist/cli/index.js", "mcp-handler": "dist/cli/index.js", "create-mcp-route": "dist/cli/index.js"}], + "~npm~mdast-util-find-and-replace@3.0.4": [0,"mdast-util-find-and-replace","sha512-z4B5c6jrnJ1ltOQemhWYPBSAo6y42Z8Gt2yEgfgNTMcLSU59B8bfj8Z8AnAlJrBGLnqkdAUZ7jdDEqQw3KuipQ=="], "~npm~mdast-util-from-markdown@2.1.0": [0,"mdast-util-from-markdown","sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A=="], + "~npm~mdast-util-gfm-autolink-literal@2.1.0": [0,"mdast-util-gfm-autolink-literal","sha512-Jx6QSK8wDo558Bcf7q+1pe0v81g2PVs09Hlvu9xJHmCFRF3sAPW7vXFVIAwBncs3sde92WbsVqNxwdqViA2XnQ=="], + "~npm~mdast-util-gfm-footnote@2.1.0": [0,"mdast-util-gfm-footnote","sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + "~npm~mdast-util-gfm-strikethrough@2.0.1": [0,"mdast-util-gfm-strikethrough","sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw=="], + "~npm~mdast-util-gfm-table@2.0.1": [0,"mdast-util-gfm-table","sha512-fiIjPrAy4kz68/eFbPHhsiWDGHhqUSVoU1XsfLM5DsEPXw7MxgiVVyulUggFRckVWeT1KXQXkg5vm3kv4oMSOA=="], + "~npm~mdast-util-gfm-task-list-item@2.0.1": [0,"mdast-util-gfm-task-list-item","sha512-jXVO1hvB2hs82g1esi7I9nIJDqiijbQLIyH2jDUb5s7jIAZMr/79h0VHSq7XHKTqpLARRm+DDNjduB7S9+6XZA=="], + "~npm~mdast-util-gfm@3.1.0": [0,"mdast-util-gfm","sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], "~npm~mdast-util-mdx-expression@2.0.1": [0,"mdast-util-mdx-expression","sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], "~npm~mdast-util-mdx-jsx@3.2.0": [0,"mdast-util-mdx-jsx","sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], "~npm~mdast-util-mdx@3.0.0": [0,"mdast-util-mdx","sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="], @@ -334,8 +365,16 @@ "~npm~mdast-util-phrasing@4.1.0": [0,"mdast-util-phrasing","sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], "~npm~mdast-util-to-hast@13.2.1": [0,"mdast-util-to-hast","sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "~npm~mdast-util-to-markdown@2.2.0": [0,"mdast-util-to-markdown","sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA=="], + "~npm~mdast-util-to-markdown@2.2.3": [0,"mdast-util-to-markdown","sha512-dImdB6VZRMRac1SwAW6o1c0ESk/7pFFAMCMuKM+9v7LpTSthMM5nzXpqKw4Mjo66aXVKhNkyrP4bMB9MurPyWg=="], "~npm~mdast-util-to-string@4.0.0": [0,"mdast-util-to-string","sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], "~npm~micromark-core-commonmark@2.0.4": [0,"micromark-core-commonmark","sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w=="], + "~npm~micromark-extension-gfm-autolink-literal@2.1.0": [0,"micromark-extension-gfm-autolink-literal","sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + "~npm~micromark-extension-gfm-footnote@2.1.0": [0,"micromark-extension-gfm-footnote","sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + "~npm~micromark-extension-gfm-strikethrough@2.1.0": [0,"micromark-extension-gfm-strikethrough","sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + "~npm~micromark-extension-gfm-table@2.1.2": [0,"micromark-extension-gfm-table","sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA=="], + "~npm~micromark-extension-gfm-tagfilter@2.0.0": [0,"micromark-extension-gfm-tagfilter","sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + "~npm~micromark-extension-gfm-task-list-item@2.1.0": [0,"micromark-extension-gfm-task-list-item","sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "~npm~micromark-extension-gfm@3.0.0": [0,"micromark-extension-gfm","sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], "~npm~micromark-extension-mdx-expression@3.0.1": [0,"micromark-extension-mdx-expression","sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="], "~npm~micromark-extension-mdx-jsx@3.0.2": [0,"micromark-extension-mdx-jsx","sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="], "~npm~micromark-extension-mdx-md@2.0.0": [0,"micromark-extension-mdx-md","sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="], @@ -371,11 +410,13 @@ "~npm~nanoid@3.3.20": [0,"nanoid","sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",null,null,null,null,null,{ "nanoid": "bin/nanoid.cjs"}], "~npm~next-themes@0.4.6~peer.2d54d32842304f52": [0,"next-themes","sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "~npm~next@16.4.2~peer.28fb75edf4399da2": [0,"next","sha512-r+oBJojfefUjOiWkBhXgLS+igxX/7BWyK59uHnyOyplMSLtBZ+QLP3okrdYTbrfrbrmNTVWLBPaHDX9KRaIt3w==",null,null,null,null,null,{ "next": "dist/bin/next"}], + "~npm~nth-check@2.1.1": [0,"nth-check","sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "~npm~nuqs@2.10.2~peer.0186f63ddb0938a2": [0,"nuqs","sha512-HaLMCr9H2wywEnTu3j+QW98EteDT3Kfmx3SJJ6JfcvCvkFi/g/fKd/oCXa5DF0x1s1l4zcFtzsq9bflj6RcM6A=="], "~npm~obug@3.0.0": [2,"obug","sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw=="], "~npm~oxfmt@0.71.0~peer.f59123c54f315cd4": [2,"oxfmt","sha512-lUPUl0d/+Io5pDrsPXWs6rB4N/bpB78oj9CTDpnbulfDz+0r3XXcHPlQ7kRPJ2GjIT4nX+/mcqunOeP9BvsEtg==",null,null,null,null,null,{ "oxfmt": "bin/oxfmt"}], "~npm~oxlint@1.89.0~peer.98ac72c705727544": [2,"oxlint","sha512-a25hkfo61okGPvJz432fRIPoxlKtNnOVy9w4rzrmGgNm4iVTPwpSQlmdqNWnp6p9QyyPVfAZRgnf5XvSBZFk/A==",null,null,null,null,null,{ "oxlint": "bin/oxlint"}], "~npm~parse-entities@4.0.2": [0,"parse-entities","sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "~npm~parse5@8.1.1": [0,"parse5","sha512-EcR/6WDcU7b2N3vQSxKALCk9lXGtUMQxXio008OEMBE0kavu/7JLfBEeFPzi43nMqVDSbEn3QCK+BMCc9zO//w=="], "~npm~picocolors@1.1.1": [0,"picocolors","sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "~npm~picomatch@4.0.7": [2,"picomatch","sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "~npm~postcss@8.5.23": [0,"postcss","sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], @@ -392,6 +433,7 @@ "~npm~recma-jsx@1.0.1~peer.5127c47082a308d9": [0,"recma-jsx","sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], "~npm~recma-parse@1.0.0": [0,"recma-parse","sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="], "~npm~recma-stringify@1.0.0": [0,"recma-stringify","sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="], + "~npm~rehype-minify-whitespace@6.0.2": [0,"rehype-minify-whitespace","sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw=="], "~npm~rehype-recma@1.0.0": [0,"rehype-recma","sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="], "~npm~remark-mdx@3.1.1": [0,"remark-mdx","sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="], "~npm~remark-parse@11.0.0": [0,"remark-parse","sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], @@ -417,6 +459,7 @@ "~npm~tinyglobby@0.2.17": [2,"tinyglobby","sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "~npm~tinypool@2.2.0": [2,"tinypool","sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="], "~npm~trim-lines@3.0.1": [0,"trim-lines","sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "~npm~trim-trailing-lines@2.1.0": [0,"trim-trailing-lines","sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg=="], "~npm~trough@2.2.0": [0,"trough","sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], "~npm~tslib@2.3.0": [0,"tslib","sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="], "~npm~tslib@2.8.1": [0,"tslib","sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -429,16 +472,20 @@ "~npm~unist-util-position@5.0.0": [0,"unist-util-position","sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], "~npm~unist-util-stringify-position@4.0.0": [0,"unist-util-stringify-position","sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "~npm~unist-util-visit-parents@6.0.2": [0,"unist-util-visit-parents","sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "~npm~unist-util-visit-parents@6.1.0": [0,"unist-util-visit-parents","sha512-vKXxz6O38sKDj1mtx/k2cWvkkFBuYGx6uGrlzBkVeWCRF/F8sdI0j3Wbkz/5rVNRRuVc8UrgRdbCxNhmYXgP3w=="], "~npm~unist-util-visit@5.1.0": [0,"unist-util-visit","sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], + "~npm~unist-util-visit@5.2.0": [0,"unist-util-visit","sha512-fpsYs/VVoZxfGY9/j3FQpZmN8wdLJaV1Pt38AaJNZu9uKIHcFU3halCdBveEq1HGKO1OEfJY65w1zvoFV1b3qQ=="], "~npm~use-callback-ref@1.3.3~peer.7d73202c725218bb": [0,"use-callback-ref","sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "~npm~use-sidecar@1.1.3~peer.e4026cae4d898bbe": [0,"use-sidecar","sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "~npm~use-sync-external-store@1.7.0~peer.1aa36aed4a0ac823": [0,"use-sync-external-store","sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="], + "~npm~vfile-location@5.0.4": [0,"vfile-location","sha512-vEvxatYZCIpNvzGk2UD+u4I/sZkSKoOsdRCbkOIsN73QpJbriJ1qvvBAL1PeI0y55Leoqf7pORbaSPpG/smmOg=="], "~npm~vfile-message@4.0.3": [0,"vfile-message","sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], "~npm~vfile@6.0.3": [0,"vfile","sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "~npm~vite@8.3.5~peer.1ed3409153f570cc": [2,"vite","sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",null,null,null,null,null,{ "vite": "bin/vite.js"}], "~npm~vite@8.3.5~peer.fcbe0df777768dd0": [2,"vite","sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",null,null,null,null,null,{ "vite": "bin/vite.js"}], "~npm~vitest@5.0.5~peer.596fcaf6db23523c": [2,"vitest","sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",null,null,null,null,null,{ "vitest": "vitest.mjs"}], "~npm~vitest@5.0.5~peer.8eaa8e30e11b4451": [2,"vitest","sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==",null,null,null,null,null,{ "vitest": "vitest.mjs"}], + "~npm~web-namespaces@2.0.1": [0,"web-namespaces","sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "~npm~why-is-node-running@3.2.1": [2,"why-is-node-running","sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",null,null,null,null,null,{ "why-is-node-running": "cli.js"}], "~npm~zod@4.6.7": [0,"zod","sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], "~npm~zrender@6.1.0": [0,"zrender","sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ=="], @@ -464,6 +511,7 @@ "workspace~apps+www tw-animate-css": "dev ^1.4.0 ~npm~tw-animate-css@1.4.0", "workspace~apps+www typescript": "dev catalog:hygiene ~npm~typescript@5.9.3", "workspace~apps+www vitest": "dev catalog:testing ~npm~vitest@5.0.5~peer.596fcaf6db23523c", + "workspace~apps+www @affordances/md": "prod 0.5.0 ~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5", "workspace~apps+www @base-ui/react": "prod 1.9.0 ~npm~@base-ui+react@1.9.0~peer.489ab62e90f7fd2c", "workspace~apps+www @mdx-js/loader": "prod ^3.1.1 ~npm~@mdx-js+loader@3.1.1~peer.376edf941af51519", "workspace~apps+www @mdx-js/react": "prod ^3.1.1 ~npm~@mdx-js+react@3.1.1~peer.bcf16a95f0dfe866", @@ -495,6 +543,19 @@ "workspace~packages+types oxlint": "dev catalog:hygiene ~npm~oxlint@1.89.0~peer.98ac72c705727544", "workspace~packages+types typescript": "dev catalog:hygiene ~npm~typescript@5.9.3", "workspace~packages+types vitest": "dev catalog:testing ~npm~vitest@5.0.5~peer.596fcaf6db23523c", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 next": "peerOptional >=16 ~npm~next@16.4.2~peer.28fb75edf4399da2", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 react": "peerOptional >=19 ~npm~react@19.3.1", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 hast-util-from-html": "prod ^2.0.3 ~npm~hast-util-from-html@2.1.0", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 hast-util-select": "prod ^6.0.4 ~npm~hast-util-select@6.0.4", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 hast-util-to-mdast": "prod ^10.1.2 ~npm~hast-util-to-mdast@10.1.2", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 hast-util-to-string": "prod ^3.0.1 ~npm~hast-util-to-string@3.0.1", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 mdast-util-gfm": "prod ^3.1.0 ~npm~mdast-util-gfm@3.1.0", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 mdast-util-to-markdown": "prod ^2.2.1 ~npm~mdast-util-to-markdown@2.2.3", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 micromark-extension-gfm": "prod ^3.0.0 ~npm~micromark-extension-gfm@3.0.0", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 micromark-util-decode-string": "prod ^2.0.1 ~npm~micromark-util-decode-string@2.0.1", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 micromark-util-normalize-identifier": "prod ^2.0.1 ~npm~micromark-util-normalize-identifier@2.0.1", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 micromark": "prod ^4.0.3 ~npm~micromark@4.0.3", + "~npm~@affordances+md@0.5.0~peer.e7c5ed70126bd1a5 unist-util-visit": "prod ^5.2.0 ~npm~unist-util-visit@5.2.0", "~npm~@base-ui+react@1.9.0~peer.489ab62e90f7fd2c react-dom": "peer ^17 || ^18 || ^19 ~npm~react-dom@19.3.1~peer.ac52619a787f06cf", "~npm~@base-ui+react@1.9.0~peer.489ab62e90f7fd2c react": "peer ^17 || ^18 || ^19 ~npm~react@19.3.1", "~npm~@base-ui+react@1.9.0~peer.489ab62e90f7fd2c @date-fns/tz": "peerOptional ^1.2.0 MISSING", @@ -1196,6 +1257,51 @@ "~npm~framer-motion@12.43.0~peer.e8b05b2ebea5635b motion-dom": "prod ^12.43.0 ~npm~motion-dom@12.43.0", "~npm~framer-motion@12.43.0~peer.e8b05b2ebea5635b motion-utils": "prod ^12.39.0 ~npm~motion-utils@12.39.0", "~npm~framer-motion@12.43.0~peer.e8b05b2ebea5635b tslib": "prod ^2.4.0 ~npm~tslib@2.8.1", + "~npm~hast-util-embedded@3.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-embedded@3.0.0 hast-util-is-element": "prod ^3.0.0 ~npm~hast-util-is-element@3.0.0", + "~npm~hast-util-from-html@2.1.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-from-html@2.1.0 devlop": "prod ^1.1.0 ~npm~devlop@1.1.0", + "~npm~hast-util-from-html@2.1.0 hast-util-from-parse5": "prod ^8.0.0 ~npm~hast-util-from-parse5@8.0.4", + "~npm~hast-util-from-html@2.1.0 parse5": "prod ^8.0.0 ~npm~parse5@8.1.1", + "~npm~hast-util-from-html@2.1.0 vfile-message": "prod ^4.0.0 ~npm~vfile-message@4.0.3", + "~npm~hast-util-from-html@2.1.0 vfile": "prod ^6.0.0 ~npm~vfile@6.0.3", + "~npm~hast-util-from-parse5@8.0.4 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-from-parse5@8.0.4 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~hast-util-from-parse5@8.0.4 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~hast-util-from-parse5@8.0.4 hastscript": "prod ^9.0.0 ~npm~hastscript@9.0.3", + "~npm~hast-util-from-parse5@8.0.4 property-information": "prod ^7.0.0 ~npm~property-information@7.2.0", + "~npm~hast-util-from-parse5@8.0.4 vfile-location": "prod ^5.0.0 ~npm~vfile-location@5.0.4", + "~npm~hast-util-from-parse5@8.0.4 vfile": "prod ^6.0.0 ~npm~vfile@6.0.3", + "~npm~hast-util-from-parse5@8.0.4 web-namespaces": "prod ^2.0.0 ~npm~web-namespaces@2.0.1", + "~npm~hast-util-has-property@3.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-is-body-ok-link@3.0.1 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-is-element@3.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-minify-whitespace@1.0.1 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-minify-whitespace@1.0.1 hast-util-embedded": "prod ^3.0.0 ~npm~hast-util-embedded@3.0.0", + "~npm~hast-util-minify-whitespace@1.0.1 hast-util-is-element": "prod ^3.0.0 ~npm~hast-util-is-element@3.0.0", + "~npm~hast-util-minify-whitespace@1.0.1 hast-util-whitespace": "prod ^3.0.0 ~npm~hast-util-whitespace@3.0.0", + "~npm~hast-util-minify-whitespace@1.0.1 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", + "~npm~hast-util-parse-selector@4.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-phrasing@3.0.1 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-phrasing@3.0.1 hast-util-embedded": "prod ^3.0.0 ~npm~hast-util-embedded@3.0.0", + "~npm~hast-util-phrasing@3.0.1 hast-util-has-property": "prod ^3.0.0 ~npm~hast-util-has-property@3.0.0", + "~npm~hast-util-phrasing@3.0.1 hast-util-is-body-ok-link": "prod ^3.0.0 ~npm~hast-util-is-body-ok-link@3.0.1", + "~npm~hast-util-phrasing@3.0.1 hast-util-is-element": "prod ^3.0.0 ~npm~hast-util-is-element@3.0.0", + "~npm~hast-util-select@6.0.4 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-select@6.0.4 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~hast-util-select@6.0.4 bcp-47-match": "prod ^2.0.0 ~npm~bcp-47-match@2.0.3", + "~npm~hast-util-select@6.0.4 comma-separated-tokens": "prod ^2.0.0 ~npm~comma-separated-tokens@2.0.3", + "~npm~hast-util-select@6.0.4 css-selector-parser": "prod ^3.0.0 ~npm~css-selector-parser@3.3.0", + "~npm~hast-util-select@6.0.4 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~hast-util-select@6.0.4 direction": "prod ^2.0.0 ~npm~direction@2.0.1", + "~npm~hast-util-select@6.0.4 hast-util-has-property": "prod ^3.0.0 ~npm~hast-util-has-property@3.0.0", + "~npm~hast-util-select@6.0.4 hast-util-to-string": "prod ^3.0.0 ~npm~hast-util-to-string@3.0.1", + "~npm~hast-util-select@6.0.4 hast-util-whitespace": "prod ^3.0.0 ~npm~hast-util-whitespace@3.0.0", + "~npm~hast-util-select@6.0.4 nth-check": "prod ^2.0.0 ~npm~nth-check@2.1.1", + "~npm~hast-util-select@6.0.4 property-information": "prod ^7.0.0 ~npm~property-information@7.2.0", + "~npm~hast-util-select@6.0.4 space-separated-tokens": "prod ^2.0.0 ~npm~space-separated-tokens@2.0.2", + "~npm~hast-util-select@6.0.4 unist-util-visit": "prod ^5.0.0 ~npm~unist-util-visit@5.2.0", + "~npm~hast-util-select@6.0.4 zwitch": "prod ^2.0.0 ~npm~zwitch@2.0.4", "~npm~hast-util-to-estree@3.1.3 @types/estree-jsx": "prod ^1.0.0 ~npm~@types+estree-jsx@1.0.5", "~npm~hast-util-to-estree@3.1.3 @types/estree": "prod ^1.0.0 ~npm~@types+estree@1.0.9", "~npm~hast-util-to-estree@3.1.3 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", @@ -1212,6 +1318,17 @@ "~npm~hast-util-to-estree@3.1.3 style-to-js": "prod ^1.0.0 ~npm~style-to-js@1.1.21", "~npm~hast-util-to-estree@3.1.3 unist-util-position": "prod ^5.0.0 ~npm~unist-util-position@5.0.0", "~npm~hast-util-to-estree@3.1.3 zwitch": "prod ^2.0.0 ~npm~zwitch@2.0.4", + "~npm~hast-util-to-html@9.0.6 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-to-html@9.0.6 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~hast-util-to-html@9.0.6 ccount": "prod ^2.0.0 ~npm~ccount@2.0.1", + "~npm~hast-util-to-html@9.0.6 comma-separated-tokens": "prod ^2.0.0 ~npm~comma-separated-tokens@2.0.3", + "~npm~hast-util-to-html@9.0.6 hast-util-whitespace": "prod ^3.0.0 ~npm~hast-util-whitespace@3.0.0", + "~npm~hast-util-to-html@9.0.6 html-void-elements": "prod ^3.0.0 ~npm~html-void-elements@3.0.0", + "~npm~hast-util-to-html@9.0.6 mdast-util-to-hast": "prod ^13.0.0 ~npm~mdast-util-to-hast@13.2.1", + "~npm~hast-util-to-html@9.0.6 property-information": "prod ^7.0.0 ~npm~property-information@7.2.0", + "~npm~hast-util-to-html@9.0.6 space-separated-tokens": "prod ^2.0.0 ~npm~space-separated-tokens@2.0.2", + "~npm~hast-util-to-html@9.0.6 stringify-entities": "prod ^4.0.0 ~npm~stringify-entities@4.0.4", + "~npm~hast-util-to-html@9.0.6 zwitch": "prod ^2.0.4 ~npm~zwitch@2.0.4", "~npm~hast-util-to-jsx-runtime@2.3.6 @types/estree": "prod ^1.0.0 ~npm~@types+estree@1.0.9", "~npm~hast-util-to-jsx-runtime@2.3.6 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", "~npm~hast-util-to-jsx-runtime@2.3.6 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", @@ -1227,7 +1344,28 @@ "~npm~hast-util-to-jsx-runtime@2.3.6 style-to-js": "prod ^1.0.0 ~npm~style-to-js@1.1.21", "~npm~hast-util-to-jsx-runtime@2.3.6 unist-util-position": "prod ^5.0.0 ~npm~unist-util-position@5.0.0", "~npm~hast-util-to-jsx-runtime@2.3.6 vfile-message": "prod ^4.0.0 ~npm~vfile-message@4.0.3", + "~npm~hast-util-to-mdast@10.1.2 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-to-mdast@10.1.2 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~hast-util-to-mdast@10.1.2 @ungap/structured-clone": "prod ^1.0.0 ~npm~@ungap+structured-clone@1.4.0", + "~npm~hast-util-to-mdast@10.1.2 hast-util-phrasing": "prod ^3.0.0 ~npm~hast-util-phrasing@3.0.1", + "~npm~hast-util-to-mdast@10.1.2 hast-util-to-html": "prod ^9.0.0 ~npm~hast-util-to-html@9.0.6", + "~npm~hast-util-to-mdast@10.1.2 hast-util-to-text": "prod ^4.0.0 ~npm~hast-util-to-text@4.0.3", + "~npm~hast-util-to-mdast@10.1.2 hast-util-whitespace": "prod ^3.0.0 ~npm~hast-util-whitespace@3.0.0", + "~npm~hast-util-to-mdast@10.1.2 mdast-util-phrasing": "prod ^4.0.0 ~npm~mdast-util-phrasing@4.1.0", + "~npm~hast-util-to-mdast@10.1.2 mdast-util-to-hast": "prod ^13.0.0 ~npm~mdast-util-to-hast@13.2.1", + "~npm~hast-util-to-mdast@10.1.2 mdast-util-to-string": "prod ^4.0.0 ~npm~mdast-util-to-string@4.0.0", + "~npm~hast-util-to-mdast@10.1.2 rehype-minify-whitespace": "prod ^6.0.0 ~npm~rehype-minify-whitespace@6.0.2", + "~npm~hast-util-to-mdast@10.1.2 trim-trailing-lines": "prod ^2.0.0 ~npm~trim-trailing-lines@2.1.0", + "~npm~hast-util-to-mdast@10.1.2 unist-util-position": "prod ^5.0.0 ~npm~unist-util-position@5.0.0", + "~npm~hast-util-to-mdast@10.1.2 unist-util-visit": "prod ^5.0.0 ~npm~unist-util-visit@5.2.0", + "~npm~hast-util-to-string@3.0.1 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hast-util-to-text@4.0.3 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", "~npm~hast-util-whitespace@3.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hastscript@9.0.3 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~hastscript@9.0.3 comma-separated-tokens": "prod ^2.0.0 ~npm~comma-separated-tokens@2.0.3", + "~npm~hastscript@9.0.3 hast-util-parse-selector": "prod ^4.0.0 ~npm~hast-util-parse-selector@4.0.0", + "~npm~hastscript@9.0.3 property-information": "prod ^7.0.0 ~npm~property-information@7.2.0", + "~npm~hastscript@9.0.3 space-separated-tokens": "prod ^2.0.0 ~npm~space-separated-tokens@2.0.2", "~npm~is-alphanumerical@2.0.1 is-alphabetical": "prod ^2.0.0 ~npm~is-alphabetical@2.0.1", "~npm~is-alphanumerical@2.0.1 is-decimal": "prod ^2.0.0 ~npm~is-decimal@2.0.1", "~npm~lightningcss@1.32.0 lightningcss-android-arm64": "optional 1.32.0 ~npm~lightningcss-android-arm64@1.32.0", @@ -1262,6 +1400,10 @@ "~npm~mcp-handler@2.3.0~peer.8acc8965eabfcd93 next": "peerOptional >=13.0.0 ~npm~next@16.4.2~peer.28fb75edf4399da2", "~npm~mcp-handler@2.3.0~peer.8acc8965eabfcd93 chalk": "prod ^5.3.0 ~npm~chalk@5.6.2", "~npm~mcp-handler@2.3.0~peer.8acc8965eabfcd93 commander": "prod ^11.1.0 ~npm~commander@11.1.0", + "~npm~mdast-util-find-and-replace@3.0.4 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-find-and-replace@3.0.4 escape-string-regexp": "prod ^5.0.0 ~npm~escape-string-regexp@5.0.0", + "~npm~mdast-util-find-and-replace@3.0.4 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", + "~npm~mdast-util-find-and-replace@3.0.4 unist-util-visit-parents": "prod ^6.1.0 ~npm~unist-util-visit-parents@6.1.0", "~npm~mdast-util-from-markdown@2.1.0 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", "~npm~mdast-util-from-markdown@2.1.0 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", "~npm~mdast-util-from-markdown@2.1.0 decode-named-character-reference": "prod ^1.0.0 ~npm~decode-named-character-reference@1.3.0", @@ -1274,6 +1416,37 @@ "~npm~mdast-util-from-markdown@2.1.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", "~npm~mdast-util-from-markdown@2.1.0 micromark": "prod ^4.0.0 ~npm~micromark@4.0.3", "~npm~mdast-util-from-markdown@2.1.0 unist-util-stringify-position": "prod ^4.0.0 ~npm~unist-util-stringify-position@4.0.0", + "~npm~mdast-util-gfm-autolink-literal@2.1.0 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-gfm-autolink-literal@2.1.0 ccount": "prod ^2.0.0 ~npm~ccount@2.0.1", + "~npm~mdast-util-gfm-autolink-literal@2.1.0 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~mdast-util-gfm-autolink-literal@2.1.0 mdast-util-find-and-replace": "prod ^3.0.0 ~npm~mdast-util-find-and-replace@3.0.4", + "~npm~mdast-util-gfm-autolink-literal@2.1.0 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~mdast-util-gfm-footnote@2.1.0 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-gfm-footnote@2.1.0 devlop": "prod ^1.1.0 ~npm~devlop@1.1.0", + "~npm~mdast-util-gfm-footnote@2.1.0 mdast-util-from-markdown": "prod ^2.0.0 ~npm~mdast-util-from-markdown@2.1.0", + "~npm~mdast-util-gfm-footnote@2.1.0 mdast-util-to-markdown": "prod ^2.0.0 ~npm~mdast-util-to-markdown@2.2.3", + "~npm~mdast-util-gfm-footnote@2.1.0 micromark-util-normalize-identifier": "prod ^2.0.0 ~npm~micromark-util-normalize-identifier@2.0.1", + "~npm~mdast-util-gfm-strikethrough@2.0.1 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-gfm-strikethrough@2.0.1 mdast-util-from-markdown": "prod ^2.0.0 ~npm~mdast-util-from-markdown@2.1.0", + "~npm~mdast-util-gfm-strikethrough@2.0.1 mdast-util-to-markdown": "prod ^2.0.0 ~npm~mdast-util-to-markdown@2.2.3", + "~npm~mdast-util-gfm-table@2.0.1 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-gfm-table@2.0.1 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~mdast-util-gfm-table@2.0.1 markdown-table": "prod ^3.0.0 ~npm~markdown-table@3.1.0", + "~npm~mdast-util-gfm-table@2.0.1 mdast-util-from-markdown": "prod ^2.0.0 ~npm~mdast-util-from-markdown@2.1.0", + "~npm~mdast-util-gfm-table@2.0.1 mdast-util-to-markdown": "prod ^2.0.0 ~npm~mdast-util-to-markdown@2.2.3", + "~npm~mdast-util-gfm-table@2.0.1 mdast-util-to-string": "prod ^4.0.0 ~npm~mdast-util-to-string@4.0.0", + "~npm~mdast-util-gfm-table@2.0.1 unist-util-visit": "prod ^5.0.0 ~npm~unist-util-visit@5.2.0", + "~npm~mdast-util-gfm-task-list-item@2.0.1 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-gfm-task-list-item@2.0.1 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~mdast-util-gfm-task-list-item@2.0.1 mdast-util-from-markdown": "prod ^2.0.0 ~npm~mdast-util-from-markdown@2.1.0", + "~npm~mdast-util-gfm-task-list-item@2.0.1 mdast-util-to-markdown": "prod ^2.0.0 ~npm~mdast-util-to-markdown@2.2.3", + "~npm~mdast-util-gfm@3.1.0 mdast-util-from-markdown": "prod ^2.0.0 ~npm~mdast-util-from-markdown@2.1.0", + "~npm~mdast-util-gfm@3.1.0 mdast-util-gfm-autolink-literal": "prod ^2.0.0 ~npm~mdast-util-gfm-autolink-literal@2.1.0", + "~npm~mdast-util-gfm@3.1.0 mdast-util-gfm-footnote": "prod ^2.0.0 ~npm~mdast-util-gfm-footnote@2.1.0", + "~npm~mdast-util-gfm@3.1.0 mdast-util-gfm-strikethrough": "prod ^2.0.0 ~npm~mdast-util-gfm-strikethrough@2.0.1", + "~npm~mdast-util-gfm@3.1.0 mdast-util-gfm-table": "prod ^2.0.0 ~npm~mdast-util-gfm-table@2.0.1", + "~npm~mdast-util-gfm@3.1.0 mdast-util-gfm-task-list-item": "prod ^2.0.0 ~npm~mdast-util-gfm-task-list-item@2.0.1", + "~npm~mdast-util-gfm@3.1.0 mdast-util-to-markdown": "prod ^2.0.0 ~npm~mdast-util-to-markdown@2.2.3", "~npm~mdast-util-mdx-expression@2.0.1 @types/estree-jsx": "prod ^1.0.0 ~npm~@types+estree-jsx@1.0.5", "~npm~mdast-util-mdx-expression@2.0.1 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", "~npm~mdast-util-mdx-expression@2.0.1 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", @@ -1325,6 +1498,17 @@ "~npm~mdast-util-to-markdown@2.2.0 micromark-util-html-tag-name": "prod ^2.0.0 ~npm~micromark-util-html-tag-name@2.0.1", "~npm~mdast-util-to-markdown@2.2.0 unist-util-visit": "prod ^5.0.0 ~npm~unist-util-visit@5.1.0", "~npm~mdast-util-to-markdown@2.2.0 zwitch": "prod ^2.0.0 ~npm~zwitch@2.0.4", + "~npm~mdast-util-to-markdown@2.2.3 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", + "~npm~mdast-util-to-markdown@2.2.3 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~mdast-util-to-markdown@2.2.3 longest-streak": "prod ^3.0.0 ~npm~longest-streak@3.1.0", + "~npm~mdast-util-to-markdown@2.2.3 mdast-util-phrasing": "prod ^4.0.0 ~npm~mdast-util-phrasing@4.1.0", + "~npm~mdast-util-to-markdown@2.2.3 mdast-util-to-string": "prod ^4.0.0 ~npm~mdast-util-to-string@4.0.0", + "~npm~mdast-util-to-markdown@2.2.3 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~mdast-util-to-markdown@2.2.3 micromark-util-classify-character": "prod ^2.0.0 ~npm~micromark-util-classify-character@2.0.1", + "~npm~mdast-util-to-markdown@2.2.3 micromark-util-decode-string": "prod ^2.0.0 ~npm~micromark-util-decode-string@2.0.1", + "~npm~mdast-util-to-markdown@2.2.3 micromark-util-html-tag-name": "prod ^2.0.0 ~npm~micromark-util-html-tag-name@2.0.1", + "~npm~mdast-util-to-markdown@2.2.3 unist-util-visit": "prod ^5.0.0 ~npm~unist-util-visit@5.2.0", + "~npm~mdast-util-to-markdown@2.2.3 zwitch": "prod ^2.0.0 ~npm~zwitch@2.0.4", "~npm~mdast-util-to-string@4.0.0 @types/mdast": "prod ^4.0.0 ~npm~@types+mdast@4.0.4", "~npm~micromark-core-commonmark@2.0.4 decode-named-character-reference": "prod ^1.0.0 ~npm~decode-named-character-reference@1.3.0", "~npm~micromark-core-commonmark@2.0.4 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", @@ -1343,6 +1527,43 @@ "~npm~micromark-core-commonmark@2.0.4 micromark-util-subtokenize": "prod ^2.0.0 ~npm~micromark-util-subtokenize@2.1.0", "~npm~micromark-core-commonmark@2.0.4 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", "~npm~micromark-core-commonmark@2.0.4 micromark-util-types": "prod ^2.0.3 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-autolink-literal@2.1.0 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~micromark-extension-gfm-autolink-literal@2.1.0 micromark-util-sanitize-uri": "prod ^2.0.0 ~npm~micromark-util-sanitize-uri@2.0.1", + "~npm~micromark-extension-gfm-autolink-literal@2.1.0 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", + "~npm~micromark-extension-gfm-autolink-literal@2.1.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-footnote@2.1.0 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-core-commonmark": "prod ^2.0.0 ~npm~micromark-core-commonmark@2.0.4", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-factory-space": "prod ^2.0.0 ~npm~micromark-factory-space@2.1.0", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-util-normalize-identifier": "prod ^2.0.0 ~npm~micromark-util-normalize-identifier@2.0.1", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-util-sanitize-uri": "prod ^2.0.0 ~npm~micromark-util-sanitize-uri@2.0.1", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", + "~npm~micromark-extension-gfm-footnote@2.1.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 micromark-util-chunked": "prod ^2.0.0 ~npm~micromark-util-chunked@2.0.1", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 micromark-util-classify-character": "prod ^2.0.0 ~npm~micromark-util-classify-character@2.0.1", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 micromark-util-resolve-all": "prod ^2.0.0 ~npm~micromark-util-resolve-all@2.0.1", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", + "~npm~micromark-extension-gfm-strikethrough@2.1.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-table@2.1.2 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~micromark-extension-gfm-table@2.1.2 micromark-factory-space": "prod ^2.0.0 ~npm~micromark-factory-space@2.1.0", + "~npm~micromark-extension-gfm-table@2.1.2 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~micromark-extension-gfm-table@2.1.2 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", + "~npm~micromark-extension-gfm-table@2.1.2 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-tagfilter@2.0.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm-task-list-item@2.1.0 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", + "~npm~micromark-extension-gfm-task-list-item@2.1.0 micromark-factory-space": "prod ^2.0.0 ~npm~micromark-factory-space@2.1.0", + "~npm~micromark-extension-gfm-task-list-item@2.1.0 micromark-util-character": "prod ^2.0.0 ~npm~micromark-util-character@2.1.1", + "~npm~micromark-extension-gfm-task-list-item@2.1.0 micromark-util-symbol": "prod ^2.0.0 ~npm~micromark-util-symbol@2.0.1", + "~npm~micromark-extension-gfm-task-list-item@2.1.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-autolink-literal": "prod ^2.0.0 ~npm~micromark-extension-gfm-autolink-literal@2.1.0", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-footnote": "prod ^2.0.0 ~npm~micromark-extension-gfm-footnote@2.1.0", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-strikethrough": "prod ^2.0.0 ~npm~micromark-extension-gfm-strikethrough@2.1.0", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-table": "prod ^2.0.0 ~npm~micromark-extension-gfm-table@2.1.2", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-tagfilter": "prod ^2.0.0 ~npm~micromark-extension-gfm-tagfilter@2.0.0", + "~npm~micromark-extension-gfm@3.0.0 micromark-extension-gfm-task-list-item": "prod ^2.0.0 ~npm~micromark-extension-gfm-task-list-item@2.1.0", + "~npm~micromark-extension-gfm@3.0.0 micromark-util-combine-extensions": "prod ^2.0.0 ~npm~micromark-util-combine-extensions@2.0.1", + "~npm~micromark-extension-gfm@3.0.0 micromark-util-types": "prod ^2.0.0 ~npm~micromark-util-types@2.0.3", "~npm~micromark-extension-mdx-expression@3.0.1 @types/estree": "prod ^1.0.0 ~npm~@types+estree@1.0.9", "~npm~micromark-extension-mdx-expression@3.0.1 devlop": "prod ^1.0.0 ~npm~devlop@1.1.0", "~npm~micromark-extension-mdx-expression@3.0.1 micromark-factory-mdx-expression": "prod ^2.0.0 ~npm~micromark-factory-mdx-expression@2.0.3", @@ -1482,6 +1703,7 @@ "~npm~next@16.4.2~peer.28fb75edf4399da2 caniuse-lite": "prod ^1.0.30001579 ~npm~caniuse-lite@1.0.30001818", "~npm~next@16.4.2~peer.28fb75edf4399da2 postcss": "prod 8.5.23 ~npm~postcss@8.5.23", "~npm~next@16.4.2~peer.28fb75edf4399da2 styled-jsx": "prod 5.1.6 ~npm~styled-jsx@5.1.6~peer.a2bb08ab544944b3", + "~npm~nth-check@2.1.1 boolbase": "prod ^1.0.0 ~npm~boolbase@1.0.0", "~npm~nuqs@2.10.2~peer.0186f63ddb0938a2 react": "peer >=18.2.0 || ^19.0.0-0 ~npm~react@19.3.1", "~npm~nuqs@2.10.2~peer.0186f63ddb0938a2 @remix-run/react": "peerOptional >=2 MISSING", "~npm~nuqs@2.10.2~peer.0186f63ddb0938a2 @tanstack/react-router": "peerOptional ^1 MISSING", @@ -1539,6 +1761,7 @@ "~npm~parse-entities@4.0.2 is-alphanumerical": "prod ^2.0.0 ~npm~is-alphanumerical@2.0.1", "~npm~parse-entities@4.0.2 is-decimal": "prod ^2.0.0 ~npm~is-decimal@2.0.1", "~npm~parse-entities@4.0.2 is-hexadecimal": "prod ^2.0.0 ~npm~is-hexadecimal@2.0.1", + "~npm~parse5@8.1.1 entities": "prod ^9.0.0 ~npm~entities@9.0.0", "~npm~postcss@8.5.23 nanoid": "prod ^3.3.16 ~npm~nanoid@3.3.20", "~npm~postcss@8.5.23 picocolors": "prod ^1.1.1 ~npm~picocolors@1.1.1", "~npm~postcss@8.5.23 source-map-js": "prod ^1.2.1 ~npm~source-map-js@1.2.2", @@ -1642,6 +1865,8 @@ "~npm~recma-stringify@1.0.0 estree-util-to-js": "prod ^2.0.0 ~npm~estree-util-to-js@2.0.0", "~npm~recma-stringify@1.0.0 unified": "prod ^11.0.0 ~npm~unified@11.0.5", "~npm~recma-stringify@1.0.0 vfile": "prod ^6.0.0 ~npm~vfile@6.0.3", + "~npm~rehype-minify-whitespace@6.0.2 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", + "~npm~rehype-minify-whitespace@6.0.2 hast-util-minify-whitespace": "prod ^1.0.0 ~npm~hast-util-minify-whitespace@1.0.1", "~npm~rehype-recma@1.0.0 @types/estree": "prod ^1.0.0 ~npm~@types+estree@1.0.9", "~npm~rehype-recma@1.0.0 @types/hast": "prod ^3.0.0 ~npm~@types+hast@3.0.5", "~npm~rehype-recma@1.0.0 hast-util-to-estree": "prod ^3.0.0 ~npm~hast-util-to-estree@3.1.3", @@ -1722,9 +1947,14 @@ "~npm~unist-util-stringify-position@4.0.0 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", "~npm~unist-util-visit-parents@6.0.2 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", "~npm~unist-util-visit-parents@6.0.2 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", + "~npm~unist-util-visit-parents@6.1.0 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~unist-util-visit-parents@6.1.0 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", "~npm~unist-util-visit@5.1.0 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", "~npm~unist-util-visit@5.1.0 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", "~npm~unist-util-visit@5.1.0 unist-util-visit-parents": "prod ^6.0.0 ~npm~unist-util-visit-parents@6.0.2", + "~npm~unist-util-visit@5.2.0 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~unist-util-visit@5.2.0 unist-util-is": "prod ^6.0.0 ~npm~unist-util-is@6.0.1", + "~npm~unist-util-visit@5.2.0 unist-util-visit-parents": "prod ^6.1.0 ~npm~unist-util-visit-parents@6.1.0", "~npm~use-callback-ref@1.3.3~peer.7d73202c725218bb react": "peer ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc ~npm~react@19.3.1", "~npm~use-callback-ref@1.3.3~peer.7d73202c725218bb @types/react": "peerOptional * ~npm~@types+react@19.3.0", "~npm~use-callback-ref@1.3.3~peer.7d73202c725218bb tslib": "prod ^2.0.0 ~npm~tslib@2.3.0", @@ -1733,6 +1963,8 @@ "~npm~use-sidecar@1.1.3~peer.e4026cae4d898bbe detect-node-es": "prod ^1.1.0 ~npm~detect-node-es@1.1.0", "~npm~use-sidecar@1.1.3~peer.e4026cae4d898bbe tslib": "prod ^2.0.0 ~npm~tslib@2.3.0", "~npm~use-sync-external-store@1.7.0~peer.1aa36aed4a0ac823 react": "peer ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 ~npm~react@19.3.1", + "~npm~vfile-location@5.0.4 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", + "~npm~vfile-location@5.0.4 vfile": "prod ^6.0.0 ~npm~vfile@6.0.3", "~npm~vfile-message@4.0.3 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3", "~npm~vfile-message@4.0.3 unist-util-stringify-position": "prod ^4.0.0 ~npm~unist-util-stringify-position@4.0.0", "~npm~vfile@6.0.3 @types/unist": "prod ^3.0.0 ~npm~@types+unist@3.0.3",