Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/okf-conformant-starter-pack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/open-knowledge": minor
---

Add an OKF-conformant starter pack: `ok seed --pack okf` (also shown by `ok seed --list-packs`) scaffolds a small knowledge base that is conformant with Google's Open Knowledge Format (OKF) v0.1 from the first commit — every non-reserved doc carries a non-empty `type`, plus a frontmatter-free lowercase `index.md` (§6 navigation) and `log.md` (§7 change history). Pure pre-populated content: the native frontmatter schema stays open-shaped and nothing is enforced or linted. Ships a guidance-only OKF conventions skill like every other pack.
14 changes: 12 additions & 2 deletions packages/app/src/components/PackCardGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
// biome-ignore-all lint/plugin/no-raw-html-interactive-element: pre-rule backlog — file uses raw <button>/<input>/<textarea> awaiting shadcn migration; tracked at https://github.com/inkeep/open-knowledge/blob/main/biome-plugins/README.md#no-raw-html-interactive-elementgrit
import { Trans, useLingui } from '@lingui/react/macro';
import { Compass, GitBranch, Library, Loader2, Network, PenLine, StickyNote } from 'lucide-react';
import {
Compass,
FileCheck,
GitBranch,
Library,
Loader2,
Network,
PenLine,
StickyNote,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import type { OkPackId, OkSeedPackInfo } from '@/lib/desktop-bridge-types';
import { seedClient } from '@/lib/seed-client';
Expand All @@ -13,6 +22,7 @@ const PACK_ICONS: Record<OkPackId, React.ComponentType<{ className?: string }>>
worldbuilding: Compass,
'writing-pipeline': PenLine,
'entity-vault': Network,
okf: FileCheck,
};

