@panaversity/ksor 0.0.20 → 0.0.21

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 (42) hide show
  1. package/CHANGELOG.md +472 -0
  2. package/dist/cli.mjs +71 -19
  3. package/package.json +3 -3
  4. package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
  5. package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
  6. package/templates/scaffold/AGENTS.md +52 -4
  7. package/templates/scaffold/instance.md +28 -20
  8. package/templates/scaffold/knowledge/governance-ladder.md +36 -0
  9. package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
  10. package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
  11. package/templates/scaffold/knowledge/surfaces/index.md +21 -0
  12. package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
  13. package/templates/scaffold/pnpm-lock.yaml +1198 -228
  14. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  15. package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
  16. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
  17. package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
  18. package/templates/scaffold/system/site/app/global.css +552 -9
  19. package/templates/scaffold/system/site/app/layout.tsx +23 -4
  20. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
  21. package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
  22. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
  23. package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
  24. package/templates/scaffold/system/site/components/governance.tsx +262 -0
  25. package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
  26. package/templates/scaffold/system/site/components/record-index.tsx +120 -0
  27. package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
  28. package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
  29. package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
  30. package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
  31. package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
  32. package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
  33. package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
  34. package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
  35. package/templates/scaffold/system/site/components.json +25 -0
  36. package/templates/scaffold/system/site/lib/governance.ts +432 -0
  37. package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
  38. package/templates/scaffold/system/site/lib/shared.ts +38 -0
  39. package/templates/scaffold/system/site/lib/source.ts +221 -5
  40. package/templates/scaffold/system/site/lib/utils.ts +6 -0
  41. package/templates/scaffold/system/site/package.json +9 -3
  42. package/templates/scaffold/knowledge/example.md +0 -23
@@ -1,6 +1,12 @@
1
1
  import { HomeLayout } from "fumadocs-ui/layouts/home";
2
+
2
3
  import { baseOptions } from "@/lib/layout.shared";
3
4
 
5
+ // The front door stands ALONE: a navbar with the record's name, search and the
6
+ // theme switch, and nothing else. It wore the full docs chrome briefly
7
+ // (2026-08-22) so the sidebar was present from the first second; the owner's
8
+ // call is that a landing page should land — the sidebar belongs to the record's
9
+ // pages, and `Open the record` is the door to them.
4
10
  export default function Layout({ children }: LayoutProps<"/">) {
5
11
  return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
6
12
  }
@@ -1,83 +1,78 @@
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.
1
+ import type { ReactElement } from "react";
2
+
5
3
  import mark from "@/app/icon.png";
6
4
  import { FooterMark } from "@/components/footer-mark";
7
- import { appName, appTitle } from "@/lib/shared";
8
- import { basePath, getSortedPages } from "@/lib/source";
5
+ import { HomeCover } from "@/components/home-cover";
6
+ import { appName, appPurpose, appTitle } from "@/lib/shared";
7
+ import { entriesUnder, entryFor, getSortedPages } from "@/lib/source";
9
8
 
