Module: Magik::Docs
- Defined in:
- lib/magik/docs.rb
Overview
The documentation Magik ships inside its own gem, and the resolver that reads it.
Magik invents a DSL — model, screen, action, ledger, channel,
flow — that no language model has in its training data. An agent writing
Magik code without the reference in front of it reconstructs a Rails-shaped
API from memory, every time. Fetching the reference over the network is
slow, rate-limited and version-blind: it returns whatever is on main,
not what the installed gem actually does.
So the gem carries its own manual. Magik::Docs locates it, catalogues it
and searches it, using nothing but the standard library — the speed comes
from there being a few dozen small markdown files, not from an index.
This is not a spec-only subsystem. Every method below does what it says.
Defined Under Namespace
Classes: AmbiguousPageError, MissingTermError, PageNotFoundError, UnavailableError
Constant Summary collapse
- PACKAGED_GLOBS =
The globs, relative to the repository or gem root, that define which files are documentation.
This constant is the single source of truth for the decision:
magik.gemspecreads it to buildspec.files, and pages reads it to build the catalogue. Keeping one list meansmagik docs listcan never offer a page thatgem buildleft out. %w[ llms.txt docs/**/*.md wiki/**/*.md ].freeze
- ROOT_ENV =
Environment variable that overrides root resolution.
Set it when the docs live somewhere the candidate search cannot guess — a vendored copy, or a test fixture.
"MAGIK_DOCS_ROOT"- AUDIENCE_BY_PREFIX =
Audience, derived from the tree a page lives in. The three trees already have distinct jobs (see
docs/README.md), so the audience is a property of the directory and never a hand-maintained per-file list. { "wiki/" => "app", "docs/ops/" => "app", "docs/architecture/" => "framework", "docs/idea/" => "both" }.freeze
- DEFAULT_AUDIENCE =
Audience for a page that matches no prefix in AUDIENCE_BY_PREFIX — the index files,
llms.txtanddocs/README.md, which route both readers. "both"- AUDIENCE_LABELS =
Human labels for each audience key, used by
magik docs list. { "app" => "Building an app with Magik", "both" => "Both audiences — the design and the reference", "framework" => "Contributing to Magik itself" }.freeze
- AUDIENCE_ORDER =
The order audiences are printed and grouped in.
%w[app both framework].freeze
- SUMMARY_LIMIT =
Longest summary pages will derive from a page's opening paragraph, in characters, before it is truncated on a word boundary.
180- EXTENSION =
File extensions slug_for strips when turning a shipped path into a slug.
/\.(?:md|markdown|txt)\z/i- HORIZONTAL_RULE =
A markdown horizontal rule, which is never a page summary.
/\A(?:-{3,}|\*{3,}|_{3,})\z/
Class Method Summary collapse
-
.audience_for(path) ⇒ String
The audience a path serves, from AUDIENCE_BY_PREFIX.
-
.candidates ⇒ Array<Pathname>
Candidate roots, in resolution order, whether or not they contain docs.
-
.find(slug_or_path) ⇒ Hash{Symbol => String}
Look a page up by slug, by shipped path, or by an unambiguous shorthand.
-
.packaged?(dir) ⇒ Boolean
Does this directory hold at least one file matched by PACKAGED_GLOBS?.
-
.pages ⇒ Array<Hash{Symbol => String}>
The catalogue: one entry per shipped page, derived from the files themselves.
-
.read(slug_or_path) ⇒ String
The raw markdown of a page.
-
.relative_paths(dir) ⇒ Array<String>
Every packaged documentation file under
dir, as sorted paths relative to it. -
.reset! ⇒ void
Forget the memoised Docs.root and Docs.pages.
-
.resolve_root(dirs) ⇒ Pathname
Pick the first candidate that contains at least one packaged page.
-
.root ⇒ Pathname
The root of the shipped documentation tree — the directory that has
docs/,wiki/andllms.txtinside it. -
.search(term) ⇒ Array<Hash{Symbol => Object}>
Substring search across every shipped page, case-insensitively.
-
.slug_for(path) ⇒ String
The stable slug for a shipped path: the path, lowercased, with its documentation extension removed.
Class Method Details
.audience_for(path) ⇒ String
The audience a path serves, from AUDIENCE_BY_PREFIX.
295 296 297 |
# File 'lib/magik/docs.rb', line 295 def audience_for(path) AUDIENCE_BY_PREFIX.find { |prefix, _| path.start_with?(prefix) }&.last || DEFAULT_AUDIENCE end |
.candidates ⇒ Array<Pathname>
Candidate roots, in resolution order, whether or not they contain docs.
152 153 154 155 156 157 |
# File 'lib/magik/docs.rb', line 152 def candidates [ENV.fetch(ROOT_ENV, nil), Magik.root, gem_path] .compact .map { |dir| Pathname.new(dir.to_s). } .uniq end |
.find(slug_or_path) ⇒ Hash{Symbol => String}
Look a page up by slug, by shipped path, or by an unambiguous shorthand.
Accepted for wiki/Models.md: "wiki/models" (the slug),
"wiki/Models.md" (the path) and "models" (the basename). A numeric
prefix is optional, so docs/idea/02-dsl-surface.md also answers to
"dsl-surface".
227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/magik/docs.rb', line 227 def find(slug_or_path) query = normalize(slug_or_path) exact = pages.find { |page| page[:slug] == query } return exact if exact matched = pages.select { |page| shorthands(page).include?(query) } return matched.first if matched.size == 1 raise AmbiguousPageError, ambiguous_cause(slug_or_path, matched) if matched.size > 1 raise PageNotFoundError, missing_cause(slug_or_path) end |
.packaged?(dir) ⇒ Boolean
Does this directory hold at least one file matched by PACKAGED_GLOBS?
183 184 185 |
# File 'lib/magik/docs.rb', line 183 def packaged?(dir) !relative_paths(dir).empty? end |
.pages ⇒ Array<Hash{Symbol => String}>
The catalogue: one entry per shipped page, derived from the files
themselves. Nothing here is hand-maintained — add a markdown file under
docs/ or wiki/ and it appears.
210 211 212 |
# File 'lib/magik/docs.rb', line 210 def pages @pages ||= relative_paths(root).map { |path| build_page(path) }.sort_by { |page| page[:slug] }.freeze end |
.read(slug_or_path) ⇒ String
The raw markdown of a page. Raw on purpose: the reader is an agent, not a terminal, and rendering only removes information.
248 249 250 |
# File 'lib/magik/docs.rb', line 248 def read(slug_or_path) page_body(find(slug_or_path)) end |
.relative_paths(dir) ⇒ Array<String>
Every packaged documentation file under dir, as sorted paths relative
to it.
194 195 196 197 198 199 |
# File 'lib/magik/docs.rb', line 194 def relative_paths(dir) base = Pathname.new(dir.to_s) return [] unless base.directory? Dir.glob(PACKAGED_GLOBS, base: base.to_s).select { |path| base.join(path).file? }.sort end |
.reset! ⇒ void
284 285 286 287 |
# File 'lib/magik/docs.rb', line 284 def reset! @root = nil @pages = nil end |
.resolve_root(dirs) ⇒ Pathname
Pick the first candidate that contains at least one packaged page.
Exposed so both layouts — a checkout and an installed gem — can be exercised directly by a test without changing the process environment.
169 170 171 172 173 174 175 |
# File 'lib/magik/docs.rb', line 169 def resolve_root(dirs) found = Array(dirs).map { |dir| Pathname.new(dir.to_s) }.find { |dir| packaged?(dir) } return found if found raise UnavailableError, "no shipped documentation found under #{Array(dirs).map(&:to_s).inspect}" end |
.root ⇒ Pathname
The root of the shipped documentation tree — the directory that has
docs/, wiki/ and llms.txt inside it.
Resolution order: the ROOT_ENV override, then Magik.root (which is
the checkout root when Magik is loaded from source and the gem root when
it is installed), then the directory RubyGems recorded for the loaded
magik gem. The first candidate that actually contains a packaged page
wins, so a layout with no docs is skipped rather than returned empty.
143 144 145 |
# File 'lib/magik/docs.rb', line 143 def root @root ||= resolve_root(candidates) end |
.search(term) ⇒ Array<Hash{Symbol => Object}>
Substring search across every shipped page, case-insensitively.
There is no index and no dependency: a few dozen small files are read in full, which is faster than any network round trip an agent would otherwise make. Pages whose headings match sort first, then pages with more hits, then alphabetically — so the answer is deterministic.
266 267 268 269 270 271 272 273 |
# File 'lib/magik/docs.rb', line 266 def search(term) needle = term.to_s.strip raise MissingTermError, "`magik docs search` was given no term to search for" if needle.empty? needle = needle.downcase pages.filter_map { |page| matched_page(page, needle) } .sort_by { |result| sort_key(result) } end |
.slug_for(path) ⇒ String
The stable slug for a shipped path: the path, lowercased, with its documentation extension removed. Unique by construction, and typeable.
306 307 308 |
# File 'lib/magik/docs.rb', line 306 def slug_for(path) path.sub(EXTENSION, "").downcase end |