feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation. **New Features:** - ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp - EXIF extraction: Camera data, GPS, timestamps using exifr library - Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats - MimeTypeDetector: Unified MIME type detection with magic byte support - FormatDetector: Enhanced with image format detection via MIME + magic bytes **Architecture Fixes:** - Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154) - Added parameter spreading for ImportSource objects to enable augmentation access - Fixed metadata propagation through ImportCoordinator to final results - Added augmentation data check in ImportCoordinator.extract() **Integration:** - ImageHandler registered as built-in handler alongside CSV, Excel, PDF - Images import as 'media' entities with 'image' subtype - Full metadata preserved in knowledge graph entities - Configuration options: enableImage, extractEXIF, imageDefaults **Test Coverage:** - 15 integration tests (image-import.test.ts) - 100% passing - 27 unit tests (image-handler.test.ts) - 100% passing - Format detection tests for all supported image types - Error handling and resilience tests **Breaking Changes:** None - backward compatible Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6345f87eb2
commit
1874b77896
26 changed files with 5079 additions and 434 deletions
687
docs/augmentations/FORMAT_HANDLERS.md
Normal file
687
docs/augmentations/FORMAT_HANDLERS.md
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
# Creating Custom Format Handlers
|
||||
|
||||
**Version:** 5.2.0+
|
||||
|
||||
Format handlers enable you to import ANY file type into Brainy as structured knowledge graph data. This guide shows how to create custom format handlers for your specific file formats.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { BaseFormatHandler, globalHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
import type { FormatHandlerOptions, ProcessedData } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
|
||||
class MyFormatHandler extends BaseFormatHandler {
|
||||
readonly format = 'myformat'
|
||||
|
||||
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
|
||||
// Option 1: Check by MIME type
|
||||
if (typeof data === 'object' && 'filename' in data) {
|
||||
const mimeType = this.getMimeType(data)
|
||||
return this.mimeTypeMatches(mimeType, ['application/x-myformat'])
|
||||
}
|
||||
|
||||
// Option 2: Check by magic bytes
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return data[0] === 0x4D && data[1] === 0x59 // "MY" magic bytes
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async process(
|
||||
data: Buffer | string,
|
||||
options: FormatHandlerOptions
|
||||
): Promise<ProcessedData> {
|
||||
// Convert to Buffer
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
|
||||
// Parse your format
|
||||
const parsed = this.parseMyFormat(buffer)
|
||||
|
||||
// Return structured data
|
||||
return {
|
||||
format: 'myformat',
|
||||
data: [
|
||||
{
|
||||
type: 'MyEntity',
|
||||
name: parsed.name,
|
||||
metadata: parsed.metadata
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
rowCount: 1,
|
||||
fields: ['type', 'name', 'metadata'],
|
||||
processingTime: Date.now() - startTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseMyFormat(buffer: Buffer): any {
|
||||
// Your parsing logic here
|
||||
return { name: 'example', metadata: {} }
|
||||
}
|
||||
}
|
||||
|
||||
// Register globally
|
||||
globalHandlerRegistry.registerHandler({
|
||||
name: 'myformat',
|
||||
mimeTypes: ['application/x-myformat'],
|
||||
extensions: ['.myf', '.myfmt'],
|
||||
loader: async () => new MyFormatHandler()
|
||||
})
|
||||
```
|
||||
|
||||
Now Brainy automatically handles your format:
|
||||
|
||||
```typescript
|
||||
await brain.import({
|
||||
type: 'file',
|
||||
data: myFormatBuffer,
|
||||
filename: 'data.myf'
|
||||
})
|
||||
// Automatically routes to MyFormatHandler!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## BaseFormatHandler
|
||||
|
||||
All format handlers should extend `BaseFormatHandler`, which provides:
|
||||
|
||||
### MIME Type Detection
|
||||
|
||||
```typescript
|
||||
protected getMimeType(data: Buffer | string | { filename?: string }): string
|
||||
```
|
||||
|
||||
Detects MIME type from filename or buffer. Uses Brainy's comprehensive MIME detection (2000+ types).
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
const mimeType = this.getMimeType({ filename: 'data.dwg' })
|
||||
// Returns: 'image/vnd.dwg'
|
||||
```
|
||||
|
||||
### MIME Type Matching
|
||||
|
||||
```typescript
|
||||
protected mimeTypeMatches(mimeType: string, patterns: string[]): boolean
|
||||
```
|
||||
|
||||
Checks if MIME type matches patterns. Supports wildcards (`image/*`).
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
if (this.mimeTypeMatches(mimeType, ['image/*', 'video/*'])) {
|
||||
// Handle all images and videos
|
||||
}
|
||||
```
|
||||
|
||||
### Extension Detection
|
||||
|
||||
```typescript
|
||||
protected detectExtension(data: string | Buffer | { filename?: string; ext?: string }): string | null
|
||||
```
|
||||
|
||||
Extracts file extension for fallback detection.
|
||||
|
||||
---
|
||||
|
||||
## canHandle() Method
|
||||
|
||||
The `canHandle()` method determines if your handler can process the given data.
|
||||
|
||||
### Strategy 1: MIME Type Detection (Recommended)
|
||||
|
||||
```typescript
|
||||
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
|
||||
if (typeof data === 'object' && 'filename' in data) {
|
||||
const mimeType = this.getMimeType(data)
|
||||
return this.mimeTypeMatches(mimeType, [
|
||||
'application/x-myformat',
|
||||
'application/myformat'
|
||||
])
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Pros:** Automatic, comprehensive, works with 2000+ types
|
||||
❌ **Cons:** Requires filename
|
||||
|
||||
### Strategy 2: Magic Byte Detection
|
||||
|
||||
```typescript
|
||||
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
// Check magic bytes
|
||||
return (
|
||||
data[0] === 0x50 && // 'P'
|
||||
data[1] === 0x4B && // 'K'
|
||||
data[2] === 0x03 &&
|
||||
data[3] === 0x04 // ZIP signature
|
||||
)
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Pros:** Works without filename, robust
|
||||
❌ **Cons:** Requires knowledge of format structure
|
||||
|
||||
### Strategy 3: Combined Approach
|
||||
|
||||
```typescript
|
||||
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
|
||||
// Try MIME type first
|
||||
if (typeof data === 'object' && 'filename' in data) {
|
||||
const mimeType = this.getMimeType(data)
|
||||
if (this.mimeTypeMatches(mimeType, ['application/x-myformat'])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to magic bytes
|
||||
if (Buffer.isBuffer(data)) {
|
||||
return this.checkMagicBytes(data)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
✅ **Pros:** Robust, works in all scenarios
|
||||
❌ **Cons:** More complex
|
||||
|
||||
---
|
||||
|
||||
## process() Method
|
||||
|
||||
The `process()` method extracts structured data from the file.
|
||||
|
||||
### Return Format
|
||||
|
||||
```typescript
|
||||
interface ProcessedData {
|
||||
/** Format identifier */
|
||||
format: string
|
||||
|
||||
/** Array of extracted entities */
|
||||
data: Array<Record<string, any>>
|
||||
|
||||
/** Metadata about processing */
|
||||
metadata: {
|
||||
rowCount: number
|
||||
fields: string[]
|
||||
processingTime: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/** Original filename (optional) */
|
||||
filename?: string
|
||||
}
|
||||
```
|
||||
|
||||
### Example: CAD File Handler
|
||||
|
||||
```typescript
|
||||
async process(data: Buffer | string, options: FormatHandlerOptions): Promise<ProcessedData> {
|
||||
const startTime = Date.now()
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
|
||||
// Parse CAD file
|
||||
const cad = await this.parseCAD(buffer)
|
||||
|
||||
// Extract entities
|
||||
const entities = []
|
||||
|
||||
// Main CAD document
|
||||
entities.push({
|
||||
type: 'CADDocument',
|
||||
filename: options.filename,
|
||||
units: cad.units,
|
||||
bounds: cad.bounds
|
||||
})
|
||||
|
||||
// Layers
|
||||
for (const layer of cad.layers) {
|
||||
entities.push({
|
||||
type: 'CADLayer',
|
||||
name: layer.name,
|
||||
color: layer.color,
|
||||
visible: layer.visible,
|
||||
objectCount: layer.objects.length
|
||||
})
|
||||
}
|
||||
|
||||
// Objects
|
||||
for (const obj of cad.objects) {
|
||||
entities.push({
|
||||
type: 'CADObject',
|
||||
objectType: obj.type,
|
||||
layer: obj.layer,
|
||||
geometry: obj.geometry,
|
||||
properties: obj.properties
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'cad',
|
||||
data: entities,
|
||||
metadata: {
|
||||
rowCount: entities.length,
|
||||
fields: ['type', 'name', 'geometry', 'properties'],
|
||||
processingTime: Date.now() - startTime,
|
||||
layerCount: cad.layers.length,
|
||||
objectCount: cad.objects.length,
|
||||
units: cad.units
|
||||
},
|
||||
filename: options.filename
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Always track processing time:**
|
||||
```typescript
|
||||
const startTime = Date.now()
|
||||
// ... processing ...
|
||||
metadata.processingTime = Date.now() - startTime
|
||||
```
|
||||
|
||||
2. **Include rich metadata:**
|
||||
```typescript
|
||||
metadata: {
|
||||
rowCount: entities.length,
|
||||
fields: ['type', 'name', ...],
|
||||
processingTime: 123,
|
||||
// Format-specific metadata
|
||||
layerCount: 5,
|
||||
objectCount: 150,
|
||||
version: '2.0'
|
||||
}
|
||||
```
|
||||
|
||||
3. **Handle errors gracefully:**
|
||||
```typescript
|
||||
try {
|
||||
const parsed = this.parse(buffer)
|
||||
return { format: 'myformat', data: parsed, ... }
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to parse myformat: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
4. **Support progress reporting (optional):**
|
||||
```typescript
|
||||
if (options.progressHooks?.onCurrentItem) {
|
||||
options.progressHooks.onCurrentItem(`Processing layer ${i}/${total}`)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FormatHandlerRegistry
|
||||
|
||||
### Global Registry
|
||||
|
||||
Use the global registry for application-wide handlers:
|
||||
|
||||
```typescript
|
||||
import { globalHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
|
||||
globalHandlerRegistry.registerHandler({
|
||||
name: 'myformat',
|
||||
mimeTypes: ['application/x-myformat'],
|
||||
extensions: ['.myf'],
|
||||
loader: async () => new MyFormatHandler()
|
||||
})
|
||||
```
|
||||
|
||||
### Local Registry
|
||||
|
||||
Create a local registry for scoped handlers:
|
||||
|
||||
```typescript
|
||||
import { FormatHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
|
||||
const registry = new FormatHandlerRegistry()
|
||||
registry.registerHandler({ ... })
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
Handlers are lazy-loaded for performance:
|
||||
|
||||
```typescript
|
||||
globalHandlerRegistry.registerHandler({
|
||||
name: 'heavy',
|
||||
mimeTypes: ['application/x-heavy'],
|
||||
extensions: ['.heavy'],
|
||||
loader: async () => {
|
||||
// Only loaded when first needed
|
||||
const { HeavyHandler } = await import('./HeavyHandler.js')
|
||||
return new HeavyHandler()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Getting Handlers
|
||||
|
||||
```typescript
|
||||
// By filename (automatic MIME detection)
|
||||
const handler = await registry.getHandler('data.myf')
|
||||
|
||||
// By MIME type
|
||||
const handler = await registry.getHandlerByMimeType('application/x-myformat')
|
||||
|
||||
// By extension
|
||||
const handler = await registry.getHandlerByExtension('.myf')
|
||||
|
||||
// By name
|
||||
const handler = await registry.getHandlerByName('myformat')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Video Metadata Extractor
|
||||
|
||||
```typescript
|
||||
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
|
||||
class VideoHandler extends BaseFormatHandler {
|
||||
readonly format = 'video'
|
||||
|
||||
canHandle(data) {
|
||||
const mimeType = this.getMimeType(data)
|
||||
return this.mimeTypeMatches(mimeType, ['video/*'])
|
||||
}
|
||||
|
||||
async process(data, options) {
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
|
||||
// Extract video metadata with ffmpeg
|
||||
const metadata = await this.extractVideoMetadata(buffer)
|
||||
|
||||
return {
|
||||
format: 'video',
|
||||
data: [{
|
||||
type: 'Video',
|
||||
duration: metadata.duration,
|
||||
codec: metadata.codec,
|
||||
resolution: metadata.resolution,
|
||||
frameRate: metadata.frameRate,
|
||||
bitrate: metadata.bitrate,
|
||||
audioTracks: metadata.audioTracks,
|
||||
subtitles: metadata.subtitles
|
||||
}],
|
||||
metadata: {
|
||||
rowCount: 1,
|
||||
fields: ['type', 'duration', 'codec', 'resolution'],
|
||||
processingTime: metadata.processingTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async extractVideoMetadata(buffer: Buffer) {
|
||||
// Use ffmpeg to extract metadata
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg(buffer)
|
||||
.ffprobe((err, data) => {
|
||||
if (err) reject(err)
|
||||
else resolve(this.parseFFmpegOutput(data))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Git Repository Parser
|
||||
|
||||
```typescript
|
||||
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
import { simpleGit } from 'simple-git'
|
||||
|
||||
class GitRepoHandler extends BaseFormatHandler {
|
||||
readonly format = 'git-repo'
|
||||
|
||||
canHandle(data) {
|
||||
// Check for .git directory
|
||||
if (typeof data === 'object' && 'filename' in data) {
|
||||
return data.filename?.includes('.git') || false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async process(data, options) {
|
||||
const repoPath = options.filename || ''
|
||||
const git = simpleGit(repoPath)
|
||||
|
||||
// Extract commits
|
||||
const log = await git.log()
|
||||
const commits = log.all.map(commit => ({
|
||||
type: 'GitCommit',
|
||||
hash: commit.hash,
|
||||
message: commit.message,
|
||||
author: commit.author_name,
|
||||
date: commit.date
|
||||
}))
|
||||
|
||||
// Extract branches
|
||||
const branchSummary = await git.branchLocal()
|
||||
const branches = Object.keys(branchSummary.branches).map(name => ({
|
||||
type: 'GitBranch',
|
||||
name,
|
||||
current: branchSummary.current === name
|
||||
}))
|
||||
|
||||
return {
|
||||
format: 'git-repo',
|
||||
data: [...commits, ...branches],
|
||||
metadata: {
|
||||
rowCount: commits.length + branches.length,
|
||||
fields: ['type', 'hash', 'message', 'author'],
|
||||
processingTime: Date.now() - startTime,
|
||||
commitCount: commits.length,
|
||||
branchCount: branches.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Database Schema Importer
|
||||
|
||||
```typescript
|
||||
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
|
||||
import { Client } from 'pg'
|
||||
|
||||
class PostgreSQLSchemaHandler extends BaseFormatHandler {
|
||||
readonly format = 'postgres-schema'
|
||||
|
||||
canHandle(data) {
|
||||
if (typeof data === 'object' && 'filename' in data) {
|
||||
return data.filename?.endsWith('.sql') || false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async process(data, options) {
|
||||
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
|
||||
const sql = buffer.toString('utf-8')
|
||||
|
||||
// Parse SQL or connect to database
|
||||
const schema = await this.parseSchema(sql)
|
||||
|
||||
const entities = []
|
||||
|
||||
// Tables
|
||||
for (const table of schema.tables) {
|
||||
entities.push({
|
||||
type: 'Table',
|
||||
name: table.name,
|
||||
schema: table.schema,
|
||||
columnCount: table.columns.length
|
||||
})
|
||||
|
||||
// Columns
|
||||
for (const column of table.columns) {
|
||||
entities.push({
|
||||
type: 'Column',
|
||||
name: column.name,
|
||||
table: table.name,
|
||||
dataType: column.dataType,
|
||||
nullable: column.nullable,
|
||||
primaryKey: column.primaryKey
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Foreign keys
|
||||
for (const fk of schema.foreignKeys) {
|
||||
entities.push({
|
||||
type: 'ForeignKey',
|
||||
from: `${fk.fromTable}.${fk.fromColumn}`,
|
||||
to: `${fk.toTable}.${fk.toColumn}`
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
format: 'postgres-schema',
|
||||
data: entities,
|
||||
metadata: {
|
||||
rowCount: entities.length,
|
||||
fields: ['type', 'name', 'table', 'dataType'],
|
||||
processingTime: Date.now() - startTime,
|
||||
tableCount: schema.tables.length,
|
||||
columnCount: schema.tables.reduce((sum, t) => sum + t.columns.length, 0),
|
||||
foreignKeyCount: schema.foreignKeys.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Premium Augmentations
|
||||
|
||||
Package your handler as a premium augmentation:
|
||||
|
||||
```typescript
|
||||
// @yourcompany/brainy-cad-importer
|
||||
|
||||
import { BaseAugmentation } from '@soulcraft/brainy'
|
||||
import { CADHandler } from './CADHandler.js'
|
||||
|
||||
export class CADImportAugmentation extends BaseAugmentation {
|
||||
readonly name = 'cad-import'
|
||||
readonly timing = 'before'
|
||||
readonly operations = ['import', 'importFile']
|
||||
|
||||
private handler: CADHandler
|
||||
|
||||
constructor(config = {}) {
|
||||
super(config)
|
||||
this.handler = new CADHandler()
|
||||
}
|
||||
|
||||
async execute(operation, params, next) {
|
||||
// Check if this is a CAD file
|
||||
if (this.isCADFile(params)) {
|
||||
const processed = await this.handler.process(params.data, params.options)
|
||||
params.data = processed.data
|
||||
params.metadata = { ...params.metadata, ...processed.metadata }
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
private isCADFile(params: any): boolean {
|
||||
return this.handler.canHandle(params.data || params)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// npm install @yourcompany/brainy-cad-importer
|
||||
// brain.addAugmentation(new CADImportAugmentation())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { MyFormatHandler } from './MyFormatHandler.js'
|
||||
|
||||
describe('MyFormatHandler', () => {
|
||||
let handler: MyFormatHandler
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new MyFormatHandler()
|
||||
})
|
||||
|
||||
describe('canHandle', () => {
|
||||
it('should handle .myf files', () => {
|
||||
expect(handler.canHandle({ filename: 'data.myf' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle by MIME type', () => {
|
||||
expect(handler.canHandle({ filename: 'data.myformat' })).toBe(true)
|
||||
})
|
||||
|
||||
it('should reject non-myformat files', () => {
|
||||
expect(handler.canHandle({ filename: 'data.txt' })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('process', () => {
|
||||
it('should extract structured data', async () => {
|
||||
const testData = Buffer.from('MY format data')
|
||||
|
||||
const result = await handler.process(testData)
|
||||
|
||||
expect(result.format).toBe('myformat')
|
||||
expect(result.data).toHaveLength(1)
|
||||
expect(result.metadata.processingTime).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
const invalidData = Buffer.from('invalid')
|
||||
|
||||
await expect(handler.process(invalidData)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always extend BaseFormatHandler** - provides MIME detection and utilities
|
||||
2. **Use MIME types for routing** - automatic, comprehensive, maintainable
|
||||
3. **Lazy load heavy dependencies** - better performance
|
||||
4. **Extract rich metadata** - make data queryable in knowledge graph
|
||||
5. **Handle errors gracefully** - fail fast with clear error messages
|
||||
6. **Test thoroughly** - test canHandle() and process() with real data
|
||||
7. **Document your format** - explain what data is extracted and how
|
||||
8. **Follow ProcessedData format** - ensures compatibility with Brainy
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [ImageHandler source](../../src/augmentations/intelligentImport/handlers/imageHandler.ts) - Reference implementation
|
||||
- [BaseFormatHandler source](../../src/augmentations/intelligentImport/handlers/base.ts) - Base class
|
||||
- [FormatHandlerRegistry source](../../src/augmentations/intelligentImport/FormatHandlerRegistry.ts) - Registry implementation
|
||||
- [Augmentations Guide](./AUGMENTATIONS.md) - Creating augmentations
|
||||
Loading…
Add table
Add a link
Reference in a new issue