linecite 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. linecite-0.1.1/.gitattributes +1 -0
  2. linecite-0.1.1/.github/workflows/ci.yml +36 -0
  3. linecite-0.1.1/.github/workflows/release.yml +48 -0
  4. linecite-0.1.1/.gitignore +6 -0
  5. linecite-0.1.1/.pre-commit-hooks.yaml +18 -0
  6. linecite-0.1.1/LICENSE +21 -0
  7. linecite-0.1.1/PKG-INFO +205 -0
  8. linecite-0.1.1/README.md +178 -0
  9. linecite-0.1.1/action/comment.py +209 -0
  10. linecite-0.1.1/action.yml +129 -0
  11. linecite-0.1.1/docs/how-it-works.md +42 -0
  12. linecite-0.1.1/pyproject.toml +48 -0
  13. linecite-0.1.1/src/linecite/__init__.py +3 -0
  14. linecite-0.1.1/src/linecite/adopt.py +195 -0
  15. linecite-0.1.1/src/linecite/affected.py +198 -0
  16. linecite-0.1.1/src/linecite/audit.py +359 -0
  17. linecite-0.1.1/src/linecite/cli.py +333 -0
  18. linecite-0.1.1/src/linecite/config.py +128 -0
  19. linecite-0.1.1/src/linecite/errors.py +6 -0
  20. linecite-0.1.1/src/linecite/history.py +144 -0
  21. linecite-0.1.1/src/linecite/repo.py +323 -0
  22. linecite-0.1.1/src/linecite/scan.py +442 -0
  23. linecite-0.1.1/src/linecite/spec.py +255 -0
  24. linecite-0.1.1/tests/conftest.py +89 -0
  25. linecite-0.1.1/tests/test_action.py +350 -0
  26. linecite-0.1.1/tests/test_adopt.py +212 -0
  27. linecite-0.1.1/tests/test_affected_report.py +162 -0
  28. linecite-0.1.1/tests/test_audit.py +355 -0
  29. linecite-0.1.1/tests/test_dogfood.py +15 -0
  30. linecite-0.1.1/tests/test_hooks.py +21 -0
  31. linecite-0.1.1/tests/test_legacy_affected_config.py +120 -0
  32. linecite-0.1.1/tests/test_links.py +209 -0
  33. linecite-0.1.1/tests/test_refs.py +115 -0
  34. linecite-0.1.1/tests/test_release.py +33 -0
  35. linecite-0.1.1/tests/test_sync.py +286 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,36 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ${{ matrix.os }}
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ os: [ubuntu-latest, windows-latest]
18
+ python: ["3.11", "3.13"]
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - uses: actions/setup-python@v7
22
+ with:
23
+ python-version: ${{ matrix.python }}
24
+ - run: python -m pip install -e ".[dev]"
25
+ - run: python -m pytest -q
26
+
27
+ docs:
28
+ # linecite on its own docs, through its own action: check, and on pull requests a comment
29
+ # listing the paragraphs whose cited code the pull request changed
30
+ runs-on: ubuntu-latest
31
+ permissions:
32
+ contents: read
33
+ pull-requests: write
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+ - uses: ./
@@ -0,0 +1,48 @@
1
+ name: release
2
+
3
+ # Publishing a GitHub release uploads that tag's build to PyPI through trusted publishing: PyPI trusts
4
+ # this workflow by name, so no API token is stored anywhere.
5
+ on:
6
+ release:
7
+ types: [published]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: actions/setup-python@v7
18
+ with:
19
+ python-version: "3.13"
20
+ - name: The tag names the version being released
21
+ run: |
22
+ version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')
23
+ if [ "v$version" != "$GITHUB_REF_NAME" ]; then
24
+ echo "::error::tag $GITHUB_REF_NAME does not match pyproject.toml version $version"
25
+ exit 1
26
+ fi
27
+ - run: python -m pip install build twine
28
+ - run: python -m build
29
+ - run: python -m twine check dist/*
30
+ - uses: actions/upload-artifact@v7
31
+ with:
32
+ name: dist
33
+ path: dist/
34
+
35
+ publish:
36
+ needs: build
37
+ runs-on: ubuntu-latest
38
+ environment:
39
+ name: pypi
40
+ url: https://pypi.org/p/linecite
41
+ permissions:
42
+ id-token: write # the OIDC token PyPI checks instead of a password
43
+ steps:
44
+ - uses: actions/download-artifact@v8
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ dist/
6
+ build/
@@ -0,0 +1,18 @@
1
+ # A commit that touches only code can move the lines a doc cites, so both hooks read every document in
2
+ # the configuration on every commit instead of the staged files.
3
+ - id: linecite-check
4
+ name: linecite check
5
+ description: Fail when doc citations drifted, no longer resolve, or cite code by number only.
6
+ entry: linecite check
7
+ language: python
8
+ pass_filenames: false
9
+ always_run: true
10
+ require_serial: true
11
+ - id: linecite-sync
12
+ name: linecite sync
13
+ description: Rewrite drifted line numbers in doc citations, then fail on what still needs a human.
14
+ entry: linecite sync
15
+ language: python
16
+ pass_filenames: false
17
+ always_run: true
18
+ require_serial: true
linecite-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gotoUSA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.5
2
+ Name: linecite
3
+ Version: 0.1.1
4
+ Summary: Code citations in docs that re-derive their line numbers from the source.
5
+ Project-URL: Homepage, https://github.com/gotoUSA/linecite
6
+ Project-URL: Source, https://github.com/gotoUSA/linecite
7
+ Project-URL: Issues, https://github.com/gotoUSA/linecite/issues
8
+ Author: gotoUSA
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ci,code citations,docs,documentation,line numbers,pre-commit,stale docs
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Documentation
20
+ Classifier: Topic :: Software Development :: Documentation
21
+ Classifier: Topic :: Software Development :: Quality Assurance
22
+ Requires-Python: >=3.11
23
+ Provides-Extra: dev
24
+ Requires-Dist: pre-commit>=3; extra == 'dev'
25
+ Requires-Dist: pytest>=8; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # linecite
29
+
30
+ Docs that cite code by line number (`orders.py:310`<!--@-->) go stale on the next commit that touches the lines above.
31
+ linecite lets a citation name the code it means — a **symbol and a quoted fragment** — derives the line number
32
+ from the source, and tells you which paragraphs to re-read after the code changes.
33
+
34
+ ```md
35
+ Locks are taken in product order at [orders.py:310](app/orders.py#L310 "create_order: order_by(\"product_id\")").
36
+ ```
37
+
38
+ That is an ordinary markdown link: it renders as `orders.py:310`<!--@-->, clicks through to the line on GitHub, and its
39
+ title says what the line is. When code above it moves, `linecite sync` rewrites both the `#L310` fragment and the
40
+ `:310` in the link text; when the quoted code disappears, `linecite check` fails.
41
+
42
+ | command | what it does |
43
+ |---|---|
44
+ | `linecite check` | reports drifted numbers, citations that no longer resolve, and number-only citations; exit 1 if any (CI) |
45
+ | `linecite sync` | rewrites drifted numbers in place (pre-commit), then reports what still needs a human |
46
+ | `linecite affected <rev>` | lists doc lines whose citations point into code changed since `<rev>` — the prose to re-read |
47
+ | `linecite locate <path> <line>` | proposes a citation for an existing `path:line` |
48
+ | `linecite list` / `where <spec>` | inspect what citations resolve to |
49
+ | `linecite audit` | traces existing number-only citations through git history: which ones already point at the wrong line |
50
+ | `linecite adopt [--write]` | converts number-only citations into links (markdown) or anchors (other files) |
51
+
52
+ ```sh
53
+ pip install linecite # Python 3.11+, git
54
+ ```
55
+
56
+ Run it where your configuration is (see [Configuration](#configuration)), locally, as a
57
+ [pre-commit hook](#pre-commit), or in [GitHub Actions](#github-action).
58
+
59
+ ## Existing docs: audit, then adopt
60
+
61
+ <!-- linecite-ignore-start -->
62
+ Docs you already have cite code as `orders.py:310` or `[orders.py:310](app/orders.py#L310)`. `linecite audit`
63
+ judges them without changing anything. For each citation it asks git when the doc line was written, reads line
64
+ 310 of the code *as it was then*, and follows that line to today's code:
65
+ <!-- linecite-ignore-end -->
66
+
67
+ ```
68
+ docs/design.md:14 ok orders.py:310 written against 9b2f41d0c3
69
+ docs/design.md:31 stale orders.py:118 -> orders.py:131 written against 4e1a9c2b07
70
+ docs/design.md:40 gone orders.py:77 `row.lock()` is gone; written against 4e1a9c2b07
71
+ docs/design.md:52 unknown orders.py:12 line 12 was blank: too little to identify; written against 4e1a9c2b07
72
+
73
+ number-only citations 4 · ok 1 · stale 1 · gone 1 · unknown 1 · unverifiable 0
74
+ ```
75
+
76
+ Today's line 310 is never the reference — it holds *some* code, so judging by it would pass numbers that are
77
+ already wrong. Where docs and code live in separate repositories, the code is read as of the doc line's
78
+ date (following the first-parent line of `HEAD`); lines not committed yet are read against the working tree.
79
+ The results are estimates: a doc line edited later (a typo fix) is dated by that edit. In a shallow clone (CI
80
+ checkouts often fetch one commit) lines older than the clone are reported `unverifiable` — fetch full history.
81
+
82
+ `linecite adopt` uses the same trace to propose conversions — a titled link in markdown, an anchor elsewhere —
83
+ with the number set to where the cited line is now. It writes nothing until `--write`; every proposal is
84
+ resolved before it is shown, so converted citations pass `check`. Citations that are gone, ambiguous or
85
+ undatable are listed and left alone.
86
+
87
+ ## Citation forms
88
+
89
+ **Link** (markdown) — a link to a code file with a `#L<n>` or `#L<a>-L<b>` fragment and a title:
90
+
91
+ | title | meaning |
92
+ |---|---|
93
+ | `"create_order: row.lock()"` | the one line in `create_order` containing `row.lock()` (whitespace-insensitive) |
94
+ | `"OrderService.create_order"` | the whole symbol, as a range |
95
+ | `": xs.reduce"` | no symbol — for languages without symbol support, cite by fragment only |
96
+ | ``"OrderService `return`#2"`` | backtick grammar: the 2nd hit; also `` `a` .. `b` `` for a range |
97
+
98
+ A link with a `#L` fragment but no title is reported as legacy: nothing records which code it meant.
99
+ Links inside fenced code blocks are examples and are not checked.
100
+
101
+ **Anchor** (hidden comment) — for HTML, for numbers in running prose, and for comments in code excerpts:
102
+
103
+ <!-- linecite-ignore-start -->
104
+ ```md
105
+ the lock is taken at line 310<!--@ app/orders.py::create_order `order_by("product_id")` -->
106
+ ```
107
+ <!-- linecite-ignore-end -->
108
+
109
+ The anchor sits right after the number it owns. Its spec grammar:
110
+
111
+ ```
112
+ [@<sha>:]<path>[::<symbol>] [<quote>[#n] [.. <quote>[#n]]]
113
+ ```
114
+
115
+ - **path** — suffix of a tracked file; must match exactly one file.
116
+ - **symbol** — Python: qualified name or a unique suffix of one, or a module-level assignment. YAML: dotted key path.
117
+ - **quote** — `` `fragment` `` (widen to ``` `` ``` when the code holds a backtick) or `「fragment」`; `#n` picks the n-th hit.
118
+ - **`@sha:`** — pin to a commit for code that no longer exists; pinned specs may use plain integers.
119
+ - A bare `<!--@-->` marks a number that is not a code line.
120
+
121
+ **Examples** — docs that teach the syntax (a contributing guide, this README) wrap their examples in
122
+ ignore markers, each on a line of its own and outside code blocks (a marker shown in a code block is an
123
+ example itself); a marker that pairs with nothing fails `check`:
124
+
125
+ ```md
126
+ <!-- linecite-ignore-start -->
127
+ Cite code as [orders.py:310](app/orders.py#L310 "create_order: row.lock()").
128
+ <!-- linecite-ignore-end -->
129
+ ```
130
+
131
+ <!-- linecite-ignore-start -->
132
+ **Symbol reference** — `` `orders.py::OrderService.cancel` `` in prose fails `check` once the method is gone.
133
+ <!-- linecite-ignore-end -->
134
+
135
+ ## Configuration
136
+
137
+ `.linecite.toml` (top-level keys) or `[tool.linecite]` in `pyproject.toml`:
138
+
139
+ <!-- linecite-ignore-start -->
140
+ ```toml
141
+ code_root = "." # git repo of the cited code, relative to this file
142
+ docs = ["docs/**/*.md", "README.md"]
143
+ number_suffixes = ["`"] # text allowed between a number and its anchor: `orders.py:12`<!--@ … -->
144
+ legacy = "error" # number-only citations: "error" | "warn" | "off"
145
+ ignore_patterns = [] # regexes of regions to skip (the legacy scan also skips fenced code)
146
+ ```
147
+ <!-- linecite-ignore-end -->
148
+
149
+ ## pre-commit
150
+
151
+ ```yaml
152
+ repos:
153
+ - repo: https://github.com/gotoUSA/linecite
154
+ rev: v0.1.1
155
+ hooks:
156
+ - id: linecite-sync # or linecite-check, to report without rewriting
157
+ ```
158
+
159
+ Both hooks read every configured document on every commit, whatever is staged: a commit that touches only
160
+ code can move the lines a doc cites. `linecite-sync` rewrites drifted numbers and pre-commit stops the
161
+ commit so you can stage the rewrite; citations whose code is gone still fail it.
162
+
163
+ ## GitHub Action
164
+
165
+ ```yaml
166
+ on: pull_request
167
+ permissions:
168
+ contents: read
169
+ pull-requests: write # for the comment
170
+ jobs:
171
+ linecite:
172
+ runs-on: ubuntu-latest
173
+ steps:
174
+ - uses: actions/checkout@v7
175
+ - uses: gotoUSA/linecite@v0.1.1
176
+ ```
177
+
178
+ The job fails when `linecite check` does. On a pull request, the action also comments with the doc lines
179
+ whose cited code the pull request changed — one comment per use of the action, rewritten on every push,
180
+ so a paragraph a later push made irrelevant drops off the list. The job summary always carries the full
181
+ report; a failure to comment (pull requests from forks get a read-only token) is a warning, not a failed
182
+ job.
183
+
184
+ | input | default | |
185
+ |---|---|---|
186
+ | `check` | `true` | run `linecite check` and fail on its findings |
187
+ | `comment` | `true` | comment on pull requests |
188
+ | `base` | the pull request's base commit | revision the changes are measured from |
189
+ | `working-directory` | `.` | where the configuration is |
190
+ | `config` | | configuration file, if not the default |
191
+ | `github-token` | `github.token` | needs `pull-requests: write` |
192
+
193
+ The output `affected` is the number of doc citations pointing into changed code.
194
+
195
+ **Shallow checkouts.** `actions/checkout` fetches a single commit by default. `affected` needs only the
196
+ base commit's files, which the action fetches itself; `audit` needs the history that dated every doc line,
197
+ so run it after `actions/checkout` with `fetch-depth: 0` — in a shallow clone it reports old lines as
198
+ `unverifiable` instead of guessing.
199
+
200
+ Other CI systems can post the same report: `linecite affected origin/main --format markdown --link-base
201
+ https://example.com/owner/repo/blob/<sha>` prints it as markdown with links to the doc lines.
202
+
203
+ ## License
204
+
205
+ MIT
@@ -0,0 +1,178 @@
1
+ # linecite
2
+
3
+ Docs that cite code by line number (`orders.py:310`<!--@-->) go stale on the next commit that touches the lines above.
4
+ linecite lets a citation name the code it means — a **symbol and a quoted fragment** — derives the line number
5
+ from the source, and tells you which paragraphs to re-read after the code changes.
6
+
7
+ ```md
8
+ Locks are taken in product order at [orders.py:310](app/orders.py#L310 "create_order: order_by(\"product_id\")").
9
+ ```
10
+
11
+ That is an ordinary markdown link: it renders as `orders.py:310`<!--@-->, clicks through to the line on GitHub, and its
12
+ title says what the line is. When code above it moves, `linecite sync` rewrites both the `#L310` fragment and the
13
+ `:310` in the link text; when the quoted code disappears, `linecite check` fails.
14
+
15
+ | command | what it does |
16
+ |---|---|
17
+ | `linecite check` | reports drifted numbers, citations that no longer resolve, and number-only citations; exit 1 if any (CI) |
18
+ | `linecite sync` | rewrites drifted numbers in place (pre-commit), then reports what still needs a human |
19
+ | `linecite affected <rev>` | lists doc lines whose citations point into code changed since `<rev>` — the prose to re-read |
20
+ | `linecite locate <path> <line>` | proposes a citation for an existing `path:line` |
21
+ | `linecite list` / `where <spec>` | inspect what citations resolve to |
22
+ | `linecite audit` | traces existing number-only citations through git history: which ones already point at the wrong line |
23
+ | `linecite adopt [--write]` | converts number-only citations into links (markdown) or anchors (other files) |
24
+
25
+ ```sh
26
+ pip install linecite # Python 3.11+, git
27
+ ```
28
+
29
+ Run it where your configuration is (see [Configuration](#configuration)), locally, as a
30
+ [pre-commit hook](#pre-commit), or in [GitHub Actions](#github-action).
31
+
32
+ ## Existing docs: audit, then adopt
33
+
34
+ <!-- linecite-ignore-start -->
35
+ Docs you already have cite code as `orders.py:310` or `[orders.py:310](app/orders.py#L310)`. `linecite audit`
36
+ judges them without changing anything. For each citation it asks git when the doc line was written, reads line
37
+ 310 of the code *as it was then*, and follows that line to today's code:
38
+ <!-- linecite-ignore-end -->
39
+
40
+ ```
41
+ docs/design.md:14 ok orders.py:310 written against 9b2f41d0c3
42
+ docs/design.md:31 stale orders.py:118 -> orders.py:131 written against 4e1a9c2b07
43
+ docs/design.md:40 gone orders.py:77 `row.lock()` is gone; written against 4e1a9c2b07
44
+ docs/design.md:52 unknown orders.py:12 line 12 was blank: too little to identify; written against 4e1a9c2b07
45
+
46
+ number-only citations 4 · ok 1 · stale 1 · gone 1 · unknown 1 · unverifiable 0
47
+ ```
48
+
49
+ Today's line 310 is never the reference — it holds *some* code, so judging by it would pass numbers that are
50
+ already wrong. Where docs and code live in separate repositories, the code is read as of the doc line's
51
+ date (following the first-parent line of `HEAD`); lines not committed yet are read against the working tree.
52
+ The results are estimates: a doc line edited later (a typo fix) is dated by that edit. In a shallow clone (CI
53
+ checkouts often fetch one commit) lines older than the clone are reported `unverifiable` — fetch full history.
54
+
55
+ `linecite adopt` uses the same trace to propose conversions — a titled link in markdown, an anchor elsewhere —
56
+ with the number set to where the cited line is now. It writes nothing until `--write`; every proposal is
57
+ resolved before it is shown, so converted citations pass `check`. Citations that are gone, ambiguous or
58
+ undatable are listed and left alone.
59
+
60
+ ## Citation forms
61
+
62
+ **Link** (markdown) — a link to a code file with a `#L<n>` or `#L<a>-L<b>` fragment and a title:
63
+
64
+ | title | meaning |
65
+ |---|---|
66
+ | `"create_order: row.lock()"` | the one line in `create_order` containing `row.lock()` (whitespace-insensitive) |
67
+ | `"OrderService.create_order"` | the whole symbol, as a range |
68
+ | `": xs.reduce"` | no symbol — for languages without symbol support, cite by fragment only |
69
+ | ``"OrderService `return`#2"`` | backtick grammar: the 2nd hit; also `` `a` .. `b` `` for a range |
70
+
71
+ A link with a `#L` fragment but no title is reported as legacy: nothing records which code it meant.
72
+ Links inside fenced code blocks are examples and are not checked.
73
+
74
+ **Anchor** (hidden comment) — for HTML, for numbers in running prose, and for comments in code excerpts:
75
+
76
+ <!-- linecite-ignore-start -->
77
+ ```md
78
+ the lock is taken at line 310<!--@ app/orders.py::create_order `order_by("product_id")` -->
79
+ ```
80
+ <!-- linecite-ignore-end -->
81
+
82
+ The anchor sits right after the number it owns. Its spec grammar:
83
+
84
+ ```
85
+ [@<sha>:]<path>[::<symbol>] [<quote>[#n] [.. <quote>[#n]]]
86
+ ```
87
+
88
+ - **path** — suffix of a tracked file; must match exactly one file.
89
+ - **symbol** — Python: qualified name or a unique suffix of one, or a module-level assignment. YAML: dotted key path.
90
+ - **quote** — `` `fragment` `` (widen to ``` `` ``` when the code holds a backtick) or `「fragment」`; `#n` picks the n-th hit.
91
+ - **`@sha:`** — pin to a commit for code that no longer exists; pinned specs may use plain integers.
92
+ - A bare `<!--@-->` marks a number that is not a code line.
93
+
94
+ **Examples** — docs that teach the syntax (a contributing guide, this README) wrap their examples in
95
+ ignore markers, each on a line of its own and outside code blocks (a marker shown in a code block is an
96
+ example itself); a marker that pairs with nothing fails `check`:
97
+
98
+ ```md
99
+ <!-- linecite-ignore-start -->
100
+ Cite code as [orders.py:310](app/orders.py#L310 "create_order: row.lock()").
101
+ <!-- linecite-ignore-end -->
102
+ ```
103
+
104
+ <!-- linecite-ignore-start -->
105
+ **Symbol reference** — `` `orders.py::OrderService.cancel` `` in prose fails `check` once the method is gone.
106
+ <!-- linecite-ignore-end -->
107
+
108
+ ## Configuration
109
+
110
+ `.linecite.toml` (top-level keys) or `[tool.linecite]` in `pyproject.toml`:
111
+
112
+ <!-- linecite-ignore-start -->
113
+ ```toml
114
+ code_root = "." # git repo of the cited code, relative to this file
115
+ docs = ["docs/**/*.md", "README.md"]
116
+ number_suffixes = ["`"] # text allowed between a number and its anchor: `orders.py:12`<!--@ … -->
117
+ legacy = "error" # number-only citations: "error" | "warn" | "off"
118
+ ignore_patterns = [] # regexes of regions to skip (the legacy scan also skips fenced code)
119
+ ```
120
+ <!-- linecite-ignore-end -->
121
+
122
+ ## pre-commit
123
+
124
+ ```yaml
125
+ repos:
126
+ - repo: https://github.com/gotoUSA/linecite
127
+ rev: v0.1.1
128
+ hooks:
129
+ - id: linecite-sync # or linecite-check, to report without rewriting
130
+ ```
131
+
132
+ Both hooks read every configured document on every commit, whatever is staged: a commit that touches only
133
+ code can move the lines a doc cites. `linecite-sync` rewrites drifted numbers and pre-commit stops the
134
+ commit so you can stage the rewrite; citations whose code is gone still fail it.
135
+
136
+ ## GitHub Action
137
+
138
+ ```yaml
139
+ on: pull_request
140
+ permissions:
141
+ contents: read
142
+ pull-requests: write # for the comment
143
+ jobs:
144
+ linecite:
145
+ runs-on: ubuntu-latest
146
+ steps:
147
+ - uses: actions/checkout@v7
148
+ - uses: gotoUSA/linecite@v0.1.1
149
+ ```
150
+
151
+ The job fails when `linecite check` does. On a pull request, the action also comments with the doc lines
152
+ whose cited code the pull request changed — one comment per use of the action, rewritten on every push,
153
+ so a paragraph a later push made irrelevant drops off the list. The job summary always carries the full
154
+ report; a failure to comment (pull requests from forks get a read-only token) is a warning, not a failed
155
+ job.
156
+
157
+ | input | default | |
158
+ |---|---|---|
159
+ | `check` | `true` | run `linecite check` and fail on its findings |
160
+ | `comment` | `true` | comment on pull requests |
161
+ | `base` | the pull request's base commit | revision the changes are measured from |
162
+ | `working-directory` | `.` | where the configuration is |
163
+ | `config` | | configuration file, if not the default |
164
+ | `github-token` | `github.token` | needs `pull-requests: write` |
165
+
166
+ The output `affected` is the number of doc citations pointing into changed code.
167
+
168
+ **Shallow checkouts.** `actions/checkout` fetches a single commit by default. `affected` needs only the
169
+ base commit's files, which the action fetches itself; `audit` needs the history that dated every doc line,
170
+ so run it after `actions/checkout` with `fetch-depth: 0` — in a shallow clone it reports old lines as
171
+ `unverifiable` instead of guessing.
172
+
173
+ Other CI systems can post the same report: `linecite affected origin/main --format markdown --link-base
174
+ https://example.com/owner/repo/blob/<sha>` prints it as markdown with links to the doc lines.
175
+
176
+ ## License
177
+
178
+ MIT