Status: Concept / Work In Progress Created: 2025-10-15 Specification: specs/005-consider-research-methodlogies/spec.md
Enable generation of disposable applications while maintaining professional engineering practices through language-portable, high-level testing that matches the flexibility of spec-kit's markdown-driven development.
Create a brutally consistent interface to system functionality that enables:
- Writing tests once, running against multiple language implementations
- Comparing correctness and performance across implementations
- Swapping languages/frameworks while preserving test suites
- Liberation from strict TDD through higher-level behavioral testing
Traditional TDD couples tests to implementation details:
- Unit tests are language-specific
- Integration tests depend on framework choices
- Changing languages requires rewriting entire test suites
- Testing becomes a barrier to experimentation
In the age of AI-generated code and spec-driven development, the specification is the source of truth, not the implementation. We need tests that reflect this reality.
Every language implementation provides an adapter - a thin entry point that:
- Accepts operation names and parameters in standard format (JSON)
- Routes to system functionality
- Returns results in standard format
- Follows tc's existing test runner contract (stdin/stdout)
Example conceptual interface:
# Input (JSON via stdin):
{
"operation": "user.create",
"params": {
"email": "user@example.com",
"name": "Test User"
}
}
# Output (JSON via stdout):
{
"result": {
"id": "user-123",
"email": "user@example.com",
"name": "Test User",
"created_at": "2025-10-15T00:00:00Z"
}
}
Tests define system behavior at operation level:
tests/user/create/
├── data/
│ ├── valid-email/
│ │ ├── input.json # {"operation": "user.create", "params": {...}}
│ │ └── expected.json # {"result": {...}}
│ └── invalid-email/
│ ├── input.json
│ └── expected.json # {"error": "Invalid email"}
└── run # Adapter (different per language)
Synchronous (immediate response):
// Input
{"operation": "math.add", "params": {"a": 5, "b": 3}}
// Output
{"result": 8}Asynchronous (deferred response):
// Input
{"operation": "email.send", "params": {"to": "...", "subject": "..."}}
// Output (immediate)
{"correlation_id": "async-job-123", "status": "pending"}
// Poll/retrieve result later
{"operation": "async.result", "params": {"correlation_id": "async-job-123"}}
// Final result
{"result": {"sent": true, "message_id": "msg-456"}, "status": "completed"}Primary: Configuration File (adapters.json)
{
"adapters": [
{
"name": "ruby",
"language": "ruby",
"version": "3.2.0",
"path": "./adapters/ruby/run",
"capabilities": ["sync", "async"]
},
{
"name": "go",
"language": "go",
"version": "1.21",
"path": "./adapters/go/bin/adapter",
"capabilities": ["sync", "async"]
}
]
}Override: Environment Variable
TC_ADAPTERS=./adapters/rust tc testsRationale:
- Config file is git-friendly and version controlled
- Explicit registration prevents accidental execution
- Supports rich metadata naturally
- Environment variable enables CI/CD flexibility
Run same test suite against multiple adapters:
tc tests --adapters ruby,go,python --compareOutput:
🚁 Comparison Report
Suite: tests/user/create
┌──────────┬────────┬────────┬──────────┐
│ Adapter │ Passed │ Failed │ Avg Time │
├──────────┼────────┼────────┼──────────┤
│ ruby │ 10 │ 0 │ 145ms │
│ go │ 10 │ 0 │ 12ms │
│ python │ 9 │ 1 │ 98ms │
└──────────┴────────┴────────┴──────────┘
⚠️ Discrepancy: python failed 'invalid-email' test
tc already has the foundation:
- Test runner contract: Executables accept input.json, output to stdout
- Directory-based suites:
suite/data/scenario/input.json+expected.json - Language-agnostic harness: Shell script test runner
Extensions needed:
- Operation-based input format (not just arbitrary JSON)
- Adapter discovery and selection
- Async operation support (correlation IDs, polling)
- Multi-adapter execution and comparison reporting
spec-kit generates system implementations from markdown specs. This pattern enables:
- Write specification (markdown) → spec-kit generates system
- Generate adapter (thin wrapper following pattern)
- Write tests (high-level operations, not implementation details)
- Iterate freely:
- Change implementation language → regenerate + new adapter → same tests
- Swap frameworks → same tests verify correctness
- Compare alternatives → parallel testing shows performance differences
The specification remains the source of truth, implementations become disposable.
Specification (spec-kit markdown):
## User Registration
System allows users to register with email and password.
- User provides email and password
- System validates email format
- System hashes password
- System creates user record
- System returns user IDTest Suite (language-agnostic):
tests/user/register/
├── data/
│ ├── valid-registration/
│ │ ├── input.json: {"operation": "user.register", "params": {"email": "test@example.com", "password": "secret123"}}
│ │ └── expected.json: {"result": {"id": "<uuid>", "email": "test@example.com"}}
│ └── invalid-email/
│ ├── input.json: {"operation": "user.register", "params": {"email": "not-an-email", "password": "secret123"}}
│ └── expected.json: {"error": "Invalid email format"}
└── run → (symlink to current adapter)
Adapters (multiple languages):
# adapters/ruby/run
require 'json'
require_relative '../../src/ruby/app'
input = JSON.parse(STDIN.read)
operation = input['operation']
params = input['params']
result = case operation
when 'user.register'
App::UserService.register(params['email'], params['password'])
else
{error: "Unknown operation: #{operation}"}
end
puts JSON.generate(result)// adapters/go/main.go
package main
import (
"encoding/json"
"os"
"myapp/services"
)
type Request struct {
Operation string `json:"operation"`
Params map[string]interface{} `json:"params"`
}
func main() {
var req Request
json.NewDecoder(os.Stdin).Decode(&req)
var result interface{}
switch req.Operation {
case "user.register":
result = services.RegisterUser(req.Params)
default:
result = map[string]string{"error": "Unknown operation"}
}
json.NewEncoder(os.Stdout).Encode(result)
}Run tests:
# Test Ruby implementation
tc tests/user/register --adapter ruby
# Test Go implementation
tc tests/user/register --adapter go
# Compare both
tc tests/user/register --adapters ruby,go --compare- Experiment freely: Try different languages without test penalty
- Validate correctness: Comparison testing reveals implementation bugs
- Optimize performance: Compare implementations empirically
- Preserve knowledge: Tests capture business logic, not implementation
- Higher abstraction: Test behavior, not implementation details
- Better durability: Tests outlive any single implementation
- Faster iteration: Change implementation without changing tests
- Clearer intent: Operation-based tests read like specifications
- Polyglot teams: Share test suites across language boundaries
- AI generation: Generate implementations freely, tests validate correctness
- Technical debt: Retire old implementations without losing test coverage
- Knowledge transfer: Tests document system behavior independently of code
- Define adapter interface contract
- Extend tc to support operation-based input format
- Implement adapter discovery (config file + env var override)
- Basic adapter execution and test running
- Deliverable: Can run same tests against 2+ language implementations
- Correlation ID generation and tracking
- Result polling mechanism
- Timeout handling for async operations
- Deliverable: Can test async operations (email, background jobs, etc.)
- Multi-adapter parallel execution
- Result aggregation and comparison
- Performance metrics collection
- Discrepancy highlighting
- Deliverable: Comparison reports showing correctness and performance differences
- Adapter scaffolding for new languages
- Documentation and examples
- Integration with spec-kit workflow
- Deliverable: Developer can add new language adapter in under 4 hours
- Brutal Consistency: Same interface, every language, every time
- Zero Magic: Explicit configuration over convention (when it matters)
- Unix Philosophy: Text streams, composability, one thing well
- Spec First: Specifications are source of truth, code is disposable
- Fail Visible: Comparison testing surfaces implementation differences immediately
- Git Friendly: Configuration and tests version controlled together
- CI/CD Ready: Environment variables override for build pipelines
- Serialization: JSON only (universal language support)
- Transport: stdin/stdout (follows tc's runner contract)
- Async Model: Polling with correlation IDs (no callbacks/websockets)
- State: Tests are stateless by default
- Execution: Local only (no distributed testing)
- Concurrency: CPU-bound parallelism (process-level)
- State management: How should adapters handle stateful operations? (Database setup/teardown, test fixtures)
- Authentication: How to handle auth-required operations in tests? (Test credentials, mocking)
- External dependencies: Mocking strategy for third-party APIs?
- Error taxonomy: Standard error format beyond {"error": "message"}?
- Operation discovery: Should adapters advertise available operations?
- Versioning: How to handle API version differences between implementations?
- Hexagonal Architecture (Ports & Adapters): System core isolated from external concerns
- Clean Architecture: Framework independence through dependency inversion
- Contract Testing: Verify implementations match agreed interface
- Adapter Pattern: Translate between incompatible interfaces
- Strategy Pattern: Swap implementations at runtime
- tc framework: Language-agnostic testing via directory structure and test runner contract
- spec-kit: Markdown-driven system generation with AI
- Cucumber/Gherkin: Behavior-driven testing with natural language specs
- JSON-RPC: Standard protocol for remote procedure calls with JSON
- GraphQL: Uniform API across different backend implementations
- WASM (WebAssembly): Run code in multiple languages with common interface
From spec.md Success Criteria:
- Write test suite once, run against 3+ language implementations ✓
- New adapter creation < 4 hours for experienced developer
- Execute 100 test scenarios in < 30 seconds (excluding operation time)
- Comparison testing identifies discrepancies correctly
- 90% of operations testable via synchronous interface
- Async operations complete within timeout 99% of time
- Support 5+ languages (Ruby, Python, Node, Go, Rust)
- Reports show clear performance differences (min/max/avg)
Disposable Applications:
# Monday: Generate Ruby app from spec
speckit generate --lang ruby
# Tuesday: Realize Go would be faster
speckit generate --lang go
# Wednesday: Run tests against both
tc tests --adapters ruby,go --compare
# Thursday: Go is 10x faster, retire Ruby
git rm -rf src/rubyThe tests never changed. The specification didn't change. Only the implementation changed.
This is the future of spec-driven development.
Next Steps:
- Prototype adapter interface with 2 languages (Ruby + Go)
- Extend tc to support operation-based routing
- Implement adapter discovery mechanism
- Build comparison testing harness
- Document adapter creation pattern
- Integrate with spec-kit workflow
See Also: