@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.
Files changed (75) hide show
  1. package/README.md +135 -15
  2. package/package.json +1 -1
  3. package/src/args.js +24 -4
  4. package/src/defaults.js +47 -0
  5. package/src/dogent.js +65 -18
  6. package/src/openai.js +8 -5
  7. package/src/prompt.js +0 -4
  8. package/src/report.js +26 -3
  9. package/src/rules/ambiguous-or.js +61 -0
  10. package/src/rules/atomic.js +3 -0
  11. package/src/rules/budget.js +3 -0
  12. package/src/rules/command.js +3 -0
  13. package/src/rules/concise.js +3 -0
  14. package/src/rules/conditional.js +58 -0
  15. package/src/rules/consistent.js +4 -1
  16. package/src/rules/counter-example.js +3 -0
  17. package/src/rules/crowded.js +3 -0
  18. package/src/rules/dead-import.js +3 -0
  19. package/src/rules/default.js +63 -0
  20. package/src/rules/description-length.js +67 -0
  21. package/src/rules/description-triggers.js +3 -0
  22. package/src/rules/description-voice.js +70 -0
  23. package/src/rules/done.js +3 -0
  24. package/src/rules/duplicate-section.js +68 -0
  25. package/src/rules/emoji.js +63 -0
  26. package/src/rules/emphasis.js +3 -0
  27. package/src/rules/empty.js +3 -0
  28. package/src/rules/example-format.js +35 -0
  29. package/src/rules/example.js +3 -0
  30. package/src/rules/external-link.js +60 -0
  31. package/src/rules/fence-language.js +58 -0
  32. package/src/rules/format.js +3 -0
  33. package/src/rules/frontmatter.js +3 -0
  34. package/src/rules/grouped.js +3 -0
  35. package/src/rules/hedging.js +3 -0
  36. package/src/rules/hidden-char.js +64 -0
  37. package/src/rules/homoglyph.js +85 -0
  38. package/src/rules/index.js +40 -0
  39. package/src/rules/inline-code.js +82 -0
  40. package/src/rules/jargon.js +17 -4
  41. package/src/rules/line-length.js +3 -0
  42. package/src/rules/meta-reference.js +60 -0
  43. package/src/rules/name-format.js +3 -0
  44. package/src/rules/name-matches-dir.js +3 -0
  45. package/src/rules/no-articles.js +3 -0
  46. package/src/rules/ordered.js +3 -0
  47. package/src/rules/passive.js +3 -0
  48. package/src/rules/persona.js +3 -0
  49. package/src/rules/placement.js +65 -0
  50. package/src/rules/polite.js +3 -0
  51. package/src/rules/positive.js +3 -0
  52. package/src/rules/pseudo-heading.js +3 -0
  53. package/src/rules/punctuation.js +3 -0
  54. package/src/rules/quantifier.js +66 -0
  55. package/src/rules/rationale.js +3 -0
  56. package/src/rules/redundant.js +3 -0
  57. package/src/rules/referential.js +3 -0
  58. package/src/rules/scope.js +34 -0
  59. package/src/rules/section-level.js +3 -0
  60. package/src/rules/self-contained.js +3 -0
  61. package/src/rules/short-sections.js +3 -0
  62. package/src/rules/simple.js +3 -0
  63. package/src/rules/stale.js +3 -0
  64. package/src/rules/terms.js +3 -0
  65. package/src/rules/token-count.js +3 -0
  66. package/src/rules/tool-clarity.js +3 -0
  67. package/src/rules/transition.js +62 -0
  68. package/src/rules/unfinished.js +3 -0
  69. package/src/rules/unique.js +3 -0
  70. package/src/rules/units.js +84 -0
  71. package/src/rules/untrusted.js +3 -0
  72. package/src/rules/vague.js +3 -0
  73. package/src/rules/weak-verb.js +65 -0
  74. package/src/sources.js +3 -0
  75. package/src/version.js +2 -2
@@ -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;
@@ -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;
@@ -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
- found.add(hit.groups.acronym);
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)", so a
60
- * single expansion licenses every later mention. Well-known acronyms sit
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
  }
@@ -22,6 +22,9 @@ class LineLength {
22
22
  this.id = 'line-length';
23
23
  this.max = max;
24
24
  }
25
+ hint() {
26
+ return 'Shorten the line below the width cap, splitting it into separate instructions if needed so each stays easy to read.';
27
+ }
25
28
  prompt() {
26
29
  return '';
27
30
  }
@@ -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;
@@ -23,6 +23,9 @@ class NameFormat {
23
23
  constructor() {
24
24
  this.id = 'name-format';
25
25
  }
26
+ hint() {
27
+ return 'Write the SKILL.md frontmatter name in kebab-case, using only lowercase letters and digits joined by single hyphens.';
28
+ }
26
29
  prompt() {
27
30
  return '';
28
31
  }
@@ -24,6 +24,9 @@ class NameMatchesDir {
24
24
  constructor() {
25
25
  this.id = 'name-matches-dir';
26
26
  }
27
+ hint() {
28
+ return 'Rename the SKILL.md frontmatter name so it matches the name of the directory that holds the file.';
29
+ }
27
30
  prompt() {
28
31
  return '';
29
32
  }
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -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;
@@ -24,6 +24,9 @@ class Polite {
24
24
  constructor() {
25
25
  this.id = 'polite';
26
26
  }
27
+ hint() {
28
+ return 'Remove courtesy and scaffolding phrases such as please or make sure to, since they waste tokens and weaken the command.';
29
+ }
27
30
  prompt() {
28
31
  return '';
29
32
  }
@@ -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
  }
@@ -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;
@@ -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
  }
@@ -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
  }