@playcode/okf-lint
v2.2.0
Published
Readout - the document format agents hand back: routed diagrams, charts, KPI tiles and notebook pages from plain text, rendered self-contained and verified. Plus the OKF docs-profile linter (frontmatter, cross-links, indexes).
Maintainers
Readme
@playcode/okf-lint
Readout: the document format agents hand back. A compact plain-text
language (```readout fences in markdown) that renders routed diagrams,
real charts, KPI tiles and notebook pages as one self-contained interactive
HTML file - written by agents, read by humans, verified faithful. This package
also ships the OKF docs-profile linter the format grew out of. The former
language name okf-d remains a permanent fence alias.
Rendered from examples/orders.diagram.md by okf diagram - no coordinates in the source, no hand-placed arrows, no Mermaid.
A linter for the docs/ knowledge base. It enforces a minimal OKF-style profile (markdown + YAML frontmatter + cross-links), generates per-folder index.md manifests, checks links, and reports staleness - so docs stay findable by both humans and agents.
Dependency-free at runtime (hand-rolled frontmatter/markdown parsing); run from source via tsx, no build step.
Usage
From the repo root:
pnpm okf check [--staged] # validate frontmatter (+ links). --staged: only git-staged docs (the ratchet)
pnpm okf links # dangling cross-links across the whole bundle
pnpm okf index [--check] # regenerate index.md. --check fails on drift, --dry previews
pnpm okf diagram [--check] # render *.diagram.md / *.diagram.json sources to sibling interactive .html
pnpm okf diagram --serve # authoring loop: watch, rebuild on save, live-reload browser (port 4990)
pnpm okf diagram --verify # prove the router + chart invariants in headless Chromium (hash-cached)
pnpm okf diagram --diff a b # what changed between two diagram sources (nodes, renames, edges)
pnpm okf stale [--days=N] # docs older than N days by git last-commit date (default 365)
pnpm okf fix # stamp missing type, add status to archive/ docs, regenerate indexes
pnpm okf precommit # hook entry: regenerate+stage canvases, check staged docsAdd --json for machine output (use pnpm -s okf ... --json so pnpm's banner stays off stdout). Exit codes: 0 ok, 1 errors found, 2 usage.
The profile
Every concept doc carries frontmatter:
---
type: guide # REQUIRED - routable kind (adr|daily|meeting|person|feature-plan|exec-plan|draft|bug|hypothesis|report|guide)
description: One line. # REQUIRED - what the doc is (exempt under archive/)
status: active # OPTIONAL - lifecycle: active|draft|superseded|archived
# delivery (plans): in-progress|building|shipped-dev|shipped|complete
tags: [sky, storage] # OPTIONAL
---index.md (one per folder, generated) and README.md are reserved. Chronological logs (daily/, meetings/) and date-shard dirs are not indexed.
Rules
| Severity | Rules |
|---|---|
| error (blocks) | frontmatter-missing, type-missing, description-missing, value-yaml-unsafe, duplicate-frontmatter-key, description-multiline, index-stale (in index --check), diagram-invalid |
| warning (informs) | link-dangling, type-unknown, status-invalid, filename-type-mismatch, archive-status-missing, adr-superseded-unmarked, multiple-h1, description-too-long, index-not-generated |
Severities live in src/config.ts (SEVERITY).
Finding docs with the structure
grep -rl '^type: adr' docs # every ADR
grep -rl '^tags:.*\bsky\b' docs # by tag
grep '^\* ' docs/sky/index.md # one-line summary of a folder's docsReadout - the full reference
One compact *.diagram.md source renders to one self-contained interactive
.html (dark + light themes, no external requests, no libraries). Two render
modes from the same grammar:
- canvas (default): an infinite pan/zoom board - architecture maps.
- doc (
render: docin frontmatter): a notebook-grade PAGE - prose, tables, KPI tiles, charts and routed diagrams in one flowing document, with a scroll-spy table of contents and a print stylesheet.
| Dark | Light | |---|---| | | |
The same canvas source in both themes - the runtime owns colour, so a source never mentions one.
Doc mode, from examples/runbook.diagram.md: KPI tiles, prose, a table, a code fence and routed diagrams in one page, with a Contents rail and a print stylesheet.
The doctrine: sources carry semantics and structure only - no colors, no
coordinates, no CSS, no hand-placed arrows. The packaged runtime owns theming,
layout reflow, and arrow routing; okf diagram --verify proves the invariants
(every edge drawn, no arrow through a card, no label over a card, chart text
inside its card, no overlapping value labels) in headless Chromium.
File shape
---
type: diagram
description: One line - becomes the doc-mode subtitle.
render: doc # omit for a canvas
---
# The H1 is the title
Doc mode: this prose flows into the page. Canvas mode: prose before the
first fence is the caption; prose after any fence is an error.
```readout
section: 1 - First section
nav: 1-First
row:
api[teal]: API | :8080 - validates
db[amber]: DB | rows of record
edges:
api -> db : SQL
```
More prose (doc mode). It attaches to the NEXT section - use a continuation
block (`section: -`) to write commentary AFTER a diagram inside one section.Sections
| Line | Meaning |
|---|---|
| section: <label> | required; the heading. id: overrides the slug, nav: the toolbar text |
| section: - | continuation: no heading, no nav/ToC entry - lets prose and diagrams alternate inside one logical section (fixes "text after the picture") |
| fold: true | doc mode: the section renders collapsed (<details>); deep links and print open it |
| legend: teal=service, red=failure, flow=flow, fk=FK | one entry per role in use; in the rendered page each swatch is a click-to-filter for that role |
| edges: | indented edge lines follow (see Edges) |
Containers
row rowT rowS rowW col colS stack grid2 cols ranks rankrow panel, plus
cluster "Heading" (subtitle) <layout>:. Any container takes a gap override:
rowT(110):. Two are special:
cols:/cols(2,1):- the doc-mode page grid. Children get equal (or weighted, one integer 1-12 per child) columns of the CONTENT width and FILL their cells - charts redraw themselves at the width the cell actually got. On phones the grid stacks. On a canvas it falls back to a top-aligned row.ranks+rankrow- layered trees (FK graphs): rank by depth, point every arrow up with@tb.
Nodes
id[role flags]: Name | line | line box (lines accept `code`, **bold**, [text](url))
inst id[role]: Name | line saturated instance box
table id[role]: Name ER card; indented fields follow:
fieldId: uid: UUID PK with id -> usable as an edge endpoint
- name: string anonymous field
kpi id[role dir]: value | label | delta stat tile, e.g.:
kpi k1[green up]: $22,667 | July gross | +37% on June
note: ... warn: ... info: ... full-width bands (green / red / blue)
sub: ... small sub-label
spacer flexible gap (with rowS)Roles: purple teal amber green red blue yellow gray. Node flags:
| Flag | Meaning |
|---|---|
| wide | relax the width cap (box: long reference boxes; chart: full content width in doc mode) |
| span | grid2 only: span both columns |
| mark | emphasis ring - "this node is the point" |
| ghost | de-emphasized context (the inverse of mark) |
| up down flat | kpi only: the delta glyph. The ROLE carries good/bad - a red down is a warning, a green down is a win |
| nudge=DX,DY | last-resort visual offset; survives reflow, arrows re-aim |
Edges
a -> b : label plain arrow, optional label chip
a -teal-> b role-colored arrow
a => b PRIMARY path - heavier stroke ( =teal=> for colored)
a -.-> b thin FK arrow (child -> parent)
a <-> b a <=> b arrowheads both ends (plain / primary)
a -> a @r self-loop (side via the @ hint)
a -green-> b @bt : PUT side hints @<exit><enter>: l r t b, "." = autoArrows are routed, never drawn: orthogonal elbows with obstacle avoidance
(straight -> L -> Z -> A*), ports spread along shared sides, parallel runs in
separate lanes, labels on free spots. If an arrow looks wrong, fix the
STRUCTURE first (escalation ladder in BEST-PRACTICES.md).
Charts
chart id[<type> <flags>]: Title | caption opens a chart; indented lines fill it:
chart c_rev[stackedBar values wide]: Monthly gross revenue | net of refunds, USD
fmt: usd # usd | pct | int | compact
y: revenue # value-axis label (donut: center caption)
x: Mar, Apr, May, Jun # categories - one value per series per category
s new[purple]: 4200, 5100, -, 3900 # "-" is a GAP (no data), never 0
s renewals[teal]: 8100, 8400, 9200, 9600
ref 16000[red]: break-even # marker line on the value axis
refx May[red]: core update # vertical event marker on a category
ann Jun: one whale = 43% # short callout in a band above the plot
hl: Jun # emphasized categories, rest dim
src: ClickHouse events, 2026-08-07 # "Source:" line under the plot
scale: rev # same-name charts share one y-domainTypes and when to reach for them:
| Type | Use for | Notes |
|---|---|---|
| bar / groupedBar | comparison between series | single series = plain columns |
| stackedBar | composition over time | values prints stack TOTALS |
| hbar | rankings with long names | long labels middle-ellipsize + tooltip |
| line | trends | value labels dodge collisions automatically |
| area | cumulative totals | bands first, lines never buried |
| donut | one moment's share | NO x: - each s line is one slice; >6 slices wants hbar |
| waterfall | a bridge between two totals | exactly ONE series of DELTAS; negatives red, gray Total bar added |
| combo | bars + a line over them | s margin[green line] = line; add y2 for a right axis (fmt2: + y2: label) |
| heatmap | a series x categories matrix | heat: green sequential ramp, heat: red green diverging around 0 |
Chart flags: wide (more room; doc mode = full content width), values
(numbers on marks - dropped when they cannot fit), facet (line/area/bar:
one mini panel per series, all sharing the whole chart's domain),
nudge=DX,DY.
| Dark | Light | |---|---| | | |
Worked examples of every type - examples/metrics.diagram.md. No chart library: the axis, the ramps and both themes are hand-rolled SVG. The arrow at the bottom points at a chart, because a chart is a card.
chart c_wf[waterfall values]: July to August bridge
fmt: usd
x: July, New subs, Renewals, Top-ups
s delta[green]: 22667, -2375, -7673, -7390
chart c_margin[combo values]: Revenue vs margin
fmt: usd
fmt2: pct
y2: margin
x: May, Jun, Jul
s gross[teal]: 19508, 17239, 22667
s margin[green line y2]: 78.7, 71.2, 68.3
chart c_ret[heatmap values]: Cohort retention
fmt: pct
heat: green
x: M0, M1, M2
s May[gray]: 100, 65, 50
s Jun[gray]: 100, 53, -Every rendered chart carries a data toggle (its numbers as a table +
copy-as-CSV) and styled hover tooltips (click to pin, Esc to release);
clicking a mark opens the data with that series' row highlighted.
Doc mode - the notebook
render: doc turns the source into a flowing page:
- Markdown between fences: paragraphs,
###/####headings (auto-anchored, hover ¶ deep links),-/1.lists with nesting,>quotes,---rules, fenced code blocks, inlinecode/ bold / italic /[text](url). - Tables: pipe tables with the alignment row honored (
---:right,:---:center); numeric columns right-align themselves; headers are sticky and click-to-sort (asc / desc / source order - view-state only). - KPI rows:
cols:+kpitiles is the standard report opener. - Images:
- relative paths only; an external URL is a parse error (a committed page must not fetch the network). A missing file is a build WARNING. - Footnotes:
A claim.[^1]...[^1]: the fine print- rendered superscripts with a collected list + backlinks at the page end. - Sparklines:
spark(4200 5100 4800)/sparkbar(1 2 3)inline in prose or table cells;spark(teal: ...)picks the role. - Navigation: a scroll-spy Contents rail (sections + h3s) replaces the pill toolbar; below 1280px it becomes a slide-over behind a floating button.
- Print: the Print button (or Cmd+P) forces the light palette, hides chrome, opens all folds, and avoids page breaks inside cards - the PDF hand-off path.
- Mobile: single column, charts redraw at the width they actually get, wide tables scroll inside their own box - the page never scrolls sideways.
- Prose before a fence attaches to the FOLLOWING section; use
section: -continuation blocks for diagram-then-commentary; trailing prose is the outro.
Interactions (rendered page)
| Gesture | Canvas | Doc |
|---|---|---|
| drag / wheel | pan / zoom | native scroll |
| / | search cards (matches name, lines, id; dims the rest; Enter cycles + jumps) | - |
| click a card | PIN its arrow spotlight; Esc or background click clears; shareable as #focus=<id> | text selection |
| double-click a card | edit text in place (exploration only - the source is the truth) | same |
| click a legend swatch | filter that role everywhere (boxes, edges, chart series) | same |
| hover a card / mark | transient arrow spotlight / value tooltip (click pins) | same |
| t / f / 1-9 | theme / fit / jump to section | - |
| section buttons / ToC | fit that section + set #sec-<id> | scroll + set hash |
A canvas with 3+ sections opens fitted to its FIRST section, not the unreadable fit-all. The static render is always complete: filters, folds and search are view-state that print and PNG ignore.
Pipeline
okf diagramrenders sources to sibling.html(+ PNG preview blocks under each canvas source's H1; doc sources get no PNG).--checkis the drift gate;okf precommitregenerates and stages, and a broken source BLOCKS the commit.okf diagram --serve [--port=N]- watch + rebuild + live reload. The reload snippet exists ONLY in served bytes; committed artifacts stay byte-identical (a test pins this).okf diagram --verify- the invariant suite per diagram in headless Chromium. Unchanged artifacts are skipped via a content-hash cache intemp/okfd-verify-cache.json(OKF_VERIFY_ALL=1re-verifies everything).okf diagram --diff <a> <b>- added/removed/renamed nodes and added/removed edges between two sources.okf diagram --to-dsl- convert*.diagram.json(the raw model, same renderer - useful for generated diagrams) to the DSL, round-trip guarded.okf diagram --fmt- align box-line columns (whitespace only).
One source, several artifacts. The .html (and local .png) are
generated siblings; exclude them in the editor (file_scan_exclusions) rather
than reorganising the repo. The generated file leads with its MODEL, then a
LIBRARY CODE BELOW - stop here banner ahead of the minified runtime -
everything diagram-specific is in the first ~40 lines.
Editor support. editors/zed/ ships okf-d syntax highlighting for Zed
(inside markdown fences and .okf-d files); Zed cannot preview HTML, which is
what the PNG preview block is for.
Design rules the renderer encodes, chart-type guidance, the escalation ladder
(reorder -> regroup -> spacer/gap -> @hints -> nudge), and the emphasis
doctrine (mark/ghost/hl are authored SEMANTICS - "this is the finding" - while
colors, sizes and coordinates stay banned): see
BEST-PRACTICES.md.
Enforcement
.husky/pre-commit runs okf precommit on docs-touching commits (the hook path is installed by the repo-root prepare script, so a fresh pnpm install wires it up - no husky package needed). It regenerates and stages indexes and canvases, enforces type+description on touched docs (errors block), and warns on inbound links a rename/delete would break.
Known limit: the hook only sees staged files, so broken inbound links from untouched files on a rename are caught only by running pnpm okf links (there is no CI). Run it periodically.
Layout
src/cli.ts commands + flags + exit codes
src/config.ts paths, taxonomy, severities, the fs walker
src/frontmatter.ts CRLF/BOM-safe parse/serialize, duplicate-key detection
src/markdown.ts code stripping + link extraction
src/links.ts link resolution + dangling rule
src/index-gen.ts manifest generation + write-guard
src/rules/ frontmatter + convention rules
src/diagram/ types, dsl (parser - its header comment IS the grammar),
emit, validate, template (runtime), chart, build, verify,
serve, images, fmt
src/{git,doc,stale,fix,precommit,report}.ts
test/ vitest fixtures per areapnpm test # vitest
pnpm typecheck # tsgo
pnpm lint # oxlint + oxfmt