@yegor256/dogent 0.9.1 → 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 CHANGED
@@ -67,20 +67,30 @@ The command exits with a non-zero status when problems are found,
67
67
  - Every section must be a level-2 (`##`) heading, below the lone `#` title.
68
68
  - Every line must be no longer than 80 symbols.
69
69
  - The whole file must stay under 4000 tokens.
70
+ - The whole file must stay short; split detail into referenced files.
70
71
  - Every line must sound like a command.
71
72
  - Every sentence must start with a capital and end with a period.
72
73
  - No articles, no noise, no bloated text.
73
74
  - Simple grammar, no ambiguity.
75
+ - No bare pronoun subjects; name the subject on the line.
74
76
  - No tangled, multi-clause instructions.
77
+ - Sequential steps must be a numbered list, not bullets.
75
78
  - A `SKILL.md` `name` must equal its parent directory.
76
79
  - No courtesy or scaffolding words.
77
80
  - No leftover markers or unfilled placeholders.
78
81
  - A section must hold at most ten instructions.
79
82
  - A `SKILL.md` `description` must say when to use the skill.
83
+ - A `SKILL.md` must carry at least one worked example.
84
+ - A `SKILL.md` that produces output must declare its format.
80
85
  - Every line must carry exactly one instruction.
81
86
  - No hedging or soft wording.
87
+ - No vague qualifiers; demand a measurable criterion.
82
88
  - No passive voice; use the active imperative.
89
+ - No ALL-CAPS shouting or "!!" markers; state it plainly.
90
+ - No persona or role-play; it adds no instruction.
91
+ - No negative phrasing; state the positive command instead.
83
92
  - No instruction may repeat another.
93
+ - No unguarded consumption of untrusted external input.
84
94
  - `SKILL.md` must open with valid frontmatter.
85
95
  - Frontmatter must declare only allowed keys.
86
96
  - A `SKILL.md` `name` must be kebab-case.
@@ -114,6 +124,13 @@ npx @yegor256/dogent --offline CLAUDE.md
114
124
 
115
125
  Pass `--sarif` to print the report as SARIF instead of plain text.
116
126
 
127
+ Pass `--suppress` to silence a rule by its id. Repeat the option or
128
+ join several ids with commas to silence many at once:
129
+
130
+ ```bash
131
+ npx @yegor256/dogent --suppress=name-matches-dir,line-length CLAUDE.md
132
+ ```
133
+
117
134
  ## GitHub Actions
118
135
 
119
136
  Because `dogent` runs through `npx`, no extra action is needed.
package/package.json CHANGED
@@ -40,8 +40,9 @@
40
40
  "lint": "eslint .",
41
41
  "test": "mocha 'test/**/*.js' --timeout 60000"
42
42
  },
43
- "version": "0.9.1",
43
+ "version": "0.10.0",
44
44
  "dependencies": {
45
- "minimist": "^1.2.8"
45
+ "minimist": "^1.2.8",
46
+ "pretty-ms": "^7.0.1"
46
47
  }
47
48
  }
package/src/args.js CHANGED
@@ -15,13 +15,22 @@ const minimist = require('minimist');
15
15
  * The `--sarif` flag switches the report to SARIF, while `--offline` forbids
16
16
  * any talk to the LLM even when a token sits in the environment. The `--help`
17
17
  * flag, also spelled `-h`, asks for the usage banner. The `--version` flag
18
- * asks for the release number. Everything after a `--` separator counts as a
19
- * path, never as an option.
18
+ * asks for the release number. The `--suppress` option names a rule to
19
+ * silence; repeat it or join names with commas to silence many at once.
20
+ * Everything after a `--` separator counts as a path, never as an option.
20
21
  */
