@yegor256/dogent 0.2.0 → 0.4.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/rules/index.js +4 -0
- package/src/rules/punctuation.js +57 -0
- package/src/rules/token-count.js +44 -0
- package/src/sources.js +42 -0
- package/src/violation.js +1 -1
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.
|
|
26
|
+
npx @yegor256/dogent@0.3.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
|
|
@@ -55,7 +62,9 @@ The command exits with a non-zero status when problems are found,
|
|
|
55
62
|
- Instructions must be grouped in sections.
|
|
56
63
|
- Section names must be short, 1-3 words.
|
|
57
64
|
- Every line must be no longer than 80 symbols.
|
|
65
|
+
- The whole file must stay under 4000 tokens.
|
|
58
66
|
- Every line must sound like a command.
|
|
67
|
+
- Every sentence must start with a capital and end with a period.
|
|
59
68
|
- No articles, no noise, no bloated text.
|
|
60
69
|
- Simple grammar, no ambiguity.
|
|
61
70
|
- `SKILL.md` must open with valid frontmatter.
|
|
@@ -119,7 +128,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
119
128
|
```yaml
|
|
120
129
|
repos:
|
|
121
130
|
- repo: https://github.com/yegor256/dogent
|
|
122
|
-
rev: 0.
|
|
131
|
+
rev: 0.3.0
|
|
123
132
|
hooks:
|
|
124
133
|
- id: dogent
|
|
125
134
|
```
|
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));
|
package/src/rules/index.js
CHANGED
|
@@ -6,18 +6,22 @@
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
8
|
const LineLength = require('./line-length');
|
|
9
|
+
const TokenCount = require('./token-count');
|
|
9
10
|
const ShortSections = require('./short-sections');
|
|
10
11
|
const Grouped = require('./grouped');
|
|
11
12
|
const NoArticles = require('./no-articles');
|
|
12
13
|
const Command = require('./command');
|
|
14
|
+
const Punctuation = require('./punctuation');
|
|
13
15
|
const Frontmatter = require('./frontmatter');
|
|
14
16
|
|
|
15
17
|
module.exports = () => [
|
|
16
18
|
new Grouped(),
|
|
17
19
|
new ShortSections(),
|
|
18
20
|
new LineLength(80),
|
|
21
|
+
new TokenCount(4000),
|
|
19
22
|
new NoArticles(),
|
|
20
23
|
new Command(),
|
|
24
|
+
new Punctuation(),
|
|
21
25
|
new Frontmatter(
|
|
22
26
|
'SKILL.md',
|
|
23
27
|
['name', 'description'],
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
* Punctuation.
|
|
13
|
+
*
|
|
14
|
+
* Demands that every instruction read as one whole sentence: opening
|
|
15
|
+
* with a capital letter and closing with a period. Headings, snippets,
|
|
16
|
+
* and frontmatter escape this rule.
|
|
17
|
+
*/
|
|
18
|
+
class Punctuation {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.id = 'punctuation';
|
|
21
|
+
}
|
|
22
|
+
violations(document) {
|
|
23
|
+
const uri = document.uri();
|
|
24
|
+
return document.walk({
|
|
25
|
+
header: () => [],
|
|
26
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
27
|
+
snippet: () => [],
|
|
28
|
+
bullets: () => [],
|
|
29
|
+
frontmatter: () => []
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
judge(text, line, uri) {
|
|
33
|
+
const [lead] = text.match(/^\s*(?:[-*+]|\d+\.)\s+/u) || text.match(/^\s*/u);
|
|
34
|
+
const sentence = text.slice(lead.length).replace(/\s+$/u, '');
|
|
35
|
+
const letter = sentence.match(/[A-Za-z]/u);
|
|
36
|
+
return [].concat(
|
|
37
|
+
sentence !== '' && letter !== null && letter[0] !== letter[0].toUpperCase()
|
|
38
|
+
? [new Violation(
|
|
39
|
+
this.id,
|
|
40
|
+
'error',
|
|
41
|
+
'sentence must start with a capital letter',
|
|
42
|
+
new Region(uri, line, lead.length + letter.index + 1)
|
|
43
|
+
)]
|
|
44
|
+
: [],
|
|
45
|
+
sentence !== '' && sentence.slice(-1) !== '.'
|
|
46
|
+
? [new Violation(
|
|
47
|
+
this.id,
|
|
48
|
+
'error',
|
|
49
|
+
'sentence must end with a period',
|
|
50
|
+
new Region(uri, line, lead.length + sentence.length)
|
|
51
|
+
)]
|
|
52
|
+
: []
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = Punctuation;
|
|
@@ -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
|
+
* TokenCount.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a whole manifesto stay small enough to ride cheaply inside
|
|
15
|
+
* an agent context. Counts every word, number, and punctuation symbol
|
|
16
|
+
* across all fragments, frontmatter included, and complains once the sum
|
|
17
|
+
* reaches a cap.
|
|
18
|
+
*/
|
|
19
|
+
class TokenCount {
|
|
20
|
+
constructor(cap) {
|
|
21
|
+
this.id = 'token-count';
|
|
22
|
+
this.cap = cap;
|
|
23
|
+
}
|
|
24
|
+
violations(document) {
|
|
25
|
+
const count = (document.walk({
|
|
26
|
+
header: (text) => [text],
|
|
27
|
+
prose: (text) => [text],
|
|
28
|
+
snippet: (text) => [text],
|
|
29
|
+
bullets: () => [],
|
|
30
|
+
frontmatter: (pairs) => pairs.map((pair) => `${pair.key} ${pair.value}`)
|
|
31
|
+
}).join(' ').match(/[A-Za-z0-9]+|[^\sA-Za-z0-9]/gu) || []).length;
|
|
32
|
+
if (count < this.cap) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
return [new Violation(
|
|
36
|
+
this.id,
|
|
37
|
+
'error',
|
|
38
|
+
`file exceeds ${this.cap} tokens, has ${count}`,
|
|
39
|
+
new Region(document.uri(), 1, 1)
|
|
40
|
+
)];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = TokenCount;
|
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;
|