@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
@@ -0,0 +1,58 @@
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
+ hint() {
27
+ return 'Break a line that packs several conditions into one case per line, so the agent never has to untangle a whole decision tree welded onto a single line.';
28
+ }
29
+ prompt() {
30
+ return `${this.id}: flag implicit branching that carries no keyword, and split each case into its own command`;
31
+ }
32
+ violations(document) {
33
+ const uri = document.uri();
34
+ return document.walk({
35
+ header: () => [],
36
+ prose: (text, line) => this.judge(text, line, uri),
37
+ snippet: () => [],
38
+ bullets: () => [],
39
+ frontmatter: () => []
40
+ });
41
+ }
42
+ judge(text, line, uri) {
43
+ const clean = mask(text);
44
+ const hits = clean.match(/\b(?:if|unless|when|else|otherwise)\b/giu);
45
+ if (hits === null || hits.length < 2) {
46
+ return [];
47
+ }
48
+ const column = clean.search(/\b(?:if|unless|when|else|otherwise)\b/iu);
49
+ return [new Violation(
50
+ this.id,
51
+ 'warning',
52
+ 'multi-branch conditional, split each case into its own command',
53
+ new Region(uri, line, column + 1)
54
+ )];
55
+ }
56
+ }
57
+
58
+ module.exports = Conditional;
@@ -19,8 +19,11 @@ class Consistent {
19
19
  constructor() {
20
20
  this.id = 'consistent';
21
21
  }
22
+ hint() {
23
+ return 'Delete the duplicate instruction or reconcile the contradictory pair, so each instruction is stated once and never both ordered and forbidden across two lines.';
24
+ }
22
25
  prompt() {
23
- return `${this.id}: flag an instruction that repeats another instruction word for word, or that directly contradicts another instruction in the same file`;
26
+ 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
27
  }
25
28
  violations() {
26
29
  return [];
@@ -25,6 +25,9 @@ class CounterExample {
25
25
  constructor() {
26
26
  this.id = 'counter-example';
27
27
  }
28
+ hint() {
29
+ return 'Remove the demonstration of the wrong form and show only the correct form, since displaying a mistake can teach the agent to repeat it.';
30
+ }
28
31
  prompt() {
29
32
  return `${this.id}: judge whether each example shows the correct behavior, and flag any example that demonstrates the incorrect form`;
30
33
  }
@@ -25,6 +25,9 @@ class Crowded {
25
25
  this.id = 'crowded';
26
26
  this.limit = limit;
27
27
  }
28
+ hint() {
29
+ return 'Split an overcrowded section into smaller sections, each holding only a handful of related instructions under its own short heading.';
30
+ }
28
31
  prompt() {
29
32
  return '';
30
33
  }
@@ -57,6 +57,9 @@ class DeadImport {
57
57
  this.id = 'dead-import';
58
58
  this.depth = 5;
59
59
  }
60
+ hint() {
61
+ return 'Fix or remove the @path import so it points to a real file, and break any circular or overly deep import chain the host tool cannot resolve.';
62
+ }
60
63
  prompt() {
61
64
  return `${this.id}: flag any @path/to/file import that points to no file on disk`;
62
65
  }
@@ -0,0 +1,63 @@
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
+ hint() {
27
+ return 'State the default outcome whenever you mark behavior optional, telling the agent exactly what to do when it declines the option.';
28
+ }
29
+ prompt() {
30
+ return `${this.id}: flag optionality that names no default even without a listed marker, and state the default`;
31
+ }
32
+ violations(document) {
33
+ const uri = document.uri();
34
+ return document.walk({
35
+ header: () => [],
36
+ prose: (text, line) => this.scan(text, line, uri),
37
+ snippet: () => [],
38
+ bullets: () => [],
39
+ frontmatter: () => []
40
+ });
41
+ }
42
+ scan(text, line, uri) {
43
+ const masked = mask(text);
44
+ if ((/\b(?:by default|default to|defaults to|otherwise)\b/iu).test(masked)) {
45
+ return [];
46
+ }
47
+ const found = [];
48
+ const regex = /\b(?:optionally|you may|you can|if you want|feel free to|as an option)\b/giu;
49
+ let hit = regex.exec(masked);
50
+ while (hit !== null) {
51
+ found.push(new Violation(
52
+ this.id,
53
+ 'warning',
54
+ `optional behavior "${hit[0]}" has no default, state it`,
55
+ new Region(uri, line, hit.index + 1)
56
+ ));
57
+ hit = regex.exec(masked);
58
+ }
59
+ return found;
60
+ }
61
+ }
62
+
63
+ module.exports = Default;
@@ -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
+ * 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
+ hint() {
29
+ return 'Write a SKILL.md description that is neither empty nor bloated, stating the capability concisely so it fits the loader budget.';
30
+ }
31
+ prompt() {
32
+ return '';
33
+ }
34
+ violations(document) {
35
+ const uri = document.uri();
36
+ if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
37
+ return [];
38
+ }
39
+ const pairs = document.walk({
40
+ header: () => [],
41
+ prose: () => [],
42
+ snippet: () => [],
43
+ bullets: () => [],
44
+ frontmatter: (keys) => keys
45
+ });
46
+ const found = pairs.filter((pair) => pair.key === 'description');
47
+ if (found.length === 0) {
48
+ return [];
49
+ }
50
+ return this.judge(found[0], uri);
51
+ }
52
+ judge(pair, uri) {
53
+ const {value} = pair;
54
+ if (value.trim() === '') {
55
+ return [this.flag('description is empty, write a concise capability statement', pair.row, uri)];
56
+ }
57
+ if (value.length > this.ceiling) {
58
+ return [this.flag(`description is ${value.length} chars, keep it under ${this.ceiling}`, pair.row, uri)];
59
+ }
60
+ return [];
61
+ }
62
+ flag(message, row, uri) {
63
+ return new Violation(this.id, 'warning', message, new Region(uri, row, 1));
64
+ }
65
+ }
66
+
67
+ module.exports = DescriptionLength;
@@ -22,6 +22,9 @@ class DescriptionTriggers {
22
22
  this.id = 'description-triggers';
23
23
  this.minimum = 20;
24
24
  }
