asciidoclint 0.5.0 → 1.1.0

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 (164) hide show
  1. package/README.md +95 -127
  2. package/dist/api/fixes.js +1 -1
  3. package/dist/api/lint.js +48 -5
  4. package/dist/api/rules.d.ts +17 -9
  5. package/dist/api/rules.js +116 -22
  6. package/dist/api/waivers.d.ts +2 -0
  7. package/dist/api/waivers.js +191 -0
  8. package/dist/cli/explain.d.ts +2 -0
  9. package/dist/cli/explain.js +43 -0
  10. package/dist/cli/index.js +59 -12
  11. package/dist/cli/init-rule.d.ts +2 -1
  12. package/dist/cli/init-rule.js +81 -11
  13. package/dist/cli/install-skill.d.ts +12 -0
  14. package/dist/cli/install-skill.js +42 -5
  15. package/dist/formatters/json.js +30 -17
  16. package/dist/formatters/pretty.js +9 -4
  17. package/dist/parsers/asciidoctor.d.ts +2 -1
  18. package/dist/parsers/asciidoctor.js +150 -1
  19. package/dist/parsers/tolerant.js +29 -5
  20. package/dist/rules/AD004.js +99 -0
  21. package/dist/rules/AD009.d.ts +2 -0
  22. package/dist/rules/AD009.js +80 -0
  23. package/dist/rules/AD010.js +17 -3
  24. package/dist/rules/AD011.js +20 -5
  25. package/dist/rules/AD012.js +14 -2
  26. package/dist/rules/AD013.js +2 -2
  27. package/dist/rules/AD016.js +16 -10
  28. package/dist/rules/AD017.js +4 -4
  29. package/dist/rules/AD020.js +41 -20
  30. package/dist/rules/AD025.js +2 -2
  31. package/dist/rules/AD028.js +77 -10
  32. package/dist/rules/AD029.js +2 -2
  33. package/dist/rules/AD039.js +2 -2
  34. package/dist/rules/AD046.d.ts +2 -0
  35. package/dist/rules/AD046.js +91 -0
  36. package/dist/rules/AD047.d.ts +2 -0
  37. package/dist/rules/AD047.js +47 -0
  38. package/dist/rules/AD048.d.ts +2 -0
  39. package/dist/rules/AD048.js +37 -0
  40. package/dist/rules/AD049.d.ts +2 -0
  41. package/dist/rules/AD049.js +46 -0
  42. package/dist/rules/AD050.d.ts +2 -0
  43. package/dist/rules/AD050.js +46 -0
  44. package/dist/rules/AD051.d.ts +2 -0
  45. package/dist/rules/AD051.js +52 -0
  46. package/dist/rules/AD052.d.ts +2 -0
  47. package/dist/rules/AD052.js +46 -0
  48. package/dist/rules/AD053.d.ts +2 -0
  49. package/dist/rules/AD053.js +46 -0
  50. package/dist/rules/AD054.d.ts +2 -0
  51. package/dist/rules/AD054.js +46 -0
  52. package/dist/rules/AD055.d.ts +2 -0
  53. package/dist/rules/AD055.js +41 -0
  54. package/dist/rules/AD056.d.ts +2 -0
  55. package/dist/rules/AD056.js +49 -0
  56. package/dist/rules/AD057.d.ts +2 -0
  57. package/dist/rules/AD057.js +79 -0
  58. package/dist/rules/AD058.d.ts +2 -0
  59. package/dist/rules/AD058.js +33 -0
  60. package/dist/rules/AD059.d.ts +2 -0
  61. package/dist/rules/AD059.js +99 -0
  62. package/dist/rules/ADW01.d.ts +1 -0
  63. package/dist/rules/ADW01.js +2 -0
  64. package/dist/rules/ADW02.d.ts +1 -0
  65. package/dist/rules/ADW02.js +2 -0
  66. package/dist/rules/ADW03.d.ts +1 -0
  67. package/dist/rules/ADW03.js +2 -0
  68. package/dist/rules/ADW04.d.ts +1 -0
  69. package/dist/rules/ADW04.js +2 -0
  70. package/dist/rules/ADW05.d.ts +1 -0
  71. package/dist/rules/ADW05.js +2 -0
  72. package/dist/rules/ADW06.d.ts +1 -0
  73. package/dist/rules/ADW06.js +2 -0
  74. package/dist/rules/ADW07.d.ts +1 -0
  75. package/dist/rules/ADW07.js +2 -0
  76. package/dist/rules/ADW08.d.ts +1 -0
  77. package/dist/rules/ADW08.js +2 -0
  78. package/dist/rules/builtin.js +46 -0
  79. package/dist/rules/specialSections.d.ts +13 -0
  80. package/dist/rules/specialSections.js +54 -0
  81. package/dist/rules/waiverRule.d.ts +104 -0
  82. package/dist/rules/waiverRule.js +108 -0
  83. package/dist/types.d.ts +17 -0
  84. package/dist/version.d.ts +1 -1
  85. package/dist/version.js +1 -1
  86. package/docs/architecture.md +1296 -0
  87. package/docs/configuration.md +126 -0
  88. package/docs/custom-rules.md +162 -0
  89. package/docs/release-workflow.md +630 -0
  90. package/docs/rule-architecture.md +94 -0
  91. package/docs/rules/AD000.md +43 -0
  92. package/docs/rules/AD001.md +36 -0
  93. package/docs/rules/AD002.md +48 -0
  94. package/docs/rules/AD003.md +36 -0
  95. package/docs/rules/AD004.md +54 -0
  96. package/docs/rules/AD005.md +87 -0
  97. package/docs/rules/AD006.md +88 -0
  98. package/docs/rules/AD007.md +59 -0
  99. package/docs/rules/AD008.md +48 -0
  100. package/docs/rules/AD009.md +59 -0
  101. package/docs/rules/AD010.md +54 -0
  102. package/docs/rules/AD011.md +56 -0
  103. package/docs/rules/AD012.md +61 -0
  104. package/docs/rules/AD013.md +75 -0
  105. package/docs/rules/AD016.md +53 -0
  106. package/docs/rules/AD017.md +52 -0
  107. package/docs/rules/AD019.md +53 -0
  108. package/docs/rules/AD020.md +100 -0
  109. package/docs/rules/AD022.md +33 -0
  110. package/docs/rules/AD023.md +59 -0
  111. package/docs/rules/AD024.md +48 -0
  112. package/docs/rules/AD025.md +64 -0
  113. package/docs/rules/AD026.md +43 -0
  114. package/docs/rules/AD027.md +50 -0
  115. package/docs/rules/AD028.md +53 -0
  116. package/docs/rules/AD029.md +57 -0
  117. package/docs/rules/AD030.md +42 -0
  118. package/docs/rules/AD031.md +48 -0
  119. package/docs/rules/AD032.md +65 -0
  120. package/docs/rules/AD034.md +48 -0
  121. package/docs/rules/AD035.md +62 -0
  122. package/docs/rules/AD036.md +51 -0
  123. package/docs/rules/AD037.md +49 -0
  124. package/docs/rules/AD039.md +59 -0
  125. package/docs/rules/AD040.md +49 -0
  126. package/docs/rules/AD041.md +56 -0
  127. package/docs/rules/AD042.md +42 -0
  128. package/docs/rules/AD043.md +54 -0
  129. package/docs/rules/AD044.md +47 -0
  130. package/docs/rules/AD045.md +62 -0
  131. package/docs/rules/AD046.md +118 -0
  132. package/docs/rules/AD047.md +46 -0
  133. package/docs/rules/AD048.md +43 -0
  134. package/docs/rules/AD049.md +43 -0
  135. package/docs/rules/AD050.md +41 -0
  136. package/docs/rules/AD051.md +49 -0
  137. package/docs/rules/AD052.md +39 -0
  138. package/docs/rules/AD053.md +41 -0
  139. package/docs/rules/AD054.md +40 -0
  140. package/docs/rules/AD055.md +60 -0
  141. package/docs/rules/AD056.md +36 -0
  142. package/docs/rules/AD057.md +44 -0
  143. package/docs/rules/AD058.md +28 -0
  144. package/docs/rules/AD059.md +68 -0
  145. package/docs/rules/ADW01.md +31 -0
  146. package/docs/rules/ADW02.md +31 -0
  147. package/docs/rules/ADW03.md +31 -0
  148. package/docs/rules/ADW04.md +31 -0
  149. package/docs/rules/ADW05.md +32 -0
  150. package/docs/rules/ADW06.md +33 -0
  151. package/docs/rules/ADW07.md +34 -0
  152. package/docs/rules/ADW08.md +31 -0
  153. package/docs/rules/rule-necessity.md +97 -0
  154. package/docs/waiver.md +201 -0
  155. package/package.json +2 -1
  156. package/skills/asciidoclint/SKILL.md +52 -66
  157. package/skills/asciidoclint/references/agentic-fix.md +29 -0
  158. package/skills/asciidoclint/references/feedback.md +82 -0
  159. package/skills/asciidoclint/references/lint-summary.md +45 -0
  160. package/skills/asciidoclint/references/result-schema.md +4 -0
  161. package/skills/asciidoclint/references/rule-create.md +147 -0
  162. package/skills/asciidoclint/references/rule-review.md +75 -0
  163. package/skills/asciidoclint/references/waivers.md +42 -0
  164. package/skills/asciidoclint/references/ai-fix-policy.md +0 -11
