feat(web): add /docs/guide and /docs/vocabulary routes

The guide topic flips hasPage true and gains a real page instead of linking
out to the repository; vocabulary is a new core-concepts topic. Both are
registered in docs-map.ts and app/sitemap.ts, so the docs index, navigation,
and sitemap stay in agreement.

/docs/guide renders the shared four-step path and the pending real-session
media slot. /docs/vocabulary renders the execution nouns, modes and permission
postures, route-identity provenance fields, the Consultant advisory role, and
the measurement principles — including the explicit statement that this site
publishes no benchmark leaderboard.

docs/fleet now reads its vocabulary from the shared module rather than an
inline copy, so the definitions cannot drift between pages.

Tests: docs-ia.test.ts covers information architecture; docs-navigation.test.ts
is updated for guide's new local-page routing.

Harvested from the #3413 web-maturity lane.
This commit is contained in:
Hmbown
2026-07-27 02:20:33 -07:00
parent 610a575a2d
commit 35bca1fd71
9 changed files with 457 additions and 18 deletions

View File

@@ -1,3 +1,4 @@
import { PRODUCT_TERMS } from "@/lib/content/vocabulary";
import { buildPageMetadata } from "@/lib/page-meta";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
@@ -19,19 +20,10 @@ export default async function FleetPage({ params }: { params: Promise<{ locale:
const bodyClass = isZh
? "text-ink-soft leading-[1.9] tracking-wide"
: "text-ink-soft leading-relaxed";
const vocabulary = isZh
? [
{ term: "Fleet", definition: "谁来做工作:配置好的 worker、角色、模型、主机和信任边界。" },
{ term: "Workflow", definition: "工作按什么顺序进行:阶段、门禁、预算、回放和汇总。" },
{ term: "Lane", definition: "一个正在运行的 Workflow 实例及其实时进度。" },
{ term: "Runtime", definition: "Lane 在哪里、如何执行:本地或远程进程、提供商路由、沙箱和 API 边界。" },
]
: [
{ term: "Fleet", definition: "Who does the work: the configured workers, roles, models, hosts, and trust boundaries." },
{ term: "Workflow", definition: "What order the work follows: phases, gates, budgets, replay, and fan-in." },
{ term: "Lane", definition: "One running Workflow instance and its live progress." },
{ term: "Runtime", definition: "Where and how a Lane executes: local or remote process, provider route, sandbox, and API boundary." },
];
const vocabulary = PRODUCT_TERMS.map((row) => ({
term: row.term,
definition: isZh ? row.long.zh : row.long.en,
}));
return (
<section className="space-y-10">

View File

@@ -0,0 +1,83 @@
import Link from "next/link";
import { GettingStartedSteps } from "@/components/getting-started-steps";
import { SessionMedia } from "@/components/session-media";
import { GUIDE_NEXT_LINKS } from "@/lib/content/getting-started";
import { getMediaAsset } from "@/lib/media-manifest";
import { buildPageMetadata } from "@/lib/page-meta";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
return buildPageMetadata({
path: "/docs/guide",
locale,
title: isZh ? "新手指引 · Codewhale 文档" : "Getting started · Codewhale Docs",
description: isZh
? "从安装到第一个 Fleet Workflow 的完整路径:安装、无需密钥的首次会话、连接提供商、运行 Fleet。"
: "The full path from install to a first Fleet workflow: install, a first keyless session, provider connection, and Fleet.",
});
}
export default async function GuidePage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const bodyClass = isZh
? "text-ink-soft leading-[1.9] tracking-wide"
: "text-ink-soft leading-relaxed";
const session = getMediaAsset("first-fleet-session");
return (
<section className="space-y-10">
<section id="overview" className="scroll-mt-32">
<h2 className="font-display text-3xl mb-1">{isZh ? "新手指引" : "Getting started"}</h2>
<p className={`${bodyClass} mt-3`}>
{isZh
? "从一条安装命令到第一个 Fleet Workflow四步走完。每一步都只陈述当前版本真实的行为未发布或待录制的内容会明确标注。"
: "Four steps from one install command to a first Fleet workflow. Every step states only what the current candidate actually does; anything unreleased or unrecorded is labeled as such."}
</p>
</section>
<section id="path" className="scroll-mt-32">
<GettingStartedSteps locale={locale} />
</section>
{session && (
<section id="session-media" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh ? "看一次真实会话" : "Watch a real session"}
</h2>
<p className={`${bodyClass} mt-3 mb-4`}>
{isZh
? "下方是真实会话媒体位。它当前处于待录制状态——这是有意为之:在 v0.9.2 候选版 dogfood 录制完成前,本站不展示任何占位或摆拍影像。"
: "Below is the real-session media slot. It is deliberately in the pending state: until the v0.9.2 candidate dogfood recording exists, this site shows no placeholder or staged footage."}
</p>
<SessionMedia asset={session} locale={locale} />
</section>
)}
<section id="next" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">{isZh ? "接下来" : "Where next"}</h2>
<div className="hairline-t mt-4">
{GUIDE_NEXT_LINKS.map((item) => (
<div key={item.href} className="py-4 hairline-b">
<h3 className="font-display text-xl">
<Link href={`/${locale}${item.href}`} className="hover:text-indigo transition-colors">
{isZh ? item.label.zh : item.label.en}
</Link>
</h3>
<p className={`${bodyClass} mt-1 text-sm`}>{isZh ? item.note.zh : item.note.en}</p>
</div>
))}
</div>
</section>
<section id="source" className="hairline-t pt-8">
<p className="text-sm text-ink-mute">
{isZh
? "来源文档docs/GUIDE.md, docs/KEYBINDINGS.md · 步骤文案来自 web/lib/content/getting-started.ts更新时请同步修改 docs-map.ts。"
: "Source documents: docs/GUIDE.md, docs/KEYBINDINGS.md · Step copy lives in web/lib/content/getting-started.ts; update docs-map.ts when changing."}
</p>
</section>
</section>
);
}

View File

@@ -28,8 +28,8 @@ export default async function DocsLayout({
<h1>{isZh ? "查找准确的使用说明。" : "Find the guidance you need."}</h1>
<p>
{isZh
? "从安装和首次运行开始或者直接查找模式、权限、工具、提供商、Fleet、MCP 与运行时 API。这些页面就是正式的产品文档每页都链接仓库中的源文档方便查阅完整细节。"
: "Start with installation and first run, or go straight to modes, permissions, tools, providers, Fleet, MCP, and the Runtime API. These pages are the product documentation; each one cites the source documents in the repository for full detail."}
? "从新手指引和安装开始,或者直接查找产品名词、模式、权限、工具、提供商、Fleet、钩子、MCP 与运行时 API。这些页面就是正式的产品文档每页都链接仓库中的源文档方便查阅完整细节。"
: "Start with the getting-started guide and installation, or go straight to vocabulary, modes, permissions, tools, providers, Fleet, hooks, MCP, and the Runtime API. These pages are the product documentation; each one cites the source documents in the repository for full detail."}
</p>
<div className="portal-actions">
<Link href={`/${locale}/install`} className="portal-button portal-button-primary">

View File

@@ -0,0 +1,141 @@
import { StatusBadge } from "@/components/status-badge";
import {
ADVISORY_ROLE,
CONTROL_MODES,
MEASUREMENT_PRINCIPLES,
PERMISSION_POSTURES,
PRODUCT_TERMS,
ROUTE_IDENTITY,
} from "@/lib/content/vocabulary";
import { buildPageMetadata } from "@/lib/page-meta";
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
return buildPageMetadata({
path: "/docs/vocabulary",
locale,
title: isZh ? "产品名词 · Codewhale 文档" : "Vocabulary · Codewhale Docs",
description: isZh
? "确切的产品名词Fleet、Workflow、Lane、Runtime、ConsultantPlan / Act / Operate 与权限姿态,以及请求→实际思考档位、路由来源与测量原则。"
: "The exact product nouns: Fleet, Workflow, Lane, Runtime, Consultant, Plan / Act / Operate and permission postures, plus requested→effective reasoning, routing source, and measurement principles.",
});
}
export default async function VocabularyPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params;
const isZh = locale === "zh";
const bodyClass = isZh
? "text-ink-soft leading-[1.9] tracking-wide"
: "text-ink-soft leading-relaxed";
return (
<section className="space-y-10">
<section id="overview" className="scroll-mt-32">
<h2 className="font-display text-3xl mb-1">{isZh ? "产品名词" : "Vocabulary"}</h2>
<p className={`${bodyClass} mt-3`}>
{isZh
? "这些名词在全站、TUI 和收据里含义完全一致。名词本身不翻译;每条定义都与仓库中的公共事实矩阵逐字对应。"
: "These nouns mean the same thing on this site, in the TUI, and in receipts. The nouns themselves are never translated; every definition matches the public fact matrix in the repository verbatim."}
</p>
</section>
<section id="execution-nouns" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh ? "执行名词" : "Execution nouns"}
</h2>
<div className="hairline-t mt-4">
{PRODUCT_TERMS.map((row) => (
<section key={row.term} className="py-4 hairline-b">
<h3 className="font-display text-xl">
{row.term}{" "}
<span className="text-sm text-ink-mute font-normal">
= {isZh ? row.short.zh : row.short.en}
</span>
</h3>
<p className={`${bodyClass} mt-1 text-sm`}>{isZh ? row.long.zh : row.long.en}</p>
</section>
))}
</div>
</section>
<section id="control-nouns" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh ? "模式与权限姿态" : "Modes and permission postures"}
</h2>
<p className={`${bodyClass} mt-3`}>
{isZh
? "模式Tab 循环输入框空闲时决定可见的交互方式权限姿态Shift+Tab 循环)决定工具执行前询问的激进程度。两者正交。"
: "Modes (Tab, composer idle) decide the visible interaction; permission postures (Shift+Tab) decide how aggressively tools ask before executing. The two are orthogonal."}
</p>
<div className="hairline-t mt-4">
{[...CONTROL_MODES, ...PERMISSION_POSTURES].map((row) => (
<section key={row.term} className="py-4 hairline-b">
<h3 className="font-display text-xl">{row.term}</h3>
<p className={`${bodyClass} mt-1 text-sm`}>
{isZh ? row.description.zh : row.description.en}
</p>
</section>
))}
</div>
</section>
<section id="route-identity" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh
? "路由身份provider · 模型 · 请求→实际思考档位 · 来源"
: "Route identity: provider · model · requested→effective reasoning · source"}
</h2>
<div className="hairline-t mt-4">
{ROUTE_IDENTITY.map((row) => (
<section key={row.term} className="py-4 hairline-b">
<h3 className="font-display text-xl">{row.term}</h3>
<p className={`${bodyClass} mt-1 text-sm`}>
{isZh ? row.description.zh : row.description.en}
</p>
</section>
))}
</div>
</section>
<section id="advisory-role" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh ? "咨询角色" : "Advisory role"}
</h2>
<div className="hairline-t mt-4 py-4 hairline-b">
<h3 className="font-display text-xl">{ADVISORY_ROLE.term}</h3>
<p className={`${bodyClass} mt-1 text-sm`}>
{isZh ? ADVISORY_ROLE.description.zh : ADVISORY_ROLE.description.en}
</p>
</div>
</section>
<section id="measurement" className="scroll-mt-32">
<h2 className="font-display text-2xl mb-1">
{isZh ? "测量原则" : "Measurement principles"}
</h2>
<ul className="mt-4 space-y-3">
{MEASUREMENT_PRINCIPLES.map((principle) => (
<li key={principle.en} className={`${bodyClass} text-sm`}>
{isZh ? principle.zh : principle.en}
</li>
))}
</ul>
<p className={`${bodyClass} mt-4 text-sm`}>
<StatusBadge kind="unavailable" locale={locale} />{" "}
{isZh
? "基准排行榜:本站没有,也不会在没有路由身份与测量工具链的情况下出现。"
: "Benchmark leaderboard: none on this site, and none will appear without route identity and a measurement harness attached."}
</p>
</section>
<section id="source" className="hairline-t pt-8">
<p className="text-sm text-ink-mute">
{isZh
? "来源文档docs/FLEET.md, docs/MODES.md, docs/public-surface-facts.json · 名词文案来自 web/lib/content/vocabulary.ts更新时请同步修改 docs-map.ts。"
: "Source documents: docs/FLEET.md, docs/MODES.md, docs/public-surface-facts.json · Vocabulary copy lives in web/lib/content/vocabulary.ts; update docs-map.ts when changing."}
</p>
</section>
</section>
);
}

