docs/cli.mdtype: Reference

The okf command line.

Eight subcommands over a bundle or a file, a self-contained Native AOT binary with no runtime to install. validate exits non-zero on a non-conformant bundle, so the whole tool drops into CI as one line.

##

Synopsis

okf <command> [args]
CommandArgumentDoes
validate<bundle>Check a bundle against OKF v0.2 conformance (§11)
audit<bundle>Report trust, freshness and lifecycle across the bundle (§5.3–§5.5)
verify<bundle> <id>…Record a review (§5.2) with --by <actor>; clears the unverified worklist, not staleness
info<bundle>Summarize concepts, types, links and version
index<bundle>(Re)generate every index.md (§8)
graph<bundle>Print the cross-link graph; --dot for Graphviz
parse<file>Parse one document and print its structure
fmt<file>Normalize by parse + re-serialize (-w writes)

Static HTML site generation lives in a separate okf-render binary, not in okf itself: okf is meant to stay small and dependency-free for CI, and the site generator pulls in a vendored copy of marked that a CI job running validate never executes.

Global options: -h/--help prints usage; -V/--version prints the build and spec version; --as-of <YYYY-MM-DD> pins today's date for validate and audit.

Everything after a -- separator is an argument, never an option — which is how a path beginning with - is passed. The rule holds for every verb and every flag, so okf fmt -- notes.md -w treats -w as a second filename rather than as the write-in-place flag; write it as okf fmt -w -- notes.md if that is what you meant. A value belonging to an option is likewise only ever a value: in okf audit b --type --stale, --stale is the type being searched for, not a filter. A lone - is never a flag — it is POSIX's "read standard input" argument, which is what lets verify's <id>… read concept ids from a pipe.

$ okf --version
okf 0.6.0 (OKF spec v0.2)
##

validate <bundle>

§11 — exit code is the interface

Loads the bundle, runs the §11 conformance check, and prints every diagnostic followed by a tally. Exits 0 when conformant, 1 otherwise. Warnings never break conformance — only errors do — so recommended-but-missing fields are surfaced without failing the build.

$ okf validate tests/fixtures/appendix_a
[warning] tests/fixtures/appendix_a/tables/users.md: missing recommended frontmatter field `description`
[warning] tests/fixtures/appendix_a/tables/users.md: missing recommended frontmatter field `timestamp`

4 concept(s); 0 error(s), 2 warning(s), 0 info.
✓ conformant with OKF v0.2

--json emits the same diagnostics as machine-readable, camelCase JSON — one object per line, no pretty-printing. Each diagnostic carries a stable code (DiagnosticCode) and, where relevant, a field naming the frontmatter key involved, so a caller can branch on the finding without parsing prose.

$ okf validate tests/fixtures/appendix_a --json
{"bundle":"tests/fixtures/appendix_a","conformant":true,"conceptCount":4,"errorCount":0,"warningCount":2,"infoCount":0,"diagnostics":[{"severity":"warning","code":"MissingRecommendedField","path":"tests/fixtures/appendix_a/tables/users.md","conceptId":"tables/users","field":"description","message":"missing recommended frontmatter field `description`"}, …]}

--as-of <YYYY-MM-DD> pins the date the §5.5 staleness warning is evaluated against. Without it that one diagnostic depends on the day the command runs, so a pipeline asserting on validate's output should pin it rather than let the calendar move underneath.

##

audit <bundle>

§5.3–§5.5 — the corpus, not the concept

Answers questions about the bundle as a whole: how much of it is human-reviewed, what has passed its stale_after instant, what is deprecated. Counts always describe the whole bundle while the worklist describes the selection — audit is a worklist, not an inventory. Always exits 0: a stale concept is editorial hygiene, not a conformance failure.

$ okf audit bundles/acme_retail --as-of 2027-06-01
bundle:     bundles/acme_retail
as of:      2027-06-01
concepts:   9

trust:
     8  human-reviewed
     0  machine-confirmed
     1  unverified

status:
     0  draft
     8  stable
     1  deprecated

stale:      7 of 9 past stale_after

