@panaversity/ksor 0.0.19 → 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.
- package/CHANGELOG.md +501 -0
- package/dist/cli.mjs +99 -19
- package/docs/authorization.md +197 -0
- package/docs/index.md +4 -0
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/AGENTS.md +52 -4
- package/templates/scaffold/instance.md +28 -20
- package/templates/scaffold/knowledge/governance-ladder.md +36 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
- package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
- package/templates/scaffold/knowledge/surfaces/index.md +21 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
- package/templates/scaffold/pnpm-lock.yaml +1198 -228
- package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
- package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
- package/templates/scaffold/system/site/app/global.css +552 -9
- package/templates/scaffold/system/site/app/layout.tsx +23 -4
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
- package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
- package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
- package/templates/scaffold/system/site/components/governance.tsx +262 -0
- package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
- package/templates/scaffold/system/site/components/record-index.tsx +120 -0
- package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
- package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
- package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
- package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
- package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
- package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
- package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
- package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
- package/templates/scaffold/system/site/components.json +25 -0
- package/templates/scaffold/system/site/lib/governance.ts +432 -0
- package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
- package/templates/scaffold/system/site/lib/shared.ts +38 -0
- package/templates/scaffold/system/site/lib/source.ts +221 -5
- package/templates/scaffold/system/site/lib/utils.ts +6 -0
- package/templates/scaffold/system/site/package.json +9 -3
- 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
|
|
2
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
<
|
|
19
|
-
<
|
|
20
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
aria-hidden
|
|
51
|
-
className="transition-transform group-hover:translate-x-0.5 motion-reduce:transform-none"
|
|
52
|
-
>
|
|
53
|
-
→
|
|
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> · {pages.length} document
|
|
60
|
-
{pages.length === 1 ? "" : "s"} · 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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 {
|
|
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
|
}
|