okf-tui 1.0.0 → 1.1.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.okf/capabilities/index.md +13 -0
  3. data/.okf/capabilities/okf-surface.md +48 -0
  4. data/.okf/capabilities/views.md +48 -0
  5. data/.okf/decisions/invents-no-analysis.md +8 -7
  6. data/.okf/decisions/no-version-ceilings.md +8 -9
  7. data/.okf/decisions/okf-capability-drift.md +16 -18
  8. data/.okf/decisions/one-door-the-plugin-seam.md +15 -14
  9. data/.okf/decisions/registry-write-boundary.md +43 -15
  10. data/.okf/decisions/ruby-floor.md +11 -9
  11. data/.okf/decisions/search-facade-coupling.md +22 -24
  12. data/.okf/decisions/undeclared-width-dependency.md +12 -10
  13. data/.okf/index.md +13 -5
  14. data/.okf/interaction/cross-bundle-scope.md +8 -8
  15. data/.okf/interaction/deferred-search.md +8 -6
  16. data/.okf/interaction/esc-peels-one-layer.md +10 -9
  17. data/.okf/interaction/filter-escalates-to-search.md +12 -11
  18. data/.okf/interaction/following-links.md +10 -8
  19. data/.okf/interaction/key-routing.md +6 -5
  20. data/.okf/interaction/which-registry.md +39 -19
  21. data/.okf/log.md +56 -0
  22. data/.okf/rendering/ansi-aware-width.md +8 -7
  23. data/.okf/rendering/fit-or-say-so.md +34 -0
  24. data/.okf/rendering/index.md +1 -0
  25. data/.okf/rendering/markdown-rendering-trap.md +8 -7
  26. data/.okf/rendering/status-vocabulary.md +6 -5
  27. data/.okf/rendering/whole-frame-painting.md +8 -6
  28. data/.okf/structure/doors.md +61 -0
  29. data/.okf/structure/index.md +16 -0
  30. data/.okf/structure/rendering.md +58 -0
  31. data/.okf/structure/the-app.md +55 -0
  32. data/.okf/structure/the-workspace.md +69 -0
  33. data/.okf/testing/adding-a-view.md +58 -0
  34. data/.okf/testing/ci-matrix.md +8 -6
  35. data/.okf/testing/headless-frames.md +6 -6
  36. data/.okf/testing/index.md +2 -0
  37. data/.okf/testing/pty-test.md +6 -5
  38. data/.okf/testing/the-suite.md +120 -0
  39. data/CHANGELOG.md +95 -1
  40. data/README.md +12 -3
  41. data/lib/okf/tui/app.rb +28 -7
  42. data/lib/okf/tui/cli.rb +2 -2
  43. data/lib/okf/tui/refs.rb +1 -1
  44. data/lib/okf/tui/version.rb +1 -1
  45. data/lib/okf/tui/views.rb +15 -0
  46. data/lib/okf/tui/workspace.rb +34 -9
  47. metadata +18 -7
@@ -3,7 +3,18 @@ type: Concept
3
3
  title: A Dead Filter Offers the Wider Search
4
4
  description: Filtering reads metadata in one bundle and searching reads bodies across all of them, so a filter that matches nothing offers the search rather than leaving a dead end.
5
5
  tags: [ux, search]
6
- timestamp: 2026-08-13
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-13
9
+ sources:
10
+ - title: "`lib/okf/tui/views.rb` — `escalation_panel`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/views.rb
12
+ - title: "`lib/okf/tui/app.rb` — `filter_found_nothing?`, which is where the two views' conditions live side by side."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
14
+ - title: "`test/integration/search_test.rb` — \"a filter that matches nothing offers the wider search\"."
15
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/search_test.rb
16
+ - title: "`test/integration/groups_test.rb` — \"a filter matching a group but no bundle has found something\"."
17
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/groups_test.rb
7
18
  ---
8
19
 
9
20
  # Overview
@@ -45,13 +56,3 @@ group and no bundle has found something, and the first cut of this read only the
45
56
  bundles pane — so `Enter` escalated on the keystroke that was accepting the
46
57
  filter, taking the filter, the view and the group the reader was pointing at with
47
58
  it. "The filter found nothing" is a claim about the whole view.
48
-
49
- # Citations
50
-
51
- [1] `lib/okf/tui/views.rb` — `escalation_panel`.
52
- [2] `lib/okf/tui/app.rb` — `filter_found_nothing?`, which is where the two views'
53
- conditions live side by side.
54
- [3] `test/integration/search_test.rb` — "a filter that matches nothing offers
55
- the wider search".
56
- [4] `test/integration/groups_test.rb` — "a filter matching a group but no bundle
57
- has found something".
@@ -3,7 +3,16 @@ type: Decision
3
3
  title: Following a Link Out of the Page