package/README.md CHANGED
@@ -1,45 +1,32 @@
1
1
  <p align="center">
2
2
  <img src="assets/logo.svg" width="160" height="160" alt="asciidoclint logo">
3
3
  </p>
4
- <p align="center">
5
- <sub><code>assets/logo.svg</code> and <code>assets/icon.svg</code> were created with <a href="https://inkscape.org/">Inkscape</a>. The <strong>lint</strong> label uses <a href="https://www.jetbrains.com/lp/mono/">JetBrains Mono</a> (SIL Open Font License).</sub>
6
- </p>
7
4
 
8
5
  # asciidoclint
9
6
 
10
7
  `asciidoclint` is an AsciiDoc syntax, structure, and document-policy linter for
11
8
  CLI, AI-agent, and editor workflows.
12
9
 
13
- `asciidoclint` began as an in-house implementation and has been open-sourced
14
- under the MIT License since June 1, 2026.
15
-
16
- Design goals:
17
-
18
- - Provide a library-first, typed, plugin-friendly rule model.
19
- - Use Asciidoctor-backed diagnostics, source mapping, include awareness, and a
20
- safe/unsafe fix model.
21
- - Keep generic AsciiDoc syntax/structure rules separate from project or
22
- organization policy rules.
23
-
24
- Start with the architecture proposal:
10
+ ## Install the npm package
25
11
 
26
- - [Architecture](docs/architecture.md)
12
+ ```bash
13
+ npm install --save-dev asciidoclint
14
+ ```
27
15
 
