asciidoclint 0.5.0 → 1.0.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 (102) hide show
  1. package/README.md +67 -118
  2. package/dist/api/fixes.js +1 -1
  3. package/dist/api/lint.js +7 -1
  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/rules/ADW01.d.ts +1 -0
  18. package/dist/rules/ADW01.js +2 -0
  19. package/dist/rules/ADW02.d.ts +1 -0
  20. package/dist/rules/ADW02.js +2 -0
  21. package/dist/rules/ADW03.d.ts +1 -0
  22. package/dist/rules/ADW03.js +2 -0
  23. package/dist/rules/ADW04.d.ts +1 -0
  24. package/dist/rules/ADW04.js +2 -0
  25. package/dist/rules/ADW05.d.ts +1 -0
  26. package/dist/rules/ADW05.js +2 -0
  27. package/dist/rules/ADW06.d.ts +1 -0
  28. package/dist/rules/ADW06.js +2 -0
  29. package/dist/rules/ADW07.d.ts +1 -0
  30. package/dist/rules/ADW07.js +2 -0
  31. package/dist/rules/ADW08.d.ts +1 -0
  32. package/dist/rules/ADW08.js +2 -0
  33. package/dist/rules/builtin.js +16 -0
  34. package/dist/rules/waiverRule.d.ts +104 -0
  35. package/dist/rules/waiverRule.js +108 -0
  36. package/dist/types.d.ts +12 -0
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/docs/architecture.md +1296 -0
  40. package/docs/configuration.md +126 -0
  41. package/docs/custom-rules.md +162 -0
  42. package/docs/release-workflow.md +630 -0
  43. package/docs/rule-architecture.md +94 -0
  44. package/docs/rules/AD000.md +43 -0
  45. package/docs/rules/AD001.md +36 -0
  46. package/docs/rules/AD002.md +48 -0
  47. package/docs/rules/AD003.md +36 -0
  48. package/docs/rules/AD004.md +54 -0
  49. package/docs/rules/AD005.md +87 -0
  50. package/docs/rules/AD006.md +88 -0
  51. package/docs/rules/AD007.md +59 -0
  52. package/docs/rules/AD008.md +48 -0
  53. package/docs/rules/AD010.md +45 -0
  54. package/docs/rules/AD011.md +48 -0
  55. package/docs/rules/AD012.md +50 -0
  56. package/docs/rules/AD013.md +75 -0
  57. package/docs/rules/AD016.md +52 -0
  58. package/docs/rules/AD017.md +51 -0
  59. package/docs/rules/AD019.md +53 -0
  60. package/docs/rules/AD020.md +54 -0
  61. package/docs/rules/AD022.md +33 -0
  62. package/docs/rules/AD023.md +59 -0
  63. package/docs/rules/AD024.md +48 -0
  64. package/docs/rules/AD025.md +64 -0
  65. package/docs/rules/AD026.md +43 -0
  66. package/docs/rules/AD027.md +50 -0
  67. package/docs/rules/AD028.md +48 -0
  68. package/docs/rules/AD029.md +57 -0
  69. package/docs/rules/AD030.md +42 -0
  70. package/docs/rules/AD031.md +48 -0
  71. package/docs/rules/AD032.md +65 -0
  72. package/docs/rules/AD034.md +48 -0
  73. package/docs/rules/AD035.md +62 -0
  74. package/docs/rules/AD036.md +51 -0
  75. package/docs/rules/AD037.md +49 -0
  76. package/docs/rules/AD039.md +59 -0
  77. package/docs/rules/AD040.md +49 -0
  78. package/docs/rules/AD041.md +56 -0
  79. package/docs/rules/AD042.md +42 -0
  80. package/docs/rules/AD043.md +54 -0
  81. package/docs/rules/AD044.md +47 -0
  82. package/docs/rules/AD045.md +62 -0
  83. package/docs/rules/ADW01.md +31 -0
  84. package/docs/rules/ADW02.md +31 -0
  85. package/docs/rules/ADW03.md +31 -0
  86. package/docs/rules/ADW04.md +31 -0
  87. package/docs/rules/ADW05.md +32 -0
  88. package/docs/rules/ADW06.md +33 -0
  89. package/docs/rules/ADW07.md +34 -0
  90. package/docs/rules/ADW08.md +31 -0
  91. package/docs/rules/rule-necessity.md +82 -0
  92. package/docs/waiver.md +201 -0
  93. package/package.json +2 -1
  94. package/skills/asciidoclint/SKILL.md +27 -60
  95. package/skills/asciidoclint/references/agentic-fix.md +29 -0
  96. package/skills/asciidoclint/references/feedback.md +79 -0
  97. package/skills/asciidoclint/references/lint-summary.md +45 -0
  98. package/skills/asciidoclint/references/result-schema.md +4 -0
  99. package/skills/asciidoclint/references/rule-create.md +136 -0
  100. package/skills/asciidoclint/references/rule-review.md +68 -0
  101. package/skills/asciidoclint/references/waivers.md +42 -0
  102. 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 |