4
4
  description: The picker is a mode rather than inline hints, the directory link okf declines to resolve, and the count it deliberately disagrees with.
5
5
  tags: [ux, keys, okf-coupling]
6
- timestamp: 2026-07-19
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-19
9
+ sources:
10
+ - title: "`lib/okf/tui/model.rb` — `links_for`, `resolve_target`, `describe_link`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/model.rb
12
+ - title: "`lib/okf/tui/app.rb` — `handle_follow`, `follow_selected`, `push_trail`, `back`."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
14
+ - title: "`test/integration/links_test.rb` — the area-link case is the one that would otherwise have been silently empty."
15
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/links_test.rb
7
16
  ---
8
17
 
9
18
  # Overview
@@ -73,10 +82,3 @@ label rather than being reconciled to the header.
73
82
  two functions every jump already went through. Opening a search hit and following
74
83
  a concept out of the graph became reversible without either being touched, which
75
84
  is why the stack lives there rather than in the picker.
76
-
77
- # Citations
78
-
79
- [1] `lib/okf/tui/model.rb` — `links_for`, `resolve_target`, `describe_link`.
80
- [2] `lib/okf/tui/app.rb` — `handle_follow`, `follow_selected`, `push_trail`, `back`.
81
- [3] `test/integration/links_test.rb` — the area-link case is the one that would
82
- otherwise have been silently empty.
@@ -3,7 +3,12 @@ type: Reference
3
3
  title: Key Routing and Its Modes
4
4
  description: handle dispatches through modes before the global keys, which is what keeps digits navigating everywhere, and the guard-fallback trap that a Ruby case statement sets for shared letters.
5
5
  tags: [ux, keys]
6
- timestamp: 2026-07-18
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-18
9
+ sources:
10
+ - title: "`lib/okf/tui/app.rb` — `handle`, `fallback`, `KEY_VIEWS`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
7
12
  ---
8
13
 
9
14
  # The order
@@ -78,7 +83,3 @@ later. That is the check worth keeping: an arming that never lets go renders
78
83
  identically to the fixed behaviour until the pair is pressed apart.
79
84
 
80
85
  `Ctrl-c` stays single. An escape hatch that needs confirming is not one.
81
-
82
- # Citations
83
-
84
- [1] `lib/okf/tui/app.rb` — `handle`, `fallback`, `KEY_VIEWS`.
@@ -1,18 +1,33 @@
1
1
  ---
2
2
  type: Constraint
3
3
  title: Which Registry a Session Is On
4
- description: okf resolves a project-local .okf-registry.json before the global $OKF_HOME one; the TUI did not, and being the single verb that disagreed was a silent wrong answer rather than an error.
4
+ description: okf resolves a project-local .okf.json before the global $OKF_HOME one; the TUI did not, and being the single verb that disagreed was a silent wrong answer rather than an error.
5
5
  tags: [registry, okf-coupling, discovery]
6
- timestamp: 2026-08-13
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-13
9
+ sources:
10
+ - title: "`lib/okf/tui/workspace.rb` — `open_registry`, and `registry_path` asking the registry rather than recomputing from `home`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/workspace.rb
12
+ - title: "okf `lib/okf/registry.rb` — `Registry.load(home:, cwd:)`, `LOCAL_FILE`, `NO_DISCOVERY_ENV`, and `#reopen` with the comment recording what a bare `new` costs."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf/lib/okf/registry.rb
14
+ - title: "okf `CHANGELOG.md` 1.12.0 — `okf registry init`, relative path storage, and the hub bug that `#reopen` fixed."
15
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/CHANGELOG.md
16
+ - title: "`test/integration/refs_test.rb` — the local/global pair, `OKF_NO_DISCOVERY`, and the embedding-app case that must stay global-only."
17
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/refs_test.rb
18
+ - title: "Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` → `<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`."
19
+ resource: "Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` → `<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`"
7
20
  ---
8
21
 
9
22
  # Overview
10
23
 
11
24
  "Which bundles can I see?" has one right answer per directory, and okf decides it:
12
- `OKF_NO_DISCOVERY` forces the global registry; otherwise a `.okf-registry.json`
25
+ `OKF_NO_DISCOVERY` forces the global registry; otherwise a `.okf.json` — or the
26
+ older `.okf-registry.json`, still discovered so no committed registry breaks —
13
27
  found by walking up from the working directory wins; otherwise `$OKF_HOME`
14
- (default `~/.okf`). Nearest local file wins, and a local registry stores paths
15
- *relative* to itself so it can be committed and travel with the repo.
28
+ (default `~/.okf`). Both names are checked in each directory before climbing, so
29
+ "nearest local file wins" keeps meaning what it says, and a local registry stores
30
+ paths *relative* to itself so it can be committed and travel with the repo.
16
31
 
