@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.
Files changed (87) hide show
  1. package/README.md +0 -17
  2. package/dist/collection-matcher.d.ts +63 -0
  3. package/dist/collection-matcher.d.ts.map +1 -0
  4. package/dist/collection-matcher.js +127 -0
  5. package/dist/collection-matcher.js.map +1 -0
  6. package/dist/config-parser.d.ts +63 -0
  7. package/dist/config-parser.d.ts.map +1 -0
  8. package/dist/config-parser.js +113 -0
  9. package/dist/config-parser.js.map +1 -0
  10. package/dist/frontmatter-validator.d.ts +12 -2
  11. package/dist/frontmatter-validator.d.ts.map +1 -1
  12. package/dist/frontmatter-validator.js +174 -18
  13. package/dist/frontmatter-validator.js.map +1 -1
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/link-validator.d.ts +25 -3
  19. package/dist/link-validator.d.ts.map +1 -1
  20. package/dist/link-validator.js +75 -49
  21. package/dist/link-validator.js.map +1 -1
  22. package/dist/multi-schema-validator.d.ts +42 -0
  23. package/dist/multi-schema-validator.d.ts.map +1 -0
  24. package/dist/multi-schema-validator.js +107 -0
  25. package/dist/multi-schema-validator.js.map +1 -0
  26. package/dist/pattern-expander.d.ts +63 -0
  27. package/dist/pattern-expander.d.ts.map +1 -0
  28. package/dist/pattern-expander.js +93 -0
  29. package/dist/pattern-expander.js.map +1 -0
  30. package/dist/resource-registry.d.ts +87 -6
  31. package/dist/resource-registry.d.ts.map +1 -1
  32. package/dist/resource-registry.js +215 -46
  33. package/dist/resource-registry.js.map +1 -1
  34. package/dist/schema-assignment.d.ts +49 -0
  35. package/dist/schema-assignment.d.ts.map +1 -0
  36. package/dist/schema-assignment.js +95 -0
  37. package/dist/schema-assignment.js.map +1 -0
  38. package/dist/schemas/project-config.d.ts +254 -0
  39. package/dist/schemas/project-config.d.ts.map +1 -0
  40. package/dist/schemas/project-config.js +57 -0
  41. package/dist/schemas/project-config.js.map +1 -0
  42. package/dist/schemas/resource-metadata.d.ts +3 -0
  43. package/dist/schemas/resource-metadata.d.ts.map +1 -1
  44. package/dist/schemas/resource-metadata.js +2 -0
  45. package/dist/schemas/resource-metadata.js.map +1 -1
  46. package/dist/schemas/validation-result.d.ts +2 -26
  47. package/dist/schemas/validation-result.d.ts.map +1 -1
  48. package/dist/schemas/validation-result.js +4 -20
  49. package/dist/schemas/validation-result.js.map +1 -1
  50. package/dist/types/resource-parser.d.ts +53 -0
  51. package/dist/types/resource-parser.d.ts.map +1 -0
  52. package/dist/types/resource-parser.js +233 -0
  53. package/dist/types/resource-parser.js.map +1 -0
  54. package/dist/types/resource-path-utils.d.ts +43 -0
  55. package/dist/types/resource-path-utils.d.ts.map +1 -0
  56. package/dist/types/resource-path-utils.js +89 -0
  57. package/dist/types/resource-path-utils.js.map +1 -0
  58. package/dist/types/resources.d.ts +140 -0
  59. package/dist/types/resources.d.ts.map +1 -0
  60. package/dist/types/resources.js +58 -0
  61. package/dist/types/resources.js.map +1 -0
  62. package/dist/types.d.ts +14 -2
  63. package/dist/types.d.ts.map +1 -1
  64. package/dist/types.js +17 -0
  65. package/dist/types.js.map +1 -1
  66. package/dist/utils.d.ts +18 -0
  67. package/dist/utils.d.ts.map +1 -1
  68. package/dist/utils.js +39 -0
  69. package/dist/utils.js.map +1 -1
  70. package/package.json +2 -2
  71. package/src/collection-matcher.ts +148 -0
  72. package/src/config-parser.ts +125 -0
  73. package/src/frontmatter-validator.ts +202 -18
  74. package/src/index.ts +7 -2
  75. package/src/link-validator.ts +100 -51
  76. package/src/multi-schema-validator.ts +128 -0
  77. package/src/pattern-expander.ts +100 -0
  78. package/src/resource-registry.ts +322 -54
  79. package/src/schema-assignment.ts +119 -0
  80. package/src/schemas/project-config.ts +71 -0
  81. package/src/schemas/resource-metadata.ts +2 -0
  82. package/src/schemas/validation-result.ts +4 -23
  83. package/src/types/resource-parser.ts +302 -0
  84. package/src/types/resource-path-utils.ts +102 -0
  85. package/src/types/resources.ts +211 -0
  86. package/src/types.ts +81 -1
  87. package/src/utils.ts +43 -0
