@yegor256/dogent 0.0.1

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.txt ADDED
@@ -0,0 +1,21 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2026 Yegor Bugayenko
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.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # Dogmatic Linter for Agent Skills and Manifestos
2
+
3
+ [![dogent](https://github.com/yegor256/dogent/actions/workflows/dogent.yml/badge.svg)](https://github.com/yegor256/dogent/actions/workflows/dogent.yml)
4
+ [![PDD status](https://www.0pdd.com/svg?name=yegor256/dogent)](https://www.0pdd.com/p?name=yegor256/dogent)
5
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/dogent/blob/master/LICENSE.txt)
6
+ [![NPM version](https://badge.fury.io/js/@yegor256%2Fdogent.svg)](https://badge.fury.io/js/@yegor256%2Fdogent)
7
+
8
+ A strict, opinionated linter for agentic manifesto files
9
+ such as `SKILL.md`, `CLAUDE.md`, and `AGENTS.md`.
10
+
11
+ These files instruct AI agents.
12
+ Vague, bloated, or ambiguous instructions make agents behave unpredictably.
13
+ `dogent` enforces a clear, command-style discipline
14
+ so every line earns its place.
15
+
16
+ We respect [agent-sh/agnix](https://github.com/agent-sh/agnix)
17
+ as a prototype of this idea.
18
+ `dogent` goes further: it is stricter, more opinionated,
19
+ and aims for extreme quality with no compromise.
20
+
21
+ ## Usage
22
+
23
+ Run it on any manifesto file, no installation required:
24
+
25
+ ```bash
26
+ npx @yegor256/dogent CLAUDE.md
27
+ ```
28
+
29
+ Lint several files at once:
30
+
31
+ ```bash
32
+ npx @yegor256/dogent SKILL.md CLAUDE.md AGENTS.md
33
+ ```
34
+
35
+ Sample output:
36
+
37
+ ```text
38
+ CLAUDE.md
39
+ 12: line exceeds 80 symbols
40
+ 18: not an instruction, sounds like description
41
+ 24: article "the" detected, remove noise
42
+ 31: section name too long, use 1-3 words
43
+
44
+ 4 problems found, exit code 1
45
+ ```
46
+
47
+ The command exits with a non-zero status when problems are found,
48
+ so it plugs directly into CI and pre-commit hooks.
49
+
50
+ ## Rules
51
+
52
+ `dogent` checks that every manifesto obeys these rules:
53
+
54
+ - Every line must be an instruction.
55
+ - Instructions must be grouped in sections.
56
+ - Section names must be short, 1-3 words.
57
+ - Every line must be no longer than 80 symbols.
58
+ - Every line must sound like a command.
59
+ - No articles, no noise, no bloated text.
60
+ - Simple grammar, no ambiguity.
61
+
62
+ ## AI verification
63
+
64
+ `dogent` works standalone by default,
65
+ using fast deterministic checks with no network access.
66
+ When `OPENAI_API_KEY` or `CLAUDE_TOKEN` is present in the environment,
67
+ it additionally uses AI to verify the text for ambiguity,
68
+ weak phrasing, and instructions that only pretend to be commands:
69
+
70
+ ```bash
71
+ export CLAUDE_TOKEN=...
72
+ npx @yegor256/dogent CLAUDE.md
73
+ ```
74
+
75
+ ## GitHub Actions
76
+
77
+ Because `dogent` runs through `npx`, no extra action is needed.
78
+ Add a single step to any workflow to lint your manifestos on every push:
79
+
80
+ ```yaml
81
+ name: dogent
82
+ on: [push, pull_request]
83
+ jobs:
84
+ lint:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@v4
88
+ - uses: actions/setup-node@v4
89
+ with:
90
+ node-version: 20
91
+ - run: npx @yegor256/dogent CLAUDE.md SKILL.md
92
+ ```
93
+
94
+ The job fails when `dogent` finds problems,
95
+ blocking the merge until the manifestos are clean.
96
+ To enable AI verification in CI, expose a token as a secret:
97
+
98
+ ```yaml
99
+ - run: npx @yegor256/dogent CLAUDE.md
100
+ env:
101
+ CLAUDE_TOKEN: ${{ secrets.CLAUDE_TOKEN }}
102
+ ```
103
+
104
+ ## Pre-commit hook
105
+
106
+ Run `dogent` before every commit so broken manifestos never reach history.
107
+ Drop this into `.git/hooks/pre-commit` and make it executable:
108
+
109
+ ```bash
110
+ #!/bin/sh
111
+ npx @yegor256/dogent CLAUDE.md SKILL.md AGENTS.md
112
+ ```
113
+
114
+ Prefer the [pre-commit](https://pre-commit.com) framework?
115
+ Add `dogent` as a local hook in `.pre-commit-config.yaml`:
116
+
117
+ ```yaml
118
+ repos:
119
+ - repo: local
120
+ hooks:
121
+ - id: dogent
122
+ name: dogent
123
+ entry: npx @yegor256/dogent
124
+ language: system
125
+ files: '(CLAUDE|SKILL|AGENTS)\.md$'
126
+ ```
127
+
128
+ Either way, the commit is rejected until every flagged line is fixed.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "author": "Yegor Bugayenko <yegor256@gmail.com> (https://www.yegor256.com/)",
3
+ "bin": {
4
+ "dogent": "./src/dogent.js"
5
+ },
6
+ "bugs": "https://github.com/yegor256/dogent/issues",
7
+ "description": "A strict, opinionated linter for agentic manifesto files (SKILL.md, CLAUDE.md, AGENTS.md)",
8
+ "devDependencies": {
9
+ "@eslint/js": "^9.28.0",
10
+ "@stylistic/eslint-plugin": "^5.10.0",
11
+ "eslint": "^9.28.0",
12
+ "globals": "^17.6.0",
13
+ "mocha": "^11.5.0"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "homepage": "https://github.com/yegor256/dogent",
22
+ "keywords": [
23
+ "linter",
24
+ "lint",
25
+ "markdown",
26
+ "claude",
27
+ "agents",
28
+ "skill",
29
+ "manifesto",
30
+ "cli"
31
+ ],
32
+ "license": "MIT",
33
+ "main": "src/dogent.js",
34
+ "name": "@yegor256/dogent",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "repository": "yegor256/dogent",
39
+ "scripts": {
40
+ "lint": "eslint .",
41
+ "test": "mocha 'test/**/*.js' --timeout 60000"
42
+ },
43
+ "version": "0.0.1"
44
+ }
@@ -0,0 +1,30 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Document.
10
+ *
11
+ * An entire manifesto already parsed into an ordered collection of
12
+ * fragments, ready to be walked by a rule that hunts for violations.
13
+ */
14
+ class Document {
15
+ constructor(uri, fragments) {
16
+ this.address = uri;
17
+ this.pieces = fragments;
18
+ }
19
+ uri() {
20
+ return this.address;
21
+ }
22
+ fragments() {
23
+ return this.pieces;
24
+ }
25
+ walk(visitor) {
26
+ return this.pieces.reduce((all, piece) => all.concat(piece.accept(visitor)), []);
27
+ }
28
+ }
29
+
30
+ module.exports = Document;
package/src/dogent.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
4
+ * SPDX-License-Identifier: MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('fs');
10
+ const Markdown = require('./markdown');
11
+ const Report = require('./report');
12
+ const rules = require('./rules');
13
+
14
+ const argv = process.argv.slice(2);
15
+ const sarif = argv.indexOf('--sarif') !== -1;
16
+ const files = argv.filter((arg) => arg !== '--sarif');
17
+ if (files.length === 0) {
18
+ process.stderr.write('Usage: dogent [--sarif] <file.md>...\n');
19
+ process.exit(2);
20
+ }
21
+ const found = [];
22
+ files.forEach((file) => {
23
+ const document = new Markdown(file, fs.readFileSync(file, 'utf8')).document();
24
+ rules().forEach((rule) => {
25
+ rule.violations(document).forEach((violation) => found.push(violation));
26
+ });
27
+ });
28
+ const report = new Report('dogent', found);
29
+ process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
30
+ process.exit(report.count() > 0 ? 1 : 0);
@@ -0,0 +1,27 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Bullets.
10
+ *
11
+ * A composite fragment holding the inner pieces of one bullet list,
12
+ * dispatching itself first and then every item it contains.
13
+ */
14
+ class Bullets {
15
+ constructor(items, line) {
16
+ this.items = items;
17
+ this.row = line;
18
+ }
19
+ accept(visitor) {
20
+ return this.items.reduce(
21
+ (all, item) => all.concat(item.accept(visitor)),
22
+ [].concat(visitor.bullets(this.row))
23
+ );
24
+ }
25
+ }
26
+
27
+ module.exports = Bullets;
@@ -0,0 +1,25 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Header.
10
+ *
11
+ * A section heading line of a manifesto, carrying its raw text,
12
+ * its line number, and its depth (the count of leading hashes).
13
+ */
14
+ class Header {
15
+ constructor(content, line, level) {
16
+ this.content = content;
17
+ this.row = line;
18
+ this.depth = level;
19
+ }
20
+ accept(visitor) {
21
+ return visitor.header(this.content, this.row, this.depth);
22
+ }
23
+ }
24
+
25
+ module.exports = Header;
@@ -0,0 +1,24 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Prose.
10
+ *
11
+ * A single line of English prose, one instruction of a manifesto,
12
+ * carrying its raw text and its line number.
13
+ */
14
+ class Prose {
15
+ constructor(content, line) {
16
+ this.content = content;
17
+ this.row = line;
18
+ }
19
+ accept(visitor) {
20
+ return visitor.prose(this.content, this.row);
21
+ }
22
+ }
23
+
24
+ module.exports = Prose;
@@ -0,0 +1,24 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Snippet.
10
+ *
11
+ * A fenced code block taken as bulk, carrying the whole block as raw
12
+ * text and the line number where the fence opens.
13
+ */
14
+ class Snippet {
15
+ constructor(content, line) {
16
+ this.content = content;
17
+ this.row = line;
18
+ }
19
+ accept(visitor) {
20
+ return visitor.snippet(this.content, this.row);
21
+ }
22
+ }
23
+
24
+ module.exports = Snippet;
@@ -0,0 +1,89 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Document = require('./document');
9
+ const Header = require('./fragments/header');
10
+ const Prose = require('./fragments/prose');
11
+ const Bullets = require('./fragments/bullets');
12
+ const Snippet = require('./fragments/snippet');
13
+
14
+ /**
15
+ * Markdown.
16
+ *
17
+ * The raw text of a manifesto file. Reads itself line by line, holding
18
+ * the context of the moment (inside a fence, inside a list), and emits
19
+ * a Document of fragments. This is a line scanner, not a grammar.
20
+ *
21
+ * @todo #1:45min Attach wrapped continuation lines to the bullet they
22
+ * belong to, instead of silently dropping their nested indentation
23
+ * context on the floor.
24
+ */
25
+ class Markdown {
26
+ constructor(uri, content) {
27
+ this.address = uri;
28
+ this.content = content;
29
+ }
30
+ document() {
31
+ const pieces = [];
32
+ let fence = '';
33
+ let block = [];
34
+ let opened = 0;
35
+ let items = [];
36
+ let started = 0;
37
+ const flush = () => {
38
+ if (items.length > 0) {
39
+ pieces.push(new Bullets(items, started));
40
+ items = [];
41
+ }
42
+ };
43
+ this.content.split('\n').forEach((line, index) => {
44
+ const row = index + 1;
45
+ const mark = line.match(/^\s*(?<fence>```|~~~)/u);
46
+ if (fence !== '') {
47
+ block.push(line);
48
+ if (mark && line.trim().indexOf(fence) === 0) {
49
+ pieces.push(new Snippet(block.join('\n'), opened));
50
+ fence = '';
51
+ block = [];
52
+ }
53
+ return;
54
+ }
55
+ if (mark) {
56
+ flush();
57
+ ({fence} = mark.groups);
58
+ block = [line];
59
+ opened = row;
60
+ return;
61
+ }
62
+ if (/^#{1,6}\s+/u.test(line)) {
63
+ flush();
64
+ pieces.push(new Header(line, row, line.match(/^#+/u)[0].length));
65
+ return;
66
+ }
67
+ if (/^\s*(?:[-*+]|\d+\.)\s+/u.test(line)) {
68
+ if (items.length === 0) {
69
+ started = row;
70
+ }
71
+ items.push(new Prose(line, row));
72
+ return;
73
+ }
74
+ if (/^\s*$/u.test(line)) {
75
+ flush();
76
+ return;
77
+ }
78
+ flush();
79
+ pieces.push(new Prose(line, row));
80
+ });
81
+ flush();
82
+ if (fence !== '') {
83
+ pieces.push(new Snippet(block.join('\n'), opened));
84
+ }
85
+ return new Document(this.address, pieces);
86
+ }
87
+ }
88
+
89
+ module.exports = Markdown;
package/src/region.js ADDED
@@ -0,0 +1,38 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Region.
10
+ *
11
+ * The exact place of a violation inside a file: the file itself, a
12
+ * one-based line, and a one-based column. Knows how to project itself
13
+ * into a SARIF physical location.
14
+ */
15
+ class Region {
16
+ constructor(uri, line, column) {
17
+ this.address = uri;
18
+ this.row = line;
19
+ this.col = column;
20
+ }
21
+ uri() {
22
+ return this.address;
23
+ }
24
+ line() {
25
+ return this.row;
26
+ }
27
+ column() {
28
+ return this.col;
29
+ }
30
+ sarif() {
31
+ return {
32
+ artifactLocation: {uri: this.address},
33
+ region: {startLine: this.row, startColumn: this.col}
34
+ };
35
+ }
36
+ }
37
+
38
+ module.exports = Region;
package/src/report.js ADDED
@@ -0,0 +1,40 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Report.
10
+ *
11
+ * The whole verdict of a run: the tool that produced it and every
12
+ * violation it gathered. Renders itself for humans or as a SARIF log.
13
+ */
14
+ class Report {
15
+ constructor(tool, violations) {
16
+ this.tool = tool;
17
+ this.bag = violations;
18
+ }
19
+ count() {
20
+ return this.bag.length;
21
+ }
22
+ text() {
23
+ return this.bag
24
+ .map((violation) => violation.text())
25
+ .concat(`${this.bag.length} problems found`)
26
+ .join('\n');
27
+ }
28
+ sarif() {
29
+ return {
30
+ version: '2.1.0',
31
+ $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
32
+ runs: [{
33
+ tool: {driver: {name: this.tool, rules: []}},
34
+ results: this.bag.map((violation) => violation.sarif())
35
+ }]
36
+ };
37
+ }
38
+ }
39
+
40
+ module.exports = Report;
@@ -0,0 +1,56 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * Command.
13
+ *
14
+ * Demands that every instruction sound like a command. A standalone
15
+ * checker can only guess: it flags lines that open with a pronoun or
16
+ * end with a question mark, both signs of description, not order.
17
+ *
18
+ * @todo #1:90min Replace this heuristic with a real imperative-mood check
19
+ * driven by an AI oracle when a token is present in the environment.
20
+ */
21
+ class Command {
22
+ constructor() {
23
+ this.id = 'command';
24
+ }
25
+ violations(document) {
26
+ const uri = document.uri();
27
+ return document.walk({
28
+ header: () => [],
29
+ prose: (text, line) => this.judge(text, line, uri),
30
+ snippet: () => [],
31
+ bullets: () => []
32
+ });
33
+ }
34
+ judge(text, line, uri) {
35
+ const clean = text.replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '').trim();
36
+ if (clean === '') {
37
+ return [];
38
+ }
39
+ const first = clean
40
+ .split(/\s+/u)[0]
41
+ .toLowerCase()
42
+ .replace(/[^a-z]/gu, '');
43
+ const weak = /^(?:i|you|we|they|he|she|it|this|that|these|those|there|here)$/u;
44
+ if (!weak.test(first) && clean.slice(-1) !== '?') {
45
+ return [];
46
+ }
47
+ return [new Violation(
48
+ this.id,
49
+ 'warning',
50
+ 'line must sound like a command',
51
+ new Region(uri, line, 1)
52
+ )];
53
+ }
54
+ }
55
+
56
+ module.exports = Command;
@@ -0,0 +1,46 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * Grouped.
13
+ *
14
+ * Demands that every instruction live under a section. Any prose that
15
+ * appears before the first heading is loose and therefore a violation.
16
+ */
17
+ class Grouped {
18
+ constructor() {
19
+ this.id = 'grouped';
20
+ }
21
+ violations(document) {
22
+ const uri = document.uri();
23
+ const marks = document.walk({
24
+ header: (text, line) => [{header: true, line}],
25
+ prose: (text, line) => [{header: false, line}],
26
+ snippet: () => [],
27
+ bullets: () => []
28
+ });
29
+ let first = Infinity;
30
+ marks.forEach((mark) => {
31
+ if (mark.header && mark.line < first) {
32
+ first = mark.line;
33
+ }
34
+ });
35
+ return marks
36
+ .filter((mark) => !mark.header && mark.line < first)
37
+ .map((mark) => new Violation(
38
+ this.id,
39
+ 'error',
40
+ 'instruction not grouped under a section',
41
+ new Region(uri, mark.line, 1)
42
+ ));
43
+ }
44
+ }
45
+
46
+ module.exports = Grouped;
@@ -0,0 +1,20 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const LineLength = require('./line-length');
9
+ const ShortSections = require('./short-sections');
10
+ const Grouped = require('./grouped');
11
+ const NoArticles = require('./no-articles');
12
+ const Command = require('./command');
13
+
14
+ module.exports = () => [
15
+ new Grouped(),
16
+ new ShortSections(),
17
+ new LineLength(80),
18
+ new NoArticles(),
19
+ new Command()
20
+ ];
@@ -0,0 +1,44 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * LineLength.
13
+ *
14
+ * Demands that every instruction and every heading stay within a
15
+ * maximum width. Code snippets are exempt, since code is not prose.
16
+ */
17
+ class LineLength {
18
+ constructor(max) {
19
+ this.id = 'line-length';
20
+ this.max = max;
21
+ }
22
+ violations(document) {
23
+ const uri = document.uri();
24
+ return document.walk({
25
+ header: (text, line) => this.over(text, line, uri),
26
+ prose: (text, line) => this.over(text, line, uri),
27
+ snippet: () => [],
28
+ bullets: () => []
29
+ });
30
+ }
31
+ over(text, line, uri) {
32
+ if (text.length <= this.max) {
33
+ return [];
34
+ }
35
+ return [new Violation(
36
+ this.id,
37
+ 'error',
38
+ `line exceeds ${this.max} symbols, has ${text.length}`,
39
+ new Region(uri, line, this.max + 1)
40
+ )];
41
+ }
42
+ }
43
+
44
+ module.exports = LineLength;
@@ -0,0 +1,47 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * NoArticles.
13
+ *
14
+ * Demands that instructions carry no articles, the cheapest kind of
15
+ * noise. Flags every standalone "a", "an", and "the".
16
+ */
17
+ class NoArticles {
18
+ constructor() {
19
+ this.id = 'no-articles';
20
+ }
21
+ violations(document) {
22
+ const uri = document.uri();
23
+ return document.walk({
24
+ header: () => [],
25
+ prose: (text, line) => this.scan(text, line, uri),
26
+ snippet: () => [],
27
+ bullets: () => []
28
+ });
29
+ }
30
+ scan(text, line, uri) {
31
+ const found = [];
32
+ const regex = /\b(?:a|an|the)\b/giu;
33
+ let hit = regex.exec(text);
34
+ while (hit !== null) {
35
+ found.push(new Violation(
36
+ this.id,
37
+ 'error',
38
+ `article "${hit[0]}" must be removed`,
39
+ new Region(uri, line, hit.index + 1)
40
+ ));
41
+ hit = regex.exec(text);
42
+ }
43
+ return found;
44
+ }
45
+ }
46
+
47
+ module.exports = NoArticles;
@@ -0,0 +1,48 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * ShortSections.
13
+ *
14
+ * Demands that every section name be a short label of one to three
15
+ * words, so the manifesto reads as a map and not as prose.
16
+ */
17
+ class ShortSections {
18
+ constructor() {
19
+ this.id = 'short-sections';
20
+ }
21
+ violations(document) {
22
+ const uri = document.uri();
23
+ return document.walk({
24
+ header: (text, line) => this.named(text, line, uri),
25
+ prose: () => [],
26
+ snippet: () => [],
27
+ bullets: () => []
28
+ });
29
+ }
30
+ named(text, line, uri) {
31
+ const words = text
32
+ .replace(/^#{1,6}\s+/u, '')
33
+ .trim()
34
+ .split(/\s+/u)
35
+ .filter((word) => word !== '');
36
+ if (words.length >= 1 && words.length <= 3) {
37
+ return [];
38
+ }
39
+ return [new Violation(
40
+ this.id,
41
+ 'error',
42
+ `section name must be 1-3 words, has ${words.length}`,
43
+ new Region(uri, line, 1)
44
+ )];
45
+ }
46
+ }
47
+
48
+ module.exports = ShortSections;
@@ -0,0 +1,39 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Violation.
10
+ *
11
+ * One breach of one rule at one region of a manifesto. Renders itself
12
+ * either as a terse human line or as a SARIF result.
13
+ */
14
+ class Violation {
15
+ constructor(rule, level, message, region) {
16
+ this.rule = rule;
17
+ this.level = level;
18
+ this.message = message;
19
+ this.spot = region;
20
+ }
21
+ text() {
22
+ return [
23
+ `${this.spot.uri()}:${this.spot.line()}:${this.spot.column()}`,
24
+ this.level,
25
+ this.rule,
26
+ this.message
27
+ ].join(' ');
28
+ }
29
+ sarif() {
30
+ return {
31
+ ruleId: this.rule,
32
+ level: this.level,
33
+ message: {text: this.message},
34
+ locations: [{physicalLocation: this.spot.sarif()}]
35
+ };
36
+ }
37
+ }
38
+
39
+ module.exports = Violation;