pan-wizard 3.5.1 → 3.7.10

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 (93) hide show
  1. package/README.md +10 -10
  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/optimize.md +13 -0
  16. package/commands/pan/patches.md +10 -1
  17. package/commands/pan/phase-tests.md +1 -4
  18. package/commands/pan/todo-add.md +1 -1
  19. package/commands/pan/todo-check.md +1 -1
  20. package/hooks/dist/pan-cost-logger.js +54 -4
  21. package/hooks/dist/pan-trace-logger.js +72 -3
  22. package/package.json +67 -66
  23. package/pan-wizard-core/bin/lib/commands.cjs +8 -0
  24. package/pan-wizard-core/bin/lib/config.cjs +13 -2
  25. package/pan-wizard-core/bin/lib/context-budget.cjs +73 -0
  26. package/pan-wizard-core/bin/lib/core.cjs +13 -0
  27. package/pan-wizard-core/bin/lib/doc-lint/frontmatter.js +270 -0
  28. package/pan-wizard-core/bin/lib/doc-lint/reporter.js +45 -0
  29. package/pan-wizard-core/bin/lib/doc-lint/schema.js +202 -0
  30. package/pan-wizard-core/bin/lib/doc-lint/validate.js +190 -0
  31. package/pan-wizard-core/bin/lib/doc-lint/walk.js +135 -0
  32. package/pan-wizard-core/bin/lib/doc-lint.cjs +287 -0
  33. package/pan-wizard-core/bin/lib/experiment.cjs +501 -0
  34. package/pan-wizard-core/bin/lib/learn-index.cjs +235 -0
  35. package/pan-wizard-core/bin/lib/learn-lint.cjs +292 -0
  36. package/pan-wizard-core/bin/lib/optimize.cjs +474 -1
  37. package/pan-wizard-core/bin/lib/runner.cjs +472 -0
  38. package/pan-wizard-core/bin/pan-tools.cjs +222 -2
  39. package/pan-wizard-core/learnings/README.md +70 -0
  40. package/pan-wizard-core/learnings/index.json +540 -0
  41. package/pan-wizard-core/learnings/internal/.gitkeep +2 -0
  42. package/pan-wizard-core/learnings/internal/experiment-runner.md +81 -0
  43. package/pan-wizard-core/learnings/internal/external-research.md +93 -0
  44. package/pan-wizard-core/learnings/internal/loop-design.md +33 -0
  45. package/pan-wizard-core/learnings/internal/pan-dev-bugs.md +181 -0
  46. package/pan-wizard-core/learnings/universal/.gitkeep +2 -0
  47. package/pan-wizard-core/learnings/universal/atomic-state.md +21 -0
  48. package/pan-wizard-core/learnings/universal/binary-io.md +21 -0
  49. package/pan-wizard-core/learnings/universal/comment-syntax.md +21 -0
  50. package/pan-wizard-core/learnings/universal/composition.md +33 -0
  51. package/pan-wizard-core/learnings/universal/concurrency.md +33 -0
  52. package/pan-wizard-core/learnings/universal/dag-scheduler.md +33 -0
  53. package/pan-wizard-core/learnings/universal/data-driven-design.md +21 -0
  54. package/pan-wizard-core/learnings/universal/design-process.md +21 -0
  55. package/pan-wizard-core/learnings/universal/empirical-spike.md +21 -0
  56. package/pan-wizard-core/learnings/universal/error-handling.md +23 -0
  57. package/pan-wizard-core/learnings/universal/error-paths.md +21 -0
  58. package/pan-wizard-core/learnings/universal/glob-semantics.md +21 -0
  59. package/pan-wizard-core/learnings/universal/idempotency.md +21 -0
  60. package/pan-wizard-core/learnings/universal/invariants.md +21 -0
  61. package/pan-wizard-core/learnings/universal/io-patterns.md +21 -0
  62. package/pan-wizard-core/learnings/universal/numeric-edge-cases.md +21 -0
  63. package/pan-wizard-core/learnings/universal/output-conventions.md +21 -0
  64. package/pan-wizard-core/learnings/universal/parser-design.md +21 -0
  65. package/pan-wizard-core/learnings/universal/phase-locking.md +21 -0
  66. package/pan-wizard-core/learnings/universal/pipe-friendly-cli.md +21 -0
  67. package/pan-wizard-core/learnings/universal/schema-design.md +21 -0
  68. package/pan-wizard-core/learnings/universal/secret-handling.md +21 -0
  69. package/pan-wizard-core/learnings/universal/streaming-io.md +21 -0
  70. package/pan-wizard-core/learnings/universal/test-patterns.md +57 -0
  71. package/pan-wizard-core/learnings/universal/test-strategy.md +33 -0
  72. package/pan-wizard-core/learnings/universal/unicode.md +21 -0
  73. package/pan-wizard-core/learnings/universal/vendor-pattern.md +21 -0
  74. package/pan-wizard-core/references/guardrails.md +58 -0
  75. package/pan-wizard-core/references/handoff-decisions.md +156 -0
  76. package/pan-wizard-core/references/schemas/pan-command.schema.yml +39 -0
  77. package/pan-wizard-core/references/verification-patterns.md +31 -0
  78. package/pan-wizard-core/templates/config.json +2 -1
  79. package/pan-wizard-core/templates/idea.md +52 -0
  80. package/pan-wizard-core/templates/summary-complex.md +14 -5
  81. package/pan-wizard-core/templates/summary-minimal.md +6 -0
  82. package/pan-wizard-core/templates/summary-standard.md +14 -3
  83. package/pan-wizard-core/workflows/discuss-phase.md +108 -1
  84. package/pan-wizard-core/workflows/exec-phase.md +37 -1
  85. package/pan-wizard-core/workflows/execute-plan.md +14 -0
  86. package/pan-wizard-core/workflows/health.md +23 -0
  87. package/pan-wizard-core/workflows/new-project.md +65 -81
  88. package/pan-wizard-core/workflows/plan-phase.md +58 -0
  89. package/pan-wizard-core/workflows/transition.md +102 -7
  90. package/pan-wizard-core/workflows/verify-phase.md +14 -0
  91. package/scripts/build-hooks.js +7 -1
  92. package/scripts/generate-skills-docs.py +10 -8
  93. package/scripts/release-check.js +184 -0