View File

@@ -4,7 +4,7 @@ import { SITE_URL } from "@/lib/page-meta";
// Public, indexable routes (locale-prefixed). /admin and /api are
// intentionally excluded; see app/robots.ts.
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/fleet", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community"];
const PATHS = ["", "/install", "/constitution", "/models", "/runtime", "/docs", "/docs/configuration", "/docs/constitution", "/docs/fleet", "/docs/guide", "/docs/hooks", "/docs/mcp", "/docs/modes", "/docs/runtime-api", "/docs/sandbox", "/docs/subagents", "/docs/tools", "/docs/troubleshooting", "/docs/vocabulary", "/docs/web", "/docs/work", "/faq", "/roadmap", "/feed", "/digest", "/contribute", "/community"];
export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();

View File

@@ -0,0 +1,35 @@
/**
* <GettingStartedSteps> — renders the shared new-user path from
* web/lib/content/getting-started.ts: install → first offline session →
* provider connection → first Fleet workflow.
*
* Used by the homepage band and the /docs/guide page so the path reads
* identically in both places. Server component, SSG-safe.
*/
import Link from "next/link";
import { GETTING_STARTED_STEPS } from "@/lib/content/getting-started";
export function GettingStartedSteps({ locale = "en" }: { locale?: string }) {
const isZh = locale === "zh";
return (
<ol className="gs-steps">
{GETTING_STARTED_STEPS.map((step, index) => (
<li key={step.id} data-step-id={step.id}>
<span className="gs-step-index" aria-hidden="true">
{String(index + 1).padStart(2, "0")}
</span>
<h3>{isZh ? step.title.zh : step.title.en}</h3>
<p>{isZh ? step.body.zh : step.body.en}</p>
{step.commands.length > 0 && (
<pre className="code-block gs-step-commands"><code>{step.commands.join("\n")}</code></pre>
)}
<Link href={`/${locale}${step.link.href}`} className="gs-step-link">
{isZh ? step.link.label.zh : step.link.label.en}
</Link>
</li>
))}
</ol>
);
}

