CtrlK
BlogDocsLog inGet started
Tessl Logo

wagneripjr/okf-maintain

Adopt and maintain an Open Knowledge Format v0.2 documentation bundle: frontmatter, generated indexes, a fail-closed conformance check, and coverage that names every unindexed document

72

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

okf.mjsscripts/

#!/usr/bin/env node
import { readdirSync, readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';

const VERSION = '1.5.0';
const OKF_VERSION = '0.2';
export { VERSION, OKF_VERSION, GEN_MARKER };

const USAGE = `usage:
  okf.mjs index    <bundle-root> [--stdout] [--describe <dir>=<text>]...
  okf.mjs check    <bundle-root>
  okf.mjs coverage <bundle-root>
  okf.mjs wire     <bundle-root> [--stdout]
  okf.mjs --version

<bundle-root>/.okfignore, if present, lists paths this skill does not own: one
bundle-root-relative path per line, trailing / for a directory, # for a comment.
They are not enumerated, not checked, and never stamped.

check reads the corpus the walk finds; coverage reads git ls-files instead, so a
document the walk never reaches is named rather than agreed with. Only an index the
root index reaches may vouch for a document; generated ones nothing links to are
named as orphan-index and their rows do not count. A tracked index whose row points
at a document git is not tracking is named as dangling-row: 'git commit -a' would
carry the index and leave the document behind.

exit: 0 ok | 1 violations | 77 nothing evaluated | 64 usage`;

const RESERVED = new Set(['index.md', 'log.md']);
const PROJECT_META = new Set([
  'readme.md', 'license.md', 'licence.md', 'contributing.md', 'code_of_conduct.md',
  'security.md', 'changelog.md', 'claude.md', 'agents.md', 'gemini.md',
  'third-party-notices.md', 'notice.md', 'authors.md', 'maintainers.md',
]);
const SKIP_DIRS = new Set(['node_modules', '__pycache__']);
// Claude Code owns every .md under these, at a plugin root: a file in commands/ IS a slash
// command, a file in agents/ IS an agent definition, and a skill folder's contract is that
// SKILL.md is the entry point with the loader's own frontmatter schema. See PAYLOAD_DIRS below.
const PAYLOAD_DIRS = new Set(['commands', 'agents', 'skills']);
const PLUGIN_MANIFESTS = ['plugin.json', 'marketplace.json'];
const IGNORE_FILE = '.okfignore';

const GEN_MARKER = '<!-- generated by okf.mjs - do not edit; regenerate after changing frontmatter -->';
const SUBDIR_HEADING = 'Subdirectories';
const UNTYPED_HEADING = 'Other';
// A title may contain brackets — "Clarifying questions: [Feature Name]" is an ordinary
// heading — so they are escaped on the way out and understood on the way back in. Left
// raw, the row is still readable to a human and unparseable to every consumer here: the
// round-trip description store loses it, and the coverage check reports the document as
// indexed by nobody.
const ENTRY_RE = /^\* \[((?:\\.|[^\\\]])*)\]\(([^)]*)\)(?: - (.*))?$/;
const escapeTitle = (title) => title.replace(/[\\\[\]]/g, (c) => `\\${c}`);
const unescapeTitle = (title) => title.replace(/\\(.)/g, '$1');
const KEY_RE = /^([A-Za-z0-9_.-]+):(?:[ \t]+(.*))?$/;
const PROFILE_RE = /^profile:[ \t]*(\S.*?)[ \t]*$/;
const NESTED_RE = /^[ \t]+(?:- )?(?:[A-Za-z0-9_.-]+:(?:[ \t].*)?|- .*|\{.*\}|.+)$/;
const BLOCK_RE = /^([|>])[1-9]?[+-]?$/;

const DESC_MAX = 160;
const oneLine = (value) => String(value ?? '').replace(/\s+/g, ' ').trim();

function gated(raw, label, reports) {
  const text = oneLine(raw);
  if (text.length <= DESC_MAX) return text;
  reports.push(`${label} (${text.length} chars, max ${DESC_MAX})`);
  return '';
}

const ENTRY_OPEN = '<!-- okf:entry -->';
const ENTRY_CLOSE = '<!-- /okf:entry -->';
const ENTRY_BLOCK_RE = /<!-- okf:entry -->[\s\S]*?<!-- \/okf:entry -->/;
const ENTRY_BLOCK = `${ENTRY_OPEN}
## Documentation

Start at [index.md](index.md). Every documentation folder carries a generated \`index.md\` listing
each document's title and one-line description — answer "which doc covers X" and "does a doc for Y
exist" from that index in one read, and open a document only after the index names it. Do not grep
\`docs/\` for a document's identity; grep stays correct only for a literal phrase inside a body that
the index cannot carry.
${ENTRY_CLOSE}`;
const PROSE_ENTRY_FILES = ['CLAUDE.md', 'AGENTS.md'];
const IMPORT_ENTRY_FILE = 'GEMINI.md';
const IMPORT_LINE = '@index.md';

const byCodepoint = (a, b) => (a < b ? -1 : a > b ? 1 : 0);

const relPath = (root, path) => relative(root, path).split(sep).join('/');

