@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.
Files changed (48) hide show
  1. package/README.md +33 -24
  2. package/dist/frontmatter-link-validator.d.ts +5 -5
  3. package/dist/frontmatter-link-validator.d.ts.map +1 -1
  4. package/dist/frontmatter-link-validator.js +25 -24
  5. package/dist/frontmatter-link-validator.js.map +1 -1
  6. package/dist/frontmatter-validator.d.ts +3 -2
  7. package/dist/frontmatter-validator.d.ts.map +1 -1
  8. package/dist/frontmatter-validator.js +8 -14
  9. package/dist/frontmatter-validator.js.map +1 -1
  10. package/dist/link-parser.js +12 -5
  11. package/dist/link-parser.js.map +1 -1
  12. package/dist/link-validator.d.ts +3 -3
  13. package/dist/link-validator.d.ts.map +1 -1
  14. package/dist/link-validator.js +32 -80
  15. package/dist/link-validator.js.map +1 -1
  16. package/dist/multi-schema-validator.d.ts.map +1 -1
  17. package/dist/multi-schema-validator.js +6 -8
  18. package/dist/multi-schema-validator.js.map +1 -1
  19. package/dist/resource-registry.d.ts +10 -2
  20. package/dist/resource-registry.d.ts.map +1 -1
  21. package/dist/resource-registry.js +25 -32
  22. package/dist/resource-registry.js.map +1 -1
  23. package/dist/schemas/project-config.d.ts +219 -171
  24. package/dist/schemas/project-config.d.ts.map +1 -1
  25. package/dist/schemas/project-config.js +2 -0
  26. package/dist/schemas/project-config.js.map +1 -1
  27. package/dist/schemas/validation-result.d.ts +36 -57
  28. package/dist/schemas/validation-result.d.ts.map +1 -1
  29. package/dist/schemas/validation-result.js +5 -27
  30. package/dist/schemas/validation-result.js.map +1 -1
  31. package/dist/types/resources.d.ts +1 -1
  32. package/dist/types/resources.d.ts.map +1 -1
  33. package/dist/types/resources.js.map +1 -1
  34. package/dist/utils.d.ts +10 -0
  35. package/dist/utils.d.ts.map +1 -1
  36. package/dist/utils.js +14 -0
  37. package/dist/utils.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/frontmatter-link-validator.ts +23 -25
  40. package/src/frontmatter-validator.ts +19 -16
  41. package/src/link-parser.ts +25 -9
  42. package/src/link-validator.ts +88 -78
  43. package/src/multi-schema-validator.ts +10 -8
  44. package/src/resource-registry.ts +48 -33
  45. package/src/schemas/project-config.ts +2 -0
  46. package/src/schemas/validation-result.ts +5 -29
  47. package/src/types/resources.ts +2 -1
  48. package/src/utils.ts +15 -0
@@ -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
- resourcePath: sourceFilePath,
87
- line: link.line,
88
- type: 'unknown_link',
89
- link: link.href,
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
- resourcePath: sourceFilePath,
114
- line: link.line,
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
- suggestion:
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
- resourcePath: sourceFilePath,
129
- line: link.line,
130
- type: 'broken_file',
131
- link: link.href,
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
- resourcePath: sourceFilePath,
155
- line: link.line,
156
- type: 'broken_file',
157
- link: link.href,
158
- 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.`,
159
- suggestion: `Use "${fileResult.actualName}" instead of "${expectedName}"`,
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
- resourcePath: sourceFilePath,
165
- line: link.line,
166
- type: 'broken_file',
167
- link: link.href,
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
- resourcePath: sourceFilePath,
207
- line: link.line,
208
- type: 'link_to_gitignored',
209
- link: link.href,
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
- resourcePath: sourceFilePath,
238
- line: link.line,
239
- type: 'broken_file',
240
- link: link.href,
241
- message: `Link target is a directory: ${fileResult.resolvedPath}`,
242
- suggestion:
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
- resourcePath: sourceFilePath,
255
- line: link.line,
256
- type: 'broken_anchor',
257
- link: link.href,
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
- resourcePath: sourceFilePath,
284
- line: link.line,
285
- type: 'broken_anchor',
286
- link: link.href,
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
- resourcePath,
94
- line: 1,
95
- type: 'frontmatter_schema_error',
96
- link: '',
97
- message: `Failed to load or validate schema ${schemaRef.schema}: ${message}`,
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
  }
@@ -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 { ValidationIssue, ValidationResult } from './schemas/validation-result.js';
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
- resourcePath: resource.filePath,
427
- line: 1,
428
- type: 'frontmatter_invalid_yaml',
429
- link: '',
430
- message: `Invalid YAML syntax in frontmatter: ${resource.frontmatterError}`,
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
- resourcePath: resource.filePath,
629
- line: 1,
630
- type: 'frontmatter_schema_error',
631
- link: '',
632
- message: `Failed to load or parse frontmatter schema '${validation.frontmatterSchema}': ${errorMessage}`,
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
- // Count issues (all are errors now)
710
- const errorCount = issues.length;
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 issueType = this.determineExternalUrlIssueType(result.statusCode, result.error);
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
- resourcePath: location.resourcePath,
839
- line: location.line,
840
- type: issueType,
841
- link: result.url,
842
- message: `External URL failed: ${errorMessage}`,
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 type based on validation error.
866
+ * Determine the registry issue code based on the external-URL validation error.
852
867
  * @private
853
868
  */
854
- private determineExternalUrlIssueType(statusCode: number, error?: string): string {
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 'external_url_timeout';
873
+ return 'EXTERNAL_URL_TIMEOUT';
859
874
  }
860
- return 'external_url_error';
875
+ return 'EXTERNAL_URL_ERROR';
861
876
  }
862
- return 'external_url_dead';
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('All validation issues found'),
42
- errorCount: z.number().int().nonnegative().describe('Number of issues found'),
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');
@@ -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
  *