needs attention (7):
  computations/gross-margin-period  stale 2026-12-31  human-reviewed  stable
  computations/revenue-ytd  stale 2026-12-31  human-reviewed  stable
  metrics/gross-margin  stale 2026-12-31  human-reviewed  stable
  metrics/revenue  stale 2026-12-31  human-reviewed  stable
  policies/margin-standard  stale 2026-12-31  human-reviewed  stable
  policies/revenue-recognition  stale 2026-12-31  human-reviewed  stable
  tables/orders  stale 2026-12-31  human-reviewed  stable

With no filter flag it selects exactly what --stale selects and prints the summary above. With any of --stale, --trust, --status or --type it prints one line per matching concept and nothing else, so the output pipes. --as-of pins the observation date to midnight UTC on that day (it never changes the mode), and --json always emits the full document.

# any filter flag switches to one line per concept — pipe-friendly
$ okf audit bundles/acme_retail --trust unverified
skills/run-on-bq  no-stale-after  unverified  stable

The question this exists for — which concepts are past their stale_after instant and have never been verified by a human? — is --stale --trust unverified,machine-confirmed: both tiers, because “machine-confirmed” also means no human ever looked.

##

verify <bundle> <id>… --by <actor>

§5.2 — the verb that answers audit

Records a review: adds — or, for a repeat review from the same actor, replaces — a {by, at} entry in each named concept's verified list. Every id is checked for existence and §11 conformance before anything is written, so a batch with one bad id is rejected as a whole at that stage; a failure partway through the write phase (I/O, permissions) can still leave the concepts already written stamped, and the output says exactly what landed. Exits 0 on success, 1 otherwise.

$ okf verify bundles/acme_retail metrics/revenue --by human:ada --at 2026-08-28T09:14:00Z
recorded metrics/revenue  human:ada  2026-08-28T09:14:00Z

# a repeat review from the same actor replaces its own stamp
$ okf verify bundles/acme_retail metrics/revenue --by human:ada --at 2026-09-15T09:00:00Z
recorded metrics/revenue  human:ada  2026-09-15T09:00:00Z  (replaces 2026-08-28T09:14:00Z)

<id>… also accepts a single -, reading one concept id per line from standard input — which is what lets okf audit's worklist feed okf verifydirectly, closing the loop in one line:

# "-" reads concept ids from standard input, one per line -- audit's worklist becomes verify's input
$ okf audit bundles/acme_retail --trust unverified | cut -d' ' -f1 | okf verify bundles/acme_retail --by human:ada -
recorded skills/run-on-bq  human:ada  2026-08-28T21:22:23Z

An empty stream there is nothing to do, not an error: okf audit exits 0 printing nothing when the worklist is empty, and okf verify - on that stream writes nothing and exits 0 too — so the loop is idempotent and safe under set -e on a healthy bundle. Naming no concept at all (okf verify <bundle>) is still an error.

Re-running okf audit … --trust unverified afterward prints nothing — the concept it just stamped left the worklist. --dry-run shows what would be recorded without writing; --at <yyyy-MM-ddTHH:mm:ssZ> overrides the default of "now" for reproducible scripting.

$ okf verify bundles/acme_retail metrics/gross-margin --by human:ada --dry-run
would record metrics/gross-margin  human:ada  (now)

A stamp is a dated declaration, not a proof. It guarantees the entry is well-formed, dated, and attached to the concepts named — it does not, and cannot, guarantee the signer's identity or that anyone read the concept: no zero-dependency tool can authenticate --by, and okf_write_concept can write the same field with no ceremony at all. What makes a stamp credible is where it lands — in a diff a human reviewed — never a stamp inferred from a PR approval; see the project README for the full reasoning.

##

info <bundle>

a summary, no mutation

Reports the bundle root, declared OKF version (if any), concept count, reserved-file counts, a breakdown by type, and the internal link total with broken-link count. Unparseable files are listed at the end. Always exits 0.

$ okf info tests/fixtures/appendix_a
bundle:     tests/fixtures/appendix_a
concepts:   4
index.md:   0
log.md:     1

types:
     1  BigQuery Dataset
     3  BigQuery Table

links:      5 internal (0 broken)

--json works here too, for scripting against the same summary.

