Back to blog
Ilustración: varias fichas de texto enlazadas a una carpeta, de la que parten haces de luz hacia un núcleo poliédrico rodeado de anillos concéntricos, en verde menta sobre fondo negro.

How to build your own knowledge graph with quartz-okf

LabQuartzKnowledge graphDocumentation

I have three knowledge corpora running that look nothing alike: a doctrinal tradition, a family-therapy library distilled for clinical use, and the teardown of an industry standard for enterprise architecture. All three are published as a browsable site with its graph, on the same engine and without a single different line of code.

The engine is called quartz-okf, and the repository is open, under an MIT license. It outgrew the site where it was born —an infrastructure base that was its first consumer— and now lives on its own. This is the long explanation of what it solves and how to use it; posts about specific projects link here so it doesn't have to be repeated.

A pile of notes is not a graph

The starting point is familiar to anyone who has accumulated markdown notes. At fifty, the [[wikilink]] does the job. At two hundred, it doesn't: you have links that say "this note mentions that other one" and nothing more. A note linking to a book doesn't distinguish whether it cites it as support, contradicts it, or names it in passing. The hairball that comes out of that answers no question.

The obvious remedy is to type the relationships. And there the second flaw appears, worse because it takes longer to see: if the vocabulary is open, each note invents its own label. At two hundred you have sixty, half of them synonyms, and the graph has gone from saying nothing to saying sixty incompatible things.

What you need is exactly the opposite of freedom: a closed vocabulary —this is a concept, that is a thesis; this relationship is written Cites and there is no other way— and a validator that breaks the build when someone steps outside. The restriction is the product.

Someone else's spec instead of a format of your own

OKF —Open Knowledge Format— is a Google Cloud specification, currently at v0.1, that represents knowledge as a directory of markdown files with YAML frontmatter. That is all, and that poverty is the point: no database, no server, no binary format. The corpus is what you see with ls.

I could have invented the format in an afternoon. I didn't, because this way the client ends up with an asset that doesn't depend on my tool —text files anyone will open in ten years— and because a spec leaves room for a profile on top, which is where my part lives.

What quartz-okf adds on top is two things: a closed vocabulary of types, and typed relationships written in the note's prose instead of in a separate data file. Every note opens with a # Topology.

---
type: divergence
title: Tres apofatismos incompatibles
---

# Topology

* **About**: [[autores/pseudo-dionisio-areopagita]], [[autores/maestro-eckhart]]
* **Depends on**: [[conceptos/lo-uno-apofatico]]
* **Cites**: [[libros/teologia-mistica]], [[libros/sermones-alemanes]]

## El desacuerdo

La nota sigue aqui, en prosa normal.

The parser reads that section, resolves the aliases, derives the inverses, and writes the graph. Relationships are not inferred: they are declared. That is the decision that holds up everything else.

Quartz pinned by SHA, and never inside the repository

The site's foundation is Quartz v5, Jacky Zhao's static generator for digital gardens: search, backlinks, breadcrumbs, theming, and an ecosystem of community plugins. I have no interest in rewriting any of that.

What I do care about is how it is consumed. The toolkit pins Quartz by commit SHA in harness/quartz.ref and the 44 community plugins by commit in harness/quartz.lock.json. The corpus repository in turn pins the toolkit in okf/quartz-okf.ref. Two chained pins: the corpus chooses the engine version and the engine chooses the Quartz version.

None of that is copied into the corpus repository: at build time the two tarballs are downloaded to a local cache and assembled there. Outside of content/, the public corpus repository has eleven files.

Vendoring would have been faster to start and a long-term sentence: if you copy the generator in, your corpus becomes a fork of a site generator and every update is a merge. With the pin, updating is forty characters, and that file also records which version produced which build.

The pieces

@zetesis/okf-core is the contract, independent of the renderer: validation, link resolution, topology parser, graph, and exporter. Node 20 or higher, zero runtime dependencies. It ships four binaries —okf-check, okf-export, okf-impact and okf-diagram— which validate, export a conformant bundle, compute which documentation needs reviewing since the last maintained head, and draw mermaid.

On top there are four Quartz plugins. quartz-okf is the adapter: it validates at build time with the same implementation the CLI uses and emits static/okf-graph.json plus a raw copy of each note. quartz-graph-okf draws the small graph next to the note, quartz-okf-panels mounts the impact-radius panel, and quartz-okf-explorer is the full-screen viewer.

