@yegor256/dogent 0.5.1 → 0.6.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
@@ -29,7 +29,7 @@ In short: `agnix` lints the harness, `dogent` lints the prompt.
29
29
  Run it on any manifesto file, no installation required:
30
30
 
31
31
  ```bash
32
- npx @yegor256/dogent@0.5.0 CLAUDE.md
32
+ npx @yegor256/dogent@0.5.1 CLAUDE.md
33
33
  ```
34
34
 
35
35
  Lint several files at once:
@@ -96,6 +96,15 @@ export OPENAI_API_KEY=...
96
96
  npx @yegor256/dogent CLAUDE.md
97
97
  ```
98
98
 
99
+ Pass `--offline` to keep `dogent` away from the LLM,
100
+ even when `OPENAI_API_KEY` is present in the environment:
101
+
102
+ ```bash
103
+ npx @yegor256/dogent --offline CLAUDE.md
104
+ ```
105
+
106
+ Pass `--sarif` to print the report as SARIF instead of plain text.
107
+
99
108
  ## GitHub Actions
100
109
 
101
110
  Because `dogent` runs through `npx`, no extra action is needed.
@@ -141,7 +150,7 @@ Reference `dogent` as a remote hook in `.pre-commit-config.yaml`:
141
150
  ```yaml
142
151
  repos:
143
152
  - repo: https://github.com/yegor256/dogent
144
- rev: 0.5.0
153
+ rev: 0.5.1
145
154
  hooks:
146
155
  - id: dogent
147
156
  ```
package/package.json CHANGED
@@ -40,5 +40,5 @@
40
40
  "lint": "eslint .",
41
41
  "test": "mocha 'test/**/*.js' --timeout 60000"
42
42
  },
43
- "version": "0.5.1"
43
+ "version": "0.6.0"
44
44
  }
package/src/args.js ADDED
@@ -0,0 +1,37 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 Yegor Bugayenko
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Args.
10
+ *
11
+ * The command-line arguments handed to dogent. It splits the raw argv into
12
+ * recognized options and the manifesto paths that remain. The `--sarif`
13
+ * flag switches the report to SARIF, while `--offline` forbids any talk to
14
+ * the LLM even when a token sits in the environment.
15
+ */
16
+ class Args {
17
+ constructor(argv, flags = ['--sarif', '--offline']) {
18
+ this.argv = argv;
19
+ this.flags = flags;
20
+ }
21
+ sarif() {
22
+ return this.argv.includes('--sarif');
23
+ }
24
+ offline() {
25
+ return this.argv.includes('--offline');
26
+ }
27
+ paths() {
28
+ return this.argv.filter((arg) => !arg.startsWith('-'));
29
+ }
30
+ unknown() {
31
+ return this.argv.filter(
32
+ (arg) => arg.startsWith('-') && !this.flags.includes(arg)
33
+ );
34
+ }
35
+ }
36
+
37
+ module.exports = Args;
package/src/dogent.js CHANGED
@@ -7,6 +7,7 @@
7
7
  'use strict';
8
8
 
9
9
  const fs = require('fs');
10
+ const Args = require('./args');
10
11
  const Markdown = require('./markdown');
11
12
  const Report = require('./report');
12
13
  const Sources = require('./sources');
@@ -14,11 +15,17 @@ const Openai = require('./openai');
14
15
  const Oracle = require('./oracle');
15
16
  const rules = require('./rules');
16
17
 
17
- const argv = process.argv.slice(2);
18
- const sarif = argv.indexOf('--sarif') !== -1;
19
- const paths = argv.filter((arg) => arg !== '--sarif');
18
+ const args = new Args(process.argv.slice(2));
19
+ const sarif = args.sarif();
20
+ const unknown = args.unknown();
21
+ if (unknown.length > 0) {
22
+ process.stderr.write(`Unknown option: ${unknown[0]}\n`);
23
+ process.stderr.write('Usage: dogent [--sarif] [--offline] <file.md|dir>...\n');
24
+ process.exit(2);
25
+ }
26
+ const paths = args.paths();
20
27
  if (paths.length === 0) {
21
- process.stderr.write('Usage: dogent [--sarif] <file.md|dir>...\n');
28
+ process.stderr.write('Usage: dogent [--sarif] [--offline] <file.md|dir>...\n');
22
29
  process.exit(2);
23
30
  }
24
31
  const scanned = new Sources(paths).files();
@@ -35,7 +42,7 @@ documents.forEach((document) => {
35
42
  });
36
43
  const key = process.env.OPENAI_API_KEY;
37
44
  (async () => {
38
- if (found.length === 0 && key) {
45
+ if (found.length === 0 && key && !args.offline()) {
39
46
  try {
40
47
  const oracle = new Oracle(
41
48
  rules(),
@@ -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 fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const Violation = require('../violation');
12
+ const Region = require('../region');
13
+
14
+ const imports = (line) => {
15
+ const found = [];
16
+ const pattern = /(?<lead>^|\s)@(?<file>\S+)/gu;
17
+ let match = pattern.exec(line);
18
+ while (match !== null) {
19
+ found.push({
20
+ file: match.groups.file.replace(/[.,:;!?]+$/u, ''),
21
+ column: match.index + match.groups.lead.length + 1
22
+ });
23
+ match = pattern.exec(line);
24
+ }
25
+ return found;
26
+ };
27
+
28
+ /**
29
+ * DeadImport.
30
+ *
31
+ * Flags `@path/to/file` imports that point to no file on disk.
32
+ *
33
+ * @todo #18:45min Detect circular import chains and depth above five
34
+ * levels so deeply nested manifesto imports fail with a clear violation,
35
+ * as requested in issue #18.
36
+ */
37
+ class DeadImport {
38
+ constructor() {
39
+ this.id = 'dead-import';
40
+ }
41
+ prompt() {
42
+ return `${this.id}: flag any @path/to/file import that points to no file on disk`;
43
+ }
44
+ violations(document) {
45
+ return document.walk({
46
+ header: () => [],
47
+ snippet: () => [],
48
+ bullets: () => [],
49
+ frontmatter: () => [],
50
+ prose: (line, row) => this.missing(document.uri(), line, row)
51
+ });
52
+ }
53
+ missing(uri, line, row) {
54
+ const base = path.dirname(uri);
55
+ return imports(line)
56
+ .filter((item) => !fs.existsSync(path.resolve(base, item.file)))
57
+ .map((item) => new Violation(
58
+ this.id,
59
+ 'error',
60
+ `@-import target not found: ${item.file}`,
61
+ new Region(uri, row, item.column)
62
+ ));
63
+ }
64
+ }
65
+
66
+ module.exports = DeadImport;
@@ -14,6 +14,7 @@ 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 DeadImport = require('./dead-import');
17
18
 
18
19
  module.exports = () => [
19
20
  new Grouped(),
@@ -24,6 +25,7 @@ module.exports = () => [
24
25
  new NoArticles(),
25
26
  new Command(),
26
27
  new Punctuation(),
28
+ new DeadImport(),
27
29
  new Frontmatter(
28
30
  'SKILL.md',
29
31
  ['name', 'description'],