Skip to content

Design Doc

Rick Hightower edited this page Aug 1, 2026 · 4 revisions

Current — this is the living version; regenerated at 2026-08-01T22:10:50Z. Historical snapshots are linked from Index-Releases.

OKF Graph Engineering Plugin — Design Document

Generated against the v0.3.0 tag. Every code claim below cites path — symbol(), lines N–M and was read from the tree at 6bf5d65. Claims are labelled Confirmed (read in the code), Assumption, Recommendation, or Open Question.


1. Document Overview

Purpose. Describe what okf-graph-eng is, how it is built, and what a developer must understand before changing it.

Audience. Contributors to this repository, and plugin users who need to know what the tooling guarantees.

Scope. The plugin surface (.claude-plugin/, skills/, commands/, agents/, hooks/), the graph engine (scripts/okf-graph.py), the two auxiliary scripts, the worked example (sample-okf/), and the CI/hook gates that protect them.

Out of scope. The vendored WikiTicket SDD / worklog tooling under bin/ and .work/ is project-management plumbing, orthogonal to the plugin's logic. It is described only where the plugin depends on it (§15, §28).

1.1 Definitions

Term Meaning
OKF Open Knowledge Format — knowledge as Markdown files with YAML frontmatter, linked by Markdown links
Bundle A directory tree of OKF Markdown with a root index.md carrying okf_version
Concept One .md file in a bundle; a node in the graph
Typed edge A link that carries a relation name (depends_on, routes_to, …) rather than a bare hyperlink
Knowledge graph Concepts describing a domain (Dataset, Metric, Reference, …)
Agent graph / harness graph Concepts describing the automation itself (AgentNode, Workflow, SharedState, …)
Dual graph Both of the above in one bundle — the plugin's core premise
Progressive disclosure Shipping a bounded, ranked subgraph ("pack") instead of a whole tree
Blast radius / impact The transitive set of concepts affected by changing one concept

1.2 Related documents


2. Executive Summary

What it is. okf-graph-eng is a Claude Code plugin that turns a directory of Markdown into a queryable graph, so an agent can answer "what breaks if I change this?" and "what is the smallest set of documents you need to read?" without loading the whole tree.

Business problem. Agent context is finite and expensive. Documentation trees are neither structured nor bounded. The plugin makes the structure explicit (typed edges), then uses it to bound what gets read.

Two graphs, one bundle. The differentiator is that the same mechanism models the domain (datasets, metrics, references) and the harness (agents, workflows, shared state). Asking for the blast radius of an AgentNode is the same operation as asking for the blast radius of a Table.

Major components.

