@yegor256/dogent 0.4.0 → 0.5.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 +24 -11
- package/package.json +1 -1
- package/src/answer.js +47 -0
- package/src/document.js +7 -2
- package/src/dogent.js +34 -5
- package/src/markdown.js +1 -1
- package/src/openai.js +45 -0
- package/src/oracle.js +30 -0
- package/src/prompt.js +54 -0
- package/src/rules/command.js +5 -4
- package/src/rules/empty.js +54 -0
- package/src/rules/frontmatter.js +3 -0
- package/src/rules/grouped.js +3 -0
- package/src/rules/index.js +2 -0
- package/src/rules/line-length.js +3 -0
- package/src/rules/no-articles.js +3 -0
- package/src/rules/punctuation.js +3 -0
- package/src/rules/short-sections.js +3 -0
- package/src/rules/token-count.js +3 -0
- package/src/sources.js +18 -8
package/README.md
CHANGED
|
@@ -14,16 +14,22 @@ Vague, bloated, or ambiguous instructions make agents behave unpredictably.
|
|
|
14
14
|
so every line earns its place.
|
|
15
15
|
|
|
16
16
|
We respect [agent-sh/agnix](https://github.com/agent-sh/agnix)
|
|
17
|
-
as a prototype of this idea.
|
|
18
|
-
`
|
|
19
|
-
|
|
17
|
+
as a prototype of this idea, but the two lint different layers.
|
|
18
|
+
`agnix` validates the harness around a prompt — frontmatter schema,
|
|
19
|
+
hook JSON, MCP config, tool wiring — asking whether the configuration
|
|
20
|
+
is well-formed and correctly wired.
|
|
21
|
+
`dogent` lints the prose of the instructions themselves,
|
|
22
|
+
asking whether every line is a tight, unambiguous command.
|
|
23
|
+
In short: `agnix` lints the harness, `dogent` lints the prompt.
|
|
24
|
+
`dogent` is the stricter, more opinionated of the two,
|
|
25
|
+
aiming for extreme quality with no compromise.
|
|
20
26
|
|
|
21
27
|
## Usage
|
|
22
28
|
|
|
23
29
|
Run it on any manifesto file, no installation required:
|
|
24
30
|
|
|
25
31
|
```bash
|
|
26
|
-
npx @yegor256/dogent@0.
|
|
32
|
+
npx @yegor256/dogent@0.5.0 CLAUDE.md
|
|
27
33
|
```
|
|
28
34
|
|
|
29
35
|
Lint several files at once:
|
|
@@ -33,7 +39,10 @@ npx @yegor256/dogent SKILL.md CLAUDE.md AGENTS.md
|
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
Point it at a directory to lint the default manifestos it holds
|
|
36
|
-
(`AGENTS.md`, `CLAUDE.md`, `SKILL.md`, `SKILLS.md`)
|
|
42
|
+
(`AGENTS.md`, `CLAUDE.md`, `SKILL.md`, `SKILLS.md`).
|
|
43
|
+
The directory is scanned recursively through every subfolder
|
|
44
|
+
(skipping `node_modules` and `.git`),
|
|
45
|
+
and each scanned file is announced on the standard error stream:
|
|
37
46
|
|
|
38
47
|
```bash
|
|
39
48
|
npx @yegor256/dogent .
|
|
@@ -74,12 +83,16 @@ The command exits with a non-zero status when problems are found,
|
|
|
74
83
|
|
|
75
84
|
`dogent` works standalone by default,
|
|
76
85
|
using fast deterministic checks with no network access.
|
|
77
|
-
When `OPENAI_API_KEY`
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
When `OPENAI_API_KEY` is present in the environment,
|
|
87
|
+
and only after the standalone rules find nothing,
|
|
88
|
+
`dogent` asks OpenAI for a second, deeper opinion.
|
|
89
|
+
It sends the manifesto together with one instruction per rule,
|
|
90
|
+
then prints any violation the model reports for ambiguity,
|
|
91
|
+
weak phrasing, and instructions that only pretend to be commands.
|
|
92
|
+
The model defaults to `gpt-4o-mini`; override it with `OPENAI_MODEL`.
|
|
80
93
|
|
|
81
94
|
```bash
|
|
82
|
-
export
|
|
95
|
+
export OPENAI_API_KEY=...
|
|
83
96
|
npx @yegor256/dogent CLAUDE.md
|
|
84
97
|
```
|
|
85
98
|
|
|
@@ -109,7 +122,7 @@ To enable AI verification in CI, expose a token as a secret:
|
|
|
109
122
|
```yaml
|
|
110
123
|
- run: npx @yegor256/dogent CLAUDE.md
|
|
111
124
|
env:
|
|
112
|
-
|
|
125
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
113
126
|
```
|
|
114
127
|
|
|
115
128
|
## Pre-commit hook
|
|
@@ -128,7 +141,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
|
|
|
128
141
|
```yaml
|
|
129
142
|
repos:
|
|
130
143
|
- repo: https://github.com/yegor256/dogent
|
|
131
|
-
rev: 0.
|
|
144
|
+
rev: 0.5.0
|
|
132
145
|
hooks:
|
|
133
146
|
- id: dogent
|
|
134
147
|
```
|
package/package.json
CHANGED
package/src/answer.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
* Answer.
|
|
13
|
+
*
|
|
14
|
+
* The oracle's raw reply, treated as untrusted input. Parses the JSON
|
|
15
|
+
* object it carries and turns every well-formed SARIF result back into a
|
|
16
|
+
* native violation, ignoring any result it cannot read.
|
|
17
|
+
*/
|
|
18
|
+
class Answer {
|
|
19
|
+
constructor(raw) {
|
|
20
|
+
this.raw = raw;
|
|
21
|
+
}
|
|
22
|
+
violations() {
|
|
23
|
+
return this.results().flatMap((result) => {
|
|
24
|
+
const spot = result?.locations?.[0]?.physicalLocation;
|
|
25
|
+
const line = spot?.region?.startLine;
|
|
26
|
+
const text = result?.message?.text;
|
|
27
|
+
if (typeof line !== 'number' || typeof text !== 'string' || !spot.artifactLocation) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [new Violation(
|
|
31
|
+
result.ruleId || 'oracle',
|
|
32
|
+
result.level || 'warning',
|
|
33
|
+
text,
|
|
34
|
+
new Region(spot.artifactLocation.uri, line, spot.region.startColumn || 1)
|
|
35
|
+
)];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
results() {
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(this.raw).results || [];
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error(`oracle returned malformed JSON: ${error.message}`, {cause: error});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = Answer;
|
package/src/document.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
* Document.
|
|
10
10
|
*
|
|
11
11
|
* An entire manifesto already parsed into an ordered collection of
|
|
12
|
-
* fragments, ready to be walked by a rule that hunts for violations.
|
|
12
|
+
* fragments, ready to be walked by a rule that hunts for violations. It
|
|
13
|
+
* also keeps the raw text, which the AI oracle reads verbatim.
|
|
13
14
|
*/
|
|
14
15
|
class Document {
|
|
15
|
-
constructor(uri, fragments) {
|
|
16
|
+
constructor(uri, fragments, content) {
|
|
16
17
|
this.address = uri;
|
|
17
18
|
this.pieces = fragments;
|
|
19
|
+
this.body = content;
|
|
18
20
|
}
|
|
19
21
|
uri() {
|
|
20
22
|
return this.address;
|
|
@@ -22,6 +24,9 @@ class Document {
|
|
|
22
24
|
fragments() {
|
|
23
25
|
return this.pieces;
|
|
24
26
|
}
|
|
27
|
+
text() {
|
|
28
|
+
return this.body;
|
|
29
|
+
}
|
|
25
30
|
walk(visitor) {
|
|
26
31
|
return this.pieces.reduce((all, piece) => all.concat(piece.accept(visitor)), []);
|
|
27
32
|
}
|
package/src/dogent.js
CHANGED
|
@@ -10,6 +10,8 @@ const fs = require('fs');
|
|
|
10
10
|
const Markdown = require('./markdown');
|
|
11
11
|
const Report = require('./report');
|
|
12
12
|
const Sources = require('./sources');
|
|
13
|
+
const Openai = require('./openai');
|
|
14
|
+
const Oracle = require('./oracle');
|
|
13
15
|
const rules = require('./rules');
|
|
14
16
|
|
|
15
17
|
const argv = process.argv.slice(2);
|
|
@@ -19,13 +21,40 @@ if (paths.length === 0) {
|
|
|
19
21
|
process.stderr.write('Usage: dogent [--sarif] <file.md|dir>...\n');
|
|
20
22
|
process.exit(2);
|
|
21
23
|
}
|
|
24
|
+
const scanned = new Sources(paths).files();
|
|
25
|
+
scanned.forEach((file) => process.stderr.write(`Scanning ${file}\n`));
|
|
26
|
+
process.stderr.write(`${scanned.length} files scanned\n`);
|
|
27
|
+
const documents = scanned.map(
|
|
28
|
+
(file) => new Markdown(file, fs.readFileSync(file, 'utf8')).document()
|
|
29
|
+
);
|
|
22
30
|
const found = [];
|
|
23
|
-
|
|
24
|
-
const document = new Markdown(file, fs.readFileSync(file, 'utf8')).document();
|
|
31
|
+
documents.forEach((document) => {
|
|
25
32
|
rules().forEach((rule) => {
|
|
26
33
|
rule.violations(document).forEach((violation) => found.push(violation));
|
|
27
34
|
});
|
|
28
35
|
});
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const key = process.env.OPENAI_API_KEY;
|
|
37
|
+
(async () => {
|
|
38
|
+
if (found.length === 0 && key) {
|
|
39
|
+
try {
|
|
40
|
+
const oracle = new Oracle(
|
|
41
|
+
rules(),
|
|
42
|
+
new Openai(
|
|
43
|
+
key,
|
|
44
|
+
process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
45
|
+
(url, options) => globalThis.fetch(url, options)
|
|
46
|
+
)
|
|
47
|
+
);
|
|
48
|
+
const extra = await Promise.all(
|
|
49
|
+
documents.map((document) => oracle.violations(document))
|
|
50
|
+
);
|
|
51
|
+
extra.forEach((bag) => bag.forEach((violation) => found.push(violation)));
|
|
52
|
+
} catch (error) {
|
|
53
|
+
process.stderr.write(`AI verification failed: ${error.message}\n`);
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const report = new Report('dogent', found);
|
|
58
|
+
process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
|
|
59
|
+
process.exit(report.count() > 0 ? 1 : 0);
|
|
60
|
+
})();
|
package/src/markdown.js
CHANGED
package/src/openai.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Openai.
|
|
10
|
+
*
|
|
11
|
+
* A thin adapter over the OpenAI chat-completions endpoint. Sends one
|
|
12
|
+
* prompt, demands a JSON object back, and returns the assistant text.
|
|
13
|
+
* The transport is injected so the class runs in tests without a socket.
|
|
14
|
+
*/
|
|
15
|
+
class Openai {
|
|
16
|
+
constructor(key, model, transport) {
|
|
17
|
+
this.key = key;
|
|
18
|
+
this.model = model;
|
|
19
|
+
this.transport = transport;
|
|
20
|
+
}
|
|
21
|
+
async answer(prompt) {
|
|
22
|
+
const response = await this.transport(
|
|
23
|
+
'https://api.openai.com/v1/chat/completions',
|
|
24
|
+
{
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Authorization: `Bearer ${this.key}`
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
model: this.model,
|
|
32
|
+
temperature: 0,
|
|
33
|
+
response_format: {type: 'json_object'},
|
|
34
|
+
messages: [{role: 'user', content: prompt}]
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`OpenAI request rejected with status ${response.status}`);
|
|
40
|
+
}
|
|
41
|
+
return (await response.json()).choices[0].message.content;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = Openai;
|
package/src/oracle.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Prompt = require('./prompt');
|
|
9
|
+
const Answer = require('./answer');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Oracle.
|
|
13
|
+
*
|
|
14
|
+
* The AI second opinion. Wraps the rules and a chat endpoint, builds one
|
|
15
|
+
* prompt from a document, asks the endpoint, and parses the reply into
|
|
16
|
+
* violations. Mirrors a rule, but consults a model instead of guessing.
|
|
17
|
+
*/
|
|
18
|
+
class Oracle {
|
|
19
|
+
constructor(rules, chat) {
|
|
20
|
+
this.rules = rules;
|
|
21
|
+
this.chat = chat;
|
|
22
|
+
}
|
|
23
|
+
async violations(document) {
|
|
24
|
+
return new Answer(
|
|
25
|
+
await this.chat.answer(new Prompt(this.rules, document).text())
|
|
26
|
+
).violations();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = Oracle;
|
package/src/prompt.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Prompt.
|
|
10
|
+
*
|
|
11
|
+
* The full request handed to the AI oracle: a header fixing the task and
|
|
12
|
+
* the reply shape, one fragment per rule, and the manifesto itself with
|
|
13
|
+
* every line numbered so the oracle can cite an exact row.
|
|
14
|
+
*/
|
|
15
|
+
class Prompt {
|
|
16
|
+
constructor(rules, document) {
|
|
17
|
+
this.rules = rules;
|
|
18
|
+
this.doc = document;
|
|
19
|
+
}
|
|
20
|
+
text() {
|
|
21
|
+
return [this.header(), this.fragments(), this.body()].join('\n\n');
|
|
22
|
+
}
|
|
23
|
+
header() {
|
|
24
|
+
const uri = this.doc.uri();
|
|
25
|
+
return [
|
|
26
|
+
'You are a strict linter for an AI-agent manifesto.',
|
|
27
|
+
`The file under review is "${uri}".`,
|
|
28
|
+
'Apply only the checks listed below this header.',
|
|
29
|
+
'Report a violation only when it is clear and certain.',
|
|
30
|
+
'When in doubt, stay silent and report nothing.',
|
|
31
|
+
'Reply with one JSON object and nothing else, shaped as',
|
|
32
|
+
'{"results":[ ... ]}, where each item is a SARIF result with',
|
|
33
|
+
'keys ruleId, level "warning", message.text, and locations.',
|
|
34
|
+
'Set ruleId to the rule name and startLine to the printed',
|
|
35
|
+
'line number; locations[0].physicalLocation must carry',
|
|
36
|
+
`artifactLocation.uri "${uri}" and region.startColumn 1.`
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
fragments() {
|
|
40
|
+
return this.rules
|
|
41
|
+
.map((rule) => rule.prompt())
|
|
42
|
+
.filter((fragment) => fragment !== '')
|
|
43
|
+
.join('\n');
|
|
44
|
+
}
|
|
45
|
+
body() {
|
|
46
|
+
return this.doc
|
|
47
|
+
.text()
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line, index) => `${index + 1}: ${line}`)
|
|
50
|
+
.join('\n');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = Prompt;
|
package/src/rules/command.js
CHANGED
|
@@ -13,15 +13,16 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands that every instruction sound like a command. A standalone
|
|
15
15
|
* checker can only guess: it flags lines that open with a pronoun or
|
|
16
|
-
* end with a question mark, both signs of description, not order.
|
|
17
|
-
*
|
|
18
|
-
* @todo #1:90min Replace this heuristic with a real imperative-mood check
|
|
19
|
-
* driven by an AI oracle when a token is present in the environment.
|
|
16
|
+
* end with a question mark, both signs of description, not order. Its
|
|
17
|
+
* prompt hands the subtler imperative-mood judgement to the AI oracle.
|
|
20
18
|
*/
|
|
21
19
|
class Command {
|
|
22
20
|
constructor() {
|
|
23
21
|
this.id = 'command';
|
|
24
22
|
}
|
|
23
|
+
prompt() {
|
|
24
|
+
return `${this.id}: flag any line that reads as a description, a question, or a plain statement rather than a direct order`;
|
|
25
|
+
}
|
|
25
26
|
violations(document) {
|
|
26
27
|
const uri = document.uri();
|
|
27
28
|
return document.walk({
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* Empty.
|
|
13
|
+
*
|
|
14
|
+
* Flags any heading that declares a section but carries no body.
|
|
15
|
+
* A heading is empty when it is immediately followed by another
|
|
16
|
+
* heading or by end-of-file — no prose, bullets, or snippet sits
|
|
17
|
+
* between them.
|
|
18
|
+
*/
|
|
19
|
+
class Empty {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.id = 'empty';
|
|
22
|
+
}
|
|
23
|
+
prompt() {
|
|
24
|
+
return `${this.id}: flag any section heading that carries no instructions beneath it`;
|
|
25
|
+
}
|
|
26
|
+
violations(document) {
|
|
27
|
+
const uri = document.uri();
|
|
28
|
+
const marks = document.walk({
|
|
29
|
+
header: (text, line) => [{header: true, line}],
|
|
30
|
+
prose: (text, line) => [{header: false, line}],
|
|
31
|
+
bullets: (row) => [{header: false, line: row}],
|
|
32
|
+
snippet: (text, line) => [{header: false, line}],
|
|
33
|
+
frontmatter: () => []
|
|
34
|
+
});
|
|
35
|
+
const result = [];
|
|
36
|
+
marks.forEach((mark, index) => {
|
|
37
|
+
if (!mark.header) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const next = marks[index + 1];
|
|
41
|
+
if (!next || next.header) {
|
|
42
|
+
result.push(new Violation(
|
|
43
|
+
this.id,
|
|
44
|
+
'error',
|
|
45
|
+
'hollow section, no instructions found',
|
|
46
|
+
new Region(uri, mark.line, 1)
|
|
47
|
+
));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = Empty;
|
package/src/rules/frontmatter.js
CHANGED
|
@@ -22,6 +22,9 @@ class Frontmatter {
|
|
|
22
22
|
this.required = required;
|
|
23
23
|
this.allowed = allowed;
|
|
24
24
|
}
|
|
25
|
+
prompt() {
|
|
26
|
+
return `${this.id}: in a ${this.name} file, flag any required key whose value is empty, vague, or a leftover placeholder`;
|
|
27
|
+
}
|
|
25
28
|
violations(document) {
|
|
26
29
|
const uri = document.uri();
|
|
27
30
|
if (uri.replace(/^.*\//u, '') !== this.name) {
|
package/src/rules/grouped.js
CHANGED
|
@@ -18,6 +18,9 @@ class Grouped {
|
|
|
18
18
|
constructor() {
|
|
19
19
|
this.id = 'grouped';
|
|
20
20
|
}
|
|
21
|
+
prompt() {
|
|
22
|
+
return `${this.id}: flag any instruction that sits under a section where it does not belong`;
|
|
23
|
+
}
|
|
21
24
|
violations(document) {
|
|
22
25
|
const uri = document.uri();
|
|
23
26
|
const marks = document.walk({
|
package/src/rules/index.js
CHANGED
|
@@ -9,6 +9,7 @@ const LineLength = require('./line-length');
|
|
|
9
9
|
const TokenCount = require('./token-count');
|
|
10
10
|
const ShortSections = require('./short-sections');
|
|
11
11
|
const Grouped = require('./grouped');
|
|
12
|
+
const Empty = require('./empty');
|
|
12
13
|
const NoArticles = require('./no-articles');
|
|
13
14
|
const Command = require('./command');
|
|
14
15
|
const Punctuation = require('./punctuation');
|
|
@@ -16,6 +17,7 @@ const Frontmatter = require('./frontmatter');
|
|
|
16
17
|
|
|
17
18
|
module.exports = () => [
|
|
18
19
|
new Grouped(),
|
|
20
|
+
new Empty(),
|
|
19
21
|
new ShortSections(),
|
|
20
22
|
new LineLength(80),
|
|
21
23
|
new TokenCount(4000),
|
package/src/rules/line-length.js
CHANGED
package/src/rules/no-articles.js
CHANGED
|
@@ -18,6 +18,9 @@ class NoArticles {
|
|
|
18
18
|
constructor() {
|
|
19
19
|
this.id = 'no-articles';
|
|
20
20
|
}
|
|
21
|
+
prompt() {
|
|
22
|
+
return `${this.id}: flag filler or noise words that add nothing to an instruction`;
|
|
23
|
+
}
|
|
21
24
|
violations(document) {
|
|
22
25
|
const uri = document.uri();
|
|
23
26
|
return document.walk({
|
package/src/rules/punctuation.js
CHANGED
|
@@ -19,6 +19,9 @@ class Punctuation {
|
|
|
19
19
|
constructor() {
|
|
20
20
|
this.id = 'punctuation';
|
|
21
21
|
}
|
|
22
|
+
prompt() {
|
|
23
|
+
return `${this.id}: flag any instruction that is not one complete, grammatical sentence`;
|
|
24
|
+
}
|
|
22
25
|
violations(document) {
|
|
23
26
|
const uri = document.uri();
|
|
24
27
|
return document.walk({
|
|
@@ -18,6 +18,9 @@ class ShortSections {
|
|
|
18
18
|
constructor() {
|
|
19
19
|
this.id = 'short-sections';
|
|
20
20
|
}
|
|
21
|
+
prompt() {
|
|
22
|
+
return `${this.id}: flag any section heading that is not a short, noun-style label`;
|
|
23
|
+
}
|
|
21
24
|
violations(document) {
|
|
22
25
|
const uri = document.uri();
|
|
23
26
|
return document.walk({
|
package/src/rules/token-count.js
CHANGED
package/src/sources.js
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
|
|
11
|
+
const PRUNED = ['node_modules', '.git'];
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Sources.
|
|
13
15
|
*
|
|
14
16
|
* The paths passed on the command line. Each path names either one
|
|
15
17
|
* manifesto file or one directory. A directory expands into the default
|
|
16
|
-
* manifesto files it
|
|
17
|
-
* manifesto in the
|
|
18
|
+
* manifesto files it holds, scanned recursively through every subfolder,
|
|
19
|
+
* so `dogent .` lints every known manifesto in the whole tree.
|
|
18
20
|
*/
|
|
19
21
|
class Sources {
|
|
20
22
|
constructor(paths, defaults = ['AGENTS.md', 'CLAUDE.md', 'SKILL.md', 'SKILLS.md']) {
|
|
@@ -25,18 +27,26 @@ class Sources {
|
|
|
25
27
|
const found = [];
|
|
26
28
|
this.paths.forEach((entry) => {
|
|
27
29
|
if (fs.statSync(entry).isDirectory()) {
|
|
28
|
-
this.
|
|
29
|
-
const file = path.join(entry, name);
|
|
30
|
-
if (fs.existsSync(file)) {
|
|
31
|
-
found.push(file);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
30
|
+
this.scan(entry, found);
|
|
34
31
|
} else {
|
|
35
32
|
found.push(entry);
|
|
36
33
|
}
|
|
37
34
|
});
|
|
38
35
|
return found;
|
|
39
36
|
}
|
|
37
|
+
scan(dir, found) {
|
|
38
|
+
this.defaults.forEach((name) => {
|
|
39
|
+
const file = path.join(dir, name);
|
|
40
|
+
if (fs.existsSync(file) && fs.statSync(file).isFile()) {
|
|
41
|
+
found.push(file);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
fs.readdirSync(dir, {withFileTypes: true}).forEach((entry) => {
|
|
45
|
+
if (entry.isDirectory() && !PRUNED.includes(entry.name)) {
|
|
46
|
+
this.scan(path.join(dir, entry.name), found);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
module.exports = Sources;
|