@yegor256/dogent 0.10.0 → 0.12.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 +135 -15
- package/package.json +1 -1
- package/src/args.js +24 -4
- package/src/defaults.js +47 -0
- package/src/dogent.js +65 -18
- package/src/openai.js +8 -5
- package/src/prompt.js +0 -4
- package/src/report.js +26 -3
- package/src/rules/ambiguous-or.js +61 -0
- package/src/rules/atomic.js +3 -0
- package/src/rules/budget.js +3 -0
- package/src/rules/command.js +3 -0
- package/src/rules/concise.js +3 -0
- package/src/rules/conditional.js +58 -0
- package/src/rules/consistent.js +4 -1
- package/src/rules/counter-example.js +3 -0
- package/src/rules/crowded.js +3 -0
- package/src/rules/dead-import.js +3 -0
- package/src/rules/default.js +63 -0
- package/src/rules/description-length.js +67 -0
- package/src/rules/description-triggers.js +3 -0
- package/src/rules/description-voice.js +70 -0
- package/src/rules/done.js +3 -0
- package/src/rules/duplicate-section.js +68 -0
- package/src/rules/emoji.js +63 -0
- package/src/rules/emphasis.js +3 -0
- package/src/rules/empty.js +3 -0
- package/src/rules/example-format.js +35 -0
- package/src/rules/example.js +3 -0
- package/src/rules/external-link.js +60 -0
- package/src/rules/fence-language.js +58 -0
- package/src/rules/format.js +3 -0
- package/src/rules/frontmatter.js +3 -0
- package/src/rules/grouped.js +3 -0
- package/src/rules/hedging.js +3 -0
- package/src/rules/hidden-char.js +64 -0
- package/src/rules/homoglyph.js +85 -0
- package/src/rules/index.js +40 -0
- package/src/rules/inline-code.js +82 -0
- package/src/rules/jargon.js +17 -4
- package/src/rules/line-length.js +3 -0
- package/src/rules/meta-reference.js +60 -0
- package/src/rules/name-format.js +3 -0
- package/src/rules/name-matches-dir.js +3 -0
- package/src/rules/no-articles.js +3 -0
- package/src/rules/ordered.js +3 -0
- package/src/rules/passive.js +3 -0
- package/src/rules/persona.js +3 -0
- package/src/rules/placement.js +65 -0
- package/src/rules/polite.js +3 -0
- package/src/rules/positive.js +3 -0
- package/src/rules/pseudo-heading.js +3 -0
- package/src/rules/punctuation.js +3 -0
- package/src/rules/quantifier.js +66 -0
- package/src/rules/rationale.js +3 -0
- package/src/rules/redundant.js +3 -0
- package/src/rules/referential.js +3 -0
- package/src/rules/scope.js +34 -0
- package/src/rules/section-level.js +3 -0
- package/src/rules/self-contained.js +3 -0
- package/src/rules/short-sections.js +3 -0
- package/src/rules/simple.js +3 -0
- package/src/rules/stale.js +3 -0
- package/src/rules/terms.js +3 -0
- package/src/rules/token-count.js +3 -0
- package/src/rules/tool-clarity.js +3 -0
- package/src/rules/transition.js +62 -0
- package/src/rules/unfinished.js +3 -0
- package/src/rules/unique.js +3 -0
- package/src/rules/units.js +84 -0
- package/src/rules/untrusted.js +3 -0
- package/src/rules/vague.js +3 -0
- package/src/rules/weak-verb.js +65 -0
- package/src/sources.js +3 -0
- package/src/version.js +2 -2
package/src/rules/hedging.js
CHANGED
|
@@ -22,6 +22,9 @@ class Hedging {
|
|
|
22
22
|
constructor() {
|
|
23
23
|
this.id = 'hedging';
|
|
24
24
|
}
|
|
25
|
+
hint() {
|
|
26
|
+
return 'Remove hedging words such as should, just, or usually and state the order firmly, since timid wording weakens the command.';
|
|
27
|
+
}
|
|
25
28
|
prompt() {
|
|
26
29
|
return `${this.id}: flag soft, non-committal, or hedging wording, including conditional escape hatches and vague scope that carry no fixed hedge word`;
|
|
27
30
|
}
|
|
@@ -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
|
+
* HiddenChar.
|
|
13
|
+
*
|
|
14
|
+
* Demands that every line carry only visible characters, rejecting any
|
|
15
|
+
* invisible or control codepoint that hides inside the text. Scans every
|
|
16
|
+
* fragment, including snippets, because a zero-width space, a bidirectional
|
|
17
|
+
* override, or a variation selector tucked into code is just as dangerous as
|
|
18
|
+
* one tucked into prose. Flags zero-width characters, bidi controls, and
|
|
19
|
+
* variation selectors, naming each by its hex codepoint so it can be deleted.
|
|
20
|
+
*
|
|
21
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
22
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
23
|
+
*/
|
|
24
|
+
class HiddenChar {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.id = 'hidden-char';
|
|
27
|
+
this.hidden = /[\u200B-\u200D\uFEFF\u202A-\u202E\u2066-\u2069\uFE00-\uFE0F\u{E0100}-\u{E01EF}]/gu;
|
|
28
|
+
}
|
|
29
|
+
hint() {
|
|
30
|
+
return 'Delete the invisible or control character named by its codepoint, since hidden characters can corrupt parsing or smuggle instructions.';
|
|
31
|
+
}
|
|
32
|
+
prompt() {
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
violations(document) {
|
|
36
|
+
const uri = document.uri();
|
|
37
|
+
return document.walk({
|
|
38
|
+
header: (text, line) => this.scan(text, line, uri),
|
|
39
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
40
|
+
snippet: (text, line) => this.scan(text, line, uri),
|
|
41
|
+
bullets: () => [],
|
|
42
|
+
frontmatter: () => []
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
scan(text, line, uri) {
|
|
46
|
+
const found = [];
|
|
47
|
+
this.hidden.lastIndex = 0;
|
|
48
|
+
let hit = this.hidden.exec(text);
|
|
49
|
+
while (hit !== null) {
|
|
50
|
+
const hex = hit[0].codePointAt(0).toString(16).toUpperCase();
|
|
51
|
+
const code = hex.padStart(4, '0');
|
|
52
|
+
found.push(new Violation(
|
|
53
|
+
this.id,
|
|
54
|
+
'error',
|
|
55
|
+
`invisible character U+${code} found, delete it`,
|
|
56
|
+
new Region(uri, line, hit.index + 1)
|
|
57
|
+
));
|
|
58
|
+
hit = this.hidden.exec(text);
|
|
59
|
+
}
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = HiddenChar;
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
* Homoglyph.
|
|
14
|
+
*
|
|
15
|
+
* Rejects mixed-script look-alike characters that masquerade as plain
|
|
16
|
+
* ASCII. A token mixing an ASCII Latin letter with a confusable from
|
|
17
|
+
* Cyrillic, Greek, or full-width Latin reads as one word yet hides a
|
|
18
|
+
* foreign codepoint, so it slips past humans while breaking tools. The
|
|
19
|
+
* check flags every such confusable character at its own column. Inline
|
|
20
|
+
* code is masked first, so a deliberately quoted example stays clean.
|
|
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 Homoglyph {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'homoglyph';
|
|
28
|
+
this.latin = /[A-Za-z]/u;
|
|
29
|
+
this.confusable = /[Ѐ-ӿͰ-Ͽ-]/u;
|
|
30
|
+
}
|
|
31
|
+
hint() {
|
|
32
|
+
return 'Replace the mixed-script look-alike character with its plain ASCII equivalent, since a foreign codepoint hidden inside a word breaks tooling.';
|
|
33
|
+
}
|
|
34
|
+
prompt() {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
violations(document) {
|
|
38
|
+
const uri = document.uri();
|
|
39
|
+
return document.walk({
|
|
40
|
+
header: (text, line) => this.scan(text, line, uri),
|
|
41
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
42
|
+
snippet: () => [],
|
|
43
|
+
bullets: () => [],
|
|
44
|
+
frontmatter: () => []
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
scan(text, line, uri) {
|
|
48
|
+
const clean = mask(text);
|
|
49
|
+
const result = [];
|
|
50
|
+
const token = /\S+/gu;
|
|
51
|
+
let match = token.exec(clean);
|
|
52
|
+
while (match !== null) {
|
|
53
|
+
const [word] = match;
|
|
54
|
+
if (this.latin.test(word) && this.confusable.test(word)) {
|
|
55
|
+
this.flag(word, match.index).forEach((spot) => {
|
|
56
|
+
result.push(new Violation(
|
|
57
|
+
this.id,
|
|
58
|
+
'error',
|
|
59
|
+
`mixed-script character "${spot.char}" (U+${spot.point}) found, use plain ASCII`,
|
|
60
|
+
new Region(uri, line, spot.column)
|
|
61
|
+
));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
match = token.exec(clean);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
flag(word, start) {
|
|
69
|
+
const spots = [];
|
|
70
|
+
[...word].forEach((char, offset) => {
|
|
71
|
+
if (!this.confusable.test(char)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const point = char
|
|
75
|
+
.codePointAt(0)
|
|
76
|
+
.toString(16)
|
|
77
|
+
.toUpperCase()
|
|
78
|
+
.padStart(4, '0');
|
|
79
|
+
spots.push({char, point, column: start + offset + 1});
|
|
80
|
+
});
|
|
81
|
+
return spots;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = Homoglyph;
|
package/src/rules/index.js
CHANGED
|
@@ -49,6 +49,26 @@ const ToolClarity = require('./tool-clarity');
|
|
|
49
49
|
const CounterExample = require('./counter-example');
|
|
50
50
|
const Rationale = require('./rationale');
|
|
51
51
|
const SelfContained = require('./self-contained');
|
|
52
|
+
const Quantifier = require('./quantifier');
|
|
53
|
+
const WeakVerb = require('./weak-verb');
|
|
54
|
+
const Default = require('./default');
|
|
55
|
+
const MetaReference = require('./meta-reference');
|
|
56
|
+
const AmbiguousOr = require('./ambiguous-or');
|
|
57
|
+
const ExternalLink = require('./external-link');
|
|
58
|
+
const Conditional = require('./conditional');
|
|
59
|
+
const Transition = require('./transition');
|
|
60
|
+
const Placement = require('./placement');
|
|
61
|
+
const InlineCode = require('./inline-code');
|
|
62
|
+
const Emoji = require('./emoji');
|
|
63
|
+
const Homoglyph = require('./homoglyph');
|
|
64
|
+
const DuplicateSection = require('./duplicate-section');
|
|
65
|
+
const DescriptionVoice = require('./description-voice');
|
|
66
|
+
const ExampleFormat = require('./example-format');
|
|
67
|
+
const DescriptionLength = require('./description-length');
|
|
68
|
+
const Scope = require('./scope');
|
|
69
|
+
const HiddenChar = require('./hidden-char');
|
|
70
|
+
const Units = require('./units');
|
|
71
|
+
const FenceLanguage = require('./fence-language');
|
|
52
72
|
|
|
53
73
|
module.exports = () => [
|
|
54
74
|
new Grouped(),
|
|
@@ -92,6 +112,26 @@ module.exports = () => [
|
|
|
92
112
|
new CounterExample(),
|
|
93
113
|
new Rationale(),
|
|
94
114
|
new SelfContained(),
|
|
115
|
+
new Quantifier(),
|
|
116
|
+
new WeakVerb(),
|
|
117
|
+
new Default(),
|
|
118
|
+
new MetaReference(),
|
|
119
|
+
new AmbiguousOr(),
|
|
120
|
+
new ExternalLink(),
|
|
121
|
+
new Conditional(),
|
|
122
|
+
new Transition(),
|
|
123
|
+
new Placement(),
|
|
124
|
+
new InlineCode(),
|
|
125
|
+
new Emoji(),
|
|
126
|
+
new Homoglyph(),
|
|
127
|
+
new DuplicateSection(),
|
|
128
|
+
new DescriptionVoice(),
|
|
129
|
+
new ExampleFormat(),
|
|
130
|
+
new DescriptionLength(),
|
|
131
|
+
new Scope(),
|
|
132
|
+
new HiddenChar(),
|
|
133
|
+
new Units(),
|
|
134
|
+
new FenceLanguage(),
|
|
95
135
|
new Unique(),
|
|
96
136
|
new Frontmatter(
|
|
97
137
|
'SKILL.md',
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
const PATTERNS = [
|
|
13
|
+
/\b(?:npm|npx|node|git|eslint|mocha|yarn|pnpm|cd|rm|mkdir|chmod|cat|sed|grep|curl|docker)\s+[\w./-]+/gu,
|
|
14
|
+
/(?<![\w/.@])[\w-]+(?:\/[\w.-]+)+/gu,
|
|
15
|
+
/(?<![\w/.@])[\w-]+\.(?:js|ts|jsx|tsx|json|md|ya?ml|sh|py|rb|go|rs|toml|cfg|lock|txt|xml|html|css)\b/gu,
|
|
16
|
+
/(?<![\w-])(?:--[A-Za-z][\w-]*|-[A-Za-z])(?![\w])/gu
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* InlineCode.
|
|
21
|
+
*
|
|
22
|
+
* When a command, path, filename, or flag sits bare in prose, the model
|
|
23
|
+
* cannot cleanly tell the literal token from the surrounding words and
|
|
24
|
+
* may reword or reformat it. Markdown inline code marks such a token as
|
|
25
|
+
* literal, and consistent code-versus-prose marking measurably lowers
|
|
26
|
+
* misinterpretation. This standalone check flags a bare literal — a
|
|
27
|
+
* slashed path, a filename carrying a known extension, a CLI flag, or a
|
|
28
|
+
* known shell command followed by an argument — once its inline-code
|
|
29
|
+
* spans are masked away, so an already-backticked literal passes. It
|
|
30
|
+
* leaves @-imports to the dead-import rule. Its prompt hands borderline
|
|
31
|
+
* literals to the AI oracle.
|
|
32
|
+
*/
|
|
33
|
+
class InlineCode {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.id = 'inline-code';
|
|
36
|
+
}
|
|
37
|
+
hint() {
|
|
38
|
+
return 'Wrap a bare literal token, such as a command, path, filename, or flag, in backticks so the model treats it as a literal and never rewords it.';
|
|
39
|
+
}
|
|
40
|
+
prompt() {
|
|
41
|
+
return `${this.id}: flag a bare literal token (command, path, filename, or flag) that should be wrapped in backticks, judging borderline cases`;
|
|
42
|
+
}
|
|
43
|
+
violations(document) {
|
|
44
|
+
const uri = document.uri();
|
|
45
|
+
return document.walk({
|
|
46
|
+
header: () => [],
|
|
47
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
48
|
+
snippet: () => [],
|
|
49
|
+
bullets: () => [],
|
|
50
|
+
frontmatter: () => []
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
scan(text, line, uri) {
|
|
54
|
+
const masked = mask(text);
|
|
55
|
+
const spans = [];
|
|
56
|
+
PATTERNS.forEach((pattern) => {
|
|
57
|
+
let hit = pattern.exec(masked);
|
|
58
|
+
while (hit !== null) {
|
|
59
|
+
spans.push({token: hit[0], from: hit.index, to: hit.index + hit[0].length});
|
|
60
|
+
hit = pattern.exec(masked);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return InlineCode.prune(spans).map((span) => new Violation(
|
|
64
|
+
this.id,
|
|
65
|
+
'warning',
|
|
66
|
+
`literal "${span.token}" must be wrapped in backticks`,
|
|
67
|
+
new Region(uri, line, span.from + 1)
|
|
68
|
+
));
|
|
69
|
+
}
|
|
70
|
+
static prune(spans) {
|
|
71
|
+
const ordered = spans.slice().sort((one, two) => one.from - two.from || two.to - one.to);
|
|
72
|
+
const kept = [];
|
|
73
|
+
ordered.forEach((span) => {
|
|
74
|
+
if (!kept.some((other) => span.from >= other.from && span.to <= other.to)) {
|
|
75
|
+
kept.push(span);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return kept;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = InlineCode;
|
package/src/rules/jargon.js
CHANGED
|
@@ -37,12 +37,20 @@ const ALLOWLIST = new Set([
|
|
|
37
37
|
'CLAUDE'
|
|
38
38
|
]);
|
|
39
39
|
|
|
40
|
+
const initials = (gloss) => (gloss.match(/[A-Za-z]+/gu) || [])
|
|
41
|
+
.map((word) => word[0].toUpperCase())
|
|
42
|
+
.join('');
|
|
43
|
+
|
|
40
44
|
const defined = (masked) => {
|
|
41
45
|
const found = new Set();
|
|
42
|
-
const regex = /\b(?<acronym>[A-Z]{2,})\s*\(/gu;
|
|
46
|
+
const regex = /\b(?<acronym>[A-Z]{2,})\s*\(|\((?<gloss>[^)]+)\)/gu;
|
|
43
47
|
let hit = regex.exec(masked);
|
|
44
48
|
while (hit !== null) {
|
|
45
|
-
|
|
49
|
+
if (hit.groups.acronym) {
|
|
50
|
+
found.add(hit.groups.acronym);
|
|
51
|
+
} else {
|
|
52
|
+
found.add(initials(hit.groups.gloss));
|
|
53
|
+
}
|
|
46
54
|
hit = regex.exec(masked);
|
|
47
55
|
}
|
|
48
56
|
return found;
|
|
@@ -56,8 +64,10 @@ const undefining = (acronym, scope) => !scope.known.has(acronym) &&
|
|
|
56
64
|
*
|
|
57
65
|
* Flags an acronym that lands in prose without ever being expanded. An
|
|
58
66
|
* acronym counts as defined when the document, anywhere, follows it with
|
|
59
|
-
* a parenthetical gloss, as in "RBAC (role-based access control)",
|
|
60
|
-
*
|
|
67
|
+
* a parenthetical gloss, as in "RBAC (role-based access control)", or when
|
|
68
|
+
* a parenthetical's word initials spell it, as in "AAA pattern
|
|
69
|
+
* (Arrange-Act-Assert)", so a single expansion licenses every later
|
|
70
|
+
* mention. Well-known acronyms sit
|
|
61
71
|
* in a built-in allowlist and pass untouched. Only the first unexpanded
|
|
62
72
|
* occurrence of each acronym is reported. Its prompt hands non-acronym
|
|
63
73
|
* domain jargon, the rare nouns a reader cannot parse, to the AI oracle.
|
|
@@ -66,6 +76,9 @@ class Jargon {
|
|
|
66
76
|
constructor() {
|
|
67
77
|
this.id = 'jargon';
|
|
68
78
|
}
|
|
79
|
+
hint() {
|
|
80
|
+
return 'Expand each acronym on first use with a parenthetical gloss, and replace rare domain jargon with plain words a fresh reader can parse.';
|
|
81
|
+
}
|
|
69
82
|
prompt() {
|
|
70
83
|
return `${this.id}: flag non-acronym domain jargon, rare nouns a fresh reader cannot parse, and ask for a plain-word definition on first use`;
|
|
71
84
|
}
|
package/src/rules/line-length.js
CHANGED
|
@@ -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
|
+
* MetaReference.
|
|
14
|
+
*
|
|
15
|
+
* Flags self-referential framing of the model or the document, such as
|
|
16
|
+
* "as an AI", "you are a model", "this prompt", or "these instructions".
|
|
17
|
+
* Such framing narrates the setup instead of issuing a command, so it
|
|
18
|
+
* adds no instruction and earns deletion. Distinct from persona, which
|
|
19
|
+
* targets role assignment like "Act as a reviewer"; this one targets
|
|
20
|
+
* the model talking about itself or the document talking about itself.
|
|
21
|
+
*/
|
|
22
|
+
class MetaReference {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'meta-reference';
|
|
25
|
+
this.phrase = /\b(?:as an ai|as a language model|you are an ai|you are a model|this prompt|these instructions|this manifesto|the system prompt)\b/giu;
|
|
26
|
+
}
|
|
27
|
+
hint() {
|
|
28
|
+
return 'Delete self-referential framing such as as an AI or this prompt, since it narrates the setup instead of issuing a command.';
|
|
29
|
+
}
|
|
30
|
+
prompt() {
|
|
31
|
+
return `${this.id}: flag self-referential framing of the model or document beyond the fixed list, and delete it`;
|
|
32
|
+
}
|
|
33
|
+
violations(document) {
|
|
34
|
+
const uri = document.uri();
|
|
35
|
+
return document.walk({
|
|
36
|
+
header: () => [],
|
|
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 out = [];
|
|
46
|
+
let hit = this.phrase.exec(masked);
|
|
47
|
+
while (hit !== null) {
|
|
48
|
+
out.push(new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
`meta self-reference "${hit[0]}" issues no command, delete it`,
|
|
52
|
+
new Region(uri, line, hit.index + 1)
|
|
53
|
+
));
|
|
54
|
+
hit = this.phrase.exec(masked);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = MetaReference;
|
package/src/rules/name-format.js
CHANGED
package/src/rules/no-articles.js
CHANGED
|
@@ -19,6 +19,9 @@ class NoArticles {
|
|
|
19
19
|
constructor() {
|
|
20
20
|
this.id = 'no-articles';
|
|
21
21
|
}
|
|
22
|
+
hint() {
|
|
23
|
+
return 'Remove filler articles such as a, an, and the, since they add noise without changing the instruction.';
|
|
24
|
+
}
|
|
22
25
|
prompt() {
|
|
23
26
|
return `${this.id}: flag filler or noise words that add nothing to an instruction`;
|
|
24
27
|
}
|
package/src/rules/ordered.js
CHANGED
|
@@ -24,6 +24,9 @@ class Ordered {
|
|
|
24
24
|
constructor() {
|
|
25
25
|
this.id = 'ordered';
|
|
26
26
|
}
|
|
27
|
+
hint() {
|
|
28
|
+
return 'Convert a sequence of steps into a numbered list, since models follow numbered ordered steps far more reliably than unordered bullets.';
|
|
29
|
+
}
|
|
27
30
|
prompt() {
|
|
28
31
|
return `${this.id}: flag an implied sequence that no marker word signals, demanding a numbered list whenever the order of steps matters`;
|
|
29
32
|
}
|
package/src/rules/passive.js
CHANGED
|
@@ -21,6 +21,9 @@ class Passive {
|
|
|
21
21
|
constructor() {
|
|
22
22
|
this.id = 'passive';
|
|
23
23
|
}
|
|
24
|
+
hint() {
|
|
25
|
+
return 'Rewrite the line in active imperative voice, naming the action to take instead of describing what gets done.';
|
|
26
|
+
}
|
|
24
27
|
prompt() {
|
|
25
28
|
return `${this.id}: flag any instruction written in passive voice, judging true grammatical voice including irregular past participles a fixed pattern misses`;
|
|
26
29
|
}
|
package/src/rules/persona.js
CHANGED
|
@@ -24,6 +24,9 @@ class Persona {
|
|
|
24
24
|
constructor() {
|
|
25
25
|
this.id = 'persona';
|
|
26
26
|
}
|
|
27
|
+
hint() {
|
|
28
|
+
return 'Delete the role-play persona such as You are a senior engineer, since assigning a role adds no instruction and can hurt performance.';
|
|
29
|
+
}
|
|
27
30
|
prompt() {
|
|
28
31
|
return `${this.id}: flag indirect persona or role-play framing that assigns the agent a role with no fixed keyword, since a persona adds no instruction`;
|
|
29
32
|
}
|
|
@@ -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
|
+
/**
|
|
12
|
+
* Placement.
|
|
13
|
+
*
|
|
14
|
+
* Transformers attend most to the start and the end of their input and
|
|
15
|
+
* skim the middle, so a critical section buried in the middle third of a
|
|
16
|
+
* manifesto sits exactly where the model is least likely to use it. This
|
|
17
|
+
* standalone check spots a critical section by a heading keyword (Safety,
|
|
18
|
+
* Security, Mission, Critical, Constraints) and warns when it lands in
|
|
19
|
+
* the middle third rather than near the top or bottom. The generic word
|
|
20
|
+
* "Rules" is left out on purpose: it names a neutral section in many
|
|
21
|
+
* manifestos and would misfire. Its prompt hands the deeper judgement to
|
|
22
|
+
* the AI oracle, which weighs which instruction matters most and whether
|
|
23
|
+
* it is well placed.
|
|
24
|
+
*/
|
|
25
|
+
class Placement {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'placement';
|
|
28
|
+
this.keyword = /^#{1,6}\s+.*\b(?:safety|security|mission|critical|constraints?)\b/iu;
|
|
29
|
+
}
|
|
30
|
+
hint() {
|
|
31
|
+
return 'Move the critical section to the top or bottom of the file, since models attend least to the buried middle of a long context.';
|
|
32
|
+
}
|
|
33
|
+
prompt() {
|
|
34
|
+
return `${this.id}: identify the single most important instruction and judge whether it sits near the top or bottom of the file rather than buried in the middle`;
|
|
35
|
+
}
|
|
36
|
+
violations(document) {
|
|
37
|
+
const uri = document.uri();
|
|
38
|
+
const total = document.text().split('\n').length;
|
|
39
|
+
return document.walk({
|
|
40
|
+
header: (text, row) => this.check(text, row, total, uri),
|
|
41
|
+
prose: () => [],
|
|
42
|
+
snippet: () => [],
|
|
43
|
+
bullets: () => [],
|
|
44
|
+
frontmatter: () => []
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
check(text, row, total, uri) {
|
|
48
|
+
if (!this.keyword.test(text)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const ratio = row / total;
|
|
52
|
+
if (ratio <= 1 / 3 || ratio >= 2 / 3) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const name = text.replace(/^#+\s*/u, '').trim();
|
|
56
|
+
return [new Violation(
|
|
57
|
+
this.id,
|
|
58
|
+
'warning',
|
|
59
|
+
`critical section "${name}" is buried, move it to the top or bottom`,
|
|
60
|
+
new Region(uri, row, 1)
|
|
61
|
+
)];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = Placement;
|
package/src/rules/polite.js
CHANGED
package/src/rules/positive.js
CHANGED
|
@@ -25,6 +25,9 @@ class Positive {
|
|
|
25
25
|
constructor() {
|
|
26
26
|
this.id = 'positive';
|
|
27
27
|
}
|
|
28
|
+
hint() {
|
|
29
|
+
return 'Rewrite a prohibition as a positive imperative stating what to do, since a ban forces the model to process the forbidden idea first.';
|
|
30
|
+
}
|
|
28
31
|
prompt() {
|
|
29
32
|
return `${this.id}: flag any instruction phrased as a prohibition, including bans carrying no fixed keyword, and rewrite each as a positive imperative`;
|
|
30
33
|
}
|
|
@@ -25,6 +25,9 @@ class PseudoHeading {
|
|
|
25
25
|
constructor() {
|
|
26
26
|
this.id = 'pseudo-heading';
|
|
27
27
|
}
|
|
28
|
+
hint() {
|
|
29
|
+
return 'Replace a bold line posing as a heading with a real level-2 heading marked by two hashes.';
|
|
30
|
+
}
|
|
28
31
|
prompt() {
|
|
29
32
|
return `${this.id}: flag any bold line posing as a section heading, deferring borderline label-versus-instruction calls to the oracle, and demand a real level-2 "##" heading`;
|
|
30
33
|
}
|
package/src/rules/punctuation.js
CHANGED
|
@@ -19,6 +19,9 @@ class Punctuation {
|
|
|
19
19
|
constructor() {
|
|
20
20
|
this.id = 'punctuation';
|
|
21
21
|
}
|
|
22
|
+
hint() {
|
|
23
|
+
return 'Write each instruction as one complete sentence that opens with a capital letter and closes with a period.';
|
|
24
|
+
}
|
|
22
25
|
prompt() {
|
|
23
26
|
return `${this.id}: flag any instruction that is not one complete, grammatical sentence`;
|
|
24
27
|
}
|
|
@@ -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 Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
const mask = require('../mask');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Quantifier.
|
|
14
|
+
*
|
|
15
|
+
* Flags vague quantity words that leave the count to the agent: "some",
|
|
16
|
+
* "several", "a few", "many", "multiple", and the like. Models track
|
|
17
|
+
* exact quantifiers well yet diverge from human intent on vague ones,
|
|
18
|
+
* whose meaning is an underspecified distribution rather than a number,
|
|
19
|
+
* so a command manifesto should state the exact count or threshold. The
|
|
20
|
+
* list is kept apart from the vague qualifiers so the two rules never
|
|
21
|
+
* double-report. Its prompt hands implicit vagueness, where no listed
|
|
22
|
+
* word appears, to the AI oracle.
|
|
23
|
+
*/
|
|
24
|
+
class Quantifier {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.id = 'quantifier';
|
|
27
|
+
}
|
|
28
|
+
hint() {
|
|
29
|
+
return 'Replace a vague quantity word such as some or several with an exact number or threshold the agent can act on.';
|
|
30
|
+
}
|
|
31
|
+
prompt() {
|
|
32
|
+
return `${this.id}: flag a vague amount that names no exact count even without a listed word, and propose a concrete number or threshold to replace it`;
|
|
33
|
+
}
|
|
34
|
+
violations(document) {
|
|
35
|
+
const uri = document.uri();
|
|
36
|
+
return document.walk({
|
|
37
|
+
header: () => [],
|
|
38
|
+
prose: (text, line) => this.scan(text, line, uri),
|
|
39
|
+
snippet: () => [],
|
|
40
|
+
bullets: () => [],
|
|
41
|
+
frontmatter: () => []
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
scan(text, line, uri) {
|
|
45
|
+
const found = [];
|
|
46
|
+
const regex = new RegExp(
|
|
47
|
+
'\\b(?:some|several|a few|a couple|many|multiple|various|' +
|
|
48
|
+
'numerous|a lot of|plenty of)\\b',
|
|
49
|
+
'giu'
|
|
50
|
+
);
|
|
51
|
+
const masked = mask(text);
|
|
52
|
+
let hit = regex.exec(masked);
|
|
53
|
+
while (hit !== null) {
|
|
54
|
+
found.push(new Violation(
|
|
55
|
+
this.id,
|
|
56
|
+
'warning',
|
|
57
|
+
`vague quantity "${hit[0]}", state an exact number or threshold`,
|
|
58
|
+
new Region(uri, line, hit.index + 1)
|
|
59
|
+
));
|
|
60
|
+
hit = regex.exec(masked);
|
|
61
|
+
}
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = Quantifier;
|
package/src/rules/rationale.js
CHANGED
|
@@ -23,6 +23,9 @@ class Rationale {
|
|
|
23
23
|
constructor() {
|
|
24
24
|
this.id = 'rationale';
|
|
25
25
|
}
|
|
26
|
+
hint() {
|
|
27
|
+
return 'Delete the explanation or convert it into a direct order, since a manifesto carries commands, not justifications.';
|
|
28
|
+
}
|
|
26
29
|
prompt() {
|
|
27
30
|
return `${this.id}: flag any line that explains a reason, motivation, or benefit instead of issuing a direct order, even when it carries no fixed marker, and convert each into a command or delete it`;
|
|
28
31
|
}
|
package/src/rules/redundant.js
CHANGED
|
@@ -54,6 +54,9 @@ class Redundant {
|
|
|
54
54
|
this.id = 'redundant';
|
|
55
55
|
this.phrases = phrases;
|
|
56
56
|
}
|
|
57
|
+
hint() {
|
|
58
|
+
return 'Delete the line that restates default model behavior, since generic advice the model already knows wastes the context budget.';
|
|
59
|
+
}
|
|
57
60
|
prompt() {
|
|
58
61
|
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`;
|
|
59
62
|
}
|