@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.
Files changed (51) hide show
  1. package/README.md +104 -12
  2. package/package.json +3 -2
  3. package/src/args.js +35 -4
  4. package/src/defaults.js +47 -0
  5. package/src/dogent.js +42 -16
  6. package/src/openai.js +8 -5
  7. package/src/prompt.js +0 -4
  8. package/src/report.js +8 -2
  9. package/src/rules/ambiguous-or.js +58 -0
  10. package/src/rules/budget.js +50 -0
  11. package/src/rules/concise.js +48 -0
  12. package/src/rules/conditional.js +55 -0
  13. package/src/rules/consistent.js +1 -1
  14. package/src/rules/counter-example.js +60 -0
  15. package/src/rules/default.js +60 -0
  16. package/src/rules/description-length.js +64 -0
  17. package/src/rules/description-voice.js +67 -0
  18. package/src/rules/done.js +53 -0
  19. package/src/rules/duplicate-section.js +65 -0
  20. package/src/rules/emoji.js +60 -0
  21. package/src/rules/emphasis.js +81 -0
  22. package/src/rules/example-format.js +32 -0
  23. package/src/rules/example.js +60 -0
  24. package/src/rules/external-link.js +57 -0
  25. package/src/rules/fence-language.js +55 -0
  26. package/src/rules/format.js +68 -0
  27. package/src/rules/hidden-char.js +61 -0
  28. package/src/rules/homoglyph.js +82 -0
  29. package/src/rules/index.js +80 -0
  30. package/src/rules/inline-code.js +79 -0
  31. package/src/rules/jargon.js +115 -0
  32. package/src/rules/meta-reference.js +57 -0
  33. package/src/rules/ordered.js +57 -0
  34. package/src/rules/persona.js +55 -0
  35. package/src/rules/placement.js +62 -0
  36. package/src/rules/positive.js +57 -0
  37. package/src/rules/pseudo-heading.js +55 -0
  38. package/src/rules/quantifier.js +63 -0
  39. package/src/rules/rationale.js +54 -0
  40. package/src/rules/referential.js +67 -0
  41. package/src/rules/scope.js +31 -0
  42. package/src/rules/self-contained.js +66 -0
  43. package/src/rules/stale.js +62 -0
  44. package/src/rules/terms.js +77 -0
  45. package/src/rules/tool-clarity.js +61 -0
  46. package/src/rules/transition.js +59 -0
  47. package/src/rules/units.js +81 -0
  48. package/src/rules/untrusted.js +59 -0
  49. package/src/rules/vague.js +63 -0
  50. package/src/rules/weak-verb.js +62 -0
  51. package/src/version.js +2 -2
