# ADR-013. When a module appeared is recorded, never computed **Status**: accepted, 7 September 2026. Still in force. ## Context `version_added` answers a question a user cannot settle any other way: *can I call this module with the collection version I pinned?* Every published module page carries it, `antsibull-docs` renders it, and the Ansible community inclusion checklist names it explicitly: > documentation and return sections use `version_added:` containing the > *collection* version for which an option, module or plugin was added Until 0.4.0 the generator wrote the **current** collection version into that field. Every generation therefore rewrote the history of every module it produced. Measured on the four published tags on 7 September 2026: | tag | what `instance_server_info` announced | when it actually appeared | |---|---|---| | `0.2.0` | `version_added: 0.2.0` | `0.1.0` | | `0.3.0` | `version_added: 0.3.0` | `0.1.0` | | `0.4.0` | `version_added: 0.4.0` | `0.1.0` | The measurement was made by looking, for each name, for the first published tag that carries it. It settled three counts that had been assumed rather than known: * all **50** modules of 0.4.0 exist since `0.1.0`, at the same path in all four tags. The issue that opened this work supposed `instance_server_info` had arrived in 0.2.0; that was wrong; * all **271** options were born with their module; * **8** return keys, all named `result`, appeared in `0.4.0`. They were added **beside** the existing key, not in its place, so they are genuine additions rather than a rename. The hand-written inventory plugin was correct throughout, carrying `version_added: 0.1.0`. That contrast is what shows the defect belongs to the generator and not to a decision anyone took. Three sources could have supplied the fact, and two were measured unusable: * **`changelogs/changelog.yaml`** declares modules per release only when a fragment says so. It names exactly one module under `0.1.0`, out of fifty. A partial record used as a complete one would have dated forty-nine modules wrong; * **git history** is true by construction, but it makes generation depend on the repository rather than on its inputs, and the CI checkout that runs `mise run check` fetches no tags. A control reading it would have degraded silently, which is the failure this repository spends its time refusing. ## Decision **The introduction version is recorded, never computed.** It lives in `generator/ansible/introductions.yml`, a journal read by the generator and by nothing else. The journal is a sequence of dated blocks. Its form carries its invariant: * **a name appears in exactly one block.** A name found in two blocks is an appearance being redated, and the loader refuses the file. The rule that a release never changes an existing `version_added` is therefore measured, not written in a comment; * **a block accepts only `modules`, `options` and `retours`.** An unknown key is refused rather than ignored, for the same reason an override refuses an unknown field: a typo would produce a silently inert record; * **an option or return dated on a module the journal does not know is an orphan**, and the file is refused. This mirrors the orphan override check; * **what is not yet dated receives `en_preparation`**, a value written by hand in the same file. That is a declared decision, not a guess, and `scripts/introductions.py` confronts it with the version the pending changelog fragments imply. A module always publishes its `version_added`. An option or a return publishes one **only when it is later than its module**: repeating the module's date on each of its options would print the same badge on every line of a page and teach nothing. ## Consequences **Adding a module is now two steps.** The generator dates an unknown module with `en_preparation`; the journal must then record it. A control lists what is covered by `en_preparation` rather than dated, so the gap is visible rather than silent. **The journal is a fourth thing to keep.** It joins the contracts, the overrides and the goldens. It is small, it is append-only, and its diff is the record of what a release adds, which makes it worth reading rather than merely maintaining. **Nothing repairs the pages already published.** Galaxy pages for 0.1.0 through 0.4.0 keep the wrong dates. They cannot be republished, and the correction starts at 0.5.0. **The two-step can be forgotten.** `en_preparation` is set by hand, and a stale value would stamp a new module with a version that never ships. The control compares it with what the changelog fragments imply, and says plainly when there are no fragments to compare against rather than passing in silence. ## What this record does not decide ADR-007 decides where a measured number lives, and it is why the counts above are here rather than in a comment. ADR-005 decides that generated documentation is refused rather than reported when it is unusable; a wrong `version_added` is not unusable documentation, it is false documentation, and that gate did not and could not see it. Whether a module should also declare its Ansible `attributes`, and what `version_added` those attributes carry, is not decided here.