169
web/lib/docs-ia.test.ts Normal file
View File

@@ -0,0 +1,169 @@
/**
* Information-architecture contracts: docs-map registration, sitemap and
* hreflang preservation, navigation parity across breakpoints and locales,
* and the accessibility hooks (skip link, labelled nav, aria-current).
*
* These are deterministic source/unit contracts in the same style as
* public-copy.test.ts: they read the real sources and assert structure, so a
* future IA change fails here first instead of drifting silently.
*/
import { existsSync, readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { DOC_TOPICS, docTopicHref, getTopic } from "./docs-map";
import { docsTopicIsCurrent } from "./docs-navigation";
import { locales } from "./i18n/config";
const webRoot = new URL("../", import.meta.url);
const repoRoot = new URL("../../", import.meta.url);
function webText(path: string): string {
return readFileSync(new URL(path, webRoot), "utf8");
}
const sitemap = webText("app/sitemap.ts");
const nav = webText("components/nav.tsx");
const navLinks = webText("components/nav-links.tsx");
const mobileMenu = webText("components/mobile-menu.tsx");
const footer = webText("components/footer.tsx");
const localeLayout = webText("app/[locale]/layout.tsx");
const css = webText("app/globals.css");
describe("docs-map registration", () => {
it("registers the guide and vocabulary topics as first-party pages", () => {
const guide = getTopic("guide");
const vocabulary = getTopic("vocabulary");
expect(guide?.hasPage).toBe(true);
expect(vocabulary?.hasPage).toBe(true);
expect(vocabulary?.category).toBe("core-concepts");
expect(docTopicHref(guide!, "en")).toBe("/en/docs/guide");
expect(docTopicHref(vocabulary!, "zh")).toBe("/zh/docs/vocabulary");
expect(docsTopicIsCurrent(vocabulary!, "en", "/en/docs/vocabulary")).toBe(true);
});
it("keeps every docs topic repo source on disk", () => {
for (const topic of DOC_TOPICS) {
const sources = Array.isArray(topic.repoSource) ? topic.repoSource : [topic.repoSource];
for (const source of sources) {
expect(existsSync(new URL(source, repoRoot)), `${topic.id}: ${source}`).toBe(true);
}
}
});
it("keeps topic labels and descriptions bilingual", () => {
for (const topic of DOC_TOPICS) {
for (const pair of [topic.label, topic.description]) {
expect(pair.en.trim().length, `${topic.id} en`).toBeGreaterThan(0);
expect(pair.zh.trim().length, `${topic.id} zh`).toBeGreaterThan(0);
}
}
});
});
describe("sitemap and hreflang preservation", () => {
it("indexes every first-party docs page", () => {
for (const topic of DOC_TOPICS) {
if (!topic.hasPage) continue;
const path = topic.sitePath ? `/${topic.sitePath}` : `/docs/${topic.slug}`;
expect(sitemap, path).toContain(`"${path}"`);
}
expect(sitemap).toContain('"/docs/guide"');
expect(sitemap).toContain('"/docs/vocabulary"');
});
it("keeps per-locale alternate pairs for every indexed route", () => {
expect(sitemap).toContain("alternates");
expect(sitemap).toContain("en: `${SITE_URL}/en${path}`");
expect(sitemap).toContain("zh: `${SITE_URL}/zh${path}`");
// Route generation is driven by the shipped locale set, never hardcoded.
expect(sitemap).toContain("locales.map");
expect(locales).toEqual(["en", "zh"]);
});
it("keeps the new docs pages on the shared metadata helper", () => {
for (const route of ["guide", "vocabulary"]) {
const page = webText(`app/[locale]/docs/${route}/page.tsx`);
expect(page, route).toContain('import { buildPageMetadata } from "@/lib/page-meta"');
expect(page, route).toContain(`path: "/docs/${route}"`);
}
});
});
describe("navigation parity and accessibility", () => {
it("keeps desktop and mobile navigation on one shared link set", () => {
// Both surfaces consume the same `links` prop from nav.tsx — assert the
// wiring rather than duplicating the arrays.
expect(nav).toContain("<NavLinks links={links} isZh={isZh} />");
expect(nav).toContain("links={links}");
expect(mobileMenu).toContain("links.map");
expect(navLinks).toContain("links.map");
});
it("keeps en/zh nav link paths in exact locale-swap parity", () => {
const enHrefs = [...nav.matchAll(/href: "\/en\/([^"]+)"/g)].map((m) => m[1]);
const zhHrefs = [...nav.matchAll(/href: "\/zh\/([^"]+)"/g)].map((m) => m[1]);
expect(enHrefs.length).toBeGreaterThanOrEqual(4);
expect(enHrefs).toEqual(zhHrefs);
expect(enHrefs).toContain("docs/guide");
expect(enHrefs).toContain("faq");
});
it("labels the primary nav and marks the current page accessibly", () => {
expect(navLinks).toContain('aria-label={isZh ? "主导航" : "Primary"}');
expect(navLinks).toContain('aria-current={isActive ? "page" : undefined}');
expect(mobileMenu).toContain('aria-current={isActive ? "page" : undefined}');
expect(mobileMenu).toContain('aria-expanded={open}');
expect(mobileMenu).toContain('aria-controls="mobile-menu"');
expect(mobileMenu).toContain('role="dialog"');
});
it("ships a keyboard-reachable skip link to the main landmark", () => {
expect(localeLayout).toContain('href="#main-content"');
expect(localeLayout).toContain('className="skip-link"');
expect(localeLayout).toContain('<main id="main-content">');
expect(css).toContain(".skip-link:focus-visible");
});
it("keeps responsive breakpoints for the getting-started steps", () => {
// 4-up grid by default, 2-up at the tablet breakpoint, 1-up on phones —
// the same responsive ladder as the existing workflow steps.
expect(css).toMatch(/\.gs-steps\s*\{[^}]*repeat\(4, minmax\(0, 1fr\)\)/);
expect(css).toMatch(
/@media \(max-width: 760px\)[\s\S]*?\.gs-steps\s*\{[^}]*repeat\(2, minmax\(0, 1fr\)\)/,
);
expect(css).toMatch(
/@media \(max-width: 520px\)[\s\S]*?\.gs-steps\s*\{\s*grid-template-columns: 1fr/,
);
});
it("keeps the footer discovery links alongside the pinned legal links", () => {
expect(footer).toContain('href: "/en/docs/guide"');
expect(footer).toContain('href: "/zh/docs/guide"');
expect(footer).toContain('href: "/en/faq"');
expect(footer).toContain(
'{ label: "MIT license", href: "https://github.com/Hmbown/CodeWhale/blob/main/LICENSE" }',
);
});
});
describe("homepage integration", () => {
const homepage = webText("app/[locale]/page.tsx");
it("renders the shared getting-started path on the homepage", () => {
expect(homepage).toContain('import { GettingStartedSteps } from "@/components/getting-started-steps"');
expect(homepage).toContain("<GettingStartedSteps locale={locale} />");
expect(homepage).toContain("product-start");
expect(homepage).toContain("/docs/guide");
expect(homepage).toContain("/docs/vocabulary");
});
it("keeps the previously pinned homepage facts intact", () => {
// Guard against the new band accidentally displacing the public-copy
// gate's required surface (the full contract lives in public-copy.test.ts).
expect(homepage).toContain("facts.latestPublishedRelease");
expect(homepage).toContain("Source candidate");
expect(homepage).toContain('src="/codewhale-tui.png"');
for (const label of ["Plan", "Act", "Operate", "Ask", "Auto-Review", "Full Access"]) {
expect(homepage).toContain(label);
}
});
});

View File

@@ -53,9 +53,21 @@ export const DOC_TOPICS: DocTopic[] = [
zh: "首次运行、会话、命令、快捷键和日常使用流程。",
},
repoSource: ["docs/GUIDE.md", "docs/KEYBINDINGS.md"],
hasPage: false,
hasPage: true,
category: "getting-started",
},
{
id: "vocabulary",
slug: "vocabulary",
label: { en: "Vocabulary", zh: "产品名词" },
description: {
en: "The exact product nouns — Fleet, Workflow, Lane, Runtime; Plan / Act / Operate; Consultant; and explicit route provenance — plus measurement principles.",
zh: "确切的产品名词——Fleet、Workflow、Lane、RuntimePlan / Act / OperateConsultant明确的路由来源——以及测量原则。",
},
repoSource: ["docs/FLEET.md", "docs/MODES.md", "docs/public-surface-facts.json"],
hasPage: true,
category: "core-concepts",
},
{
id: "configuration",
slug: "configuration",

View File

@@ -28,8 +28,15 @@ describe("docsTopicIsCurrent", () => {
expect(docTopicIsExternal(topic("providers"))).toBe(false);
});
it("routes the guide to its dedicated docs page in either locale", () => {
expect(docTopicHref(topic("guide"), "en")).toBe("/en/docs/guide");
expect(docsTopicIsCurrent(topic("guide"), "en", "/en/docs/guide")).toBe(true);
expect(docsTopicIsCurrent(topic("guide"), "zh", "/zh/docs/guide/")).toBe(true);
expect(docTopicIsExternal(topic("guide"))).toBe(false);
});
it("never marks source-document links as local pages", () => {
expect(docsTopicIsCurrent(topic("guide"), "en", "/en/docs/guide")).toBe(false);
expect(docsTopicIsCurrent(topic("contribution"), "en", "/en/docs/contribution")).toBe(false);
});
it("routes former link-out topics to their dedicated docs pages", () => {