function iconForPack(id: string): React.ComponentType<{ className?: string }> {
Expand Down Expand Up @@ -74,7 +84,7 @@ export function PackCardGrid({ onPackSelect, className, packs: externalPacks }:
aria-busy="true"
aria-label={t`Loading starter packs`}
>
{Array.from({ length: 6 }, (_, i) => i).map((i) => (
{Array.from({ length: Object.keys(PACK_ICONS).length }, (_, i) => i).map((i) => (
<PackCardSkeleton key={`skeleton-${i}`} />
))}
</div>
Expand Down
3 changes: 2 additions & 1 deletion packages/app/src/lib/desktop-bridge-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export type OkPackId =
| 'plain-notes'
| 'worldbuilding'
| 'writing-pipeline'
| 'entity-vault';
| 'entity-vault'
| 'okf';

interface OkSeedPlanOptions {
rootDir?: string;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/desktop-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ type OkPackId =
| 'plain-notes'
| 'worldbuilding'
| 'writing-pipeline'
| 'entity-vault';
| 'entity-vault'
| 'okf';

interface OkSeedPlanOptions {
rootDir?: string;
Expand Down
52 changes: 52 additions & 0 deletions packages/server/assets/skills/packs/okf/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: open-knowledge-pack-okf
description: "How to work in an OKF starter project (the `okf` starter pack): a knowledge base that is conformant with Google's Open Knowledge Format (OKF) from commit one — `concepts/`, `references/`, `notes/`, a reserved `index.md` navigation hub, and a reserved `log.md` change history. Read when the project has these folders + reserved files. Carries the OKF conventions (non-empty `type` on every non-reserved doc; reserved files carry no frontmatter) as guidance, not enforcement. Complements the platform `open-knowledge` skill; does not replace it."
compatibility: "Claude Code, Claude Desktop, Claude Cowork, Claude.ai web. Requires Open Knowledge MCP server. Installed project-local by `ok seed --pack okf`."
# `type` keeps this skill doc OKF-conformant: it installs as project-local
# markdown under `.claude`/`.cursor`/`.agents` skills dirs, which OK admits into
# the content corpus — so without a non-empty `type` it would be a non-reserved
# doc that violates the pack's own "every non-reserved doc has a `type`" contract.
type: Document
metadata:
pack: "okf"
author: "Inkeep"
repository: "https://github.com/inkeep/open-knowledge"
---
# OKF starter pack — how to work here

This project was scaffolded to be conformant with **Google's Open Knowledge Format (OKF) v0.1** from the first commit — markdown + YAML frontmatter, a standard-markdown link graph, and two reserved files. Conformance here is pre-populated, **not enforced**: Open Knowledge's native frontmatter schema stays open-shaped, nothing is linted, and you are free to author however you like. This skill explains the conventions so the kit stays OKF-portable as it grows.

> This skill is pack guidance. The platform `open-knowledge` skill (read/write/preview/grounding rules) still governs every markdown operation — this layers OKF conventions on top.

## The one rule (keep the kit conformant)

OKF requires exactly one thing of every **non-reserved** document: a **non-empty `type`** in its frontmatter. That is the whole conformance contract for your content.

- The value is **yours to choose** — `concept`, `reference`, `note`, `person`, `event`, anything that fits. There is no blessed taxonomy.
- `Document` is a fine **generic fallback** when nothing more specific fits (it is just a non-empty value, not a special keyword).
- The folder templates already set a sensible `type` per section — create docs with `write({ document: { path, template: "<name>" } })` and you inherit it.

## Folders

- **`concepts/`** — durable ideas and definitions, one file per concept (`type: concept`).
- **`references/`** — external sources and citations you rely on (`type: reference`).
- **`notes/`** — working notes and observations (`type: note`).

Link liberally with **standard markdown links** (`[text](./path.md)`) — the value is the graph that emerges from the links between typed docs, and standard links keep that graph portable to any OKF consumer. (Open Knowledge also accepts `[[wiki-link]]` shorthand as a native superset, and the OKF export normalizes it to standard links — but seeded content uses standard links so the bundle is conformant as-is.)

## Reserved files (keep them frontmatter-free)

OKF reserves two lowercase files at the project root. **Neither carries frontmatter** — adding any frontmatter to a reserved file breaks OKF conformance.

- **`index.md`** (OKF §6) — the navigation hub: a link-list to the key docs and sections. Keep it current as you add important docs; it is how a reader (or a strict OKF consumer) finds their way in.
- **`log.md`** (OKF §7) — the change history: newest-first dated entries shaped `## YYYY-MM-DD: <summary>`. Add an entry whenever you create, edit, or restructure content. The seed ships a prose instruction documenting this format — add your first dated entry on your first edit.

The tool does not keep these live for you (that would be enforcement) — maintaining them is part of authoring here.

## What stays OKF-portable

- Every non-reserved doc has a non-empty `type`. ✅
- `index.md` / `log.md` stay lowercase and frontmatter-free. ✅
- Links use standard markdown / wiki-link syntax. ✅

If you ever want to hand this knowledge base to a strict OKF consumer, those three habits are all it takes.
204 changes: 204 additions & 0 deletions packages/server/src/seed/okf-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { describe, expect, test } from 'bun:test';
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, relative } from 'node:path';
import {
parseFrontmatterYaml,
stripFrontmatter,
unwrapFrontmatterFences,
} from '@inkeep/open-knowledge-core';
import { applySubstitution } from '../content/substitution.ts';
import { applySeed } from './apply.ts';
import { planSeed } from './plan.ts';
import { OKF_RESERVED_FILENAMES, STARTER_PACKS } from './starter.ts';

const OKF_PACK = STARTER_PACKS.okf;
const OKF_INDEX_BODY = OKF_PACK.rootFiles?.['index.md'];
const OKF_LOG_BODY = OKF_PACK.rootFiles?.['log.md'];
if (!OKF_INDEX_BODY || !OKF_LOG_BODY) {
throw new Error('okf pack is missing its reserved index.md / log.md root files');
}

const RESERVED_FILES = new Set(OKF_RESERVED_FILENAMES);

function collectMarkdown(root: string, dir = root): string[] {
const out: string[] = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const abs = join(dir, entry.name);
if (entry.isDirectory()) {
out.push(...collectMarkdown(root, abs));
} else if (entry.name.endsWith('.md')) {
out.push(relative(root, abs));
}
}
return out;
}

function consumerFrontmatterYaml(relPath: string, raw: string): string | null {
const isTemplate = relPath.includes('/.ok/templates/');
const docSource = isTemplate
? applySubstitution(stripFrontmatter(raw).body, { date: '2026-01-01', user: 'Test User' })
: raw;
const { frontmatter } = stripFrontmatter(docSource);
if (frontmatter === '') return null;
return unwrapFrontmatterFences(frontmatter);
}

async function seedOkf(): Promise<{ projectDir: string; cleanup: () => Promise<void> }> {
const projectDir = await mkdtemp(join(tmpdir(), 'seed-okf-'));
mkdirSync(join(projectDir, '.ok'), { recursive: true });
writeFileSync(join(projectDir, '.ok', 'config.yml'), '', 'utf-8');
const plan = await planSeed({ projectDir, packId: 'okf' });
const result = await applySeed(plan, { projectDir, packId: 'okf' });
expect(result.errors).toEqual([]);
return {
projectDir,
cleanup: () => rm(projectDir, { recursive: true, force: true }),
};
}

describe('okf pack — OKF §9 conformance by construction', () => {
test('rule 1+2: every non-reserved seed .md parses to frontmatter with a non-empty type', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
const docs = collectMarkdown(projectDir).filter((p) => !RESERVED_FILES.has(p));
expect(docs).toContain('welcome.md');
expect(docs.length).toBeGreaterThanOrEqual(4);

for (const relPath of docs) {
const raw = readFileSync(join(projectDir, relPath), 'utf-8');
const yaml = consumerFrontmatterYaml(relPath, raw);
expect(yaml, `${relPath}: rule 1 — no parseable frontmatter block`).not.toBeNull();

const parsed = parseFrontmatterYaml(yaml ?? '');
expect(
parsed.map,
`${relPath}: rule 1 — frontmatter failed to parse (${parsed.parseError ?? ''})`,
).not.toBeNull();

const type = parsed.map?.type;
expect(
typeof type === 'string' && type.trim().length > 0,
`${relPath}: rule 2 — \`type\` must be a non-empty string, got ${JSON.stringify(type)}`,
).toBe(true);
}
} finally {
await cleanup();
}
});

test('rule 3: reserved index.md/log.md are lowercase, present at root, and carry ZERO frontmatter', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
for (const reserved of RESERVED_FILES) {
const abs = join(projectDir, reserved);
expect(existsSync(abs), `${reserved} must be seeded at the bundle root`).toBe(true);
const raw = readFileSync(abs, 'utf-8');
expect(raw.startsWith('---'), `${reserved} must NOT carry frontmatter`).toBe(false);
expect(stripFrontmatter(raw).frontmatter, `${reserved} has a frontmatter block`).toBe('');
}
} finally {
await cleanup();
}
});

test('rule 3: index.md matches OKF §6 navigation structure (H1 + standard-markdown link list)', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
const index = readFileSync(join(projectDir, 'index.md'), 'utf-8');
expect(index.startsWith('# '), 'index.md should open with an H1 navigation heading').toBe(
true,
);
for (const link of [
'[welcome](./welcome.md)',
'[concepts/](./concepts/)',
'[references/](./references/)',
'[notes/](./notes/)',
]) {
expect(index, `index.md should link ${link} in standard markdown`).toContain(link);
}
expect(index, 'seeded nav must not use [[wiki-link]] shorthand').not.toMatch(
/\[\[[^\]]+\]\]/,
);
} finally {
await cleanup();
}
});

