@ferrule-io/ok-fine 0.0.0-stage → 0.3.6

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/dist/main.js ADDED
@@ -0,0 +1,32 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { startServer } from "./server.js";
3
+ let config;
4
+ try {
5
+ config = loadConfig();
6
+ }
7
+ catch (err) {
8
+ console.error(err instanceof Error ? err.message : err);
9
+ process.exit(1);
10
+ }
11
+ let server;
12
+ try {
13
+ server = await startServer(config);
14
+ }
15
+ catch (err) {
16
+ console.error("ok-fine failed to start:", err);
17
+ process.exit(1);
18
+ }
19
+ let shuttingDown = false;
20
+ for (const signal of ["SIGTERM", "SIGINT"]) {
21
+ process.on(signal, () => {
22
+ if (shuttingDown)
23
+ return;
24
+ shuttingDown = true;
25
+ server.app.log.info({ signal }, "shutting down");
26
+ server.close().then(() => process.exit(0), (err) => {
27
+ server.app.log.error({ err }, "shutdown failed");
28
+ process.exit(1);
29
+ });
30
+ });
31
+ }
32
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,202 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import { z } from "zod";
3
+ import { OkfError } from "../errors.js";
4
+ import { FEEDBACK_TYPES, feedbackLink } from "../service/feedback.js";
5
+ import { VERSION } from "../version.js";
6
+ export const INSTRUCTIONS = `ok-fine holds shared project knowledge outside the codebase, as OKF v0.2 markdown concepts grouped into projects. In a git repository, first run \`git remote get-url origin\` and call list_projects with that URL as \`repository\`; use the returned project(s) for every read and write. If none match, say the repository is not onboarded and offer to onboard it (ok-fine-onboard skill). Search before non-trivial work; record durable decisions, conventions, and runbooks afterwards.
7
+ 1. Discover: get_index (progressive disclosure) or search_concepts with \`project\`.
8
+ 2. Read: read_concept returns frontmatter, body, trust tier (unverified | machine-confirmed | human-reviewed), staleness, and links. Freshness comes first: a concept that is stale, or whose code sources changed since \`sources[].commit\`, is a lead to re-check whatever its tier; among fresh concepts prefer higher trust tiers. Deprecated concepts are history; when code contradicts a concept, trust the code and update the concept.
9
+ 3. Write: write_concept with frontmatter containing \`type\` (e.g. Decision, Convention, Architecture, Component, Playbook, Interface, Reference) plus \`title\`, \`description\`, \`tags\`, and \`stale_after\` (ISO 8601, e.g. 180 days ahead). Record provenance in \`sources\` (each with \`resource\` and a stable \`id\`; code sources carry \`commit\`) and cite claims with footnotes [^id]. Link concepts with bundle-absolute links such as [orders](/tables/orders.md). After re-checking a concept against the code, refresh \`sources[].commit\` and \`stale_after\` with write_concept, then call verify_concept.
10
+ 4. Pass \`actor\` as <harness>/<model> (e.g. claude-code/claude-opus-4-5, codex/gpt-5-codex, gemini-cli/gemini-2.5-pro). Use human:<email>, with the email from \`git config user.email\`, only when the user personally reviewed the concept; on forbidden_actor, report both identities instead of retrying as another. The server stamps \`generated\`; \`verified\` changes only through verify_concept.
11
+ 5. When updating, pass expectedRevision from read_concept (null to create only).
12
+ 6. Prefer \`status: deprecated\` over delete_concept. index.md and log.md are maintained by the server; do not write them. A project is bound to repositories through the \`repositories\` list in its overview frontmatter.
13
+ 7. If ok-fine itself misbehaves or lacks something you need, call submit_feedback and show the user the returned url; nothing is filed until they submit the prefilled GitHub issue.`;
14
+ const project = z.string().describe("Project (bundle) name, e.g. payments-api");
15
+ const id = z.string().describe("Concept ID = bundle-relative path without .md, e.g. tables/orders");
16
+ const actor = z
17
+ .string()
18
+ .describe("Who is writing, per the OKF actor convention: <producer>/<version> for agents (e.g. claude-code/claude-opus-4-5), human:<email> (the user's `git config user.email`, matching their token identity) only for personal review, or process:<id>");
19
+ const expectedRevision = z
20
+ .string()
21
+ .nullable()
22
+ .optional()
23
+ .describe("Revision from read_concept; null = must not exist yet; omit to overwrite unconditionally");
24
+ const expectedExisting = z.string().optional().describe("Revision from read_concept; omit to act unconditionally");
25
+ const limit = z.number().int().min(1).max(100).optional();
26
+ function fail(code, message, details) {
27
+ return {
28
+ isError: true,
29
+ content: [{ type: "text", text: `${code}: ${message}` }],
30
+ structuredContent: { error: { code, message, details } },
31
+ };
32
+ }
33
+ export function createMcpServer(service, principal, log) {
34
+ const server = new McpServer({ name: "ok-fine", version: VERSION }, { instructions: INSTRUCTIONS });
35
+ const run = async (name, fn) => {
36
+ try {
37
+ const result = await fn();
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
40
+ structuredContent: result,
41
+ };
42
+ }
43
+ catch (err) {
44
+ if (err instanceof OkfError)
45
+ return fail(err.code, err.message, err.details);
46
+ log.error({ err, tool: name }, "tool failed");
47
+ return fail("internal", "internal error");
48
+ }
49
+ };
50
+ const tool = (name, need, config, fn) => {
51
+ const permitted = need === "admin" ? principal.canAdmin : need === "write" ? principal.canWrite : principal.canRead;
52
+ if (!permitted)
53
+ return;
54
+ // Widen to the concrete ZodObject so the SDK's overload resolves; the SDK has already validated `args`
55
+ // against this schema, so the cast only restores the inferred type.
56
+ const inputSchema = config.inputSchema;
57
+ server.registerTool(name, { ...config, inputSchema }, async (args) => run(name, () => fn(args)));
58
+ };
59
+ const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
60
+ tool("list_projects", "read", {
61
+ title: "List projects",
62
+ description: "List projects (OKF bundles) with concept and staleness counts and the git repositories bound to each; pass repository to find the projects that hold a codebase's knowledge.",
63
+ inputSchema: z.object({
64
+ repository: z
65
+ .string()
66
+ .optional()
67
+ .describe("Git remote URL of the current repository, e.g. the output of `git remote get-url origin`; returns only the projects bound to it"),
68
+ }),
69
+ annotations: readOnly,
70
+ }, (a) => service.listProjects({ repository: a.repository }));
71
+ tool("get_index", "read", {
72
+ title: "Get directory index",
73
+ description: "Progressive-disclosure listing of one bundle directory: its concepts grouped by type, files, and subdirectories.",
74
+ inputSchema: z.object({ project, path: z.string().optional().describe("directory, default bundle root") }),
75
+ annotations: readOnly,
76
+ }, (a) => service.getIndex(a.project, a.path ?? ""));
77
+ tool("read_concept", "read", {
78
+ title: "Read concept",
79
+ description: "Read one concept with frontmatter, body, derived trust/staleness, links, and lint issues; returns `revision` to pass back as expectedRevision when updating.",
80
+ inputSchema: z.object({ project, id }),
81
+ annotations: readOnly,
82
+ }, (a) => service.readConcept(a.project, a.id));
83
+ tool("search_concepts", "read", {
84
+ title: "Search concepts",
85
+ description: "Keyword full-text search over concepts, optionally filtered by project, type, tags, status, trust tier, and staleness.",
86
+ inputSchema: z.object({
87
+ query: z.string().optional(),
88
+ project: z.string().optional(),
89
+ type: z.string().optional(),
90
+ tags: z.array(z.string()).optional(),
91
+ status: z.enum(["draft", "stable", "deprecated"]).optional().describe("omitted hides deprecated"),
92
+ trustTier: z.enum(["unverified", "machine-confirmed", "human-reviewed"]).optional(),
93
+ stale: z.boolean().optional(),
94
+ limit,
95
+ }),
96
+ annotations: readOnly,
97
+ }, (a) => service.search(a));
98
+ tool("get_history", "read", {
99
+ title: "Get history",
100
+ description: "List git commits that changed a concept, or the whole project when id is omitted.",
101
+ inputSchema: z.object({ project, id: id.optional(), limit }),
102
+ annotations: readOnly,
103
+ }, (a) => service.history(a.project, a.id, a.limit));
104
+ tool("read_file", "read", {
105
+ title: "Read file",
106
+ description: "Read any text file in a bundle verbatim (including index.md, log.md, and non-markdown assets).",
107
+ inputSchema: z.object({ project, path: z.string().describe("bundle-relative file path") }),
108
+ annotations: readOnly,
109
+ }, (a) => service.readFile(a.project, a.path));
110
+ tool("lint_project", "read", {
111
+ title: "Lint project",
112
+ description: "Check a bundle for OKF v0.2 conformance and return errors, warnings, and info issues.",
113
+ inputSchema: z.object({ project }),
114
+ annotations: readOnly,
115
+ }, (a) => service.lint(a.project));
116
+ tool("submit_feedback", "read", {
117
+ title: "Submit feedback",
118
+ description: "Draft feedback about ok-fine itself (a bug, a confusing or missing tool, wrong results, or a feature gap) as a prefilled public GitHub issue on ferrule-io/ok-fine. This call files nothing: show the returned url to the user, who opens it, reviews it, and submits it with their own GitHub account. The issue is public: never include concept content, project names, repository URLs, credentials, or personal data. Not for problems in project knowledge; fix those with write_concept.",
119
+ inputSchema: z.object({
120
+ type: z.enum(FEEDBACK_TYPES).describe("bug | feature | general"),
121
+ title: z.string().min(5).max(160).describe("Short summary of the problem or request"),
122
+ body: z.string().min(20).max(6000).describe("What happened or what is needed, in detail"),
123
+ tool: z.string().max(100).optional().describe("ok-fine tool involved, e.g. write_concept"),
124
+ expected: z.string().max(2000).optional().describe("Expected behavior"),
125
+ actual: z.string().max(2000).optional().describe("Actual behavior, including any error code returned"),
126
+ reproduction: z
127
+ .string()
128
+ .max(2000)
129
+ .optional()
130
+ .describe("Minimal steps a maintainer can replay, with no private data"),
131
+ }),
132
+ annotations: readOnly,
133
+ }, (a) => feedbackLink(a));
134
+ tool("create_project", "write", {
135
+ title: "Create project",
136
+ description: "Create a new project bundle with an overview concept, log.md, and index.md.",
137
+ inputSchema: z.object({ project, title: z.string(), description: z.string().optional(), actor }),
138
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
139
+ }, (a) => service.createProject(principal, a));
140
+ tool("write_concept", "write", {
141
+ title: "Write concept",
142
+ description: "Create or replace a concept's whole frontmatter and body; the server stamps `generated` and ignores `verified` (use verify_concept).",
143
+ inputSchema: z.object({
144
+ project,
145
+ id,
146
+ frontmatter: z.record(z.string(), z.unknown()).describe("YAML frontmatter as an object; `type` is required"),
147
+ body: z.string().describe("Markdown body"),
148
+ actor,
149
+ expectedRevision,
150
+ message: z.string().optional().describe("Optional note recorded in log.md and the commit"),
151
+ }),
152
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
153
+ }, (a) => service.writeConcept(principal, a));
154
+ tool("verify_concept", "write", {
155
+ title: "Verify concept",
156
+ description: "Append a verification by `actor` to a concept's `verified` list, raising its trust tier.",
157
+ inputSchema: z.object({ project, id, actor, expectedRevision: expectedExisting }),
158
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
159
+ }, (a) => service.verifyConcept(principal, a));
160
+ tool("delete_concept", "write", {
161
+ title: "Delete concept",
162
+ description: "Permanently remove a concept; prefer write_concept with `status: deprecated` to keep history visible.",
163
+ inputSchema: z.object({ project, id, actor, expectedRevision: expectedExisting }),
164
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
165
+ }, (a) => service.deleteConcept(principal, a));
166
+ tool("write_file", "write", {
167
+ title: "Write file",
168
+ description: "Create or replace a non-markdown asset file in a bundle (e.g. references/attesters/x.py).",
169
+ inputSchema: z.object({
170
+ project,
171
+ path: z.string().describe("bundle-relative path; must not end with .md"),
172
+ content: z.string(),
173
+ actor,
174
+ expectedRevision,
175
+ }),
176
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
177
+ }, (a) => service.writeFile(principal, a));
178
+ tool("delete_file", "write", {
179
+ title: "Delete file",
180
+ description: "Remove a non-markdown asset file from a bundle.",
181
+ inputSchema: z.object({ project, path: z.string(), actor, expectedRevision: expectedExisting }),
182
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
183
+ }, (a) => service.deleteFile(principal, a));
184
+ tool("delete_project", "admin", {
185
+ title: "Delete project",
186
+ description: "Delete an entire project bundle; `confirm` must repeat the project name.",
187
+ inputSchema: z.object({ project, actor, confirm: z.string().describe("must equal project") }),
188
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
189
+ }, (a) => {
190
+ if (a.confirm !== a.project)
191
+ throw new OkfError("bad_request", 400, "confirm must equal project");
192
+ return service.deleteProject(principal, { project: a.project, actor: a.actor });
193
+ });
194
+ tool("sync_now", "admin", {
195
+ title: "Sync now",
196
+ description: "Fetch, rebase onto, and push to the configured git remote immediately and return sync status.",
197
+ inputSchema: z.object({}),
198
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
199
+ }, () => service.syncNow());
200
+ return server;
201
+ }
202
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,99 @@
1
+ import { parseFrontmatter, splitFrontmatter } from "./frontmatter.js";
2
+ import { extractLinks } from "./markdown.js";
3
+ import { blobRevision } from "./paths.js";
4
+ import { displayTitle, effectiveStatus, generatedAt, generatedBy, lastVerifiedAt, staleAfter, trustTier, } from "./semantics.js";
5
+ export function parseConcept(project, id, file, buf) {
6
+ const text = buf.toString("utf8");
7
+ const revision = blobRevision(buf);
8
+ const split = splitFrontmatter(text);
9
+ if (!split) {
10
+ return {
11
+ project,
12
+ id,
13
+ file,
14
+ revision,
15
+ frontmatter: null,
16
+ body: text,
17
+ parseError: "missing_frontmatter",
18
+ title: displayTitle(null, id),
19
+ description: null,
20
+ type: null,
21
+ tags: [],
22
+ status: "stable",
23
+ trustTier: "unverified",
24
+ staleAfter: null,
25
+ generatedAt: null,
26
+ generatedBy: null,
27
+ lastVerifiedAt: null,
28
+ outbound: [],
29
+ };
30
+ }
31
+ const parsed = parseFrontmatter(split.yaml);
32
+ if ("error" in parsed) {
33
+ return {
34
+ project,
35
+ id,
36
+ file,
37
+ revision,
38
+ frontmatter: null,
39
+ body: split.body,
40
+ parseError: parsed.error,
41
+ title: displayTitle(null, id),
42
+ description: null,
43
+ type: null,
44
+ tags: [],
45
+ status: "stable",
46
+ trustTier: "unverified",
47
+ staleAfter: null,
48
+ generatedAt: null,
49
+ generatedBy: null,
50
+ lastVerifiedAt: null,
51
+ outbound: [],
52
+ };
53
+ }
54
+ const { data } = parsed;
55
+ if (typeof data.type !== "string" || data.type.trim().length === 0) {
56
+ return {
57
+ project,
58
+ id,
59
+ file,
60
+ revision,
61
+ frontmatter: null,
62
+ body: split.body,
63
+ parseError: "missing_type",
64
+ title: displayTitle(data, id),
65
+ description: typeof data.description === "string" ? data.description : null,
66
+ type: null,
67
+ tags: [],
68
+ status: effectiveStatus(data),
69
+ trustTier: trustTier(data),
70
+ staleAfter: staleAfter(data),
71
+ generatedAt: generatedAt(data),
72
+ generatedBy: generatedBy(data),
73
+ lastVerifiedAt: lastVerifiedAt(data),
74
+ outbound: extractLinks(split.body, id),
75
+ };
76
+ }
77
+ const tags = Array.isArray(data.tags) ? data.tags.filter((t) => typeof t === "string") : [];
78
+ return {
79
+ project,
80
+ id,
81
+ file,
82
+ revision,
83
+ frontmatter: data,
84
+ body: split.body,
85
+ parseError: null,
86
+ title: displayTitle(data, id),
87
+ description: typeof data.description === "string" ? data.description : null,
88
+ type: data.type,
89
+ tags,
90
+ status: effectiveStatus(data),
91
+ trustTier: trustTier(data),
92
+ staleAfter: staleAfter(data),
93
+ generatedAt: generatedAt(data),
94
+ generatedBy: generatedBy(data),
95
+ lastVerifiedAt: lastVerifiedAt(data),
96
+ outbound: extractLinks(split.body, id),
97
+ };
98
+ }
99
+ //# sourceMappingURL=concept.js.map
@@ -0,0 +1,180 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { Document, isMap, parseDocument } from "yaml";
3
+ export function splitFrontmatter(text) {
4
+ if (text.charCodeAt(0) === 0xfeff) {
5
+ text = text.slice(1);
6
+ }
7
+ const match = /^---\r?\n/.exec(text);
8
+ if (!match) {
9
+ return null;
10
+ }
11
+ const yamlStart = match[0].length;
12
+ const closingRegex = /\r?\n---\r?\n|\r?\n---$/g;
13
+ closingRegex.lastIndex = yamlStart;
14
+ const closeMatch = closingRegex.exec(text);
15
+ if (!closeMatch) {
16
+ return null;
17
+ }
18
+ const yamlEnd = closeMatch.index;
19
+ const bodyStart = closeMatch.index + closeMatch[0].length;
20
+ const yaml = text.slice(yamlStart, yamlEnd);
21
+ let body = text.slice(bodyStart);
22
+ body = body.replace(/^(\r?\n)+/, "");
23
+ return { yaml, body };
24
+ }
25
+ export function parseFrontmatter(yaml) {
26
+ const doc = parseDocument(yaml);
27
+ if (doc.errors.length > 0) {
28
+ return {
29
+ error: "invalid_yaml",
30
+ message: doc.errors.map((e) => e.message).join("; "),
31
+ };
32
+ }
33
+ if (!doc.contents || !isMap(doc.contents)) {
34
+ return {
35
+ error: "frontmatter_not_mapping",
36
+ message: "frontmatter must be a YAML mapping",
37
+ };
38
+ }
39
+ const data = (doc.toJS() ?? {});
40
+ return { doc, data };
41
+ }
42
+ export function serializeConcept(doc, body) {
43
+ const normalizedBody = body.replace(/\r\n/g, "\n").replace(/^\n+/, "").replace(/\s*$/, "");
44
+ return `---\n${doc.toString({ lineWidth: 0 })}---\n\n${normalizedBody}\n`;
45
+ }
46
+ function createFormattedNode(doc, key, val) {
47
+ if (key === "tags" && Array.isArray(val)) {
48
+ return doc.createNode(val, { flow: true });
49
+ }
50
+ if (key === "usage_window" && val && typeof val === "object" && !Array.isArray(val)) {
51
+ return doc.createNode(val, { flow: true });
52
+ }
53
+ if (key === "parameters" && Array.isArray(val)) {
54
+ return doc.createNode(val.map((item) => item && typeof item === "object" && !Array.isArray(item) ? doc.createNode(item, { flow: true }) : item));
55
+ }
56
+ if (key === "generated" && val && typeof val === "object") {
57
+ return doc.createNode(val, { flow: true });
58
+ }
59
+ return val;
60
+ }
61
+ export function applyFrontmatter(existing, input, server) {
62
+ const ignoredKeys = [];
63
+ if (existing !== null) {
64
+ const existingJs = (existing.toJS() ?? {});
65
+ if ("generated" in input && !isDeepStrictEqual(input.generated, existingJs.generated)) {
66
+ ignoredKeys.push("generated");
67
+ }
68
+ if ("verified" in input && !isDeepStrictEqual(input.verified, existingJs.verified)) {
69
+ ignoredKeys.push("verified");
70
+ }
71
+ const cleanInput = {};
72
+ for (const [k, v] of Object.entries(input)) {
73
+ if (k !== "generated" && k !== "verified") {
74
+ cleanInput[k] = v;
75
+ }
76
+ }
77
+ const keysToDelete = [];
78
+ if (isMap(existing.contents)) {
79
+ for (const item of existing.contents.items) {
80
+ if (!item || typeof item !== "object" || !("key" in item) || item.key == null) {
81
+ continue;
82
+ }
83
+ let keyName = "";
84
+ const rawKey = item.key;
85
+ if (typeof rawKey === "string") {
86
+ keyName = rawKey;
87
+ }
88
+ else if (typeof rawKey === "object" && "value" in rawKey && typeof rawKey.value === "string") {
89
+ keyName = rawKey.value;
90
+ }
91
+ else {
92
+ keyName = String(rawKey);
93
+ }
94
+ if (keyName && keyName !== "generated" && keyName !== "verified" && !(keyName in cleanInput)) {
95
+ keysToDelete.push(keyName);
96
+ }
97
+ }
98
+ }
99
+ for (const k of keysToDelete) {
100
+ existing.delete(k);
101
+ }
102
+ for (const [k, v] of Object.entries(cleanInput)) {
103
+ if (existing.has(k)) {
104
+ if (!isDeepStrictEqual(existingJs[k], v)) {
105
+ existing.set(k, createFormattedNode(existing, k, v));
106
+ }
107
+ }
108
+ else {
109
+ existing.set(k, createFormattedNode(existing, k, v));
110
+ }
111
+ }
112
+ existing.set("generated", existing.createNode(server.generated, { flow: true }));
113
+ return { doc: existing, ignoredKeys };
114
+ }
115
+ else {
116
+ if ("generated" in input) {
117
+ ignoredKeys.push("generated");
118
+ }
119
+ if ("verified" in input) {
120
+ ignoredKeys.push("verified");
121
+ }
122
+ const cleanInput = {};
123
+ for (const [k, v] of Object.entries(input)) {
124
+ if (k !== "generated" && k !== "verified") {
125
+ cleanInput[k] = v;
126
+ }
127
+ }
128
+ const doc = new Document();
129
+ const firstKeys = ["type", "title", "description", "resource", "tags", "status"];
130
+ const endKeys = ["stale_after", "sources", "usage_window"];
131
+ for (const k of firstKeys) {
132
+ if (k in cleanInput) {
133
+ doc.set(k, createFormattedNode(doc, k, cleanInput[k]));
134
+ }
135
+ }
136
+ for (const [k, v] of Object.entries(cleanInput)) {
137
+ if (!firstKeys.includes(k) && !endKeys.includes(k)) {
138
+ doc.set(k, createFormattedNode(doc, k, v));
139
+ }
140
+ }
141
+ doc.set("generated", doc.createNode(server.generated, { flow: true }));
142
+ for (const k of endKeys) {
143
+ if (k in cleanInput) {
144
+ doc.set(k, createFormattedNode(doc, k, cleanInput[k]));
145
+ }
146
+ }
147
+ return { doc, ignoredKeys };
148
+ }
149
+ }
150
+ export function appendVerification(doc, entry) {
151
+ const js = (doc.toJS() ?? {});
152
+ const raw = js.verified;
153
+ let list = [];
154
+ if (raw && typeof raw === "object") {
155
+ if (Array.isArray(raw)) {
156
+ list = raw.filter((item) => Boolean(item && typeof item === "object" && typeof item.by === "string" && item.by.length > 0));
157
+ }
158
+ else {
159
+ if ("by" in raw && typeof raw.by === "string" && raw.by.length > 0) {
160
+ const atVal = "at" in raw && typeof raw.at === "string" ? raw.at : undefined;
161
+ list = [{ by: raw.by, at: atVal }];
162
+ }
163
+ }
164
+ }
165
+ list.push(entry);
166
+ if (list.length === 1) {
167
+ doc.set("verified", doc.createNode(list[0], { flow: true }));
168
+ }
169
+ else {
170
+ doc.set("verified", doc.createNode(list.map((e) => doc.createNode(e, { flow: true }))));
171
+ }
172
+ }
173
+ export function nowIso() {
174
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
175
+ }
176
+ export function isoAfterDays(now, days) {
177
+ const d = new Date(now.getTime() + days * 86_400_000);
178
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
179
+ }
180
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1,74 @@
1
+ export function renderIndex(listing) {
2
+ const sections = [];
3
+ // Group concepts by type
4
+ const byType = new Map();
5
+ for (const c of listing.concepts) {
6
+ const typeKey = c.type && c.type.length > 0 ? c.type : "Other";
7
+ let list = byType.get(typeKey);
8
+ if (!list) {
9
+ list = [];
10
+ byType.set(typeKey, list);
11
+ }
12
+ list.push({ file: c.file, title: c.title, description: c.description });
13
+ }
14
+ const sortedTypes = Array.from(byType).sort(([a], [b]) => a.localeCompare(b, "en"));
15
+ for (const [type, items] of sortedTypes) {
16
+ items.sort((a, b) => {
17
+ const titleCmp = a.title.toLowerCase().localeCompare(b.title.toLowerCase(), "en");
18
+ if (titleCmp !== 0) {
19
+ return titleCmp;
20
+ }
21
+ return a.file.localeCompare(b.file, "en");
22
+ });
23
+ const lines = [`# ${type}`];
24
+ for (const item of items) {
25
+ const uri = encodeURI(item.file);
26
+ if (item.description != null) {
27
+ const collapsedDesc = item.description.replace(/\s+/g, " ").trim();
28
+ lines.push(`* [${item.title}](${uri}) - ${collapsedDesc}`);
29
+ }
30
+ else {
31
+ lines.push(`* [${item.title}](${uri})`);
32
+ }
33
+ }
34
+ sections.push(lines.join("\n"));
35
+ }
36
+ // Non-markdown files
37
+ if (listing.files.length > 0) {
38
+ const sortedFiles = listing.files.slice().sort((a, b) => a.localeCompare(b, "en"));
39
+ const lines = ["# Files"];
40
+ for (const f of sortedFiles) {
41
+ lines.push(`* [${f}](${encodeURI(f)})`);
42
+ }
43
+ sections.push(lines.join("\n"));
44
+ }
45
+ // Directories
46
+ if (listing.dirs.length > 0) {
47
+ const sortedDirs = listing.dirs.slice().sort((a, b) => a.name.localeCompare(b.name, "en"));
48
+ const lines = ["# Directories"];
49
+ for (const d of sortedDirs) {
50
+ const uri = `${encodeURI(d.name)}/`;
51
+ let countText;
52
+ if (d.conceptCount > 0) {
53
+ countText = d.conceptCount === 1 ? "1 concept" : `${d.conceptCount} concepts`;
54
+ }
55
+ else {
56
+ countText = d.fileCount === 1 ? "1 file" : `${d.fileCount} files`;
57
+ }
58
+ lines.push(`* [${d.name}](${uri}) - ${countText}`);
59
+ }
60
+ sections.push(lines.join("\n"));
61
+ }
62
+ let body;
63
+ if (sections.length === 0) {
64
+ body = "# Concepts\n";
65
+ }
66
+ else {
67
+ body = `${sections.join("\n\n")}\n`;
68
+ }
69
+ if (listing.isRoot) {
70
+ return `---\nokf_version: "0.2"\n---\n\n${body}`;
71
+ }
72
+ return body;
73
+ }
74
+ //# sourceMappingURL=index-file.js.map