@panaversity/ksor 0.0.39 → 0.0.41
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 +965 -0
- package/README.md +29 -9
- package/dist/checker/check-main.mjs +14049 -0
- package/dist/cli.mjs +11528 -5213
- package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
- package/dist/gateway.d.mts +52 -13
- package/dist/gateway.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
- package/docs/authorization.md +8 -6
- package/docs/deploying.md +36 -25
- package/docs/index.md +26 -13
- package/docs/ingesting.md +70 -22
- package/docs/tool-surface.md +69 -16
- package/package.json +4 -3
- package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
- package/schema/schema.sql +77 -14
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.github/workflows/validate.yml +9 -1
- package/templates/scaffold/.ksor/governance.yaml +17 -0
- package/templates/scaffold/AGENTS.md +293 -119
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +37 -6
- package/templates/scaffold/gitignore +14 -6
- package/templates/scaffold/instance.md +21 -17
- package/templates/scaffold/knowledge/governance-ladder.md +11 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
- package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
- package/templates/scaffold/knowledge/surfaces/index.md +4 -20
- package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
- package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
- package/templates/scaffold/package.json +3 -4
- package/templates/scaffold/pnpm-lock.yaml +3 -0
- package/templates/scaffold/system/gateways/content.ts +13 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
- package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
- package/templates/scaffold/system/site/app/global.css +198 -6
- package/templates/scaffold/system/site/app/layout.tsx +8 -3
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
- package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
- package/templates/scaffold/system/site/components/code-block.tsx +87 -0
- package/templates/scaffold/system/site/components/embed.tsx +276 -0
- package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +219 -87
- package/templates/scaffold/system/site/components/mdx.tsx +10 -2
- package/templates/scaffold/system/site/components/record-index.tsx +5 -5
- package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
- package/templates/scaffold/system/site/components/record-views.tsx +6 -17
- package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
- package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
- package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
- package/templates/scaffold/system/site/lib/attachments.ts +0 -28
- package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
- package/templates/scaffold/system/site/lib/audience.ts +42 -146
- package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
- package/templates/scaffold/system/site/lib/governance.ts +339 -225
- package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
- package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
- package/templates/scaffold/system/site/lib/lock.ts +282 -0
- package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
- package/templates/scaffold/system/site/lib/record-href.ts +68 -0
- package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
- package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
- package/templates/scaffold/system/site/lib/shared.ts +67 -104
- package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
- package/templates/scaffold/system/site/lib/source.ts +256 -186
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
- package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
- package/templates/scaffold/system/site/package.json +1 -0
- package/templates/scaffold/system/site/record/actor.ts +23 -0
- package/templates/scaffold/system/site/record/check.ts +571 -0
- package/templates/scaffold/system/site/record/citations.ts +312 -0
- package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
- package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
- package/templates/scaffold/system/site/record/hygiene.ts +320 -0
- package/templates/scaffold/system/site/record/index-file.ts +150 -0
- package/templates/scaffold/system/site/record/index.ts +103 -0
- package/templates/scaffold/system/site/record/instance.ts +257 -0
- package/templates/scaffold/system/site/record/instant.ts +43 -0
- package/templates/scaffold/system/site/record/ledger.ts +694 -0
- package/templates/scaffold/system/site/record/load.ts +129 -0
- package/templates/scaffold/system/site/record/lock.ts +306 -0
- package/templates/scaffold/system/site/record/near-miss.ts +37 -0
- package/templates/scaffold/system/site/record/policy.ts +414 -0
- package/templates/scaffold/system/site/record/profile.ts +535 -0
- package/templates/scaffold/system/site/record/refusal.ts +106 -0
- package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
- package/templates/scaffold/system/site/source.config.ts +109 -23
- package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
- package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
- package/templates/scaffold/system/site/lib/page-order.ts +0 -93
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
readHandler,
|
|
33
33
|
SEARCH_OUTPUT,
|
|
34
34
|
searchHandler,
|
|
35
|
+
TRUST_TIERS,
|
|
35
36
|
z,
|
|
36
37
|
type ServiceContext,
|
|
37
38
|
} from "@panaversity/ksor/gateway";
|
|
@@ -75,6 +76,18 @@ export default function buildGateway(ctx: ServiceContext, version: string): McpS
|
|
|
75
76
|
.max(MAX_SEARCH_K)
|
|
76
77
|
.default(10)
|
|
77
78
|
.describe(`Maximum passages to return (1–${MAX_SEARCH_K})`),
|
|
79
|
+
// The caller's trust floor. Keep it: dropping the parameter does not
|
|
80
|
+
// weaken the record — the handler still applies `unverified` and this
|
|
81
|
+
// deployment's own floor — but it takes away the only way a caller can
|
|
82
|
+
// ask to be answered ONLY from what a human reviewed, and the door says
|
|
83
|
+
// so at boot instead of failing quietly.
|
|
84
|
+
min_trust_tier: z
|
|
85
|
+
.enum(TRUST_TIERS)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe(
|
|
88
|
+
"Lowest trust tier to answer from (default unverified). This deployment's own " +
|
|
89
|
+
"floor still applies: it can only be raised here, never lowered.",
|
|
90
|
+
),
|
|
78
91
|
}),
|
|
79
92
|
outputSchema: SEARCH_OUTPUT,
|
|
80
93
|
annotations: READ_ONLY,
|
|
@@ -42,8 +42,8 @@ export default function HomePage(): ReactElement {
|
|
|
42
42
|
// entries standing behind it. The lead is looked up among the top-level
|
|
43
43
|
// entries so a folder keeps the count of what it holds, and falls back to
|
|
44
44
|
// building an entry directly — the first document in governed order can sit
|
|
45
|
-
// BELOW the top level, where
|
|
46
|
-
const entries = entriesUnder(
|
|
45
|
+
// BELOW the top level, where the root's own listing would never return it.
|
|
46
|
+
const entries = entriesUnder("");
|
|
47
47
|
const lead = entries.find((entry) => entry.url === first.url) ?? entryFor(first);
|
|
48
48
|
const behind = entries.filter((entry) => entry.url !== lead.url).slice(0, 3);
|
|
49
49
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appName, mcpEndpoint, mcpNamespace, recordDescription, recordVersion } from "@/lib/shared";
|
|
2
|
+
import { readStageManifest } from "@/lib/stage-manifest";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* `/.well-known/mcp/server.json` — how an agent DISCOVERS this record's MCP
|
|
@@ -16,6 +17,10 @@ import { appName, mcpEndpoint, mcpNamespace, recordDescription, recordVersion }
|
|
|
16
17
|
* `name:` has no slash, so it was rejected outright.
|
|
17
18
|
* version REQUIRED. Absent, the document failed validation on its own.
|
|
18
19
|
* capabilities NOT a field in the schema; it was invented here.
|
|
20
|
+
* _meta the schema's extension point, namespaced by reverse DNS —
|
|
21
|
+
* where the build's stamps go (build spec §3, R14), so a
|
|
22
|
+
* validating client still accepts the document and a consumer
|
|
23
|
+
* can connect it to the publication it describes.
|
|
19
24
|
*
|
|
20
25
|
* (Checked against the 2025-12-11 schema, round-6 review of #43, which built
|
|
21
26
|
* the scaffold and validated the emitted file.)
|
|
@@ -27,11 +32,16 @@ export const dynamic = "force-static";
|
|
|
27
32
|
|
|
28
33
|
const SCHEMA = "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json";
|
|
29
34
|
|
|
35
|
+
/** The `_meta` key the stamps live under: a namespace this project controls. */
|
|
36
|
+
const META_KEY = "com.panaversity.ksor/build";
|
|
37
|
+
|
|
30
38
|
export function GET(): Response {
|
|
31
39
|
const endpoint = mcpEndpoint();
|
|
40
|
+
const { build_id, source_commit, dirty, ksor_version, unstamped } = readStageManifest().stamps;
|
|
32
41
|
return Response.json(
|
|
33
42
|
{
|
|
34
43
|
$schema: SCHEMA,
|
|
44
|
+
_meta: { [META_KEY]: { build_id, source_commit, dirty, ksor_version, unstamped } },
|
|
35
45
|
name: `${mcpNamespace()}/${appName}`,
|
|
36
46
|
// The record's OWN account of itself — see recordDescription. A
|
|
37
47
|
// description identical in every ksor record cannot help an agent choose
|
|
@@ -1,19 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
basePath,
|
|
3
|
+
entriesUnder,
|
|
4
|
+
folderHeading,
|
|
5
|
+
folderOfRoute,
|
|
6
|
+
folderSlugs,
|
|
7
|
+
getSortedPages,
|
|
8
|
+
markdownPath,
|
|
9
|
+
source,
|
|
10
|
+
} from "@/lib/source";
|
|
2
11
|
import { RecordIndex } from "@/components/record-index";
|
|
3
12
|
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/layouts/docs/page";
|
|
4
13
|
import { TOCPopover, TOCProvider } from "fumadocs-ui/layouts/docs/page/slots/toc";
|
|
5
14
|
import { notFound } from "next/navigation";
|
|
6
15
|
import { getMDXComponents } from "@/components/mdx";
|
|
7
16
|
import type { Metadata } from "next";
|
|
8
|
-
import {
|
|
17
|
+
import type { ReactElement } from "react";
|
|
18
|
+
import { recordLink } from "@/lib/record-link";
|
|
9
19
|
import {
|
|
20
|
+
DeprecatedNotice,
|
|
10
21
|
GovernanceMeta,
|
|
22
|
+
LifecycleCaveat,
|
|
11
23
|
Provenance,
|
|
12
|
-
SupersededNotice,
|
|
13
24
|
type Successor,
|
|
14
25
|
} from "@/components/governance";
|
|
15
26
|
import { predecessorsOf, readGovernance, resolveSuccessorUrl } from "@/lib/governance";
|
|
16
27
|
import { showGovernance } from "@/lib/shared";
|
|
28
|
+
import { stagePageOf } from "@/lib/stage-manifest";
|
|
17
29
|
import { RecordBreadcrumb } from "@/components/record-breadcrumb";
|
|
18
30
|
import { RecordToc, TocItems } from "@/components/record-toc";
|
|
19
31
|
import { RecordViews } from "@/components/record-views";
|
|
@@ -24,10 +36,53 @@ import { StudyAids } from "@/components/study-aids";
|
|
|
24
36
|
import { deckFor, quizFor, slidesFor, summaryFor } from "@/lib/attachments";
|
|
25
37
|
import { readingMinutes } from "@/lib/reading-time";
|
|
26
38
|
|
|
39
|
+
/** The route a slug names: `/docs`, or `/docs/a/b`. */
|
|
40
|
+
function routeOf(slug: readonly string[] | undefined): string {
|
|
41
|
+
return slug === undefined || slug.length === 0 ? "/docs" : `/docs/${slug.join("/")}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Every page reads `rel="describedby"`: the record describes itself in
|
|
46
|
+
* `llms.txt`, and a consumer that follows the link learns what this record is
|
|
47
|
+
* authoritative for before it reads a page of it (build spec §3).
|
|
48
|
+
*/
|
|
49
|
+
function DescribedBy(): ReactElement {
|
|
50
|
+
return <link rel="describedby" href={`${basePath}/llms.txt`} type="text/markdown" />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A directory's page: the regenerated `index.md` of this viewer's stage,
|
|
55
|
+
* rendered as a listing. A folder has no body and no governance of its own
|
|
56
|
+
* (record spec §1) — it is admitted iff a descendant is, and its page is the
|
|
57
|
+
* list of what it holds. Excluded from the docs collection, so it has no
|
|
58
|
+
* twin, no `llms.txt` line and no search entry.
|
|
59
|
+
*/
|
|
60
|
+
function FolderPage({ dir }: { dir: string }): ReactElement {
|
|
61
|
+
return (
|
|
62
|
+
<TocItems items={[]}>
|
|
63
|
+
<DocsPage
|
|
64
|
+
toc={[]}
|
|
65
|
+
slots={{
|
|
66
|
+
breadcrumb: RecordBreadcrumb,
|
|
67
|
+
toc: { provider: TOCProvider, main: RecordToc, popover: TOCPopover },
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<DescribedBy />
|
|
71
|
+
<DocsTitle>{folderHeading(dir)}</DocsTitle>
|
|
72
|
+
<RecordIndex entries={entriesUnder(dir)} heading="In this section" />
|
|
73
|
+
</DocsPage>
|
|
74
|
+
</TocItems>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
27
78
|
export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
28
79
|
const params = await props.params;
|
|
29
80
|
const page = source.getPage(params.slug);
|
|
30
|
-
if (!page)
|
|
81
|
+
if (!page) {
|
|
82
|
+
const dir = folderOfRoute(routeOf(params.slug));
|
|
83
|
+
if (dir === null) notFound();
|
|
84
|
+
return <FolderPage dir={dir} />;
|
|
85
|
+
}
|
|
31
86
|
|
|
32
87
|
const MDX = page.data.body;
|
|
33
88
|
// Attachments of THIS document, found by suffix on its own path. Null is the
|
|
@@ -39,40 +94,35 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
39
94
|
const presentation = slidesFor(page.path);
|
|
40
95
|
// Counted at BUILD time from the document's own markdown, so the figure is in
|
|
41
96
|
// the shipped HTML for a reader with a failed bundle, a crawler and an agent
|
|
42
|
-
// alike.
|
|
43
|
-
// out of reach of all three.
|
|
97
|
+
// alike.
|
|
44
98
|
const minutes = readingMinutes(await page.data.getText("processed"));
|
|
45
99
|
const summaryMinutes =
|
|
46
100
|
summary === null ? null : readingMinutes(await summary.getText("processed"));
|
|
47
101
|
// What the record says about this document. The page renders it; it never
|
|
48
|
-
// supplies it — an undeclared key shows nothing
|
|
102
|
+
// supplies it — an undeclared key shows nothing.
|
|
49
103
|
const governance = readGovernance(page.data, page.path);
|
|
104
|
+
// What staging decided about it: the badge, and whether a twin exists.
|
|
105
|
+
const staged = stagePageOf(page.path);
|
|
106
|
+
const badge = staged?.badge ?? null;
|
|
107
|
+
const twin = staged?.machine === true ? markdownPath(page.url) : undefined;
|
|
50
108
|
|
|
109
|
+
const allPages = getSortedPages();
|
|
51
110
|
let successor: Successor | null = null;
|
|
52
111
|
// Gated on the STATUS, not on the pointer: a document the record calls
|
|
53
|
-
// current must never be published under a "
|
|
54
|
-
// stale successor pointer it still carries
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Against page.path, not page.url: a route cannot tell a file from a
|
|
59
|
-
// folder index, and `./terms.md` means a different document in each.
|
|
60
|
-
const href = resolveSuccessorUrl(governance.supersededBy, page.path, pages);
|
|
61
|
-
// Name the successor by its title, not by its path: the notice is for a
|
|
112
|
+
// current must never be published under a "Deprecated" banner, whatever
|
|
113
|
+
// stale successor pointer it still carries.
|
|
114
|
+
if (governance.status === "deprecated" && governance.supersededBy !== null) {
|
|
115
|
+
const href = resolveSuccessorUrl(governance.supersededBy, allPages);
|
|
116
|
+
// Name the successor by its title, not by its id: the notice is for a
|
|
62
117
|
// reader, and the pointer is only the fallback when the route did not
|
|
63
118
|
// resolve — never a dead link.
|
|
64
|
-
const target = href === null ? undefined :
|
|
119
|
+
const target = href === null ? undefined : allPages.find((c) => c.url === href.split("#")[0]);
|
|
65
120
|
successor = { href, label: target?.data.title ?? governance.supersededBy };
|
|
66
121
|
}
|
|
67
122
|
|
|
68
123
|
// What this document replaced, derived by asking every document in the record
|
|
69
124
|
// where its successor pointer lands (research/site-design.md F4). No new
|
|
70
125
|
// frontmatter key: the record already says it, in the other direction.
|
|
71
|
-
// The same address `generateMetadata` advertises, shown to a person as well:
|
|
72
|
-
// the human surface handing an agent the record's own bytes is a better
|
|
73
|
-
// demonstration of the product than any copy on the home page.
|
|
74
|
-
const markdownUrl = markdownPath(page.url);
|
|
75
|
-
const allPages = getSortedPages();
|
|
76
126
|
const replaces = predecessorsOf(
|
|
77
127
|
page.url,
|
|
78
128
|
allPages,
|
|
@@ -91,16 +141,9 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
91
141
|
toc={page.data.toc}
|
|
92
142
|
full={page.data.full}
|
|
93
143
|
// The rail is ours; the provider and the small-screen popover stay the
|
|
94
|
-
// shell's
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// headings AHEAD of the reader. The observer's options are not
|
|
98
|
-
// configurable and the observer is not exported, so the selection could
|
|
99
|
-
// only be replaced — `slots.toc.main` is the seam for that.
|
|
100
|
-
// The breadcrumb is ours for one reason, recorded in the component:
|
|
101
|
-
// the shell's renders nothing at all on a top-level document, so the
|
|
102
|
-
// block above the title came and went as a reader moved through the
|
|
103
|
-
// record.
|
|
144
|
+
// shell's (its own rail ran two to four headings ahead of the reader on
|
|
145
|
+
// this record's short-sectioned documents). The breadcrumb is ours
|
|
146
|
+
// because the shell's renders nothing on a top-level document.
|
|
104
147
|
slots={{
|
|
105
148
|
breadcrumb: RecordBreadcrumb,
|
|
106
149
|
toc: {
|
|
@@ -110,83 +153,68 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
110
153
|
},
|
|
111
154
|
}}
|
|
112
155
|
// The table-of-contents column is HELD on every page, including the many
|
|
113
|
-
// in a governed record that have no headings
|
|
114
|
-
//
|
|
115
|
-
// 2026-08-21) bought a wider column at the price of a moving one: the
|
|
116
|
-
// article is centred in whatever the column leaves, so the prose jumped
|
|
117
|
-
// 134px sideways between a document with headings and one without
|
|
118
|
-
// (measured at 1728px: text at x=446 against x=580). A reader clicking
|
|
119
|
-
// through a record saw the page slide under them.
|
|
120
|
-
//
|
|
121
|
-
// Held, the grid is `0 | 268 | main | 268 | 0` — sidebar and rail the
|
|
122
|
-
// same width, so the main column is centred in the viewport and the
|
|
123
|
-
// reading measure capped in global.css sits centred inside it, in the
|
|
124
|
-
// same place on every document. The rail is the natural home for the
|
|
125
|
-
// governance facts this record already carries; until it holds them it
|
|
126
|
-
// is quiet space on the side, which is what the earlier collapse was
|
|
127
|
-
// really objecting to — the 900px measure beside it, since fixed.
|
|
156
|
+
// in a governed record that have no headings, so the prose sits in the
|
|
157
|
+
// same place on every document (measured 134px of drift without it).
|
|
128
158
|
>
|
|
129
|
-
|
|
159
|
+
<DescribedBy />
|
|
160
|
+
{governance.status === "deprecated" ? <DeprecatedNotice successor={successor} /> : null}
|
|
130
161
|
<DocsTitle>{page.data.title}</DocsTitle>
|
|
131
162
|
<DocsDescription>{page.data.description}</DocsDescription>
|
|
132
163
|
{showGovernance ? (
|
|
133
|
-
<GovernanceMeta
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
164
|
+
<GovernanceMeta
|
|
165
|
+
governance={governance}
|
|
166
|
+
badge={badge}
|
|
167
|
+
replaces={replaces}
|
|
168
|
+
markdownUrl={twin}
|
|
169
|
+
// Only when there is no view strip to carry it — with a summary,
|
|
170
|
+
// the number belongs to the view the reader picked, not to the
|
|
171
|
+
// document as a whole.
|
|
172
|
+
minutes={summary === null ? minutes : undefined}
|
|
173
|
+
/>
|
|
174
|
+
) : (
|
|
175
|
+
// …and with the strip off, the CAVEAT still shows. `site.governance`
|
|
176
|
+
// hides attribution, not the record's word on whether this document
|
|
177
|
+
// is in force — which the sidebar, the folder listing and the search
|
|
178
|
+
// result all keep saying about this same page (`plainBadge`).
|
|
179
|
+
<LifecycleCaveat badge={badge} effectiveFrom={governance.effectiveFrom} />
|
|
180
|
+
)}
|
|
181
|
+
{/* grow-0, against the shell's own `flex-1`: short documents end where
|
|
182
|
+
their text ends rather than pushing Sources to the bottom of the
|
|
183
|
+
screen (measured, 2026-08-21). */}
|
|
141
184
|
<DocsBody style={{ flexGrow: 0 }}>
|
|
142
185
|
{/* The summary panel is built HERE, on the server, and handed to the
|
|
143
186
|
client tab strip as a prop — so it is in the shipped HTML whether or
|
|
144
|
-
not the bundle runs
|
|
145
|
-
|
|
146
|
-
summary, RecordViews renders the body alone and no tab strip exists
|
|
147
|
-
(specs/ksor/study-attachments C3, C20). */}
|
|
187
|
+
not the bundle runs. Presence-driven: with no summary, RecordViews
|
|
188
|
+
renders the body alone and no tab strip exists. */}
|
|
148
189
|
<RecordViews
|
|
149
190
|
documentMinutes={minutes}
|
|
150
191
|
summaryMinutes={summaryMinutes ?? undefined}
|
|
151
192
|
summary={
|
|
152
193
|
Summary === null ? null : (
|
|
153
|
-
<Summary components={getMDXComponents({ a:
|
|
194
|
+
<Summary components={getMDXComponents({ a: recordLink(source, page) })} />
|
|
154
195
|
)
|
|
155
196
|
}
|
|
156
197
|
>
|
|
157
198
|
<MDX
|
|
158
199
|
components={getMDXComponents({
|
|
159
|
-
//
|
|
160
|
-
// their rendered pages
|
|
161
|
-
a:
|
|
200
|
+
// links between documents in knowledge/, in both OKF forms,
|
|
201
|
+
// resolve to their rendered pages
|
|
202
|
+
a: recordLink(source, page),
|
|
162
203
|
// The deck, rendered where the record's own shape puts it:
|
|
163
|
-
// after the introduction, before the first section.
|
|
164
|
-
// rehype plugin marks the place on every document and this
|
|
165
|
-
// decides whether there is anything to put there — so "does
|
|
166
|
-
// this document have a teaching aid" stays one question,
|
|
167
|
-
// answered by the attachment, not two.
|
|
204
|
+
// after the introduction, before the first section.
|
|
168
205
|
TeachingAid: () =>
|
|
169
206
|
presentation === null ? null : <Slides slides={presentation} />,
|
|
170
207
|
})}
|
|
171
208
|
/>
|
|
172
209
|
</RecordViews>
|
|
173
210
|
</DocsBody>
|
|
174
|
-
{/* What a reader DOES with this document once they have read it.
|
|
175
|
-
|
|
176
|
-
not a new argument about where it goes. Renders nothing at all when
|
|
177
|
-
the document has no study aids. */}
|
|
178
|
-
{/* Recall first, then the check on it — and each renders only if the
|
|
179
|
-
document carries one, so a page with just a quiz shows just a quiz. */}
|
|
211
|
+
{/* What a reader DOES with this document once they have read it.
|
|
212
|
+
Renders nothing at all when the document has no study aids. */}
|
|
180
213
|
<StudyAids>
|
|
181
214
|
{deck === null ? null : <Flashcards deck={deck} />}
|
|
182
215
|
{quiz === null ? null : <Quiz quiz={quiz} />}
|
|
183
216
|
</StudyAids>
|
|
184
|
-
{
|
|
185
|
-
page ended at its own sentence and the documents below it were
|
|
186
|
-
reachable only from the sidebar (research/site-design.md F5). Empty
|
|
187
|
-
for a leaf document, which renders nothing. */}
|
|
188
|
-
<RecordIndex entries={entriesUnder(page.url)} heading="In this section" />
|
|
189
|
-
{showGovernance ? <Provenance entries={governance.provenance} /> : null}
|
|
217
|
+
{showGovernance ? <Provenance entries={governance.sources} /> : null}
|
|
190
218
|
</DocsPage>
|
|
191
219
|
</TocItems>
|
|
192
220
|
);
|
|
@@ -194,29 +222,44 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
194
222
|
|
|
195
223
|
export async function generateStaticParams() {
|
|
196
224
|
const params = source.generateParams();
|
|
197
|
-
|
|
225
|
+
// Every directory this viewer's stage holds an index for is a page too —
|
|
226
|
+
// the root included, which is the record's own map.
|
|
227
|
+
const folders = folderSlugs().map((slug) => ({ slug }));
|
|
228
|
+
if (params.length === 0 && folders.length === 0) {
|
|
198
229
|
// Without this, Next fails the empty-record build with an error that
|
|
199
230
|
// names neither the record nor the rule (found live, 2026-08-18).
|
|
231
|
+
//
|
|
232
|
+
// NOT the same state as "this build publishes no document". The emitted
|
|
233
|
+
// starter is all drafts by design (R25), and build spec §4 acceptance 4
|
|
234
|
+
// requires it to BUILD and publish none of them — so a stage with the root
|
|
235
|
+
// index and no page is a record waiting for its first approval, and
|
|
236
|
+
// throwing here failed `pnpm build` on every freshly scaffolded project
|
|
237
|
+
// (found live 2026-08-25 through the scaffold e2e). What is left is the
|
|
238
|
+
// genuinely impossible state: no page AND no index, which means the stage
|
|
239
|
+
// itself is missing.
|
|
200
240
|
throw new Error(
|
|
201
|
-
"the record
|
|
241
|
+
"the staged record holds neither a document nor a folder index — the stage is missing or empty; run `ksor build` and check knowledge/ (`pnpm check` says the same).",
|
|
202
242
|
);
|
|
203
243
|
}
|
|
204
|
-
return params;
|
|
244
|
+
return [...params, ...folders];
|
|
205
245
|
}
|
|
206
246
|
|
|
207
247
|
export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
|
|
208
248
|
const params = await props.params;
|
|
209
249
|
const page = source.getPage(params.slug);
|
|
210
|
-
if (!page)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const markdownUrl = markdownPath(page.url);
|
|
250
|
+
if (!page) {
|
|
251
|
+
const dir = folderOfRoute(routeOf(params.slug));
|
|
252
|
+
if (dir === null) notFound();
|
|
253
|
+
return { title: folderHeading(dir) };
|
|
254
|
+
}
|
|
216
255
|
|
|
256
|
+
// The markdown twin, advertised rather than left to be guessed — and only
|
|
257
|
+
// where one exists: a page the machine surfaces decline has no twin, and
|
|
258
|
+
// advertising one would be a dead link (build spec §3).
|
|
259
|
+
const twin = stagePageOf(page.path)?.machine === true;
|
|
217
260
|
return {
|
|
218
261
|
title: page.data.title,
|
|
219
262
|
description: page.data.description,
|
|
220
|
-
alternates: { types: { "text/markdown":
|
|
263
|
+
...(twin ? { alternates: { types: { "text/markdown": markdownPath(page.url) } } } : {}),
|
|
221
264
|
};
|
|
222
265
|
}
|