okf-tui 1.0.0 → 1.0.1

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 (42) 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 +15 -14
  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 +14 -15
  21. data/.okf/log.md +22 -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 +57 -1
  40. data/README.md +7 -2
  41. data/lib/okf/tui/version.rb +1 -1
  42. metadata +18 -7
@@ -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`.
@@ -3,7 +3,20 @@ type: Constraint
3
3
  title: Which Registry a Session Is On
4
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.
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
@@ -69,17 +82,3 @@ too. That is not decoration — it is the only way a user can tell which of the
69
82
  registries is in force, and it is what `refs_test.rb` asserts on to prove the CLI
70
83
  opts into discovery at all: an empty *local* registry reports the local path,
71
84
  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,27 @@
1
1
  # Update Log
2
2
 
3
+ ## 2026-08-19
4
+
5
+ * **This bundle moved to OKF v0.2.** `timestamp:` became
6
+ `generated: { by, at }` on all 30 concepts, and the 22 body `# Citations`
7
+ lists became `sources:` — the text preserved, a GitHub URL where the entry
8
+ named a real file, a scope descriptor where it recorded a measurement or a
9
+ repro. Three concepts carried positional `[1]` markers in their bodies; those
10
+ are now `[^1]` footnotes keyed to `sources[].id`, and the sources nothing
11
+ cites lost the id rather than keeping a join half-made.
12
+
13
+ * **The structure and the catalogue moved into this bundle, and a test holds
14
+ them to the code.** `AGENTS.md` carried a hand-maintained Map of `lib/**` that
15
+ nothing checked, so a file could arrive, move or leave and the Map would keep
16
+ reading plausibly. [Structure](/structure/) now owns it — four concepts over
17
+ ten files — and [Capabilities](/capabilities/) owns the catalogue of the six
18
+ views and the kernel surface behind them, which is the list to read before
19
+ building a seventh. `test/unit/bundle_catalog_test.rb` is the pin, and it
20
+ bites in both directions: a file no concept names, a concept naming a file
21
+ that is gone, or a view catalogue that disagrees with `App::TABS`. The test
22
+ suite's own map moved too, to [the-suite](/testing/the-suite.md), and
23
+ [adding-a-view](/testing/adding-a-view.md) is the walk a new screen owes.
24
+
3
25
  ## 2026-08-15
4
26
 
5
27
  * **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.
@@ -0,0 +1,58 @@
1
+ ---
2
+ type: Playbook
3
+ title: Adding a View, a Key, or a Panel
4
+ description: The steps a new screen owes, in order — where the builder goes, which tables it joins, what the frame test must assert, and the catalogue entry the pin will demand.
5
+ tags: [testing, playbook, views]
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-08-19
9
+ ---
10
+
11
+ # Before you add a view, try not to
12
+
13
+ Six is the whole tab bar, and a seventh is a real cost on every screen. One view
14
+ has already been withdrawn for not earning its tab: the `index` map, because
15
+ browse answered the same question with a key the reader already had. Check
16
+ [the catalogue](/capabilities/views.md) first.
17
+
18
+ The same instinct applies one level down: a new *number* on an existing screen
19
+ usually belongs in okf, not here — [invents-no-analysis](/decisions/invents-no-analysis.md).
20
+
21
+ # The walk
22
+
23
+ 1. **Write the failing test first**, in `test/integration/`, headless. It renders
24
+ a frame and asserts what is in it. Run it: it must fail for the reason you
25
+ predicted. And prove the assertion can fail at all — a frame test that passes
26
+ against the *old* code is asserting nothing. See
27
+ [headless-frames](/testing/headless-frames.md).
28
+ 2. **Build rows, not output.** The builder goes in `views.rb` and returns an
29
+ array of strings; it may not touch the terminal. Anything that measures or
30
+ cuts a string goes through `ui.rb`, never `String#length` —
31
+ [ansi-aware-width](/rendering/ansi-aware-width.md).
32
+ 3. **Join the tables in `app.rb`.** `TABS` (order matters — `help` stays last)
33
+ and `KEY_VIEWS`. Then decide the two behaviours: is it a
34
+ `SINGLE_BUNDLE_VIEW` (follows the active bundle) or scope-wide? Is it a
35
+ `CONTENT_VIEW` (a scrolling page) or a list? Is it a `FILTERABLE_VIEW`?
36
+ 4. **Ask the reader, don't compute.** `Model` is memoized and already answers
37
+ most of it — [the-workspace](/structure/the-workspace.md) has the table. If
38
+ the answer isn't there, the question is probably okf's.
39
+ 5. **A destructive action gets a `Prompt`.** Every registry write asks first,
40
+ and the write itself goes through `Workspace` and nowhere else —
41
+ [registry-write-boundary](/decisions/registry-write-boundary.md).
42
+ 6. **Esc must peel exactly one layer.** A new mode adds a layer to that stack;
43
+ [esc-peels-one-layer](/interaction/esc-peels-one-layer.md) is the rule it has
44
+ to obey.
45
+ 7. **Update the catalogue** — [capabilities/views](/capabilities/views.md). This
46
+ is not etiquette: `test/unit/bundle_catalog_test.rb` compares that table
47
+ against `App::TABS`, so the suite is red until you do. A new file under
48
+ `lib/` needs its line in [structure/](/structure/) for the same reason.
49
+ 8. **Run the same test unedited**, then the geometry suite — it runs twice, at
50
+ two widths, and that is where a row that fits in ASCII and overflows in colour
51
+ shows up.
52
+
53
+ # What the suite structurally cannot catch
54
+
55
+ One pty test proves the binary boots and nothing else ([pty-test](/testing/pty-test.md)),
56
+ and a local run cannot see the dependency resolution that only the matrix
57
+ exercises ([ci-matrix](/testing/ci-matrix.md)). Neither is a gap to fill with
58
+ more headless tests; they are different questions.
@@ -3,7 +3,14 @@ type: Runbook
3
3
  title: The CI Matrix and What Only It Catches
