@yegor256/dogent 0.9.1 → 0.10.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 +17 -0
- package/package.json +3 -2
- package/src/args.js +21 -5
- package/src/dogent.js +23 -13
- package/src/report.js +8 -2
- package/src/rules/budget.js +50 -0
- package/src/rules/concise.js +48 -0
- package/src/rules/counter-example.js +60 -0
- package/src/rules/done.js +53 -0
- package/src/rules/emphasis.js +81 -0
- package/src/rules/example.js +60 -0
- package/src/rules/format.js +68 -0
- package/src/rules/index.js +40 -0
- package/src/rules/jargon.js +105 -0
- package/src/rules/ordered.js +57 -0
- package/src/rules/persona.js +55 -0
- package/src/rules/positive.js +57 -0
- package/src/rules/pseudo-heading.js +55 -0
- package/src/rules/rationale.js +54 -0
- package/src/rules/referential.js +67 -0
- package/src/rules/self-contained.js +66 -0
- package/src/rules/stale.js +62 -0
- package/src/rules/terms.js +77 -0
- package/src/rules/tool-clarity.js +61 -0
- package/src/rules/untrusted.js +59 -0
- package/src/rules/vague.js +63 -0
- package/src/version.js +2 -2
package/README.md
CHANGED
|
@@ -67,20 +67,30 @@ The command exits with a non-zero status when problems are found,
|
|
|
67
67
|
- Every section must be a level-2 (`##`) heading, below the lone `#` title.
|
|
68
68
|
- Every line must be no longer than 80 symbols.
|
|
69
69
|
- The whole file must stay under 4000 tokens.
|
|
70
|
+
- The whole file must stay short; split detail into referenced files.
|
|
70
71
|
- Every line must sound like a command.
|
|
71
72
|
- Every sentence must start with a capital and end with a period.
|
|
72
73
|
- No articles, no noise, no bloated text.
|
|
73
74
|
- Simple grammar, no ambiguity.
|
|
75
|
+
- No bare pronoun subjects; name the subject on the line.
|
|
74
76
|
- No tangled, multi-clause instructions.
|
|
77
|
+
- Sequential steps must be a numbered list, not bullets.
|
|
75
78
|
- A `SKILL.md` `name` must equal its parent directory.
|
|
76
79
|
- No courtesy or scaffolding words.
|
|
77
80
|
- No leftover markers or unfilled placeholders.
|
|
78
81
|
- A section must hold at most ten instructions.
|
|
79
82
|
- A `SKILL.md` `description` must say when to use the skill.
|
|
83
|
+
- A `SKILL.md` must carry at least one worked example.
|
|
84
|
+
- A `SKILL.md` that produces output must declare its format.
|
|
80
85
|
- Every line must carry exactly one instruction.
|
|
81
86
|
- No hedging or soft wording.
|
|
87
|
+
- No vague qualifiers; demand a measurable criterion.
|
|
82
88
|
- No passive voice; use the active imperative.
|
|
89
|
+
- No ALL-CAPS shouting or "!!" markers; state it plainly.
|
|
90
|
+
- No persona or role-play; it adds no instruction.
|
|
91
|
+
- No negative phrasing; state the positive command instead.
|
|
83
92
|
- No instruction may repeat another.
|
|
93
|
+
- No unguarded consumption of untrusted external input.
|
|
84
94
|
- `SKILL.md` must open with valid frontmatter.
|
|
85
95
|
- Frontmatter must declare only allowed keys.
|
|
86
96
|
- A `SKILL.md` `name` must be kebab-case.
|
|
@@ -114,6 +124,13 @@ npx @yegor256/dogent --offline CLAUDE.md
|
|
|
114
124
|
|
|
115
125
|
Pass `--sarif` to print the report as SARIF instead of plain text.
|
|
116
126
|
|
|
127
|
+
Pass `--suppress` to silence a rule by its id. Repeat the option or
|
|
128
|
+
join several ids with commas to silence many at once:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx @yegor256/dogent --suppress=name-matches-dir,line-length CLAUDE.md
|
|
132
|
+
```
|
|
133
|
+
|
|
117
134
|
## GitHub Actions
|
|
118
135
|
|
|
119
136
|
Because `dogent` runs through `npx`, no extra action is needed.
|
package/package.json
CHANGED
package/src/args.js
CHANGED
|
@@ -15,13 +15,22 @@ const minimist = require('minimist');
|
|
|
15
15
|
* The `--sarif` flag switches the report to SARIF, while `--offline` forbids
|
|
16
16
|
* any talk to the LLM even when a token sits in the environment. The `--help`
|
|
17
17
|
* flag, also spelled `-h`, asks for the usage banner. The `--version` flag
|
|
18
|
-
* asks for the release number.
|
|
19
|
-
*
|
|
18
|
+
* asks for the release number. The `--suppress` option names a rule to
|
|
19
|
+
* silence; repeat it or join names with commas to silence many at once.
|
|
20
|
+
* Everything after a `--` separator counts as a path, never as an option.
|
|
20
21
|
*/
|
|
21
22
|
class Args {
|
|
22
|
-
constructor(
|
|
23
|
+
constructor(
|
|
24
|
+
argv,
|
|
25
|
+
flags = ['sarif', 'offline', 'help', 'version'],
|
|
26
|
+
options = ['suppress']
|
|
27
|
+
) {
|
|
23
28
|
this.flags = flags;
|
|
24
|
-
this.
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.parsed = minimist(
|
|
31
|
+
argv,
|
|
32
|
+
{boolean: flags, string: options, alias: {help: 'h'}, '--': true}
|
|
33
|
+
);
|
|
25
34
|
}
|
|
26
35
|
sarif() {
|
|
27
36
|
return this.parsed.sarif === true;
|
|
@@ -35,12 +44,19 @@ class Args {
|
|
|
35
44
|
version() {
|
|
36
45
|
return this.parsed.version === true;
|
|
37
46
|
}
|
|
47
|
+
suppress() {
|
|
48
|
+
return [].concat(this.parsed.suppress || [])
|
|
49
|
+
.flatMap((item) => String(item).split(','))
|
|
50
|
+
.map((name) => name.trim())
|
|
51
|
+
.filter((name) => name !== '');
|
|
52
|
+
}
|
|
38
53
|
paths() {
|
|
39
54
|
return this.parsed._.concat(this.parsed['--']).map(String);
|
|
40
55
|
}
|
|
41
56
|
unknown() {
|
|
42
57
|
return Object.keys(this.parsed)
|
|
43
|
-
.filter((key) => key !== '_' && key !== '--' && key !== 'h' &&
|
|
58
|
+
.filter((key) => key !== '_' && key !== '--' && key !== 'h' &&
|
|
59
|
+
!this.flags.includes(key) && !this.options.includes(key))
|
|
44
60
|
.map((key) => `${key.length === 1 ? '-' : '--'}${key}`);
|
|
45
61
|
}
|
|
46
62
|
}
|
package/src/dogent.js
CHANGED
|
@@ -14,12 +14,13 @@ const Sources = require('./sources');
|
|
|
14
14
|
const Openai = require('./openai');
|
|
15
15
|
const Oracle = require('./oracle');
|
|
16
16
|
const Usage = require('./usage');
|
|
17
|
+
const prettyMs = require('pretty-ms');
|
|
17
18
|
const version = require('./version');
|
|
18
19
|
const rules = require('./rules');
|
|
19
20
|
|
|
20
21
|
const args = new Args(process.argv.slice(2));
|
|
21
22
|
const sarif = args.sarif();
|
|
22
|
-
const banner = 'Usage: dogent [--sarif] [--offline] <file.md|dir>...';
|
|
23
|
+
const banner = 'Usage: dogent [--sarif] [--offline] [--suppress=RULE,...] <file.md|dir>...';
|
|
23
24
|
if (args.version()) {
|
|
24
25
|
process.stdout.write(`${version}\n`);
|
|
25
26
|
process.exit(0);
|
|
@@ -31,6 +32,7 @@ if (args.help()) {
|
|
|
31
32
|
'Options:\n' +
|
|
32
33
|
' --sarif render the report as SARIF JSON\n' +
|
|
33
34
|
' --offline never call the LLM, even when a token exists\n' +
|
|
35
|
+
' --suppress silence a rule by id; repeat or comma-join to silence many\n' +
|
|
34
36
|
' --version show the version and exit\n' +
|
|
35
37
|
' --help show this help and exit\n'
|
|
36
38
|
);
|
|
@@ -49,20 +51,24 @@ if (paths.length === 0) {
|
|
|
49
51
|
}
|
|
50
52
|
const scanned = new Sources(paths).files();
|
|
51
53
|
scanned.forEach((file) => process.stderr.write(`Scanning ${file}\n`));
|
|
52
|
-
|
|
54
|
+
const checks = rules();
|
|
55
|
+
process.stderr.write(`${scanned.length} files scanned, ${checks.length} rules applied\n`);
|
|
53
56
|
const documents = scanned.map(
|
|
54
57
|
(file) => new Markdown(file, fs.readFileSync(file, 'utf8')).document()
|
|
55
58
|
);
|
|
59
|
+
const started = Date.now();
|
|
60
|
+
const suppressed = args.suppress();
|
|
61
|
+
const allowed = (violation) => !suppressed.includes(violation.rule);
|
|
56
62
|
const found = [];
|
|
57
63
|
documents.forEach((document) => {
|
|
58
|
-
|
|
59
|
-
rule.violations(document).forEach((violation) => found.push(violation));
|
|
64
|
+
checks.forEach((rule) => {
|
|
65
|
+
rule.violations(document).filter(allowed).forEach((violation) => found.push(violation));
|
|
60
66
|
});
|
|
61
67
|
});
|
|
62
68
|
const key = process.env.OPENAI_API_KEY;
|
|
63
69
|
const audit = async (docs) => {
|
|
64
70
|
const oracle = new Oracle(
|
|
65
|
-
|
|
71
|
+
checks,
|
|
66
72
|
new Openai(
|
|
67
73
|
key,
|
|
68
74
|
process.env.OPENAI_MODEL || 'gpt-4o-mini',
|
|
@@ -78,25 +84,29 @@ const audit = async (docs) => {
|
|
|
78
84
|
{extra: [], usage: new Usage('', 0, 0)}
|
|
79
85
|
);
|
|
80
86
|
};
|
|
81
|
-
const finish = (usage) => {
|
|
82
|
-
const report = new Report('dogent', found);
|
|
87
|
+
const finish = (usage, aiMillis) => {
|
|
88
|
+
const report = new Report('dogent', found, Date.now() - started);
|
|
83
89
|
process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
|
|
84
90
|
if (usage !== null) {
|
|
85
|
-
process.stderr.write(`${usage.text()}\n`);
|
|
91
|
+
process.stderr.write(`${usage.text()}, analysed in ${prettyMs(aiMillis)}\n`);
|
|
86
92
|
}
|
|
87
93
|
process.exit(report.count() > 0 ? 1 : 0);
|
|
88
94
|
};
|
|
95
|
+
const verify = async () => {
|
|
96
|
+
const clock = Date.now();
|
|
97
|
+
const result = await audit(documents);
|
|
98
|
+
result.extra.filter(allowed).forEach((violation) => found.push(violation));
|
|
99
|
+
return {usage: result.usage, aiMillis: Date.now() - clock};
|
|
100
|
+
};
|
|
89
101
|
(async () => {
|
|
90
|
-
let
|
|
102
|
+
let outcome = {aiMillis: 0, usage: null};
|
|
91
103
|
if (found.length === 0 && key && !args.offline()) {
|
|
92
104
|
try {
|
|
93
|
-
|
|
94
|
-
result.extra.forEach((violation) => found.push(violation));
|
|
95
|
-
({usage} = result);
|
|
105
|
+
outcome = await verify();
|
|
96
106
|
} catch (error) {
|
|
97
107
|
process.stderr.write(`AI verification failed: ${error.message}\n`);
|
|
98
108
|
process.exit(2);
|
|
99
109
|
}
|
|
100
110
|
}
|
|
101
|
-
finish(usage);
|
|
111
|
+
finish(outcome.usage, outcome.aiMillis);
|
|
102
112
|
})();
|
package/src/report.js
CHANGED
|
@@ -5,24 +5,30 @@
|
|
|
5
5
|
|
|
6
6
|
'use strict';
|
|
7
7
|
|
|
8
|
+
const prettyMs = require('pretty-ms');
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Report.
|
|
10
12
|
*
|
|
11
13
|
* The whole verdict of a run: the tool that produced it and every
|
|
12
14
|
* violation it gathered. Renders itself for humans or as a SARIF log.
|
|
15
|
+
* When handed the analysis duration in milliseconds, the human text
|
|
16
|
+
* closes with a friendly "in 340ms" rendered through pretty-ms.
|
|
13
17
|
*/
|
|
14
18
|
class Report {
|
|
15
|
-
constructor(tool, violations) {
|
|
19
|
+
constructor(tool, violations, millis = null) {
|
|
16
20
|
this.tool = tool;
|
|
17
21
|
this.bag = violations;
|
|
22
|
+
this.millis = millis;
|
|
18
23
|
}
|
|
19
24
|
count() {
|
|
20
25
|
return this.bag.length;
|
|
21
26
|
}
|
|
22
27
|
text() {
|
|
28
|
+
const suffix = this.millis === null ? '' : ` in ${prettyMs(this.millis)}`;
|
|
23
29
|
return this.bag
|
|
24
30
|
.map((violation) => violation.text())
|
|
25
|
-
.concat(`${this.bag.length} problems found`)
|
|
31
|
+
.concat(`${this.bag.length} problems found${suffix}`)
|
|
26
32
|
.join('\n');
|
|
27
33
|
}
|
|
28
34
|
sarif() {
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
* Budget.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a whole manifesto stay short enough to stay readable,
|
|
15
|
+
* holding no more instructions than the cap allows. Counts every prose
|
|
16
|
+
* line plus every bullet item across the file and complains once with a
|
|
17
|
+
* single file-level violation when the total exceeds the cap.
|
|
18
|
+
*
|
|
19
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
20
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
21
|
+
*/
|
|
22
|
+
class Budget {
|
|
23
|
+
constructor(cap) {
|
|
24
|
+
this.id = 'budget';
|
|
25
|
+
this.cap = cap;
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
violations(document) {
|
|
31
|
+
const count = document.walk({
|
|
32
|
+
header: () => [],
|
|
33
|
+
prose: () => [1],
|
|
34
|
+
snippet: () => [],
|
|
35
|
+
bullets: () => [],
|
|
36
|
+
frontmatter: () => []
|
|
37
|
+
}).length;
|
|
38
|
+
if (count <= this.cap) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return [new Violation(
|
|
42
|
+
this.id,
|
|
43
|
+
'error',
|
|
44
|
+
`file holds ${count} instructions, budget ${this.cap}, split the manifesto`,
|
|
45
|
+
new Region(document.uri(), 1, 1)
|
|
46
|
+
)];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = Budget;
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
* Concise.
|
|
13
|
+
*
|
|
14
|
+
* Bounds a manifesto by structure, not only by token volume. Models read
|
|
15
|
+
* the start and end of a long context and skip the middle, so a manifesto
|
|
16
|
+
* that runs past a line budget silently buries its middle instructions.
|
|
17
|
+
* Counts physical lines and warns once the file crosses a configurable
|
|
18
|
+
* ceiling, recommending a split into referenced detail files in the
|
|
19
|
+
* spirit of progressive disclosure. This is distinct from token-count: it
|
|
20
|
+
* measures structure and position risk, not raw token volume. Its prompt
|
|
21
|
+
* hands the deeper split judgement to the AI oracle.
|
|
22
|
+
*/
|
|
23
|
+
class Concise {
|
|
24
|
+
constructor(max) {
|
|
25
|
+
this.id = 'concise';
|
|
26
|
+
this.max = max;
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return `${this.id}: flag a manifesto so long its middle instructions risk being lost, and recommend splitting detail into referenced files`;
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const lines = document.text().split('\n');
|
|
33
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
34
|
+
lines.pop();
|
|
35
|
+
}
|
|
36
|
+
if (lines.length <= this.max) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return [new Violation(
|
|
40
|
+
this.id,
|
|
41
|
+
'warning',
|
|
42
|
+
`file too long (${lines.length} lines), split detail into referenced files`,
|
|
43
|
+
new Region(document.uri(), this.max + 1, 1)
|
|
44
|
+
)];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = Concise;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CounterExample.
|
|
14
|
+
*
|
|
15
|
+
* Rejects "bad example" demonstrations that show the wrong form, since
|
|
16
|
+
* displaying a mistake can reinforce it. A standalone checker flags a
|
|
17
|
+
* line that opens a counterexample with an introducer phrase ("bad
|
|
18
|
+
* example", "wrong example", "for example, do not", "instead of
|
|
19
|
+
* writing", "avoid writing") and then carries a quoted or backticked
|
|
20
|
+
* sample of the wrong form. Its prompt hands subtler cases to the AI
|
|
21
|
+
* oracle, which judges whether an example shows the correct or the
|
|
22
|
+
* incorrect behavior.
|
|
23
|
+
*/
|
|
24
|
+
class CounterExample {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.id = 'counter-example';
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return `${this.id}: judge whether each example shows the correct behavior, and flag any example that demonstrates the incorrect form`;
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
return document.walk({
|
|
34
|
+
header: () => [],
|
|
35
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
36
|
+
snippet: () => [],
|
|
37
|
+
bullets: () => [],
|
|
38
|
+
frontmatter: () => []
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
scan(text, line, uri) {
|
|
42
|
+
const regex = /bad example|wrong example|for example, do not|instead of writing|avoid writing/iu;
|
|
43
|
+
const hit = regex.exec(mask(text));
|
|
44
|
+
if (hit === null) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const tail = text.slice(hit.index + hit[0].length);
|
|
48
|
+
if (!/["'`]/u.test(tail)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return [new Violation(
|
|
52
|
+
this.id,
|
|
53
|
+
'warning',
|
|
54
|
+
'counterexample may reinforce the wrong behavior, show the right form',
|
|
55
|
+
new Region(uri, line, hit.index + 1)
|
|
56
|
+
)];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = CounterExample;
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
* Done.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md state a verifiable completion check, symmetric
|
|
15
|
+
* to the description trigger requirement. A standalone checker can only
|
|
16
|
+
* approximate: it scans headings and prose for a verification signal. Its
|
|
17
|
+
* prompt hands the deeper judgement to the AI oracle, which weighs whether
|
|
18
|
+
* the stated check is truly pass/fail testable rather than vague.
|
|
19
|
+
*/
|
|
20
|
+
class Done {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.id = 'done';
|
|
23
|
+
}
|
|
24
|
+
prompt() {
|
|
25
|
+
return `${this.id}: in a SKILL.md, judge whether the stated completion check is actually pass/fail testable rather than a vague gesture toward being finished`;
|
|
26
|
+
}
|
|
27
|
+
violations(document) {
|
|
28
|
+
const uri = document.uri();
|
|
29
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const signals = document.walk({
|
|
33
|
+
header: (text) => [/\b(?:verify|done|check|validation|acceptance)\b/iu.test(text)],
|
|
34
|
+
prose: (text) => [/\b(?:confirm|assert|verify|the test passes|tests pass|exit code|pass\/fail)\b/iu.test(text)],
|
|
35
|
+
snippet: () => [],
|
|
36
|
+
bullets: () => [],
|
|
37
|
+
frontmatter: () => []
|
|
38
|
+
});
|
|
39
|
+
if (signals.some((signal) => signal)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
return [
|
|
43
|
+
new Violation(
|
|
44
|
+
this.id,
|
|
45
|
+
'warning',
|
|
46
|
+
'SKILL.md never says how to verify completion',
|
|
47
|
+
new Region(uri, 1, 1)
|
|
48
|
+
)
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = Done;
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Emphasis.
|
|
14
|
+
*
|
|
15
|
+
* Flags shouting that tries to force compliance through volume rather
|
|
16
|
+
* than clarity: a curated all-caps word like "IMPORTANT" or "NEVER", a
|
|
17
|
+
* run of two or more consecutive all-caps words, and repeated marks like
|
|
18
|
+
* "!!" or "!?". The model gains nothing from volume, so the emphasis is
|
|
19
|
+
* pure noise. A lone short acronym such as "JSON" or "AI" is left alone.
|
|
20
|
+
* Its prompt hands the borderline emphasis and reward framing the
|
|
21
|
+
* patterns miss to the AI oracle.
|
|
22
|
+
*/
|
|
23
|
+
class Emphasis {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'emphasis';
|
|
26
|
+
this.shout = new Set(['IMPORTANT', 'ALWAYS', 'NEVER', 'MUST', 'CRITICAL', 'REQUIRED']);
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return `${this.id}: flag emphatic shouting the patterns miss, including borderline all-caps and reward framing, since emphasis adds no instruction`;
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
return document.walk({
|
|
34
|
+
header: () => [],
|
|
35
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
36
|
+
snippet: () => [],
|
|
37
|
+
bullets: () => [],
|
|
38
|
+
frontmatter: () => []
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
scan(text, line, uri) {
|
|
42
|
+
const masked = mask(text);
|
|
43
|
+
return this.punctuation(masked, line, uri).concat(this.shouting(masked, line, uri));
|
|
44
|
+
}
|
|
45
|
+
punctuation(masked, line, uri) {
|
|
46
|
+
const found = [];
|
|
47
|
+
const regex = /!{2,}|!\?|\?!/gu;
|
|
48
|
+
let hit = regex.exec(masked);
|
|
49
|
+
while (hit !== null) {
|
|
50
|
+
found.push(this.flag(hit[0], line, hit.index, uri));
|
|
51
|
+
hit = regex.exec(masked);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
shouting(masked, line, uri) {
|
|
56
|
+
const found = [];
|
|
57
|
+
const regex = /[A-Z]{2,}(?:\s+[A-Z]{2,})*/gu;
|
|
58
|
+
let hit = regex.exec(masked);
|
|
59
|
+
while (hit !== null) {
|
|
60
|
+
const tokens = hit[0].split(/\s+/u);
|
|
61
|
+
const loud = tokens.length > 1
|
|
62
|
+
? tokens.some((token) => token.length >= 5 || this.shout.has(token))
|
|
63
|
+
: this.shout.has(tokens[0]);
|
|
64
|
+
if (loud) {
|
|
65
|
+
found.push(this.flag(hit[0], line, hit.index, uri));
|
|
66
|
+
}
|
|
67
|
+
hit = regex.exec(masked);
|
|
68
|
+
}
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
flag(marker, line, index, uri) {
|
|
72
|
+
return new Violation(
|
|
73
|
+
this.id,
|
|
74
|
+
'warning',
|
|
75
|
+
`emphasis marker "${marker}" adds no instruction, state it plainly`,
|
|
76
|
+
new Region(uri, line, index + 1)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = Emphasis;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
* Example.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md demonstrate, not only describe. A skill that
|
|
15
|
+
* states rules in prose alone leaves the agent to infer the exact shape
|
|
16
|
+
* of correct output, while a single worked example is one of the most
|
|
17
|
+
* reliable levers in prompt engineering. A standalone checker passes the
|
|
18
|
+
* skill that carries at least one fenced code block or an explicit
|
|
19
|
+
* "Example" section heading, and flags the one that has neither. Its
|
|
20
|
+
* prompt hands the deeper judgement to the AI oracle, which weighs
|
|
21
|
+
* whether a present code block is truly illustrative.
|
|
22
|
+
*/
|
|
23
|
+
class Example {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'example';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return `${this.id}: in a SKILL.md, judge whether a present code block is a genuine worked example rather than a stray snippet, and flag a skill that only describes without demonstrating`;
|
|
29
|
+
}
|
|
30
|
+
violations(document) {
|
|
31
|
+
const uri = document.uri();
|
|
32
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const hints = document.walk({
|
|
36
|
+
header: (text) => this.heading(text),
|
|
37
|
+
prose: () => [],
|
|
38
|
+
snippet: () => ['snippet'],
|
|
39
|
+
bullets: () => [],
|
|
40
|
+
frontmatter: () => []
|
|
41
|
+
});
|
|
42
|
+
if (hints.length > 0) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return [new Violation(
|
|
46
|
+
this.id,
|
|
47
|
+
'warning',
|
|
48
|
+
'SKILL.md has no example, add a worked input/output sample',
|
|
49
|
+
new Region(uri, 1, 1)
|
|
50
|
+
)];
|
|
51
|
+
}
|
|
52
|
+
heading(text) {
|
|
53
|
+
if (/^#{1,6}\s+examples?\b/iu.test(text)) {
|
|
54
|
+
return [this.id];
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = Example;
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
* Format.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md which produces output pin down that output's
|
|
15
|
+
* shape. Structured-output generation grows far more reliable when the
|
|
16
|
+
* expected format is declared and shown, while leaving it implicit
|
|
17
|
+
* produces brittle, drifting output. A standalone checker flags a skill
|
|
18
|
+
* whose instructions describe producing output (verbs like "produce",
|
|
19
|
+
* "output", "return", "generate", "write", "emit") yet no section or
|
|
20
|
+
* snippet declares the output shape. This is distinct from the example
|
|
21
|
+
* rule: an example shows one case, a format spec defines the contract.
|
|
22
|
+
* Its prompt asks the AI oracle whether the declared format is concrete
|
|
23
|
+
* enough to be machine-checkable.
|
|
24
|
+
*/
|
|
25
|
+
class Format {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'format';
|
|
28
|
+
}
|
|
29
|
+
prompt() {
|
|
30
|
+
return `${this.id}: in a SKILL.md, judge whether the declared output format is concrete and machine-checkable, and flag a generating skill that pins down no format`;
|
|
31
|
+
}
|
|
32
|
+
violations(document) {
|
|
33
|
+
const uri = document.uri();
|
|
34
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const heading = /^#{1,6}\s+.*\b(?:format|schema|structure|output)\b/iu;
|
|
38
|
+
const verb = /\b(?:produces?|outputs?|returns?|generates?|writes?|emits?)\b/iu;
|
|
39
|
+
const signals = document.walk({
|
|
40
|
+
header: (text) => {
|
|
41
|
+
if (heading.test(text)) {
|
|
42
|
+
return ['declared'];
|
|
43
|
+
}
|
|
44
|
+
return [];
|
|
45
|
+
},
|
|
46
|
+
prose: (text) => {
|
|
47
|
+
if (verb.test(text)) {
|
|
48
|
+
return ['generates'];
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
},
|
|
52
|
+
snippet: () => ['declared'],
|
|
53
|
+
bullets: () => [],
|
|
54
|
+
frontmatter: () => []
|
|
55
|
+
});
|
|
56
|
+
if (!signals.includes('generates') || signals.includes('declared')) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
return [new Violation(
|
|
60
|
+
this.id,
|
|
61
|
+
'warning',
|
|
62
|
+
'SKILL.md generates output but never declares its format',
|
|
63
|
+
new Region(uri, 1, 1)
|
|
64
|
+
)];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = Format;
|