@vibe-agent-toolkit/resources 0.1.3 → 0.1.4

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 +52 -40
  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 +70 -43
  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
@@ -1,40 +1,23 @@
1
1
  import { z } from 'zod';
2
2
 
3
- /**
4
- * Severity level for validation issues.
5
- *
6
- * - `error`: Critical issue that should block usage (e.g., broken file link)
7
- * - `warning`: Non-critical issue that should be addressed (e.g., questionable link format)
8
- * - `info`: Informational message (e.g., external URL not validated)
9
- */
10
- export const ValidationSeveritySchema = z.enum([
11
- 'error',
12
- 'warning',
13
- 'info',
14
- ]).describe('Severity level for validation issues');
15
-
16
- export type ValidationSeverity = z.infer<typeof ValidationSeveritySchema>;
17
-
18
3
  /**
19
4
  * A single validation issue found during link validation.
20
5
  *
21
6
  * Issue types:
22
7
  * - broken_file: Local file link points to non-existent file
23
8
  * - broken_anchor: Anchor link points to non-existent heading
24
- * - external_url: External URL (informational, not validated)
25
- * - unknown_link: Unknown link type
26
9
  * - frontmatter_missing: Schema requires frontmatter, file has none
27
10
  * - frontmatter_invalid_yaml: YAML syntax error in frontmatter
28
11
  * - frontmatter_schema_error: Frontmatter fails JSON Schema validation
12
+ * - unknown_link: Unknown link type
29
13
  *
30
14
  * Includes details about what went wrong, where it occurred, and optionally
31
15
  * how to fix it.
32
16
  */
