@panaversity/ksor 0.0.40 → 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 +869 -0
- package/README.md +11 -7
- 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 +234 -113
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +37 -6
- package/templates/scaffold/gitignore +13 -8
- package/templates/scaffold/instance.md +21 -17
- package/templates/scaffold/knowledge/governance-ladder.md +6 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +7 -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 +126 -91
- package/templates/scaffold/system/site/app/global.css +13 -5
- 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/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +205 -87
- 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/sidebar-status.tsx +19 -18
- 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 +9 -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 +566 -492
- 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 +77 -22
- 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,91 +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
164
|
<GovernanceMeta
|
|
134
165
|
governance={governance}
|
|
166
|
+
badge={badge}
|
|
135
167
|
replaces={replaces}
|
|
136
|
-
markdownUrl={
|
|
168
|
+
markdownUrl={twin}
|
|
137
169
|
// Only when there is no view strip to carry it — with a summary,
|
|
138
170
|
// the number belongs to the view the reader picked, not to the
|
|
139
171
|
// document as a whole.
|
|
140
172
|
minutes={summary === null ? minutes : undefined}
|
|
141
173
|
/>
|
|
142
|
-
) :
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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). */}
|
|
149
184
|
<DocsBody style={{ flexGrow: 0 }}>
|
|
150
185
|
{/* The summary panel is built HERE, on the server, and handed to the
|
|
151
186
|
client tab strip as a prop — so it is in the shipped HTML whether or
|
|
152
|
-
not the bundle runs
|
|
153
|
-
|
|
154
|
-
summary, RecordViews renders the body alone and no tab strip exists
|
|
155
|
-
(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. */}
|
|
156
189
|
<RecordViews
|
|
157
190
|
documentMinutes={minutes}
|
|
158
191
|
summaryMinutes={summaryMinutes ?? undefined}
|
|
159
192
|
summary={
|
|
160
193
|
Summary === null ? null : (
|
|
161
|
-
<Summary components={getMDXComponents({ a:
|
|
194
|
+
<Summary components={getMDXComponents({ a: recordLink(source, page) })} />
|
|
162
195
|
)
|
|
163
196
|
}
|
|
164
197
|
>
|
|
165
198
|
<MDX
|
|
166
199
|
components={getMDXComponents({
|
|
167
|
-
//
|
|
168
|
-
// their rendered pages
|
|
169
|
-
a:
|
|
200
|
+
// links between documents in knowledge/, in both OKF forms,
|
|
201
|
+
// resolve to their rendered pages
|
|
202
|
+
a: recordLink(source, page),
|
|
170
203
|
// The deck, rendered where the record's own shape puts it:
|
|
171
|
-
// after the introduction, before the first section.
|
|
172
|
-
// rehype plugin marks the place on every document and this
|
|
173
|
-
// decides whether there is anything to put there — so "does
|
|
174
|
-
// this document have a teaching aid" stays one question,
|
|
175
|
-
// answered by the attachment, not two.
|
|
204
|
+
// after the introduction, before the first section.
|
|
176
205
|
TeachingAid: () =>
|
|
177
206
|
presentation === null ? null : <Slides slides={presentation} />,
|
|
178
207
|
})}
|
|
179
208
|
/>
|
|
180
209
|
</RecordViews>
|
|
181
210
|
</DocsBody>
|
|
182
|
-
{/* What a reader DOES with this document once they have read it.
|
|
183
|
-
|
|
184
|
-
not a new argument about where it goes. Renders nothing at all when
|
|
185
|
-
the document has no study aids. */}
|
|
186
|
-
{/* Recall first, then the check on it — and each renders only if the
|
|
187
|
-
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. */}
|
|
188
213
|
<StudyAids>
|
|
189
214
|
{deck === null ? null : <Flashcards deck={deck} />}
|
|
190
215
|
{quiz === null ? null : <Quiz quiz={quiz} />}
|
|
191
216
|
</StudyAids>
|
|
192
|
-
{
|
|
193
|
-
page ended at its own sentence and the documents below it were
|
|
194
|
-
reachable only from the sidebar (research/site-design.md F5). Empty
|
|
195
|
-
for a leaf document, which renders nothing. */}
|
|
196
|
-
<RecordIndex entries={entriesUnder(page.url)} heading="In this section" />
|
|
197
|
-
{showGovernance ? <Provenance entries={governance.provenance} /> : null}
|
|
217
|
+
{showGovernance ? <Provenance entries={governance.sources} /> : null}
|
|
198
218
|
</DocsPage>
|
|
199
219
|
</TocItems>
|
|
200
220
|
);
|
|
@@ -202,29 +222,44 @@ export default async function Page(props: PageProps<"/docs/[[...slug]]">) {
|
|
|
202
222
|
|
|
203
223
|
export async function generateStaticParams() {
|
|
204
224
|
const params = source.generateParams();
|
|
205
|
-
|
|
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) {
|
|
206
229
|
// Without this, Next fails the empty-record build with an error that
|
|
207
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.
|
|
208
240
|
throw new Error(
|
|
209
|
-
"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).",
|
|
210
242
|
);
|
|
211
243
|
}
|
|
212
|
-
return params;
|
|
244
|
+
return [...params, ...folders];
|
|
213
245
|
}
|
|
214
246
|
|
|
215
247
|
export async function generateMetadata(props: PageProps<"/docs/[[...slug]]">): Promise<Metadata> {
|
|
216
248
|
const params = await props.params;
|
|
217
249
|
const page = source.getPage(params.slug);
|
|
218
|
-
if (!page)
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
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
|
+
}
|
|
224
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;
|
|
225
260
|
return {
|
|
226
261
|
title: page.data.title,
|
|
227
262
|
description: page.data.description,
|
|
228
|
-
alternates: { types: { "text/markdown":
|
|
263
|
+
...(twin ? { alternates: { types: { "text/markdown": markdownPath(page.url) } } } : {}),
|
|
229
264
|
};
|
|
230
265
|
}
|
|
@@ -262,7 +262,7 @@ html > body[data-scroll-locked] {
|
|
|
262
262
|
/* The caution colour is a TOKEN, declared as a light/dark pair on the root
|
|
263
263
|
rather than inside the one class that first used it. Every surface that has
|
|
264
264
|
to say "the record withdrew this" now spends the same colour — the banner
|
|
265
|
-
over a
|
|
265
|
+
over a deprecated document, and the chip that marks it in the sidebar, the
|
|
266
266
|
listings, the front door and search. Declared in both themes deliberately:
|
|
267
267
|
a colour written only on `:root` leaks into dark, because `:root` and
|
|
268
268
|
`.dark` have equal specificity and whichever comes later wins. */
|
|
@@ -489,10 +489,10 @@ html > body[data-scroll-locked] {
|
|
|
489
489
|
}
|
|
490
490
|
|
|
491
491
|
/* A withdrawn document's status chip, wherever one renders. A `draft` chip and
|
|
492
|
-
a `
|
|
492
|
+
a `deprecated` chip were pixel-identical — the same hairline border and the
|
|
493
493
|
same muted text — so the two statuses that mean the most different things
|
|
494
494
|
looked the same at the moment a reader picks between two documents. The
|
|
495
|
-
colour is additive, never the whole signal: the word "
|
|
495
|
+
colour is additive, never the whole signal: the word "deprecated" is beside
|
|
496
496
|
it in every one of these places (GOV.UK's rule — never rely on colour alone).
|
|
497
497
|
Unlayered, so it beats the Tailwind utility that sets the chip's border and
|
|
498
498
|
text colour; the banner class above has always worked this way. */
|
|
@@ -519,8 +519,16 @@ html > body[data-scroll-locked] {
|
|
|
519
519
|
}
|
|
520
520
|
|
|
521
521
|
/* …and in search too, where a reader is choosing between results. The chip is
|
|
522
|
-
drawn by `content`, so the tone has to be set on the same pseudo-element.
|
|
523
|
-
|
|
522
|
+
drawn by `content`, so the tone has to be set on the same pseudo-element.
|
|
523
|
+
|
|
524
|
+
found live 2026-08-25: this selector still read `superseded`, the PRE-PROFILE
|
|
525
|
+
status word. Under the profile the withdrawn state is `deprecated` (record
|
|
526
|
+
spec §2.2), so it matched nothing and a withdrawn document's search row wore
|
|
527
|
+
the ordinary grey chip — on the one surface whose snippet quotes the
|
|
528
|
+
withdrawn figure, which is why the rule exists. The value here is
|
|
529
|
+
`lifecycleBadge`'s, not `status`'s; the two agree on this word and the badge
|
|
530
|
+
is what the map carries. */
|
|
531
|
+
[data-ksor-status="deprecated"]::after {
|
|
524
532
|
border-color: color-mix(in oklab, var(--ksor-caution) 45%, transparent);
|
|
525
533
|
color: var(--ksor-caution);
|
|
526
534
|
}
|
|
@@ -2,7 +2,8 @@ import { RootProvider } from "fumadocs-ui/provider/next";
|
|
|
2
2
|
import "./global.css";
|
|
3
3
|
import type { Metadata } from "next";
|
|
4
4
|
import { appTitle } from "@/lib/shared";
|
|
5
|
-
import { basePath,
|
|
5
|
+
import { basePath, badgeByUrl } from "@/lib/source";
|
|
6
|
+
import { readStageManifest } from "@/lib/stage-manifest";
|
|
6
7
|
import KsorSearchDialog from "@/components/search-dialog";
|
|
7
8
|
|
|
8
9
|
// No next/font/google: it fetches the face from Google at BUILD time, so a
|
|
@@ -17,13 +18,17 @@ export const metadata: Metadata = {
|
|
|
17
18
|
template: `%s | ${appTitle}`,
|
|
18
19
|
},
|
|
19
20
|
description: "The Knowledge System of Record for humans and AI agents.",
|
|
21
|
+
// A build that shows drafts (`KSOR_DRAFTS=show`) is a preview, and a static
|
|
22
|
+
// site's pages are open-web artefacts: it says so to every crawler rather
|
|
23
|
+
// than letting a draft be indexed under the record's name (build spec §3).
|
|
24
|
+
...(readStageManifest().drafts === "shown" ? { robots: { index: false, follow: false } } : {}),
|
|
20
25
|
};
|
|
21
26
|
|
|
22
27
|
export default function Layout({ children }: LayoutProps<"/">) {
|
|
23
28
|
return (
|
|
24
29
|
<html lang="en" suppressHydrationWarning>
|
|
25
30
|
<body className="flex flex-col min-h-screen">
|
|
26
|
-
{/* Which documents carry a
|
|
31
|
+
{/* Which documents carry a badge, for the search dialog — it
|
|
27
32
|
runs in the browser over a static index that has no field for it.
|
|
28
33
|
Delivered in the document rather than as a dialog prop because
|
|
29
34
|
RootProvider types `options` against the SHIPPED dialog's props, and
|
|
@@ -34,7 +39,7 @@ export default function Layout({ children }: LayoutProps<"/">) {
|
|
|
34
39
|
type="application/json"
|
|
35
40
|
id="ksor-statuses"
|
|
36
41
|
dangerouslySetInnerHTML={{
|
|
37
|
-
__html: JSON.stringify(
|
|
42
|
+
__html: JSON.stringify(badgeByUrl()).replaceAll("<", "\\u003c"),
|
|
38
43
|
}}
|
|
39
44
|
/>
|
|
40
45
|
<RootProvider
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { getLLMText,
|
|
1
|
+
import { getLLMText, getMachinePages } from "@/lib/source";
|
|
2
|
+
import { stampLines } from "@/lib/governance";
|
|
3
|
+
import { appName, appTitle } from "@/lib/shared";
|
|
4
|
+
import { readStageManifest } from "@/lib/stage-manifest";
|
|
2
5
|
|
|
3
6
|
export const revalidate = false;
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The whole machine-admitted record in one file, opened by the stamps that
|
|
10
|
+
* connect it to one publication (R14) — so a consumer holding a copy can say
|
|
11
|
+
* which build it came from — then every document with its own governance
|
|
12
|
+
* block, exactly as its twin serves it.
|
|
13
|
+
*/
|
|
5
14
|
export async function GET(): Promise<Response> {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const scanned = await Promise.all(pages.map((page) => getLLMText(page, pages)));
|
|
10
|
-
|
|
11
|
-
return new Response(scanned.join("\n\n"));
|
|
15
|
+
const head = `---\ntitle: ${JSON.stringify(appTitle)}\nname: ${appName}\n${stampLines(readStageManifest().stamps).join("\n")}\n---`;
|
|
16
|
+
const scanned = getMachinePages().map((page) => getLLMText(page));
|
|
17
|
+
return new Response([head, ...scanned].join("\n\n"));
|
|
12
18
|
}
|
|
@@ -3,15 +3,20 @@ import { recordIndexText } from "@/lib/source";
|
|
|
3
3
|
export const revalidate = false;
|
|
4
4
|
|
|
5
5
|
// The agent-facing index of the record: this instance's name, then every
|
|
6
|
-
// document in sidebar order, each link usable
|
|
7
|
-
//
|
|
6
|
+
// document a MACHINE surface may carry, in sidebar order, each link usable
|
|
7
|
+
// as-is on a sub-path host.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// There is no caveat marker any more, because there is nothing to mark: the
|
|
10
|
+
// §2.5 table admits only stable, effective, unexpired, undenied concepts here,
|
|
11
|
+
// so a draft, a deprecated document and one past its `stale_after` are not
|
|
12
|
+
// entries at all. That replaced marking with exclusion — the older worry was a
|
|
13
|
+
// withdrawn document and its replacement sitting adjacent, told apart only by
|
|
14
|
+
// whatever a human typed into a title (research/site-design.md F1), and an
|
|
15
|
+
// agent picking either.
|
|
12
16
|
//
|
|
13
|
-
// The
|
|
14
|
-
//
|
|
17
|
+
// The set is decided ONCE, by staging, and read back here: the bytes are built
|
|
18
|
+
// in lib/source (`recordIndexText`) because the home page shows this same index
|
|
19
|
+
// to a reader — one index, one spelling.
|
|
15
20
|
export function GET(): Response {
|
|
16
21
|
return new Response(recordIndexText());
|
|
17
22
|
}
|
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { notFound } from "next/navigation";
|
|
2
2
|
|
|
3
|
-
import { getLLMText,
|
|
3
|
+
import { getLLMText, getMachinePages, rootIndexTwin, source } from "@/lib/source";
|
|
4
|
+
import { machineAdmits } from "@/lib/stage-manifest";
|
|
4
5
|
|
|
5
6
|
export const revalidate = false;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Every document as markdown, at a stable address derived
|
|
9
|
-
* `/docs/policies/purchase-approval` is also served at
|
|
10
|
-
* `/md/policies/purchase-approval.md
|
|
9
|
+
* Every MACHINE-admitted document as markdown, at a stable address derived
|
|
10
|
+
* from its path: `/docs/policies/purchase-approval` is also served at
|
|
11
|
+
* `/md/policies/purchase-approval.md`, and the record's own index at
|
|
12
|
+
* `/md/index.md` — the one index with a twin (build spec §3).
|
|
11
13
|
*
|
|
12
14
|
* The record already IS markdown; without this an agent handed a document URL
|
|
13
15
|
* had to scrape a React app to reach text the record holds verbatim
|
|
14
|
-
* (research/site-design.md F2). The body carries the document's governance
|
|
15
|
-
* frontmatter, exactly as `llms-full.txt` does
|
|
16
|
-
* document knows its status, owner, sources and successor.
|
|
16
|
+
* (research/site-design.md F2). The body carries the document's governance and
|
|
17
|
+
* the build's stamps as frontmatter, exactly as `llms-full.txt` does.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* A page the machine surfaces decline — a draft, a deprecated, a not-yet-
|
|
20
|
+
* effective or a stale concept (record spec §2.5) — has no twin at all: the
|
|
21
|
+
* decision is staging's, read back here, never re-derived.
|
|
22
|
+
*
|
|
23
|
+
* Why a `/md/` prefix rather than appending `.md` to the document's own URL:
|
|
24
|
+
* under `output: "export"` a Route Handler cannot share a route segment with a
|
|
25
|
+
* Page, and there is no middleware to rewrite one onto the other. The prefix
|
|
22
26
|
* survives a static host, and the page advertises it in a `rel="alternate"`
|
|
23
|
-
* link so a consumer discovers it rather than guessing.
|
|
24
|
-
* canonical URL becomes possible the day a build emits these artifacts itself.
|
|
27
|
+
* link so a consumer discovers it rather than guessing.
|
|
25
28
|
*/
|
|
26
29
|
export function generateStaticParams(): { slug: string[] }[] {
|
|
27
|
-
|
|
28
|
-
const segments =
|
|
29
|
-
const last = segments.at(-1);
|
|
30
|
-
return {
|
|
31
|
-
slug: last === undefined ? ["index.md"] : [...segments.slice(0, -1), `${last}.md`],
|
|
32
|
-
};
|
|
30
|
+
const twins = getMachinePages().map((page) => {
|
|
31
|
+
const segments = page.url.replace(/^\/docs\/?/, "").split("/");
|
|
32
|
+
const last = segments.at(-1) ?? "";
|
|
33
|
+
return { slug: [...segments.slice(0, -1), `${last}.md`] };
|
|
33
34
|
});
|
|
35
|
+
return [{ slug: ["index.md"] }, ...twins];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export async function GET(
|
|
@@ -40,12 +42,11 @@ export async function GET(
|
|
|
40
42
|
const { slug = [] } = await params;
|
|
41
43
|
const last = slug.at(-1);
|
|
42
44
|
if (last === undefined || !last.endsWith(".md")) notFound();
|
|
43
|
-
const
|
|
45
|
+
const headers = { "content-type": "text/markdown; charset=utf-8" };
|
|
46
|
+
if (slug.length === 1 && last === "index.md") return new Response(rootIndexTwin(), { headers });
|
|
44
47
|
|
|
45
|
-
const page = source.getPage(
|
|
46
|
-
if (!page) notFound();
|
|
48
|
+
const page = source.getPage([...slug.slice(0, -1), last.slice(0, -".md".length)]);
|
|
49
|
+
if (!page || !machineAdmits(page.path)) notFound();
|
|
47
50
|
|
|
48
|
-
return new Response(
|
|
49
|
-
headers: { "content-type": "text/markdown; charset=utf-8" },
|
|
50
|
-
});
|
|
51
|
+
return new Response(getLLMText(page), { headers });
|
|
51
52
|
}
|