asciidoclint 0.5.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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/assets/README.md +12 -0
- package/assets/icon.svg +198 -0
- package/assets/logo.svg +203 -0
- package/dist/api/fixes.d.ts +6 -0
- package/dist/api/fixes.js +61 -0
- package/dist/api/lint.d.ts +2 -0
- package/dist/api/lint.js +191 -0
- package/dist/api/rules.d.ts +33 -0
- package/dist/api/rules.js +115 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +86 -0
- package/dist/cli/init-rule.d.ts +7 -0
- package/dist/cli/init-rule.js +74 -0
- package/dist/cli/install-skill.d.ts +10 -0
- package/dist/cli/install-skill.js +37 -0
- package/dist/formatters/json.d.ts +2 -0
- package/dist/formatters/json.js +30 -0
- package/dist/formatters/pretty.d.ts +2 -0
- package/dist/formatters/pretty.js +41 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/parsers/asciidoctor.d.ts +4 -0
- package/dist/parsers/asciidoctor.js +444 -0
- package/dist/parsers/tolerant.d.ts +4 -0
- package/dist/parsers/tolerant.js +528 -0
- package/dist/rules/AD001.d.ts +2 -0
- package/dist/rules/AD001.js +28 -0
- package/dist/rules/AD002.d.ts +2 -0
- package/dist/rules/AD002.js +30 -0
- package/dist/rules/AD003.d.ts +2 -0
- package/dist/rules/AD003.js +28 -0
- package/dist/rules/AD004.d.ts +2 -0
- package/dist/rules/AD004.js +58 -0
- package/dist/rules/AD005.d.ts +2 -0
- package/dist/rules/AD005.js +31 -0
- package/dist/rules/AD006.d.ts +2 -0
- package/dist/rules/AD006.js +53 -0
- package/dist/rules/AD007.d.ts +2 -0
- package/dist/rules/AD007.js +39 -0
- package/dist/rules/AD008.d.ts +2 -0
- package/dist/rules/AD008.js +88 -0
- package/dist/rules/AD010.d.ts +2 -0
- package/dist/rules/AD010.js +39 -0
- package/dist/rules/AD011.d.ts +2 -0
- package/dist/rules/AD011.js +31 -0
- package/dist/rules/AD012.d.ts +2 -0
- package/dist/rules/AD012.js +28 -0
- package/dist/rules/AD013.d.ts +2 -0
- package/dist/rules/AD013.js +43 -0
- package/dist/rules/AD016.d.ts +2 -0
- package/dist/rules/AD016.js +83 -0
- package/dist/rules/AD017.d.ts +2 -0
- package/dist/rules/AD017.js +53 -0
- package/dist/rules/AD019.d.ts +2 -0
- package/dist/rules/AD019.js +58 -0
- package/dist/rules/AD020.d.ts +2 -0
- package/dist/rules/AD020.js +40 -0
- package/dist/rules/AD022.d.ts +2 -0
- package/dist/rules/AD022.js +55 -0
- package/dist/rules/AD023.d.ts +2 -0
- package/dist/rules/AD023.js +59 -0
- package/dist/rules/AD024.d.ts +2 -0
- package/dist/rules/AD024.js +30 -0
- package/dist/rules/AD025.d.ts +2 -0
- package/dist/rules/AD025.js +32 -0
- package/dist/rules/AD026.d.ts +2 -0
- package/dist/rules/AD026.js +26 -0
- package/dist/rules/AD027.d.ts +2 -0
- package/dist/rules/AD027.js +31 -0
- package/dist/rules/AD028.d.ts +2 -0
- package/dist/rules/AD028.js +113 -0
- package/dist/rules/AD029.d.ts +2 -0
- package/dist/rules/AD029.js +46 -0
- package/dist/rules/AD030.d.ts +2 -0
- package/dist/rules/AD030.js +33 -0
- package/dist/rules/AD031.d.ts +2 -0
- package/dist/rules/AD031.js +66 -0
- package/dist/rules/AD032.d.ts +2 -0
- package/dist/rules/AD032.js +81 -0
- package/dist/rules/AD034.d.ts +2 -0
- package/dist/rules/AD034.js +50 -0
- package/dist/rules/AD035.d.ts +2 -0
- package/dist/rules/AD035.js +77 -0
- package/dist/rules/AD036.d.ts +2 -0
- package/dist/rules/AD036.js +34 -0
- package/dist/rules/AD037.d.ts +2 -0
- package/dist/rules/AD037.js +34 -0
- package/dist/rules/AD039.d.ts +2 -0
- package/dist/rules/AD039.js +58 -0
- package/dist/rules/AD040.d.ts +2 -0
- package/dist/rules/AD040.js +56 -0
- package/dist/rules/AD041.d.ts +2 -0
- package/dist/rules/AD041.js +66 -0
- package/dist/rules/AD042.d.ts +2 -0
- package/dist/rules/AD042.js +62 -0
- package/dist/rules/AD043.d.ts +2 -0
- package/dist/rules/AD043.js +30 -0
- package/dist/rules/AD044.d.ts +2 -0
- package/dist/rules/AD044.js +54 -0
- package/dist/rules/AD045.d.ts +2 -0
- package/dist/rules/AD045.js +66 -0
- package/dist/rules/builtin.d.ts +3 -0
- package/dist/rules/builtin.js +81 -0
- package/dist/rules/helpers.d.ts +2 -0
- package/dist/rules/helpers.js +11 -0
- package/dist/rules/registry.d.ts +3 -0
- package/dist/rules/registry.js +34 -0
- package/dist/rules/utils.d.ts +42 -0
- package/dist/rules/utils.js +274 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/package.json +70 -0
- package/skills/asciidoclint/SKILL.md +84 -0
- package/skills/asciidoclint/references/ai-fix-policy.md +11 -0
- package/skills/asciidoclint/references/result-schema.md +22 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { lintFiles } from "../api/lint.js";
|
|
4
|
+
import { loadRules, ruleMetadata } from "../api/rules.js";
|
|
5
|
+
import { formatJson } from "../formatters/json.js";
|
|
6
|
+
import { formatPretty } from "../formatters/pretty.js";
|
|
7
|
+
import { resolveRuleReference, validateRules } from "../rules/registry.js";
|
|
8
|
+
import { getVersion } from "../version.js";
|
|
9
|
+
import { initRule } from "./init-rule.js";
|
|
10
|
+
import { installSkill } from "./install-skill.js";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name("asciidoclint")
|
|
14
|
+
.description("Lint AsciiDoc syntax, structure, dependencies, and policy")
|
|
15
|
+
.version(getVersion());
|
|
16
|
+
program
|
|
17
|
+
.command("init-rule")
|
|
18
|
+
.description("scaffold a custom rule")
|
|
19
|
+
.requiredOption("--pack <name>", "rule pack name")
|
|
20
|
+
.requiredOption("--id <id>", "stable rule id")
|
|
21
|
+
.requiredOption("--alias <alias>", "readable rule alias")
|
|
22
|
+
.option("--directory <dir>", "rule directory", "lint-rules")
|
|
23
|
+
.action((options) => {
|
|
24
|
+
const files = initRule(options);
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
console.log(file);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("install-skill")
|
|
31
|
+
.description("install the bundled Codex skill")
|
|
32
|
+
.option("--dest <directory>", "skills root directory")
|
|
33
|
+
.option("--project", "install into .codex/skills in the current project")
|
|
34
|
+
.option("--force", "replace an existing installed skill")
|
|
35
|
+
.action((options) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = installSkill(options);
|
|
38
|
+
console.log(`Installed asciidoclint skill to ${result.destination}`);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
42
|
+
process.exitCode = 2;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.argument("[files...]", "AsciiDoc files or glob patterns")
|
|
47
|
+
.option("-f, --format <format>", "output format: pretty or json", "pretty")
|
|
48
|
+
.option("-c, --config <file>", "configuration file")
|
|
49
|
+
.option("--fix", "apply safe fixes")
|
|
50
|
+
.option("--unsafe", "also apply unsafe fixes with --fix")
|
|
51
|
+
.option("--output-diagnostics <file>", "write JSON diagnostics artifact for editor import")
|
|
52
|
+
.option("--list-rules", "list configured built-in and custom rules")
|
|
53
|
+
.option("--explain <rule>", "explain a configured rule by id or alias")
|
|
54
|
+
.option("--validate-rules", "validate configured rule registry")
|
|
55
|
+
.action(async (files, options) => {
|
|
56
|
+
const { rules } = await loadRules({ configFile: options.config });
|
|
57
|
+
if (options.validateRules) {
|
|
58
|
+
validateRules(rules);
|
|
59
|
+
console.log("Rule registry is valid");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (options.listRules) {
|
|
63
|
+
console.log(JSON.stringify(rules.map(ruleMetadata), null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (options.explain) {
|
|
67
|
+
const rule = resolveRuleReference(rules, options.explain);
|
|
68
|
+
if (!rule) {
|
|
69
|
+
console.error(`Unknown rule: ${options.explain}`);
|
|
70
|
+
process.exitCode = 2;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
console.log(JSON.stringify(ruleMetadata(rule), null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const result = await lintFiles(files, {
|
|
77
|
+
configFile: options.config,
|
|
78
|
+
format: options.format,
|
|
79
|
+
fix: options.fix,
|
|
80
|
+
unsafeFixes: options.unsafe,
|
|
81
|
+
outputDiagnosticsFile: options.outputDiagnostics,
|
|
82
|
+
});
|
|
83
|
+
console.log(options.format === "json" ? formatJson(result) : formatPretty(result));
|
|
84
|
+
process.exitCode = result.findings.some((finding) => finding.severity === "error") ? 1 : 0;
|
|
85
|
+
});
|
|
86
|
+
await program.parseAsync();
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function initRule(options) {
|
|
4
|
+
const directory = path.resolve(options.directory ?? "lint-rules");
|
|
5
|
+
const basename = `${options.id}-${options.alias}`;
|
|
6
|
+
const rulePath = path.join(directory, `${basename}.ts`);
|
|
7
|
+
const testPath = path.join(directory, `${basename}.test.ts`);
|
|
8
|
+
const fixtureDirectory = path.join(directory, "fixtures", basename);
|
|
9
|
+
const badFixture = path.join(fixtureDirectory, "bad.adoc");
|
|
10
|
+
const goodFixture = path.join(fixtureDirectory, "good.adoc");
|
|
11
|
+
fs.mkdirSync(fixtureDirectory, { recursive: true });
|
|
12
|
+
writeNew(rulePath, ruleTemplate(options));
|
|
13
|
+
writeNew(testPath, testTemplate(options));
|
|
14
|
+
writeNew(badFixture, "This line has TODO work.\n");
|
|
15
|
+
writeNew(goodFixture, "This line links to issue PROJ-123.\n");
|
|
16
|
+
return [rulePath, testPath, badFixture, goodFixture];
|
|
17
|
+
}
|
|
18
|
+
function writeNew(file, content) {
|
|
19
|
+
if (fs.existsSync(file)) {
|
|
20
|
+
throw new Error(`Refusing to overwrite existing file: ${file}`);
|
|
21
|
+
}
|
|
22
|
+
fs.writeFileSync(file, content);
|
|
23
|
+
}
|
|
24
|
+
function ruleTemplate(options) {
|
|
25
|
+
return `import type { Rule } from "asciidoclint";
|
|
26
|
+
|
|
27
|
+
export default {
|
|
28
|
+
id: "${options.id}",
|
|
29
|
+
alias: "${options.alias}",
|
|
30
|
+
description: "Describe what this rule checks",
|
|
31
|
+
tags: ["${options.pack}"],
|
|
32
|
+
docs: {
|
|
33
|
+
summary: "Explain the rule in one sentence.",
|
|
34
|
+
rationale: "Explain why the problematic structure should be avoided.",
|
|
35
|
+
badExamples: [{ code: "Problematic AsciiDoc example" }],
|
|
36
|
+
goodExamples: [{ code: "Expected AsciiDoc example" }],
|
|
37
|
+
},
|
|
38
|
+
parser: "text",
|
|
39
|
+
function: (params, onError) => {
|
|
40
|
+
params.lines.forEach((line, index) => {
|
|
41
|
+
const column = line.indexOf("TODO");
|
|
42
|
+
if (column !== -1) {
|
|
43
|
+
onError({
|
|
44
|
+
severity: "warning",
|
|
45
|
+
message: "Replace this placeholder rule logic",
|
|
46
|
+
range: {
|
|
47
|
+
start: {
|
|
48
|
+
file: params.file,
|
|
49
|
+
line: index + 1,
|
|
50
|
+
column: column + 1,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
} satisfies Rule;
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
function testTemplate(options) {
|
|
61
|
+
const basename = `${options.id}-${options.alias}`;
|
|
62
|
+
return `import { describe, expect, it } from "vitest";
|
|
63
|
+
import rule from "./${basename}.js";
|
|
64
|
+
|
|
65
|
+
describe("${options.id}/${options.alias}", () => {
|
|
66
|
+
it("has complete metadata", () => {
|
|
67
|
+
expect(rule.id).toBe("${options.id}");
|
|
68
|
+
expect(rule.alias).toBe("${options.alias}");
|
|
69
|
+
expect(rule.docs?.badExamples?.length).toBeGreaterThan(0);
|
|
70
|
+
expect(rule.docs?.goodExamples?.length).toBeGreaterThan(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface InstallSkillOptions {
|
|
2
|
+
dest?: string;
|
|
3
|
+
project?: boolean;
|
|
4
|
+
force?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface InstallSkillResult {
|
|
7
|
+
source: string;
|
|
8
|
+
destination: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function installSkill(options?: InstallSkillOptions): InstallSkillResult;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
const skillName = "asciidoclint";
|
|
6
|
+
export function installSkill(options = {}) {
|
|
7
|
+
const source = findBundledSkill();
|
|
8
|
+
const root = options.dest
|
|
9
|
+
? path.resolve(options.dest)
|
|
10
|
+
: options.project
|
|
11
|
+
? path.resolve(process.cwd(), ".codex", "skills")
|
|
12
|
+
: path.join(process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex"), "skills");
|
|
13
|
+
const destination = path.join(root, skillName);
|
|
14
|
+
if (fs.existsSync(destination)) {
|
|
15
|
+
if (!options.force) {
|
|
16
|
+
throw new Error(`${destination} already exists; pass --force to replace it`);
|
|
17
|
+
}
|
|
18
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
fs.mkdirSync(root, { recursive: true });
|
|
21
|
+
fs.cpSync(source, destination, { recursive: true });
|
|
22
|
+
return { source, destination };
|
|
23
|
+
}
|
|
24
|
+
function findBundledSkill() {
|
|
25
|
+
let current = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
while (true) {
|
|
27
|
+
const candidate = path.join(current, "skills", skillName);
|
|
28
|
+
if (fs.existsSync(path.join(candidate, "SKILL.md"))) {
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(current);
|
|
32
|
+
if (parent === current) {
|
|
33
|
+
throw new Error(`Could not find bundled ${skillName} skill`);
|
|
34
|
+
}
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function formatJson(result) {
|
|
3
|
+
return JSON.stringify({
|
|
4
|
+
files: result.files.map((file) => path.relative(process.cwd(), file)),
|
|
5
|
+
findings: result.findings.map((finding) => ({
|
|
6
|
+
...finding,
|
|
7
|
+
range: {
|
|
8
|
+
...finding.range,
|
|
9
|
+
start: {
|
|
10
|
+
...finding.range.start,
|
|
11
|
+
file: path.relative(process.cwd(), finding.range.start.file),
|
|
12
|
+
},
|
|
13
|
+
end: finding.range.end
|
|
14
|
+
? {
|
|
15
|
+
...finding.range.end,
|
|
16
|
+
file: path.relative(process.cwd(), finding.range.end.file),
|
|
17
|
+
}
|
|
18
|
+
: undefined,
|
|
19
|
+
},
|
|
20
|
+
})),
|
|
21
|
+
summary: summarize(result),
|
|
22
|
+
}, null, 2);
|
|
23
|
+
}
|
|
24
|
+
function summarize(result) {
|
|
25
|
+
return result.findings.reduce((summary, finding) => {
|
|
26
|
+
summary.total += 1;
|
|
27
|
+
summary[finding.severity] += 1;
|
|
28
|
+
return summary;
|
|
29
|
+
}, { total: 0, error: 0, warning: 0, info: 0 });
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { ruleLabel } from "../rules/builtin.js";
|
|
3
|
+
export function formatPretty(result) {
|
|
4
|
+
if (result.findings.length === 0) {
|
|
5
|
+
return "0 findings";
|
|
6
|
+
}
|
|
7
|
+
const lines = [];
|
|
8
|
+
for (const finding of result.findings) {
|
|
9
|
+
lines.push(primaryLine(finding));
|
|
10
|
+
if (finding.context) {
|
|
11
|
+
lines.push(` ${finding.context}`);
|
|
12
|
+
}
|
|
13
|
+
if (finding.fixHelper) {
|
|
14
|
+
lines.push(` fix helper: ${finding.fixHelper}`);
|
|
15
|
+
}
|
|
16
|
+
lines.push("");
|
|
17
|
+
}
|
|
18
|
+
const summary = result.findings.reduce((counts, finding) => {
|
|
19
|
+
counts.total += 1;
|
|
20
|
+
counts[finding.severity] += 1;
|
|
21
|
+
if (finding.fix?.applicability === "safe") {
|
|
22
|
+
counts.safe += 1;
|
|
23
|
+
}
|
|
24
|
+
if (finding.fix?.applicability === "unsafe") {
|
|
25
|
+
counts.unsafe += 1;
|
|
26
|
+
}
|
|
27
|
+
return counts;
|
|
28
|
+
}, { total: 0, error: 0, warning: 0, info: 0, safe: 0, unsafe: 0 });
|
|
29
|
+
lines.push(`${summary.total} findings: ${summary.error} errors, ${summary.warning} warnings, ${summary.info} info`);
|
|
30
|
+
if (summary.safe) {
|
|
31
|
+
lines.push(`${summary.safe} safe fixes available; run with --fix to apply them`);
|
|
32
|
+
}
|
|
33
|
+
if (summary.unsafe) {
|
|
34
|
+
lines.push(`${summary.unsafe} unsafe fixes available; run with --fix --unsafe to apply them`);
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
function primaryLine(finding) {
|
|
39
|
+
const file = path.relative(process.cwd(), finding.range.start.file);
|
|
40
|
+
return `${file}:${finding.range.start.line}:${finding.range.start.column} ${ruleLabel(finding)} ${finding.severity} ${finding.message}`;
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BlockNode, LintFinding, ReferenceTarget } from "../types.js";
|
|
2
|
+
export declare function collectAsciidoctorDiagnostics(file: string): LintFinding[];
|
|
3
|
+
export declare function collectAsciidoctorBlocks(file: string): BlockNode[];
|
|
4
|
+
export declare function collectAsciidoctorReferenceTargets(file: string): ReferenceTarget[];
|