okf-tui 1.0.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 +7 -0
- data/.okf/decisions/index.md +12 -0
- data/.okf/decisions/invents-no-analysis.md +53 -0
- data/.okf/decisions/no-version-ceilings.md +69 -0
- data/.okf/decisions/okf-capability-drift.md +120 -0
- data/.okf/decisions/one-door-the-plugin-seam.md +131 -0
- data/.okf/decisions/registry-write-boundary.md +175 -0
- data/.okf/decisions/ruby-floor.md +59 -0
- data/.okf/decisions/search-facade-coupling.md +146 -0
- data/.okf/decisions/undeclared-width-dependency.md +73 -0
- data/.okf/index.md +28 -0
- data/.okf/interaction/cross-bundle-scope.md +61 -0
- data/.okf/interaction/deferred-search.md +49 -0
- data/.okf/interaction/esc-peels-one-layer.md +70 -0
- data/.okf/interaction/filter-escalates-to-search.md +57 -0
- data/.okf/interaction/following-links.md +82 -0
- data/.okf/interaction/index.md +12 -0
- data/.okf/interaction/key-routing.md +84 -0
- data/.okf/interaction/which-registry.md +85 -0
- data/.okf/log.md +38 -0
- data/.okf/rendering/ansi-aware-width.md +74 -0
- data/.okf/rendering/index.md +8 -0
- data/.okf/rendering/markdown-rendering-trap.md +63 -0
- data/.okf/rendering/status-vocabulary.md +45 -0
- data/.okf/rendering/whole-frame-painting.md +52 -0
- data/.okf/testing/ci-matrix.md +80 -0
- data/.okf/testing/headless-frames.md +74 -0
- data/.okf/testing/index.md +8 -0
- data/.okf/testing/pty-test.md +73 -0
- data/CHANGELOG.md +239 -0
- data/LICENSE.txt +201 -0
- data/NOTICE +10 -0
- data/README.md +194 -0
- data/lib/okf/plugin.rb +63 -0
- data/lib/okf/tui/app.rb +1908 -0
- data/lib/okf/tui/cli.rb +154 -0
- data/lib/okf/tui/model.rb +410 -0
- data/lib/okf/tui/refs.rb +63 -0
- data/lib/okf/tui/ui.rb +308 -0
- data/lib/okf/tui/version.rb +7 -0
- data/lib/okf/tui/views.rb +1648 -0
- data/lib/okf/tui/workspace.rb +527 -0
- data/lib/okf/tui.rb +76 -0
- metadata +229 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: def7a9aa9c4d6fd614896848e6e6712491f062f830d487e260142bfdd8698374
|
|
4
|
+
data.tar.gz: 96e3fb4d8bdbb6c78aef7cf00392372667d89066793bd81feacb6e486e13967a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9520521f0c1ca38385d2ab974b81d1f574923be0ae9f4612c4dd590d66d11a92dbc0e88ac57d81793944134b349df664e802d102609d24d899bb7bf577aeb383
|
|
7
|
+
data.tar.gz: f8f24b77abed3ef919c40ff34d322ff81d006be77f27c2daf1bb1377bc1634d8834eaa80cd4d93ad987f57a13bd08bce499166a01ebb04578cc6874ed05d5223
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Decisions
|
|
2
|
+
|
|
3
|
+
The choices that shape okf-tui, each with the tradeoff it accepted.
|
|
4
|
+
|
|
5
|
+
* [It Invents No Analysis](invents-no-analysis.md) - Every judgement comes from okf; the negative rule this imposes on any feature request, and what it costs.
|
|
6
|
+
* [The Search Facade Coupling](search-facade-coupling.md) - Why search rides Bundle::Search.across, and why that needs a check at boot rather than a rescue.
|
|
7
|
+
* [Ruby 2.4 Floor, Inherited](ruby-floor.md) - The floor taken from okf, the 2.5 API that slipped past RuboCop, and why the linter loads conditionally.
|
|
8
|
+
* [No Version Ceilings on the Markdown Stack](no-version-ceilings.md) - Pinning kramdown and rouge down to protect the floor broke every Ruby above it.
|
|
9
|
+
* [One Door — the Plugin Seam Is the Entry Point](one-door-the-plugin-seam.md) - okf-tui ships no executable and registers as an `okf` command; why one front end rather than two, and why registration must not build anything.
|
|
10
|
+
* [The Undeclared Width Dependency](undeclared-width-dependency.md) - Column measurement rests on a gem that arrives through tty-box; why that is accepted, and what fails loudly if it stops.
|
|
11
|
+
* [okf Moves, and This Breaks Quietly](okf-capability-drift.md) - Inventing no analysis means okf's evolution is this program's; every drift found so far failed silently, and agreement tests are the only defence that works.
|
|
12
|
+
* [It Writes the Registry, Never a Bundle](registry-write-boundary.md) - The line under the TUI's side effects is the registry versus the knowledge — what that admits, and why `registry init` falls outside it.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: It Invents No Analysis
|
|
4
|
+
description: Every judgement on screen is a pure call into okf; the TUI adds no check, score, or verdict of its own, which bounds what a feature request may be answered with.
|
|
5
|
+
tags: [okf-coupling, dependencies]
|
|
6
|
+
timestamp: 2026-07-19
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
`OKF::Bundle::Reader` and `OKF::Registry` are the only parts that touch disk, and
|
|
12
|
+
everything drawn is a pure call on the resulting in-memory bundles — `catalog`,
|
|
13
|
+
`graph`, `validate`, `lint`, `Bundle::Search`. The TUI is one more shell over the
|
|
14
|
+
same core the `okf` CLI and the graph server already use.
|
|
15
|
+
|
|
16
|
+
The rule this actually imposes is a *negative* one, and it is the reason to write
|
|
17
|
+
it down: **a question the TUI cannot answer by asking okf is a question it does
|
|
18
|
+
not answer.** No extra lint check, no derived score, no "helpful" verdict
|
|
19
|
+
computed locally. If a screen wants an analysis okf does not expose, the change
|
|
20
|
+
belongs in okf.
|
|
21
|
+
|
|
22
|
+
# What that keeps intact
|
|
23
|
+
|
|
24
|
+
okf's own contracts survive the port to a screen, rather than being softened for
|
|
25
|
+
presentation:
|
|
26
|
+
|
|
27
|
+
| okf contract | How the screen keeps it |
|
|
28
|
+
|--------------|------------------------|
|
|
29
|
+
| `validate` and `lint` answer different questions | separate sections in the health view, never merged into one score |
|
|
30
|
+
| reads are best-effort | an unreadable file reports as `⊘ n unreadable`, not fatal |
|
|
31
|
+
| exit codes mean something | a bad directory exits `2`, the usage-error code |
|
|
32
|
+
|
|
33
|
+
The one place the TUI *does* add vocabulary is presentational: the clean /
|
|
34
|
+
warnings / not-conformant verdict that colours the bundle name everywhere it
|
|
35
|
+
appears. That is a rendering of `validate` + `lint` output, not a fourth
|
|
36
|
+
judgement — see [status-vocabulary](/rendering/status-vocabulary.md).
|
|
37
|
+
|
|
38
|
+
# The cost
|
|
39
|
+
|
|
40
|
+
It makes okf-tui's release calendar depend on okf's. The search view demonstrated
|
|
41
|
+
the sharp edge: [search-facade-coupling](/decisions/search-facade-coupling.md)
|
|
42
|
+
was blocked entirely on an okf release, with no local workaround *by
|
|
43
|
+
construction* — implementing a fallback search here would have broken this rule.
|
|
44
|
+
|
|
45
|
+
That one has since resolved (okf 1.9.0 shipped the facade), which is the point
|
|
46
|
+
rather than a footnote: the cost of this rule is paid in **waiting**, not in
|
|
47
|
+
capability. The wait ended, and nothing had to be built twice.
|
|
48
|
+
|
|
49
|
+
# Citations
|
|
50
|
+
|
|
51
|
+
[1] `lib/okf/tui.rb` — the module comment stating the split.
|
|
52
|
+
[2] `lib/okf/tui/model.rb`, `lib/okf/tui/workspace.rb` — every analysis is a
|
|
53
|
+
memoized call into okf.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: No Version Ceilings on the Markdown Stack
|
|
4
|
+
description: Pinning kramdown and rouge to their old lines to protect the Ruby 2.4 floor broke modern Ruby instead; the gems declare their own floors, so resolution handles it per-Ruby.
|
|
5
|
+
tags: [ruby-floor, dependencies]
|
|
6
|
+
timestamp: 2026-07-18
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
`tty-markdown` pulls in `kramdown` and `rouge`. Both have dropped Ruby 2.4 in
|
|
12
|
+
their current lines, so the first instinct was to pin everyone to the last
|
|
13
|
+
versions that still ran on the floor:
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
spec.add_dependency "kramdown", ">= 2.3", "< 2.5" # wrong
|
|
17
|
+
spec.add_dependency "rouge", ">= 3.14", "< 4.0" # wrong
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is backwards, and it broke the *modern* end of the matrix: kramdown 2.4
|
|
21
|
+
calls a `CGI` method that no longer exists on Ruby 4.0, so every job above the
|
|
22
|
+
floor died with `NoMethodError: undefined method 'parse' for class CGI`.
|
|
23
|
+
|
|
24
|
+
# The rule
|
|
25
|
+
|
|
26
|
+
**Do not pin a dependency down to protect an old Ruby.** A ceiling is a claim
|
|
27
|
+
about every Ruby, made to serve one of them.
|
|
28
|
+
|
|
29
|
+
The gems already carry `required_ruby_version`, and Bundler honours it: on 2.4 it
|
|
30
|
+
resolves kramdown 2.4.0 and rouge 3.30.0 by itself, on 4.0 it resolves the
|
|
31
|
+
current lines. Declaring only floors lets each Ruby get the newest version that
|
|
32
|
+
actually runs there — which is more than the ceiling would have allowed, and
|
|
33
|
+
correct on every Ruby rather than one.
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
spec.add_dependency "kramdown", ">= 2.3"
|
|
37
|
+
spec.add_dependency "rouge", ">= 3.14"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is the same reasoning that gates the *development* tooling instead of
|
|
41
|
+
pinning it — see [ruby-floor](/decisions/ruby-floor.md), where RuboCop and irb
|
|
42
|
+
load only on 2.7+ because they cannot install on the floor at all.
|
|
43
|
+
|
|
44
|
+
# The one ceiling, and what earns it
|
|
45
|
+
|
|
46
|
+
`okf` carries `< 3`, and it is not a hedge against a Ruby — it is the exception
|
|
47
|
+
that shows what the rule is actually about. The rule refuses a ceiling *made to
|
|
48
|
+
serve one Ruby*, because `required_ruby_version` already expresses that and
|
|
49
|
+
better. It does not refuse a ceiling that expresses a real incompatibility
|
|
50
|
+
resolution cannot see.
|
|
51
|
+
|
|
52
|
+
This one does, and the evidence is this gem's own scar tissue: an okf *major* is
|
|
53
|
+
where the renames come from, and those renames break screens
|
|
54
|
+
[silently](/decisions/okf-capability-drift.md) — `area` became `top_dir` and
|
|
55
|
+
every bundle reported one directory; `timestamp` became `generated_at` and the
|
|
56
|
+
"updated" row stopped rendering. Neither raised. Neither turned a suite red.
|
|
57
|
+
|
|
58
|
+
So `< 3` buys the one thing a floor cannot: it turns the *next* one into a
|
|
59
|
+
resolution failure a maintainer reads, instead of a wrong number a reader
|
|
60
|
+
believes. `test/unit/gemspec_test.rb` pins both bounds — the floor may never lag
|
|
61
|
+
the kernel this gem develops against, and the ceiling may not quietly go away.
|
|
62
|
+
|
|
63
|
+
# Citations
|
|
64
|
+
|
|
65
|
+
[1] `okf-tui.gemspec` — the two floors, with the comment recording the failed
|
|
66
|
+
ceiling attempt.
|
|
67
|
+
[2] Verified in `ruby:2.4` (Docker): resolved kramdown 2.4.0 + rouge 3.30.0,
|
|
68
|
+
34 runs / 6622 assertions green, and green across
|
|
69
|
+
[the whole matrix](/testing/ci-matrix.md) up to 4.0.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: okf Moves, and This Breaks Quietly
|
|
4
|
+
description: Inventing no analysis means every answer on screen is okf's — so okf renaming a field or adding a faster path is a change to this program, and every such drift found so far failed silently rather than loudly.
|
|
5
|
+
tags: [okf-coupling, dependencies, testing, maintenance]
|
|
6
|
+
timestamp: 2026-08-13
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
[invents-no-analysis](/decisions/invents-no-analysis.md) says every judgement on
|
|
12
|
+
screen comes from okf. The cost of that rule is not the one it looks like. It is
|
|
13
|
+
not that features are hard to add; it is that **okf's evolution is this program's
|
|
14
|
+
evolution**, and the failures arrive without a stack trace.
|
|
15
|
+
|
|
16
|
+
This file records the shape of that drift, found by catching up four okf minor
|
|
17
|
+
versions at once (1.9 → 1.13) and discovering that the TUI had been wrong in
|
|
18
|
+
production for a release without anything going red.
|
|
19
|
+
|
|
20
|
+
# Every drift found so far was silent
|
|
21
|
+
|
|
22
|
+
Four, and not one of them raised:
|
|
23
|
+
|
|
24
|
+
| What okf did | How it presented here |
|
|
25
|
+
|---|---|
|
|
26
|
+
| renamed the catalog's `area` to `top_dir` (1.12.0) | `rows.map { row[:area] }` returned `[nil]`, so **every** bundle reported `1 areas` — a wrong number that looks like a right one |
|
|
27
|
+
| added `Search.prepare`/`with` (1.11.0) | nothing broke; the TUI simply kept rebuilding a full-text index per query, 392 ms where 12 ms was available |
|
|
28
|
+
| added registry groups (1.12.0) | `registry.listing` returns bundles only, so groups were absent from the one view whose job is the registry |
|
|
29
|
+
| added local-registry discovery (1.12.0) | `Registry.load` without `cwd:` still worked — it just answered about a *different registry* than every other okf verb in the same directory |
|
|
30
|
+
|
|
31
|
+
Two patterns, and the second is the dangerous one:
|
|
32
|
+
|
|
33
|
+
- **A renamed field reads as nil.** Ruby hands back `nil` for a missing hash key,
|
|
34
|
+
so a derived-field rename cannot fail loudly. It becomes a plausible number.
|
|
35
|
+
- **A new capability is invisible by construction.** Nothing is broken when okf
|
|
36
|
+
adds a faster path or a new concept; the TUI is merely *less* than it was
|
|
37
|
+
written to be. No test can fail for a feature nobody has written yet, which
|
|
38
|
+
means the only defence is reading okf's changelog on purpose.
|
|
39
|
+
|
|
40
|
+
The renames are why the floor is now stated per capability in the gemspec, one
|
|
41
|
+
line each, naming the version and what fails without it. The additions are why
|
|
42
|
+
catching up is a periodic *task*, not an event that gets triggered.
|
|
43
|
+
|
|
44
|
+
# The defence that works: agreement, not assertion
|
|
45
|
+
|
|
46
|
+
The instinct after the `area` break is to assert the number: "this fixture has six
|
|
47
|
+
directories". That catches a regression in this code and nothing about drift — okf
|
|
48
|
+
could change what counts as a directory tomorrow and the test would keep passing
|
|
49
|
+
while the screen went wrong.
|
|
50
|
+
|
|
51
|
+
What works is asking okf the same question and comparing:
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# dirs_test.rb — the dir facet, against okf's own subtree column
|
|
55
|
+
OKF::CLI.start([ "dirs", fixture(name), "--json" ], out: out, err: StringIO.new)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`structure_test.rb` does the same against `okf graph --traffic`, parsing its
|
|
59
|
+
**human** table and matching the rendered frame row for row. Both run in-process,
|
|
60
|
+
so they cost milliseconds, and both fail the moment okf and this disagree about a
|
|
61
|
+
number — which is the actual contract, and the one an asserted constant does not
|
|
62
|
+
express.
|
|
63
|
+
|
|
64
|
+
Where a formula had to be reproduced (cohesion is `internal / (internal + out +
|
|
65
|
+
in)`, okf's arithmetic over `Bundle::Skeleton`), the agreement test is what makes
|
|
66
|
+
the copy safe. A comment saying "same as okf's" is a claim; the test is the check.
|
|
67
|
+
|
|
68
|
+
# Prefer inheriting a rule to copying it
|
|
69
|
+
|
|
70
|
+
Two couplings arrived in this pass, and they were resolved differently on purpose:
|
|
71
|
+
|
|
72
|
+
**The ref grammar is inherited.** `OKF::TUI::Refs` subclasses `OKF::CLI::Command`
|
|
73
|
+
to reach `resolve_ref_expanding`, okf's own resolver — so `@slug`, bare `@`,
|
|
74
|
+
`@group` fan-out, the vanished-member note and the `@all` refusal all behave
|
|
75
|
+
identically to `okf server`, messages and exit codes included. That is a coupling
|
|
76
|
+
to a **private** method, accepted because the alternative is a second copy of a
|
|
77
|
+
grammar with five branches and its own error strings, which is precisely the
|
|
78
|
+
"second argument grammar" [one-door-the-plugin-seam](/decisions/one-door-the-plugin-seam.md)
|
|
79
|
+
exists to prevent. `refs_test.rb` pins the seam by name, so okf moving it fails
|
|
80
|
+
loudly rather than quietly restoring `not a directory`.
|
|
81
|
+
|
|
82
|
+
**The `--dir` matching rule is reimplemented**, in one line
|
|
83
|
+
(`Model.under_dir?`), and that is not inconsistent. okf's flag handling is mostly
|
|
84
|
+
about *arguments a human typed* — the `root` alias, a trailing slash, case folding
|
|
85
|
+
— and none of it applies to a facet whose value the TUI got out of
|
|
86
|
+
`Bundle#directories` and whose rows carry `OKF.dir_of`. Both sides are already
|
|
87
|
+
okf's canonical spellings, so what is left is the set relation between two values
|
|
88
|
+
okf handed over. The agreement test is what holds it to okf's answer.
|
|
89
|
+
|
|
90
|
+
The rule of thumb: **inherit a grammar, reimplement a predicate, and test
|
|
91
|
+
agreement either way.**
|
|
92
|
+
|
|
93
|
+
# What this costs, and why it is still right
|
|
94
|
+
|
|
95
|
+
The bill is real: a periodic read of okf's changelog, a floor that has to be
|
|
96
|
+
raised deliberately, and a test suite that runs okf's CLI in-process to check its
|
|
97
|
+
own arithmetic. None of that would exist if the TUI parsed markdown itself.
|
|
98
|
+
|
|
99
|
+
It is still right, and the four drifts above are the argument. Every one of them
|
|
100
|
+
was a *number this program did not have to compute*, and three of the four were
|
|
101
|
+
okf getting better at answering it. A TUI with its own catalog would not have had
|
|
102
|
+
the `area` break — it would have had its own, permanently, with no upstream to
|
|
103
|
+
inherit the fix from.
|
|
104
|
+
|
|
105
|
+
# Citations
|
|
106
|
+
|
|
107
|
+
[1] `okf-tui.gemspec` — the seven capabilities and the version each shipped in.
|
|
108
|
+
[2] `test/integration/dirs_test.rb` — `okf_subtree_counts`, the `okf dirs --json`
|
|
109
|
+
oracle; and the `nested` fixture, which exists because at one directory level
|
|
110
|
+
`dir` and `top_dir` are the same string and an assertion cannot tell them apart.
|
|
111
|
+
[3] `test/integration/structure_test.rb` — `okf_traffic_rows`, parsing okf's human
|
|
112
|
+
traffic table; the row count is asserted first so the comparison loop cannot
|
|
113
|
+
pass vacuously.
|
|
114
|
+
[4] `lib/okf/tui/refs.rb` — the inherited grammar, and why it subclasses Command
|
|
115
|
+
without ever being registered.
|
|
116
|
+
[5] okf `CHANGELOG.md`, 1.12.0 "Changed": "The derived `area` field is renamed
|
|
117
|
+
`top_dir`" — "`area` was never the OKF spec's word (`grep -ci area SPEC.md` → 0)".
|
|
118
|
+
[6] Measured 2026-08-13 over five registered bundles / 129 concepts: first query
|
|
119
|
+
391.8 ms, then 16.2 / 13.0 / 12.0 / 14.4 ms. Before, every query paid the first
|
|
120
|
+
figure.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: One door — the plugin seam is the entry point
|
|
4
|
+
description: okf-tui ships no executable; `okf tui` is the only way in, and the registration stays cheap enough that a run wanting `okf lint` pays nothing for it.
|
|
5
|
+
tags: [okf-coupling, dependencies, cli]
|
|
6
|
+
timestamp: 2026-07-19T18:00:00Z
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
okf's CLI dispatches through a registry and finds extensions by convention: any
|
|
12
|
+
gem with `okf/plugin.rb` on its load path can add a verb. This gem ships one, and
|
|
13
|
+
that file is its **only** entry point: there is no `exe/`, and `okf help` lists
|
|
14
|
+
`tui` under `installed extensions:`. It is the second registry this gem plugs
|
|
15
|
+
into, after
|
|
16
|
+
[the search facade](search-facade-coupling.md) — and unlike that one it carries
|
|
17
|
+
no unreleased-API risk, because a plugin file an old okf never discovers is
|
|
18
|
+
simply inert.
|
|
19
|
+
|
|
20
|
+
Installing the gem is the whole installation. There is no config file, no
|
|
21
|
+
`okf plugin add`, and nothing to edit in okf — which is the point of a convention
|
|
22
|
+
over a list. See okf's
|
|
23
|
+
[extension points](https://github.com/serradura/okf-gem/blob/main/.okf/design/extension-points.md).
|
|
24
|
+
|
|
25
|
+
# One front end, because two would drift
|
|
26
|
+
|
|
27
|
+
There was an `exe/okf-tui`. It did nothing but call the same `CLI.run` the
|
|
28
|
+
plugin's `#call` calls, and it went before the first release, while removing a
|
|
29
|
+
name still cost nobody anything. okf-mcp had already made the same call for the
|
|
30
|
+
same reason, and its gemspec records it in one line: a second binary that only
|
|
31
|
+
aliases a verb is one more name to install, document and keep working.
|
|
32
|
+
|
|
33
|
+
The cost of keeping it was not the file. It was that **two front ends are two
|
|
34
|
+
argument grammars, and the drift between them is invisible** — each one passes
|
|
35
|
+
its own tests while they disagree, because nothing compares them. That is not
|
|
36
|
+
hypothetical here: `--home` had to be dropped precisely because the same tool
|
|
37
|
+
answered differently depending on which door you came through (see below). The
|
|
38
|
+
door that survives is the one a user is told about in `okf help`.
|
|
39
|
+
|
|
40
|
+
What the seam buys instead is discoverability. Somebody who installed okf-tui
|
|
41
|
+
finds it in okf's own map without having to learn that a second command exists.
|
|
42
|
+
|
|
43
|
+
The obligation that replaces the two-door test: **the adapter must carry argv
|
|
44
|
+
and the streams and add nothing else.** `plugin_test.rb` drives the same run
|
|
45
|
+
through `OKF::CLI.start` and straight into `OKF::TUI::CLI.run`, and compares the
|
|
46
|
+
exit code and the message — driven in the same
|
|
47
|
+
[headless, stream-injected](../testing/headless-frames.md) style every other
|
|
48
|
+
surface here is proven in. `terminal_test.rb` covers what neither of those can,
|
|
49
|
+
being in-process: it spawns okf's executable on a real pty, so process boot and
|
|
50
|
+
plugin *discovery* are on the path too.
|
|
51
|
+
|
|
52
|
+
# Registration must not build anything
|
|
53
|
+
|
|
54
|
+
okf reads `okf/plugin.rb` whenever a verb misses or `okf help` runs. That file
|
|
55
|
+
therefore cannot be expensive, and this gem's library is: six TTY gems plus
|
|
56
|
+
kramdown and rouge. Somebody typing `okf lint` should not pay for a terminal
|
|
57
|
+
toolkit they are not going to see.
|
|
58
|
+
|
|
59
|
+
So the plugin file registers a class and nothing else; `require "okf/tui/cli"`
|
|
60
|
+
happens inside `#call`. A subprocess test asserts that loading the plugin leaves
|
|
61
|
+
`TTY::Box` undefined — in-process it would assert nothing, since the suite has
|
|
62
|
+
long since loaded it.
|
|
63
|
+
|
|
64
|
+
This is the same shape as okf's own laziness one level up: discovery itself only
|
|
65
|
+
runs for an unknown verb or for `help`, so a built-in verb never even reads this
|
|
66
|
+
file.
|
|
67
|
+
|
|
68
|
+
# `input:` is why the base command carries a terminal
|
|
69
|
+
|
|
70
|
+
Every verb okf ships is a one-shot read that never looks at stdin. This one
|
|
71
|
+
cannot work without it — a full-screen UI with no terminal has nothing to drive —
|
|
72
|
+
so `Command` carries an `input:` alongside `out:`/`err:`, and the TUI takes it
|
|
73
|
+
from there rather than reaching for `$stdin`.
|
|
74
|
+
|
|
75
|
+
That is not ceremony. Reaching past the injected streams would put the command
|
|
76
|
+
outside the stream discipline
|
|
77
|
+
[the whole suite depends on](../testing/headless-frames.md), and it would make
|
|
78
|
+
"it refuses to run without a terminal" unassertable through `okf tui` — the
|
|
79
|
+
refusal only works because the stream okf handed down is the one the TUI checks.
|
|
80
|
+
|
|
81
|
+
# It could not ship before okf did, and that debt is paid
|
|
82
|
+
|
|
83
|
+
The seam was once newer than any okf release, so `okf tui` did not work against
|
|
84
|
+
the published gem and this suite would have been asserting against a CLI with no
|
|
85
|
+
`register` at all — the same shape as
|
|
86
|
+
[the search facade](search-facade-coupling.md). It was handled the way that one
|
|
87
|
+
taught rather than discovered again: the plugin tests **skipped, named**, on an
|
|
88
|
+
okf without the registry, and the floor stayed honest rather than guessing at a
|
|
89
|
+
version that did not exist yet — guessing one is how the last dependency fix
|
|
90
|
+
[became the next bug](../testing/ci-matrix.md).
|
|
91
|
+
|
|
92
|
+
Both debts are settled. `OKF::CLI.register` shipped in okf 1.10.0, the floor now
|
|
93
|
+
names `okf >= 2.0, < 3`, and the skip is gone — deleted on its own instruction,
|
|
94
|
+
because a skip left in place after the reason for it is gone is a suite that
|
|
95
|
+
quietly tests nothing.
|
|
96
|
+
|
|
97
|
+
One piece of that arrangement stays, and it should: `lib/okf/plugin.rb` raises a
|
|
98
|
+
named `LoadError` rather than a bare `NameError` if it is ever required against
|
|
99
|
+
an okf without the registry. The floor makes that unreachable through
|
|
100
|
+
resolution, and unreachable is not the same as impossible — a second okf ahead
|
|
101
|
+
of the intended one on the load path is exactly how it would happen.
|
|
102
|
+
|
|
103
|
+
# What it cost: the `--home` flag
|
|
104
|
+
|
|
105
|
+
`okf-tui --home DIR` is gone. okf had dropped its own `--home` before 1.8 for a
|
|
106
|
+
reason it wrote down — a flag whose whole job is to name a location `$OKF_HOME`
|
|
107
|
+
already names has to be remembered on the verbs that offer it and forgotten on
|
|
108
|
+
the ones that do not — and this gem kept it anyway.
|
|
109
|
+
|
|
110
|
+
That was survivable while `okf-tui` was its own binary. It stopped being
|
|
111
|
+
survivable the moment `okf tui` existed, because then the same tool had two
|
|
112
|
+
grammars depending on which door you came through. The flag went first and the
|
|
113
|
+
binary followed, which is the order the lesson actually arrived in: the drift was
|
|
114
|
+
visible before its cause was.
|
|
115
|
+
|
|
116
|
+
The library keyword is untouched: `App` and `Workspace` still take `home:`, which
|
|
117
|
+
is how an embedding app and the suite name a registry without mutating a
|
|
118
|
+
process-global. That is exactly the split okf itself draws — the env var is the
|
|
119
|
+
CLI's only lever, the keyword is the library's.
|
|
120
|
+
|
|
121
|
+
# Citations
|
|
122
|
+
|
|
123
|
+
[1] `lib/okf/plugin.rb` — the whole seam, including why the heavy require is in `#call`.
|
|
124
|
+
[2] `okf-tui.gemspec` — the comment recording why there is no `spec.executables`.
|
|
125
|
+
[3] `test/integration/plugin_test.rb` — the dispatcher adding nothing but the
|
|
126
|
+
streams, and the subprocess check that registering does not load tty-box.
|
|
127
|
+
[4] `test/integration/terminal_test.rb` — okf's own executable on a real pty, the
|
|
128
|
+
one test that walks process boot and plugin discovery rather than the
|
|
129
|
+
in-process dispatcher.
|
|
130
|
+
[5] Verified 2026-07-19: `okf --help` lists `tui` under `installed extensions:`;
|
|
131
|
+
`okf validate` still runs without triggering discovery at all.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: It Writes the Registry, Never a Bundle
|
|
4
|
+
description: The line the TUI's side effects sit on is not read-versus-write — it is the registry versus the knowledge; what that admits, what it excludes, and why `registry init` is on the far side of it.
|
|
5
|
+
tags: [registry, boundary, side-effects]
|
|
6
|
+
timestamp: 2026-08-13
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
The TUI has side effects, and more of them than a reader expects from something
|
|
12
|
+
described as a viewer. It registers bundles, removes them, renames slugs, moves
|
|
13
|
+
the default. The question that keeps coming up — *should it also create groups?
|
|
14
|
+
rename them? do everything the CLI does?* — cannot be answered by asking whether
|
|
15
|
+
a feature writes to disk, because the answer is already yes.
|
|
16
|
+
|
|
17
|
+
The line is **the registry, not read-versus-write**: this program edits the user's
|
|
18
|
+
*configuration* freely and never touches the *knowledge*.
|
|
19
|
+
|
|
20
|
+
# What that admits
|
|
21
|
+
|
|
22
|
+
Seven of okf's eight `registry` verbs have a place here, and it is the same place —
|
|
23
|
+
the bundles view, where `Workspace` owns every write and each is followed by a
|
|
24
|
+
reload, per `AGENTS.md` constraint 8:
|
|
25
|
+
|
|
26
|
+
| Key | `Workspace` | okf CLI |
|
|
27
|
+
|-----|-------------|---------|
|
|
28
|
+
| `a` | `add` | `okf registry set <dir>` |
|
|
29
|
+
| `x` | `remove` | `okf registry del <@slug>` — spans a group |
|
|
30
|
+
| `d` | `make_default` | `okf registry default <@slug>` |
|
|
31
|
+
| `n` | `rename` | `okf registry rename <@slug> <new>` — spans a group |
|
|
32
|
+
| `c` | `create_group` | `okf registry group <slug> <@member…>` |
|
|
33
|
+
| `+` | `add_to_group` | `okf registry group <slug> <@member…>` |
|
|
34
|
+
| `-` | `remove_from_group` | `okf registry ungroup <slug> <@member…>` |
|
|
35
|
+
| — | the view itself | `okf registry list` |
|
|
36
|
+
|
|
37
|
+
Group create/edit was the question that forced this file to be written, and it
|
|
38
|
+
falls on the admitted side: a group is registry configuration in exactly the sense
|
|
39
|
+
a slug rename is. Stopping at read-only would have put a `@mkt` row on a screen
|
|
40
|
+
where `x` already deletes a bundle, and refused to let you add a member to it —
|
|
41
|
+
not restraint, just an arbitrary stopping point.
|
|
42
|
+
|
|
43
|
+
**The members come from the scope, not from a typed list.** `◉` already means
|
|
44
|
+
"these bundles" in this view, so the gesture is toggle what you want and then name
|
|
45
|
+
it (`c`), or put the cursor on a group and push the scope into it (`+`) or out of
|
|
46
|
+
it (`-`). That is a gesture the CLI cannot offer, and it is why these keys are
|
|
47
|
+
worth having rather than being a second spelling of `okf registry group`.
|
|
48
|
+
|
|
49
|
+
okf owns every cascade. `set_group` creates-or-adds and refuses a cycle,
|
|
50
|
+
`unset_group_members` deletes a group it empties, and `rename`/`del` span a group
|
|
51
|
+
slug and cascade through every member list that names it. None of that is
|
|
52
|
+
reimplemented; `groups_test.rb` asserts the *effects* — that renaming `@docs`
|
|
53
|
+
leaves `@everything` naming `@papers` — precisely so the cascades stay okf's.
|
|
54
|
+
|
|
55
|
+
# What it excludes
|
|
56
|
+
|
|
57
|
+
Nothing in `lib/` writes a bundle. There is no `OKF::Bundle::Writer` reference in
|
|
58
|
+
this gem, and that is the invariant worth grepping for: no concept editing, no
|
|
59
|
+
"fix this lint warning for me", no touching a `log.md`. Authoring is okf's CLI and
|
|
60
|
+
its companion skill, which have the vocabulary and the review path for it.
|
|
61
|
+
|
|
62
|
+
The reason is not timidity. A registry is small, local, reversible and *about the
|
|
63
|
+
session* — a list of references, which is why removing a bundle famously does not
|
|
64
|
+
delete it. A bundle is the knowledge itself, and a full-screen keyboard UI is a
|
|
65
|
+
poor place to make an irreversible edit to it by muscle memory.
|
|
66
|
+
|
|
67
|
+
# `registry init` is excluded, for a different reason
|
|
68
|
+
|
|
69
|
+
okf 1.12.0 added `okf registry init`, which creates a project-local
|
|
70
|
+
`.okf-registry.json`. It is registry configuration, so the line above admits it —
|
|
71
|
+
and it is still not offered, on mechanics rather than principle.
|
|
72
|
+
|
|
73
|
+
The registry is resolved **once, at boot**: `Workspace#load_entries` opens it, and
|
|
74
|
+
every reload after that goes through `Registry#reopen` on the same file. Creating
|
|
75
|
+
a *new* registry mid-session is not an edit to the thing the session is on, it is
|
|
76
|
+
a **re-anchoring** — after it, the file every other okf verb in that directory
|
|
77
|
+
would now resolve is one this session has never read. Either the key appears to do
|
|
78
|
+
nothing, or the entire workspace silently swaps out from under every open view,
|
|
79
|
+
including the active bundle the other five views are about.
|
|
80
|
+
|
|
81
|
+
Both outcomes are worse than not having the key. The CLI is the right door, and
|
|
82
|
+
once the file exists the TUI discovers it on the next launch — which is
|
|
83
|
+
`Workspace.new(cwd:)`, and the fix recorded in
|
|
84
|
+
[which-registry](/interaction/which-registry.md).
|
|
85
|
+
|
|
86
|
+
# The removal key, got wrong twice
|
|
87
|
+
|
|
88
|
+
The first cut put removal on `-` at the group row, acting on the bundles the scope
|
|
89
|
+
and the member list had in common. It shipped, and the report came back at once:
|
|
90
|
+
someone looking for how to *turn the scope off* pressed `-`, and it deleted a group.
|
|
91
|
+
|
|
92
|
+
Three things lined up, and the missing confirmation was only one of them:
|
|
93
|
+
|
|
94
|
+
- **`-` reads as "remove", and the nearest removable thing was the scope.** Where
|
|
95
|
+
`◉` means "a search covers this", a key marked `-` is a scope key until proven
|
|
96
|
+
otherwise.
|
|
97
|
+
- **The footer had dropped the scope keys on a group row** and labelled the member
|
|
98
|
+
keys `+/- add/remove scoped` — so the one screen someone stands on while wanting
|
|
99
|
+
to stop searching everything offered no `A`/`N`, and did offer a `-` described
|
|
100
|
+
with the word *scoped*. Close to an instruction.
|
|
101
|
+
- **okf deletes a group whose last member leaves.** Right for a CLI where the
|
|
102
|
+
command names its members explicitly; here it meant one keystroke could destroy
|
|
103
|
+
the group rather than trim it.
|
|
104
|
+
|
|
105
|
+
Adding a confirmation fixed the damage and left the real fault in place, which the
|
|
106
|
+
next round of feedback named: `-` acted on **`scope ∩ members`, a set with no row
|
|
107
|
+
on screen**. To predict the key you had to intersect two lists in your head, and
|
|
108
|
+
the review comment was simply "it took me some time to understand how this works".
|
|
109
|
+
|
|
110
|
+
The fix that stuck was structural, and it came from the maintainer rather than from
|
|
111
|
+
me: **give the groups a pane of their own.** The bundles view is three panes now —
|
|
112
|
+
bundles, groups, and the detail of whichever has focus — with `Tab` cycling.
|
|
113
|
+
|
|
114
|
+
That does more than tidy the layout. Two panes hold *two selections at once*, and
|
|
115
|
+
suddenly every editing key has visible rows to name: `+` is the bundle under the
|
|
116
|
+
bundles cursor joining the group selected below, `-` is the member under the members
|
|
117
|
+
cursor. Nothing reads the search scope any more except `c`, where naming the bundles
|
|
118
|
+
you have been searching together is precisely the gesture. The key I had got wrong
|
|
119
|
+
twice stopped being hard the moment the layout gave it something to point at.
|
|
120
|
+
|
|
121
|
+
It also fixed a fault nobody had reported yet: the groups were a heading inside the
|
|
122
|
+
bundle list, and a heading scrolls away. Thirteen registered bundles put them below
|
|
123
|
+
the fold on a short terminal — no way to show a thing you are meant to select. A
|
|
124
|
+
pane is always on screen, and `Tab` is the jump to it.
|
|
125
|
+
|
|
126
|
+
Three rules came out of it, in increasing order of how far they carry:
|
|
127
|
+
|
|
128
|
+
1. A write that can lose configuration confirms, and the question names the
|
|
129
|
+
outcome — `remove @nested — its last, so @docs goes too`, not a bare "remove?".
|
|
130
|
+
2. **Act on a row, not on a set the reader has to compute.** A key whose effect is
|
|
131
|
+
an intersection of two lists cannot be predicted from the screen, and every
|
|
132
|
+
misreading of it is reasonable.
|
|
133
|
+
3. **A hint must not borrow the vocabulary of a different mechanism.** Two
|
|
134
|
+
independent axes live in this view — the active bundle and the scope
|
|
135
|
+
([cross-bundle-scope](/interaction/cross-bundle-scope.md)) — so a registry write
|
|
136
|
+
described in the scope's words will be read as the scope's key.
|
|
137
|
+
|
|
138
|
+
Reuse also mattered: `Tab` already meant "switch pane" in browse and graph, so three
|
|
139
|
+
panes cost a reader nothing new, and `Esc` peels them one at a time before the
|
|
140
|
+
filter, exactly as
|
|
141
|
+
[esc-peels-one-layer](/interaction/esc-peels-one-layer.md) requires. A key that
|
|
142
|
+
belongs to a different pane *says where it lives* rather than going silent — a key
|
|
143
|
+
that quietly stopped working is indistinguishable from a broken one.
|
|
144
|
+
|
|
145
|
+
The last thing, and the smallest, was the one the report actually opened with:
|
|
146
|
+
"it is not clear how to control the scope." It was not. Neither `A` nor `N` appeared
|
|
147
|
+
in the footer on a bundle row, and the footer truncates, so at 80 columns everything
|
|
148
|
+
past `d` was already off screen. The scope is one of this view's two axes
|
|
149
|
+
([cross-bundle-scope](/interaction/cross-bundle-scope.md)); it belongs in the
|
|
150
|
+
visible prefix, ahead of the config keys, and that is where it is now.
|
|
151
|
+
|
|
152
|
+
# The test that keeps it honest
|
|
153
|
+
|
|
154
|
+
Every write test builds its own registry: `with_registry` under a temporary
|
|
155
|
+
`$OKF_HOME`, and `with_local_registry` for the project-local file. Nothing in the
|
|
156
|
+
suite may touch the real `~/.okf` — it is the maintainer's own configuration, and
|
|
157
|
+
this is a suite that exercises `remove`.
|
|
158
|
+
|
|
159
|
+
Two states in `groups_test.rb` are reached by writing the registry JSON directly,
|
|
160
|
+
because okf's own API refuses to produce them: a member naming nothing registered,
|
|
161
|
+
and a cycle (`group cycle: @g would contain itself`). Both are legitimate things
|
|
162
|
+
to survive, since okf invites you to *commit* a local registry — so a hand-edited
|
|
163
|
+
file is a normal input, not an abuse.
|
|
164
|
+
|
|
165
|
+
# Citations
|
|
166
|
+
|
|
167
|
+
[1] `lib/okf/tui/workspace.rb` — the seven writes, each returning a status message
|
|
168
|
+
and reloading; `not_registry_backed` for the ad-hoc case, which has no registry
|
|
169
|
+
to change.
|
|
170
|
+
[2] `AGENTS.md` constraint 8 — the same boundary, stated where a change is judged.
|
|
171
|
+
[3] `grep -rn "Bundle::Writer" lib/` → no matches, 2026-08-13.
|
|
172
|
+
[4] okf `CHANGELOG.md` 1.12.0 — `okf registry init`, and the discovery rule that
|
|
173
|
+
makes the nearest `.okf-registry.json` win.
|
|
174
|
+
[5] `test/test_helper.rb` — `with_registry`, `with_local_registry`, and the
|
|
175
|
+
`$OKF_HOME` save/restore around both.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: Decision
|
|
3
|
+
title: Ruby 2.4 Floor, Inherited
|
|
4
|
+
description: okf-tui takes okf's floor rather than its own, which costs the ergonomic syntax and forces the development tooling to load conditionally.
|
|
5
|
+
tags: [ruby-floor, dependencies]
|
|
6
|
+
timestamp: 2026-07-18
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Overview
|
|
10
|
+
|
|
11
|
+
`required_ruby_version = ">= 2.4.0"`, taken from [okf](@okf), which takes it from
|
|
12
|
+
rack. A UI over a library that runs on the Ruby an OS already ships would be a
|
|
13
|
+
strange thing to require a newer Ruby for.
|
|
14
|
+
|
|
15
|
+
The cost lands in two places that are not obvious until you hit them.
|
|
16
|
+
|
|
17
|
+
# The port had to give up keyword_init
|
|
18
|
+
|
|
19
|
+
The prototype built its one-off structs the modern way:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
Prompt = Struct.new(:kind, :label, :buffer, :subject, :free_text, keyword_init: true)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`keyword_init:` is Ruby **2.5**. RuboCop at `TargetRubyVersion: 2.4` parses this
|
|
26
|
+
happily — it catches *syntax*, not *APIs* — so nothing local complained; it would
|
|
27
|
+
have failed on the floor at runtime. It is now a plain class with an
|
|
28
|
+
`attr_reader` and a `free_text?` predicate.
|
|
29
|
+
|
|
30
|
+
This is the general trap: the floor is a library-API constraint that only two
|
|
31
|
+
things actually prove — reading the version each method arrived in, and running
|
|
32
|
+
the suite on 2.4.
|
|
33
|
+
|
|
34
|
+
# The tooling cannot install on the floor it guards
|
|
35
|
+
|
|
36
|
+
RuboCop needs Ruby ≥ 2.5, so requiring it unconditionally makes `bundle install`
|
|
37
|
+
fail on 2.4 — the tooling that enforces the floor blocks the floor. Both dev gems
|
|
38
|
+
load conditionally, and the Rakefile degrades to test-only when RuboCop is
|
|
39
|
+
absent:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
if RUBY_VERSION >= "2.7"
|
|
43
|
+
gem "irb"
|
|
44
|
+
gem "rubocop", "~> 1.21"
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
So on 2.4 the *suite* is the proof and lint is skipped; on 2.7+ the default task
|
|
49
|
+
runs both. Same split okf uses, and it is why
|
|
50
|
+
[the CI matrix](/testing/ci-matrix.md) — not RuboCop — is what actually holds
|
|
51
|
+
the floor.
|
|
52
|
+
|
|
53
|
+
Nothing is pinned *down* to make this work — see
|
|
54
|
+
[no-version-ceilings](/decisions/no-version-ceilings.md).
|
|
55
|
+
|
|
56
|
+
# Citations
|
|
57
|
+
|
|
58
|
+
[1] `okf-tui.gemspec`, `Gemfile`, `Rakefile`.
|
|
59
|
+
[2] Verified in `ruby:2.4` (Docker) and on CI, which runs 2.4 through 4.0.
|