diff --git a/examples/server/vibecoder/go.mod b/examples/server/vibecoder/go.mod new file mode 100644 index 00000000..cc3b23fc --- /dev/null +++ b/examples/server/vibecoder/go.mod @@ -0,0 +1,14 @@ +module github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder + +go 1.24.3 + +replace github.com/modelcontextprotocol/go-sdk => ../../.. + +require github.com/modelcontextprotocol/go-sdk v0.0.0-00010101000000-000000000000 + +require ( + github.com/google/jsonschema-go v0.3.0 // indirect + github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect + golang.org/x/oauth2 v0.30.0 // indirect +) diff --git a/examples/server/vibecoder/go.sum b/examples/server/vibecoder/go.sum new file mode 100644 index 00000000..f013ba2d --- /dev/null +++ b/examples/server/vibecoder/go.sum @@ -0,0 +1,12 @@ +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82 h1:6C8qej6f1bStuePVkLSFxoU22XBS165D3klxlzRg8F4= +github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82/go.mod h1:xe4pgH49k4SsmkQq5OT8abwhWmnzkhpgnXeekbx2efw= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= diff --git a/examples/server/vibecoder/internal/vibecoder/analysis/analysis.go b/examples/server/vibecoder/internal/vibecoder/analysis/analysis.go new file mode 100644 index 00000000..1bb1303f --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/analysis/analysis.go @@ -0,0 +1,343 @@ +package analysis + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/domain" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/graph" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/parser" +) + +type Analyzer struct { + Graph *graph.Graph +} + +func NewAnalyzer(g *graph.Graph) *Analyzer { + return &Analyzer{Graph: g} +} + +func (a *Analyzer) AnalyzeFile(path string, content []byte) error { + // 1. Determine Layer/Type + layer := detectLayer(path) + + // 2. Create/Update Node + nodeID := path // Use path as ID for simplicity + var node *domain.Node + + // Handle Gherkin + if strings.HasSuffix(path, ".feature") { + return a.analyzeGherkin(path, content) + } + + // Handle Code + lang := parser.DetectLanguage(path) + if lang == parser.LangUnknown { + // Just register generic file? Or skip. + // Let's register generic code if inside source + if layer != "" { + node = &domain.Node{ + ID: nodeID, + Kind: domain.NodeKindCode, + Metadata: map[string]interface{}{ + "layer": layer, + "language": "unknown", + }, + } + a.Graph.AddNode(node) + } + return nil + } + + node = &domain.Node{ + ID: nodeID, + Kind: domain.NodeKindCode, + Metadata: map[string]interface{}{ + "layer": layer, + "language": string(lang), + }, + } + a.Graph.AddNode(node) + + // 3. Parse Imports + imports, err := parser.ParseImports(content, lang) + if err == nil { + for _, imp := range imports { + // Resolve import path to ID (simplified) + // Assuming import path is relative or absolute? + // For now, we store the raw import path. + // In a real system, we'd resolve this to the actual file ID. + // Let's assume a simplified resolver or just store edge to "potential" ID. + targetID := resolveImport(path, imp) + a.Graph.AddEdge(nodeID, targetID, domain.EdgeTypeImports) + } + } + + // 4. Parse Step Definitions (if Test layer) + if layer == "interface" || strings.Contains(path, "test") || strings.Contains(path, "steps") { + steps, err := parser.ParseStepDefinitions(content, lang) + if err == nil && len(steps) > 0 { + for _, s := range steps { + stepID := fmt.Sprintf("stepdef:%s:%s", s.FunctionName, s.Pattern) + stepNode := &domain.Node{ + ID: stepID, + Kind: domain.NodeKindStepDefinition, + Properties: map[string]interface{}{ + "regex_pattern": s.Pattern, + "function_name": s.FunctionName, + "filepath": path, + "line": s.Line, + }, + } + a.Graph.AddNode(stepNode) + a.Graph.AddEdge(stepID, nodeID, domain.EdgeTypeCalls) + } + } + } + + return nil +} + +func (a *Analyzer) analyzeGherkin(path string, content []byte) error { + feat, err := parser.ParseGherkin(content) + if err != nil { + return err + } + + // Create GherkinFeature Node + featID := "gh:feat:" + strings.ReplaceAll(feat.Name, " ", "_") + featNode := &domain.Node{ + ID: featID, + Kind: domain.NodeKindGherkinFeature, + Properties: map[string]interface{}{ + "name": feat.Name, + "file": path, + }, + } + a.Graph.AddNode(featNode) + + // Create Scenarios + for _, sc := range feat.Scenarios { + scID := "gh:scen:" + strings.ReplaceAll(sc.Name, " ", "_") + scNode := &domain.Node{ + ID: scID, + Kind: domain.NodeKindGherkinScenario, + Properties: map[string]interface{}{ + "name": sc.Name, + "file": path, + "steps_hash": sc.StepsHash, + "line": sc.Line, + "steps": sc.Steps, + }, + } + a.Graph.AddNode(scNode) + // Link Feature -> Scenario (Conceptual containment, generic edge? or just naming convention) + // Or assume implicit relationship. SRS doesn't define edge between Feat/Scen. + } + return nil +} + +func detectLayer(path string) string { + if strings.Contains(path, "/domain/") { + return "domain" + } + if strings.Contains(path, "/application/") { + return "application" + } + if strings.Contains(path, "/infrastructure/") { + return "infrastructure" + } + if strings.Contains(path, "/interface/") || strings.Contains(path, "/api/") { + return "interface" + } + return "" +} + +// resolveImport attempts to map an import string to a file ID. +// This is very heuristic for the example. +func resolveImport(sourcePath, importStr string) string { + // Remove quotes + importStr = strings.Trim(importStr, "\"'") + + // If it starts with ., it's relative + if strings.HasPrefix(importStr, ".") { + dir := filepath.Dir(sourcePath) + return filepath.Join(dir, importStr) // Simplified + } + // Else assume absolute or package alias? + // For TS: src/domain/... + // For this example, we'll return it as is, or prepend prefix if matches known patterns. + return importStr +} + +func (a *Analyzer) FindViolations() []domain.Violation { + var violations []domain.Violation + + nodes := a.Graph.GetAllNodes() + for _, node := range nodes { + if node.Kind == domain.NodeKindCode { + layer := node.Metadata["layer"] + if layer == nil { + continue + } + lStr := layer.(string) + + // Get imports + edges := a.Graph.GetEdgesFrom(node.ID) + for _, edge := range edges { + if edge.Type == domain.EdgeTypeImports { + target, ok := a.Graph.GetNode(edge.TargetID) + // If we can't find the target node, we might try fuzzy matching or skip + // For now skip if not found (external lib) + if !ok { + // Heuristic: check if targetID looks like infra/app + if strings.Contains(edge.TargetID, "infrastructure") { + // Check rules + if lStr == "domain" { + violations = append(violations, domain.Violation{ + Severity: domain.SeverityCritical, + Message: fmt.Sprintf("Domain Rule Broken: '%s' imports '%s' (Infrastructure).", node.ID, edge.TargetID), + File: node.ID, + Kind: domain.ViolationKindArchLayer, + }) + } + } + continue + } + + targetLayer := target.Metadata["layer"] + if targetLayer == nil { + continue + } + tlStr := targetLayer.(string) + + // Rule: Domain cannot import Infra or App + if lStr == "domain" { + if tlStr == "infrastructure" || tlStr == "application" { + violations = append(violations, domain.Violation{ + Severity: domain.SeverityCritical, + Message: fmt.Sprintf("Domain Rule Broken: '%s' imports '%s' (%s).", node.ID, target.ID, tlStr), + File: node.ID, + Kind: domain.ViolationKindArchLayer, + }) + } + } + // Rule: App cannot import Infra (strict) or should use ports. + // SRS says: Alert if App imports Infra concrete. + if lStr == "application" && tlStr == "infrastructure" { + violations = append(violations, domain.Violation{ + Severity: domain.SeverityWarning, + Message: fmt.Sprintf("Application Alert: '%s' imports '%s' (Infrastructure). Should use Ports.", node.ID, target.ID), + File: node.ID, + Kind: domain.ViolationKindArchLayer, + }) + } + } + } + } + } + + // BDD Drift Check + scenarios := a.filterNodes(domain.NodeKindGherkinScenario) + stepDefs := a.filterNodes(domain.NodeKindStepDefinition) + + for _, sc := range scenarios { + scSteps, ok := sc.Properties["steps"].([]string) + if !ok { + continue + } + + for _, stepText := range scSteps { + cleanedStep := cleanStepText(stepText) + matched := false + for _, sd := range stepDefs { + pattern, ok := sd.Properties["regex_pattern"].(string) + if !ok { + continue + } + if matchStep(cleanedStep, pattern) { + matched = true + break + } + } + + if !matched { + violations = append(violations, domain.Violation{ + Severity: domain.SeverityWarning, + Message: fmt.Sprintf("BDD Drift/Missing: Step '%s' in '%s' has no matching StepDefinition.", stepText, sc.ID), + File: sc.Properties["file"].(string), + Kind: domain.ViolationKindBDDDrift, + Line: sc.Properties["line"].(int), + }) + } + } + } + + return violations +} + +// IndexStepDefinitions tries to link Scenarios to Steps +func (a *Analyzer) IndexStepDefinitions() { + scenarios := a.filterNodes(domain.NodeKindGherkinScenario) + stepDefs := a.filterNodes(domain.NodeKindStepDefinition) + + for _, sc := range scenarios { + scSteps, ok := sc.Properties["steps"].([]string) + if !ok { + continue + } + + for _, stepText := range scSteps { + // Clean step text (remove Keyword) + // "Given I have 5 items" -> "I have 5 items" + cleanedStep := cleanStepText(stepText) + + for _, sd := range stepDefs { + pattern, ok := sd.Properties["regex_pattern"].(string) + if !ok { + continue + } + + // Simplified Regex matching + // In real world, we'd use robust cucumber expression matching + // Here we just try to see if it matches. + // Pattern might be regex string. + // Note: StepDef pattern often assumes full match. + + if matchStep(cleanedStep, pattern) { + a.Graph.AddEdge(sc.ID, sd.ID, domain.EdgeTypeExecutes) + } + } + } + } +} + +func cleanStepText(step string) string { + parts := strings.Fields(step) + if len(parts) > 1 { + return strings.Join(parts[1:], " ") + } + return step +} + +func matchStep(text, pattern string) bool { + // Simple check: if pattern is regex + re, err := regexp.Compile(pattern) + if err == nil { + return re.MatchString(text) + } + // Fallback to substring + return strings.Contains(text, pattern) +} + +func (a *Analyzer) filterNodes(kind domain.NodeKind) []*domain.Node { + var res []*domain.Node + for _, n := range a.Graph.GetAllNodes() { + if n.Kind == kind { + res = append(res, n) + } + } + return res +} diff --git a/examples/server/vibecoder/internal/vibecoder/domain/models.go b/examples/server/vibecoder/internal/vibecoder/domain/models.go new file mode 100644 index 00000000..ce3ea9c1 --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/domain/models.go @@ -0,0 +1,73 @@ +package domain + +type NodeKind string + +const ( + NodeKindCode NodeKind = "Code" + NodeKindRequirement NodeKind = "Requirement" + NodeKindFeature NodeKind = "Feature" + NodeKindTest NodeKind = "Test" + NodeKindGherkinFeature NodeKind = "GherkinFeature" + NodeKindGherkinScenario NodeKind = "GherkinScenario" + NodeKindStepDefinition NodeKind = "StepDefinition" +) + +type EdgeType string + +const ( + EdgeTypeDefines EdgeType = "DEFINES" // Requirement -> Feature + EdgeTypeImplementedBy EdgeType = "IMPLEMENTED_BY" // Feature -> Code, Requirement -> Code + EdgeTypeVerifies EdgeType = "VERIFIES" // Test/Scenario -> Requirement + EdgeTypeExecutes EdgeType = "EXECUTES" // GherkinScenario -> StepDefinition + EdgeTypeCalls EdgeType = "CALLS" // StepDefinition -> Code + EdgeTypeDescribedBy EdgeType = "DESCRIBED_BY" // Requirement -> GherkinFeature + EdgeTypeImports EdgeType = "IMPORTS" // Code -> Code (for architectural analysis) +) + +type Node struct { + ID string `json:"id"` + Kind NodeKind `json:"kind"` + Properties map[string]interface{} `json:"properties,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +type Edge struct { + SourceID string `json:"source_id"` + TargetID string `json:"target_id"` + Type EdgeType `json:"type"` +} + +type ViolationSeverity string + +const ( + SeverityCritical ViolationSeverity = "CRITICAL" + SeverityWarning ViolationSeverity = "WARNING" +) + +type ViolationKind string + +const ( + ViolationKindArchLayer ViolationKind = "ARCH_LAYER_VIOLATION" + ViolationKindBDDDrift ViolationKind = "BDD_DRIFT" +) + +type Violation struct { + Severity ViolationSeverity `json:"severity"` + Message string `json:"message"` + File string `json:"file"` + Kind ViolationKind `json:"kind"` + Line int `json:"line,omitempty"` // Optional +} + +// Helper structs for specific node properties (optional, for type safety if needed) +// For now, we rely on the generic Properties map for flexibility, +// but we can define structs to marshal/unmarshal specific kinds. + +type RequirementProps struct { + Title string `json:"title"` + Description string `json:"description"` + Status string `json:"status"` + Priority string `json:"priority"` + ExternalLink string `json:"externalLink"` + AcceptanceCriteria []string `json:"acceptanceCriteria"` +} diff --git a/examples/server/vibecoder/internal/vibecoder/graph/graph.go b/examples/server/vibecoder/internal/vibecoder/graph/graph.go new file mode 100644 index 00000000..e25f2ac0 --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/graph/graph.go @@ -0,0 +1,157 @@ +package graph + +import ( + "sync" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/domain" +) + +type Graph struct { + mu sync.RWMutex + nodes map[string]*domain.Node + edges map[string][]*domain.Edge // SourceID -> Edges + reverseEdges map[string][]*domain.Edge // TargetID -> Edges +} + +func NewGraph() *Graph { + return &Graph{ + nodes: make(map[string]*domain.Node), + edges: make(map[string][]*domain.Edge), + reverseEdges: make(map[string][]*domain.Edge), + } +} + +func (g *Graph) AddNode(node *domain.Node) { + g.mu.Lock() + defer g.mu.Unlock() + g.nodes[node.ID] = node +} + +func (g *Graph) GetNode(id string) (*domain.Node, bool) { + g.mu.RLock() + defer g.mu.RUnlock() + n, ok := g.nodes[id] + return n, ok +} + +func (g *Graph) GetAllNodes() []*domain.Node { + g.mu.RLock() + defer g.mu.RUnlock() + nodes := make([]*domain.Node, 0, len(g.nodes)) + for _, n := range g.nodes { + nodes = append(nodes, n) + } + return nodes +} + +func (g *Graph) AddEdge(sourceID, targetID string, edgeType domain.EdgeType) { + g.mu.Lock() + defer g.mu.Unlock() + + edge := &domain.Edge{ + SourceID: sourceID, + TargetID: targetID, + Type: edgeType, + } + + // Avoid duplicates + for _, e := range g.edges[sourceID] { + if e.TargetID == targetID && e.Type == edgeType { + return + } + } + + g.edges[sourceID] = append(g.edges[sourceID], edge) + g.reverseEdges[targetID] = append(g.reverseEdges[targetID], edge) +} + +func (g *Graph) GetEdgesFrom(sourceID string) []*domain.Edge { + g.mu.RLock() + defer g.mu.RUnlock() + // Return copy + edges := g.edges[sourceID] + result := make([]*domain.Edge, len(edges)) + copy(result, edges) + return result +} + +func (g *Graph) GetEdgesTo(targetID string) []*domain.Edge { + g.mu.RLock() + defer g.mu.RUnlock() + edges := g.reverseEdges[targetID] + result := make([]*domain.Edge, len(edges)) + copy(result, edges) + return result +} + +// BlastRadius calculates impacted features and requirements given a code node ID. +// It traverses upwards (reverse edges) looking for Features and Requirements. +// The traversal follows: Code <- IMPLEMENTED_BY - Feature <- DEFINES - Requirement +// Or Code <- IMPLEMENTED_BY - Requirement +func (g *Graph) BlastRadius(codeID string) ([]string, []string) { + g.mu.RLock() + defer g.mu.RUnlock() + + visited := make(map[string]bool) + queue := []string{codeID} + + impactedFeatures := make(map[string]bool) + impactedRequirements := make(map[string]bool) + + visited[codeID] = true + + for len(queue) > 0 { + currentID := queue[0] + queue = queue[1:] + + // Find what depends on currentID (reverse edges) + // We care about relationships that imply "X depends on currentID" + // If Feature IMPLEMENTED_BY Code, then Feature depends on Code. + // So we traverse backwards along IMPLEMENTED_BY, DEFINES, etc. + // Wait, IMPLEMENTED_BY is Feature -> Code. + // So if I change Code, I look at who IMPLEMENTED_BY me. (The reverse edge of IMPLEMENTED_BY). + + for _, edge := range g.reverseEdges[currentID] { + if !visited[edge.SourceID] { + sourceNode, exists := g.nodes[edge.SourceID] + if !exists { + continue + } + + if edge.Type == domain.EdgeTypeImplementedBy || + edge.Type == domain.EdgeTypeDefines || + edge.Type == domain.EdgeTypeCalls { + + visited[edge.SourceID] = true + queue = append(queue, edge.SourceID) + + if sourceNode.Kind == domain.NodeKindFeature { + impactedFeatures[sourceNode.ID] = true + } + if sourceNode.Kind == domain.NodeKindRequirement { + impactedRequirements[sourceNode.ID] = true + } + } + } + } + } + + features := make([]string, 0, len(impactedFeatures)) + for k := range impactedFeatures { + features = append(features, k) + } + requirements := make([]string, 0, len(impactedRequirements)) + for k := range impactedRequirements { + requirements = append(requirements, k) + } + + return features, requirements +} + +func (g *Graph) Clear() { + g.mu.Lock() + defer g.mu.Unlock() + g.nodes = make(map[string]*domain.Node) + g.edges = make(map[string][]*domain.Edge) + g.reverseEdges = make(map[string][]*domain.Edge) +} diff --git a/examples/server/vibecoder/internal/vibecoder/mcp/excalidraw.go b/examples/server/vibecoder/internal/vibecoder/mcp/excalidraw.go new file mode 100644 index 00000000..d6f34321 --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/mcp/excalidraw.go @@ -0,0 +1,254 @@ +package mcp + +import ( + "encoding/json" + "fmt" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/domain" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/graph" +) + +// Excalidraw structs +type ExcalidrawDoc struct { + Type string `json:"type"` + Version int `json:"version"` + Source string `json:"source"` + Elements []ExcalidrawElement `json:"elements"` + AppState ExcalidrawAppState `json:"appState"` +} + +type ExcalidrawElement struct { + ID string `json:"id"` + Type string `json:"type"` + X float64 `json:"x"` + Y float64 `json:"y"` + Width float64 `json:"width"` + Height float64 `json:"height"` + Angle float64 `json:"angle"` + StrokeColor string `json:"strokeColor"` + BackgroundColor string `json:"backgroundColor"` + FillStyle string `json:"fillStyle"` + StrokeWidth float64 `json:"strokeWidth"` + StrokeStyle string `json:"strokeStyle"` + Roughness float64 `json:"roughness"` + Opacity float64 `json:"opacity"` + GroupID []string `json:"groupIds"` + BoundElements []Binding `json:"boundElements,omitempty"` + + // For Text + Text string `json:"text,omitempty"` + FontSize float64 `json:"fontSize,omitempty"` + FontFamily int `json:"fontFamily,omitempty"` + TextAlign string `json:"textAlign,omitempty"` + VerticalAlign string `json:"verticalAlign,omitempty"` + + // For Arrow + Points [][]float64 `json:"points,omitempty"` + StartBinding *Binding `json:"startBinding,omitempty"` + EndBinding *Binding `json:"endBinding,omitempty"` + EndArrowhead string `json:"endArrowhead,omitempty"` +} + +type Binding struct { + ID string `json:"id"` // Element ID +} + +type ExcalidrawAppState struct { + ViewBackgroundColor string `json:"viewBackgroundColor"` + GridSize int `json:"gridSize"` +} + +func generateExcalidraw(g *graph.Graph) ([]byte, error) { + nodes := g.GetAllNodes() + elements := []ExcalidrawElement{} + + // Layout Logic + // Columns: Req(0), Feat(1), Domain(2), App(3), Infra(4), Interface(5), Tests(6) + + colWidth := 250.0 + rowHeight := 100.0 + + // Group nodes by column + cols := make(map[int][]*domain.Node) + nodePos := make(map[string][2]float64) // id -> x, y + + for _, n := range nodes { + cIndex := 7 // default (Misc) + switch n.Kind { + case domain.NodeKindRequirement: + cIndex = 0 + case domain.NodeKindFeature, domain.NodeKindGherkinFeature, domain.NodeKindGherkinScenario: + cIndex = 1 + case domain.NodeKindCode: + layer, _ := n.Metadata["layer"].(string) + switch layer { + case "domain": cIndex = 2 + case "application": cIndex = 3 + case "infrastructure": cIndex = 4 + case "interface": cIndex = 5 + default: + // check if test + if layer == "" && (n.Kind == domain.NodeKindTest || n.Kind == domain.NodeKindStepDefinition) { + cIndex = 6 + } else { + cIndex = 3 // fallback + } + } + case domain.NodeKindStepDefinition: + cIndex = 6 + } + cols[cIndex] = append(cols[cIndex], n) + } + + for col, ns := range cols { + startX := float64(col) * colWidth + startY := 100.0 + + // Add Column Header + header := ExcalidrawElement{ + ID: fmt.Sprintf("header-%d", col), + Type: "text", + X: startX, + Y: 50, + Text: getColName(col), + FontSize: 20, + FontFamily: 1, + StrokeColor: "#000000", + } + elements = append(elements, header) + + for i, n := range ns { + x := startX + 20 + y := startY + float64(i)*rowHeight + + nodePos[n.ID] = [2]float64{x, y} + + bgColor := "#ffffff" + switch n.Kind { + case domain.NodeKindRequirement: bgColor = "#ffec99" // yellow + case domain.NodeKindCode: bgColor = "#d0ebff" // blue + case domain.NodeKindFeature: bgColor = "#b2f2bb" // green + case domain.NodeKindGherkinScenario: bgColor = "#b2f2bb" + case domain.NodeKindStepDefinition: bgColor = "#e599f7" // purple + } + + // Rectangle + rectID := "rect-" + n.ID + rect := ExcalidrawElement{ + ID: rectID, + Type: "rectangle", + X: x, + Y: y, + Width: 200, + Height: 60, + StrokeColor: "#000000", + BackgroundColor: bgColor, + FillStyle: "solid", + StrokeWidth: 1, + Roughness: 1, + Opacity: 100, + } + + // Label + label := ExcalidrawElement{ + ID: "text-" + n.ID, + Type: "text", + X: x + 10, + Y: y + 20, + Width: 180, + Height: 20, + Text: shortID(n.ID), + FontSize: 14, + FontFamily: 1, + StrokeColor: "#000000", + TextAlign: "center", + VerticalAlign: "middle", + } + + elements = append(elements, rect, label) + } + } + + // Edges + for _, n := range nodes { + edges := g.GetEdgesFrom(n.ID) + startP, ok1 := nodePos[n.ID] + if !ok1 { continue } + + for _, e := range edges { + endP, ok2 := nodePos[e.TargetID] + if !ok2 { continue } + + // Simple straight line (simplified) + // Excalidraw expects points relative to X,Y? No, points are relative to 0,0 of the arrow? + // "points": [[0, 0], [dx, dy]] where 0,0 is at X,Y. + + // Start from right of source to left of target? + // Rect width is 200. + + sx := startP[0] + 200 + sy := startP[1] + 30 + tx := endP[0] + ty := endP[1] + 30 + + arrowX := sx + arrowY := sy + dx := tx - sx + dy := ty - sy + + edgeColor := "#000000" + if e.Type == domain.EdgeTypeImports { + edgeColor = "#868e96" // gray + } + + arrow := ExcalidrawElement{ + ID: fmt.Sprintf("edge-%s-%s-%s", n.ID, e.TargetID, e.Type), + Type: "arrow", + X: arrowX, + Y: arrowY, + StrokeColor: edgeColor, + StrokeWidth: 1, + Roughness: 0, + Points: [][]float64{{0, 0}, {dx, dy}}, + EndArrowhead: "arrow", + StartBinding: &Binding{ID: "rect-" + n.ID}, + EndBinding: &Binding{ID: "rect-" + e.TargetID}, + } + elements = append(elements, arrow) + } + } + + doc := ExcalidrawDoc{ + Type: "excalidraw", + Version: 2, + Source: "https://vibecoder.com", + Elements: elements, + AppState: ExcalidrawAppState{ + ViewBackgroundColor: "#ffffff", + GridSize: 20, + }, + } + + return json.MarshalIndent(doc, "", " ") +} + +func getColName(i int) string { + switch i { + case 0: return "Requirements" + case 1: return "Features / BDD" + case 2: return "Domain" + case 3: return "Application" + case 4: return "Infrastructure" + case 5: return "Interface" + case 6: return "Tests / Steps" + default: return "Misc" + } +} + +func shortID(id string) string { + // Returns last part of path or id + if len(id) > 25 { + return "..." + id[len(id)-22:] + } + return id +} diff --git a/examples/server/vibecoder/internal/vibecoder/mcp/server.go b/examples/server/vibecoder/internal/vibecoder/mcp/server.go new file mode 100644 index 00000000..4734c094 --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/mcp/server.go @@ -0,0 +1,303 @@ +package mcp + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/analysis" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/domain" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/graph" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +type VibecoderServer struct { + Graph *graph.Graph + Analyzer *analysis.Analyzer + RootDir string +} + +func NewServer(rootDir string) (*mcp.Server, error) { + g := graph.NewGraph() + an := analysis.NewAnalyzer(g) + + // Scan initial root + scanDirectory(rootDir, an) + // Index steps + an.IndexStepDefinitions() + + vs := &VibecoderServer{ + Graph: g, + Analyzer: an, + RootDir: rootDir, + } + + s := mcp.NewServer(&mcp.Implementation{ + Name: "vibecoder", + Version: "0.0.1", + }, &mcp.ServerOptions{}) + + // Register Tools + mcp.AddTool(s, &mcp.Tool{ + Name: "scaffold_feature", + Description: "Creates structure for a new feature", + }, vs.scaffoldFeature) + + mcp.AddTool(s, &mcp.Tool{ + Name: "link_requirement", + Description: "Links a file to a requirement", + }, vs.linkRequirement) + + mcp.AddTool(s, &mcp.Tool{ + Name: "blast_radius", + Description: "Analyze impact of changing a code node", + }, vs.blastRadius) + + mcp.AddTool(s, &mcp.Tool{ + Name: "index_step_definitions", + Description: "Re-index BDD step definitions", + }, vs.indexStepDefinitions) + + // Register Resources + s.AddResource(&mcp.Resource{ + Name: "status", + URI: "mcp://vibecoder/status", + }, vs.handleStatus) + + s.AddResource(&mcp.Resource{ + Name: "violations", + URI: "mcp://vibecoder/violations", + }, vs.handleViolations) + + s.AddResource(&mcp.Resource{ + Name: "live_docs", + URI: "mcp://vibecoder/live_docs", + }, vs.handleLiveDocs) + + s.AddResource(&mcp.Resource{ + Name: "traceability_matrix", + URI: "mcp://vibecoder/traceability_matrix", + }, vs.handleTraceability) + + s.AddResource(&mcp.Resource{ + Name: "architecture_diagram", + URI: "mcp://vibecoder/architecture_diagram", + }, vs.handleExcalidraw) + + return s, nil +} + +func scanDirectory(root string, an *analysis.Analyzer) { + filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + if info.Name() == "node_modules" || info.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + + content, err := os.ReadFile(path) + if err == nil { + an.AnalyzeFile(path, content) + } + return nil + }) +} + +// Tool Inputs + +type ScaffoldInput struct { + Name string `json:"name" jsonschema:"description=The name of the feature,required"` + Description string `json:"description" jsonschema:"description=Description of the feature,required"` +} + +type LinkRequirementInput struct { + FilePath string `json:"file_path" jsonschema:"description=Path of the file to link,required"` + ReqID string `json:"req_id" jsonschema:"description=Requirement ID,required"` +} + +type BlastRadiusInput struct { + CodeID string `json:"code_id" jsonschema:"description=ID of the code node,required"` +} + +type EmptyInput struct{} + +// Tool Handlers + +func (vs *VibecoderServer) scaffoldFeature(ctx context.Context, req *mcp.CallToolRequest, input ScaffoldInput) (*mcp.CallToolResult, any, error) { + if input.Name == "" { + return &mcp.CallToolResult{IsError: true, Content: []mcp.Content{&mcp.TextContent{Text: "Name required"}}}, nil, nil + } + + // Create directories (simplified) + base := filepath.Join(vs.RootDir, "src") + dirs := []string{ + filepath.Join(base, "domain", strings.ToLower(input.Name)), + filepath.Join(base, "domain", strings.ToLower(input.Name), "ports"), + filepath.Join(base, "application", strings.ToLower(input.Name)), + filepath.Join(base, "infrastructure", "adapters"), + } + + for _, d := range dirs { + os.MkdirAll(d, 0755) + } + + msg := fmt.Sprintf("Scaffolded feature '%s': %s", input.Name, input.Description) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: msg}, + }, + }, nil, nil +} + +func (vs *VibecoderServer) linkRequirement(ctx context.Context, req *mcp.CallToolRequest, input LinkRequirementInput) (*mcp.CallToolResult, any, error) { + // Create Requirement Node if not exists + reqNode, exists := vs.Graph.GetNode(input.ReqID) + if !exists { + reqNode = &domain.Node{ + ID: input.ReqID, + Kind: domain.NodeKindRequirement, + Properties: map[string]interface{}{"title": "Manually Linked Requirement"}, + } + vs.Graph.AddNode(reqNode) + } + + vs.Graph.AddEdge(input.ReqID, input.FilePath, domain.EdgeTypeImplementedBy) + + msg := fmt.Sprintf("Linked %s to %s", input.ReqID, input.FilePath) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: msg}, + }, + }, nil, nil +} + +func (vs *VibecoderServer) blastRadius(ctx context.Context, req *mcp.CallToolRequest, input BlastRadiusInput) (*mcp.CallToolResult, any, error) { + features, reqs := vs.Graph.BlastRadius(input.CodeID) + + res := map[string]interface{}{ + "code_id": input.CodeID, + "impacted_features": features, + "impacted_requirements": reqs, + } + + jsonBytes, _ := json.MarshalIndent(res, "", " ") + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: string(jsonBytes)}, + }, + }, nil, nil +} + +func (vs *VibecoderServer) indexStepDefinitions(ctx context.Context, req *mcp.CallToolRequest, input EmptyInput) (*mcp.CallToolResult, any, error) { + // Re-scan? For now just re-index + vs.Analyzer.IndexStepDefinitions() + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: "Indexed step definitions"}, + }, + }, nil, nil +} + +// Resource Handlers + +func (vs *VibecoderServer) handleStatus(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + nodes := vs.Graph.GetAllNodes() + status := map[string]interface{}{ + "node_count": len(nodes), + "status": "healthy", + } + bytes, _ := json.MarshalIndent(status, "", " ") + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "application/json", Text: string(bytes)}, + }, + }, nil +} + +func (vs *VibecoderServer) handleExcalidraw(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + bytes, err := generateExcalidraw(vs.Graph) + if err != nil { + return nil, err + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "application/json", Text: string(bytes)}, + }, + }, nil +} + +func (vs *VibecoderServer) handleViolations(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + violations := vs.Analyzer.FindViolations() + bytes, _ := json.MarshalIndent(violations, "", " ") + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "application/json", Text: string(bytes)}, + }, + }, nil +} + +func (vs *VibecoderServer) handleLiveDocs(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + nodes := vs.Graph.GetAllNodes() + var sb strings.Builder + sb.WriteString("# Vibecoder Live Docs\n\n") + sb.WriteString("## Nodes\n") + for _, n := range nodes { + sb.WriteString(fmt.Sprintf("- **%s** (%s)\n", n.ID, n.Kind)) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "text/markdown", Text: sb.String()}, + }, + }, nil +} + +func (vs *VibecoderServer) handleTraceability(ctx context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + // Build Matrix + // For each Requirement, find Features, Code, Tests + matrix := []map[string]interface{}{} + + nodes := vs.Graph.GetAllNodes() + for _, n := range nodes { + if n.Kind == domain.NodeKindRequirement { + entry := map[string]interface{}{ + "requirement_id": n.ID, + } + // Find implemented by + edges := vs.Graph.GetEdgesFrom(n.ID) + var code []string + for _, e := range edges { + if e.Type == domain.EdgeTypeImplementedBy { + code = append(code, e.TargetID) + } + } + entry["code"] = code + + // Find verifiers (Tests) - Reverse edge VERIFIES + revEdges := vs.Graph.GetEdgesTo(n.ID) + var verifiers []string + for _, e := range revEdges { + if e.Type == domain.EdgeTypeVerifies { + verifiers = append(verifiers, e.SourceID) + } + } + entry["verifiers"] = verifiers + + matrix = append(matrix, entry) + } + } + + bytes, _ := json.MarshalIndent(matrix, "", " ") + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "application/json", Text: string(bytes)}, + }, + }, nil +} diff --git a/examples/server/vibecoder/internal/vibecoder/parser/gherkin.go b/examples/server/vibecoder/internal/vibecoder/parser/gherkin.go new file mode 100644 index 00000000..ca140f13 --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/parser/gherkin.go @@ -0,0 +1,81 @@ +package parser + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "strings" +) + +type GherkinFeature struct { + Name string + Scenarios []GherkinScenario +} + +type GherkinScenario struct { + Name string + Steps []string + StepsHash string + Line int +} + +func ParseGherkin(content []byte) (*GherkinFeature, error) { + scanner := bufio.NewScanner(bytes.NewReader(content)) + feature := &GherkinFeature{} + var currentScenario *GherkinScenario + + lineNum := 0 + for scanner.Scan() { + lineNum++ + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "@") { + continue + } + + if strings.HasPrefix(line, "Feature:") { + feature.Name = strings.TrimSpace(strings.TrimPrefix(line, "Feature:")) + } else if strings.HasPrefix(line, "Scenario:") { + if currentScenario != nil { + finalizeScenario(currentScenario) + feature.Scenarios = append(feature.Scenarios, *currentScenario) + } + currentScenario = &GherkinScenario{ + Name: strings.TrimSpace(strings.TrimPrefix(line, "Scenario:")), + Line: lineNum, + } + } else if isStep(line) { + if currentScenario != nil { + currentScenario.Steps = append(currentScenario.Steps, line) + } + } + } + if currentScenario != nil { + finalizeScenario(currentScenario) + feature.Scenarios = append(feature.Scenarios, *currentScenario) + } + + return feature, nil +} + +func isStep(line string) bool { + words := strings.Fields(line) + if len(words) == 0 { + return false + } + kw := words[0] + switch kw { + case "Given", "When", "Then", "And", "But": + return true + } + return false +} + +func finalizeScenario(sc *GherkinScenario) { + // Calculate hash of steps + h := sha256.New() + for _, s := range sc.Steps { + h.Write([]byte(s)) + } + sc.StepsHash = hex.EncodeToString(h.Sum(nil))[:8] +} diff --git a/examples/server/vibecoder/internal/vibecoder/parser/parser.go b/examples/server/vibecoder/internal/vibecoder/parser/parser.go new file mode 100644 index 00000000..b6f3e59e --- /dev/null +++ b/examples/server/vibecoder/internal/vibecoder/parser/parser.go @@ -0,0 +1,175 @@ +package parser + +import ( + "context" + "strings" + + sitter "github.com/smacker/go-tree-sitter" + "github.com/smacker/go-tree-sitter/java" + "github.com/smacker/go-tree-sitter/typescript/typescript" +) + +type Language string + +const ( + LangTypeScript Language = "typescript" + LangJava Language = "java" + LangUnknown Language = "unknown" +) + +type StepDefFound struct { + Pattern string + FunctionName string + Line int +} + +func DetectLanguage(filename string) Language { + if strings.HasSuffix(filename, ".ts") || strings.HasSuffix(filename, ".tsx") { + return LangTypeScript + } + if strings.HasSuffix(filename, ".java") { + return LangJava + } + return LangUnknown +} + +func ParseImports(content []byte, lang Language) ([]string, error) { + var sl *sitter.Language + switch lang { + case LangTypeScript: + sl = typescript.GetLanguage() + case LangJava: + sl = java.GetLanguage() + default: + return nil, nil + } + + parser := sitter.NewParser() + parser.SetLanguage(sl) + + tree, _ := parser.ParseCtx(context.Background(), nil, content) + root := tree.RootNode() + + var queryStr string + if lang == LangTypeScript { + queryStr = ` + (import_statement source: (string (string_fragment) @path)) + (export_statement source: (string (string_fragment) @path)) + ` + } else if lang == LangJava { + // Java imports are usually package names, not file paths. + // But for analysis we collect them. + queryStr = `(import_declaration (scoped_identifier) @path)` + } + + q, err := sitter.NewQuery([]byte(queryStr), sl) + if err != nil { + return nil, err + } + qc := sitter.NewQueryCursor() + qc.Exec(q, root) + + var imports []string + for { + m, ok := qc.NextMatch() + if !ok { + break + } + for _, c := range m.Captures { + if c.Node != nil { + text := string(content[c.Node.StartByte():c.Node.EndByte()]) + imports = append(imports, text) + } + } + } + + return imports, nil +} + +func ParseStepDefinitions(content []byte, lang Language) ([]StepDefFound, error) { + var sl *sitter.Language + switch lang { + case LangTypeScript: + sl = typescript.GetLanguage() + case LangJava: + sl = java.GetLanguage() + default: + return nil, nil + } + + parser := sitter.NewParser() + parser.SetLanguage(sl) + tree, _ := parser.ParseCtx(context.Background(), nil, content) + root := tree.RootNode() + + // Heuristic queries for Cucumber steps + // Note: these are simplified and might need tuning for specific frameworks + var queryStr string + if lang == LangTypeScript { + // Matches: Given("pattern", function() {}) + queryStr = ` + (call_expression + function: (identifier) @keyword + arguments: (arguments + (string (string_fragment) @pattern) + ) + ) + ` + } else if lang == LangJava { + // Matches: @Given("pattern") public void method() + queryStr = ` + (method_declaration + (modifiers + (marker_annotation + name: (identifier) @keyword + arguments: (argument_list (string (string_fragment) @pattern)) + ) + ) + name: (identifier) @method + ) + ` + } + + q, err := sitter.NewQuery([]byte(queryStr), sl) + if err != nil { + return nil, err + } + qc := sitter.NewQueryCursor() + qc.Exec(q, root) + + var results []StepDefFound + + // We need to group captures by match to keep keyword, pattern, and method together + // iterate matches + for { + m, ok := qc.NextMatch() + if !ok { + break + } + + var pattern, method string + var line int + + for _, c := range m.Captures { + name := q.CaptureNameForId(c.Index) + if name == "pattern" { + pattern = string(content[c.Node.StartByte():c.Node.EndByte()]) + line = int(c.Node.StartPoint().Row) + 1 + } else if name == "method" { + method = string(content[c.Node.StartByte():c.Node.EndByte()]) + } + } + + // Filter for Gherkin keywords if needed? + // The query assumes specific structure. + if pattern != "" { + results = append(results, StepDefFound{ + Pattern: pattern, + FunctionName: method, + Line: line, + }) + } + } + + return results, nil +} diff --git a/examples/server/vibecoder/main.go b/examples/server/vibecoder/main.go new file mode 100644 index 00000000..2e879914 --- /dev/null +++ b/examples/server/vibecoder/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/mcp" + sdk "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func main() { + rootDir := "." + if len(os.Args) > 1 { + rootDir = os.Args[1] + } + + fmt.Printf("Starting Vibecoder Server in %s...\n", rootDir) + + // Create server + server, err := mcp.NewServer(rootDir) + if err != nil { + log.Fatal(err) + } + + // Run server + if err := server.Run(context.Background(), &sdk.StdioTransport{}); err != nil { + log.Fatal(err) + } +} diff --git a/examples/server/vibecoder/main_test.go b/examples/server/vibecoder/main_test.go new file mode 100644 index 00000000..8aa96398 --- /dev/null +++ b/examples/server/vibecoder/main_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/analysis" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/domain" + "github.com/modelcontextprotocol/go-sdk/examples/server/vibecoder/internal/vibecoder/graph" +) + +func TestVibecoder(t *testing.T) { + g := graph.NewGraph() + an := analysis.NewAnalyzer(g) + + cwd, _ := os.Getwd() + testRoot := filepath.Join(cwd, "testdata") + + // Manually scan + err := filepath.Walk(testRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + content, _ := os.ReadFile(path) + return an.AnalyzeFile(path, content) + } + return nil + }) + if err != nil { + t.Fatalf("Walk failed: %v", err) + } + + an.IndexStepDefinitions() + + // Check Violation + violations := an.FindViolations() + found := false + for _, v := range violations { + if v.Kind == domain.ViolationKindArchLayer { + // Check if it's the expected one + if strings.Contains(v.Message, "Broken.ts") { + found = true + break + } + } + } + if !found { + t.Error("Expected architecture violation in Broken.ts") + } + + // Check BDD Linking + // Find scenario + nodes := g.GetAllNodes() + var scenNode *domain.Node + for _, n := range nodes { + if n.Kind == domain.NodeKindGherkinScenario { + scenNode = n + break + } + } + if scenNode == nil { + t.Fatal("Scenario not found") + } + + // Check edge to StepDef + edges := g.GetEdgesFrom(scenNode.ID) + foundStep := false + for _, e := range edges { + if e.Type == domain.EdgeTypeExecutes { + foundStep = true + break + } + } + + if !foundStep { + // print all step defs for debugging + t.Log("Scenario edges:", len(edges)) + stepDefs := 0 + for _, n := range nodes { + if n.Kind == domain.NodeKindStepDefinition { + stepDefs++ + t.Logf("StepDef: %s Props: %v", n.ID, n.Properties) + } + } + t.Logf("Total StepDefs: %d", stepDefs) + t.Error("Expected Scenario to EXECUTE StepDefinition") + } +} diff --git a/examples/server/vibecoder/testdata/features/login.feature b/examples/server/vibecoder/testdata/features/login.feature new file mode 100644 index 00000000..ad742e60 --- /dev/null +++ b/examples/server/vibecoder/testdata/features/login.feature @@ -0,0 +1,5 @@ +Feature: Login + Scenario: Successful Login + Given I have a valid user + When I login + Then I am redirected diff --git a/examples/server/vibecoder/testdata/src/domain/user/Broken.ts b/examples/server/vibecoder/testdata/src/domain/user/Broken.ts new file mode 100644 index 00000000..272110a3 --- /dev/null +++ b/examples/server/vibecoder/testdata/src/domain/user/Broken.ts @@ -0,0 +1,2 @@ +import { Postgres } from '../../infrastructure/db/Postgres'; +export class Broken {} diff --git a/examples/server/vibecoder/testdata/src/domain/user/User.ts b/examples/server/vibecoder/testdata/src/domain/user/User.ts new file mode 100644 index 00000000..4f82c145 --- /dev/null +++ b/examples/server/vibecoder/testdata/src/domain/user/User.ts @@ -0,0 +1 @@ +export class User {} diff --git a/examples/server/vibecoder/testdata/src/infrastructure/db/Postgres.ts b/examples/server/vibecoder/testdata/src/infrastructure/db/Postgres.ts new file mode 100644 index 00000000..0b60e208 --- /dev/null +++ b/examples/server/vibecoder/testdata/src/infrastructure/db/Postgres.ts @@ -0,0 +1 @@ +export class Postgres {} diff --git a/examples/server/vibecoder/testdata/src/test/LoginSteps.ts b/examples/server/vibecoder/testdata/src/test/LoginSteps.ts new file mode 100644 index 00000000..e2b0d463 --- /dev/null +++ b/examples/server/vibecoder/testdata/src/test/LoginSteps.ts @@ -0,0 +1,2 @@ +// Mocking step definition +Given("I have a valid user", function() {});