grokking simplicity and refactoring

This commit is contained in:
Geir Okkenhaug Jerstad 2025-06-16 13:43:21 +02:00
parent 89a7fe100d
commit fb4361d938
67 changed files with 6275 additions and 56 deletions

View file

@ -0,0 +1,846 @@
# Guile Scheme Coding Instructions for Home Lab Tool
## Functional Programming Principles
**Core Philosophy**: Functional programming is about actions, data, and computation - compose small, pure functions to build complex behaviors.
### 1. Pure Functions First
- Functions should be deterministic and side-effect free when possible
- Separate pure computation from I/O operations
- Use immutable data structures as default
```scheme
;; Good: Pure function
(define (calculate-deployment-hash config)
(sha256 (scm->json-string config)))
;; Better: Separate pure logic from I/O
(define (deployment-ready? machine-config current-state)
(and (eq? (assoc-ref machine-config 'status) 'configured)
(eq? (assoc-ref current-state 'connectivity) 'online)))
;; I/O operations separate
(define (check-machine-deployment machine)
(let ((config (load-machine-config machine))
(state (probe-machine-state machine)))
(deployment-ready? config state)))
```
### 2. Data-Driven Design
- Represent configurations and state as data structures
- Use association lists (alists) and vectors for structured data
- Leverage Guile's homoiconicity (code as data)
```scheme
;; Machine configuration as data
(define machine-specs
`((grey-area
(services (ollama jellyfin forgejo))
(deployment-method deploy-rs)
(backup-schedule weekly))
(sleeper-service
(services (nfs zfs monitoring))
(deployment-method hybrid-update)
(backup-schedule daily))))
;; Operations on data
(define (get-machine-services machine)
(assoc-ref (assoc-ref machine-specs machine) 'services))
(define (machines-with-service service)
(filter (lambda (machine-spec)
(member service (get-machine-services (car machine-spec))))
machine-specs))
```
## Guile-Specific Idioms
### 3. Module Organization
- Use meaningful module hierarchies
- Export only necessary public interfaces
- Group related functionality together
```scheme
;; File: modules/lab/machines.scm
(define-module (lab machines)
#:use-module (srfi srfi-1) ; List processing
#:use-module (srfi srfi-26) ; Cut/cute
#:use-module (ice-9 match) ; Pattern matching
#:use-module (ssh session)
#:export (machine-status
deploy-machine
list-machines
machine-services))
;; File: modules/lab/deployment.scm
(define-module (lab deployment)
#:use-module (lab machines)
#:use-module (json)
#:export (deploy-rs
hybrid-update
rollback-deployment))
```
### 4. Error Handling the Scheme Way
- Use exceptions for exceptional conditions
- Return #f or special values for expected failures
- Provide meaningful error context
```scheme
;; Use exceptions for programming errors
(define (deploy-machine machine method)
(unless (member machine (list-machines))
(throw 'invalid-machine "Unknown machine" machine))
(unless (member method '(deploy-rs hybrid-update legacy))
(throw 'invalid-method "Unknown deployment method" method))
;; ... deployment logic)
;; Return #f for expected failures
(define (machine-reachable? machine)
(catch #t
(lambda ()
(ssh-connect machine)
#t)
(lambda (key . args)
#f)))
;; Provide context with failure info
(define (deployment-result success? machine method details)
`((success . ,success?)
(machine . ,machine)
(method . ,method)
(timestamp . ,(current-time))
(details . ,details)))
```
### 5. Higher-Order Functions and Composition
- Use map, filter, fold for list processing
- Compose functions to build complex operations
- Leverage SRFI-1 for advanced list operations
```scheme
(use-modules (srfi srfi-1))
;; Functional composition
(define (healthy-machines machines)
(filter machine-reachable?
(filter (lambda (m) (not (maintenance-mode? m)))
machines)))
;; Map operations across machines
(define (update-all-machines)
(map (lambda (machine)
(cons machine (update-machine machine)))
(healthy-machines (list-machines))))
;; Fold for aggregation
(define (deployment-summary results)
(fold (lambda (result acc)
(if (assoc-ref result 'success)
(cons 'successful (1+ (assoc-ref acc 'successful)))
(cons 'failed (1+ (assoc-ref acc 'failed)))))
'((successful . 0) (failed . 0))
results))
```
### 6. Pattern Matching for Control Flow
- Use `match` for destructuring and dispatch
- Pattern match on data structures
- Cleaner than nested if/cond statements
```scheme
(use-modules (ice-9 match))
(define (handle-deployment-event event)
(match event
(('start machine method)
(log-info "Starting deployment of ~a using ~a" machine method))
(('progress machine percent)
(update-progress-bar machine percent))
(('success machine result)
(log-success "Deployment completed: ~a" machine)
(notify-success machine result))
(('error machine error-msg)
(log-error "Deployment failed: ~a - ~a" machine error-msg)
(initiate-rollback machine))
(_ (log-warning "Unknown event: ~a" event))))
;; Pattern matching for configuration parsing
(define (parse-machine-config config-sexp)
(match config-sexp
(('machine name ('services services ...) ('options options ...))
`((name . ,name)
(services . ,services)
(options . ,(alist->hash-table options))))
(_ (throw 'invalid-config "Malformed machine config" config-sexp))))
```
### 7. REPL-Driven Development
- Design for interactive development
- Provide introspection functions
- Make state queryable and modifiable
```scheme
;; REPL helpers for development
(define (debug-machine-state machine)
"Display comprehensive machine state for debugging"
(format #t "Machine: ~a~%" machine)
(format #t "Status: ~a~%" (machine-status machine))
(format #t "Services: ~a~%" (machine-services machine))
(format #t "Last deployment: ~a~%" (last-deployment machine))
(format #t "Reachable: ~a~%" (machine-reachable? machine)))
;; Interactive deployment with confirmation
(define (interactive-deploy machine)
(let ((current-config (get-machine-config machine)))
(display-config current-config)
(when (yes-or-no? "Proceed with deployment?")
(deploy-machine machine 'deploy-rs))))
;; State introspection
(define (lab-status)
`((total-machines . ,(length (list-machines)))
(reachable . ,(length (filter machine-reachable? (list-machines))))
(services-running . ,(total-running-services))
(pending-deployments . ,(length (pending-deployments)))))
```
### 8. Concurrency with Fibers
- Use fibers for concurrent operations
- Non-blocking I/O for better performance
- Coordinate parallel deployments safely
```scheme
(use-modules (fibers) (fibers channels))
;; Concurrent machine checking
(define (check-all-machines-concurrent machines)
(run-fibers
(lambda ()
(let ((results-channel (make-channel)))
;; Spawn fiber for each machine
(for-each (lambda (machine)
(spawn-fiber
(lambda ()
(let ((status (check-machine-status machine)))
(put-message results-channel
(cons machine status))))))
machines)
;; Collect results
(let loop ((remaining (length machines))
(results '()))
(if (zero? remaining)
results
(loop (1- remaining)
(cons (get-message results-channel) results))))))))
;; Parallel deployment with coordination
(define (deploy-machines-parallel machines)
(run-fibers
(lambda ()
(let ((deployment-channel (make-channel))
(coordinator (spawn-fiber (deployment-coordinator deployment-channel))))
(par-map (lambda (machine)
(deploy-with-coordination machine deployment-channel))
machines)))))
```
### 9. MCP Server Implementation Patterns
- Structured message handling
- Capability-based tool organization
- Resource management with caching
```scheme
;; MCP message dispatch
(define (handle-mcp-request request)
(match (json-ref request "method")
("tools/list"
(mcp-tools-list))
("tools/call"
(let ((tool (json-ref request "params" "name"))
(args (json-ref request "params" "arguments")))
(call-lab-tool tool args)))
("resources/list"
(mcp-resources-list))
("resources/read"
(let ((uri (json-ref request "params" "uri")))
(read-lab-resource uri)))
(method
(mcp-error -32601 "Method not found" method))))
;; Tool capability definition
(define lab-tools
`((deploy-machine
(description . "Deploy configuration to a specific machine")
(inputSchema . ,(json-schema
`((type . "object")
(properties . ((machine (type . "string"))
(method (type . "string")
(enum . ("deploy-rs" "hybrid-update")))))
(required . ("machine")))))
(handler . ,deploy-machine-tool))
(check-status
(description . "Check machine status and connectivity")
(inputSchema . ,(json-schema
`((type . "object")
(properties . ((machines (type . "array")
(items (type . "string"))))))))
(handler . ,check-status-tool))))
```
### 10. Configuration and Environment
- Use parameters for configuration
- Environment-aware defaults
- Validate configuration on startup
```scheme
;; Configuration parameters
(define lab-config-dir
(make-parameter (or (getenv "LAB_CONFIG_DIR")
"/etc/lab-tool")))
(define deployment-timeout
(make-parameter (string->number (or (getenv "DEPLOYMENT_TIMEOUT") "300"))))
(define ssh-key-path
(make-parameter (or (getenv "LAB_SSH_KEY")
(string-append (getenv "HOME") "/.ssh/lab_key"))))
;; Configuration validation
(define (validate-lab-config)
(unless (file-exists? (lab-config-dir))
(throw 'config-error "Lab config directory not found" (lab-config-dir)))
(unless (file-exists? (ssh-key-path))
(throw 'config-error "SSH key not found" (ssh-key-path)))
(unless (> (deployment-timeout) 0)
(throw 'config-error "Invalid deployment timeout" (deployment-timeout))))
;; Initialize with validation
(define (init-lab-tool)
(validate-lab-config)
(load-machine-configurations)
(initialize-ssh-agent)
(setup-logging))
```
## Code Style Guidelines
### 11. Naming Conventions
- Use kebab-case for variables and functions
- Predicates end with `?`
- Mutating procedures end with `!`
- Constants in ALL-CAPS with hyphens
```scheme
;; Good naming
(define DEFAULT-SSH-PORT 22)
(define machine-deployment-status ...)
(define (machine-reachable? machine) ...)
(define (update-machine-config! machine config) ...)
;; Avoid
(define defaultSSHPort 22) ; camelCase
(define machine_status ...) ; snake_case
(define (is-machine-reachable ...) ; unnecessary 'is-'
```
### 12. Documentation and Comments
- Document module purposes and exports
- Use docstrings for complex functions
- Comment the "why", not the "what"
```scheme
(define (deploy-machine machine method)
"Deploy configuration to MACHINE using METHOD.
Returns a deployment result alist with success status, timing,
and any error messages. May throw exceptions for invalid inputs."
;; Validate inputs early to fail fast
(validate-machine machine)
(validate-deployment-method method)
;; Use atomic operations to prevent partial deployments
(call-with-deployment-lock machine
(lambda ()
(let ((start-time (current-time)))
;; ... deployment logic
))))
```
### 13. Testing Approach
- Write tests for pure functions first
- Mock I/O operations
- Use SRFI-64 testing framework
```scheme
(use-modules (srfi srfi-64))
(test-begin "machine-configuration")
(test-equal "machine services extraction"
'(ollama jellyfin forgejo)
(get-machine-services 'grey-area))
(test-assert "deployment readiness check"
(deployment-ready?
'((status . configured) (health . good))
'((connectivity . online) (load . normal))))
(test-error "invalid machine throws exception"
'invalid-machine
(deploy-machine 'non-existent-machine 'deploy-rs))
(test-end "machine-configuration")
```
## Project Structure Best Practices
### 14. Module Organization
```
modules/
├── lab/
│ ├── core.scm ; Core data structures and utilities
│ ├── machines.scm ; Machine management
│ ├── deployment.scm ; Deployment strategies
│ ├── monitoring.scm ; Status checking and metrics
│ └── config.scm ; Configuration handling
├── mcp/
│ ├── server.scm ; MCP server implementation
│ ├── tools.scm ; MCP tool definitions
│ └── resources.scm ; MCP resource handlers
└── utils/
├── ssh.scm ; SSH utilities
├── json.scm ; JSON helpers
└── logging.scm ; Logging facilities
```
### 15. Build and Development Workflow
- Use Guile's module compilation
- Leverage REPL for iterative development
- Provide development/production configurations
```scheme
;; Development helpers in separate module
(define-module (lab dev)
#:use-module (lab core)
#:export (reload-config
reset-state
dev-deploy))
;; Hot-reload for development
(define (reload-config)
(reload-module (resolve-module '(lab config)))
(init-lab-tool))
;; Safe deployment for development
(define (dev-deploy machine)
(if (eq? (current-environment) 'development)
(deploy-machine machine 'deploy-rs)
(error "dev-deploy only available in development mode")))
```
## VS Code and GitHub Copilot Integration
### 16. MCP Client Integration with VS Code
- Implement MCP client in VS Code extension
- Bridge home lab context to Copilot
- Provide real-time infrastructure state
```typescript
// VS Code extension structure for MCP integration
// File: vscode-extension/src/extension.ts
import * as vscode from 'vscode';
import { MCPClient } from './mcp-client';
export function activate(context: vscode.ExtensionContext) {
const mcpClient = new MCPClient('stdio', {
command: 'guile',
args: ['-c', '(use-modules (mcp server)) (run-mcp-server)']
});
// Register commands for home lab operations
const deployCommand = vscode.commands.registerCommand(
'homelab.deploy',
async (machine: string) => {
const result = await mcpClient.callTool('deploy-machine', {
machine: machine,
method: 'deploy-rs'
});
vscode.window.showInformationMessage(
`Deployment ${result.success ? 'succeeded' : 'failed'}`
);
}
);
// Provide context to Copilot through workspace state
const statusProvider = new HomeLab StatusProvider(mcpClient);
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(
'homelab', statusProvider
)
);
context.subscriptions.push(deployCommand);
}
class HomeLabStatusProvider implements vscode.TextDocumentContentProvider {
constructor(private mcpClient: MCPClient) {}
async provideTextDocumentContent(uri: vscode.Uri): Promise<string> {
// Fetch current lab state for Copilot context
const resources = await this.mcpClient.listResources();
const status = await this.mcpClient.readResource('machines://status/all');
return `# Home Lab Status
Current Infrastructure State:
${JSON.stringify(status, null, 2)}
Available Resources:
${resources.map(r => `- ${r.uri}: ${r.description}`).join('\n')}
`;
}
}
```
### 17. MCP Server Configuration for IDE Integration
- Provide IDE-specific tools and resources
- Format responses for developer consumption
- Include code suggestions and snippets
```scheme
;; IDE-specific MCP tools
(define ide-tools
`((generate-nix-config
(description . "Generate NixOS configuration for new machine")
(inputSchema . ,(json-schema
`((type . "object")
(properties . ((machine-name (type . "string"))
(services (type . "array")
(items (type . "string")))
(hardware-profile (type . "string"))))
(required . ("machine-name")))))
(handler . ,generate-nix-config-tool))
(suggest-deployment-strategy
(description . "Suggest optimal deployment strategy for changes")
(inputSchema . ,(json-schema
`((type . "object")
(properties . ((changed-files (type . "array")
(items (type . "string")))
(target-machines (type . "array")
(items (type . "string")))))
(required . ("changed-files")))))
(handler . ,suggest-deployment-strategy-tool))
(validate-config
(description . "Validate NixOS configuration syntax and dependencies")
(inputSchema . ,(json-schema
`((type . "object")
(properties . ((config-path (type . "string"))
(machine (type . "string"))))
(required . ("config-path")))))
(handler . ,validate-config-tool))))
;; IDE-specific resources
(define ide-resources
`(("homelab://templates/machine-config"
(description . "Template for new machine configuration")
(mimeType . "application/x-nix"))
("homelab://examples/service-configs"
(description . "Example service configurations")
(mimeType . "application/x-nix"))
("homelab://docs/deployment-guide"
(description . "Step-by-step deployment procedures")
(mimeType . "text/markdown"))
("homelab://status/real-time"
(description . "Real-time infrastructure status for context")
(mimeType . "application/json"))))
;; Generate contextual code suggestions
(define (generate-nix-config-tool args)
(let ((machine-name (assoc-ref args "machine-name"))
(services (assoc-ref args "services"))
(hardware-profile (assoc-ref args "hardware-profile")))
`((content . ,(format #f "# Generated configuration for ~a
{ config, pkgs, ... }:
{
imports = [
./hardware-configuration.nix
~/args
];
# Machine-specific configuration
networking.hostName = \"~a\";
# Services configuration
~a
# System packages
environment.systemPackages = with pkgs; [
# Add your packages here
];
system.stateVersion = \"24.05\";
}"
machine-name
machine-name
(if services
(string-join
(map (lambda (service)
(format #f " services.~a.enable = true;" service))
services)
"\n")
" # No services specified")))
(isError . #f))))
```
### 18. Copilot Context Enhancement
- Provide infrastructure context to improve suggestions
- Include deployment patterns and best practices
- Real-time system state for informed recommendations
```scheme
;; Context provider for Copilot integration
(define (provide-copilot-context)
`((infrastructure-state . ,(get-current-infrastructure-state))
(deployment-patterns . ,(get-common-deployment-patterns))
(service-configurations . ,(get-service-config-templates))
(best-practices . ,(get-deployment-best-practices))
(current-issues . ,(get-active-alerts))))
(define (get-current-infrastructure-state)
`((machines . ,(map (lambda (machine)
`((name . ,machine)
(status . ,(machine-status machine))
(services . ,(machine-services machine))
(last-deployment . ,(last-deployment-time machine))))
(list-machines)))
(network-topology . ,(get-network-topology))
(resource-usage . ,(get-resource-utilization))))
(define (get-common-deployment-patterns)
`((safe-deployment . "Use deploy-rs for production, hybrid-update for development")
(rollback-strategy . "Always test deployments in staging first")
(service-dependencies . "Ensure database services start before applications")
(backup-before-deploy . "Create snapshots before major configuration changes")))
;; Format context for IDE consumption
(define (format-ide-context context)
(scm->json-string context #:pretty #t))
```
### 19. VS Code Extension Development
- Create extension for seamless MCP integration
- Provide commands, views, and context
- Enable real-time collaboration with infrastructure
```typescript
// package.json for VS Code extension
{
"name": "homelab-mcp-integration",
"displayName": "Home Lab MCP Integration",
"description": "Integrate home lab infrastructure with VS Code through MCP",
"version": "0.1.0",
"engines": {
"vscode": "^1.74.0"
},
"categories": ["Other"],
"activationEvents": [
"onCommand:homelab.connect",
"workspaceContains:**/flake.nix"
],
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "homelab.deploy",
"title": "Deploy Machine",
"category": "Home Lab"
},
{
"command": "homelab.status",
"title": "Check Status",
"category": "Home Lab"
},
{
"command": "homelab.generateConfig",
"title": "Generate Config",
"category": "Home Lab"
}
],
"views": {
"explorer": [
{
"id": "homelabStatus",
"name": "Home Lab Status",
"when": "homelab:connected"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "homelab",
"title": "Home Lab",
"icon": "$(server-environment)"
}
]
}
}
}
// MCP Client implementation
class MCPClient {
private transport: MCPTransport;
private capabilities: MCPCapabilities;
constructor(transportType: 'stdio' | 'websocket', config: any) {
this.transport = this.createTransport(transportType, config);
this.initialize();
}
async callTool(name: string, arguments: any): Promise<any> {
return this.transport.request('tools/call', {
name: name,
arguments: arguments
});
}
async listResources(): Promise<MCPResource[]> {
const response = await this.transport.request('resources/list', {});
return response.resources;
}
async readResource(uri: string): Promise<any> {
return this.transport.request('resources/read', { uri });
}
// Integration with Copilot context
async getCopilotContext(): Promise<string> {
const context = await this.readResource('homelab://context/copilot');
return context.content;
}
}
```
### 20. GitHub Copilot Workspace Integration
- Configure workspace for optimal Copilot suggestions
- Provide infrastructure context files
- Set up context patterns for deployment scenarios
```json
// .vscode/settings.json
{
"github.copilot.enable": {
"*": true,
"yaml": true,
"nix": true,
"scheme": true
},
"github.copilot.advanced": {
"length": 500,
"temperature": 0.2
},
"homelab.mcpServer": {
"command": "guile",
"args": ["-L", "modules", "-c", "(use-modules (mcp server)) (run-mcp-server)"],
"autoStart": true
},
"files.associations": {
"*.scm": "scheme",
"flake.lock": "json"
}
}
// .copilot/context.md for workspace context
```markdown
# Home Lab Infrastructure Context
## Current Architecture
- NixOS-based infrastructure with multiple machines
- Deploy-rs for safe deployments
- Services: Ollama, Jellyfin, Forgejo, NFS, ZFS
- Network topology: reverse-proxy, grey-area, sleeper-service, congenital-optimist
## Common Patterns
- Use `deploy-rs` for production deployments
- Test with `hybrid-update` in development
- Always backup before major changes
- Follow NixOS module structure in `/modules/`
## Configuration Standards
- Machine configs in `/machines/{hostname}/`
- Shared modules in `/modules/`
- Service-specific configs in `services/` subdirectories
```
### 21. Real-time Context Updates
- Stream infrastructure changes to VS Code
- Update Copilot context automatically
- Provide deployment feedback in editor
```scheme
;; Real-time context streaming
(define (start-context-stream port)
"Stream infrastructure changes to connected IDE clients"
(let ((clients (make-hash-table)))
(spawn-fiber
(lambda ()
(let loop ()
(let ((update (get-infrastructure-update)))
(hash-for-each
(lambda (client-id websocket)
(catch #t
(lambda ()
(websocket-send websocket
(scm->json-string update)))
(lambda (key . args)
(hash-remove! clients client-id))))
clients)
(sleep 5)
(loop)))))
;; WebSocket server for IDE connections
(run-websocket-server
(lambda (ws)
(let ((client-id (generate-client-id)))
(hash-set! clients client-id ws)
(websocket-send ws
(scm->json-string
`((type . "welcome")
(context . ,(get-current-context)))))
(handle-client-messages ws client-id clients)))
#:port port)))
;; Integration with file watchers
(define (watch-config-changes)
"Watch for configuration file changes and update context"
(file-system-watcher
(list "/home/geir/Home-lab/machines"
"/home/geir/Home-lab/modules")
(lambda (event)
(match event
(('modify path)
(when (string-suffix? ".nix" path)
(update-copilot-context path)))
(_ #f)))))
```