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.

Examples:

Point an agent's own grep at the shipped tree

Magik::Docs.root # => #<Pathname:/…/gems/magik-0.0.1>

Read the DSL reference for screens

Magik::Docs.read("wiki/screens-and-components")

See Also:

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.gemspec reads it to build spec.files, and pages reads it to build the catalogue. Keeping one list means magik docs list can never offer a page that gem build left out.

Examples:

Magik::Docs::PACKAGED_GLOBS # => ["llms.txt", "docs/**/*.md", "wiki/**/*.md"]

Returns:

  • (Array<String>) —

    Dir.glob patterns

%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.

Returns:

  • (String)
"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.

Returns:

  • (Hash{String => String}) —

    path prefix => audience key

{
  "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.txt and docs/README.md, which route both readers.

Returns:

  • (String)
"both"
AUDIENCE_LABELS =

Human labels for each audience key, used by magik docs list.

Returns:

  • (Hash{String => String})
{
  "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.

Returns:

  • (Array<String>)
%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.

Returns:

  • (Integer)
180
EXTENSION =

File extensions slug_for strips when turning a shipped path into a slug.

Returns:

  • (Regexp)
/\.(?:md|markdown|txt)\z/i
HORIZONTAL_RULE =

A markdown horizontal rule, which is never a page summary.

Returns:

  • (Regexp)
/\A(?:-{3,}|\*{3,}|_{3,})\z/

Class Method Summary collapse

Class Method Details

.audience_for(path) ⇒ String

The audience a path serves, from AUDIENCE_BY_PREFIX.

Examples:

Magik::Docs.audience_for("wiki/Models.md") # => "app"

Parameters:

  • path (String) —

    a path relative to root

Returns:

  • (String) —

    "app", "framework" or "both"



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.

Examples:

Magik::Docs.candidates.first # => #<Pathname:/…/magik>

Returns:

  • (Array<Pathname>) —

    absolute directories, duplicates removed



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).expand_path }
    .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".

Examples:

Magik::Docs.find("ledgers")[:path] # => "wiki/Money-And-Ledgers.md"

Parameters:

  • slug_or_path (String, Symbol) —

    any of the forms above

Returns:

  • (Hash{Symbol => String}) —

    the catalogue entry

Raises:



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?

Examples:

Magik::Docs.packaged?(Magik.root) # => true

Parameters:

  • dir (Pathname, String) —

    a candidate root

Returns:

  • (Boolean)


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.

Examples:

Magik::Docs.pages.first
# => {slug: "docs/readme", title: "Magik — documentation map", …}

Returns:

  • (Array<Hash{Symbol => String}>) —

    frozen entries with keys :slug, :title, :path, :summary and :audience



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.

Examples:

Magik::Docs.read("error-codes").lines.first # => "# Error codes\n"

Parameters:

  • slug_or_path (String, Symbol) —

    anything find accepts

Returns:

  • (String) —

    the file's contents, verbatim

Raises:

  • (PageNotFoundError) —

    if the page is not in the catalogue or the file behind it has gone missing



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.

Examples:

Magik::Docs.relative_paths(Magik.root).include?("wiki/Models.md") # => true

Parameters:

  • dir (Pathname, String) —

    a root to look under

Returns:

  • (Array<String>) —

    e.g. ["docs/README.md", …, "llms.txt"]



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

This method returns an undefined value.

Forget the memoised root and pages.

Needed only when the tree moves underneath a live process — a test pointing ROOT_ENV somewhere else, or a vendored copy being swapped.

Examples:

ENV["MAGIK_DOCS_ROOT"] = fixture_dir
Magik::Docs.reset!


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.

Examples:

Magik::Docs.resolve_root([Pathname.new("/nope"), Magik.root])

Parameters:

  • dirs (Array<Pathname, String>) —

    candidate roots, in priority order

Returns:

  • (Pathname) —

    the first candidate holding documentation

Raises:



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.

Examples:

Hand the directory to a tool that greps

system("grep", "-rn", "ledger", Magik::Docs.root.to_s)

Returns:

  • (Pathname) —

    an absolute directory

Raises:



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.

Examples:

Magik::Docs.search("UUIDv7").first[:matches].first[:line] # => 42

Parameters:

  • term (String) —

    the substring to look for

Returns:

  • (Array<Hash{Symbol => Object}>) —

    catalogue entries with an extra :matches key: an array of {line: Integer, text: String, heading: Boolean}

Raises:



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.

Examples:

Magik::Docs.slug_for("docs/idea/02-dsl-surface.md") # => "docs/idea/02-dsl-surface"

Parameters:

  • path (String) —

    a path relative to root

Returns:

  • (String) —

    e.g. "wiki/models"



306
307
308
# File 'lib/magik/docs.rb', line 306

def slug_for(path)
  path.sub(EXTENSION, "").downcase
end