@yegor256/dogent 0.7.2 → 0.7.4
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 +3 -9
- package/package.json +1 -1
- package/src/markdown.js +8 -3
- package/src/rules/atomic.js +7 -9
- package/src/rules/dead-import.js +70 -14
- package/src/rules/description-triggers.js +3 -6
- package/src/rules/hedging.js +4 -7
- package/src/rules/passive.js +5 -8
- package/src/rules/redundant.js +5 -9
- package/src/rules/simple.js +4 -8
- package/src/rules/unique.js +4 -6
package/README.md
CHANGED
|
@@ -29,13 +29,7 @@ In short: `agnix` lints the harness, `dogent` lints the prompt.
|
|
|
29
29
|
Run it on any manifesto file, no installation required:
|
|
30
30
|
|
|
31
31
|
```bash
|
|
32
|
-
npx @yegor256/dogent@0.
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Lint several files at once:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npx @yegor256/dogent SKILL.md CLAUDE.md AGENTS.md
|
|
32
|
+
npx @yegor256/dogent@0.7.2 SKILL.md
|
|
39
33
|
```
|
|
40
34
|
|
|
41
35
|
Point it at a directory to lint the default manifestos it holds
|
|
@@ -135,7 +129,7 @@ jobs:
|
|
|
135
129
|
- uses: actions/setup-node@v4
|
|
136
130
|
with:
|
|
137
131
|
node-version: 20
|
|
138
|
-
- run: npx @yegor256/dogent
|
|
132
|
+
- run: npx @yegor256/dogent .
|
|
139
133
|
```
|
|
140
134
|
|
|
141
135
|
The job fails when `dogent` finds problems,
|
|
@@ -164,7 +158,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
164
158
|
```yaml
|
|
165
159
|
repos:
|
|
166
160
|
- repo: https://github.com/yegor256/dogent
|
|
167
|
-
rev: 0.
|
|
161
|
+
rev: 0.7.2
|
|
168
162
|
hooks:
|
|
169
163
|
- id: dogent
|
|
170
164
|
```
|
package/package.json
CHANGED
package/src/markdown.js
CHANGED
|
@@ -20,9 +20,9 @@ const Yaml = require('./yaml');
|
|
|
20
20
|
* the context of the moment (inside a fence, inside a list), and emits
|
|
21
21
|
* a Document of fragments. This is a line scanner, not a grammar.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* A wrapped continuation line, indented under an open bullet and
|
|
24
|
+
* carrying no marker of its own, folds into the bullet it belongs to
|
|
25
|
+
* rather than breaking the list and floating off as a stray paragraph.
|
|
26
26
|
*/
|
|
27
27
|
class Markdown {
|
|
28
28
|
constructor(uri, content) {
|
|
@@ -92,6 +92,11 @@ class Markdown {
|
|
|
92
92
|
flush();
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
+
if (items.length > 0 && /^\s+\S/u.test(line)) {
|
|
96
|
+
const last = items[items.length - 1];
|
|
97
|
+
items[items.length - 1] = new Prose(`${last.content} ${line.trim()}`, last.row);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
95
100
|
flush();
|
|
96
101
|
pieces.push(new Prose(line, row));
|
|
97
102
|
});
|
package/src/rules/atomic.js
CHANGED
|
@@ -15,19 +15,15 @@ const Region = require('../region');
|
|
|
15
15
|
* checker only spots the loud signs: a sentence terminator sitting
|
|
16
16
|
* mid-line with more text after it, or two verb phrases welded together
|
|
17
17
|
* with a semicolon, an " and ", or a " then ". The prompt hands the
|
|
18
|
-
* subtler clause-counting to the AI oracle
|
|
19
|
-
*
|
|
20
|
-
* @todo #21:45min Upgrade to a real clause-count check through an AI
|
|
21
|
-
* oracle so that subtle multi-instruction lines, which the conservative
|
|
22
|
-
* heuristic cannot see today, are reliably caught, as requested in
|
|
23
|
-
* issue #21.
|
|
18
|
+
* subtler clause-counting to the AI oracle, which catches the
|
|
19
|
+
* multi-instruction lines that carry no such welding token.
|
|
24
20
|
*/
|
|
25
21
|
class Atomic {
|
|
26
22
|
constructor() {
|
|
27
23
|
this.id = 'atomic';
|
|
28
24
|
}
|
|
29
25
|
prompt() {
|
|
30
|
-
return `${this.id}: flag any line that carries more than one instruction`;
|
|
26
|
+
return `${this.id}: flag any line that carries more than one instruction, counting distinct clauses even when no semicolon, "and", or "then" welds them together`;
|
|
31
27
|
}
|
|
32
28
|
violations(document) {
|
|
33
29
|
const uri = document.uri();
|
|
@@ -41,8 +37,10 @@ class Atomic {
|
|
|
41
37
|
}
|
|
42
38
|
judge(text, line, uri) {
|
|
43
39
|
const clean = text.replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '').trimEnd();
|
|
44
|
-
const
|
|
45
|
-
|
|
40
|
+
const weld = /(?<!,)\s(?:and|then)\s+(?<verb>[a-z]+)\s+\S/u.exec(clean);
|
|
41
|
+
const welded = weld !== null &&
|
|
42
|
+
!/(?:ly|al|ial|ous|ive|less|ic|ary|ory|able|ible)$/u.test(weld.groups.verb);
|
|
43
|
+
if (!/[.!?]\s+\S/u.test(clean) && !/;/u.test(clean) && !welded) {
|
|
46
44
|
return [];
|
|
47
45
|
}
|
|
48
46
|
return [new Violation(
|
package/src/rules/dead-import.js
CHANGED
|
@@ -10,6 +10,7 @@ const path = require('path');
|
|
|
10
10
|
|
|
11
11
|
const Violation = require('../violation');
|
|
12
12
|
const Region = require('../region');
|
|
13
|
+
const Markdown = require('../markdown');
|
|
13
14
|
|
|
14
15
|
const imports = (line) => {
|
|
15
16
|
const found = [];
|
|
@@ -25,42 +26,97 @@ const imports = (line) => {
|
|
|
25
26
|
return found;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
const targets = (file) => {
|
|
30
|
+
const base = path.dirname(file);
|
|
31
|
+
try {
|
|
32
|
+
return new Markdown(file, fs.readFileSync(file, 'utf8'))
|
|
33
|
+
.document()
|
|
34
|
+
.walk({
|
|
35
|
+
header: () => [],
|
|
36
|
+
snippet: () => [],
|
|
37
|
+
bullets: () => [],
|
|
38
|
+
frontmatter: () => [],
|
|
39
|
+
prose: (line) => imports(line).map((item) => path.resolve(base, item.file))
|
|
40
|
+
})
|
|
41
|
+
.filter((target) => fs.existsSync(target));
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
28
47
|
/**
|
|
29
48
|
* DeadImport.
|
|
30
49
|
*
|
|
31
|
-
* Flags `@path/to/file` imports that point to no file on disk
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* as requested in issue #18.
|
|
50
|
+
* Flags `@path/to/file` imports that point to no file on disk, and
|
|
51
|
+
* walks the chain of imports that do resolve: a chain that loops back
|
|
52
|
+
* on itself, or that nests deeper than five levels, fails with a clear
|
|
53
|
+
* violation rather than looping or loading forever in the host tool.
|
|
36
54
|
*/
|
|
37
55
|
class DeadImport {
|
|
38
56
|
constructor() {
|
|
39
57
|
this.id = 'dead-import';
|
|
58
|
+
this.depth = 5;
|
|
40
59
|
}
|
|
41
60
|
prompt() {
|
|
42
61
|
return `${this.id}: flag any @path/to/file import that points to no file on disk`;
|
|
43
62
|
}
|
|
44
63
|
violations(document) {
|
|
45
|
-
|
|
64
|
+
const uri = document.uri();
|
|
65
|
+
const base = path.dirname(uri);
|
|
66
|
+
const links = document.walk({
|
|
46
67
|
header: () => [],
|
|
47
68
|
snippet: () => [],
|
|
48
69
|
bullets: () => [],
|
|
49
70
|
frontmatter: () => [],
|
|
50
|
-
prose: (line, row) =>
|
|
71
|
+
prose: (line, row) => imports(line).map(
|
|
72
|
+
(item) => ({...item, target: path.resolve(base, item.file), row})
|
|
73
|
+
)
|
|
51
74
|
});
|
|
75
|
+
return this.missing(uri, links).concat(this.chains(uri, links));
|
|
52
76
|
}
|
|
53
|
-
missing(uri,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.
|
|
57
|
-
.map((item) => new Violation(
|
|
77
|
+
missing(uri, links) {
|
|
78
|
+
return links
|
|
79
|
+
.filter((link) => !fs.existsSync(link.target))
|
|
80
|
+
.map((link) => new Violation(
|
|
58
81
|
this.id,
|
|
59
82
|
'error',
|
|
60
|
-
`@-import target not found: ${
|
|
61
|
-
new Region(uri, row,
|
|
83
|
+
`@-import target not found: ${link.file}`,
|
|
84
|
+
new Region(uri, link.row, link.column)
|
|
62
85
|
));
|
|
63
86
|
}
|
|
87
|
+
chains(uri, links) {
|
|
88
|
+
const root = path.resolve(uri);
|
|
89
|
+
const found = [];
|
|
90
|
+
links
|
|
91
|
+
.filter((link) => fs.existsSync(link.target))
|
|
92
|
+
.forEach((link) => {
|
|
93
|
+
const flags = {cycle: false, deep: false};
|
|
94
|
+
this.explore([root, link.target], 1, flags);
|
|
95
|
+
if (flags.cycle) {
|
|
96
|
+
found.push(this.flag(uri, link, `@-import chain is circular via ${link.file}`));
|
|
97
|
+
}
|
|
98
|
+
if (flags.deep) {
|
|
99
|
+
found.push(this.flag(uri, link, `@-import chain nests deeper than ${this.depth} levels via ${link.file}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
explore(stack, depth, flags) {
|
|
105
|
+
if (depth > this.depth) {
|
|
106
|
+
flags.deep = true;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
targets(stack[stack.length - 1]).forEach((target) => {
|
|
110
|
+
if (stack.includes(target)) {
|
|
111
|
+
flags.cycle = true;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.explore(stack.concat(target), depth + 1, flags);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
flag(uri, link, message) {
|
|
118
|
+
return new Violation(this.id, 'error', message, new Region(uri, link.row, link.column));
|
|
119
|
+
}
|
|
64
120
|
}
|
|
65
121
|
|
|
66
122
|
module.exports = DeadImport;
|
|
@@ -14,11 +14,8 @@ const Region = require('../region');
|
|
|
14
14
|
* Demands that a SKILL.md description say when to use the skill. A
|
|
15
15
|
* standalone checker can only approximate: it flags a value that is too
|
|
16
16
|
* short or that never names a trigger with the word "when". Its prompt
|
|
17
|
-
* hands the deeper judgement to the AI oracle
|
|
18
|
-
*
|
|
19
|
-
* @todo #19:30min Upgrade the trigger check to an AI oracle that judges
|
|
20
|
-
* whether the description truly names the situations and user phrases
|
|
21
|
-
* that should activate the skill, as requested in issue #19.
|
|
17
|
+
* hands the deeper judgement to the AI oracle, which weighs whether the
|
|
18
|
+
* description truly names the situations and phrases that activate it.
|
|
22
19
|
*/
|
|
23
20
|
class DescriptionTriggers {
|
|
24
21
|
constructor() {
|
|
@@ -26,7 +23,7 @@ class DescriptionTriggers {
|
|
|
26
23
|
this.minimum = 20;
|
|
27
24
|
}
|
|
28
25
|
prompt() {
|
|
29
|
-
return `${this.id}: in a SKILL.md, flag a description that is too short or
|
|
26
|
+
return `${this.id}: in a SKILL.md, flag a description that is too short or fails to name the concrete situations and user phrases that should activate the skill, even when it contains the word "when"`;
|
|
30
27
|
}
|
|
31
28
|
violations(document) {
|
|
32
29
|
const uri = document.uri();
|
package/src/rules/hedging.js
CHANGED
|
@@ -13,19 +13,16 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Flags soft, non-committal, or hedging wording that weakens an order.
|
|
15
15
|
* Catches words like "should", "just", "usually", and phrases like
|
|
16
|
-
* "try to" or "if possible", each a sign of timid instruction.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* such as conditional escape hatches and vague scope, which the fixed
|
|
20
|
-
* blacklist of hedge words cannot detect today, as requested in
|
|
21
|
-
* issue #22.
|
|
16
|
+
* "try to" or "if possible", each a sign of timid instruction. Its
|
|
17
|
+
* prompt hands subtler hedging to the AI oracle, which catches the
|
|
18
|
+
* conditional escape hatches and vague scope no fixed list can.
|
|
22
19
|
*/
|
|
23
20
|
class Hedging {
|
|
24
21
|
constructor() {
|
|
25
22
|
this.id = 'hedging';
|
|
26
23
|
}
|
|
27
24
|
prompt() {
|
|
28
|
-
return `${this.id}: flag soft, non-committal, or hedging wording`;
|
|
25
|
+
return `${this.id}: flag soft, non-committal, or hedging wording, including conditional escape hatches and vague scope that carry no fixed hedge word`;
|
|
29
26
|
}
|
|
30
27
|
violations(document) {
|
|
31
28
|
const uri = document.uri();
|
package/src/rules/passive.js
CHANGED
|
@@ -13,19 +13,16 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands active imperative voice. A standalone checker can only guess:
|
|
15
15
|
* it flags a "be" verb followed, perhaps through an adverb, by a past
|
|
16
|
-
* participle, the surest mark of passive voice.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* detection, since the regular-expression heuristic both misses many
|
|
20
|
-
* irregular participles and cannot judge true grammatical voice, as
|
|
21
|
-
* requested in issue #24.
|
|
16
|
+
* participle, the surest mark of passive voice. Its prompt hands true
|
|
17
|
+
* grammatical-voice judgement to the AI oracle, which catches the
|
|
18
|
+
* irregular participles the regular expression misses.
|
|
22
19
|
*/
|
|
23
20
|
class Passive {
|
|
24
21
|
constructor() {
|
|
25
22
|
this.id = 'passive';
|
|
26
23
|
}
|
|
27
24
|
prompt() {
|
|
28
|
-
return `${this.id}: flag any instruction written in passive voice`;
|
|
25
|
+
return `${this.id}: flag any instruction written in passive voice, judging true grammatical voice including irregular past participles a fixed pattern misses`;
|
|
29
26
|
}
|
|
30
27
|
violations(document) {
|
|
31
28
|
const uri = document.uri();
|
|
@@ -38,7 +35,7 @@ class Passive {
|
|
|
38
35
|
});
|
|
39
36
|
}
|
|
40
37
|
scan(text, line, uri) {
|
|
41
|
-
const regex = /\b(?:is|are|was|were|be|been|being)\b\s+(?:\w+ly\s+)?(?:\w+ed|written|done|made|built|kept|sent|shown|seen|taken|given|held|found|run|read|set)\b/iu;
|
|
38
|
+
const regex = /\b(?:is|are|was|were|be|been|being)\b\s+(?:\w+ly\s+)?(?:\w+ed|written|done|made|built|kept|sent|shown|seen|taken|given|held|found|run|read|set)\b(?=\s*$|\s*[.,;:!?)]|\s+(?:by|in|into|onto|to|from|with|within|without|for|on|at|as|through|against|over|under|after|before|during|upon|per|about)\b)/iu;
|
|
42
39
|
if (!regex.test(text)) {
|
|
43
40
|
return [];
|
|
44
41
|
}
|
package/src/rules/redundant.js
CHANGED
|
@@ -44,14 +44,10 @@ const PHRASES = [
|
|
|
44
44
|
* Flags a line that restates default model behavior, like
|
|
45
45
|
* "Be helpful and accurate" or "Write clean code". Such filler
|
|
46
46
|
* burns the context budget and drowns the project-specific
|
|
47
|
-
* guidance the manifesto exists to carry.
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* redundancy detection covers paraphrases beyond the curated
|
|
52
|
-
* blacklist below. The hybrid pattern from `src/rules/command.js`
|
|
53
|
-
* is the model: keep the deterministic check as the default,
|
|
54
|
-
* let the oracle catch the rest.
|
|
47
|
+
* guidance the manifesto exists to carry. Following the hybrid
|
|
48
|
+
* pattern of `command.js`, the curated blacklist below stays the
|
|
49
|
+
* deterministic default and the prompt hands paraphrases beyond
|
|
50
|
+
* that list to the AI oracle.
|
|
55
51
|
*/
|
|
56
52
|
class Redundant {
|
|
57
53
|
constructor(phrases = PHRASES) {
|
|
@@ -59,7 +55,7 @@ class Redundant {
|
|
|
59
55
|
this.phrases = phrases;
|
|
60
56
|
}
|
|
61
57
|
prompt() {
|
|
62
|
-
return `${this.id}: flag any line that restates default agent behavior already known to the model, not a project-specific instruction`;
|
|
58
|
+
return `${this.id}: flag any line that restates default agent behavior already known to the model, not a project-specific instruction, including reworded paraphrases that match no fixed phrase list`;
|
|
63
59
|
}
|
|
64
60
|
violations(document) {
|
|
65
61
|
const uri = document.uri();
|
package/src/rules/simple.js
CHANGED
|
@@ -13,19 +13,15 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands simple grammar over ambiguity. A standalone checker can only
|
|
15
15
|
* guess: it counts commas and conjunctions to flag lines that pile up
|
|
16
|
-
* clauses. Its prompt hands the subtler tangle judgement to the oracle
|
|
17
|
-
*
|
|
18
|
-
* @todo #29:45min Upgrade to true clause-depth analysis through an AI
|
|
19
|
-
* oracle so that subtle tangled instructions, which the comma and
|
|
20
|
-
* conjunction heuristic cannot measure today, are reliably caught, as
|
|
21
|
-
* requested in issue #29.
|
|
16
|
+
* clauses. Its prompt hands the subtler tangle judgement to the oracle,
|
|
17
|
+
* which weighs true clause depth rather than counting punctuation.
|
|
22
18
|
*/
|
|
23
19
|
class Simple {
|
|
24
20
|
constructor() {
|
|
25
21
|
this.id = 'simple';
|
|
26
22
|
}
|
|
27
23
|
prompt() {
|
|
28
|
-
return `${this.id}: flag any grammatically tangled, multi-clause instruction`;
|
|
24
|
+
return `${this.id}: flag any grammatically tangled, multi-clause instruction, judging true clause depth even when the line carries few commas or conjunctions`;
|
|
29
25
|
}
|
|
30
26
|
violations(document) {
|
|
31
27
|
const uri = document.uri();
|
|
@@ -41,7 +37,7 @@ class Simple {
|
|
|
41
37
|
const commas = text.match(/,/gu);
|
|
42
38
|
const commaCount = commas === null ? 0 : commas.length;
|
|
43
39
|
const hasConjunction = /\b(?:if|when|unless|because|although|while)\b/iu.test(text);
|
|
44
|
-
const tangled = hasConjunction && commaCount >= 2
|
|
40
|
+
const tangled = hasConjunction && commaCount >= 2;
|
|
45
41
|
if (!tangled) {
|
|
46
42
|
return [];
|
|
47
43
|
}
|
package/src/rules/unique.js
CHANGED
|
@@ -23,18 +23,16 @@ const normalize = (text) => {
|
|
|
23
23
|
*
|
|
24
24
|
* Flags any instruction that repeats another instruction in the file.
|
|
25
25
|
* It normalizes each prose line, then remembers the first line where
|
|
26
|
-
* each normal form appeared, so a later twin earns one violation.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* embeddings or an AI oracle, to catch same-meaning different-words
|
|
30
|
-
* pairs the normalizer misses, as requested in issue #25.
|
|
26
|
+
* each normal form appeared, so a later twin earns one violation. Its
|
|
27
|
+
* prompt hands semantic near-duplicates to the AI oracle, catching
|
|
28
|
+
* same-meaning different-words pairs the normalizer misses.
|
|
31
29
|
*/
|
|
32
30
|
class Unique {
|
|
33
31
|
constructor() {
|
|
34
32
|
this.id = 'unique';
|
|
35
33
|
}
|
|
36
34
|
prompt() {
|
|
37
|
-
return `${this.id}: flag any instruction that repeats another instruction in the file`;
|
|
35
|
+
return `${this.id}: flag any instruction that repeats another instruction in the file, including two lines that carry the same meaning in different words, not only lines matching after normalized case, punctuation, and word order`;
|
|
38
36
|
}
|
|
39
37
|
violations(document) {
|
|
40
38
|
const uri = document.uri();
|