The viewer is a self-contained page on a <canvas> 2D with d3 served from the site itself, not from a CDN. It queries no database because there is none: it does a fetch of the okf-graph.json that the plugin wrote during the build. The graph being an artifact rather than a service has a practical consequence: it can't fall out of sync with the content, because it is regenerated in full on every deploy.

Loading diagram...

What you have to write to get started

A consumer provides three things: its content/ with the notes, an okf.config.mjs and a build script that downloads the toolkit by SHA. The vocabulary lives entirely in the config. An excerpt from the public corpus, trimmed but literal.

export const profile = {
  // Set cerrado. Los cuatro primeros son el armazon de cualquier corpus de lectura;
  // los tres siguientes son propios de este: una tradicion doctrinal no se describe
  // solo con conceptos, sino con las corrientes que los sostienen, las divergencias
  // que el canon no declara y los frentes que quedan abiertos.
  types: ["book", "author", "concept", "claim", "current", "divergence", "front", "report"],
  edgeLabels: [
    "Part of", "Contains", "Authored by", "Cites",
    "About", "Depends on", "Opposes", "Continues",
  ],
  // El motor deriva el espejo, asi que cada relacion se declara una sola vez.
  inverseLabels: { "Part of": "Contains", Contains: "Part of", Opposes: "Opposes" },
}

export const explorer = {
  typeLabels: { book: "obra", author: "autor", concept: "concepto", claim: "tesis" },
  typeColors: { book: "#8a8a8a", author: "#b58b6a", concept: "#4c7ecf", claim: "#c2544d" },
  // Anillos por tipo: los frentes y las divergencias al centro, porque son lo que el
  // corpus tiene de vivo; las obras y sus autores fuera, que son el sustrato.
  layout: {
    charge: -45,
    link: {
      "*": { distance: 28, strength: 0.12 },
      Cites: { distance: 70, strength: 0.03 },
      "Authored by": { distance: 30, strength: 0.25 },
    },
    radial: {
      strength: 0.9,
      byType: { front: 0, divergence: 0.16, claim: 0.3, concept: 0.44, book: 0.74, author: 0.98 },
    },
  },
  modes: [
    { id: "full", label: "Grafo completo", edges: "*" },
    {
      id: "fundamentacion",
      label: "Fundamentacion",
      desc: "<b>Cuantas obras sostienen cada nota.</b> Rojo si se apoya en poco, verde si esta bien respaldada.",
      edges: ["Cites"],
      colorBy: {
        countEdge: "Cites",
        scale: [
          { max: 1, color: "#c2544d", label: "1 obra o ninguna" },
          { max: 3, color: "#e0a03c", label: "2 o 3 obras" },
          { max: 999, color: "#3fa34d", label: "4 o mas" },
        ],
      },
    },
  ],
}

There are three things to read carefully there.

  • The set of types and labels is closed. Eight and eight in this corpus. Anything outside them gets reported by the validator with name and file.
  • Each relationship is declared exactly once. inverseLabels tells the engine which mirror to derive. Of the 902 relationships in the public graph, 808 are written by hand and 94 are derived by the engine.
  • Modes are data, not code. A mode is a question to the corpus: which edges it keeps, which property it colors by, what determines node size. The one above paints in red the notes that rely on a single work or on none. A new question is eight lines.
They're data: the engine knows nothing about HERM; it draws the questions declared here.
Comment in okf.config.mjsindustry-standard corpus

The command line does the same as the build, without the build.

quartz-okf

$ npm test

# tests 31 # pass 31 # fail 0

$ node core/bin/okf-check.js .

[okf] checked 218 markdown files: 0 error(s), 6 warning(s)

$ node core/bin/okf-check.js content/

[okf] checked 218 markdown files: 165 error(s), 310 warning(s)

The last line is not a bug: it's the mechanism. The same corpus, without its okf.config.mjs next to it, validates against the toolkit's reference profile, where neither divergence nor Cites exists. Without the consumer's overlay, the engine doesn't know what you're talking about.

Three corpora that look nothing alike

The public one is the Hispanic perennialism corpus, at Zetesis-Labs: 217 notes and 902 relationships about a doctrinal tradition, zero unresolved relationships. Eight types, eight labels, four modes. It's the one in the examples above.

The second is a private professional wiki: 134 notes of knowledge distilled from a family-therapy library, for a clinical team. Nine types, seven labels, and a CI gate that doesn't publish if the graph drops below 300 nodes or if any of the viewer's three files is missing. The day a generator breaks silently, the site gets published anyway, with less content.

