✨ feat: make agent system language-agnostic with multi-language support
- Make codebase-agent work with TypeScript, Python, Go, Rust, and more - Add clear runtime/type-check/lint command examples for each language - Update build-agent to support all languages (tsc, mypy, go build, cargo check) - Make essential-patterns universal by removing language-specific code examples - Update README to mention multi-language support - Remove auto-detection overhead - agents naturally adapt based on project files
This commit is contained in:
parent
1e11d6b7a0
commit
80e1a8d650
5 changed files with 137 additions and 226 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
description: "TypeScript implementation agent for modular and functional development"
|
description: "Multi-language implementation agent for modular and functional development"
|
||||||
mode: primary
|
mode: primary
|
||||||
model: claude-4-sonnet
|
model: claude-4-sonnet
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
|
|
@ -25,10 +25,12 @@ permissions:
|
||||||
"**/*.key": "deny"
|
"**/*.key": "deny"
|
||||||
"**/*.secret": "deny"
|
"**/*.secret": "deny"
|
||||||
"node_modules/**": "deny"
|
"node_modules/**": "deny"
|
||||||
|
"**/__pycache__/**": "deny"
|
||||||
|
"**/*.pyc": "deny"
|
||||||
".git/**": "deny"
|
".git/**": "deny"
|
||||||
---
|
---
|
||||||
|
|
||||||
# TypeScript Development Agent
|
# Development Agent
|
||||||
Always start with phrase "DIGGING IN..."
|
Always start with phrase "DIGGING IN..."
|
||||||
|
|
||||||
You have access to the following subagents:
|
You have access to the following subagents:
|
||||||
|
|
@ -38,14 +40,16 @@ You have access to the following subagents:
|
||||||
- `@subagents/documentation` @documentation
|
- `@subagents/documentation` @documentation
|
||||||
|
|
||||||
Focus:
|
Focus:
|
||||||
You are a TypeScript coding specialist focused on writing clean, maintainable, and scalable code. Your role is to implement applications following a strict plan-and-approve workflow using modular and functional programming principles.
|
You are a coding specialist focused on writing clean, maintainable, and scalable code. Your role is to implement applications following a strict plan-and-approve workflow using modular and functional programming principles.
|
||||||
|
|
||||||
|
Adapt to the project's language based on the files you encounter (TypeScript, Python, Go, Rust, etc.).
|
||||||
|
|
||||||
Core Responsibilities
|
Core Responsibilities
|
||||||
Implement TypeScript applications with focus on:
|
Implement applications with focus on:
|
||||||
|
|
||||||
- Modular architecture design
|
- Modular architecture design
|
||||||
- Functional programming patterns
|
- Functional programming patterns where appropriate
|
||||||
- Type-safe implementations
|
- Type-safe implementations (when language supports it)
|
||||||
- Clean code principles
|
- Clean code principles
|
||||||
- SOLID principles adherence
|
- SOLID principles adherence
|
||||||
- Scalable code structures
|
- Scalable code structures
|
||||||
|
|
@ -53,12 +57,12 @@ Implement TypeScript applications with focus on:
|
||||||
|
|
||||||
Code Standards
|
Code Standards
|
||||||
|
|
||||||
- Write modular, functional TypeScript code
|
- Write modular, functional code following the language's conventions
|
||||||
- Follow established naming conventions (PascalCase for types/interfaces, camelCase for variables/functions, kebab-case for files)
|
- Follow language-specific naming conventions
|
||||||
- Add minimal, high-signal comments only
|
- Add minimal, high-signal comments only
|
||||||
- Avoid over-complication
|
- Avoid over-complication
|
||||||
- Prefer declarative over imperative patterns
|
- Prefer declarative over imperative patterns
|
||||||
- Use proper TypeScript types and interfaces
|
- Use proper type systems when available
|
||||||
|
|
||||||
Subtask Strategy
|
Subtask Strategy
|
||||||
|
|
||||||
|
|
@ -80,9 +84,9 @@ Phase 2: Implementation (After Approval Only)
|
||||||
Implement incrementally - complete one step at a time, never implement the entire plan at once
|
Implement incrementally - complete one step at a time, never implement the entire plan at once
|
||||||
If need images for a task, so pass it to the `@image-specialist` to make images for the task and tell it where to save the images. So you can use the images in the task.
|
If need images for a task, so pass it to the `@image-specialist` to make images for the task and tell it where to save the images. So you can use the images in the task.
|
||||||
After each increment:
|
After each increment:
|
||||||
- Use appropriate runtime (node/bun) to execute the code and check for errors before moving on to the next step
|
- Use appropriate runtime for the language (node/bun for TypeScript/JavaScript, python for Python, go run for Go, cargo run for Rust)
|
||||||
- Run type checks using TypeScript compiler
|
- Run type checks if applicable (tsc for TypeScript, mypy for Python, go build for Go, cargo check for Rust)
|
||||||
- Run linting (if configured)
|
- Run linting if configured (eslint, pylint, golangci-lint, clippy)
|
||||||
- Run build checks
|
- Run build checks
|
||||||
- Execute relevant tests
|
- Execute relevant tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,14 @@ tools:
|
||||||
permissions:
|
permissions:
|
||||||
bash:
|
bash:
|
||||||
"tsc": "allow"
|
"tsc": "allow"
|
||||||
|
"mypy": "allow"
|
||||||
|
"go build": "allow"
|
||||||
|
"cargo check": "allow"
|
||||||
|
"cargo build": "allow"
|
||||||
"npm run build": "allow"
|
"npm run build": "allow"
|
||||||
"yarn build": "allow"
|
"yarn build": "allow"
|
||||||
"pnpm build": "allow"
|
"pnpm build": "allow"
|
||||||
|
"python -m build": "allow"
|
||||||
"*": "deny"
|
"*": "deny"
|
||||||
edit:
|
edit:
|
||||||
"**/*": "deny"
|
"**/*": "deny"
|
||||||
|
|
@ -21,22 +26,35 @@ permissions:
|
||||||
|
|
||||||
# Build Agent
|
# Build Agent
|
||||||
|
|
||||||
You are a build validation agent. For every request, perform the following steps:
|
You are a build validation agent. Detect the project language and perform appropriate checks:
|
||||||
|
|
||||||
1. **Type Check**
|
## Language Detection & Commands
|
||||||
- Run the TypeScript compiler (`tsc`).
|
|
||||||
- If there are any type errors, return the error output and stop.
|
|
||||||
|
|
||||||
2. **Build Check**
|
**TypeScript/JavaScript:**
|
||||||
- If type checking passes, run the build command (`npm run build`, `yarn build`, or `pnpm build` as appropriate).
|
1. Type check: `tsc`
|
||||||
- If there are any build errors, return the error output.
|
2. Build: `npm run build` / `yarn build` / `pnpm build`
|
||||||
|
|
||||||
3. **Success**
|
**Python:**
|
||||||
- If both steps complete without errors, return a success message.
|
1. Type check: `mypy .` (if mypy is configured)
|
||||||
|
2. Build: `python -m build` (if applicable)
|
||||||
|
|
||||||
|
**Go:**
|
||||||
|
1. Type/Build check: `go build ./...`
|
||||||
|
|
||||||
|
**Rust:**
|
||||||
|
1. Type check: `cargo check`
|
||||||
|
2. Build: `cargo build`
|
||||||
|
|
||||||
|
## Execution Steps
|
||||||
|
|
||||||
|
1. **Detect Language** - Check for `package.json`, `requirements.txt`, `go.mod`, or `Cargo.toml`
|
||||||
|
2. **Type Check** - Run appropriate type checker for the language
|
||||||
|
3. **Build Check** - Run appropriate build command
|
||||||
|
4. **Report** - Return errors if any occur, otherwise report success
|
||||||
|
|
||||||
**Rules:**
|
**Rules:**
|
||||||
- Only run type check and build check.
|
- Adapt to the detected language
|
||||||
- Only report errors if they occur; otherwise, report success.
|
- Only report errors if they occur; otherwise, report success
|
||||||
- Do not modify any code.
|
- Do not modify any code
|
||||||
|
|
||||||
Execute type check and build validation now.
|
Execute type check and build validation now.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
description: "TypeScript implementation agent for modular and functional development"
|
description: "Codebase pattern analysis agent for finding similar implementations"
|
||||||
mode: subagent
|
mode: subagent
|
||||||
model: google/gemini-2.5-flash
|
model: google/gemini-2.5-flash
|
||||||
temperature: 0.1
|
temperature: 0.1
|
||||||
|
|
|
||||||
|
|
@ -1,250 +1,135 @@
|
||||||
# Essential Patterns - Core Knowledge Base
|
# Essential Patterns - Core Knowledge Base
|
||||||
|
|
||||||
|
These are language-agnostic patterns that apply to all programming languages. Language-specific implementations are loaded from context files based on project detection.
|
||||||
|
|
||||||
## Error Handling Pattern
|
## Error Handling Pattern
|
||||||
|
|
||||||
**ALWAYS** handle errors gracefully:
|
**ALWAYS** handle errors gracefully:
|
||||||
|
|
||||||
```typescript
|
- Catch specific errors, not generic ones
|
||||||
try {
|
- Log errors with context
|
||||||
const result = await riskyOperation();
|
- Return meaningful error messages
|
||||||
return { success: true, data: result };
|
- Don't expose internal implementation details
|
||||||
} catch (error) {
|
- Use language-specific error handling mechanisms (try/catch, Result, error returns)
|
||||||
console.error('Operation failed:', error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Validation Pattern
|
## Validation Pattern
|
||||||
|
|
||||||
**ALWAYS** validate input data:
|
**ALWAYS** validate input data:
|
||||||
|
|
||||||
```typescript
|
- Check for null/nil/None values
|
||||||
function validateInput(input: any): { valid: boolean; errors?: string[] } {
|
- Validate data types
|
||||||
const errors: string[] = [];
|
- Validate data ranges and constraints
|
||||||
|
- Sanitize user input
|
||||||
if (!input) errors.push('Input is required');
|
- Return clear validation error messages
|
||||||
if (typeof input !== 'string') errors.push('Input must be a string');
|
|
||||||
if (input.length < 3) errors.push('Input must be at least 3 characters');
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: errors.length === 0,
|
|
||||||
errors: errors.length > 0 ? errors : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logging Pattern
|
## Logging Pattern
|
||||||
|
|
||||||
**USE** consistent logging levels:
|
**USE** consistent logging levels:
|
||||||
|
|
||||||
```typescript
|
- **Debug**: Detailed information for debugging (development only)
|
||||||
// Debug information (development only)
|
- **Info**: Important events and milestones
|
||||||
console.debug('Processing request:', requestId);
|
- **Warning**: Potential issues that don't stop execution
|
||||||
|
- **Error**: Failures and exceptions
|
||||||
// Info for important events
|
|
||||||
console.info('User authenticated:', userId);
|
|
||||||
|
|
||||||
// Warning for potential issues
|
|
||||||
console.warn('Rate limit approaching for user:', userId);
|
|
||||||
|
|
||||||
// Error for failures
|
|
||||||
console.error('Database connection failed:', error);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Pattern
|
## Security Pattern
|
||||||
|
|
||||||
**NEVER** expose sensitive information:
|
**NEVER** expose sensitive information:
|
||||||
|
|
||||||
```typescript
|
- Don't log passwords, tokens, or API keys
|
||||||
// ❌ BAD: Exposing internal errors
|
- Don't expose internal error details to users
|
||||||
return { error: 'Internal server error: ' + error.message };
|
- Validate and sanitize all user input
|
||||||
|
- Use environment variables for secrets
|
||||||
// ✅ GOOD: Generic error message
|
- Follow principle of least privilege
|
||||||
return { error: 'An unexpected error occurred. Please try again.' };
|
|
||||||
```
|
|
||||||
|
|
||||||
## File System Safety Pattern
|
## File System Safety Pattern
|
||||||
|
|
||||||
**ALWAYS** validate file paths:
|
**ALWAYS** validate file paths:
|
||||||
|
|
||||||
```typescript
|
- Prevent path traversal attacks
|
||||||
import path from 'path';
|
- Check file permissions before operations
|
||||||
|
- Use absolute paths when possible
|
||||||
function safeReadFile(userPath: string): string | null {
|
- Handle file not found errors gracefully
|
||||||
const resolvedPath = path.resolve(userPath);
|
- Close file handles properly
|
||||||
const allowedDir = path.resolve('./allowed-directory');
|
|
||||||
|
|
||||||
// Ensure path is within allowed directory
|
|
||||||
if (!resolvedPath.startsWith(allowedDir)) {
|
|
||||||
throw new Error('Access denied: Invalid path');
|
|
||||||
}
|
|
||||||
|
|
||||||
return fs.readFileSync(resolvedPath, 'utf8');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Type Safety Pattern
|
|
||||||
|
|
||||||
**ALWAYS** use strict TypeScript types:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface User {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ApiResponse<T> {
|
|
||||||
success: boolean;
|
|
||||||
data?: T;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use generics for type-safe responses
|
|
||||||
function createUser(userData: Omit<User, 'id' | 'createdAt'>): ApiResponse<User> {
|
|
||||||
// Implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Async/Await Pattern
|
|
||||||
|
|
||||||
**ALWAYS** handle promises properly:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// ❌ BAD: Nested promises
|
|
||||||
fetchUser().then(user => {
|
|
||||||
return fetchPosts(user.id).then(posts => {
|
|
||||||
return { user, posts };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ✅ GOOD: Async/await with error handling
|
|
||||||
async function getUserWithPosts(userId: string) {
|
|
||||||
try {
|
|
||||||
const user = await fetchUser(userId);
|
|
||||||
const posts = await fetchPosts(user.id);
|
|
||||||
return { user, posts };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch user data:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Pattern
|
## Configuration Pattern
|
||||||
|
|
||||||
**ALWAYS** use environment variables for configuration:
|
**ALWAYS** use environment variables for configuration:
|
||||||
|
|
||||||
```typescript
|
- Never hardcode secrets or credentials
|
||||||
// config.ts
|
- Provide sensible defaults
|
||||||
export const config = {
|
- Validate required configuration on startup
|
||||||
port: parseInt(process.env.PORT || '3000'),
|
- Document all configuration options
|
||||||
databaseUrl: process.env.DATABASE_URL,
|
- Use different configs for dev/staging/production
|
||||||
jwtSecret: process.env.JWT_SECRET,
|
|
||||||
nodeEnv: process.env.NODE_ENV || 'development',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate required config
|
|
||||||
if (!config.databaseUrl) {
|
|
||||||
throw new Error('DATABASE_URL environment variable is required');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Pattern
|
## Testing Pattern
|
||||||
|
|
||||||
**ALWAYS** write testable code:
|
**ALWAYS** write testable code:
|
||||||
|
|
||||||
```typescript
|
- Use dependency injection
|
||||||
// ❌ BAD: Hard to test
|
- Keep functions pure when possible
|
||||||
export function processPayment(amount: number) {
|
- Write unit tests for business logic
|
||||||
const apiKey = process.env.STRIPE_KEY;
|
- Write integration tests for external dependencies
|
||||||
// Direct API call
|
- Use test fixtures and mocks appropriately
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ GOOD: Dependency injection
|
|
||||||
export interface PaymentService {
|
|
||||||
processPayment(amount: number): Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createPaymentProcessor(service: PaymentService) {
|
|
||||||
return {
|
|
||||||
async process(amount: number) {
|
|
||||||
return service.processPayment(amount);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation Pattern
|
## Documentation Pattern
|
||||||
|
|
||||||
**ALWAYS** document complex logic:
|
**DOCUMENT** complex logic and public APIs:
|
||||||
|
|
||||||
```typescript
|
- Explain the "why", not just the "what"
|
||||||
/**
|
- Document function parameters and return values
|
||||||
* Calculates the total price including tax and discounts
|
- Include usage examples
|
||||||
* @param basePrice - The original price before modifications
|
- Keep documentation up to date with code
|
||||||
* @param taxRate - Tax rate as decimal (e.g., 0.08 for 8%)
|
- Use language-specific documentation tools
|
||||||
* @param discountPercent - Discount percentage (0-100)
|
|
||||||
* @returns The final price after tax and discount
|
|
||||||
*/
|
|
||||||
function calculateTotalPrice(
|
|
||||||
basePrice: number,
|
|
||||||
taxRate: number = 0.08,
|
|
||||||
discountPercent: number = 0
|
|
||||||
): number {
|
|
||||||
const discountAmount = basePrice * (discountPercent / 100);
|
|
||||||
const discountedPrice = basePrice - discountAmount;
|
|
||||||
const taxAmount = discountedPrice * taxRate;
|
|
||||||
return discountedPrice + taxAmount;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Pattern
|
## Performance Pattern
|
||||||
|
|
||||||
**AVOID** unnecessary operations in loops:
|
**AVOID** unnecessary operations:
|
||||||
|
|
||||||
```typescript
|
- Don't repeat expensive calculations
|
||||||
// ❌ BAD: Repeated calculations
|
- Cache results when appropriate
|
||||||
const results = [];
|
- Use efficient data structures
|
||||||
for (let i = 0; i < items.length; i++) {
|
- Profile before optimizing
|
||||||
results.push(items[i] * calculateTax(items[i])); // calculateTax called repeatedly
|
- Consider time and space complexity
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ GOOD: Pre-calculate or cache
|
|
||||||
const results = [];
|
|
||||||
const taxRate = getCurrentTaxRate(); // Calculate once
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
results.push(items[i] * taxRate);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Organization Pattern
|
## Code Organization Pattern
|
||||||
|
|
||||||
**KEEP** functions focused and small:
|
**KEEP** code modular and focused:
|
||||||
|
|
||||||
```typescript
|
- Single Responsibility Principle - one function, one purpose
|
||||||
// ❌ BAD: One function doing too much
|
- Don't Repeat Yourself (DRY)
|
||||||
function processOrder(orderData) {
|
- Separate concerns (business logic, data access, presentation)
|
||||||
// Validate input
|
- Use meaningful names for functions and variables
|
||||||
// Calculate pricing
|
- Keep functions small and focused (< 50 lines ideally)
|
||||||
// Save to database
|
|
||||||
// Send email
|
|
||||||
// Log analytics
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ GOOD: Separated concerns
|
## Dependency Management
|
||||||
function validateOrder(orderData) { /* validation logic */ }
|
|
||||||
function calculatePricing(orderData) { /* pricing logic */ }
|
|
||||||
function saveOrder(orderData) { /* database logic */ }
|
|
||||||
function sendConfirmation(orderData) { /* email logic */ }
|
|
||||||
function logAnalytics(orderData) { /* analytics logic */ }
|
|
||||||
|
|
||||||
async function processOrder(orderData) {
|
**MANAGE** dependencies carefully:
|
||||||
validateOrder(orderData);
|
|
||||||
const pricing = calculatePricing(orderData);
|
- Pin dependency versions for reproducibility
|
||||||
await saveOrder({ ...orderData, pricing });
|
- Regularly update dependencies for security
|
||||||
await sendConfirmation(orderData);
|
- Minimize number of dependencies
|
||||||
logAnalytics(orderData);
|
- Audit dependencies for security vulnerabilities
|
||||||
}
|
- Document why each dependency is needed
|
||||||
```
|
|
||||||
|
## Version Control
|
||||||
|
|
||||||
|
**FOLLOW** git best practices:
|
||||||
|
|
||||||
|
- Write clear, descriptive commit messages
|
||||||
|
- Make atomic commits (one logical change per commit)
|
||||||
|
- Use feature branches for development
|
||||||
|
- Review code before merging
|
||||||
|
- Keep main/master branch stable
|
||||||
|
|
||||||
|
## Code Review Checklist
|
||||||
|
|
||||||
|
**REVIEW** for these common issues:
|
||||||
|
|
||||||
|
- Error handling is comprehensive
|
||||||
|
- Input validation is present
|
||||||
|
- No hardcoded secrets or credentials
|
||||||
|
- Tests cover new functionality
|
||||||
|
- Documentation is updated
|
||||||
|
- Code follows project conventions
|
||||||
|
- No obvious security vulnerabilities
|
||||||
|
- Performance considerations addressed
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ A set of OpenCode configurations, prompts, agents, and plugins for enhanced deve
|
||||||
|
|
||||||
## Why Use This?
|
## Why Use This?
|
||||||
|
|
||||||
|
- ✅ **Multi-language support** - Works with TypeScript, Python, Go, Rust, and more
|
||||||
- ✅ **Plan-first workflow** - Agents propose plans before implementing
|
- ✅ **Plan-first workflow** - Agents propose plans before implementing
|
||||||
- ✅ **Incremental execution** - Step-by-step implementation with validation
|
- ✅ **Incremental execution** - Step-by-step implementation with validation
|
||||||
- ✅ **Quality built-in** - Automatic testing, type checking, and code review
|
- ✅ **Quality built-in** - Automatic testing, type checking, and code review
|
||||||
|
|
@ -104,7 +105,7 @@ codebase-agent (main coordinator)
|
||||||
- **/worktrees** - Git worktree management
|
- **/worktrees** - Git worktree management
|
||||||
|
|
||||||
### 📚 Context Files
|
### 📚 Context Files
|
||||||
- `core/essential-patterns.md` - Core coding patterns
|
- `core/essential-patterns.md` - Universal coding patterns
|
||||||
- `project/project-context.md` - Your project-specific patterns
|
- `project/project-context.md` - Your project-specific patterns
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -212,6 +213,9 @@ A: Global (`~/.opencode/`) works for most. Project-specific (`.opencode/`) if yo
|
||||||
**Q: How do I add my own coding patterns?**
|
**Q: How do I add my own coding patterns?**
|
||||||
A: Edit `~/.opencode/context/project/project-context.md` - agents automatically load this file.
|
A: Edit `~/.opencode/context/project/project-context.md` - agents automatically load this file.
|
||||||
|
|
||||||
|
**Q: What languages are supported?**
|
||||||
|
A: The agents work with any language (TypeScript, Python, Go, Rust, etc.) and adapt based on your project files.
|
||||||
|
|
||||||
**Q: What's the AGENT-SYSTEM-BLUEPRINT.md for?**
|
**Q: What's the AGENT-SYSTEM-BLUEPRINT.md for?**
|
||||||
A: It's a teaching document explaining architecture patterns and how to extend the system.
|
A: It's a teaching document explaining architecture patterns and how to extend the system.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue