@vibe-agent-toolkit/resources 0.1.3 → 0.1.5
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 +0 -17
- package/dist/collection-matcher.d.ts +63 -0
- package/dist/collection-matcher.d.ts.map +1 -0
- package/dist/collection-matcher.js +127 -0
- package/dist/collection-matcher.js.map +1 -0
- package/dist/config-parser.d.ts +63 -0
- package/dist/config-parser.d.ts.map +1 -0
- package/dist/config-parser.js +113 -0
- package/dist/config-parser.js.map +1 -0
- package/dist/frontmatter-validator.d.ts +12 -2
- package/dist/frontmatter-validator.d.ts.map +1 -1
- package/dist/frontmatter-validator.js +174 -18
- package/dist/frontmatter-validator.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/link-validator.d.ts +25 -3
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +75 -49
- package/dist/link-validator.js.map +1 -1
- package/dist/multi-schema-validator.d.ts +42 -0
- package/dist/multi-schema-validator.d.ts.map +1 -0
- package/dist/multi-schema-validator.js +107 -0
- package/dist/multi-schema-validator.js.map +1 -0
- package/dist/pattern-expander.d.ts +63 -0
- package/dist/pattern-expander.d.ts.map +1 -0
- package/dist/pattern-expander.js +93 -0
- package/dist/pattern-expander.js.map +1 -0
- package/dist/resource-registry.d.ts +87 -6
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +215 -46
- package/dist/resource-registry.js.map +1 -1
- package/dist/schema-assignment.d.ts +49 -0
- package/dist/schema-assignment.d.ts.map +1 -0
- package/dist/schema-assignment.js +95 -0
- package/dist/schema-assignment.js.map +1 -0
- package/dist/schemas/project-config.d.ts +254 -0
- package/dist/schemas/project-config.d.ts.map +1 -0
- package/dist/schemas/project-config.js +57 -0
- package/dist/schemas/project-config.js.map +1 -0
- package/dist/schemas/resource-metadata.d.ts +3 -0
- package/dist/schemas/resource-metadata.d.ts.map +1 -1
- package/dist/schemas/resource-metadata.js +2 -0
- package/dist/schemas/resource-metadata.js.map +1 -1
- package/dist/schemas/validation-result.d.ts +2 -26
- package/dist/schemas/validation-result.d.ts.map +1 -1
- package/dist/schemas/validation-result.js +4 -20
- package/dist/schemas/validation-result.js.map +1 -1
- package/dist/types/resource-parser.d.ts +53 -0
- package/dist/types/resource-parser.d.ts.map +1 -0
- package/dist/types/resource-parser.js +233 -0
- package/dist/types/resource-parser.js.map +1 -0
- package/dist/types/resource-path-utils.d.ts +43 -0
- package/dist/types/resource-path-utils.d.ts.map +1 -0
- package/dist/types/resource-path-utils.js +89 -0
- package/dist/types/resource-path-utils.js.map +1 -0
- package/dist/types/resources.d.ts +140 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +58 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -0
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +39 -0
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/collection-matcher.ts +148 -0
- package/src/config-parser.ts +125 -0
- package/src/frontmatter-validator.ts +202 -18
- package/src/index.ts +7 -2
- package/src/link-validator.ts +100 -51
- package/src/multi-schema-validator.ts +128 -0
- package/src/pattern-expander.ts +100 -0
- package/src/resource-registry.ts +322 -54
- package/src/schema-assignment.ts +119 -0
- package/src/schemas/project-config.ts +71 -0
- package/src/schemas/resource-metadata.ts +2 -0
- package/src/schemas/validation-result.ts +4 -23
- package/src/types/resource-parser.ts +302 -0
- package/src/types/resource-path-utils.ts +102 -0
- package/src/types/resources.ts +211 -0
- package/src/types.ts +81 -1
- package/src/utils.ts +43 -0
package/src/link-validator.ts
CHANGED
|
@@ -2,21 +2,42 @@
|
|
|
2
2
|
* Link validation for markdown resources.
|
|
3
3
|
*
|
|
4
4
|
* Validates different types of links:
|
|
5
|
-
* - local_file: Checks if file exists, validates anchors if present
|
|
5
|
+
* - local_file: Checks if file exists, validates anchors if present, checks git-ignore safety
|
|
6
6
|
* - anchor: Validates heading exists in current or target file
|
|
7
7
|
* - external: Returns info (not validated)
|
|
8
8
|
* - email: Returns null (valid by default)
|
|
9
9
|
* - unknown: Returns warning
|
|
10
|
+
*
|
|
11
|
+
* Git-ignore safety (Phase 3):
|
|
12
|
+
* - Non-ignored files cannot link to ignored files (error: link_to_gitignored)
|
|
13
|
+
* - Ignored files CAN link to ignored files (no error)
|
|
14
|
+
* - Ignored files CAN link to non-ignored files (no error)
|
|
15
|
+
* - External resources (outside project) skip git-ignore checks
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
|
-
import fs from 'node:fs/promises';
|
|
13
18
|
import path from 'node:path';
|
|
14
19
|
|
|
15
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isGitIgnored,
|
|
22
|
+
type GitTracker,
|
|
23
|
+
verifyCaseSensitiveFilename,
|
|
24
|
+
} from '@vibe-agent-toolkit/utils';
|
|
16
25
|
|
|
17
26
|
import type { ValidationIssue } from './schemas/validation-result.js';
|
|
18
27
|
import type { HeadingNode, ResourceLink } from './types.js';
|
|
19
|
-
import { splitHrefAnchor } from './utils.js';
|
|
28
|
+
import { isWithinProject, splitHrefAnchor } from './utils.js';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for link validation.
|
|
32
|
+
*/
|
|
33
|
+
export interface ValidateLinkOptions {
|
|
34
|
+
/** Project root directory (for git-ignore checking) */
|
|
35
|
+
projectRoot?: string;
|
|
36
|
+
/** Skip git-ignore checks (optimization when checkGitIgnored is false) */
|
|
37
|
+
skipGitIgnoreCheck?: boolean;
|
|
38
|
+
/** Git tracker for efficient git-ignore checking (optional, improves performance) */
|
|
39
|
+
gitTracker?: GitTracker;
|
|
40
|
+
}
|
|
20
41
|
|
|
21
42
|
/**
|
|
22
43
|
* Validate a single link in a markdown resource.
|
|
@@ -24,11 +45,15 @@ import { splitHrefAnchor } from './utils.js';
|
|
|
24
45
|
* @param link - The link to validate
|
|
25
46
|
* @param sourceFilePath - Absolute path to the file containing the link
|
|
26
47
|
* @param headingsByFile - Map of file paths to their heading trees
|
|
48
|
+
* @param options - Validation options (projectRoot, skipGitIgnoreCheck)
|
|
27
49
|
* @returns ValidationIssue if link is broken, null if valid
|
|
28
50
|
*
|
|
29
51
|
* @example
|
|
30
52
|
* ```typescript
|
|
31
|
-
* const issue = await validateLink(link, '/project/docs/guide.md', headingsMap
|
|
53
|
+
* const issue = await validateLink(link, '/project/docs/guide.md', headingsMap, {
|
|
54
|
+
* projectRoot: '/project',
|
|
55
|
+
* skipGitIgnoreCheck: false
|
|
56
|
+
* });
|
|
32
57
|
* if (issue) {
|
|
33
58
|
* console.log(`${issue.severity}: ${issue.message}`);
|
|
34
59
|
* }
|
|
@@ -37,25 +62,19 @@ import { splitHrefAnchor } from './utils.js';
|
|
|
37
62
|
export async function validateLink(
|
|
38
63
|
link: ResourceLink,
|
|
39
64
|
sourceFilePath: string,
|
|
40
|
-
headingsByFile: Map<string, HeadingNode[]
|
|
65
|
+
headingsByFile: Map<string, HeadingNode[]>,
|
|
66
|
+
options?: ValidateLinkOptions
|
|
41
67
|
): Promise<ValidationIssue | null> {
|
|
42
68
|
switch (link.type) {
|
|
43
69
|
case 'local_file':
|
|
44
|
-
return await validateLocalFileLink(link, sourceFilePath, headingsByFile);
|
|
70
|
+
return await validateLocalFileLink(link, sourceFilePath, headingsByFile, options);
|
|
45
71
|
|
|
46
72
|
case 'anchor':
|
|
47
73
|
return await validateAnchorLink(link, sourceFilePath, headingsByFile);
|
|
48
74
|
|
|
49
75
|
case 'external':
|
|
50
|
-
// External URLs are not validated -
|
|
51
|
-
return
|
|
52
|
-
severity: 'info',
|
|
53
|
-
resourcePath: sourceFilePath,
|
|
54
|
-
line: link.line,
|
|
55
|
-
type: 'external_url',
|
|
56
|
-
link: link.href,
|
|
57
|
-
message: 'External URL not validated',
|
|
58
|
-
};
|
|
76
|
+
// External URLs are not validated - don't report them
|
|
77
|
+
return null;
|
|
59
78
|
|
|
60
79
|
case 'email':
|
|
61
80
|
// Email links are valid by default
|
|
@@ -63,7 +82,6 @@ export async function validateLink(
|
|
|
63
82
|
|
|
64
83
|
case 'unknown':
|
|
65
84
|
return {
|
|
66
|
-
severity: 'warning',
|
|
67
85
|
resourcePath: sourceFilePath,
|
|
68
86
|
line: link.line,
|
|
69
87
|
type: 'unknown_link',
|
|
@@ -85,7 +103,8 @@ export async function validateLink(
|
|
|
85
103
|
async function validateLocalFileLink(
|
|
86
104
|
link: ResourceLink,
|
|
87
105
|
sourceFilePath: string,
|
|
88
|
-
headingsByFile: Map<string, HeadingNode[]
|
|
106
|
+
headingsByFile: Map<string, HeadingNode[]>,
|
|
107
|
+
options?: ValidateLinkOptions
|
|
89
108
|
): Promise<ValidationIssue | null> {
|
|
90
109
|
// Extract file path and anchor from href
|
|
91
110
|
const [filePath, anchor] = splitHrefAnchor(link.href);
|
|
@@ -94,29 +113,58 @@ async function validateLocalFileLink(
|
|
|
94
113
|
const fileResult = await validateLocalFile(filePath, sourceFilePath);
|
|
95
114
|
|
|
96
115
|
if (!fileResult.exists) {
|
|
116
|
+
// Check if it's a case mismatch
|
|
117
|
+
if (fileResult.actualName) {
|
|
118
|
+
const expectedName = path.basename(fileResult.resolvedPath);
|
|
119
|
+
return {
|
|
120
|
+
resourcePath: sourceFilePath,
|
|
121
|
+
line: link.line,
|
|
122
|
+
type: 'broken_file',
|
|
123
|
+
link: link.href,
|
|
124
|
+
message: `File found but case mismatch: expected "${expectedName}" but found "${fileResult.actualName}". This will fail on case-sensitive filesystems (Linux). Update the link to match the actual filename.`,
|
|
125
|
+
suggestion: `Use "${fileResult.actualName}" instead of "${expectedName}"`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
97
129
|
return {
|
|
98
|
-
severity: 'error',
|
|
99
130
|
resourcePath: sourceFilePath,
|
|
100
131
|
line: link.line,
|
|
101
132
|
type: 'broken_file',
|
|
102
133
|
link: link.href,
|
|
103
134
|
message: `File not found: ${fileResult.resolvedPath}`,
|
|
104
|
-
suggestion: '
|
|
135
|
+
suggestion: '',
|
|
105
136
|
};
|
|
106
137
|
}
|
|
107
138
|
|
|
108
|
-
// Check
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
// Check git-ignore safety (Phase 3)
|
|
140
|
+
// Only check if:
|
|
141
|
+
// 1. skipGitIgnoreCheck is NOT true
|
|
142
|
+
// 2. projectRoot is provided
|
|
143
|
+
// 3. target is within project (skip for external resources)
|
|
144
|
+
if (
|
|
145
|
+
options?.skipGitIgnoreCheck !== true &&
|
|
146
|
+
options?.projectRoot !== undefined &&
|
|
147
|
+
isWithinProject(fileResult.resolvedPath, options.projectRoot)
|
|
148
|
+
) {
|
|
149
|
+
// Use GitTracker if available (cached), otherwise fall back to isGitIgnored
|
|
150
|
+
const sourceIsIgnored = options.gitTracker
|
|
151
|
+
? options.gitTracker.isIgnored(sourceFilePath)
|
|
152
|
+
: isGitIgnored(sourceFilePath, options.projectRoot);
|
|
153
|
+
const targetIsIgnored = options.gitTracker
|
|
154
|
+
? options.gitTracker.isIgnored(fileResult.resolvedPath)
|
|
155
|
+
: isGitIgnored(fileResult.resolvedPath, options.projectRoot);
|
|
156
|
+
|
|
157
|
+
// Error ONLY if: source is NOT ignored AND target IS ignored
|
|
158
|
+
if (!sourceIsIgnored && targetIsIgnored) {
|
|
159
|
+
return {
|
|
160
|
+
resourcePath: sourceFilePath,
|
|
161
|
+
line: link.line,
|
|
162
|
+
type: 'link_to_gitignored',
|
|
163
|
+
link: link.href,
|
|
164
|
+
message: `Non-ignored file links to gitignored file: ${fileResult.resolvedPath}. Gitignored files are local-only and will not exist in the repository. Remove this link or unignore the target file.`,
|
|
165
|
+
suggestion: '',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
120
168
|
}
|
|
121
169
|
|
|
122
170
|
// If there's an anchor, validate it too
|
|
@@ -129,13 +177,12 @@ async function validateLocalFileLink(
|
|
|
129
177
|
|
|
130
178
|
if (!anchorValid) {
|
|
131
179
|
return {
|
|
132
|
-
severity: 'error',
|
|
133
180
|
resourcePath: sourceFilePath,
|
|
134
181
|
line: link.line,
|
|
135
182
|
type: 'broken_anchor',
|
|
136
183
|
link: link.href,
|
|
137
184
|
message: `Anchor not found: #${anchor} in ${fileResult.resolvedPath}`,
|
|
138
|
-
suggestion: '
|
|
185
|
+
suggestion: '',
|
|
139
186
|
};
|
|
140
187
|
}
|
|
141
188
|
}
|
|
@@ -159,13 +206,12 @@ async function validateAnchorLink(
|
|
|
159
206
|
|
|
160
207
|
if (!isValid) {
|
|
161
208
|
return {
|
|
162
|
-
severity: 'error',
|
|
163
209
|
resourcePath: sourceFilePath,
|
|
164
210
|
line: link.line,
|
|
165
211
|
type: 'broken_anchor',
|
|
166
212
|
link: link.href,
|
|
167
213
|
message: `Anchor not found: ${link.href}`,
|
|
168
|
-
suggestion: '
|
|
214
|
+
suggestion: '',
|
|
169
215
|
};
|
|
170
216
|
}
|
|
171
217
|
|
|
@@ -174,41 +220,44 @@ async function validateAnchorLink(
|
|
|
174
220
|
|
|
175
221
|
|
|
176
222
|
/**
|
|
177
|
-
* Validate that a local file exists
|
|
223
|
+
* Validate that a local file exists with the correct case.
|
|
178
224
|
*
|
|
179
225
|
* @param href - The href to the file (relative or absolute)
|
|
180
226
|
* @param sourceFilePath - Absolute path to the source file
|
|
181
|
-
* @returns Object with exists flag, resolved absolute path, and
|
|
227
|
+
* @returns Object with exists flag, resolved absolute path, and optional case mismatch info
|
|
182
228
|
*
|
|
183
229
|
* @example
|
|
184
230
|
* ```typescript
|
|
185
231
|
* const result = await validateLocalFile('./docs/guide.md', '/project/README.md');
|
|
186
|
-
* if (result.exists
|
|
232
|
+
* if (result.exists) {
|
|
187
233
|
* console.log('File exists at:', result.resolvedPath);
|
|
234
|
+
* } else if (result.actualName) {
|
|
235
|
+
* console.log('Case mismatch:', result.actualName);
|
|
188
236
|
* }
|
|
189
237
|
* ```
|
|
190
238
|
*/
|
|
191
239
|
async function validateLocalFile(
|
|
192
240
|
href: string,
|
|
193
241
|
sourceFilePath: string
|
|
194
|
-
): Promise<{ exists: boolean; resolvedPath: string;
|
|
242
|
+
): Promise<{ exists: boolean; resolvedPath: string; actualName?: string }> {
|
|
195
243
|
// Resolve the path relative to the source file's directory
|
|
196
244
|
const sourceDir = path.dirname(sourceFilePath);
|
|
197
245
|
const resolvedPath = path.resolve(sourceDir, href);
|
|
198
246
|
|
|
199
|
-
// Check if file exists
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
await fs.access(resolvedPath, fs.constants.F_OK);
|
|
203
|
-
exists = true;
|
|
204
|
-
} catch {
|
|
205
|
-
exists = false;
|
|
206
|
-
}
|
|
247
|
+
// Check if file exists with correct case
|
|
248
|
+
const verification = await verifyCaseSensitiveFilename(resolvedPath);
|
|
207
249
|
|
|
208
|
-
//
|
|
209
|
-
const
|
|
250
|
+
// Build result with optional actualName (only include if present)
|
|
251
|
+
const result: { exists: boolean; resolvedPath: string; actualName?: string } = {
|
|
252
|
+
exists: verification.exists,
|
|
253
|
+
resolvedPath,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (verification.actualName) {
|
|
257
|
+
result.actualName = verification.actualName;
|
|
258
|
+
}
|
|
210
259
|
|
|
211
|
-
return
|
|
260
|
+
return result;
|
|
212
261
|
}
|
|
213
262
|
|
|
214
263
|
/**
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-schema validation for resources
|
|
3
|
+
*
|
|
4
|
+
* Validates a resource against multiple schemas from different sources,
|
|
5
|
+
* tracking validation results per schema.
|
|
6
|
+
*
|
|
7
|
+
* Supports validation modes:
|
|
8
|
+
* - strict: Enforce schema exactly (respect additionalProperties: false)
|
|
9
|
+
* - permissive: Allow extra fields (schema layering use case)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { validateFrontmatter } from './frontmatter-validator.js';
|
|
16
|
+
import type { ValidationMode } from './schemas/project-config.js';
|
|
17
|
+
import type { ValidationIssue } from './schemas/validation-result.js';
|
|
18
|
+
import type { SchemaReference } from './types/resources.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load a JSON Schema from a file path
|
|
22
|
+
*
|
|
23
|
+
* @param schemaPath - Path to JSON Schema file
|
|
24
|
+
* @param projectRoot - Optional project root for resolving relative paths
|
|
25
|
+
* @returns Parsed JSON Schema object
|
|
26
|
+
*/
|
|
27
|
+
async function loadSchema(schemaPath: string, projectRoot?: string): Promise<object> {
|
|
28
|
+
let resolvedPath = schemaPath;
|
|
29
|
+
|
|
30
|
+
// If path is relative and we have a project root, resolve it
|
|
31
|
+
if (!path.isAbsolute(schemaPath) && projectRoot) {
|
|
32
|
+
resolvedPath = path.join(projectRoot, schemaPath);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
36
|
+
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
37
|
+
return JSON.parse(content) as object;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Validate frontmatter against multiple schemas
|
|
42
|
+
*
|
|
43
|
+
* Each schema is validated independently and results are tracked separately.
|
|
44
|
+
* The resource-level validation status: fails if ANY schema fails.
|
|
45
|
+
*
|
|
46
|
+
* @param frontmatter - Parsed frontmatter object (or undefined if no frontmatter)
|
|
47
|
+
* @param schemas - Schema references to validate against
|
|
48
|
+
* @param resourcePath - File path for error reporting
|
|
49
|
+
* @param mode - Validation mode (strict or permissive)
|
|
50
|
+
* @param projectRoot - Optional project root for resolving relative schema paths
|
|
51
|
+
* @returns Updated schema references with validation results
|
|
52
|
+
*/
|
|
53
|
+
export async function validateFrontmatterMultiSchema(
|
|
54
|
+
frontmatter: Record<string, unknown> | undefined,
|
|
55
|
+
schemas: SchemaReference[],
|
|
56
|
+
resourcePath: string,
|
|
57
|
+
mode: ValidationMode,
|
|
58
|
+
projectRoot?: string,
|
|
59
|
+
): Promise<SchemaReference[]> {
|
|
60
|
+
const results: SchemaReference[] = [];
|
|
61
|
+
|
|
62
|
+
for (const schemaRef of schemas) {
|
|
63
|
+
try {
|
|
64
|
+
// Load schema
|
|
65
|
+
const schema = await loadSchema(schemaRef.schema, projectRoot);
|
|
66
|
+
|
|
67
|
+
// Validate frontmatter
|
|
68
|
+
const issues = validateFrontmatter(frontmatter, schema, resourcePath, mode);
|
|
69
|
+
|
|
70
|
+
// Update schema reference with results
|
|
71
|
+
const result: SchemaReference = {
|
|
72
|
+
...schemaRef,
|
|
73
|
+
applied: true,
|
|
74
|
+
valid: issues.length === 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Only set errors if there are any (exactOptionalPropertyTypes)
|
|
78
|
+
if (issues.length > 0) {
|
|
79
|
+
result.errors = issues;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
results.push(result);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Schema loading or validation failed
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
results.push({
|
|
87
|
+
...schemaRef,
|
|
88
|
+
applied: true,
|
|
89
|
+
valid: false,
|
|
90
|
+
errors: [{
|
|
91
|
+
resourcePath,
|
|
92
|
+
line: 1,
|
|
93
|
+
type: 'frontmatter_schema_error',
|
|
94
|
+
link: '',
|
|
95
|
+
message: `Failed to load or validate schema ${schemaRef.schema}: ${message}`,
|
|
96
|
+
}],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if any schema validation failed
|
|
106
|
+
*
|
|
107
|
+
* @param schemas - Schema references with validation results
|
|
108
|
+
* @returns True if any schema failed validation
|
|
109
|
+
*/
|
|
110
|
+
export function hasSchemaErrors(schemas: SchemaReference[]): boolean {
|
|
111
|
+
return schemas.some((ref) => ref.valid === false);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get all validation errors from all schemas
|
|
116
|
+
*
|
|
117
|
+
* @param schemas - Schema references with validation results
|
|
118
|
+
* @returns Flat array of all validation issues
|
|
119
|
+
*/
|
|
120
|
+
export function getAllSchemaErrors(schemas: SchemaReference[]): ValidationIssue[] {
|
|
121
|
+
const allErrors: ValidationIssue[] = [];
|
|
122
|
+
for (const ref of schemas) {
|
|
123
|
+
if (ref.errors) {
|
|
124
|
+
allErrors.push(...ref.errors);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return allErrors;
|
|
128
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern expansion utilities for converting paths to glob patterns.
|
|
3
|
+
*
|
|
4
|
+
* Expands directory paths to glob patterns while preserving explicit glob patterns.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFAULT_EXTENSIONS = '**/*.{md,json}';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check if a string is a glob pattern or a plain path.
|
|
11
|
+
*
|
|
12
|
+
* A string is considered a glob pattern if it contains glob metacharacters.
|
|
13
|
+
*
|
|
14
|
+
* @param pattern - String to check
|
|
15
|
+
* @returns True if string contains glob metacharacters
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* isGlobPattern('docs') // false
|
|
20
|
+
* isGlobPattern('docs/**\/*.md') // true
|
|
21
|
+
* isGlobPattern('**\/*.json') // true
|
|
22
|
+
* isGlobPattern('path/to/file.md') // false
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function isGlobPattern(pattern: string): boolean {
|
|
26
|
+
return /[*?[\]{}]/.test(pattern);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Expand a path to a glob pattern.
|
|
31
|
+
*
|
|
32
|
+
* - If input already starts with **\/ or is absolute (starts with /), return as-is
|
|
33
|
+
* - If input is a glob pattern without **\/, prepend **\/ to match absolute paths
|
|
34
|
+
* - If input is a path, expand to **\/path/**\/*.{md,json}
|
|
35
|
+
* - Trailing slashes are stripped before expansion
|
|
36
|
+
* - The **\/ prefix ensures patterns match absolute paths from any location
|
|
37
|
+
*
|
|
38
|
+
* Note: Patterns are expected to use forward slashes. Use toForwardSlash() on paths before
|
|
39
|
+
* passing to this function if they might contain backslashes (Windows).
|
|
40
|
+
*
|
|
41
|
+
* @param pathOrPattern - Path or glob pattern (with forward slashes)
|
|
42
|
+
* @returns Expanded glob pattern
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* expandPattern('docs') // '**\/docs/**\/*.{md,json}'
|
|
47
|
+
* expandPattern('docs/') // '**\/docs/**\/*.{md,json}'
|
|
48
|
+
* expandPattern('docs/**\/*.md') // '**\/docs/**\/*.md' (prepend **\/)
|
|
49
|
+
* expandPattern('**\/*.schema.json') // '**\/*.schema.json' (unchanged)
|
|
50
|
+
* expandPattern('*.md') // '*.md' (root-level pattern, no prefix)
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function expandPattern(pathOrPattern: string): string {
|
|
54
|
+
// Note: This function expects pattern strings (from config), not file paths.
|
|
55
|
+
// Pattern strings from YAML config should already use forward slashes (YAML standard).
|
|
56
|
+
// This function doesn't normalize because it operates on pattern syntax, not paths.
|
|
57
|
+
|
|
58
|
+
// If already starts with **/ or is absolute, return as-is
|
|
59
|
+
// Pattern strings are always forward-slash based (YAML/config standard)
|
|
60
|
+
// eslint-disable-next-line local/no-path-startswith -- checking pattern syntax, not paths
|
|
61
|
+
if (pathOrPattern.startsWith('**/') || pathOrPattern.startsWith('/')) {
|
|
62
|
+
return pathOrPattern;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If it's a glob pattern
|
|
66
|
+
if (isGlobPattern(pathOrPattern)) {
|
|
67
|
+
// Root-level patterns (*.md, *.json) should match only root level
|
|
68
|
+
// These are purely glob metacharacters, not paths
|
|
69
|
+
// eslint-disable-next-line local/no-path-startswith -- checking pattern syntax, not paths
|
|
70
|
+
if (pathOrPattern.startsWith('*')) {
|
|
71
|
+
return pathOrPattern;
|
|
72
|
+
}
|
|
73
|
+
// Other glob patterns need **/ prefix to match absolute paths
|
|
74
|
+
return `**/${pathOrPattern}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Strip trailing slash for plain paths
|
|
78
|
+
const normalizedPath = pathOrPattern.replace(/\/$/, '');
|
|
79
|
+
|
|
80
|
+
// Expand path to pattern with **/ prefix for absolute path matching
|
|
81
|
+
return `**/${normalizedPath}/${DEFAULT_EXTENSIONS}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Expand an array of paths/patterns to glob patterns.
|
|
86
|
+
*
|
|
87
|
+
* Each item is processed individually using expandPattern().
|
|
88
|
+
*
|
|
89
|
+
* @param patterns - Array of paths or glob patterns
|
|
90
|
+
* @returns Array of expanded glob patterns
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* expandPatterns(['docs', 'src/**\/*.ts', 'README.md'])
|
|
95
|
+
* // ['docs/**\/*.{md,json}', 'src/**\/*.ts', 'README.md/**\/*.{md,json}']
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function expandPatterns(patterns: string[]): string[] {
|
|
99
|
+
return patterns.map(expandPattern);
|
|
100
|
+
}
|