linecite
Docs that cite code by line number (orders.py:310) go stale on the next commit that touches the lines above.
linecite lets a citation name the code it means — a symbol and a quoted fragment — derives the line number
from the source, and tells you which paragraphs to re-read after the code changes.
Locks are taken in product order at [orders.py:310](app/orders.py#L310 "create_order: order_by(\"product_id\")").
That is an ordinary markdown link: it renders as orders.py:310, clicks through to the line on GitHub, and its
title says what the line is. When code above it moves, linecite sync rewrites both the #L310 fragment and the
:310 in the link text; when the quoted code disappears, linecite check fails.
| command | what it does |
|---|---|
linecite check |
reports drifted numbers, citations that no longer resolve, and number-only citations; exit 1 if any (CI) |
linecite sync |
rewrites drifted numbers in place (pre-commit), then reports what still needs a human |
linecite affected <rev> |
lists doc lines whose citations point into code changed since <rev> — the prose to re-read |
linecite locate <path> <line> |
proposes a citation for an existing path:line |
linecite list / where <spec> |
inspect what citations resolve to |
linecite audit |
traces existing number-only citations through git history: which ones already point at the wrong line |
linecite adopt [--write] |
converts number-only citations into links (markdown) or anchors (other files) |
pip install linecite # Python 3.11+, git
Run it where your configuration is (see Configuration), locally, as a pre-commit hook, or in GitHub Actions.
Existing docs: audit, then adopt
Docs you already have cite code as orders.py:310 or [orders.py:310](app/orders.py#L310). linecite audit
judges them without changing anything. For each citation it asks git when the doc line was written, reads line
310 of the code as it was then, and follows that line to today's code:
docs/design.md:14 ok orders.py:310 written against 9b2f41d0c3
docs/design.md:31 stale orders.py:118 -> orders.py:131 written against 4e1a9c2b07
docs/design.md:40 gone orders.py:77 `row.lock()` is gone; written against 4e1a9c2b07
docs/design.md:52 unknown orders.py:12 line 12 was blank: too little to identify; written against 4e1a9c2b07
number-only citations 4 · ok 1 · stale 1 · gone 1 · unknown 1 · unverifiable 0
Today's line 310 is never the reference — it holds some code, so judging by it would pass numbers that are
already wrong. Where docs and code live in separate repositories, the code is read as of the doc line's
date (following the first-parent line of HEAD); lines not committed yet are read against the working tree.
The results are estimates: a doc line edited later (a typo fix) is dated by that edit. In a shallow clone (CI
checkouts often fetch one commit) lines older than the clone are reported unverifiable — fetch full history.
linecite adopt uses the same trace to propose conversions — a titled link in markdown, an anchor elsewhere —
with the number set to where the cited line is now. It writes nothing until --write; every proposal is
resolved before it is shown, so converted citations pass check. Citations that are gone, ambiguous or
undatable are listed and left alone.
Citation forms
Link (markdown) — a link to a code file with a #L<n> or #L<a>-L<b> fragment and a title:
| title | meaning |
|---|---|
"create_order: row.lock()" |
the one line in create_order containing row.lock() (whitespace-insensitive) |
"OrderService.create_order" |
the whole symbol, as a range |
": xs.reduce" |
no symbol — for languages without symbol support, cite by fragment only |
"OrderService `return`#2" |
backtick grammar: the 2nd hit; also `a` .. `b` for a range |
A link with a #L fragment but no title is reported as legacy: nothing records which code it meant.
Links inside fenced code blocks are examples and are not checked.
Anchor (hidden comment) — for HTML, for numbers in running prose, and for comments in code excerpts:
the lock is taken at line 310<!--@ app/orders.py::create_order `order_by("product_id")` -->
The anchor sits right after the number it owns. Its spec grammar:
[@<sha>:]<path>[::<symbol>] [<quote>[#n] [.. <quote>[#n]]]
- path — suffix of a tracked file; must match exactly one file.
- symbol — Python: qualified name or a unique suffix of one, or a module-level assignment. YAML: dotted key path.
- quote —
`fragment`(widen to``when the code holds a backtick) or「fragment」;#npicks the n-th hit. @sha:— pin to a commit for code that no longer exists; pinned specs may use plain integers.- A bare
<!--@-->marks a number that is not a code line.
Examples — docs that teach the syntax (a contributing guide, this README) wrap their examples in
ignore markers, each on a line of its own and outside code blocks (a marker shown in a code block is an
example itself); a marker that pairs with nothing fails check:
<!-- linecite-ignore-start -->
Cite code as [orders.py:310](app/orders.py#L310 "create_order: row.lock()").
<!-- linecite-ignore-end -->
Symbol reference — `orders.py::OrderService.cancel` in prose fails check once the method is gone.
Configuration
.linecite.toml (top-level keys) or [tool.linecite] in pyproject.toml:
code_root = "." # git repo of the cited code, relative to this file
docs = ["docs/**/*.md", "README.md"]
number_suffixes = ["`"] # text allowed between a number and its anchor: `orders.py:12`<!--@ … -->
legacy = "error" # number-only citations: "error" | "warn" | "off"
ignore_patterns = [] # regexes of regions to skip (the legacy scan also skips fenced code)
pre-commit
repos:
- repo: https://github.com/gotoUSA/linecite
rev: v0.1.1
hooks:
- id: linecite-sync # or linecite-check, to report without rewriting
Both hooks read every configured document on every commit, whatever is staged: a commit that touches only
code can move the lines a doc cites. linecite-sync rewrites drifted numbers and pre-commit stops the
commit so you can stage the rewrite; citations whose code is gone still fail it.
GitHub Action
on: pull_request
permissions:
contents: read
pull-requests: write # for the comment
jobs:
linecite:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: gotoUSA/linecite@v0.1.1
The job fails when linecite check does. On a pull request, the action also comments with the doc lines
whose cited code the pull request changed — one comment per use of the action, rewritten on every push,
so a paragraph a later push made irrelevant drops off the list. The job summary always carries the full
report; a failure to comment (pull requests from forks get a read-only token) is a warning, not a failed
job.
| input | default | |
|---|---|---|
check |
true |
run linecite check and fail on its findings |
comment |
true |
comment on pull requests |
base |
the pull request's base commit | revision the changes are measured from |
working-directory |
. |
where the configuration is |
config |
configuration file, if not the default | |
github-token |
github.token |
needs pull-requests: write |
The output affected is the number of doc citations pointing into changed code.
Shallow checkouts. actions/checkout fetches a single commit by default. affected needs only the
base commit's files, which the action fetches itself; audit needs the history that dated every doc line,
so run it after actions/checkout with fetch-depth: 0 — in a shallow clone it reports old lines as
unverifiable instead of guessing.
Other CI systems can post the same report: linecite affected origin/main --format markdown --link-base https://example.com/owner/repo/blob/<sha> prints it as markdown with links to the doc lines.
License
MIT
Release files for linecite 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| linecite-0.1.1.tar.gz | 55.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| linecite-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 95.4 kB
Release files / linecite-0.1.1.tar.gz
| Download URL | linecite-0.1.1.tar.gz |
|---|---|
| Size | 55.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d77474b21d6b36634970a223fd16b814e417f6f7f6b2a4411a7379982f498104
|
|
BLAKE2b-256 checksum How to use checksums |
2fe6835a44a9fc58ed1b95a1479fc90b5363535d58c54fcf5af12a8dd5867867
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / linecite-0.1.1-py3-none-any.whl
| Download URL | linecite-0.1.1-py3-none-any.whl |
|---|---|
| Size | 39.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3dd77b2e0a9b2f334ba9258c099df858110def5dcf8115b9505f4b468bd0f3fa
|
|
BLAKE2b-256 checksum How to use checksums |
b9f86cfdafc2a59219fb95d9f0423d4ee5da3f4c6a1bfb3dbc0cedeb23bfb97a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log