@@ -207,13 +152,19 @@ then run **asciidoclint: Lint Current File** or **asciidoclint: Lint Workspace**
207
152
  | `AD043/section-title-start-left` | Section title syntax should start at the beginning of the line |
208
153
  | `AD044/local-adoc-link` | Local AsciiDoc files should be referenced with xref, not link |
209
154
  | `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.
155
+ | `ADW01/unknown-waiver-directive` | Waiver directive names should be known |
156
+ | `ADW02/missing-waiver-rule-list` | Waiver directives should include a rule list |
157
+ | `ADW03/malformed-waiver-rule-list` | Waiver rule lists should use comma-separated rule IDs |
158
+ | `ADW04/unknown-waiver-rule-id` | Waiver rule IDs should be defined rules |
159
+ | `ADW05/unpaired-waiver-enable-block` | Waiver enable-block directives should have a preceding disable-block |
160
+ | `ADW06/unpaired-waiver-disable-block` | Waiver disable-block directives should have a following enable-block |
161
+ | `ADW07/mismatched-waiver-block-rule-list` | Waiver block delimiters should use matching rule lists |
162
+ | `ADW08/waiver-targets-waiver-rule` | Source waivers should not target ADW waiver diagnostics |
213
163
 
214
164
  ## Tags
215
165
 
216
- Tags group related rules and can be used to enable or disable classes of rules.
166
+ Tags group related rules and can be used to enable or disable classes of normal
167
+ rules. `ADW##` waiver diagnostics use tags for discovery, but remain always on.
217
168
 
218
169
  | Group | IDs |
219
170
  |---|---|
@@ -232,6 +183,7 @@ Tags group related rules and can be used to enable or disable classes of rules.
232
183
  | `lists` | `AD008`, `AD036` |
233
184
  | `parser` | `AD000` |
234
185
  | `table` | `AD004`, `AD010`, `AD017`, `AD030` |
186
+ | `waiver` | `ADW01`, `ADW02`, `ADW03`, `ADW04`, `ADW05`, `ADW06`, `ADW07`, `ADW08` |
235
187
  | `whitespace` | `AD034` |
236
188
  | `references` | `AD023` |
237
189
  | `links` | `AD027`, `AD031`, `AD042`, `AD044` |
@@ -240,19 +192,16 @@ Tags group related rules and can be used to enable or disable classes of rules.
240
192
  | `maintainability` | `AD045` |
241
193
  | `xref` | `AD026`, `AD042`, `AD044` |
242
194
 
243
- ## Rule ID Namespaces
244
-
245
- Built-in rule IDs use one reserved namespace:
246
-
247
- - `AD###` - all built-in `asciidoclint` rules.
195
+ ## Documentation
248
196
 
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.
197
+ Start with the architecture proposal:
256
198
 
257
- The rule-by-rule rendering and severity rationale is in
258
- [docs/rules/rule-necessity.md](docs/rules/rule-necessity.md).
199
+ - [Architecture](docs/architecture.md)
200
+ - [Configuration](docs/configuration.md)
201
+ - [Rule architecture](docs/rule-architecture.md)
202
+ - [Waivers](docs/waiver.md)
203
+ - [Custom rules](docs/custom-rules.md)
204
+
205
+ Detailed per-rule docs live under [docs/rules](docs/rules/). The CLI exposes
206
+ the rule catalog through `--list-rules`, readable rule help through `--explain`,
207
+ 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,14 @@ 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
+ }
20
26
  mergeAsciidoctorBlocks(document, await collectParserBlocks(file));
21
27
  mergeAsciidoctorReferenceTargets(document, await collectParserReferenceTargets(file));
22
28
  resolveDocumentXrefs(document);
@@ -45,7 +51,7 @@ async function lintFilesInternal(patterns, options, afterFix) {
45
51
  });
46
52
  }
47
53
  }
48
- const result = { files, findings: sortFindings(findings) };
54
+ const result = { files, findings: sortFindings(applyWaivers(findings, [...parsedFiles.values()], knownRuleIds)) };
49
55
  if (options.fix && !afterFix) {
50
56
  applyFixes(result.findings, options.unsafeFixes ?? false);
51
57
  return lintFilesInternal(patterns, { ...options, fix: false }, true);
@@ -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) {
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) {
@@ -0,0 +1,2 @@
1
+ import type { LintFinding, ParsedFile } from "../types.js";
2
+ export declare function applyWaivers(findings: LintFinding[], files: ParsedFile[], knownRuleIds: Set<string>): LintFinding[];