@yegor256/dogent 0.1.0 → 0.3.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/README.md +11 -2
- package/package.json +1 -1
- package/src/dogent.js +5 -4
- package/src/fragments/frontmatter.js +25 -0
- package/src/markdown.js +17 -1
- package/src/rules/command.js +2 -1
- package/src/rules/frontmatter.js +70 -0
- package/src/rules/grouped.js +2 -1
- package/src/rules/index.js +7 -1
- package/src/rules/line-length.js +2 -1
- package/src/rules/no-articles.js +2 -1
- package/src/rules/short-sections.js +2 -1
- package/src/sources.js +42 -0
- package/src/yaml.js +37 -0
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ We respect [agent-sh/agnix](https://github.com/agent-sh/agnix)
|
|
|
23
23
|
Run it on any manifesto file, no installation required:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx @yegor256/dogent@0.0
|
|
26
|
+
npx @yegor256/dogent@0.1.0 CLAUDE.md
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Lint several files at once:
|
|
@@ -32,6 +32,13 @@ Lint several files at once:
|
|
|
32
32
|
npx @yegor256/dogent SKILL.md CLAUDE.md AGENTS.md
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Point it at a directory to lint the default manifestos it holds
|
|
36
|
+
(`AGENTS.md`, `CLAUDE.md`, `SKILL.md`, `SKILLS.md`):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @yegor256/dogent .
|
|
40
|
+
```
|
|
41
|
+
|
|
35
42
|
Sample output:
|
|
36
43
|
|
|
37
44
|
```text
|
|
@@ -58,6 +65,8 @@ The command exits with a non-zero status when problems are found,
|
|
|
58
65
|
- Every line must sound like a command.
|
|
59
66
|
- No articles, no noise, no bloated text.
|
|
60
67
|
- Simple grammar, no ambiguity.
|
|
68
|
+
- `SKILL.md` must open with valid frontmatter.
|
|
69
|
+
- Frontmatter must declare only allowed keys.
|
|
61
70
|
|
|
62
71
|
## AI verification
|
|
63
72
|
|
|
@@ -117,7 +126,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
117
126
|
```yaml
|
|
118
127
|
repos:
|
|
119
128
|
- repo: https://github.com/yegor256/dogent
|
|
120
|
-
rev: 0.0
|
|
129
|
+
rev: 0.1.0
|
|
121
130
|
hooks:
|
|
122
131
|
- id: dogent
|
|
123
132
|
```
|
package/package.json
CHANGED
package/src/dogent.js
CHANGED
|
@@ -9,17 +9,18 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const Markdown = require('./markdown');
|
|
11
11
|
const Report = require('./report');
|
|
12
|
+
const Sources = require('./sources');
|
|
12
13
|
const rules = require('./rules');
|
|
13
14
|
|
|
14
15
|
const argv = process.argv.slice(2);
|
|
15
16
|
const sarif = argv.indexOf('--sarif') !== -1;
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
process.stderr.write('Usage: dogent [--sarif] <file.md>...\n');
|
|
17
|
+
const paths = argv.filter((arg) => arg !== '--sarif');
|
|
18
|
+
if (paths.length === 0) {
|
|
19
|
+
process.stderr.write('Usage: dogent [--sarif] <file.md|dir>...\n');
|
|
19
20
|
process.exit(2);
|
|
20
21
|
}
|
|
21
22
|
const found = [];
|
|
22
|
-
files.forEach((file) => {
|
|
23
|
+
new Sources(paths).files().forEach((file) => {
|
|
23
24
|
const document = new Markdown(file, fs.readFileSync(file, 'utf8')).document();
|
|
24
25
|
rules().forEach((rule) => {
|
|
25
26
|
rule.violations(document).forEach((violation) => found.push(violation));
|
|
@@ -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
|
+
* Frontmatter.
|
|
10
|
+
*
|
|
11
|
+
* The leading YAML block of a manifesto, already parsed into an ordered
|
|
12
|
+
* list of key pairs, carrying the line where the opening fence sits.
|
|
13
|
+
* Stands apart from prose so no prose rule ever sees its keys.
|
|
14
|
+
*/
|
|
15
|
+
class Frontmatter {
|
|
16
|
+
constructor(pairs, line) {
|
|
17
|
+
this.keys = pairs;
|
|
18
|
+
this.row = line;
|
|
19
|
+
}
|
|
20
|
+
accept(visitor) {
|
|
21
|
+
return visitor.frontmatter(this.keys, this.row);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = Frontmatter;
|
package/src/markdown.js
CHANGED
|
@@ -10,6 +10,8 @@ const Header = require('./fragments/header');
|
|
|
10
10
|
const Prose = require('./fragments/prose');
|
|
11
11
|
const Bullets = require('./fragments/bullets');
|
|
12
12
|
const Snippet = require('./fragments/snippet');
|
|
13
|
+
const Frontmatter = require('./fragments/frontmatter');
|
|
14
|
+
const Yaml = require('./yaml');
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Markdown.
|
|
@@ -40,7 +42,21 @@ class Markdown {
|
|
|
40
42
|
items = [];
|
|
41
43
|
}
|
|
42
44
|
};
|
|
43
|
-
this.content.split('\n')
|
|
45
|
+
const lines = this.content.split('\n');
|
|
46
|
+
let skip = 0;
|
|
47
|
+
if (lines[0].trim() === '---') {
|
|
48
|
+
const rest = lines.slice(1);
|
|
49
|
+
const close = rest.findIndex((line) => line.trim() === '---');
|
|
50
|
+
if (close !== -1) {
|
|
51
|
+
const front = rest.slice(0, close).join('\n');
|
|
52
|
+
pieces.push(new Frontmatter(new Yaml(front, 2).pairs(), 1));
|
|
53
|
+
skip = close + 2;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
lines.forEach((line, index) => {
|
|
57
|
+
if (index < skip) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
44
60
|
const row = index + 1;
|
|
45
61
|
const mark = line.match(/^\s*(?<fence>```|~~~)/u);
|
|
46
62
|
if (fence !== '') {
|
package/src/rules/command.js
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
* Frontmatter.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a skill file open with a YAML frontmatter block, declare
|
|
15
|
+
* every required key, and carry no key outside the allowed set. The
|
|
16
|
+
* block is mandatory for the named file and ignored for every other.
|
|
17
|
+
*/
|
|
18
|
+
class Frontmatter {
|
|
19
|
+
constructor(name, required, allowed) {
|
|
20
|
+
this.id = 'frontmatter';
|
|
21
|
+
this.name = name;
|
|
22
|
+
this.required = required;
|
|
23
|
+
this.allowed = allowed;
|
|
24
|
+
}
|
|
25
|
+
violations(document) {
|
|
26
|
+
const uri = document.uri();
|
|
27
|
+
if (uri.replace(/^.*\//u, '') !== this.name) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const blocks = document.walk({
|
|
31
|
+
header: () => [],
|
|
32
|
+
prose: () => [],
|
|
33
|
+
snippet: () => [],
|
|
34
|
+
bullets: () => [],
|
|
35
|
+
frontmatter: (pairs) => [pairs]
|
|
36
|
+
});
|
|
37
|
+
if (blocks.length === 0) {
|
|
38
|
+
return [new Violation(
|
|
39
|
+
this.id,
|
|
40
|
+
'error',
|
|
41
|
+
'skill must open with frontmatter',
|
|
42
|
+
new Region(uri, 1, 1)
|
|
43
|
+
)];
|
|
44
|
+
}
|
|
45
|
+
return this.missing(blocks[0], uri).concat(this.extra(blocks[0], uri));
|
|
46
|
+
}
|
|
47
|
+
missing(pairs, uri) {
|
|
48
|
+
const present = pairs.map((pair) => pair.key);
|
|
49
|
+
return this.required
|
|
50
|
+
.filter((key) => !present.includes(key))
|
|
51
|
+
.map((key) => new Violation(
|
|
52
|
+
this.id,
|
|
53
|
+
'error',
|
|
54
|
+
`frontmatter must declare "${key}"`,
|
|
55
|
+
new Region(uri, 1, 1)
|
|
56
|
+
));
|
|
57
|
+
}
|
|
58
|
+
extra(pairs, uri) {
|
|
59
|
+
return pairs
|
|
60
|
+
.filter((pair) => !this.allowed.includes(pair.key))
|
|
61
|
+
.map((pair) => new Violation(
|
|
62
|
+
this.id,
|
|
63
|
+
'error',
|
|
64
|
+
`frontmatter key "${pair.key}" forbidden`,
|
|
65
|
+
new Region(uri, pair.row, 1)
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = Frontmatter;
|
package/src/rules/grouped.js
CHANGED
|
@@ -24,7 +24,8 @@ class Grouped {
|
|
|
24
24
|
header: (text, line) => [{header: true, line}],
|
|
25
25
|
prose: (text, line) => [{header: false, line}],
|
|
26
26
|
snippet: () => [],
|
|
27
|
-
bullets: () => []
|
|
27
|
+
bullets: () => [],
|
|
28
|
+
frontmatter: () => []
|
|
28
29
|
});
|
|
29
30
|
let first = Infinity;
|
|
30
31
|
marks.forEach((mark) => {
|
package/src/rules/index.js
CHANGED
|
@@ -10,11 +10,17 @@ const ShortSections = require('./short-sections');
|
|
|
10
10
|
const Grouped = require('./grouped');
|
|
11
11
|
const NoArticles = require('./no-articles');
|
|
12
12
|
const Command = require('./command');
|
|
13
|
+
const Frontmatter = require('./frontmatter');
|
|
13
14
|
|
|
14
15
|
module.exports = () => [
|
|
15
16
|
new Grouped(),
|
|
16
17
|
new ShortSections(),
|
|
17
18
|
new LineLength(80),
|
|
18
19
|
new NoArticles(),
|
|
19
|
-
new Command()
|
|
20
|
+
new Command(),
|
|
21
|
+
new Frontmatter(
|
|
22
|
+
'SKILL.md',
|
|
23
|
+
['name', 'description'],
|
|
24
|
+
['name', 'description', 'license', 'allowed-tools']
|
|
25
|
+
)
|
|
20
26
|
];
|
package/src/rules/line-length.js
CHANGED
package/src/rules/no-articles.js
CHANGED
package/src/sources.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Sources.
|
|
13
|
+
*
|
|
14
|
+
* The paths passed on the command line. Each path names either one
|
|
15
|
+
* manifesto file or one directory. A directory expands into the default
|
|
16
|
+
* manifesto files it actually contains, so `dogent .` lints every known
|
|
17
|
+
* manifesto in the current folder.
|
|
18
|
+
*/
|
|
19
|
+
class Sources {
|
|
20
|
+
constructor(paths, defaults = ['AGENTS.md', 'CLAUDE.md', 'SKILL.md', 'SKILLS.md']) {
|
|
21
|
+
this.paths = paths;
|
|
22
|
+
this.defaults = defaults;
|
|
23
|
+
}
|
|
24
|
+
files() {
|
|
25
|
+
const found = [];
|
|
26
|
+
this.paths.forEach((entry) => {
|
|
27
|
+
if (fs.statSync(entry).isDirectory()) {
|
|
28
|
+
this.defaults.forEach((name) => {
|
|
29
|
+
const file = path.join(entry, name);
|
|
30
|
+
if (fs.existsSync(file)) {
|
|
31
|
+
found.push(file);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
found.push(entry);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = Sources;
|
package/src/yaml.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Yaml.
|
|
10
|
+
*
|
|
11
|
+
* A frontmatter block read as a flat YAML mapping. Splits itself line by
|
|
12
|
+
* line and emits one pair per top-level "key: value", carrying the key,
|
|
13
|
+
* the value, and the absolute line the key sits on. Nested mappings,
|
|
14
|
+
* blank lines, and comments hold no keys and yield nothing.
|
|
15
|
+
*/
|
|
16
|
+
class Yaml {
|
|
17
|
+
constructor(text, base) {
|
|
18
|
+
this.text = text;
|
|
19
|
+
this.base = base;
|
|
20
|
+
}
|
|
21
|
+
pairs() {
|
|
22
|
+
return this.text
|
|
23
|
+
.split('\n')
|
|
24
|
+
.map((line, index) => ({line, row: this.base + index}))
|
|
25
|
+
.filter((spot) => /^[^\s#][^:]*:/u.test(spot.line))
|
|
26
|
+
.map((spot) => {
|
|
27
|
+
const colon = spot.line.indexOf(':');
|
|
28
|
+
return {
|
|
29
|
+
key: spot.line.slice(0, colon).trim(),
|
|
30
|
+
value: spot.line.slice(colon + 1).trim(),
|
|
31
|
+
row: spot.row
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = Yaml;
|