@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 +4 -2
- package/package.json +1 -1
- 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/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:
|
|
@@ -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
|
|
122
|
+
rev: 0.1.0
|
|
121
123
|
hooks:
|
|
122
124
|
- id: dogent
|
|
123
125
|
```
|
package/package.json
CHANGED
|
@@ -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/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;
|