@@ -8,16 +8,19 @@
8
8
  * - Query capabilities (by path, ID, or glob pattern)
9
9
  */
10
10
 
11
+ import type fs from 'node:fs/promises';
11
12
  import path from 'node:path';
12
13
 
13
- import { crawlDirectory, type CrawlOptions as UtilsCrawlOptions } from '@vibe-agent-toolkit/utils';
14
+ import { crawlDirectory, type CrawlOptions as UtilsCrawlOptions, type GitTracker } from '@vibe-agent-toolkit/utils';
14
15
 
15
16
  import { calculateChecksum } from './checksum.js';
17
+ import { getCollectionsForFile } from './collection-matcher.js';
16
18
  import { validateFrontmatter } from './frontmatter-validator.js';
17
19
  import { parseMarkdown } from './link-parser.js';
18
20
  import { validateLink } from './link-validator.js';
19
21
  import type { ResourceCollectionInterface } from './resource-collection-interface.js';
20
22
  import type { SHA256 } from './schemas/checksum.js';
23
+ import type { ProjectConfig } from './schemas/project-config.js';
21
24
  import type { HeadingNode, ResourceMetadata } from './schemas/resource-metadata.js';
22
25
  import type { ValidationIssue, ValidationResult } from './schemas/validation-result.js';
23
26
  import { matchesGlobPattern, splitHrefAnchor } from './utils.js';
@@ -42,8 +45,10 @@ export interface CrawlOptions {
42
45
  export interface ResourceRegistryOptions {
43
46
  /** Root directory for resources (optional) */
44
47
  rootDir?: string;
45
- /** Validate resources when they are added (default: false) */
46
- validateOnAdd?: boolean;
48
+ /** Project configuration (optional, enables collection support) */
49
+ config?: ProjectConfig;
50
+ /** Git tracker for efficient git-ignore checking (optional, improves performance) */
51
+ gitTracker?: GitTracker;
47
52
  }
48
53
 
49
54
  /**
@@ -52,6 +57,10 @@ export interface ResourceRegistryOptions {
52
57
  export interface ValidateOptions {
53
58
  /** Optional JSON Schema to validate frontmatter against */
54
59
  frontmatterSchema?: object;
60
+ /** Skip git-ignore checks (default: false) */
61
+ skipGitIgnoreCheck?: boolean;
62
+ /** Validation mode for schemas: strict (default) or permissive */
63
+ validationMode?: 'strict' | 'permissive';
55
64
  }
56
65
 
57
66
  /**
@@ -63,6 +72,30 @@ export interface RegistryStats {
63
72
  linksByType: Record<string, number>;
64
73
  }
65
74
 
75
+ /**
76
+ * Statistics for a single collection.
77
+ */
78
+ export interface CollectionStat {
79
+ /** Number of resources in this collection */
80
+ resourceCount: number;
81
+ /** Whether this collection has a frontmatter schema configured */
82
+ hasSchema: boolean;
83
+ /** Validation mode for this collection's schema */
84
+ validationMode?: 'strict' | 'permissive';
85
+ }
86
+
87
+ /**
88
+ * Statistics about all collections in the registry.
89
+ */
90
+ export interface CollectionStats {
91
+ /** Total number of configured collections */
92
+ totalCollections: number;
93
+ /** Total number of resources that belong to at least one collection */
94
+ resourcesInCollections: number;
95
+ /** Statistics per collection ID */
96
+ collections: Record<string, CollectionStat>;
97
+ }
98
+
66
99
  /**
67
100
  * Resource registry for managing collections of markdown resources.
68
101
  *
@@ -96,17 +129,27 @@ export class ResourceRegistry implements ResourceCollectionInterface {
96
129
  /** Optional root directory for resources */
