@panaversity/ksor 0.0.40 → 0.0.42
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 +932 -0
- package/README.md +11 -7
- package/dist/checker/check-main.mjs +14049 -0
- package/dist/cli.mjs +11529 -5214
- 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 +54 -23
- 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 +237 -116
- package/templates/scaffold/Dockerfile +5 -1
- package/templates/scaffold/README.md +160 -42
- package/templates/scaffold/env.example +40 -8
- package/templates/scaffold/gitignore +13 -8
- package/templates/scaffold/instance.md +30 -23
- package/templates/scaffold/knowledge/governance-ladder.md +6 -2
- package/templates/scaffold/knowledge/index.md +9 -0
- package/templates/scaffold/knowledge/surfaces/for-agents.md +7 -6
- package/templates/scaffold/knowledge/surfaces/for-people.md +7 -6
- package/templates/scaffold/knowledge/surfaces/index.md +4 -20
- package/templates/scaffold/knowledge/surfaces/overview.md +25 -0
- package/templates/scaffold/knowledge/what-is-a-ksor.md +6 -5
- package/templates/scaffold/knowledge/what-is-a-ksor.summary.md +4 -0
- package/templates/scaffold/package.json +3 -4
- package/templates/scaffold/pnpm-lock.yaml +3 -0
- package/templates/scaffold/system/gateways/content.ts +13 -0
- package/templates/scaffold/system/site/app/(home)/page.tsx +2 -2
- package/templates/scaffold/system/site/app/.well-known/mcp/server.json/route.ts +10 -0
- package/templates/scaffold/system/site/app/docs/[[...slug]]/page.tsx +126 -91
- package/templates/scaffold/system/site/app/global.css +13 -5
- package/templates/scaffold/system/site/app/layout.tsx +8 -3
- package/templates/scaffold/system/site/app/llms-full.txt/route.ts +13 -7
- package/templates/scaffold/system/site/app/llms.txt/route.ts +12 -7
- package/templates/scaffold/system/site/app/md/[[...slug]]/route.ts +26 -25
- package/templates/scaffold/system/site/components/footer-mark.tsx +3 -2
- package/templates/scaffold/system/site/components/governance.tsx +205 -87
- package/templates/scaffold/system/site/components/record-index.tsx +5 -5
- package/templates/scaffold/system/site/components/record-stack.tsx +10 -9
- package/templates/scaffold/system/site/components/sidebar-status.tsx +19 -18
- package/templates/scaffold/system/site/lib/attachment-rule.ts +6 -1
- package/templates/scaffold/system/site/lib/attachments.ts +0 -28
- package/templates/scaffold/system/site/lib/audience-rule.ts +15 -21
- package/templates/scaffold/system/site/lib/audience.ts +42 -146
- package/templates/scaffold/system/site/lib/embed-rule.ts +9 -0
- package/templates/scaffold/system/site/lib/governance.ts +339 -225
- package/templates/scaffold/system/site/lib/index-routes.ts +125 -0
- package/templates/scaffold/system/site/lib/lifecycle-rule.ts +52 -0
- package/templates/scaffold/system/site/lib/lock.ts +282 -0
- package/templates/scaffold/system/site/lib/order-rule.ts +37 -0
- package/templates/scaffold/system/site/lib/record-href.ts +68 -0
- package/templates/scaffold/system/site/lib/record-link.tsx +26 -0
- package/templates/scaffold/system/site/lib/rules-version.ts +11 -0
- package/templates/scaffold/system/site/lib/shared.ts +67 -104
- package/templates/scaffold/system/site/lib/sim-rule.ts +49 -0
- package/templates/scaffold/system/site/lib/source.ts +256 -186
- package/templates/scaffold/system/site/lib/stage-knowledge.ts +566 -492
- package/templates/scaffold/system/site/lib/stage-manifest.ts +128 -0
- package/templates/scaffold/system/site/package.json +1 -0
- package/templates/scaffold/system/site/record/actor.ts +23 -0
- package/templates/scaffold/system/site/record/check.ts +571 -0
- package/templates/scaffold/system/site/record/citations.ts +312 -0
- package/templates/scaffold/system/site/record/frontmatter.ts +134 -0
- package/templates/scaffold/system/site/record/git-ledger.ts +171 -0
- package/templates/scaffold/system/site/record/hygiene.ts +320 -0
- package/templates/scaffold/system/site/record/index-file.ts +150 -0
- package/templates/scaffold/system/site/record/index.ts +103 -0
- package/templates/scaffold/system/site/record/instance.ts +257 -0
- package/templates/scaffold/system/site/record/instant.ts +43 -0
- package/templates/scaffold/system/site/record/ledger.ts +694 -0
- package/templates/scaffold/system/site/record/load.ts +129 -0
- package/templates/scaffold/system/site/record/lock.ts +306 -0
- package/templates/scaffold/system/site/record/near-miss.ts +37 -0
- package/templates/scaffold/system/site/record/policy.ts +414 -0
- package/templates/scaffold/system/site/record/profile.ts +535 -0
- package/templates/scaffold/system/site/record/refusal.ts +106 -0
- package/templates/scaffold/system/site/record/yaml-file.ts +103 -0
- package/templates/scaffold/system/site/source.config.ts +77 -22
- package/dist/gateway-api-CmIthmJS-IUA9qS-T.mjs +0 -3225
- package/templates/scaffold/system/site/lib/denial-rule.ts +0 -220
- package/templates/scaffold/system/site/lib/page-order.ts +0 -93
|
@@ -1,36 +1,38 @@
|
|
|
1
1
|
import { notFound } from "next/navigation";
|
|
2
2
|
|
|
3
|
-
import { getLLMText,
|
|
3
|
+
import { getLLMText, getMachinePages, rootIndexTwin, source } from "@/lib/source";
|
|
4
|
+
import { machineAdmits } from "@/lib/stage-manifest";
|
|
4
5
|
|
|
5
6
|
export const revalidate = false;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Every document as markdown, at a stable address derived
|
|
9
|
-
* `/docs/policies/purchase-approval` is also served at
|
|
10
|
-
* `/md/policies/purchase-approval.md
|
|
9
|
+
* Every MACHINE-admitted document as markdown, at a stable address derived
|
|
10
|
+
* from its path: `/docs/policies/purchase-approval` is also served at
|
|
11
|
+
* `/md/policies/purchase-approval.md`, and the record's own index at
|
|
12
|
+
* `/md/index.md` — the one index with a twin (build spec §3).
|
|
11
13
|
*
|
|
12
14
|
* The record already IS markdown; without this an agent handed a document URL
|
|
13
15
|
* had to scrape a React app to reach text the record holds verbatim
|
|
14
|
-
* (research/site-design.md F2). The body carries the document's governance
|
|
15
|
-
* frontmatter, exactly as `llms-full.txt` does
|
|
16
|
-
* document knows its status, owner, sources and successor.
|
|
16
|
+
* (research/site-design.md F2). The body carries the document's governance and
|
|
17
|
+
* the build's stamps as frontmatter, exactly as `llms-full.txt` does.
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* A page the machine surfaces decline — a draft, a deprecated, a not-yet-
|
|
20
|
+
* effective or a stale concept (record spec §2.5) — has no twin at all: the
|
|
21
|
+
* decision is staging's, read back here, never re-derived.
|
|
22
|
+
*
|
|
23
|
+
* Why a `/md/` prefix rather than appending `.md` to the document's own URL:
|
|
24
|
+
* under `output: "export"` a Route Handler cannot share a route segment with a
|
|
25
|
+
* Page, and there is no middleware to rewrite one onto the other. The prefix
|
|
22
26
|
* survives a static host, and the page advertises it in a `rel="alternate"`
|
|
23
|
-
* link so a consumer discovers it rather than guessing.
|
|
24
|
-
* canonical URL becomes possible the day a build emits these artifacts itself.
|
|
27
|
+
* link so a consumer discovers it rather than guessing.
|
|
25
28
|
*/
|
|
26
29
|
export function generateStaticParams(): { slug: string[] }[] {
|
|
27
|
-
|
|
28
|
-
const segments =
|
|
29
|
-
const last = segments.at(-1);
|
|
30
|
-
return {
|
|
31
|
-
slug: last === undefined ? ["index.md"] : [...segments.slice(0, -1), `${last}.md`],
|
|
32
|
-
};
|
|
30
|
+
const twins = getMachinePages().map((page) => {
|
|
31
|
+
const segments = page.url.replace(/^\/docs\/?/, "").split("/");
|
|
32
|
+
const last = segments.at(-1) ?? "";
|
|
33
|
+
return { slug: [...segments.slice(0, -1), `${last}.md`] };
|
|
33
34
|
});
|
|
35
|
+
return [{ slug: ["index.md"] }, ...twins];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export async function GET(
|
|
@@ -40,12 +42,11 @@ export async function GET(
|
|
|
40
42
|
const { slug = [] } = await params;
|
|
41
43
|
const last = slug.at(-1);
|
|
42
44
|
if (last === undefined || !last.endsWith(".md")) notFound();
|
|
43
|
-
const
|
|
45
|
+
const headers = { "content-type": "text/markdown; charset=utf-8" };
|
|
46
|
+
if (slug.length === 1 && last === "index.md") return new Response(rootIndexTwin(), { headers });
|
|
44
47
|
|
|
45
|
-
const page = source.getPage(
|
|
46
|
-
if (!page) notFound();
|
|
48
|
+
const page = source.getPage([...slug.slice(0, -1), last.slice(0, -".md".length)]);
|
|
49
|
+
if (!page || !machineAdmits(page.path)) notFound();
|
|
47
50
|
|
|
48
|
-
return new Response(
|
|
49
|
-
headers: { "content-type": "text/markdown; charset=utf-8" },
|
|
50
|
-
});
|
|
51
|
+
return new Response(getLLMText(page), { headers });
|
|
51
52
|
}
|
|
@@ -13,10 +13,11 @@ import { audienceNotice } from "@/lib/audience";
|
|
|
13
13
|
* carry the shape of a disclosure.
|
|
14
14
|
*/
|
|
15
15
|
export function FooterMark(): ReactElement {
|
|
16
|
-
|
|
16
|
+
const notice = audienceNotice();
|
|
17
|
+
if (notice === null) return <BuiltWith />;
|
|
17
18
|
return (
|
|
18
19
|
<>
|
|
19
|
-
<BuiltWith /> · <span className="text-fd-muted-foreground">{
|
|
20
|
+
<BuiltWith /> · <span className="text-fd-muted-foreground">{notice}</span>
|
|
20
21
|
</>
|
|
21
22
|
);
|
|
22
23
|
}
|
|
@@ -5,18 +5,25 @@ import { Clock } from "lucide-react";
|
|
|
5
5
|
import type { ReactElement } from "react";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
badgeAddsToStatus,
|
|
9
|
+
badgeText,
|
|
10
|
+
badgeTone,
|
|
11
|
+
dayOf,
|
|
10
12
|
isCalendarDate,
|
|
13
|
+
plainBadge,
|
|
11
14
|
sourceHref,
|
|
15
|
+
statusLabel,
|
|
16
|
+
statusTone,
|
|
17
|
+
trustSignal,
|
|
12
18
|
type DocumentGovernance,
|
|
13
19
|
} from "@/lib/governance";
|
|
20
|
+
import type { LifecycleBadge } from "@/lib/lifecycle-rule";
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
23
|
* What the record says about the document you are reading.
|
|
17
24
|
*
|
|
18
25
|
* The site renders the record; these render the record's governance — the
|
|
19
|
-
* frontmatter `pnpm check` enforces on every
|
|
26
|
+
* profile's frontmatter `pnpm check` enforces on every concept. Nothing here is
|
|
20
27
|
* authored in the site (critical rule 1) and nothing is inferred: a key the
|
|
21
28
|
* document does not declare renders nothing at all.
|
|
22
29
|
*
|
|
@@ -27,9 +34,9 @@ import {
|
|
|
27
34
|
*/
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
|
-
* The successor of a
|
|
31
|
-
* `href` is null when the pointer could not be resolved to a route
|
|
32
|
-
* 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.
|
|
33
40
|
*/
|
|
34
41
|
export interface Successor {
|
|
35
42
|
readonly href: string | null;
|
|
@@ -37,11 +44,12 @@ export interface Successor {
|
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
40
|
-
* The
|
|
47
|
+
* The deprecation notice. Deliberately the first thing on the page, above the
|
|
41
48
|
* title: a reader must not have to notice a subtle badge to learn that what
|
|
42
|
-
* 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.
|
|
43
51
|
*/
|
|
44
|
-
export function
|
|
52
|
+
export function DeprecatedNotice({ successor }: { successor: Successor | null }): ReactElement {
|
|
45
53
|
return (
|
|
46
54
|
<aside
|
|
47
55
|
// A landmark, not a note: GOV.UK ships this as role="region" with
|
|
@@ -49,7 +57,7 @@ export function SupersededNotice({ successor }: { successor: Successor }): React
|
|
|
49
57
|
// thing on the page by landmark, rather than only meeting it in reading
|
|
50
58
|
// order. role="note" is announced but not navigable.
|
|
51
59
|
role="region"
|
|
52
|
-
aria-labelledby="ksor-
|
|
60
|
+
aria-labelledby="ksor-deprecated"
|
|
53
61
|
// Tinted and ruled down the left edge in the CAUTION role, never
|
|
54
62
|
// --color-fd-muted: the shipped light theme defines --color-fd-muted and
|
|
55
63
|
// --color-fd-background as the same value, so the callout composited to
|
|
@@ -57,22 +65,29 @@ export function SupersededNotice({ successor }: { successor: Successor }): React
|
|
|
57
65
|
// Chromium, 2026-08-20). The one thing this notice cannot be is missable.
|
|
58
66
|
className="ksor-caution mb-8 rounded-lg border border-l-4 px-4 py-3 text-sm"
|
|
59
67
|
>
|
|
60
|
-
<p id="ksor-
|
|
61
|
-
|
|
68
|
+
<p id="ksor-deprecated" className="font-medium text-fd-foreground">
|
|
69
|
+
Deprecated
|
|
62
70
|
</p>
|
|
63
71
|
<p className="mt-1 text-fd-muted-foreground">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
<code className="break-words text-fd-foreground">{successor.label}</code>
|
|
72
|
+
{successor === null ? (
|
|
73
|
+
"The record has withdrawn this document."
|
|
67
74
|
) : (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
76
91
|
</p>
|
|
77
92
|
</aside>
|
|
78
93
|
);
|
|
@@ -89,13 +104,10 @@ function Fact({
|
|
|
89
104
|
<div className="flex items-baseline gap-2.5">
|
|
90
105
|
{/* Mono, uppercase, letterspaced: these are the record's checkable facts,
|
|
91
106
|
and they read as a register's column heads rather than as a form.
|
|
92
|
-
|
|
93
107
|
The label is deliberately SMALLER and more letterspaced than the value
|
|
94
|
-
it introduces
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(reported by the owner, 2026-08-22, quoting the run back verbatim).
|
|
98
|
-
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). */}
|
|
99
111
|
<dt className="font-mono text-[0.625rem] tracking-[0.18em] text-fd-muted-foreground uppercase">
|
|
100
112
|
{label}
|
|
101
113
|
</dt>
|
|
@@ -106,20 +118,82 @@ function Fact({
|
|
|
106
118
|
);
|
|
107
119
|
}
|
|
108
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
|
+
|
|
144
|
+
/**
|
|
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
|
+
|
|
109
173
|
/**
|
|
110
|
-
* The one-line governance strip under the document's title:
|
|
111
|
-
*
|
|
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.
|
|
112
183
|
*/
|
|
113
184
|
export function GovernanceMeta({
|
|
114
185
|
governance,
|
|
186
|
+
badge,
|
|
115
187
|
replaces = [],
|
|
116
188
|
markdownUrl,
|
|
117
189
|
minutes,
|
|
118
190
|
}: {
|
|
119
191
|
governance: DocumentGovernance;
|
|
192
|
+
/** Why the machine surfaces decline this page, or null. */
|
|
193
|
+
badge: LifecycleBadge | null;
|
|
120
194
|
/** Documents this one replaced — derived from the record, never declared. */
|
|
121
195
|
replaces?: readonly Successor[];
|
|
122
|
-
/** The document's markdown twin, offered beside its governance. */
|
|
196
|
+
/** The document's markdown twin, offered beside its governance — only where one exists. */
|
|
123
197
|
markdownUrl?: string;
|
|
124
198
|
/**
|
|
125
199
|
* How long the document takes to read, when this row is the only place for
|
|
@@ -127,30 +201,80 @@ export function GovernanceMeta({
|
|
|
127
201
|
* because there the number belongs to the view you picked.
|
|
128
202
|
*/
|
|
129
203
|
minutes?: number;
|
|
130
|
-
}): ReactElement
|
|
131
|
-
const { owner,
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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";
|
|
135
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.
|
|
136
219
|
return (
|
|
137
220
|
<dl className="mb-7 flex flex-wrap items-baseline gap-x-8 gap-y-2.5 border-b border-fd-border pb-4">
|
|
138
|
-
{
|
|
221
|
+
{state === null && alsoBadge === null ? null : (
|
|
139
222
|
<Fact label="Status">
|
|
140
|
-
<span
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
)}
|
|
144
228
|
</span>
|
|
145
229
|
</Fact>
|
|
146
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>
|
|
147
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
|
+
)}
|
|
148
272
|
{replaces.length === 0 ? null : (
|
|
149
273
|
// The other half of a supersession. The withdrawn document names its
|
|
150
274
|
// successor above the title; this is the successor naming what it
|
|
151
275
|
// replaced, so the history the record kept is reachable from the
|
|
152
276
|
// current document instead of only from the retired one.
|
|
153
|
-
<Fact label=
|
|
277
|
+
<Fact label="Replaces">
|
|
154
278
|
<>
|
|
155
279
|
{replaces.map((entry, index) => (
|
|
156
280
|
<span key={entry.href ?? `${index}-${entry.label}`}>
|
|
@@ -170,39 +294,15 @@ export function GovernanceMeta({
|
|
|
170
294
|
</>
|
|
171
295
|
</Fact>
|
|
172
296
|
)}
|
|
173
|
-
{
|
|
174
|
-
<Fact label="Effective">
|
|
175
|
-
{/* The machine attribute is stamped only for a real day on the
|
|
176
|
-
calendar. A SHAPE test was not enough: the checker's own remedy
|
|
177
|
-
for `2026-06-31` is to QUOTE it, and quoted text arrives here — so
|
|
178
|
-
a shape test published `<time dateTime="2026-06-31">`, which is
|
|
179
|
-
invalid HTML and which a consumer reads as July 1st. That is the
|
|
180
|
-
precise hazard the record's date rule exists to prevent. */}
|
|
181
|
-
{isCalendarDate(effective) ? (
|
|
182
|
-
<time dateTime={effective}>{effective}</time>
|
|
183
|
-
) : (
|
|
184
|
-
<span>{effective}</span>
|
|
185
|
-
)}
|
|
186
|
-
</Fact>
|
|
297
|
+
{effectiveFrom === null || !showEffective ? null : (
|
|
298
|
+
<Fact label="Effective from">{day(effectiveFrom)}</Fact>
|
|
187
299
|
)}
|
|
300
|
+
{staleAfter === null ? null : <Fact label="Review by">{day(staleAfter)}</Fact>}
|
|
188
301
|
{markdownUrl === undefined ? null : (
|
|
189
302
|
// On the governance row, not as a footnote below the sources: it is
|
|
190
|
-
// how a reader hands this document to an agent
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// different acts, but two bare controls on a row of read-only facts
|
|
194
|
-
// made the row look half clickable. It wears neither the bordered badge
|
|
195
|
-
// (that means "a status the record declares") nor the accent at rest —
|
|
196
|
-
// the page had gone blue enough that the accent had stopped meaning
|
|
197
|
-
// anything (owner, 2026-08-22).
|
|
198
|
-
// RIGHT-ALIGNED (owner, and a reversal). The 2026-08-21 finding stands
|
|
199
|
-
// on its own terms: `ms-auto` once parked this 498px from the nearest
|
|
200
|
-
// thing on a two-fact document, where it read as belonging to nothing.
|
|
201
|
-
// What changed is the column beside it — the reading time now sits at
|
|
202
|
-
// the right end of the strip directly below, so the far edge is no
|
|
203
|
-
// longer empty space but a line the eye already follows. The two form
|
|
204
|
-
// a right-hand column of things you DO, against a left-hand row of
|
|
205
|
-
// 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).
|
|
206
306
|
<span className="ms-auto">
|
|
207
307
|
<DocumentActions href={markdownUrl} />
|
|
208
308
|
</span>
|
|
@@ -217,36 +317,54 @@ export function GovernanceMeta({
|
|
|
217
317
|
);
|
|
218
318
|
}
|
|
219
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
|
+
|
|
220
326
|
/**
|
|
221
|
-
* Where the document came from
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* of `provenance` being a list: a citation has to be able to point at exactly
|
|
225
|
-
* 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.
|
|
226
330
|
*/
|
|
227
|
-
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 {
|
|
228
340
|
if (entries.length === 0) return null;
|
|
229
341
|
|
|
230
342
|
return (
|
|
231
343
|
<section className="mt-10 border-t border-fd-border pt-5 text-sm">
|
|
232
344
|
<h2 className="ksor-section-label mb-2">Sources</h2>
|
|
233
|
-
{/* break-words, because a
|
|
345
|
+
{/* break-words, because a source is often a long unbroken URL: on a
|
|
234
346
|
phone it overflowed its row by 175px under an ancestor with
|
|
235
347
|
`overflow-x: clip`, so the middle of the source was clipped away with
|
|
236
348
|
no ellipsis and nothing to scroll (measured, 2026-08-20). A source
|
|
237
349
|
nobody can read is not provenance. */}
|
|
238
350
|
<ul className="space-y-1 break-words text-fd-muted-foreground">
|
|
239
351
|
{entries.map((entry, index) => {
|
|
240
|
-
//
|
|
241
|
-
// `rel="noreferrer"` because the
|
|
242
|
-
// record, not chosen by this site.
|
|
243
|
-
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;
|
|
244
357
|
return (
|
|
245
358
|
// Position, not text: a record may cite the same source twice, and
|
|
246
359
|
// duplicate keys are a console error on a governed page.
|
|
247
|
-
<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
|
+
)}
|
|
248
366
|
{href === null ? (
|
|
249
|
-
|
|
367
|
+
label
|
|
250
368
|
) : (
|
|
251
369
|
<a
|
|
252
370
|
href={href}
|
|
@@ -254,7 +372,7 @@ export function Provenance({ entries }: { entries: readonly string[] }): ReactEl
|
|
|
254
372
|
rel="noreferrer"
|
|
255
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"
|
|
256
374
|
>
|
|
257
|
-
{
|
|
375
|
+
{label}
|
|
258
376
|
</a>
|
|
259
377
|
)}
|
|
260
378
|
</li>
|
|
@@ -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
|
}
|