25
+ hint() {
26
+ return 'Name the concrete situations and user phrases that should activate the skill in its description, so the loader knows exactly when to invoke it.';
27
+ }
25
28
  prompt() {
26
29
  return `${this.id}: in a SKILL.md, flag a description that is too short or fails to name the concrete situations and user phrases that should activate the skill, even when it contains the word "when"`;
27
30
  }
@@ -0,0 +1,70 @@
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
+ hint() {
31
+ return 'Write the SKILL.md description as a third-person capability statement such as Extracts tables, never in first or second person.';
32
+ }
33
+ prompt() {
34
+ return `${this.id}: in a SKILL.md, flag a description written in first or second person and demand a third-person capability statement`;
35
+ }
36
+ violations(document) {
37
+ const uri = document.uri();
38
+ if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
39
+ return [];
40
+ }
41
+ const pairs = document.walk({
42
+ header: () => [],
43
+ prose: () => [],
44
+ snippet: () => [],
45
+ bullets: () => [],
46
+ frontmatter: (keys) => keys
47
+ });
48
+ const found = pairs.filter((pair) => pair.key === 'description');
49
+ if (found.length === 0) {
50
+ return [];
51
+ }
52
+ return this.judge(found[0], uri);
53
+ }
54
+ judge(pair, uri) {
55
+ const text = pair.value.replace(/use when.*$/isu, '');
56
+ this.pronoun.lastIndex = 0;
57
+ const hit = this.pronoun.exec(text);
58
+ if (hit === null) {
59
+ return [];
60
+ }
61
+ return [new Violation(
62
+ this.id,
63
+ 'warning',
64
+ `description must be third person, not "${hit[0]}"`,
65
+ new Region(uri, pair.row, 1)
66
+ )];
67
+ }
68
+ }
69
+
70
+ module.exports = DescriptionVoice;
package/src/rules/done.js CHANGED
@@ -21,6 +21,9 @@ class Done {
21
21
  constructor() {
22
22
  this.id = 'done';
23
23
  }
24
+ hint() {
25
+ return 'Add a verifiable, pass-or-fail completion check to the SKILL.md so the agent knows exactly how to confirm the work is finished.';
26
+ }
24
27
  prompt() {
25
28
  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
29
  }
@@ -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
+ 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
+ hint() {
32
+ return 'Give every section a distinct heading, merging or renaming any two sections that share the same name.';
33
+ }
34
+ prompt() {
35
+ return '';
36
+ }
37
+ violations(document) {
38
+ const uri = document.uri();
39
+ const headers = document.walk({
40
+ header: (text, row) => [{text, row}],
41
+ prose: () => [],
42
+ snippet: () => [],
43
+ bullets: () => [],
44
+ frontmatter: () => []
45
+ });
46
+ return this.repeats(uri, headers);
47
+ }
48
+ repeats(uri, headers) {
49
+ const seen = new Set();
50
+ const found = [];
51
+ headers.forEach((header) => {
52
+ const norm = normalize(header.text);
53
+ if (seen.has(norm)) {
54
+ found.push(new Violation(
55
+ this.id,
56
+ 'warning',
57
+ `duplicate section "${bare(header.text)}", give each section a distinct name`,
58
+ new Region(uri, header.row, 1)
59
+ ));
60
+ } else {
61
+ seen.add(norm);
62
+ }
63
+ });
64
+ return found;
65
+ }
66
+ }
67
+
68
+ module.exports = DuplicateSection;
@@ -0,0 +1,63 @@
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
+ hint() {
31
+ return 'Delete decorative emoji and pictographic symbols, since they add token noise without carrying any instruction, and keep the text plain.';
32
+ }
33
+ prompt() {
34
+ return '';
35
+ }
36
+ violations(document) {
37
+ const uri = document.uri();
38
+ return document.walk({
39
+ header: (text, line) => this.scan(text, line, uri),
40
+ prose: (text, line) => this.scan(text, line, uri),
41
+ snippet: () => [],
42
+ bullets: () => [],
43
+ frontmatter: () => []
44
+ });
45
+ }
46
+ scan(text, line, uri) {
47
+ const masked = mask(text);
48
+ const result = [];
49
+ let hit = this.glyph.exec(masked);
50
+ while (hit !== null) {
51
+ result.push(new Violation(
52
+ this.id,
53
+ 'warning',
54
+ `decorative character "${hit[0]}" adds token noise, use plain text`,
55
+ new Region(uri, line, hit.index + 1)
56
+ ));
57
+ hit = this.glyph.exec(masked);
58
+ }
59
+ return result;
60
+ }
61
+ }
62
+
63
+ module.exports = Emoji;
@@ -25,6 +25,9 @@ class Emphasis {
25
25
  this.id = 'emphasis';
26
26
  this.shout = new Set(['IMPORTANT', 'ALWAYS', 'NEVER', 'MUST', 'CRITICAL', 'REQUIRED']);
27
27
  }