28
- ## Install the npm package
16
+ ## Use the CLI
29
17
 
30
- Install in a project:
18
+ Run lint:
31
19
 
32
20
  ```bash
33
- npm install --save-dev asciidoclint
21
+ npx asciidoclint index.adoc
22
+ npx asciidoclint --format json index.adoc
34
23
  ```
35
24
 
36
- Run the CLI:
25
+ Apply deterministic fixes:
37
26
 
38
27
  ```bash
39
- npx asciidoclint docs/**/*.adoc
40
- npx asciidoclint --format json docs/**/*.adoc
41
- npx asciidoclint --fix docs/**/*.adoc
42
- npx asciidoclint --fix --unsafe docs/**/*.adoc
28
+ npx asciidoclint --fix index.adoc
29
+ npx asciidoclint --fix --unsafe index.adoc
43
30
  ```
44
31
 
45
32
  Inspect rules:
@@ -48,122 +35,80 @@ Inspect rules:
48
35
  npx asciidoclint --list-rules
49
36
  npx asciidoclint --explain AD001
50
37
  npx asciidoclint --explain heading-level-progression
38
+ npx asciidoclint --explain AD001 --format json
51
39
  ```
52
40
 
53
- Load organization-specific conformance or style rules as custom rules:
41
+ Use a project config file when the same lint settings should be reused:
54
42
 
55
43
  ```yaml
44
+ # .asciidoclint/config.yaml
56
45
  extends:
57
46
  - asciidoclint:recommended
47
+ ```
58
48
 
59
- ignores:
60
- - build/**
49
+ Use a global config file for settings that should apply across projects:
61
50
 
51
+ ```yaml
52
+ # ~/.asciidoclint/config.yaml
62
53
  customRules:
63
- - ./lint-rules/ORG001-no-todo.js
64
- - ./lint-rules/ORG002-section-policy.js
54
+ - "@example/asciidoclint-rules"
65
55
  ```
66
56
 
67
- Scaffold a custom rule without modifying `asciidoclint` source:
68
-
69
- ```bash
70
- npx asciidoclint init-rule --pack my-org --id ORG001 --alias no-todo
71
- ```
57
+ See [docs/configuration.md](docs/configuration.md) for configuration fields and
58
+ merge order. See [docs/waiver.md](docs/waiver.md) for source waiver syntax and
59
+ reporting.
72
60
 
73
61
  ## Install the AI skill
74
62
 
75
- The repository ships an `asciidoclint` skill under `skills/asciidoclint`. The
76
- skill lets AI agents trigger lint, summarize results, apply safe fixes, apply
77
- explicit unsafe fixes, and use reported `fixHelper` guidance for focused
78
- AI-assisted repairs.
79
-
80
- Install the skill directly from GitHub with the open skills CLI:
63
+ The repository ships an `asciidoclint` skill for AI agents. Install it from the
64
+ npm package:
81
65
 
82
66
  ```bash
83
- npx skills add f33lgood/asciidoclint --skill asciidoclint -a codex -g
67
+ npx asciidoclint install-skill
84
68
  ```
85
69
 
86
- The repository hides repo-maintenance skills from normal discovery, so the
87
- shorter form installs the public `asciidoclint` skill too:
70
+ Or install it directly from GitHub with the open skills CLI:
88
71
 
89
72
  ```bash
90
- npx skills add f33lgood/asciidoclint
73
+ npx skills add f33lgood/asciidoclint --skill asciidoclint -a codex -g
91
74
  ```
92
75
 
93
- If you already installed the npm package and want the matching bundled skill
94
- version, install it through the `asciidoclint` CLI:
76
+ Remove the installed skill when you want to use `asciidoclint` without AI skill
77
+ assistance:
95
78
 
96
79
  ```bash
97
- npx asciidoclint install-skill
80
+ npx asciidoclint uninstall-skill
98
81
  ```
99
82
 
100
- Useful installer options:
83
+ The public skill exposes these user-facing workflows:
101
84
 
102
- ```bash
103
- npx asciidoclint install-skill --project
104
- npx asciidoclint install-skill --dest ./tmp/skills --force
105
- ```
85
+ | Workflow | Purpose |
86
+ |---|---|
87
+ | `lint-summary` | Run lint and summarize findings by severity, rule, file, waiver status, and fixability. |
88
+ | `agentic-fix` | Use lint guidance and local source context to repair findings that deterministic fixes cannot safely handle. |
89
+ | `waivers` | Add narrow source waivers and verify waiver syntax. |
90
+ | `rule-create` | Create project-local or shared custom rules. |
91
+ | `rule-review` | Review rule behavior, overlap, documentation, and tests. |
92
+ | `feedback` | Prepare a sanitized, paste-ready GitHub issue message. |
106
93
 
107
- ## VS Code and Cursor extension
94
+ ## VS Code / Open VSX-Compatible Extension
108
95
 
109
- `asciidoclint` is also available as a VS Code/Cursor extension. The extension
110
- shows lint issues as source-file diagnostics in the editor and Problems panel,
111
- including diagnostics mapped back to included source files.
96
+ `asciidoclint` is also available as a VS Code / Open VSX-compatible extension.
97
+ Editors compatible with VS Code's diagnostic model can use it to show lint
98
+ issues in the editor and Problems panel, including diagnostics mapped back to
99
+ included source files.
112
100
 
113
101
  The extension can import CLI diagnostics written by:
114
102
 
115
103
  ```bash
116
104
  npx asciidoclint --format json \