17
32
  The TUI ignored all of that for a release. `Workspace` called
18
33
  `OKF::Registry.load(home: home)` with no `cwd:`, and okf only discovers when it is
@@ -32,6 +47,25 @@ This is the shape [okf-capability-drift](/decisions/okf-capability-drift.md)
32
47
  describes: okf added a resolution rule, the old call kept working, and "kept
33
48
  working" meant "kept answering the wrong question".
34
49
 
50
+ # A third input arrived, and the answer stayed one method
51
+
52
+ okf later gained `okf registry link`: the global registry can point at another
53
+ registry file, and that file's bundles and groups resolve into the set. The TUI
54
+ needed no change to show them — `registry_entries` reads `listing` and
55
+ `registry_groups` reads `groups_listing`, and okf folds the linked half into
56
+ both. That is the point. The rule this concept states is that one question has
57
+ one answer per directory; a linked group listed by a *second* method would have
58
+ put the TUI back where it started, showing a smaller set than its own `@ref`
59
+ resolution could open, with nothing failing to say so.
60
+
61
+ What the TUI had to add for itself is that a linked bundle is **read-only**. The
62
+ config keys reached okf, which refused, and the message arrived on the status
63
+ line — correct, and too late: the user had already typed a new name or confirmed
64
+ a removal. `d`, `n`, `x` and `+` now refuse before the prompt, and the detail
65
+ pane says where the bundle comes from. That is
66
+ [registry-write-boundary](/decisions/registry-write-boundary.md)'s, not this
67
+ file's — what belongs here is only that the *set* stayed one answer.
68
+
35
69
  # The library keeps okf's own line
36
70
 
37
71
  `cwd:` is a parameter, not a default of `Dir.pwd`, and that mirrors okf exactly:
@@ -69,17 +103,3 @@ too. That is not decoration — it is the only way a user can tell which of the
69
103
  registries is in force, and it is what `refs_test.rb` asserts on to prove the CLI
70
104
  opts into discovery at all: an empty *local* registry reports the local path,
71
105
  where a run that ignored discovery would name the global one.
72
-
73
- # Citations
74
-
75
- [1] `lib/okf/tui/workspace.rb` — `open_registry`, and `registry_path` asking the
76
- registry rather than recomputing from `home`.
77
- [2] okf `lib/okf/registry.rb` — `Registry.load(home:, cwd:)`, `LOCAL_FILE`,
78
- `NO_DISCOVERY_ENV`, and `#reopen` with the comment recording what a bare `new`
79
- costs.
80
- [3] okf `CHANGELOG.md` 1.12.0 — `okf registry init`, relative path storage, and the
81
- hub bug that `#reopen` fixed.
82
- [4] `test/integration/refs_test.rb` — the local/global pair, `OKF_NO_DISCOVERY`, and
83
- the embedding-app case that must stay global-only.
84
- [5] Reproduced 2026-08-13 in a scratch project: `Registry.load(cwd: pwd).path` →
85
- `<project>/.okf-registry.json`, `Registry.load.path` → `<home>/registry.json`.
data/.okf/log.md CHANGED
@@ -1,5 +1,61 @@
1
1
  # Update Log
2
2
 
