@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.
- package/README.md +104 -12
- package/package.json +3 -2
- package/src/args.js +35 -4
- package/src/defaults.js +47 -0
- package/src/dogent.js +42 -16
- package/src/openai.js +8 -5
- package/src/prompt.js +0 -4
- package/src/report.js +8 -2
- package/src/rules/ambiguous-or.js +58 -0
- package/src/rules/budget.js +50 -0
- package/src/rules/concise.js +48 -0
- package/src/rules/conditional.js +55 -0
- package/src/rules/consistent.js +1 -1
- package/src/rules/counter-example.js +60 -0
- package/src/rules/default.js +60 -0
- package/src/rules/description-length.js +64 -0
- package/src/rules/description-voice.js +67 -0
- package/src/rules/done.js +53 -0
- package/src/rules/duplicate-section.js +65 -0
- package/src/rules/emoji.js +60 -0
- package/src/rules/emphasis.js +81 -0
- package/src/rules/example-format.js +32 -0
- package/src/rules/example.js +60 -0
- package/src/rules/external-link.js +57 -0
- package/src/rules/fence-language.js +55 -0
- package/src/rules/format.js +68 -0
- package/src/rules/hidden-char.js +61 -0
- package/src/rules/homoglyph.js +82 -0
- package/src/rules/index.js +80 -0
- package/src/rules/inline-code.js +79 -0
- package/src/rules/jargon.js +115 -0
- package/src/rules/meta-reference.js +57 -0
- package/src/rules/ordered.js +57 -0
- package/src/rules/persona.js +55 -0
- package/src/rules/placement.js +62 -0
- package/src/rules/positive.js +57 -0
- package/src/rules/pseudo-heading.js +55 -0
- package/src/rules/quantifier.js +63 -0
- package/src/rules/rationale.js +54 -0
- package/src/rules/referential.js +67 -0
- package/src/rules/scope.js +31 -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/transition.js +59 -0
- package/src/rules/units.js +81 -0
- package/src/rules/untrusted.js +59 -0
- package/src/rules/vague.js +63 -0
- package/src/rules/weak-verb.js +62 -0
- 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;
|