21
22
  class Args {
22
- constructor(argv, flags = ['sarif', 'offline', 'help', 'version']) {
23
+ constructor(
24
+ argv,
25
+ flags = ['sarif', 'offline', 'help', 'version'],
26
+ options = ['suppress']
27
+ ) {
23
28
  this.flags = flags;
24
- this.parsed = minimist(argv, {boolean: flags, alias: {help: 'h'}, '--': true});
29
+ this.options = options;
30
+ this.parsed = minimist(
31
+ argv,
32
+ {boolean: flags, string: options, alias: {help: 'h'}, '--': true}
33
+ );
25
34
  }
26
35
  sarif() {
27
36
  return this.parsed.sarif === true;
@@ -35,12 +44,19 @@ class Args {
35
44
  version() {
36
45
  return this.parsed.version === true;
37
46
  }
47
+ suppress() {
48
+ return [].concat(this.parsed.suppress || [])
49
+ .flatMap((item) => String(item).split(','))
50
+ .map((name) => name.trim())
51
+ .filter((name) => name !== '');
52
+ }
38
53
  paths() {
39
54
  return this.parsed._.concat(this.parsed['--']).map(String);
40
55
  }
41
56
  unknown() {
42
57
  return Object.keys(this.parsed)
43
- .filter((key) => key !== '_' && key !== '--' && key !== 'h' && !this.flags.includes(key))
58
+ .filter((key) => key !== '_' && key !== '--' && key !== 'h' &&
59
+ !this.flags.includes(key) && !this.options.includes(key))
44
60
  .map((key) => `${key.length === 1 ? '-' : '--'}${key}`);
45
61
  }
46
62
  }
package/src/dogent.js CHANGED
@@ -14,12 +14,13 @@ const Sources = require('./sources');
14
14
  const Openai = require('./openai');
15
15
  const Oracle = require('./oracle');
16
16
  const Usage = require('./usage');
17
+ const prettyMs = require('pretty-ms');
17
18
  const version = require('./version');
18
19
  const rules = require('./rules');
19
20
 
20
21
  const args = new Args(process.argv.slice(2));
21
22
  const sarif = args.sarif();
22
- const banner = 'Usage: dogent [--sarif] [--offline] <file.md|dir>...';
23
+ const banner = 'Usage: dogent [--sarif] [--offline] [--suppress=RULE,...] <file.md|dir>...';
23
24
  if (args.version()) {
24
25
  process.stdout.write(`${version}\n`);
25
26
  process.exit(0);
@@ -31,6 +32,7 @@ if (args.help()) {
31
32
  'Options:\n' +
32
33
  ' --sarif render the report as SARIF JSON\n' +
33
34
  ' --offline never call the LLM, even when a token exists\n' +
35
+ ' --suppress silence a rule by id; repeat or comma-join to silence many\n' +
34
36
  ' --version show the version and exit\n' +
35
37
  ' --help show this help and exit\n'
36
38
  );
@@ -49,20 +51,24 @@ if (paths.length === 0) {
49
51
  }
50
52
  const scanned = new Sources(paths).files();
51
53
  scanned.forEach((file) => process.stderr.write(`Scanning ${file}\n`));
52
- process.stderr.write(`${scanned.length} files scanned\n`);
54
+ const checks = rules();
55
+ process.stderr.write(`${scanned.length} files scanned, ${checks.length} rules applied\n`);
53
56
  const documents = scanned.map(
54
57
  (file) => new Markdown(file, fs.readFileSync(file, 'utf8')).document()
55
58
  );
59
+ const started = Date.now();
60
+ const suppressed = args.suppress();
61
+ const allowed = (violation) => !suppressed.includes(violation.rule);
56
62
  const found = [];
57
63
  documents.forEach((document) => {
58
- rules().forEach((rule) => {
59
- rule.violations(document).forEach((violation) => found.push(violation));
64
+ checks.forEach((rule) => {
65
+ rule.violations(document).filter(allowed).forEach((violation) => found.push(violation));
60
66
  });
61
67
  });
62
68
  const key = process.env.OPENAI_API_KEY;
63
69
  const audit = async (docs) => {
64
70
  const oracle = new Oracle(
65
- rules(),
71
+ checks,
66
72
  new Openai(
67
73
  key,
68
74
  process.env.OPENAI_MODEL || 'gpt-4o-mini',
@@ -78,25 +84,29 @@ const audit = async (docs) => {
78
84
  {extra: [], usage: new Usage('', 0, 0)}
79
85
  );
80
86
  };
81
- const finish = (usage) => {
82
- const report = new Report('dogent', found);
87
+ const finish = (usage, aiMillis) => {
88
+ const report = new Report('dogent', found, Date.now() - started);
83
89
  process.stdout.write(`${sarif ? JSON.stringify(report.sarif(), null, 2) : report.text()}\n`);
84
90
  if (usage !== null) {
85
- process.stderr.write(`${usage.text()}\n`);
91
+ process.stderr.write(`${usage.text()}, analysed in ${prettyMs(aiMillis)}\n`);
86
92
  }
87
93
  process.exit(report.count() > 0 ? 1 : 0);
88
94
  };
95
+ const verify = async () => {
96
+ const clock = Date.now();
97
+ const result = await audit(documents);
98
+ result.extra.filter(allowed).forEach((violation) => found.push(violation));
99
+ return {usage: result.usage, aiMillis: Date.now() - clock};
100
+ };
89
101
  (async () => {
90
- let usage = null;
102
+ let outcome = {aiMillis: 0, usage: null};
91
103
  if (found.length === 0 && key && !args.offline()) {
92
104
  try {
93
- const result = await audit(documents);
94
- result.extra.forEach((violation) => found.push(violation));
95
- ({usage} = result);
105
+ outcome = await verify();
96
106
  } catch (error) {
97
107
  process.stderr.write(`AI verification failed: ${error.message}\n`);
98
108
  process.exit(2);
99
109
  }
100
110
  }
101
- finish(usage);
111
+ finish(outcome.usage, outcome.aiMillis);
102
112
  })();
package/src/report.js CHANGED
@@ -5,24 +5,30 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ const prettyMs = require('pretty-ms');
9
+
8
10
  /**
9
11
  * Report.
10
12
  *
11
13
  * The whole verdict of a run: the tool that produced it and every
12
14
  * violation it gathered. Renders itself for humans or as a SARIF log.
15
+ * When handed the analysis duration in milliseconds, the human text
16
+ * closes with a friendly "in 340ms" rendered through pretty-ms.
13
17
  */
14
18
  class Report {
15
- constructor(tool, violations) {
19
+ constructor(tool, violations, millis = null) {
16
20
  this.tool = tool;
17
21
  this.bag = violations;
22
+ this.millis = millis;
18
23
  }
19
24
  count() {
20
25
  return this.bag.length;
21
26
  }
22
27
  text() {
28
+ const suffix = this.millis === null ? '' : ` in ${prettyMs(this.millis)}`;
23
29
  return this.bag
24
30
  .map((violation) => violation.text())
25
- .concat(`${this.bag.length} problems found`)
31
+ .concat(`${this.bag.length} problems found${suffix}`)
26
32
  .join('\n');
27
33
  }
28
34
  sarif() {
@@ -0,0 +1,50 @@
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
+ * Budget.
13
+ *
14
+ * Demands that a whole manifesto stay short enough to stay readable,
15
+ * holding no more instructions than the cap allows. Counts every prose
16
+ * line plus every bullet item across the file and complains once with a
17
+ * single file-level violation when the total exceeds the cap.
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 Budget {
23
+ constructor(cap) {
24
+ this.id = 'budget';
25
+ this.cap = cap;
26
+ }
27
+ prompt() {
28
+ return '';
29
+ }
30
+ violations(document) {
31
+ const count = document.walk({
32
+ header: () => [],
33
+ prose: () => [1],
34
+ snippet: () => [],
35
+ bullets: () => [],
36
+ frontmatter: () => []
37
+ }).length;
38
+ if (count <= this.cap) {
39
+ return [];
40
+ }
41
+ return [new Violation(
42
+ this.id,
43
+ 'error',
44
+ `file holds ${count} instructions, budget ${this.cap}, split the manifesto`,
45
+ new Region(document.uri(), 1, 1)
46
+ )];
47
+ }
48
+ }
49
+
50
+ module.exports = Budget;
@@ -0,0 +1,48 @@
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
+ * Concise.
13
+ *
14
+ * Bounds a manifesto by structure, not only by token volume. Models read
15
+ * the start and end of a long context and skip the middle, so a manifesto
16
+ * that runs past a line budget silently buries its middle instructions.
17
+ * Counts physical lines and warns once the file crosses a configurable
18
+ * ceiling, recommending a split into referenced detail files in the
19
+ * spirit of progressive disclosure. This is distinct from token-count: it
20
+ * measures structure and position risk, not raw token volume. Its prompt
21
+ * hands the deeper split judgement to the AI oracle.
22
+ */
23
+ class Concise {
24
+ constructor(max) {
25
+ this.id = 'concise';
26
+ this.max = max;
27
+ }
28
+ prompt() {
29
+ return `${this.id}: flag a manifesto so long its middle instructions risk being lost, and recommend splitting detail into referenced files`;
30
+ }
31
+ violations(document) {
32
+ const lines = document.text().split('\n');
33
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
34
+ lines.pop();
35
+ }
36
+ if (lines.length <= this.max) {
37
+ return [];
38
+ }
39
+ return [new Violation(
40
+ this.id,
41
+ 'warning',
42
+ `file too long (${lines.length} lines), split detail into referenced files`,
43
+ new Region(document.uri(), this.max + 1, 1)
44
+ )];
45
+ }
46
+ }
47
+
48
+ module.exports = Concise;
@@ -0,0 +1,60 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+ const mask = require('../mask');
11
+
12
+ /**
13
+ * CounterExample.
14
+ *
15
+ * Rejects "bad example" demonstrations that show the wrong form, since
16
+ * displaying a mistake can reinforce it. A standalone checker flags a
17
+ * line that opens a counterexample with an introducer phrase ("bad
18
+ * example", "wrong example", "for example, do not", "instead of
19
+ * writing", "avoid writing") and then carries a quoted or backticked
20
+ * sample of the wrong form. Its prompt hands subtler cases to the AI
21
+ * oracle, which judges whether an example shows the correct or the
22
+ * incorrect behavior.
23
+ */
24
+ class CounterExample {
25
+ constructor() {
26
+ this.id = 'counter-example';
27
+ }
28
+ prompt() {
29
+ return `${this.id}: judge whether each example shows the correct behavior, and flag any example that demonstrates the incorrect form`;
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 = /bad example|wrong example|for example, do not|instead of writing|avoid writing/iu;
43
+ const hit = regex.exec(mask(text));
44
+ if (hit === null) {
45
+ return [];
46
+ }
47
+ const tail = text.slice(hit.index + hit[0].length);
48
+ if (!/["'`]/u.test(tail)) {
49
+ return [];
50
+ }
51
+ return [new Violation(
52
+ this.id,
53
+ 'warning',
54
+ 'counterexample may reinforce the wrong behavior, show the right form',
55
+ new Region(uri, line, hit.index + 1)
56
+ )];
57
+ }
58
+ }
59
+
60
+ module.exports = CounterExample;
@@ -0,0 +1,53 @@
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
+ * Done.
13
+ *
14
+ * Demands that a SKILL.md state a verifiable completion check, symmetric
15
+ * to the description trigger requirement. A standalone checker can only
16
+ * approximate: it scans headings and prose for a verification signal. Its
17
+ * prompt hands the deeper judgement to the AI oracle, which weighs whether
18
+ * the stated check is truly pass/fail testable rather than vague.
19
+ */
20
+ class Done {
21
+ constructor() {
22
+ this.id = 'done';
23
+ }
24
+ prompt() {
25
+ return `${this.id}: in a SKILL.md, judge whether the stated completion check is actually pass/fail testable rather than a vague gesture toward being finished`;
26
+ }
27
+ violations(document) {
28
+ const uri = document.uri();
29
+ if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
30
+ return [];
31
+ }
32
+ const signals = document.walk({
33
+ header: (text) => [/\b(?:verify|done|check|validation|acceptance)\b/iu.test(text)],
34
+ prose: (text) => [/\b(?:confirm|assert|verify|the test passes|tests pass|exit code|pass\/fail)\b/iu.test(text)],
35
+ snippet: () => [],
36
+ bullets: () => [],
37
+ frontmatter: () => []
38
+ });
39
+ if (signals.some((signal) => signal)) {
40
+ return [];
41
+ }
42
+ return [
43
+ new Violation(
44
+ this.id,
45
+ 'warning',
46
+ 'SKILL.md never says how to verify completion',
47
+ new Region(uri, 1, 1)
48
+ )
49
+ ];
50
+ }
51
+ }
52
+
53
+ module.exports = Done;
@@ -0,0 +1,81 @@
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
+ * Emphasis.
14
+ *
15
+ * Flags shouting that tries to force compliance through volume rather
16
+ * than clarity: a curated all-caps word like "IMPORTANT" or "NEVER", a
17
+ * run of two or more consecutive all-caps words, and repeated marks like
18
+ * "!!" or "!?". The model gains nothing from volume, so the emphasis is
19
+ * pure noise. A lone short acronym such as "JSON" or "AI" is left alone.
20
+ * Its prompt hands the borderline emphasis and reward framing the
21
+ * patterns miss to the AI oracle.
22
+ */
23
+ class Emphasis {
24
+ constructor() {
25
+ this.id = 'emphasis';
26
+ this.shout = new Set(['IMPORTANT', 'ALWAYS', 'NEVER', 'MUST', 'CRITICAL', 'REQUIRED']);
27
+ }
28
+ prompt() {
29
+ return `${this.id}: flag emphatic shouting the patterns miss, including borderline all-caps and reward framing, since emphasis adds no instruction`;
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 masked = mask(text);
43
+ return this.punctuation(masked, line, uri).concat(this.shouting(masked, line, uri));
44
+ }
45
+ punctuation(masked, line, uri) {
46
+ const found = [];
47
+ const regex = /!{2,}|!\?|\?!/gu;
48
+ let hit = regex.exec(masked);
49
+ while (hit !== null) {
50
+ found.push(this.flag(hit[0], line, hit.index, uri));
51
+ hit = regex.exec(masked);
52
+ }
53
+ return found;
54
+ }
55
+ shouting(masked, line, uri) {
56
+ const found = [];
57
+ const regex = /[A-Z]{2,}(?:\s+[A-Z]{2,})*/gu;
58
+ let hit = regex.exec(masked);
59
+ while (hit !== null) {
60
+ const tokens = hit[0].split(/\s+/u);
61
+ const loud = tokens.length > 1
62
+ ? tokens.some((token) => token.length >= 5 || this.shout.has(token))
63
+ : this.shout.has(tokens[0]);
64
+ if (loud) {
65
+ found.push(this.flag(hit[0], line, hit.index, uri));
66
+ }
67
+ hit = regex.exec(masked);
68
+ }
69
+ return found;
70
+ }
71
+ flag(marker, line, index, uri) {
72
+ return new Violation(
73
+ this.id,
74
+ 'warning',
75
+ `emphasis marker "${marker}" adds no instruction, state it plainly`,
76
+ new Region(uri, line, index + 1)
77
+ );
78
+ }
79
+ }
80
+
81
+ module.exports = Emphasis;
@@ -0,0 +1,60 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * Example.
13
+ *
14
+ * Demands that a SKILL.md demonstrate, not only describe. A skill that
15
+ * states rules in prose alone leaves the agent to infer the exact shape
16
+ * of correct output, while a single worked example is one of the most
17
+ * reliable levers in prompt engineering. A standalone checker passes the
18
+ * skill that carries at least one fenced code block or an explicit
19
+ * "Example" section heading, and flags the one that has neither. Its
20
+ * prompt hands the deeper judgement to the AI oracle, which weighs
21
+ * whether a present code block is truly illustrative.
22
+ */
23
+ class Example {
24
+ constructor() {
25
+ this.id = 'example';
26
+ }
27
+ prompt() {
28
+ return `${this.id}: in a SKILL.md, judge whether a present code block is a genuine worked example rather than a stray snippet, and flag a skill that only describes without demonstrating`;
29
+ }
30
+ violations(document) {
31
+ const uri = document.uri();
32
+ if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
33
+ return [];
34
+ }
35
+ const hints = document.walk({
36
+ header: (text) => this.heading(text),
37
+ prose: () => [],
38
+ snippet: () => ['snippet'],
39
+ bullets: () => [],
40
+ frontmatter: () => []
41
+ });
42
+ if (hints.length > 0) {
43
+ return [];
44
+ }
45
+ return [new Violation(
46
+ this.id,
47
+ 'warning',
48
+ 'SKILL.md has no example, add a worked input/output sample',
49
+ new Region(uri, 1, 1)
50
+ )];
51
+ }
52
+ heading(text) {
53
+ if (/^#{1,6}\s+examples?\b/iu.test(text)) {
54
+ return [this.id];
55
+ }
56
+ return [];
57
+ }
58
+ }
59
+
60
+ module.exports = Example;
@@ -0,0 +1,68 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const Violation = require('../violation');
9
+ const Region = require('../region');
10
+
11
+ /**
12
+ * Format.
13
+ *
14
+ * Demands that a SKILL.md which produces output pin down that output's
15
+ * shape. Structured-output generation grows far more reliable when the
16
+ * expected format is declared and shown, while leaving it implicit
17
+ * produces brittle, drifting output. A standalone checker flags a skill
18
+ * whose instructions describe producing output (verbs like "produce",
19
+ * "output", "return", "generate", "write", "emit") yet no section or
20
+ * snippet declares the output shape. This is distinct from the example
21
+ * rule: an example shows one case, a format spec defines the contract.
22
+ * Its prompt asks the AI oracle whether the declared format is concrete
23
+ * enough to be machine-checkable.
24
+ */
25
+ class Format {
26
+ constructor() {
27
+ this.id = 'format';
28
+ }
29
+ prompt() {
30
+ return `${this.id}: in a SKILL.md, judge whether the declared output format is concrete and machine-checkable, and flag a generating skill that pins down no format`;
31
+ }
32
+ violations(document) {
33
+ const uri = document.uri();
34
+ if (uri.replace(/^.*\//u, '') !== 'SKILL.md') {
35
+ return [];
36
+ }
37
+ const heading = /^#{1,6}\s+.*\b(?:format|schema|structure|output)\b/iu;
38
+ const verb = /\b(?:produces?|outputs?|returns?|generates?|writes?|emits?)\b/iu;
39
+ const signals = document.walk({
40
+ header: (text) => {
41
+ if (heading.test(text)) {
42
+ return ['declared'];
43
+ }
44
+ return [];
45
+ },
46
+ prose: (text) => {
47
+ if (verb.test(text)) {
48
+ return ['generates'];
49
+ }
50
+ return [];
51
+ },
52
+ snippet: () => ['declared'],
53
+ bullets: () => [],
54
+ frontmatter: () => []
55
+ });
56
+ if (!signals.includes('generates') || signals.includes('declared')) {
57
+ return [];
58
+ }
59
+ return [new Violation(
60
+ this.id,
61
+ 'warning',
62
+ 'SKILL.md generates output but never declares its format',
63
+ new Region(uri, 1, 1)
64
+ )];
65
+ }
66
+ }
67
+
68
+ module.exports = Format;