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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.okf/decisions/index.md +12 -0
  3. data/.okf/decisions/invents-no-analysis.md +53 -0
  4. data/.okf/decisions/no-version-ceilings.md +69 -0
  5. data/.okf/decisions/okf-capability-drift.md +120 -0
  6. data/.okf/decisions/one-door-the-plugin-seam.md +131 -0
  7. data/.okf/decisions/registry-write-boundary.md +175 -0
  8. data/.okf/decisions/ruby-floor.md +59 -0
  9. data/.okf/decisions/search-facade-coupling.md +146 -0
  10. data/.okf/decisions/undeclared-width-dependency.md +73 -0
  11. data/.okf/index.md +28 -0
  12. data/.okf/interaction/cross-bundle-scope.md +61 -0
  13. data/.okf/interaction/deferred-search.md +49 -0
  14. data/.okf/interaction/esc-peels-one-layer.md +70 -0
  15. data/.okf/interaction/filter-escalates-to-search.md +57 -0
  16. data/.okf/interaction/following-links.md +82 -0
  17. data/.okf/interaction/index.md +12 -0
  18. data/.okf/interaction/key-routing.md +84 -0
  19. data/.okf/interaction/which-registry.md +85 -0
  20. data/.okf/log.md +38 -0
  21. data/.okf/rendering/ansi-aware-width.md +74 -0
  22. data/.okf/rendering/index.md +8 -0
  23. data/.okf/rendering/markdown-rendering-trap.md +63 -0
  24. data/.okf/rendering/status-vocabulary.md +45 -0
  25. data/.okf/rendering/whole-frame-painting.md +52 -0
  26. data/.okf/testing/ci-matrix.md +80 -0
  27. data/.okf/testing/headless-frames.md +74 -0
  28. data/.okf/testing/index.md +8 -0
  29. data/.okf/testing/pty-test.md +73 -0
  30. data/CHANGELOG.md +239 -0
  31. data/LICENSE.txt +201 -0
  32. data/NOTICE +10 -0
  33. data/README.md +194 -0
  34. data/lib/okf/plugin.rb +63 -0
  35. data/lib/okf/tui/app.rb +1908 -0
  36. data/lib/okf/tui/cli.rb +154 -0
  37. data/lib/okf/tui/model.rb +410 -0
  38. data/lib/okf/tui/refs.rb +63 -0
  39. data/lib/okf/tui/ui.rb +308 -0
  40. data/lib/okf/tui/version.rb +7 -0
  41. data/lib/okf/tui/views.rb +1648 -0
  42. data/lib/okf/tui/workspace.rb +527 -0
  43. data/lib/okf/tui.rb +76 -0
  44. metadata +229 -0
data/CHANGELOG.md ADDED
@@ -0,0 +1,239 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-08-15
9
+
10
+ First release.
11
+
12
+ ### Added
13
+
14
+ - **Six views over OKF bundles**: bundles, browse, search, graph, health, help.
15
+ It invents no analysis — okf owns the format, the model, and every question on
16
+ screen.
17
+ - **It targets OKF v0.2, and reads v0.1 as well as okf does.** The rule is okf's
18
+ own and it decides every §5 surface below: v0.2 only added optional keys, so a
19
+ bundle that adopted none of them must not read as deficient. A v0.1 bundle
20
+ reads as v0.1 — no empty columns, no rows saying "unverified" about a family it
21
+ never had — and §13.1 does the rest, so a v0.1 `timestamp:` shows as the date
22
+ it records, with no actor invented for it because none was ever recorded. Which spec a bundle is on is asked of the bundle (`Bundle#okf_version`)
23
+ rather than assumed: the health view names v0.1 or v0.2 as declared, and calls a
24
+ bundle that declares neither conformant rather than guessing at a number.
25
+ - **§5's provenance, on the concept you are reading.** The browse detail pane says
26
+ when a concept was generated and by whom, how many sources back it, when it
27
+ expires, and its status where that status is not the `stable` an absent one
28
+ already means. The trust tier is the one that needs a rule rather than a
29
+ presence check: §5.3 *derives* `unverified` for every concept that verified
30
+ nothing, so printing it unconditionally would paint a provenance verdict onto
31
+ documents that never made one. `Bundle::RowFilter.shows_trust?` is okf's
32
+ predicate — shared with its server and its graph page — and the graph facet
33
+ gates on the same call, so a tier is claimed in exactly one place or in none.
34
+ - **`status` and `trust` are the graph's fourth and fifth facets**, offered only
35
+ where the bundle has something to say — a status needs one *declared* value,
36
+ since a column of `stable` is what an undeclared status already means, and
37
+ trust needs one tier okf is willing to claim. Narrowing goes through
38
+ `Bundle::RowFilter.matches?`, so the two folds that make this non-trivial are
39
+ okf's rather than a copy: an absent status reads `stable` here exactly as it
40
+ does under `--status`, and a tier folds both spellings. The count and the
41
+ narrowing read one predicate, so a facet selects precisely the rows it counted.
42
+ - **Browse reads a bundle in spec order** — `index.md`, `log.md`, then each
43
+ directory — and renders concept bodies as markdown. `/` looks through whatever
44
+ has focus: a list filters, a document finds.
45
+ - **Follow a markdown link out of the document you are reading**: `f` lists what
46
+ it points at, `1`–`9` or `Enter` goes there, `Esc` puts the body back. The list
47
+ comes from okf's own link extraction — the same one the graph builds edges
48
+ with — so a concept reads as its title and a dead link as not written yet.
49
+ Directory links (`[Decisions](decisions/)`, how every index points at its area)
50
+ follow to that directory's `index.md`.
51
+ - **`Backspace` returns to wherever a jump started.** Shared by every jump, so
52
+ opening a search hit and following a concept out of the graph are reversible
53
+ too.
54
+ - **Search spans every bundle in scope through one shared index**, so the scores
55
+ compare between them; submitted with `Enter` rather than run per keystroke. The
56
+ corpus is built once and held — measured over five registered bundles, 129
57
+ concepts between them: **392 ms** for the first query, then **12–16 ms** for
58
+ every one after. It is built on first use rather than at load — a session that
59
+ never searches should not pay to index bundles nobody opened — keyed on the
60
+ scope, and dropped on any reload, because a held index outliving the set it was
61
+ built from is a wrong answer rather than a slow one.
62
+ - **`e` chooses how a query is asked** — `fuzzy`, `text` or `regexp`. okf's index
63
+ and scan disagree *by design*, so offering only the index would leave every term
64
+ glued to a symbol unfindable with nothing on screen saying so. Measured on okf's
65
+ own bundle: the index finds three of the five concepts that say `minifts` (a
66
+ backtick is not punctuation, so a word in a code span indexes as
67
+ `` `minifts` ``), and returns fourteen results for `OKF_HOME` where the scan
68
+ returns five, because it splits the term and matches the halves. `text` is okf's
69
+ own CLI default and has no tokenizer to get in the way; `regexp` is a capability
70
+ only the scan declares. The mode is named on screen beside the query rather than
71
+ living only in a keystroke, the hits are keyed on it so switching re-asks rather
72
+ than relabelling the previous engine's answer, and an unparseable pattern reports
73
+ itself instead of being rescued into "no matches" — which is indistinguishable
74
+ from a term that is genuinely absent. One held corpus serves all three; the scan
75
+ declares no `prepare`, so it reuses the documents rather than an index.
76
+ - **A registry filter that matches nothing offers the search**, exactly as browse
77
+ does: `Enter` takes the term to view 3 and searches every bundle for it. A filter
78
+ over a dozen slugs is a narrow thing to be typing, and a term matching none of
79
+ them is usually a question about what the bundles *say*. Both panes have to be
80
+ empty, so a filter naming a group still accepts on `Enter`.
81
+ - **The graph is navigable**: pick a type or tag to narrow it, or a concept to
82
+ open it. A concept whose type is blank or absent is named `Untyped` rather than
83
+ hidden — §9.2 requires a type, which makes those concepts exactly what a curator
84
+ is looking for, and okf's own graph index labels them the same way.
85
+ - **`dir` is a third facet in the graph view.** okf 1.11.0 made the full directory
86
+ path a filter on six verbs and 1.12.0 deprecated the first-segment `--area` for
87
+ "losing every level below it". Selecting a directory reaches everything beneath
88
+ it, exactly as `--dir` does, and the counts are the subtree counts `okf dirs`
89
+ prints. Offered only where the bundle actually nests: at one level deep every dir
90
+ facet says what a top-level rollup says, which is what `--area` was deprecated
91
+ for being.
92
+ - **Each bundle's standing** (conformance, curation) colours it wherever it is
93
+ named, and flags the health tab.
94
+ - **Health is two panes: findings on the left, standing on the right.** One page
95
+ mixing a bounded summary with an unbounded findings list, list on top, drops the
96
+ dir traffic and the stats below the fold — and drops them further the more
97
+ findings a bundle has, which is exactly the bundle whose structure you opened the
98
+ view to read. The summary cannot be pushed away, and the two scroll apart: `Tab`
99
+ names which one the keys move. The right pane carries verdicts and numbers and
100
+ never a path, which is what lets it hold a fixed width; the errors behind its
101
+ counts are listed on the left, where the columns are. Below 112 columns the two
102
+ would each be too narrow for the paths a finding is *about*, so the same `Tab`
103
+ shows one at a time instead.
104
+ - **Health says which checks did not run.** §5.5's freshness pair is clock-gated,
105
+ and the pure library runs neither unless handed one — so lint reports
106
+ `expired, stale not run — no clock supplied` rather than letting "lint clean"
107
+ stand for a verdict it did not earn. A check that quietly sat out is the one way
108
+ a health screen is worse than no health screen.
109
+ - **The bundle's §5 posture, beside its verdicts.** `trust unverified 31` and
110
+ `status stable 22 · deprecated 1` — okf's own lint numbers, unedited, on the
111
+ pane built for numbers. Shown only where the bundle declared something, on the
112
+ same gate the facets use, and a tier the bundle has none of is dropped rather
113
+ than clipped: this pane holds a fixed width because its rows are short by
114
+ construction, and all three tiers with their counts is not.
115
+ - **Hubs and directory traffic, on the health view.** `Bundle#hubs` ranks concepts
116
+ by inbound links and says which directories those links come from — the evidence
117
+ for "is this hub well homed?" A hub drawing its majority from outside its own
118
+ top-level dir is flagged, and where a single foreign dir carries that majority it
119
+ is named, because that is the better home the concept has already found.
120
+ `Bundle#skeleton` adds the same question one grain coarser: each directory's
121
+ internal, outbound and inbound traffic, with the internal share as a
122
+ **cohesion**, sorted so the directories with a case to answer come first. A
123
+ directory with no traffic at all reads `—` rather than a `0%` it did not earn.
124
+ Evidence, not verdicts: okf is explicit that near-zero cohesion under heavy
125
+ inbound can be a shared vocabulary doing its job.
126
+ - **The directory arcs, under the cohesion table.** The table says how much of a
127
+ directory's traffic stays home; the arcs say where the rest of it goes, which is
128
+ the other half of `graph --traffic` and the half that names a pair. Narrowed to
129
+ the cut okf *fits to the bundle* — asked for rather than guessed, since okf
130
+ measured ten bundles at a fixed weight and got anywhere from 2 arcs to 136 — and
131
+ the row says how many of how many survived it, because a silently shortened list
132
+ reads as a complete one. Cohesion is still computed over every arc, which is
133
+ okf's rule and is asserted, so narrowing the picture never moves the evidence.
134
+ - **Registry configuration in place** — `a` registers a bundle, `x` removes one,
135
+ `d` sets the default, `n` renames. Every write goes through okf's `Registry` and
136
+ is followed by a reload, so the screen shows what the file now says rather than
137
+ what memory believes. `okf registry init` is deliberately not offered: the
138
+ registry is resolved once at boot, so creating one mid-session would swap the
139
+ whole workspace out from under every open view rather than edit the one it is on.
140
+ - **Registry groups are on screen, and one key scopes a search to one.** A group
141
+ is a named, recursive set of bundles — which is to say a named search scope, and
142
+ the scope is what this view already manages. Groups list under their own heading
143
+ with the bundles they resolve to; `Enter` makes a search cover exactly those, the
144
+ same set `okf search @group` merges into one ranking. The detail pane shows the
145
+ members as the registry records them *and* the bundles they resolve to, because
146
+ for a nested group those differ. A member naming nothing registered is called out
147
+ rather than dropped, and a hand-edited cycle — which okf refuses to create and
148
+ reports by declining to resolve — says so and refuses to be scoped rather than
149
+ silently covering nothing.
150
+ - **Groups can be built and edited here too**, which takes the TUI to seven of
151
+ okf's eight `registry` verbs. `c` names the bundles now in scope as a new group,
152
+ because `◉` already means "these bundles" in this view; `+` adds, `-` removes,
153
+ `n` renames and `x` deletes. okf owns every cascade — a rename reaches every
154
+ member list that named the group, a delete drops it from all of them, and a group
155
+ left with no members is deleted.
156
+
157
+ **The bundles view is three panes** — the registry's bundles, its groups, and the
158
+ detail of whichever has focus — with `Tab` cycling and `Esc` stepping back out
159
+ one pane at a time. A heading inside the bundle list scrolls away: thirteen
160
+ registered bundles put the groups below the fold on a short terminal, which is no
161
+ way to show something you are meant to select.
162
+
163
+ Two panes also mean two selections at once, and that is what makes the editing
164
+ keys direct. Each acts on a row that is on screen: `+` in the bundles pane adds
165
+ the bundle under that cursor to the group selected below, `-` in the members pane
166
+ removes the member under that cursor, and `n`/`x` in the groups pane rename or
167
+ delete the selected group. Nothing consults the search scope except `c`, where
168
+ naming the bundles you have been searching together is the point. The groups pane
169
+ keeps its cursor while the bundles have focus, dimmed, so the row `+` acts on is
170
+ one the reader can point at — and the footer deliberately does not spell that
171
+ group's slug out, since naming an off-screen target claims a selection nothing on
172
+ screen agrees with.
173
+
174
+ The removal key reads a row for that reason above all. A `-` acting on the
175
+ *intersection* of the scope and the member list would act on a set with no row
176
+ on screen, so the key could not be predicted without computing it in your head,
177
+ and its most obvious misreading ("turn the scope off") would take a whole group
178
+ with it. Anything that can lose configuration asks first and names the
179
+ consequence — `x`,
180
+ and `-`, which says when the member being removed is the last one and the group
181
+ goes with it. `+`, `c` and `d` add or reorder, and do not ask.
182
+
183
+ A `+` has to change something visible, and what it changes is not the row: the
184
+ bundle detail pane lists every group that names the bundle — `in @docs
185
+ @everything` — and the status line names both sides of the write,
186
+ `@minimal joined @docs — 3 bundles`. An edit to a group that *is* the scope in
187
+ force re-applies that scope, in both directions, so set equality keeps holding
188
+ and the bundle just added does not read as out of scope on its own row. A group
189
+ nobody scoped is left alone, since re-scoping on every edit would replace a
190
+ hand-made selection.
191
+ - **The status row is either asking you something or telling you something**, and
192
+ it wears yellow for the first and cyan for the second — one colour each tells the
193
+ two apart before a word is read. A flash can afford the mark because the next
194
+ keystroke clears it along with the message, so it cannot become permanent noise.
195
+ - **Quitting is `q q`.** A single `q` ends a session on one stray keystroke; the
196
+ first press arms and says so, and any other key disarms it. `Ctrl-c` still quits
197
+ outright.
198
+ - **`okf tui` is the entry point, and the only one — this gem ships no
199
+ executable.** okf dispatches through a command registry and finds extensions by
200
+ convention (any gem with `okf/plugin.rb` on its load path can register a verb),
201
+ so installing this gem is the whole installation: no configuration, and `okf
202
+ help` lists `tui` under `installed extensions:`. There is no second binary: one
203
+ that only aliased the verb would be one more name to install, document and keep
204
+ working, and two front ends are two argument grammars waiting to drift, with
205
+ each one passing its own tests while they disagree. What ships instead is one
206
+ adapter that carries argv and the streams and adds nothing else, which a test
207
+ pins by running the same invocation both ways and comparing the message.
208
+ - **Every ref form okf's other multi-bundle verb takes**:
209
+ `@slug`, bare `@` for the registry default, `@group` fanned out to its members, a
210
+ vanished member skipped with a note, and `@all` refused by name as search's
211
+ alone. The grammar is *inherited* rather than copied — `OKF::TUI::Refs`
212
+ subclasses `OKF::CLI::Command` for okf's own resolver — so there is one copy of
213
+ it, and a test pins the seam so that okf moving it fails loudly instead of
214
+ quietly restoring "not a directory".
215
+ - **A project-local `.okf-registry.json` resolves ahead of the global one**, the
216
+ same as every other okf verb, discovered by walking up from the working
217
+ directory; `OKF_NO_DISCOVERY=1` opts out and `$OKF_HOME` names the global one.
218
+ Reloads go through `Registry#reopen` so a local registry's relative paths keep
219
+ the anchor they are stored against. The library keyword mirrors okf's own rule:
220
+ `Workspace.new(cwd:)` opts in, and an embedding app that passes no cwd stays
221
+ global-only.
222
+
223
+ ### Requires
224
+
225
+ - **Ruby >= 2.4**, the same floor as okf.
226
+ - **`okf >= 2.0, < 3`** — the kernel release that ships OKF v0.2. A real floor and
227
+ not a formality: eleven okf capabilities are load-bearing here, and the gemspec
228
+ lists which and why. Most of them fail *silently* against an older okf — a
229
+ renamed field reads as nil, a missing method is rescued into "no matches" —
230
+ which is why the floor is stated rather than left to chance, and why the boot
231
+ checks stay: `OKF::TUI.search_capable?` and `.spec_capable?` refuse to start and
232
+ name the okf that answered, because resolution cannot stop a second one sitting
233
+ ahead of the intended one on the load path. The ceiling is earned rather than
234
+ conventional, and by the same evidence: an okf *major* is where the silent drift
235
+ comes from (`area` → `top_dir`, then `timestamp` → `generated_at`), so `< 3`
236
+ turns the next one into a resolution failure a maintainer sees instead of a
237
+ wrong number a reader believes.
238
+
239
+ [1.0.0]: https://github.com/serradura/okf-gem/releases/tag/okf-tui/v1.0.0
data/LICENSE.txt ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright 2026 Rodrigo Serradura
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/NOTICE ADDED
@@ -0,0 +1,10 @@
1
+ OKF gem
2
+ Copyright (c) 2026 Rodrigo Serradura
3
+
4
+ Licensed under the Apache License, Version 2.0 (see LICENSE.txt).
5
+
6
+ This product bundles the Open Knowledge Format (OKF) v0.1 specification
7
+ (lib/okf/skill/reference/SPEC.md), authored by Google Cloud Platform and
8
+ licensed under the Apache License, Version 2.0, Copyright (c) Google LLC.
9
+ Source: https://github.com/GoogleCloudPlatform/knowledge-catalog
10
+ Full license text: lib/okf/skill/reference/APACHE-2.0.txt
data/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # okf-tui
2
+
3
+ A full-screen terminal UI for [Open Knowledge Format](https://github.com/serradura/okf-gem)
4
+ bundles: read one, switch between many, configure the registry, and search
5
+ across all of them at once. Built on the [TTY toolkit](https://ttytoolkit.org/components/).
6
+
7
+ Installing it teaches `okf` a verb. There is no second command to learn and
8
+ nothing else to set up:
9
+
10
+ ```bash
11
+ gem install okf-tui
12
+ okf tui # every bundle in your registry
13
+ okf tui path/to/bundle ... # those bundles, ad-hoc — the registry is left alone
14
+ okf tui @handbook # a registered bundle; bare @ is the default
15
+ okf tui @backend # a registry group, fanned out to its members
16
+ OKF_HOME=/tmp/scratch okf tui # a different global registry
17
+ okf help # lists `tui` under "installed extensions:"
18
+ ```
19
+
20
+ This gem ships **no executable of its own** — deliberately. A second binary that
21
+ only aliased `okf tui` would be one more name to install, document and keep
22
+ working, and two front ends is two argument grammars waiting to drift. `okf`
23
+ finds this gem because it ships `okf/plugin.rb`, the convention any gem can use
24
+ to add a verb — see [extension points](https://github.com/serradura/okf-gem/blob/main/.okf/design/extension-points.md).
25
+
26
+ The argument shape mirrors `okf server` exactly, because it is the same grammar:
27
+ `@slug` names a registered bundle, bare `@` the registry default, `@group` fans out
28
+ to its members, and `@all` is refused as `okf search`'s alone. Naming directories
29
+ is an ad-hoc look at them and never enrols them in the registry — registering stays
30
+ an explicit act, here the `a` key.
31
+
32
+ ## Six views
33
+
34
+ | View | Answers |
35
+ |------|---------|
36
+ | **1 bundles** | What can I open, which is active, which is the default — the registry's groups, and all registry config |
37
+ | **2 browse** | What is in the bundle, in reading order — `index.md`, `log.md`, then each directory |
38
+ | **3 search** | Which concept covers X — across *every bundle in scope*, ranked together, in whichever of okf's engines answers it |
39
+ | **4 graph** | What shape is its knowledge — narrow by type, tag or dir, or pick a concept to go read |
40
+ | **5 health** | Is it legal, well curated, and well structured — findings on the left (`validate`, `lint`, hubs), standing on the right (dir cohesion, the arcs between dirs, stats) |
41
+ | **6 help** | The keys |
42
+
43
+ Browse is where okf's §6 map lands too: `/index` narrows the list to the way in
44
+ each directory authored, one row per directory, and opens the authored file.
45
+
46
+ ## Two axes, kept apart
47
+
48
+ "Which bundle" is really two questions, and they move independently:
49
+
50
+ - the **active bundle** (`●`) — what browse, graph and health are about. `Enter`
51
+ on a bundle changes it.
52
+ - the **scope** (`◉`) — which bundles a search covers. `space` toggles one, `A`
53
+ all, `N` none, and `Enter` on a group makes it exactly that group's bundles.
54
+ `N` is how you start a fresh selection.
55
+
56
+ So you can read one bundle while searching all of them. Opening a search hit
57
+ that lives in a *different* bundle switches the active bundle to it.
58
+
59
+ The scope is also what the search corpus is built over, and it is built **once** and
60
+ held — the first query pays for it, the rest are effectively free. Changing the
61
+ scope or reloading drops it, because an index that outlives the set it was built
62
+ from gives a wrong answer rather than a slow one.
63
+
64
+ `e` chooses how the query is asked, because okf's two engines disagree by design
65
+ and each is wrong for what the other is right for:
66
+
67
+ | Mode | Engine | Good for |
68
+ |------|--------|----------|
69
+ | `fuzzy` | full-text index | ranked results, forgives typos |
70
+ | `text` | raw scan (okf's own default) | `$OKF_HOME`, `` `minifts` ``, `7.2.0` — terms the index tokenizer splits apart |
71
+ | `regexp` | raw scan | a pattern over the same text |
72
+
73
+ An unparseable pattern says so rather than quietly returning nothing.
74
+
75
+ Search runs **one index over every scoped bundle**, which is what makes the
76
+ scores comparable between them rather than only within one — the same thing
77
+ `okf search @all` does.
78
+
79
+ ## `/` looks through whatever has focus
80
+
81
+ One key, whichever thing the cursor is in:
82
+
83
+ - **a list** — filters it. Bundles by slug or path, browse by title, id, type or
84
+ tag, graph by type, tag or concept id.
85
+ - **a document** — the concept body, and the health and help pages — finds
86
+ within it and scrolls to the hit, with `n` / `N` stepping through the matches.
87
+
88
+ Nothing grabs the field on arrival, so `1`–`6` mean the same thing everywhere.
89
+ In search, `Enter` submits the query and `Esc` stops editing without leaving the
90
+ view.
91
+
92
+ When a browse filter matches nothing, `Enter` takes the term to the search view
93
+ and runs it across every bundle — the filter reads metadata in one bundle, and
94
+ search reads bodies across all of them.
95
+
96
+ ## `f` follows a link out of the page
97
+
98
+ A bundle is a graph, and its markdown links are the edges. `f` lists the ones
99
+ leaving the document you are reading — pick with `↑` `↓` and `Enter`, or press
100
+ `1`–`9` to go straight there. `Esc` puts the body back where you left it, and
101
+ `Backspace` returns you to wherever you jumped from.
102
+
103
+ It reads as what is at the far end, not as the path it was written with: a
104
+ concept by its title, a nested `index.md` by its area, and a link with nothing
105
+ behind it as knowledge not written yet. That last one is `lint`'s to report, not
106
+ an error to raise.
107
+
108
+ The reserved files are where this earns its keep — an `index.md` is a list of
109
+ links by design (§6) and `log.md` is a list of what changed where, so both become
110
+ menus into the bundle. The trail is shared, so opening a search hit or following
111
+ a concept out of the graph is undone by the same key.
112
+
113
+ ## The bundle wears its verdict
114
+
115
+ A bundle is clean, has lint warnings, or is not conformant, and that one
116
+ judgement drives its colour everywhere it is named — the header, the footer
117
+ badge, its row in the registry, its detail pane. The **health tab carries it
118
+ too**, so a problem is visible from any view without opening the tab.
119
+
120
+ ## Registry config
121
+
122
+ In the bundles view: `d` default, `a` add, `n` rename, `x` remove (asks first),
123
+ `r` reload. Each writes the registry file and re-reads from disk, so the screen
124
+ shows what the file now says. Removing a bundle never touches the bundle on
125
+ disk — the registry is a list of references.
126
+
127
+ It edits your *configuration*, never your knowledge: there is no bundle writing
128
+ anywhere in this gem. Authoring belongs to `okf` and its companion skill.
129
+
130
+ **Groups.** A registry group is a named, recursive set of bundles — which is to
131
+ say a named search scope. Groups list under their own heading, and `Enter` on one
132
+ makes a search cover exactly its bundles: the same set `okf search @backend`
133
+ merges into one ranking. The detail pane shows both the members the registry
134
+ records and the bundles they resolve to, since for a nested group those differ.
135
+
136
+ **The view has three panes**, and `Tab` cycles them: the registry's bundles, its
137
+ groups, and the detail of whichever of the two has focus. `Esc` steps back out one
138
+ pane at a time. Two panes means two selections at once, and that is what makes
139
+ editing direct — every key acts on a row you can see:
140
+
141
+ | Key | Pane | Does |
142
+ |-----|------|------|
143
+ | `Enter` | groups | make a search cover exactly this group's bundles |
144
+ | `+` | bundles | the bundle under this cursor joins the group selected below |
145
+ | `-` | members | remove the member under this cursor |
146
+ | `n` / `x` | groups | rename or delete the group; okf cascades through every member list |
147
+ | `c` | bundles | name the bundles now in scope as a new group |
148
+
149
+ Anything that can lose configuration asks first and names what it is about to do —
150
+ `x`, and `-`, which says when the member you are removing is the last one and the
151
+ group goes with it. `+`, `c` and `d` add or reorder, and do not ask.
152
+
153
+ **Which registry.** The same one every other `okf` verb run from the same
154
+ directory resolves: a project-local `.okf-registry.json` when one is on the path
155
+ up from here, and `$OKF_HOME` (default `~/.okf`) otherwise. `OKF_NO_DISCOVERY=1`
156
+ forces the global one. The header names the file it read, so there is never a
157
+ question which is in force. A workspace of directories named on the command line
158
+ has no registry, and says so rather than pretending to configure one.
159
+
160
+ ## It invents no analysis
161
+
162
+ `OKF::Bundle::Reader` and `OKF::Registry` are the only parts that touch disk;
163
+ everything on screen is a pure call on the resulting in-memory bundles —
164
+ `catalog`, `graph`, `validate`, `lint`, `directories`, `hubs`, `skeleton`,
165
+ `Bundle::Search`. The TUI is one more shell over the same core the `okf` CLI and
166
+ the graph server already use.
167
+
168
+ That is a rule with teeth, not a slogan: the dir facet's counts are checked in CI
169
+ against `okf dirs --json`, and the cohesion table against `okf graph --traffic`,
170
+ row for row. If okf and this disagree about a number, the suite fails.
171
+
172
+ It keeps okf's contracts visible too: `validate` and `lint` are separate
173
+ sections because they answer different questions, reads are best-effort so an
174
+ unreadable file is reported (`⊘ n unreadable`) rather than fatal, and a bad
175
+ directory exits `2`, the usage-error code.
176
+
177
+ ## Requirements
178
+
179
+ Ruby >= 2.4 — the same floor as okf, which takes it from rack: the tool should
180
+ run on whatever Ruby the OS already ships. `okf >= 2.0` is the other requirement,
181
+ and installing this gem pulls it in: every screen here answers with something the
182
+ kernel's OKF v0.2 work published, and against an older one each would fail
183
+ silently rather than loudly.
184
+
185
+ ## Development
186
+
187
+ `bin/setup` to install, `bundle exec rake` to run the tests and RuboCop.
188
+ `bundle exec okf tui` runs it from the checkout — okf is a bundle dependency, so
189
+ the verb resolves the same way it does for a user. See [AGENTS.md](AGENTS.md)
190
+ for the contracts a change has to keep.
191
+
192
+ ## License
193
+
194
+ Apache-2.0.