@yegor256/dogent 0.1.0 → 0.2.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.0.1 CLAUDE.md
26
+ npx @yegor256/dogent@0.1.0 CLAUDE.md
27
27
  ```
28
28
 
29
29
  Lint several files at once:
@@ -58,6 +58,8 @@ The command exits with a non-zero status when problems are found,
58
58
  - Every line must sound like a command.
59
59
  - No articles, no noise, no bloated text.
60
60
  - Simple grammar, no ambiguity.
61
+ - `SKILL.md` must open with valid frontmatter.
62
+ - Frontmatter must declare only allowed keys.
61
63
 
62
64
  ## AI verification
63
65
 
@@ -117,7 +119,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
117
119
  ```yaml
118
120
  repos:
119
121
  - repo: https://github.com/yegor256/dogent
120
- rev: 0.0.1
122
+ rev: 0.1.0
121
123
  hooks:
122
124
  - id: dogent
123
125
  ```
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.1.0"
43
+ "version": "0.2.0"
44
44
  }
@@ -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').forEach((line, index) => {
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 !== '') {
@@ -28,7 +28,8 @@ class Command {
28
28
  header: () => [],
29
29
  prose: (text, line) => this.judge(text, line, uri),
30
30
  snippet: () => [],
31
- bullets: () => []
31
+ bullets: () => [],
32
+ frontmatter: () => []
32
33
  });
33
34
  }
34
35
  judge(text, line, uri) {
@@ -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;
@@ -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) => {
@@ -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
  ];
@@ -25,7 +25,8 @@ class LineLength {
25
25
  header: (text, line) => this.over(text, line, uri),
26
26
  prose: (text, line) => this.over(text, line, uri),
27
27
  snippet: () => [],
28
- bullets: () => []
28
+ bullets: () => [],
29
+ frontmatter: () => []
29
30
  });
30
31
  }
31
32
  over(text, line, uri) {
@@ -24,7 +24,8 @@ class NoArticles {
24
24
  header: () => [],
25
25
  prose: (text, line) => this.scan(text, line, uri),
26
26
  snippet: () => [],
27
- bullets: () => []
27
+ bullets: () => [],
28
+ frontmatter: () => []
28
29
  });
29
30
  }
30
31
  scan(text, line, uri) {
@@ -24,7 +24,8 @@ class ShortSections {
24
24
  header: (text, line) => this.named(text, line, uri),
25
25
  prose: () => [],
26
26
  snippet: () => [],
27
- bullets: () => []
27
+ bullets: () => [],
28
+ frontmatter: () => []
28
29
  });
29
30
  }
30
31
  named(text, line, uri) {
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;