The third is the teardown of an industry standard for enterprise architecture, for Singular Solving: 427 nodes and 571 edges, the standard's entire catalog as a hierarchy plus the notes that cite it. Sixteen types, because the standard brings its own taxonomy. I told the whole story in the post about dismantling a reference framework.

All three download the same tarball, pinned by the same SHA. The only thing that changes between them is the config.

217 / 902notes and relationships · public corpus
134distilled notes · private wiki
427 / 571nodes and edges · industry standard
0domain lines in the engine

Validators are part of the design

Each rule has a default level the consumer can raise or lower. Errors are frontmatter that doesn't parse, a note without a type, and a type outside the closed set. Warnings are the unknown label, the note without a title, and —my favorite— the relationship declared from both ends, the mistake of someone who doesn't trust the derived mirror. One is off on purpose: pointing to a note that doesn't exist yet is a legitimate way of noting pending work.

When it fails at build time, it fails completely. The plugin runs in strict mode by default and throws [okf] build failed: N OKF conformance error(s); the site is not published. Outside the build, okf-check prints one line per violation and exits with code 1.

The reason I insist so much is one I learned by losing. The convention writes # Topology and the note body in ##, and by Markdown containment a # contains the ## that follow it: the extractor swallowed the entire note, and any prose bullet with the shape * **Something**: [[link]] became a typed relationship. In the clinical wiki that injected 154 edges nobody had declared, with 68 nonexistent labels, into a graph of 1,871.

The worst part is that it looked plausible. Denser, even. In a knowledge graph, a parser failure is indistinguishable from a model hallucination: no one detects it by looking at the picture. What gave it away was that the labels were not on the list.

What's stable and what isn't

The core contract I consider stable: the okf-graph/v1 format, the rules, and the inverse derivation. There are three published corpora on top and 31 Node tests covering the topology parser, link resolution, the graph, the exporter, and the rules. They pass.

The rest I don't sell as mature, because it isn't.

  • There is no CI in the toolkit repository. The tests pass, but I run them by hand. Consumers do have CI; the engine doesn't. It's the first item on the list.
  • It's not published. private: true, version 0.1.0, no tags or releases. It's consumed as a GitHub tarball pinned by SHA, which works but is not a package.
  • The license is already there. MIT, declared in the package.json and with its LICENSE file at the root.
  • The reference profile drags its origin along. The fourteen types it ships by default are infrastructure ones, and no consumer uses them as-is.

Thirty-eight commits in fourteen days, a single author. It is not a product: it is a reusable internal asset, published in the open, with four consumers in-house: the three published corpora and the infrastructure base where the toolkit was born.

Why a company should care about this

An organization's knowledge usually lives in a third-party wiki. It works until the day you switch tools and discover that what you have is not knowledge, it's an export. Here the corpus is markdown files in git, in a format with a public specification, and the tool that publishes them is open source, pinned by SHA. If the toolkit disappears tomorrow, the knowledge can still be read with cat.

The second part shows more in day-to-day work. Forcing every relationship to be declared inside a closed vocabulary turns documentation into something auditable: you can ask the graph which notes rest on a single source, what breaks if you remove a piece, where the corpus asserts more than it cites. And the answers come from what someone wrote on purpose, not from an inference.

Frequently asked questions

Can I use it in my repository today?

Technically yes: the repository is public and licensed, it's consumed by SHA, and the core has no runtime dependencies. But it's not on npm and has no CI of its own. If you try it now, do it knowing that.

Do you need OKF to have a knowledge graph?

No. You need a closed vocabulary and something that enforces it on every build. What leaning on someone else's specification buys you is that the format isn't mine: your corpus is markdown files that can be read without my tool and without my permission.

Is the graph built by a language model?

There isn't a single line of AI in the engine. Relationships are not extracted: they are declared by hand in the note's prose, and the validator rejects those outside the vocabulary. A model can help write the notes; the topology is deterministic and is audited by reading the markdown.

What if my documentation lives in Confluence or Notion?

Exporting to markdown is work, but it's known work. The part that can't be automated comes after: deciding which types and which relationships describe your domain. That's where the project is won or lost, and it's why the engine ships no vocabulary.

Zetesis-Labs/quartz-okf

Renderer-independent contract for knowledge graphs: validation, resolution, and typed topology on Quartz.

JavaScript

More from the lab

Do you have documentation no one consults because you can't ask it anything?

An internal library, a project archive, a regulation with hundreds of controls. If decisions have to come out of there, a search box isn't enough. Tell me what corpus you have and I'll tell you what vocabulary I'd give it.