@@ -0,0 +1,57 @@
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
+ * Ordered.
14
+ *
15
+ * Demands a numbered list when order matters. Models follow numbered,
16
+ * sequentially ordered steps far more reliably than unordered bullets,
17
+ * and shuffling steps drops accuracy sharply. A standalone checker flags
18
+ * an unordered bullet item that carries a sequence marker like "first",
19
+ * "then", "next", "after that", "finally", or "step 2", since the order
20
+ * is real but the structure hides it. Its prompt hands implicit ordering
21
+ * with no marker word to the AI oracle.
22
+ */
23
+ class Ordered {
24
+ constructor() {
25
+ this.id = 'ordered';
26
+ }
27
+ prompt() {
28
+ return `${this.id}: flag an implied sequence that no marker word signals, demanding a numbered list whenever the order of steps matters`;
29
+ }
30
+ violations(document) {
31
+ const uri = document.uri();
32
+ return document.walk({
33
+ header: () => [],
34
+ prose: (text, line) => this.scan(text, line, uri),
35
+ snippet: () => [],
36
+ bullets: () => [],
37
+ frontmatter: () => []
38
+ });
39
+ }
40
+ scan(text, line, uri) {
41
+ if (!/^\s*[-*+]\s+/u.test(text)) {
42
+ return [];
43
+ }
44
+ const markers = /\b(?:first|second|third|then|next|after that|afterwards|finally|lastly|step\s+\d+)\b/iu;
45
+ if (!markers.test(mask(text))) {
46
+ return [];
47
+ }
48
+ return [new Violation(
49
+ this.id,
50
+ 'warning',
51
+ 'sequence detected, use a numbered list to fix the order',
52
+ new Region(uri, line, 1)
53
+ )];
54
+ }
55
+ }
56
+
57
+ module.exports = Ordered;
@@ -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
+ * Persona.
14
+ *
15
+ * Flags gratuitous role-play that opens a manifesto with a persona:
16
+ * "You are a senior engineer", "Act as an expert reviewer", and the
17
+ * like. The largest controlled study finds personas do not improve task
18
+ * performance and can hurt, so a role-play line is pure context bloat
19
+ * that adds no instruction. A standalone checker flags the line whose
20
+ * head assigns the agent a role; its prompt hands the indirect persona
21
+ * framing the regex misses to the AI oracle.
22
+ */
23
+ class Persona {
24
+ constructor() {
25
+ this.id = 'persona';
26
+ }
27
+ prompt() {
28
+ 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
+ }
30
+ violations(document) {
31
+ const uri = document.uri();
32
+ return document.walk({
33
+ header: () => [],
34
+ prose: (text, line) => this.scan(text, line, uri),
35
+ snippet: () => [],
36
+ bullets: () => [],
37
+ frontmatter: () => []
38
+ });
39
+ }
40
+ scan(text, line, uri) {
41
+ const regex = /^(?<marker>\s*(?:[-*+]|\d+\.)\s+)?(?:you are an? |act as |imagine you are |pretend to be |as an? \w+,)/iu;
42
+ const hit = regex.exec(mask(text));
43
+ if (hit === null) {
44
+ return [];
45
+ }
46
+ return [new Violation(
47
+ this.id,
48
+ 'warning',
49
+ 'persona assignment adds no instruction, delete it',
50
+ new Region(uri, line, (hit.groups.marker || '').length + 1)
51
+ )];
52
+ }
53
+ }
54
+
55
+ module.exports = Persona;
@@ -0,0 +1,62 @@
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
+ prompt() {
31
+ 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`;
32
+ }
33
+ violations(document) {
34
+ const uri = document.uri();
35
+ const total = document.text().split('\n').length;
36
+ return document.walk({
37
+ header: (text, row) => this.check(text, row, total, uri),
38
+ prose: () => [],
39
+ snippet: () => [],
40
+ bullets: () => [],
41
+ frontmatter: () => []
42
+ });
43
+ }
44
+ check(text, row, total, uri) {
45
+ if (!this.keyword.test(text)) {
46
+ return [];
47
+ }
48
+ const ratio = row / total;
49
+ if (ratio <= 1 / 3 || ratio >= 2 / 3) {
50
+ return [];
51
+ }
52
+ const name = text.replace(/^#+\s*/u, '').trim();
53
+ return [new Violation(
54
+ this.id,
55
+ 'warning',
56
+ `critical section "${name}" is buried, move it to the top or bottom`,
57
+ new Region(uri, row, 1)
58
+ )];
59
+ }
60
+ }
61
+
62
+ module.exports = Placement;
@@ -0,0 +1,57 @@
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
+ * Positive.
14
+ *
15
+ * Demands positive, goal-oriented imperatives over bans. A standalone
16
+ * checker flags a line whose head is an obvious prohibition: "do not",
17
+ * "don't", "never", "avoid", "refrain from", "must not", or "no longer".
18
+ * A ban forces the model to process the forbidden concept first, so
19
+ * "Only use real data" beats "Don't use mock data". Its prompt hands
20
+ * subtler bans, those carrying no head keyword, to the AI
21
+ * oracle, which rewrites a prohibition with no keyword as a positive
22
+ * command.
23
+ */
24
+ class Positive {
25
+ constructor() {
26
+ this.id = 'positive';
27
+ }
28
+ prompt() {
29
+ return `${this.id}: flag any instruction phrased as a prohibition, including bans carrying no fixed keyword, and rewrite each as a positive imperative`;
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 = /^(?<marker>\s*(?:[-*+]|\d+\.)\s+)?(?:do not|don't|never|avoid|refrain from|must not|no longer)\b/iu;
43
+ const hit = regex.exec(mask(text));
44
+ if (hit === null) {
45
+ return [];
46
+ }
47
+ const marker = hit.groups.marker || '';
48
+ return [new Violation(
49
+ this.id,
50
+ 'warning',
51
+ 'negative phrasing detected, state the positive command instead',
52
+ new Region(uri, line, marker.length + 1)
53
+ )];
54
+ }
55
+ }
56
+
57
+ module.exports = Positive;
@@ -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
+
11
+ /**
12
+ * PseudoHeading.
13
+ *
14
+ * Rejects a bold line posing as a section heading, such as
15
+ * "**Setup:**" standing alone. The whole line, once an optional list
16
+ * marker drops, must sit inside one emphasis run, wrapped by "**" or
17
+ * "__" and ending in an optional colon. A line carrying only inline
18
+ * bold inside other words, like "Use **bold** sparingly.", stays free,
19
+ * since the emphasis wraps a fragment, not the whole label.
20
+ *
21
+ * Its prompt hands borderline label-versus-instruction calls to the AI
22
+ * oracle.
23
+ */
24
+ class PseudoHeading {
25
+ constructor() {
26
+ this.id = 'pseudo-heading';
27
+ }
28
+ prompt() {
29
+ 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
+ }
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 body = text.trim().replace(/^(?:[-*+]|\d+\.)\s+/u, '');
43
+ if (!/^(?<fence>\*\*|__)(?!\s)(?:.+?)(?<!\s)\k<fence>:?$/u.test(body)) {
44
+ return [];
45
+ }
46
+ return [new Violation(
47
+ this.id,
48
+ 'warning',
49
+ 'bold pseudo-heading found, use a level-2 "##" heading',
50
+ new Region(uri, line, 1)
51
+ )];
52
+ }
53
+ }
54
+
55
+ module.exports = PseudoHeading;
@@ -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
+ * 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
+ prompt() {
29
+ 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`;
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 found = [];
43
+ const regex = new RegExp(
44
+ '\\b(?:some|several|a few|a couple|many|multiple|various|' +
45
+ 'numerous|a lot of|plenty of)\\b',
46
+ 'giu'
47
+ );
48
+ const masked = mask(text);
49
+ let hit = regex.exec(masked);
50
+ while (hit !== null) {
51
+ found.push(new Violation(
52
+ this.id,
53
+ 'warning',
54
+ `vague quantity "${hit[0]}", state an exact number or threshold`,
55
+ new Region(uri, line, hit.index + 1)
56
+ ));
57
+ hit = regex.exec(masked);
58
+ }
59
+ return found;
60
+ }
61
+ }
62
+
63
+ module.exports = Quantifier;
@@ -0,0 +1,54 @@
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
+ * Rationale.
14
+ *
15
+ * Demands orders, not explanations. A standalone checker flags a line
16
+ * that opens with a justification marker such as "because", "the
17
+ * reason", "this keeps", "this ensures", "this helps", "so that", or
18
+ * "in order to", since such a line argues a point instead of issuing a
19
+ * command. Justification belongs in commit messages and design docs, so
20
+ * its prompt hands subtler explanation-only lines to the AI oracle.
21
+ */
22
+ class Rationale {
23
+ constructor() {
24
+ this.id = 'rationale';
25
+ }
26
+ prompt() {
27
+ 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
+ }
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 clean = mask(text).replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '');
41
+ const regex = /^(?:because|the reason|this keeps|this ensures|this helps|so that|in order to)\b/iu;
42
+ if (!regex.test(clean)) {
43
+ return [];
44
+ }
45
+ return [new Violation(
46
+ this.id,
47
+ 'warning',
48
+ 'rationale carries no command, delete or convert to an order',
49
+ new Region(uri, line, 1)
50
+ )];
51
+ }
52
+ }
53
+
54
+ module.exports = Rationale;
@@ -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
+ const mask = require('../mask');
11
+
12
+ /**
13
+ * Referential.
14
+ *
15
+ * Demands that every line name its own subject. A standalone checker
16
+ * flags a line that opens with a bare pronoun acting as the subject:
17
+ * "it", "they", and "them" always, and "this", "that", "these", or
18
+ * "those" only when a verb follows rather than a noun, so a determiner
19
+ * like "These rules stay final" stays clean. Such a pronoun points at a
20
+ * previous line, breaking the "one line, one instruction" contract. Its
21
+ * prompt hands the subtler mid-line dangling references to the AI oracle.
22
+ */
23
+ class Referential {
24
+ constructor() {
25
+ this.id = 'referential';
26
+ this.verbs = new RegExp(
27
+ '^(?:is|are|was|were|be|been|being|will|would|can|could|shall|' +
28
+ 'should|must|may|might|has|have|had|do|does|did|only|then|runs|' +
29
+ 'run|applies|apply|happens|happen|means|requires|needs|comes|' +
30
+ 'goes|makes|breaks|points|refers)$',
31
+ 'iu'
32
+ );
33
+ }
34
+ prompt() {
35
+ return `${this.id}: flag any line whose subject is a pronoun with no antecedent on the same line, including mid-line dangling references a head pattern misses`;
36
+ }
37
+ violations(document) {
38
+ const uri = document.uri();
39
+ return document.walk({
40
+ header: () => [],
41
+ prose: (text, line) => this.scan(text, line, uri),
42
+ snippet: () => [],
43
+ bullets: () => [],
44
+ frontmatter: () => []
45
+ });
46
+ }
47
+ scan(text, line, uri) {
48
+ const regex = /^(?<marker>\s*(?:[-*+]|\d+\.)\s+)?(?<pro>it|this|that|they|them|these|those)\b\s+(?<next>[\w']+)/iu;
49
+ const hit = regex.exec(mask(text));
50
+ if (hit === null) {
51
+ return [];
52
+ }
53
+ const pronoun = hit.groups.pro.toLowerCase();
54
+ const ambiguous = /^(?:this|that|these|those)$/u.test(pronoun);
55
+ if (ambiguous && !this.verbs.test(hit.groups.next)) {
56
+ return [];
57
+ }
58
+ return [new Violation(
59
+ this.id,
60
+ 'warning',
61
+ `pronoun "${hit.groups.pro}" has no antecedent on this line, name the subject`,
62
+ new Region(uri, line, (hit.groups.marker || '').length + 1)
63
+ )];
64
+ }
65
+ }
66
+
67
+ module.exports = Referential;
@@ -0,0 +1,31 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Scope.
10
+ *
11
+ * Demands that one SKILL.md stay bound to a single coherent
12
+ * responsibility. Agent reliability scales with specialisation: a
13
+ * well-scoped single-stage skill beats a monolith that conflates
14
+ * unrelated subtasks. Whether the sections cohere or diverge is not
15
+ * visible line by line, so this check is pure judgement: prompt()
16
+ * defers the verdict to the AI oracle and violations() finds nothing
17
+ * on its own.
18
+ */
19
+ class Scope {
20
+ constructor() {
21
+ this.id = 'scope';
22
+ }
23
+ prompt() {
24
+ return `${this.id}: in a SKILL.md, judge whether the sections describe a single coherent responsibility or several unrelated ones, and recommend a split when they diverge`;
25
+ }
26
+ violations() {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ module.exports = Scope;
@@ -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
+ * SelfContained.
14
+ *
15
+ * Demands that every line stand on its own without leaning on its
16
+ * neighbours. A standalone checker flags a relative cross-reference
17
+ * phrase like "see above", "as mentioned below", or "the previous
18
+ * step" that breaks the moment the file is reordered or chunked. A
19
+ * line pointing somewhere concrete through a markdown link stays
20
+ * clean. Distinct from referential, which targets bare pronouns; this
21
+ * one targets positional cross-references. Its prompt hands subtler
22
+ * dangling references to the AI oracle.
23
+ */
24
+ class SelfContained {
25
+ constructor() {
26
+ this.id = 'self-contained';
27
+ this.phrase = new RegExp(
28
+ 'mentioned above|mentioned below|see above|see below|' +
29
+ 'as discussed|the section above|the section below|' +
30
+ 'the previous step|as stated earlier|mentioned earlier|' +
31
+ 'refer to the guide',
32
+ 'iu'
33
+ );
34
+ }
35
+ prompt() {
36
+ return `${this.id}: flag any line leaning on a relative cross-reference such as "see above" that breaks when the file is reordered or chunked, deferring subtler dangling references to the oracle`;
37
+ }
38
+ violations(document) {
39
+ const uri = document.uri();
40
+ return document.walk({
41
+ header: () => [],
42
+ prose: (text, line) => this.scan(text, line, uri),
43
+ snippet: () => [],
44
+ bullets: () => [],
45
+ frontmatter: () => []
46
+ });
47
+ }
48
+ scan(text, line, uri) {
49
+ const clean = mask(text);
50
+ if (clean.includes('](')) {
51
+ return [];
52
+ }
53
+ const hit = this.phrase.exec(clean);
54
+ if (hit === null) {
55
+ return [];
56
+ }
57
+ return [new Violation(
58
+ this.id,
59
+ 'warning',
60
+ `relative reference "${hit[0]}" breaks when reordered, name the target`,
61
+ new Region(uri, line, hit.index + 1)
62
+ )];
63
+ }
64
+ }
65
+
66
+ module.exports = SelfContained;
@@ -0,0 +1,62 @@
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
+ * Stale.
14
+ *
15
+ * Flags volatile time and version references that rot over time:
16
+ * words like "currently", "now", "today", "recently", and hardcoded
17
+ * version literals such as "18.17.0". Each pins an instruction to a
18
+ * moment or release that drifts, so the manifesto silently ages. The
19
+ * rule scans only prose, never fenced snippets, so version pins inside
20
+ * code blocks survive untouched. Its prompt hands implicit time-bound
21
+ * claims with no keyword to the AI oracle.
22
+ */
23
+ class Stale {
24
+ constructor() {
25
+ this.id = 'stale';
26
+ }
27
+ prompt() {
28
+ return `${this.id}: flag any implicit time-bound or version-bound claim that carries no keyword, and propose a durable rule that never rots`;
29
+ }
30
+ violations(document) {
31
+ const uri = document.uri();
32
+ return document.walk({
33
+ header: () => [],
34
+ prose: (text, line) => this.scan(text, line, uri),
35
+ snippet: () => [],
36
+ bullets: () => [],
37
+ frontmatter: () => []
38
+ });
39
+ }
40
+ scan(text, line, uri) {
41
+ const found = [];
42
+ const regex = new RegExp(
43
+ '\\b(?:currently|now|today|recently|lately|at present|as of|' +
44
+ 'the latest)\\b|\\bv?\\d+\\.\\d+(?:\\.\\d+)?\\b',
45
+ 'giu'
46
+ );
47
+ const masked = mask(text);
48
+ let hit = regex.exec(masked);
49
+ while (hit !== null) {
50
+ found.push(new Violation(
51
+ this.id,
52
+ 'warning',
53
+ `volatile reference "${hit[0]}" will rot, state a durable rule`,
54
+ new Region(uri, line, hit.index + 1)
55
+ ));
56
+ hit = regex.exec(masked);
57
+ }
58
+ return found;
59
+ }
60
+ }
61
+
62
+ module.exports = Stale;