Guide for extracting existing QNTX features into domain plugins.
Extract a feature into a plugin when:
✅ Domain Cohesion: Feature belongs to a distinct functional domain (code, finance, biotech, legal) ✅ Independent Evolution: Feature needs to evolve separately from core QNTX ✅ Third-Party Use Case: External developers might want to customize/replace this domain ✅ Size: Feature is substantial enough to justify plugin overhead (>500 LOC, multiple files)
Do not extract when:
❌ Core Infrastructure: Feature is fundamental to QNTX (attestation system, database, Ax query) ❌ Cross-Cutting: Feature is used by multiple domains (logger, config) ❌ Too Small: Feature is a single function/utility (creates unnecessary overhead)
Move code to domains/<name>/ but keep it built-in:
Before: After:
code/ domains/code/
├── github/ → ├── vcs/github/
├── gopls/ → ├── langserver/gopls/
└── ast/ → ├── ast/
└── plugin.go (new)
Benefits:
Extract to separate repository/binary:
QNTX Repository: External Plugin Repository:
main main
├── domains/ └── qntx-code-plugin/
│ └── grpc/ ├── main.go (gRPC server)
│ └── protocol/ ├── plugin.go (DomainPlugin impl)
│ └── domain.proto └── go.mod
└── cmd/qntx/
Benefits:
The code domain migration (PR #130) demonstrates the internal plugin phase:
Before (scattered across codebase):
code/
├── github/ # GitHub PR integration
├── gopls/ # Go language server
├── ast/ # AST transformations
└── ixgest/git/ # Git ingestion (was in ixgest/git/)
cmd/qntx/commands/
├── code.go # CLI commands
└── ixgest_git.go
server/
├── code_handler.go # HTTP handlers
└── gopls_handler.go
After (cohesive plugin):
domains/code/
├── plugin.go # DomainPlugin implementation
├── commands.go # CLI command builders
├── handlers.go # HTTP handlers
├── vcs/github/ # GitHub integration
├── langserver/gopls/ # gopls language server
├── ast/ # AST utilities
└── ixgest/git/ # Git repository ingestion
plugin.go implementing DomainPlugincmd/ to plugin.Commands()server/ to plugin.RegisterHTTP()Initialize() instead of package-level initqntx code ix git <repo> (identical)/api/code/github/pr (identical)am.code.* settings (identical)Determine what belongs in the plugin:
Domain: finance
Includes:
✅ finance/stocks/ # Stock price ingestion
✅ finance/analysis/ # Financial analysis
✅ finance/reporting/ # Report generation
Excludes:
❌ ats/ # Core attestation system (used by all domains)
❌ pulse/ # Job system (infrastructure)
mkdir -p domains/finance
touch domains/finance/plugin.go
domains/finance/plugin.go:
package finance
import (
"context"
"net/http"
"github.com/spf13/cobra"
"github.com/teranos/QNTX/plugin"
)
type Plugin struct {
services domains.ServiceRegistry
}
func NewPlugin() *Plugin {
return &Plugin{}
}
func (p *Plugin) Metadata() domains.Metadata {
return domains.Metadata{
Name: "finance",
Version: "0.1.0",
QNTXVersion: ">= 0.1.0",
Description: "Financial analysis and reporting domain",
Author: "Your Organization",
License: "MIT",
}
}
func (p *Plugin) Initialize(ctx context.Context, services domains.ServiceRegistry) error {
p.services = services
logger := services.Logger("finance")
logger.Info("Finance domain plugin initialized")
return nil
}
func (p *Plugin) Shutdown(ctx context.Context) error {
if p.services != nil {
logger := p.services.Logger("finance")
logger.Info("Finance domain plugin shutting down")
}
return nil
}
func (p *Plugin) Commands() []*cobra.Command {
// TODO: Implement
return nil
}
func (p *Plugin) RegisterHTTP(mux *http.ServeMux) error {
// TODO: Implement
return nil
}
func (p *Plugin) RegisterWebSocket() (map[string]domains.WebSocketHandler, error) {
return nil, nil
}
func (p *Plugin) Health(ctx context.Context) domains.HealthStatus {
return domains.HealthStatus{
Healthy: true,
Message: "Finance domain operational",
Details: make(map[string]interface{}),
}
}
# Move existing code
mv finance/ domains/finance/analysis/
mv cmd/qntx/commands/finance.go domains/finance/commands.go
mv server/finance_handler.go domains/finance/handlers.go
Update import paths:
// Before
import "github.com/teranos/QNTX/finance/analysis"
// After
import "github.com/teranos/QNTX/plugin/finance/analysis"
domains/finance/commands.go:
func (p *Plugin) Commands() []*cobra.Command {
financeCmd := &cobra.Command{
Use: "finance",
Short: "Financial analysis tools",
}
financeCmd.AddCommand(&cobra.Command{
Use: "analyze <company>",
Short: "Analyze company financials",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return p.analyzeCompany(args[0])
},
})
return []*cobra.Command{financeCmd}
}
domains/finance/handlers.go:
func (p *Plugin) RegisterHTTP(mux *http.ServeMux) error {
mux.HandleFunc("/api/finance/stocks", p.handleStocks)
mux.HandleFunc("/api/finance/reports/", p.handleReports)
return nil
}
func (p *Plugin) handleStocks(w http.ResponseWriter, r *http.Request) {
logger := p.services.Logger("finance")
stocks, err := p.fetchStockData()
if err != nil {
logger.Errorw("Failed to fetch stocks", "error", err)
http.Error(w, "Failed to fetch stocks", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(stocks)
}
cmd/qntx/main.go:
import "github.com/teranos/QNTX/plugin/finance"
func initializePluginRegistry() {
registry := domains.NewRegistry("0.1.0")
domains.SetDefaultRegistry(registry)
// Register built-in plugins
registry.Register(code.NewPlugin())
registry.Register(finance.NewPlugin()) // Add new plugin
}
~/.qntx/am.finance.toml:
# Finance domain configuration
# API configuration
api.key = "${FINANCE_API_KEY}" # Read from env
api.endpoint = "https://api.example.com"
# Analysis settings
analysis.update_interval_minutes = 60
analysis.cache_results = true
# Build
make
# Test CLI
./bin/qntx finance analyze AAPL
# Test HTTP (with server running)
curl http://localhost:877/api/finance/stocks
# Test initialization
./bin/qntx server
# Should see: "Finance domain plugin initialized"
Test plugin in isolation:
// domains/finance/plugin_test.go
func TestFinancePlugin_Initialize(t *testing.T) {
db := qntxtest.CreateTestDB(t)
logger := zaptest.NewLogger(t).Sugar()
store := storage.NewSQLStore(db, logger)
config := &mockConfig{}
services := domains.NewServiceRegistry(db, logger, store, config, queue)
plugin := NewPlugin()
err := plugin.Initialize(context.Background(), services)
assert.NoError(t, err)
assert.NotNil(t, plugin.services)
}
Test plugin with QNTX server:
// server/server_test.go
func TestServer_WithFinancePlugin(t *testing.T) {
db := qntxtest.CreateTestDB(t)
server, err := NewQNTXServer(db, "test.db", 0)
require.NoError(t, err)
// Verify plugin loaded
registry := domains.GetDefaultRegistry()
plugin, ok := registry.Get("finance")
assert.True(t, ok)
assert.NotNil(t, plugin)
}
Test HTTP endpoints:
func TestFinancePlugin_StocksEndpoint(t *testing.T) {
// Create test server with plugin
mux := http.NewServeMux()
plugin := NewPlugin()
plugin.Initialize(ctx, services)
plugin.RegisterHTTP(mux)
// Test request
req := httptest.NewRequest("GET", "/api/finance/stocks", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
If migration causes issues:
During migration, temporarily keep old code:
domains/finance/ # New plugin code
legacy/finance/ # Old code (temporary)
Build flags to toggle:
//go:build !use_finance_plugin
// Use legacy finance code
Migration should be in single PR:
git revert <migration-commit>
git push
Make plugin optional:
# am.toml
[plugins]
finance.enabled = false # Disable plugin, use legacy code
✅ Atomic Migration: Migrate entire domain at once (don't split across PRs) ✅ Backward Compatibility: Maintain same CLI/HTTP interfaces ✅ Comprehensive Tests: Test all plugin entry points ✅ Configuration Migration: Document config changes in migration guide ✅ Gradual Rollout: Test internally before external release
After successful internal plugin migration: