AGENTS.md@gems/okf-tui · diff
git:20260820.4775275 to git:20260820.3ce9341
78 added, 334 removed. Audit A to A.
# AGENTS.md
- Maintainer guide for okf-tui — `okf-tui` on RubyGems. A full-screen terminal UI
- over [Open Knowledge Format](https://github.com/serradura/okf-gem) bundles: read
- one, switch between many, configure the registry, and search across all of them
- at once. This file documents how to change the code without breaking its
- contracts.
-
- This gem is a sibling in the okf-gem monorepo, one directory per gem under
- `gems/`, beside the baseline `gems/okf/` it depends on.
- [`../../AGENTS.md`](../../AGENTS.md) is the repo-level
- guide and owns everything above a single gem — the layout, the PR shape, the
- release-title convention, the Git attribution rule. What is here is okf-tui's
- own: its floor, its dependency limits, its rendering and interaction contracts.
- Where the two overlap, the root is the general rule and this is the instance.
+ okf-tui — a full-screen terminal UI over OKF bundles: read one, switch between
+ many, configure the registry, search across all of them at once. A sibling in the
+ okf-gem monorepo, beside the baseline `gems/okf/` it depends on.
- ## Read the bundle first
+ **This file is context, routing and reference.** [`../../AGENTS.md`](../../AGENTS.md)
+ binds every change in the repo; what is below is okf-tui's own, and every
+ argument for it is in `.okf/` rather than here.
- **`.okf/` is this gem's structural documentation and its catalogue, and this
- file no longer restates them.** What the code is, where each responsibility
- lives, what the six views answer, which kernel calls back them, and how to add
- one all live there — once, in the concept that owns them:
+ ## Where to read
| you want | read |
| --- | --- |
- | what a file under `lib/` does | [`.okf/structure/`](.okf/structure/) — one concept per layer, and it names every file |
- | whether a capability already exists | [`.okf/capabilities/`](.okf/capabilities/) — the six views and the okf surface, before you build a seventh |
+ | what a file under `lib/` does | [`.okf/structure/`](.okf/structure/) — one concept per layer, naming every file |
+ | whether a view already answers this | [`.okf/capabilities/`](.okf/capabilities/) — six views and the okf surface |
| why a rule is a rule | [`.okf/decisions/`](.okf/decisions/), [`.okf/interaction/`](.okf/interaction/), [`.okf/rendering/`](.okf/rendering/) |
| how to add a view, a key or a panel | [`.okf/testing/adding-a-view.md`](.okf/testing/adding-a-view.md) |
-
- `okf server .okf` from this directory reads it as a graph; `okf search @okf-tui
- <term>` searches it from anywhere in the checkout.
-
- The split used to run the other way: this file carried a hand-written Map of
- `lib/**` and nothing checked it. `test/unit/bundle_catalog_test.rb` now fails
- when a file under `lib/` is named by no concept, when a concept names a file
- that is gone, or when the view catalogue and `App::TABS` disagree — so the
- structural layer is pinned where it lives, rather than trusted where nobody
- looks.
-
- ## One door
-
- This gem ships **no executable**. `lib/okf/plugin.rb` registers a `tui` command
- with okf's command registry, and `okf tui` is how a user gets here — there is no
- second name to install, document and keep working, and no second argument
- grammar to drift. There was an `exe/okf-tui` that did nothing but call the same
- `CLI.run`; it went before the first release, while removing a name still cost
- nobody anything, which is the same call okf-mcp made for the same reason. Adding
- one back needs an argument stronger than symmetry with other gems.
-
- Three consequences a reviewer checks, all of them pinned:
-
- * **The dispatcher adds nothing but argv and the streams.** `plugin_test.rb`
- drives the same run through `OKF::CLI.start` and straight into
- `OKF::TUI::CLI.run` and compares the exit code and the message.
- * **`plugin.rb` stays cheap.** okf reads it whenever a verb misses or `okf help`
- runs, so it registers a class and nothing else; the TTY toolkit is required
- inside `#call`, and a subprocess test asserts that loading the plugin leaves
- `TTY::Box` undefined.
- * **The advertisement and the behaviour stay in step.** `help_rows` read
- `tui [DIR|@slug…]` for a whole release while the CLI rejected every `@slug` as
- "not a directory"; the test now asserts the advertised form resolves.
-
- How the seam is put together — the borrowed ref grammar, the private helper it
- reaches, what loads when — is
- [`.okf/structure/doors.md`](.okf/structure/doors.md).
-
- ## Hard constraints
-
- 1. **It invents no analysis.** okf owns the format, the model, and every
- question this renders — `catalog`, `graph`, `validate`, `lint`, `directories`,
- `hubs`, `skeleton`, `Bundle::Search`, `Registry`. A question the TUI cannot
- answer by asking okf is a question it has no business answering. Reaching past
- the library to parse markdown, walk a directory, or re-derive a count is the
- one change to refuse outright.
-
- **It targets OKF v0.2, and reads v0.1 as well as okf does.** okf's own rule,
- and it decides every §5 surface here: v0.2 only added optional keys, so a
- bundle that adopted none must not read as deficient — no empty columns, and no
- row saying "unverified" about a family it never had. Two consequences worth
- stating, because both were arrived at by getting them wrong first. A *derived*
- value is not a declared one: §5.3 gives every unverified concept a tier, and
- claiming it would be the false provenance the trust system exists to prevent,
- so `Bundle::RowFilter.shows_trust?` gates the chip, the facet, its counts and
- its narrowing alike — one predicate, or the facet promises rows it will not
- return. And a version is a thing the bundle *says*: `Bundle#okf_version`, never
- a literal. The health view told every reader "legal OKF v0.1" for a release,
- about migrated bundles included.
-
- **The corollary that keeps biting: when okf renames something, this breaks
- silently.** `area` became `top_dir` in 1.12.0 and every bundle then reported
- one directory, in two places, for a release — no exception, no empty screen,
- just a wrong number that looked like a right one. It happened again with v0.2:
- the catalog's `timestamp` column was removed for `generated_at`, and browse's
- guard was `unless item[:timestamp].to_s.empty?` — so a missing key read as a
- concept with no date and the "updated" row silently stopped rendering, with a
- green suite either side of it. So a derived okf field read here
- wants a test that would notice, and the strongest form is an *agreement*
- test: ask okf the same question and compare. `dirs_test.rb` checks the dir
- facet against `okf dirs --json`'s subtree column and `structure_test.rb` checks
- the traffic section against `okf graph --traffic` row for row, both in-process.
- That catches a drift a formula copied into a comment never would.
-
- 2. **Ruby >= 2.4**, the same floor as okf, which takes it from rack: the tool
- should run on whatever Ruby the OS already ships. RuboCop parses at 2.4 and
- catches syntax, but **not APIs**. Do not introduce: `Struct.new(keyword_init:)`,
- `delete_prefix`/`delete_suffix`, `transform_keys`, `Dir.children`,
- `yield_self` (2.5); `to_h { }`, `then`, `rescue`/`ensure` directly inside a
- `do…end` block, endless string slices `str[i..]` (2.6); `filter_map`, `tally`,
- numbered block params (2.7); endless methods, hash shorthand (3.x).
-
- The truth test — it copies the tree and drops `Gemfile.lock`, because a
- lockfile written by a modern Bundler is one 2.4's own cannot read, and
- mounting the checkout read-only keeps the run from writing one back. **Run it
- from the repo root**, and let it step in here: the Gemfile resolves okf from
- `../okf`, so a container holding only this directory fails at `bundle
- install` before a test runs.
-
- ```bash
- docker run --rm -v "$PWD":/src:ro ruby:2.4 bash -c \
- "cp -a /src /build && cd /build/gems/okf-tui && rm -f Gemfile.lock && bundle install --quiet && bundle exec rake test"
- ```
-
- 3. **No version ceilings for the floor's sake.** `kramdown` and `rouge` have
- lines that need a newer Ruby than 2.4 — but they declare that themselves, so
- resolution already picks the newest each Ruby accepts. Pinning the old lines
- for everyone was tried first and broke on modern Ruby, where kramdown 2.4
- calls a `CGI` method that no longer exists. A ceiling here has to be justified
- by an incompatibility that resolution cannot see.
-
- 4. **The runtime dependencies are okf plus the TTY toolkit** — one gem per job
- (`pastel`, `tty-box`, `tty-cursor`, `tty-markdown`, `tty-reader`,
- `tty-screen`) and no more. A seventh TTY gem is a design decision, not a
- convenience; challenge it. Nothing in `lib/` may require a gem the gemspec
- does not name.
-
- 5. **Width is measured on the ANSI-stripped text, always.** A composed terminal
- UI breaks the moment a row's display width disagrees with `String#length`,
- which is exactly what colour causes. `Ui::Line` builds a row segment by
- segment, tracking columns spent, and clips a segment *before* colouring it;
- panes are squared off to an exact rectangle before being joined, in both
- directions — a short row smears the pane beside it, a long one wraps and
- shoves the frame down. New rendering goes through `Ui`, never around it.
-
- 6. **tty-markdown is never asked to wrap.** Its wrapper (the `strings` gem)
- miscounts ANSI escapes and raises `IndexError` from `String#insert` on
- coloured input. `PARSE_WIDTH` is a width it can never reach, and `Ui.reflow`
- does the wrapping instead. Related: the colour mode is passed to it
- explicitly rather than sniffed from the terminal, which is what lets a test
- turn colour on — see below.
-
- 7. **A layout that cannot fit says so rather than clipping.** Health is two panes
- above 112 columns and one pane at a time below it, reached by the same `Tab`
- either way — the key keeps its meaning and the narrow case stops pretending both
- halves fit. Judge a new split by what its *narrowest* column does to the longest
- row it must carry: health's right pane holds a fixed width because every row in
- it is short by construction, and the rows that carry paths are all on the left.
-
- 8. **A view returns rows; it never writes to the terminal.** `Views` is pure, so
- a frame is a value a test can assert on. Only `App#paint` prints.
-
- 9. **The registry is the user's configuration.** Every write goes through
- `Workspace` and is followed by a reload, so the screen shows what the file
- now says rather than what memory believes. Tests never touch the real
- `~/.okf` — `with_registry` builds one under a temporary `$OKF_HOME`, and
- `with_local_registry` builds a project-local `.okf-registry.json` with an empty
- global one beside it, so a test can prove *which* of the two a run resolved.
-
- **The line is the registry, not read-versus-write.** The TUI edits the user's
- own configuration freely — `a` registers, `x` removes, `d` sets the default,
- `n` renames, `c`/`+`/`-` build and edit groups, seven of okf's eight `registry`
- verbs — and never writes a bundle: there is no `Bundle::Writer` in `lib/`, and
- authoring belongs to okf's CLI and skill. Judge a proposed write against that
- line, and let okf own the cascades: a group rename reaching every member list
- is `registry.rename`'s job, and the tests assert its effects rather than
- reproducing it.
-
- **A write that can lose configuration asks first, and names the consequence.**
- `x` always did; `-` shipped without it and deleted a group on one press. The
- question now names the outcome, because "remove a member" and "delete the group"
- are the same keystroke when that member is the last one.
-
- **Act on a row, not on a set the reader has to compute.** The first `-` removed
- `scope ∩ members` — a set with *no row on screen*. Every misreading of it was
- reasonable, and the one that got reported ("turn the scope off") was destructive.
- The fix that stuck was structural: **the bundles view has three panes**, so two
- rows are selected at once and every editing key names visible rows — `+` is the
- bundle under the bundles cursor joining the group selected below, `-` is the
- member under the members cursor. Only `c` still reads the scope, where naming
- what you have been searching together *is* the gesture.
-
- `-` did briefly act in the bundles pane too, as the inverse of `+`, and went back:
- the members pane already gives every member a row of its own, and one editing
- gesture with two homes is a wider surface than the flow needs. **A key that lost
- a pane still answers there** — it says which pane it lives in, because a key that
- quietly stopped working is indistinguishable from a broken one.
-
- **`+` has to change something visible, and what it changes is not the row.** An
- add that leaves the screen alone reads as an add that did not happen, which is how
- this was reported. A bundle row did carry the slug of the group selected below for
- a round, and it went: the column was relative to a cursor in *another pane*, so it
- changed as that cursor moved and read as noise while working in this one. **A
- column that answers a question the reader is not asking is worse than no column.**
- The membership lives in the detail pane instead (`in @docs @everything`), which
- names every group rather than whichever is selected, and the highlighted status
- line names both sides of the write.
-
- **The groups pane keeps its cursor when it does not have focus**, dimmed, because
- `+` reaches across to that row. The footer tried spelling the slug out instead
- (`+/- join/leave @onm`) and it read as misleading: it claimed a selection that
- nothing on the screen agreed with. A hint that names an off-screen target is worse
- than a vague one — the fix is to make the target visible and let the hint stay
- short.
-
- **A scope that *is* a group follows that group across an edit to it.** `◉` on a
- group row is set equality, so growing the group without growing the scope emptied
- the mark and left the bundle just added reading as out of scope — one write
- looking like two failures. `App#keeping_group_scoped` re-applies the scope after
- an edit, but only when the group was in force beforehand: re-scoping on every
- edit would replace a selection the reader made by hand, which is the worse
- surprise of the two.
-
- Related, and more general: **a hint must not borrow the vocabulary of a different
- mechanism** — `◉` means scope here, so labelling a registry write "scoped" is
- close to an instruction to press it. And **reuse a mechanism before adding one**:
- `Tab` already meant "switch pane" in browse and graph, so three panes cost a
- reader nothing new, and `Esc` peels them one at a time before the filter, exactly
- as [esc-peels-one-layer] requires.
-
- The same rule reaches the status row, which is app-wide: it is either **asking**
- you something or **telling** you something, and it wears yellow for the first and
- cyan for the second. A flash can afford the mark because `#handle` clears it on
- the very next key — it is on screen until the user does anything at all, and
- never longer. It rendered `bright_black` for a release, which made the one line
- reporting what just happened the quietest thing on the screen.
-
- Each pane owns a cursor (`@cursor`, `@group_cursor`, `@member_cursor`) and the
- groups pane owns a scroll of its own, or paging one list would drag the other.
- `#clamp_cursor` clamps all three and hands focus back when a pane empties, since
- every registry write rebuilds all of them. A key that belongs to another pane
- *says so* rather than going silent — a key that quietly stopped working is
- indistinguishable from a broken one — and `groups_test.rb` asserts that too.
-
- `okf registry init` is deliberately *not* offered, for a
- different reason than caution: the registry is resolved once at boot, so
- creating one mid-session would either show nothing or swap the whole workspace
- out from under every open view. That is a re-anchoring, not an edit.
+ | what each test layer can and cannot catch | [`.okf/testing/`](.okf/testing/) |
- **Which registry a session is on is a question with one answer.** Every other
- okf verb resolves a discovered `.okf-registry.json` before the global
- `$OKF_HOME` one; being the exception is a bug, and was one. `Workspace` takes
- `cwd:` to opt in and reloads through `Registry#reopen` — never
- `Registry.new(path)`, which drops the `relative_base` a local registry's
- portable relative paths are stored against.
+ `okf server .okf` reads it as a graph; `okf search @okf-tui <term>` from anywhere
+ in the checkout.
- ## Testing
+ ## The contract
- **Integration first.** `test/integration/` is the critical layer: it drives the
- app the way a user does — real keys, real frames, real exit codes. A unit test
- proves a method behaves; an integration test proves the *product* behaves. A
- change starts with a failing test there, red for the reason you predicted, then
- the code, then the same test green and unedited.
+ Nine rules. Each line is the whole of what you must hold; the link is why.
- What each file proves, the two fixtures built to reach a branch nothing else
- could, why the render sweep runs with colour *on*, and the two assertion traps
- this suite has already hit are all in
- [`.okf/testing/the-suite.md`](.okf/testing/the-suite.md). The walk a new view,
- key or panel owes is
- [`.okf/testing/adding-a-view.md`](.okf/testing/adding-a-view.md).
+ 1. **It invents no analysis.** okf owns the format, the model and every question
+ this renders. Reaching past the library to parse markdown, walk a directory or
+ re-derive a count is the one change to refuse outright —
+ [`.okf/decisions/invents-no-analysis.md`](.okf/decisions/invents-no-analysis.md).
+ 2. **A derived okf field wants a test that would notice when okf renames it.**
+ Every drift found so far shipped a wrong number with a green suite either
+ side; the strongest form is an agreement test against okf itself —
+ [`.okf/decisions/okf-capability-drift.md`](.okf/decisions/okf-capability-drift.md).
+ 3. **Ruby >= 2.4**, okf's floor, binding `test/` too. The forbidden API list is
+ `@okf design/ruby-floor`; what taking okf's floor costs this gem is
+ [`.okf/decisions/ruby-floor.md`](.okf/decisions/ruby-floor.md).
+ 4. **No version ceilings for the floor's sake** — the gems declare their own, so
+ resolution handles it per-Ruby —
+ [`.okf/decisions/no-version-ceilings.md`](.okf/decisions/no-version-ceilings.md).
+ 5. **Dependencies are okf plus the TTY toolkit**, one gem per job, and nothing
+ in `lib/` may require a gem the gemspec does not name —
+ [`.okf/decisions/undeclared-width-dependency.md`](.okf/decisions/undeclared-width-dependency.md).
+ 6. **Width is measured on ANSI-stripped text, always.** New rendering goes
+ through `Ui`, never around it —
+ [`.okf/rendering/ansi-aware-width.md`](.okf/rendering/ansi-aware-width.md).
+ 7. **tty-markdown is never asked to wrap** — `Ui.reflow` does it instead, and the
+ colour mode is passed explicitly rather than sniffed —
+ [`.okf/rendering/markdown-rendering-trap.md`](.okf/rendering/markdown-rendering-trap.md).
+ 8. **A layout that cannot fit says so rather than clipping**, and a new split is
+ judged by its narrowest column —
+ [`.okf/rendering/fit-or-say-so.md`](.okf/rendering/fit-or-say-so.md).
+ 9. **A view returns rows; only `App#paint` prints**, which is what makes a frame
+ a value a test can assert on —
+ [`.okf/rendering/whole-frame-painting.md`](.okf/rendering/whole-frame-painting.md).
- Three obligations that are this file's, because a reviewer checks them:
+ Two boundaries that decide whether a feature belongs here at all:
- - **Prove the check can fail.** Break the code on purpose and watch the test
- report it. A green suite that cannot go red is not verification.
- - **Run against the *published* okf before pushing anything that reads okf's
- analysis, and before a release.** No suite here does by default — the Gemfile
- resolves the checkout next door — and a released kernel resolves different
- analysis output, which is a difference no floor expresses.
+ - **The registry is the user's configuration, and that is the line** — this
+ program edits the registry, never the knowledge —
+ [`.okf/decisions/registry-write-boundary.md`](.okf/decisions/registry-write-boundary.md).
+ - **This gem ships no executable**; `okf tui` arrives through the kernel's plugin
+ seam — [`.okf/structure/doors.md`](.okf/structure/doors.md),
+ `@okf-eco decisions/one-door-per-sibling`.
- ```bash
- sed '/gem "okf", path:/d' Gemfile > Gemfile.ci-check
- BUNDLE_GEMFILE=Gemfile.ci-check bundle install && BUNDLE_GEMFILE=Gemfile.ci-check bundle exec rake
- ```
- - **A new file under `lib/` earns its line in `.okf/structure/`, and a new view
- its row in `.okf/capabilities/views.md`.** `bundle_catalog_test.rb` fails
- otherwise — the documentation is pinned, not trusted.
+ **Run the suite against the *published* okf** before pushing anything that reads
+ okf's analysis, and before a release. Nothing here does by default — the Gemfile
+ resolves the checkout next door — and a released kernel resolves different
+ analysis output, which is a difference no floor expresses.
## Commands
```bash
bin/setup # install dependencies
bundle exec rake # test + rubocop — the default task, what CI runs
bundle exec rake test # just the suite
- # there is no exe/ here — `okf tui` is the entry point, and the bundle resolves
- # okf from ../okf, so `bundle exec okf` is the checkout's own CLI
- bundle exec okf tui # the TUI from the checkout, on your registry
+ # no exe/ here — `okf tui` is the entry point, and `bundle exec okf` is the checkout's CLI
+ bundle exec okf tui # on your registry
bundle exec okf tui path/to/bundle # those bundles, ad-hoc
bundle exec okf tui @okf @mkt # a registered bundle, and a group
OKF_HOME=tmp/home bundle exec okf tui # a scratch global registry
OKF_NO_DISCOVERY=1 bundle exec okf tui # ignore a project-local .okf-registry.json
- # the suite against the *published* okf, which is what a user resolves — see Testing
+ # against the published okf — see the contract
sed '/gem "okf", path:/d' Gemfile > Gemfile.ci-check
BUNDLE_GEMFILE=Gemfile.ci-check bundle install && BUNDLE_GEMFILE=Gemfile.ci-check bundle exec rake
```
- From the repo root, `rake` runs every gem's default task including this one, and
- `rake test` every gem's suite — see [`../../AGENTS.md`](../../AGENTS.md).
+ The 2.4 floor, from the repo root so okf's checkout comes with it:
- CI (`../../.github/workflows/main.yml`) runs this gem's default task on every
- supported Ruby, 2.4 through the current stable, as its own `okf-tui` job with
- `working-directory: gems/okf-tui`. It is one job per gem rather than a gem axis on one
- matrix, because the floors diverge. A change is not done until that matrix is
- green.
+ ```bash
+ docker run --rm -v "$PWD":/src:ro ruby:2.4 bash -c \
+ "cp -a /src /build && cd /build/gems/okf-tui && rm -f Gemfile.lock && bundle install --quiet && bundle exec rake test"
+ ```
## Releasing
- A release is cut **from this directory** — `cd gems/okf-tui` first. Bundler reads the
- gemspec in its working directory and derives the tag from it, so the root `rake
- release` refuses rather than doing something plausible.
-
- 1. Bump `lib/okf/tui/version.rb` and move the `Unreleased` notes in
- `CHANGELOG.md` under the new version.
- 2. Move the gemspec's `okf` floor if the kernel bumped in the same cycle —
- `test/unit/gemspec_test.rb` fails until it does.
- 3. `bundle exec rake release` — tags **`okf-tui/vX.Y.Z`**, pushes commits + tag,
- pushes the gem to RubyGems (MFA required).
-
- **The tag prefix is load-bearing, not cosmetic.** The bare `v*` series belongs to
- the baseline gem, and the Docker workflow fires on `v*` — so a bare tag pushed
- from here would rebuild and republish the *okf* image, `:latest` included, from a
- release that ships something else. A glob does not match across `/`, which is the
- whole reason `Rakefile` sets `helper.tag_prefix = "okf-tui/"` before `#install`.
-
- `release:guard_clean` is repo-wide: Bundler runs `git diff` with no pathspec, so
- an edited file in a sibling gem blocks a release of this one, and all it says is
- "There are files that need to be committed first."
-
- Gem packaging detail: `spec.files` comes from `git ls-files` run with `chdir:`
- into this directory, minus `test/`, `bin/`, the Gemfile, the Rakefile,
- `.rubocop.yml`, `.gitignore`, `AGENTS.md` and `CLAUDE.md` — the last because it
- is one line pointing at the first, and a shipped pointer to a rejected file
- resolves to nothing for the reader of an installed gem. Everything at the repo
- root is invisible to it, so a new *root* file needs no reject — but a top-level
- file **here** ships unless the gemspec rejects it, so check `gem build` output
- when adding one. `.okf/` is not rejected on purpose: the gem ships its own knowledge
- bundle. And `LICENSE.txt` and `NOTICE` are real duplicates of the repo root's,
- never symlinks — `gem build` packages a symlink as a symlink, RubyGems >= 3.2
- refuses to extract one, and older RubyGems (which the 2.4 end of this matrix
- runs) installs it dangling, so the gem ships with no licence and exits 0.
- `test/unit/packaging_test.rb` pins all of those claims.
+ From this directory — `cd gems/okf-tui` first; the root `rake release` refuses.
- The PR that carries a version bump is a release PR, and takes the repo-level
- shape: the `release` label, the title `Release okf-tui X.Y.Z — <summary>`, and
- the body skeleton. [`../../AGENTS.md`](../../AGENTS.md) owns all three.
+ 1. Bump `lib/okf/tui/version.rb`, move `CHANGELOG.md`'s `Unreleased` notes.
+ 2. Move the gemspec's `okf` floor if the kernel bumped — `test/unit/gemspec_test.rb`
+ fails until it does.
+ 3. `bundle exec rake release` — tags **`okf-tui/vX.Y.Z`**.
- ## Git
+ **The tag prefix is load-bearing**: a bare tag from here would republish the
+ *okf* Docker image, `:latest` included, from a release that ships something else.
+ `Rakefile` sets `helper.tag_prefix` before `#install` — `@okf-eco
+ decisions/release-and-tags`. The PR shape is `@okf-eco design/pull-requests`.
- Commits are attributed to the human maintainer only — no AI co-author trailers,
- no "generated by" lines, in commits or PRs.
+ A new top-level file here ships unless the gemspec rejects it; `.okf/` is not
+ rejected on purpose, and `LICENSE.txt`/`NOTICE` are real duplicates rather than
+ symlinks. `test/unit/packaging_test.rb` pins all of it.
- ## Working style
+ ## Its own bundle
- - **Think before coding.** State assumptions; if the request is ambiguous, name
- the interpretations instead of picking one silently; push back when a simpler
- approach exists.
- - **Simplicity first.** Minimum code that solves the problem — no speculative
- flexibility, no abstractions for single-use code.
- - **Surgical changes.** Match the existing style (see `.rubocop.yml` — spaced
- array brackets `[ 1, 2 ]`, double quotes). Don't improve adjacent code; remove
- only orphans your own change created.
- - **Verify against a goal.** Turn every task into a check that can fail, and
- prove it can: break the code on purpose and watch the test report it. A green
- suite that cannot go red is not verification. "Works on my Ruby" is not either
- — the floor is.
+ `.okf/` ships inside the gem and `rake okf` at the repo root keeps it clean.
+ Maintain it in the same commit as the code. A file under `lib/` with no concept
+ naming it is a red suite, and so is a view in `App::TABS` with no catalogue row.