3
+ ## 2026-08-22
4
+
5
+ * **The project-local registry is `.okf.json`, and the TUI inherits that for
6
+ free** — [which-registry](interaction/which-registry.md),
7
+ [registry-write-boundary](decisions/registry-write-boundary.md). okf renamed the
8
+ file and kept discovering the old `.okf-registry.json`, checking both names in
9
+ each directory on the way up. `Workspace` needed no change, which is the point
10
+ of asking okf rather than recomputing: the one method that answers "which
11
+ registry am I on" answered the new question the moment the kernel did. What the
12
+ two concepts owed was accuracy — one stated the discovery rule and would have
13
+ stated half of it, and the other named the file it writes.
14
+
15
+ ## 2026-08-21
16
+
17
+ * **A linked bundle is read-only, and the four config keys say so before they
18
+ ask** — [registry-write-boundary](decisions/registry-write-boundary.md),
19
+ [which-registry](interaction/which-registry.md). okf's registry can now `link`
20
+ another registry file, whose bundles and groups resolve into the bundles view.
21
+ They list, load, scope and search like any other — but the file that owns them
22
+ is elsewhere, so okf refuses every write. `d`, `n`, `x` and `+` reached okf,
23
+ okf raised, and the message landed on the status line *after* the user had
24
+ typed a new name or confirmed a removal. They now refuse first, naming the
25
+ link; the bundle detail carries `read-only — linked from @onm` and the owning
26
+ file underneath. The registry pane is left alone, because 42 columns cannot
27
+ take another marker without clipping.
28
+
29
+ * **Nothing was needed to *show* linked bundles at all.** The entries come from
30
+ okf's `listing` and the groups from its `groups_listing`, and okf folds the
31
+ linked half into both rather than into a second method — which is the whole
32
+ point of [which-registry](interaction/which-registry.md)'s rule, arriving from
33
+ the kernel's side this time. `Entry#link` and `Group#link` are read off those
34
+ rows, and an agreement test asserts the field against okf itself, since a
35
+ rename there would leave every refusal above quietly not firing.
36
+
37
+ ## 2026-08-19
38
+
39
+ * **This bundle moved to OKF v0.2.** `timestamp:` became
40
+ `generated: { by, at }` on all 30 concepts, and the 22 body `# Citations`
41
+ lists became `sources:` — the text preserved, a GitHub URL where the entry
42
+ named a real file, a scope descriptor where it recorded a measurement or a
43
+ repro. Three concepts carried positional `[1]` markers in their bodies; those
44
+ are now `[^1]` footnotes keyed to `sources[].id`, and the sources nothing
45
+ cites lost the id rather than keeping a join half-made.
46
+
47
+ * **The structure and the catalogue moved into this bundle, and a test holds
48
+ them to the code.** `AGENTS.md` carried a hand-maintained Map of `lib/**` that
49
+ nothing checked, so a file could arrive, move or leave and the Map would keep
50
+ reading plausibly. [Structure](/structure/) now owns it — four concepts over
51
+ ten files — and [Capabilities](/capabilities/) owns the catalogue of the six
52
+ views and the kernel surface behind them, which is the list to read before
53
+ building a seventh. `test/unit/bundle_catalog_test.rb` is the pin, and it
54
+ bites in both directions: a file no concept names, a concept naming a file
55
+ that is gone, or a view catalogue that disagrees with `App::TABS`. The test
56
+ suite's own map moved too, to [the-suite](/testing/the-suite.md), and
57
+ [adding-a-view](/testing/adding-a-view.md) is the walk a new screen owes.
58
+
3
59
  ## 2026-08-15
4
60
 
5
61
  * **Release**: **1.0.0**, the first. Six views over one bundle or many —
@@ -3,7 +3,14 @@ type: Component
3
3
  title: ANSI-aware Width
4
4
  description: Every layout primitive measures display width on colour-stripped text, because String#length counts escape bytes and a composed frame breaks the moment those two disagree.
5
5
  tags: [rendering, terminal, ansi]
6
- timestamp: 2026-07-19
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-19
9
+ sources:
10
+ - title: "`lib/okf/tui/ui.rb` — the primitives."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/ui.rb
12
+ - title: "`test/integration/geometry_test.rb` — the two-colour matrix and `with_colour`."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/integration/geometry_test.rb
7
14
  ---
8
15
 
9
16
  # Overview
@@ -66,9 +73,3 @@ the same broken measure.
66
73
 
67
74
  Which matters because `Ui.width` depends on a gem nobody declared — see
68
75
  [the undeclared width dependency](/decisions/undeclared-width-dependency.md).
69
-
70
- # Citations
71
-
72
- [1] `lib/okf/tui/ui.rb` — the primitives.
73
- [2] `test/integration/geometry_test.rb` — the two-colour matrix and
74
- `with_colour`.
@@ -0,0 +1,34 @@
1
+ ---
2
+ type: Constraint
3
+ title: A layout that cannot fit says so
4
+ description: Health is two panes above 112 columns and one at a time below it, reached by the same key either way — a narrow terminal changes what is shown rather than silently clipping it, and a new split is judged by its narrowest column.
5
+ tags: [rendering, layout, terminal, width]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19T12:00:00Z
9
+ ---
10
+
11
+ # The rule
12
+
13
+ A pane that cannot fit its content does not clip it. It stops claiming to show
14
+ both halves.
15
+
16
+ The health view is the instance: two panes above 112 columns, one pane at a time
17
+ below it, and the **same `Tab`** reaches the other one in both modes. The key
18
+ keeps its meaning across the breakpoint, so nothing a reader has learned stops
19
+ working when they narrow the window — the mode changes, the vocabulary does not.
20
+
21
+ # How to judge a new split
22
+
23
+ By what its *narrowest* column does to the longest row it must carry, not by how
24
+ it looks at the width you happen to be developing at.
25
+
26
+ Health passes that test by construction rather than by tuning: its right pane
27
+ holds a fixed width because every row in it is short by design, and every row
28
+ that carries a path — the ones with no bound on their length — is on the left.
29
+ A split that puts an unbounded row in a fixed column has already failed, and
30
+ will fail invisibly, because at a comfortable width it looks correct.
31
+
32
+ This is the layout-level form of the same discipline
33
+ [width measurement](ansi-aware-width.md) applies at the character level: a frame
34
+ is only correct if it is correct at the width it was not designed for.
@@ -6,3 +6,4 @@ How a frame is composed, and the arithmetic that breaks when colour is involved.
6
6
  * [Whole-frame Painting](whole-frame-painting.md) - Repaint everything each keystroke; the purity that buys, and the constraints it imposes.
