@yegor256/dogent 0.6.2 → 0.7.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 +11 -0
- package/package.json +1 -1
- package/src/answer.js +11 -2
- package/src/markdown.js +3 -2
- package/src/prompt.js +31 -8
- package/src/rules/atomic.js +57 -0
- package/src/rules/command.js +1 -1
- package/src/rules/consistent.js +30 -0
- package/src/rules/crowded.js +67 -0
- package/src/rules/description-triggers.js +64 -0
- package/src/rules/grouped.js +4 -1
- package/src/rules/hedging.js +61 -0
- package/src/rules/index.js +25 -1
- package/src/rules/name-format.js +59 -0
- package/src/rules/name-matches-dir.js +61 -0
- package/src/rules/passive.js +54 -0
- package/src/rules/polite.js +56 -0
- package/src/rules/short-sections.js +4 -1
- package/src/rules/simple.js +57 -0
- package/src/rules/unfinished.js +59 -0
- package/src/rules/unique.js +73 -0
- package/src/usage.js +2 -2
package/README.md
CHANGED
|
@@ -76,8 +76,19 @@ The command exits with a non-zero status when problems are found,
|
|
|
76
76
|
- Every sentence must start with a capital and end with a period.
|
|
77
77
|
- No articles, no noise, no bloated text.
|
|
78
78
|
- Simple grammar, no ambiguity.
|
|
79
|
+
- No tangled, multi-clause instructions.
|
|
80
|
+
- A `SKILL.md` `name` must equal its parent directory.
|
|
81
|
+
- No courtesy or scaffolding words.
|
|
82
|
+
- No leftover markers or unfilled placeholders.
|
|
83
|
+
- A section must hold at most ten instructions.
|
|
84
|
+
- A `SKILL.md` `description` must say when to use the skill.
|
|
85
|
+
- Every line must carry exactly one instruction.
|
|
86
|
+
- No hedging or soft wording.
|
|
87
|
+
- No passive voice; use the active imperative.
|
|
88
|
+
- No instruction may repeat another.
|
|
79
89
|
- `SKILL.md` must open with valid frontmatter.
|
|
80
90
|
- Frontmatter must declare only allowed keys.
|
|
91
|
+
- A `SKILL.md` `name` must be kebab-case.
|
|
81
92
|
|
|
82
93
|
## AI verification
|
|
83
94
|
|
package/package.json
CHANGED
package/src/answer.js
CHANGED
|
@@ -8,16 +8,22 @@
|
|
|
8
8
|
const Violation = require('./violation');
|
|
9
9
|
const Region = require('./region');
|
|
10
10
|
|
|
11
|
+
const FLOOR = 0.6;
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Answer.
|
|
13
15
|
*
|
|
14
16
|
* The oracle's raw reply, treated as untrusted input. Parses the JSON
|
|
15
17
|
* object it carries and turns every well-formed SARIF result back into a
|
|
16
|
-
* native violation, ignoring any result it cannot read.
|
|
18
|
+
* native violation, ignoring any result it cannot read. Drops a result
|
|
19
|
+
* whose self-reported confidence sits below the floor, so the model's
|
|
20
|
+
* own doubt filters out its guesses; a result without a confidence is
|
|
21
|
+
* trusted and kept.
|
|
17
22
|
*/
|
|
18
23
|
class Answer {
|
|
19
|
-
constructor(raw) {
|
|
24
|
+
constructor(raw, floor = FLOOR) {
|
|
20
25
|
this.raw = raw;
|
|
26
|
+
this.floor = floor;
|
|
21
27
|
}
|
|
22
28
|
violations() {
|
|
23
29
|
return this.results().flatMap((result) => {
|
|
@@ -27,6 +33,9 @@ class Answer {
|
|
|
27
33
|
if (typeof line !== 'number' || typeof text !== 'string' || !spot.artifactLocation) {
|
|
28
34
|
return [];
|
|
29
35
|
}
|
|
36
|
+
if (typeof result.confidence === 'number' && result.confidence < this.floor) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
30
39
|
return [new Violation(
|
|
31
40
|
result.ruleId || 'oracle',
|
|
32
41
|
result.level || 'warning',
|
package/src/markdown.js
CHANGED
|
@@ -42,7 +42,8 @@ class Markdown {
|
|
|
42
42
|
items = [];
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
|
-
const
|
|
45
|
+
const body = this.content.replace(/\r\n?/gu, '\n');
|
|
46
|
+
const lines = body.split('\n');
|
|
46
47
|
let skip = 0;
|
|
47
48
|
if (lines[0].trim() === '---') {
|
|
48
49
|
const rest = lines.slice(1);
|
|
@@ -98,7 +99,7 @@ class Markdown {
|
|
|
98
99
|
if (fence !== '') {
|
|
99
100
|
pieces.push(new Snippet(block.join('\n'), opened));
|
|
100
101
|
}
|
|
101
|
-
return new Document(this.address, pieces,
|
|
102
|
+
return new Document(this.address, pieces, body);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
package/src/prompt.js
CHANGED
|
@@ -25,14 +25,35 @@ class Prompt {
|
|
|
25
25
|
return [
|
|
26
26
|
'You are a strict linter for an AI-agent manifesto.',
|
|
27
27
|
`The file under review is "${uri}".`,
|
|
28
|
+
'The manifesto is written in a terse house style: each line is',
|
|
29
|
+
'one compressed imperative command, with articles and filler',
|
|
30
|
+
'words deliberately stripped. Read the first word of every line',
|
|
31
|
+
'as an imperative verb, so "Tag release before shipping" is the',
|
|
32
|
+
'order "tag the release", not a noun phrase about a tag. Never',
|
|
33
|
+
'flag a line for being short, for dropping articles, or for',
|
|
34
|
+
'omitting a subject; that compression is the required style.',
|
|
35
|
+
'A heading opens a section, and every line beneath it, until the',
|
|
36
|
+
'next heading, belongs to that section. Treat a deeper heading as',
|
|
37
|
+
'a subsection of the one above, never as a misplaced instruction.',
|
|
28
38
|
'Apply only the checks listed below this header.',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
39
|
+
'Most manifestos are clean. An empty result list is the normal,',
|
|
40
|
+
'expected reply: when no line clearly breaks a listed check,',
|
|
41
|
+
'report nothing and return {"results": []}. Never invent a',
|
|
42
|
+
'violation to seem useful, and never flag a line merely for',
|
|
43
|
+
'sitting in a plain, on-topic section.',
|
|
44
|
+
'One clash is never a false alarm: when two lines give the same',
|
|
45
|
+
'order twice, or demand opposite things, report each offending',
|
|
46
|
+
'line with high confidence, even while every other check stays',
|
|
47
|
+
'conservative. A clean file has no such clash and stays silent.',
|
|
48
|
+
'Report a violation only when you are certain it breaks a check.',
|
|
49
|
+
'Give every result a "confidence" number from 0 to 1, your own',
|
|
50
|
+
'probability that the violation is real, and omit any result you',
|
|
51
|
+
'score below 0.6. When unsure, lower the confidence, never guess.',
|
|
31
52
|
'Reply with one JSON object and nothing else, shaped as',
|
|
32
53
|
'{"results":[ ... ]}, where each item is a SARIF result with',
|
|
33
|
-
'keys ruleId, level "warning", message.text, and
|
|
34
|
-
'Set ruleId to the rule name and startLine to the
|
|
35
|
-
'line number; locations[0].physicalLocation must carry',
|
|
54
|
+
'keys ruleId, level "warning", message.text, confidence, and',
|
|
55
|
+
'locations. Set ruleId to the rule name and startLine to the',
|
|
56
|
+
'printed line number; locations[0].physicalLocation must carry',
|
|
36
57
|
`artifactLocation.uri "${uri}" and region.startColumn 1.`
|
|
37
58
|
].join('\n');
|
|
38
59
|
}
|
|
@@ -43,9 +64,11 @@ class Prompt {
|
|
|
43
64
|
.join('\n');
|
|
44
65
|
}
|
|
45
66
|
body() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
67
|
+
const lines = this.doc.text().split('\n');
|
|
68
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
69
|
+
lines.pop();
|
|
70
|
+
}
|
|
71
|
+
return lines
|
|
49
72
|
.map((line, index) => `${index + 1}: ${line}`)
|
|
50
73
|
.join('\n');
|
|
51
74
|
}
|
|
@@ -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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Atomic.
|
|
13
|
+
*
|
|
14
|
+
* Demands that every line carry exactly one instruction. A standalone
|
|
15
|
+
* checker only spots the loud signs: a sentence terminator sitting
|
|
16
|
+
* mid-line with more text after it, or two verb phrases welded together
|
|
17
|
+
* with a semicolon, an " and ", or a " then ". The prompt hands the
|
|
18
|
+
* subtler clause-counting to the AI oracle.
|
|
19
|
+
*
|
|
20
|
+
* @todo #21:45min Upgrade to a real clause-count check through an AI
|
|
21
|
+
* oracle so that subtle multi-instruction lines, which the conservative
|
|
22
|
+
* heuristic cannot see today, are reliably caught, as requested in
|
|
23
|
+
* issue #21.
|
|
24
|
+
*/
|
|
25
|
+
class Atomic {
|
|
26
|
+
constructor() {
|
|
27
|
+
this.id = 'atomic';
|
|
28
|
+
}
|
|
29
|
+
prompt() {
|
|
30
|
+
return `${this.id}: flag any line that carries more than one instruction`;
|
|
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 = text.replace(/^\s*(?:[-*+]|\d+\.)\s+/u, '').trimEnd();
|
|
44
|
+
const welded = /;|\s(?:and|then)\s+[a-z]+\s+\S/u;
|
|
45
|
+
if (!/[.!?]\s+\S/u.test(clean) && !welded.test(clean)) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
'line carries more than one instruction',
|
|
52
|
+
new Region(uri, line, 1)
|
|
53
|
+
)];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = Atomic;
|
package/src/rules/command.js
CHANGED
|
@@ -21,7 +21,7 @@ class Command {
|
|
|
21
21
|
this.id = 'command';
|
|
22
22
|
}
|
|
23
23
|
prompt() {
|
|
24
|
-
return `${this.id}: flag any line that reads as a description, a question, or a plain statement rather than a direct order`;
|
|
24
|
+
return `${this.id}: flag any line that reads as a description, a question, or a plain statement rather than a direct order; a line opening with a base-form imperative verb, such as "Write", "Strip", "Drop", or "Keep", is itself a direct order and must never be flagged`;
|
|
25
25
|
}
|
|
26
26
|
violations(document) {
|
|
27
27
|
const uri = document.uri();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Consistent.
|
|
10
|
+
*
|
|
11
|
+
* Demands that the manifesto never say one thing twice nor say two
|
|
12
|
+
* things that fight each other. A duplicate instruction wastes the
|
|
13
|
+
* context budget; a contradictory pair leaves the agent guessing which
|
|
14
|
+
* order wins. Neither is visible line by line, so this check is pure
|
|
15
|
+
* judgement: prompt() hands the whole comparison to the AI oracle and
|
|
16
|
+
* violations() finds nothing on its own.
|
|
17
|
+
*/
|
|
18
|
+
class Consistent {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.id = 'consistent';
|
|
21
|
+
}
|
|
22
|
+
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`;
|
|
24
|
+
}
|
|
25
|
+
violations() {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = Consistent;
|
|
@@ -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
|
+
* Crowded.
|
|
13
|
+
*
|
|
14
|
+
* Demands that every section stay small, holding no more instructions
|
|
15
|
+
* than the limit allows. A section runs from one heading to the next
|
|
16
|
+
* heading or to end-of-file; its instructions are the prose lines that
|
|
17
|
+
* sit between them. Prose before the first heading is loose and belongs
|
|
18
|
+
* to the grouped rule, so this rule ignores it.
|
|
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 Crowded {
|
|
24
|
+
constructor(limit) {
|
|
25
|
+
this.id = 'crowded';
|
|
26
|
+
this.limit = limit;
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
const marks = document.walk({
|
|
34
|
+
header: (text, line) => [{header: true, line}],
|
|
35
|
+
prose: (text, line) => [{header: false, line}],
|
|
36
|
+
snippet: () => [],
|
|
37
|
+
bullets: () => [],
|
|
38
|
+
frontmatter: () => []
|
|
39
|
+
});
|
|
40
|
+
const result = [];
|
|
41
|
+
let open = null;
|
|
42
|
+
let count = 0;
|
|
43
|
+
marks.forEach((mark) => {
|
|
44
|
+
if (mark.header) {
|
|
45
|
+
this.flush(open, count, uri, result);
|
|
46
|
+
open = mark;
|
|
47
|
+
count = 0;
|
|
48
|
+
} else if (open) {
|
|
49
|
+
count += 1;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
this.flush(open, count, uri, result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
flush(open, count, uri, result) {
|
|
56
|
+
if (open && count > this.limit) {
|
|
57
|
+
result.push(new Violation(
|
|
58
|
+
this.id,
|
|
59
|
+
'error',
|
|
60
|
+
`section holds ${count} instructions, limit ${this.limit}`,
|
|
61
|
+
new Region(uri, open.line, 1)
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = Crowded;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const Violation = require('../violation');
|
|
9
|
+
const Region = require('../region');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* DescriptionTriggers.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md description say when to use the skill. A
|
|
15
|
+
* standalone checker can only approximate: it flags a value that is too
|
|
16
|
+
* short or that never names a trigger with the word "when". Its prompt
|
|
17
|
+
* hands the deeper judgement to the AI oracle.
|
|
18
|
+
*
|
|
19
|
+
* @todo #19:30min Upgrade the trigger check to an AI oracle that judges
|
|
20
|
+
* whether the description truly names the situations and user phrases
|
|
21
|
+
* that should activate the skill, as requested in issue #19.
|
|
22
|
+
*/
|
|
23
|
+
class DescriptionTriggers {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'description-triggers';
|
|
26
|
+
this.minimum = 20;
|
|
27
|
+
}
|
|
28
|
+
prompt() {
|
|
29
|
+
return `${this.id}: in a SKILL.md, flag a description that is too short or never says when to use the skill`;
|
|
30
|
+
}
|
|
31
|
+
violations(document) {
|
|
32
|
+
const uri = document.uri();
|
|
33
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const pairs = document.walk({
|
|
37
|
+
header: () => [],
|
|
38
|
+
prose: () => [],
|
|
39
|
+
snippet: () => [],
|
|
40
|
+
bullets: () => [],
|
|
41
|
+
frontmatter: (keys) => keys
|
|
42
|
+
});
|
|
43
|
+
const found = pairs.filter((pair) => pair.key === 'description');
|
|
44
|
+
if (found.length === 0) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return this.judge(found[0], uri);
|
|
48
|
+
}
|
|
49
|
+
judge(pair, uri) {
|
|
50
|
+
const value = pair.value.trim();
|
|
51
|
+
if (value.length < this.minimum) {
|
|
52
|
+
return [this.flag('description too short', pair.row, uri)];
|
|
53
|
+
}
|
|
54
|
+
if (!/\bwhen\b/iu.test(value)) {
|
|
55
|
+
return [this.flag('description must say when to use the skill', pair.row, uri)];
|
|
56
|
+
}
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
flag(message, row, uri) {
|
|
60
|
+
return new Violation(this.id, 'warning', message, new Region(uri, row, 1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = DescriptionTriggers;
|
package/src/rules/grouped.js
CHANGED
|
@@ -13,13 +13,16 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands that every instruction live under a section. Any prose that
|
|
15
15
|
* appears before the first heading is loose and therefore a violation.
|
|
16
|
+
*
|
|
17
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
18
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
16
19
|
*/
|
|
17
20
|
class Grouped {
|
|
18
21
|
constructor() {
|
|
19
22
|
this.id = 'grouped';
|
|
20
23
|
}
|
|
21
24
|
prompt() {
|
|
22
|
-
return
|
|
25
|
+
return '';
|
|
23
26
|
}
|
|
24
27
|
violations(document) {
|
|
25
28
|
const uri = document.uri();
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
* Hedging.
|
|
13
|
+
*
|
|
14
|
+
* Flags soft, non-committal, or hedging wording that weakens an order.
|
|
15
|
+
* Catches words like "should", "just", "usually", and phrases like
|
|
16
|
+
* "try to" or "if possible", each a sign of timid instruction.
|
|
17
|
+
*
|
|
18
|
+
* @todo #22:30min Upgrade to an AI oracle that catches subtler hedging,
|
|
19
|
+
* such as conditional escape hatches and vague scope, which the fixed
|
|
20
|
+
* blacklist of hedge words cannot detect today, as requested in
|
|
21
|
+
* issue #22.
|
|
22
|
+
*/
|
|
23
|
+
class Hedging {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'hedging';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return `${this.id}: flag soft, non-committal, or hedging wording`;
|
|
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(?:should|try to|if possible|as appropriate|as needed|' +
|
|
44
|
+
'when necessary|usually|generally|etc|just|simply|very)\\b',
|
|
45
|
+
'giu'
|
|
46
|
+
);
|
|
47
|
+
let hit = regex.exec(text);
|
|
48
|
+
while (hit !== null) {
|
|
49
|
+
found.push(new Violation(
|
|
50
|
+
this.id,
|
|
51
|
+
'warning',
|
|
52
|
+
`hedge word "${hit[0]}" must be removed`,
|
|
53
|
+
new Region(uri, line, hit.index + 1)
|
|
54
|
+
));
|
|
55
|
+
hit = regex.exec(text);
|
|
56
|
+
}
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = Hedging;
|
package/src/rules/index.js
CHANGED
|
@@ -14,8 +14,20 @@ const NoArticles = require('./no-articles');
|
|
|
14
14
|
const Command = require('./command');
|
|
15
15
|
const Punctuation = require('./punctuation');
|
|
16
16
|
const Frontmatter = require('./frontmatter');
|
|
17
|
+
const NameFormat = require('./name-format');
|
|
17
18
|
const DeadImport = require('./dead-import');
|
|
18
19
|
const Redundant = require('./redundant');
|
|
20
|
+
const NameMatchesDir = require('./name-matches-dir');
|
|
21
|
+
const Polite = require('./polite');
|
|
22
|
+
const Unfinished = require('./unfinished');
|
|
23
|
+
const Crowded = require('./crowded');
|
|
24
|
+
const DescriptionTriggers = require('./description-triggers');
|
|
25
|
+
const Atomic = require('./atomic');
|
|
26
|
+
const Hedging = require('./hedging');
|
|
27
|
+
const Passive = require('./passive');
|
|
28
|
+
const Unique = require('./unique');
|
|
29
|
+
const Consistent = require('./consistent');
|
|
30
|
+
const Simple = require('./simple');
|
|
19
31
|
|
|
20
32
|
module.exports = () => [
|
|
21
33
|
new Grouped(),
|
|
@@ -28,9 +40,21 @@ module.exports = () => [
|
|
|
28
40
|
new Punctuation(),
|
|
29
41
|
new DeadImport(),
|
|
30
42
|
new Redundant(),
|
|
43
|
+
new Consistent(),
|
|
44
|
+
new Simple(),
|
|
45
|
+
new NameMatchesDir(),
|
|
46
|
+
new Polite(),
|
|
47
|
+
new Unfinished(),
|
|
48
|
+
new Crowded(10),
|
|
49
|
+
new DescriptionTriggers(),
|
|
50
|
+
new Atomic(),
|
|
51
|
+
new Hedging(),
|
|
52
|
+
new Passive(),
|
|
53
|
+
new Unique(),
|
|
31
54
|
new Frontmatter(
|
|
32
55
|
'SKILL.md',
|
|
33
56
|
['name', 'description'],
|
|
34
57
|
['name', 'description', 'license', 'allowed-tools']
|
|
35
|
-
)
|
|
58
|
+
),
|
|
59
|
+
new NameFormat()
|
|
36
60
|
];
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* NameFormat.
|
|
13
|
+
*
|
|
14
|
+
* Demands that a SKILL.md frontmatter "name" read as kebab-case: lower
|
|
15
|
+
* letters and digits joined by single hyphens, no leading or trailing
|
|
16
|
+
* hyphen. Ignores every other file and leaves a missing name to the
|
|
17
|
+
* frontmatter rule.
|
|
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 NameFormat {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'name-format';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
violations(document) {
|
|
30
|
+
const uri = document.uri();
|
|
31
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
const blocks = document.walk({
|
|
35
|
+
header: () => [],
|
|
36
|
+
prose: () => [],
|
|
37
|
+
snippet: () => [],
|
|
38
|
+
bullets: () => [],
|
|
39
|
+
frontmatter: (pairs) => [pairs]
|
|
40
|
+
});
|
|
41
|
+
if (blocks.length === 0) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return this.check(blocks[0].find((pair) => pair.key === 'name'), uri);
|
|
45
|
+
}
|
|
46
|
+
check(name, uri) {
|
|
47
|
+
if (!name || /^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(name.value)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return [new Violation(
|
|
51
|
+
this.id,
|
|
52
|
+
'error',
|
|
53
|
+
`name "${name.value}" must be kebab-case`,
|
|
54
|
+
new Region(uri, name.row, 1)
|
|
55
|
+
)];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = NameFormat;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const Violation = require('../violation');
|
|
11
|
+
const Region = require('../region');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* NameMatchesDir.
|
|
15
|
+
*
|
|
16
|
+
* Demands that a SKILL.md frontmatter "name" equal the name of the
|
|
17
|
+
* directory that holds the file. The check applies only to SKILL.md
|
|
18
|
+
* and stays silent when the file carries no "name" key.
|
|
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 NameMatchesDir {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'name-matches-dir';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
violations(document) {
|
|
31
|
+
const uri = document.uri();
|
|
32
|
+
if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const blocks = document.walk({
|
|
36
|
+
header: () => [],
|
|
37
|
+
prose: () => [],
|
|
38
|
+
snippet: () => [],
|
|
39
|
+
bullets: () => [],
|
|
40
|
+
frontmatter: (pairs) => [pairs]
|
|
41
|
+
});
|
|
42
|
+
const name = blocks.length === 0
|
|
43
|
+
? null
|
|
44
|
+
: blocks[0].find((pair) => pair.key === 'name');
|
|
45
|
+
return this.mismatch(uri, name);
|
|
46
|
+
}
|
|
47
|
+
mismatch(uri, name) {
|
|
48
|
+
const parent = path.basename(path.dirname(uri));
|
|
49
|
+
if (!name || name.value === parent) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return [new Violation(
|
|
53
|
+
this.id,
|
|
54
|
+
'error',
|
|
55
|
+
`name "${name.value}" must match directory "${parent}"`,
|
|
56
|
+
new Region(uri, name.row, 1)
|
|
57
|
+
)];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = NameMatchesDir;
|
|
@@ -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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Passive.
|
|
13
|
+
*
|
|
14
|
+
* Demands active imperative voice. A standalone checker can only guess:
|
|
15
|
+
* it flags a "be" verb followed, perhaps through an adverb, by a past
|
|
16
|
+
* participle, the surest mark of passive voice.
|
|
17
|
+
*
|
|
18
|
+
* @todo #24:45min Upgrade to an AI oracle for accurate passive-voice
|
|
19
|
+
* detection, since the regular-expression heuristic both misses many
|
|
20
|
+
* irregular participles and cannot judge true grammatical voice, as
|
|
21
|
+
* requested in issue #24.
|
|
22
|
+
*/
|
|
23
|
+
class Passive {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'passive';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return `${this.id}: flag any instruction written in passive voice`;
|
|
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 = /\b(?:is|are|was|were|be|been|being)\b\s+(?:\w+ly\s+)?(?:\w+ed|written|done|made|built|kept|sent|shown|seen|taken|given|held|found|run|read|set)\b/iu;
|
|
42
|
+
if (!regex.test(text)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return [new Violation(
|
|
46
|
+
this.id,
|
|
47
|
+
'warning',
|
|
48
|
+
'line uses passive voice',
|
|
49
|
+
new Region(uri, line, 1)
|
|
50
|
+
)];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = Passive;
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
* Polite.
|
|
13
|
+
*
|
|
14
|
+
* Flags courtesy and scaffolding phrases that soften an instruction
|
|
15
|
+
* without adding meaning: "please", "kindly", "feel free to", and the
|
|
16
|
+
* like. Each phrase wastes tokens and weakens a command, so every hit
|
|
17
|
+
* earns its own violation.
|
|
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 Polite {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.id = 'polite';
|
|
25
|
+
}
|
|
26
|
+
prompt() {
|
|
27
|
+
return '';
|
|
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 found = [];
|
|
41
|
+
const regex = /\b(?:please|kindly|feel free to|make sure to|be sure to|don't forget to|remember to|note that|it is important to)\b/giu;
|
|
42
|
+
let hit = regex.exec(text);
|
|
43
|
+
while (hit !== null) {
|
|
44
|
+
found.push(new Violation(
|
|
45
|
+
this.id,
|
|
46
|
+
'error',
|
|
47
|
+
`courtesy phrase "${hit[0]}" must be removed`,
|
|
48
|
+
new Region(uri, line, hit.index + 1)
|
|
49
|
+
));
|
|
50
|
+
hit = regex.exec(text);
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = Polite;
|
|
@@ -13,13 +13,16 @@ const Region = require('../region');
|
|
|
13
13
|
*
|
|
14
14
|
* Demands that every section name be a short label of one to three
|
|
15
15
|
* words, so the manifesto reads as a map and not as prose.
|
|
16
|
+
*
|
|
17
|
+
* The check is standalone and deterministic, so prompt() returns an
|
|
18
|
+
* empty string and the AI oracle never re-checks this rule.
|
|
16
19
|
*/
|
|
17
20
|
class ShortSections {
|
|
18
21
|
constructor() {
|
|
19
22
|
this.id = 'short-sections';
|
|
20
23
|
}
|
|
21
24
|
prompt() {
|
|
22
|
-
return
|
|
25
|
+
return '';
|
|
23
26
|
}
|
|
24
27
|
violations(document) {
|
|
25
28
|
const uri = document.uri();
|
|
@@ -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
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Simple.
|
|
13
|
+
*
|
|
14
|
+
* Demands simple grammar over ambiguity. A standalone checker can only
|
|
15
|
+
* guess: it counts commas and conjunctions to flag lines that pile up
|
|
16
|
+
* clauses. Its prompt hands the subtler tangle judgement to the oracle.
|
|
17
|
+
*
|
|
18
|
+
* @todo #29:45min Upgrade to true clause-depth analysis through an AI
|
|
19
|
+
* oracle so that subtle tangled instructions, which the comma and
|
|
20
|
+
* conjunction heuristic cannot measure today, are reliably caught, as
|
|
21
|
+
* requested in issue #29.
|
|
22
|
+
*/
|
|
23
|
+
class Simple {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'simple';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return `${this.id}: flag any grammatically tangled, multi-clause instruction`;
|
|
29
|
+
}
|
|
30
|
+
violations(document) {
|
|
31
|
+
const uri = document.uri();
|
|
32
|
+
return document.walk({
|
|
33
|
+
header: () => [],
|
|
34
|
+
prose: (text, line) => this.judge(text, line, uri),
|
|
35
|
+
snippet: () => [],
|
|
36
|
+
bullets: () => [],
|
|
37
|
+
frontmatter: () => []
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
judge(text, line, uri) {
|
|
41
|
+
const commas = text.match(/,/gu);
|
|
42
|
+
const commaCount = commas === null ? 0 : commas.length;
|
|
43
|
+
const hasConjunction = /\b(?:if|when|unless|because|although|while)\b/iu.test(text);
|
|
44
|
+
const tangled = hasConjunction && commaCount >= 2 || commaCount >= 3;
|
|
45
|
+
if (!tangled) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [new Violation(
|
|
49
|
+
this.id,
|
|
50
|
+
'warning',
|
|
51
|
+
'line is grammatically tangled, split into simpler lines',
|
|
52
|
+
new Region(uri, line, 1)
|
|
53
|
+
)];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = Simple;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
* Unfinished.
|
|
13
|
+
*
|
|
14
|
+
* Flags any prose line that still carries a leftover marker, the
|
|
15
|
+
* fingerprint of work left half done. It catches the uppercase tokens
|
|
16
|
+
* TODO, TBD, FIXME, XXX, and WIP, the placeholder phrase "lorem ipsum",
|
|
17
|
+
* a trailing bare ellipsis, and an unfilled angle-bracket placeholder
|
|
18
|
+
* such as <placeholder>.
|
|
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 Unfinished {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.id = 'unfinished';
|
|
26
|
+
}
|
|
27
|
+
prompt() {
|
|
28
|
+
return '';
|
|
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 markers = [
|
|
42
|
+
/\b(?:TODO|TBD|FIXME|XXX|WIP)\b/u,
|
|
43
|
+
/lorem ipsum/iu,
|
|
44
|
+
/\.\.\.\s*$/u,
|
|
45
|
+
/<[^>\n]*>/u
|
|
46
|
+
];
|
|
47
|
+
if (!markers.some((marker) => marker.test(text))) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
return [new Violation(
|
|
51
|
+
this.id,
|
|
52
|
+
'error',
|
|
53
|
+
'leftover marker found, file looks unfinished',
|
|
54
|
+
new Region(uri, line, 1)
|
|
55
|
+
)];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = Unfinished;
|
|
@@ -0,0 +1,73 @@
|
|
|
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 normalize = (text) => {
|
|
12
|
+
const clean = text
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9\s]/gu, '')
|
|
15
|
+
.replace(/\b(?:a|an|the)\b/gu, '')
|
|
16
|
+
.replace(/\s+/gu, ' ')
|
|
17
|
+
.trim();
|
|
18
|
+
return clean.split(' ').sort().join(' ');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Unique.
|
|
23
|
+
*
|
|
24
|
+
* Flags any instruction that repeats another instruction in the file.
|
|
25
|
+
* It normalizes each prose line, then remembers the first line where
|
|
26
|
+
* each normal form appeared, so a later twin earns one violation.
|
|
27
|
+
*
|
|
28
|
+
* @todo #25:45min Upgrade to semantic near-duplicate detection through
|
|
29
|
+
* embeddings or an AI oracle, to catch same-meaning different-words
|
|
30
|
+
* pairs the normalizer misses, as requested in issue #25.
|
|
31
|
+
*/
|
|
32
|
+
class Unique {
|
|
33
|
+
constructor() {
|
|
34
|
+
this.id = 'unique';
|
|
35
|
+
}
|
|
36
|
+
prompt() {
|
|
37
|
+
return `${this.id}: flag any instruction that repeats another instruction in the file`;
|
|
38
|
+
}
|
|
39
|
+
violations(document) {
|
|
40
|
+
const uri = document.uri();
|
|
41
|
+
const lines = document.walk({
|
|
42
|
+
header: () => [],
|
|
43
|
+
prose: (text, line) => [{text, line}],
|
|
44
|
+
snippet: () => [],
|
|
45
|
+
bullets: () => [],
|
|
46
|
+
frontmatter: () => []
|
|
47
|
+
});
|
|
48
|
+
return this.repeats(uri, lines);
|
|
49
|
+
}
|
|
50
|
+
repeats(uri, lines) {
|
|
51
|
+
const seen = new Map();
|
|
52
|
+
const found = [];
|
|
53
|
+
lines.forEach((item) => {
|
|
54
|
+
const norm = normalize(item.text);
|
|
55
|
+
if (norm === '') {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (seen.has(norm)) {
|
|
59
|
+
found.push(new Violation(
|
|
60
|
+
this.id,
|
|
61
|
+
'warning',
|
|
62
|
+
`instruction repeats line ${seen.get(norm)}`,
|
|
63
|
+
new Region(uri, item.line, 1)
|
|
64
|
+
));
|
|
65
|
+
} else {
|
|
66
|
+
seen.set(norm, item.line);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = Unique;
|
package/src/usage.js
CHANGED
|
@@ -25,7 +25,7 @@ const PRICES = {
|
|
|
25
25
|
*
|
|
26
26
|
* One immutable tally of an OpenAI exchange: the model, the tokens sent,
|
|
27
27
|
* and the tokens received. Sums itself with another tally and renders a
|
|
28
|
-
* single human summary line, complete with an estimated
|
|
28
|
+
* single human summary line, complete with an estimated cost in cents.
|
|
29
29
|
*/
|
|
30
30
|
class Usage {
|
|
31
31
|
constructor(model, sent, received) {
|
|
@@ -45,7 +45,7 @@ class Usage {
|
|
|
45
45
|
return this.sent / 1e6 * price.input + this.received / 1e6 * price.output;
|
|
46
46
|
}
|
|
47
47
|
text() {
|
|
48
|
-
return `OpenAI: ${this.model}, ${this.sent} sent, ${this.received} received,
|
|
48
|
+
return `OpenAI: ${this.model}, ${this.sent} sent, ${this.received} received, ~${(this.cost() * 100).toFixed(2)}¢`;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|