| name | writing-40ants-doc-documentation |
| description | Use when creating or updating documentation for Common Lisp projects that use (or should use) the 40ants-doc system with docs-builder for README, changelog, and HTML site generation. Apply when a project has no docs, has markdown/rst files that should be migrated to 40ants-doc, or when build-docs produces XREF warnings or "source location not found" errors. |
Writing 40ants-Doc Documentation
Overview
40ants-doc generates README.md, ChangeLog.md, and HTML documentation from Lisp source files. Each documentation project has its own ASDF system, with content defined via defsection, defchangelog, and defautodoc macros.
When to Use
- Creating documentation for a CL project from scratch
- Migrating existing README.md/ChangeLog.md to 40ants-doc
- Fixing
build-docs warnings (XREF, undocumented symbols, source location)
- Adding API reference autodocs to an existing 40ants-doc project
- Updating changelog after any functional change
Core Pattern
Project Structure
project/
├── project-docs.asd # ASDF system for docs (package-inferred, :pathname "docs")
├── docs/
│ ├── index.lisp # Main content sections + README mirror
│ └── changelog.lisp # Changelog entries
└── README.md # Generated by build-docs (auto-committed)
ASDF System (project-docs.asd)
(defsystem "project-docs"
:class :package-inferred-system
:pathname "docs"
:depends-on ("project"
"project-docs/index"))
Main Documentation (docs/index.lisp)
(uiop:define-package #:project-docs/index
(:use #:cl)
(:import-from #:pythonic-string-reader #:pythonic-string-syntax)
(:import-from #:named-readtables #:in-readtable)
(:import-from #:40ants-doc #:defsection #:defsection-copy)
(:import-from #:project-docs/changelog #:@changelog)
(:import-from #:docs-config #:docs-config)
(:import-from #:40ants-doc/autodoc #:defautodoc)
(:export #:@index #:@readme #:@changelog))
(in-package #:project-docs/index)
(in-readtable pythonic-string-syntax)
(defmethod docs-config ((system (eql (asdf:find-system "project-docs"))))
#+quicklisp (ql:quickload "40ants-doc-theme-40ants")
#-quicklisp (asdf:load-system "40ants-doc-theme-40ants")
(list :theme (find-symbol "40ANTS-THEME" (find-package "40ANTS-DOC-THEME-40ANTS"))))
(defsection @index (:title "Project Name — description."
:ignore-words ("ASDF" "API" "URL" "REPL" "CL" "OS" "HTTP" "JSON"))
(project system)
"""
Overview text...
"""
(@installation section)
(@api section))
(defsection-copy @readme @index)
(defautodoc @api (:system "project"
:ignore-packages ("project/internal")))
Changelog (docs/changelog.lisp)
(uiop:define-package #:project-docs/changelog
(:use #:cl)
(:import-from #:40ants-doc/changelog #:defchangelog))
(in-package #:project-docs/changelog)
(defchangelog (:ignore-words ("ASDF" "REPL"))
(1.0.0 2026-06-14
"* Initial public version."))
Build Prerequisites
.qlot/bin/build-docs is the entry point. If it is missing from the project,
install it once. The ros install target is the GitHub repository
40ants/docs-builder — the quicklisp system name 40ants-docs-builder is NOT
a valid ros install target and will fail with "is not a valid target":
qlot exec ros install 40ants/docs-builder
This creates .qlot/bin/build-docs.
Build
Run from the project root. Set CL_SOURCE_REGISTRY so ASDF resolves the local
project:
CL_SOURCE_REGISTRY=$(pwd)/ .qlot/bin/build-docs project-docs
Do not wrap .qlot/bin/build-docs in qlot exec — the script self-bootstraps
the qlot Lisp image, so qlot exec .qlot/bin/build-docs only prints a spurious
"No need to exec scripts in .qlot/bin/" warning.
Zero warnings required with default settings (error-on-warnings: true). If warnings remain, fix them at the source — do not silence with -e false.
Use -e false only as a temporary workaround during development:
CL_SOURCE_REGISTRY=$(pwd)/ .qlot/bin/build-docs -e false project-docs
MANDATORY: Rebuild and Verify After Every Documentation Change
After any change that can affect generated docs, you MUST run build-docs
and confirm a clean build before considering the task done. This is not optional
and must be done proactively — do not wait for the user to ask.
Rebuild whenever you touch any of:
docs/changelog.lisp (even a one-line changelog entry)
docs/index.lisp or any other docs/*.lisp section file
- a docstring of an exported symbol, or a newly added/removed export
(autodoc picks these up automatically)
:ignore-words / :ignore-packages settings
- anything that changes the set of documented symbols or their source locations
Verification checklist:
- Run
CL_SOURCE_REGISTRY=$(pwd)/ .qlot/bin/build-docs project-docs.
- Confirm the command exits 0 and the final log line is
Done.
- Confirm there are no warning/error lines, in particular:
Object referenced as #<XREF "..."> is not documented
No source location found for reference
These symbols are external, but not documented
- Confirm the generated files changed as expected via
git status / git diff — at minimum README.md and/or ChangeLog.md.
If a warning appears, fix the root cause (see Common Warnings and Fixes below)
and rebuild. Never leave a documentation change unverified.
Common Warnings and Fixes
| Warning | Root Cause | Fix |
|---|
Object referenced as #<XREF "OS"> is not documented | ALL-CAPS word in docstring or markdown that 40ants-doc tries to auto-link | Add the word to :ignore-words in the parent defsection |
These symbols are external, but not documented | Exported CL symbol without a docstring | Add a docstring to the symbol, OR exclude its package via :ignore-packages on defautodoc |
No source location found for reference | Generic function created implicitly (by :accessor or defmethod without defgeneric), OR loaded from stale fasl | Add explicit (defgeneric name (args) (:documentation "...")) before class/method definitions; clear fasl cache and rebuild |
+DEFAULT-FONT-SIZE+ (or similar) listed as undocumented | Exported defvar/defparameter without docstring | Add docstring: (defvar +foo+ 42 "Explanation.") |
:ignore-words Propagation
The :ignore-words set on @index propagates to all subsections, including @api when included via (@api section). Add commonly used ALL-CAPS terms (tool names, protocols, acronyms) there.
Preserve :external-docs
If a project already uses :external-docs on a defsection, do not remove it just to silence XREF warnings or to work around a local network/SSL problem. :external-docs powers cross-library references and is part of the documentation's functionality.
When warnings mention symbols from another library:
- first prefer fully-qualified references (
my-lib:foo instead of bare FOO) when the text refers to a documented external symbol;
- if the text is only mentioning a name and should not become a link, add it to
:ignore-words;
- treat local failures fetching external references as an environment issue unless CI reproduces them.
defautodoc Package Filtering
Use :ignore-packages to exclude internal packages from autodoc:
(defautodoc @api (:system "project"
:ignore-packages ("project/low-level" "project/objc")))
CRITICAL: Changelog Is for Functional Changes, Not Documentation
The changelog records functional, user-visible changes to the library or
tool: new features, bug fixes, behavior changes, breaking changes.
Never add changelog entries for documentation work. The changelog is itself
documentation, so writing about documentation in it is redundant noise. None of
these belong as changelog bullets:
- writing, updating, or reorganizing docs/sections (
README.md, docs/*.lisp)
- adding or editing docstrings
- fixing
build-docs / XREF / "source location" / "not documented" warnings
- regenerating
README.md / ChangeLog.md
Bullets must not start with "Documented ...", "Added a section",
"Fixed a warning", or similar. Rule of thumb: a user who runs the library but
never reads the docs should still find the changelog informative — if a change
only affects generated text, it does not go in the changelog. (A newly exported
public symbol is changelog-worthy only when it ships a new capability, not
because it got documented.)
After editing docs/changelog.lisp, always follow the
"MANDATORY: Rebuild and Verify After Every Documentation Change" workflow above —
a changelog edit only becomes visible once build-docs regenerates ChangeLog.md.
Use the defchangelog form with version, date, and markdown bullet points that
describe behavior, not documentation:
(0.2.0 2026-06-14
"* Added a :FOO argument to MAKE-THING to control the output size.
* Fixed MAKE-THING dropping the last item when :FOO was NIL.")
Images
40ants-doc copies images referenced via markdown syntax from the project root into docs/build/images/ automatically during build:

The source image should live at project-root/images/demo.gif.
Generated Files Policy
README.md and ChangeLog.md are generated artifacts. Regenerate them locally to validate documentation changes, but do not automatically include them in a pull request if the project's CI regenerates them itself and the team prefers to keep PRs focused on source docs (docs/*.lisp and code/docstrings).
Before committing:
- check whether the repository convention is to commit generated docs or let CI update them;
- if CI owns regeneration, revert generated-file diffs before pushing the branch;
- do not "fix" source-doc warnings by editing generated markdown directly.
When Sources Are Not Compiled After Changes
If build-docs does not pick up source changes (docstrings, defgeneric forms, etc.), the fasl cache is stale. See the clearing-asdf-fasl-cache skill.
Local Validation Without .qlot/bin/build-docs
If the project-local build-docs wrapper is missing but you have a live Lisp image with the needed systems loaded, you can still validate the docs source by loading the docs system and invoking the builder from the REPL. Use this as a local diagnostic aid, not as a reason to change project policy around generated files.