@panaversity/ksor 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +24 -6
  3. package/dist/cli.mjs +344 -2
  4. package/docs/index.md +34 -6
  5. package/package.json +3 -1
  6. package/templates/LICENSE +23 -0
  7. package/templates/scaffold/.agents/skills/add-sources/SKILL.md +43 -0
  8. package/templates/scaffold/.agents/skills/format-checker/SKILL.md +39 -0
  9. package/templates/scaffold/.agents/skills/format-checker/check.mjs +782 -0
  10. package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +46 -0
  11. package/templates/scaffold/.claude/skills/add-sources/SKILL.md +43 -0
  12. package/templates/scaffold/.claude/skills/format-checker/SKILL.md +39 -0
  13. package/templates/scaffold/.claude/skills/format-checker/check.mjs +782 -0
  14. package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +46 -0
  15. package/templates/scaffold/.gemini/settings.json +5 -0
  16. package/templates/scaffold/.gitattributes +5 -0
  17. package/templates/scaffold/.github/workflows/validate.yml +23 -0
  18. package/templates/scaffold/AGENTS.md +104 -0
  19. package/templates/scaffold/CLAUDE.md +1 -0
  20. package/templates/scaffold/README.md +63 -0
  21. package/templates/scaffold/gitignore +13 -0
  22. package/templates/scaffold/instance.md +26 -0
  23. package/templates/scaffold/knowledge/example.md +23 -0
  24. package/templates/scaffold/package.json +15 -0
  25. package/templates/scaffold/pnpm-lock.yaml +4041 -0
  26. package/templates/scaffold/pnpm-workspace.yaml +19 -0
  27. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  28. package/templates/scaffold/system/site/app/(home)/page.tsx +83 -0
  29. package/templates/scaffold/system/site/app/api/search/route.ts +11 -0
  30. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +53 -0
  31. package/templates/scaffold/system/site/app/docs/layout.tsx +24 -0
  32. package/templates/scaffold/system/site/app/global.css +26 -0
  33. package/templates/scaffold/system/site/app/icon.png +0 -0
  34. package/templates/scaffold/system/site/app/layout.tsx +41 -0
  35. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +10 -0
  36. package/templates/scaffold/system/site/app/llms.txt/route.ts +15 -0
  37. package/templates/scaffold/system/site/components/built-with.tsx +18 -0
  38. package/templates/scaffold/system/site/components/mdx.tsx +15 -0
  39. package/templates/scaffold/system/site/lib/layout.shared.tsx +17 -0
  40. package/templates/scaffold/system/site/lib/shared.ts +51 -0
  41. package/templates/scaffold/system/site/lib/source.ts +119 -0
  42. package/templates/scaffold/system/site/next-env.d.ts +6 -0
  43. package/templates/scaffold/system/site/next.config.mjs +32 -0
  44. package/templates/scaffold/system/site/package.json +29 -0
  45. package/templates/scaffold/system/site/postcss.config.mjs +7 -0
  46. package/templates/scaffold/system/site/source.config.ts +35 -0
  47. package/templates/scaffold/system/site/tsconfig.json +35 -0
