diff --git a/entire-sem/README.md b/entire-sem/README.md index e3c3ee9..708f710 100644 --- a/entire-sem/README.md +++ b/entire-sem/README.md @@ -2,8 +2,8 @@ `entire-sem` is an Entire CLI plugin prototype for entity-level checkpoint context. -Entire already knows a checkpoint touched `auth.py` or `.github/workflows/ci.yml`. -This plugin answers the next question: which semantic entities changed inside that file? +Entire already knows a checkpoint touched `auth.py`. This plugin answers the next question: +which functions, classes, types, or methods changed inside that file? Once built as `entire-sem` and installed as an Entire plugin, it is invoked as: @@ -25,7 +25,6 @@ The MVP uses a tree-sitter-backed parser for: - Python - JavaScript / TypeScript - Rust, including inherent `impl` methods -- YAML, including GitHub Actions workflow sections and jobs The parser is isolated behind `internal/sem`, so the command surface can stay stable while the semantic model gets richer. @@ -88,8 +87,7 @@ checkpoint context at the entity level instead of stopping at "this file changed `entire-sem` is a plugin-shaped proof of concept for that idea: - parse the before and after git trees with tree-sitter -- extract named entities like functions, classes, methods, structs, traits, types, - YAML workflow sections, and GitHub Actions jobs +- extract named entities like functions, classes, methods, structs, traits, and types - compare signatures and normalized bodies - build a heuristic dependent count from parsed references in the target tree, marking same-short-name method references as ambiguous when they cannot be diff --git a/entire-sem/internal/sem/analyze_test.go b/entire-sem/internal/sem/analyze_test.go index 8d91249..d6ee4cd 100644 --- a/entire-sem/internal/sem/analyze_test.go +++ b/entire-sem/internal/sem/analyze_test.go @@ -80,65 +80,6 @@ func TestAnalyzeGitRangeDependentCounts(t *testing.T) { } } -func TestAnalyzeGitRangeIncludesGitHubWorkflowYAML(t *testing.T) { - repo := t.TempDir() - git(t, repo, "init") - git(t, repo, "config", "user.name", "Entire Sem Test") - git(t, repo, "config", "user.email", "sem@example.com") - - write(t, repo, ".github/workflows/ci.yml", `name: CI -on: - push: - branches: [main] -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: go test ./... -`) - git(t, repo, "add", ".") - git(t, repo, "commit", "-m", "initial") - base := rev(t, repo, "HEAD") - - write(t, repo, ".github/workflows/ci.yml", `name: CI -on: - push: - branches: [main] - pull_request: -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: go test -race ./... -`) - git(t, repo, "add", ".") - git(t, repo, "commit", "-m", "update workflow") - head := rev(t, repo, "HEAD") - - result, err := AnalyzeGitRange(context.Background(), repo, base, head, nil) - if err != nil { - t.Fatal(err) - } - if len(result.Files) != 1 { - t.Fatalf("files = %#v", result.Files) - } - file := result.Files[0] - if file.Path != ".github/workflows/ci.yml" { - t.Fatalf("path = %q", file.Path) - } - if file.Language != "YAML" { - t.Fatalf("language = %q", file.Language) - } - if change := requireChangeKind(t, result, "job", "jobs.test"); change.Type != "body_changed" { - t.Fatalf("workflow job change = %#v, want body_changed", change) - } - if change := requireChangeKind(t, result, "section", "on"); change.Type != "body_changed" { - t.Fatalf("workflow trigger change = %#v, want body_changed", change) - } -} - func TestAnalyzeGitRangePythonAssignedLambdaSignatureChange(t *testing.T) { repo := t.TempDir() git(t, repo, "init") diff --git a/entire-sem/internal/sem/parser.go b/entire-sem/internal/sem/parser.go index 46d060e..13805fe 100644 --- a/entire-sem/internal/sem/parser.go +++ b/entire-sem/internal/sem/parser.go @@ -15,7 +15,6 @@ import ( "github.com/smacker/go-tree-sitter/rust" treesittertsx "github.com/smacker/go-tree-sitter/typescript/tsx" treesitterts "github.com/smacker/go-tree-sitter/typescript/typescript" - treesitteryaml "github.com/smacker/go-tree-sitter/yaml" ) type languageSpec struct { @@ -24,15 +23,13 @@ type languageSpec struct { } var treeSitterLanguages = map[string]languageSpec{ - ".go": {language: "Go", grammar: golang.GetLanguage()}, - ".py": {language: "Python", grammar: python.GetLanguage()}, - ".js": {language: "JavaScript", grammar: javascript.GetLanguage()}, - ".jsx": {language: "JavaScript", grammar: treesittertsx.GetLanguage()}, - ".ts": {language: "TypeScript", grammar: treesitterts.GetLanguage()}, - ".tsx": {language: "TypeScript", grammar: treesittertsx.GetLanguage()}, - ".rs": {language: "Rust", grammar: rust.GetLanguage()}, - ".yaml": {language: "YAML", grammar: treesitteryaml.GetLanguage()}, - ".yml": {language: "YAML", grammar: treesitteryaml.GetLanguage()}, + ".go": {language: "Go", grammar: golang.GetLanguage()}, + ".py": {language: "Python", grammar: python.GetLanguage()}, + ".js": {language: "JavaScript", grammar: javascript.GetLanguage()}, + ".jsx": {language: "JavaScript", grammar: treesittertsx.GetLanguage()}, + ".ts": {language: "TypeScript", grammar: treesitterts.GetLanguage()}, + ".tsx": {language: "TypeScript", grammar: treesittertsx.GetLanguage()}, + ".rs": {language: "Rust", grammar: rust.GetLanguage()}, } type TreeSitterParser struct{} @@ -47,9 +44,6 @@ func (TreeSitterParser) Parse(path, content string) ([]Entity, string) { if err != nil || root == nil || root.IsNull() { return nil, spec.language } - if spec.language == "YAML" { - return yamlEntities(path, content), spec.language - } var entities []Entity walkEntities(root, src, "", &entities) diff --git a/entire-sem/internal/sem/parser_test.go b/entire-sem/internal/sem/parser_test.go index 9f086c0..291b5ad 100644 --- a/entire-sem/internal/sem/parser_test.go +++ b/entire-sem/internal/sem/parser_test.go @@ -622,29 +622,6 @@ impl IntoIterator for Bag { fn into_iter(self) -> Iter { todo!() `, names: []string{"User", "Bag", "validate", "Run", "Run.run", "User.active", "Bag.unwrap_owned", "User.fmt", "Bag.into_iter"}, }, - { - path: ".github/workflows/ci.yml", - language: "YAML", - input: `name: CI -on: - push: - branches: [main] -permissions: - contents: read -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: go test ./... - deploy: - if: github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - run: ./scripts/deploy.sh -`, - names: []string{"ci", "on", "permissions", "jobs.test", "jobs.deploy"}, - }, } for _, tt := range tests { @@ -664,40 +641,6 @@ jobs: } } -func TestTreeSitterParserSupportsYAMLWorkflowExtensions(t *testing.T) { - if !Supported(".github/workflows/ci.yml") { - t.Fatal(".yml workflow should be supported") - } - if !Supported(".github/workflows/deploy.yaml") { - t.Fatal(".yaml workflow should be supported") - } - - entities, language := TreeSitterParser{}.Parse(".github/workflows/deploy.yaml", `name: Deploy -on: workflow_dispatch -jobs: - publish: - runs-on: ubuntu-latest - steps: - - run: echo deploy -`) - if language != "YAML" { - t.Fatalf("language = %q", language) - } - seen := map[string]string{} - for _, entity := range entities { - seen[entity.Name] = entity.Kind - } - for name, kind := range map[string]string{ - "deploy": "workflow", - "on": "section", - "jobs.publish": "job", - } { - if seen[name] != kind { - t.Fatalf("%s kind = %q, want %q in %#v", name, seen[name], kind, entities) - } - } -} - func TestTreeSitterParserIgnoresNonFunctionTypeScriptProperties(t *testing.T) { entities, language := TreeSitterParser{}.Parse("app.ts", `interface Api { url: string diff --git a/entire-sem/internal/sem/yaml.go b/entire-sem/internal/sem/yaml.go deleted file mode 100644 index f48a151..0000000 --- a/entire-sem/internal/sem/yaml.go +++ /dev/null @@ -1,275 +0,0 @@ -package sem - -import ( - "path/filepath" - "sort" - "strings" - "unicode" -) - -type yamlBlock struct { - Key string - StartLine int - EndLine int -} - -func yamlEntities(path, content string) []Entity { - lines := strings.Split(content, "\n") - topLevel := yamlTopLevelBlocks(lines) - if len(topLevel) == 0 { - return nil - } - - var entities []Entity - entities = append(entities, yamlEntity("workflow", yamlWorkflowEntityName(path), yamlWorkflowSignature(path, lines, topLevel), 1, len(lines), lines)) - for _, block := range topLevel { - switch block.Key { - case "name": - continue - case "jobs": - entities = append(entities, yamlJobEntities(block, lines)...) - default: - entities = append(entities, yamlEntity("section", block.Key, "section "+block.Key, block.StartLine, block.EndLine, lines)) - } - } - - sort.Slice(entities, func(i, j int) bool { - if entities[i].StartLine == entities[j].StartLine { - return entities[i].Name < entities[j].Name - } - return entities[i].StartLine < entities[j].StartLine - }) - return entities -} - -func yamlTopLevelBlocks(lines []string) []yamlBlock { - var blocks []yamlBlock - for index, line := range lines { - if yamlIndent(line) != 0 || yamlIgnoreLine(line) { - continue - } - key, ok := yamlLineKey(line) - if !ok { - continue - } - if len(blocks) > 0 { - blocks[len(blocks)-1].EndLine = index - } - blocks = append(blocks, yamlBlock{Key: key, StartLine: index + 1, EndLine: len(lines)}) - } - return blocks -} - -func yamlJobEntities(jobs yamlBlock, lines []string) []Entity { - jobIndent := yamlDirectChildIndent(jobs, lines) - if jobIndent < 0 { - return nil - } - - var blocks []yamlBlock - for index := jobs.StartLine; index < jobs.EndLine && index < len(lines); index++ { - line := lines[index] - if yamlIndent(line) != jobIndent || yamlIgnoreLine(line) { - continue - } - key, ok := yamlLineKey(line) - if !ok { - continue - } - if len(blocks) > 0 { - blocks[len(blocks)-1].EndLine = index - } - blocks = append(blocks, yamlBlock{Key: key, StartLine: index + 1, EndLine: jobs.EndLine}) - } - - entities := make([]Entity, 0, len(blocks)) - for _, block := range blocks { - name := "jobs." + block.Key - entities = append(entities, yamlEntity("job", name, "job "+name, block.StartLine, block.EndLine, lines)) - } - return entities -} - -func yamlDirectChildIndent(parent yamlBlock, lines []string) int { - parentIndent := yamlIndent(lines[parent.StartLine-1]) - childIndent := -1 - for index := parent.StartLine; index < parent.EndLine && index < len(lines); index++ { - line := lines[index] - if yamlIgnoreLine(line) { - continue - } - indent := yamlIndent(line) - if indent <= parentIndent { - continue - } - if _, ok := yamlLineKey(line); !ok { - continue - } - if childIndent < 0 || indent < childIndent { - childIndent = indent - } - } - return childIndent -} - -func yamlEntity(kind, name, signature string, startLine, endLine int, lines []string) Entity { - if startLine < 1 { - startLine = 1 - } - if endLine < startLine { - endLine = startLine - } - if endLine > len(lines) { - endLine = len(lines) - } - block := strings.Join(lines[startLine-1:endLine], "\n") - return Entity{ - Kind: kind, - Name: name, - Signature: normalizeSignature(signature), - StartLine: startLine, - EndLine: endLine, - BodyHash: hash(normalize(block)), - Fingerprint: hash(normalize(entityFingerprintSource(Entity{Name: name, Signature: signature}, block))), - } -} - -func yamlWorkflowEntityName(path string) string { - base := filepath.Base(path) - ext := filepath.Ext(base) - name := strings.TrimSuffix(base, ext) - if name == "" || name == "." { - return "workflow" - } - return name -} - -func yamlWorkflowSignature(path string, lines []string, topLevel []yamlBlock) string { - name := yamlTopLevelScalar("name", lines, topLevel) - if name == "" { - name = yamlWorkflowEntityName(path) - } - return "workflow " + name -} - -func yamlTopLevelScalar(key string, lines []string, blocks []yamlBlock) string { - for _, block := range blocks { - if block.Key != key || block.StartLine < 1 || block.StartLine > len(lines) { - continue - } - return yamlLineValue(lines[block.StartLine-1]) - } - return "" -} - -func yamlLineKey(line string) (string, bool) { - trimmed := strings.TrimSpace(line) - if yamlIgnoreTrimmedLine(trimmed) || strings.HasPrefix(trimmed, "- ") { - return "", false - } - colon := yamlKeyColonIndex(trimmed) - if colon <= 0 { - return "", false - } - key := strings.TrimSpace(trimmed[:colon]) - if key == "" || strings.HasPrefix(key, "{") || strings.HasPrefix(key, "[") { - return "", false - } - return yamlUnquote(key), true -} - -func yamlLineValue(line string) string { - trimmed := strings.TrimSpace(line) - colon := yamlKeyColonIndex(trimmed) - if colon < 0 || colon+1 >= len(trimmed) { - return "" - } - return yamlUnquote(strings.TrimSpace(yamlStripInlineComment(trimmed[colon+1:]))) -} - -func yamlKeyColonIndex(value string) int { - var quote rune - escaped := false - for index, char := range value { - if quote != 0 { - if quote == '"' && char == '\\' && !escaped { - escaped = true - continue - } - if char == quote && !escaped { - quote = 0 - } - escaped = false - continue - } - switch char { - case '\'', '"': - quote = char - case ':': - return index - } - } - return -1 -} - -func yamlStripInlineComment(value string) string { - var quote rune - escaped := false - for index, char := range value { - if quote != 0 { - if quote == '"' && char == '\\' && !escaped { - escaped = true - continue - } - if char == quote && !escaped { - quote = 0 - } - escaped = false - continue - } - switch char { - case '\'', '"': - quote = char - case '#': - if index == 0 || unicode.IsSpace(rune(value[index-1])) { - return strings.TrimSpace(value[:index]) - } - } - } - return strings.TrimSpace(value) -} - -func yamlUnquote(value string) string { - value = strings.TrimSpace(value) - if len(value) >= 2 { - first := value[0] - last := value[len(value)-1] - if (first == '"' && last == '"') || (first == '\'' && last == '\'') { - return value[1 : len(value)-1] - } - } - return value -} - -func yamlIndent(line string) int { - count := 0 - for _, char := range line { - switch char { - case ' ': - count++ - case '\t': - count += 2 - default: - return count - } - } - return count -} - -func yamlIgnoreLine(line string) bool { - return yamlIgnoreTrimmedLine(strings.TrimSpace(line)) -} - -func yamlIgnoreTrimmedLine(line string) bool { - return line == "" || strings.HasPrefix(line, "#") || line == "---" || line == "..." -}