10
- export default function HomePage() {
9
+ /**
10
+ * The front door of a system of record.
11
+ *
12
+ * It has four jobs and no fifth (research/site-design.md §4): say what the
13
+ * record is authoritative for, name the identity citations carry, open the
14
+ * record, and point at the surfaces agents read — the last from `/llms.txt`
15
+ * and each document's markdown twin, where agents already look, rather than
16
+ * from a list of addresses printed at a reader. Every string comes from
17
+ * `instance.md` or a document's own frontmatter; the site never contains
18
+ * authored content (scaffolded AGENTS.md, critical rule 1).
19
+ *
20
+ * One full screen, standing alone: no sidebar, no document chrome. The design
21
+ * lives in components/home-cover; this file's job is to hand it the record.
22
+ */
23
+ export default function HomePage(): ReactElement {
11
24
  // 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.
25
+ // the example the scaffold ships cannot leave a link pointing at nothing, and
26
+ // a record that grows a root `index.md` opens on that instead with no change
27
+ // here.
13
28
  const pages = getSortedPages();
14
29
  const [first] = pages;
15
30
 
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.
31
+ if (first === undefined) {
32
+ return (
33
+ <main className="mx-auto w-full max-w-6xl flex-1 px-6 py-20">
34
+ <p className="text-fd-muted-foreground">
35
+ the record is empty — add a document to <code>knowledge/</code>
40
36
  </p>
37
+ </main>
38
+ );
39
+ }
41
40
 
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>
41
+ // The record as the cover shows it: the document the button opens, then the
42
+ // entries standing behind it. The lead is looked up among the top-level
43
+ // entries so a folder keeps the count of what it holds, and falls back to
44
+ // building an entry directly — the first document in governed order can sit
45
+ // BELOW the top level, where `entriesUnder(null)` would never return it.
46
+ const entries = entriesUnder(null);
47
+ const lead = entries.find((entry) => entry.url === first.url) ?? entryFor(first);
48
+ const behind = entries.filter((entry) => entry.url !== lead.url).slice(0, 3);
75
49
 
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
- <FooterMark />
79
- </p>
80
- </footer>
50
+ return (
51
+ <main className="flex flex-1 flex-col">
52
+ <HomeCover
53
+ mark={mark}
54
+ name={appName}
55
+ // instance.md's own H1 — a human name, not the machine slug — so a
56
+ // fresh scaffold reads "Knowledge System of Record" until the intake
57
+ // interview writes the real one.
58
+ title={appTitle}
59
+ // The record's own words: instance.md's first paragraph, which is also
60
+ // what `ksor serve` gives the MCP server as its instructions. The
61
+ // framework's marketing line used to sit here, which put ksor's voice
62
+ // above somebody else's knowledge (research/site-design.md F7).
63
+ purpose={appPurpose}
64
+ documents={pages.length}
65
+ firstUrl={first.url}
66
+ lead={lead}
67
+ behind={behind}
68
+ // Signed from inside the cover, so the front door is one screen rather
69
+ // than a band with a strip of page beneath it.
70
+ foot={
71
+ <p className="mx-auto w-full max-w-6xl px-6 font-mono text-xs tracking-wider text-[var(--ksor-cover-muted)] uppercase">
72
+ <FooterMark />
73
+ </p>
74
+ }
75
+ />
81
76
  </main>
82
77
  );
83
78
  }
@@ -1,9 +1,20 @@
1
- import { source } from "@/lib/source";
1
+ import { entriesUnder, getSortedPages, markdownPath, source } from "@/lib/source";
2
+ import { RecordIndex } from "@/components/record-index";
2
3
  import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
4
+ import { TOCPopover, TOCProvider } from "fumadocs-ui/layouts/docs/page/slots/toc";
3
5
  import { notFound } from "next/navigation";
4
6
  import { getMDXComponents } from "@/components/mdx";
5
7
  import type { Metadata } from "next";
6
8
  import { createRelativeLink } from "fumadocs-ui/mdx";
9
+ import {
10
+ GovernanceMeta,
11
+ Provenance,
12
+ SupersededNotice,
13
+ type Successor,
14
+ } from "@/components/governance";
15
+ import { predecessorsOf, readGovernance, resolveSuccessorUrl } from "@/lib/governance";
16
+ import { showGovernance } from "@/lib/shared";
17
+ import { RecordToc, TocItems } from "@/components/record-toc";
7
18
 
8
19
  export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
9
20
  const params = await props.params;
@@ -11,21 +22,112 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
11
22
  if (!page) notFound();
12
23
 
13
24
  const MDX = page.data.body;
25
+ // What the record says about this document. The page renders it; it never
26
+ // supplies it — an undeclared key shows nothing (specs/ksor/site-governance).
27
+ const governance = readGovernance(page.data, page.path);
28
+
29
+ let successor: Successor | null = null;
30
+ // Gated on the STATUS, not on the pointer: a document the record calls
31
+ // current must never be published under a "Superseded" banner, whatever
32
+ // stale successor pointer it still carries (`pnpm check` refuses that
33
+ // combination too — this is the second lock on the same door).
34
+ if (governance.status === "superseded" && governance.supersededBy !== null) {
35
+ const pages = getSortedPages();
36
+ // Against page.path, not page.url: a route cannot tell a file from a
37
+ // folder index, and `./terms.md` means a different document in each.
38
+ const href = resolveSuccessorUrl(governance.supersededBy, page.path, pages);
39
+ // Name the successor by its title, not by its path: the notice is for a
40
+ // reader, and the pointer is only the fallback when the route did not
41
+ // resolve — never a dead link.
42
+ const target = href === null ? undefined : pages.find((c) => c.url === href.split("#")[0]);
43
+ successor = { href, label: target?.data.title ?? governance.supersededBy };
44
+ }
45
+
46
+ // What this document replaced, derived by asking every document in the record
47
+ // where its successor pointer lands (research/site-design.md F4). No new
48
+ // frontmatter key: the record already says it, in the other direction.
49
+ // The same address `generateMetadata` advertises, shown to a person as well:
50
+ // the human surface handing an agent the record's own bytes is a better
51
+ // demonstration of the product than any copy on the home page.
52
+ const markdownUrl = markdownPath(page.url);
53
+ const allPages = getSortedPages();
54
+ const replaces = predecessorsOf(
55
+ page.url,
56
+ allPages,
57
+ allPages.map((candidate) => ({
58
+ path: candidate.path,
59
+ supersededBy: readGovernance(candidate.data, candidate.path).supersededBy,
60
+ })),
61
+ ).map((url) => ({
62
+ href: url,
63
+ label: allPages.find((candidate) => candidate.url === url)?.data.title ?? url,
64
+ }));
14
65
 
15
66
  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>
67
+ <TocItems items={page.data.toc}>
68
+ <DocsPage
69
+ toc={page.data.toc}
70
+ full={page.data.full}
71
+ // The rail is ours; the provider and the small-screen popover stay the
72
+ // shell's. Its own rail marks a heading active when 90% of it is visible
73
+ // anywhere in the viewport and then highlights whichever became active
74
+ // last, which on this record's short-sectioned documents ran two to four
75
+ // headings AHEAD of the reader. The observer's options are not
76
+ // configurable and the observer is not exported, so the selection could
77
+ // only be replaced — `slots.toc.main` is the seam for that.
78
+ slots={{
79
+ toc: {
80
+ provider: TOCProvider,
81
+ main: RecordToc,
82
+ popover: TOCPopover,
83
+ },
84
+ }}
85
+ // The table-of-contents column is HELD on every page, including the many
86
+ // in a governed record that have no headings at all and render nothing
87
+ // into it. Collapsing it for those documents (which this file did, from
88
+ // 2026-08-21) bought a wider column at the price of a moving one: the
89
+ // article is centred in whatever the column leaves, so the prose jumped
90
+ // 134px sideways between a document with headings and one without
91
+ // (measured at 1728px: text at x=446 against x=580). A reader clicking
92
+ // through a record saw the page slide under them.
93
+ //
94
+ // Held, the grid is `0 | 268 | main | 268 | 0` — sidebar and rail the
95
+ // same width, so the main column is centred in the viewport and the
96
+ // reading measure capped in global.css sits centred inside it, in the
97
+ // same place on every document. The rail is the natural home for the
98
+ // governance facts this record already carries; until it holds them it
99
+ // is quiet space on the side, which is what the earlier collapse was
100
+ // really objecting to — the 900px measure beside it, since fixed.
101
+ >
102
+ {successor === null ? null : <SupersededNotice successor={successor} />}
103
+ <DocsTitle>{page.data.title}</DocsTitle>
104
+ <DocsDescription>{page.data.description}</DocsDescription>
105
+ {showGovernance ? (
106
+ <GovernanceMeta governance={governance} replaces={replaces} markdownUrl={markdownUrl} />
107
+ ) : null}
108
+ {/* grow-0, against the shell's own `flex-1`: the article is a flex column
109
+ stretched to the viewport, so the body inflated from ~150px of text to
110
+ 402px and pushed Sources and everything after it to the bottom of the
111
+ screen — a governance block floating 400px below the document it
112
+ describes (measured, 2026-08-21). Short documents now end where their
113
+ text ends. */}
114
+ <DocsBody style={{ flexGrow: 0 }}>
115
+ <MDX
116
+ components={getMDXComponents({
117
+ // relative links between documents in knowledge/ resolve to
118
+ // their rendered pages
119
+ a: createRelativeLink(source, page),
120
+ })}
121
+ />
122
+ </DocsBody>
123
+ {/* A folder's index page lists what the folder holds. Without it the
124
+ page ended at its own sentence and the documents below it were
125
+ reachable only from the sidebar (research/site-design.md F5). Empty
126
+ for a leaf document, which renders nothing. */}
127
+ <RecordIndex entries={entriesUnder(page.url)} heading="In this section" />
128
+ {showGovernance ? <Provenance entries={governance.provenance} /> : null}
129
+ </DocsPage>
130
+ </TocItems>
29
131
  );
30
132
  }
31
133
 
@@ -46,8 +148,14 @@ export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): P
46
148
  const page = source.getPage(params.slug);
47
149
  if (!page) notFound();
48
150
 
151
+ // The markdown twin, advertised rather than left to be guessed: a consumer
152
+ // that follows `rel="alternate"` reaches the record's own bytes instead of
153
+ // scraping this page (research/site-design.md F2).
154
+ const markdownUrl = markdownPath(page.url);
155
+
49
156
  return {
50
157
  title: page.data.title,
51
158
  description: page.data.description,
159
+ alternates: { types: { "text/markdown": markdownUrl } },
52
160
  };
53
161
  }
@@ -1,24 +1,5 @@
1
- import { getSortedPageTree } from "@/lib/source";
2
- import { DocsLayout } from "fumadocs-ui/layouts/docs";
3
- import { baseOptions } from "@/lib/layout.shared";
4
- import { FooterMark } from "@/components/footer-mark";
1
+ import { RecordShell } from "@/components/record-shell";
5
2
 
6
3
  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
- <FooterMark />
17
- </p>
18
- ),
19
- }}
20
- >
21
- {children}
22
- </DocsLayout>
23
- );
4
+ return <RecordShell>{children}</RecordShell>;
24
5
  }