@@ -0,0 +1,19 @@
1
+ packages:
2
+ - system/site
3
+ # reserved homes for future system components (gateways, MCP surface, packages)
4
+ - system/gateways/*
5
+ - system/packages/*
6
+
7
+ # 48-hour quarantine on newly published dependency versions — a routine
8
+ # `pnpm install` here never picks up a day-zero compromised release.
9
+ minimumReleaseAge: 2880
10
+
11
+ # Dependency install scripts are denied by default. Flip an entry to true
12
+ # only with a comment naming what breaks without it. esbuild and sharp are
13
+ # reviewed and stay denied: both ship prebuilt platform binaries as
14
+ # optionalDependencies, so their install scripts are download fallbacks the
15
+ # site never needs — but pnpm 11 exits 1 on every install/dev until each is
16
+ # explicitly decided. (found live: fresh-scaffold pnpm dev, 2026-08-18)
17
+ allowBuilds:
18
+ esbuild: false
19
+ sharp: false
@@ -0,0 +1,6 @@
1
+ import { HomeLayout } from "fumadocs-ui/layouts/home";
2
+ import { baseOptions } from "@/lib/layout.shared";
3
+
4
+ export default function Layout({ children }: LayoutProps<"/">) {
5
+ return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
6
+ }
@@ -0,0 +1,83 @@
1
+ import Image from "next/image";
2
+ import Link from "next/link";
3
+ // The same file Next serves as the favicon (app/icon.png) — one mark, one
4
+ // asset. Replace it with your own and the tab icon changes with the page.
5
+ import mark from "@/app/icon.png";
6
+ import { BuiltWith } from "@/components/built-with";
7
+ import { appName, appTitle } from "@/lib/shared";
8
+ import { basePath, getSortedPages } from "@/lib/source";
9
+
10
+ export default function HomePage() {
11
+ // The first document in sidebar order — never a hardcoded path, so deleting
12
+ // the example the scaffold ships cannot leave a link pointing at nothing.
13
+ const pages = getSortedPages();
14
+ const [first] = pages;
15
+
16
+ return (
17
+ <main className="flex flex-1 flex-col">
18
+ <div className="mx-auto flex w-full max-w-2xl flex-1 flex-col justify-center px-6 py-24">
19
+ <Image
20
+ src={mark}
21
+ alt=""
22
+ width={56}
23
+ height={56}
24
+ priority
25
+ className="mb-7 size-14 rounded-xl ring-1 ring-fd-border"
26
+ />
27
+
28
+ {/* The frame is KSoR's; the title is the record's. The headline is
29
+ instance.md's own H1 — a human name, not the machine slug — so a
30
+ fresh scaffold reads "Knowledge System of Record" until the
31
+ intake interview writes the real one. */}
32
+ <p className="mb-2 text-xs font-medium uppercase tracking-widest text-fd-muted-foreground">
33
+ KSoR
34
+ </p>
35
+ <h1 className="text-4xl font-semibold tracking-tight text-balance break-words sm:text-5xl">
36
+ {appTitle}
37
+ </h1>
38
+ <p className="mt-3 text-lg text-fd-muted-foreground">
39
+ Knowledge you can govern. Answers you can trace. Boundaries agents can respect.
40
+ </p>
41
+
42
+ {first ? (
43
+ <div className="mt-9 flex flex-col gap-4">
44
+ <Link
45
+ href={first.url}
46
+ className="group inline-flex w-fit items-center gap-2 rounded-lg bg-fd-primary px-4 py-2.5 text-sm font-medium text-fd-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
47
+ >
48
+ Open the record
49
+ <span
50
+ aria-hidden
51
+ className="transition-transform group-hover:translate-x-0.5 motion-reduce:transform-none"
52
+ >
53
+ &rarr;
54
+ </span>
55
+ </Link>
56
+ {/* The machine identity and the agent door — the slug is what
57
+ citations will carry, so it stays visible where agents look. */}
58
+ <p className="text-xs text-fd-muted-foreground">
59
+ <span className="font-mono">{appName}</span> &middot; {pages.length} document
60
+ {pages.length === 1 ? "" : "s"} &middot; agents read{" "}
61
+ <a
62
+ href={`${basePath}/llms.txt`}
63
+ className="underline underline-offset-4 transition-colors hover:text-fd-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
64
+ >
65
+ llms.txt
66
+ </a>
67
+ </p>
68
+ </div>
69
+ ) : (
70
+ <p className="mt-9 text-fd-muted-foreground">
71
+ the record is empty — add a document to <code>knowledge/</code>
72
+ </p>
73
+ )}
74
+ </div>
75
+
76
+ <footer className="mx-auto w-full max-w-2xl px-6 pb-10">
77
+ <p className="border-t border-fd-border pt-6 text-xs">
78
+ <BuiltWith />
79
+ </p>
80
+ </footer>
81
+ </main>
82
+ );
83
+ }
@@ -0,0 +1,11 @@
1
+ import { source } from "@/lib/source";
2
+ import { createFromSource } from "fumadocs-core/search/server";
3
+
4
+ // Static export: this route is prerendered into a JSON index file that the
5
+ // search dialog downloads and queries client-side (see app/layout.tsx).
6
+ export const revalidate = false;
7
+
8
+ export const { staticGET: GET } = createFromSource(source, {
9
+ // https://docs.orama.com/docs/orama-js/supported-languages
10
+ language: "english",
11
+ });
@@ -0,0 +1,53 @@
1
+ import { source } from "@/lib/source";
2
+ import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
3
+ import { notFound } from "next/navigation";
4
+ import { getMDXComponents } from "@/components/mdx";
5
+ import type { Metadata } from "next";
6
+ import { createRelativeLink } from "fumadocs-ui/mdx";
7
+
8
+ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
9
+ const params = await props.params;
10
+ const page = source.getPage(params.slug);
11
+ if (!page) notFound();
12
+
13
+ const MDX = page.data.body;
14
+
15
+ return (
16
+ <DocsPage toc={page.data.toc} full={page.data.full}>
17
+ <DocsTitle>{page.data.title}</DocsTitle>
18
+ <DocsDescription>{page.data.description}</DocsDescription>
19
+ <DocsBody>
20
+ <MDX
21
+ components={getMDXComponents({
22
+ // relative links between documents in knowledge/ resolve to
23
+ // their rendered pages
24
+ a: createRelativeLink(source, page),
25
+ })}
26
+ />
27
+ </DocsBody>
28
+ </DocsPage>
29
+ );
30
+ }
31
+
32
+ export async function generateStaticParams() {
33
+ const params = source.generateParams();
34
+ if (params.length === 0) {
35
+ // Without this, Next fails the empty-record build with an error that
36
+ // names neither the record nor the rule (found live, 2026-08-18).
37
+ throw new Error(
38
+ "the record has no documents — a KSoR is never empty; add one to knowledge/ or restore one from git history (pnpm check says the same).",
39
+ );
40
+ }
41
+ return params;
42
+ }
43
+
44
+ export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
45
+ const params = await props.params;
46
+ const page = source.getPage(params.slug);
47
+ if (!page) notFound();
48
+
49
+ return {
50
+ title: page.data.title,
51
+ description: page.data.description,
52
+ };
53
+ }
@@ -0,0 +1,24 @@
1
+ import { getSortedPageTree } from "@/lib/source";
2
+ import { DocsLayout } from "fumadocs-ui/layouts/docs";
3
+ import { baseOptions } from "@/lib/layout.shared";
4
+ import { BuiltWith } from "@/components/built-with";
5
+
6
+ export default function Layout({ children }: LayoutProps<"/docs">) {
7
+ return (
8
+ <DocsLayout
9
+ tree={getSortedPageTree()}
10
+ {...baseOptions()}
11
+ // After the spread: a future sidebar key in baseOptions must not
12
+ // silently swallow the attribution (review finding, 2026-08-18).
13
+ sidebar={{
14
+ footer: (
15
+ <p className="mt-3 text-xs">
16
+ <BuiltWith />
17
+ </p>
18
+ ),
19
+ }}
20
+ >
21
+ {children}
22
+ </DocsLayout>
23
+ );
24
+ }
@@ -0,0 +1,26 @@
1
+ @import "tailwindcss";
2
+ @import "fumadocs-ui/css/neutral.css";
3
+ @import "fumadocs-ui/css/preset.css";
4
+
5
+ /* The KSoR default accent, drawn from the mark — the one brand value here.
6
+ Re-brand by changing this pair; every accented element follows. */
7
+ :root {
8
+ --color-fd-primary: #1d4ed8;
9
+ --color-fd-primary-foreground: #ffffff;
10
+ --color-fd-ring: #1d4ed8;
11
+ }
12
+
13
+ .dark {
14
+ --color-fd-primary: #7fb0f9;
15
+ --color-fd-primary-foreground: #081226;
16
+ --color-fd-ring: #7fb0f9;
17
+ }
18
+
19
+ html {
20
+ scrollbar-gutter: stable;
21
+ }
22
+
23
+ html > body[data-scroll-locked] {
24
+ margin-right: 0px !important;
25
+ --removed-body-scroll-bar-size: 0px !important;
26
+ }
@@ -0,0 +1,41 @@
1
+ import { RootProvider } from "fumadocs-ui/provider/next";
2
+ import "./global.css";
3
+ import type { Metadata } from "next";
4
+ import { appTitle } from "@/lib/shared";
5
+
6
+ // No next/font/google: it fetches the face from Google at BUILD time, so a
7
+ // scaffolded project could not build offline and two builds of one commit
8
+ // could differ byte-wise (review finding, 2026-08-18). The system UI stack
9
+ // costs zero bytes and zero network; replace it with a self-hosted @font-face
10
+ // if the project wants a specific face.
11
+
12
+ export const metadata: Metadata = {
13
+ title: {
14
+ default: appTitle,
15
+ template: `%s | ${appTitle}`,
16
+ },
17
+ description: "The Knowledge System of Record for humans and AI agents.",
18
+ };
19
+
20
+ export default function Layout({ children }: LayoutProps<"/">) {
21
+ return (
22
+ <html lang="en" suppressHydrationWarning>
23
+ <body className="flex flex-col min-h-screen">
24
+ <RootProvider
25
+ search={{
26
+ // Static search: the browser downloads the index that
27
+ // app/api/search exports at build time (staticGET) and runs
28
+ // Orama client-side — no server needed, so search keeps
29
+ // working on any static host.
30
+ options: {
31
+ type: "static",
32
+ api: `${process.env.KSOR_BASE_PATH ?? ""}/api/search`,
33
+ },
34
+ }}
35
+ >
36
+ {children}
37
+ </RootProvider>
38
+ </body>
39
+ </html>
40
+ );
41
+ }
@@ -0,0 +1,10 @@
1
+ import { getLLMText, getSortedPages } from "@/lib/source";
2
+
3
+ export const revalidate = false;
4
+
5
+ export async function GET(): Promise<Response> {
6
+ const scan = getSortedPages().map(getLLMText);
7
+ const scanned = await Promise.all(scan);
8
+
9
+ return new Response(scanned.join("\n\n"));
10
+ }
@@ -0,0 +1,15 @@
1
+ import { basePath, getSortedPages } from "@/lib/source";
2
+ import { appName } from "@/lib/shared";
3
+
4
+ export const revalidate = false;
5
+
6
+ // The agent-facing index of the record: this instance's name, then every
7
+ // document in sidebar order, each link usable as-is on a sub-path host.
8
+ export function GET(): Response {
9
+ const lines = getSortedPages().map((page) => {
10
+ const link = `- [${page.data.title}](${basePath}${page.url})`;
11
+ return page.data.description ? `${link}: ${page.data.description}` : link;
12
+ });
13
+
14
+ return new Response(`# ${appName}\n\n${lines.join("\n")}\n`);
15
+ }
@@ -0,0 +1,18 @@
1
+ import type { ReactElement } from "react";
2
+
3
+ /**
4
+ * The maker's mark. Attribution, not a dependency: the record and this site
5
+ * are yours (MIT-0) — delete this component and both keep working.
6
+ */
7
+ export function BuiltWith(): ReactElement {
8
+ return (
9
+ <a
10
+ href="https://github.com/panaversity/ksor"
11
+ target="_blank"
12
+ rel="noreferrer"
13
+ className="text-fd-muted-foreground underline-offset-4 transition-colors hover:text-fd-foreground hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
14
+ >
15
+ Built with KSoR
16
+ </a>
17
+ );
18
+ }
@@ -0,0 +1,15 @@
1
+ import defaultMdxComponents from "fumadocs-ui/mdx";
2
+ import type { MDXComponents } from "mdx/types";
3
+
4
+ export function getMDXComponents(components?: MDXComponents) {
5
+ return {
6
+ ...defaultMdxComponents,
7
+ ...components,
8
+ } satisfies MDXComponents;
9
+ }
10
+
11
+ export const useMDXComponents = getMDXComponents;
12
+
13
+ declare global {
14
+ type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
15
+ }
@@ -0,0 +1,17 @@
1
+ import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
2
+ import { appTitle } from "./shared";
3
+
4
+ export function baseOptions(): BaseLayoutProps {
5
+ return {
6
+ nav: {
7
+ // The record's display title (instance.md's H1), not the machine slug.
8
+ // Truncated: a long title forced horizontal scroll on mobile without it
9
+ // (found live, 2026-08-18).
10
+ title: (
11
+ <span className="max-w-[60vw] truncate font-medium tracking-tight sm:max-w-none">
12
+ {appTitle}
13
+ </span>
14
+ ),
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,51 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // The record's one identity source: instance.md's `name:` — the same file
5
+ // every other shell reads, so renaming the instance renames every surface
6
+ // at the next build, and no shell carries a baked-in copy (found live
7
+ // 2026-08-18: a stamped constant survived a restore-from-templates as the
8
+ // literal placeholder name, with every gate green).
9
+ function findInstance(start: string): string {
10
+ let dir = start;
11
+ for (let i = 0; i < 5; i += 1) {
12
+ const candidate = path.join(dir, "instance.md");
13
+ if (existsSync(candidate)) return candidate;
14
+ const parent = path.dirname(dir);
15
+ if (parent === dir) break;
16
+ dir = parent;
17
+ }
18
+ throw new Error(
19
+ "instance.md not found — it is the project's identity; build from the project (pnpm dev / pnpm build at the repo root).",
20
+ );
21
+ }
22
+
23
+ function readInstanceName(): string {
24
+ const text = readFileSync(findInstance(process.cwd()), "utf8");
25
+ // Only the frontmatter block: body prose mentioning `name:` must never
26
+ // become the site's identity (review finding, 2026-08-18).
27
+ const block = /^?---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? "";
28
+ const raw = /^name:[ \t]*(.*)$/m.exec(block)?.[1]?.trim() ?? "";
29
+ const unquoted = /^(['"])(.*)\1$/.exec(raw);
30
+ const name = unquoted?.[2] ?? raw;
31
+ if (name === "") {
32
+ throw new Error("instance.md carries no name: — it is the project's identity; run pnpm check.");
33
+ }
34
+ return name;
35
+ }
36
+
37
+ export const appName: string = readInstanceName();
38
+
39
+ /**
40
+ * The record's DISPLAY TITLE: instance.md's first body heading. The slug in
41
+ * `name:` is the machine identity (llms.txt, future citations); the H1 is
42
+ * the human name every page leads with. A fresh scaffold reads "Knowledge
43
+ * System of Record" until the intake interview writes the real one.
44
+ */
45
+ function readInstanceTitle(): string {
46
+ const text = readFileSync(findInstance(process.cwd()), "utf8");
47
+ const body = text.replace(/^\uFEFF?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
48
+ return /^#[ \t]+(.+)$/m.exec(body)?.[1]?.trim() ?? appName;
49
+ }
50
+
51
+ export const appTitle: string = readInstanceTitle();
@@ -0,0 +1,119 @@
1
+ import { docs } from "collections/server";
2
+ import { loader } from "fumadocs-core/source";
3
+ import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
4
+ import type { Node, Root } from "fumadocs-core/page-tree";
5
+
6
+ // See https://fumadocs.dev/docs/headless/source-api for more info
7
+ export const source = loader({
8
+ baseUrl: "/docs",
9
+ source: docs.toFumadocsSource(),
10
+ plugins: [lucideIconsPlugin()],
11
+ });
12
+
13
+ export type KnowledgePage = (typeof source)["$inferPage"];
14
+
15
+ // Sub-path hosting prefix, for URLs we WRITE INTO TEXT (llms.txt,
16
+ // llms-full.txt). Next's <Link> and the router prepend basePath themselves,
17
+ // so this must never touch the loader's baseUrl — that would double-prefix
18
+ // every rendered link.
19
+ export const basePath: string = process.env.KSOR_BASE_PATH ?? "";
20
+
21
+ // `order:` is a governed frontmatter key; a document without one, or with a
22
+ // value that is not a number, sorts after every document that declares one.
23
+ function orderOf(page: KnowledgePage): number {
24
+ const raw: unknown = page.data.order;
25
+ const value = typeof raw === "string" ? Number(raw) : raw;
26
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
27
+ }
28
+
29
+ function nodeOrder(node: Node, orders: ReadonlyMap<string, number>): number {
30
+ if (node.type === "page") return orders.get(node.url) ?? Number.POSITIVE_INFINITY;
31
+ if (node.type === "folder" && node.index) {
32
+ return orders.get(node.index.url) ?? Number.POSITIVE_INFINITY;
33
+ }
34
+ return Number.POSITIVE_INFINITY;
35
+ }
36
+
37
+ // The tie-break key: a page's url, a folder's index url or first page's url.
38
+ // Ties break on it so unordered documents read in plain name order, folders
39
+ // interleaved — the canonical reading order both shells implement (found
40
+ // live 2026-08-18: the loader's own tie order grouped folders after loose
41
+ // files, silently diverging from the Docusaurus shell on the same record).
42
+ function nodeName(node: Node): string {
43
+ if (node.type === "page") return node.url;
44
+ if (node.type === "folder") {
45
+ if (node.index) return node.index.url;
46
+ for (const child of node.children) {
47
+ const name = nodeName(child);
48
+ if (name !== "") return name;
49
+ }
50
+ }
51
+ return "";
52
+ }
53
+
54
+ function sortNodes(nodes: readonly Node[], orders: ReadonlyMap<string, number>): Node[] {
55
+ return nodes
56
+ .map((node) =>
57
+ node.type === "folder" ? { ...node, children: sortNodes(node.children, orders) } : node,
58
+ )
59
+ .sort((a, b) => {
60
+ const left = nodeOrder(a, orders);
61
+ const right = nodeOrder(b, orders);
62
+ if (left !== right) return left < right ? -1 : 1;
63
+ const leftName = nodeName(a);
64
+ const rightName = nodeName(b);
65
+ // Codepoint comparison, not locale: reading order must be one bytewise
66
+ // truth on every machine.
67
+ return leftName < rightName ? -1 : leftName > rightName ? 1 : 0;
68
+ });
69
+ }
70
+
71
+ /**
72
+ * The page tree with every folder's children ordered by `order:` frontmatter.
73
+ * Rebuilt on each call from a fresh clone — the loader's own tree is shared
74
+ * state and mutating it would survive a hot reload.
75
+ */
76
+ export function getSortedPageTree(): Root {
77
+ const orders = new Map(source.getPages().map((page) => [page.url, orderOf(page)] as const));
78
+ const tree = source.getPageTree();
79
+ return { ...tree, children: sortNodes(tree.children, orders) };
80
+ }
81
+
82
+ function collectUrls(nodes: readonly Node[], urls: string[]): void {
83
+ for (const node of nodes) {
84
+ if (node.type === "page") urls.push(node.url);
85
+ else if (node.type === "folder") {
86
+ if (node.index) urls.push(node.index.url);
87
+ collectUrls(node.children, urls);
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Every page, in the order the sidebar shows them — the one reading order the
94
+ * site, llms.txt and llms-full.txt all serve.
95
+ */
96
+ export function getSortedPages(): KnowledgePage[] {
97
+ const urls: string[] = [];
98
+ collectUrls(getSortedPageTree().children, urls);
99
+
100
+ const remaining = new Map(source.getPages().map((page) => [page.url, page] as const));
101
+ const ordered: KnowledgePage[] = [];
102
+ for (const url of urls) {
103
+ const page = remaining.get(url);
104
+ if (page) {
105
+ ordered.push(page);
106
+ remaining.delete(url);
107
+ }
108
+ }
109
+ // A page the tree never displayed is still part of the record.
110
+ return [...ordered, ...remaining.values()];
111
+ }
112
+
113
+ export async function getLLMText(page: KnowledgePage): Promise<string> {
114
+ const processed = await page.data.getText("processed");
115
+
116
+ return `# ${page.data.title} (${basePath}${page.url})
117
+
118
+ ${processed}`;
119
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./.next/dev/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,32 @@
1
+ import { createMDX } from "fumadocs-mdx/next";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const withMDX = createMDX();
6
+
7
+ // The scaffold repo root, two levels up from system/site. The record
8
+ // (<root>/knowledge) lives outside this app dir, so Turbopack's
9
+ // module-resolution root and Next's file tracing are both anchored there —
10
+ // resolved from this file's own location so it holds wherever the repo lands.
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
12
+
13
+ /** @type {import('next').NextConfig} */
14
+ const config = {
15
+ reactStrictMode: true,
16
+ // Static export: `pnpm build` writes the whole site into out/, servable
17
+ // from any static host. No redirects here — they don't work under export.
18
+ output: "export",
19
+ // Emit docs/example/index.html rather than docs/example.html — the layout
20
+ // every static file host serves correctly, GitHub Pages included.
21
+ trailingSlash: true,
22
+ images: { unoptimized: true },
23
+ // Hosting under a sub-path (e.g. a GitHub Pages project site):
24
+ // KSOR_BASE_PATH="/my-repo" pnpm build
25
+ basePath: process.env.KSOR_BASE_PATH ?? "",
26
+ turbopack: {
27
+ root: repoRoot,
28
+ },
29
+ outputFileTracingRoot: repoRoot,
30
+ };
31
+
32
+ export default withMDX(config);
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "site",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "next build",
7
+ "dev": "next dev"
8
+ },
9
+ "dependencies": {
10
+ "fumadocs-core": "16.10.3",
11
+ "fumadocs-mdx": "15.0.13",
12
+ "fumadocs-ui": "16.10.3",
13
+ "lucide-react": "1.31.0",
14
+ "next": "16.2.9",
15
+ "react": "19.2.8",
16
+ "react-dom": "19.2.8",
17
+ "zod": "4.4.3"
18
+ },
19
+ "devDependencies": {
20
+ "@tailwindcss/postcss": "4.3.3",
21
+ "@types/mdx": "2.0.14",
22
+ "@types/node": "25.9.5",
23
+ "@types/react": "19.2.18",
24
+ "@types/react-dom": "19.2.4",
25
+ "postcss": "8.5.26",
26
+ "tailwindcss": "4.3.3",
27
+ "typescript": "6.0.3"
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
@@ -0,0 +1,35 @@
1
+ import { defineConfig, defineDocs } from "fumadocs-mdx/config";
2
+ import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
3
+ import { z } from "zod";
4
+
5
+ // The record lives at <repo>/knowledge — two levels up from this site.
6
+ // Governance frontmatter (status, owner, provenance, superseded_by) is
7
+ // tolerated on top of the default page schema so a governed document
8
+ // always renders; `pnpm check` at the repo root is what enforces it.
9
+ export const docs = defineDocs({
10
+ dir: "../../knowledge",
11
+ docs: {
12
+ schema: pageSchema
13
+ .extend({
14
+ status: z.string().optional(),
15
+ owner: z.string().optional(),
16
+ provenance: z.array(z.string()).optional(),
17
+ superseded_by: z.string().optional(),
18
+ })
19
+ .catchall(z.any()),
20
+ postprocess: {
21
+ // Exposes each page's processed markdown — llms.txt/llms-full.txt
22
+ // depend on it.
23
+ includeProcessedMarkdown: true,
24
+ },
25
+ },
26
+ meta: {
27
+ schema: metaSchema,
28
+ },
29
+ });
30
+
31
+ export default defineConfig({
32
+ mdxOptions: {
33
+ // MDX options
34
+ },
35
+ });