// Package hcfs provides a Go SDK for the Context-Aware Hierarchical Context File System package hcfs import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "sync" "time" "golang.org/x/time/rate" ) // Version of the HCFS Go SDK const Version = "2.0.0" // Default configuration values const ( DefaultTimeout = 30 * time.Second DefaultRetries = 3 DefaultRateLimit = 100 // requests per second DefaultCacheSize = 1000 DefaultCacheTTL = time.Hour ) // ContextStatus represents the status of a context type ContextStatus string const ( ContextStatusActive ContextStatus = "active" ContextStatusArchived ContextStatus = "archived" ContextStatusDeleted ContextStatus = "deleted" ContextStatusDraft ContextStatus = "draft" ) // SearchType represents the type of search to perform type SearchType string const ( SearchTypeSemantic SearchType = "semantic" SearchTypeKeyword SearchType = "keyword" SearchTypeHybrid SearchType = "hybrid" SearchTypeFuzzy SearchType = "fuzzy" ) // Context represents a context in the HCFS system type Context struct { ID *int `json:"id,omitempty"` Path string `json:"path"` Content string `json:"content"` Summary *string `json:"summary,omitempty"` Author *string `json:"author,omitempty"` Tags []string `json:"tags,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` Status *ContextStatus `json:"status,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"` Version *int `json:"version,omitempty"` SimilarityScore *float64 `json:"similarity_score,omitempty"` } // ContextCreate represents data for creating a new context type ContextCreate struct { Path string `json:"path"` Content string `json:"content"` Summary *string `json:"summary,omitempty"` Author *string `json:"author,omitempty"` Tags []string `json:"tags,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` } // ContextUpdate represents data for updating a context type ContextUpdate struct { Content *string `json:"content,omitempty"` Summary *string `json:"summary,omitempty"` Tags []string `json:"tags,omitempty"` Metadata map[string]interface{} `json:"metadata,omitempty"` Status *ContextStatus `json:"status,omitempty"` } // SearchResult represents a search result type SearchResult struct { Context Context `json:"context"` Score float64 `json:"score"` Explanation *string `json:"explanation,omitempty"` Highlights []string `json:"highlights,omitempty"` } // SearchOptions represents search configuration options type SearchOptions struct { SearchType *SearchType `json:"search_type,omitempty"` TopK *int `json:"top_k,omitempty"` SimilarityThreshold *float64 `json:"similarity_threshold,omitempty"` PathPrefix *string `json:"path_prefix,omitempty"` SemanticWeight *float64 `json:"semantic_weight,omitempty"` IncludeContent *bool `json:"include_content,omitempty"` IncludeHighlights *bool `json:"include_highlights,omitempty"` MaxHighlights *int `json:"max_highlights,omitempty"` } // ContextFilter represents filtering options for listing contexts type ContextFilter struct { PathPrefix *string `json:"path_prefix,omitempty"` Author *string `json:"author,omitempty"` Status *ContextStatus `json:"status,omitempty"` Tags []string `json:"tags,omitempty"` CreatedAfter *time.Time `json:"created_after,omitempty"` CreatedBefore *time.Time `json:"created_before,omitempty"` ContentContains *string `json:"content_contains,omitempty"` MinContentLength *int `json:"min_content_length,omitempty"` MaxContentLength *int `json:"max_content_length,omitempty"` } // PaginationOptions represents pagination configuration type PaginationOptions struct { Page *int `json:"page,omitempty"` PageSize *int `json:"page_size,omitempty"` SortBy *string `json:"sort_by,omitempty"` SortOrder *string `json:"sort_order,omitempty"` } // PaginationMeta represents pagination metadata type PaginationMeta struct { Page int `json:"page"` PageSize int `json:"page_size"` TotalItems int `json:"total_items"` TotalPages int `json:"total_pages"` HasNext bool `json:"has_next"` HasPrevious bool `json:"has_previous"` } // BatchResult represents the result of a batch operation type BatchResult struct { SuccessCount int `json:"success_count"` ErrorCount int `json:"error_count"` TotalItems int `json:"total_items"` SuccessfulItems []interface{} `json:"successful_items"` FailedItems []map[string]interface{} `json:"failed_items"` ExecutionTime time.Duration `json:"execution_time"` SuccessRate float64 `json:"success_rate"` } // APIResponse represents a generic API response wrapper type APIResponse[T any] struct { Success bool `json:"success"` Data T `json:"data"` Timestamp time.Time `json:"timestamp"` APIVersion string `json:"api_version"` RequestID *string `json:"request_id,omitempty"` } // ListResponse represents a paginated list response type ListResponse[T any] struct { APIResponse[[]T] Pagination PaginationMeta `json:"pagination"` } // SearchResponse represents a search response type SearchResponse struct { Success bool `json:"success"` Data []SearchResult `json:"data"` Query string `json:"query"` SearchType SearchType `json:"search_type"` TotalResults int `json:"total_results"` SearchTimeMs float64 `json:"search_time_ms"` FiltersApplied map[string]any `json:"filters_applied,omitempty"` Timestamp time.Time `json:"timestamp"` APIVersion string `json:"api_version"` } // HealthStatus represents health status type HealthStatus string const ( HealthStatusHealthy HealthStatus = "healthy" HealthStatusDegraded HealthStatus = "degraded" HealthStatusUnhealthy HealthStatus = "unhealthy" ) // ComponentHealth represents health of a system component type ComponentHealth struct { Name string `json:"name"` Status HealthStatus `json:"status"` ResponseTimeMs *float64 `json:"response_time_ms,omitempty"` ErrorMessage *string `json:"error_message,omitempty"` } // HealthResponse represents health check response type HealthResponse struct { Status HealthStatus `json:"status"` Version string `json:"version"` UptimeSeconds float64 `json:"uptime_seconds"` Components []ComponentHealth `json:"components"` } // Config represents HCFS client configuration type Config struct { BaseURL string APIKey string JWTToken string Timeout time.Duration UserAgent string MaxRetries int RetryDelay time.Duration RateLimit float64 MaxConcurrentRequests int CacheEnabled bool CacheSize int CacheTTL time.Duration } // DefaultConfig returns a default configuration func DefaultConfig(baseURL, apiKey string) *Config { return &Config{ BaseURL: baseURL, APIKey: apiKey, Timeout: DefaultTimeout, UserAgent: fmt.Sprintf("hcfs-go/%s", Version), MaxRetries: DefaultRetries, RetryDelay: time.Second, RateLimit: DefaultRateLimit, MaxConcurrentRequests: 100, CacheEnabled: true, CacheSize: DefaultCacheSize, CacheTTL: DefaultCacheTTL, } } // Client represents the HCFS API client type Client struct { config *Config httpClient *http.Client rateLimiter *rate.Limiter cache *cache analytics *analytics mu sync.RWMutex } // NewClient creates a new HCFS client func NewClient(config *Config) *Client { if config == nil { panic("config cannot be nil") } client := &Client{ config: config, httpClient: &http.Client{ Timeout: config.Timeout, }, rateLimiter: rate.NewLimiter(rate.Limit(config.RateLimit), int(config.RateLimit)), analytics: newAnalytics(), } if config.CacheEnabled { client.cache = newCache(config.CacheSize, config.CacheTTL) } return client } // HealthCheck checks the API health status func (c *Client) HealthCheck(ctx context.Context) (*HealthResponse, error) { var response HealthResponse err := c.request(ctx, "GET", "/health", nil, nil, &response) if err != nil { return nil, err } return &response, nil } // CreateContext creates a new context func (c *Client) CreateContext(ctx context.Context, contextData *ContextCreate) (*Context, error) { if contextData == nil { return nil, fmt.Errorf("contextData cannot be nil") } if !validatePath(contextData.Path) { return nil, fmt.Errorf("invalid context path: %s", contextData.Path) } // Normalize path contextData.Path = normalizePath(contextData.Path) var response APIResponse[Context] err := c.request(ctx, "POST", "/api/v1/contexts", nil, contextData, &response) if err != nil { return nil, err } // Invalidate relevant cache entries c.invalidateCache("/api/v1/contexts") return &response.Data, nil } // GetContext retrieves a context by ID func (c *Client) GetContext(ctx context.Context, contextID int) (*Context, error) { path := fmt.Sprintf("/api/v1/contexts/%d", contextID) // Check cache first if c.cache != nil { if cached, ok := c.cache.get(path); ok { if context, ok := cached.(*Context); ok { c.analytics.recordCacheHit() return context, nil } } c.analytics.recordCacheMiss() } var response APIResponse[Context] err := c.request(ctx, "GET", path, nil, nil, &response) if err != nil { return nil, err } // Cache the result if c.cache != nil { c.cache.set(path, &response.Data) } return &response.Data, nil } // ListContexts lists contexts with filtering and pagination func (c *Client) ListContexts(ctx context.Context, filter *ContextFilter, pagination *PaginationOptions) ([]Context, *PaginationMeta, error) { params := url.Values{} // Add filter parameters if filter != nil { if filter.PathPrefix != nil { params.Set("path_prefix", *filter.PathPrefix) } if filter.Author != nil { params.Set("author", *filter.Author) } if filter.Status != nil { params.Set("status", string(*filter.Status)) } if filter.CreatedAfter != nil { params.Set("created_after", filter.CreatedAfter.Format(time.RFC3339)) } if filter.CreatedBefore != nil { params.Set("created_before", filter.CreatedBefore.Format(time.RFC3339)) } if filter.ContentContains != nil { params.Set("content_contains", *filter.ContentContains) } if filter.MinContentLength != nil { params.Set("min_content_length", strconv.Itoa(*filter.MinContentLength)) } if filter.MaxContentLength != nil { params.Set("max_content_length", strconv.Itoa(*filter.MaxContentLength)) } } // Add pagination parameters if pagination != nil { if pagination.Page != nil { params.Set("page", strconv.Itoa(*pagination.Page)) } if pagination.PageSize != nil { params.Set("page_size", strconv.Itoa(*pagination.PageSize)) } if pagination.SortBy != nil { params.Set("sort_by", *pagination.SortBy) } if pagination.SortOrder != nil { params.Set("sort_order", *pagination.SortOrder) } } var response ListResponse[Context] err := c.request(ctx, "GET", "/api/v1/contexts", params, nil, &response) if err != nil { return nil, nil, err } return response.Data, &response.Pagination, nil } // UpdateContext updates an existing context func (c *Client) UpdateContext(ctx context.Context, contextID int, updates *ContextUpdate) (*Context, error) { if updates == nil { return nil, fmt.Errorf("updates cannot be nil") } path := fmt.Sprintf("/api/v1/contexts/%d", contextID) var response APIResponse[Context] err := c.request(ctx, "PUT", path, nil, updates, &response) if err != nil { return nil, err } // Invalidate cache c.invalidateCache(path) c.invalidateCache("/api/v1/contexts") return &response.Data, nil } // DeleteContext deletes a context func (c *Client) DeleteContext(ctx context.Context, contextID int) error { path := fmt.Sprintf("/api/v1/contexts/%d", contextID) err := c.request(ctx, "DELETE", path, nil, nil, nil) if err != nil { return err } // Invalidate cache c.invalidateCache(path) c.invalidateCache("/api/v1/contexts") return nil } // SearchContexts searches contexts using various search methods func (c *Client) SearchContexts(ctx context.Context, query string, options *SearchOptions) ([]SearchResult, error) { if query == "" { return nil, fmt.Errorf("query cannot be empty") } searchData := map[string]interface{}{ "query": query, } if options != nil { if options.SearchType != nil { searchData["search_type"] = string(*options.SearchType) } if options.TopK != nil { searchData["top_k"] = *options.TopK } if options.SimilarityThreshold != nil { searchData["similarity_threshold"] = *options.SimilarityThreshold } if options.PathPrefix != nil { searchData["path_prefix"] = *options.PathPrefix } if options.SemanticWeight != nil { searchData["semantic_weight"] = *options.SemanticWeight } if options.IncludeContent != nil { searchData["include_content"] = *options.IncludeContent } if options.IncludeHighlights != nil { searchData["include_highlights"] = *options.IncludeHighlights } if options.MaxHighlights != nil { searchData["max_highlights"] = *options.MaxHighlights } } var response SearchResponse err := c.request(ctx, "POST", "/api/v1/search", nil, searchData, &response) if err != nil { return nil, err } return response.Data, nil } // BatchCreateContexts creates multiple contexts in batch func (c *Client) BatchCreateContexts(ctx context.Context, contexts []*ContextCreate) (*BatchResult, error) { if len(contexts) == 0 { return nil, fmt.Errorf("contexts cannot be empty") } startTime := time.Now() // Validate and normalize all contexts for _, context := range contexts { if !validatePath(context.Path) { return nil, fmt.Errorf("invalid context path: %s", context.Path) } context.Path = normalizePath(context.Path) } batchData := map[string]interface{}{ "contexts": contexts, } var response APIResponse[BatchResult] err := c.request(ctx, "POST", "/api/v1/contexts/batch", nil, batchData, &response) if err != nil { return nil, err } // Calculate additional metrics result := response.Data result.ExecutionTime = time.Since(startTime) result.SuccessRate = float64(result.SuccessCount) / float64(result.TotalItems) // Invalidate cache c.invalidateCache("/api/v1/contexts") return &result, nil } // IterateContexts iterates through all contexts with automatic pagination func (c *Client) IterateContexts(ctx context.Context, filter *ContextFilter, pageSize int, callback func(Context) error) error { if pageSize <= 0 { pageSize = 100 } page := 1 for { pagination := &PaginationOptions{ Page: &page, PageSize: &pageSize, } contexts, paginationMeta, err := c.ListContexts(ctx, filter, pagination) if err != nil { return err } if len(contexts) == 0 { break } for _, context := range contexts { if err := callback(context); err != nil { return err } } // If we got fewer contexts than requested, we've reached the end if len(contexts) < pageSize || !paginationMeta.HasNext { break } page++ } return nil } // GetAnalytics returns client analytics func (c *Client) GetAnalytics() map[string]interface{} { c.mu.RLock() defer c.mu.RUnlock() analytics := map[string]interface{}{ "session_start": c.analytics.sessionStart, "operation_count": c.analytics.operationCount, "error_count": c.analytics.errorCount, "total_requests": c.analytics.totalRequests, "failed_requests": c.analytics.failedRequests, } if c.cache != nil { analytics["cache_stats"] = map[string]interface{}{ "enabled": true, "size": c.cache.size(), "max_size": c.cache.maxSize, "hit_rate": c.analytics.getCacheHitRate(), } } else { analytics["cache_stats"] = map[string]interface{}{ "enabled": false, } } return analytics } // ClearCache clears the client cache func (c *Client) ClearCache() { if c.cache != nil { c.cache.clear() } } // Close closes the client and cleans up resources func (c *Client) Close() error { if c.cache != nil { c.cache.clear() } return nil } // Internal method to make HTTP requests func (c *Client) request(ctx context.Context, method, path string, params url.Values, body interface{}, result interface{}) error { // Rate limiting if err := c.rateLimiter.Wait(ctx); err != nil { return fmt.Errorf("rate limit error: %w", err) } // Build URL u, err := url.Parse(c.config.BaseURL + path) if err != nil { return fmt.Errorf("invalid URL: %w", err) } if params != nil { u.RawQuery = params.Encode() } // Prepare body var bodyReader io.Reader if body != nil { bodyBytes, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal body: %w", err) } bodyReader = bytes.NewReader(bodyBytes) } // Create request req, err := http.NewRequestWithContext(ctx, method, u.String(), bodyReader) if err != nil { return fmt.Errorf("failed to create request: %w", err) } // Set headers req.Header.Set("User-Agent", c.config.UserAgent) req.Header.Set("Content-Type", "application/json") if c.config.APIKey != "" { req.Header.Set("X-API-Key", c.config.APIKey) } if c.config.JWTToken != "" { req.Header.Set("Authorization", "Bearer "+c.config.JWTToken) } // Execute request with retries var resp *http.Response for attempt := 0; attempt <= c.config.MaxRetries; attempt++ { c.analytics.recordRequest() resp, err = c.httpClient.Do(req) if err != nil { if attempt == c.config.MaxRetries { c.analytics.recordError(err.Error()) return fmt.Errorf("request failed after %d attempts: %w", c.config.MaxRetries+1, err) } time.Sleep(c.config.RetryDelay * time.Duration(attempt+1)) continue } // Check if we should retry based on status code if shouldRetry(resp.StatusCode) && attempt < c.config.MaxRetries { resp.Body.Close() time.Sleep(c.config.RetryDelay * time.Duration(attempt+1)) continue } break } defer resp.Body.Close() // Handle error responses if resp.StatusCode >= 400 { c.analytics.recordError(fmt.Sprintf("HTTP %d", resp.StatusCode)) return c.handleHTTPError(resp) } // Parse response if result is provided if result != nil { if err := json.NewDecoder(resp.Body).Decode(result); err != nil { return fmt.Errorf("failed to decode response: %w", err) } } return nil } // Handle HTTP errors and convert to appropriate error types func (c *Client) handleHTTPError(resp *http.Response) error { body, _ := io.ReadAll(resp.Body) var errorResp struct { Error string `json:"error"` ErrorDetails []struct { Field string `json:"field"` Message string `json:"message"` Code string `json:"code"` } `json:"error_details"` } json.Unmarshal(body, &errorResp) message := errorResp.Error if message == "" { message = fmt.Sprintf("HTTP %d error", resp.StatusCode) } switch resp.StatusCode { case 400: return &ValidationError{Message: message, Details: errorResp.ErrorDetails} case 401: return &AuthenticationError{Message: message} case 404: return &NotFoundError{Message: message} case 429: retryAfter := resp.Header.Get("Retry-After") return &RateLimitError{Message: message, RetryAfter: retryAfter} case 500, 502, 503, 504: return &ServerError{Message: message, StatusCode: resp.StatusCode} default: return &APIError{Message: message, StatusCode: resp.StatusCode} } } // Utility functions func validatePath(path string) bool { return strings.HasPrefix(path, "/") && !strings.Contains(path, "//") } func normalizePath(path string) string { if !strings.HasPrefix(path, "/") { path = "/" + path } // Remove duplicate slashes for strings.Contains(path, "//") { path = strings.ReplaceAll(path, "//", "/") } return path } func (c *Client) invalidateCache(pattern string) { if c.cache == nil { return } c.cache.invalidatePattern(pattern) } func shouldRetry(statusCode int) bool { return statusCode == 429 || statusCode >= 500 }