@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/resource-registry.ts
CHANGED
|
@@ -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
|
-
/**
|
|
46
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
//
|
|
599
|
+
// Global frontmatter validation (if schema provided)
|
|
392
600
|
if (options?.frontmatterSchema) {
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
404
|
-
const errorCount = issues.
|
|
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>;
|