117
105
  --output-diagnostics .asciidoclint/diagnostics.json \
118
- docs/index.adoc
106
+ index.adoc
119
107
  ```
120
108
 
121
109
  See [packages/vscode-asciidoclint](packages/vscode-asciidoclint/README.md) for
122
110
  extension commands and settings.
123
111
 
124
- ## Use this repository
125
-
126
- Install dependencies:
127
-
128
- ```bash
129
- npm install
130
- ```
131
-
132
- Run the check loop:
133
-
134
- ```bash
135
- npm run check
136
- ```
137
-
138
- Run coverage only:
139
-
140
- ```bash
141
- npm run test:coverage
142
- ```
143
-
144
- Coverage thresholds and the latest metrics are documented in
145
- [docs/reports/report-coverage.md](docs/reports/report-coverage.md).
146
-
147
- Run the CLI from source:
148
-
149
- ```bash
150
- npx tsx src/cli/index.ts test/fixtures/api/structural_errors.adoc
151
- npx tsx src/cli/index.ts --format json test/fixtures/api/structural_errors.adoc
152
- npx tsx src/cli/index.ts --fix test/fixtures/api/structure_only.adoc
153
- npx tsx src/cli/index.ts install-skill --dest ./tmp/skills --force
154
- ```
155
-
156
- Build and package the VS Code/Cursor extension:
157
-
158
- ```bash
159
- npm run build:extension
160
- npm test -w vscode-asciidoclint
161
- npm run package -w vscode-asciidoclint
162
- ```
163
-
164
- Install the generated `.vsix` in Cursor with **Extensions: Install from VSIX**,
165
- then run **asciidoclint: Lint Current File** or **asciidoclint: Lint Workspace**.
166
-
167
112
  ## Built-in Rules
168
113
 
169
114
  | ID / Alias | Short description |
@@ -177,6 +122,7 @@ then run **asciidoclint: Lint Current File** or **asciidoclint: Lint Workspace**
177
122
  | `AD006/included-document-title` | Included AsciiDoc files should not introduce a level-0 title without level offset |
178
123
  | `AD007/heading-depth-limit` | Section headings should not exceed Asciidoctor's supported depth |
179
124
  | `AD008/blank-before-list` | Lists should be separated from preceding paragraph text |
125
+ | `AD009/blank-after-list` | Lists should be separated from following structural blocks |
180
126
  | `AD010/table-title` | Table blocks should have a title |
181
127
  | `AD011/image-title` | Block images should have a title |
182
128
  | `AD012/diagram-title` | Diagram blocks should have a title |
@@ -184,14 +130,14 @@ then run **asciidoclint: Lint Current File** or **asciidoclint: Lint Workspace**
184
130
  | `AD016/malformed-figure-caption` | Figure captions should use AsciiDoc title syntax |
185
131
  | `AD017/malformed-table-caption` | Table captions should use AsciiDoc title syntax |
186
132
  | `AD019/content-after-include` | Text should not be attached directly after include directives |
187
- | `AD020/appendix-section-level` | Appendices should be section-level blocks in article documents |
133
+ | `AD020/appendix-placement` | Appendix markers should apply to documented appendix section levels |
188
134
  | `AD022/circular-include` | Include trees must not contain cycles |
189
135
  | `AD023/empty-section` | Sections should contain body content or child sections |
190
136
  | `AD024/missing-include` | Include targets should exist after attribute substitution |
191
137
  | `AD025/missing-image` | Image targets should exist after attribute substitution |
192
138
  | `AD026/missing-xref` | Cross-reference targets should resolve to an anchor or file |
193
139
  | `AD027/missing-local-link` | Local link targets should resolve to existing files |
194
- | `AD028/image-alt-text` | Images should not explicitly set empty alt text |
140
+ | `AD028/image-alt-text` | Images should provide meaningful alt text |
195
141
  | `AD029/markdown-link-image-residue` | Markdown link and image residue should not render as text |
196
142
  | `AD030/markdown-table-residue` | Markdown pipe table residue should not render as text |
197
143
  | `AD031/no-nested-link-text` | Link text should not contain nested links or cross references |
@@ -207,52 +153,74 @@ then run **asciidoclint: Lint Current File** or **asciidoclint: Lint Workspace**
207
153
  | `AD043/section-title-start-left` | Section title syntax should start at the beginning of the line |
208
154
  | `AD044/local-adoc-link` | Local AsciiDoc files should be referenced with xref, not link |
209
155
  | `AD045/markdown-heading-mix` | Markdown-compatible headings should not be mixed with AsciiDoc headings |
210
-
211
- Detailed per-rule docs live under `docs/rules/`; `--explain` exposes the same
212
- metadata programmatically.
156
+ | `AD046/preface-placement` | Preface sections should match documented book placement |
157
+ | `AD047/abstract-placement` | Abstract sections should match documented article placement |
158
+ | `AD048/bibliography-placement` | Bibliography markers should apply to documented bibliography sections |
159
+ | `AD049/glossary-placement` | Glossary markers should apply to documented glossary section levels |
160
+ | `AD050/index-placement` | Index markers should apply to documented index section levels |
161
+ | `AD051/partintro-placement` | Part introduction markers should apply to the introductory block of a book part |
162
+ | `AD052/acknowledgments-placement` | Acknowledgments markers should apply to documented book section levels |
163
+ | `AD053/dedication-placement` | Dedication markers should apply to documented book section levels |
164
+ | `AD054/colophon-placement` | Colophon markers should apply to documented book section levels |
165
+ | `AD055/docx-anchor-caption-residue` | DOCX anchor caption residue should be converted to AsciiDoc titles |
166
+ | `AD056/dangling-blank-list-continuation` | Blank list continuations should not capture following content |
167
+ | `AD057/semantic-anchor-target` | Semantic anchors should attach to the matching block type |
168
+ | `AD058/docx-bookmark-hash-residue` | DOCX bookmark/hash residue should be cleaned up |
169
+ | `AD059/docx-nested-table-structure` | DOCX-converted nested tables should use valid nested table separators and shallow nesting |
170
+ | `ADW01/unknown-waiver-directive` | Waiver directive names should be known |
171
+ | `ADW02/missing-waiver-rule-list` | Waiver directives should include a rule list |
172
+ | `ADW03/malformed-waiver-rule-list` | Waiver rule lists should use comma-separated rule IDs |
173
+ | `ADW04/unknown-waiver-rule-id` | Waiver rule IDs should be defined rules |
174
+ | `ADW05/unpaired-waiver-enable-block` | Waiver enable-block directives should have a preceding disable-block |
175
+ | `ADW06/unpaired-waiver-disable-block` | Waiver disable-block directives should have a following enable-block |
176
+ | `ADW07/mismatched-waiver-block-rule-list` | Waiver block delimiters should use matching rule lists |
177
+ | `ADW08/waiver-targets-waiver-rule` | Source waivers should not target ADW waiver diagnostics |
213
178
 
214
179
  ## Tags
215
180
 
216
- Tags group related rules and can be used to enable or disable classes of rules.
181
+ Tags group related rules and can be used to enable or disable classes of normal
182
+ rules. `ADW##` waiver diagnostics use tags for discovery, but remain always on.
217
183
 
