@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,39 +1,28 @@
|
|
|
1
1
|
import { docs } from "collections/server";
|
|
2
2
|
import { loader } from "fumadocs-core/source";
|
|
3
3
|
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
|
|
4
|
-
import { statusBadgesPlugin } from "fumadocs-core/source/plugins/status-badges";
|
|
5
4
|
import type { Node, Root } from "fumadocs-core/page-tree";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
6
|
|
|
7
|
+
import { agentFrontmatter, badgeLabel, readGovernance, stampLines } from "./governance";
|
|
8
|
+
import { dirOfRoute, listingOf, readingOrder } from "./index-routes";
|
|
9
|
+
import type { LifecycleBadge } from "./lifecycle-rule";
|
|
10
|
+
import { appName, appTitle, appDescription, showGovernance } from "./shared";
|
|
7
11
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { orderValue } from "./order-rule";
|
|
17
|
-
import { sortNodes } from "./page-order";
|
|
12
|
+
readStagedIndex,
|
|
13
|
+
readStageManifest,
|
|
14
|
+
stagedBody,
|
|
15
|
+
stagedFrontmatter,
|
|
16
|
+
stagePageOf,
|
|
17
|
+
} from "./stage-manifest";
|
|
18
|
+
import { renderBadge } from "@/components/sidebar-status";
|
|
19
|
+
import { generateIndexes, humanise, type IndexEntry } from "../record/index-file";
|
|
18
20
|
|
|
19
21
|
// See https://fumadocs.dev/docs/headless/source-api for more info
|
|
20
22
|
export const source = loader({
|
|
21
23
|
baseUrl: "/docs",
|
|
22
24
|
source: docs.toFumadocsSource(),
|
|
23
|
-
plugins: [
|
|
24
|
-
lucideIconsPlugin(),
|
|
25
|
-
// The shell's own status plugin, which reads `status` from a document's
|
|
26
|
-
// frontmatter and puts it on the tree node. This used to be a map of
|
|
27
|
-
// statuses by url and a second walk over the tree that rewrote each row's
|
|
28
|
-
// `name` — the plugin does the walk, so the record's own key reaches the
|
|
29
|
-
// sidebar without us restating it.
|
|
30
|
-
//
|
|
31
|
-
// `renderBadge` returns null for anything that is not a caveat, which is
|
|
32
|
-
// the one rule that is OURS: a reader already assumes a document in the
|
|
33
|
-
// record is current, so `approved` shows nothing and the marker stays rare
|
|
34
|
-
// enough to be noticed where it matters.
|
|
35
|
-
statusBadgesPlugin({ renderBadge: (status) => renderCaveatBadge(status) }),
|
|
36
|
-
],
|
|
25
|
+
plugins: [lucideIconsPlugin()],
|
|
37
26
|
});
|
|
38
27
|
|
|
39
28
|
// The page tree's root is named "Docs" by default — fumadocs' fallback for a
|
|
@@ -52,88 +41,170 @@ export type KnowledgePage = (typeof source)["$inferPage"];
|
|
|
52
41
|
// every rendered link.
|
|
53
42
|
export const basePath: string = process.env.KSOR_BASE_PATH ?? "";
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// kernel's ORDER_CASES table, and this half is asserted against the same rows.
|
|
59
|
-
function orderOf(page: KnowledgePage): number {
|
|
60
|
-
return orderValue(page.data.order);
|
|
44
|
+
/** A page's bundle-relative path with forward slashes — the manifest's key. */
|
|
45
|
+
function pathOf(page: KnowledgePage): string {
|
|
46
|
+
return page.path.replaceAll("\\", "/");
|
|
61
47
|
}
|
|
62
48
|
|
|
63
49
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
50
|
+
* Every directory this viewer's stage holds an index for, root first, with
|
|
51
|
+
* its parsed index — walked from the root index's folder bullets, so a folder
|
|
52
|
+
* this viewer may not see is never even asked for.
|
|
67
53
|
*/
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return
|
|
54
|
+
function stagedIndexes(): Map<string, IndexEntry[]> {
|
|
55
|
+
const out = new Map<string, IndexEntry[]>();
|
|
56
|
+
const walk = (dir: string): void => {
|
|
57
|
+
const entries = readStagedIndex(dir);
|
|
58
|
+
if (entries === null) return;
|
|
59
|
+
out.set(dir, entries);
|
|
60
|
+
for (const item of listingOf(dir, entries)) if (item.kind === "folder") walk(item.path);
|
|
61
|
+
};
|
|
62
|
+
walk("");
|
|
63
|
+
return out;
|
|
78
64
|
}
|
|
79
65
|
|
|
80
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Reading order is ONE rule — the index generator's (build spec §1: one bullet
|
|
68
|
+
* list, concepts and folders together, by `order:` then name) — and every
|
|
69
|
+
* surface takes it from the regenerated indexes rather than restating it. A
|
|
70
|
+
* route the indexes never listed sorts last, by url.
|
|
71
|
+
*/
|
|
72
|
+
function positions(): Map<string, number> {
|
|
73
|
+
return new Map(readingOrder(stagedIndexes()).map((url, i) => [url, i] as const));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The index of a top-level entry's own segment: "/docs/x" splits to
|
|
77
|
+
// ["", "docs", "x"], so its segment is at 2 — the baseUrl's segment count.
|
|
78
|
+
const BASE_SEGMENTS = "/docs".split("/").length;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A node's own route at `depth`: a page's url, or a folder's route derived
|
|
82
|
+
* from any descendant, TRUNCATED to this depth.
|
|
83
|
+
*
|
|
84
|
+
* The truncation is the whole point, not tidiness. A folder with no index
|
|
85
|
+
* document takes its url from a descendant (`/docs/guides/first`), and
|
|
86
|
+
* comparing that whole url against a sibling `/docs/guides-x` puts the two in
|
|
87
|
+
* the opposite order, because the separator `/` (47) sorts after `-` (45).
|
|
88
|
+
* Cutting to this depth compares `/docs/guides` against `/docs/guides-x`,
|
|
89
|
+
* which is what the reading-order rule means by a sibling name.
|
|
90
|
+
*/
|
|
91
|
+
function routeAt(node: Node, depth: number): string {
|
|
92
|
+
if (node.type === "page") return node.url;
|
|
93
|
+
if (node.type === "folder") {
|
|
94
|
+
const descendant = node.index?.url ?? firstPageUrl(node.children);
|
|
95
|
+
if (descendant !== null) {
|
|
96
|
+
return descendant
|
|
97
|
+
.split("/")
|
|
98
|
+
.slice(0, BASE_SEGMENTS + depth + 1)
|
|
99
|
+
.join("/");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function firstPageUrl(nodes: readonly Node[]): string | null {
|
|
81
106
|
for (const node of nodes) {
|
|
82
|
-
if (node.type === "page")
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
if (node.type === "page") return node.url;
|
|
108
|
+
if (node.type === "folder") {
|
|
109
|
+
const url = node.index?.url ?? firstPageUrl(node.children);
|
|
110
|
+
if (url !== null) return url;
|
|
86
111
|
}
|
|
87
112
|
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sortTree(
|
|
117
|
+
nodes: readonly Node[],
|
|
118
|
+
order: ReadonlyMap<string, number>,
|
|
119
|
+
depth: number,
|
|
120
|
+
): Node[] {
|
|
121
|
+
const rank = (node: Node): number => order.get(routeAt(node, depth)) ?? Number.POSITIVE_INFINITY;
|
|
122
|
+
return nodes
|
|
123
|
+
.map((node): Node => {
|
|
124
|
+
if (node.type === "folder") {
|
|
125
|
+
const url = routeAt(node, depth);
|
|
126
|
+
// The folder's own page — the regenerated index rendered as a listing
|
|
127
|
+
// — so the sidebar row LINKS the folder rather than only toggling it.
|
|
128
|
+
const index: Node & { type: "page" } = { type: "page", name: node.name, url };
|
|
129
|
+
return { ...node, index, children: sortTree(node.children, order, depth + 1) };
|
|
130
|
+
}
|
|
131
|
+
if (node.type === "page") {
|
|
132
|
+
const badge = stagePageOf(pagePathByUrl().get(node.url) ?? "")?.badge ?? null;
|
|
133
|
+
return badge === null ? node : { ...node, name: withBadge(node.name, badge) };
|
|
134
|
+
}
|
|
135
|
+
return node;
|
|
136
|
+
})
|
|
137
|
+
.sort((a, b) => rank(a) - rank(b) || routeAt(a, depth).localeCompare(routeAt(b, depth)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function withBadge(name: ReactNode, badge: LifecycleBadge): ReactNode {
|
|
141
|
+
return renderBadge(name, badge);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let urlToPath: Map<string, string> | null = null;
|
|
145
|
+
function pagePathByUrl(): Map<string, string> {
|
|
146
|
+
if (urlToPath === null) {
|
|
147
|
+
urlToPath = new Map(source.getPages().map((page) => [page.url, pathOf(page)] as const));
|
|
148
|
+
}
|
|
149
|
+
return urlToPath;
|
|
88
150
|
}
|
|
89
151
|
|
|
90
152
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
153
|
+
* The page tree in reading order, every folder linking its own page and every
|
|
154
|
+
* page carrying its badge. Rebuilt on each call from a fresh clone — the
|
|
155
|
+
* loader's own tree is shared state and mutating it would survive a hot reload.
|
|
156
|
+
*/
|
|
157
|
+
export function getSortedPageTree(): Root {
|
|
158
|
+
const tree = source.getPageTree();
|
|
159
|
+
return { ...tree, children: sortTree(tree.children, positions(), 0) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Every page the human surfaces show, in the one reading order — the sidebar,
|
|
164
|
+
* the folder pages and the home page all walk this list.
|
|
93
165
|
*/
|
|
94
166
|
export function getSortedPages(): KnowledgePage[] {
|
|
95
|
-
const
|
|
96
|
-
|
|
167
|
+
const order = positions();
|
|
168
|
+
return [...source.getPages()].sort(
|
|
169
|
+
(a, b) =>
|
|
170
|
+
(order.get(a.url) ?? Number.POSITIVE_INFINITY) -
|
|
171
|
+
(order.get(b.url) ?? Number.POSITIVE_INFINITY) || a.url.localeCompare(b.url),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
97
174
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
// A page the tree never displayed is still part of the record.
|
|
108
|
-
return [...ordered, ...remaining.values()];
|
|
175
|
+
/**
|
|
176
|
+
* The pages the MACHINE surfaces admit — `llms.txt`, `llms-full.txt`, the
|
|
177
|
+
* twins: stable, effective, unexpired at the build's `as_of`, decided ONCE by
|
|
178
|
+
* staging and read back from its manifest (record spec §2.5). A route cannot
|
|
179
|
+
* widen this; it can only read it.
|
|
180
|
+
*/
|
|
181
|
+
export function getMachinePages(): KnowledgePage[] {
|
|
182
|
+
return getSortedPages().filter((page) => stagePageOf(pathOf(page))?.machine === true);
|
|
109
183
|
}
|
|
110
184
|
|
|
111
185
|
/**
|
|
112
|
-
* One document as the full-corpus file
|
|
113
|
-
*
|
|
186
|
+
* One document as the full-corpus file and its twin carry it: heading, then the
|
|
187
|
+
* record's OWN frontmatter intact under the build's stamps, then the record's
|
|
188
|
+
* OWN body.
|
|
114
189
|
*
|
|
115
|
-
* The frontmatter is the point
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
190
|
+
* The frontmatter is the point — without it a consumer ingesting the corpus had
|
|
191
|
+
* no way to tell a passage's status, owner or source, and nothing connecting it
|
|
192
|
+
* to the publication it came from (R14) — and both halves are served intact so
|
|
193
|
+
* that what a consumer parses is the profile's grammar rather than this shell's
|
|
194
|
+
* rendering of it. Body from the STAGE, never from fumadocs' processed
|
|
195
|
+
* markdown: see `stagedBody`, and the door, which serves these same bytes.
|
|
120
196
|
*/
|
|
121
|
-
export
|
|
122
|
-
page
|
|
123
|
-
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// found live 2026-08-21: the processed markdown arrives with its own leading
|
|
134
|
-
// blank lines, so every block opened with three of them — and adding the
|
|
135
|
-
// frontmatter above made it four. One blank line between each part, always.
|
|
136
|
-
return [`# ${page.data.title} (${basePath}${page.url})`, front.trimEnd(), processed.trimStart()]
|
|
197
|
+
export function getLLMText(page: KnowledgePage): string {
|
|
198
|
+
const body = stagedBody(page.path);
|
|
199
|
+
const front = agentFrontmatter(
|
|
200
|
+
stagedFrontmatter(page.path),
|
|
201
|
+
readGovernance(page.data, page.path),
|
|
202
|
+
readStageManifest().stamps,
|
|
203
|
+
);
|
|
204
|
+
// found live 2026-08-21: the body arrives with its own leading blank lines,
|
|
205
|
+
// so every block opened with three of them — and adding the frontmatter above
|
|
206
|
+
// made it four. One blank line between each part, always.
|
|
207
|
+
return [`# ${page.data.title} (${basePath}${page.url})`, front.trimEnd(), body.trimStart()]
|
|
137
208
|
.filter((part) => part !== "")
|
|
138
209
|
.join("\n\n");
|
|
139
210
|
}
|
|
@@ -143,8 +214,8 @@ export interface RecordEntry {
|
|
|
143
214
|
readonly url: string;
|
|
144
215
|
readonly title: string;
|
|
145
216
|
readonly description: string | null;
|
|
146
|
-
/**
|
|
147
|
-
readonly
|
|
217
|
+
/** Why the machine surfaces decline it, when they do; else null. */
|
|
218
|
+
readonly badge: LifecycleBadge | null;
|
|
148
219
|
/**
|
|
149
220
|
* Who stands behind it. Null when the record declares no owner — and null
|
|
150
221
|
* for every document when `site.governance` is off, because an owner is a
|
|
@@ -155,104 +226,118 @@ export interface RecordEntry {
|
|
|
155
226
|
readonly documents: number;
|
|
156
227
|
}
|
|
157
228
|
|
|
158
|
-
/**
|
|
159
|
-
* The record's entry for one page — what any listing needs.
|
|
160
|
-
*
|
|
161
|
-
* Exported because the front door leads with the document `Open the record`
|
|
162
|
-
* opens, which is the first page in governed order and may sit BELOW the top
|
|
163
|
-
* level, where `entriesUnder(null)` would never return it.
|
|
164
|
-
*/
|
|
229
|
+
/** The record's entry for one page — what any listing needs. */
|
|
165
230
|
export function entryFor(page: KnowledgePage): RecordEntry {
|
|
166
|
-
const data: Record<string, unknown> = page.data as unknown as Record<string, unknown>;
|
|
167
|
-
const description = typeof data["description"] === "string" ? data["description"].trim() : "";
|
|
168
|
-
const status = typeof data["status"] === "string" ? data["status"].trim() : "";
|
|
169
231
|
return {
|
|
170
232
|
url: page.url,
|
|
171
233
|
title: page.data.title,
|
|
172
|
-
description: description
|
|
173
|
-
|
|
234
|
+
description: page.data.description?.trim() || null,
|
|
235
|
+
badge: stagePageOf(pathOf(page))?.badge ?? null,
|
|
174
236
|
owner: showGovernance ? readGovernance(page.data, page.path).owner : null,
|
|
175
237
|
documents: 0,
|
|
176
238
|
};
|
|
177
239
|
}
|
|
178
240
|
|
|
241
|
+
/** How many documents the stage holds under a bundle-relative directory. */
|
|
242
|
+
function countUnder(dir: string): number {
|
|
243
|
+
const prefix = `${dir}/`;
|
|
244
|
+
return Object.keys(readStageManifest().pages).filter((p) => p.startsWith(prefix)).length;
|
|
245
|
+
}
|
|
246
|
+
|
|
179
247
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* By url in a set, not by adding lengths: whether a folder's index also
|
|
184
|
-
* appears among its children is the loader's business, and counting it twice
|
|
185
|
-
* would publish a number the record cannot support.
|
|
248
|
+
* The entries directly below a directory of the record (`""` for the root),
|
|
249
|
+
* in the governed reading order — the folder's regenerated `index.md`,
|
|
250
|
+
* rendered. A folder this viewer's stage does not hold lists nothing.
|
|
186
251
|
*/
|
|
187
|
-
function
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
252
|
+
export function entriesUnder(dir: string): RecordEntry[] {
|
|
253
|
+
const byUrl = new Map(source.getPages().map((page) => [page.url, page] as const));
|
|
254
|
+
return listingOf(dir, readStagedIndex(dir) ?? []).flatMap((item): RecordEntry[] => {
|
|
255
|
+
if (item.kind === "folder") {
|
|
256
|
+
return [
|
|
257
|
+
{
|
|
258
|
+
url: item.url,
|
|
259
|
+
title: item.title,
|
|
260
|
+
description: null,
|
|
261
|
+
badge: null,
|
|
262
|
+
owner: null,
|
|
263
|
+
documents: countUnder(item.path),
|
|
264
|
+
},
|
|
265
|
+
];
|
|
196
266
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
return urls.size;
|
|
267
|
+
const page = byUrl.get(item.url);
|
|
268
|
+
return page === undefined ? [] : [entryFor(page)];
|
|
269
|
+
});
|
|
201
270
|
}
|
|
202
271
|
|
|
203
|
-
/**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
return entries;
|
|
272
|
+
/** The heading of a directory's page: the index's own H1, or its humanised name. */
|
|
273
|
+
export function folderHeading(dir: string): string {
|
|
274
|
+
const entries = readStagedIndex(dir) ?? [];
|
|
275
|
+
return (
|
|
276
|
+
entries[0]?.heading || (dir === "" ? appTitle : humanise(dir.slice(dir.lastIndexOf("/") + 1)))
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Every directory the stage holds an index for, as `/docs/...` slugs — the folder routes to export. */
|
|
281
|
+
export function folderSlugs(): string[][] {
|
|
282
|
+
return [...stagedIndexes().keys()].map((dir) => (dir === "" ? [] : dir.split("/")));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Is this route a folder page in this viewer's stage? Returns its directory. */
|
|
286
|
+
export function folderOfRoute(url: string): string | null {
|
|
287
|
+
const dir = dirOfRoute(url);
|
|
288
|
+
return dir !== null && readStagedIndex(dir) !== null ? dir : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** The stamp block, as the text artefacts print it: one `key: value` bullet per stamp. */
|
|
292
|
+
function stampBullets(): string[] {
|
|
293
|
+
return [`- name: ${appName}`, ...stampLines(readStageManifest().stamps).map((l) => `- ${l}`)];
|
|
228
294
|
}
|
|
229
295
|
|
|
230
296
|
/**
|
|
231
|
-
* The record as `llms.txt` serves it: the
|
|
232
|
-
* the
|
|
233
|
-
*
|
|
297
|
+
* The record as `llms.txt` serves it: the display title, the record's own
|
|
298
|
+
* description, the stamps that connect this file to one publication, then
|
|
299
|
+
* every MACHINE-admitted document in reading order.
|
|
234
300
|
*
|
|
235
|
-
* Here rather than in the route, because the home page shows
|
|
301
|
+
* Here rather than in the route, because the home page shows the same index
|
|
236
302
|
* to a reader. Two spellings of the record's index would be two indexes, and
|
|
237
303
|
* the one on the page would be the one nobody checked.
|
|
238
304
|
*/
|
|
239
305
|
export function recordIndexText(): string {
|
|
240
|
-
const
|
|
241
|
-
const lines = pages.map((page) => {
|
|
242
|
-
const governance = readGovernance(page.data, page.path);
|
|
243
|
-
const successor =
|
|
244
|
-
governance.supersededBy === null
|
|
245
|
-
? null
|
|
246
|
-
: resolveSuccessorUrl(governance.supersededBy, page.path, pages);
|
|
306
|
+
const lines = getMachinePages().map((page) => {
|
|
247
307
|
const link = `- [${page.data.title}](${basePath}${page.url})`;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
308
|
+
return page.data.description ? `${link}: ${page.data.description}` : link;
|
|
309
|
+
});
|
|
310
|
+
const head = [`# ${appTitle}`, ""];
|
|
311
|
+
if (appDescription !== null) head.push(`> ${appDescription.replace(/\s+/g, " ").trim()}`, "");
|
|
312
|
+
return `${[...head, ...stampBullets(), "", "## Documents", "", ...lines].join("\n")}\n`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The record-root twin, `/md/index.md`: the index REGENERATED over the machine
|
|
317
|
+
* set with the record's own generator (OKF §8 form), under the stamps. A
|
|
318
|
+
* folder's index has no twin — its page is the listing — so this is the one
|
|
319
|
+
* index a consumer can fetch.
|
|
320
|
+
*/
|
|
321
|
+
export function rootIndexTwin(): string {
|
|
322
|
+
const pages = getMachinePages();
|
|
323
|
+
const dirs = new Set<string>();
|
|
324
|
+
for (const page of pages) {
|
|
325
|
+
const parts = pathOf(page).split("/").slice(0, -1);
|
|
326
|
+
for (let i = 1; i <= parts.length; i += 1) dirs.add(parts.slice(0, i).join("/"));
|
|
327
|
+
}
|
|
328
|
+
const generated = generateIndexes({
|
|
329
|
+
title: appTitle,
|
|
330
|
+
concepts: pages.map((page) => ({
|
|
331
|
+
id: pathOf(page).replace(/\.md$/, ""),
|
|
332
|
+
title: page.data.title,
|
|
333
|
+
description: page.data.description ?? "",
|
|
334
|
+
order: typeof page.data.order === "number" ? page.data.order : null,
|
|
335
|
+
})),
|
|
336
|
+
dirs: [...dirs],
|
|
254
337
|
});
|
|
255
|
-
|
|
338
|
+
const root = generated.get("index.md") ?? "";
|
|
339
|
+
const body = root.replace(/^---\n[\s\S]*?\n---\n\n?/, "");
|
|
340
|
+
return `---\nokf_version: "0.2"\n${stampLines(readStageManifest().stamps).join("\n")}\n---\n\n${body}`;
|
|
256
341
|
}
|
|
257
342
|
|
|
258
343
|
/**
|
|
@@ -268,37 +353,22 @@ export function markdownPath(url: string): string {
|
|
|
268
353
|
return `${basePath}/md/${slug === "" ? "index" : slug}.md`;
|
|
269
354
|
}
|
|
270
355
|
|
|
271
|
-
/** The children of the folder whose index page is at `url`, or []. */
|
|
272
|
-
function childrenOfFolder(url: string): Node[] {
|
|
273
|
-
const find = (nodes: readonly Node[]): Node[] | null => {
|
|
274
|
-
for (const node of nodes) {
|
|
275
|
-
if (node.type !== "folder") continue;
|
|
276
|
-
if (node.index?.url === url) return [...node.children];
|
|
277
|
-
const deeper = find(node.children);
|
|
278
|
-
if (deeper !== null) return deeper;
|
|
279
|
-
}
|
|
280
|
-
return null;
|
|
281
|
-
};
|
|
282
|
-
return find(getSortedPageTree().children) ?? [];
|
|
283
|
-
}
|
|
284
|
-
|
|
285
356
|
/**
|
|
286
|
-
* Every
|
|
287
|
-
*
|
|
357
|
+
* Every page's badge label, keyed by route — the small map the search dialog
|
|
358
|
+
* needs on the client.
|
|
288
359
|
*
|
|
289
360
|
* Search was the last surface where a withdrawn document and the one that
|
|
290
361
|
* replaced it looked identical, and its snippet quotes the withdrawn figure
|
|
291
362
|
* (research/site-design.md F3). The dialog runs in the browser over a static
|
|
292
|
-
* index that has no field for
|
|
293
|
-
*
|
|
294
|
-
*
|
|
363
|
+
* index that has no field for it, so the map travels as a prop instead: a few
|
|
364
|
+
* dozen bytes per badged document, and nothing at all for a record whose
|
|
365
|
+
* documents are all current.
|
|
295
366
|
*/
|
|
296
|
-
export function
|
|
367
|
+
export function badgeByUrl(): Record<string, string> {
|
|
297
368
|
const out: Record<string, string> = {};
|
|
298
369
|
for (const page of source.getPages()) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
if (status !== null) out[page.url] = status;
|
|
370
|
+
const label = badgeLabel(stagePageOf(pathOf(page))?.badge ?? null);
|
|
371
|
+
if (label !== null) out[page.url] = label;
|
|
302
372
|
}
|
|
303
373
|
return out;
|
|
304
374
|
}
|