fix(generator)+feat(code): page anchor once per file; Code infers lexer from filename

Merged
MH
2mo ago
+139-17

Closes #77

Summary

  • docs_kit:page injected the registry line once per group. #registry_anchor returned a Regexp (page line not followed by a page line), and Thor's inject_into_file replaces every match of a Regexp after: — so a grouped registry got one copy per group. The anchor is now the file's last page line as a String: one match, end of the last group.
  • DocsUI::Code infers the lexer from filename:. lexer: now defaults to nil; resolution is explicit lexer: → guess from the filename via Rouge's globs → ruby. config/deploy.yml → yaml, Dockerfile → docker, *.sh → shell. Unguessable filename / no filename → ruby as before.
  • Docs: README authoring convention, the Authoring + Components pages, the generated AGENTS.md template, and CHANGELOG.

Test plan

  • spec/generators/page_generator_spec.rb — grouped fixture (two groups, blank line + comment): exactly one line added after the last page line of the file; second run idempotent.
  • spec/docs_ui/code_spec.rb — .yml → data-md-lang="yaml" with no err spans; Dockerfile → docker; explicit lexer: wins over the filename; unguessable filename → ruby; no args → ruby.
  • bundle exec rspec — 953 examples, 0 failures (94.7% line coverage)
  • bundle exec rubocop — no offenses
  • Manual (post-merge, dash docs/ with a path gem): bin/rails g docs_kit:page "Scratch" --group=Deploying twice → one line at the end of the last group

Deviations & judgment calls

  • Rouge::Lexer.guesses instead of Rouge::Lexer.guess. The issue named guess(filename:) with an Ambiguous rescue, but guess answers PlainText on a no-match (e.g. filename: "notes"), which is indistinguishable from a real guess and would have turned unguessable filenames into plaintext instead of the required ruby fallback. guesses returns an array — exactly one hit is a guess, zero or several is nil — and needs no rescue.
  • No code_filename_lexers config knob. The issue floated it as a possible follow-up for extension-less files (.dash/secrets); those still work with an explicit lexer:, so I didn't add config surface nobody has asked for yet.
  • No --sync migration. The AGENTS.md template change is a content edit an existing site picks up via the normal install/sync path; nothing new to install.

https://claude.ai/code/session_01KsqvKPn1c1hWu6NSEQHy1W


Summary by cubic

Fixes the docs_kit:page generator adding a registry entry once per group instead of once per file, and makes DocsUI::Code infer the lexer from filename: when no lexer: is given.

Bug Fixes

  • docs_kit:page now anchors on the file's last page line as a string, so grouped registries get exactly one new entry per run.
  • A second run is idempotent — no duplicate entries are added.

New Features

  • DocsUI::Code resolves the lexer as explicit lexer: → filename guess via Rouge → ruby; unguessable filenames and no filename stay ruby.
  • Explicit lexer: always overrides the filename guess.
  • Docs, README, and the generated AGENTS.md template now describe the new behavior; no migration step is needed—existing sites pick up the template change via the normal install/sync path.

Closes #77.