7
7
  * [The tty-markdown Wrapping Trap](markdown-rendering-trap.md) - An `IndexError` that only appears with colour on, and the parse width that avoids it.
8
8
  * [One Verdict, Worn Everywhere](status-vocabulary.md) - Collapsing `validate` and `lint` into one colour so a problem is visible from any view.
9
+ * [A layout that cannot fit says so](fit-or-say-so.md) - Two panes or one, the same key either way, and a new split judged by its narrowest column rather than by how it looks now.
@@ -3,7 +3,14 @@ type: Reference
3
3
  title: The tty-markdown Wrapping Trap
4
4
  description: tty-markdown raises IndexError on some documents when colour is on, so the renderer asks it never to wrap and does its own wrapping instead.
5
5
  tags: [rendering, terminal, ansi, scar-tissue]
6
- timestamp: 2026-07-18
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-18
9
+ sources:
10
+ - title: "`lib/okf/tui/app.rb` — `PARSE_WIDTH` and the render."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
12
+ - title: "Reproduced by sweeping every concept × width with colour forced on: 50/192 raised `IndexError`; 0/192 with the fix."
13
+ resource: "Reproduced by sweeping every concept × width with colour forced on: 50/192 raised `IndexError`; 0/192 with the fix"
7
14
  ---
8
15
 
9
16
  # The symptom
@@ -55,9 +62,3 @@ A rendering bug that only appears with colour on cannot be caught by any test
55
62
  that captures output through a pipe. When a screen misbehaves in the terminal but
56
63
  not in a test, **suspect the colour path first** — it is the one the harness
57
64
  never walks.
58
-
59
- # Citations
60
-
61
- [1] `lib/okf/tui/app.rb` — `PARSE_WIDTH` and the render.
62
- [2] Reproduced by sweeping every concept × width with colour forced on: 50/192
63
- raised `IndexError`; 0/192 with the fix.
@@ -3,7 +3,12 @@ type: Concept
3
3
  title: One Verdict, Worn Everywhere
4
4
  description: A bundle is clean, warned, or not conformant, and that single judgement drives its colour in every place it is named — so a problem is visible without opening the health view.
5
5
  tags: [rendering, ux]
6
- timestamp: 2026-07-18
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-18
9
+ sources:
10
+ - title: "`lib/okf/tui/views.rb` — `health_status`, `STATUS`, `status_style`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/views.rb
7
12
  ---
8
13
 
9
14
  # Overview
@@ -39,7 +44,3 @@ Because the colour is attached to the name rather than to a view, switching the
39
44
  active bundle changes it everywhere at once, which is what makes it readable
40
45
  while moving between bundles — see
41
46
  [cross-bundle-scope](/interaction/cross-bundle-scope.md).
42
-
43
- # Citations
44
-
45
- [1] `lib/okf/tui/views.rb` — `health_status`, `STATUS`, `status_style`.
@@ -3,7 +3,14 @@ type: Component
3
3
  title: Whole-frame Painting
4
4
  description: Each keystroke repaints every row from cursor-home rather than diffing, which makes a frame a pure function of state and is what lets the tests render without a terminal.
5
5
  tags: [rendering, terminal, testing]
6
- timestamp: 2026-07-18
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-18
9
+ sources:
10
+ - title: "`lib/okf/tui/app.rb` — `paint`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/lib/okf/tui/app.rb
12
+ - title: "`test/test_helper.rb` — `FixedScreen`, `frame_for`, `render`."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/test_helper.rb
7
14
  ---
8
15
 
9
16
  # Overview
@@ -45,8 +52,3 @@ differently on the machine running the suite.
45
52
  earlier per-pane budget that could go negative on a short terminal and crash.
46
53
  - **Views stay pure row builders.** No view writes to the terminal; they return
47
54
  arrays of rows. The app is the only thing that prints.