@@ -0,0 +1,202 @@
1
+ 'use strict';
2
+ /**
3
+ * schema.js — parse + validate schema definition files.
4
+ *
5
+ * Schema source format (also YAML-ish, parsed by frontmatter.js's parser
6
+ * applied to a fenced wrapper):
7
+ *
8
+ * fields:
9
+ * title:
10
+ * required: true
11
+ * type: string
12
+ * pattern: ^[A-Z]
13
+ *
14
+ * Returns: {schema, errors} where schema is the normalized form with regexes
15
+ * compiled, types validated, etc.
16
+ *
17
+ * Note: we don't reuse the frontmatter parser's block style — schemas use a
18
+ * deeper nested map shape than v0.1 frontmatter supports. We hand-roll a tiny
19
+ * indentation-aware parser tuned for the schema shape only. This is a
20
+ * scope-bounded compromise documented in DESIGN_SPEC §"YAML subset".
21
+ */
22
+
23
+ const VALID_TYPES = ['string', 'number', 'boolean', 'enum', 'array'];
24
+ const FIELD_KEYS = ['required', 'type', 'pattern', 'values', 'default', 'items'];
25
+
26
+ /**
27
+ * Parse a schema source string into a normalized schema or return errors.
28
+ * @returns {{schema: object|null, errors: Array<{line:number, message:string}>}}
29
+ */
30
+ function parseSchema(text) {
31
+ const errors = [];
32
+ if (text == null || text.trim() === '') {
33
+ return { schema: null, errors: [{ line: 1, message: 'schema is empty' }] };
34
+ }
35
+
36
+ const lines = text.split(/\r?\n/);
37
+
38
+ // Find `fields:` line
39
+ let fieldsLine = -1;
40
+ for (let i = 0; i < lines.length; i++) {
41
+ if (lines[i].trim() === 'fields:') {
42
+ fieldsLine = i;
43
+ break;
44
+ }
45
+ }
46
+
47
+ if (fieldsLine === -1) {
48
+ errors.push({ line: 1, message: 'schema must start with `fields:` block' });
49
+ return { schema: null, errors };
50
+ }
51
+
52
+ // Parse fields block
53
+ const fields = {};
54
+ let currentField = null;
55
+ let currentFieldLine = 0;
56
+
57
+ for (let i = fieldsLine + 1; i < lines.length; i++) {
58
+ const lineNum = i + 1; // 1-indexed
59
+ const raw = lines[i];
60
+
61
+ // Skip blank lines
62
+ if (raw.trim() === '') continue;
63
+ // Skip comment lines
64
+ if (raw.trim().startsWith('#')) continue;
65
+
66
+ // Field name: 2-space indent, ends with `:`
67
+ const fieldMatch = raw.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):\s*$/);
68
+ if (fieldMatch) {
69
+ currentField = fieldMatch[1];
70
+ currentFieldLine = lineNum;
71
+ if (currentField in fields) {
72
+ errors.push({ line: lineNum, message: `duplicate field "${currentField}"` });
73
+ }
74
+ fields[currentField] = { _line: lineNum };
75
+ continue;
76
+ }
77
+
78
+ // Field property: 4-space indent
79
+ const propMatch = raw.match(/^ ([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
80
+ if (propMatch && currentField) {
81
+ const key = propMatch[1];
82
+ const rawValue = propMatch[2].trim();
83
+
84
+ if (!FIELD_KEYS.includes(key)) {
85
+ errors.push({ line: lineNum, message: `unknown field property "${key}" (allowed: ${FIELD_KEYS.join(', ')})` });
86
+ continue;
87
+ }
88
+
89
+ const parsed = parseSchemaValue(key, rawValue, lineNum);
90
+ if (parsed.error) {
91
+ errors.push({ line: lineNum, message: parsed.error });
92
+ continue;
93
+ }
94
+ fields[currentField][key] = parsed.value;
95
+ continue;
96
+ }
97
+
98
+ // Anything else (including incorrectly-indented lines)
99
+ if (raw.length > 0 && !/^\s*$/.test(raw)) {
100
+ errors.push({ line: lineNum, message: `unexpected line in fields block: ${JSON.stringify(raw)}` });
101
+ }
102
+ }
103
+
104
+ // Validate each field
105
+ const normalized = {};
106
+ for (const [name, def] of Object.entries(fields)) {
107
+ const fieldErrors = checkFieldDefinition(name, def);
108
+ errors.push(...fieldErrors);
109
+ if (fieldErrors.length === 0) {
110
+ // Strip the _line marker from the public shape; keep all other props
111
+ const { _line, ...publicDef } = def;
112
+ normalized[name] = publicDef;
113
+ }
114
+ }
115
+
116
+ return {
117
+ schema: errors.length === 0 ? { fields: normalized } : null,
118
+ errors,
119
+ };
120
+ }
121
+
122
+ function parseSchemaValue(key, rawValue, lineNum) {
123
+ if (key === 'required') {
124
+ if (rawValue === 'true') return { value: true };
125
+ if (rawValue === 'false') return { value: false };
126
+ return { error: `required must be true|false, got ${JSON.stringify(rawValue)}` };
127
+ }
128
+
129
+ if (key === 'type') {
130
+ if (!VALID_TYPES.includes(rawValue)) {
131
+ return { error: `type must be one of ${VALID_TYPES.join(', ')}, got ${JSON.stringify(rawValue)}` };
132
+ }
133
+ return { value: rawValue };
134
+ }
135
+
136
+ if (key === 'pattern') {
137
+ try {
138
+ return { value: new RegExp(rawValue) };
139
+ } catch (e) {
140
+ return { error: `invalid regex pattern: ${e.message}` };
141
+ }
142
+ }
143
+
144
+ if (key === 'values') {
145
+ // Inline list shape: [a, b, c]
146
+ if (!rawValue.startsWith('[') || !rawValue.endsWith(']')) {
147
+ return { error: `values must be a flow list [a, b, c], got ${JSON.stringify(rawValue)}` };
148
+ }
149
+ const inner = rawValue.slice(1, -1).trim();
150
+ if (inner === '') return { value: [] };
151
+ return { value: inner.split(',').map(s => s.trim()).filter(Boolean) };
152
+ }
153
+
154
+ if (key === 'default') {
155
+ return { value: rawValue }; // store raw
156
+ }
157
+
158
+ if (key === 'items') {
159
+ if (!VALID_TYPES.includes(rawValue)) {
160
+ return { error: `items must be one of ${VALID_TYPES.join(', ')}, got ${JSON.stringify(rawValue)}` };
161
+ }
162
+ return { value: rawValue };
163
+ }
164
+
165
+ return { error: `unknown property ${key}` };
166
+ }
167
+
168
+ function checkFieldDefinition(name, def) {
169
+ const errors = [];
170
+ const line = def._line || 1;
171
+
172
+ if (!('type' in def)) {
173
+ errors.push({ line, message: `field "${name}" missing required property "type"` });
174
+ return errors;
175
+ }
176
+
177
+ if (def.type === 'enum' && !('values' in def)) {
178
+ errors.push({ line, message: `field "${name}" type=enum requires "values:"` });
179
+ }
180
+
181
+ if (def.type === 'array' && !('items' in def)) {
182
+ errors.push({ line, message: `field "${name}" type=array requires "items:"` });
183
+ }
184
+
185
+ if (def.required === true && 'default' in def) {
186
+ errors.push({ line, message: `field "${name}" cannot have both required:true and default:` });
187
+ }
188
+
189
+ return errors;
190
+ }
191
+
192
+ /**
193
+ * Public API: validate that a parsed schema is well-formed (no extra checks
194
+ * beyond what parseSchema already does, but exposed for the CLI's
195
+ * `whooo schema check` subcommand).
196
+ */
197
+ function checkSchema(schemaText) {
198
+ const result = parseSchema(schemaText);
199
+ return { ok: result.errors.length === 0, errors: result.errors };
200
+ }
201
+
202
+ module.exports = { parseSchema, checkSchema, VALID_TYPES, FIELD_KEYS };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+ /**
3
+ * validate.js — validate parsed frontmatter data against a parsed schema.
4
+ *
5
+ * Returns an array of violations matching the shape documented in
6
+ * DESIGN_SPEC.md §"Key data shapes":
7
+ *
8
+ * { file, line, field, code, message, severity }
9
+ *
10
+ * Implements the 8 error codes in DESIGN_SPEC.md §"Error codes":
11
+ * frontmatter-missing, frontmatter-malformed, required-missing,
12
+ * type-mismatch, enum-violation, pattern-mismatch, array-item-type,
13
+ * unknown-field
14
+ *
15
+ * The {data, errors, hasFrontmatter} input is whatever frontmatter.js
16
+ * returned for a given file. line numbers are absolute in the source file.
17
+ */
18
+
19
+ /**
20
+ * @param {object} fmResult - {data, bodyStart, errors, hasFrontmatter} from parseFrontmatter
21
+ * @param {object} schema - parsed schema from parseSchema (i.e., {fields: {...}})
22
+ * @param {string} sourceFile - POSIX-style relative path for the violation report
23
+ * @param {object} [opts] - {strict: bool} — if true, frontmatter-missing is error severity
24
+ * @returns {Array<Violation>}
25
+ */
26
+ function validateAgainstSchema(fmResult, schema, sourceFile, opts = {}) {
27
+ const violations = [];
28
+ const strict = !!opts.strict;
29
+
30
+ // 1. Frontmatter parse errors → frontmatter-malformed
31
+ if (fmResult.errors && fmResult.errors.length > 0) {
32
+ for (const err of fmResult.errors) {
33
+ violations.push(violation({
34
+ file: sourceFile, line: err.line, field: null,
35
+ code: 'frontmatter-malformed',
36
+ message: err.message,
37
+ severity: 'error',
38
+ }));
39
+ }
40
+ return violations; // can't validate fields if parse failed
41
+ }
42
+
43
+ // 2. No frontmatter at all
44
+ if (fmResult.hasFrontmatter === false) {
45
+ violations.push(violation({
46
+ file: sourceFile, line: 1, field: null,
47
+ code: 'frontmatter-missing',
48
+ message: 'file has no frontmatter',
49
+ severity: strict ? 'error' : 'warning',
50
+ }));
51
+ return violations;
52
+ }
53
+
54
+ const data = fmResult.data || {};
55
+ const fields = schema.fields || {};
56
+
57
+ // 3. Per-field checks
58
+ for (const [name, def] of Object.entries(fields)) {
59
+ const present = name in data;
60
+ const value = data[name];
61
+
62
+ if (!present) {
63
+ if (def.required) {
64
+ violations.push(violation({
65
+ file: sourceFile, line: 1, field: name,
66
+ code: 'required-missing',
67
+ message: `field "${name}" is required`,
68
+ severity: 'error',
69
+ }));
70
+ }
71
+ continue;
72
+ }
73
+
74
+ // Type check
75
+ const typeError = checkType(value, def);
76
+ if (typeError) {
77
+ violations.push(violation({
78
+ file: sourceFile, line: 1, field: name,
79
+ code: typeError.code,
80
+ message: typeError.message,
81
+ severity: 'error',
82
+ }));
83
+ continue;
84
+ }
85
+
86
+ // Enum check
87
+ if (def.type === 'enum' && Array.isArray(def.values) && !def.values.includes(String(value))) {
88
+ violations.push(violation({
89
+ file: sourceFile, line: 1, field: name,
90
+ code: 'enum-violation',
91
+ message: `field "${name}" value ${JSON.stringify(value)} not in [${def.values.join(', ')}]`,
92
+ severity: 'error',
93
+ }));
94
+ continue;
95
+ }
96
+
97
+ // Pattern check
98
+ if (def.pattern instanceof RegExp && typeof value === 'string' && !def.pattern.test(value)) {
99
+ violations.push(violation({
100
+ file: sourceFile, line: 1, field: name,
101
+ code: 'pattern-mismatch',
102
+ message: `field "${name}" value ${JSON.stringify(value)} does not match pattern ${def.pattern}`,
103
+ severity: 'error',
104
+ }));
105
+ continue;
106
+ }
107
+ }
108
+
109
+ // 4. Unknown fields (warning only — info-level)
110
+ for (const key of Object.keys(data)) {
111
+ if (!(key in fields)) {
112
+ violations.push(violation({
113
+ file: sourceFile, line: 1, field: key,
114
+ code: 'unknown-field',
115
+ message: `field "${key}" is not in schema (consider adding it or removing from file)`,
116
+ severity: 'warning',
117
+ }));
118
+ }
119
+ }
120
+
121
+ return violations;
122
+ }
123
+
124
+ function checkType(value, def) {
125
+ switch (def.type) {
126
+ case 'string':
127
+ if (typeof value !== 'string') {
128
+ return { code: 'type-mismatch', message: `expected string, got ${typeOf(value)}` };
129
+ }
130
+ return null;
131
+ case 'number':
132
+ if (typeof value !== 'number') {
133
+ return { code: 'type-mismatch', message: `expected number, got ${typeOf(value)}` };
134
+ }
135
+ return null;
136
+ case 'boolean':
137
+ if (typeof value !== 'boolean') {
138
+ return { code: 'type-mismatch', message: `expected boolean, got ${typeOf(value)}` };
139
+ }
140
+ return null;
141
+ case 'enum':
142
+ if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
143
+ return { code: 'type-mismatch', message: `expected scalar (for enum), got ${typeOf(value)}` };
144
+ }
145
+ return null;
146
+ case 'array':
147
+ if (!Array.isArray(value)) {
148
+ return { code: 'type-mismatch', message: `expected array, got ${typeOf(value)}` };
149
+ }
150
+ // Item type check
151
+ if (def.items) {
152
+ for (let i = 0; i < value.length; i++) {
153
+ const itemType = typeOf(value[i]);
154
+ const expected = def.items;
155
+ // Note: the item-type contract treats string/number/boolean only;
156
+ // enum/array items are out of scope for v0.1.
157
+ const matches = (
158
+ (expected === 'string' && itemType === 'string') ||
159
+ (expected === 'number' && itemType === 'number') ||
160
+ (expected === 'boolean' && itemType === 'boolean')
161
+ );
162
+ if (!matches) {
163
+ return { code: 'array-item-type', message: `array item [${i}] expected ${expected}, got ${itemType}` };
164
+ }
165
+ }
166
+ }
167
+ return null;
168
+ default:
169
+ return null;
170
+ }
171
+ }
172
+
173
+ function typeOf(v) {
174
+ if (v === null) return 'null';
175
+ if (Array.isArray(v)) return 'array';
176
+ return typeof v;
177
+ }
178
+
179
+ function violation(v) {
180
+ return {
181
+ file: v.file,
182
+ line: v.line || 1,
183
+ field: v.field || null,
184
+ code: v.code,
185
+ message: v.message,
186
+ severity: v.severity || 'error',
187
+ };
188
+ }
189
+
190
+ module.exports = { validateAgainstSchema };
@@ -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 };