pan-wizard 3.5.2 → 3.8.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.
Files changed (98) hide show
  1. package/README.md +28 -9
  2. package/agents/pan-executor.md +18 -0
  3. package/agents/pan-experiment-runner.md +126 -0
  4. package/agents/pan-phase-researcher.md +16 -0
  5. package/agents/pan-plan-checker.md +80 -0
  6. package/agents/pan-planner.md +19 -0
  7. package/agents/pan-reviewer.md +2 -0
  8. package/agents/pan-verifier.md +41 -0
  9. package/bin/install-lib.cjs +55 -0
  10. package/bin/install.js +71 -22
  11. package/commands/pan/debug.md +1 -1
  12. package/commands/pan/experiment.md +219 -0
  13. package/commands/pan/health.md +1 -1
  14. package/commands/pan/learn.md +15 -1
  15. package/commands/pan/links.md +102 -0
  16. package/commands/pan/optimize.md +13 -0
  17. package/commands/pan/patches.md +10 -1
  18. package/commands/pan/phase-tests.md +1 -4
  19. package/commands/pan/todo-add.md +1 -1
  20. package/commands/pan/todo-check.md +1 -1
  21. package/hooks/dist/pan-cost-logger.js +54 -4
  22. package/hooks/dist/pan-trace-logger.js +72 -3
  23. package/package.json +67 -66
  24. package/pan-wizard-core/bin/lib/codebase.cjs +2 -0
  25. package/pan-wizard-core/bin/lib/commands.cjs +8 -0
  26. package/pan-wizard-core/bin/lib/config.cjs +13 -2
  27. package/pan-wizard-core/bin/lib/context-budget.cjs +73 -0
  28. package/pan-wizard-core/bin/lib/core.cjs +13 -0
  29. package/pan-wizard-core/bin/lib/doc-lint/frontmatter.js +270 -0
  30. package/pan-wizard-core/bin/lib/doc-lint/reporter.js +45 -0
  31. package/pan-wizard-core/bin/lib/doc-lint/schema.js +202 -0
  32. package/pan-wizard-core/bin/lib/doc-lint/validate.js +190 -0
  33. package/pan-wizard-core/bin/lib/doc-lint/walk.js +135 -0
  34. package/pan-wizard-core/bin/lib/doc-lint.cjs +287 -0
  35. package/pan-wizard-core/bin/lib/experiment.cjs +502 -0
  36. package/pan-wizard-core/bin/lib/learn-index.cjs +235 -0
  37. package/pan-wizard-core/bin/lib/learn-lint.cjs +292 -0
  38. package/pan-wizard-core/bin/lib/links.cjs +549 -0
  39. package/pan-wizard-core/bin/lib/optimize.cjs +474 -1
  40. package/pan-wizard-core/bin/lib/runner.cjs +473 -0
  41. package/pan-wizard-core/bin/lib/verify.cjs +23 -0
  42. package/pan-wizard-core/bin/pan-tools.cjs +247 -3
  43. package/pan-wizard-core/learnings/README.md +70 -0
  44. package/pan-wizard-core/learnings/index.json +540 -0
  45. package/pan-wizard-core/learnings/internal/.gitkeep +2 -0
  46. package/pan-wizard-core/learnings/internal/experiment-runner.md +81 -0
  47. package/pan-wizard-core/learnings/internal/external-research.md +93 -0
  48. package/pan-wizard-core/learnings/internal/loop-design.md +33 -0
  49. package/pan-wizard-core/learnings/internal/pan-dev-bugs.md +181 -0
  50. package/pan-wizard-core/learnings/universal/.gitkeep +2 -0
  51. package/pan-wizard-core/learnings/universal/atomic-state.md +21 -0
  52. package/pan-wizard-core/learnings/universal/binary-io.md +21 -0
  53. package/pan-wizard-core/learnings/universal/comment-syntax.md +21 -0
  54. package/pan-wizard-core/learnings/universal/composition.md +33 -0
  55. package/pan-wizard-core/learnings/universal/concurrency.md +33 -0
  56. package/pan-wizard-core/learnings/universal/dag-scheduler.md +33 -0
  57. package/pan-wizard-core/learnings/universal/data-driven-design.md +21 -0
  58. package/pan-wizard-core/learnings/universal/design-process.md +21 -0
  59. package/pan-wizard-core/learnings/universal/empirical-spike.md +21 -0
  60. package/pan-wizard-core/learnings/universal/error-handling.md +23 -0
  61. package/pan-wizard-core/learnings/universal/error-paths.md +21 -0
  62. package/pan-wizard-core/learnings/universal/glob-semantics.md +21 -0
  63. package/pan-wizard-core/learnings/universal/idempotency.md +21 -0
  64. package/pan-wizard-core/learnings/universal/invariants.md +21 -0
  65. package/pan-wizard-core/learnings/universal/io-patterns.md +21 -0
  66. package/pan-wizard-core/learnings/universal/numeric-edge-cases.md +21 -0
  67. package/pan-wizard-core/learnings/universal/output-conventions.md +21 -0
  68. package/pan-wizard-core/learnings/universal/parser-design.md +21 -0
  69. package/pan-wizard-core/learnings/universal/phase-locking.md +21 -0
  70. package/pan-wizard-core/learnings/universal/pipe-friendly-cli.md +21 -0
  71. package/pan-wizard-core/learnings/universal/schema-design.md +21 -0
  72. package/pan-wizard-core/learnings/universal/secret-handling.md +21 -0
  73. package/pan-wizard-core/learnings/universal/streaming-io.md +21 -0
  74. package/pan-wizard-core/learnings/universal/test-patterns.md +57 -0
  75. package/pan-wizard-core/learnings/universal/test-strategy.md +33 -0
  76. package/pan-wizard-core/learnings/universal/unicode.md +21 -0
  77. package/pan-wizard-core/learnings/universal/vendor-pattern.md +21 -0
  78. package/pan-wizard-core/references/guardrails.md +58 -0
  79. package/pan-wizard-core/references/handoff-decisions.md +156 -0
  80. package/pan-wizard-core/references/schemas/pan-command.schema.yml +39 -0
  81. package/pan-wizard-core/references/verification-patterns.md +31 -0
  82. package/pan-wizard-core/templates/config.json +2 -1
  83. package/pan-wizard-core/templates/idea.md +52 -0
  84. package/pan-wizard-core/templates/summary-complex.md +14 -5
  85. package/pan-wizard-core/templates/summary-minimal.md +6 -0
  86. package/pan-wizard-core/templates/summary-standard.md +14 -3
  87. package/pan-wizard-core/workflows/discuss-phase.md +108 -1
  88. package/pan-wizard-core/workflows/exec-phase.md +37 -1
  89. package/pan-wizard-core/workflows/execute-plan.md +14 -0
  90. package/pan-wizard-core/workflows/health.md +23 -0
  91. package/pan-wizard-core/workflows/new-project.md +65 -81
  92. package/pan-wizard-core/workflows/plan-phase.md +58 -0
  93. package/pan-wizard-core/workflows/transition.md +102 -7
  94. package/pan-wizard-core/workflows/verify-phase.md +14 -0
  95. package/scripts/build-hooks.js +7 -1
  96. package/scripts/generate-skills-docs.py +10 -8
  97. package/scripts/git-hooks/pre-commit +40 -0
  98. package/scripts/release-check.js +184 -0
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+ /**
3
+ * walk.js — recursive .md file walker with --exclude glob support.
4
+ *
5
+ * Synchronous (no need for async — file count is bounded; perf budget allows
6
+ * it; matches PAN's CLI shape). Returns an array of {path, content} where
7
+ * `path` is an absolute path with forward-slash normalization (POSIX).
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /** Normalize a path to POSIX-style forward slashes. */
14
+ function toPosix(p) {
15
+ return p.replace(/\\/g, '/');
16
+ }
17
+
18
+ /**
19
+ * Walk a directory recursively and return all .md files.
20
+ *
21
+ * @param {string} dir - absolute directory path
22
+ * @param {object} [opts]
23
+ * @param {Array<string>} [opts.exclude] - simple glob patterns to exclude
24
+ * (matches against the POSIX path; supported tokens: ** and *)
25
+ * @returns {Array<{path: string, content: string, relativePath: string}>}
26
+ */
27
+ function walkMarkdownFiles(dir, opts = {}) {
28
+ const exclude = opts.exclude || [];
29
+ const excludeRegexes = exclude.map(globToRegex);
30
+
31
+ if (!fs.existsSync(dir)) {
32
+ throw new Error(`directory not found: ${dir}`);
33
+ }
34
+ const stat = fs.statSync(dir);
35
+ if (!stat.isDirectory()) {
36
+ throw new Error(`not a directory: ${dir}`);
37
+ }
38
+
39
+ const out = [];
40
+ const baseAbs = path.resolve(dir);
41
+ walkRecursive(baseAbs, baseAbs, excludeRegexes, out);
42
+ return out;
43
+ }
44
+
45
+ function walkRecursive(currentDir, baseDir, excludeRegexes, out) {
46
+ let entries;
47
+ try {
48
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
49
+ } catch (err) {
50
+ // Permission denied, etc. — emit a synthetic entry the caller can convert
51
+ // to a violation.
52
+ out.push({
53
+ path: toPosix(currentDir),
54
+ content: null,
55
+ relativePath: toPosix(path.relative(baseDir, currentDir)),
56
+ readError: err.message,
57
+ });
58
+ return;
59
+ }
60
+
61
+ for (const entry of entries) {
62
+ const fullPath = path.join(currentDir, entry.name);
63
+ const posixFull = toPosix(fullPath);
64
+ const relative = toPosix(path.relative(baseDir, fullPath));
65
+
66
+ if (matchesAny(posixFull, excludeRegexes) || matchesAny(relative, excludeRegexes)) {
67
+ continue;
68
+ }
69
+
70
+ if (entry.isDirectory()) {
71
+ walkRecursive(fullPath, baseDir, excludeRegexes, out);
72
+ continue;
73
+ }
74
+
75
+ if (entry.isFile() && entry.name.endsWith('.md')) {
76
+ let content = '';
77
+ let readError = null;
78
+ try {
79
+ content = fs.readFileSync(fullPath, 'utf-8');
80
+ } catch (err) {
81
+ readError = err.message;
82
+ }
83
+ out.push({
84
+ path: posixFull,
85
+ relativePath: relative,
86
+ content,
87
+ readError,
88
+ });
89
+ }
90
+ }
91
+ }
92
+
93
+ function matchesAny(s, regexes) {
94
+ for (const re of regexes) if (re.test(s)) return true;
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Convert a simple glob to a RegExp.
100
+ * Supports: ** (matches any chars including /), * (matches any chars except /)
101
+ * Anchored to full string match.
102
+ *
103
+ * Convention: when `**` is followed by `/`, the slash is also optional —
104
+ * `**\/foo.md` matches both `foo.md` (root) and `a/b/foo.md` (nested).
105
+ * Mirrors gitignore / minimatch behavior.
106
+ */
107
+ function globToRegex(glob) {
108
+ let re = '^';
109
+ for (let i = 0; i < glob.length; i++) {
110
+ const ch = glob[i];
111
+ if (ch === '*') {
112
+ if (glob[i + 1] === '*') {
113
+ // Match the standard `**\/<name>` pattern: the trailing slash is
114
+ // optional, so `**\/foo.md` matches root-level `foo.md` too.
115
+ if (glob[i + 2] === '/') {
116
+ re += '(?:.*/)?';
117
+ i += 2; // skip ** and /
118
+ } else {
119
+ re += '.*';
120
+ i++; // skip next *
121
+ }
122
+ } else {
123
+ re += '[^/]*';
124
+ }
125
+ } else if ('.+?^$()|{}[]\\'.includes(ch)) {
126
+ re += '\\' + ch;
127
+ } else {
128
+ re += ch;
129
+ }
130
+ }
131
+ re += '$';
132
+ return new RegExp(re);
133
+ }
134
+
135
+ module.exports = { walkMarkdownFiles, toPosix, globToRegex };
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+ /**
3
+ * doc-lint.cjs — markdown frontmatter linter for PAN's own files.
4
+ *
5
+ * Vendored from whooo (https://github.com/oharms/PanWizard experiments/whooo).
6
+ * Wraps doc-lint/{frontmatter,schema,validate,walk,reporter}.js with PAN's
7
+ * core.cjs output() pattern.
8
+ *
9
+ * Spec: docs/specs/self_improvement_loop_featureai.md (whooo experiment outputs)
10
+ * Pattern source: P-201 + P-202 + P-301 (promoted from whooo run, v3.7.0)
11
+ *
12
+ * Usage (CLI):
13
+ * pan-tools doc-lint <dir> [--schema <path>] [--format json|human] [--strict]
14
+ *
15
+ * Default schema: pan-wizard-core/references/schemas/pan-command.schema.yml
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { output, error } = require('./core.cjs');
21
+
22
+ const { parseFrontmatter } = require('./doc-lint/frontmatter.js');
23
+ const { parseSchema } = require('./doc-lint/schema.js');
24
+ const { validateAgainstSchema } = require('./doc-lint/validate.js');
25
+ const { walkMarkdownFiles } = require('./doc-lint/walk.js');
26
+ const { formatHuman, formatJson, summaryLine } = require('./doc-lint/reporter.js');
27
+
28
+ const DEFAULT_SCHEMA_PATH = path.join(
29
+ __dirname,
30
+ '..',
31
+ '..',
32
+ 'references',
33
+ 'schemas',
34
+ 'pan-command.schema.yml'
35
+ );
36
+
37
+ /**
38
+ * Lint a directory of markdown files against a schema.
39
+ * @param {string} cwd - working directory (used to resolve relative paths)
40
+ * @param {string} dir - directory to scan (relative to cwd or absolute)
41
+ * @param {object} opts - { schema: string, format: 'json'|'human', strict: bool, exclude: string[], raw: bool }
42
+ * @returns {void} — writes to stdout via output(); exit code via process.exit
43
+ */
44
+ function cmdDocLint(cwd, dir, opts = {}) {
45
+ const targetDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
46
+ if (!fs.existsSync(targetDir)) {
47
+ error(`directory not found: ${targetDir}`);
48
+ }
49
+
50
+ const schemaPath = opts.schema
51
+ ? (path.isAbsolute(opts.schema) ? opts.schema : path.join(cwd, opts.schema))
52
+ : DEFAULT_SCHEMA_PATH;
53
+ if (!fs.existsSync(schemaPath)) {
54
+ error(`schema not found: ${schemaPath}`);
55
+ }
56
+
57
+ const schemaText = fs.readFileSync(schemaPath, 'utf-8');
58
+ const { schema, errors: schemaErrors } = parseSchema(schemaText);
59
+ if (schemaErrors.length > 0) {
60
+ if (opts.raw) {
61
+ process.stderr.write(`schema has ${schemaErrors.length} error(s):\n`);
62
+ for (const e of schemaErrors) {
63
+ process.stderr.write(` ${schemaPath}:${e.line} — ${e.message}\n`);
64
+ }
65
+ } else {
66
+ output({ schema_errors: schemaErrors, schema: schemaPath }, false);
67
+ }
68
+ process.exit(2);
69
+ }
70
+
71
+ const exclude = opts.exclude || [];
72
+ const files = walkMarkdownFiles(targetDir, { exclude });
73
+ const violations = [];
74
+ for (const file of files) {
75
+ if (file.readError) {
76
+ violations.push({
77
+ file: file.relativePath,
78
+ line: 1, field: null,
79
+ code: 'file-read-error',
80
+ message: file.readError,
81
+ severity: 'error',
82
+ });
83
+ continue;
84
+ }
85
+ const fm = parseFrontmatter(file.content);
86
+ const v = validateAgainstSchema(fm, schema, file.relativePath, { strict: !!opts.strict });
87
+ violations.push(...v);
88
+ }
89
+
90
+ const fileCount = files.length;
91
+ const errorCount = violations.filter(v => v.severity === 'error').length;
92
+ const warningCount = violations.filter(v => v.severity === 'warning').length;
93
+
94
+ const format = opts.format || 'human';
95
+ if (opts.raw) {
96
+ if (format === 'json') {
97
+ const txt = formatJson(violations);
98
+ if (txt) process.stdout.write(txt + '\n');
99
+ } else {
100
+ const txt = formatHuman(violations);
101
+ if (txt) process.stdout.write(txt + '\n');
102
+ process.stdout.write(summaryLine(violations, fileCount) + '\n');
103
+ }
104
+ } else {
105
+ output({
106
+ directory: dir,
107
+ schema: schemaPath,
108
+ file_count: fileCount,
109
+ error_count: errorCount,
110
+ warning_count: warningCount,
111
+ violations,
112
+ }, false);
113
+ }
114
+
115
+ process.exit(errorCount > 0 ? 1 : 0);
116
+ }
117
+
118
+ /**
119
+ * Validate that a schema file is well-formed.
120
+ */
121
+ function cmdDocLintSchemaCheck(cwd, schemaPath, opts = {}) {
122
+ const resolved = path.isAbsolute(schemaPath) ? schemaPath : path.join(cwd, schemaPath);
123
+ if (!fs.existsSync(resolved)) {
124
+ error(`schema not found: ${resolved}`);
125
+ }
126
+ const text = fs.readFileSync(resolved, 'utf-8');
127
+ const { errors } = parseSchema(text);
128
+ const result = {
129
+ schema: resolved,
130
+ ok: errors.length === 0,
131
+ error_count: errors.length,
132
+ errors,
133
+ };
134
+ output(result, opts.raw);
135
+ process.exit(result.ok ? 0 : 1);
136
+ }
137
+
138
+ // ─── Count-drift lint (IMPROVEMENT-TODO P1, v3.7.10) ────────────────────────
139
+ //
140
+ // Counts (tests, commands, agents, modules, etc.) are supposed to live ONLY
141
+ // in CLAUDE.md. Other docs MUST NOT embed them — they drift instantly. This
142
+ // linter scans markdown files and flags any drift-prone numeric count it
143
+ // finds outside the allowed paths.
144
+
145
+ // Negative lookbehind `(?<!\.)` excludes version-number captures like
146
+ // "v3.5 module" or "v4.7 Commands" — these are version refs, not counts.
147
+ const COUNT_PATTERNS = [
148
+ // "52 commands", "21 agents", "30 modules", "2667 tests", etc.
149
+ // Word boundaries + allowed plurals; case-insensitive matching.
150
+ { re: /(?<!\.)\b(\d+)\s+(commands?|agents?|modules?|workflows?|templates?|references?|specs?|adrs?|test\s+files?|test\s+suites?)\b/gi,
151
+ label: 'noun-phrase count' },
152
+ // "27th module", "21st agent", "52nd command" — drift-prone ordinals
153
+ { re: /(?<!\.)\b(\d+)(th|st|nd|rd)\s+(module|reference|agent|command|template|hook|workflow|spec|adr)\b/gi,
154
+ label: 'ordinal' },
155
+ // "(9 files)", "(58 tests)" — parenthetical counts
156
+ { re: /(?<!\.)\((\d+)\s+(files?|tests?)\)/gi, label: 'parenthetical count' },
157
+ ];
158
+
159
+ // Files where counts ARE allowed:
160
+ // - CLAUDE.md (the SSoT)
161
+ // - CHANGELOG.md (frozen historical record)
162
+ // - MEMORY.md (user memory file, not shipped)
163
+ // - SKILLS-FULL-TEXT.md / SKILLS-REFERENCE.md (auto-generated; embed command
164
+ // prompt text that itself may legitimately reference numbers)
165
+ // - EXAMPLES.md (illustrative tool-output scenarios, not authoritative claims)
166
+ const COUNT_ALLOWED_RE = /(^|[\\/])(CLAUDE\.md|CHANGELOG\.md|MEMORY\.md|SKILLS-FULL-TEXT\.md|SKILLS-REFERENCE\.md|EXAMPLES\.md)$/i;
167
+
168
+ // Path SEGMENTS that mark a directory as count-allowed (frozen historical
169
+ // content). Matched as path segments so they catch both project-root-relative
170
+ // paths (e.g. "docs/decisions/X.md") and scan-root-relative paths (e.g.
171
+ // "decisions/X.md" when the scan rooted at docs/).
172
+ const COUNT_ALLOWED_DIR_SEGMENTS = [
173
+ 'decisions', // ADRs — frozen
174
+ 'specs', // feature specs — frozen
175
+ 'experiments', // harvested experiment artifacts
176
+ 'learnings', // AI-derived patterns; evidence quotes reference numbers
177
+ 'archive', // archived old docs
178
+ ];
179
+
180
+ function isCountAllowed(relativePath) {
181
+ if (COUNT_ALLOWED_RE.test(relativePath)) return true;
182
+ const norm = relativePath.replace(/\\/g, '/');
183
+ const segments = norm.split('/');
184
+ return segments.some(seg => COUNT_ALLOWED_DIR_SEGMENTS.includes(seg));
185
+ }
186
+
187
+ // Things that LOOK like counts but are stable identities (allowed everywhere):
188
+ const STABLE_IDENTITIES = [
189
+ /\b5\s+(target\s+)?runtimes\b/i, // 5 target runtimes
190
+ /\b5\s+hooks\b/i, // 5 hooks (named individually)
191
+ /\bLAYER\s+\d+\b/, // architecture layer labels
192
+ /\b5\s+(parallel\s+)?(researchers?|research\s+)/i, // 5 parallel researchers
193
+ /\b6\s+(parallel\s+)?agents\b/i, // 6 parallel agents (codebase mapper)
194
+ /\b6\s+focus\s+areas\b/i, // 6 focus areas (mapper)
195
+ /\b4\s+parallel\s+research/i, // 4 parallel research
196
+ /\bthree\s+phases\b|\bfour\s+phases\b/i, // generic phase counts in narrative
197
+ ];
198
+
199
+ function isStableIdentity(matchText, surrounding) {
200
+ for (const re of STABLE_IDENTITIES) {
201
+ if (re.test(matchText) || re.test(surrounding)) return true;
202
+ }
203
+ return false;
204
+ }
205
+
206
+ /**
207
+ * Scan a directory tree for drift-prone count violations.
208
+ * @param {string} cwd - working directory
209
+ * @param {string} dir - dir to scan (relative or absolute)
210
+ * @param {object} opts - { format, raw, exclude }
211
+ */
212
+ function cmdDocLintCounts(cwd, dir, opts = {}) {
213
+ const targetDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir);
214
+ if (!fs.existsSync(targetDir)) {
215
+ error(`directory not found: ${targetDir}`);
216
+ }
217
+
218
+ const exclude = opts.exclude || [];
219
+ const files = walkMarkdownFiles(targetDir, { exclude });
220
+ const violations = [];
221
+
222
+ for (const file of files) {
223
+ if (isCountAllowed(file.relativePath)) continue;
224
+ if (file.readError) continue;
225
+
226
+ const lines = file.content.split(/\r?\n/);
227
+ let inFence = false; // track ```fenced``` code blocks; skip their content
228
+ for (let i = 0; i < lines.length; i++) {
229
+ const line = lines[i];
230
+ // Toggle fence on lines starting with ```
231
+ if (/^\s{0,3}```/.test(line)) {
232
+ inFence = !inFence;
233
+ continue;
234
+ }
235
+ if (inFence) continue;
236
+
237
+ for (const { re, label } of COUNT_PATTERNS) {
238
+ re.lastIndex = 0; // reset for /g
239
+ let m;
240
+ while ((m = re.exec(line)) !== null) {
241
+ // Skip if this whole line is in a stable-identity surrounding
242
+ if (isStableIdentity(m[0], line)) continue;
243
+ violations.push({
244
+ file: file.relativePath,
245
+ line: i + 1,
246
+ column: m.index + 1,
247
+ match: m[0],
248
+ label,
249
+ severity: 'warning',
250
+ message: `Drift-prone count "${m[0]}" outside CLAUDE.md. Counts live only in CLAUDE.md; replace with qualitative phrasing or remove.`,
251
+ });
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ const fileCount = files.length;
258
+ const result = {
259
+ directory: dir,
260
+ file_count: fileCount,
261
+ violation_count: violations.length,
262
+ violations,
263
+ };
264
+
265
+ if (opts.raw) {
266
+ if (violations.length === 0) {
267
+ process.stdout.write(`OK — ${fileCount} files scanned, no count violations\n`);
268
+ } else {
269
+ for (const v of violations) {
270
+ process.stdout.write(`${v.file}:${v.line}:${v.column} — ${v.match} (${v.label})\n`);
271
+ }
272
+ process.stdout.write(`\n${violations.length} violation(s) across ${fileCount} files\n`);
273
+ }
274
+ } else {
275
+ output(result, false);
276
+ }
277
+ process.exit(violations.length > 0 ? 1 : 0);
278
+ }
279
+
280
+ module.exports = {
281
+ cmdDocLint,
282
+ cmdDocLintSchemaCheck,
283
+ cmdDocLintCounts,
284
+ isCountAllowed,
285
+ COUNT_PATTERNS,
286
+ DEFAULT_SCHEMA_PATH,
287
+ };