export function readIgnores(root) {
  const state = { entries: [], skipped: [], nested: [], payload: [], present: false };
  let text;
  try { text = readFileSync(join(root, IGNORE_FILE), 'utf8'); } catch { return state; }
  state.present = true;
  text.split('\n').forEach((raw, i) => {
    const line = raw.replace(/\r$/, '').trim();
    if (!line || line.startsWith('#')) return;
    const isDir = line.endsWith('/');
    const target = (isDir ? line.slice(0, -1) : line).replace(/^\.\//, '').replace(/\/+$/, '');
    if (!target) return;
    state.entries.push({ target, isDir, lineno: i + 1, hits: 0 });
  });
  return state;
}

function ignoreHit(state, rel, isDir) {
  for (const entry of state.entries) {
    if (entry.isDir !== isDir || entry.target !== rel) continue;
    entry.hits += 1;
    state.skipped.push(`${rel}${isDir ? '/' : ''} (${IGNORE_FILE}:${entry.lineno})`);
    return true;
  }
  return false;
}

// The walk prunes a declared directory by never descending into it. A path list
// arriving from outside the walk (coverage) has no such moment, so a directory
// line has to be re-read as "this path, or anything beneath it".
export function ignoredFile(state, rel) {
  for (const entry of state.entries) {
    const hit = entry.isDir ? rel.startsWith(`${entry.target}/`) : rel === entry.target;
    if (!hit) continue;
    entry.hits += 1;
    if (!entry.isDir || entry.hits === 1) {
      state.skipped.push(`${entry.target}${entry.isDir ? '/' : ''} (${IGNORE_FILE}:${entry.lineno})`);
    }
    return true;
  }
  return false;
}

function reportIgnores(state) {
  for (const rel of [...new Set(state.payload)].sort(byCodepoint)) {
    process.stderr.write(
      `plugin-payload: ${rel}/ (Claude Code owns this directory's markdown; not indexed, not checked)\n`);
  }
  for (const rel of [...new Set(state.nested)].sort(byCodepoint)) {
    process.stderr.write(`separate-repo: ${rel}/ (own git work tree; not descended into, nothing written there)\n`);
  }
  for (const line of [...state.skipped].sort(byCodepoint)) {
    process.stderr.write(`ignored: ${line}\n`);
  }
  for (const entry of state.entries) {
    if (entry.hits) continue;
    process.stderr.write(
      `unused-ignore: ${entry.target}${entry.isDir ? '/' : ''} (${IGNORE_FILE}:${entry.lineno})\n`);
  }
}

export function declaredProfile(root) {
  for (const candidate of [join(root, 'docs', 'okf.yaml'), join(root, 'okf.yaml')]) {
    let text;
    try { text = readFileSync(candidate, 'utf8'); } catch { continue; }
    for (const line of text.split('\n')) {
      const m = PROFILE_RE.exec(line.replace(/\r$/, ''));
      if (m) return [candidate, m[1]];
    }
  }
  return [null, null];
}

export function scalar(raw) {
  if (raw === undefined || raw === null) return '';
  return raw.trim();
}

function unquote(value) {
  const q = value[0];
  if ((q === '"' || q === "'") && value.length >= 2 && value.endsWith(q)) return value.slice(1, -1);
  return value;
}

function scalarViolation(key, value, lineno) {
  if (!value) return null;
  const q = value[0];
  if ((q === '"' || q === "'") && !(value.length >= 2 && value.endsWith(q))) {
    return `line ${lineno}: unterminated quoted value for '${key}'`;
  }
  if (value[0] === '[' && !value.endsWith(']')) {
    return `line ${lineno}: unterminated flow sequence for '${key}'`;
  }
  if (value[0] === '{' && !value.endsWith('}')) {
    return `line ${lineno}: unterminated flow mapping for '${key}'`;
  }
  return null;
}

function readBlockScalar(lines, start, style) {
  const body = [];
  let i = start;
  for (; i < lines.length; i += 1) {
    const line = lines[i].replace(/\r$/, '');
    if (!line.trim()) { body.push(''); continue; }
    if (line[0] !== ' ' && line[0] !== '\t') break;
    body.push(line.replace(/^[ \t]+/, ''));
  }
  while (body.length && !body[body.length - 1]) body.pop();
  if (style === '|') return [body.join('\n'), i];
  const folded = [];
  for (const line of body) {
    if (!line) { folded.push('\n'); continue; }
    if (folded.length && folded[folded.length - 1] !== '\n') folded.push(' ');
    folded.push(line);
  }
  return [folded.join(''), i];
}

export function parseBlock(block) {
  const data = {};
  let seenKey = false;
  const lines = block.split('\n');
  for (let i = 0; i < lines.length; i += 1) {
    const line = lines[i].replace(/\r$/, '');
    const lineno = i + 2;
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith('#')) continue;
    if (line[0] === ' ' || line[0] === '\t') {
      if (!seenKey) return [null, `line ${lineno}: indented content before any key`];
      if (!NESTED_RE.test(line)) return [null, `line ${lineno}: unparseable nested content`];
      continue;
    }
    if (trimmed.startsWith('- ')) {
      return [null, `line ${lineno}: top-level sequence; frontmatter must be a mapping`];
    }
    const m = KEY_RE.exec(line);
    if (!m) return [null, `line ${lineno}: not a mapping entry`];
    const key = m[1];
    if (Object.prototype.hasOwnProperty.call(data, key)) {
      return [null, `line ${lineno}: duplicate key '${key}'`];
    }
    const value = scalar(m[2]);
    const sigil = BLOCK_RE.exec(value);
    if (sigil) {
      const [text, next] = readBlockScalar(lines, i + 1, sigil[1]);
      data[key] = text;
      seenKey = true;
      i = next - 1;
      continue;
    }
    const bad = scalarViolation(key, value, lineno);
    if (bad) return [null, bad];
    data[key] = unquote(value);
    seenKey = true;
  }
  return [data, null];
}