218
184
  | Group | IDs |
219
185
  |---|---|
220
- | `blank_lines` | `AD008` |
186
+ | `anchor` | `AD057` |
187
+ | `blank_lines` | `AD008`, `AD009` |
221
188
  | `blocks` | `AD003`, `AD032`, `AD035` |
222
189
  | `accessibility` | `AD028`, `AD042` |
223
- | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041` |
224
- | `conversion` | `AD029`, `AD030`, `AD031`, `AD036`, `AD037`, `AD039`, `AD040` |
190
+ | `cleanup` | `AD032`, `AD034`, `AD035`, `AD036`, `AD037`, `AD039`, `AD040`, `AD041`, `AD055`, `AD058`, `AD059` |
191
+ | `conversion` | `AD029`, `AD030`, `AD031`, `AD037`, `AD039`, `AD040`, `AD055`, `AD056`, `AD058`, `AD059` |
192
+ | `docx` | `AD016`, `AD017`, `AD028`, `AD055`, `AD056`, `AD058`, `AD059` |
225
193
  | `dependencies` | `AD024`, `AD025`, `AD026`, `AD027` |
226
194
  | `diagram` | `AD012` |
227
195
  | `format` | `AD041` |
228
196
  | `headings` | `AD001`, `AD002`, `AD005`, `AD006`, `AD007`, `AD043`, `AD045` |
229
- | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028` |
197
+ | `image` | `AD011`, `AD013`, `AD016`, `AD025`, `AD028`, `AD057` |
230
198
  | `include` | `AD006`, `AD019`, `AD022`, `AD024` |
231
199
  | `inline` | `AD041` |
232
- | `lists` | `AD008`, `AD036` |
200
+ | `list` | `AD056` |
201
+ | `lists` | `AD008`, `AD009`, `AD036` |
202
+ | `pandoc` | `AD059` |
233
203
  | `parser` | `AD000` |
234
- | `table` | `AD004`, `AD010`, `AD017`, `AD030` |
204
+ | `table` | `AD004`, `AD010`, `AD017`, `AD030`, `AD057`, `AD059` |
205
+ | `waiver` | `ADW01`, `ADW02`, `ADW03`, `ADW04`, `ADW05`, `ADW06`, `ADW07`, `ADW08` |
235
206
  | `whitespace` | `AD034` |
236
207
  | `references` | `AD023` |
237
208
  | `links` | `AD027`, `AD031`, `AD042`, `AD044` |
238
- | `structure` | `AD043` |
209
+ | `structure` | `AD009`, `AD019`, `AD020`, `AD022`, `AD023`, `AD043`, `AD046`, `AD047`, `AD048`, `AD049`, `AD050`, `AD051`, `AD052`, `AD053`, `AD054`, `AD056`, `AD057` |
239
210
  | `markdown-compatibility` | `AD045` |
240
211
  | `maintainability` | `AD045` |
241
212
  | `xref` | `AD026`, `AD042`, `AD044` |
242
213
 
243
- ## Rule ID Namespaces
244
-
245
- Built-in rule IDs use one reserved namespace:
246
-
247
- - `AD###` - all built-in `asciidoclint` rules.
214
+ ## Documentation
248
215
 
249
- Rule responsibility is expressed through tags such as `headings`,
250
- `dependencies`, `policy`, and `cleanup`, not through multiple built-in ID
251
- prefixes. This keeps built-in IDs predictable as the rule set grows.
252
-
253
- Company, product, or template-specific rules should not be built-ins. Use a
254
- three-letter custom prefix such as `ORG`, `ABC`, or a team-owned namespace. The
255
- registry rejects duplicate IDs and aliases across built-in and custom rules.
216
+ Start with the architecture proposal:
256
217
 
