Background
Google Cloud released OKF v0.2 (2026-07-24): an open, vendor-neutral format for knowledge as a directory of Markdown files with YAML frontmatter, authored by people and agents.
Spec: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
The studio currently exports json | markdown | html | pdf | docx | encrypted (src/components/studio/views/export-types.ts). The Markdown export concatenates all entities into a single .md with non-standard frontmatter. ADR 010 flags the missing canonical schema and missing round-trip import.
OKF v0.2 gives us: (a) agent-readable exports with zero SDK, (b) trust/provenance/freshness signals for LLM-generated knowledge, (c) a governed schema closing ADR 010's round-trip gap.
Spec requirements to implement
- Bundle = directory tree of
.md files; only type frontmatter REQUIRED
- Reserved files:
index.md (root may carry okf_version: "0.2"), log.md (date-grouped, newest first)
- Provenance:
sources[] (id, resource REQUIRED, title, author, usage_count, last_modified) + sibling usage_window; per-claim attribution via footnotes keyed to sources[].id
- Trust:
generated: { by, at }, verified: [{ by, at }]; bare mapping MUST be treated as 1-element list; actor convention human:<id> | <producer>/<version> | process:<id>
- Lifecycle:
status: draft|stable|deprecated (absent ⇒ stable), stale_after: YYYY-MM-DD (stale when today >= stale_after)
- New concept type
Attested Computation: runtime REQUIRED, parameters, computation, executor, attester
- Conformance §11: consumers MUST NOT reject unknown types/keys, broken links, or missing optional fields
Code changes (file by file)
1. NEW src/lib/okf/types.ts — Zod schemas mirroring the spec
import { z } from 'zod'
/** OKF actor convention (§7): human:<id> | process:<id> | <producer>/<version> */
export const OkfActorSchema = z
.string()
.regex(/^(human:|process:|[\w.-]+\/).+$/, 'invalid OKF actor')
export const OkfIsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
export const OkfSourceSchema = z.object({
id: z.string().optional(), // stable join key for footnote attribution (§5.1)
resource: z.string().min(1), // REQUIRED within an entry (§5.1)
title: z.string().optional(),
author: OkfActorSchema.optional(),
usage_count: z.number().int().nonnegative().optional(),
last_modified: OkfIsoDateSchema.optional(),
usage_window: z.object({ from: OkfIsoDateSchema, to: OkfIsoDateSchema }).optional(),
})
export const OkfActorEventSchema = z.object({
by: OkfActorSchema, // REQUIRED within generated/verified (§5.2)
at: z.string().datetime({ offset: true }).optional(),
})
export const OkfStatusSchema = z.enum(['draft', 'stable', 'deprecated'])
/** Frontmatter shared by every OKF concept (§4.1 + §5). */
export const OkfConceptFrontmatterSchema = z
.object({
type: z.string().min(1), // the ONLY always-required key (§4.1)
title: z.string().optional(),
description: z.string().optional(),
resource: z.string().optional(),
tags: z.array(z.string()).optional(),
sources: z.array(OkfSourceSchema).optional(),
usage_window: z.object({ from: OkfIsoDateSchema, to: OkfIsoDateSchema }).optional(),
generated: OkfActorEventSchema.optional(),
// §5.2: a bare mapping MUST be accepted as a one-element list
verified: z.union([OkfActorEventSchema, z.array(OkfActorEventSchema)]).optional(),
status: OkfStatusSchema.optional(),
stale_after: OkfIsoDateSchema.optional(),
})
.passthrough() // §4.1 extensions: consumers MUST preserve unknown keys
/** Attested Computation contract (§10.2). */
export const OkfAttestedComputationSchema = OkfConceptFrontmatterSchema.extend({
type: z.literal('Attested Computation'),
runtime: z.string().min(1), // REQUIRED for this type (§10.2)
parameters: z
.array(z.object({ name: z.string(), type: z.string(), required: z.boolean().default(false) }))
.optional(),
computation: z.string().optional(), // path (§6.2); absent ⇒ body "# Computation" fence
executor: z.object({ resource: z.string(), receipt: z.array(z.string()) }).optional(),
attester: z.object({ resource: z.string() }).optional(),
})
export interface OkfBundleFile {
path: string // bundle-relative, e.g. "concepts/foo.md"
content: string
}
export interface OkfBundle {
files: OkfBundleFile[]
okfVersion: '0.2'
}
2. NEW src/lib/okf/bundle.ts — studio state → OKF bundle
import yaml from 'yaml'
import type { Entity, Claim, GraphEdge } from '@/lib/studio/types'
import type { OkfBundle, OkfBundleFile } from './types'
export const slug = (s: string): string =>
s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'untitled'
/** §4.1: type values are not centrally registered; pick descriptive, self-explanatory strings. */
const OKF_TYPE_MAP: Record<string, string> = {
note: 'Note',
concept: 'Concept',
person: 'Person',
project: 'Project',
}
/** §3.1: index.md / log.md are reserved and MUST NOT be used for concepts. */
const RESERVED = new Set(['index', 'log'])
function conceptPath(e: Entity): string {
const typeName = OKF_TYPE_MAP[e.type] ?? 'Concept'
let name = slug(e.name)
if (RESERVED.has(name)) name = `${name}-concept` // never collide with reserved filenames
return `${typeName.toLowerCase()}s/${name}.md`
}
interface SourceEntry { id: string; resource: string; title?: string; last_modified?: string }
function buildConceptDoc(e: Entity, claims: Claim[], studioVersion: string, now: Date): string {
const frontmatter: Record<string, unknown> = {
type: OKF_TYPE_MAP[e.type] ?? 'Concept',
title: e.name,
description: e.summary, // adjust to the actual Entity field used for one-line summaries
tags: e.tags?.map((t) => t.name),
status: 'stable',
generated: { by: `do-knowledge-studio/${studioVersion}`, at: now.toISOString() },
}
// §5.1 provenance: claims with a source become sources[] entries with STABLE ids
const sources: SourceEntry[] = []
const sourceIdByResource = new Map<string, string>()
for (const c of claims) {
if (!c.source) continue
let id = sourceIdByResource.get(c.source)
if (!id) {
id = `src-${sources.length + 1}`
sourceIdByResource.set(c.source, id)
sources.push({
id,
resource: c.source,
title: c.sourceTitle,
last_modified: c.updatedAt?.slice(0, 10),
})
}
}
if (sources.length) frontmatter.sources = sources
const body = [
e.content ?? '',
claims.length ? '\n# Claims\n' : '',
...claims.map((c) => {
const id = c.source ? sourceIdByResource.get(c.source) : undefined
return `- ${c.text}${id ? `[^${id}]` : ''}`
}),
// §5.1: footnote label is the join key into sources[], NOT positional
...sources.map((s) => `[^${s.id}]: ${s.title ?? s.resource}`),
].join('\n')
return `---\n${yaml.stringify(frontmatter)}---\n\n${body}\n`
}
function buildIndex(files: OkfBundleFile[], entities: Entity[]): string {
// §8: root index.md MAY carry okf_version frontmatter (the only index allowed frontmatter)
const byDir = new Map<string, { title: string; href: string; desc: string }[]>()
for (const f of files) {
if (f.path === 'index.md' || f.path === 'log.md') continue
const dir = f.path.split('/')
const entity = entities.find((e) => f.path.endsWith(`${slug(e.name)}.md`))
const entries = byDir.get(dir) ?? []
entries.push({
title: entity?.name ?? f.path,
href: `/${f.path}`, // §6.1: bundle-relative absolute links are the recommended form
desc: entity?.summary ?? '',
})
byDir.set(dir, entries)
}
const sections = [...byDir.entries()]
.map(([dir, items]) =>
[`# ${dir[0].toUpperCase()}${dir.slice(1)}`, '',
...items.map((i) => `* [${i.title}](${i.href}) - ${i.desc}`)].join('\n'))
.join('\n\n')
return `---\nokf_version: "0.2"\n---\n\n# Knowledge Bundle\n\n${sections}\n`
}
function buildLog(now: Date): string {
// §9: date headings MUST be ISO YYYY-MM-DD, newest first
const day = now.toISOString().slice(0, 10)
return `# Directory Update Log\n\n## ${day}\n* **Export**: Bundle generated by do-knowledge-studio.\n`
}
export function buildOkfBundle(
entities: Entity[],
claims: Claim[],
edges: GraphEdge[],
studioVersion: string,
now: Date = new Date(),
): OkfBundle {
const claimsByEntity = new Map<string, Claim[]>()
for (const c of claims) {
claimsByEntity.set(c.entityId, [...(claimsByEntity.get(c.entityId) ?? []), c])
}
const conceptFiles: OkfBundleFile[] = entities.map((e) => ({
path: conceptPath(e),
content: buildConceptDoc(e, claimsByEntity.get(e.id) ?? [], studioVersion, now),
}))
// §6.1: rewrite GraphEdge relationships as bundle-relative markdown links appended
// under a "# Related" heading in each linked concept (edges are untyped relationships).
const pathByEntityId = new Map(entities.map((e) => [e.id, `/${conceptPath(e)}`]))
for (const edge of edges) {
const from = conceptFiles.find((f) => f.path === pathByEntityId.get(edge.from)?.slice(1))
const toPath = pathByEntityId.get(edge.to)
if (from && toPath && !from.content.includes(`](${toPath})`)) {
const name = entities.find((e) => `/${from.path}` === pathByEntityId.get(edge.from))?.name
from.content = from.content.replace(
/\n?$/,
`\n\n# Related\n\n* [${entities.find((e) => e.id === edge.to)?.name ?? toPath}](${toPath})\n`,
)
void name
}
}
const files: OkfBundleFile[] = [
{ path: 'log.md', content: buildLog(now) },
...conceptFiles,
]
files.unshift({ path: 'index.md', content: buildIndex(conceptFiles, entities) })
return { files, okfVersion: '0.2' }
}
3. NEW src/lib/okf/import.ts — round-trip (closes ADR 010 gap)
import yaml from 'yaml'
import { OkfConceptFrontmatterSchema } from './types'
import type { Entity, Claim } from '@/lib/studio/types'
export interface OkfImportResult {
entities: Entity[]
claims: Claim[]
errors: string[]
}
const OKF_TYPE_REVERSE: Record<string, Entity['type']> = {
Note: 'note', Concept: 'concept', Person: 'person', Project: 'project',
}
/** Parse an OKF bundle (path → content) back into studio state.
* §11: MUST NOT reject unknown types, unknown keys, broken links, or missing
* optional fields — collect errors and continue. */
export function parseOkfBundle(files: Map<string, string>): OkfImportResult {
const result: OkfImportResult = { entities: [], claims: [], errors: [] }
for (const [path, content] of files) {
if (/(^|\/)index\.md$/.test(path) || /(^|\/)log\.md$/.test(path)) continue // reserved (§3.1)
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
if (!match) {
result.errors.push(`${path}: missing or unparseable frontmatter`) // §11 conformance rule 1
continue
}
const parsed = OkfConceptFrontmatterSchema.safeParse(yaml.parse(match))
if (!parsed.success) {
result.errors.push(`${path}: ${parsed.error.issues[0]?.message ?? 'invalid frontmatter'}`)
continue
}
const fm = parsed.data // passthrough preserves unknown keys for round-trip (§4.1)
const entity: Entity = {
id: path.replace(/\.md$/, ''), // Concept ID = path minus .md (§2)
name: fm.title ?? path.split('/').pop()!.replace(/\.md$/, ''),
type: OKF_TYPE_REVERSE[fm.type] ?? 'concept', // unknown types tolerated (§11)
content: match.trim(),
tags: (fm.tags ?? []).map((name) => ({ name })),
// … map remaining Entity fields with defaults
} as Entity
result.entities.push(entity)
// Per-claim attribution: footnote labels join back to sources[].id (§5.1)
const sourceById = new Map((fm.sources ?? []).filter((s) => s.id).map((s) => [s.id!, s]))
for (const m of match.matchAll(/^- (.+?)(?:\[\^([\w-]+)\])?$/gm)) {
result.claims.push({
entityId: entity.id,
text: m,
source: m ? sourceById.get(m)?.resource : undefined,
// … map remaining Claim fields with defaults
} as Claim)
}
}
return result
}
4. src/components/studio/views/export-types.ts — register the format
// BEFORE
export type ExportFormatId = 'json' | 'markdown' | 'html' | 'pdf' | 'docx' | 'encrypted'
// AFTER
export type ExportFormatId = 'json' | 'markdown' | 'html' | 'pdf' | 'docx' | 'encrypted' | 'okf'
// Add to FORMATS:
{
id: 'okf',
name: 'OKF Bundle',
description:
'Open Knowledge Format v0.2 — agent-readable Markdown bundle with provenance, trust & lifecycle frontmatter',
color: 'sky',
}
5. src/components/studio/views/use-export-handlers.ts — export handler
import { zipSync, strToU8 } from 'fflate' // NEW dep: fflate (tiny, client-side zip; §3 allows zip distribution)
import { buildOkfBundle } from '@/lib/okf/bundle'
import pkg from '../../../package.json'
const handleExportOkf = () => {
const bundle = buildOkfBundle(entities, claims, edges, pkg.version)
const zipped = zipSync(
Object.fromEntries(bundle.files.map((f) => [`okf-bundle/${f.path}`, strToU8(f.content)])),
)
downloadBlob(
`do-knowledge-studio-okf-${stamp}.zip`,
new Blob([zipped], { type: 'application/zip' }),
)
toast.success('OKF v0.2 bundle exported', {
description: `${bundle.files.length} files — consumable by any OKF-aware agent, no SDK required`,
})
}
// in handleExport switch:
// case 'okf': return handleExportOkf()
6. Import flow (handleImportClick path) — accept OKF bundles
// In the existing import handler: detect .zip with a root index.md carrying okf_version
import { unzipSync, strFromU8 } from 'fflate'
import { parseOkfBundle } from '@/lib/okf/import'
async function parseImportFile(file: File): Promise<ImportResult> {
if (file.name.endsWith('.zip')) {
const entries = unzipSync(new Uint8Array(await file.arrayBuffer()))
const files = new Map(
Object.entries(entries)
.filter(([p]) => p.endsWith('.md'))
.map(([p, data]) => [p.replace(/^okf-bundle\//, ''), strFromU8(data)]),
)
const rootIndex = files.get('index.md') ?? ''
if (!rootIndex.includes('okf_version')) {
return { errors: ['zip does not contain an OKF bundle (no okf_version in index.md)'] }
}
// §12: unknown versions → best-effort consumption, not refusal
const { entities, claims, errors } = parseOkfBundle(files)
return { data: { entities, claims }, errors } // merge via existing ImportPreview + Zod validation
}
// … existing json/markdown paths unchanged
}
7. NEW src/lib/okf/trust.ts — trust tiers & staleness (phase 2 UI badges)
import type { z } from 'zod'
import type { OkfConceptFrontmatterSchema } from './types'
type Frontmatter = z.infer<typeof OkfConceptFrontmatterSchema>
/** §5.3 trust tiers — derived, never stored. */
export function trustTier(
verified: Frontmatter['verified'],
): 'unverified' | 'machine-confirmed' | 'human-reviewed' {
const list = !verified ? [] : Array.isArray(verified) ? verified : [verified]
if (list.some((v) => v.by.startsWith('human:'))) return 'human-reviewed'
return list.length ? 'machine-confirmed' : 'unverified'
}
/** §5.5: stale when today >= stale_after (plain date comparison). */
export const isStale = (staleAfter?: string, today = new Date()): boolean =>
!!staleAfter && today.toISOString().slice(0, 10) >= staleAfter
8. Tests (matching existing vitest conventions)
src/lib/okf/bundle.test.ts:
- every entity → exactly one concept file; every frontmatter has non-empty
type (§11)
- concepts never use reserved filenames
index/log (§3.1) — slug collision test
- root
index.md carries okf_version: "0.2"; other indexes carry none (§8)
- footnote ids stable when
sources reorder (keyed, not positional — §5.1)
generated.by matches <producer>/<version> actor convention (§7)
- graph edges become bundle-relative absolute links (
/dir/x.md — §6.1)
src/lib/okf/import.test.ts:
- round-trip export → import preserves entities/claims incl. sources
- unknown frontmatter keys survive (passthrough, §4.1)
- unknown
type tolerated, maps to generic concept (§11)
- bare
verified mapping accepted as one-element list (§5.2 MUST)
- broken links tolerated, no rejection (§6.1)
src/lib/okf/trust.test.ts: tier derivation (none/machine/human), stale_after boundary (today == stale_after ⇒ stale)
- Update
use-export-handlers.test.ts & export-format-grid.test.tsx for the 'okf' case
9. Dependencies & docs
package.json: add fflate (runtime dep) and yaml (if not already transitive)
- New ADR
plans/ADRs/03x-okf-v02-export.md superseding the Markdown portion of ADR 010
agents-docs/ + README.md: document OKF as the canonical agent-facing export; link spec
Out of scope (follow-ups)
Attested Computation export for LLM-extracted claim statistics (schema is already forward-compatible)
- Serving the bundle via MCP (see okf-go
okf mcp for precedent)
verified/stale_after editing UI
Acceptance criteria
Refs:
Background
Google Cloud released OKF v0.2 (2026-07-24): an open, vendor-neutral format for knowledge as a directory of Markdown files with YAML frontmatter, authored by people and agents.
Spec: https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md
The studio currently exports
json | markdown | html | pdf | docx | encrypted(src/components/studio/views/export-types.ts). The Markdown export concatenates all entities into a single.mdwith non-standard frontmatter. ADR 010 flags the missing canonical schema and missing round-trip import.OKF v0.2 gives us: (a) agent-readable exports with zero SDK, (b) trust/provenance/freshness signals for LLM-generated knowledge, (c) a governed schema closing ADR 010's round-trip gap.
Spec requirements to implement
.mdfiles; onlytypefrontmatter REQUIREDindex.md(root may carryokf_version: "0.2"),log.md(date-grouped, newest first)sources[](id,resourceREQUIRED,title,author,usage_count,last_modified) + siblingusage_window; per-claim attribution via footnotes keyed tosources[].idgenerated: { by, at },verified: [{ by, at }]; bare mapping MUST be treated as 1-element list; actor conventionhuman:<id>|<producer>/<version>|process:<id>status: draft|stable|deprecated(absent ⇒ stable),stale_after: YYYY-MM-DD(stale whentoday >= stale_after)Attested Computation:runtimeREQUIRED,parameters,computation,executor,attesterCode changes (file by file)
1. NEW
src/lib/okf/types.ts— Zod schemas mirroring the spec2. NEW
src/lib/okf/bundle.ts— studio state → OKF bundle3. NEW
src/lib/okf/import.ts— round-trip (closes ADR 010 gap)4.
src/components/studio/views/export-types.ts— register the format5.
src/components/studio/views/use-export-handlers.ts— export handler6. Import flow (
handleImportClickpath) — accept OKF bundles7. NEW
src/lib/okf/trust.ts— trust tiers & staleness (phase 2 UI badges)8. Tests (matching existing vitest conventions)
src/lib/okf/bundle.test.ts:type(§11)index/log(§3.1) — slug collision testindex.mdcarriesokf_version: "0.2"; other indexes carry none (§8)sourcesreorder (keyed, not positional — §5.1)generated.bymatches<producer>/<version>actor convention (§7)/dir/x.md— §6.1)src/lib/okf/import.test.ts:typetolerated, maps to generic concept (§11)verifiedmapping accepted as one-element list (§5.2 MUST)src/lib/okf/trust.test.ts: tier derivation (none/machine/human),stale_afterboundary (today == stale_after⇒ stale)use-export-handlers.test.ts&export-format-grid.test.tsxfor the'okf'case9. Dependencies & docs
package.json: addfflate(runtime dep) andyaml(if not already transitive)plans/ADRs/03x-okf-v02-export.mdsuperseding the Markdown portion of ADR 010agents-docs/+README.md: document OKF as the canonical agent-facing export; link specOut of scope (follow-ups)
Attested Computationexport for LLM-extracted claim statistics (schema is already forward-compatible)okf mcpfor precedent)verified/stale_afterediting UIAcceptance criteria
pnpm testpasses incl. new OKF suites (coverage ≥ current threshold)type, reserved-file structure)Refs: