@panaversity/ksor 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +965 -0
- package/README.md +29 -9
- package/dist/checker/check-main.mjs +14049 -0
- package/dist/cli.mjs +11528 -5213
- package/dist/gateway-api-CF4ED9_g-BQusM_dK.mjs +10895 -0
- package/dist/gateway.d.mts +52 -13
- package/dist/gateway.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-pl4aOpVs.mjs → src-dqpI-p1a.mjs} +1 -0
- package/docs/authorization.md +8 -6
- package/docs/deploying.md +36 -25
- package/docs/index.md +26 -13
- package/docs/ingesting.md +70 -22
- package/docs/tool-surface.md +69 -16
- package/package.json +4 -3
- package/schema/migrations/2.4-2.5__okf-profile.sql +114 -0
- package/schema/schema.sql +77 -14
- package/templates/scaffold/.agents/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.agents/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.agents/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.agents/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.agents/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.agents/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.claude/skills/add-sources/SKILL.md +63 -18
- package/templates/scaffold/.claude/skills/format-checker/SKILL.md +42 -33
- package/templates/scaffold/.claude/skills/format-checker/check.mjs +13827 -1314
- package/templates/scaffold/.claude/skills/intake-interview/SKILL.md +65 -27
- package/templates/scaffold/.claude/skills/make-slides/SKILL.md +7 -5
- package/templates/scaffold/.claude/skills/make-summary/SKILL.md +13 -6
- package/templates/scaffold/.github/workflows/validate.yml +9 -1
- package/templates/scaffold/.ksor/governance.yaml +17 -0
- package/templates/scaffold/AGENTS.md +293 -119
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +37 -6
- package/templates/scaffold/gitignore +14 -6
- package/templates/scaffold/instance.md +21 -17
- package/templates/scaffold/knowledge/governance-ladder.md +11 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +13 -6
- package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
- package/templates/scaffold/knowledge/surfaces/index.md +4 -20
- package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
- package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
- package/templates/scaffold/package.json +3 -4
- package/templates/scaffold/pnpm-lock.yaml +3 -0
- package/templates/scaffold/system/gateways/content.ts +13 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
- package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +134 -91
- package/templates/scaffold/system/site/app/global.css +198 -6
- package/templates/scaffold/system/site/app/layout.tsx +8 -3
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
- package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
- package/templates/scaffold/system/site/components/code-block.tsx +87 -0
- package/templates/scaffold/system/site/components/embed.tsx +276 -0
- package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +219 -87
- package/templates/scaffold/system/site/components/mdx.tsx +10 -2
- package/templates/scaffold/system/site/components/record-index.tsx +5 -5
- package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
- package/templates/scaffold/system/site/components/record-views.tsx +6 -17
- package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
- package/templates/scaffold/system/site/lib/alert-rule.ts +214 -0
- package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
- package/templates/scaffold/system/site/lib/attachments.ts +0 -28
- package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
- package/templates/scaffold/system/site/lib/audience.ts +42 -146
- package/templates/scaffold/system/site/lib/embed-rule.ts +255 -0
- package/templates/scaffold/system/site/lib/governance.ts +339 -225
- package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
- package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
- package/templates/scaffold/system/site/lib/lock.ts +282 -0
- package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
- package/templates/scaffold/system/site/lib/record-href.ts +68 -0
- package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
- package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
- package/templates/scaffold/system/site/lib/shared.ts +67 -104
- package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
- package/templates/scaffold/system/site/lib/source.ts +256 -186
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +638 -467
- package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
- package/templates/scaffold/system/site/package.json +1 -0
- package/templates/scaffold/system/site/record/actor.ts +23 -0
- package/templates/scaffold/system/site/record/check.ts +571 -0
- package/templates/scaffold/system/site/record/citations.ts +312 -0
- package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
- package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
- package/templates/scaffold/system/site/record/hygiene.ts +320 -0
- package/templates/scaffold/system/site/record/index-file.ts +150 -0
- package/templates/scaffold/system/site/record/index.ts +103 -0
- package/templates/scaffold/system/site/record/instance.ts +257 -0
- package/templates/scaffold/system/site/record/instant.ts +43 -0
- package/templates/scaffold/system/site/record/ledger.ts +694 -0
- package/templates/scaffold/system/site/record/load.ts +129 -0
- package/templates/scaffold/system/site/record/lock.ts +306 -0
- package/templates/scaffold/system/site/record/near-miss.ts +37 -0
- package/templates/scaffold/system/site/record/policy.ts +414 -0
- package/templates/scaffold/system/site/record/profile.ts +535 -0
- package/templates/scaffold/system/site/record/refusal.ts +106 -0
- package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
- package/templates/scaffold/system/site/source.config.ts +109 -23
- package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
- package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
- package/templates/scaffold/system/site/lib/page-order.ts +0 -93
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
|
|
3
3
|
import { DocumentActions } from "@/components/document-actions";
|
|
4
|
+
import { Clock } from "lucide-react";
|
|
4
5
|
import type { ReactElement } from "react";
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
badgeAddsToStatus,
|
|
9
|
+
badgeText,
|
|
10
|
+
badgeTone,
|
|
11
|
+
dayOf,
|
|
9
12
|
isCalendarDate,
|
|
13
|
+
plainBadge,
|
|
10
14
|
sourceHref,
|
|
15
|
+
statusLabel,
|
|
16
|
+
statusTone,
|
|
17
|
+
trustSignal,
|
|
11
18
|
type DocumentGovernance,
|
|
12
19
|
} from "@/lib/governance";
|
|
20
|
+
import type { LifecycleBadge } from "@/lib/lifecycle-rule";
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* What the record says about the document you are reading.
|
|
16
24
|
*
|
|
17
25
|
* The site renders the record; these render the record's governance — the
|
|
18
|
-
* frontmatter `pnpm check` enforces on every
|
|
26
|
+
* profile's frontmatter `pnpm check` enforces on every concept. Nothing here is
|
|
19
27
|
* authored in the site (critical rule 1) and nothing is inferred: a key the
|
|
20
28
|
* document does not declare renders nothing at all.
|
|
21
29
|
*
|
|
@@ -26,9 +34,9 @@ import {
|
|
|
26
34
|
*/
|
|
27
35
|
|
|
28
36
|
/**
|
|
29
|
-
* The successor of a
|
|
30
|
-
* `href` is null when the pointer could not be resolved to a route
|
|
31
|
-
* the pointer itself is shown as text rather than as a dead link.
|
|
37
|
+
* The successor of a deprecated document: its route and the text naming it.
|
|
38
|
+
* `href` is null when the pointer could not be resolved to a route in this
|
|
39
|
+
* build, and then the pointer itself is shown as text rather than as a dead link.
|
|
32
40
|
*/
|
|
33
41
|
export interface Successor {
|
|
34
42
|
readonly href: string | null;
|
|
@@ -36,11 +44,12 @@ export interface Successor {
|
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
/**
|
|
39
|
-
* The
|
|
47
|
+
* The deprecation notice. Deliberately the first thing on the page, above the
|
|
40
48
|
* title: a reader must not have to notice a subtle badge to learn that what
|
|
41
|
-
* they are about to read has been
|
|
49
|
+
* they are about to read has been withdrawn. `successor` is null when the
|
|
50
|
+
* record deprecated the document without naming a replacement.
|
|
42
51
|
*/
|
|
43
|
-
export function
|
|
52
|
+
export function DeprecatedNotice({ successor }: { successor: Successor | null }): ReactElement {
|
|
44
53
|
return (
|
|
45
54
|
<aside
|
|
46
55
|
// A landmark, not a note: GOV.UK ships this as role="region" with
|
|
@@ -48,7 +57,7 @@ export function SupersededNotice({ successor }: { successor: Successor }): React
|
|
|
48
57
|
// thing on the page by landmark, rather than only meeting it in reading
|
|
49
58
|
// order. role="note" is announced but not navigable.
|
|
50
59
|
role="region"
|
|
51
|
-
aria-labelledby="ksor-
|
|
60
|
+
aria-labelledby="ksor-deprecated"
|
|
52
61
|
// Tinted and ruled down the left edge in the CAUTION role, never
|
|
53
62
|
// --color-fd-muted: the shipped light theme defines --color-fd-muted and
|
|
54
63
|
// --color-fd-background as the same value, so the callout composited to
|
|
@@ -56,22 +65,29 @@ export function SupersededNotice({ successor }: { successor: Successor }): React
|
|
|
56
65
|
// Chromium, 2026-08-20). The one thing this notice cannot be is missable.
|
|
57
66
|
className="ksor-caution mb-8 rounded-lg border border-l-4 px-4 py-3 text-sm"
|
|
58
67
|
>
|
|
59
|
-
<p id="ksor-
|
|
60
|
-
|
|
68
|
+
<p id="ksor-deprecated" className="font-medium text-fd-foreground">
|
|
69
|
+
Deprecated
|
|
61
70
|
</p>
|
|
62
71
|
<p className="mt-1 text-fd-muted-foreground">
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<code className="break-words text-fd-foreground">{successor.label}</code>
|
|
72
|
+
{successor === null ? (
|
|
73
|
+
"The record has withdrawn this document."
|
|
66
74
|
) : (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
<>
|
|
76
|
+
This document has been replaced by{" "}
|
|
77
|
+
{successor.href === null ? (
|
|
78
|
+
<code className="break-words text-fd-foreground">{successor.label}</code>
|
|
79
|
+
) : (
|
|
80
|
+
<Link
|
|
81
|
+
href={successor.href}
|
|
82
|
+
className="font-medium text-fd-foreground underline underline-offset-4 transition-colors hover:text-fd-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
|
|
83
|
+
>
|
|
84
|
+
{successor.label}
|
|
85
|
+
</Link>
|
|
86
|
+
)}
|
|
87
|
+
.
|
|
88
|
+
</>
|
|
89
|
+
)}{" "}
|
|
90
|
+
It is kept because the record never deletes what it replaces.
|
|
75
91
|
</p>
|
|
76
92
|
</aside>
|
|
77
93
|
);
|
|
@@ -88,13 +104,10 @@ function Fact({
|
|
|
88
104
|
<div className="flex items-baseline gap-2.5">
|
|
89
105
|
{/* Mono, uppercase, letterspaced: these are the record's checkable facts,
|
|
90
106
|
and they read as a register's column heads rather than as a form.
|
|
91
|
-
|
|
92
107
|
The label is deliberately SMALLER and more letterspaced than the value
|
|
93
|
-
it introduces
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(reported by the owner, 2026-08-22, quoting the run back verbatim).
|
|
97
|
-
The step is now 10px against 13px, and the value carries the weight. */}
|
|
108
|
+
it introduces (10px against 13px), so "Owner Product Effective
|
|
109
|
+
2026-08-22" reads as two facts with names rather than one mono run
|
|
110
|
+
(reported by the owner, 2026-08-22). */}
|
|
98
111
|
<dt className="font-mono text-[0.625rem] tracking-[0.18em] text-fd-muted-foreground uppercase">
|
|
99
112
|
{label}
|
|
100
113
|
</dt>
|
|
@@ -105,44 +118,163 @@ function Fact({
|
|
|
105
118
|
);
|
|
106
119
|
}
|
|
107
120
|
|
|
121
|
+
/** One chip: a word the record says about this document, in the register's voice. */
|
|
122
|
+
function Chip({ text, tone = "" }: { text: string; tone?: string }): ReactElement {
|
|
123
|
+
return (
|
|
124
|
+
<span
|
|
125
|
+
className={`rounded-sm border border-fd-border px-1.5 py-0.5 tracking-widest whitespace-nowrap uppercase ${tone}`}
|
|
126
|
+
>
|
|
127
|
+
{text}
|
|
128
|
+
</span>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The chip a badge wears, in every listing and on the page. */
|
|
133
|
+
export function BadgeChip({
|
|
134
|
+
badge,
|
|
135
|
+
effectiveFrom = null,
|
|
136
|
+
}: {
|
|
137
|
+
badge: LifecycleBadge;
|
|
138
|
+
/** `ksor.effective_from`, which fills record spec §2.5's "effective from …". */
|
|
139
|
+
effectiveFrom?: string | null;
|
|
140
|
+
}): ReactElement {
|
|
141
|
+
return <Chip text={badgeText(badge, effectiveFrom) ?? ""} tone={badgeTone(badge)} />;
|
|
142
|
+
}
|
|
143
|
+
|
|
108
144
|
/**
|
|
109
|
-
* The
|
|
110
|
-
*
|
|
145
|
+
* The lifecycle caveat ALONE, for a page with no governance strip under its
|
|
146
|
+
* title — a record whose `site.governance` is off.
|
|
147
|
+
*
|
|
148
|
+
* That key turns off attribution, not caveats: the deprecation notice has
|
|
149
|
+
* always survived it, and the sidebar row, the folder card and the search
|
|
150
|
+
* result for this same document carry their badge whatever it says. The rule
|
|
151
|
+
* for which states reach here is `plainBadge`, beside the vocabulary itself.
|
|
152
|
+
*
|
|
153
|
+
* Typed like the strip's own values (mono, 13px), because a badge is the
|
|
154
|
+
* record speaking about a document and that is the voice it speaks in
|
|
155
|
+
* everywhere else.
|
|
156
|
+
*/
|
|
157
|
+
export function LifecycleCaveat({
|
|
158
|
+
badge,
|
|
159
|
+
effectiveFrom = null,
|
|
160
|
+
}: {
|
|
161
|
+
badge: LifecycleBadge | null;
|
|
162
|
+
effectiveFrom?: string | null;
|
|
163
|
+
}): ReactElement | null {
|
|
164
|
+
const caveat = plainBadge(badge);
|
|
165
|
+
if (caveat === null) return null;
|
|
166
|
+
return (
|
|
167
|
+
<div className="mb-7 font-mono text-[0.8125rem] font-medium text-fd-foreground">
|
|
168
|
+
<BadgeChip badge={caveat} effectiveFrom={effectiveFrom} />
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The one-line governance strip under the document's title: what the record
|
|
175
|
+
* says about this document, and who said it.
|
|
176
|
+
*
|
|
177
|
+
* Two kinds of thing sit here and they are drawn differently on purpose. The
|
|
178
|
+
* CHIPS are states — the lifecycle status, and the date badge when the calendar
|
|
179
|
+
* keeps an otherwise current document off the machine surfaces (record spec
|
|
180
|
+
* §2.5) — and the FACTS are attributions: who owns it, who approved it, who
|
|
181
|
+
* verified it and when. A reader deciding whether to act on a document needs
|
|
182
|
+
* both halves, and the predecessor's failure was showing neither.
|
|
111
183
|
*/
|
|
112
184
|
export function GovernanceMeta({
|
|
113
185
|
governance,
|
|
186
|
+
badge,
|
|
114
187
|
replaces = [],
|
|
115
188
|
markdownUrl,
|
|
189
|
+
minutes,
|
|
116
190
|
}: {
|
|
117
191
|
governance: DocumentGovernance;
|
|
192
|
+
/** Why the machine surfaces decline this page, or null. */
|
|
193
|
+
badge: LifecycleBadge | null;
|
|
118
194
|
/** Documents this one replaced — derived from the record, never declared. */
|
|
119
195
|
replaces?: readonly Successor[];
|
|
120
|
-
/** The document's markdown twin, offered beside its governance. */
|
|
196
|
+
/** The document's markdown twin, offered beside its governance — only where one exists. */
|
|
121
197
|
markdownUrl?: string;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
198
|
+
/**
|
|
199
|
+
* How long the document takes to read, when this row is the only place for
|
|
200
|
+
* it — a document with a summary shows it on that view's own strip instead,
|
|
201
|
+
* because there the number belongs to the view you picked.
|
|
202
|
+
*/
|
|
203
|
+
minutes?: number;
|
|
204
|
+
}): ReactElement {
|
|
205
|
+
const { status, owner, effectiveFrom, staleAfter, approval, deprecated } = governance;
|
|
206
|
+
const state = statusLabel(status);
|
|
207
|
+
// The badge is a SECOND chip only where it says something the status does
|
|
208
|
+
// not: `draft` and `deprecated` are both words, and printing either twice
|
|
209
|
+
// reads as two facts about one document.
|
|
210
|
+
const alsoBadge = badgeAddsToStatus(badge, status) ? badge : null;
|
|
211
|
+
const trust = trustSignal(governance.verified);
|
|
212
|
+
// …and where the badge carries the date, the fact beside it would repeat it.
|
|
213
|
+
const showEffective = effectiveFrom !== null && alsoBadge !== "effective-from";
|
|
127
214
|
|
|
215
|
+
// There is no "nothing to show" case any more: every concept has a trust
|
|
216
|
+
// tier, `unverified` included, and that is the whole point of printing it.
|
|
217
|
+
// The early return this replaced would have hidden the tier on exactly the
|
|
218
|
+
// documents whose tier is the only governance fact they have.
|
|
128
219
|
return (
|
|
129
220
|
<dl className="mb-7 flex flex-wrap items-baseline gap-x-8 gap-y-2.5 border-b border-fd-border pb-4">
|
|
130
|
-
{
|
|
221
|
+
{state === null && alsoBadge === null ? null : (
|
|
131
222
|
<Fact label="Status">
|
|
132
|
-
<span
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
223
|
+
<span className="flex flex-wrap items-baseline gap-1.5">
|
|
224
|
+
{state === null ? null : <Chip text={state} tone={statusTone(status)} />}
|
|
225
|
+
{alsoBadge === null ? null : (
|
|
226
|
+
<BadgeChip badge={alsoBadge} effectiveFrom={effectiveFrom} />
|
|
227
|
+
)}
|
|
136
228
|
</span>
|
|
137
229
|
</Fact>
|
|
138
230
|
)}
|
|
231
|
+
{/* The tier OKF's own vocabulary names, on every document including the
|
|
232
|
+
unverified ones — that is the honest state of a stable, approved
|
|
233
|
+
concept nobody has reviewed, and hiding it would leave a reader unable
|
|
234
|
+
to tell "checked" from "never mentioned" (research/okf-native.md
|
|
235
|
+
§1.1). Never a colour: a tier is a fact about review, not a warning. */}
|
|
236
|
+
<Fact label="Trust">
|
|
237
|
+
<span className="flex flex-wrap items-baseline gap-1.5">
|
|
238
|
+
<Chip text={trust.tier} />
|
|
239
|
+
{trust.by === null ? null : (
|
|
240
|
+
<span className="font-normal text-fd-muted-foreground">
|
|
241
|
+
{trust.by}
|
|
242
|
+
{trust.at === null ? null : <> · {day(trust.at)}</>}
|
|
243
|
+
</span>
|
|
244
|
+
)}
|
|
245
|
+
</span>
|
|
246
|
+
</Fact>
|
|
139
247
|
{owner === null ? null : <Fact label="Owner">{owner}</Fact>}
|
|
248
|
+
{/* Who let this into the record. `ksor.approval` is what makes a `stable`
|
|
249
|
+
document stable at all (record spec §2.2), so a page that showed the
|
|
250
|
+
word and not the signature would be publishing the claim without its
|
|
251
|
+
author. */}
|
|
252
|
+
{approval === null ? null : (
|
|
253
|
+
<Fact label="Approved">
|
|
254
|
+
<>
|
|
255
|
+
{approval.by} · {day(approval.at)}
|
|
256
|
+
</>
|
|
257
|
+
</Fact>
|
|
258
|
+
)}
|
|
259
|
+
{/* found live 2026-08-25: a deprecated page named its successor and said
|
|
260
|
+
nothing about WHO withdrew it, though `ksor.deprecated` is required on
|
|
261
|
+
every deprecated concept (record spec §2.2) and readGovernance already
|
|
262
|
+
refuses a document that omits it. Withdrawal is the most consequential
|
|
263
|
+
act in a document's life; publishing it unattributed is exactly the
|
|
264
|
+
gap the approver fact above closes at the other end. */}
|
|
265
|
+
{deprecated === null ? null : (
|
|
266
|
+
<Fact label="Withdrawn">
|
|
267
|
+
<>
|
|
268
|
+
{deprecated.by} · {day(deprecated.at)}
|
|
269
|
+
</>
|
|
270
|
+
</Fact>
|
|
271
|
+
)}
|
|
140
272
|
{replaces.length === 0 ? null : (
|
|
141
273
|
// The other half of a supersession. The withdrawn document names its
|
|
142
274
|
// successor above the title; this is the successor naming what it
|
|
143
275
|
// replaced, so the history the record kept is reachable from the
|
|
144
276
|
// current document instead of only from the retired one.
|
|
145
|
-
<Fact label=
|
|
277
|
+
<Fact label="Replaces">
|
|
146
278
|
<>
|
|
147
279
|
{replaces.map((entry, index) => (
|
|
148
280
|
<span key={entry.href ?? `${index}-${entry.label}`}>
|
|
@@ -162,77 +294,77 @@ export function GovernanceMeta({
|
|
|
162
294
|
</>
|
|
163
295
|
</Fact>
|
|
164
296
|
)}
|
|
165
|
-
{
|
|
166
|
-
<Fact label="Effective">
|
|
167
|
-
{/* The machine attribute is stamped only for a real day on the
|
|
168
|
-
calendar. A SHAPE test was not enough: the checker's own remedy
|
|
169
|
-
for `2026-06-31` is to QUOTE it, and quoted text arrives here — so
|
|
170
|
-
a shape test published `<time dateTime="2026-06-31">`, which is
|
|
171
|
-
invalid HTML and which a consumer reads as July 1st. That is the
|
|
172
|
-
precise hazard the record's date rule exists to prevent. */}
|
|
173
|
-
{isCalendarDate(effective) ? (
|
|
174
|
-
<time dateTime={effective}>{effective}</time>
|
|
175
|
-
) : (
|
|
176
|
-
<span>{effective}</span>
|
|
177
|
-
)}
|
|
178
|
-
</Fact>
|
|
297
|
+
{effectiveFrom === null || !showEffective ? null : (
|
|
298
|
+
<Fact label="Effective from">{day(effectiveFrom)}</Fact>
|
|
179
299
|
)}
|
|
300
|
+
{staleAfter === null ? null : <Fact label="Review by">{day(staleAfter)}</Fact>}
|
|
180
301
|
{markdownUrl === undefined ? null : (
|
|
181
302
|
// On the governance row, not as a footnote below the sources: it is
|
|
182
|
-
// how a reader hands this document to an agent
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// different acts, but two bare controls on a row of read-only facts
|
|
186
|
-
// made the row look half clickable. It wears neither the bordered badge
|
|
187
|
-
// (that means "a status the record declares") nor the accent at rest —
|
|
188
|
-
// the page had gone blue enough that the accent had stopped meaning
|
|
189
|
-
// anything (owner, 2026-08-22).
|
|
190
|
-
// RIGHT-ALIGNED (owner, and a reversal). The 2026-08-21 finding stands
|
|
191
|
-
// on its own terms: `ms-auto` once parked this 498px from the nearest
|
|
192
|
-
// thing on a two-fact document, where it read as belonging to nothing.
|
|
193
|
-
// What changed is the column beside it — the reading time now sits at
|
|
194
|
-
// the right end of the strip directly below, so the far edge is no
|
|
195
|
-
// longer empty space but a line the eye already follows. The two form
|
|
196
|
-
// a right-hand column of things you DO, against a left-hand row of
|
|
197
|
-
// things the record DECLARES.
|
|
303
|
+
// how a reader hands this document to an agent (research/site-design.md
|
|
304
|
+
// F2). Right-aligned: a column of things you DO, against a row of
|
|
305
|
+
// things the record DECLARES (owner, 2026-08-22).
|
|
198
306
|
<span className="ms-auto">
|
|
199
307
|
<DocumentActions href={markdownUrl} />
|
|
200
308
|
</span>
|
|
201
309
|
)}
|
|
310
|
+
{minutes === undefined ? null : (
|
|
311
|
+
<div className="ms-auto flex items-center gap-2 text-sm text-fd-muted-foreground">
|
|
312
|
+
<Clock aria-hidden className="size-3.5 shrink-0" />
|
|
313
|
+
<span>{minutes} min read</span>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
202
316
|
</dl>
|
|
203
317
|
);
|
|
204
318
|
}
|
|
205
319
|
|
|
320
|
+
/** An instant as a day, stamped as machine-readable only when it is a real one. */
|
|
321
|
+
function day(instant: string): ReactElement {
|
|
322
|
+
const value = dayOf(instant);
|
|
323
|
+
return isCalendarDate(value) ? <time dateTime={value}>{value}</time> : <span>{value}</span>;
|
|
324
|
+
}
|
|
325
|
+
|
|
206
326
|
/**
|
|
207
|
-
* Where the document came from
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
* of `provenance` being a list: a citation has to be able to point at exactly
|
|
211
|
-
* one of them. Rendering them as prose would take that away.
|
|
327
|
+
* Where the document came from: `sources`, one entry each — that is the whole
|
|
328
|
+
* point of it being a list: a footnote has to be able to point at exactly one
|
|
329
|
+
* of them. Rendering them as prose would take that away.
|
|
212
330
|
*/
|
|
213
|
-
export function Provenance({
|
|
331
|
+
export function Provenance({
|
|
332
|
+
entries,
|
|
333
|
+
}: {
|
|
334
|
+
entries: readonly {
|
|
335
|
+
readonly id: string | null;
|
|
336
|
+
readonly title: string | null;
|
|
337
|
+
readonly resource: string;
|
|
338
|
+
}[];
|
|
339
|
+
}): ReactElement | null {
|
|
214
340
|
if (entries.length === 0) return null;
|
|
215
341
|
|
|
216
342
|
return (
|
|
217
343
|
<section className="mt-10 border-t border-fd-border pt-5 text-sm">
|
|
218
344
|
<h2 className="ksor-section-label mb-2">Sources</h2>
|
|
219
|
-
{/* break-words, because a
|
|
345
|
+
{/* break-words, because a source is often a long unbroken URL: on a
|
|
220
346
|
phone it overflowed its row by 175px under an ancestor with
|
|
221
347
|
`overflow-x: clip`, so the middle of the source was clipped away with
|
|
222
348
|
no ellipsis and nothing to scroll (measured, 2026-08-20). A source
|
|
223
349
|
nobody can read is not provenance. */}
|
|
224
350
|
<ul className="space-y-1 break-words text-fd-muted-foreground">
|
|
225
351
|
{entries.map((entry, index) => {
|
|
226
|
-
//
|
|
227
|
-
// `rel="noreferrer"` because the
|
|
228
|
-
// record, not chosen by this site.
|
|
229
|
-
const href = sourceHref(entry);
|
|
352
|
+
// A resource that IS a URL becomes followable; a bundle path or a
|
|
353
|
+
// scope descriptor stays text. `rel="noreferrer"` because the
|
|
354
|
+
// destination is authored in the record, not chosen by this site.
|
|
355
|
+
const href = sourceHref(entry.resource);
|
|
356
|
+
const label = entry.title ?? entry.resource;
|
|
230
357
|
return (
|
|
231
358
|
// Position, not text: a record may cite the same source twice, and
|
|
232
359
|
// duplicate keys are a console error on a governed page.
|
|
233
|
-
<li key={`${index}-${entry}`}>
|
|
360
|
+
<li key={`${index}-${entry.resource}`}>
|
|
361
|
+
{entry.id === null ? null : (
|
|
362
|
+
<span className="me-2 font-mono text-xs text-fd-muted-foreground">
|
|
363
|
+
[^{entry.id}]
|
|
364
|
+
</span>
|
|
365
|
+
)}
|
|
234
366
|
{href === null ? (
|
|
235
|
-
|
|
367
|
+
label
|
|
236
368
|
) : (
|
|
237
369
|
<a
|
|
238
370
|
href={href}
|
|
@@ -240,7 +372,7 @@ export function Provenance({ entries }: { entries: readonly string[] }): ReactEl
|
|
|
240
372
|
rel="noreferrer"
|
|
241
373
|
className="underline underline-offset-4 transition-colors hover:text-fd-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fd-ring"
|
|
242
374
|
>
|
|
243
|
-
{
|
|
375
|
+
{label}
|
|
244
376
|
</a>
|
|
245
377
|
)}
|
|
246
378
|
</li>
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import defaultMdxComponents from "fumadocs-ui/mdx";
|
|
2
|
+
|
|
3
|
+
import { WrappableCodeBlock } from "@/components/code-block";
|
|
4
|
+
import { Embed } from "@/components/embed";
|
|
2
5
|
import { CodeBlockTabsTrigger } from "fumadocs-ui/components/codeblock";
|
|
3
6
|
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
|
4
7
|
import type { MDXComponents } from "mdx/types";
|
|
@@ -32,9 +35,14 @@ export function getMDXComponents(components?: MDXComponents) {
|
|
|
32
35
|
// here or a document whose page forgot to pass one serves a 500 rather
|
|
33
36
|
// than a page without an aid.
|
|
34
37
|
TeachingAid: () => null,
|
|
38
|
+
// A long line is the reader's to unwrap, per block — see
|
|
39
|
+
// components/code-block.tsx. Replaces fumadocs' own `pre`.
|
|
40
|
+
pre: WrappableCodeBlock,
|
|
41
|
+
// `rehypeEmbeds` (source.config.ts) rewrites a link titled `embed` into
|
|
42
|
+
// this; an unknown component fails the build, so it has to be in the map.
|
|
43
|
+
Embed,
|
|
35
44
|
// `remarkCodeTab` (source.config.ts) rewrites consecutive fenced blocks
|
|
36
|
-
// that declare `tab="…"` into these,
|
|
37
|
-
// build fails on an unknown component rather than at authoring time.
|
|
45
|
+
// that declare `tab="…"` into these, for the same reason.
|
|
38
46
|
Tabs,
|
|
39
47
|
Tab,
|
|
40
48
|
CodeBlockTabsTrigger: BrandedTabsTrigger,
|
|
@@ -2,7 +2,7 @@ import { ArrowRight, FileText, Folder } from "lucide-react";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import type { ReactElement } from "react";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { badgeLabel, badgeTone } from "@/lib/governance";
|
|
6
6
|
import type { RecordEntry } from "@/lib/source";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -28,7 +28,7 @@ import type { RecordEntry } from "@/lib/source";
|
|
|
28
28
|
*
|
|
29
29
|
* What survives from the register is the part that was never about looks: the
|
|
30
30
|
* record's serif for its own words, mono for what the record says ABOUT them —
|
|
31
|
-
* who owns it, how much it holds, whether it carries a caveat — and `
|
|
31
|
+
* who owns it, how much it holds, whether it carries a caveat — and `stable`
|
|
32
32
|
* staying silent, because a label on every row is a label nobody reads.
|
|
33
33
|
*
|
|
34
34
|
* Server-rendered plain markup — it survives print, a failed bundle and
|
|
@@ -70,11 +70,11 @@ export function RecordIndex({
|
|
|
70
70
|
<span className="font-display text-lg leading-snug font-semibold tracking-[-0.008em] transition-colors group-hover:text-fd-primary">
|
|
71
71
|
{entry.title}
|
|
72
72
|
</span>
|
|
73
|
-
{entry.
|
|
73
|
+
{entry.badge === null ? null : (
|
|
74
74
|
<span
|
|
75
|
-
className={`rounded-sm border border-fd-border px-1.5 py-0.5 font-mono text-[10px] tracking-widest text-fd-foreground uppercase ${
|
|
75
|
+
className={`rounded-sm border border-fd-border px-1.5 py-0.5 font-mono text-[10px] tracking-widest text-fd-foreground uppercase ${badgeTone(entry.badge)}`}
|
|
76
76
|
>
|
|
77
|
-
{entry.
|
|
77
|
+
{badgeLabel(entry.badge)}
|
|
78
78
|
</span>
|
|
79
79
|
)}
|
|
80
80
|
</span>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import Link from "next/link";
|
|
2
2
|
import type { ReactElement } from "react";
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { badgeLabel, badgeTone } from "@/lib/governance";
|
|
5
|
+
import type { LifecycleBadge } from "@/lib/lifecycle-rule";
|
|
5
6
|
import type { RecordEntry } from "@/lib/source";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -53,7 +54,7 @@ export function RecordStack({
|
|
|
53
54
|
<span className="font-mono text-[10px] tracking-[0.2em] text-[var(--ksor-cover-muted)] uppercase">
|
|
54
55
|
Opens here
|
|
55
56
|
</span>
|
|
56
|
-
{lead.
|
|
57
|
+
{lead.badge === null ? null : <StatusChip badge={lead.badge} />}
|
|
57
58
|
</div>
|
|
58
59
|
|
|
59
60
|
<h2 className="mt-4 font-display text-2xl leading-snug font-semibold tracking-[-0.008em] transition-colors group-hover:text-fd-primary">
|
|
@@ -99,7 +100,7 @@ export function RecordStack({
|
|
|
99
100
|
{entry.documents === 0
|
|
100
101
|
? null
|
|
101
102
|
: `${entry.documents} ${entry.documents === 1 ? "doc" : "docs"}`}
|
|
102
|
-
{entry.
|
|
103
|
+
{entry.badge === null ? null : <StatusChip badge={entry.badge} />}
|
|
103
104
|
</span>
|
|
104
105
|
</span>
|
|
105
106
|
</Link>
|
|
@@ -116,16 +117,16 @@ export function RecordStack({
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
/**
|
|
119
|
-
* A caveat
|
|
120
|
-
*
|
|
121
|
-
* document in the record is current.
|
|
120
|
+
* A caveat badge, in the same chip the record's listings use — only ever a
|
|
121
|
+
* state the machine surfaces decline (record spec §2.5), because a reader
|
|
122
|
+
* already assumes a document in the record is current and stable.
|
|
122
123
|
*/
|
|
123
|
-
function StatusChip({
|
|
124
|
+
function StatusChip({ badge }: { badge: LifecycleBadge }): ReactElement {
|
|
124
125
|
return (
|
|
125
126
|
<span
|
|
126
|
-
className={`rounded-sm border border-[var(--ksor-cover-rule)] px-1.5 py-0.5 font-mono text-[10px] tracking-widest text-[var(--ksor-cover-foreground)] uppercase ${
|
|
127
|
+
className={`rounded-sm border border-[var(--ksor-cover-rule)] px-1.5 py-0.5 font-mono text-[10px] tracking-widest text-[var(--ksor-cover-foreground)] uppercase ${badgeTone(badge)}`}
|
|
127
128
|
>
|
|
128
|
-
{
|
|
129
|
+
{badgeLabel(badge)}
|
|
129
130
|
</span>
|
|
130
131
|
);
|
|
131
132
|
}
|
|
@@ -146,23 +146,12 @@ export function RecordViews({
|
|
|
146
146
|
[select, views],
|
|
147
147
|
);
|
|
148
148
|
|
|
149
|
-
// No summary, so no tabs —
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<div className="ksor-views">
|
|
156
|
-
{documentMinutes === undefined ? null : (
|
|
157
|
-
<p className="mb-6 flex items-center justify-end gap-2 border-b border-fd-border pb-2.5 text-sm text-fd-muted-foreground">
|
|
158
|
-
<Clock aria-hidden className="size-3.5 shrink-0" />
|
|
159
|
-
<span>{documentMinutes} min read</span>
|
|
160
|
-
</p>
|
|
161
|
-
)}
|
|
162
|
-
{children}
|
|
163
|
-
</div>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
149
|
+
// No summary, so no tabs — and so no strip. It used to keep the row anyway,
|
|
150
|
+
// for the reading time alone: a full-width rule under an empty band with one
|
|
151
|
+
// number at its far end, which is what most documents got, because most have
|
|
152
|
+
// no summary. The number moved to the governance row, where it sits with the
|
|
153
|
+
// document's other facts and needs no furniture of its own.
|
|
154
|
+
if (only) return <div className="ksor-views">{children}</div>;
|
|
166
155
|
|
|
167
156
|
return (
|
|
168
157
|
<div className="ksor-views">
|
|
@@ -1,35 +1,36 @@
|
|
|
1
|
-
import type { ReactElement } from "react";
|
|
1
|
+
import type { ReactElement, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { badgeLabel, badgeTone } from "@/lib/governance";
|
|
4
|
+
import type { LifecycleBadge } from "@/lib/lifecycle-rule";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* The sidebar's
|
|
7
|
-
*
|
|
7
|
+
* The sidebar's badge, drawn beside a row's name (`lib/source.ts` composes it
|
|
8
|
+
* into the page tree while sorting it).
|
|
8
9
|
*
|
|
9
10
|
* The sidebar is where a reader chooses. Without this, a withdrawn document and
|
|
10
11
|
* the one that replaced it were pixel-identical rows — the governance appeared
|
|
11
12
|
* only after the click, which is the moment it is least useful
|
|
12
13
|
* (research/site-design.md F3).
|
|
13
14
|
*
|
|
14
|
-
* Only a
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Only a BADGE is drawn — a state the machine surfaces decline (record spec
|
|
16
|
+
* §2.5). A current document shows nothing, because a reader already assumes a
|
|
17
|
+
* document in the record is current and a label that never varies stops being
|
|
18
|
+
* read; the marker stays rare enough to be noticed on the rows where it matters.
|
|
18
19
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
if (caveat === null) return null;
|
|
22
|
-
// `inline-block` with the row's own wrapping, not a flex wrapper: the plugin
|
|
23
|
-
// composes `<>{name}{badge}</>` with no element around the pair, so the badge
|
|
20
|
+
export function renderBadge(name: ReactNode, badge: LifecycleBadge): ReactElement {
|
|
21
|
+
// `inline-block` with the row's own wrapping, not a flex wrapper: the badge
|
|
24
22
|
// has to survive beside a title that runs two lines in a ~200px column. An
|
|
25
23
|
// earlier hand-rolled version pinned the chip right with `truncate`, which
|
|
26
24
|
// clipped the title AND the chip to "sup…" (seen in Chromium, 2026-08-21) —
|
|
27
25
|
// the marker has to fit around the name, not fight it.
|
|
28
26
|
return (
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
<>
|
|
28
|
+
{name}
|
|
29
|
+
<span
|
|
30
|
+
className={`ms-1.5 inline-block rounded border border-fd-border px-1 py-px align-middle text-[0.6rem] font-medium whitespace-nowrap text-fd-muted-foreground ${badgeTone(badge)}`}
|
|
31
|
+
>
|
|
32
|
+
{badgeLabel(badge)}
|
|
33
|
+
</span>
|
|
34
|
+
</>
|
|
34
35
|
);
|
|
35
36
|
}
|