test('rule 3: log.md matches OKF §7 change-history structure (H1 + documents the dated-entry format)', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
const log = readFileSync(join(projectDir, 'log.md'), 'utf-8');
expect(log.startsWith('# '), 'log.md should open with an H1 heading').toBe(true);
expect(log).toMatch(/## YYYY-MM-DD: <summary>/);
expect(log.toLowerCase()).toContain('change history');
} finally {
await cleanup();
}
});

test('apply writes the reserved-file bodies to disk verbatim (no {{date}} substitution on root files)', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
expect(readFileSync(join(projectDir, 'index.md'), 'utf-8')).toBe(OKF_INDEX_BODY);
expect(readFileSync(join(projectDir, 'log.md'), 'utf-8')).toBe(OKF_LOG_BODY);
} finally {
await cleanup();
}
});

test('idempotent + non-destructive: a second seed writes nothing new and never overwrites', async () => {
const { projectDir, cleanup } = await seedOkf();
try {
const welcomeAbs = join(projectDir, 'welcome.md');
writeFileSync(welcomeAbs, 'EDITED BY USER\n', 'utf-8');

const plan2 = await planSeed({ projectDir, packId: 'okf' });
expect(plan2.created, 're-run should plan zero new writes').toEqual([]);
const result2 = await applySeed(plan2, { projectDir, packId: 'okf' });
expect(result2.errors).toEqual([]);
expect(result2.applied).toBe(0);
expect(readFileSync(welcomeAbs, 'utf-8')).toBe('EDITED BY USER\n');
} finally {
await cleanup();
}
});

