@yegor256/dogent 0.5.1 → 0.6.1
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/args.js +37 -0
- package/src/dogent.js +12 -5
- package/src/rules/dead-import.js +66 -0
- package/src/rules/empty.js +4 -1
- package/src/rules/index.js +4 -0
- package/src/rules/redundant.js +92 -0
package/README.md
CHANGED
|
@@ -29,7 +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.
|
|
32
|
+
npx @yegor256/dogent@0.6.0 CLAUDE.md
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
Lint several files at once:
|
|
@@ -96,6 +96,15 @@ export OPENAI_API_KEY=...
|
|
|
96
96
|
npx @yegor256/dogent CLAUDE.md
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
Pass `--offline` to keep `dogent` away from the LLM,
|
|
100
|
+
even when `OPENAI_API_KEY` is present in the environment:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx @yegor256/dogent --offline CLAUDE.md
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Pass `--sarif` to print the report as SARIF instead of plain text.
|
|
107
|
+
|
|
99
108
|
## GitHub Actions
|
|
100
109
|
|
|
101
110
|
Because `dogent` runs through `npx`, no extra action is needed.
|
|
@@ -141,7 +150,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
141
150
|
```yaml
|
|
142
151
|
repos:
|
|
143
152
|
- repo: https://github.com/yegor256/dogent
|
|
144
|
-
rev: 0.
|
|
153
|
+
rev: 0.6.0
|
|
145
154
|
hooks:
|
|
146
155
|
- id: dogent
|
|
147
156
|
```
|
package/package.json
CHANGED
package/src/args.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
|
+
* Args.
|
|
10
|
+
*
|
|
11
|
+
* The command-line arguments handed to dogent. It splits the raw argv into
|
|
12
|
+
* recognized options and the manifesto paths that remain. The `--sarif`
|
|
13
|
+
* flag switches the report to SARIF, while `--offline` forbids any talk to
|
|
14
|
+
* the LLM even when a token sits in the environment.
|
|
15
|
+
*/
|
|
16
|
+
class Args {
|
|
17
|
+
constructor(argv, flags = ['--sarif', '--offline']) {
|
|
18
|
+
this.argv = argv;
|
|
19
|
+
this.flags = flags;
|
|
20
|
+
}
|
|
21
|
+
sarif() {
|
|
22
|
+
return this.argv.includes('--sarif');
|
|
23
|
+
}
|
|
24
|
+
offline() {
|
|
25
|
+
return this.argv.includes('--offline');
|
|
26
|
+
}
|
|
27
|
+
paths() {
|
|
28
|
+
return this.argv.filter((arg) => !arg.startsWith('-'));
|
|
29
|
+
}
|
|
30
|
+
unknown() {
|
|
31
|
+
return this.argv.filter(
|
|
32
|
+
(arg) => arg.startsWith('-') && !this.flags.includes(arg)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = Args;
|
package/src/dogent.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
|
+
const Args = require('./args');
|
|
10
11
|
const Markdown = require('./markdown');
|
|
11
12
|
const Report = require('./report');
|
|
12
13
|
const Sources = require('./sources');
|
|
@@ -14,11 +15,17 @@ const Openai = require('./openai');
|
|
|
14
15
|
const Oracle = require('./oracle');
|
|
15
16
|
const rules = require('./rules');
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
-
const sarif =
|
|
19
|
-
const
|
|
18
|
+
const args = new Args(process.argv.slice(2));
|
|
19
|
+
const sarif = args.sarif();
|
|
20
|
+
const unknown = args.unknown();
|
|
21
|
+
if (unknown.length > 0) {
|
|
22
|
+
process.stderr.write(`Unknown option: ${unknown[0]}\n`);
|
|
23
|
+
process.stderr.write('Usage: dogent [--sarif] [--offline] <file.md|dir>...\n');
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const paths = args.paths();
|
|
20
27
|
if (paths.length === 0) {
|
|
21
|
-
process.stderr.write('Usage: dogent [--sarif] <file.md|dir>...\n');
|
|
28
|
+
process.stderr.write('Usage: dogent [--sarif] [--offline] <file.md|dir>...\n');
|
|
22
29
|
process.exit(2);
|
|
23
30
|
}
|
|
24
31
|
const scanned = new Sources(paths).files();
|
|
@@ -35,7 +42,7 @@ documents.forEach((document) => {
|
|
|
35
42
|
});
|
|
36
43
|
const key = process.env.OPENAI_API_KEY;
|
|
37
44
|
(async () => {
|
|
38
|
-
if (found.length === 0 && key) {
|
|
45
|
+
if (found.length === 0 && key && !args.offline()) {
|
|
39
46
|
try {
|
|
40
47
|
const oracle = new Oracle(
|
|
41
48
|
rules(),
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
const Violation = require('../violation');
|
|
12
|
+
const Region = require('../region');
|
|
13
|
+
|
|
14
|
+
const imports = (line) => {
|
|
15
|
+
const found = [];
|
|
16
|
+
const pattern = /(?<lead>^|\s)@(?<file>\S+)/gu;
|
|
17
|
+
let match = pattern.exec(line);
|
|
18
|
+
while (match !== null) {
|
|
19
|
+
found.push({
|
|
20
|
+
file: match.groups.file.replace(/[.,:;!?]+$/u, ''),
|
|
21
|
+
column: match.index + match.groups.lead.length + 1
|
|
22
|
+
});
|
|
23
|
+
match = pattern.exec(line);
|
|
24
|
+
}
|
|
25
|
+
return found;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* DeadImport.
|
|
30
|
+
*
|
|
31
|
+
* Flags `@path/to/file` imports that point to no file on disk.
|
|
32
|
+
*
|
|
33
|
+
* @todo #18:45min Detect circular import chains and depth above five
|
|
34
|
+
* levels so deeply nested manifesto imports fail with a clear violation,
|
|
35
|
+
* as requested in issue #18.
|
|
36
|
+
*/
|
|
37
|
+
class DeadImport {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.id = 'dead-import';
|
|
40
|
+
}
|
|
41
|
+
prompt() {
|
|
42
|
+
return `${this.id}: flag any @path/to/file import that points to no file on disk`;
|
|
43
|
+
}
|
|
44
|
+
violations(document) {
|
|
45
|
+
return document.walk({
|
|
46
|
+
header: () => [],
|
|
47
|
+
snippet: () => [],
|
|
48
|
+
bullets: () => [],
|
|
49
|
+
frontmatter: () => [],
|
|
50
|
+
prose: (line, row) => this.missing(document.uri(), line, row)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
missing(uri, line, row) {
|
|
54
|
+
const base = path.dirname(uri);
|
|
55
|
+
return imports(line)
|
|
56
|
+
.filter((item) => !fs.existsSync(path.resolve(base, item.file)))
|
|
57
|
+
.map((item) => new Violation(
|
|
58
|
+
this.id,
|
|
59
|
+
'error',
|
|
60
|
+
`@-import target not found: ${item.file}`,
|
|
61
|
+
new Region(uri, row, item.column)
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = DeadImport;
|
package/src/rules/empty.js
CHANGED
|
@@ -15,13 +15,16 @@ const Region = require('../region');
|
|
|
15
15
|
* A heading is empty when it is immediately followed by another
|
|
16
16
|
* heading or by end-of-file — no prose, bullets, or snippet sits
|
|
17
17
|
* between them.
|
|
18
|
+
*
|
|
19
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
20
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
18
21
|
*/
|
|
19
22
|
class Empty {
|
|
20
23
|
constructor() {
|
|
21
24
|
this.id = 'empty';
|
|
22
25
|
}
|
|
23
26
|
prompt() {
|
|
24
|
-
return
|
|
27
|
+
return '';
|
|
25
28
|
}
|
|
26
29
|
violations(document) {
|
|
27
30
|
const uri = document.uri();
|
package/src/rules/index.js
CHANGED
|
@@ -14,6 +14,8 @@ const NoArticles = require('./no-articles');
|
|
|
14
14
|
const Command = require('./command');
|
|
15
15
|
const Punctuation = require('./punctuation');
|
|
16
16
|
const Frontmatter = require('./frontmatter');
|
|
17
|
+
const DeadImport = require('./dead-import');
|
|
18
|
+
const Redundant = require('./redundant');
|
|
17
19
|
|
|
18
20
|
module.exports = () => [
|
|
19
21
|
new Grouped(),
|
|
@@ -24,6 +26,8 @@ module.exports = () => [
|
|
|
24
26
|
new NoArticles(),
|
|
25
27
|
new Command(),
|
|
26
28
|
new Punctuation(),
|
|
29
|
+
new DeadImport(),
|
|
30
|
+
new Redundant(),
|
|
27
31
|
new Frontmatter(
|
|
28
32
|
'SKILL.md',
|
|
29
33
|
['name', 'description'],
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
const PHRASES = [
|
|
12
|
+
'be helpful',
|
|
13
|
+
'be helpful and accurate',
|
|
14
|
+
'be accurate',
|
|
15
|
+
'be concise',
|
|
16
|
+
'be clear',
|
|
17
|
+
'be polite',
|
|
18
|
+
'be professional',
|
|
19
|
+
'write clean code',
|
|
20
|
+
'write good code',
|
|
21
|
+
'write readable code',
|
|
22
|
+
'write maintainable code',
|
|
23
|
+
'follow best practices',
|
|
24
|
+
'follow industry best practices',
|
|
25
|
+
'use best practices',
|
|
26
|
+
'apply best practices',
|
|
27
|
+
'use meaningful variable names',
|
|
28
|
+
'use descriptive variable names',
|
|
29
|
+
'use good variable names',
|
|
30
|
+
'handle errors properly',
|
|
31
|
+
'handle errors gracefully',
|
|
32
|
+
'handle exceptions properly',
|
|
33
|
+
'avoid bugs',
|
|
34
|
+
'avoid mistakes',
|
|
35
|
+
'think step by step',
|
|
36
|
+
'do your best',
|
|
37
|
+
'try your best',
|
|
38
|
+
'pay attention to detail'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Redundant.
|
|
43
|
+
*
|
|
44
|
+
* Flags a line that restates default model behavior, like
|
|
45
|
+
* "Be helpful and accurate" or "Write clean code". Such filler
|
|
46
|
+
* burns the context budget and drowns the project-specific
|
|
47
|
+
* guidance the manifesto exists to carry.
|
|
48
|
+
*
|
|
49
|
+
* @todo #15:60min Promote the standalone heuristic into a
|
|
50
|
+
* proper AI-oracle check when `OPENAI_API_KEY` is present, so
|
|
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.
|
|
55
|
+
*/
|
|
56
|
+
class Redundant {
|
|
57
|
+
constructor(phrases = PHRASES) {
|
|
58
|
+
this.id = 'redundant';
|
|
59
|
+
this.phrases = phrases;
|
|
60
|
+
}
|
|
61
|
+
prompt() {
|
|
62
|
+
return `${this.id}: flag any line that restates default agent behavior already known to the model, not a project-specific instruction`;
|
|
63
|
+
}
|
|
64
|
+
violations(document) {
|
|
65
|
+
const uri = document.uri();
|
|
66
|
+
return document.walk({
|
|
67
|
+
header: () => [],
|
|
68
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
69
|
+
snippet: () => [],
|
|
70
|
+
bullets: () => [],
|
|
71
|
+
frontmatter: () => []
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
judge(text, line, uri) {
|
|
75
|
+
const clean = text
|
|
76
|
+
.replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '')
|
|
77
|
+
.replace(/[.!?]+\s*$/u, '')
|
|
78
|
+
.trim()
|
|
79
|
+
.toLowerCase();
|
|
80
|
+
if (!this.phrases.includes(clean)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
return [new Violation(
|
|
84
|
+
this.id,
|
|
85
|
+
'error',
|
|
86
|
+
'generic instruction, model already knows this',
|
|
87
|
+
new Region(uri, line, 1)
|
|
88
|
+
)];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = Redundant;
|