From 52aefa1c6aaaf13de160337852e32ce83557d8e4 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:06:11 -0700 Subject: [PATCH 1/3] fix(control-panel): require Metric kind and direction on comparison props ExperimentResultsPanel still declared metrics as Pick after #611 made the comparison read kind and direction. The narrower type was structurally assignable to the Partial target, so the colouring only worked because the route happens to pass full Metric objects. Make the comparison type demand both fields so a caller that narrows the list fails typecheck instead of rendering every row as neutral. Issue: SPL-663 Co-Authored-By: Claude Fable 5.1 --- .../experiments/experiment-results-comparison.test.tsx | 4 ++-- .../components/experiments/experiment-results-panel.tsx | 6 +++--- .../experiments/experiment-results-test-fixtures.ts | 7 ++++--- .../src/lib/experiments/metric-comparison-rows.ts | 3 +-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/control-panel/src/components/experiments/experiment-results-comparison.test.tsx b/apps/control-panel/src/components/experiments/experiment-results-comparison.test.tsx index 8009acbd..d14b7bac 100644 --- a/apps/control-panel/src/components/experiments/experiment-results-comparison.test.tsx +++ b/apps/control-panel/src/components/experiments/experiment-results-comparison.test.tsx @@ -227,13 +227,13 @@ describe("ExperimentResultsComparison ordering and states", () => { expect(text).toContain("Confidence interval unavailable"); }); - it("does not format estimates or differences without the Metric type", () => { + it("does not format estimates or differences when the Metric is missing from the catalog", () => { const text = visibleText( renderToStaticMarkup( , diff --git a/apps/control-panel/src/components/experiments/experiment-results-panel.tsx b/apps/control-panel/src/components/experiments/experiment-results-panel.tsx index c78ceb10..4cb28591 100644 --- a/apps/control-panel/src/components/experiments/experiment-results-panel.tsx +++ b/apps/control-panel/src/components/experiments/experiment-results-panel.tsx @@ -7,7 +7,6 @@ const ExperimentConclusionDialog = lazy(() => })), ); -import type { Metric } from "@splitch/contracts"; import type { PanelExperimentRun } from "@splitch/control-plane-sdk/panel-experiments"; import { useSuspenseQuery } from "@tanstack/react-query"; import { @@ -16,6 +15,7 @@ import { ExperimentResultsWaiting, } from "#components/experiments/experiment-results"; import { experimentResultsQuery } from "#lib/experiments/experiments-query"; +import type { ComparisonMetric } from "#lib/experiments/metric-comparison-rows"; /** * Route-facing wrapper: resolves the Run to read, then renders it. @@ -43,7 +43,7 @@ export function ExperimentResultsPanel({ flagId: string; environmentId: string; experimentId: string; - metrics: readonly Pick[]; + metrics: readonly ComparisonMetric[]; run: PanelExperimentRun | undefined; }) { if (!run) return ; @@ -77,7 +77,7 @@ function ExperimentResultsForRun({ flagId: string; environmentId: string; experimentId: string; - metrics: readonly Pick[]; + metrics: readonly ComparisonMetric[]; run: PanelExperimentRun; }) { const [concluding, setConcluding] = useState(false); diff --git a/apps/control-panel/src/components/experiments/experiment-results-test-fixtures.ts b/apps/control-panel/src/components/experiments/experiment-results-test-fixtures.ts index a503b851..4aa8ea57 100644 --- a/apps/control-panel/src/components/experiments/experiment-results-test-fixtures.ts +++ b/apps/control-panel/src/components/experiments/experiment-results-test-fixtures.ts @@ -9,6 +9,7 @@ import type { PanelExperimentResultsReady, PanelExperimentRun, } from "@splitch/control-plane-sdk/panel-experiments"; +import type { ComparisonMetric } from "#lib/experiments/metric-comparison-rows"; /** * Panel fixtures built the way the Worker builds the payload: the gate is @@ -251,10 +252,10 @@ export function resultsNoDataFixture( } /** Catalog rows for the Metric ids the stats fixtures use, as the detail read returns them. */ -export function metricsFixture(): { id: string; name: string }[] { +export function metricsFixture(): ComparisonMetric[] { return [ - { id: "checkout_conversion", name: "Checkout conversion" }, - { id: "checkout_latency_p95", name: "Checkout latency p95" }, + { id: "checkout_conversion", name: "Checkout conversion", kind: "binomial", direction: null }, + { id: "checkout_latency_p95", name: "Checkout latency p95", kind: "count", direction: null }, ]; } diff --git a/apps/control-panel/src/lib/experiments/metric-comparison-rows.ts b/apps/control-panel/src/lib/experiments/metric-comparison-rows.ts index 3d233321..8b8722b0 100644 --- a/apps/control-panel/src/lib/experiments/metric-comparison-rows.ts +++ b/apps/control-panel/src/lib/experiments/metric-comparison-rows.ts @@ -20,8 +20,7 @@ import { type MetricNames, metricDisplayName, metricNamesById } from "./metric-n * arrive computed. */ -export type ComparisonMetric = Pick & - Partial>; +export type ComparisonMetric = Pick; export type MetricComparisonRole = "decision" | "guardrail" | "exploratory"; From 7c0caed07455a26553b3af5685e48b2a242979b3 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:15:28 -0700 Subject: [PATCH 2/3] refactor(marketing): drop the markdown file routes the Worker already serves Since #609 the Worker answers every `.md` URL before TanStack Start runs, so the five `[.]md.ts` file routes were unreachable. The dynamic twins (SDK topics, error codes) never had file routes at all. One handler now serves all of them. The docs link linter used those file routes as its proof that a markdown twin exists. It now applies one rule to every markdown URL: strip `.md` and require the HTML page to be a route, which is how it already accepted the dynamic ones. The marketing test that guards the twin invariant walks the canonical page list from the sitemap, so a new page without markdown fails there. Issue: SPL-663 Co-Authored-By: Claude Fable 5.1 --- .../marketing/src/docs/markdown-route.test.ts | 20 ++-- apps/marketing/src/docs/markdown-route.ts | 2 - apps/marketing/src/routeTree.gen.ts | 105 ------------------ apps/marketing/src/routes/docs.cli[.]md.ts | 11 -- .../src/routes/docs.code-agents[.]md.ts | 11 -- apps/marketing/src/routes/docs.errors[.]md.ts | 11 -- apps/marketing/src/routes/docs.flags[.]md.ts | 11 -- apps/marketing/src/routes/quickstart[.]md.ts | 11 -- scripts/docs-link-lint.mjs | 8 +- scripts/docs-link-lint.test.mjs | 5 +- 10 files changed, 15 insertions(+), 180 deletions(-) delete mode 100644 apps/marketing/src/routes/docs.cli[.]md.ts delete mode 100644 apps/marketing/src/routes/docs.code-agents[.]md.ts delete mode 100644 apps/marketing/src/routes/docs.errors[.]md.ts delete mode 100644 apps/marketing/src/routes/docs.flags[.]md.ts delete mode 100644 apps/marketing/src/routes/quickstart[.]md.ts diff --git a/apps/marketing/src/docs/markdown-route.test.ts b/apps/marketing/src/docs/markdown-route.test.ts index 72b7ec6d..31d7be80 100644 --- a/apps/marketing/src/docs/markdown-route.test.ts +++ b/apps/marketing/src/docs/markdown-route.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { quickstartMarkdown } from "./markdown"; -import { markdownForPath, staticMarkdownPaths } from "./markdown-route"; -import { staticPagePaths } from "./sitemap"; +import { markdownForPath } from "./markdown-route"; +import { canonicalPageUrls, staticPagePaths } from "./sitemap"; function markdownUrlForPage(path: string): string { if (path === "/") return "/.md"; @@ -32,16 +32,14 @@ describe("markdownForPath", () => { expect(markdown).toMatch(/^# Zero to a resolving Flag/m); }); - it("serves the .md suffix for every static HTML page, including top-level ones", () => { - const topLevelHtmlPages = staticPagePaths.filter((path) => !path.startsWith("/docs")); - expect(topLevelHtmlPages).toEqual(["/", "/quickstart"]); - expect(staticMarkdownPaths).toContain("/quickstart"); - expect(staticMarkdownPaths.filter((path) => !path.startsWith("/docs"))).toEqual([ - "/", - "/quickstart", - ]); + it("serves the .md suffix for every canonical HTML page, including top-level ones", () => { + // The Worker answers `.md` before the router runs, so this is the + // only guard that a new page ships with its markdown twin. + const pages = canonicalPageUrls.map((href) => new URL(href).pathname); + expect(pages.filter((path) => !path.startsWith("/docs"))).toEqual(["/", "/quickstart"]); + expect(pages.length).toBeGreaterThan(staticPagePaths.length); - for (const path of staticMarkdownPaths) { + for (const path of pages) { const markdown = markdownForPath(path); expect(markdown, path).not.toBeNull(); expect(markdownForPath(markdownUrlForPage(path)), markdownUrlForPage(path)).toBe(markdown); diff --git a/apps/marketing/src/docs/markdown-route.ts b/apps/marketing/src/docs/markdown-route.ts index e5d70c0d..9e42920c 100644 --- a/apps/marketing/src/docs/markdown-route.ts +++ b/apps/marketing/src/docs/markdown-route.ts @@ -23,8 +23,6 @@ const staticMarkdown = new Map string>([ ["/docs/errors", errorIndexMarkdown], ]); -export const staticMarkdownPaths = [...staticMarkdown.keys()] as const; - export function markdownForPath(pathname: string): string | null { const documentPath = htmlPathForMarkdownUrl(pathname); const staticDocument = staticMarkdown.get(documentPath); diff --git a/apps/marketing/src/routeTree.gen.ts b/apps/marketing/src/routeTree.gen.ts index 82cc80b2..1a323689 100644 --- a/apps/marketing/src/routeTree.gen.ts +++ b/apps/marketing/src/routeTree.gen.ts @@ -13,19 +13,14 @@ import { Route as IndexRouteImport } from './routes/index' import { Route as HealthRouteImport } from './routes/health' import { Route as LlmsDottxtRouteImport } from './routes/llms[.]txt' import { Route as QuickstartRouteImport } from './routes/quickstart' -import { Route as QuickstartDotmdRouteImport } from './routes/quickstart[.]md' import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt' import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml' import { Route as DotwellKnownApiCatalogRouteImport } from './routes/[.]well-known.api-catalog' import { Route as DocsIndexRouteImport } from './routes/docs.index' import { Route as DocsCliRouteImport } from './routes/docs.cli' -import { Route as DocsCliDotmdRouteImport } from './routes/docs.cli[.]md' import { Route as DocsCodeAgentsRouteImport } from './routes/docs.code-agents' -import { Route as DocsCodeAgentsDotmdRouteImport } from './routes/docs.code-agents[.]md' import { Route as DocsErrorsRouteImport } from './routes/docs.errors' -import { Route as DocsErrorsDotmdRouteImport } from './routes/docs.errors[.]md' import { Route as DocsFlagsRouteImport } from './routes/docs.flags' -import { Route as DocsFlagsDotmdRouteImport } from './routes/docs.flags[.]md' import { Route as DocsErrorCodeRouteImport } from './routes/docs.error.$code' import { Route as DocsSdkTopicRouteImport } from './routes/docs.sdk.$topic' @@ -49,11 +44,6 @@ const QuickstartRoute = QuickstartRouteImport.update({ path: '/quickstart', getParentRoute: () => rootRouteImport, } as any) -const QuickstartDotmdRoute = QuickstartDotmdRouteImport.update({ - id: '/quickstart.md', - path: '/quickstart.md', - getParentRoute: () => rootRouteImport, -} as any) const RobotsDottxtRoute = RobotsDottxtRouteImport.update({ id: '/robots.txt', path: '/robots.txt', @@ -79,41 +69,21 @@ const DocsCliRoute = DocsCliRouteImport.update({ path: '/docs/cli', getParentRoute: () => rootRouteImport, } as any) -const DocsCliDotmdRoute = DocsCliDotmdRouteImport.update({ - id: '/docs/cli.md', - path: '/docs/cli.md', - getParentRoute: () => rootRouteImport, -} as any) const DocsCodeAgentsRoute = DocsCodeAgentsRouteImport.update({ id: '/docs/code-agents', path: '/docs/code-agents', getParentRoute: () => rootRouteImport, } as any) -const DocsCodeAgentsDotmdRoute = DocsCodeAgentsDotmdRouteImport.update({ - id: '/docs/code-agents.md', - path: '/docs/code-agents.md', - getParentRoute: () => rootRouteImport, -} as any) const DocsErrorsRoute = DocsErrorsRouteImport.update({ id: '/docs/errors', path: '/docs/errors', getParentRoute: () => rootRouteImport, } as any) -const DocsErrorsDotmdRoute = DocsErrorsDotmdRouteImport.update({ - id: '/docs/errors.md', - path: '/docs/errors.md', - getParentRoute: () => rootRouteImport, -} as any) const DocsFlagsRoute = DocsFlagsRouteImport.update({ id: '/docs/flags', path: '/docs/flags', getParentRoute: () => rootRouteImport, } as any) -const DocsFlagsDotmdRoute = DocsFlagsDotmdRouteImport.update({ - id: '/docs/flags.md', - path: '/docs/flags.md', - getParentRoute: () => rootRouteImport, -} as any) const DocsErrorCodeRoute = DocsErrorCodeRouteImport.update({ id: '/docs/error/$code', path: '/docs/error/$code', @@ -130,18 +100,13 @@ export interface FileRoutesByFullPath { '/health': typeof HealthRoute '/llms.txt': typeof LlmsDottxtRoute '/quickstart': typeof QuickstartRoute - '/quickstart.md': typeof QuickstartDotmdRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/.well-known/api-catalog': typeof DotwellKnownApiCatalogRoute '/docs/cli': typeof DocsCliRoute - '/docs/cli.md': typeof DocsCliDotmdRoute '/docs/code-agents': typeof DocsCodeAgentsRoute - '/docs/code-agents.md': typeof DocsCodeAgentsDotmdRoute '/docs/errors': typeof DocsErrorsRoute - '/docs/errors.md': typeof DocsErrorsDotmdRoute '/docs/flags': typeof DocsFlagsRoute - '/docs/flags.md': typeof DocsFlagsDotmdRoute '/docs/': typeof DocsIndexRoute '/docs/error/$code': typeof DocsErrorCodeRoute '/docs/sdk/$topic': typeof DocsSdkTopicRoute @@ -151,18 +116,13 @@ export interface FileRoutesByTo { '/health': typeof HealthRoute '/llms.txt': typeof LlmsDottxtRoute '/quickstart': typeof QuickstartRoute - '/quickstart.md': typeof QuickstartDotmdRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/.well-known/api-catalog': typeof DotwellKnownApiCatalogRoute '/docs/cli': typeof DocsCliRoute - '/docs/cli.md': typeof DocsCliDotmdRoute '/docs/code-agents': typeof DocsCodeAgentsRoute - '/docs/code-agents.md': typeof DocsCodeAgentsDotmdRoute '/docs/errors': typeof DocsErrorsRoute - '/docs/errors.md': typeof DocsErrorsDotmdRoute '/docs/flags': typeof DocsFlagsRoute - '/docs/flags.md': typeof DocsFlagsDotmdRoute '/docs': typeof DocsIndexRoute '/docs/error/$code': typeof DocsErrorCodeRoute '/docs/sdk/$topic': typeof DocsSdkTopicRoute @@ -173,18 +133,13 @@ export interface FileRoutesById { '/health': typeof HealthRoute '/llms.txt': typeof LlmsDottxtRoute '/quickstart': typeof QuickstartRoute - '/quickstart.md': typeof QuickstartDotmdRoute '/robots.txt': typeof RobotsDottxtRoute '/sitemap.xml': typeof SitemapDotxmlRoute '/.well-known/api-catalog': typeof DotwellKnownApiCatalogRoute '/docs/cli': typeof DocsCliRoute - '/docs/cli.md': typeof DocsCliDotmdRoute '/docs/code-agents': typeof DocsCodeAgentsRoute - '/docs/code-agents.md': typeof DocsCodeAgentsDotmdRoute '/docs/errors': typeof DocsErrorsRoute - '/docs/errors.md': typeof DocsErrorsDotmdRoute '/docs/flags': typeof DocsFlagsRoute - '/docs/flags.md': typeof DocsFlagsDotmdRoute '/docs/': typeof DocsIndexRoute '/docs/error/$code': typeof DocsErrorCodeRoute '/docs/sdk/$topic': typeof DocsSdkTopicRoute @@ -196,18 +151,13 @@ export interface FileRouteTypes { | '/health' | '/llms.txt' | '/quickstart' - | '/quickstart.md' | '/robots.txt' | '/sitemap.xml' | '/.well-known/api-catalog' | '/docs/cli' - | '/docs/cli.md' | '/docs/code-agents' - | '/docs/code-agents.md' | '/docs/errors' - | '/docs/errors.md' | '/docs/flags' - | '/docs/flags.md' | '/docs/' | '/docs/error/$code' | '/docs/sdk/$topic' @@ -217,18 +167,13 @@ export interface FileRouteTypes { | '/health' | '/llms.txt' | '/quickstart' - | '/quickstart.md' | '/robots.txt' | '/sitemap.xml' | '/.well-known/api-catalog' | '/docs/cli' - | '/docs/cli.md' | '/docs/code-agents' - | '/docs/code-agents.md' | '/docs/errors' - | '/docs/errors.md' | '/docs/flags' - | '/docs/flags.md' | '/docs' | '/docs/error/$code' | '/docs/sdk/$topic' @@ -238,18 +183,13 @@ export interface FileRouteTypes { | '/health' | '/llms.txt' | '/quickstart' - | '/quickstart.md' | '/robots.txt' | '/sitemap.xml' | '/.well-known/api-catalog' | '/docs/cli' - | '/docs/cli.md' | '/docs/code-agents' - | '/docs/code-agents.md' | '/docs/errors' - | '/docs/errors.md' | '/docs/flags' - | '/docs/flags.md' | '/docs/' | '/docs/error/$code' | '/docs/sdk/$topic' @@ -260,18 +200,13 @@ export interface RootRouteChildren { HealthRoute: typeof HealthRoute LlmsDottxtRoute: typeof LlmsDottxtRoute QuickstartRoute: typeof QuickstartRoute - QuickstartDotmdRoute: typeof QuickstartDotmdRoute RobotsDottxtRoute: typeof RobotsDottxtRoute SitemapDotxmlRoute: typeof SitemapDotxmlRoute DotwellKnownApiCatalogRoute: typeof DotwellKnownApiCatalogRoute DocsCliRoute: typeof DocsCliRoute - DocsCliDotmdRoute: typeof DocsCliDotmdRoute DocsCodeAgentsRoute: typeof DocsCodeAgentsRoute - DocsCodeAgentsDotmdRoute: typeof DocsCodeAgentsDotmdRoute DocsErrorsRoute: typeof DocsErrorsRoute - DocsErrorsDotmdRoute: typeof DocsErrorsDotmdRoute DocsFlagsRoute: typeof DocsFlagsRoute - DocsFlagsDotmdRoute: typeof DocsFlagsDotmdRoute DocsIndexRoute: typeof DocsIndexRoute DocsErrorCodeRoute: typeof DocsErrorCodeRoute DocsSdkTopicRoute: typeof DocsSdkTopicRoute @@ -307,13 +242,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof QuickstartRouteImport parentRoute: typeof rootRouteImport } - '/quickstart.md': { - id: '/quickstart.md' - path: '/quickstart.md' - fullPath: '/quickstart.md' - preLoaderRoute: typeof QuickstartDotmdRouteImport - parentRoute: typeof rootRouteImport - } '/robots.txt': { id: '/robots.txt' path: '/robots.txt' @@ -349,13 +277,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsCliRouteImport parentRoute: typeof rootRouteImport } - '/docs/cli.md': { - id: '/docs/cli.md' - path: '/docs/cli.md' - fullPath: '/docs/cli.md' - preLoaderRoute: typeof DocsCliDotmdRouteImport - parentRoute: typeof rootRouteImport - } '/docs/code-agents': { id: '/docs/code-agents' path: '/docs/code-agents' @@ -363,13 +284,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsCodeAgentsRouteImport parentRoute: typeof rootRouteImport } - '/docs/code-agents.md': { - id: '/docs/code-agents.md' - path: '/docs/code-agents.md' - fullPath: '/docs/code-agents.md' - preLoaderRoute: typeof DocsCodeAgentsDotmdRouteImport - parentRoute: typeof rootRouteImport - } '/docs/errors': { id: '/docs/errors' path: '/docs/errors' @@ -377,13 +291,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsErrorsRouteImport parentRoute: typeof rootRouteImport } - '/docs/errors.md': { - id: '/docs/errors.md' - path: '/docs/errors.md' - fullPath: '/docs/errors.md' - preLoaderRoute: typeof DocsErrorsDotmdRouteImport - parentRoute: typeof rootRouteImport - } '/docs/flags': { id: '/docs/flags' path: '/docs/flags' @@ -391,13 +298,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsFlagsRouteImport parentRoute: typeof rootRouteImport } - '/docs/flags.md': { - id: '/docs/flags.md' - path: '/docs/flags.md' - fullPath: '/docs/flags.md' - preLoaderRoute: typeof DocsFlagsDotmdRouteImport - parentRoute: typeof rootRouteImport - } '/docs/error/$code': { id: '/docs/error/$code' path: '/docs/error/$code' @@ -420,18 +320,13 @@ const rootRouteChildren: RootRouteChildren = { HealthRoute: HealthRoute, LlmsDottxtRoute: LlmsDottxtRoute, QuickstartRoute: QuickstartRoute, - QuickstartDotmdRoute: QuickstartDotmdRoute, RobotsDottxtRoute: RobotsDottxtRoute, SitemapDotxmlRoute: SitemapDotxmlRoute, DotwellKnownApiCatalogRoute: DotwellKnownApiCatalogRoute, DocsCliRoute: DocsCliRoute, - DocsCliDotmdRoute: DocsCliDotmdRoute, DocsCodeAgentsRoute: DocsCodeAgentsRoute, - DocsCodeAgentsDotmdRoute: DocsCodeAgentsDotmdRoute, DocsErrorsRoute: DocsErrorsRoute, - DocsErrorsDotmdRoute: DocsErrorsDotmdRoute, DocsFlagsRoute: DocsFlagsRoute, - DocsFlagsDotmdRoute: DocsFlagsDotmdRoute, DocsIndexRoute: DocsIndexRoute, DocsErrorCodeRoute: DocsErrorCodeRoute, DocsSdkTopicRoute: DocsSdkTopicRoute, diff --git a/apps/marketing/src/routes/docs.cli[.]md.ts b/apps/marketing/src/routes/docs.cli[.]md.ts deleted file mode 100644 index 6853c44f..00000000 --- a/apps/marketing/src/routes/docs.cli[.]md.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { cliDocMarkdown } from "../docs/markdown"; -import { markdownResponse } from "../docs/serve-markdown"; - -export const Route = createFileRoute("/docs/cli.md")({ - server: { - handlers: { - GET: async () => markdownResponse(cliDocMarkdown()), - }, - }, -}); diff --git a/apps/marketing/src/routes/docs.code-agents[.]md.ts b/apps/marketing/src/routes/docs.code-agents[.]md.ts deleted file mode 100644 index b82e432a..00000000 --- a/apps/marketing/src/routes/docs.code-agents[.]md.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { codeAgentsDocMarkdown } from "../docs/markdown"; -import { markdownResponse } from "../docs/serve-markdown"; - -export const Route = createFileRoute("/docs/code-agents.md")({ - server: { - handlers: { - GET: async () => markdownResponse(codeAgentsDocMarkdown()), - }, - }, -}); diff --git a/apps/marketing/src/routes/docs.errors[.]md.ts b/apps/marketing/src/routes/docs.errors[.]md.ts deleted file mode 100644 index 8c1054f1..00000000 --- a/apps/marketing/src/routes/docs.errors[.]md.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { errorIndexMarkdown } from "../docs/markdown"; -import { markdownResponse } from "../docs/serve-markdown"; - -export const Route = createFileRoute("/docs/errors.md")({ - server: { - handlers: { - GET: async () => markdownResponse(errorIndexMarkdown()), - }, - }, -}); diff --git a/apps/marketing/src/routes/docs.flags[.]md.ts b/apps/marketing/src/routes/docs.flags[.]md.ts deleted file mode 100644 index 72880e2f..00000000 --- a/apps/marketing/src/routes/docs.flags[.]md.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { flagsDocMarkdown } from "../docs/markdown"; -import { markdownResponse } from "../docs/serve-markdown"; - -export const Route = createFileRoute("/docs/flags.md")({ - server: { - handlers: { - GET: async () => markdownResponse(flagsDocMarkdown()), - }, - }, -}); diff --git a/apps/marketing/src/routes/quickstart[.]md.ts b/apps/marketing/src/routes/quickstart[.]md.ts deleted file mode 100644 index c8753347..00000000 --- a/apps/marketing/src/routes/quickstart[.]md.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import { quickstartMarkdown } from "../docs/markdown"; -import { markdownResponse } from "../docs/serve-markdown"; - -export const Route = createFileRoute("/quickstart.md")({ - server: { - handlers: { - GET: async () => markdownResponse(quickstartMarkdown()), - }, - }, -}); diff --git a/scripts/docs-link-lint.mjs b/scripts/docs-link-lint.mjs index a4d88408..19f448b6 100644 --- a/scripts/docs-link-lint.mjs +++ b/scripts/docs-link-lint.mjs @@ -135,11 +135,11 @@ export function lintPublishedDocsText(filePath, text, inventory) { function violationForPublishedUrl(url, inventory) { const pathname = normalizePathname(decodeURIComponent(url.pathname)); - const route = routeForPath(inventory, pathname); + // The Worker serves `.md` for every page, so a markdown URL is valid + // exactly when its HTML page is a route. There are no markdown file routes. + const pagePath = pathname.endsWith(".md") ? pathname.slice(0, -3) : pathname; + const route = routeForPath(inventory, pagePath); if (!route) return `${url.href} names no marketing route.`; - if (pathname.endsWith(".md") && !routeForPath(inventory, pathname.slice(0, -3))) { - return `${url.href} has no HTML twin.`; - } const anchor = decodeURIComponent(url.hash.slice(1)); if (!anchor) return undefined; diff --git a/scripts/docs-link-lint.test.mjs b/scripts/docs-link-lint.test.mjs index 4869e52d..26766a64 100644 --- a/scripts/docs-link-lint.test.mjs +++ b/scripts/docs-link-lint.test.mjs @@ -20,7 +20,6 @@ async function fixtureRoutes(t) { 'createFileRoute("/docs/")({});\n
', ), writeFile(path.join(routes, "docs.errors.tsx"), 'createFileRoute("/docs/errors")({});'), - writeFile(path.join(routes, "docs.errors[.]md.ts"), 'createFileRoute("/docs/errors.md")({});'), writeFile( path.join(routes, "docs.error.$code.tsx"), 'createFileRoute("/docs/error/$code")({});', @@ -63,14 +62,14 @@ test("rejects a route-side mutation that removes a published target", async (t) ); }); -test("rejects markdown routes without an HTML twin", async (t) => { +test("rejects a markdown URL whose HTML page is not a route", async (t) => { const routes = await fixtureRoutes(t); await rm(path.join(routes, "docs.errors.tsx")); const inventory = await buildRouteInventory(routes); assert.match( lintPublishedDocsText("README.md", `${origin}/docs/errors.md`, inventory)[0].message, - /has no HTML twin/, + /names no marketing route/, ); }); From c6b34b367711a98a1ca968577ca597ed7a546cc4 Mon Sep 17 00:00:00 2001 From: "useotto-dev[bot]" <252773270+useotto-dev[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:29:16 -0700 Subject: [PATCH 3/3] fix(scripts): check stale fragments against the page path for markdown URLs The route lookup strips `.md` but the anchor collision check still used the raw pathname, so `/docs.md#errors` never matched the `/docs/errors` route and the stale-link verdict silently no-oped for markdown URLs. Issue: SPL-663 Co-Authored-By: Claude Fable 5.1 --- scripts/docs-link-lint.mjs | 4 ++-- scripts/docs-link-lint.test.mjs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/docs-link-lint.mjs b/scripts/docs-link-lint.mjs index 19f448b6..13190510 100644 --- a/scripts/docs-link-lint.mjs +++ b/scripts/docs-link-lint.mjs @@ -143,11 +143,11 @@ function violationForPublishedUrl(url, inventory) { const anchor = decodeURIComponent(url.hash.slice(1)); if (!anchor) return undefined; - const routedAlternative = `${pathname === "/" ? "" : pathname}/${anchor}`; + const routedAlternative = `${pagePath === "/" ? "" : pagePath}/${anchor}`; if (hasStaticRoute(inventory, routedAlternative)) { return `${url.href} is stale; ${origin}${routedAlternative} is a route, not a section link.`; } - if (!route.anchors.has(anchor)) return `${url.href} names no section anchor on ${pathname}.`; + if (!route.anchors.has(anchor)) return `${url.href} names no section anchor on ${pagePath}.`; return undefined; } diff --git a/scripts/docs-link-lint.test.mjs b/scripts/docs-link-lint.test.mjs index 26766a64..89c9f3ea 100644 --- a/scripts/docs-link-lint.test.mjs +++ b/scripts/docs-link-lint.test.mjs @@ -83,4 +83,8 @@ test("rejects missing anchors and stale fragment collisions", async (t) => { /names no section anchor/, ); assert.match(lintPublishedDocsText("README.md", movedCatalog, inventory)[0].message, /is stale/); + assert.match( + lintPublishedDocsText("README.md", `${origin}/docs.md#${"errors"}`, inventory)[0].message, + /is stale/, + ); });