33
17
  export const ValidationIssueSchema = z.object({
34
- severity: ValidationSeveritySchema.describe('Issue severity level'),
35
18
  resourcePath: z.string().describe('Absolute path to the resource containing the issue'),
36
19
  line: z.number().int().positive().optional().describe('Line number where the issue occurs'),
37
- type: z.string().describe('Issue type identifier (e.g., "broken_file", "broken_anchor", "external_url")'),
20
+ type: z.string().describe('Issue type identifier (e.g., "broken_file", "broken_anchor", "frontmatter_schema_error", "unknown_link")'),
38
21
  link: z.string().describe('The problematic link'),
39
22
  message: z.string().describe('Human-readable description of the issue'),
40
23
  suggestion: z.string().optional().describe('Optional suggestion for fixing the issue'),
@@ -46,16 +29,14 @@ export type ValidationIssue = z.infer<typeof ValidationIssueSchema>;
46
29
  * Complete results from validating a collection of resources.
47
30
  *
48
31
  * Provides summary statistics, detailed issues, and validation metadata.
49
- * The `passed` field indicates whether validation succeeded (no errors).
32
+ * The `passed` field indicates whether validation succeeded (no issues found).
50
33
  */
51
34
  export const ValidationResultSchema = z.object({
52
35
  totalResources: z.number().int().nonnegative().describe('Total number of resources validated'),
53
36
  totalLinks: z.number().int().nonnegative().describe('Total number of links found across all resources'),
54
37
  linksByType: z.record(z.string(), z.number().int().nonnegative()).describe('Count of links by type (e.g., {"local_file": 10, "external": 5})'),
55
38
  issues: z.array(ValidationIssueSchema).describe('All validation issues found'),
56
- errorCount: z.number().int().nonnegative().describe('Number of error-level issues'),
57
- warningCount: z.number().int().nonnegative().describe('Number of warning-level issues'),
58
- infoCount: z.number().int().nonnegative().describe('Number of info-level issues'),
39
+ errorCount: z.number().int().nonnegative().describe('Number of issues found'),
59
40
  passed: z.boolean().describe('True if validation succeeded (errorCount === 0)'),
60
41
  durationMs: z.number().nonnegative().describe('Validation duration in milliseconds'),
61
42
  timestamp: z.date().describe('When validation was performed'),
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Resource parsing functions
3
+ *
4
+ * Parses files into typed Resource objects with proper metadata extraction.
5
+ * Handles markdown, JSON, JSON Schema, and YAML formats.
6
+ */
7
+
8
+ import { promises as fs } from 'node:fs';
9
+
10
+ import yaml from 'js-yaml';
11
+
12
+ import { calculateChecksum } from '../checksum.js';
13
+ import { parseMarkdown } from '../link-parser.js';
14
+ import type { HeadingNode, ResourceLink } from '../schemas/resource-metadata.js';
15
+
16
+ import type {
17
+ Heading,
18
+ JsonResource,
19
+ JsonSchemaResource,
20
+ MarkdownResource,
21
+ SchemaReference,
22
+ YamlResource,
23
+ } from './resources.js';
24
+ import { isJsonSchema, ResourceType } from './resources.js';
25
+
26
+ /**
27
+ * Parse markdown file into MarkdownResource
28
+ *
29
+ * @param absolutePath - Absolute path to markdown file
30
+ * @param projectPath - Relative path from project root
31
+ * @param collectionName - Collection this resource belongs to
32
+ * @returns Parsed MarkdownResource
33
+ */
34
+ export async function parseMarkdownResource(
35
+ absolutePath: string,
36
+ projectPath: string,
37
+ collectionName: string,
38
+ ): Promise<MarkdownResource> {
39
+ // Get file stats
40
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
41
+ const stats = await fs.stat(absolutePath);
42
+
43
+ // Parse markdown content
44
+ const parsed = await parseMarkdown(absolutePath);
45
+
46
+ // Calculate checksum
47
+ const checksum = await calculateChecksum(absolutePath);
48
+
49
+ // Extract link hrefs
50
+ const links = parsed.links.map((link: ResourceLink) => link.href);
51
+
52
+ // Convert HeadingNode[] to Heading[]
53
+ const headings: Heading[] = convertHeadingsToSimple(parsed.headings);
54
+
55
+ // Estimate token count (chars / 4)
56
+ const estimatedTokenCount = Math.floor(parsed.content.length / 4);
57
+
58
+ // Extract self-asserted schema from frontmatter $schema field
59
+ const schemas: SchemaReference[] = [];
60
+ if (parsed.frontmatter?.['$schema'] !== undefined) {
61
+ if (typeof parsed.frontmatter['$schema'] === 'string') {
62
+ schemas.push({
63
+ schema: parsed.frontmatter['$schema'],
64
+ source: 'self',
65
+ applied: false,
66
+ });
67
+ }
68
+ }
69
+
70
+ const resource: MarkdownResource = {
71
+ id: projectPath,
72
+ projectPath,
73
+ absolutePath,
74
+ type: ResourceType.MARKDOWN,
75
+ mimeType: 'text/markdown',
76
+ sizeBytes: stats.size,
77
+ modifiedAt: stats.mtime,
78
+ checksum,
79
+ collections: [collectionName],
80
+ schemas,
81
+ content: parsed.content,
82
+ links,
83
+ headings,
84
+ estimatedTokenCount,
85
+ };
86
+
87
+ // Only set frontmatter if it exists (exactOptionalPropertyTypes)
88
+ if (parsed.frontmatter !== undefined) {
89
+ resource.frontmatter = parsed.frontmatter;
90
+ }
91
+
92
+ return resource;
93
+ }
94
+
95
+ /**
96
+ * Parse JSON Schema file into JsonSchemaResource
97
+ *
98
+ * @param absolutePath - Absolute path to JSON Schema file
99
+ * @param projectPath - Relative path from project root
100
+ * @param collectionName - Collection this resource belongs to
101
+ * @returns Parsed JsonSchemaResource
102
+ */
103
+ export async function parseJsonSchemaResource(
104
+ absolutePath: string,
105
+ projectPath: string,
106
+ collectionName: string,
107
+ ): Promise<JsonSchemaResource> {
108
+ // Get file stats
109
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
110
+ const stats = await fs.stat(absolutePath);
111
+
112
+ // Read and parse JSON
113
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
114
+ const content = await fs.readFile(absolutePath, 'utf-8');
115
+ const schema = JSON.parse(content) as object & {
116
+ $id?: string;
117
+ $schema?: string;
118
+ title?: string;
119
+ description?: string;
120
+ };
121
+
122
+ // Calculate checksum
123
+ const checksum = await calculateChecksum(absolutePath);
124
+
125
+ // Build resource with required fields
126
+ const resource: JsonSchemaResource = {
127
+ id: projectPath,
128
+ projectPath,
129
+ absolutePath,
130
+ type: ResourceType.JSON_SCHEMA,
131
+ mimeType: 'application/schema+json',
132
+ sizeBytes: stats.size,
133
+ modifiedAt: stats.mtime,
134
+ checksum,
135
+ collections: [collectionName],
136
+ schema,
137
+ referencedBy: [],
138
+ };
139
+
140
+ // Only set optional fields if they exist (exactOptionalPropertyTypes)
141
+ if (schema.$id !== undefined) {
142
+ resource.schemaId = schema.$id;
143
+ }
144
+ if (schema.$schema !== undefined) {
145
+ resource.schemaVersion = schema.$schema;
146
+ }
147
+ if (schema.title !== undefined) {
148
+ resource.title = schema.title;
149
+ }
150
+ if (schema.description !== undefined) {
151
+ resource.description = schema.description;
152
+ }
153
+
154
+ return resource;
155
+ }
156
+
157
+ /**
158
+ * Parse JSON file into JsonResource
159
+ *
160
+ * @param absolutePath - Absolute path to JSON file
161
+ * @param projectPath - Relative path from project root
162
+ * @param collectionName - Collection this resource belongs to
163
+ * @returns Parsed JsonResource
164
+ */
165
+ export async function parseJsonResource(
166
+ absolutePath: string,
167
+ projectPath: string,
168
+ collectionName: string,
169
+ ): Promise<JsonResource> {
170
+ // Get file stats
171
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
172
+ const stats = await fs.stat(absolutePath);
173
+
174
+ // Read and parse JSON
175
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
176
+ const content = await fs.readFile(absolutePath, 'utf-8');
177
+ const data: unknown = JSON.parse(content);
178
+
179
+ // Calculate checksum
180
+ const checksum = await calculateChecksum(absolutePath);
181
+
182
+ return {
183
+ id: projectPath,
184
+ projectPath,
185
+ absolutePath,
186
+ type: ResourceType.JSON,
187
+ mimeType: 'application/json',
188
+ sizeBytes: stats.size,
189
+ modifiedAt: stats.mtime,
190
+ checksum,
191
+ collections: [collectionName],
192
+ data,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Parse YAML file into YamlResource
198
+ *
199
+ * @param absolutePath - Absolute path to YAML file
200
+ * @param projectPath - Relative path from project root
201
+ * @param collectionName - Collection this resource belongs to
202
+ * @returns Parsed YamlResource
203
+ */
204
+ export async function parseYamlResource(
205
+ absolutePath: string,
206
+ projectPath: string,
207
+ collectionName: string,
208
+ ): Promise<YamlResource> {
209
+ // Get file stats
210
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
211
+ const stats = await fs.stat(absolutePath);
212
+
213
+ // Read and parse YAML
214
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
215
+ const content = await fs.readFile(absolutePath, 'utf-8');
216
+ const data = yaml.load(content);
217
+
218
+ // Calculate checksum
219
+ const checksum = await calculateChecksum(absolutePath);
220
+
221
+ return {
222
+ id: projectPath,
223
+ projectPath,
224
+ absolutePath,
225
+ type: ResourceType.YAML,
226
+ mimeType: 'application/yaml',
227
+ sizeBytes: stats.size,
228
+ modifiedAt: stats.mtime,
229
+ checksum,
230
+ collections: [collectionName],
231
+ data,
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Detect resource type from file path and content
237
+ *
238
+ * @param filePath - Path to file
239
+ * @param data - Parsed data (for JSON/YAML detection)
240
+ * @returns Detected ResourceType
241
+ */
242
+ export function detectResourceType(filePath: string, data?: unknown): ResourceType {
243
+ const ext = filePath.toLowerCase().split('.').pop();
244
+
245
+ // Check extension first
246
+ if (ext === 'md' || ext === 'markdown') {
247
+ return ResourceType.MARKDOWN;
248
+ }
249
+
250
+ if (ext === 'yaml' || ext === 'yml') {
251
+ return ResourceType.YAML;
252
+ }
253
+
254
+ if (ext === 'json') {
255
+ // Use heuristics to detect JSON Schema
256
+ if (data && isJsonSchema(data)) {
257
+ return ResourceType.JSON_SCHEMA;
258
+ }
259
+ return ResourceType.JSON;
260
+ }
261
+
262
+ // Default fallback
263
+ if (data && typeof data === 'object' && isJsonSchema(data)) {
264
+ return ResourceType.JSON_SCHEMA;
265
+ }
266
+
267
+ return ResourceType.JSON; // Default
268
+ }
269
+
270
+ // ============================================================================
271
+ // Helper functions
272
+ // ============================================================================
273
+
274
+ /**
275
+ * Convert HeadingNode[] to simple Heading[] for Phase 1
276
+ *
277
+ * @param nodes - Parsed heading nodes
278
+ * @returns Simplified heading array
279
+ */
280
+ function convertHeadingsToSimple(nodes: HeadingNode[]): Heading[] {
281
+ const result: Heading[] = [];
282
+
283
+ function traverse(node: HeadingNode): void {
284
+ result.push({
285
+ level: node.level,
286
+ text: node.text,
287
+ id: node.slug,
288
+ });
289
+
290
+ if (node.children) {
291
+ for (const child of node.children) {
292
+ traverse(child);
293
+ }
294
+ }
295
+ }
296
+
297
+ for (const node of nodes) {
298
+ traverse(node);
299
+ }
300
+
301
+ return result;
302
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Path utilities for resource management
3
+ *
4
+ * Handles path normalization, validation, and absolutePath computation.
5
+ * All projectPath values must be relative with forward slashes.
6
+ */
7
+
8
+ import { join, normalize, sep } from 'node:path';
9
+
10
+ import { toForwardSlash } from '@vibe-agent-toolkit/utils';
11
+
12
+ /**
13
+ * Normalize a path to projectPath format
14
+ *
15
+ * Converts to relative path with forward slashes:
16
+ * - Removes leading /, //, file://, file:///
17
+ * - Converts backslashes to forward slashes
18
+ * - Preserves relative path structure (including ../)
19
+ *
20
+ * @param path - Path to normalize
21
+ * @returns Normalized projectPath (relative, forward slashes)
22
+ */
23
+ export function normalizeProjectPath(path: string): string {
24
+ let normalized = path;
25
+
26
+ // Remove file:// or file:/// protocol
27
+ normalized = normalized.replace(/^file:\/\/\/?/, '');
28
+
29
+ // Convert to forward slashes first (handles Windows backslashes)
30
+ normalized = toForwardSlash(normalized);
31
+
32
+ // Remove leading / or // (after converting backslashes)
33
+ normalized = normalized.replace(/^\/+/, '');
34
+
35
+ return normalized;
36
+ }
37
+
38
+ /**
39
+ * Validate that a projectPath is safe and relative
40
+ *
41
+ * Requirements:
42
+ * - Must be relative (no leading /, //, C:/, etc.)
43
+ * - Must use forward slashes only (no backslashes)
44
+ * - Must not escape project root with ../
45
+ * - Must not be a URL (http://, https://, file://)
46
+ * - Must not be empty
47
+ *
48
+ * @param projectPath - Path to validate
49
+ * @returns true if valid projectPath
50
+ */
51
+ export function isValidProjectPath(projectPath: string): boolean {
52
+ if (!projectPath) {
53
+ return false;
54
+ }
55
+
56
+ // Reject absolute paths (leading slash or drive letter)
57
+ if (projectPath.startsWith('/') || /^[A-Za-z]:/.test(projectPath)) {
58
+ return false;
59
+ }
60
+
61
+ // Reject URLs
62
+ if (projectPath.startsWith('http://') || projectPath.startsWith('https://') || projectPath.startsWith('file://')) {
63
+ return false;
64
+ }
65
+
66
+ // Reject backslashes (must use forward slashes)
67
+ if (projectPath.includes('\\')) {
68
+ return false;
69
+ }
70
+
71
+ // Reject paths that escape project root
72
+ // Normalize the path and check if it starts with ../
73
+ const normalized = normalize(projectPath);
74
+ const parts = normalized.split(sep);
75
+ let depth = 0;
76
+
77
+ for (const part of parts) {
78
+ if (part === '..') {
79
+ depth--;
80
+ if (depth < 0) {
81
+ return false; // Escaped project root
82
+ }
83
+ } else if (part !== '.' && part !== '') {
84
+ depth++;
85
+ }
86
+ }
87
+
88
+ return true;
89
+ }
90
+
91
+ /**
92
+ * Compute absolute path from project root and projectPath
93
+ *
94
+ * This is a runtime utility - absolutePath should never be serialized.
95
+ *
96
+ * @param projectRoot - Absolute path to project root
97
+ * @param projectPath - Relative path from project root
98
+ * @returns Absolute path to resource
99
+ */
100
+ export function getResourceAbsolutePath(projectRoot: string, projectPath: string): string {
101
+ return join(projectRoot, projectPath);
102
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Core resource type system for vibe-agent-toolkit
3
+ *
4
+ * Defines discriminated unions for different resource types with unified interface.
5
+ * All resources use projectPath (relative, forward slashes) for serialization,
6
+ * with optional absolutePath for runtime operations.
7
+ */
8
+
9
+ import type { SHA256 } from '../schemas/checksum.js';
10
+ import type { ValidationIssue } from '../schemas/validation-result.js';
11
+
12
+ /**
13
+ * Resource type discriminator for type-safe handling
14
+ */
15
+ export enum ResourceType {
16
+ MARKDOWN = 'markdown',
17
+ JSON_SCHEMA = 'json-schema',
18
+ JSON = 'json',
19
+ YAML = 'yaml',
20
+ }
21
+
22
+ /**
23
+ * Reference to a JSON Schema with validation tracking
24
+ */
25
+ export interface SchemaReference {
26
+ /** Path or URL to schema */
27
+ schema: string;
28
+
29
+ /** Source of schema assignment: 'self' (from $schema field), 'cli' (from CLI flag), or collection name */
30
+ source: string;
31
+
32
+ /** Whether validation was attempted for this schema */
33
+ applied: boolean;
34
+
35
+ /** Validation result (undefined if not validated) */
36
+ valid?: boolean;
37
+
38
+ /** Validation errors specific to this schema */
39
+ errors?: ValidationIssue[];
40
+ }
41
+
42
+ /**
43
+ * Simple heading structure for Phase 1
44
+ * Expanded in future phases
45
+ */
46
+ export interface Heading {
47
+ level: number;
48
+ text: string;
49
+ id?: string;
50
+ }
51
+
52
+ /**
53
+ * Base resource properties shared by all resource types
54
+ */
55
+ export interface BaseResource {
56
+ /** Unique identifier for this resource */
57
+ id: string;
58
+
59
+ /** Relative path from project root (forward slashes, no leading /) */
60
+ projectPath: string;
61
+
62
+ /** Absolute path computed at runtime (never serialized) */
63
+ absolutePath?: string;
64
+
65
+ /** Resource type discriminator */
66
+ type: ResourceType;
67
+
68
+ /** MIME type for the resource */
69
+ mimeType: string;
70
+
71
+ /** File size in bytes */
72
+ sizeBytes: number;
73
+
74
+ /** Last modification timestamp */
75
+ modifiedAt: Date;
76
+
77
+ /** SHA-256 checksum of file content */
78
+ checksum: SHA256;
79
+
80
+ /** Collections this resource belongs to (empty for Phase 1) */
81
+ collections: string[];
82
+ }
83
+
84
+ /**
85
+ * Markdown resource with parsed content and metadata
86
+ */
87
+ export interface MarkdownResource extends BaseResource {
88
+ type: ResourceType.MARKDOWN;
89
+ mimeType: 'text/markdown';
90
+
91
+ /** Parsed YAML frontmatter (if present) */
92
+ frontmatter?: Record<string, unknown>;
93
+
94
+ /** JSON Schema references with validation tracking */
95
+ schemas: SchemaReference[];
96
+
97
+ /** Raw markdown content */
98
+ content: string;
99
+
100
+ /** Link hrefs found in content */
101
+ links: string[];
102
+
103
+ /** Document heading structure */
104
+ headings: Heading[];
105
+
106
+ /** Estimated token count (chars / 4) */
107
+ estimatedTokenCount: number;
108
+ }
109
+
110
+ /**
111
+ * JSON Schema resource
112
+ */
113
+ export interface JsonSchemaResource extends BaseResource {
114
+ type: ResourceType.JSON_SCHEMA;
115
+ mimeType: 'application/schema+json';
116
+
117
+ /** Parsed schema object */
118
+ schema: object;
119
+
120
+ /** Schema $id field (if present) */
121
+ schemaId?: string;
122
+
123
+ /** Schema version (from $schema field) */
124
+ schemaVersion?: string;
125
+
126
+ /** Schema title (if present) */
127
+ title?: string;
128
+
129
+ /** Schema description (if present) */
130
+ description?: string;
131
+
132
+ /** Resource IDs that reference this schema */
133
+ referencedBy: string[];
134
+ }
135
+
136
+ /**
137
+ * JSON resource (not a schema)
138
+ */
139
+ export interface JsonResource extends BaseResource {
140
+ type: ResourceType.JSON;
141
+ mimeType: 'application/json';
142
+
143
+ /** Parsed JSON data */
144
+ data: unknown;
145
+
146
+ /** JSON Schema references with validation tracking */
147
+ schemas?: SchemaReference[];
148
+ }
149
+
150
+ /**
151
+ * YAML resource
152
+ */
153
+ export interface YamlResource extends BaseResource {
154
+ type: ResourceType.YAML;
155
+ mimeType: 'application/yaml';
156
+
157
+ /** Parsed YAML data */
158
+ data: unknown;
159
+
160
+ /** JSON Schema references with validation tracking */
161
+ schemas?: SchemaReference[];
162
+ }
163
+
164
+ /**
165
+ * Union type for all resource types
166
+ */
167
+ export type Resource = MarkdownResource | JsonSchemaResource | JsonResource | YamlResource;
168
+
169
+ // ============================================================================
170
+ // JSON Schema Detection
171
+ // ============================================================================
172
+
173
+ /**
174
+ * JSON Schema keywords for heuristic detection
175
+ * See: https://json-schema.org/understanding-json-schema/reference/
176
+ */
177
+ const SCHEMA_KEYWORDS = [
178
+ '$schema',
179
+ '$id',
180
+ 'title',
181
+ 'description',
182
+ 'type',
183
+ 'properties',
184
+ 'required',
185
+ 'items',
186
+ 'enum',
187
+ 'definitions',
188
+ '$defs',
189
+ 'allOf',
190
+ 'anyOf',
191
+ 'oneOf',
192
+ 'not',
193
+ ] as const;
194
+
195
+ /**
196
+ * Detect if data is likely a JSON Schema using heuristics
197
+ *
198
+ * Returns true if data is an object with 2+ recognized schema keywords.
199
+ * This heuristic catches schemas without explicit $schema field.
200
+ *
201
+ * @param data - Data to check
202
+ * @returns true if data appears to be a JSON Schema
203
+ */
204
+ export function isJsonSchema(data: unknown): boolean {
205
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
206
+ return false;
207
+ }
208
+
209
+ const matchCount = SCHEMA_KEYWORDS.filter((keyword) => keyword in data).length;
210
+ return matchCount >= 2;
211
+ }