// The first H1 of the body, skipping fenced blocks — a shell comment and a heading
// are the same three characters, and the fences are where shell comments live.
function firstHeading(lines, start) {
  let fenced = false;
  for (let i = start; i < lines.length; i += 1) {
    const line = lines[i];
    if (/^\s{0,3}(?:```|~~~)/.test(line)) { fenced = !fenced; continue; }
    if (fenced) continue;
    const m = /^# +(.+?)\s*$/.exec(line);
    if (m) return m[1].replace(/\s+#+\s*$/, '').trim();
  }
  return '';
}

// [frontmatter, error, first-heading]. The heading is returned even when the
// frontmatter is absent or unparseable, because a row still has to be rendered.
export function readDoc(path) {
  let text;
  try { text = readFileSync(path, 'utf8'); }
  catch (err) { return [null, `unreadable: ${err.message}`, '']; }
  const lines = text.split('\n').map((l) => l.replace(/\r$/, ''));
  if (!lines.length || lines[0].trim() !== '---') {
    return [null, 'no YAML frontmatter block', firstHeading(lines, 0)];
  }
  for (let i = 1; i < lines.length; i += 1) {
    if (lines[i].trim() === '---') {
      const [data, err] = parseBlock(lines.slice(1, i).join('\n'));
      return [data, err, firstHeading(lines, i + 1)];
    }
  }
  return [null, 'unterminated YAML frontmatter block', ''];
}

// Two different questions, deliberately not one predicate.
//
// isConcept answers "must this document carry the required keys" — the job it has
// always had, unchanged in name and in bytes, because installs elsewhere read it
// that way. `check` is its only caller now.
//
// isListable answers "does this document get a row". Everything tracked does,
// because a reader looking for a document cannot know in advance that the corpus
// filed it as project furniture. A row costs a line and imposes nothing: the title
// degrades to the first heading, then to the filename, so a file with no
// frontmatter is listed exactly as well as one with.
const isConcept = (name) => {
  const low = name.toLowerCase();
  return name.endsWith('.md') && !RESERVED.has(low) && !PROJECT_META.has(low);
};

const isListable = (name) => name.endsWith('.md') && !RESERVED.has(name.toLowerCase());

// A directory holding a `.git` entry is another repository's working tree — a submodule
// (where `.git` is a FILE containing a gitdir: pointer) or a nested clone. The parent only
// pins a submodule by SHA; writing inside one edits a repository the caller does not own,
// and it is invisible twice over: nothing in the output distinguishes those files from the
// caller's own, and `coverage` cannot catch it because git reports a submodule as a single
// gitlink. So the boundary is refused structurally, not left to a per-repo .okfignore line
// nobody can add before the first run. `existsSync` on `.git` needs no git dependency.
const isRepoBoundary = (dirpath) => existsSync(join(dirpath, '.git'));

// A directory holding plugin.json or marketplace.json - at the package root or under
// .claude-plugin/, both of which Claude Code loads - is a plugin root, and its
// commands/, agents/ and skills/ children are payload the loader parses, not
// documentation. Writing an index.md there puts a document where the loader expects executable
// payload; demanding OKF frontmatter there asks a SKILL.md to carry keys that are not its schema
// and a progressively-disclosed reference file to pay context for keys nobody reads. Neither is a
// per-repo preference, so the boundary is refused structurally, exactly as isRepoBoundary is -
// an .okfignore line cannot be written before the first run has already done the damage.
//
// The anchor is the manifest rather than the directory name on purpose: a docs/commands/ folder
// documenting a CLI is ordinary knowledge, and a name-only rule would silently swallow it.
//
// Both manifest locations are probed because Claude Code accepts both: an installed plugin in
// ~/.claude/plugins/cache/ carries plugin.json at its package root with no .claude-plugin/ at all,
// and a marketplace entry pointing at such a directory loads its commands, skills and hooks
// normally. A probe that recognised only the nested form was stricter than the loader it models,
// which is the same mistake in the opposite direction: it let payload back in.
const isPluginRoot = (dirpath) => PLUGIN_MANIFESTS.some(
  (m) => existsSync(join(dirpath, '.claude-plugin', m)) || existsSync(join(dirpath, m)));

// The walk prunes a payload directory by never descending into it. A path list arriving from
// outside the walk (coverage) has no such moment, so each tracked path re-derives its own
// nearest payload ancestor; the plugin-root probe is memoised because siblings share it.
function payloadOwner(root, rel, cache) {
  const parts = rel.split('/');
  for (let i = 0; i < parts.length - 1; i += 1) {
    if (!PAYLOAD_DIRS.has(parts[i])) continue;
    const parent = parts.slice(0, i).join('/');
    let isRoot = cache.get(parent);
    if (isRoot === undefined) {
      isRoot = isPluginRoot(join(root, parent));
      cache.set(parent, isRoot);
    }
    if (isRoot) return parts.slice(0, i + 1).join('/');
  }
  return null;
}

function walk(root, ignores) {
  const out = [];
  const keep = (dirpath, name, isDir) =>
    !ignoreHit(ignores, relPath(root, join(dirpath, name)), isDir);
  const crossesPayload = (dirpath, name) => {
    if (!PAYLOAD_DIRS.has(name) || !isPluginRoot(dirpath)) return false;
    ignores.payload.push(relPath(root, join(dirpath, name)));
    return true;
  };
  const crossesBoundary = (dirpath, name) => {
    if (!isRepoBoundary(join(dirpath, name))) return false;
    ignores.nested.push(relPath(root, join(dirpath, name)));
    return true;
  };
  const visit = (dirpath) => {
    let entries;
    try { entries = readdirSync(dirpath, { withFileTypes: true }); } catch { return; }
    const dirnames = entries
      .filter((e) => e.isDirectory())
      .filter((e) => keep(dirpath, e.name, true))
      .filter((e) => !e.name.startsWith('.') && !SKIP_DIRS.has(e.name))
      .filter((e) => !crossesPayload(dirpath, e.name))
      .filter((e) => !crossesBoundary(dirpath, e.name))
      .map((e) => e.name).sort(byCodepoint);
    const filenames = entries
      .filter((e) => e.isFile())
      .filter((e) => keep(dirpath, e.name, false))
      .map((e) => e.name).sort(byCodepoint);
    out.push([dirpath, dirnames, filenames]);
    for (const d of dirnames) visit(join(dirpath, d));
  };
  visit(root);
  return out;
}

function indexable(root, ignores) {
  const tree = walk(root, ignores);
  const carries = new Map();
  for (let i = tree.length - 1; i >= 0; i -= 1) {
    const [dirpath, dirnames, filenames] = tree[i];
    const own = filenames.some(isListable);
    const below = dirnames.some((d) => carries.get(join(dirpath, d)));
    carries.set(dirpath, own || below);
  }
  const ordered = [...tree].reverse().filter(([d]) => carries.get(d));
  return [ordered, carries];
}

function priorDescriptions(dirpath) {
  const found = new Map();
  let lines;
  try { lines = readFileSync(join(dirpath, 'index.md'), 'utf8').split('\n'); } catch { return found; }
  let inSubdirs = false;
  for (const line of lines) {
    if (line.startsWith('# ')) { inSubdirs = line.slice(2).trim() === SUBDIR_HEADING; continue; }
    if (!inSubdirs) continue;
    const m = ENTRY_RE.exec(line);
    if (m && m[3]) found.set(join(dirpath, unescapeTitle(m[1])), m[3].trim());
  }
  return found;
}

// Only a file this tool wrote may be rewritten by it. The generation marker is the evidence,
// and it is the file's own claim rather than a convention held somewhere else.
//
// A hand-maintained index, or one a dialect's own generator produces, carries rows v0.2 does
// not project — an id, a status, a richer description — and regenerating replaces that catalog
// with a poorer one, silently, destroying the very lookup the index exists for. That used to be
// prevented by refusing profiled repositories outright, which was the wrong instrument (FR-OKF-3)
// and took this protection with it when it went. This is the right-sized replacement: it turns on
// evidence in the file, so it also covers a hand-written index in a repository with no manifest
// at all, which the old refusal never did.
const ownsIndex = (target) => {
  let text;
  try { text = readFileSync(target, 'utf8'); } catch { return true; }
  return text.includes(GEN_MARKER);
};

// A rewrite that changes nothing is not free: it churns mtime, so a watcher fires, a build
// re-runs, and `git status` shows a file the run did not actually change. Comparing first
// makes `index` safe to call on every edit, which is what the regen hook does.
const currentText = (target) => {
  try { return readFileSync(target, 'utf8'); } catch { return null; }
};

// Which dialect this repository's catalog is written in, and the two places that can say so:
// the manifest declares it, and the root index carries what the generator that wrote it stamped.
// The higher of the two wins, because either one being newer means a richer generator has been
// here and this one would be a downgrade.
const versionOrder = (a, b) => {
  const pa = String(a).split('.').map(Number);
  const pb = String(b).split('.').map(Number);
  for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
    const x = pa[i] || 0;
    const y = pb[i] || 0;
    if (x !== y) return x < y ? -1 : 1;
  }
  return 0;
};

export function declaredOkfVersion(root) {
  let best = null;
  const consider = (source, raw) => {
    const value = unquote(String(raw ?? '').trim());
    if (!/^\d+(?:\.\d+)*$/.test(value)) return;
    if (!best || versionOrder(value, best[1]) > 0) best = [source, value];
  };
  for (const candidate of [join(root, 'docs', 'okf.yaml'), join(root, 'okf.yaml')]) {
    let text;
    try { text = readFileSync(candidate, 'utf8'); } catch { continue; }
    for (const line of text.split('\n')) {
      const m = /^okf_version:[ \t]*(\S.*?)[ \t]*$/.exec(line.replace(/\r$/, ''));
      if (m) consider(relPath(root, candidate), m[1]);
    }
  }
  const [fm] = readDoc(join(root, 'index.md'));
  if (fm && fm.okf_version) consider('index.md', fm.okf_version);
  return best || [null, null];
}

function renderRows(heading, rows) {
  const out = [`# ${heading}`, ''];
  const sorted = [...rows].sort((a, b) => byCodepoint(a[0].toLowerCase(), b[0].toLowerCase()));
  for (const [title, link, desc] of sorted) {
    out.push(`* [${escapeTitle(title)}](${link})${desc ? ` - ${desc}` : ''}`);
  }
  return out.join('\n');
}

function render(sections, subdirs, isRoot) {
  const blocks = [];
  for (const heading of [...sections.keys()].sort(byCodepoint)) {
    blocks.push(renderRows(heading, sections.get(heading)));
  }
  if (subdirs.length) blocks.push(renderRows(SUBDIR_HEADING, subdirs));
  const body = `${GEN_MARKER}\n\n${blocks.join('\n\n')}\n`;
  return isRoot ? `---\nokf_version: "${OKF_VERSION}"\n---\n\n${body}` : body;
}

export function cmdIndex(root, toStdout, described, ignores) {
  const [dirs, carries] = indexable(root, ignores);
  if (!dirs.length) {
    process.stderr.write(`okf.mjs: no documents found under ${root} - nothing written\n`);
    reportIgnores(ignores);
    return 77;
  }

  const relOf = (p) => relPath(root, p);
  const overlong = [];

  const resolved = new Map();
  for (const [dirpath] of dirs) {
    for (const [k, v] of priorDescriptions(dirpath)) resolved.set(k, oneLine(v));
  }
  for (const [k, v] of described) {
    const text = gated(v, relOf(k) || '.', overlong);
    if (text) resolved.set(k, text);
    else resolved.delete(k);
  }

  const pending = new Set();
  const foreign = [];
  let written = 0;

  for (const [dirpath, dirnames, filenames] of dirs) {
    const sections = new Map();
    for (const name of filenames) {
      if (!isListable(name)) continue;
      const [fm, , h1] = readDoc(join(dirpath, name));
      const front = fm || {};
      const heading = String(front.type || '').trim() || UNTYPED_HEADING;
      const title = String(front.title || '').trim() || h1 || name.slice(0, -3);
      const desc = gated(front.description, relOf(join(dirpath, name)), overlong);
      if (!sections.has(heading)) sections.set(heading, []);
      sections.get(heading).push([title, name, desc]);
    }

    const subdirs = [];
    for (const name of dirnames) {
      const child = join(dirpath, name);
      if (!carries.get(child)) continue;
      subdirs.push([name, `${name}/index.md`, resolved.get(child) || '']);
    }

    if (!sections.size && !subdirs.length) continue;

    if (dirpath !== root && !resolved.get(dirpath)) {
      const entries = [...sections.values()].flat();
      if (entries.length === 1 && entries[0][2] && !subdirs.length) {
        resolved.set(dirpath, entries[0][2]);
      } else {
        pending.add(relative(root, dirpath).split(sep).join('/'));
      }
    }

    const text = render(sections, subdirs, dirpath === root);
    const target = join(dirpath, 'index.md');
    if (!ownsIndex(target)) {
      foreign.push(relOf(target));
      continue;
    }
    if (toStdout) {
      process.stdout.write(`==> ${relative(root, target).split(sep).join('/')} <==\n${text}\n`);
    } else if (currentText(target) !== text) {
      writeFileSync(target, text, 'utf8');
      written += 1;
    }
  }

  if (!toStdout) process.stderr.write(`okf.mjs: wrote ${written} index file(s)\n`);
  reportIgnores(ignores);
  for (const rel of [...pending].sort(byCodepoint)) {
    process.stderr.write(`needs-description: ${rel}\n`);
  }
  for (const report of [...overlong].sort(byCodepoint)) {
    process.stderr.write(`long-description: ${report}\n`);
  }
  for (const rel of [...foreign].sort(byCodepoint)) {
    process.stderr.write(`foreign-index: ${rel} (not written by okf.mjs - left untouched)\n`);
  }
  if (foreign.length) {
    process.stderr.write(
      `okf.mjs: ${foreign.length} index file(s) carry no generation marker, so another hand or another tool maintains them\n`);
    process.stderr.write(
      `okf.mjs: read one before replacing it - a dialect's rows can carry an id, a status or a shape v${OKF_VERSION} does not project, and overwriting is a lossy downgrade. Delete the file to hand this tool the directory, or name it in ${IGNORE_FILE} to leave it with its owner\n`);
  }
  return 0;
}

export function cmdCheck(root, ignores) {
  const violations = [];
  const notes = [];
  let scanned = 0;
  const rootIndex = join(root, 'index.md');

  for (const [dirpath, , filenames] of walk(root, ignores)) {
    for (const name of filenames) {
      if (!name.endsWith('.md')) continue;
      const path = join(dirpath, name);
      const rel = relPath(root, path);
      const low = name.toLowerCase();
      if (low === 'log.md') {
        notes.push(`${rel}: log.md duplicates git history (this skill removes it; not a §11 failure)`);
        continue;
      }
      if (low === 'index.md') {
        if (path !== rootIndex) {
          const first = readFileSync(path, 'utf8').split('\n')[0].trim();
          if (first === '---') {
            violations.push(`${rel}: index.md carries frontmatter; only the bundle-root index may (§8, §12)`);
          }
        }
        continue;
      }
      if (!isConcept(name)) continue;
      scanned += 1;
      const [fm, err] = readDoc(path);
      if (err) { violations.push(`${rel}: ${err}`); continue; }
      if (!String(fm.type || '').trim()) {
        violations.push(`${rel}: missing or empty required key 'type' (§4.1, §11)`);
      }
      const desc = oneLine(fm.description);
      if (desc.length > DESC_MAX) {
        notes.push(`${rel}: description is ${desc.length} chars (max ${DESC_MAX}); the index omits it rather than truncating (§4.1 is one sentence)`);
      }
    }
  }

  for (const note of notes) process.stdout.write(`note: ${note}\n`);
  reportIgnores(ignores);

  if (scanned === 0) {
    process.stderr.write(`okf.mjs: evaluated 0 concept documents under ${root} - nothing was verified\n`);
    return 77;
  }

  for (const line of violations) process.stdout.write(`${line}\n`);
  process.stdout.write(`okf.mjs: ${scanned} document(s) evaluated, ${violations.length} violation(s)\n`);
  return violations.length ? 1 : 0;
}

// git ls-files, resolved from the repository root: the one enumerator here that is
// complete, honours ignore rules, and costs nothing. Returns null when there is no
// repository to ask, which is a refusal to verify, not a clean bill.
//
// `--cached --others --exclude-standard`, and the choice is load-bearing rather than
// incidental. Plain `--cached` enumerates the *index*, so a document written but not yet
// staged is invisible — and that is exactly the document at risk, the one being added right
// now. A check blind to it passes, the commit lands with no row for it, and nothing
// complains, which is the same self-consistent silence this command exists to break.
// `--others` adds the working tree; `--exclude-standard` keeps the repo's own ignore rules
// authoritative, so build output and vendored trees stay out.
function trackedMarkdown(root) {
  const git = (args) => execFileSync('git', args, {
    cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 64 * 1024 * 1024,
  });
  try { git(['rev-parse', '--show-toplevel']); } catch { return null; }
  const list = (args) => {
    const out = git(['ls-files', '-z', ...args, '--', '*.md']);
    return [...new Set(out.split('\0').filter(Boolean).map((p) => p.split(sep).join('/')))];
  };
  try {
    // `all` is what must be indexed; `cached` is what `git commit -a` will actually carry.
    // The gap between them is a real hazard, not a bookkeeping detail — see dangling rows below.
    return { all: list(['--cached', '--others', '--exclude-standard']), cached: new Set(list(['--cached'])) };
  } catch { return null; }
}

function normalizeLink(base, link) {
  const parts = (base === '.' ? [] : base.split('/')).concat(link.split('/'));
  const out = [];
  for (const part of parts) {
    if (!part || part === '.') continue;
    if (part === '..') { if (!out.length) return null; out.pop(); continue; }
    out.push(part);
  }
  return out.join('/');
}

function rowTargets(root, rel) {
  const targets = new Set();
  let lines;
  try { lines = readFileSync(join(root, rel), 'utf8').split('\n'); } catch { return targets; }
  const base = dirname(rel);
  for (const line of lines) {
    const m = ENTRY_RE.exec(line.replace(/\r$/, ''));
    if (!m) continue;
    const link = m[2].split('#')[0].trim();
    if (!link || link.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(link)) continue;
    const target = normalizeLink(base, decodeURIComponent(link));
    if (target) targets.add(target);
  }
  return targets;
}

function listedDocuments(root, indexes) {
  const listed = new Set();
  for (const rel of indexes) for (const t of rowTargets(root, rel)) listed.add(t);
  return listed;
}

// A row pointing at a document git will not commit.
//
// `index` indexes the working tree, so a document written and not yet added is listed on
// purpose — that is the whole reason the regeneration hook can run on the edit that created it.
// The hazard is what happens next: `git commit -a` stages modifications to TRACKED files, so the
// updated index goes in and the document it now vouches for does not. The commit is self-
// consistent and wrong, and every enumerator here reads the working tree, where both files are
// present, so nothing else can see it.
//
// Scoped to indexes that are themselves tracked, because an untracked index is not going into
// that commit either — there is no half-commit to warn about, only a bundle nobody has added yet.
function danglingRows(root, indexes, cached) {
  const found = [];
  for (const rel of indexes) {
    if (!cached.has(rel)) continue;
    for (const target of rowTargets(root, rel)) {
      if (!target.endsWith('.md') || cached.has(target)) continue;
      found.push(`${target} (listed by ${rel})`);
    }
  }
  return [...new Set(found)].sort(byCodepoint);
}

// Which indexes are allowed to vouch for a document.
//
// `index` writes but never deletes: narrow what gets indexed - exclude a path, prune a payload
// directory, remove the last document from a folder - and the index.md it stops maintaining stays
// on disk with its rows intact. Counting those rows is a fail-open, and a quiet one, because an
// orphan is not an unreached document but a reached *nothing*: coverage reports zero findings
// before and after the debris is removed. Worse, a document listed ONLY by an orphan is credited
// as indexed while nothing a reader can follow leads to it, which is the exact question this
// command exists to answer.
//
// So an index vouches for its rows only if the root index reaches it, by the chain of
// Subdirectories links the format is built on. With no root index at all there is a larger
// finding already in flight, so every index is trusted rather than piling a second report on it.
function reachableIndexes(root, indexes) {
  const have = new Set(indexes);
  if (!have.has('index.md')) return indexes;
  const seen = new Set(['index.md']);
  const queue = ['index.md'];
  while (queue.length) {
    const rel = queue.shift();
    let lines;
    try { lines = readFileSync(join(root, rel), 'utf8').split('\n'); } catch { continue; }
    const dir = dirname(rel);
    for (const line of lines) {
      const m = ENTRY_RE.exec(line.replace(/\r$/, ''));
      if (!m) continue;
      const link = m[2].split('#')[0].trim();
      if (!link || link.startsWith('/') || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(link)) continue;
      const target = normalizeLink(dir, decodeURIComponent(link));
      if (target && have.has(target) && !seen.has(target)) { seen.add(target); queue.push(target); }
    }
  }
  return [...seen];
}

// Only debris this tool left behind is named. A hand-written index nobody links to is somebody
// else's file and a different conversation; the generation marker is the same evidence `ownsIndex`
// uses before overwriting one.
const wasGenerated = (root, rel) => {
  try { return readFileSync(join(root, rel), 'utf8').includes(GEN_MARKER); } catch { return false; }
};

// The completeness half, and the reason it cannot reuse the walk.
//
// `check` regenerates from the same enumerator that wrote the committed index, so a
// document the walk never reaches is missing from both sides and compares equal. It
// is a projection checked against itself, and it cannot report a missing input by
// construction. Anchoring to the tracked-file list gives the comparison a second,
// independent source — which is the only way a document nobody indexed can be named.
export function cmdCoverage(root, ignores) {
  const enumerated = trackedMarkdown(root);
  if (!enumerated) {
    process.stderr.write(`okf.mjs: ${root} is not inside a git work tree (or git is unavailable) - nothing was verified\n`);
    return 77;
  }
  const { all: tracked, cached } = enumerated;

  // Where an index could live is derived from the tracked documents themselves, not
  // from the walk — an index inside a directory the walk prunes still counts, and a
  // directory the walk never reached still owes one.
  const dirs = new Set(['.']);
  for (const rel of tracked) {
    for (let d = dirname(rel); d !== '.' && d !== sep && d !== ''; d = dirname(d)) dirs.add(d);
  }
  const indexes = [...dirs].sort(byCodepoint)
    .map((d) => (d === '.' ? 'index.md' : `${d}/index.md`))
    .filter((p) => existsSync(join(root, p)));
  const reachable = reachableIndexes(root, indexes);
  const listed = listedDocuments(root, reachable);
  const orphans = indexes.filter((p) => !reachable.includes(p) && wasGenerated(root, p));
  const dangling = danglingRows(root, reachable, cached);

  const required = [];
  const payloadCache = new Map();
  const payload = new Set();
  for (const rel of tracked) {
    if (RESERVED.has(rel.slice(rel.lastIndexOf('/') + 1).toLowerCase())) continue;
    if (ignoredFile(ignores, rel)) continue;
    const owner = payloadOwner(root, rel, payloadCache);
    if (owner) { payload.add(owner); continue; }
    required.push(rel);
  }
  for (const owner of payload) ignores.payload.push(owner);
  reportIgnores(ignores);

  if (!required.length) {
    process.stderr.write(`okf.mjs: git tracks no document under ${root} - nothing was verified\n`);
    return 77;
  }

  const missing = required.filter((rel) => !listed.has(rel)).sort(byCodepoint);
  for (const rel of missing) process.stdout.write(`unindexed: ${rel}\n`);
  for (const rel of orphans) process.stdout.write(`orphan-index: ${rel}\n`);
  for (const line of dangling) process.stdout.write(`dangling-row: ${line}\n`);
  process.stdout.write(
    `okf.mjs: ${required.length} tracked document(s) in ${reachable.length} index file(s), ${missing.length} reachable only by ls\n`);
  if (orphans.length) {
    process.stdout.write(
      `okf.mjs: ${orphans.length} generated index file(s) are linked from no index, so \`index\` no longer maintains them and their rows go stale unseen\n`);
    process.stdout.write(
      `okf.mjs: delete them - their rows were not counted here, so a document only they listed is named above\n`);
  }

  if (dangling.length) {
    process.stdout.write(
      `okf.mjs: ${dangling.length} index row(s) point at a document git is not tracking, so \`git commit -a\` commits the index without it\n`);
    process.stdout.write(
      'okf.mjs: git add the document(s) named above, or remove the row by deleting the document and regenerating\n');
  }

  // The walk skips dot-directories on purpose — they hold tooling, not knowledge, and
  // descending into one reaches .git, .venv and every editor's cache. But naming a document
  // there without naming the remedy leaves exactly one obvious next move, hand-writing an
  // index.md, which is the drift this command exists to find and which nothing would ever
  // regenerate. So say what the two real answers are.
  const ancestors = (rel) => {
    const parts = rel.split('/').slice(0, -1);
    return parts.map((_, i) => parts.slice(0, i + 1).join('/'));
  };
  const hidden = missing.filter((rel) => ancestors(rel).some((p) => p.split('/').pop().startsWith('.')));
  const nested = missing.filter((rel) => ancestors(rel).some((p) => isRepoBoundary(join(root, p))));
  if (hidden.length) {
    process.stdout.write(
      `okf.mjs: ${hidden.length} of those sit under a dot-directory, which the walk does not descend into - \`index\` will never write one there\n`);
    process.stdout.write(
      `okf.mjs: move the document out, or name the path in ${IGNORE_FILE}; do not hand-write an index.md, because nothing regenerates it and it goes stale unseen\n`);
  }
  if (nested.length) {
    process.stdout.write(
      `okf.mjs: ${nested.length} of those sit inside a separate git work tree, which the walk refuses to enter - it is not this repository's to write into\n`);
    process.stdout.write(
      `okf.mjs: name the path in ${IGNORE_FILE}; index it from inside that repository if it needs one, never from here\n`);
  }
  return missing.length || orphans.length || dangling.length ? 1 : 0;
}

export function cmdWire(root, toStdout) {
  if (!existsSync(join(root, 'index.md'))) {
    process.stderr.write(`okf.mjs: no index.md at ${root} - run \`index\` first\n`);
    return 1;
  }

  const touched = [];
  for (const name of PROSE_ENTRY_FILES) {
    const path = join(root, name);
    let text = '';
    try { text = readFileSync(path, 'utf8'); } catch { text = ''; }
    let updated;
    let action;
    if (ENTRY_BLOCK_RE.test(text)) {
      updated = text.replace(ENTRY_BLOCK_RE, () => ENTRY_BLOCK);
      action = 'updated';
    } else {
      updated = (text.trim() ? `${text.replace(/\n+$/, '')}\n\n` : '') + ENTRY_BLOCK + '\n';
      action = 'added';
    }
    if (updated === text) action = 'unchanged';
    if (toStdout) process.stdout.write(`==> ${name} (${action}) <==\n${ENTRY_BLOCK}\n`);
    else if (action !== 'unchanged') writeFileSync(path, updated, 'utf8');
    touched.push(`${name} ${action}`);
  }

  const geminiPath = join(root, IMPORT_ENTRY_FILE);
  if (existsSync(geminiPath)) {
    const text = readFileSync(geminiPath, 'utf8');
    if (text.split(/\s+/).includes(IMPORT_LINE)) {
      touched.push(`${IMPORT_ENTRY_FILE} unchanged`);
    } else {
      if (!toStdout) writeFileSync(geminiPath, `${text.replace(/\n+$/, '')}\n${IMPORT_LINE}\n`, 'utf8');
      touched.push(`${IMPORT_ENTRY_FILE} added`);
    }
  }

  process.stderr.write(`okf.mjs: ${touched.join(', ')}\n`);
  return 0;
}

function usage(message) {
  if (message) process.stderr.write(`okf.mjs: ${message}\n`);
  process.stderr.write(`${USAGE}\n`);
  return 64;
}

export function main(argv) {
  if (argv.includes('--version')) {
    process.stdout.write(`okf.mjs ${VERSION} (OKF v${OKF_VERSION})\n`);
    return 0;
  }
  if (!argv.length) return usage('no command given');

  const [command, ...rest] = argv;
  if (!['index', 'check', 'coverage', 'wire'].includes(command)) {
    return usage(`unknown command '${command}'`);
  }

  let toStdout = false;
  const described = new Map();
  const positional = [];
  for (let i = 0; i < rest.length; i += 1) {
    const arg = rest[i];
    if (arg === '--stdout') toStdout = true;
    else if (arg === '--describe') {
      i += 1;
      if (i >= rest.length) return usage('--describe needs <dir>=<text>');
      if (!rest[i].includes('=')) return usage(`--describe expects <dir>=<text>, got '${rest[i]}'`);
      const at = rest[i].indexOf('=');
      described.set(resolve(rest[i].slice(0, at)), rest[i].slice(at + 1).trim());
    } else if (arg.startsWith('--')) return usage(`unknown option '${arg}'`);
    else positional.push(arg);
  }

  if (positional.length !== 1) return usage(`expected exactly one <bundle-root>, got ${positional.length}`);
  const root = resolve(positional[0]);
  try { if (!statSync(root).isDirectory()) throw new Error('not a dir'); }
  catch { return usage(`not a directory: ${positional[0]}`); }

  // A profile names an enforcement dialect — which documents must carry which keys.
  // It is reported because it changes how `check` should be read, and it no longer
  // stops anything: it never registered an index generator to collide with, so
  // refusing to index on its account left the corpus with no index at all.
  const [manifest, profile] = declaredProfile(root);
  if (profile) {
    process.stderr.write(
      `okf.mjs: ${relPath(root, manifest)} declares profile ${profile} - proceeding; a profile scopes which documents carry required keys, not which are enumerated\n`);
  }

  if (command === 'wire') {
    if (described.size) return usage('wire does not take --describe');
    return cmdWire(root, toStdout);
  }

  const ignores = readIgnores(root);
  if (command === 'check' || command === 'coverage') {
    if (toStdout || described.size) return usage(`${command} takes no options`);
    return command === 'check' ? cmdCheck(root, ignores) : cmdCoverage(root, ignores);
  }
  return cmdIndex(root, toStdout, described, ignores);
}

// Importable core plus a CLI, and the guard is what makes the split real: a bare
// `process.exit(main(...))` at the tail runs — and terminates the host process — the moment
// anything imports this file, which is why the regen hook could not reuse the generator it
// is a client of. argv[1] is absent when Node is fed a script on stdin, so it is checked.
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  process.exit(main(process.argv.slice(2)));
}

scripts

SKILL.md

tile.json