# ADR-0010: Contract of the code metrics and coverage badges workflow ## Metadata | Key | Value | | --- | --- | | ID | ADR-0010 | | CrossReference | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | ## Version History | Date | Status | Author | Reviewer | Change | Commit | | --- | --- | --- | --- | --- | --- | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; reviewed (RC-072) | pending | | 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | The report reaches the workflow as an artifact of the caller's run (input `coverage-artifact`), because a called workflow runs in its own job; reviewed (RC-073) | pending | Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`. --- ## Context MIL-010 adds a reusable workflow that shows the size of a repository's code and its test coverage as badges (UC-008, US-001.18 to US-001.20). ADR-0005 defines the contract of the quality-check workflow: inputs with working defaults, a pinned version per tool, a failure that names the cause, and a script file of this repository, fetched at `tools-ref`, that the workflow runs. Decisions the user took in chat on 2026-10-14 bound this one: the counter is a Python script of this repository with no dependency outside the standard library, the delivery is a reusable workflow, the badges are committed to a `badges` branch, there are three badges (code files, lines of code, coverage), and the coverage report is read in the Cobertura XML format. Options for the counter: 1. **Wrap `cloc`, `tokei` or `scc`.** Many languages, mature rules, but a tool to install and pin on every run and a download from outside the instance. 2. **A Python script of this repository.** No install, fully tested offline, and the rules are ours; fewer languages and simple scanning rules. Options for the delivery of the badges: a run artifact (disappears and has no stable URL), a file on the default branch (a commit per change on the branch that protects the code), or a dedicated branch (stable raw URL, no effect on the default branch). Facts: the job of a reusable workflow runs on `ubuntu-latest` with `python3` 3.12; a push made with the token of a run does not start another run; a workflow checks the shared scripts out into the workspace (`shared-scripts`), so the counter must not count them. ## Decision A reusable workflow `code-metrics.yml` runs three script files of this repository, fetched at its input `tools-ref` (default `v1`): `tools/count_loc.py` counts the code, `tools/badges.py` reads the coverage report and writes the badges, and `tools/code-metrics.sh` validates the inputs, calls both and publishes the badge files to the `badges` branch of the calling repository with the token of the run (`contents: write`, no other permission). The rules that follow from it: - **Code file.** A file under `working-directory` whose extension is in the language table below, that is not excluded, and that contains no NUL byte (it is text; it is read as UTF-8 and invalid bytes are replaced). Its language comes from the extension. A file with a code extension but no line is still a code file. - **Languages and comments (first version).** | Language | Extensions | Line comment | Block comment | | --- | --- | --- | --- | | Python | `.py` | `#` | none | | Shell | `.sh`, `.bash` | `#` at the start of a word | none | | PowerShell | `.ps1`, `.psm1` | `#` | `<#` ... `#>` | | C | `.c`, `.h` | `//` | `/*` ... `*/` | | C++ | `.cc`, `.cpp`, `.cxx`, `.hh`, `.hpp` | `//` | `/*` ... `*/` | | C# | `.cs` | `//` | `/*` ... `*/` | | Java | `.java` | `//` | `/*` ... `*/` | | JavaScript | `.js`, `.mjs`, `.jsx` | `//` | `/*` ... `*/` | | TypeScript | `.ts`, `.tsx` | `//` | `/*` ... `*/` | | Go | `.go` | `//` | `/*` ... `*/` | | Rust | `.rs` | `//` | `/*` ... `*/` | | SQL | `.sql` | `--` | `/*` ... `*/` | Markup, data and configuration files (Markdown, YAML, JSON, XML, HTML, CSS) are not code files. A language is added by a minor release. - **Line classes.** Every physical line is exactly one of: *blank* (only white space, also inside a block comment), *comment* (not blank, and only comment text and white space), or *code* (anything else, including a line with code and a trailing comment). A comment marker inside a string literal is not a comment. String literals are the quoted forms of the language that end on the same line, plus the Python triple-quoted string and the backtick string of JavaScript, TypeScript and Go, which may span lines; a Python docstring is a string and so counts as code. Raw-string and verbatim forms that differ from these (Rust `r#"..."#`, C# `@"..."` with doubled quotes, C++ raw strings) are scanned as ordinary strings and can be mis-counted; this is a stated limit. - **Excluded by default.** Every directory whose name starts with `.`, and the directories `node_modules`, `vendor`, `third_party`, `dist`, `build`, `out`, `bin`, `obj`, `target`, `venv`, `__pycache__` and `shared-scripts` (the checkout of the shared scripts); symbolic links are not followed. The input `exclude` is a comma list of patterns that add to this: a pattern without `/` matches a directory or file name anywhere, a pattern with `/` matches the path relative to `working-directory` (`*` does not cross `/`). The counter does not read `.gitignore` or generated-file markers; a caller excludes generated code with `exclude`. - **Totals.** `files` is the number of code files and `lines` is the sum of their code lines (comment and blank lines are not in it); the result also holds `comment` and `blank` totals and the same four numbers per language, sorted by language name, in `code-metrics.json`. - **Coverage report.** The input `coverage-report` is the path, relative to the repository root, of a Cobertura XML file. The percentage is `lines-covered / lines-valid` of the root `coverage` element, shown rounded half up to a whole number; when a count is absent, `line-rate` is used. A path that does not exist, a file that is not well-formed Cobertura XML, and a report with no valid lines are failures with the category `[configuration]` that names the file; no coverage badge is written. An empty `coverage-report` means no coverage badge and a notice. The workflow does not run tests. A called workflow runs in its own job, so it cannot see the files of the caller's test job: the caller runs its tests in an earlier job, uploads the report as an artifact and gives the artifact's name as the input `coverage-artifact`. The workflow then downloads that artifact into the directory `coverage-artifact` of its workspace and `coverage-report` names the file inside the artifact. Without `coverage-artifact`, `coverage-report` is a path in the checkout of the repository (a report the repository commits). `coverage-artifact` without `coverage-report` is a `[configuration]` failure. - **Badges.** Three flat SVG files with a label and a value, written to the root of the `badges` branch with fixed names: `code-files.svg` (label `code files`), `code-lines.svg` (label `lines of code`) and `coverage.svg` (label `coverage`); the values are the whole number with `,` between groups of three digits and, for coverage, `%`. The value colour of the first two is blue `#007ec6`; coverage is red `#e05d44` below the input `coverage-warn` (default `60`), yellow `#dfb317` from there to below `coverage-good` (default `80`) and green `#44cc11` from `coverage-good`. Each file has `role="img"`, a `