@panaversity/ksor 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +501 -0
- package/dist/cli.mjs +99 -19
- package/docs/authorization.md +197 -0
- package/docs/index.md +4 -0
- package/package.json +1 -1
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +232 -9
- package/templates/scaffold/AGENTS.md +52 -4
- package/templates/scaffold/instance.md +28 -20
- package/templates/scaffold/knowledge/governance-ladder.md +36 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +29 -0
- package/templates/scaffold/knowledge/surfaces/for-people.md +35 -0
- package/templates/scaffold/knowledge/surfaces/index.md +21 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +39 -0
- package/templates/scaffold/pnpm-lock.yaml +1198 -228
- package/templates/scaffold/system/site/app/(home)/layout.tsx +6 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +65 -70
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +122 -14
- package/templates/scaffold/system/site/app/docs/layout.tsx +2 -21
- package/templates/scaffold/system/site/app/global.css +552 -9
- package/templates/scaffold/system/site/app/layout.tsx +23 -4
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +4 -2
- package/templates/scaffold/system/site/app/llms.txt/route.ts +11 -9
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +51 -0
- package/templates/scaffold/system/site/components/copy-markdown.tsx +70 -0
- package/templates/scaffold/system/site/components/governance.tsx +262 -0
- package/templates/scaffold/system/site/components/home-cover.tsx +137 -0
- package/templates/scaffold/system/site/components/record-index.tsx +120 -0
- package/templates/scaffold/system/site/components/record-shell.tsx +68 -0
- package/templates/scaffold/system/site/components/record-stack.tsx +131 -0
- package/templates/scaffold/system/site/components/record-toc.tsx +160 -0
- package/templates/scaffold/system/site/components/search-dialog.tsx +130 -0
- package/templates/scaffold/system/site/components/sidebar-status.tsx +35 -0
- package/templates/scaffold/system/site/components/ui/badge.tsx +46 -0
- package/templates/scaffold/system/site/components/ui/button.tsx +62 -0
- package/templates/scaffold/system/site/components/ui/separator.tsx +28 -0
- package/templates/scaffold/system/site/components.json +25 -0
- package/templates/scaffold/system/site/lib/governance.ts +432 -0
- package/templates/scaffold/system/site/lib/layout.shared.tsx +1 -1
- package/templates/scaffold/system/site/lib/shared.ts +38 -0
- package/templates/scaffold/system/site/lib/source.ts +221 -5
- package/templates/scaffold/system/site/lib/utils.ts +6 -0
- package/templates/scaffold/system/site/package.json +9 -3
- package/templates/scaffold/knowledge/example.md +0 -23
|
@@ -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-
|
|
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.
|