257
- The rule-by-rule rendering and severity rationale is in
258
- [docs/rules/rule-necessity.md](docs/rules/rule-necessity.md).
218
+ - [Architecture](docs/architecture.md)
219
+ - [Configuration](docs/configuration.md)
220
+ - [Rule architecture](docs/rule-architecture.md)
221
+ - [Waivers](docs/waiver.md)
222
+ - [Custom rules](docs/custom-rules.md)
223
+
224
+ Detailed per-rule docs live under [docs/rules](docs/rules/). The CLI exposes
225
+ the rule catalog through `--list-rules`, readable rule help through `--explain`,
226
+ and structured rule metadata through `--explain <rule> --format json`.
package/dist/api/fixes.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  export function applyFixes(findings, unsafeFixes) {
3
3
  const allowed = unsafeFixes ? ["safe", "unsafe"] : ["safe"];
4
4
  const edits = findings.flatMap((finding) => {
5
- if (!finding.fix || !allowed.includes(finding.fix.applicability)) {
5
+ if (finding.waived || !finding.fix || !allowed.includes(finding.fix.applicability)) {
6
6
  return [];
7
7
  }
8
8
  return finding.fix.edits;
package/dist/api/lint.js CHANGED
@@ -6,6 +6,7 @@ import { helpers } from "../rules/helpers.js";
6
6
  import { getVersion } from "../version.js";
7
7
  import { applyFixes } from "./fixes.js";
8
8
  import { loadRules } from "./rules.js";
9
+ import { applyWaivers } from "./waivers.js";
9
10
  export async function lintFiles(patterns, options = {}) {
10
11
  return lintFilesInternal(patterns, options, false);
11
12
  }
@@ -14,9 +15,15 @@ async function lintFilesInternal(patterns, options, afterFix) {
14
15
  const { config, rules } = await loadRules(options);
15
16
  const files = await expandFiles(patterns, cwd, config);
16
17
  const enabledRules = filterEnabledRules(rules, config);
18
+ const knownRuleIds = new Set(["AD000", ...rules.map((rule) => rule.id)]);
17
19
  const findings = [];
20
+ const parsedFiles = new Map();
18
21
  for (const file of files) {
19
22
  const document = parseDocument(file);
23
+ for (const parsedFile of document.files) {
24
+ parsedFiles.set(path.resolve(parsedFile.file), parsedFile);
25
+ }
26
+ mergeAsciidoctorSections(document, await collectParserSections(file));
20
27
  mergeAsciidoctorBlocks(document, await collectParserBlocks(file));
21
28
  mergeAsciidoctorReferenceTargets(document, await collectParserReferenceTargets(file));
22
29
  resolveDocumentXrefs(document);
@@ -45,7 +52,7 @@ async function lintFilesInternal(patterns, options, afterFix) {
45
52
  });
46
53
  }
47
54
  }
48
- const result = { files, findings: sortFindings(findings) };
55
+ const result = { files, findings: sortFindings(applyWaivers(findings, [...parsedFiles.values()], knownRuleIds)) };
49
56
  if (options.fix && !afterFix) {
50
57
  applyFixes(result.findings, options.unsafeFixes ?? false);
51
58
  return lintFilesInternal(patterns, { ...options, fix: false }, true);
@@ -63,24 +70,60 @@ async function collectParserBlocks(file) {
63
70
  const { collectAsciidoctorBlocks } = await import("../parsers/asciidoctor.js");
64
71
  return collectAsciidoctorBlocks(file);
65
72
  }
73
+ async function collectParserSections(file) {
74
+ const { collectAsciidoctorSections } = await import("../parsers/asciidoctor.js");
75
+ return collectAsciidoctorSections(file);
76
+ }
66
77
  async function collectParserReferenceTargets(file) {
67
78
  const { collectAsciidoctorReferenceTargets } = await import("../parsers/asciidoctor.js");
68
79
  return collectAsciidoctorReferenceTargets(file);
69
80
  }
81
+ function mergeAsciidoctorSections(document, sections) {
82
+ if (!sections.length) {
83
+ return;
84
+ }
85
+ for (const section of sections) {
86
+ const existing = document.sections.find((candidate) => (path.resolve(candidate.range.start.file) === path.resolve(section.range.start.file)
87
+ && candidate.range.start.line === section.range.start.line
88
+ && candidate.title === section.title));
89
+ if (!existing) {
90
+ continue;
91
+ }
92
+ existing.sectname = section.sectname;
93
+ existing.source = "asciidoctor";
94
+ if (!existing.style && section.style) {
95
+ existing.style = section.style;
96
+ }
97
+ }
98
+ }
70
99
  function mergeAsciidoctorBlocks(document, blocks) {
71
100
  if (!blocks.length) {
72
101
  return;
73
102
  }
74
- const authoritativeTypes = new Set(blocks.map((block) => block.type));
103
+ const authoritativeTypes = new Set(blocks
104
+ .filter((block) => ["table", "image", "diagram"].includes(block.type))
105
+ .map((block) => block.type));
75
106
  const authoritativeFiles = new Set(blocks.map((block) => path.resolve(block.range.start.file)));
76
- document.blocks = [
107
+ const merged = [
77
108
  ...document.blocks.filter((block) => (!authoritativeTypes.has(block.type)
78
109
  || !authoritativeFiles.has(path.resolve(block.range.start.file)))),
79
- ...blocks,
80
- ].sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
110
+ ];
111
+ for (const block of blocks) {
112
+ if (!merged.some((candidate) => sameBlock(candidate, block))) {
113
+ merged.push(block);
114
+ }
115
+ }
116
+ document.blocks = merged.sort((a, b) => (a.range.start.file.localeCompare(b.range.start.file)
81
117
  || a.range.start.line - b.range.start.line
82
118
  || a.range.start.column - b.range.start.column));
83
119
  }
120
+ function sameBlock(a, b) {
121
+ return path.resolve(a.range.start.file) === path.resolve(b.range.start.file)
122
+ && a.range.start.line === b.range.start.line
123
+ && a.range.start.column === b.range.start.column
124
+ && a.type === b.type
125
+ && a.style === b.style;
126
+ }
84
127
  function mergeAsciidoctorReferenceTargets(document, targets) {
85
128
  const byKey = new Map();
86
129
  for (const target of document.referenceTargets) {
@@ -5,29 +5,37 @@ export interface Config {
5
5
  customRules?: string[];
6
6
  ignores?: string[];
7
7
  rules?: Record<string, RuleSetting>;
8
- editor?: EditorConfig;
9
- baseDir?: string;
10
8
  }
11
9
  export type RuleSetting = boolean | {
12
10
  severity?: "error" | "warning" | "info";
13
11
  enabled?: boolean;
14
12
  };
15
- export interface EditorConfig {
16
- defaultScope?: "file" | "document" | "workspace";
17
- lintOnSave?: boolean;
18
- followSymlinks?: boolean;
19
- importCliDiagnostics?: boolean;
20
- }
21
13
  export interface RuleLoadOptions {
22
14
  configFile?: string;
23
15
  customRules?: string[];
24
16
  cwd?: string;
17
+ homeDir?: string;
18
+ noGlobalConfig?: boolean;
19
+ }
20
+ export interface ConfigSource {
21
+ kind: "global" | "project" | "explicit";
22
+ file: string;
23
+ }
24
+ export interface ConfigLoadOptions {
25
+ configFile?: string;
26
+ cwd?: string;
27
+ homeDir?: string;
28
+ noGlobalConfig?: boolean;
25
29
  }
26
30
  export declare function loadRules(options?: RuleLoadOptions): Promise<{
27
31
  config: Config;
28
32
  rules: Rule[];
29
33
  }>;
30
34
  export declare function ruleMetadata(rule: Rule): object;
31
- export declare function loadConfig(configFile: string | undefined, cwd: string): Config;
35
+ export declare function loadConfig(configFile: string | undefined, cwd: string, options?: ConfigLoadOptions): Config;
36
+ export declare function loadConfigDetails(options?: ConfigLoadOptions): {
37
+ config: Config;
38
+ sources: ConfigSource[];
39
+ };
32
40
  export declare function loadCustomRules(references: string[], cwd: string): Promise<Rule[]>;
33
41
  export declare function normalizeConfig(config: Config | undefined): Config;
package/dist/api/rules.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import yaml from "js-yaml";
@@ -6,9 +7,9 @@ import { builtInRules } from "../rules/builtin.js";
6
7
  import { validateRules } from "../rules/registry.js";
7
8
  export async function loadRules(options = {}) {
8
9
  const cwd = path.resolve(options.cwd ?? process.cwd());
9
- const config = loadConfig(options.configFile, cwd);
10
+ const config = loadConfig(options.configFile, cwd, options);
10
11
  const customRules = [
11
- ...await loadCustomRules(config.customRules ?? [], config.baseDir ?? cwd),
12
+ ...await loadCustomRules(config.customRules ?? [], cwd),
12
13
  ...await loadCustomRules(options.customRules ?? [], cwd),
13
14
  ];
14
15
  const rules = [...builtInRules, ...customRules];
@@ -26,18 +27,19 @@ export function ruleMetadata(rule) {
26
27
  docs: rule.docs,
27
28
  };
28
29
  }
29
- export function loadConfig(configFile, cwd) {
30
- const candidates = configFile ? [configFile] : [".asciidoclint.yaml", ".asciidoclint.yml"];
31
- for (const candidate of candidates) {
32
- const absolute = path.resolve(cwd, candidate);
33
- if (fs.existsSync(absolute)) {
34
- return {
35
- ...normalizeConfig(yaml.load(fs.readFileSync(absolute, "utf8"))),
36
- baseDir: path.dirname(absolute),
37
- };
38
- }
39
- }
40
- return {};
30
+ export function loadConfig(configFile, cwd, options = {}) {
31
+ return loadConfigDetails({ ...options, configFile, cwd }).config;
32
+ }
33
+ export function loadConfigDetails(options = {}) {
34
+ const cwd = path.resolve(options.cwd ?? process.cwd());
35
+ const sources = configSources({
36
+ configFile: options.configFile,
37
+ cwd,
38
+ homeDir: options.homeDir,
39
+ noGlobalConfig: options.noGlobalConfig,
40
+ });
41
+ const config = sources.reduce((base, source) => (mergeConfig(base, configFromFile(source.file))), {});
42
+ return { config, sources };
41
43
  }
42
44
  export async function loadCustomRules(references, cwd) {
43
45
  const rules = [];
@@ -63,20 +65,71 @@ export function normalizeConfig(config) {
63
65
  function mergeConfig(base, override) {
64
66
  return {
65
67
  extends: override.extends ?? base.extends,
66
- baseDir: override.baseDir ?? base.baseDir,
67
68
  documents: [...(base.documents ?? []), ...(override.documents ?? [])],
68
69
  customRules: [...(base.customRules ?? []), ...(override.customRules ?? [])],
69
70
  ignores: [...(base.ignores ?? []), ...(override.ignores ?? [])],
70
- editor: {
71
- ...(base.editor ?? {}),
72
- ...(override.editor ?? {}),
73
- },
74
71
  rules: {
75
72
  ...(base.rules ?? {}),
76
73
  ...(override.rules ?? {}),
77
74
  },
78
75
  };
79
76
  }
77
+ function configSources(options) {
78
+ const sources = [];
79
+ if (!options.noGlobalConfig) {
80
+ const global = path.join(path.resolve(options.homeDir ?? os.homedir()), ".asciidoclint", "config.yaml");
81
+ if (fs.existsSync(global)) {
82
+ sources.push({ kind: "global", file: global });
83
+ }
84
+ }
85
+ if (options.configFile) {
86
+ const explicit = path.resolve(options.cwd, options.configFile);
87
+ if (fs.existsSync(explicit)) {
88
+ sources.push({ kind: "explicit", file: explicit });
89
+ }
90
+ return sources;
91
+ }
92
+ const project = findProjectConfig(options.cwd);
93
+ if (project && !sources.some((source) => path.resolve(source.file) === path.resolve(project))) {
94
+ sources.push({ kind: "project", file: project });
95
+ }
96
+ return sources;
97
+ }
98
+ function findProjectConfig(cwd) {
99
+ let directory = path.resolve(cwd);
100
+ while (true) {
101
+ const candidate = path.join(directory, ".asciidoclint", "config.yaml");
102
+ if (fs.existsSync(candidate)) {
103
+ return candidate;
104
+ }
105
+ const parent = path.dirname(directory);
106
+ if (parent === directory) {
107
+ return undefined;
108
+ }
109
+ directory = parent;
110
+ }
111
+ }
112
+ function configFromFile(file) {
113
+ const baseDir = configReferenceBaseDir(file);
114
+ const loaded = normalizeConfig(yaml.load(fs.readFileSync(file, "utf8")));
115
+ return {
116
+ ...loaded,
117
+ customRules: loaded.customRules?.map((reference) => resolveConfigReference(reference, baseDir)),
118
+ };
119
+ }
120
+ function configReferenceBaseDir(file) {
121
+ const directory = path.dirname(file);
122
+ if (path.basename(file) === "config.yaml" && path.basename(directory) === ".asciidoclint") {
123
+ return path.dirname(directory);
124
+ }
125
+ return directory;
126
+ }
127
+ function resolveConfigReference(reference, baseDir) {
128
+ if (reference.startsWith(".") || reference.startsWith("/")) {
129
+ return path.resolve(baseDir, reference);
130
+ }
131
+ return reference;
132
+ }
80
133
  function presetConfig(name) {
81
134
  const rules = {};
82
135
  const enableByTag = (tag) => {
@@ -102,10 +155,51 @@ function presetConfig(name) {
102
155
  }
103
156
  }
104
157
  function resolveImport(reference, cwd) {
105
- if (reference.startsWith(".") || reference.startsWith("/") || /\.(ts|mts|cts|m?js|cjs)$/.test(reference)) {
106
- return pathToFileURL(path.resolve(cwd, reference)).href;
158
+ if (!isLocalReference(reference)) {
159
+ return reference;
107
160
  }
108
- return reference;
161
+ const absolute = path.resolve(cwd, reference);
162
+ if (fs.existsSync(absolute) && fs.statSync(absolute).isDirectory()) {
163
+ return pathToFileURL(resolveRulePackageEntry(absolute)).href;
164
+ }
165
+ return pathToFileURL(absolute).href;
166
+ }
167
+ function isLocalReference(reference) {
168
+ return reference.startsWith(".") || reference.startsWith("/") || /\.(ts|mts|cts|m?js|cjs)$/.test(reference);
169
+ }
170
+ function resolveRulePackageEntry(directory) {
171
+ const packageJson = path.join(directory, "package.json");
172
+ const packageEntry = fs.existsSync(packageJson) ? packageEntryFromJson(packageJson) : undefined;
173
+ const candidates = [
174
+ packageEntry && path.resolve(directory, packageEntry),
175
+ path.join(directory, "dist", "index.js"),
176
+ path.join(directory, "dist", "index.mjs"),
177
+ path.join(directory, "src", "index.js"),
178
+ path.join(directory, "src", "index.mjs"),
179
+ path.join(directory, "src", "index.ts"),
180
+ path.join(directory, "src", "index.mts"),
181
+ path.join(directory, "index.js"),
182
+ path.join(directory, "index.mjs"),
183
+ path.join(directory, "index.ts"),
184
+ ].filter((candidate) => Boolean(candidate));
185
+ const entry = candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
186
+ if (!entry) {
187
+ throw new Error(`Custom rule package has no loadable entry: ${directory}`);
188
+ }
189
+ return entry;
190
+ }
191
+ function packageEntryFromJson(file) {
192
+ const packageJson = JSON.parse(fs.readFileSync(file, "utf8"));
193
+ if (typeof packageJson.exports === "string") {
194
+ return packageJson.exports;
195
+ }
196
+ if (packageJson.exports && typeof packageJson.exports["."] === "string") {
197
+ return packageJson.exports["."];
198
+ }
199
+ if (packageJson.exports && typeof packageJson.exports["."] === "object") {
200
+ return packageJson.exports["."].import ?? packageJson.exports["."].default;
201
+ }
202
+ return packageJson.module ?? packageJson.main;
109
203
  }
110
204
  function asArray(value) {
111
205
  if (!value) {