<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Julien Chable</title>
    <description>The latest articles on DEV Community by Julien Chable (@julien_chable).</description>
    <link>https://dev.to/julien_chable</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4049224%2F5f7150a2-62bf-46fa-b034-04ce97614bdb.jpeg</url>
      <title>DEV Community: Julien Chable</title>
      <link>https://dev.to/julien_chable</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/julien_chable"/>
    <language>en</language>
    <item>
      <title>A folder of markdown you can actually browse: OKF4net 0.7 and the interactive okf-render</title>
      <dc:creator>Julien Chable</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:23:33 +0000</pubDate>
      <link>https://dev.to/julien_chable/a-folder-of-markdown-you-can-actually-browse-okf4net-07-and-the-interactive-okf-render-p37</link>
      <guid>https://dev.to/julien_chable/a-folder-of-markdown-you-can-actually-browse-okf4net-07-and-the-interactive-okf-render-p37</guid>
      <description>&lt;p&gt;An &lt;a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md" rel="noopener noreferrer"&gt;Open Knowledge Format&lt;/a&gt; bundle is a directory of markdown files with YAML frontmatter. That is what makes it good for agents and for code review: an agent reads it with &lt;code&gt;cat&lt;/code&gt;, a reviewer reads a change as a plain diff, and git does the rest. It is also what makes it tedious for a person to browse. Open the folder and you see file names; open a file and you see one concept, with no idea what links to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jchable/okf4net" rel="noopener noreferrer"&gt;OKF4net&lt;/a&gt;, our independent .NET implementation of the OKF spec, ships a site generator, &lt;code&gt;okf-render&lt;/code&gt;, for that person. Up to 0.6.0 it wrote a plain static site. Since OKF4net 0.7.0 it writes an interactive viewer. Here is a tour, then how it is built and tested, then what else changed in the release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In (very) short&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;okf-render &amp;lt;bundle&amp;gt; --out &amp;lt;dir&amp;gt;&lt;/code&gt; writes a folder of static files. Open &lt;code&gt;index.html&lt;/code&gt; from disk: no server, no network, nothing to install in the browser.&lt;/li&gt;
&lt;li&gt;Concept pages get a tree explorer with trust and staleness marks read from each concept's frontmatter; every page gets a &lt;em&gt;Jump to&lt;/em&gt; palette (Ctrl+K or /) and a light/dark theme.&lt;/li&gt;
&lt;li&gt;Each concept page draws its &lt;strong&gt;Neighbourhood&lt;/strong&gt;, the concepts it links to and that link to it, and a separate page draws the &lt;strong&gt;whole bundle's link graph&lt;/strong&gt; with facets, a detail drawer and shareable &lt;code&gt;graph.html#&amp;lt;concept id&amp;gt;&lt;/code&gt; addresses.&lt;/li&gt;
&lt;li&gt;Every drawing has a text equivalent and a keyboard path. The graph is one tab stop; the arrow keys walk from a node to its neighbours.&lt;/li&gt;
&lt;li&gt;Under the hood: no new runtime dependency, a graph layout that gives the same positions in Chromium, Firefox and WebKit, a DOM sanitizer as the whole defence against hostile markdown, and a test harness that runs the real scripts.&lt;/li&gt;
&lt;li&gt;Also in 0.7.0: &lt;code&gt;okf&lt;/code&gt; is now a dotnet tool you can pin in a repository's tool manifest for CI, a hardening wave closes a few ways to crash or fool the library, and four changes are worth checking when you upgrade (the list).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem: easy to diff, hard to browse
&lt;/h2&gt;

&lt;p&gt;OKF is Google's open format for knowledge that both people and agents read. A &lt;strong&gt;bundle&lt;/strong&gt; is a directory tree of markdown files. Each file is a &lt;strong&gt;concept&lt;/strong&gt;: a YAML frontmatter block (&lt;code&gt;type&lt;/code&gt; is the only required field; &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;tags&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and others are common) and a markdown body. A concept's id is its path without &lt;code&gt;.md&lt;/code&gt;, and concepts link to each other with ordinary markdown links. The spec also defines provenance and trust fields: &lt;code&gt;verified&lt;/code&gt; records who reviewed a concept and when (§5.2), and &lt;code&gt;stale_after&lt;/code&gt; says when it should be looked at again (§5.5).&lt;/p&gt;

&lt;p&gt;OKF4net implements v0.2 on the .NET base class library alone (its own YAML-subset parser and link scanner, no third-party packages), built from the spec rather than ported from another implementation. Besides the library it ships two Native AOT tools: &lt;code&gt;okf&lt;/code&gt;, which validates, audits and formats bundles, and &lt;code&gt;okf-render&lt;/code&gt;, which turns one into a website.&lt;/p&gt;

&lt;p&gt;Here is what &lt;code&gt;okf-render&lt;/code&gt; 0.6.0 produced for a concept of &lt;code&gt;acme_retail&lt;/code&gt;, the sample bundle in the repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zpvzoln92ce4favzs7u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8zpvzoln92ce4favzs7u.png" alt="The okf-render 0.6.0 page for the concept " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One page per concept, the frontmatter as a table, the body rendered, links that work, and an index page. It was correct, and it answered none of the questions you have while reading: where am I in the bundle, what else is there, who points at this, and can I trust it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; an OKF bundle is easy for agents and diffs, hard for people to browse. &lt;code&gt;okf-render&lt;/code&gt; used to write one flat page per concept; the new version turns those pages into a viewer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  One command, a folder of files
&lt;/h2&gt;

&lt;p&gt;The command is the same as before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf-render bundles/acme_retail &lt;span class="nt"&gt;--out&lt;/span&gt; ./acme-site
&lt;span class="c"&gt;# then open ./acme-site/index.html in any browser: double-click it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it writes is still a folder of plain files: &lt;code&gt;index.html&lt;/code&gt;, one page per concept at the concept's own path (&lt;code&gt;computations/gross-margin-period.html&lt;/code&gt;), a graph page for the whole bundle, and an &lt;code&gt;assets/&lt;/code&gt; folder with the stylesheet, the scripts, the fonts with their licences, and one generated data file, &lt;code&gt;okf-index.js&lt;/code&gt;. The graph page is &lt;code&gt;graph.html&lt;/code&gt; in the usual case, and the rest of this article calls it that; if a concept already owns that name (a root-level &lt;code&gt;graph.md&lt;/code&gt;, say), &lt;code&gt;okf-render&lt;/code&gt; picks &lt;code&gt;graph-1.html&lt;/code&gt;, then &lt;code&gt;graph-2.html&lt;/code&gt;, and so on, and the header link follows. Nothing is fetched from the network when a page opens, so the folder works from &lt;code&gt;file://&lt;/code&gt;, from a USB stick or from any static host. &lt;code&gt;okf-render&lt;/code&gt; itself is a self-contained Native AOT binary: the machine that renders needs no .NET runtime.&lt;/p&gt;

&lt;p&gt;It is deliberately a separate binary from &lt;code&gt;okf&lt;/code&gt;, the small validator that runs in CI, so that &lt;code&gt;okf&lt;/code&gt; does not carry what only a website needs: a vendored copy of &lt;a href="https://github.com/markedjs/marked" rel="noopener noreferrer"&gt;marked&lt;/a&gt; to render markdown in the browser, and three embedded font families.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Getting it.&lt;/strong&gt; The &lt;a href="https://github.com/jchable/okf4net/releases/tag/v0.7.0" rel="noopener noreferrer"&gt;0.7.0 release&lt;/a&gt; carries a prebuilt &lt;code&gt;okf-render&lt;/code&gt; archive for each of Windows, Linux and macOS, on x64 and arm64, next to a &lt;code&gt;checksums.txt&lt;/code&gt;. On Linux and macOS the install script fetches the right one, checks its SHA-256 and puts it on your &lt;code&gt;PATH&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/jchable/okf4net/main/packaging/install.sh | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--bin&lt;/span&gt; okf-render
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Windows, download the &lt;code&gt;okf-render-0.7.0-win-x64.zip&lt;/code&gt; (or &lt;code&gt;win-arm64&lt;/code&gt;) asset from the release page and unzip it. The &lt;code&gt;okf-render&lt;/code&gt; winget package has been submitted and is waiting in the winget-pkgs moderation queue, so &lt;code&gt;winget install Coderise.OKF4net.Render&lt;/code&gt; does not resolve yet. If you would rather build it, the &lt;code&gt;dotnet run&lt;/code&gt; command at the end of this post needs only the .NET SDK 10.0 or later.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; one command, one folder, &lt;code&gt;index.html&lt;/code&gt; opened from disk. Prebuilt archives are on the 0.7.0 release; the install script covers Linux and macOS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A guided tour
&lt;/h2&gt;

