@vibe-agent-toolkit/resources 0.1.38 → 0.1.39-rc.1
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 +33 -24
- package/dist/frontmatter-link-validator.d.ts +5 -5
- package/dist/frontmatter-link-validator.d.ts.map +1 -1
- package/dist/frontmatter-link-validator.js +25 -24
- package/dist/frontmatter-link-validator.js.map +1 -1
- package/dist/frontmatter-validator.d.ts +3 -2
- package/dist/frontmatter-validator.d.ts.map +1 -1
- package/dist/frontmatter-validator.js +8 -14
- package/dist/frontmatter-validator.js.map +1 -1
- package/dist/link-parser.js +12 -5
- package/dist/link-parser.js.map +1 -1
- package/dist/link-validator.d.ts +3 -3
- package/dist/link-validator.d.ts.map +1 -1
- package/dist/link-validator.js +32 -80
- package/dist/link-validator.js.map +1 -1
- package/dist/multi-schema-validator.d.ts.map +1 -1
- package/dist/multi-schema-validator.js +6 -8
- package/dist/multi-schema-validator.js.map +1 -1
- package/dist/resource-registry.d.ts +10 -2
- package/dist/resource-registry.d.ts.map +1 -1
- package/dist/resource-registry.js +25 -32
- package/dist/resource-registry.js.map +1 -1
- package/dist/schemas/project-config.d.ts +219 -171
- package/dist/schemas/project-config.d.ts.map +1 -1
- package/dist/schemas/project-config.js +2 -0
- package/dist/schemas/project-config.js.map +1 -1
- package/dist/schemas/validation-result.d.ts +36 -57
- package/dist/schemas/validation-result.d.ts.map +1 -1
- package/dist/schemas/validation-result.js +5 -27
- package/dist/schemas/validation-result.js.map +1 -1
- package/dist/types/resources.d.ts +1 -1
- package/dist/types/resources.d.ts.map +1 -1
- package/dist/types/resources.js.map +1 -1
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +14 -0
- package/dist/utils.js.map +1 -1
- package/package.json +3 -3
- package/src/frontmatter-link-validator.ts +23 -25
- package/src/frontmatter-validator.ts +19 -16
- package/src/link-parser.ts +25 -9
- package/src/link-validator.ts +88 -78
- package/src/multi-schema-validator.ts +10 -8
- package/src/resource-registry.ts +48 -33
- package/src/schemas/project-config.ts +2 -0
- package/src/schemas/validation-result.ts +5 -29
- package/src/types/resources.ts +2 -1
- package/src/utils.ts +15 -0
package/src/link-validator.ts
CHANGED
|
@@ -18,15 +18,36 @@
|
|
|
18
18
|
import fs from 'node:fs/promises';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
|
|
21
|
+
import { createRegistryIssue, type ValidationIssue } from '@vibe-agent-toolkit/agent-schema';
|
|
21
22
|
import {
|
|
22
23
|
isGitIgnored,
|
|
23
24
|
type GitTracker,
|
|
24
25
|
verifyCaseSensitiveFilename,
|
|
25
26
|
} from '@vibe-agent-toolkit/utils';
|
|
26
27
|
|
|
27
|
-
import type { ValidationIssue } from './schemas/validation-result.js';
|
|
28
28
|
import type { HeadingNode, ResourceLink } from './types.js';
|
|
29
|
-
import { isWithinProject, resolveLocalHref } from './utils.js';
|
|
29
|
+
import { isWithinProject, issueLocation, resolveLocalHref } from './utils.js';
|
|
30
|
+
|
|
31
|
+
type LinkIssueExtras = Partial<Pick<ValidationIssue, 'location' | 'line' | 'link' | 'suggestion'>>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the common `createRegistryIssue` extras for a link issue: relative
|
|
35
|
+
* location, the problematic href, the line (only when defined — required for
|
|
36
|
+
* exactOptionalPropertyTypes), and an optional suggestion.
|
|
37
|
+
*/
|
|
38
|
+
function linkExtras(
|
|
39
|
+
link: ResourceLink,
|
|
40
|
+
sourceFilePath: string,
|
|
41
|
+
projectRoot: string | undefined,
|
|
42
|
+
suggestion?: string,
|
|
43
|
+
): LinkIssueExtras {
|
|
44
|
+
return {
|
|
45
|
+
location: issueLocation(sourceFilePath, projectRoot),
|
|
46
|
+
link: link.href,
|
|
47
|
+
...(link.line !== undefined && { line: link.line }),
|
|
48
|
+
...(suggestion !== undefined && { suggestion }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Options for link validation.
|
|
@@ -71,7 +92,7 @@ export async function validateLink(
|
|
|
71
92
|
return await validateLocalFileLink(link, sourceFilePath, headingsByFile, options);
|
|
72
93
|
|
|
73
94
|
case 'anchor':
|
|
74
|
-
return await validateAnchorLink(link, sourceFilePath, headingsByFile);
|
|
95
|
+
return await validateAnchorLink(link, sourceFilePath, headingsByFile, options?.projectRoot);
|
|
75
96
|
|
|
76
97
|
case 'external':
|
|
77
98
|
// External URLs are not validated - don't report them
|
|
@@ -82,13 +103,11 @@ export async function validateLink(
|
|
|
82
103
|
return null;
|
|
83
104
|
|
|
84
105
|
case 'unknown':
|
|
85
|
-
return
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
message: 'Unknown link type',
|
|
91
|
-
};
|
|
106
|
+
return createRegistryIssue(
|
|
107
|
+
'LINK_UNKNOWN',
|
|
108
|
+
'Unknown link type',
|
|
109
|
+
linkExtras(link, sourceFilePath, options?.projectRoot),
|
|
110
|
+
);
|
|
92
111
|
|
|
93
112
|
default: {
|
|
94
113
|
// TypeScript exhaustiveness check
|
|
@@ -107,31 +126,29 @@ export function resolutionFailureIssue(
|
|
|
107
126
|
resolved: ReturnType<typeof resolveLocalHref>,
|
|
108
127
|
link: ResourceLink,
|
|
109
128
|
sourceFilePath: string,
|
|
129
|
+
projectRoot?: string,
|
|
110
130
|
): ValidationIssue | null {
|
|
111
131
|
if (resolved.kind === 'absolute_no_root') {
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
type: 'broken_file',
|
|
116
|
-
link: link.href,
|
|
117
|
-
message:
|
|
118
|
-
`Absolute-path link "${link.href}" requires a configured projectRoot; ` +
|
|
132
|
+
return createRegistryIssue(
|
|
133
|
+
'LINK_BROKEN_FILE',
|
|
134
|
+
`Absolute-path link "${link.href}" requires a configured projectRoot; ` +
|
|
119
135
|
`none was provided. Configure vibe-agent-toolkit.config.yaml or run ` +
|
|
120
136
|
`from within a git repository.`,
|
|
121
|
-
|
|
137
|
+
linkExtras(
|
|
138
|
+
link,
|
|
139
|
+
sourceFilePath,
|
|
140
|
+
projectRoot,
|
|
122
141
|
'Rewrite as a source-relative link, or run from a directory with a config or git ancestor.',
|
|
123
|
-
|
|
142
|
+
),
|
|
143
|
+
);
|
|
124
144
|
}
|
|
125
145
|
|
|
126
146
|
if (resolved.kind === 'absolute_escapes_root') {
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
message: `Absolute-path link "${link.href}" escapes the project root via path traversal.`,
|
|
133
|
-
suggestion: '',
|
|
134
|
-
};
|
|
147
|
+
return createRegistryIssue(
|
|
148
|
+
'LINK_BROKEN_FILE',
|
|
149
|
+
`Absolute-path link "${link.href}" escapes the project root via path traversal.`,
|
|
150
|
+
linkExtras(link, sourceFilePath, projectRoot, ''),
|
|
151
|
+
);
|
|
135
152
|
}
|
|
136
153
|
|
|
137
154
|
return null;
|
|
@@ -145,29 +162,29 @@ export function fileExistenceIssue(
|
|
|
145
162
|
fileResult: { exists: boolean; resolvedPath: string; actualName?: string },
|
|
146
163
|
link: ResourceLink,
|
|
147
164
|
sourceFilePath: string,
|
|
165
|
+
projectRoot?: string,
|
|
148
166
|
): ValidationIssue | null {
|
|
149
167
|
if (fileResult.exists) return null;
|
|
150
168
|
|
|
151
169
|
if (fileResult.actualName) {
|
|
152
170
|
const expectedName = path.basename(fileResult.resolvedPath);
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
return createRegistryIssue(
|
|
172
|
+
'LINK_BROKEN_FILE',
|
|
173
|
+
`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.`,
|
|
174
|
+
linkExtras(
|
|
175
|
+
link,
|
|
176
|
+
sourceFilePath,
|
|
177
|
+
projectRoot,
|
|
178
|
+
`Use "${fileResult.actualName}" instead of "${expectedName}"`,
|
|
179
|
+
),
|
|
180
|
+
);
|
|
161
181
|
}
|
|
162
182
|
|
|
163
|
-
return
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
message: `File not found: ${fileResult.resolvedPath}`,
|
|
169
|
-
suggestion: '',
|
|
170
|
-
};
|
|
183
|
+
return createRegistryIssue(
|
|
184
|
+
'LINK_BROKEN_FILE',
|
|
185
|
+
`File not found: ${fileResult.resolvedPath}`,
|
|
186
|
+
linkExtras(link, sourceFilePath, projectRoot, ''),
|
|
187
|
+
);
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
/**
|
|
@@ -202,14 +219,11 @@ export function gitIgnoreSafetyIssue(
|
|
|
202
219
|
|
|
203
220
|
if (sourceIsIgnored || !targetIsIgnored) return null;
|
|
204
221
|
|
|
205
|
-
return
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
message: `Non-ignored file links to gitignored file: ${resolvedTarget}. Gitignored files are local-only and will not exist in the repository. Remove this link or unignore the target file.`,
|
|
211
|
-
suggestion: '',
|
|
212
|
-
};
|
|
222
|
+
return createRegistryIssue(
|
|
223
|
+
'LINK_TO_GITIGNORED',
|
|
224
|
+
`Non-ignored file links to gitignored file: ${resolvedTarget}. Gitignored files are local-only and will not exist in the repository. Remove this link or unignore the target file.`,
|
|
225
|
+
linkExtras(link, sourceFilePath, options.projectRoot, ''),
|
|
226
|
+
);
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
/**
|
|
@@ -225,23 +239,24 @@ async function validateLocalFileLink(
|
|
|
225
239
|
|
|
226
240
|
if (resolved.kind !== 'resolved') {
|
|
227
241
|
// anchor_only → null no-op; absolute_no_root / absolute_escapes_root → broken_file.
|
|
228
|
-
return resolutionFailureIssue(resolved, link, sourceFilePath);
|
|
242
|
+
return resolutionFailureIssue(resolved, link, sourceFilePath, options?.projectRoot);
|
|
229
243
|
}
|
|
230
244
|
|
|
231
245
|
const fileResult = await validateResolvedFile(resolved.resolvedPath);
|
|
232
|
-
const notFound = fileExistenceIssue(fileResult, link, sourceFilePath);
|
|
246
|
+
const notFound = fileExistenceIssue(fileResult, link, sourceFilePath, options?.projectRoot);
|
|
233
247
|
if (notFound) return notFound;
|
|
234
248
|
|
|
235
249
|
if (fileResult.isDirectory) {
|
|
236
|
-
return
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
250
|
+
return createRegistryIssue(
|
|
251
|
+
'LINK_BROKEN_FILE',
|
|
252
|
+
`Link target is a directory: ${fileResult.resolvedPath}`,
|
|
253
|
+
linkExtras(
|
|
254
|
+
link,
|
|
255
|
+
sourceFilePath,
|
|
256
|
+
options?.projectRoot,
|
|
243
257
|
'Link to a file inside the directory (e.g., README.md or index.md), or fix the link to point at the intended file.',
|
|
244
|
-
|
|
258
|
+
),
|
|
259
|
+
);
|
|
245
260
|
}
|
|
246
261
|
|
|
247
262
|
const gitIgnoreIssue = gitIgnoreSafetyIssue(link, sourceFilePath, fileResult.resolvedPath, options);
|
|
@@ -250,14 +265,11 @@ async function validateLocalFileLink(
|
|
|
250
265
|
if (resolved.anchor) {
|
|
251
266
|
const anchorValid = await validateAnchor(resolved.anchor, fileResult.resolvedPath, headingsByFile);
|
|
252
267
|
if (!anchorValid) {
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
message: `Anchor not found: #${resolved.anchor} in ${fileResult.resolvedPath}`,
|
|
259
|
-
suggestion: '',
|
|
260
|
-
};
|
|
268
|
+
return createRegistryIssue(
|
|
269
|
+
'LINK_BROKEN_ANCHOR',
|
|
270
|
+
`Anchor not found: #${resolved.anchor} in ${fileResult.resolvedPath}`,
|
|
271
|
+
linkExtras(link, sourceFilePath, options?.projectRoot, ''),
|
|
272
|
+
);
|
|
261
273
|
}
|
|
262
274
|
}
|
|
263
275
|
|
|
@@ -270,7 +282,8 @@ async function validateLocalFileLink(
|
|
|
270
282
|
async function validateAnchorLink(
|
|
271
283
|
link: ResourceLink,
|
|
272
284
|
sourceFilePath: string,
|
|
273
|
-
headingsByFile: Map<string, HeadingNode[]
|
|
285
|
+
headingsByFile: Map<string, HeadingNode[]>,
|
|
286
|
+
projectRoot?: string,
|
|
274
287
|
): Promise<ValidationIssue | null> {
|
|
275
288
|
// Extract anchor (strip leading #)
|
|
276
289
|
const anchor = link.href.startsWith('#') ? link.href.slice(1) : link.href;
|
|
@@ -279,14 +292,11 @@ async function validateAnchorLink(
|
|
|
279
292
|
const isValid = await validateAnchor(anchor, sourceFilePath, headingsByFile);
|
|
280
293
|
|
|
281
294
|
if (!isValid) {
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
message: `Anchor not found: ${link.href}`,
|
|
288
|
-
suggestion: '',
|
|
289
|
-
};
|
|
295
|
+
return createRegistryIssue(
|
|
296
|
+
'LINK_BROKEN_ANCHOR',
|
|
297
|
+
`Anchor not found: ${link.href}`,
|
|
298
|
+
linkExtras(link, sourceFilePath, projectRoot, ''),
|
|
299
|
+
);
|
|
290
300
|
}
|
|
291
301
|
|
|
292
302
|
return null;
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
import { promises as fs } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
|
|
15
|
+
import { createRegistryIssue } from '@vibe-agent-toolkit/agent-schema';
|
|
15
16
|
import { safePath } from '@vibe-agent-toolkit/utils';
|
|
16
17
|
|
|
17
18
|
import { validateFrontmatter } from './frontmatter-validator.js';
|
|
18
19
|
import type { ValidationMode } from './schemas/project-config.js';
|
|
19
20
|
import type { ValidationIssue } from './schemas/validation-result.js';
|
|
20
21
|
import type { SchemaReference } from './types/resources.js';
|
|
22
|
+
import { issueLocation } from './utils.js';
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Load a JSON Schema from a file path
|
|
@@ -67,7 +69,7 @@ export async function validateFrontmatterMultiSchema(
|
|
|
67
69
|
const schema = await loadSchema(schemaRef.schema, projectRoot);
|
|
68
70
|
|
|
69
71
|
// Validate frontmatter
|
|
70
|
-
const issues = validateFrontmatter(frontmatter, schema, resourcePath, mode);
|
|
72
|
+
const issues = validateFrontmatter(frontmatter, schema, resourcePath, mode, undefined, projectRoot);
|
|
71
73
|
|
|
72
74
|
// Update schema reference with results
|
|
73
75
|
const result: SchemaReference = {
|
|
@@ -89,13 +91,13 @@ export async function validateFrontmatterMultiSchema(
|
|
|
89
91
|
...schemaRef,
|
|
90
92
|
applied: true,
|
|
91
93
|
valid: false,
|
|
92
|
-
errors: [
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
errors: [
|
|
95
|
+
createRegistryIssue(
|
|
96
|
+
'FRONTMATTER_SCHEMA_ERROR',
|
|
97
|
+
`Failed to load or validate schema ${schemaRef.schema}: ${message}`,
|
|
98
|
+
{ location: issueLocation(resourcePath, projectRoot), line: 1 },
|
|
99
|
+
),
|
|
100
|
+
],
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
103
|
}
|
package/src/resource-registry.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type fs from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
|
|
14
|
+
import { createRegistryIssue, type IssueCode, runValidationFramework, type ValidationConfig, type ValidationIssue } from '@vibe-agent-toolkit/agent-schema';
|
|
14
15
|
import { crawlDirectory, type CrawlOptions as UtilsCrawlOptions, type GitTracker, normalizedTmpdir, resolveAssetReference, safePath, toForwardSlash } from '@vibe-agent-toolkit/utils';
|
|
15
16
|
|
|
16
17
|
import { calculateChecksum } from './checksum.js';
|
|
@@ -27,8 +28,8 @@ import type { ResourceCollectionInterface } from './resource-collection-interfac
|
|
|
27
28
|
import type { SHA256 } from './schemas/checksum.js';
|
|
28
29
|
import type { ProjectConfig } from './schemas/project-config.js';
|
|
29
30
|
import type { HeadingNode, ResourceMetadata } from './schemas/resource-metadata.js';
|
|
30
|
-
import type {
|
|
31
|
-
import { matchesGlobPattern, splitHrefAnchor } from './utils.js';
|
|
31
|
+
import type { ValidationResult } from './schemas/validation-result.js';
|
|
32
|
+
import { issueLocation, matchesGlobPattern, splitHrefAnchor } from './utils.js';
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Options for crawling directories to add resources.
|
|
@@ -72,6 +73,13 @@ export interface ValidateOptions {
|
|
|
72
73
|
checkExternalUrls?: boolean;
|
|
73
74
|
/** Disable cache for external URL checks (default: false) */
|
|
74
75
|
noCache?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Validation framework config (severity overrides + per-code allow entries).
|
|
78
|
+
* Applied INSIDE validate() via runValidationFramework — the library, not the
|
|
79
|
+
* CLI, resolves severity and drops ignored issues. Defaults to `{}` (no
|
|
80
|
+
* overrides: every issue keeps its registry default severity).
|
|
81
|
+
*/
|
|
82
|
+
validationConfig?: ValidationConfig;
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
/**
|
|
@@ -422,13 +430,13 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
422
430
|
const issues: ValidationIssue[] = [];
|
|
423
431
|
for (const resource of this.resourcesByPath.values()) {
|
|
424
432
|
if (resource.frontmatterError) {
|
|
425
|
-
issues.push(
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
issues.push(
|
|
434
|
+
createRegistryIssue(
|
|
435
|
+
'FRONTMATTER_INVALID_YAML',
|
|
436
|
+
`Invalid YAML syntax in frontmatter: ${resource.frontmatterError}`,
|
|
437
|
+
{ location: issueLocation(resource.filePath, this.baseDir), line: 1 },
|
|
438
|
+
),
|
|
439
|
+
);
|
|
432
440
|
}
|
|
433
441
|
}
|
|
434
442
|
return issues;
|
|
@@ -479,7 +487,9 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
479
487
|
resource.frontmatter,
|
|
480
488
|
schema,
|
|
481
489
|
resource.filePath,
|
|
482
|
-
mode
|
|
490
|
+
mode,
|
|
491
|
+
undefined,
|
|
492
|
+
this.baseDir,
|
|
483
493
|
);
|
|
484
494
|
issues.push(...frontmatterIssues);
|
|
485
495
|
}
|
|
@@ -593,6 +603,7 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
593
603
|
resource.filePath,
|
|
594
604
|
mode,
|
|
595
605
|
schemaPath,
|
|
606
|
+
this.baseDir,
|
|
596
607
|
);
|
|
597
608
|
|
|
598
609
|
// New: walk URI-family frontmatter values. Default-on; explicit `false` disables.
|
|
@@ -624,13 +635,13 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
624
635
|
} catch (error) {
|
|
625
636
|
// Handle missing or invalid schema files gracefully
|
|
626
637
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
627
|
-
return [
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
638
|
+
return [
|
|
639
|
+
createRegistryIssue(
|
|
640
|
+
'FRONTMATTER_SCHEMA_ERROR',
|
|
641
|
+
`Failed to load or parse frontmatter schema '${validation.frontmatterSchema}': ${errorMessage}`,
|
|
642
|
+
{ location: issueLocation(resource.filePath, this.baseDir), line: 1 },
|
|
643
|
+
),
|
|
644
|
+
];
|
|
634
645
|
}
|
|
635
646
|
}
|
|
636
647
|
|
|
@@ -706,8 +717,11 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
706
717
|
issues.push(...externalUrlIssues);
|
|
707
718
|
}
|
|
708
719
|
|
|
709
|
-
//
|
|
710
|
-
|
|
720
|
+
// Resolve severity + apply allow-filter INSIDE the library (not the CLI).
|
|
721
|
+
// `emitted` = post-allow-filter, severity-resolved, with `ignore`d dropped.
|
|
722
|
+
const framework = runValidationFramework(issues, options?.validationConfig ?? {});
|
|
723
|
+
const emitted = framework.emitted;
|
|
724
|
+
const errorCount = emitted.length;
|
|
711
725
|
|
|
712
726
|
// Count links by type
|
|
713
727
|
const linksByType: Record<string, number> = {};
|
|
@@ -726,9 +740,10 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
726
740
|
0
|
|
727
741
|
),
|
|
728
742
|
linksByType,
|
|
729
|
-
issues,
|
|
743
|
+
issues: emitted,
|
|
730
744
|
errorCount,
|
|
731
745
|
passed: errorCount === 0,
|
|
746
|
+
hasErrors: framework.hasErrors,
|
|
732
747
|
durationMs,
|
|
733
748
|
timestamp: new Date(),
|
|
734
749
|
};
|
|
@@ -830,17 +845,17 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
830
845
|
continue;
|
|
831
846
|
}
|
|
832
847
|
|
|
833
|
-
const
|
|
848
|
+
const issueCode = this.determineExternalUrlIssueCode(result.statusCode, result.error);
|
|
834
849
|
const errorMessage = result.error ?? `HTTP ${result.statusCode}`;
|
|
835
850
|
|
|
836
851
|
for (const location of locations) {
|
|
837
|
-
issues.push(
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
852
|
+
issues.push(
|
|
853
|
+
createRegistryIssue(issueCode, `External URL failed: ${errorMessage}`, {
|
|
854
|
+
location: issueLocation(location.resourcePath, this.baseDir),
|
|
855
|
+
link: result.url,
|
|
856
|
+
...(location.line !== undefined && { line: location.line }),
|
|
857
|
+
}),
|
|
858
|
+
);
|
|
844
859
|
}
|
|
845
860
|
}
|
|
846
861
|
|
|
@@ -848,18 +863,18 @@ export class ResourceRegistry implements ResourceCollectionInterface {
|
|
|
848
863
|
}
|
|
849
864
|
|
|
850
865
|
/**
|
|
851
|
-
* Determine issue
|
|
866
|
+
* Determine the registry issue code based on the external-URL validation error.
|
|
852
867
|
* @private
|
|
853
868
|
*/
|
|
854
|
-
private
|
|
869
|
+
private determineExternalUrlIssueCode(statusCode: number, error?: string): IssueCode {
|
|
855
870
|
if (statusCode === 0) {
|
|
856
871
|
const errorLower = error?.toString().toLowerCase();
|
|
857
872
|
if (errorLower?.includes('timeout')) {
|
|
858
|
-
return '
|
|
873
|
+
return 'EXTERNAL_URL_TIMEOUT';
|
|
859
874
|
}
|
|
860
|
-
return '
|
|
875
|
+
return 'EXTERNAL_URL_ERROR';
|
|
861
876
|
}
|
|
862
|
-
return '
|
|
877
|
+
return 'EXTERNAL_URL_DEAD';
|
|
863
878
|
}
|
|
864
879
|
|
|
865
880
|
/**
|
|
@@ -96,6 +96,8 @@ export const ResourcesConfigSchema = z.object({
|
|
|
96
96
|
.describe('Global default exclude patterns (not used by collections in Phase 2)'),
|
|
97
97
|
collections: z.record(z.string(), CollectionConfigSchema).optional()
|
|
98
98
|
.describe('Named collections of resources'),
|
|
99
|
+
validation: ValidationConfigSchema.optional()
|
|
100
|
+
.describe('Validation framework config: severity overrides and per-code allow entries (applied inside ResourceRegistry.validate)'),
|
|
99
101
|
}).describe('Resources section of project configuration');
|
|
100
102
|
|
|
101
103
|
export type ResourcesConfig = z.infer<typeof ResourcesConfigSchema>;
|
|
@@ -1,32 +1,7 @@
|
|
|
1
|
+
import { ValidationIssueSchema } from '@vibe-agent-toolkit/agent-schema';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
* A single validation issue found during link validation.
|
|
5
|
-
*
|
|
6
|
-
* Issue types:
|
|
7
|
-
* - broken_file: Local file link points to non-existent file
|
|
8
|
-
* - broken_anchor: Anchor link points to non-existent heading
|
|
9
|
-
* - frontmatter_missing: Schema requires frontmatter, file has none
|
|
10
|
-
* - frontmatter_invalid_yaml: YAML syntax error in frontmatter
|
|
11
|
-
* - frontmatter_schema_error: Frontmatter fails JSON Schema validation
|
|
12
|
-
* - external_url_dead: External URL returned error status (4xx, 5xx)
|
|
13
|
-
* - external_url_timeout: External URL request timed out
|
|
14
|
-
* - external_url_error: External URL validation failed (DNS, network, etc.)
|
|
15
|
-
* - unknown_link: Unknown link type
|
|
16
|
-
*
|
|
17
|
-
* Includes details about what went wrong, where it occurred, and optionally
|
|
18
|
-
* how to fix it.
|
|
19
|
-
*/
|
|
20
|
-
export const ValidationIssueSchema = z.object({
|
|
21
|
-
resourcePath: z.string().describe('Absolute path to the resource containing the issue'),
|
|
22
|
-
line: z.number().int().positive().optional().describe('Line number where the issue occurs'),
|
|
23
|
-
type: z.string().describe('Issue type identifier (e.g., "broken_file", "broken_anchor", "frontmatter_schema_error", "unknown_link")'),
|
|
24
|
-
link: z.string().describe('The problematic link'),
|
|
25
|
-
message: z.string().describe('Human-readable description of the issue'),
|
|
26
|
-
suggestion: z.string().optional().describe('Optional suggestion for fixing the issue'),
|
|
27
|
-
}).describe('A single validation issue found during link validation');
|
|
28
|
-
|
|
29
|
-
export type ValidationIssue = z.infer<typeof ValidationIssueSchema>;
|
|
4
|
+
export { type ValidationIssue, ValidationIssueSchema } from '@vibe-agent-toolkit/agent-schema';
|
|
30
5
|
|
|
31
6
|
/**
|
|
32
7
|
* Complete results from validating a collection of resources.
|
|
@@ -38,9 +13,10 @@ export const ValidationResultSchema = z.object({
|
|
|
38
13
|
totalResources: z.number().int().nonnegative().describe('Total number of resources validated'),
|
|
39
14
|
totalLinks: z.number().int().nonnegative().describe('Total number of links found across all resources'),
|
|
40
15
|
linksByType: z.record(z.string(), z.number().int().nonnegative()).describe('Count of links by type (e.g., {"local_file": 10, "external": 5})'),
|
|
41
|
-
issues: z.array(ValidationIssueSchema).describe('
|
|
42
|
-
errorCount: z.number().int().nonnegative().describe('Number of issues
|
|
16
|
+
issues: z.array(ValidationIssueSchema).describe('Emitted validation issues (allow-filtered + severity-resolved; ignored issues dropped)'),
|
|
17
|
+
errorCount: z.number().int().nonnegative().describe('Number of emitted issues'),
|
|
43
18
|
passed: z.boolean().describe('True if validation succeeded (errorCount === 0)'),
|
|
19
|
+
hasErrors: z.boolean().describe('True when any emitted issue has resolved severity "error"'),
|
|
44
20
|
durationMs: z.number().nonnegative().describe('Validation duration in milliseconds'),
|
|
45
21
|
timestamp: z.date().describe('When validation was performed'),
|
|
46
22
|
}).describe('Complete results from validating a collection of resources');
|
package/src/types/resources.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* with optional absolutePath for runtime operations.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { ValidationIssue } from '@vibe-agent-toolkit/agent-schema';
|
|
10
|
+
|
|
9
11
|
import type { SHA256 } from '../schemas/checksum.js';
|
|
10
|
-
import type { ValidationIssue } from '../schemas/validation-result.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Resource type discriminator for type-safe handling
|
package/src/utils.ts
CHANGED
|
@@ -9,6 +9,21 @@ import path from 'node:path';
|
|
|
9
9
|
import { toForwardSlash, safePath } from '@vibe-agent-toolkit/utils';
|
|
10
10
|
import picomatch from 'picomatch';
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Compute a `ValidationIssue.location`: the (absolute) source file path made
|
|
14
|
+
* relative to the project root. When no project root is known, fall back to the
|
|
15
|
+
* source path forward-slashed so the location is still useful and stable.
|
|
16
|
+
*
|
|
17
|
+
* @param sourceFilePath - Absolute path to the file the issue was found in.
|
|
18
|
+
* @param projectRoot - Project root, or undefined when none could be determined.
|
|
19
|
+
* @returns Forward-slashed relative location (or the forward-slashed absolute path).
|
|
20
|
+
*/
|
|
21
|
+
export function issueLocation(sourceFilePath: string, projectRoot: string | undefined): string {
|
|
22
|
+
return projectRoot === undefined
|
|
23
|
+
? toForwardSlash(sourceFilePath)
|
|
24
|
+
: safePath.relative(projectRoot, sourceFilePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
12
27
|
/**
|
|
13
28
|
* Check if a file path matches a glob pattern.
|
|
14
29
|
*
|