test('rule 2 holds with an editor present: the installed pack skill markdown carries a non-empty type', async () => {
const projectDir = await mkdtemp(join(tmpdir(), 'seed-okf-skill-'));
try {
mkdirSync(join(projectDir, '.ok'), { recursive: true });
writeFileSync(join(projectDir, '.ok', 'config.yml'), '', 'utf-8');
const platformSkillDir = join(projectDir, '.claude', 'skills', 'open-knowledge');
mkdirSync(platformSkillDir, { recursive: true });
writeFileSync(
join(platformSkillDir, 'SKILL.md'),
'---\nname: open-knowledge\n---\n',
'utf-8',
);

const plan = await planSeed({ projectDir, packId: 'okf' });
const result = await applySeed(plan, { projectDir, packId: 'okf' });
expect(result.errors).toEqual([]);
expect(result.packSkillsInstalled).toContain('Claude Code');

const packSkillDir = join(projectDir, '.claude', 'skills', 'open-knowledge-pack-okf');
const skillDocs = collectMarkdown(packSkillDir).map((p) => join(packSkillDir, p));
expect(skillDocs.length).toBeGreaterThanOrEqual(1);
for (const abs of skillDocs) {
const raw = readFileSync(abs, 'utf-8');
const { frontmatter } = stripFrontmatter(raw);
expect(frontmatter, `${abs}: installed skill doc must carry frontmatter`).not.toBe('');
const parsed = parseFrontmatterYaml(unwrapFrontmatterFences(frontmatter));
const type = parsed.map?.type;
expect(
typeof type === 'string' && type.trim().length > 0,
`${abs}: installed skill doc must carry a non-empty \`type\` (OKF rule 2), got ${JSON.stringify(type)}`,
).toBe(true);
}
} finally {
await rm(projectDir, { recursive: true, force: true });
}
});
});
11 changes: 8 additions & 3 deletions packages/server/src/seed/starter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test';
import {
buildStarterFolderFrontmatterYaml,
listStarterPacks,
OKF_RESERVED_FILENAMES,
STARTER_FOLDER_FRONTMATTER_FILENAME,
STARTER_PACK_IDS,
STARTER_PACKS,
Expand Down Expand Up @@ -136,11 +137,12 @@ describe('STARTER_FOLDER_FRONTMATTER_FILENAME', () => {
});

describe('STARTER_PACKS — all packs structural validation', () => {
test('STARTER_PACK_IDS contains exactly the 6 expected packs (pinned to detect silent additions/deletions)', () => {
expect(STARTER_PACK_IDS.length).toBe(6);
test('STARTER_PACK_IDS contains exactly the 7 expected packs (pinned to detect silent additions/deletions)', () => {
expect(STARTER_PACK_IDS.length).toBe(7);
expect([...STARTER_PACK_IDS].sort()).toEqual([
'entity-vault',
'knowledge-base',
'okf',
'plain-notes',
'software-lifecycle',
'worldbuilding',
Expand Down Expand Up @@ -254,9 +256,12 @@ describe('STARTER_PACKS — all packs structural validation', () => {
}
});

test('every rootFile body has frontmatter with a non-empty title', () => {
const OKF_RESERVED_ROOTFILES = new Set(OKF_RESERVED_FILENAMES);

test('every non-reserved rootFile body has frontmatter with a non-empty title', () => {
for (const pack of Object.values(STARTER_PACKS)) {
for (const [filename, body] of Object.entries(pack.rootFiles ?? {})) {
if (pack.id === 'okf' && OKF_RESERVED_ROOTFILES.has(filename)) continue;
expect(
body.startsWith('---\n'),
`Pack "${pack.id}" rootFile "${filename}" missing frontmatter`,
Expand Down
Loading