$ okf info tests/fixtures/appendix_a --json
{"bundle":"tests/fixtures/appendix_a","okfVersion":null,"conceptCount":4,"indexFileCount":0,"logFileCount":1,"types":{"BigQuery Dataset":1,"BigQuery Table":3},"linkCount":5,"brokenLinkCount":0,"parseErrors":[]}
##

index <bundle>

§8 — progressive disclosure

Regenerates every index.md directory listing in the bundle and prints each path written, then a total. On an empty bundle it prints no index files written (empty bundle?). Exits 0. This command writes files — run it after adding or renaming concepts.

$ okf index ./my_bundle
wrote my_bundle/index.md
wrote my_bundle/tables/index.md

2 index file(s) regenerated.
##

graph <bundle> [--dot]

§6 — cross-links

Prints each concept's outgoing links. Resolved links use ->, broken ones -x. Pass --dot to emit Graphviz DOT (broken edges dashed and red) — pipe it straight into dot.

$ okf graph tests/fixtures/appendix_a
datasets/sales
  -> tables/orders
  -> tables/customers
tables/customers
  -> tables/orders
tables/orders
  -> datasets/sales
  -> tables/customers
$ okf graph tests/fixtures/appendix_a --dot
digraph okf {
  rankdir=LR; node [shape=box, fontsize=10];
  "datasets/sales" -> "tables/orders";
  "datasets/sales" -> "tables/customers";
  "tables/customers" -> "tables/orders";
  "tables/orders" -> "datasets/sales";
  "tables/orders" -> "tables/customers";
}

$ okf graph ./my_bundle --dot | dot -Tsvg > graph.svg
##

parse <file>

§4 — one document

Parses a single concept document (strict UTF-8) and prints its frontmatter keys, whether it has a non-empty type, the body size in bytes, and any links and citations. Exits 0 if the document is conformant, 1 if not.

$ okf parse tests/fixtures/appendix_a/tables/orders.md
frontmatter (6 key(s)):
  type: BigQuery Table
  title: Orders
  description: One row per completed customer order.
  resource: https://console.cloud.google.com/bigquery?…
  tags: [sales, orders]
  timestamp: 2026-05-28T00:00:00Z

has non-empty `type`: true
body: 99 byte(s)

links (2):
  [Absolute] sales dataset -> /datasets/sales.md
  [Absolute] customers -> /tables/customers.md
##

fmt <file> [-w]

the round-trip, made tangible

Parses the document and re-serializes it — normalizing frontmatter and block structure while preserving unknown keys byte for byte. Prints to stdout by default; -w (or --write) writes back in place. Exits 0.

$ okf fmt tests/fixtures/appendix_a/tables/orders.md
---
type: BigQuery Table
title: Orders
description: One row per completed customer order.
resource: https://console.cloud.google.com/bigquery?p=acme&d=sales&t=orders
tags:
  - sales
  - orders
timestamp: 2026-05-28T00:00:00Z
---

# Schema

Part of the [sales dataset](/datasets/sales.md). FK to [customers](/tables/customers.md).
##

Exit codes & CI

copy the file, run it
CodeMeaning
0Success — or, for validate/parse, conformant
1Non-conformant, missing argument, unknown subcommand, or I/O error

Because the binary is self-contained, a CI image needs no .NET runtime, no SDK, no restore:

# any pipeline — fail the build on non-conformant knowledge
okf validate ./bundles/ga4

→ library.md — the same engine, as a C# API · getting-started.md

##

Install it

winget on Windows, install.sh on Linux/macOS, or Native AOT publish

On Windows, install via winget:

$ winget install Coderise.OKF4net

On Linux or macOS, install a release binary with the install script:

$ curl -sSL https://raw.githubusercontent.com/jchable/okf4net/main/packaging/install.sh | sh

It detects your OS/architecture, downloads the matching archive from the latest GitHub Release, verifies its SHA-256 checksum, and installs to /usr/local/bin (falling back to ~/.local/bin when that isn't writable — it never calls sudo). Pass --bin okf-render to install the site generator instead, --version <tag> to pin a release, or --dry-run to see what it would do first; -h lists every option.

On any OS, you can also build it from source:

$ git clone https://github.com/jchable/okf4net
$ dotnet publish src/OKF4net.Cli -c Release   # self-contained okf binary