Bug fixes, documentation improvements, and focused feature proposals are all welcome.
One thing to know before you start: the JSON DocShell writes is consumed by renderers in other repositories. A change to an artifact shape is not a local refactor — it is a breaking change to a contract, and it needs to be treated that way. Everything else is ordinary library work.
Reporting bugs
Check the existing issues first, then include:
- your Elixir and OTP versions (
elixir --version) - minimal reproduction steps
- the full error message and stacktrace
For artifact problems, the offending JSON fragment is worth more than a description of it.
Suggesting features
Open an issue describing the use case rather than the implementation. DocShell turns down proposals that pull renderer concerns, authorization policy, or host-specific taxonomy into the package, so leading with the problem tends to find a better answer than leading with a patch.
Development setup
git clone https://github.com/futhr/doc_shell.git
cd doc_shell
mix setup
mix test
The pinned toolchain is in .tool-versions; mise install or asdf install
will fetch it.
Workflow
mix setup # fetch and compile
mix test # run the suite
mix test.watch # re-run affected tests on save
mix test.cover # run the suite with coverage
mix test.cover.html # coverage report in cover/
mix lint # format check, credo, dialyzer
mix check # the full gate, as CI runs it
mix ci # setup + lint + coverage
mix docs # build the documentation
mix bench # run every benchmark
mix bench.ast # run one
Benchmarks write Markdown reports to bench/output/, which are published as
the Performance section of the documentation and are committed to the repo. If
you change a hot path — parsing, projection, or artifact encoding — re-run the
suites and commit the updated reports.
mix check runs everything below in one pass. Run it before opening a pull
request.
| Tool | What it enforces |
|---|---|
mix format --check-formatted | Formatting, including doctests |
mix compile --warnings-as-errors | No compiler warnings |
MIX_ENV=no_optional mix compile --no-optional-deps | Compiles without AshOaskit or Plug |
mix credo --strict | Style, complexity, nesting depth |
mix doctor --summary | Documentation and typespec coverage |
mix coveralls | Test coverage, floor 95% |
mix hex.audit / mix deps.audit | Retired packages and advisories |
mix dialyzer | Type inconsistencies |
mix docs --warnings-as-errors | HTML, Markdown, and EPUB all build cleanly |
The no-optional-deps compile matters more than it looks. DocShell.Web.Plug
and DocShell.Web.Controller are compiled conditionally, and the AshOaskit
adapter resolves its library at runtime. Without that check, a compile-time
reference slips in and breaks every project that does not install the optional
dependency.
Code standards
- Document every public module, function, callback, and struct. Moduledocs should explain what the module is for and why it works the way it does — a restatement of the module name is not documentation.
- Leave the
mix docsformatters alone. Pinning them to["html"]drops the Markdown formatter, and with itllms.txtand the per-module.mdfiles that machine readers use. - Give every public function a
@spec. - Prefer pattern matching, guards, and small total functions. Credo caps conditional nesting at two levels.
- Return tagged tuples at boundaries. Raise for programmer error, not for malformed input or a failing integration.
- Preserve native JSON values through normalization — a number should not arrive at a renderer as a string.
- Add a regression test with every bug fix and a behaviour test with every feature.
- Do not exclude production code from coverage. Test the behaviour or delete the unreachable branch.
Test layout
test/ mirrors lib/ exactly: every module has one test module at the
matching path, so lib/doc_shell/web/plug.ex is tested by
test/doc_shell/web/plug_test.exs and nowhere else. Adding a module means
adding its test file.
A module whose documentation contains iex> examples must be covered by
doctest in that test module, so the examples are executed rather than merely
read.
Property tests belong in a describe "properties" block inside the module's
own test file, using StreamData. They are the
right tool for the normalization and round-trip code, where the interesting
inputs are the ones nobody thinks to write by hand.
Changing the artifact contract
The shapes in the artifact contract notebook are
public API. Adding an optional field is usually safe. Renaming one, removing
one, or changing its type is not, and needs a schema_version bump plus a
coordinated release across every producer and renderer.
If you are unsure which side of that line a change falls on, open an issue before writing it.
Commits
Conventional Commits, with an imperative sentence as the subject:
<type>(<scope>): <imperative sentence>feat(build): add the renderer-neutral artifact pipeline
fix(cache): preserve the last valid snapshot when reload fails
docs(usage): explain optional host integrations
test(presentation): cover invalid projector resultsUse the narrowest accurate type and scope. Keep each commit to one coherent, reviewable change, and commit tests alongside the behaviour they verify. These subjects are what git_ops turns into the changelog, so write them for the person reading the release notes.
Pull requests
- Fork and branch (
git checkout -b feat/some-feature) - Make the change, with tests
- Run
mix check - Open a PR describing what changed and why
Releases
Maintainers only, via git_ops. CHANGELOG.md is
generated from Conventional Commit subjects — do not hand-edit it, and do not
create it by hand: --initial refuses to run if the file already exists.
The first release creates the changelog and takes the version from mix.exs:
mix git_ops.release --initial
Then add CHANGELOG.md to docs.extras and package.files in mix.exs,
which are deliberately without it until the file exists. After that:
mix checkmix release— updates the changelog, bumps the version, commits, and tagsgit push --follow-tagsmix hex.publish
Questions
Open an issue. Questions about whether something belongs in DocShell at all are especially welcome — that boundary is the most useful thing about the package, and it is worth defending together.