@yegor256/dogent 0.9.0 → 0.10.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 +17 -0
- package/package.json +3 -2
- package/src/args.js +21 -5
- package/src/dogent.js +23 -13
- package/src/report.js +8 -2
- package/src/rules/budget.js +50 -0
- package/src/rules/concise.js +48 -0
- package/src/rules/counter-example.js +60 -0
- package/src/rules/done.js +53 -0
- package/src/rules/emphasis.js +81 -0
- package/src/rules/example.js +60 -0
- package/src/rules/format.js +68 -0
- package/src/rules/index.js +40 -0
- package/src/rules/jargon.js +105 -0
- package/src/rules/name-matches-dir.js +1 -1
- package/src/rules/ordered.js +57 -0
- package/src/rules/persona.js +55 -0
- package/src/rules/positive.js +57 -0
- package/src/rules/pseudo-heading.js +55 -0
- package/src/rules/rationale.js +54 -0
- package/src/rules/referential.js +67 -0
- package/src/rules/self-contained.js +66 -0
- package/src/rules/stale.js +62 -0
- package/src/rules/terms.js +77 -0
- package/src/rules/tool-clarity.js +61 -0
- package/src/rules/untrusted.js +59 -0
- package/src/rules/vague.js +63 -0
- package/src/version.js +2 -2
package/src/rules/index.js
CHANGED
|
@@ -21,6 +21,7 @@ const NameMatchesDir = require('./name-matches-dir');
|
|
|
21
21
|
const Polite = require('./polite');
|
|
22
22
|
const Unfinished = require('./unfinished');
|
|
23
23
|
const Crowded = require('./crowded');
|
|
24
|
+
const Budget = require('./budget');
|
|
24
25
|
const DescriptionTriggers = require('./description-triggers');
|
|
25
26
|
const Atomic = require('./atomic');
|
|
26
27
|
const Hedging = require('./hedging');
|
|
@@ -29,6 +30,25 @@ const Unique = require('./unique');
|
|
|
29
30
|
const Consistent = require('./consistent');
|
|
30
31
|
const Simple = require('./simple');
|
|
31
32
|
const SectionLevel = require('./section-level');
|
|
33
|
+
const Format = require('./format');
|
|
34
|
+
const Untrusted = require('./untrusted');
|
|
35
|
+
const Ordered = require('./ordered');
|
|
36
|
+
const Emphasis = require('./emphasis');
|
|
37
|
+
const Persona = require('./persona');
|
|
38
|
+
const Concise = require('./concise');
|
|
39
|
+
const Example = require('./example');
|
|
40
|
+
const Referential = require('./referential');
|
|
41
|
+
const Vague = require('./vague');
|
|
42
|
+
const Positive = require('./positive');
|
|
43
|
+
const Done = require('./done');
|
|
44
|
+
const Terms = require('./terms');
|
|
45
|
+
const Jargon = require('./jargon');
|
|
46
|
+
const PseudoHeading = require('./pseudo-heading');
|
|
47
|
+
const Stale = require('./stale');
|
|
48
|
+
const ToolClarity = require('./tool-clarity');
|
|
49
|
+
const CounterExample = require('./counter-example');
|
|
50
|
+
const Rationale = require('./rationale');
|
|
51
|
+
const SelfContained = require('./self-contained');
|
|
32
52
|
|
|
33
53
|
module.exports = () => [
|
|
34
54
|
new Grouped(),
|
|
@@ -37,6 +57,7 @@ module.exports = () => [
|
|
|
37
57
|
new SectionLevel(),
|
|
38
58
|
new LineLength(80),
|
|
39
59
|
new TokenCount(4000),
|
|
60
|
+
new Concise(200),
|
|
40
61
|
new NoArticles(),
|
|
41
62
|
new Command(),
|
|
42
63
|
new Punctuation(),
|
|
@@ -44,14 +65,33 @@ module.exports = () => [
|
|
|
44
65
|
new Redundant(),
|
|
45
66
|
new Consistent(),
|
|
46
67
|
new Simple(),
|
|
68
|
+
new Referential(),
|
|
47
69
|
new NameMatchesDir(),
|
|
48
70
|
new Polite(),
|
|
49
71
|
new Unfinished(),
|
|
50
72
|
new Crowded(10),
|
|
73
|
+
new Budget(60),
|
|
51
74
|
new DescriptionTriggers(),
|
|
75
|
+
new Example(),
|
|
76
|
+
new Format(),
|
|
52
77
|
new Atomic(),
|
|
78
|
+
new Ordered(),
|
|
53
79
|
new Hedging(),
|
|
80
|
+
new Vague(),
|
|
81
|
+
new ToolClarity(),
|
|
54
82
|
new Passive(),
|
|
83
|
+
new Untrusted(),
|
|
84
|
+
new Emphasis(),
|
|
85
|
+
new Persona(),
|
|
86
|
+
new Positive(),
|
|
87
|
+
new Done(),
|
|
88
|
+
new Terms(),
|
|
89
|
+
new Jargon(),
|
|
90
|
+
new PseudoHeading(),
|
|
91
|
+
new Stale(),
|
|
92
|
+
new CounterExample(),
|
|
93
|
+
new Rationale(),
|
|
94
|
+
new SelfContained(),
|
|
55
95
|
new Unique(),
|
|
56
96
|
new Frontmatter(
|
|
57
97
|
'SKILL.md',
|
|
@@ -0,0 +1,105 @@
|
|
|
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 ALLOWLIST = new Set([
|
|
13
|
+
'AI',
|
|
14
|
+
'CI',
|
|
15
|
+
'CD',
|
|
16
|
+
'CLI',
|
|
17
|
+
'API',
|
|
18
|
+
'URL',
|
|
19
|
+
'URI',
|
|
20
|
+
'HTTP',
|
|
21
|
+
'HTTPS',
|
|
22
|
+
'JSON',
|
|
23
|
+
'YAML',
|
|
24
|
+
'XML',
|
|
25
|
+
'HTML',
|
|
26
|
+
'CSS',
|
|
27
|
+
'SQL',
|
|
28
|
+
'ID',
|
|
29
|
+
'OK',
|
|
30
|
+
'OS',
|
|
31
|
+
'IO',
|
|
32
|
+
'NPM',
|
|
33
|
+
'PR',
|
|
34
|
+
'MIT',
|
|
35
|
+
'SARIF',
|
|
36
|
+
'SKILL',
|
|
37
|
+
'CLAUDE'
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const defined = (masked) => {
|
|
41
|
+
const found = new Set();
|
|
42
|
+
const regex = /\b(?<acronym>[A-Z]{2,})\s*\(/gu;
|
|
43
|
+
let hit = regex.exec(masked);
|
|
44
|
+
while (hit !== null) {
|
|
45
|
+
found.add(hit.groups.acronym);
|
|
46
|
+
hit = regex.exec(masked);
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const undefining = (acronym, scope) => !scope.known.has(acronym) &&
|
|
52
|
+
!ALLOWLIST.has(acronym);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Jargon.
|
|
56
|
+
*
|
|
57
|
+
* Flags an acronym that lands in prose without ever being expanded. An
|
|
58
|
+
* 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
|
|
61
|
+
* in a built-in allowlist and pass untouched. Only the first unexpanded
|
|
62
|
+
* occurrence of each acronym is reported. Its prompt hands non-acronym
|
|
63
|
+
* domain jargon, the rare nouns a reader cannot parse, to the AI oracle.
|
|
64
|
+
*/
|
|
65
|
+
class Jargon {
|
|
66
|
+
constructor() {
|
|
67
|
+
this.id = 'jargon';
|
|
68
|
+
}
|
|
69
|
+
prompt() {
|
|
70
|
+
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
|
+
}
|
|
72
|
+
violations(document) {
|
|
73
|
+
const uri = document.uri();
|
|
74
|
+
const known = defined(mask(document.text()));
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
return document.walk({
|
|
77
|
+
header: () => [],
|
|
78
|
+
prose: (text, line) => this.scan(text, line, {uri, known, seen}),
|
|
79
|
+
snippet: () => [],
|
|
80
|
+
bullets: () => [],
|
|
81
|
+
frontmatter: () => []
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
scan(text, line, scope) {
|
|
85
|
+
const hits = [...mask(text).matchAll(/\b[A-Z]{2,}\b/gu)];
|
|
86
|
+
return hits.reduce((found, hit) => {
|
|
87
|
+
const [acronym] = hit;
|
|
88
|
+
const novel = !scope.seen.has(acronym) && undefining(acronym, scope);
|
|
89
|
+
scope.seen.add(acronym);
|
|
90
|
+
return novel
|
|
91
|
+
? found.concat(this.flag(acronym, new Region(scope.uri, line, hit.index + 1)))
|
|
92
|
+
: found;
|
|
93
|
+
}, []);
|
|
94
|
+
}
|
|
95
|
+
flag(acronym, region) {
|
|
96
|
+
return new Violation(
|
|
97
|
+
this.id,
|
|
98
|
+
'warning',
|
|
99
|
+
`acronym "${acronym}" never expanded, define it on first use`,
|
|
100
|
+
region
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = Jargon;
|
|
@@ -45,7 +45,7 @@ class NameMatchesDir {
|
|
|
45
45
|
return this.mismatch(uri, name);
|
|
46
46
|
}
|
|
47
47
|
mismatch(uri, name) {
|
|
48
|
-
const parent = path.basename(path.dirname(uri));
|
|
48
|
+
const parent = path.basename(path.dirname(path.resolve(uri)));
|
|
49
49
|
if (!name || name.value === parent) {
|
|
50
50
|
return [];
|
|
51
51
|
}
|
|
@@ -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,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,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,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;
|