@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +501 -0
  2. package/dist/cli.mjs +99 -19
  3. package/docs/authorization.md +197 -0
  4. package/docs/index.md +4 -0
  5. package/package.json +1 -1
  6. package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
  7. package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
  8. package/templates/scaffold/AGENTS.md +52 -4
  9. package/templates/scaffold/instance.md +28 -20
  10. package/templates/scaffold/knowledge/governance-ladder.md +36 -0
  11. package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
  12. package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
  13. package/templates/scaffold/knowledge/surfaces/index.md +21 -0
  14. package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
  15. package/templates/scaffold/pnpm-lock.yaml +1198 -228
  16. package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
  17. package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
  18. package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
  19. package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
  20. package/templates/scaffold/system/site/app/global.css +552 -9
  21. package/templates/scaffold/system/site/app/layout.tsx +23 -4
  22. package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
  23. package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
  24. package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
  25. package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
  26. package/templates/scaffold/system/site/components/governance.tsx +262 -0
  27. package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
  28. package/templates/scaffold/system/site/components/record-index.tsx +120 -0
  29. package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
  30. package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
  31. package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
  32. package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
  33. package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
  34. package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
  35. package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
  36. package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
  37. package/templates/scaffold/system/site/components.json +25 -0
  38. package/templates/scaffold/system/site/lib/governance.ts +432 -0
  39. package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
  40. package/templates/scaffold/system/site/lib/shared.ts +38 -0
  41. package/templates/scaffold/system/site/lib/source.ts +221 -5
  42. package/templates/scaffold/system/site/lib/utils.ts +6 -0
  43. package/templates/scaffold/system/site/package.json +9 -3
  44. package/templates/scaffold/knowledge/example.md +0 -23
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/global.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
@@ -0,0 +1,432 @@
1
+ // What the record says about a document, projected for rendering.
2
+ //
3
+ // `knowledge/` documents carry a governance vocabulary that `pnpm check`
4
+ // enforces — status, owner, provenance, effective, superseded_by — and until
5
+ // this module existed the site parsed four of those keys and threw them away.
6
+ // Provenance is load-bearing: a reader has to be able to see who stands behind
7
+ // a document and where it came from, or the site is showing them prose while
8
+ // the agent surface answers with citations.
9
+ //
10
+ // Import-free on purpose: this is the pure half, so it is unit-tested directly
11
+ // (packages/ksor/src/site-governance.test.ts) without a site install. Anything
12
+ // needing the Fumadocs loader — resolving a successor pointer to its route —
13
+ // lives outside it.
14
+ //
15
+ // Contract: specs/ksor/site-governance/spec.md
16
+
17
+ export interface DocumentGovernance {
18
+ /** `draft` | `review` | `approved` | `superseded`. Required by the checker; null only when a document skipped it. */
19
+ readonly status: string | null;
20
+ /** Who stands behind this document. */
21
+ readonly owner: string | null;
22
+ /** One entry per source — a citation must be able to point at exactly one of them. */
23
+ readonly provenance: readonly string[];
24
+ /** When it took effect, as an ISO date. */
25
+ readonly effective: string | null;
26
+ /** The successor's pointer as the document declares it, e.g. `./refund-policy-v5.md`. */
27
+ readonly supersededBy: string | null;
28
+ }
29
+
30
+ /**
31
+ * A declared value, or null. Blank and whitespace-only count as undeclared: a
32
+ * key an author started and left empty is not a governance fact.
33
+ *
34
+ * An unquoted `effective: 2026-04-01` parses to a Date, so dates normalize to
35
+ * their ISO day here — rendering the object would print a locale- and
36
+ * timezone-dependent string into the record.
37
+ */
38
+ function declared(value: unknown): string | null {
39
+ // A bare `effective: 2026` types as a NUMBER in YAML and used to disappear
40
+ // from the page entirely — the record declared it and the page said nothing.
41
+ // `pnpm check` refuses it now; showing what the author wrote is still the
42
+ // honest fallback for a record that skipped the checker.
43
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : null;
44
+ if (value instanceof Date) {
45
+ return Number.isNaN(value.getTime()) ? null : (value.toISOString().split("T")[0] ?? null);
46
+ }
47
+ if (typeof value !== "string") return null;
48
+ const trimmed = value.trim();
49
+ return trimmed === "" ? null : trimmed;
50
+ }
51
+
52
+ /**
53
+ * The document's governance, exactly as it declares it.
54
+ *
55
+ * Nothing is inferred, defaulted or synthesized — an undeclared key yields
56
+ * null and renders nothing. A placeholder ("unknown", "none") would read as
57
+ * governed, which is worse than a gap the reader can see.
58
+ *
59
+ * `where` names the document in the one error this can raise.
60
+ */
61
+ export function readGovernance(data: unknown, where: string): DocumentGovernance {
62
+ // `unknown` rather than the loader's page-data type: the projection reads
63
+ // frontmatter, which is whatever the author wrote, and a shell that crashes
64
+ // on a shape the checker would have named is worse than one that renders
65
+ // what it can.
66
+ const record: Record<string, unknown> =
67
+ typeof data === "object" && data !== null ? (data as Record<string, unknown>) : {};
68
+
69
+ const status = declared(record["status"]);
70
+ const supersededBy = declared(record["superseded_by"]);
71
+
72
+ // Defense in depth: `pnpm check` refuses this, so reaching it means the
73
+ // adopter skipped the checker. Failing the build is the honest outcome —
74
+ // the alternative is serving a document that says it was replaced and
75
+ // cannot say by what.
76
+ if (status === "superseded" && supersededBy === null) {
77
+ throw new Error(
78
+ `${where} is status: superseded with no superseded_by — a document that says it was ` +
79
+ "replaced must say by what, or the reader is told to stop trusting it and given nowhere " +
80
+ "to go. Add superseded_by: ./<successor>.md (pnpm check refuses this too).",
81
+ );
82
+ }
83
+
84
+ // A scalar provenance is a checker finding, not a crash: turning one into an
85
+ // unexplained build failure hides the real message `pnpm check` would print.
86
+ const raw: unknown = record["provenance"];
87
+ const provenance = Array.isArray(raw)
88
+ ? raw.map(declared).filter((entry): entry is string => entry !== null)
89
+ : [];
90
+
91
+ return {
92
+ status,
93
+ owner: declared(record["owner"]),
94
+ provenance,
95
+ effective: declared(record["effective"]),
96
+ supersededBy,
97
+ };
98
+ }
99
+
100
+ /** One document as the loader reports it: its source path, and its route. */
101
+ export interface RecordPage {
102
+ /** Path under `knowledge/`, e.g. `legal.md` or `handbook/index.md`. */
103
+ readonly path: string;
104
+ /** The route it renders at, e.g. `/docs/legal`. */
105
+ readonly url: string;
106
+ }
107
+
108
+ /** Resolve a relative pointer against the directory holding `from`. */
109
+ function resolveFrom(from: string, relative: string): string {
110
+ // The source path is the authority, so it is normalized like one: Windows
111
+ // separators included (the loader reports whatever the filesystem gave it).
112
+ const segments = from.replaceAll("\\", "/").split("/").slice(0, -1);
113
+ for (const part of relative.split("/")) {
114
+ if (part === "" || part === ".") continue;
115
+ if (part === "..") segments.pop();
116
+ else segments.push(part);
117
+ }
118
+ return segments.join("/");
119
+ }
120
+
121
+ /**
122
+ * The route a `superseded_by` pointer names, or null.
123
+ *
124
+ * Resolved against the document's SOURCE PATH, never against its route. A
125
+ * route cannot tell `knowledge/legal.md` from `knowledge/handbook/index.md` —
126
+ * both render at one path segment — yet `./terms.md` means a sibling in the
127
+ * first and a folder child in the second. Resolving on routes had to guess,
128
+ * and refused to link a record the checker calls well-formed (found live,
129
+ * 2026-08-20: `legal.md` pointing at `./terms.md` beside a `legal/terms.md`
130
+ * rendered the raw pointer instead of a link).
131
+ *
132
+ * Null means the successor is not in THIS build — legitimate for a
133
+ * per-audience build, which stages a subset — and the caller then shows the
134
+ * pointer as text. A dead link on a supersession notice is the worst outcome:
135
+ * it tells the reader to stop trusting the page and then strands them.
136
+ */
137
+ export function resolveSuccessorUrl(
138
+ pointer: string,
139
+ currentPath: string,
140
+ pages: readonly RecordPage[],
141
+ ): string | null {
142
+ // Leaves the record: an absolute URL or a site-absolute path is not a
143
+ // pointer into knowledge/ at all.
144
+ if (pointer.includes("://") || pointer.startsWith("/")) return null;
145
+
146
+ const [target = "", anchor] = pointer.split("#", 2);
147
+ if (!target.endsWith(".md")) return null;
148
+
149
+ const resolved = resolveFrom(currentPath, target);
150
+ const match = pages.find((page) => page.path.replaceAll("\\", "/") === resolved);
151
+ if (match === undefined) return null;
152
+
153
+ return anchor === undefined ? match.url : `${match.url}#${anchor}`;
154
+ }
155
+
156
+ /**
157
+ * Whether a string is a real day on the calendar, not merely date-SHAPED.
158
+ *
159
+ * `2026-06-31` and `2026-13-45` both match a `\d{4}-\d{2}-\d{2}` shape, and
160
+ * stamping either into `<time datetime>` publishes a day that does not exist:
161
+ * invalid HTML, and a consumer parsing it gets July 1st. The record's checker
162
+ * refuses those unquoted and offers QUOTING as the escape hatch — which is
163
+ * exactly how one reaches this function (found 2026-08-21).
164
+ */
165
+ export function isCalendarDate(value: string): boolean {
166
+ const parts = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
167
+ if (parts === null) return false;
168
+ const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];
169
+ const probe = new Date(Date.UTC(year, month - 1, day));
170
+ return (
171
+ probe.getUTCFullYear() === year &&
172
+ probe.getUTCMonth() === month - 1 &&
173
+ probe.getUTCDate() === day
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Whether this record's site shows the governance it declares — the
179
+ * `site.governance` key in instance.md, default **on**:
180
+ *
181
+ * site:
182
+ * governance: false
183
+ *
184
+ * The record often wants `owner:` and `provenance:` filled in for the agent
185
+ * surface and the audit trail while the published page stays plain. That is a
186
+ * publication choice, so it belongs to the instance, not to each document —
187
+ * per-document control is already the frontmatter itself (declare a key and it
188
+ * shows; leave it off and nothing does).
189
+ *
190
+ * Default on, and additive: every record written before this key existed keeps
191
+ * rendering exactly as it did. Turning it off never hides the SUPERSESSION
192
+ * notice — that is a correctness warning, not decoration, and a reader handed a
193
+ * replaced document with no word of its successor has been misled.
194
+ *
195
+ * Takes the frontmatter block (not a path) so it stays pure and testable; the
196
+ * site binds it once in lib/shared.ts.
197
+ */
198
+ export function governanceVisible(instanceFrontmatterBlock: string): boolean {
199
+ const lines = instanceFrontmatterBlock.split("\n");
200
+ const start = lines.findIndex((line) => /^site[ \t]*:[ \t]*(?:#.*)?$/.test(line));
201
+ if (start === -1) {
202
+ // A flow mapping (`site: { governance: false }`) is a scalar to every
203
+ // reader of this block, so a block-only scan would fall through to the
204
+ // default and publish what the owner turned off. `pnpm check` refuses the
205
+ // shape; refuse it here too rather than default past it silently.
206
+ const inline = lines.find((line) => /^site[ \t]*:[ \t]*\S/.test(line));
207
+ if (inline !== undefined) {
208
+ throw new Error(
209
+ `instance.md has ${JSON.stringify(inline.trim())} — a site: group written on one line is ` +
210
+ "not read as a group, so every key inside it is dropped without a word. Write it as an " +
211
+ "indented block:\n site:\n governance: false",
212
+ );
213
+ }
214
+ return true;
215
+ }
216
+
217
+ for (const line of lines.slice(start + 1)) {
218
+ if (line.trim() === "") continue;
219
+ // A non-indented line ends the block: a TOP-LEVEL `governance:` is a
220
+ // different key and must never be mistaken for this setting.
221
+ if (!/^[ \t]/.test(line)) break;
222
+ const match = /^[ \t]+governance[ \t]*:[ \t]*(.*)$/.exec(line);
223
+ if (match === null) continue;
224
+
225
+ const raw = (match[1] ?? "").trim();
226
+ // ` #` starts a YAML comment on an unquoted value (the grammar the
227
+ // audience model already follows).
228
+ const value = (/^["']/.test(raw) ? raw : raw.replace(/\s+#.*$/, ""))
229
+ .trim()
230
+ .replace(/^(['"])(.*)\1$/, "$2")
231
+ .toLowerCase();
232
+
233
+ if (value === "true") return true;
234
+ if (value === "false") return false;
235
+ throw new Error(
236
+ `instance.md site.governance is ${JSON.stringify(raw)} — it must be true or false. ` +
237
+ "Defaulting silently would publish the governance you asked to hide, or hide what you " +
238
+ "asked to publish. Write `governance: false` to keep the pages plain, or remove the key.",
239
+ );
240
+ }
241
+ return true;
242
+ }
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // The AGENT surface's projection of the same record.
246
+ //
247
+ // The page shows a superseded document under an unmissable notice; llms.txt and
248
+ // llms-full.txt used to serve that same document as ordinary prose — no status,
249
+ // no successor, no owner — so an agent answered from a policy the reader had
250
+ // been warned about, and could not know (measured on shipped bytes,
251
+ // research/site-design.md F1). Two surfaces, two truths, which product
252
+ // principle 2 forbids.
253
+ //
254
+ // Deliberately NOT gated on `site.governance`: that key decides what the PAGES
255
+ // publish. The record keeps every key for the agent surface and the audit trail
256
+ // (specs/ksor/site-governance/spec.md), so gating this on it would rebuild the
257
+ // defect above on purpose.
258
+
259
+ /** What a reader already assumes of a document in a system of record. */
260
+ const ASSUMED_STATUS = "approved";
261
+
262
+ /**
263
+ * The document's status when it is worth showing, or null.
264
+ *
265
+ * ONE definition, shared by every surface that shows a status — the page chip,
266
+ * the agent index, the record listings. `approved` is silent because that is
267
+ * what a reader already assumes of a document in a system of record, and a
268
+ * label that appears everywhere and always says the same thing trains people to
269
+ * skip it, including on the page where it mattered. An unrecognized state is
270
+ * passed through rather than swallowed: `pnpm check` holds status to a closed
271
+ * set, so reaching here with one means the record skipped the checker, and
272
+ * showing what it wrote beats hiding it.
273
+ */
274
+ /**
275
+ * The tone class a status chip wears, in every surface that renders one.
276
+ *
277
+ * Only a WITHDRAWN document gets a colour. `draft` and `review` are ordinary
278
+ * states of a live document; `superseded` says the record has replaced it, and
279
+ * before this the three were pixel-identical chips — the same hairline, the
280
+ * same muted text — at exactly the moment a reader picks between a document
281
+ * and its successor. One function rather than the ternary repeated at five
282
+ * call sites, so the rule cannot drift between the sidebar and search.
283
+ */
284
+ export function statusTone(status: string | null): string {
285
+ return status === "superseded" ? "ksor-withdrawn" : "";
286
+ }
287
+
288
+ export function caveatStatus(status: string | null): string | null {
289
+ return status === null || status === ASSUMED_STATUS ? null : status;
290
+ }
291
+
292
+ /**
293
+ * A YAML scalar a consumer can parse back to exactly what the record said. The
294
+ * shapes below are the ones the record's own checker refuses unquoted, for the
295
+ * same reason: unquoted, YAML reads them as something else.
296
+ */
297
+ function yamlScalar(value: string): string {
298
+ const risky =
299
+ value.includes(": ") ||
300
+ value.endsWith(":") ||
301
+ value.includes(" #") ||
302
+ /^[-?:,[\]{}#&*!|>'"%@`]/.test(value) ||
303
+ value !== value.trim();
304
+ return risky ? JSON.stringify(value) : value;
305
+ }
306
+
307
+ /**
308
+ * The suffix for one line of the compact index (`llms.txt`), or "" when the
309
+ * document carries no caveat.
310
+ *
311
+ * Caveats only, unlike the full block below: the index is one line per
312
+ * document, and a marker on every line is noise an agent learns to skip — the
313
+ * same argument that keeps the page's status chip rare.
314
+ *
315
+ * `successorUrl` is the successor's RESOLVED route (the caller owns
316
+ * resolution, including any base path), or null when it is not in this build —
317
+ * a per-audience build stages a subset. A missing successor never suppresses
318
+ * the SUPERSEDED marker: dropping the warning with the link would serve the
319
+ * withdrawn document looking clean, which is the whole defect.
320
+ */
321
+ export function agentIndexSuffix(
322
+ governance: DocumentGovernance,
323
+ successorUrl: string | null,
324
+ ): string {
325
+ const status = caveatStatus(governance.status);
326
+ if (status === null) return "";
327
+ const replaced =
328
+ status === "superseded" && successorUrl !== null ? `, replaced by ${successorUrl}` : "";
329
+ return ` — ${status.toUpperCase()}${replaced}`;
330
+ }
331
+
332
+ /**
333
+ * The governance block that precedes a document's body in `llms-full.txt`,
334
+ * written as frontmatter — the record's own grammar, so a consumer parses the
335
+ * corpus the way the corpus is authored.
336
+ *
337
+ * `status` is emitted even when it is `approved`, which is the opposite call to
338
+ * the page's. A reader assumes a document in a record is current; a consumer
339
+ * assumes nothing, and that silence is exactly what F1 was.
340
+ *
341
+ * Nothing is inferred: an undeclared key is absent, never an empty one, and a
342
+ * document declaring no governance at all yields "".
343
+ */
344
+ export function agentFrontmatter(
345
+ governance: DocumentGovernance,
346
+ successorUrl: string | null,
347
+ ): string {
348
+ const { status, owner, effective, supersededBy, provenance } = governance;
349
+ const lines: string[] = [];
350
+ if (status !== null) lines.push(`status: ${yamlScalar(status)}`);
351
+ if (owner !== null) lines.push(`owner: ${yamlScalar(owner)}`);
352
+ if (effective !== null) lines.push(`effective: ${yamlScalar(effective)}`);
353
+ // The resolved route, never the raw `./successor.md` pointer: a consumer that
354
+ // never sees the record's file tree cannot follow one.
355
+ if (status === "superseded" && supersededBy !== null) {
356
+ lines.push(`superseded_by: ${yamlScalar(successorUrl ?? supersededBy)}`);
357
+ }
358
+ if (provenance.length > 0) {
359
+ lines.push("provenance:");
360
+ for (const entry of provenance) lines.push(` - ${yamlScalar(entry)}`);
361
+ }
362
+ return lines.length === 0 ? "" : `---\n${lines.join("\n")}\n---\n`;
363
+ }
364
+
365
+ /**
366
+ * The href for a provenance entry that IS a source URL, or null when the entry
367
+ * is an ordinary citation.
368
+ *
369
+ * Deliberately narrow in two directions. The WHOLE entry must be the URL —
370
+ * linkifying a fragment inside "See https://x for the signed copy" would have
371
+ * to guess where the URL ends, and the citation is the entry, not the fragment.
372
+ * And only `http(s)` is accepted: `provenance` is AUTHORED content, so a
373
+ * `javascript:` or `data:` entry rendered into an href would let the record
374
+ * execute a script in the page that serves it. Other schemes (`mailto:`,
375
+ * `ftp:`) are refused as citations rather than links — widen only with a reason.
376
+ */
377
+ export function sourceHref(entry: string): string | null {
378
+ const value = entry.trim();
379
+ if (value === "" || /\s/.test(value)) return null;
380
+ let url: URL;
381
+ try {
382
+ url = new URL(value);
383
+ } catch {
384
+ return null;
385
+ }
386
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
387
+ // `new URL("https://")` throws, but a host-less shape that parses would
388
+ // render an href pointing nowhere.
389
+ return url.host === "" ? null : value;
390
+ }
391
+
392
+ /** A document's supersession pointer, as the record declares it. */
393
+ export interface SupersessionPointer {
394
+ /** Path under `knowledge/`, e.g. `policies/purchase-approval-2019.md`. */
395
+ readonly path: string;
396
+ /** Its `superseded_by:` value, or null. */
397
+ readonly supersededBy: string | null;
398
+ }
399
+
400
+ /**
401
+ * The routes of every document that names THIS page as its successor.
402
+ *
403
+ * Supersession ran one way: the withdrawn document names its replacement, and
404
+ * the replacement said nothing about what it replaced, so a reader on the
405
+ * current policy could not reach the history the record deliberately kept. RFC
406
+ * has carried both directions since 1969 (`Obsoletes:` on the new document,
407
+ * `Obsoleted by:` on the old).
408
+ *
409
+ * Derived, never declared — there is no new frontmatter key. Each pointer is
410
+ * resolved through the same path-relative rule the forward notice uses, so the
411
+ * two directions can never disagree about what points where.
412
+ */
413
+ export function predecessorsOf(
414
+ currentUrl: string,
415
+ pages: readonly RecordPage[],
416
+ pointers: readonly SupersessionPointer[],
417
+ ): string[] {
418
+ const byPath = new Map(pages.map((page) => [page.path.replaceAll("\\", "/"), page] as const));
419
+ const found: string[] = [];
420
+ for (const pointer of pointers) {
421
+ if (pointer.supersededBy === null) continue;
422
+ const resolved = resolveSuccessorUrl(pointer.supersededBy, pointer.path, pages);
423
+ if (resolved === null || resolved.split("#")[0] !== currentUrl) continue;
424
+ // A document that supersedes itself would otherwise render "Replaces: this
425
+ // page" on the page you are reading. `pnpm check` refuses it; this is the
426
+ // second lock.
427
+ const self = byPath.get(pointer.path.replaceAll("\\", "/"));
428
+ if (self !== undefined && self.url === currentUrl) continue;
429
+ if (self !== undefined) found.push(self.url);
430
+ }
431
+ return found;
432
+ }
@@ -8,7 +8,7 @@ export function baseOptions(): BaseLayoutProps {
8
8
  // Truncated: a long title forced horizontal scroll on mobile without it
9
9
  // (found live, 2026-08-18).
10
10
  title: (
11
- <span className="max-w-[60vw] truncate font-medium tracking-tight sm:max-w-none">
11
+ <span className="max-w-[60vw] truncate font-display text-[0.9375rem] font-semibold tracking-[-0.005em] sm:max-w-none">
12
12
  {appTitle}
13
13
  </span>
14
14
  ),
@@ -1,6 +1,8 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
+ import { governanceVisible } from "./governance";
5
+
4
6
  // The record's one identity source: instance.md's `name:` — the same file
5
7
  // every other shell reads, so renaming the instance renames every surface
6
8
  // at the next build, and no shell carries a baked-in copy (found live
@@ -59,6 +61,42 @@ function readInstanceTitle(): string {
59
61
 
60
62
  export const appTitle: string = readInstanceTitle();
61
63
 
64
+ /**
65
+ * Whether the pages show the governance each document declares
66
+ * (`site.governance` in instance.md, default on). Read once at build/server
67
+ * start, like the identity above — restart `pnpm dev` after changing it.
68
+ */
69
+ export const showGovernance: boolean = governanceVisible(instanceFrontmatter());
70
+
71
+ /**
72
+ * The record's own statement of what it is authoritative for: the first
73
+ * paragraph of instance.md's body, below the display title.
74
+ *
75
+ * The home page used to carry a line of ksor's marketing copy instead — the
76
+ * framework's voice above somebody else's knowledge, which the project's own
77
+ * critical rule 1 forbids. This is the record speaking for itself, and it is
78
+ * the same prose `ksor serve` hands the MCP server as its instructions, so the
79
+ * two surfaces open with one sentence.
80
+ *
81
+ * Null when the body says nothing yet; the page then renders nothing rather
82
+ * than inventing a purpose.
83
+ */
84
+ function readInstancePurpose(): string | null {
85
+ const text = readFileSync(findInstance(process.cwd()), "utf8");
86
+ const body = text.replace(/^?---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, "");
87
+ const afterTitle = body.replace(/^#[ \t]+.*$/m, "");
88
+ const paragraph = afterTitle
89
+ .split(/\n\s*\n/)
90
+ .map((block) => block.trim())
91
+ .find((block) => block !== "");
92
+ if (paragraph === undefined) return null;
93
+ // One paragraph, as one line: the body wraps at 80 columns for the file's
94
+ // sake, and those newlines are not sentence breaks.
95
+ return paragraph.replaceAll(/\s*\n\s*/g, " ").trim();
96
+ }
97
+
98
+ export const appPurpose: string | null = readInstancePurpose();
99
+
62
100
  /**
63
101
  * How this record describes ITSELF, in one line — what an agent reads in a
64
102
  * registry listing to decide whether this record can answer its question.