48
-
49
- # Citations
50
-
51
- [1] `lib/okf/tui/app.rb` — `paint`.
52
- [2] `test/test_helper.rb` — `FixedScreen`, `frame_for`, `render`.
@@ -0,0 +1,61 @@
1
+ ---
2
+ type: Component
3
+ title: The Doors, and What Loads When
4
+ description: One entry point through okf's plugin seam, an argv shell that loads on demand, and a ref grammar subclassed from okf rather than rewritten.
5
+ tags: [structure, cli, plugin, loading]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19
9
+ ---
10
+
11
+ # The files
12
+
13
+ | file | what it owns |
14
+ |---|---|
15
+ | `lib/okf/tui.rb` | `OKF::TUI`, `OKF::TUI::Error`, and the two capability probes |
16
+ | `lib/okf/plugin.rb` | `OKF::CLI::Tui` — registers `okf tui`, the gem's only entry point |
17
+ | `lib/okf/tui/cli.rb` | `OKF::TUI::CLI` — the only layer that parses argv, prints, and exits |
18
+ | `lib/okf/tui/refs.rb` | `OKF::TUI::Refs` — argv to bundle dirs, through okf's own ref grammar |
19
+ | `lib/okf/tui/version.rb` | `OKF::TUI::VERSION` |
20
+
21
+ # One door, and what that costs the dispatcher
22
+
23
+ There is **no executable**. `okf tui` is the whole surface, and the reasoning is
24
+ [one-door-the-plugin-seam](/decisions/one-door-the-plugin-seam.md).
25
+
26
+ The consequence to hold on to: **the dispatcher must add nothing but argv and
27
+ the streams.** `plugin_test.rb` pins it by driving the same run both through
28
+ `OKF::CLI.start` and straight into `CLI.run`, comparing the exit code and the
29
+ message.
30
+
31
+ `help_rows` is a promise as much as a help line. It read `tui [DIR|@slug…]` for
32
+ a whole release while the CLI rejected every `@slug` as "not a directory", so
33
+ the test now asserts the advertised form actually resolves.
34
+
35
+ # The ref grammar is okf's
36
+
37
+ `Refs` subclasses `OKF::CLI::Command`, so `@slug`, a bare `@`, an `@group` and
38
+ the refusal of `@all` mean exactly what they mean to `okf server` — same
39
+ messages, same exit codes. It reaches a *private* helper
40
+ (`resolve_ref_expanding`), which is a deliberate trade: one copy of the grammar,
41
+ at the cost of a coupling that `refs_test.rb` pins **by name**, so okf moving it
42
+ fails loudly rather than quietly restoring "not a directory".
43
+
44
+ The same call is what opts this gem into registry discovery, since okf's
45
+ `open_registry` is `Registry.load(cwd: Dir.pwd)` — which is why the TUI resolves
46
+ the same registry every other `okf` verb run from that directory resolves. See
47
+ [which-registry](/interaction/which-registry.md).
48
+
49
+ # What loads when
50
+
51
+ `require "okf/tui"` loads the library only. `cli.rb` arrives on demand — the
52
+ plugin's `#call` requires it, and so must any test that drives it.
53
+
54
+ That is not tidiness. okf reads `plugin.rb` whenever a verb misses or `okf help`
55
+ runs, so it must stay cheap: it registers a class and nothing else, and the TTY
56
+ toolkit is required inside `#call`. A subprocess test asserts that loading the
57
+ plugin leaves `TTY::Box` undefined.
58
+
59
+ `OKF::TUI.search_capable?` and `.spec_capable?` are the other half of that
60
+ caution — they ask the installed okf what it can do rather than assuming a
61
+ version, which is [okf-capability-drift](/decisions/okf-capability-drift.md).
@@ -0,0 +1,16 @@
1
+ # Structure
2
+
3
+ Every file under `lib/`, grouped by the layer that owns it. One concept owns
4
+ each file, and `test/unit/bundle_catalog_test.rb` fails if that stops being true
5
+ in either direction — a file no concept names, or a concept naming a file that
6
+ is gone.
7
+
8
+ Ten files, four layers, and the dependency runs one way: the doors build a
9
+ workspace, the workspace answers questions about bundles, the app holds the
10
+ state and the key loop, and the rendering layer turns that state into rows.
11
+ Nothing below reaches back up.
12
+
13
+ * [The Doors](doors.md) - `lib/okf/tui.rb`, `lib/okf/plugin.rb`, `lib/okf/tui/cli.rb`, `lib/okf/tui/refs.rb`, `lib/okf/tui/version.rb` — the one entry point, the argv shell, and the ref grammar borrowed rather than rewritten.
14
+ * [The Workspace and the Model](the-workspace.md) - `lib/okf/tui/workspace.rb`, `lib/okf/tui/model.rb` — which bundles a session can see, and every answer about one of them.
15
+ * [The App](the-app.md) - `lib/okf/tui/app.rb` — state, the key loop, and the frame.
16
+ * [The Rendering Layer](rendering.md) - `lib/okf/tui/views.rb`, `lib/okf/tui/ui.rb` — the six screens as row builders, and the primitives that make a row fit.
@@ -0,0 +1,58 @@
1
+ ---
2
+ type: Component
3
+ title: The Rendering Layer
4
+ description: Six screens built as rows of text with no terminal I/O, over primitives that measure and cut strings by display width rather than by character count.
5
+ tags: [structure, rendering, ansi, layout]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19
9
+ ---
10
+
11
+ # The files
12
+
13
+ | file | pure? | what it owns |
14
+ |---|---|---|
15
+ | `lib/okf/tui/views.rb` | pure | the six screens — row builders, no terminal I/O |
16
+ | `lib/okf/tui/ui.rb` | pure | layout primitives: width, clipping, wrapping, boxes |
17
+
18
+ Both are pure, and that is the property the test suite is built on: a view
19
+ returns an array of strings, so a test can assert a frame without a pty.
20
+
21
+ # Views: one builder per screen, plus the chrome
22
+
23
+ `Views` is a module of builders taking `(app, width, height)` and returning rows.
24
+ Beyond the six screens it owns the chrome every screen wears — `header`,
25
+ `workspace_header`, `tabs`, `footer`, `active_badge` — and the two vocabularies
26
+ that must be identical everywhere: `TYPE_COLOURS` / `type_colour`, and `STATUS` /
27
+ `health_status` / `status_style`.
28
+
29
+ That second one is the point of [status-vocabulary](/rendering/status-vocabulary.md):
30
+ `validate` and `lint` collapse into **one** verdict so a problem is visible from
31
+ any view, rather than only from the one that ran the check.
32
+
33
+ `pair` is the label/value row every detail panel is made of, and it is worth
34
+ reusing rather than re-aligning by hand.
35
+
36
+ # Ui: nothing here counts characters
37
+
38
+ `String#length` is the wrong answer twice over — a wide CJK glyph occupies two
39
+ columns, and an ANSI escape occupies none — so every primitive here measures
40
+ display width instead. `ANSI` is the escape pattern, `width` the measure, and
41
+ `clip` / `clip_ansi` / `wrap_ansi` / `reflow` the cuts that respect it.
42
+ [ansi-aware-width](/rendering/ansi-aware-width.md) is the arithmetic and why the
43
+ geometry suite runs twice.
44
+
45
+ `TABULAR` exists because a box-drawing run must not be reflowed as prose, and
46
+ `LIST_START` because a bullet's continuation lines have to hang under its text.
47
+ `reset_if_styled` closes a style at the end of a row so a colour cannot bleed
48
+ into the next one — a frame is printed whole, so one unterminated sequence
49
+ stains everything below it.
50
+
51
+ `Line` is the builder for a single row under a width budget; `blank_line`,
52
+ `fit_block`, `hjoin`, `box` and `title_label` compose them. `PASTEL` is
53
+ constructed once, with `FORCE_COLOR` honoured, because colour detection is a
54
+ terminal question and the tests need to answer it themselves.
55
+
56
+ The trap that only appears with colour on is
57
+ [markdown-rendering-trap](/rendering/markdown-rendering-trap.md); read it before
58
+ touching anything that hands a width to tty-markdown.
@@ -0,0 +1,55 @@
1
+ ---
2
+ type: Component
3
+ title: The App — State, the Key Loop, and the Frame
4
+ description: "One class holding the whole session: which view, which selection, which prompt is open, and the loop that reads a key, changes that state, and repaints every row."
5
+ tags: [structure, keyboard, state, terminal]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19
9
+ ---
10
+
11
+ # The file
12
+
13
+ | file | pure? | what it owns |
14
+ |---|---|---|
15
+ | `lib/okf/tui/app.rb` | shell | state, the key loop, frame painting |
16
+
17
+ It is the largest file in the gem and the only one that reads a keystroke, and
18
+ those two facts are the same fact: everything that is *not* state or input has
19
+ been pushed down into `Views` and `Ui`, or out into `Workspace` and `Model`.
20
+
21
+ # The tables that define the surface
22
+
23
+ | constant | what it fixes |
24
+ |---|---|
25
+ | `TABS` | the six views, in the order the tab bar shows them, `help` last |
26
+ | `KEY_VIEWS` | `1`–`6`, the direct jump to each |
27
+ | `SINGLE_BUNDLE_VIEWS` | `browse`, `health`, `graph` — the ones with nothing to show when no bundle can be read |
28
+ | `CONTENT_VIEWS` | `health`, `help` — a scrolling page rather than a selectable list |
29
+ | `FILTERABLE_VIEWS` | `bundles`, `browse`, `graph` — the ones `/` narrows |
30
+ | `PANES` | `bundles`, `groups`, `members` — what Tab cycles inside the bundles view |
31
+
32
+ There were seven views once. The `index` map went because browse answers the
33
+ same question with a key the reader already has, and what the map could show and
34
+ browse cannot is real but narrow. A seventh tab is a real cost on a six-tab bar.
35
+
36
+ `Prompt` is a pending question on the status line: `kind` decides what the answer
37
+ does, and `free_text?` says whether it collects a line or a single confirming
38
+ key. Every destructive registry action goes through one.
39
+
40
+ # The loop, and why the frame is a pure function
41
+
42
+ `run` reads a key, routes it, and calls `paint`. `paint` moves the cursor home
43
+ and prints exactly `height` rows — no damage tracking, no dirty regions. That is
44
+ what makes a frame a pure function of state, which is in turn what lets the whole
45
+ test suite render without a terminal
46
+ ([headless-frames](/testing/headless-frames.md)).
47
+
48
+ Key routing has modes, and Esc peels one layer at a time rather than quitting
49
+ outright — both arrived at by getting them wrong first:
50
+ [key-routing](/interaction/key-routing.md),
51
+ [esc-peels-one-layer](/interaction/esc-peels-one-layer.md).
52
+ `/` starts a filter, and the filter belongs to the *view*, so switching away
53
+ drops it rather than carrying a bundle filter into the concept list; where the
54
+ filter runs out, [filter-escalates-to-search](/interaction/filter-escalates-to-search.md)
55
+ is what happens next.
@@ -0,0 +1,69 @@
1
+ ---
2
+ type: Component
3
+ title: The Workspace and the Model
4
+ description: Two files answering two different questions — which bundles this session can see, and everything there is to know about one of them — with the memoization that makes a repaint free.
5
+ tags: [structure, registry, model, memoization]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19
9
+ ---
10
+
11
+ # The files
12
+
13
+ | file | pure? | what it owns |
14
+ |---|---|---|
15
+ | `lib/okf/tui/workspace.rb` | shell | the bundles a session can see; the **only** registry writes in the gem |
16
+ | `lib/okf/tui/model.rb` | pure | one bundle, and every answer about it, memoized |
17
+
18
+ # Workspace: which bundles, and which of them count
19
+
20
+ `Workspace` holds `Entry` (a bundle: slug, dir, default?, registered?, loaded?)
21
+ and `Group` (a registry group, with `cyclic?` for the one it cannot resolve).
22
+ Its two axes are independent and it is worth stating plainly, because conflating
23
+ them is the mistake the code is shaped to prevent:
24
+
25
+ * **active** — `switch`, `active`, `model`: the one bundle the single-bundle
26
+ views are about;
27
+ * **scope** — `scope`, `scoped?`, `toggle_scope`, `scope_all`, `scope_none`,
28
+ `scope_only`, `scope_group`: the set search runs across.
29
+
30
+ [cross-bundle-scope](/interaction/cross-bundle-scope.md) is why they are two
31
+ things and not one.
32
+
33
+ `add`, `remove` and the rest of the mutators are the **only** writes to the
34
+ registry anywhere in this gem, deliberately — the boundary and what it refuses
35
+ are [registry-write-boundary](/decisions/registry-write-boundary.md).
36
+ `registry_backed?` and `registry_path` are how a view says which registry it is
37
+ looking at without guessing.
38
+
39
+ `search` is the cross-bundle one, and it goes through okf's search facade rather
40
+ than reimplementing ranking — with the sharp edge recorded in
41
+ [search-facade-coupling](/decisions/search-facade-coupling.md).
42
+
43
+ # Model: every answer about one bundle, computed once
44
+
45
+ `Model` is pure and **memoized**, and both properties are load-bearing. Pure,
46
+ because the app repaints the entire frame on every keystroke
47
+ ([whole-frame-painting](/rendering/whole-frame-painting.md)) and a view that
48
+ recomputed a lint on each one would be unusable. Memoized, for the same reason.
49
+
50
+ What it already answers — read this before computing anything in a view:
51
+
52
+ | question | reader |
53
+ |---|---|
54
+ | the bundle's concepts, as rows | `rows`, `row_by_id`, `concept_by_id` |
55
+ | the catalog and the graph | `catalog`, `graph` |
56
+ | conformance and curation | `validation`, `lint`, `skipped_checks` |
57
+ | the two v0.2 postures | `trust_posture`, `status_posture`, `shows_trust?` |
58
+ | size and shape | `concept_count`, `edge_count`, `orphan_ids`, `hubs` |
59
+ | the directory tree | `dirs`, `dir_traffic`, `dir_arcs`, `under_dir?` |
60
+ | the facets | `types`, `tags`, `types_of`, `tags_of`, `statuses_of`, `tiers_of` |
61
+ | what the bundle declares itself to be | `okf_version` |
62
+
63
+ Every one of those is okf's judgement, not this gem's. `Model` is a reader over
64
+ the kernel's analysers, and the rule that keeps it that way is
65
+ [invents-no-analysis](/decisions/invents-no-analysis.md): a number on screen
66
+ that okf did not compute is a number that will disagree with `okf lint`.
67
+
68
+ `UNTYPED` and `type_label` are the one exception, and they are labelling rather
69
+ than analysis — a concept with no `type` still needs a row.