97
130
  readonly rootDir?: string;
98
131
 
132
+ /** Optional project configuration (enables collection support) */
133
+ readonly config?: ProjectConfig;
134
+
135
+ /** Optional git tracker for efficient git-ignore checking */
136
+ readonly gitTracker?: GitTracker;
137
+
99
138
  private readonly resourcesByPath: Map<string, ResourceMetadata> = new Map();
100
139
  private readonly resourcesById: Map<string, ResourceMetadata> = new Map();
101
140
  private readonly resourcesByName: Map<string, ResourceMetadata[]> = new Map();
102
141
  private readonly resourcesByChecksum: Map<SHA256, ResourceMetadata[]> = new Map();
103
- private readonly validateOnAdd: boolean;
104
142
 
105
143
  constructor(options?: ResourceRegistryOptions) {
106
144
  if (options?.rootDir !== undefined) {
107
145
  this.rootDir = options.rootDir;
108
146
  }
109
- this.validateOnAdd = options?.validateOnAdd ?? false;
147
+ if (options?.config !== undefined) {
148
+ this.config = options.config;
149
+ }
150
+ if (options?.gitTracker !== undefined) {
151
+ this.gitTracker = options.gitTracker;
152
+ }
110
153
  }
111
154
 
112
155
  /**
@@ -204,7 +247,6 @@ export class ResourceRegistry implements ResourceCollectionInterface {
204
247
  * Add a single resource to the registry.
205
248
  *
206
249
  * Parses the markdown file, generates a unique ID, and stores the resource.
207
- * If validateOnAdd is true, validates the resource immediately.
208
250
  *
209
251
  * @param filePath - Path to the markdown file (will be normalized to absolute)
210
252
  * @returns The parsed resource metadata
@@ -233,6 +275,11 @@ export class ResourceRegistry implements ResourceCollectionInterface {
233
275
  // Calculate checksum eagerly
234
276
  const checksum = await calculateChecksum(absolutePath);
235
277
 
278
+ // Determine collections if config is present
279
+ const collections = this.config?.resources?.collections
280
+ ? getCollectionsForFile(absolutePath, this.config.resources.collections)
281
+ : undefined;
282
+
236
283
  // Create resource metadata
237
284
  const resource: ResourceMetadata = {
238
285
  id,
@@ -245,22 +292,12 @@ export class ResourceRegistry implements ResourceCollectionInterface {
245
292
  estimatedTokenCount: parseResult.estimatedTokenCount,
246
293
  modifiedAt: stats.mtime,
247
294
  checksum,
295
+ ...(collections !== undefined && collections.length > 0 && { collections }),
248
296
  };
249
297
 
250
298
  // Index the resource
251
299
  this.indexResource(resource);
252
300
 
253
- // Validate if requested
254
- if (this.validateOnAdd) {
255
- const headingsByFile = this.buildHeadingsByFileMap();
256
- for (const link of resource.links) {
257
- const issue = await validateLink(link, absolutePath, headingsByFile);
258
- if (issue) {
259
- throw new Error(`Validation failed: ${issue.message}`);
260
- }
261
- }
262
- }
263
-
264
301
  return resource;
265
302
  }
266
303
 
@@ -323,6 +360,188 @@ export class ResourceRegistry implements ResourceCollectionInterface {
323
360
  return await this.addResources(files);
324
361
  }
325
362
 
363
+ /**
364
+ * Check for YAML parsing errors in all resources.
365
+ * @private
366
+ */
367
+ private collectYamlErrors(): ValidationIssue[] {
368
+ const issues: ValidationIssue[] = [];
369
+ for (const resource of this.resourcesByPath.values()) {
370
+ if (resource.frontmatterError) {
371
+ issues.push({
372
+ resourcePath: resource.filePath,
373
+ line: 1,
374
+ type: 'frontmatter_invalid_yaml',
375
+ link: '',
376
+ message: `Invalid YAML syntax in frontmatter: ${resource.frontmatterError}`,
377
+ });
378
+ }
379
+ }
380
+ return issues;
381
+ }
382
+
383
+ /**
384
+ * Validate all links in all resources.
385
+ * @private
386
+ */
387
+ private async validateAllLinks(
388
+ headingsByFile: Map<string, HeadingNode[]>,
389
+ skipGitIgnoreCheck: boolean
390
+ ): Promise<ValidationIssue[]> {
391
+ const issues: ValidationIssue[] = [];
392
+
393
+ for (const resource of this.resourcesByPath.values()) {
394
+ for (const link of resource.links) {
395
+ // Only pass options if projectRoot is defined (exactOptionalPropertyTypes requirement)
396
+ const validateOptions = this.rootDir === undefined
397
+ ? { skipGitIgnoreCheck }
398
+ : {
399
+ projectRoot: this.rootDir,
400
+ skipGitIgnoreCheck,
401
+ ...(this.gitTracker !== undefined && { gitTracker: this.gitTracker })
402
+ };
403
+
404
+ const issue = await validateLink(link, resource.filePath, headingsByFile, validateOptions);
405
+ if (issue) {
406
+ issues.push(issue);
407
+ }
408
+ }
409
+ }
410
+
411
+ return issues;
412
+ }
413
+
414
+ /**
415
+ * Validate frontmatter against a JSON Schema.
416
+ * @private
417
+ */
418
+ private validateAllFrontmatter(
419
+ schema: object,
420
+ mode: 'strict' | 'permissive' = 'strict'
421
+ ): ValidationIssue[] {
422
+ const issues: ValidationIssue[] = [];
423
+ for (const resource of this.resourcesByPath.values()) {
424
+ const frontmatterIssues = validateFrontmatter(
425
+ resource.frontmatter,
426
+ schema,
427
+ resource.filePath,
428
+ mode
429
+ );
430
+ issues.push(...frontmatterIssues);
431
+ }
432
+ return issues;
433
+ }
434
+
435
+ /**
436
+ * Validate frontmatter against per-collection schemas.
437
+ * @private
438
+ */
439
+ private async validateCollectionFrontmatter(): Promise<ValidationIssue[]> {
440
+ const issues: ValidationIssue[] = [];
441
+
442
+ // Skip if no config
443
+ if (!this.config?.resources?.collections) {
444
+ return issues;
445
+ }
446
+
447
+ const fsPromises = await import('node:fs/promises');
448
+
449
+ for (const resource of this.resourcesByPath.values()) {
450
+ // Skip if resource has no collections
451
+ if (!resource.collections || resource.collections.length === 0) {
452
+ continue;
453
+ }
454
+
455
+ // Validate against each collection's schema
456
+ const collectionIssues = await this.validateResourceCollectionSchemas(
457
+ resource,
458
+ fsPromises
459
+ );
460
+ issues.push(...collectionIssues);
461
+ }
462
+
463
+ return issues;
464
+ }
465
+
466
+ /**
467
+ * Validate a single resource against its collection schemas.
468
+ * @private
469
+ */
470
+ private async validateResourceCollectionSchemas(
471
+ resource: ResourceMetadata,
472
+ fsModule: typeof fs
473
+ ): Promise<ValidationIssue[]> {
474
+ const issues: ValidationIssue[] = [];
475
+
476
+ if (!resource.collections || !this.config?.resources?.collections) {
477
+ return issues;
478
+ }
479
+
480
+ for (const collectionId of resource.collections) {
481
+ const collection = this.config.resources.collections[collectionId];
482
+
483
+ // Skip if collection has no validation or no schema
484
+ if (!collection?.validation?.frontmatterSchema) {
485
+ continue;
486
+ }
487
+
488
+ const collectionIssues = await this.validateAgainstCollectionSchema(
489
+ resource,
490
+ collection.validation,
491
+ fsModule
492
+ );
493
+ issues.push(...collectionIssues);
494
+ }
495
+
496
+ return issues;
497
+ }
498
+
499
+ /**
500
+ * Validate resource frontmatter against a specific collection schema.
501
+ * @private
502
+ */
503
+ private async validateAgainstCollectionSchema(
504
+ resource: ResourceMetadata,
505
+ validation: NonNullable<ProjectConfig['resources']>['collections'][string]['validation'],
506
+ fsModule: typeof fs
507
+ ): Promise<ValidationIssue[]> {
508
+ if (!validation?.frontmatterSchema) {
509
+ return [];
510
+ }
511
+
512
+ const schemaPath = path.resolve(
513
+ this.rootDir ?? process.cwd(),
514
+ validation.frontmatterSchema
515
+ );
516
+
517
+ try {
518
+ const schemaContent = await fsModule.readFile(schemaPath, 'utf-8');
519
+ const schema = JSON.parse(schemaContent) as object;
520
+
521
+ // Determine validation mode (default to permissive)
522
+ const mode = validation.mode ?? 'permissive';
523
+
524
+ // Validate frontmatter
525
+ return validateFrontmatter(
526
+ resource.frontmatter,
527
+ schema,
528
+ resource.filePath,
529
+ mode,
530
+ schemaPath
531
+ );
532
+ } catch (error) {
533
+ // Handle missing or invalid schema files gracefully
534
+ const errorMessage = error instanceof Error ? error.message : String(error);
535
+ return [{
536
+ resourcePath: resource.filePath,
537
+ line: 1,
538
+ type: 'frontmatter_schema_error',
539
+ link: '',
540
+ message: `Failed to load or parse frontmatter schema '${validation.frontmatterSchema}': ${errorMessage}`,
541
+ }];
542
+ }
543
+ }
544
+
326
545
  /**
327
546
  * Validate all links and optionally frontmatter in all resources in the registry.
328
547
  *
@@ -348,10 +567,9 @@ export class ResourceRegistry implements ResourceCollectionInterface {
348
567
  *
349
568
  * console.log(`Passed: ${result.passed}`);
350
569
  * console.log(`Errors: ${result.errorCount}`);
351
- * console.log(`Warnings: ${result.warningCount}`);
352
570
  * console.log(`Total resources: ${result.totalResources}`);
353
571
  * for (const issue of result.issues) {
354
- * console.log(`${issue.severity}: ${issue.message}`);
572
+ * console.log(`${issue.message}`);
355
573
  * }
356
574
  * ```
357
575
  */