4
4
  description: Ten Rubies on every push, what only the 2.4 container proves, and the resolution gap the matrix cannot see because every run resolves the sibling checkout.
5
5
  tags: [testing, ruby-floor, dependencies]
6
- timestamp: 2026-07-19
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-19
9
+ sources:
10
+ - title: "The repository's `.github/workflows/main.yml`, `okf-tui` job."
11
+ resource: https://github.com/serradura/okf/blob/main/.github/workflows/main.yml
12
+ - title: "`AGENTS.md` — the scripted `Gemfile.ci-check` run against the published okf."
13
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/AGENTS.md
7
14
  ---
8
15
 
9
16
  # The matrix
@@ -73,8 +80,3 @@ Two details keep that reproduction honest:
73
80
  something no user has. Drop the lockfile in the copy, exactly as the Docker
74
81
  command above drops it for its own reason (a lockfile written by a modern
75
82
  Bundler is one 2.4's cannot read).
76
-
77
- # Citations
78
-
79
- [1] The repository's `.github/workflows/main.yml`, `okf-tui` job.
80
- [2] `AGENTS.md` — the scripted `Gemfile.ci-check` run against the published okf.
@@ -3,7 +3,12 @@ type: Playbook
3
3
  title: Testing Frames Without a Terminal
4
4
  description: How the suite drives real interactions and asserts on real frames with no pty — and the discipline of proving each check can actually fail.
5
5
  tags: [testing, rendering]
6
- timestamp: 2026-07-19
6
+ generated:
7
+ by: human:maintainer
8
+ at: 2026-07-19
9
+ sources:
10
+ - title: "`test/test_helper.rb` — `FixedScreen`, `with_registry`, `app_for`, `frame_for`, `keystrokes`."
11
+ resource: https://github.com/serradura/okf/blob/main/gems/okf-tui/test/test_helper.rb
7
12
  ---
8
13
 
9
14
  # The shape
@@ -67,8 +72,3 @@ Two things, each covered elsewhere because no headless frame can reach them:
67
72
  [the pty test](/testing/pty-test.md);
68
73
  - anything that depends on which okf actually resolves, since the Gemfile prefers
69
74
  the sibling checkout locally — that is [the CI matrix](/testing/ci-matrix.md).
70
-
71
- # Citations
72
-
73
- [1] `test/test_helper.rb` — `FixedScreen`, `with_registry`, `app_for`,
74
- `frame_for`, `keystrokes`.
@@ -3,6 +3,8 @@
3
3
  How a full-screen terminal app is proven, and what each layer can and cannot
4
4
  catch.
5
5
 
6
+ * [The Suite — Its Layers, and the Two Fixtures That Exist to Bite](the-suite.md) - What each test file proves, and the two fixtures built to reach a branch nothing else could.
6
7
  * [Testing Frames Without a Terminal](headless-frames.md) - Driving real interactions headlessly, and the discipline of proving a check can fail.
7
8
  * [The One Test That Opens a Terminal](pty-test.md) - The single pty walk, and the three timing traps that made it flake on the floor.
8
9
  * [The CI Matrix and What Only It Catches](ci-matrix.md) - Ten Rubies, the Docker floor check, and the dependency bug a local run cannot see.
10
+ * [Adding a View, a Key, or a Panel](adding-a-view.md) - The steps a new screen owes, and the catalogue entry the pin will demand.