okf 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.okf/capabilities/agent-skill.md +112 -0
- data/.okf/capabilities/bundles-manager.md +144 -0
- data/.okf/capabilities/graph-server.md +678 -0
- data/.okf/capabilities/index.md +26 -0
- data/.okf/capabilities/library-api.md +82 -0
- data/.okf/capabilities/linter.md +83 -0
- data/.okf/capabilities/read-views.md +228 -0
- data/.okf/capabilities/render.md +66 -0
- data/.okf/capabilities/search.md +297 -0
- data/.okf/capabilities/validator.md +60 -0
- data/.okf/cli.md +214 -0
- data/.okf/design/browser-tests.md +211 -0
- data/.okf/design/core-shell-split.md +73 -0
- data/.okf/design/index.md +17 -0
- data/.okf/design/integration-first.md +140 -0
- data/.okf/design/packaging.md +65 -0
- data/.okf/design/ruby-floor.md +53 -0
- data/.okf/design/runtime-dependencies.md +82 -0
- data/.okf/design/search-engines.md +154 -0
- data/.okf/design/server-trust-boundary.md +139 -0
- data/.okf/index.md +40 -0
- data/.okf/log.md +724 -0
- data/.okf/model/bundle.md +47 -0
- data/.okf/model/concept.md +75 -0
- data/.okf/model/graph.md +59 -0
- data/.okf/model/index.md +9 -0
- data/.okf/model/skeleton.md +76 -0
- data/.okf/overview.md +87 -0
- data/.okf/registry.md +432 -0
- data/.okf/structure/format-layer.md +59 -0
- data/.okf/structure/index.md +22 -0
- data/.okf/structure/search.md +53 -0
- data/.okf/structure/the-analysers.md +60 -0
- data/.okf/structure/the-cli.md +99 -0
- data/.okf/structure/the-disk-shell.md +76 -0
- data/.okf/structure/the-model.md +81 -0
- data/.okf/structure/the-server.md +74 -0
- data/.okf/structure/the-skill.md +52 -0
- data/.okf/testing/adding-a-verb.md +76 -0
- data/.okf/testing/index.md +12 -0
- data/.okf/testing/the-harness.md +45 -0
- data/CHANGELOG.md +161 -16
- data/README.md +226 -17
- data/lib/okf/cli/command.rb +8 -3
- data/lib/okf/cli/registry.rb +306 -47
- data/lib/okf/cli.rb +1 -1
- data/lib/okf/registry.rb +448 -22
- data/lib/okf/render/graph/template.html.erb +6 -2
- data/lib/okf/server/hub.rb +6 -2
- data/lib/okf/skill/reference/cli/registry.md +49 -6
- data/lib/okf/skill/reference/cli.md +1 -1
- data/lib/okf/version.rb +1 -1
- metadata +46 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Capabilities
|
|
2
|
+
|
|
3
|
+
The seven things the baseline gem does over a bundle, plus the read views that
|
|
4
|
+
print it at a glance — and the sibling surfaces built on the same kernel. All of
|
|
5
|
+
them run over the same [pure model](../model/); the gem's own are fronted by the
|
|
6
|
+
[CLI](../cli.md).
|
|
7
|
+
|
|
8
|
+
# Judge
|
|
9
|
+
|
|
10
|
+
* [Conformance validator](validator.md) - the §11 legal check; the only capability that can fail a bundle.
|
|
11
|
+
* [Curation linter](linter.md) - advisory quality report across eight categories; never rejects.
|
|
12
|
+
|
|
13
|
+
# Serve & read
|
|
14
|
+
|
|
15
|
+
* [Interactive graph server](graph-server.md) - a self-contained HTML graph over HTTP — one bundle or many behind a hub — mountable as a Rack app.
|
|
16
|
+
* MCP server (`@okf-mcp design/the-tool-set`) - the sibling `okf-mcp` gem: the kernel's capabilities as MCP tools, resources and prompts for any agent host.
|
|
17
|
+
* [Bundles manager](bundles-manager.md) - the hub's `/b/` page: every bundle with its size, health and default marker, and the forms that manage the registry from a browser.
|
|
18
|
+
* [Static render](render.md) - the same page written to one self-contained static file, the bundle baked in, to host where there is no server (`okf render`).
|
|
19
|
+
* [Read views](read-views.md) - `index`, `catalog`, `files`, `types`, `tags`, `stats`, `loose`, `graph` — the browser views as text, plus the `index` map.
|
|
20
|
+
* [Ranked text search](search.md) - deterministic ranked retrieval over metadata and bodies; answers "which concept covers X?" in a few rows.
|
|
21
|
+
|
|
22
|
+
# Use & author
|
|
23
|
+
|
|
24
|
+
* [Library API](library-api.md) - the Ruby surface: pure model plus on-disk handles.
|
|
25
|
+
* [Companion agent skill](agent-skill.md) - the skill shipped inside the gem that teaches an agent to author OKF.
|
|
26
|
+
* Enforcement layer (`@okf-eco gems/okf-pro`) - the sibling `okf-pro` gem: writes an agent's knowledge repository, then holds it to a few invariants at three doors under a fail-closed contract.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: Library API
|
|
4
|
+
description: The Ruby surface — a pure in-memory model plus on-disk handles, an "ActiveRecord for the filesystem".
|
|
5
|
+
resource: gems/okf/lib/okf.rb
|
|
6
|
+
tags: [ruby, diagram]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-13T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: README.md — Library
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/README.md
|
|
13
|
+
- title: gems/okf/lib/okf.rb
|
|
14
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf.rb
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Overview
|
|
18
|
+
|
|
19
|
+
Everything the [CLI](../cli.md) does is available in-process. `require "okf"`
|
|
20
|
+
gives you two layers, split cleanly by the [core/shell rule](../design/core-shell-split.md):
|
|
21
|
+
|
|
22
|
+
- **pure, in-memory** — [`OKF::Concept`](../model/concept.md) and
|
|
23
|
+
[`OKF::Bundle`](../model/bundle.md), which you build, interrogate, validate,
|
|
24
|
+
lint, and graph with **no disk involved**;
|
|
25
|
+
- **on-disk handles** — `OKF::Concept::File` and `OKF::Bundle::Folder`, which add
|
|
26
|
+
`load` / `save` / `reload` / `delete` on top of the pure model.
|
|
27
|
+
|
|
28
|
+
`require "okf"` stops at those two layers: the [CLI](../cli.md) and the skill
|
|
29
|
+
installer load only when asked for (from `okf/exe/okf`, or an explicit
|
|
30
|
+
`require "okf/cli"` / `require "okf/skill"`), so an app embedding the library
|
|
31
|
+
never drags in the command-line machinery. The
|
|
32
|
+
[registry](../registry.md) and the server's hub sit behind the same door — the CLI
|
|
33
|
+
requires them at the moment a registry verb or a multi-bundle `server` runs, so
|
|
34
|
+
the library surface stays the same size no matter what the executable grows.
|
|
35
|
+
|
|
36
|
+
```mermaid
|
|
37
|
+
classDiagram
|
|
38
|
+
class Concept {
|
|
39
|
+
<<pure>>
|
|
40
|
+
}
|
|
41
|
+
class Bundle {
|
|
42
|
+
<<pure>>
|
|
43
|
+
}
|
|
44
|
+
class Graph {
|
|
45
|
+
<<pure>>
|
|
46
|
+
}
|
|
47
|
+
class ConceptFile {
|
|
48
|
+
<<on-disk>>
|
|
49
|
+
}
|
|
50
|
+
class Folder {
|
|
51
|
+
<<on-disk>>
|
|
52
|
+
}
|
|
53
|
+
Bundle "1" o-- "*" Concept : contains
|
|
54
|
+
Bundle ..> Graph : builds
|
|
55
|
+
ConceptFile ..> Concept : wraps
|
|
56
|
+
Folder ..> Bundle : wraps
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`ConceptFile` is `OKF::Concept::File`; `Folder` is `OKF::Bundle::Folder` — each
|
|
60
|
+
on-disk handle wraps a pure counterpart and adds load/save/reload/delete.
|
|
61
|
+
|
|
62
|
+
# Build knowledge without touching Markdown
|
|
63
|
+
|
|
64
|
+
The pure layer is the surface an embedding app uses to reuse the gem over
|
|
65
|
+
knowledge it already holds as records. Construct concepts from data, assemble a
|
|
66
|
+
bundle, and call `#validate`, `#lint`, or `#graph` — no Markdown round-trip
|
|
67
|
+
needed. The lower-level pieces work standalone too:
|
|
68
|
+
`OKF::Bundle::Validator.call`, `OKF::Bundle::Linter.call`,
|
|
69
|
+
`OKF::Bundle::Search.call`, `OKF::Bundle::Graph.build`,
|
|
70
|
+
`OKF::Markdown::Frontmatter.parse`.
|
|
71
|
+
|
|
72
|
+
# Folder is an ActiveRecord for the filesystem
|
|
73
|
+
|
|
74
|
+
`OKF::Bundle::Folder.load(dir)` reads a directory into a pure bundle;
|
|
75
|
+
`Folder.new(bundle:, root:).save` materializes one back — and **validates §11
|
|
76
|
+
before publishing** through an atomic writer, so it never leaves a broken bundle
|
|
77
|
+
on disk. `OKF::Server::App.new(folder)` turns a folder straight into the
|
|
78
|
+
[graph server](graph-server.md) — mountable anywhere, and the one option a mount
|
|
79
|
+
owns is `search_endpoint:`, since the page resolves it against the reader's URL
|
|
80
|
+
and only the host knows its own prefix (the `/search` route answers either way).
|
|
81
|
+
And `OKF::Render::Graph.static(folder)` bakes that
|
|
82
|
+
same page into one self-contained file — the Ruby side of [`okf render`](render.md).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: Curation linter (lint)
|
|
4
|
+
description: An advisory curation-quality report across eight categories and twenty-six checks — pinned severities, an explicit clock, and it never rejects a bundle.
|
|
5
|
+
resource: gems/okf/lib/okf/bundle/linter.rb
|
|
6
|
+
tags: [curation, cli]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-14T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/bundle/linter.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/bundle/linter.rb
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Overview
|
|
16
|
+
|
|
17
|
+
`okf lint` answers the question [validate](validator.md) is forbidden to touch:
|
|
18
|
+
*is this bundle well-curated — navigable, complete, trustworthy?* It reports over
|
|
19
|
+
exactly the soft things §11 tolerates, has its own `OKF::Bundle::Linter` and
|
|
20
|
+
report, and never emits a conformance error. It is **advisory**: exit `0` even
|
|
21
|
+
with findings unless you opt in with `--fail-on warn` (any warn) or
|
|
22
|
+
`--fail-on info` (any finding at all).
|
|
23
|
+
|
|
24
|
+
# Eight categories, twenty-six checks — with pinned severities
|
|
25
|
+
|
|
26
|
+
| Category | Checks |
|
|
27
|
+
|----------|--------|
|
|
28
|
+
| Reachability | `orphan`, `not_in_index`, `disconnected_component`, `unlinked` |
|
|
29
|
+
| Backlog | `missing_concept`, `broken_index_entry` |
|
|
30
|
+
| Completeness | `stub`, `missing_title`, `missing_description`, `missing_generated` |
|
|
31
|
+
| Freshness | `expired`, `stale` |
|
|
32
|
+
| Provenance | `uncited_external`, `broken_source`, `unattributed_claim`, `unused_source`, `unprefixed_actor` |
|
|
33
|
+
| Attestation | `incomplete_computation`, `broken_attestation_ref` |
|
|
34
|
+
| Migration | `legacy_timestamp`, `legacy_citations` |
|
|
35
|
+
| Hygiene | `duplicate_title`, `unused_reference_def`, `undefined_reference`, `self_link`, `log_order` |
|
|
36
|
+
|
|
37
|
+
`log_order` is the first log-side check: §9 describes the log as date-grouped
|
|
38
|
+
entries newest first — prose, not an RFC keyword, so disorder is curation
|
|
39
|
+
slack here, never a [validator](validator.md) error, and only shape-valid
|
|
40
|
+
headings are compared (a malformed date is already the validator's, reported
|
|
41
|
+
once). `unprefixed_actor` reads both §7 identity fields with one consequence
|
|
42
|
+
each: a bare `verified[].by` reads as machine-confirmed (the §5.3 misread the
|
|
43
|
+
tier system exists to prevent), while a bare `generated.by` feeds no tier and
|
|
44
|
+
costs the audit trail — nobody downstream can tell a person from a process.
|
|
45
|
+
|
|
46
|
+
**Severity is API.** Every id has a pinned level in one tested constant
|
|
47
|
+
(`Linter::SEVERITIES`): machine consumers gate repo edits and CI on `:warn`
|
|
48
|
+
and drop `:info`, so a severity change is a behavior change for them — a new
|
|
49
|
+
gateable state gets a flag (`--fail-on info`), never a severity promotion.
|
|
50
|
+
Two of the calls carry their argument in the code: `unattributed_claim` warns
|
|
51
|
+
while its join-twin `unused_source` informs (a dangling footnote misattributes
|
|
52
|
+
a claim; an uncited source is only slack), and `expired` informs because a
|
|
53
|
+
`stale_after` passes on the calendar, not on a change — a warn would fail a
|
|
54
|
+
`--fail-on warn` gate on a morning nobody chose. The Migration pair is info
|
|
55
|
+
for the same reason: §13 says a v0.1 bundle is consumable forever, and a
|
|
56
|
+
migration campaign gates explicitly with
|
|
57
|
+
`--only legacy_timestamp,legacy_citations --fail-on info`.
|
|
58
|
+
|
|
59
|
+
Select with `--only` / `--except` (by check id), tune the stub threshold with
|
|
60
|
+
`--min-body`, and get the whole report as a machine substrate with `--json` —
|
|
61
|
+
which also carries the bundle's posture: a `trust` tier distribution (in the
|
|
62
|
+
hyphenated wire spelling) and an effective-`status` frequency.
|
|
63
|
+
|
|
64
|
+
# The clock contract
|
|
65
|
+
|
|
66
|
+
The linter is pure and never reads a clock: `expired` runs only when the
|
|
67
|
+
caller supplies `today:` (the CLI passes today, or `--today YYYY-MM-DD` for a
|
|
68
|
+
reproducible report), and `stale` only when `--stale-after
|
|
69
|
+
<90d | 12w | 2026-01-01>` supplies a cutoff over `generated_at`. Every
|
|
70
|
+
clock-gated check that was selected but could not run is *named* in
|
|
71
|
+
`stats[:skipped_checks]` — a gate that is sometimes absent and does not
|
|
72
|
+
confess converts "unchecked" into "checked and fine". The flag and the
|
|
73
|
+
frontmatter field that share the `stale-after` spelling are different
|
|
74
|
+
mechanisms: the flag is the reader's age cutoff (`stale`), the field the
|
|
75
|
+
author's declared expiry (`expired`).
|
|
76
|
+
|
|
77
|
+
# Where lint stops and an agent begins
|
|
78
|
+
|
|
79
|
+
`lint` is structural: it cannot judge **contradictions** or **semantic**
|
|
80
|
+
staleness (a concept that parses fine but no longer matches reality). Those need
|
|
81
|
+
meaning. `lint --json` is precisely the structured input an agent reasons over to
|
|
82
|
+
close that gap. The [`loose`](read-views.md) view is a folder-grouped lens over
|
|
83
|
+
the single `unlinked` check.
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: Read views (index, dirs, catalog, files, references, types, tags, stats, loose, graph)
|
|
4
|
+
description: The server's browser panels reproduced on the CLI, plus the index map, so an agent reads a bundle at a glance without a browser.
|
|
5
|
+
tags: [read, cli, json]
|
|
6
|
+
generated:
|
|
7
|
+
by: human:maintainer
|
|
8
|
+
at: 2026-08-13T12:00:00Z
|
|
9
|
+
sources:
|
|
10
|
+
- title: cli.md — read views
|
|
11
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/skill/reference/cli.md
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Overview
|
|
15
|
+
|
|
16
|
+
The [graph server](graph-server.md) renders a bundle in a browser; these verbs
|
|
17
|
+
render the same knowledge as text, so an agent (or a terminal) reads it directly.
|
|
18
|
+
Each prints a scannable human view by default and machine JSON with `--json`, and
|
|
19
|
+
all are advisory reads that exit `0`. They share
|
|
20
|
+
[`OKF::Bundle#catalog`](../model/bundle.md) and the
|
|
21
|
+
[graph indexes](../model/graph.md) for their data. When the question is lexical —
|
|
22
|
+
"which concept covers X?" — [ranked search](search.md) cuts across every field
|
|
23
|
+
these views group by, for the price of a few rows.
|
|
24
|
+
|
|
25
|
+
# The views
|
|
26
|
+
|
|
27
|
+
| Verb | Shows | Grouped by |
|
|
28
|
+
|------|-------|------------|
|
|
29
|
+
| `index` | each directory's index body, type/tag rollup, child dirs, and concept listing | directory (root first) |
|
|
30
|
+
| `dirs` | every directory with the concepts living directly in it | directory (root first) |
|
|
31
|
+
| `catalog` | concepts with type, tags, link counts, status | top-level dir |
|
|
32
|
+
| `files` | files with titles | folder |
|
|
33
|
+
| `references` | the on-disk `references/` tree (§6.3) with each file's citers and the dangling §6.2 pointers | folder |
|
|
34
|
+
| `types` | types (`@okf-eco format/frontmatter`) with their concepts | count |
|
|
35
|
+
| `tags` | tags (`@okf-eco format/frontmatter`) with their concepts | count |
|
|
36
|
+
| `stats` | rollups: concepts, dirs, types, cross-links, tags | — |
|
|
37
|
+
| `loose` | degree-0 concepts (no links (`@okf-eco format/cross-links`) in or out) | folder |
|
|
38
|
+
| `graph` | the raw nodes and edges; `--hubs` ranks inbound links by source top-level dir; `--traffic` collapses concepts into dirs and links into weighted arcs, with cohesion | — (`--minimal` / `--no-body`) |
|
|
39
|
+
|
|
40
|
+
`dirs` and `stats` answer about the *same* set of directories, deliberately:
|
|
41
|
+
both read `Bundle#directory_index`, the map `--dir` is resolved against. Grouping
|
|
42
|
+
the catalog instead — which knows only the directories that happen to hold a
|
|
43
|
+
concept — made the two verbs disagree about how big a bundle was (`stats` said 2
|
|
44
|
+
where `dirs` listed 3 on a bundle whose root carries an index.md and no
|
|
45
|
+
concepts), and worse, left a directory out of `by_dir` that `--dir` answers
|
|
46
|
+
about. Counts stay direct, so a directory holding nothing itself reports the zero
|
|
47
|
+
it holds rather than disappearing; `by_dir.keys` is therefore the complete list
|
|
48
|
+
of what `--dir` can name.
|
|
49
|
+
|
|
50
|
+
`references` is the one view whose rows are not concepts: the reader models
|
|
51
|
+
only markdown, so a `.py` attester or a `.sql` computation exists to no other
|
|
52
|
+
surface, and this verb lists the `references/` tree from disk with which
|
|
53
|
+
concepts cite each file through the §6.2 path-valued fields — plus every
|
|
54
|
+
pointer that resolves to nothing, the bare-path trap (§6.2 resolves a bare
|
|
55
|
+
`references/…` relative to the concept) named with its leading-slash fix.
|
|
56
|
+
Folder scope is deliberate: §6.3 is a naming convention, not a requirement,
|
|
57
|
+
and a computation stored beside its concept is legal and not inventoried.
|
|
58
|
+
|
|
59
|
+
Every one of them names the bundle it answers about, in the identity the caller
|
|
60
|
+
used — the rule the [CLI](../cli.md) keeps: `bundle` is always the directory,
|
|
61
|
+
`slug` always a registry slug, and a header that reads `@handbook (/path)` when a
|
|
62
|
+
[ref](../registry.md) named it. `graph` was the last holdout, printing a bare pair
|
|
63
|
+
of counts over a bare `nodes`/`edges` payload; an agent holding several bundles
|
|
64
|
+
had nothing in that answer to tell them apart.
|
|
65
|
+
|
|
66
|
+
# `index` is the orient-first map (§8)
|
|
67
|
+
|
|
68
|
+
Alone among the read views, `index` shows the reserved `index.md` layer: the
|
|
69
|
+
concept views skip those structural files, so only `index` renders the
|
|
70
|
+
progressive-disclosure map (`@okf-eco format/okf-format`) — one entry per directory
|
|
71
|
+
(root first) with its authored index body, a type/tag rollup over the concepts
|
|
72
|
+
living directly there, its child directories, and the concept listing. `--dir`
|
|
73
|
+
narrows to a directory and its subtree and repeats (`root` names the bundle
|
|
74
|
+
root, unless one is really called that) — bringing the chain up to the root with
|
|
75
|
+
it, marked `↑`, since orientation
|
|
76
|
+
is the whole point of this view and a branch shown alone has lost it —
|
|
77
|
+
`--depth N` bounds how far below the starting point that reaches, and
|
|
78
|
+
`--no-body` drops the prose to a skeleton. The last two are what make the map
|
|
79
|
+
usable at scale: every directory is a section, so a few hundred concepts is a map
|
|
80
|
+
nobody reads whole — `--depth 1 --no-body` orients, `--dir <branch> --depth 1`
|
|
81
|
+
descends, and the pair walks the tree a level at a time instead of paging it. It is the cheapest orientation when picking up a
|
|
82
|
+
bundle, and the only view that exposes *enumeration drift* — a listing entry that
|
|
83
|
+
should exist but is missing, which no grep can find. A directory that holds
|
|
84
|
+
concepts but no `index.md` gets its listing synthesized and tagged `(no index.md)`,
|
|
85
|
+
a prompt to write a real map rather than a defect.
|
|
86
|
+
|
|
87
|
+
# `dirs` is the shape, `index` is the contents
|
|
88
|
+
|
|
89
|
+
`dirs` answers the question `--dir` is pointed at: every directory the bundle
|
|
90
|
+
has — those holding concepts, those carrying an `index.md` or a scoped
|
|
91
|
+
`log.md`, and the empty intermediates that exist only to connect the tree —
|
|
92
|
+
with two counts each. A directory whose only file is its history is real (the
|
|
93
|
+
log verbs read it); one holding only a file the reader skipped is not, and
|
|
94
|
+
stays out of the map on every surface at once.
|
|
95
|
+
|
|
96
|
+
`count` is **direct**, never cumulative: the column sums to the bundle's concept
|
|
97
|
+
count, and a directory holding nothing but sub-directories reads `0` rather than
|
|
98
|
+
borrowing its children's weight. That honesty is also its limit, and why
|
|
99
|
+
`subtree` sits beside it: truncate the listing on a deep bundle and every row at
|
|
100
|
+
the top of the tree reads `0`, which is precisely where "where is the mass?" is
|
|
101
|
+
being asked. `subtree` is defined as *exactly what `--dir` on that row returns*,
|
|
102
|
+
so the number and the flag can never disagree — and the root's subtree is
|
|
103
|
+
therefore its own direct count, since `.` is a prefix of nothing. The human
|
|
104
|
+
table shows the second column only where some directory actually nests; a flat
|
|
105
|
+
bundle would only see the first one repeated.
|
|
106
|
+
|
|
107
|
+
`--dir` (repeatable) takes a subtree — **and the chain up to the root with it**,
|
|
108
|
+
so a branch is never shown adrift of the authored context that says what it is.
|
|
109
|
+
Those rows are marked (`↑`, `ancestor: true`) and stay out of `total`, which is
|
|
110
|
+
what keeps a row's `subtree` equal to the total `--dir` on that row returns;
|
|
111
|
+
`--no-ancestors` drops them. A `--dir` naming nothing gains no chain, since a
|
|
112
|
+
lone root row would read as a partial answer to a query that matched nothing.
|
|
113
|
+
`--depth N` bounds how far below the starting point the descent reaches — the `--dir` when one is given, the bundle root
|
|
114
|
+
otherwise. **Relative, not absolute**, so `--dir a/b --depth 1` reads "a/b and
|
|
115
|
+
one level under it" without the caller first working out how deep `a/b` is, and
|
|
116
|
+
the two compose the way a reader descending a tree actually moves. `--depth 0`
|
|
117
|
+
is the starting point alone. The deprecated `--area` combines with **neither**
|
|
118
|
+
`--depth` nor `--dir` and is refused (exit 2) for one reason wearing two shapes:
|
|
119
|
+
it is *exact*. With `--depth` it names no starting point to be relative to, so
|
|
120
|
+
the pair unioned the area with every directory at that depth from the root; with
|
|
121
|
+
`--dir` one side is exact where the other is a prefix, so the map came back with
|
|
122
|
+
the area *and* the subtree. Both read like an answer and are an answer to neither
|
|
123
|
+
question.
|
|
124
|
+
|
|
125
|
+
Matching folds case, but a row is found by its *stored* spelling, so the chain is
|
|
126
|
+
walked folded and handed back in the map's own words. It used to be handed back
|
|
127
|
+
folded, which silently dropped every ancestor of a directory spelled with a
|
|
128
|
+
capital — the chain the flag exists to draw, missing exactly the row that places
|
|
129
|
+
the branch.
|
|
130
|
+
|
|
131
|
+
One line per directory where `index` is the whole map, so it is the cheaper of
|
|
132
|
+
the two when the question is shape rather than contents — and `dirs` is the first
|
|
133
|
+
thing to run on an unfamiliar bundle. Cheaper structurally, not by measurement:
|
|
134
|
+
`dirs` emits one row per *directory* and `index` one listing row per *concept*
|
|
135
|
+
even under `--no-body`, so their costs scale with different things. The root stores `.` and
|
|
136
|
+
prints `(root)` — the split every grouped view keeps, so a table and its `--json`
|
|
137
|
+
never disagree about which spelling is the data.
|
|
138
|
+
|
|
139
|
+
# JSON output — compact, and projectable
|
|
140
|
+
|
|
141
|
+
`--json` is **compact by default** — single-line, the token-efficient substrate an
|
|
142
|
+
agent consumes; `--pretty` (which implies `--json`) indents the same JSON for a
|
|
143
|
+
human. On the per-item list views — `index`, `catalog`, `files`, `dirs` — `--fields a,b`
|
|
144
|
+
keeps only those properties and `--except a,b` drops them (mutually exclusive; an
|
|
145
|
+
unknown name is a usage error that lists the valid ones). Projection runs before
|
|
146
|
+
emission, so an agent never pays tokens for a field it dropped: `okf index <dir>
|
|
147
|
+
--except body,listing` is the lean directory skeleton, the difference between a few
|
|
148
|
+
hundred bytes and hundreds of KB on a large bundle.
|
|
149
|
+
|
|
150
|
+
# Narrowing and regrouping
|
|
151
|
+
|
|
152
|
+
The four list views — `catalog`, `files`, `types`, `tags` — accept the filters
|
|
153
|
+
*orthogonal* to how they group, so you ask a narrow question instead of paging
|
|
154
|
+
the whole bundle (matching is case-insensitive):
|
|
155
|
+
|
|
156
|
+
| View | Filters it accepts |
|
|
157
|
+
|------|--------------------|
|
|
158
|
+
| `catalog`, `files` | `--type`, `--dir`, `--tag` |
|
|
159
|
+
| `types` | `--dir`, `--tag` (it already groups by type) |
|
|
160
|
+
| `tags` | `--type`, `--dir` (it already groups by tag) |
|
|
161
|
+
|
|
162
|
+
`--dir` is one rule: a concept matches when its directory *is* the path or sits
|
|
163
|
+
below it, so `--dir platform` reaches `platform/services/api` and `--dir .` (or
|
|
164
|
+
`root`) means the root alone. A trailing slash is accepted and ignored, because
|
|
165
|
+
the human views print one — `index` labels a row `platform/services/` — and a
|
|
166
|
+
flag that refuses the label the tool just printed answers "nothing found" to a
|
|
167
|
+
directory that is full.
|
|
168
|
+
|
|
169
|
+
The `root` spelling has one exception, and it is the shape that showed why an
|
|
170
|
+
alias is never free: a bundle holding a real `root/` directory. There the word
|
|
171
|
+
names that directory, not the bundle root, because the alias only ever bought a
|
|
172
|
+
shell quote and the directory would otherwise be unaddressable — answering for
|
|
173
|
+
the root instead, exit 0, nothing said. The resolution is against the bundle's
|
|
174
|
+
own directories rather than the string alone, which is also what the *stored*
|
|
175
|
+
spellings must not go through: `dirs` folded each row's own dir through the
|
|
176
|
+
same helper, so a row named `root` computed its `subtree` against the bundle
|
|
177
|
+
root and disagreed with what `--dir` on that row returns.
|
|
178
|
+
|
|
179
|
+
"The bundle's own directories" has to mean one list, and that is the lesson the
|
|
180
|
+
first attempt missed. The concept views asked the **catalog**, which knows only
|
|
181
|
+
directories that hold concepts, while `dirs` and `index` asked the directory
|
|
182
|
+
map, which counts an `index.md` too — so a `root/` carrying nothing but its
|
|
183
|
+
index stayed folded in `catalog` and named in `dirs`, in the same bundle, on the
|
|
184
|
+
same flag. One question gets one source (`Bundle#directories`) — and the list
|
|
185
|
+
counts every file kind that makes a directory real, because the same bug came
|
|
186
|
+
back one kind over: a `root/` holding only a scoped `log.md` was invisible to
|
|
187
|
+
the set until the log joined the seed. The other half
|
|
188
|
+
is *when*: the alias is a fact about a bundle, so a run naming several resolves
|
|
189
|
+
it once across the served set — resolving inside the per-bundle loop let a
|
|
190
|
+
single `--dir root` mean the subtree in one bundle and the root in the next,
|
|
191
|
+
merged into one ranking with nothing saying so. It replaces `--area`, which saw only a concept id's
|
|
192
|
+
first path segment — a word the OKF format (`@okf-eco format/okf-format`) never used —
|
|
193
|
+
and which still works, warning on stderr, until a later release drops it.
|
|
194
|
+
|
|
195
|
+
`tags --by type|dir` regroups the tag index under each concept dimension with
|
|
196
|
+
within-group counts — the view for curating a
|
|
197
|
+
tag (`@okf-eco format/frontmatter`) vocabulary: which tags cluster in which directory,
|
|
198
|
+
which type leans on which tags. Each row also carries the tag's total across
|
|
199
|
+
the narrowed set, printed `count/total` when they differ (`async 2/3`) and as
|
|
200
|
+
a plain count when the tag is wholly local — so a tag's *locality* (domain
|
|
201
|
+
confined to one directory, or concern cutting across several) reads per row. The
|
|
202
|
+
same evidence question for the graph: `graph --hubs` ranks every concept with
|
|
203
|
+
inbound links (`@okf-eco format/cross-links`) by inbound degree and groups each
|
|
204
|
+
hub's links by *source top-level dir* — whether a hub is well-homed, answered
|
|
205
|
+
mechanically.
|
|
206
|
+
|
|
207
|
+
`graph --traffic` asks it one grain coarser, about **directories**. It reads the
|
|
208
|
+
[skeleton](../model/skeleton.md): concepts collapse into the directory they live in
|
|
209
|
+
and the links between two directories into one weighted arc, so each row carries
|
|
210
|
+
that directory's traffic split three ways — internal, out, in — plus
|
|
211
|
+
**cohesion**, its internal share. That is
|
|
212
|
+
cohesion-versus-coupling applied to a knowledge tree, and it is the only read at
|
|
213
|
+
the grain `refine` actually decides at: `--hubs` says whether a *concept* is
|
|
214
|
+
well-homed, `--traffic` says whether a *directory* is holding together, acting
|
|
215
|
+
as a shared vocabulary everyone cites, or behaving like a projection that should
|
|
216
|
+
have been an index. `--cut` is fitted to the bundle rather than fixed, because a
|
|
217
|
+
fixed weight left 2 arcs on one bundle and 136 on another; cohesion is computed
|
|
218
|
+
over every arc regardless of the cut, so narrowing the picture never moves the
|
|
219
|
+
evidence.
|
|
220
|
+
|
|
221
|
+
# `loose` is a curation lens, not an error
|
|
222
|
+
|
|
223
|
+
`loose` is the folder-grouped view over [lint](linter.md)'s `unlinked` check —
|
|
224
|
+
distinct from `orphan`. An `index.md` listing makes a file *reachable* (not an
|
|
225
|
+
orphan) but is **not a graph edge**, so a listed file can still float here. A
|
|
226
|
+
loose file may be perfectly fine — a terminal leaf like a backlog item is loose
|
|
227
|
+
by design — so `loose` surfaces the set for a human or agent to judge and always
|
|
228
|
+
exits `0`.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Capability
|
|
3
|
+
title: Static render (render)
|
|
4
|
+
description: One self-contained HTML file with the whole graph baked in — the same page `okf server` serves, written to disk so it hosts where nothing runs (`okf render`).
|
|
5
|
+
resource: gems/okf/lib/okf/render/graph.rb
|
|
6
|
+
tags: [server, graph]
|
|
7
|
+
generated:
|
|
8
|
+
by: human:maintainer
|
|
9
|
+
at: 2026-08-13T12:00:00Z
|
|
10
|
+
sources:
|
|
11
|
+
- title: gems/okf/lib/okf/cli/render.rb
|
|
12
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/cli/render.rb
|
|
13
|
+
- title: gems/okf/lib/okf/render/graph.rb
|
|
14
|
+
resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/render/graph.rb
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
# Overview
|
|
18
|
+
|
|
19
|
+
`okf render` writes the [graph page](graph-server.md) as one static,
|
|
20
|
+
self-contained HTML file with the whole bundle baked in — the *same* page
|
|
21
|
+
[`okf server`](graph-server.md) serves live, one switch apart — so the graph
|
|
22
|
+
hosts anywhere there is no server to answer a `fetch()`. GitHub Pages is the
|
|
23
|
+
motivating case: commit the file and the graph is browsable with no process
|
|
24
|
+
running behind it.
|
|
25
|
+
|
|
26
|
+
# One template, two modes
|
|
27
|
+
|
|
28
|
+
There is no second renderer to keep in sync with the server, because there is no
|
|
29
|
+
second renderer. One `OKF::Render::Graph` — the view paired with the pure
|
|
30
|
+
[graph model](../model/graph.md) — draws the page for both modes, and `okf render`
|
|
31
|
+
bakes the bundle in through its `.static`/`.payload`. Every data read the page
|
|
32
|
+
makes — a body, a description, the
|
|
33
|
+
catalog, the §8 map, the §9 logs — flows through a small set of getter functions,
|
|
34
|
+
and an injected `EMBED` constant chooses their source: `null` when served, so the
|
|
35
|
+
getters `fetch()` the live endpoints; the whole payload when rendered, so they
|
|
36
|
+
resolve from the page itself. One interface, two adapters, and the views never
|
|
37
|
+
know which is behind them. The [bundle switcher](graph-server.md) obeys the same
|
|
38
|
+
discipline in reverse — a static file injects an empty sibling list, so the
|
|
39
|
+
affordance that could only dead-end never appears.
|
|
40
|
+
|
|
41
|
+
# It carries both XSS guards
|
|
42
|
+
|
|
43
|
+
Baking the bodies in does not lower the page's defenses. An embedded body takes
|
|
44
|
+
the *inlined* path and the *rendered* one at once: `json_for_script` escapes it at
|
|
45
|
+
inject time so a `</script>` inside a body cannot break out of its `<script>`, and
|
|
46
|
+
it is still run through `DOMPurify.sanitize(marked.parse(...))` before the getter
|
|
47
|
+
hands it to the DOM. The [same trust boundary](../design/server-trust-boundary.md)
|
|
48
|
+
the live server keeps, carried onto the one static path — a rendered file is no
|
|
49
|
+
laxer than the server it was baked from.
|
|
50
|
+
|
|
51
|
+
# Output, flags, and the price
|
|
52
|
+
|
|
53
|
+
`okf render <dir>` prints the page to stdout — `okf render ./docs > public/index.html` —
|
|
54
|
+
or writes `-o FILE` directly, reporting the concept count when it does. A `-o`
|
|
55
|
+
path it cannot write is a usage error (exit `2`), not a backtrace — the same
|
|
56
|
+
[best-effort contract](../cli.md) the read views keep. `--layout NAME` bakes in a
|
|
57
|
+
graph layout, `--map` bakes the [Map view](graph-server.md) as the opening state
|
|
58
|
+
(no arrows, the directories boxed), and `-t`/`-l` set the title and the link the
|
|
59
|
+
page unfurls as a preview card, since a static file cannot be told them later. The price is weight:
|
|
60
|
+
every body is inlined, where a live [`okf server`](graph-server.md) ships a minimal
|
|
61
|
+
graph and pulls bodies on demand — so the server stays the choice for a bundle too
|
|
62
|
+
large to ship whole, and the static file carries no compression of its own
|
|
63
|
+
(whatever host serves it compresses instead). That weight buys something back:
|
|
64
|
+
because every body is already in the page, the static file's
|
|
65
|
+
[full-text search](graph-server.md) reaches **bodies and source text** offline,
|
|
66
|
+
where the live server — bodies still lazy — can only index their metadata.
|