Component What it is
Seven skills (skills/*/SKILL.md) The portable intelligence — Markdown instructions the host model follows
Seven slash commands (commands/*.md) Thin wrappers so a user can type /okf-impact
One agent (agents/graph-engineer.md) Specialist subagent for multi-hop graph work
Graph engine (scripts/okf-graph.py, 924 lines) Deterministic CLI: eight subcommands over a bundle
Post-edit hook (hooks/hooks.jsonscripts/okf-curate.sh) Validates the touched bundle after every Write/Edit/MultiEdit
sample-okf/ 22-concept, 83-edge self-describing bundle used as a worked example and a CI tripwire
tests/test_okf_graph.py 16 plain-assert cases; the only automated coverage in the repo

External dependencies. Python 3 and Bash — nothing else at runtime. Confirmed: scripts/okf-graph.py imports only the standard library (argparse, html, json, re, sys, collections, dataclasses, datetime, pathlib, typing — lines 17–26). No package manifest, no lockfile, no install step.

Key architectural decisions. Package once as a Claude Code plugin and let Grok Build read it natively (§6.1). Keep Markdown links canonical and treat frontmatter typed edges as enrichment (§6.2). Ship a hand-rolled frontmatter parser rather than depend on PyYAML (§6.3). Default the validator to lenient and gate CI with --strict (§6.4).

Primary risks. The frontmatter parser is a hand-rolled approximation of YAML and will mis-handle constructs it has never seen (§31 R1). The sample-okf concept/edge counts are asserted as literals in the test suite, so any legitimate edit to the sample fails CI until the numbers are updated (§31 R3).


3. Requirements Summary

Derived from README.md, CLAUDE.md, docs/plans/2026-08-01-v030-plumbing-and-tests.md, and the CHANGELOG.md release notes.

3.1 Functional

# Requirement Where satisfied
F1 Compute transitive impact (inbound + outbound) of a concept scripts/okf-graph.py — cmd_impact(), lines 375–416
F2 List direct inbound references to a concept cmd_backlinks(), lines 419–438
F3 Extract an N-hop neighborhood cmd_subgraph(), lines 441–486
F4 Emit a bounded, ranked context pack cmd_pack(), lines 489–590
F5 List edges, filterable by source concept and relation cmd_edges(), lines 593–606
F6 Render the graph as Mermaid, JSON, or standalone HTML cmd_graph(), lines 690–761
F7 Validate structure, links, and graph hygiene cmd_validate(), lines 764–835
F8 Report disconnected concepts cmd_orphans(), lines 838–848
F9 Support typed edges without breaking plain Markdown links merge_edges(), lines 251–261
F10 Bridge worklog items / GitHub issues into the graph as TicketLink concepts scripts/okf-ticket-link.py — emit(), lines 114–195
F11 Curate automatically after an agent edits a bundle hooks/hooks.json + scripts/okf-curate.sh
F12 Work in Claude Code and Grok Build from one package .claude-plugin/plugin.json, .grok-plugin/marketplace.json

3.2 Non-functional

# Requirement Where satisfied
N1 Zero runtime dependencies beyond python3 + bash stdlib-only imports, scripts/okf-graph.py:17–26
N2 Portable across macOS and Linux without coreutils okf-curate.sh fallback uses okf-graph.py, not realpath -m (lines 71–76)
N3 Every subcommand is scriptable JSON on stdout for six of eight; graph prints the artifact by design (cmd_graph() docstring, lines 691–699)
N4 HTML output must open offline and inside a locked-down viewer render_html(), lines 609–687; asserted by tests/test_okf_graph.py — test_graph_html_is_self_contained(), lines 201–214
N5 The graph engine is covered by tests that run in CI .github/workflows/worklog.yml, "graph engine tests" step
N6 The plugin version cannot drift across its four manifests test_version_is_consistent_across_manifests(), lines 238–255
N7 Skills must keep working when the validator emits warnings cmd_validate() returns 0 on warnings unless --strict, lines 833–835

Omitted NFR classes: availability, scalability, disaster recovery, data retention and compliance — there is no service, no tenancy, and no hosted data. The tool is a local CLI over files in a git repository.


4. System Context

Actors.

  • Plugin user — a developer in Claude Code or Grok Build who types /okf-impact or asks a question that triggers a skill.
  • Host model — Claude Code / Grok Build. It reads SKILL.md files and decides when to shell out to the CLI.
  • Post-edit hook — the host, acting automatically after each file write.
  • CI — GitHub Actions, running the same checks non-bypassably.

Trust boundaries. One: the local filesystem. The graph engine reads *.md under a bundle root and writes nothing. The only outbound network call in the repository is scripts/substack_okf.py — http_get(), lines 77–97, and that script is a local integration harness, not part of the plugin surface (§20).

flowchart TB
    User["Plugin user"]
    subgraph Host["Host — Claude Code / Grok Build"]
        Model["Host model"]
        Skills["skills/*/SKILL.md<br/>(7 skills)"]
        Cmds["commands/*.md<br/>(7 slash commands)"]
        Agent["agents/graph-engineer.md"]
        Hook["PostToolUse hook"]
    end
    subgraph Local["Local filesystem — the only trust boundary"]
        Graph["scripts/okf-graph.py<br/>(graph engine)"]
        Curate["scripts/okf-curate.sh"]
        Ticket["scripts/okf-ticket-link.py"]
        Bundle[("OKF bundle<br/>*.md + frontmatter")]
        Work[(".work/*.jsonl<br/>worklog event log")]
    end
    CI["GitHub Actions<br/>worklog-invariants"]

    User -->|"slash command or<br/>natural language"| Model
    Model --> Cmds --> Skills
    Model --> Agent
    Skills -->|"prefer okf/okfcli,<br/>else python3"| Graph
    Hook -->|"stdin JSON:<br/>.tool_input.file_path"| Curate
    Curate -->|"fallback validator"| Graph
    Graph -->|"read only"| Bundle
    Ticket -->|"writes TicketLink concepts"| Bundle
    Ticket -.->|"reads worklog fold"| Work
    CI --> Graph
    CI --> Work
Loading

How to read it. Everything above the Local box is instructions the host model interprets; everything inside it is deterministic code. The plugin's working rule (CLAUDE.md, "Deterministic tools first") is exactly this boundary: prose decides when, code decides what.

Failure behavior. If okf/okfcli is absent the skills fall back to python3 scripts/okf-graph.py (skills/okf-impact/SKILL.md and siblings). If okf-curate.sh cannot find a bundle root it prints a note and exits 0 (scripts/okf-curate.sh:57–60) — curation never blocks an edit.


5. High-Level Architecture

5.1 Logical architecture

flowchart LR
    subgraph Intelligence["Portable intelligence (Markdown)"]
        S1["okf-init-graph"]
        S2["okf-author"]
        S3["okf-impact"]
        S4["okf-query"]
        S5["okf-maintain"]
        S6["okf-validate"]
        S7["okf-visualize"]
    end
    subgraph Engine["Graph engine — scripts/okf-graph.py"]
        Load["load_bundle()<br/>lines 264–293"]
        Parse["parse_frontmatter()<br/>lines 72–156"]
        LinkX["extract_markdown_links()<br/>extract_frontmatter_links()<br/>lines 220–248"]
        Merge["merge_edges()<br/>lines 251–261"]
        BFS["bfs_closure()<br/>lines 318–332"]
        Render["mermaid_id() / render_mermaid()<br/>lines 159–187"]
        HTML["render_html()<br/>lines 609–687"]
    end
    subgraph Cmds["Eight subcommands"]
        C1["impact"]; C2["backlinks"]; C3["subgraph"]; C4["pack"]
        C5["edges"]; C6["graph"]; C7["validate"]; C8["orphans"]
    end

    S3 --> C1; S4 --> C3; S4 --> C4; S6 --> C7; S5 --> C7; S5 --> C8; S7 --> C6; S2 --> C5
    Load --> Parse --> LinkX --> Merge
    C1 --> BFS; C3 --> BFS; C4 --> BFS; C6 --> BFS
    C4 --> Render; C6 --> Render; C6 --> HTML
Loading

How to read it. Every subcommand goes through the same ingest pipeline — load_bundle() is called first in all eight (cmd_impact():376, cmd_backlinks():420, cmd_subgraph():442, cmd_pack():496, cmd_edges():594, cmd_graph():700, cmd_validate():765, cmd_orphans():839). There is no cache and no incremental mode: each invocation re-reads the bundle from disk. For a bundle of a few hundred files this is milliseconds; see §31 R5.

Confirmed: render_mermaid() is shared by exactly two callers — cmd_pack() line 569 and cmd_graph() line 745. It was extracted in v0.3.0 so the pack diagram and the standalone graph diagram cannot drift apart.

5.2 Data flow — one invocation

flowchart TD
    A["bundle.rglob('*.md')<br/>sorted, dotfiles skipped<br/>load_bundle():266–268"]
    B["read_text(errors='replace')<br/>line 270"]
    C["parse_frontmatter(text)<br/>→ meta dict"]
    D["extract_markdown_links()<br/>regex over body + frontmatter text"]
    E["extract_frontmatter_links()<br/>from meta['links']"]
    F["merge_edges()<br/>typed rel beats links_to"]
    G["Concept(path, rel, title, type,<br/>status, verified, tags, meta,<br/>outbound, edges)"]
    H["outbound filtered to loaded concepts<br/>lines 291–292"]
    I["edges kept unfiltered<br/>→ validate can see broken links"]
    J["build_inbound()<br/>reverse adjacency"]
    K["subcommand"]

    A --> B --> C --> D --> F
    C --> E --> F --> G --> H --> J --> K
    G --> I --> K
Loading

The load-bearing subtlety. Concept.outbound is pruned to targets that actually exist, but Concept.edges is not (load_bundle():288–292). Traversal therefore never walks into a void, while cmd_validate() can still report the dangling target as a broken link (cmd_validate():776–778). Conflating the two lists would either crash the BFS or blind the validator.

5.3 Runtime and deployment

There is no runtime to deploy. Installation is a marketplace entry pointing at this repository (marketplace.json, .claude-plugin/marketplace.json, .grok-plugin/marketplace.json). The host clones or links the tree and resolves intra-plugin paths through ${CLAUDE_PLUGIN_ROOT} — used by hooks/hooks.json:9 and by every commands/*.md file (v0.3.0 fixed bare relative paths that did not resolve from a consuming project; see CHANGELOG "Fixed").


6. Architectural Decisions

6.1 One Claude Code plugin, two hosts

Decision. Ship a single Claude Code plugin; do not maintain Grok-specific packaging beyond a thin marketplace pin. Context. Grok Build reads Claude plugins, skills, agents, and hooks natively. Alternatives. Separate Grok package (drift risk); Grok-only features (breaks Claude). Consequences. One install path, two hosts. No feature may depend on a Grok-only capability. Recorded as an in-repo ADR: sample-okf/decisions/single-claude-plugin.md (status: accepted, verified: true). docs/adr/ exists but is empty — the decision records live in the sample bundle, which is itself the plugin's self-description.

6.2 Markdown links are canonical; typed edges enrich

Decision. A plain [Title](/path.md) link is a real edge with relation links_to. Frontmatter links: [{target, rel}] entries add a relation name. Implementation. merge_edges(), lines 251–261 — frontmatter wins for the same target, so a concept can upgrade a prose link to depends_on without duplicating it. Rationale. A bundle stays readable and useful in any Markdown viewer; typing is optional and additive. Tradeoff. Two edge sources means two parsers and a merge rule to keep correct. Covered by test_merge_edges_frontmatter_wins(), lines 89–93. Revisit if: relations ever need attributes beyond a name (weight, direction, validity window) — a dict-per-edge merge would no longer suffice.

6.3 A hand-rolled frontmatter parser instead of PyYAML

Decision. Parse frontmatter with a line-oriented state machine (parse_frontmatter(), lines 72–156). Rationale. PyYAML is not in the standard library. Requiring it would turn a copy-the-file plugin into a package with an install step, on two hosts. Tradeoff. It is not YAML. It handles scalars, booleans, inline lists ([a, b]), block sequences (- a under a bare key), and the links: list of mappings — and nothing else. Nested maps, multi-line scalars, anchors, and comments-after-values are unsupported. History. v0.2.0 shipped without block-sequence support: tags: followed by - item returned '', which load_bundle()'s isinstance guard (line 282) then coerced to [] with no warning. Every generator in this repo emits inline lists, so sample-okf looked clean and only user-authored bundles were affected. Fixed in v0.3.0 by the pending_list_key mechanism (lines 83, 121–130) and pinned by test_frontmatter_block_sequence(), lines 47–56. Revisit if: users start hitting the parser's edges. The honest upgrade is an optional PyYAML path with this parser as the fallback.

6.4 Lenient by default, --strict for CI

Decision. validate exits 0 on warnings; --strict makes warnings non-zero (cmd_validate(), lines 833–835). Rationale. The skills call validate mid-conversation and treat non-zero as failure. A bundle with an unverified AgentNode is not broken — it is in-progress. CI, however, needs warnings to actually gate. Consequence. The one place strictness is applied is .github/workflows/worklog.yml ("sample bundle stays valid"). The post-edit hook deliberately uses the lenient default (okf-curate.sh:75). Pinned by: test_strict_validate_flags_warnings(), lines 217–235.

6.5 Mermaid node ids derive from the full relative path

Decision. mermaid_id(rel) sanitizes the whole path, not the stem (lines 159–168). Context. v0.2.0 derived ids from Path(rel).stem, so all seven index.md files in sample-okf collapsed into one node and agents/foo.md merged with docs/foo.md. Rationale. Path is the concept's identity everywhere else in the engine (dict keys in load_bundle(), edge endpoints, resolve_concept() output). The renderer had been the only component using a different identity. Pinned by: test_mermaid_ids_are_unique_per_path(), lines 96–108.

6.6 graph prints its artifact; every other subcommand prints JSON

Decision. --format mermaid|html write the raw artifact to stdout; --format json writes JSON. Rationale, quoted from cmd_graph() lines 694–698: "a rendered graph is the only product here, so wrapping it would force every caller through jq -r before it could be pasted into a doc or written to a file." Contrast with pack, whose JSON envelope carries included / excluded / edges alongside its markdown byproduct (cmd_pack():580–588) — there the structured data is the point and the Markdown is a convenience. Tradeoff. One subcommand breaks the "always JSON" uniformity. Callers must know which mode they are in. Documented in the CLI reference and in --help.

6.7 The HTML map is self-contained by construction

Decision. render_html() emits one file with inlined CSS, no <script>, no src=, no @import, no url(), no http(s):// (lines 609–687). Rationale, quoted from the docstring (lines 616–621): "No CDN, no JS, no network fetches — the file must open from disk and from a locked-down viewer. Renderers that understand pre.mermaid draw the diagram; everywhere else the tables carry the same information." Consequence. The diagram degrades to two tables rather than to nothing. This is a security property as much as a portability one (§22). Pinned by: test_graph_html_is_self_contained(), lines 201–214, which asserts each forbidden token is absent.


7. Component Inventory

Component Type Responsibility Inputs Outputs Depends on Failure impact
skills/*/SKILL.md (7) Markdown instructions Tell the host model when and how to do graph work User intent Tool calls, prose Graph engine (optional) Model falls back to ad-hoc reasoning
commands/*.md (7) Slash-command wrappers Give each skill an explicit entry point $ARGUMENTS Delegation to a skill ${CLAUDE_PLUGIN_ROOT} Slash command unavailable; skill still auto-triggers
agents/graph-engineer.md Subagent definition Multi-hop graph work in an isolated context Task prompt Report Skills + engine Main thread does the work inline
scripts/okf-graph.py Python CLI, 924 lines All deterministic graph operations Bundle path, concept path, flags JSON / Mermaid / HTML stdlib only Every skill degrades to manual file crawling
scripts/okf-curate.sh Bash, 78 lines Post-edit validation of the touched bundle $1 or PostToolUse stdin JSON stdout report python3, okf-graph.py Silent drift after agent edits
scripts/okf-ticket-link.py Python CLI, 220 lines Render worklog items / GitHub issues as TicketLink concepts worklog fold JSON or flags <bundle>/tickets/*.md + index worklog (optional) Tickets absent from the graph
scripts/substack_okf.py Python CLI, 917 lines Local end-to-end integration harness (§20) Substack archive API Gitignored bundle under integration/ network, okf-graph.py No end-to-end signal; unit tests unaffected
hooks/hooks.json Host hook config Bind PostToolUse → curate Write/Edit/MultiEdit events Command invocation ${CLAUDE_PLUGIN_ROOT} Curation never runs (this was the v0.2.0 defect)
sample-okf/ 22 concepts, 83 edges Worked example and CI drift tripwire Demos break; CI fails loudly
tests/test_okf_graph.py 16 plain-assert cases Regression net for the engine Exit code okf-graph.py, sample-okf, 4 manifests Regressions ship silently (the v0.2.0 condition)
hooks/pre-commit, hooks/commit-msg Git hooks (vendored worklog) Log invariants, roadmap freshness, graph tests, ULID in message Staged tree Exit code bin/, python3 Bad commits reach the branch; CI still catches them

8. End-to-End Workflows

8.1 Impact-first change (the primary flow)

Trigger. User asks "what depends on X?" or is about to edit a high-impact concept. CLAUDE.md working rule 2 makes this mandatory before structural edits to AgentNode, Workflow, or SharedState.

sequenceDiagram
    actor User
    participant Model as Host model
    participant Skill as skills/okf-impact/SKILL.md
    participant CLI as okf-graph.py impact
    participant FS as Bundle (*.md)

    User->>Model: "what breaks if I change the graph-engineer agent?"
    Model->>Skill: auto-trigger on "blast radius / depends on"
    Skill->>CLI: python3 okf-graph.py impact sample-okf agents/graph-engineer.md
    CLI->>FS: rglob("*.md"), read each
    FS-->>CLI: 22 concepts
    CLI->>CLI: resolve_concept() → exact, stem, or suffix match
    alt not found
        CLI-->>Skill: error payload, exit 1
        Skill-->>User: ask for a valid path
    else found
        CLI->>CLI: bfs_closure over inbound + outbound
        CLI->>CLI: enrich_nodes() → criticality, sort
        CLI-->>Skill: JSON: inbound, outbound, direct_edges, suggested_order, stats
        Skill-->>Model: rank + narrate
        Model-->>User: affected concepts in update order
    end
Loading

Main flow (cited). cmd_impact(), lines 375–416: load (376) → build reverse adjacency (377) → resolve (379) → BFS both directions (383–384) → collect direct typed edges in and out (386–395) → emit payload (396–415).

Failure flows. Unresolvable concept → {"error": …} and exit 1 (380–382). Broken outbound links are invisible here: outbound was pruned at load (load_bundle():291–292), which is why validate is the tool that reports them.

Idempotency / state. Read-only. No writes, no retries, no timeouts.

suggested_order is inbound-only (line 407) — the things that reference the target, ordered by criticality then depth then title (enrich_nodes():362–363). Callers wanting a full ordering must merge in outbound themselves. Confirmed by reading the field construction; the field name does not make this obvious.

8.2 Progressive-disclosure pack

Trigger. A long-running agent needs context on a concept without reading the tree. /okf-query or the okf-query skill.

flowchart TD
    A["pack bundle concept<br/>--hops 2 --max-nodes 20"] --> B["load_bundle()"]
    B --> C{"--undirected?"}
    C -->|no, default| D["outbound-only adjacency<br/>cmd_pack():512"]
    C -->|yes| E["symmetric adjacency<br/>cmd_pack():504–510"]
    D --> F["bfs_closure(hops)"]
    E --> F
    F --> G["score(): root first,<br/>verified first,<br/>high-impact first,<br/>then title<br/>lines 518–528"]
    G --> H["included = first max_nodes of ranked<br/>excluded = the rest"]
    H --> I["edges among included only<br/>lines 534–538"]
    I --> J["read_order: root, high-impact,<br/>SharedState, title<br/>lines 542–549"]
    J --> K["Markdown pack + Mermaid<br/>via render_mermaid()"]
    K --> L["JSON envelope: included,<br/>excluded, edges, markdown"]
Loading

Why outbound-only is the default. Quoted from cmd_pack() lines 491–494: "Outbound-only keeps packs inside a theme (e.g. group → members) instead of flooding through hub catalogs that link everything." A bundle's index.md links to everything; an undirected 2-hop walk from any leaf reaches the index and then the entire bundle. --undirected exists for deliberate neighborhood exploration (main():870–874).

Two different orderings, deliberately. score() (518–528) decides what survives the cut; read_order (542–549) decides what order the survivor list is presented in, and additionally promotes SharedState. They are not the same sort and should not be collapsed.

Unverified high-impact concepts are flagged inline in the Markdown with ⚠ unverified high-impact (lines 563–566) rather than dropped — the reader is told the node is untrusted, not denied it.

Truncation is disclosed, never silent: up to 15 excluded titles are listed, then "… and N more" (lines 572–577).

8.3 Visualization

Trigger. /okf-visualize, or a request for a diagram or an HTML map.

cmd_graph(), lines 690–761. Without --focus, nodes are every concept (sorted(concepts), line 702). With --focus, an undirected neighborhood BFS bounded by --hops (704–715). Edges are then restricted to pairs where both endpoints survived (line 718).

The isolated-node repair. render_mermaid() only declares nodes it sees on an edge. cmd_graph() therefore appends a node line for every unlinked concept (lines 746–752), with the reason in the comment: "isolated concepts would otherwise disappear from the diagram but stay in the JSON view." This was a v0.3.0 fix; test_graph_mermaid_is_a_fenced_block(), lines 151–159 asserts every node from the JSON view appears in the Mermaid view.

--hops is reported as null when there is no focus (line 726) — hops are meaningless for a whole-bundle render, and test_graph_json_shape(), line 165 asserts it.

8.4 Post-edit curation (the automation path)

sequenceDiagram
    participant Host as Claude Code
    participant Hook as okf-curate.sh
    participant Py as okf-graph.py validate

    Host->>Hook: PostToolUse (Write|Edit|MultiEdit), payload on stdin
    Hook->>Hook: FILE from $1, else tool_input.file_path on stdin
    alt FILE empty
        Hook-->>Host: exit 0, silent
    else path not OKF-shaped
        Note over Hook: case filter on .okf/, knowledge/, sample-okf/
        Hook-->>Host: exit 0, silent
    else
        Hook->>Hook: find_bundle_root walks up for index.md with okf_version,<br/>then .okf/, then repo fallbacks
        alt no root
            Hook-->>Host: "no OKF bundle root found — skipping", exit 0
        else okf / okfcli present
            Hook->>Hook: okf validate (+ okf lint if supported)
        else
            Hook->>Py: python3 okf-graph.py validate BUNDLE_ROOT
            Py-->>Hook: JSON report
        end
        Hook-->>Host: report, exit 0 always
    end
Loading

The v0.2.0 defect, in full. hooks/hooks.json passed "$FILE_PATH", but the host delivers the PostToolUse payload as JSON on stdin — there is no such environment variable. okf-curate.sh bound an empty string and hit its first guard on every edit. The hook had never run in any install. It now reads .tool_input.file_path from stdin (okf-curate.sh:9–16), and the matcher covers MultiEdit, which previously bypassed curation entirely (hooks/hooks.json:5).

Why python3 and not jq — stated in the comment at lines 6–8: "the plugin already requires python3 everywhere, jq is not guaranteed present."

The hook never fails a build. Every branch exits 0, and each validator call is suffixed || true (lines 65, 67, 70, 75). Curation reports; it does not block.

Limitation (Confirmed). The path filter at lines 22–25 keys on .okf/, knowledge/, or sample-okf/ in the path. A bundle rooted elsewhere — say integration/substack-okf/index.md — is skipped unless the edited file happens to sit under a knowledge/ subdirectory. See §31 R4.

8.5 TicketLink emission

scripts/okf-ticket-link.py — emit(), lines 114–195. Reads bin/worklog fold JSON from stdin (or --worklog-fold, or a single --id), maps worklog status to ticket status (status_map(), lines 32–39), renders one TicketLink concept per item into <bundle>/tickets/<slug>.md, and rewrites tickets/index.md including pre-existing entries it did not just write (lines 184–191). --dry-run reports paths without writing (159–161); --open-only skips done and cancelled (125–126).

Idempotency. Re-running overwrites by slug. A retitled item produces a new file and leaves the old one — Confirmed by reading lines 147–163, which derive the filename from the current title with no cleanup of prior slugs.


9. Complex Business Logic

9.1 The frontmatter parser state machine

This is the most intricate logic in the repository and the source of a shipped defect, so it gets a state diagram.

stateDiagram-v2
    [*] --> NoMatch: no leading fence
    NoMatch --> [*]: return empty dict
    [*] --> Normal: frontmatter fence matched

    Normal --> InLinks: bare links key
    Normal --> PendingList: key with empty value, arm pending_list_key
    Normal --> Normal: key with scalar, bool, or inline list value
    Normal --> Normal: blank or comment line, skip

    PendingList --> PendingList: list item, append to the pending key
    PendingList --> Normal: any other line, disarm and re-handle as key

    InLinks --> InLinks: list item, flush previous and start new mapping
    InLinks --> InLinks: indented key, add field to current mapping
    InLinks --> Normal: unindented key, flush current and leave block

    Normal --> [*]: end of block, flush and attach links
    InLinks --> [*]: end of block, flush and attach links
Loading

Decision table — value coercion (parse_frontmatter(), lines 132–150):

Input after key: Result Line
(empty) "", and pending_list_key = key 137–140
true / false (any case) Python bool 142–143
[a, b] list[str], quotes and whitespace stripped 144–148
[] [] 146–147
anything else str, outer " or ' stripped 149–150

Edge cases.

  • A bare key followed by no list keeps "" (comment, line 129: "no list materialized; the bare key keeps its empty-string value"). load_bundle()'s isinstance(..., list) guard at line 282 then yields [] for tags. Confirmed — and this is precisely the path that hid the v0.2.0 bug.
  • A line with no : outside a list context is skipped entirely (line 132–133).
  • Inside links:, a list item whose text starts with { is not split on : (line 104) — flow-style mappings are recognized as "not my problem" rather than mis-parsed into a wrong key.
  • The links key only appears in the returned dict if at least one item was collected (lines 154–155).

Invalid input is not reported. The parser has no error channel; malformed frontmatter degrades to a partial dict. cmd_validate() catches the consequences (missing title, missing type) but never the cause. See §31 R1.

9.2 Criticality ranking

criticality_of(), lines 335–343:

Type in Verified Result
HIGH_IMPACT_TYPES (AgentNode, Workflow, Harness, SharedState) true high
HIGH_IMPACT_TYPES false critical
MEDIUM_IMPACT_TYPES (Dataset, Table, Metric, API, ToolCapability) either medium
anything else either low

Confirmed dead branch. Line 342 reads criticality = "critical" if criticality == "high" else criticality. Inside the criticality != "low" guard the only other reachable value is "medium", for which the expression assigns the variable to itself. Medium-impact concepts never escalate on being unverified. Whether that is the intent is an Open Question (§34 Q2); the code as written is a no-op, not a bug that changes output.

Sort order (enrich_nodes(), lines 362–363): criticality rank (critical 0, high 1, medium 2, low 3), then BFS depth, then title. Unknown criticality sorts last via the 9 default.

9.3 Link normalization

_normalize_target(), lines 190–217 — every edge target passes through here.

Input Behavior Line
#anchor suffix stripped before resolution 191
empty, http…, mailto: not an edge → None 192–193
/abs/path.md resolved against the bundle root 194–195
relative.md resolved against the source file's directory 196–197
a directory rewritten to <dir>/index.md when that file exists 198–201
extension-less non-file tried as <path>/index.md, then <path>.md 202–210
resolves outside the bundle None (the ValueError branch) 211–214

Invariant: a target that escapes the bundle root is not an edge. relative_to() raising ValueError is the enforcement (lines 211–214), pinned by test_normalize_target(), lines 72–86 ("../../elsewhere.md"None).


10. Domain Model

classDiagram
    class Concept {
        +Path path
        +str rel
        +str title
        +str type
        +str status
        +bool verified
        +list~str~ tags
        +dict meta
        +list~str~ outbound
        +list~TypedEdge~ edges
    }
    class TypedEdge {
        +str target
        +str rel
        +str source
    }
    Concept "1" --> "0..*" TypedEdge : edges
Loading

Conceptscripts/okf-graph.py, lines 58–69. One per .md file. rel (the bundle-relative POSIX path) is the identity used as the dict key in load_bundle(), as both endpoints of every edge, and as the input to mermaid_id(). Invariant: rel is the only identity; title and stem are convenience lookups in resolve_concept() and must never become identity.

Defaults on load (load_bundle(), lines 275–286):

Field Fallback
title meta["title"], else the file stem
type meta["type"], else "Index" for index.md, else "Unknown"
verified bool(meta.get("verified", False)) — absent means untrusted
tags meta["tags"] only if it is already a list, else []

TypedEdge — lines 51–55. source records provenance (markdown or frontmatter) and is what merge_edges() and cmd_validate() reason over.

Concept types. Not an enum in code — type is a free string. The two frozen sets that carry behavior are HIGH_IMPACT_TYPES and MEDIUM_IMPACT_TYPES (lines 47–48). The wider vocabulary lives in the skills and templates:

Kind Types
Knowledge Dataset, Table, Metric, Playbook, Runbook, API, Reference
Harness AgentNode, Workflow, Harness, DecisionRecord, SharedState, ToolCapability, TicketLink
Structural Index (auto-assigned to index.md)

Relations. KNOWN_RELS (lines 32–45) holds ten: depends_on, routes_to, implements, documents, uses, owns, supersedes, related_to, tracks, maps_to. An unlisted relation is kept, not rewrittenextract_frontmatter_links() line 242 only substitutes related_to when the value is empty — and cmd_validate() then reports it at severity info as "non-standard rel … (allowed but uncommon)" (lines 779–786). The vocabulary is advisory by design.


11. Module-by-Module Design

scripts/okf-graph.py is one flat module, deliberately: it is a single file a user can copy next to a bundle. Its internal layering is nonetheless strict, and there are no cycles.

flowchart TD
    subgraph L1["Layer 1 — parsing"]
        PF["parse_frontmatter()"]
        NT["_normalize_target()"]
        EML["extract_markdown_links()"]
        EFL["extract_frontmatter_links()"]
        ME["merge_edges()"]
    end
    subgraph L2["Layer 2 — model"]
        LB["load_bundle()"]
        BI["build_inbound()"]
        RC["resolve_concept()"]
        EI["edge_index()"]
    end
    subgraph L3["Layer 3 — algorithms"]
        BFS["bfs_closure()"]
        CR["criticality_of()"]
        EN["enrich_nodes()"]
    end
    subgraph L4["Layer 4 — rendering"]
        MI["mermaid_id()"]
        RM["render_mermaid()"]
        RH["render_html()"]
    end
    subgraph L5["Layer 5 — subcommands"]
        CMDS["cmd_impact · cmd_backlinks · cmd_subgraph<br/>cmd_pack · cmd_edges · cmd_graph<br/>cmd_validate · cmd_orphans"]
    end
    MAIN["main() — argparse"]

    EML --> NT
    EFL --> NT
    LB --> PF
    LB --> EML
    LB --> EFL
    LB --> ME
    EN --> CR
    RM --> MI
    L5 --> L2
    L5 --> L3
    L5 --> L4
    MAIN --> L5
Loading

Confirmed: no circular dependencies. Every arrow points down a layer. render_html() takes already-rendered Mermaid lines as a parameter (line 614) rather than calling render_mermaid() itself, which keeps Layer 4 internally acyclic and lets cmd_graph() apply its isolated-node repair before handing lines to either renderer.

Extension points.

  1. New subcommand — add a cmd_* function plus a subparser in main() (lines 851–920) and a dispatch line (904–919). No other file changes.
  2. New relation — add to KNOWN_RELS (lines 32–45). Anything not listed still works, it just gets an info note from validate.
  3. New impact type — add to HIGH_IMPACT_TYPES / MEDIUM_IMPACT_TYPES (lines 47–48). This changes pack ranking, criticality, and validate warnings in one edit.
  4. New output format — extend --format choices (line 883) and branch in cmd_graph() (720–760).
  5. New skill — a directory under skills/ with a SKILL.md, plus a matching commands/*.md. The seven-and-seven parity is a convention, not enforced.

Coupling risks. cmd_pack() (102 lines) and cmd_graph() (72 lines) both do selection, ranking, and rendering inline. They share render_mermaid() and bfs_closure() but each carries its own adjacency construction — cmd_subgraph():449–458, cmd_pack():503–512, cmd_graph():709–714 are three separate hand-built undirected maps. Recommendation: extract one undirected_adjacency(concepts) helper if a fourth caller appears. Two of the three are already provably redundant in their construction: cmd_subgraph() adds edges from outbound_map in both directions (450–453) and from inbound_map in both directions (454–457), which is the same edge set twice — harmless only because line 458 deduplicates with sorted(set(v)).


12. Auxiliary Scripts

12.1 scripts/okf-ticket-link.py (220 lines)

One subcommand, emit. Bridges WikiTicket SDD / worklog items and GitHub issues into the graph as TicketLink concepts. Rendering is a single f-string template (render_ticket(), lines 42–91) that always emits verified: true and a documents edge to /knowledge/okf-conventions.md. GitHub URLs are synthesized only when the external key is all digits (emit():141–146).

12.2 scripts/substack_okf.py (917 lines)

The repository's end-to-end integration harness, and — until this document — its least-documented component. It is not part of the plugin surface: no skill, command, or hook references it, and its output tree is gitignored (.gitignore: integration/, integration-okf/).

What it does. Fetches the archive of a Substack publication, classifies each post by type and subject with deterministic regex rules, emits a complete OKF v0.2 bundle from the result, then runs the plugin's own CLI against that bundle and asserts the answers are sane. It is the only test in the repository that exercises the engine against a bundle it did not hand-author.

Subcommand Function Behavior
fetch cmd_fetch(), lines 221–262 Pages /api/v1/archive 50 at a time to --limit; snapshots /feed; writes articles.json + taxonomy.json
classify cmd_classify(), lines 265–290 Re-runs the taxonomy over cached articles.json without refetching
emit cmd_emit(), lines 452–717 Writes the bundle: article concepts, four type hubs, N subject hubs, knowledge/agents/workflows indexes, root index.md, log.md
verify cmd_verify(), lines 737–820 Shells out to okf-graph.py and asserts the results
run cmd_run(), lines 863–873 fetchemitverify

Taxonomy. Four article types (ARTICLE_TYPES, line 32: news, tutorial, guide, one-off) assigned by ordered regex in classify_type(), lines 134–151, falling through to one-off. Twelve subject rules (SUBJECT_RULES, lines 34–47) are all-match, not first-match (classify_subjects(), lines 154–160), defaulting to ["general"]. Both are pure functions of title + subtitle, so classification is reproducible from cached JSON with no network.

What verify asserts (lines 737–820) — this is the integration contract:

  1. articles.json exists and holds at least min_count articles.
  2. Every article has a type in ARTICLE_TYPES and a non-empty subject list.
  3. The bundle exists and has a root index.md.
  4. The count of files in knowledge/articles/ equals the count in JSON.
  5. okf-graph.py validate exits 0 with error_count == 0.
  6. impact on the largest non-ai-news subject hub returns at least one neighbor under knowledge/articles/.
  7. pack --hops 2 --max-nodes 30 on that hub returns at least 2 nodes.
  8. orphans runs (reported, not asserted).

Network resilience. http_get(), lines 77–97 tries urllib with a custom User-Agent, then falls back to a curl -sL subprocess. The comment at line 78 gives the reason: "Substack often 403s". The /feed snapshot is best-effort and its failure is caught and printed, not raised (lines 229–233).

Emit is destructive by design. cmd_emit() unlinks every *.md under the target bundle before writing (lines 470–473), so a shrinking article set cannot leave stale concepts behind. It deletes only *.md, leaving the raw JSON alongside. Confirmed — and the reason the output path is gitignored and defaulted to integration/ (line 29) rather than anywhere a user keeps work.

All four type hubs are always emitted, even when empty (lines 488–490), "so catalogs are stable" — an empty hub renders a placeholder bullet rather than vanishing from the index and breaking inbound links.


13. API Design — the CLI is the API

All eight subcommands take a bundle path as the first positional argument. main() resolves it and exits 1 with {"error": "bundle not found: …"} if it is not a directory (lines 899–902).

Subcommand Args Output Exit 0 Exit 1
impact <bundle> <concept> JSON: target, inbound, outbound, direct_edges, suggested_order, stats always when resolved concept not found
backlinks <bundle> <concept> JSON: target, backlinks[] with rels resolved concept not found
subgraph <bundle> <concept> [--hops 2] JSON: root, hops, nodes, edges resolved concept not found
pack <bundle> <concept> [--hops 2] [--max-nodes 20] [--undirected] JSON: root, hops, max_nodes, included, excluded, edges, markdown resolved concept not found
edges <bundle> [--from PATH] [--rel REL] JSON: edges, count, typed_count always --from not found
graph <bundle> [--format mermaid|json|html] [--focus PATH] [--hops 2] raw artifact for mermaid/html; JSON for json always --focus not found
validate <bundle> [--strict] JSON: concept_count, edge_count, issues[], error_count, warn_count, strict no errors (and no warnings under --strict) any error; any warning under --strict
orphans <bundle> JSON: orphans[], count always

Concept resolution (resolve_concept(), lines 305–315) accepts, in order: the exact bundle-relative path; a leading / (stripped); a case-insensitive file stem; a case-insensitive title; or any path suffix, with or without .md. First match wins in dict iteration order — Open Question Q3: ambiguous stems resolve non-obviously.

Validation rules (cmd_validate(), lines 764–835):

Severity Condition Line
error bundle has no root index.md 767–768
error edge target is not a loaded concept (broken link) 776–778
warn non-index concept with type "" or Unknown 772–773
warn non-index concept with no title in frontmatter 774–775
warn TicketLink with neither external_id nor worklog_id 788–796
warn concept in HIGH_IMPACT_TYPES with verified: false 808–816
info frontmatter relation outside KNOWN_RELS 779–786
info orphan — no inbound and no outbound links 797–807

index.md and log.md are exempt from the per-concept checks (line 770), and any file named index.md at any depth is exempt from the type and title warnings (lines 772, 774).

Exit-code contract. return 1 if errors or (strict and warnings) else 0 (line 835). The comment above it states the constraint explicitly: "the skills call validate and expect 0 on warnings."


14. Persistent State

The plugin writes nothing. Two stores exist in the repository and both belong to the vendored worklog tooling:

Store Format Owner Notes
.work/todo.jsonl, .work/done.jsonl Append-only JSONL event log bin/worklog Union-merge friendly. Never hand-edited (CLAUDE.md policy). hooks/pre-commit enforces a trailing newline and a per-event schema
docs/.index/ Generated JSON + rendered Markdown worklog ia-* Inventory, graph, aliases, publish manifest

The trailing-newline invariant is the reason hooks/pre-commit exists, and its own comment says so: without it "union merge fuses the last line of one side with the first line of the other and you lose two events to one unparseable line."

Generated files that must never be hand-edited: docs/roadmap.md (pre-commit regenerates and diffs it), docs/.index/**, and roadmap snapshots under docs/roadmap/.


15. External Service Integrations

Exactly one, and it is not in the plugin surface.

Service Used by Protocol Auth Timeout Failure handling
Substack archive API (<pub>/api/v1/archive, <pub>/feed) scripts/substack_okf.py HTTPS GET none 60 s urllib, 90 s curl urllib failure falls back to curl; curl failure raises RuntimeError; /feed failure is caught and skipped

No retries and no backoff (http_get(), lines 77–97). Paging stops on an empty batch, a short batch, or reaching --limit (fetch_archive(), lines 177–191). Recommendation: if this harness ever runs in CI, add a cached-fixture mode so a Substack outage cannot fail the build.


16. Security Design

Small surface, but three properties are load-bearing.

# Property Enforcement Threat if violated
S1 The HTML map makes no network requests and executes no script render_html() inlines all CSS and emits no <script>/src=/@import/url(); test_graph_html_is_self_contained(), lines 201–214 asserts each token is absent A generated artifact could exfiltrate or execute when opened; the file is explicitly meant to open in a locked-down viewer
S2 Concept titles and paths are HTML-escaped before rendering render_html(), line 622 binds e = html.escape and applies it to every interpolated value (626–635) A crafted title: in frontmatter injects markup into the map
S3 Graph traversal cannot escape the bundle root _normalize_target(), lines 211–214 returns None on relative_to() failure ../../ links would pull files outside the bundle into the graph and into packs

Command execution. okf-curate.sh runs on every Write/Edit/MultiEdit. It reads a path from host-supplied stdin JSON and uses it in dirname and shell case matching. It never evals it, and every validator call goes through command -v guards (lines 64–75). Both python3 invocations use -c with a fixed program and no interpolation (lines 11–15).

Subprocess use. substack_okf.py calls curl and sys.executable with argument lists, never shell=True (lines 89–94, 726–727). No injection path from the fetched content.

Secrets. None in the repository. No credential is read, written, or required by any script. .gitignore covers .env and .env.*.

Mermaid label escaping (render_mermaid(), line 181): double quotes in a title are replaced with single quotes before being wrapped in "…". Sufficient for well-formed titles; a title containing ] or a newline is untested. Open Question Q4.


17. Error Handling and Resilience

Class Handling Cited
Concept not resolvable {"error": …} on stdout, exit 1 cmd_impact():380–382 and the same pattern in backlinks, subgraph, pack, edges, graph
Bundle path not a directory {"error": "bundle not found: …"}, exit 1 main():900–902
Unreadable bytes in a file read_text(errors="replace") — never raises load_bundle():270
Broken link Reported as a validation error; excluded from traversal load_bundle():291–292, cmd_validate():776–778
Malformed frontmatter Silently partial — no error channel parse_frontmatter() throughout
Curation cannot find a bundle Message, exit 0 okf-curate.sh:57–60
External CLI missing Fall back to okf-graph.py okf-curate.sh:64–76
Validator non-zero inside the hook Suppressed with || true okf-curate.sh:65,67,70,75
Substack fetch failure urllib → curl → RuntimeError substack_okf.py:88–97

No retries, no timeouts, no circuit breakers anywhere in the plugin surface — every operation is a local read that either succeeds or fails immediately. This is a deliberate consequence of §6 (stdlib-only, filesystem-only).

Graceful degradation is the recurring pattern: the HTML map degrades to tables, curation degrades to skipping, the skills degrade to manual crawling, and validate degrades from gate to advisory. Nothing in the plugin ever hard-fails a user's edit.


18. Testing Strategy

18.1 The suite

tests/test_okf_graph.py — 16 cases, plain assert, no framework, no fixtures. Its own docstring (lines 6–9) states the scope: "Kept deliberately small: it exists to catch the defects that shipped in v0.2.0 … and to stop sample-okf and the four version manifests from drifting."

Group Cases Boundary
Frontmatter parsing inline_list, block_sequence, block_sequence_then_links, booleans_survive (41–69) Pure function, in-process
Link handling normalize_target (72–86, uses a tempfile bundle), merge_edges_frontmatter_wins (89–93) Pure functions
Rendering mermaid_ids_are_unique_per_path (96–108) Pure function
CLI over sample-okf sample_bundle_validates, pack_mermaid_has_no_collapsed_nodes, graph_mermaid_is_a_fenced_block, graph_json_shape, graph_focus_narrows_the_node_set, graph_focus_unknown_concept_errors, graph_html_is_self_contained (121–214) Subprocess, real bundle
CLI over a temp bundle strict_validate_flags_warnings (217–235) Subprocess, synthetic bundle
Repo invariants version_is_consistent_across_manifests (238–255) File reads

The module-loading trick. load_graph_module(), lines 24–35 imports okf-graph.py by path — the filename is not a valid identifier. The sys.modules["okf_graph"] = mod line is load-bearing and the docstring says why: "without it the @dataclass decorators raise AttributeError on Python 3.13, because dataclasses looks the class's module up in sys.modules and gets None."

Tripwires, not assertions about behavior. test_sample_bundle_validates() asserts concept_count == 22 and edge_count == 83 (lines 127–128), with the rationale in a comment: "sample-okf is the plugin's worked example, and the skills quote these numbers. A surprise change here means an unreviewed edit." Verified against the tree at v0.3.0: validate reports exactly 22 / 83, 0 errors, 0 warnings, 0 issues. Of the 83 edges, 13 are typed (edges subcommand).

18.2 Where the suite runs

Gate Command Bypassable
hooks/pre-commit python3 tests/test_okf_graph.py -q (guarded on the file existing) yes, --no-verify
GitHub Actions "graph engine tests" python3 tests/test_okf_graph.py -q no
GitHub Actions "sample bundle stays valid" python3 scripts/okf-graph.py validate sample-okf --strict no

The CI workflow comment states the intent directly: "The graph engine is what the plugin exists to do; nothing exercised it before v0.3.0."

18.3 Coverage gaps (Confirmed by absence)

  • No test for cmd_impact, cmd_backlinks, cmd_subgraph, cmd_edges, or cmd_orphans output shape. Five of eight subcommands are exercised only transitively.
  • No test for criticality_of() or enrich_nodes() ordering.
  • No test for scripts/okf-ticket-link.py at all.
  • No test for scripts/okf-curate.sh — the v0.2.0 hook defect would not be caught today by anything except a human reading hooks.json.
  • scripts/substack_okf.py is an integration harness requiring network; it is not run by CI or by the pre-commit hook.

19. Local Development

Prerequisites. python3 and bash. Nothing to install.

# graph engine tests
python3 tests/test_okf_graph.py          # verbose
python3 tests/test_okf_graph.py -q       # quiet, CI mode

# the same checks CI runs
WORKLOG_SKIP_BRANCH_GUARD=1 hooks/pre-commit
python3 scripts/okf-graph.py validate sample-okf --strict

# exercise the engine
python3 scripts/okf-graph.py impact   sample-okf agents/graph-engineer.md
python3 scripts/okf-graph.py pack     sample-okf agents/graph-engineer.md --hops 2
python3 scripts/okf-graph.py graph    sample-okf --format mermaid
python3 scripts/okf-graph.py graph    sample-okf --format html --focus agents/graph-engineer.md > /tmp/map.html
python3 scripts/okf-graph.py edges    sample-okf --rel routes_to
python3 scripts/okf-graph.py orphans  sample-okf

# ticket bridge
bin/worklog fold | python3 scripts/okf-ticket-link.py emit --bundle sample-okf --open-only --dry-run

# end-to-end integration (network; writes to gitignored integration/)
python3 scripts/substack_okf.py run --limit 20

Git hooks. git config core.hooksPath hooks (stated in hooks/pre-commit:3).

Common setup failures.

  • Commit rejected on main. The branch guard refuses commits authored directly on main/master (hooks/pre-commit:22–39). Branch first. WORKLOG_SKIP_BRANCH_GUARD=1 is only for non-commit callers running the script standalone.
  • "docs/roadmap.md is stale or hand-edited". It is generated. Run worklog roadmap-render (hooks/pre-commit, roadmap block).
  • Commit message rejected. hooks/commit-msg requires a 26-character ULID or a #123 ticket reference. Merge commits are exempt via MERGE_HEAD.
  • __pycache__ collisions during merges. hooks/pre-commit:12 exports PYTHONDONTWRITEBYTECODE=1 specifically to prevent this.

20. Risks, Tradeoffs, and Technical Debt

# Item Area Probability Impact Mitigation Trigger to act
R1 The frontmatter parser is not YAML and reports nothing on malformed input parse_frontmatter() high medium Four parser tests pin known shapes; validate catches consequences A user reports a silently-dropped key
R2 Five of eight subcommands have no direct output-shape test tests/ medium medium CI runs what exists Any change to enrich_nodes() or a payload shape
R3 sample-okf counts (22 / 83) are asserted as literals test_sample_bundle_validates() high low Deliberate tripwire; the failure message is clear Every legitimate sample edit must update two numbers
R4 Curation's path filter misses bundles not under .okf/, knowledge/, or sample-okf/ okf-curate.sh:22–25 medium medium Manual validate A user roots a bundle elsewhere and reports no curation
R5 The bundle is fully re-read on every invocation; no cache, no incremental mode load_bundle() low low Bundles are small; the cost is milliseconds A bundle in the thousands of files
R6 Three separate hand-built undirected adjacency maps subgraph / pack / graph medium low All three are dedup-guarded A fourth caller appears
R7 okf-ticket-link.py leaves an orphaned file when an item is retitled emit():147–163 medium low --dry-run before real runs Ticket directories accumulate stale slugs
R8 substack_okf.py (917 lines) has no test and no cached-fixture mode integration medium low Not on any gate It is ever added to CI
R9 docs/adr/ is empty while hooks/pre-commit runs worklog adr check on it docs low low Decision records live in sample-okf/decisions/ Contributors look for ADRs and find nothing

21. Extension Roadmap

The build is done; this is the ordered list of what to do next.

Order Work Rationale Depends on
1 Output-shape tests for impact, backlinks, subgraph, edges, orphans Closes R2; these are the payloads the skills parse
2 Decide and fix the criticality_of() medium branch (§34 Q2) A dead line invites a wrong "fix" later
3 Widen or make configurable the okf-curate.sh path filter Closes R4; the hook only just started working
4 Extract one undirected_adjacency() helper Closes R6; removes the duplicate construction in cmd_subgraph()
5 Optional PyYAML path with the hand parser as fallback Closes R1 without adding a hard dependency Evidence that users are hitting the edges
6 Cached-fixture mode for substack_okf.py Makes the end-to-end harness CI-safe
7 Real docs/adr/ records, or drop the empty directory Closes R9 Decision on where ADRs live

Out of scope, unchanged from prior design: replacing okfcli, embeddings / semantic search, and any Grok-only feature that breaks Claude Code.


22. Open Questions

# Question Why it matters Options Recommendation
Q1 Should validate gain an error class for unparseable frontmatter? Today a malformed block degrades to a partial dict and only its symptoms are reported (a) leave it; (b) surface a warn when a frontmatter block is present but yields no keys (b) — cheap, and it turns R1 from silent to visible
Q2 Is criticality_of()'s medium branch (line 342) intended to escalate? The line is currently a no-op; a future reader will "fix" it one way or the other (a) delete the branch; (b) escalate unverified medium to high (a) unless there is a ranking reason for (b); either way, add a test
Q3 Should ambiguous resolve_concept() matches be an error? First-match-wins over dict order makes stem and title lookups non-deterministic across bundles (a) leave; (b) error on multiple matches; (c) prefer exact-path, warn otherwise (c)
Q4 Does Mermaid label escaping need to handle ] and newlines? render_mermaid():181 only substitutes double quotes (a) leave; (b) strip or escape the full unsafe set (b) — one re.sub, removes a whole class of broken diagrams

23. Omitted Sections

Per the template's menu rule, sections whose subject does not exist here, each with its reason:

Template section Reason omitted
12 Package-by-Package No packages. Three independent single-file scripts, no imports between them
16 Cache Design Nothing caches. Every invocation re-reads the bundle
17 MCP Server Integration The plugin neither ships nor consumes an MCP server
18 AI Endpoint Design No code in this repository calls a model. The host model reads the skills; the plugin never makes an inference request
19 Managed AI Platform No Bedrock / Vertex / Azure OpenAI integration
21 Event-Driven Processing No queues, topics, or async consumers. The .work/*.jsonl event log is append-only project state, covered in §14
24 Performance and Scalability No load model applies. Single-user CLI over a local directory; see R5 for the one scaling note
25 Observability No logs, metrics, traces, or dashboards. The subcommands print JSON to stdout and exit; that is the entire observability surface
26 Configuration and Secrets No plugin configuration and no secrets. .work/config.yml configures the vendored worklog tooling, not the plugin
27 Deployment Architecture No deployment. Installation is a marketplace entry pointing at this repository (§5.3)
30 Operations and Support No running system to operate. Recovery is git checkout
33 Traceability Matrix Folded into §3.1 and §3.2, which map each requirement directly to its implementing function and line range

24. Appendix

24.1 Diagram index

§ Diagram Type
4 System context and trust boundary flowchart
5.1 Logical architecture flowchart
5.2 Ingest data flow flowchart
8.1 Impact-first change sequenceDiagram
8.2 Progressive-disclosure pack flowchart
8.4 Post-edit curation sequenceDiagram
9.1 Frontmatter parser states stateDiagram-v2
10 Domain model classDiagram
11 Module layering flowchart

24.2 Closing summary

Top architectural risks. (1) The hand-rolled frontmatter parser is the single point of silent failure for user-authored bundles — R1. (2) Five of eight subcommand payloads have no direct test, and those payloads are exactly what the skills parse — R2. (3) The curation hook, working for the first time as of v0.3.0, only fires on three path shapes — R4.

Immediate decisions required. Q2 (the dead criticality branch) and Q3 (ambiguous concept resolution). Both are small, both get worse with time.

Recommended implementation order. §21, items 1–4 — all are additive, none changes a shipped output shape.

Information still needed. Real usage data on user-authored bundles. Every known parser defect so far was found by reading, not by a report, because the repository's own generators emit only the shapes the parser already handles.

Clone this wiki locally