Skip to content

ADR-06: Unified Framework Definition System

🇰🇷 한국어 버전

DateAuthorRepos
2025-12-23@KubrickCodecore

Status: Accepted

Context

Problem Statement

The original architecture used a dual registry pattern where framework components were split across separate registries:

  1. Matchers Registry: Stored framework detection rules
  2. Strategies Registry: Stored test file parsers

This separation caused several issues:

  • Synchronization burden: Adding a new framework required modifications in multiple places
  • Registration fragility: Easy to register a matcher without a corresponding parser (or vice versa)
  • Scattered definitions: Framework behavior was spread across multiple files and packages
  • Testing complexity: Mocking required coordinating multiple registries

Requirements

  1. Single registration point: One place to define everything about a framework
  2. Self-contained definition: All framework components bundled together
  3. Type safety: Compile-time verification of complete framework definitions
  4. Extensibility: Easy to add new frameworks with minimal boilerplate

Strategic Question

How should framework components (detection, configuration parsing, test parsing) be organized to minimize coupling and maintenance burden?

Decision

Consolidate all framework components into a single framework.Definition type with a unified registry.

Each framework provides one Definition struct that bundles:

  • Framework identity (name, supported languages)
  • Detection rules (matchers)
  • Configuration parser
  • Test file parser
  • Priority for detection ordering

Options Considered

Option A: Unified Definition (Selected)

Single struct type containing all framework components.

Pros:

  • Single file per framework: Complete framework definition in one definition.go
  • Self-documenting: All framework behavior visible in one place
  • Compile-time complete: Missing components cause compilation errors
  • Simple registration: Single framework.Register() call in init()
  • Easy testing: Mock entire framework with one struct

Cons:

  • Larger struct size (contains all components)
  • All components must be defined (no partial frameworks)

Option B: Dual Registry (Original)

Separate registries for matchers and parsers.

Pros:

  • Fine-grained control over individual components
  • Potentially smaller memory footprint per registry

Cons:

  • Synchronization required: Must update both registries for each framework
  • Easy to forget: Registration in one registry without the other
  • Scattered code: Framework logic spread across packages
  • Harder to test: Must coordinate multiple mock registries

Option C: Plugin System

Dynamic plugin loading at runtime.

Pros:

  • Maximum flexibility for adding frameworks
  • No recompilation needed for new frameworks

Cons:

  • Complexity: Plugin discovery, loading, and lifecycle management
  • Type safety loss: Runtime errors instead of compile-time
  • Deployment burden: Manage separate plugin binaries
  • Overkill: Static registration sufficient for current needs

Implementation Details

Definition Structure

go
type Definition struct {
    // Framework identity
    Name      string
    Languages []domain.Language

    // Detection components
    Matchers []Matcher

    // Configuration parsing (optional)
    ConfigParser ConfigParser

    // Test file parsing
    Parser Parser

    // Priority for detection ordering
    Priority int
}

Core Interfaces

go
// Matcher evaluates detection signals
type Matcher interface {
    Match(ctx context.Context, signal Signal) MatchResult
}

// ConfigParser extracts settings from framework config files
type ConfigParser interface {
    Parse(ctx context.Context, configPath string, content []byte) (*ConfigScope, error)
}

// Parser extracts test definitions from source code
type Parser interface {
    Parse(ctx context.Context, source []byte, filename string) (*domain.TestFile, error)
}

Registration Pattern

Each framework registers via init():

go
// pkg/parser/strategies/jest/definition.go
func init() {
    framework.Register(NewDefinition())
}

func NewDefinition() *framework.Definition {
    return &framework.Definition{
        Name:      "jest",
        Languages: []domain.Language{domain.LanguageTypeScript, domain.LanguageJavaScript},
        Matchers: []framework.Matcher{
            matchers.NewImportMatcher("@jest/globals", "@jest/", "jest"),
            matchers.NewConfigMatcher("jest.config.js", "jest.config.ts"),
            &JestContentMatcher{},
        },
        ConfigParser: &JestConfigParser{},
        Parser:       &JestParser{},
        Priority:     framework.PriorityGeneric,
    }
}

Critical: Blank import is required to trigger init():

go
import (
    _ "github.com/specvital/core/pkg/parser/strategies/jest"
)

Registry Architecture

pkg/parser/
├── framework/
│   ├── definition.go     # Definition type and interfaces
│   ├── registry.go       # Single unified registry
│   ├── scope.go          # ConfigScope for config file handling
│   ├── constants.go      # Priority levels, framework names
│   └── matchers/         # Reusable matcher implementations
│       ├── import.go     # Import statement matcher
│       ├── config.go     # Config file matcher
│       └── content.go    # Content pattern matcher
└── strategies/
    ├── jest/definition.go
    ├── vitest/definition.go
    ├── playwright/definition.go
    └── gotesting/definition.go

Priority System

go
const (
    PriorityGeneric     = 100  // Jest, Go testing
    PriorityE2E         = 150  // Playwright, Cypress
    PrioritySpecialized = 200  // Vitest (needs explicit import detection)
)

Higher priority frameworks are evaluated first during detection.

Consequences

Positive

  1. Single Source of Truth

    • All framework behavior defined in one file
    • No cross-file coordination required
    • Clear ownership and responsibility
  2. Reduced Boilerplate

    • New framework requires only one definition.go file
    • Reusable matcher components (matchers/ package)
    • Shared parsing utilities (shared/jstest/, etc.)
  3. Improved Maintainability

    • Framework changes localized to single file
    • Compile-time verification of completeness
    • Self-documenting structure
  4. Better Testability

    • Mock entire framework with single struct
    • Test detection, config parsing, and test parsing together
    • Isolated unit tests per framework

Negative

  1. Blank Import Requirement

    • Consumers must explicitly import each framework package
    • Missing import causes silent framework exclusion
    • Mitigation: Document required imports in README; consider registry validation
  2. Global State Dependency

    • defaultRegistry is a package-level variable
    • init() order matters for registration
    • Mitigation: Go guarantees init() runs before main(); registry is thread-safe
  3. All-or-Nothing Definition

    • Cannot register partial framework (e.g., matcher only)
    • Mitigation: This is intentional; partial frameworks cause bugs in dual registry

Trade-off Summary

AspectUnified DefinitionDual Registry
RegistrationSingle callMultiple calls
File organizationOne file/frameworkMultiple files
Component couplingHigh (intentional)Low
Maintenance burdenLowHigh
Type safetyCompile-timeRuntime potential

References

Open-source test coverage insights