&lt;p&gt;All the captures below are of &lt;code&gt;acme_retail&lt;/code&gt; (nine concepts) or of a synthetic bundle of 112 concepts that the screenshot script generates, with fictional names, to show what nine concepts cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  The concept page
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nwr1folyt35veopo4ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3nwr1folyt35veopo4ad.png" alt="A concept page of acme_retail: the explorer on the left; in the middle a breadcrumb, the title " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concept pages and the index share one header: the bundle's name with its concept and link counts, the &lt;em&gt;Jump to a concept…&lt;/em&gt; button, &lt;em&gt;Filters&lt;/em&gt;, &lt;em&gt;Global graph&lt;/em&gt; and the theme toggle (the graph page has the same header, with &lt;em&gt;Reading view&lt;/em&gt; in place of &lt;em&gt;Filters&lt;/em&gt;). Below it, three columns from 1,100 px of window width up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On the left, the explorer&lt;/strong&gt;, described below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the middle, the concept.&lt;/strong&gt; A breadcrumb, the title, and chips for the type, the &lt;code&gt;status&lt;/code&gt;, the trust tier with who verified the concept and when, and its staleness. The frontmatter box shows four entries the chips do not already show, with &lt;em&gt;Show all&lt;/em&gt; for the rest. Then the body, rendered in the browser through a sanitizer (more on that below), with broken links flagged and heading anchors that make an author's own &lt;code&gt;#usage&lt;/code&gt; links land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the right, the context.&lt;/strong&gt; &lt;em&gt;On this page&lt;/em&gt; lists the headings and marks the one you are reading, &lt;em&gt;Neighbourhood&lt;/em&gt; draws the concept's links, and &lt;em&gt;Referenced by&lt;/em&gt; lists the concepts that link to this one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The explorer
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1dybtlpzxxrwi4gvaui0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1dybtlpzxxrwi4gvaui0.png" alt="The explorer of the synthetic bundle, with the Metric and Runbook type chips pressed and " width="578" height="1208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The explorer lists the bundle the way its directories do. A concept that is also a folder (&lt;code&gt;foo.md&lt;/code&gt; beside &lt;code&gt;foo/&lt;/code&gt;) can be opened and expanded separately. Rows show the last segment of the id, with the title as a tooltip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Types are shapes, not only colours.&lt;/strong&gt; The types are ranked by how many concepts carry them. The five most frequent get a circle, a square, a diamond, a triangle and a ring, each with its own colour; every further type shares a sixth shape. The same shape stands for the type in the explorer, the palette, the chips, the lists and both graphs. The type chips filter the tree: press several to show any of them, then narrow by name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trust and staleness come from the concept's own frontmatter.&lt;/strong&gt; A filled dot marks a &lt;em&gt;human-reviewed&lt;/em&gt; concept: its &lt;code&gt;verified&lt;/code&gt; field (§5.2) holds at least one stamp by a &lt;code&gt;human:&amp;lt;id&amp;gt;&lt;/code&gt; actor. A ring marks a &lt;em&gt;machine-confirmed&lt;/em&gt; one: stamps, none by a human. No &lt;code&gt;verified&lt;/code&gt; key, no mark. The tiers come from the same audit code &lt;code&gt;okf audit&lt;/code&gt; runs. An hourglass marks a concept past its &lt;code&gt;stale_after&lt;/code&gt; date, judged &lt;strong&gt;as of reading&lt;/strong&gt;: the page compares it with your browser's clock when it opens and when the tab comes back to the foreground, so a site rendered in March can show a concept as stale in June.&lt;/p&gt;

&lt;p&gt;A trust mark is a declaration, not a proof: it shows who a concept &lt;em&gt;says&lt;/em&gt; reviewed it. The spec calls these tiers advisory signals, not access control.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jump to a concept
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxdw07w0bmk93a5s65wj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxdw07w0bmk93a5s65wj6.png" alt="Three frames of the Jump to palette over a concept page: (1) opened empty with Ctrl+K, (2) the query " title="" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Press Ctrl+K or / on any page, or click the button, and type. The palette matches what you type against concept &lt;strong&gt;titles, ids and tags&lt;/strong&gt;, using fixed tiers tried best first: an exact id, then a prefix of the title or id, then a substring of either, then an exact tag. Ties keep the bundle's order. There are no weights, and it never reads a concept's body.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgv514gkq0scgszi10ry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frgv514gkq0scgszi10ry.png" alt="The palette at twice the pixel density, with the query " width="799" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why there is no full-text search.&lt;/strong&gt; OKF4net has exactly one full-text scorer, &lt;code&gt;ConceptSearch&lt;/code&gt;, in the .NET library, shared by the agent tools and the catalog so that "search" means one thing everywhere. A static site has no process to run it in, and copying its weights into JavaScript would create a second scorer that drifts from the first. So the palette navigates; it does not search. Body search exists today through the library and the &lt;code&gt;okf-mcp&lt;/code&gt; server, and interactive browsing with search is planned as a VS Code extension (&lt;a href="https://github.com/jchable/okf4net/issues/163" rel="noopener noreferrer"&gt;#163&lt;/a&gt;). There is no &lt;code&gt;okf serve&lt;/code&gt;, and none is planned.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Neighbourhood
&lt;/h3&gt;

&lt;p&gt;A concept page whose concept has neighbours (links out or links in) gets a &lt;em&gt;Neighbourhood&lt;/em&gt; section: the concept in the middle, its neighbours around it in rings, at &lt;strong&gt;1 hop&lt;/strong&gt; or, with the button, &lt;strong&gt;2 hops&lt;/strong&gt;; the legend reads &lt;em&gt;solid = links to, dashed = referenced by&lt;/em&gt;. A link to a concept that does not exist is drawn as a ghost that never navigates. &lt;em&gt;Open in graph&lt;/em&gt; opens the global graph on this concept.&lt;/p&gt;

&lt;p&gt;The panel draws &lt;strong&gt;at most 40 nodes&lt;/strong&gt;, the concept included, direct neighbours first, and says &lt;em&gt;+N omitted&lt;/em&gt; beyond. At the cap it is crowded. Here is the hub of the synthetic bundle, a glossary term with 61 neighbours:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flaox46lcxbzrp201x3v6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flaox46lcxbzrp201x3v6.png" alt="The Neighbourhood panel of the synthetic hub concept " width="668" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two things make it usable anyway. Under the drawing, &lt;em&gt;List&lt;/em&gt; gives &lt;strong&gt;every&lt;/strong&gt; neighbour, drawn or not, with its relation to the concept (links to, referenced by, or two hops via which neighbour). And &lt;em&gt;Enlarge the neighbourhood&lt;/em&gt;, the icon beside the hop toggle, opens the same graph in a dialog of up to 1,100 × 760 px, with the list beside it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7j5nr265kglpz450vfcd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7j5nr265kglpz450vfcd.png" alt="The same neighbourhood enlarged in a dialog titled " width="800" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Shapes and text keep their size; the rings spread out, and in a drawing this wide labels are cut at 32 characters instead of 20.&lt;/p&gt;

&lt;h3&gt;
  
  
  The global graph
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Global graph&lt;/em&gt; in the header opens &lt;code&gt;graph.html&lt;/code&gt;, which draws every body link of the bundle, the same edge set &lt;code&gt;okf graph&lt;/code&gt; exports, with arrows pointing at the target.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljmzx12n6yoyz8d5h1xg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fljmzx12n6yoyz8d5h1xg.png" alt="The global graph of acme_retail: facets on the left (type, trust, freshness, tags with " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Facets&lt;/strong&gt; narrow what is drawn: type, trust, freshness (&lt;em&gt;Stale only, as of now&lt;/em&gt;) and tags, &lt;em&gt;and&lt;/em&gt; between facets, &lt;em&gt;or&lt;/em&gt; inside one. Unmatched concepts are hidden or, as here, dimmed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The drawing&lt;/strong&gt; pans, zooms and lets you drag a node; &lt;em&gt;Fit&lt;/em&gt; frames everything drawn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The drawer&lt;/strong&gt; shows the selected concept: chips, description, what it links to, what references it, and &lt;em&gt;Open page&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Addresses.&lt;/strong&gt; Selecting a concept writes &lt;code&gt;graph.html#&amp;lt;concept id&amp;gt;&lt;/code&gt; to the address bar without adding a history entry, and opening such an address selects it, so you can paste it into a chat. A concept page's &lt;em&gt;Global graph&lt;/em&gt; link uses it to land on that concept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The layout is a force simulation computed in your browser. Here it is on the 112 concepts of the synthetic bundle:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8b2at8ggq6n18j1vrj7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc8b2at8ggq6n18j1vrj7.png" alt="The global graph of the synthetic bundle of 112 concepts: nodes of seven types spread over the canvas, the hub " width="800" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Above 1,500 visible nodes (concepts and absent targets), the page does not draw at all: it shows the list and asks for narrower filters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The side column, the theme and small screens
&lt;/h3&gt;

&lt;p&gt;The right-hand column (the context panel, or the graph's drawer) has a splitter. Drag it with a mouse, a finger or a pen, or focus it and use the arrow keys (16 px a step, 64 with Shift, Enter to reset). The width stays between 240 and 720 px, never leaving the middle column less than 360, and is remembered in the browser's local storage. The global graph refits to its new canvas unless you have moved the view yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1m6nwuz69yvm6plq792.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1m6nwuz69yvm6plq792.png" alt="The global graph page with the drawer widened by the splitter, which shows its focus ring; the drawing has refitted to the narrower canvas." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pages follow your system's light or dark setting; the moon button overrides it and remembers the choice where the browser allows storage.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wjtv7uzatdsquipdrqh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8wjtv7uzatdsquipdrqh.png" alt="The global graph of acme_retail in the dark theme, with the policy " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Below 1,100 px the columns stack (the page first, then its context, then the explorer), and on a phone the header wraps:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxliyibvkydfeezlhau1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmxliyibvkydfeezlhau1.png" alt="A concept page at 390 px wide: the header on three rows, then the breadcrumb, the title, the chips and the frontmatter box." width="780" height="1688"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; an explorer with type shapes and trust marks, a palette that jumps by title, id or tag, a Neighbourhood per concept with a dialog when it gets crowded, a global graph with facets, a drawer and shareable addresses, and a layout that holds from a phone to a wide screen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Accessibility and the keyboard
&lt;/h2&gt;

&lt;p&gt;We treated "a graph is a picture" as the main accessibility risk and designed around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One tab stop, then the arrows.&lt;/strong&gt; The global graph's drawing is a single tab stop. Inside it, Page Up and Page Down walk the nodes in the bundle's order, so an isolated node or a separate component is always reachable; Home and End go to the first and the last; an arrow key goes to the nearest neighbour in that direction; Space selects and Enter opens the concept's page. The selected node carries &lt;code&gt;aria-current&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66sqhokpxsz0q4cl8uum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F66sqhokpxsz0q4cl8uum.png" alt="Three frames of the global graph driven by the keyboard: (1) Tab and Home put a dashed focus contour on gross-margin-period, the first node in index order; (2) the left arrow moves it to its nearest neighbour on the left, revenue-ytd; (3) Space selects revenue-ytd, whose links turn blue." width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A text equivalent for every drawing.&lt;/strong&gt; The Neighbourhood has its &lt;em&gt;List&lt;/em&gt;. The global graph has its &lt;em&gt;List&lt;/em&gt;, which replaces the drawing in place and honours the same facets, plus the detail drawer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5twkbddst007tjyncm4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5twkbddst007tjyncm4k.png" alt="The List view of the global graph: every concept of acme_retail with its type shape and, under it, each concept it links to and each concept that references it." width="800" height="716"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus is handled in dialogs.&lt;/strong&gt; The palette is a modal dialog: focus goes to its field, stays inside, and returns to where it was on Esc. In the enlarged Neighbourhood, Tab wraps round the dialog's own controls, the page behind is inert and does not scroll, and Esc, &lt;em&gt;Close&lt;/em&gt; or a click on the backdrop return the focus to the button that opened it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never colour alone.&lt;/strong&gt; Types differ by shape and colour, trust by a filled dot or a ring. Text targets 4.5:1 contrast and shapes 3:1, in both themes, measured in real browsers. Under reduced motion, the graph's layout still runs, but only its start and end are drawn.&lt;/p&gt;

&lt;p&gt;What we have not done yet: manual checks with an input method editor and with a screen reader. They are tracked in &lt;a href="https://github.com/jchable/okf4net/issues/177" rel="noopener noreferrer"&gt;#177&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; one tab stop and arrow-key navigation in the graph, a list for every drawing, real focus management in both dialogs, shapes as well as colours. IME and screen-reader checks are still to do.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;This part is for the engineers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "zero dependency" means here
&lt;/h3&gt;

&lt;p&gt;OKF4net allows no third-party runtime dependency per project, and &lt;code&gt;OKF4net.Viewer&lt;/code&gt;, the library behind &lt;code&gt;okf-render&lt;/code&gt;, references only the core library. There is no npm package and no bundler: the browser scripts are plain files embedded in the .NET assembly. Two things are vendored, as named exceptions: &lt;a href="https://github.com/markedjs/marked" rel="noopener noreferrer"&gt;marked&lt;/a&gt; v15.0.12 (MIT), and the Inter, Inter Tight and Space Mono fonts (SIL OFL 1.1). Their licence texts are written into every generated site and ship beside the binary. &lt;code&gt;okf&lt;/code&gt; never references any of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  One generated data file, classic scripts
&lt;/h3&gt;

&lt;p&gt;The interactive parts need the whole bundle on every page: the tree, every concept's title, type, tags, trust tier and deadline, every edge. &lt;code&gt;okf-render&lt;/code&gt; writes it once as &lt;code&gt;assets/okf-index.js&lt;/code&gt;, a script that sets &lt;code&gt;window.OKF_INDEX&lt;/code&gt;, because a &lt;code&gt;fetch&lt;/code&gt; of a local JSON file is blocked under &lt;code&gt;file://&lt;/code&gt;. For the same reason the scripts are classic scripts, not ES modules: each one is wrapped in its own function, exposes one global, and does nothing if what it needs is missing, which also lets the Node test harness load each one exactly as a page does.&lt;/p&gt;

&lt;p&gt;Bundle text is untrusted and ends up in a JavaScript file, so every string from the bundle goes through one C# quoting function that writes a JSON string literal and also escapes &lt;code&gt;&amp;lt;&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;amp;&lt;/code&gt;, U+2028 and U+2029. The index holds only arrays and fixed-key records, never an object keyed by concept id: &lt;code&gt;__proto__&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt; and &lt;code&gt;toString&lt;/code&gt; are valid concept ids, and an object literal keyed by one of them does not mean what &lt;code&gt;JSON.parse&lt;/code&gt; of the same text means. Measured during development on OKF4net's own generated bundle (795 concepts, 912 edges), the file was 407,325 bytes.&lt;/p&gt;

&lt;h3&gt;
  
  
  A layout that is the same everywhere
&lt;/h3&gt;

&lt;p&gt;A force layout usually starts from random positions and runs until it looks settled, so two people opening the same bundle see two different pictures. We wanted the same picture, so the simulation, &lt;code&gt;okf-sim.js&lt;/code&gt;, is a pure module with constraints written at its top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;no DOM, no clock, no &lt;code&gt;Math.random&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;only &lt;code&gt;+ - * / % &amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;Math.sqrt&lt;/code&gt; (whose rounding ECMAScript fixes) and the exact &lt;code&gt;Math.floor&lt;/code&gt;, &lt;code&gt;ceil&lt;/code&gt;, &lt;code&gt;trunc&lt;/code&gt;, &lt;code&gt;abs&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt; and &lt;code&gt;imul&lt;/code&gt;; never &lt;code&gt;sin&lt;/code&gt;, &lt;code&gt;cos&lt;/code&gt;, &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;pow&lt;/code&gt; or &lt;code&gt;**&lt;/code&gt;, whose results the standard lets each engine approximate;&lt;/li&gt;
&lt;li&gt;a fixed order for every accumulation;&lt;/li&gt;
&lt;li&gt;work &lt;strong&gt;counted, never timed&lt;/strong&gt;, spread over animation frames: a frame may stop mid-iteration, but positions only change when an iteration completes, so the state after &lt;em&gt;k&lt;/em&gt; iterations does not depend on the slicing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The starting positions are a grid in index order, jittered by an integer generator seeded from the node and edge counts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 32-bit linear congruential generator on an integer state: exact.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nextState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1664525&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1013904223&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ... in initialLayout(n, edgeCount):&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;73856093&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;imul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edgeCount&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;19349663&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Labels are part of the layout: each node gives the box its shape and label cover, with the label width &lt;em&gt;estimated&lt;/em&gt; from its length rather than measured, because measured text differs between browsers and would make the layout differ too. The result: the same positions, bit for bit, in Chromium, Firefox and WebKit, checked on &lt;code&gt;acme_retail&lt;/code&gt;, on the harness fixture, on OKF4net's own bundle and on a synthetic bundle of 1,414 nodes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5hdfndxdo3djinyz7sa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo5hdfndxdo3djinyz7sa.png" alt="Three frames of the layout of the 112-concept synthetic bundle, labels off: (1) the starting grid in index order with its seeded jitter, (2) the layout three slices of work later, (3) the finished, fitted layout." width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The sanitizer is the whole defence
&lt;/h3&gt;

&lt;p&gt;A bundle's markdown is untrusted input. &lt;code&gt;marked&lt;/code&gt; turns it into HTML in the browser, and &lt;code&gt;viewer.js&lt;/code&gt; then sanitizes the &lt;strong&gt;parsed DOM&lt;/strong&gt;: an element allowlist, a per-tag attribute allowlist that drops every &lt;code&gt;on*&lt;/code&gt; handler, URL-scheme checks on &lt;code&gt;href&lt;/code&gt; and &lt;code&gt;src&lt;/code&gt;, and opaque elements (&lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; and a few more) removed with their content. Other unknown elements, such as &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, are unwrapped: the element goes, its sanitized children stay.&lt;/p&gt;

&lt;p&gt;An earlier version also patched marked's renderer hooks to drop raw HTML. Measured against the harness's hostile payloads, it stopped nothing the DOM sanitizer did not already stop, and it deleted benign content: a &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; block with a summary and some text came out empty. It also could not see the case the DOM sanitizer exists for: marked's &lt;code&gt;Renderer.image()&lt;/code&gt; writes the &lt;code&gt;alt&lt;/code&gt; attribute without escaping it, so a plain markdown image, &lt;code&gt;![foo" onerror="alert(1)](x.png)&lt;/code&gt;, with no raw HTML anywhere, breaks out of the attribute. Only a defence that looks at the resulting DOM catches that, so the hooks were removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;class&lt;/code&gt; attribute problem
&lt;/h3&gt;

&lt;p&gt;The sanitizer keeps &lt;code&gt;class&lt;/code&gt; in one place only: on &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;, where marked writes a fenced block's &lt;code&gt;language-…&lt;/code&gt; class. But it means a concept body can contain &lt;code&gt;&amp;lt;code class="okf-chip"&amp;gt;&lt;/code&gt;, wearing the viewer's own class names. If the stylesheet styled &lt;code&gt;.okf-chip&lt;/code&gt; wherever it appears, a hostile concept could draw a fake &lt;em&gt;human-reviewed&lt;/em&gt; badge, hide text from sighted readers, or lay an overlay that looks like a dialog.&lt;/p&gt;

&lt;p&gt;So every rule for the viewer's own interface, its &lt;em&gt;chrome&lt;/em&gt;, is &lt;strong&gt;anchored&lt;/strong&gt; to a container body content cannot produce, such as &lt;code&gt;#okf-explorer&lt;/code&gt; or &lt;code&gt;body &amp;gt; header.bar&lt;/code&gt; (the sanitizer strips &lt;code&gt;id&lt;/code&gt; from the body), and every lookup of the viewer's own elements starts from a fixed element id or an anchored selector, with any descendant query by class kept inside that root, so a class name worn by body content is never searched for from the whole document. The one lookup that does start from the whole document, resolving a &lt;code&gt;#fragment&lt;/code&gt; to a heading, checks that the element it finds is inside the page body before using it. The harness enforces it in three layers, none claimed as a proof on its own: a static scan of every selector in the stylesheet; a selector-matching comparison between a body element wearing each chrome class and its unclassed twin; and a comparison of their computed styles. Each delivery slice adds a fixture page listing its chrome classes, so new chrome is covered as soon as it exists.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing code that xunit cannot run
&lt;/h3&gt;

&lt;p&gt;The .NET test suite cannot execute JavaScript, and a test that greps a &lt;code&gt;.js&lt;/code&gt; file for a marker string stays green when the code behind the marker is gutted. So the guarantees live elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A jsdom harness&lt;/strong&gt; (&lt;code&gt;tools/viewer-security-check/&lt;/code&gt;) loads the real &lt;code&gt;marked.min.js&lt;/code&gt; and every viewer script into a site generated by the real &lt;code&gt;okf-render&lt;/code&gt; from a &lt;strong&gt;hostile fixture bundle&lt;/strong&gt;: titles containing HTML, concepts named &lt;code&gt;__proto__&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt; and &lt;code&gt;toString&lt;/code&gt;, broken links, clobbering attempts, body code wearing chrome classes. It drives the layout through an injected scheduler, so its checks are deterministic. It runs in CI as the &lt;code&gt;viewer sanitizer (JS)&lt;/code&gt; job, with 453 cases at the time of writing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mutation checks.&lt;/strong&gt; For the newest parts, deliberate bugs were planted in the scripts to check that a case fails for each: the splitter's cases killed 50 such mutants, the enlarged dialog's 30.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tooled recette in real browsers&lt;/strong&gt;, run by hand: it opens the generated sites from &lt;code&gt;file://&lt;/code&gt; in Chrome, Edge, Firefox and WebKit and measures what jsdom cannot see, such as fonts actually loaded, contrast in both themes, layout at three widths, and that no request leaves the site. Playwright is resolved at run time, never a dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Native AOT smoke test in CI&lt;/strong&gt; publishes the native &lt;code&gt;okf-render&lt;/code&gt; on Linux, macOS and Windows, renders a bundle, checks the output, and executes the &lt;code&gt;okf-index.js&lt;/code&gt; the native binary wrote.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The honest limits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 40-node cap is crowded at the cap.&lt;/strong&gt; On a hub, some labels sit far from their node and the edges converge on the centre. The dialog and the list are the answer, not the panel drawing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Big graphs.&lt;/strong&gt; Above 1,500 visible nodes the global graph shows a list instead of a drawing. The palette scans every concept on each keystroke, which very large bundles will feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not checked by hand yet:&lt;/strong&gt; input method editors and screen readers (#177), and Safari: the recette runs WebKit, which is not Safari.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No full-text search&lt;/strong&gt;, by design (see above), and &lt;strong&gt;no live reload&lt;/strong&gt;: the site is a snapshot, so render again after the bundle changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; no new runtime dependency, one generated index, classic scripts for &lt;code&gt;file://&lt;/code&gt;, a layout made deterministic by construction, a DOM sanitizer that does the whole job, anchored chrome selectors, and tests that run the real JavaScript. The limits are listed rather than hidden.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The viewer was specified before it was written: a design document in the repository (&lt;code&gt;docs/superpowers/specs/2026-10-06-okf-viewer-interactive-design.md&lt;/code&gt;, fifteen revisions) with validated mockups and the owner's decisions. It was delivered in slices on one branch (navigation, fidelity to the mockups, the local graph, the global graph, then the splitter and the enlarged dialog), each with its own plan, harness cases and browser recette. It was reviewed task by task and checked in real browsers, not only by the harness.&lt;/p&gt;

&lt;p&gt;Those browser checks earned their place, because jsdom does no layout. Labels overlapped on the first open of the global graph, which is why the simulation now lays out boxes, not points. A CSS rule pushed the selected list entry a screen below the fold, in Firefox only. The graph page held the main thread too long near the node limit until its drawing was spread over frames.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In short:&lt;/strong&gt; a written spec, small slices, and a browser in the loop for every slice.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Also in 0.7.0
&lt;/h2&gt;

&lt;p&gt;The viewer is the headline, but the release has more.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;okf&lt;/code&gt; as a dotnet tool
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;okf&lt;/code&gt; has been a Native AOT binary so far (winget, an install script, release archives). That still works, and still needs no .NET runtime. New in 0.7.0: a repository that already builds with the .NET SDK can pin &lt;code&gt;okf&lt;/code&gt; the way it pins any other tool, which suits CI better than downloading a binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new tool-manifest          &lt;span class="c"&gt;# once, in the repository; commit the dotnet-tools.json it creates&lt;/span&gt;
dotnet tool &lt;span class="nb"&gt;install &lt;/span&gt;OKF4net.Cli   &lt;span class="c"&gt;# records the version in the manifest&lt;/span&gt;
dotnet tool restore               &lt;span class="c"&gt;# what CI runs&lt;/span&gt;
dotnet okf validate ./knowledge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dotnet tool install -g OKF4net.Cli&lt;/code&gt; installs it for your own shell, and &lt;code&gt;dnx OKF4net.Cli --yes validate ./knowledge&lt;/code&gt; runs it once with nothing installed. &lt;code&gt;okf validate&lt;/code&gt; exits 1 on a non-conformant bundle, so the step fails the build; warnings are reported and never fail it. The package is portable IL and needs the .NET 10 runtime or a later one. A new CI job packs it, restores it from a manifest in a fresh directory and asserts &lt;code&gt;okf validate&lt;/code&gt;'s exit code on a conformant and on a non-conformant bundle, because the in-process tests never see the package itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  A hardening wave
&lt;/h3&gt;

&lt;p&gt;Four fixes in 0.7.0 close ways to crash, hang or fool the library:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;okf verify&lt;/code&gt; could be made to print a forged line.&lt;/strong&gt; The &lt;code&gt;(replaces …)&lt;/code&gt; suffix echoed the timestamp already stored in a concept without escaping it, so a line break in that value printed a fake &lt;code&gt;recorded …&lt;/code&gt; line. That mattered because &lt;code&gt;okf verify&lt;/code&gt; and the &lt;code&gt;okf_verify&lt;/code&gt; agent tool feed lines to people and to models. Every value on those lines is now made line-safe by the renderer itself (#123).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A value built in code could crash or hang the library.&lt;/strong&gt; A YAML mapping can hold itself, or hold the same sub-mapping at every one of 60 levels. Hashing such a value, comparing two of them or printing one used to overflow the stack, run out of memory or never finish. They now answer, or refuse with a normal exception (#158, #189). Nothing the parser returns is affected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loading a bundle with a very popular concept was quadratic.&lt;/strong&gt; A concept cited by 100,000 others took about 170 seconds to build its backlinks; it now takes under one second, and the order of the results is unchanged (#148).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A junction or symlink to a bundle now shares the bundle's write lock&lt;/strong&gt;, so two writers in one process reaching the same bundle by two paths can no longer interleave and lose an update (#86). The lock is still in-process only: a second process writing the same bundle is not serialized against.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What to check when you upgrade
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Frontmatter.RequiredKeys&lt;/code&gt; and &lt;code&gt;Frontmatter.RecommendedFields&lt;/code&gt; are read-only &lt;code&gt;IReadOnlyList&amp;lt;string&amp;gt;&lt;/code&gt; properties instead of mutable arrays (#113): compiled references to the old fields break, and so do &lt;code&gt;.Length&lt;/code&gt; and element assignment.&lt;/li&gt;
&lt;li&gt;The YAML subset has one nesting rule, at most 1000 nested collections, for block and flow alike. Block documents nested 501 to 1000 deep now load. Documents that mix block and flow past 1000 in total, which used to load, are now rejected. Real frontmatter is a handful of levels deep.&lt;/li&gt;
&lt;li&gt;A bare &lt;code&gt;okf&lt;/code&gt; or &lt;code&gt;okf-render&lt;/code&gt; prints its usage on stdout and exits 0, like &lt;code&gt;--help&lt;/code&gt;; it used to use stderr and exit 1. A verb missing a required argument still fails.&lt;/li&gt;
&lt;li&gt;The YAML emitter refuses, with a &lt;code&gt;YamlEmitException&lt;/code&gt;, a value built in code that reuses collections or long strings past a 16 MiB budget, or whose text would pass 256 MiB. Everything the parser returns is unaffected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://github.com/jchable/okf4net/blob/main/CHANGELOG.md#070---2026-10-14" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it, and where to help
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v0.7.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/jchable/okf4net &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;okf4net
dotnet run &lt;span class="nt"&gt;-c&lt;/span&gt; Release &lt;span class="nt"&gt;--project&lt;/span&gt; src/OKF4net.Render &lt;span class="nt"&gt;--&lt;/span&gt; bundles/acme_retail &lt;span class="nt"&gt;--out&lt;/span&gt; ./acme-site
&lt;span class="c"&gt;# open ./acme-site/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That needs only the .NET SDK 10.0 or later. With the prebuilt &lt;code&gt;okf-render&lt;/code&gt; from the section above, the second line becomes &lt;code&gt;okf-render bundles/acme_retail --out ./acme-site&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then point it at your own bundle. If you do not have one, &lt;code&gt;okf&lt;/code&gt; can validate one you write by hand, and the &lt;a href="https://jchable.github.io/okf4net/docs/getting-started/" rel="noopener noreferrer"&gt;getting-started guide&lt;/a&gt; walks through it.&lt;/p&gt;

&lt;p&gt;OKF4net is a small project and contributions are welcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;&lt;code&gt;good first issue&lt;/code&gt;&lt;/a&gt;: each one names the files to touch and the test to make pass.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/jchable/okf4net/labels/help%20wanted" rel="noopener noreferrer"&gt;&lt;code&gt;help wanted&lt;/code&gt;&lt;/a&gt;: bigger pieces.&lt;/li&gt;
&lt;li&gt;Viewer-specific help we would value: a screen-reader and IME pass (&lt;a href="https://github.com/jchable/okf4net/issues/177" rel="noopener noreferrer"&gt;#177&lt;/a&gt;), a check in real Safari, and measurements on large real-world bundles.&lt;/li&gt;
&lt;li&gt;Questions before you code: &lt;a href="https://github.com/jchable/okf4net/discussions" rel="noopener noreferrer"&gt;GitHub Discussions&lt;/a&gt;. How to build, test and submit: &lt;a href="https://github.com/jchable/okf4net/blob/main/CONTRIBUTING.md" rel="noopener noreferrer"&gt;&lt;code&gt;CONTRIBUTING.md&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full viewer guide is on the &lt;a href="https://jchable.github.io/okf4net/docs/viewer/" rel="noopener noreferrer"&gt;project site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A static site you open from disk, no server, no network&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;okf-render &amp;lt;bundle&amp;gt; --out &amp;lt;dir&amp;gt;&lt;/code&gt;, then &lt;code&gt;index.html&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tree explorer with type shapes, type filters, trust and staleness marks&lt;/td&gt;
&lt;td&gt;concept pages and the index, left column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;em&gt;Jump to&lt;/em&gt; palette over titles, ids and tags (not full-text search)&lt;/td&gt;
&lt;td&gt;every page: Ctrl+K or /
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concept page: chips, folding frontmatter, sanitized body, contents, &lt;em&gt;Referenced by&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;every concept page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neighbourhood at 1 or 2 hops, 40-node cap, full list, enlarge into a dialog&lt;/td&gt;
&lt;td&gt;concept page, right column&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global graph: deterministic layout, facets, drawer, list, keyboard, &lt;code&gt;#&amp;lt;concept id&amp;gt;&lt;/code&gt; addresses&lt;/td&gt;
&lt;td&gt;&lt;code&gt;graph.html&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resizable side column, light and dark themes, phone layout&lt;/td&gt;
&lt;td&gt;everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Try it:&lt;/strong&gt; prebuilt archives are on the &lt;a href="https://github.com/jchable/okf4net/releases/tag/v0.7.0" rel="noopener noreferrer"&gt;0.7.0 release&lt;/a&gt;, &lt;code&gt;install.sh --bin okf-render&lt;/code&gt; installs one on Linux and macOS, and &lt;code&gt;dotnet run --project src/OKF4net.Render&lt;/code&gt; builds it from the tag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Also in 0.7.0:&lt;/strong&gt; &lt;code&gt;okf&lt;/code&gt; as the &lt;code&gt;OKF4net.Cli&lt;/code&gt; dotnet tool for CI; fixes for a forged &lt;code&gt;okf verify&lt;/code&gt; line, crashes and hangs on values built in code, and a quadratic backlink build; four changes to check when you upgrade.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Limits and next:&lt;/strong&gt; no full-text search in the site (search is planned in the VS Code extension, #163); IME, screen-reader and Safari checks to do (#177 for the first two); a list instead of a drawing above 1,500 nodes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute:&lt;/strong&gt; &lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;good first issues&lt;/a&gt;, &lt;a href="https://github.com/jchable/okf4net/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt;, the &lt;a href="https://github.com/jchable/okf4net" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OKF is an open format from Google; OKF4net is an independent .NET implementation of it, LGPL-3.0-or-later. If the viewer is useful to you, or broken for you, we would like to hear about it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>markdown</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>OKF4net 0.6.0: auditing what your AI agent knows, in zero-dependency .NET</title>
      <dc:creator>Julien Chable</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:26:57 +0000</pubDate>
      <link>https://dev.to/julien_chable/okf4net-060-auditing-what-your-ai-agent-knows-in-zero-dependency-net-492d</link>
      <guid>https://dev.to/julien_chable/okf4net-060-auditing-what-your-ai-agent-knows-in-zero-dependency-net-492d</guid>
      <description>&lt;p&gt;OKF4net 0.6.0 shipped on 1 October. This post covers what it does and what&lt;br&gt;
changed, with commands you can run.&lt;/p&gt;
&lt;h2&gt;
  
  
  What OKF4net is
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md" rel="noopener noreferrer"&gt;Open Knowledge Format&lt;/a&gt;&lt;br&gt;
(OKF) is Google's open format for knowledge an AI agent can read: a directory&lt;br&gt;
of markdown files, each with YAML frontmatter saying what the file is, where it&lt;br&gt;
came from and how far to trust it. No database, no server. You ship it with&lt;br&gt;
&lt;code&gt;git clone&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jchable/okf4net" rel="noopener noreferrer"&gt;OKF4net&lt;/a&gt; is an independent .NET 10&lt;br&gt;
implementation of OKF v0.2. The core library and the &lt;code&gt;okf&lt;/code&gt; CLI use the base&lt;br&gt;
class library only: the YAML-subset parser, the markdown link scanner and the&lt;br&gt;
argument parser are all in the repository. The CLI is published as a Native AOT&lt;br&gt;
binary.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem 0.6.0 goes after
&lt;/h2&gt;

&lt;p&gt;Once an agent reads from a knowledge bundle, the question stops being "is this&lt;br&gt;
file valid?" and becomes "should anyone believe it?". A concept may be&lt;br&gt;
well-formed and two years out of date. It may have been written by a model and&lt;br&gt;
never read by a person.&lt;/p&gt;

&lt;p&gt;OKF v0.2 has fields for this: who generated a concept, who verified it, when it&lt;br&gt;
goes stale. 0.6.0 adds the tooling to query and maintain them.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;okf audit&lt;/code&gt;: ask the whole bundle
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf audit bundles/acme_retail &lt;span class="nt"&gt;--stale&lt;/span&gt; &lt;span class="nt"&gt;--trust&lt;/span&gt; unverified,machine-confirmed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This lists the concepts that are past their &lt;code&gt;stale_after&lt;/code&gt; date and that no&lt;br&gt;
human has confirmed. With no flags, &lt;code&gt;okf audit&lt;/code&gt; gives you the staleness&lt;br&gt;
worklist. It exits 0 and prints nothing when there is nothing to do, so it&lt;br&gt;
drops into a CI job or a pipe.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;okf verify&lt;/code&gt;: record a review
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf verify bundles/acme_retail metrics/revenue &lt;span class="nt"&gt;--by&lt;/span&gt; human:ada
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This writes a dated verification stamp into the concept's frontmatter. It reads&lt;br&gt;
ids from stdin too, so the two verbs compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf audit bundles/acme_retail &lt;span class="nt"&gt;--trust&lt;/span&gt; unverified | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | okf verify bundles/acme_retail &lt;span class="nt"&gt;--by&lt;/span&gt; human:ada -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat, stated in the README as well: the stamp is a dated declaration. The&lt;br&gt;
tool cannot authenticate the name you pass to &lt;code&gt;--by&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;okf-render&lt;/code&gt;: a browsable site from a bundle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf-render bundles/ga4 &lt;span class="nt"&gt;--out&lt;/span&gt; ./ga4-site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is static HTML you can open from disk or host anywhere. It is a&lt;br&gt;
separate binary from &lt;code&gt;okf&lt;/code&gt; so that the validator people run in CI stays small.&lt;/p&gt;

&lt;p&gt;Because the viewer renders untrusted markdown in a browser, it sanitizes the&lt;br&gt;
parsed DOM against an element and attribute allowlist. That sanitizer is tested&lt;br&gt;
by a Node harness that runs the real viewer code against hostile payloads in CI.&lt;br&gt;
We added the harness after finding that two .NET tests "proving" raw HTML was&lt;br&gt;
disabled were green while the sanitizer had a hole: they were reading the &lt;code&gt;.js&lt;/code&gt;&lt;br&gt;
file as text, because xunit cannot execute JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attested computation, in a real container
&lt;/h2&gt;

&lt;p&gt;OKF §10 lets a concept declare a computation (a script or SQL) and an attester&lt;br&gt;
script that judges the result. &lt;code&gt;OKF4net.Attestation.Containers&lt;/code&gt; runs both in&lt;br&gt;
Docker, Podman or nerdctl:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the bundle's own script runs, not a C# port of it;&lt;/li&gt;
&lt;li&gt;containers run as an unprivileged user, all capabilities dropped;&lt;/li&gt;
&lt;li&gt;no volume is mounted, every payload travels over stdin;&lt;/li&gt;
&lt;li&gt;SQL parameters are bound by the database driver, never spliced into the text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is in the repository with a worked sample, but not published to NuGet yet,&lt;br&gt;
since it needs a container engine on the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  For agent builders: MCP, read-only by default
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet tool &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; OKF4net.Mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;okf-mcp&lt;/code&gt; exposes a bundle to any MCP client over stdio. As of 0.6.0 it serves&lt;br&gt;
the bundle read-only; set &lt;code&gt;OKF_MCP_WRITABLE=1&lt;/code&gt; to register the write tools.&lt;br&gt;
There are 13 &lt;code&gt;okf_*&lt;/code&gt; tools in total, including the new &lt;code&gt;okf_audit&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;okf_verify&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;0.5.0 (31 Jul)&lt;/th&gt;
&lt;th&gt;0.6.0 (1 Oct)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Commits in the release&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;642&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of C# in &lt;code&gt;src/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;13,439&lt;/td&gt;
&lt;td&gt;24,120&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of C# in tests&lt;/td&gt;
&lt;td&gt;13,418&lt;/td&gt;
&lt;td&gt;30,512&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests run&lt;/td&gt;
&lt;td&gt;912&lt;/td&gt;
&lt;td&gt;2,352&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projects under &lt;code&gt;src/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The project is ten weeks old and 1,194 commits in. In this release the test&lt;br&gt;
code overtook the product code, which is a fair summary of where the effort&lt;br&gt;
went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 0.5.0
&lt;/h2&gt;

&lt;p&gt;0.6.0 has breaking changes. The four you are most likely to meet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bare &lt;code&gt;attester.resource&lt;/code&gt; and &lt;code&gt;computation&lt;/code&gt; paths resolve from the bundle
root (§6.2). Run &lt;code&gt;okf validate&lt;/code&gt;; it tells you what to rewrite.&lt;/li&gt;
&lt;li&gt;The frontmatter fence must be &lt;code&gt;---&lt;/code&gt; at column 0, and YAML anchors, aliases
and tags are rejected with a named error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Bundle.ReadResourceText&lt;/code&gt; throws for a path outside the bundle root.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;okf-mcp&lt;/code&gt; no longer writes unless you opt in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full list is at the top of the&lt;br&gt;
&lt;a href="https://github.com/jchable/okf4net/blob/main/CHANGELOG.md#060---2026-10-01" rel="noopener noreferrer"&gt;0.6.0 changelog section&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it, or get involved
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package OKF4net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Binaries for Windows, Linux and macOS are on the&lt;br&gt;
&lt;a href="https://github.com/jchable/okf4net/releases/tag/v0.6.0" rel="noopener noreferrer"&gt;release page&lt;/a&gt;, and the&lt;br&gt;
docs are at &lt;a href="https://jchable.github.io/okf4net/" rel="noopener noreferrer"&gt;jchable.github.io/okf4net&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The project is looking for contributors. There are issues labelled&lt;br&gt;
&lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;&lt;code&gt;good first issue&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
and a public &lt;a href="https://github.com/jchable/okf4net/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;br&gt;
If you run Podman or nerdctl, trying the container runtime on it would be a&lt;br&gt;
real contribution: so far it has only been exercised on Docker.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Agent memory you can 'git blame' and how OKF4net grew into a zero-dependency .NET toolkit</title>
      <dc:creator>Julien Chable</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:51:01 +0000</pubDate>
      <link>https://dev.to/julien_chable/agent-memory-you-can-git-blame-and-how-okf4net-grew-into-a-zero-dependency-net-toolkit-2bpl</link>
      <guid>https://dev.to/julien_chable/agent-memory-you-can-git-blame-and-how-okf4net-grew-into-a-zero-dependency-net-toolkit-2bpl</guid>
      <description>&lt;p&gt;Most answers to "give my AI agent long-term memory" hand you an opaque vector store: embeddings in a database you can't read, can't diff, and can't easily redact. I wanted the opposite : memory I can open in an editor, review in a pull request, and 'git blame' line by line. That itch is what pushed &lt;strong&gt;OKF4net&lt;/strong&gt; from a single library into a small toolkit over the last few months. It just reached &lt;strong&gt;v0.5&lt;/strong&gt;, and this post is about what's new — starting with the part I'm most excited about.&lt;/p&gt;

&lt;p&gt;If you haven't seen it before: &lt;a href="https://github.com/jchable/okf4net" rel="noopener noreferrer"&gt;&lt;strong&gt;OKF4net&lt;/strong&gt;&lt;/a&gt; (&lt;a href="https://jchable.github.io/okf4net/" rel="noopener noreferrer"&gt;docs &amp;amp; project site&lt;/a&gt;) is an independent, zero-dependency .NET implementation of Google's &lt;a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md" rel="noopener noreferrer"&gt;Open Knowledge Format (OKF)&lt;/a&gt;. OKF represents knowledge as a directory of markdown files with YAML frontmatter, cross-linked like a wiki, versioned in git. No database, no proprietary format: if you can 'cat' a file you can read it, if you can 'git clone' a repo you can ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline: agent memory as plain markdown
&lt;/h2&gt;

&lt;p&gt;'OKF4net.Agents' turns a bundle into tools and context for the &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Microsoft Agent Framework&lt;/a&gt;. 'OkfBundleTools' exposes read, browse, graph, search, write, append-log, validate and more as function tools an agent can call directly. Layer 'OkfContextProvider' onto the same instance and the agent automatically gets relevant bundle context injected into each turn — and, when you opt in, its exchanges captured back into the bundle as long-term memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Agents.AI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.AI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;OKF4net.Agents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkfBundleTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./my_bundle"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Memory capture is opt-in (Disabled by default) — turn it on explicitly.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkfContextProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;OkfContextProviderOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;MemoryCapture&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MemoryCaptureMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;AIAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ChatClientAgentOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ChatOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ChatOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;AIContextProviders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"What do we know about orders?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what makes it different. Capture is &lt;strong&gt;deterministic (no extra LLM call)&lt;/strong&gt;: after each turn, the last user message and the agent's final response are appended to a single memory concept for the day ('memory/'), with a matching 'log.md' change-history entry. Those writes go through the exact same validated, lock-protected, path-safe write path any other caller uses, and the captured text is blockquote-neutralized so a payload smuggled into a message can't fake document structure.&lt;/p&gt;

&lt;p&gt;The payoff is that memory is just files in your bundle. You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;'git blame'&lt;/strong&gt; a remembered fact to see exactly when and in which exchange it entered the memory,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;diff&lt;/strong&gt; memory across commits and &lt;strong&gt;redact&lt;/strong&gt; a line with a normal edit,&lt;/li&gt;
&lt;li&gt;point a &lt;strong&gt;second agent&lt;/strong&gt; at the same directory with no export step,&lt;/li&gt;
&lt;li&gt;and review the whole thing in a PR like any other change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll be honest about where v1 stands: this captured memory is &lt;strong&gt;bundle-global, unscoped, and opt-in&lt;/strong&gt; : it carries no session/user/tenant key, so it's meant for a shared, non-sensitive knowledge base, which is exactly why it ships disabled by default. If you need per-caller isolation, that lives one layer up in the catalog (below). I'd rather ship an honest, inspectable v1 than a magic box (maybe next version ?).&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of what's new (v0.2 → v0.5)
&lt;/h2&gt;

&lt;p&gt;The memory story is the headline, but the project grew in every direction since the first release. OKF4net now ships as &lt;strong&gt;five NuGet packages&lt;/strong&gt; plus a couple of standalone tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A local MCP server, 'OKF4net.Mcp'.&lt;/strong&gt; The 'okf-mcp' 'dotnet tool' exposes a bundle to Claude Desktop or Claude Code over the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; now with &lt;strong&gt;bundle auto-discovery&lt;/strong&gt;, so it finds your knowledge base instead of making you wire a path. Recipes for other MCP-capable editors (VS Code, Cursor, Rider…) are on the &lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;issue tracker&lt;/a&gt; as good-first contributions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A multi-bundle catalog, 'OKF4net.Catalog'.&lt;/strong&gt; A 'catalog.json' manifest, a source resolver, and full-text search across &lt;em&gt;many&lt;/em&gt; bundles with read-only knowledge sources and writable &lt;strong&gt;memory tiers&lt;/strong&gt; ('session' / 'user' / 'tenant') for exactly the per-caller scoping the agent provider's v1 memory doesn't do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Machine-readable CLI output.&lt;/strong&gt; The Native AOT 'okf' CLI ('validate'/'info'/'index'/'graph'/'parse'/'fmt') gained a '--json' flag on 'validate' and 'info', so a CI step can parse structured diagnostics instead of scraping text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A producer that turns a repo into a bundle.&lt;/strong&gt; 'producers/OkfProducer' generates a validated OKF bundle straight from an existing repository (npm/NuGet/README detection so far). It's an early walking skeleton, but it closes the loop: you don't have to hand-author a bundle to try the ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Through all of it, the core constraint held: 'OKF4net' and its CLI have &lt;strong&gt;zero third-party runtime dependencies&lt;/strong&gt; — base class library only, hand-written YAML-subset parser and link scanner included. That's what lets the CLI ship as a single self-contained Native AOT binary with no runtime to install, and it keeps the barrier to contributing low: there's no framework to learn before you can read the code. Much of OKF4net is built with AI assistance (Claude Code), with the OKF spec and an extensive test suite — including byte-exact golden CLI captures — as the ground truth every change has to satisfy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come contribute
&lt;/h2&gt;

&lt;p&gt;OKF4net is open source under &lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt; and deliberately welcoming to first contributions, no prior OKF knowledge needed. The &lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;'good first issue'&lt;/a&gt; label names the files to touch and the test that should go green, &lt;a href="https://github.com/jchable/okf4net/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;'ROADMAP.md'&lt;/a&gt; shows where it's headed, and &lt;a href="https://github.com/jchable/okf4net/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; is the place to ask before you write any code. If you'd rather start by reading, the &lt;a href="https://jchable.github.io/okf4net/" rel="noopener noreferrer"&gt;project site and docs&lt;/a&gt; are the friendlier way in. Your first PR is three commands away: 'dotnet build', 'dotnet test', 'dotnet format'.&lt;/p&gt;

&lt;p&gt;If "agents that remember things in files you can read" sounds useful, I'd love the help and the feedback.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OKF4net is built and maintained by &lt;a href="https://www.linkedin.com/company/coderise-fr" rel="noopener noreferrer"&gt;Coderise&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I ported a knowledge-format (OKF) library to zero-dependency .NET — here's what I learned</title>
      <dc:creator>Julien Chable</dc:creator>
      <pubDate>Mon, 27 Jul 2026 10:09:05 +0000</pubDate>
      <link>https://dev.to/julien_chable/i-ported-a-knowledge-format-okf-library-to-zero-dependency-net-heres-what-i-learned-1mb7</link>
      <guid>https://dev.to/julien_chable/i-ported-a-knowledge-format-okf-library-to-zero-dependency-net-heres-what-i-learned-1mb7</guid>
      <description>&lt;p&gt;If you can &lt;code&gt;cat&lt;/code&gt; a file, you can read the knowledge base. If you can &lt;code&gt;git clone&lt;/code&gt; a repo, you can ship it. No vector database to stand up, no proprietary export format, no vendor lock-in — just a directory of markdown files with YAML frontmatter that a human can open in any editor and an agent can read with &lt;code&gt;ReadFile&lt;/code&gt;. That's the whole pitch, and it's the reason I spent the last few weeks porting a library to C# to get it onto .NET.&lt;/p&gt;

&lt;p&gt;The format behind that pitch is Google's &lt;a href="https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md" rel="noopener noreferrer"&gt;Open Knowledge Format (OKF) v0.2&lt;/a&gt;, and the library is &lt;a href="https://github.com/jchable/okf4net" rel="noopener noreferrer"&gt;&lt;strong&gt;OKF4net&lt;/strong&gt;&lt;/a&gt; (&lt;a href="https://jchable.github.io/okf4net/" rel="noopener noreferrer"&gt;docs &amp;amp; project site&lt;/a&gt;) — a zero-dependency .NET&lt;br&gt;
 (C#, net10.0) implementation, plus an optional layer for wiring OKF bundles straight into agents built on the Microsoft Agent Framework. This post is the launch story: what OKF actually is, why I ported it instead of writing a wrapper, and what "zero dependency" really costs and buys you.&lt;/p&gt;
&lt;h2&gt;
  
  
  What OKF is
&lt;/h2&gt;

&lt;p&gt;OKF defines a &lt;strong&gt;bundle&lt;/strong&gt;: a directory tree of UTF-8 markdown files, where each file is a &lt;strong&gt;concept&lt;/strong&gt; — a YAML frontmatter block followed by a markdown body. Concepts cross-link each other with ordinary markdown links, &lt;code&gt;index.md&lt;/code&gt; files give you progressive-disclosure directory listings, and &lt;code&gt;log.md&lt;/code&gt; files record date-grouped change history. The only hard conformance requirement is a non-empty &lt;code&gt;type&lt;/code&gt; field on every concept; everything else — unknown types, unknown keys, broken links — has to be tolerated by a conformant consumer. It's deliberately boring as a format, which is the point: the format is context here, not the pitch. The pitch is what you get to do with plain files — diff them, review them in a PR, grep them, back them up with nothing but &lt;code&gt;git&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The port story
&lt;/h2&gt;

&lt;p&gt;This repository used to ship a Rust implementation of OKF. I removed it at commit &lt;code&gt;d20343c&lt;/code&gt; — but only after proving, file by file and command by command, that the C# port produced byte-identical output. &lt;code&gt;tests/fixtures/golden/&lt;/code&gt; holds five golden captures taken directly from the Rust binary's stdout — &lt;code&gt;validate&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;graph --dot&lt;/code&gt;, &lt;code&gt;fmt&lt;/code&gt;, and &lt;code&gt;index&lt;/code&gt; — against a shared example bundle, and the C# CLI is diffed byte-for-byte against every one of them in CI. As of today the full suite passes end-to-end, including five byte-exact golden CLI comparisons against the original captures.&lt;/p&gt;

&lt;p&gt;I'll say the quiet part out loud: this port was AI-assisted, done largely with Claude Code driving the migration file by file, spec section by spec section, with the golden fixtures as the ground truth it had to match exactly. I think that's worth stating plainly rather than glossing over — a byte-exact port across languages is a fairly mechanical, well-specified translation task with an unambiguous pass/fail signal (does the output match the captured bytes, yes or no), which is exactly the kind of task where an AI pair-programmer earns its keep and where you can trust the result &lt;em&gt;because&lt;/em&gt; you can verify it byte-for-byte rather than having to take anyone's word for it. The interesting design decisions — the YAML subset, the permissive-loading philosophy, the two-tier validation split — came from following the spec and the Python reference implementation; the AI assistance was in the grinding, get-every-byte-right execution, not the architecture.&lt;/p&gt;
&lt;h2&gt;
  
  
  Show, don't tell
&lt;/h2&gt;

&lt;p&gt;Here's the library, loading a bundle and running a conformance check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;OKF4net&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./my_bundle"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; concepts"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Conformance check (§9).&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BundleValidator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsConformant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"conformant with OKF v&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;OkfSpec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Traverse the cross-link graph.&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConceptId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tables/orders"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LinksFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Target&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (exists: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Bundle.Load&lt;/code&gt; never aborts on a malformed concept file — it collects parse failures into &lt;code&gt;bundle.ParseErrors&lt;/code&gt; and keeps walking the tree, because a knowledge base that one bad file can take down entirely is a bad knowledge base.&lt;/p&gt;

&lt;p&gt;And here's the CLI, which is the same tool the Rust binary used to be, invocation-for-invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;okf validate ./bundles/ga4
okf graph ./bundles/ga4 &lt;span class="nt"&gt;--dot&lt;/span&gt; | dot &lt;span class="nt"&gt;-Tsvg&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; graph.svg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;okf validate&lt;/code&gt; exits non-zero on a non-conformant bundle, so it drops straight into a CI step. The CLI ships as a self-contained, Native AOT single-file binary — no .NET runtime install required on the machine that runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agents angle
&lt;/h2&gt;

&lt;p&gt;The reason I care about this format enough to port a whole library for it is &lt;code&gt;OKF4net.Agents&lt;/code&gt;, which turns an OKF bundle into tools and context for the &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Microsoft Agent Framework&lt;/a&gt;. &lt;code&gt;OkfBundleTools&lt;/code&gt; wraps one bundle root and exposes nine function tools — read, browse, graph, search, write, append-log, regenerate-indexes, validate, changes-since — that an &lt;code&gt;AIAgent&lt;/code&gt; can call directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OkfBundleTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./my_bundle"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;AIAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chatClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTools&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Search the bundle for concepts about refunds."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Layer &lt;code&gt;OkfContextProvider&lt;/code&gt; onto the same tools instance and, opted in explicitly, an agent's exchanges get captured as long-term memory — one markdown concept per UTC day, written through the same validated, lock-protected write path the tools use, plus a matching &lt;code&gt;log.md&lt;/code&gt; entry. That's the part I think is genuinely different from the usual answer to "give my agent memory": instead of an opaque vector store you can't audit, memory is a markdown file in a git-tracked directory. You can open it, diff it across commits, redact a line, or point a second agent at the exact same directory with no export step. It's not a fit for every use case — the README is upfront that v1 memory is bundle-global and unscoped, so it's opt-in and meant for a shared, non-sensitive bundle rather than a multi-tenant deployment — but for a single team's shared knowledge base, "memory you can &lt;code&gt;git blame&lt;/code&gt;" is a real capability, not a slogan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design choices
&lt;/h2&gt;

&lt;p&gt;The whole library — &lt;code&gt;OKF4net&lt;/code&gt; and &lt;code&gt;OKF4net.Cli&lt;/code&gt; — has zero third-party runtime dependencies: no YAML library, no CLI-parsing package, nothing. It has its own documented YAML &lt;em&gt;subset&lt;/em&gt; parser (frontmatter is scalars, lists, and shallow maps — no anchors, no tags, no multi-document files, and it says so with a clear error if you hand it those), its own markdown link scanner, and its own argument parsing, all on top of the .NET base class library. That constraint is what makes the CLI publishable as a single-file Native AOT binary with no runtime to install, and it's what keeps the barrier to contributing low — there's no framework to learn before you can read the code. &lt;code&gt;OKF4net.Agents&lt;/code&gt; is the one exception, since talking to &lt;code&gt;Microsoft.Agents.AI&lt;/code&gt; requires depending on it; everything else stays dependency-free by design, enforced project by project. The project also ships &lt;code&gt;OKF4net.Catalog&lt;/code&gt;, a local multi-bundle catalog with search-by-source resolution, and &lt;code&gt;OKF4net.Mcp&lt;/code&gt;, an MCP server that plugs a bundle straight into Claude Desktop or Claude Code — so agents and tools have a ready path to discover and query bundles without writing that plumbing themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Come contribute
&lt;/h2&gt;

&lt;p&gt;OKF4net is young and I'd rather it stay welcoming than gate-kept. If you'd like to start by reading, the &lt;a href="https://jchable.github.io/okf4net/" rel="noopener noreferrer"&gt;project site and docs&lt;/a&gt; are the friendlier entry point — a guided overview and getting-started walkthroughs.&lt;br&gt;
You don't need any prior OKF knowledge to help — the &lt;a href="https://github.com/jchable/okf4net/labels/good%20first%20issue" rel="noopener noreferrer"&gt;&lt;code&gt;good first issue&lt;/code&gt;&lt;/a&gt; label names the files to touch and the test that should go green when you're done, &lt;a href="https://github.com/jchable/okf4net/blob/main/ROADMAP.md" rel="noopener noreferrer"&gt;&lt;code&gt;ROADMAP.md&lt;/code&gt;&lt;/a&gt; lays out where the project is headed, and &lt;a href="https://github.com/jchable/okf4net/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; is the place to ask a question before you write any code. The project is licensed &lt;strong&gt;LGPL-3.0-or-later&lt;/strong&gt;, and the bar to your first PR is exactly three commands: &lt;code&gt;dotnet build&lt;/code&gt;, &lt;code&gt;dotnet test&lt;/code&gt;, &lt;code&gt;dotnet format&lt;/code&gt;. If any part of "knowledge bundles you can &lt;code&gt;cat&lt;/code&gt; and agents that remember things in files you can read" sounds useful to you, I'd love the help — and the feedback.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>opensource</category>
      <category>softwaredevelopment</category>
    </item>
  </channel>
</rss>
