@yegor256/dogent 0.9.1 → 0.11.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 +104 -12
- package/package.json +3 -2
- package/src/args.js +35 -4
- package/src/defaults.js +47 -0
- package/src/dogent.js +42 -16
- package/src/openai.js +8 -5
- package/src/prompt.js +0 -4
- package/src/report.js +8 -2
- package/src/rules/ambiguous-or.js +58 -0
- package/src/rules/budget.js +50 -0
- package/src/rules/concise.js +48 -0
- package/src/rules/conditional.js +55 -0
- package/src/rules/consistent.js +1 -1
- package/src/rules/counter-example.js +60 -0
- package/src/rules/default.js +60 -0
- package/src/rules/description-length.js +64 -0
- package/src/rules/description-voice.js +67 -0
- package/src/rules/done.js +53 -0
- package/src/rules/duplicate-section.js +65 -0
- package/src/rules/emoji.js +60 -0
- package/src/rules/emphasis.js +81 -0
- package/src/rules/example-format.js +32 -0
- package/src/rules/example.js +60 -0
- package/src/rules/external-link.js +57 -0
- package/src/rules/fence-language.js +55 -0
- package/src/rules/format.js +68 -0
- package/src/rules/hidden-char.js +61 -0
- package/src/rules/homoglyph.js +82 -0
- package/src/rules/index.js +80 -0
- package/src/rules/inline-code.js +79 -0
- package/src/rules/jargon.js +115 -0
- package/src/rules/meta-reference.js +57 -0
- package/src/rules/ordered.js +57 -0
- package/src/rules/persona.js +55 -0
- package/src/rules/placement.js +62 -0
- package/src/rules/positive.js +57 -0
- package/src/rules/pseudo-heading.js +55 -0
- package/src/rules/quantifier.js +63 -0
- package/src/rules/rationale.js +54 -0
- package/src/rules/referential.js +67 -0
- package/src/rules/scope.js +31 -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/transition.js +59 -0
- package/src/rules/units.js +81 -0
- package/src/rules/untrusted.js +59 -0
- package/src/rules/vague.js +63 -0
- package/src/rules/weak-verb.js +62 -0
- package/src/version.js +2 -2
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
* Conditional.
|
|
14
|
+
*
|
|
15
|
+
* Demands that branching never collapse onto one line. A line carrying
|
|
16
|
+
* more than one condition keyword (if, unless, when, else, otherwise)
|
|
17
|
+
* spells out a whole branch tree at once, so each case must split into
|
|
18
|
+
* its own command. Distinct from simple, which weighs clause depth, and
|
|
19
|
+
* from atomic, which counts instructions; this one targets branching
|
|
20
|
+
* alone. A lone guard keeps just one keyword and stays clean.
|
|
21
|
+
*/
|
|
22
|
+
class Conditional {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'conditional';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return `${this.id}: flag implicit branching that carries no keyword, and split each case into its own command`;
|
|
28
|
+
}
|
|
29
|
+
violations(document) {
|
|
30
|
+
const uri = document.uri();
|
|
31
|
+
return document.walk({
|
|
32
|
+
header: () => [],
|
|
33
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
34
|
+
snippet: () => [],
|
|
35
|
+
bullets: () => [],
|
|
36
|
+
frontmatter: () => []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
judge(text, line, uri) {
|
|
40
|
+
const clean = mask(text);
|
|
41
|
+
const hits = clean.match(/\b(?:if|unless|when|else|otherwise)\b/giu);
|
|
42
|
+
if (hits === null || hits.length < 2) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const column = clean.search(/\b(?:if|unless|when|else|otherwise)\b/iu);
|
|
46
|
+
return [new Violation(
|
|
47
|
+
this.id,
|
|
48
|
+
'warning',
|
|
49
|
+
'multi-branch conditional, split each case into its own command',
|
|
50
|
+
new Region(uri, line, column + 1)
|
|
51
|
+
)];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = Conditional;
|
package/src/rules/consistent.js
CHANGED
|
@@ -20,7 +20,7 @@ class Consistent {
|
|
|
20
20
|
this.id = 'consistent';
|
|
21
21
|
}
|
|
22
22
|
prompt() {
|
|
23
|
-
return `${this.id}: flag an instruction that repeats another instruction word for word, or that
|
|
23
|
+
return `${this.id}: flag an instruction that repeats another instruction word for word, or that logically contradicts another instruction about the very same subject, where one line orders exactly what another forbids; ignore lines that merely share a theme but govern different concerns, since complementary instructions never clash`;
|
|
24
24
|
}
|
|
25
25
|
violations() {
|
|
26
26
|
return [];
|
|
@@ -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,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
|
+
* Default.
|
|
14
|
+
*
|
|
15
|
+
* Demands that optional behavior names its default. A line marking work
|
|
16
|
+
* as optional through "optionally", "you may", or "feel free to" leaves
|
|
17
|
+
* the agent guessing what happens when it declines, so the line must
|
|
18
|
+
* state a default. A line that already declares one through "by
|
|
19
|
+
* default", "defaults to", or "otherwise" passes untouched. Its prompt
|
|
20
|
+
* hands subtler optionality with no stated default to the AI oracle.
|
|
21
|
+
*/
|
|
22
|
+
class Default {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'default';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return `${this.id}: flag optionality that names no default even without a listed marker, and state the default`;
|
|
28
|
+
}
|
|
29
|
+
violations(document) {
|
|
30
|
+
const uri = document.uri();
|
|
31
|
+
return document.walk({
|
|
32
|
+
header: () => [],
|
|
33
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
34
|
+
snippet: () => [],
|
|
35
|
+
bullets: () => [],
|
|
36
|
+
frontmatter: () => []
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
scan(text, line, uri) {
|
|
40
|
+
const masked = mask(text);
|
|
41
|
+
if ((/\b(?:by default|default to|defaults to|otherwise)\b/iu).test(masked)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
const found = [];
|
|
45
|
+
const regex = /\b(?:optionally|you may|you can|if you want|feel free to|as an option)\b/giu;
|
|
46
|
+
let hit = regex.exec(masked);
|
|
47
|
+
while (hit !== null) {
|
|
48
|
+
found.push(new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
`optional behavior "${hit[0]}" has no default, state it`,
|
|
52
|
+
new Region(uri, line, hit.index + 1)
|
|
53
|
+
));
|
|
54
|
+
hit = regex.exec(masked);
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = Default;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
* DescriptionLength.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md description stay within a sane size. The
|
|
15
|
+
* loader keeps every description in context, so an overgrown one wastes
|
|
16
|
+
* the budget that the instructions need. Flags a value longer than the
|
|
17
|
+
* ceiling and a value that is empty, leaving the wording itself to
|
|
18
|
+
* sibling rules.
|
|
19
|
+
*
|
|
20
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
21
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
22
|
+
*/
|
|
23
|
+
class DescriptionLength {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'description-length';
|
|
26
|
+
this.ceiling = 1024;
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const pairs = document.walk({
|
|
37
|
+
header: () => [],
|
|
38
|
+
prose: () => [],
|
|
39
|
+
snippet: () => [],
|
|
40
|
+
bullets: () => [],
|
|
41
|
+
frontmatter: (keys) => keys
|
|
42
|
+
});
|
|
43
|
+
const found = pairs.filter((pair) => pair.key === 'description');
|
|
44
|
+
if (found.length === 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return this.judge(found[0], uri);
|
|
48
|
+
}
|
|
49
|
+
judge(pair, uri) {
|
|
50
|
+
const {value} = pair;
|
|
51
|
+
if (value.trim() === '') {
|
|
52
|
+
return [this.flag('description is empty, write a concise capability statement', pair.row, uri)];
|
|
53
|
+
}
|
|
54
|
+
if (value.length > this.ceiling) {
|
|
55
|
+
return [this.flag(`description is ${value.length} chars, keep it under ${this.ceiling}`, pair.row, uri)];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
flag(message, row, uri) {
|
|
60
|
+
return new Violation(this.id, 'warning', message, new Region(uri, row, 1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = DescriptionLength;
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
* DescriptionVoice.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md description stay in the third person, reading
|
|
15
|
+
* as a capability statement like "Extracts tables ..." rather than a
|
|
16
|
+
* first- or second-person sentence like "I extract ..." or "You can
|
|
17
|
+
* use ...". A standalone checker flags first- and second-person
|
|
18
|
+
* pronouns as whole words, after dropping the trigger clause that opens
|
|
19
|
+
* with "Use when" so a legitimate "Use when ..." phrase stays clean.
|
|
20
|
+
* Distinct from description-triggers, which checks that a "when" clause
|
|
21
|
+
* exists, and from description-length, which checks the size; this one
|
|
22
|
+
* checks the grammatical voice. Its prompt hands subtler voice
|
|
23
|
+
* judgement to the AI oracle.
|
|
24
|
+
*/
|
|
25
|
+
class DescriptionVoice {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'description-voice';
|
|
28
|
+
this.pronoun = /\b(?:I|we|you|your|my|our)\b/giu;
|
|
29
|
+
}
|
|
30
|
+
prompt() {
|
|
31
|
+
return `${this.id}: in a SKILL.md, flag a description written in first or second person and demand a third-person capability statement`;
|
|
32
|
+
}
|
|
33
|
+
violations(document) {
|
|
34
|
+
const uri = document.uri();
|
|
35
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const pairs = document.walk({
|
|
39
|
+
header: () => [],
|
|
40
|
+
prose: () => [],
|
|
41
|
+
snippet: () => [],
|
|
42
|
+
bullets: () => [],
|
|
43
|
+
frontmatter: (keys) => keys
|
|
44
|
+
});
|
|
45
|
+
const found = pairs.filter((pair) => pair.key === 'description');
|
|
46
|
+
if (found.length === 0) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
return this.judge(found[0], uri);
|
|
50
|
+
}
|
|
51
|
+
judge(pair, uri) {
|
|
52
|
+
const text = pair.value.replace(/use when.*$/isu, '');
|
|
53
|
+
this.pronoun.lastIndex = 0;
|
|
54
|
+
const hit = this.pronoun.exec(text);
|
|
55
|
+
if (hit === null) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
return [new Violation(
|
|
59
|
+
this.id,
|
|
60
|
+
'warning',
|
|
61
|
+
`description must be third person, not "${hit[0]}"`,
|
|
62
|
+
new Region(uri, pair.row, 1)
|
|
63
|
+
)];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = DescriptionVoice;
|
|
@@ -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,65 @@
|
|
|
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 bare = (text) => text.replace(/^#{1,6}\s*/u, '').trim();
|
|
12
|
+
|
|
13
|
+
const normalize = (text) => bare(text).toLowerCase().replace(/\s+/gu, ' ');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* DuplicateSection.
|
|
17
|
+
*
|
|
18
|
+
* Rejects two headings that carry the same name, so each section owns
|
|
19
|
+
* a distinct title. It collects every heading in order, normalizes it
|
|
20
|
+
* by case and whitespace, then flags the second and any later twin
|
|
21
|
+
* while leaving the first occurrence clean. Distinct from unique,
|
|
22
|
+
* which targets repeated prose instructions, and from short-sections,
|
|
23
|
+
* which targets heading length; this one targets repeated heading
|
|
24
|
+
* names. Its prompt stays empty since the check is fully
|
|
25
|
+
* deterministic.
|
|
26
|
+
*/
|
|
27
|
+
class DuplicateSection {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.id = 'duplicate-section';
|
|
30
|
+
}
|
|
31
|
+
prompt() {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
violations(document) {
|
|
35
|
+
const uri = document.uri();
|
|
36
|
+
const headers = document.walk({
|
|
37
|
+
header: (text, row) => [{text, row}],
|
|
38
|
+
prose: () => [],
|
|
39
|
+
snippet: () => [],
|
|
40
|
+
bullets: () => [],
|
|
41
|
+
frontmatter: () => []
|
|
42
|
+
});
|
|
43
|
+
return this.repeats(uri, headers);
|
|
44
|
+
}
|
|
45
|
+
repeats(uri, headers) {
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const found = [];
|
|
48
|
+
headers.forEach((header) => {
|
|
49
|
+
const norm = normalize(header.text);
|
|
50
|
+
if (seen.has(norm)) {
|
|
51
|
+
found.push(new Violation(
|
|
52
|
+
this.id,
|
|
53
|
+
'warning',
|
|
54
|
+
`duplicate section "${bare(header.text)}", give each section a distinct name`,
|
|
55
|
+
new Region(uri, header.row, 1)
|
|
56
|
+
));
|
|
57
|
+
} else {
|
|
58
|
+
seen.add(norm);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = DuplicateSection;
|
|
@@ -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
|
+
* Emoji.
|
|
14
|
+
*
|
|
15
|
+
* Flags any emoji or decorative pictographic symbol that adds token
|
|
16
|
+
* noise without instruction. Inline code is masked first, so a fenced
|
|
17
|
+
* or inline example may keep a needed glyph. Distinct from homoglyph,
|
|
18
|
+
* which targets letters borrowed from other scripts; this one stays to
|
|
19
|
+
* pictographs, symbols, and dingbats only and never flags a foreign
|
|
20
|
+
* letter.
|
|
21
|
+
*
|
|
22
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
23
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
24
|
+
*/
|
|
25
|
+
class Emoji {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'emoji';
|
|
28
|
+
this.glyph = /[\p{Extended_Pictographic}\u{2190}-\u{21FF}\u{2300}-\u{27BF}\u{2B00}-\u{2BFF}]/gu;
|
|
29
|
+
}
|
|
30
|
+
prompt() {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
violations(document) {
|
|
34
|
+
const uri = document.uri();
|
|
35
|
+
return document.walk({
|
|
36
|
+
header: (text, line) => this.scan(text, line, uri),
|
|
37
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
38
|
+
snippet: () => [],
|
|
39
|
+
bullets: () => [],
|
|
40
|
+
frontmatter: () => []
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
scan(text, line, uri) {
|
|
44
|
+
const masked = mask(text);
|
|
45
|
+
const result = [];
|
|
46
|
+
let hit = this.glyph.exec(masked);
|
|
47
|
+
while (hit !== null) {
|
|
48
|
+
result.push(new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
`decorative character "${hit[0]}" adds token noise, use plain text`,
|
|
52
|
+
new Region(uri, line, hit.index + 1)
|
|
53
|
+
));
|
|
54
|
+
hit = this.glyph.exec(masked);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = Emoji;
|
|
@@ -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,32 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Example format.
|
|
10
|
+
*
|
|
11
|
+
* A few-shot demonstration regulates the shape of the output more
|
|
12
|
+
* strongly than any prose, so an example that disagrees with the
|
|
13
|
+
* declared format teaches the agent the wrong shape. This rule ties the
|
|
14
|
+
* `example` and `format` rules together by checking their consistency:
|
|
15
|
+
* when one SKILL.md both shows an example and declares an output format,
|
|
16
|
+
* the two must agree. The mismatch hides between two distant fragments,
|
|
17
|
+
* so this check is pure judgement: prompt() hands the comparison to the
|
|
18
|
+
* AI oracle and violations() finds nothing on its own.
|
|
19
|
+
*/
|
|
20
|
+
class ExampleFormat {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.id = 'example-format';
|
|
23
|
+
}
|
|
24
|
+
prompt() {
|
|
25
|
+
return `${this.id}: in a SKILL.md that both shows an example and declares an output format, judge whether the example conforms to the declared format and flag any mismatch`;
|
|
26
|
+
}
|
|
27
|
+
violations() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = ExampleFormat;
|
|
@@ -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;
|