28
+ hint() {
29
+ return 'Drop shouting such as all-caps words or repeated exclamation marks and state the instruction plainly, since volume adds no meaning for the model.';
30
+ }
28
31
  prompt() {
29
32
  return `${this.id}: flag emphatic shouting the patterns miss, including borderline all-caps and reward framing, since emphasis adds no instruction`;
30
33
  }
@@ -23,6 +23,9 @@ class Empty {
23
23
  constructor() {
24
24
  this.id = 'empty';
25
25
  }
26
+ hint() {
27
+ return 'Fill the hollow section with at least one instruction, or delete the heading, so no section declares itself without a body.';
28
+ }
26
29
  prompt() {
27
30
  return '';
28
31
  }
@@ -0,0 +1,35 @@
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
+ hint() {
25
+ return 'Make the example in the SKILL.md conform exactly to the declared output format, since a mismatched example teaches the agent the wrong shape.';
26
+ }
27
+ prompt() {
28
+ 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`;
29
+ }
30
+ violations() {
31
+ return [];
32
+ }
33
+ }
34
+
35
+ module.exports = ExampleFormat;
@@ -24,6 +24,9 @@ class Example {
24
24
  constructor() {
25
25
  this.id = 'example';
26
26
  }
27
+ hint() {
28
+ return 'Add at least one concrete worked input and output example to the SKILL.md, since a single demonstration guides the agent far better than prose alone.';
29
+ }
27
30
  prompt() {
28
31
  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
32
  }
@@ -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
+ * ExternalLink.
14
+ *
15
+ * Flags a bare http(s):// URL sitting in prose or a bullet item, where
16
+ * the page behind it may rot or inject hidden instructions. Durable
17
+ * guidance belongs inlined, not fetched at run time. A URL inside
18
+ * inline code or a fenced snippet is exempt, since those are examples.
19
+ * Distinct from dead-import, which targets local @path imports; this
20
+ * one complements untrusted and stale.
21
+ */
22
+ class ExternalLink {
23
+ constructor() {
24
+ this.id = 'external-link';
25
+ }
26
+ hint() {
27
+ return 'Inline the durable guidance instead of linking to an external URL, since the page may rot or smuggle hidden instructions at run time.';
28
+ }
29
+ prompt() {
30
+ return `${this.id}: judge whether an external link is load-bearing, and flag durable guidance that should be inlined instead`;
31
+ }
32
+ violations(document) {
33
+ const uri = document.uri();
34
+ return document.walk({
35
+ header: () => [],
36
+ prose: (text, line) => this.scan(text, line, uri),
37
+ snippet: () => [],
38
+ bullets: () => [],
39
+ frontmatter: () => []
40
+ });
41
+ }
42
+ scan(text, line, uri) {
43
+ const found = [];
44
+ const regex = /(?:https?:\/\/)\S+/giu;
45
+ const masked = mask(text);
46
+ let hit = regex.exec(masked);
47
+ while (hit !== null) {
48
+ found.push(new Violation(
49
+ this.id,
50
+ 'warning',
51
+ 'external URL may rot or inject, encode durable guidance instead',
52
+ new Region(uri, line, hit.index + 1)
53
+ ));
54
+ hit = regex.exec(masked);
55
+ }
56
+ return found;
57
+ }
58
+ }
59
+
60
+ module.exports = ExternalLink;
@@ -0,0 +1,58 @@
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
+ * FenceLanguage.
13
+ *
14
+ * Demands that every fenced code block declare a language right after
15
+ * its opening fence. A bare fence of backticks or tildes with no info
16
+ * string leaves readers and tooling guessing at the snippet's syntax,
17
+ * so it earns a warning. A fence that names a language stays clean.
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 FenceLanguage {
23
+ constructor() {
24
+ this.id = 'fence-language';
25
+ this.fence = /^\s*(?:```|~~~)\s*(?<lang>\S*)/u;
26
+ }
27
+ hint() {
28
+ return 'Declare a language right after the opening fence of every code block so readers and tooling know the snippet syntax.';
29
+ }
30
+ prompt() {
31
+ return '';
32
+ }
33
+ violations(document) {
34
+ const uri = document.uri();
35
+ return document.walk({
36
+ header: () => [],
37
+ prose: () => [],
38
+ snippet: (content, row) => this.scan(content, row, uri),
39
+ bullets: () => [],
40
+ frontmatter: () => []
41
+ });
42
+ }
43
+ scan(content, row, uri) {
44
+ const [first] = content.split('\n');
45
+ const hit = this.fence.exec(first);
46
+ if (hit !== null && hit.groups.lang !== '') {
47
+ return [];
48
+ }
49
+ return [new Violation(
50
+ this.id,
51
+ 'warning',
52
+ 'fenced block has no language tag, declare one',
53
+ new Region(uri, row, 1)
54
+ )];
55
+ }
56
+ }
57
+
58
+ module.exports = FenceLanguage;
@@ -26,6 +26,9 @@ class Format {
26
26
  constructor() {
27
27
  this.id = 'format';
28
28
  }
29
+ hint() {
30
+ return 'Declare and show the exact output format whenever the skill generates output, since a pinned-down contract makes structured output far more reliable.';
31
+ }
29
32
  prompt() {
30
33
  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
34
  }
@@ -22,6 +22,9 @@ class Frontmatter {
22
22
  this.required = required;
23
23
  this.allowed = allowed;
24
24
  }
25
+ hint() {
26
+ return 'Open the file with a YAML frontmatter block that declares every required key with a real value and carries no key outside the allowed set.';
27
+ }
25
28
  prompt() {
26
29
  return `${this.id}: in a ${this.name} file, flag any required key whose value is empty, vague, or a leftover placeholder`;
27
30
  }
@@ -21,6 +21,9 @@ class Grouped {
21
21
  constructor() {
22
22
  this.id = 'grouped';
23
23
  }
24
+ hint() {
25
+ return 'Move every loose instruction under a section heading, since prose before the first heading belongs to no section.';
26
+ }
24
27
  prompt() {
25
28
  return '';
26
29
  }