@@ -365,45 +583,27 @@ export class ResourceRegistry implements ResourceCollectionInterface {
365
583
  const issues: ValidationIssue[] = [];
366
584
 
367
585
  // Check for YAML parsing errors first
368
- for (const resource of this.resourcesByPath.values()) {
369
- if (resource.frontmatterError) {
370
- issues.push({
371
- severity: 'error',
372
- resourcePath: resource.filePath,
373
- line: 1,
374
- type: 'frontmatter_invalid_yaml',
375
- link: '',
376
- message: `Invalid YAML syntax in frontmatter: ${resource.frontmatterError}`,
377
- });
378
- }
379
- }
586
+ issues.push(...this.collectYamlErrors());
380
587
 
381
588
  // Validate each link in each resource
382
- for (const resource of this.resourcesByPath.values()) {
383
- for (const link of resource.links) {
384
- const issue = await validateLink(link, resource.filePath, headingsByFile);
385
- if (issue) {
386
- issues.push(issue);
387
- }
388
- }
389
- }
589
+ const linkIssues = await this.validateAllLinks(
590
+ headingsByFile,
591
+ options?.skipGitIgnoreCheck ?? false
592
+ );
593
+ issues.push(...linkIssues);
594
+
595
+ // Per-collection frontmatter validation
596
+ const collectionFrontmatterIssues = await this.validateCollectionFrontmatter();
597
+ issues.push(...collectionFrontmatterIssues);
390
598
 
391
- // Frontmatter validation (if schema provided)
599
+ // Global frontmatter validation (if schema provided)
392
600
  if (options?.frontmatterSchema) {
393
- for (const resource of this.resourcesByPath.values()) {
394
- const frontmatterIssues = validateFrontmatter(
395
- resource.frontmatter,
396
- options.frontmatterSchema,
397
- resource.filePath
398
- );
399
- issues.push(...frontmatterIssues);
400
- }
601
+ const mode = options.validationMode ?? 'strict';
602
+ issues.push(...this.validateAllFrontmatter(options.frontmatterSchema, mode));
401
603
  }
402
604
 
403
- // Count issues by severity
404
- const errorCount = issues.filter((i) => i.severity === 'error').length;
405
- const warningCount = issues.filter((i) => i.severity === 'warning').length;
406
- const infoCount = issues.filter((i) => i.severity === 'info').length;
605
+ // Count issues (all are errors now)
606
+ const errorCount = issues.length;
407
607
 
408
608
  // Count links by type
409
609
  const linksByType: Record<string, number> = {};
@@ -424,8 +624,6 @@ export class ResourceRegistry implements ResourceCollectionInterface {
424
624
  linksByType,
425
625
  issues,
426
626
  errorCount,
427
- warningCount,
428
- infoCount,
429
627
  passed: errorCount === 0,
430
628
  durationMs,
431
629
  timestamp: new Date(),
@@ -717,6 +915,76 @@ export class ResourceRegistry implements ResourceCollectionInterface {
717
915
  };
718
916
  }
719
917
 
918
+ /**
919
+ * Get collection-level statistics.
920
+ *
921
+ * Returns undefined if collections are not configured in the project config.
922
+ *
923
+ * @returns Collection statistics or undefined if no collections configured
924
+ *
925
+ * @example
926
+ * ```typescript
927
+ * const collectionStats = registry.getCollectionStats();
928
+ * if (collectionStats) {
929
+ * console.log(`Total collections: ${collectionStats.totalCollections}`);
930
+ * console.log(`Resources in collections: ${collectionStats.resourcesInCollections}`);
931
+ * for (const [id, stat] of Object.entries(collectionStats.collections)) {
932
+ * console.log(`${id}: ${stat.resourceCount} resources`);
933
+ * }
934
+ * }
935
+ * ```
936
+ */
937
+ getCollectionStats(): CollectionStats | undefined {
938
+ if (!this.config?.resources?.collections) {
939
+ return undefined;
940
+ }
941
+
942
+ // Group resources by collection
943
+ const collectionMap = new Map<string, ResourceMetadata[]>();
944
+
945
+ for (const resource of this.resourcesByPath.values()) {
946
+ if (resource.collections) {
947
+ for (const collectionId of resource.collections) {
948
+ const resources = collectionMap.get(collectionId) ?? [];
949
+ resources.push(resource);
950
+ collectionMap.set(collectionId, resources);
951
+ }
952
+ }
953
+ }
954
+
955
+ // Build stats per collection
956
+ const collections: Record<string, CollectionStat> = {};
957
+
958
+ for (const [id, resources] of collectionMap.entries()) {
959
+ const collection = this.config.resources.collections[id];
960
+ const stat: CollectionStat = {
961
+ resourceCount: resources.length,
962
+ hasSchema: !!collection?.validation?.frontmatterSchema,
963
+ };
964
+
965
+ // Only add validationMode if it's defined (exactOptionalPropertyTypes requirement)
966
+ if (collection?.validation?.mode !== undefined) {
967
+ stat.validationMode = collection.validation.mode;
968
+ }
969
+
970
+ collections[id] = stat;
971
+ }
972
+
973
+ // Calculate total unique resources in collections (a resource may be in multiple collections)
974
+ const uniqueResourcesInCollections = new Set<string>();
975
+ for (const resource of this.resourcesByPath.values()) {
976
+ if (resource.collections && resource.collections.length > 0) {
977
+ uniqueResourcesInCollections.add(resource.filePath);
978
+ }
979
+ }
980
+
981
+ return {
982
+ totalCollections: Object.keys(this.config.resources.collections).length,
983
+ resourcesInCollections: uniqueResourcesInCollections.size,
984
+ collections,
985
+ };
986
+ }
987
+
720
988
  /**
721
989
  * Generate a unique ID from a file path.
722
990
  *
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Schema assignment pipeline for resources
3
+ *
4
+ * Handles the multi-source schema assignment:
5
+ * 1. Self-asserted schemas (from $schema field in frontmatter)
6
+ * 2. Collection-imposed schemas (from collection config)
7
+ * 3. CLI-imposed schemas (from --frontmatter-schema flag)
8
+ *
9
+ * Each resource can have multiple schemas from different sources,
10
+ * and each is validated independently.
11
+ */
12
+
13
+ import type { CollectionConfig } from './schemas/project-config.js';
14
+ import type { SchemaReference } from './types/resources.js';
15
+
16
+ /**
17
+ * Add collection-imposed schema to a resource's schema list
18
+ *
19
+ * @param existingSchemas - Current schemas (including self-asserted)
20
+ * @param collectionName - Name of the collection
21
+ * @param collectionConfig - Collection configuration
22
+ * @returns Updated schema list with collection schema added (if defined)
23
+ */
24
+ export function addCollectionSchema(
25
+ existingSchemas: SchemaReference[],
26
+ collectionName: string,
27
+ collectionConfig: CollectionConfig,
28
+ ): SchemaReference[] {
29
+ const schemaPath = collectionConfig.validation?.frontmatterSchema;
30
+
31
+ if (!schemaPath) {
32
+ return existingSchemas;
33
+ }
34
+
35
+ // Check if this schema is already present from any source
36
+ const alreadyExists = existingSchemas.some((ref) => ref.schema === schemaPath);
37
+ if (alreadyExists) {
38
+ return existingSchemas;
39
+ }
40
+
41
+ // Add collection-imposed schema
42
+ return [
43
+ ...existingSchemas,
44
+ {
45
+ schema: schemaPath,
46
+ source: collectionName,
47
+ applied: false,
48
+ },
49
+ ];
50
+ }
51
+
52
+ /**
53
+ * Add CLI-imposed schema to a resource's schema list
54
+ *
55
+ * @param existingSchemas - Current schemas (including self-asserted and collection)
56
+ * @param schemaPath - Path to schema from CLI flag
57
+ * @returns Updated schema list with CLI schema added
58
+ */
59
+ export function addCLISchema(
60
+ existingSchemas: SchemaReference[],
61
+ schemaPath: string,
62
+ ): SchemaReference[] {
63
+ // Check if this schema is already present from any source
64
+ const alreadyExists = existingSchemas.some((ref) => ref.schema === schemaPath);
65
+ if (alreadyExists) {
66
+ return existingSchemas;
67
+ }
68
+
69
+ // Add CLI-imposed schema
70
+ return [
71
+ ...existingSchemas,
72
+ {
73
+ schema: schemaPath,
74
+ source: 'cli',
75
+ applied: false,
76
+ },
77
+ ];
78
+ }
79
+
80
+ /**
81
+ * Apply schema assignments to a resource
82
+ *
83
+ * Pipeline order:
84
+ * 1. Self-asserted (already in resource.schemas from parser)
85
+ * 2. Collection-imposed (from each collection the resource belongs to)
86
+ * 3. CLI-imposed (from --frontmatter-schema flag)
87
+ *
88
+ * Deduplicates schemas by path - same schema from multiple sources
89
+ * only appears once (first source wins).
90
+ *
91
+ * @param resourceSchemas - Current schemas (from parser)
92
+ * @param collections - Collections this resource belongs to
93
+ * @param collectionsConfig - Collection configurations
94
+ * @param cliSchema - Optional CLI-imposed schema
95
+ * @returns Complete list of schemas to validate
96
+ */
97
+ export function assignSchemas(
98
+ resourceSchemas: SchemaReference[],
99
+ collections: string[],
100
+ collectionsConfig: Record<string, CollectionConfig>,
101
+ cliSchema?: string,
102
+ ): SchemaReference[] {
103
+ let schemas = resourceSchemas;
104
+
105
+ // Add collection-imposed schemas
106
+ for (const collectionName of collections) {
107
+ const collectionConfig = collectionsConfig[collectionName];
108
+ if (collectionConfig) {
109
+ schemas = addCollectionSchema(schemas, collectionName, collectionConfig);
110
+ }
111
+ }
112
+
113
+ // Add CLI-imposed schema
114
+ if (cliSchema) {
115
+ schemas = addCLISchema(schemas, cliSchema);
116
+ }
117
+
118
+ return schemas;
119
+ }
@@ -0,0 +1,71 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * Validation mode for frontmatter schema validation.
5
+ *
6
+ * - `strict`: Enforce schema exactly (respect additionalProperties: false)
7
+ * - `permissive`: Allow extra fields (ignore additionalProperties: false)
8
+ */
9
+ export const ValidationModeSchema = z.enum(['strict', 'permissive'])
10
+ .describe('Validation mode for frontmatter schema validation');
11
+
12
+ export type ValidationMode = z.infer<typeof ValidationModeSchema>;
13
+
14
+ /**
15
+ * Validation configuration for a collection.
16
+ */
17
+ export const CollectionValidationSchema = z.object({
18
+ frontmatterSchema: z.string().optional()
19
+ .describe('Path to JSON Schema file for frontmatter validation (relative to config file or package reference like @vibe-agent-toolkit/schemas/skill.v1.json)'),
20
+ mode: ValidationModeSchema.optional()
21
+ .describe('Validation mode (default: strict)'),
22
+ checkUrlLinks: z.boolean().optional()
23
+ .describe('Whether to validate external URL links (default: false)'),
24
+ checkGitIgnored: z.boolean().optional()
25
+ .describe('Whether to check if non-ignored files link to git-ignored files (default: true)'),
26
+ }).describe('Validation configuration for a collection');
27
+
28
+ export type CollectionValidation = z.infer<typeof CollectionValidationSchema>;
29
+
30
+ /**
31
+ * Configuration for a named collection of resources.
32
+ *
33
+ * Collections define include/exclude patterns and validation rules.
34
+ * A file can belong to multiple collections.
35
+ */
36
+ export const CollectionConfigSchema = z.object({
37
+ include: z.array(z.string()).min(1)
38
+ .describe('Include patterns (paths or globs like docs/**/*.md)'),
39
+ exclude: z.array(z.string()).optional()
40
+ .describe('Exclude patterns (globs)'),
41
+ validation: CollectionValidationSchema.optional()
42
+ .describe('Validation configuration for this collection'),
43
+ }).describe('Configuration for a named collection of resources');
44
+
45
+ export type CollectionConfig = z.infer<typeof CollectionConfigSchema>;
46
+
47
+ /**
48
+ * Resources section of project configuration.
49
+ */
50
+ export const ResourcesConfigSchema = z.object({
51
+ include: z.array(z.string()).optional()
52
+ .describe('Global default include patterns (not used by collections in Phase 2)'),
53
+ exclude: z.array(z.string()).optional()
54
+ .describe('Global default exclude patterns (not used by collections in Phase 2)'),
55
+ collections: z.record(z.string(), CollectionConfigSchema)
56
+ .describe('Named collections of resources'),
57
+ }).describe('Resources section of project configuration');
58
+
59
+ export type ResourcesConfig = z.infer<typeof ResourcesConfigSchema>;
60
+
61
+ /**
62
+ * Complete project configuration schema.
63
+ */
64
+ export const ProjectConfigSchema = z.object({
65
+ version: z.literal(1)
66
+ .describe('Config file version (must be 1)'),
67
+ resources: ResourcesConfigSchema.optional()
68
+ .describe('Resources configuration'),
69
+ }).describe('vibe-agent-toolkit project configuration');
70
+
71
+ export type ProjectConfig = z.infer<typeof ProjectConfigSchema>;
@@ -88,6 +88,8 @@ export const ResourceMetadataSchema = z.object({
88
88
  estimatedTokenCount: z.number().int().nonnegative().describe('Estimated token count for LLM context (roughly 1 token per 4 chars)'),
89
89
  modifiedAt: z.date().describe('Last modified timestamp'),
90
90
  checksum: SHA256Schema.describe('SHA-256 checksum of file content'),
91
+ collections: z.array(z.string()).optional()
92
+ .describe('Collection names this resource belongs to (populated when using config-based discovery)'),
91
93
  }).describe('Complete metadata for a markdown resource');
92
94
 
93
95
  export type ResourceMetadata = z.infer<typeof ResourceMetadataSchema>;