@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 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.1.0 CLAUDE.md
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.1.0
131
+ rev: 0.3.0
123
132
  hooks:
124
133
  - id: dogent
125
134
  ```
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "lint": "eslint .",
41
41
  "test": "mocha 'test/**/*.js' --timeout 60000"
42
42
  },
43
- "version": "0.2.0"
43
+ "version": "0.4.0"
44
44
  }
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 files = argv.filter((arg) => arg !== '--sarif');
17
- if (files.length === 0) {
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));
@@ -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;
package/src/violation.js CHANGED
@@ -22,7 +22,7 @@ class Violation {
22
22
  return [
23
23
  `${this.spot.uri()}:${this.spot.line()}:${this.spot.column()}`,
24
24
  this.level,
25
- this.rule,
25
+ `[${this.rule}]:`,
26
26
  this.message
27
27
  ].join(' ');
28
28
  }