From a8ef88217b983eb2bbca193a99413c7ed7328854 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Mon, 24 Aug 2026 11:31:19 +0530 Subject: [PATCH 1/5] Expand existing tool format detection --- README.md | 2 +- cmd/brief/list_test.go | 21 +++++ cmd/brief/main.go | 24 +++-- detect/detect.go | 6 ++ detect/detect_test.go | 162 +++++++++++++++++++++++++++++++++ kb/kb.go | 1 + knowledge/_shared/ansible.toml | 2 +- knowledge/_shared/bazel.toml | 3 +- knowledge/_shared/helm.toml | 2 +- knowledge/csharp/nuget.toml | 2 +- knowledge/java/gradle.toml | 2 +- knowledge/node/pnpm.toml | 2 +- knowledge/node/yarn.toml | 2 +- knowledge/python/pants.toml | 2 +- 14 files changed, 211 insertions(+), 22 deletions(-) create mode 100644 cmd/brief/list_test.go diff --git a/README.md b/README.md index bf674d9..938e980 100644 --- a/README.md +++ b/README.md @@ -288,7 +288,7 @@ Separately from resources, brief reports agent skills the project provides. Thes ## What it detects -54 language ecosystems with 547 tool definitions across 22 categories. +Language ecosystems and development tools across multiple categories. **Languages:** Ada, C, C#, C++, COBOL, Clojure, Common Lisp, Crystal, D, Dart, Deno, Elixir, Elm, Emacs Lisp, Erlang, F#, Fortran, GDScript, Gleam, Go, Groovy, Haskell, Haxe, Java, JavaScript, Julia, Kotlin, Lua, Mojo, Nim, Nix, OCaml, Objective-C, Odin, PHP, Perl, Prolog, Python, R, Racket, Roc, Ruby, Rust, Scala, Scheme, Solidity, Swift, Tcl, TypeScript, V, VHDL, Verilog, Zig. diff --git a/cmd/brief/list_test.go b/cmd/brief/list_test.go new file mode 100644 index 0000000..3f1c686 --- /dev/null +++ b/cmd/brief/list_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "strings" + "testing" + + "github.com/git-pkgs/brief/kb" +) + +func TestWriteToolsReadmeOmitsKnowledgeBaseTotals(t *testing.T) { + var out strings.Builder + writeToolsReadme(&out, &kb.KnowledgeBase{}) + + const introduction = "Language ecosystems and development tools across multiple categories." + if !strings.Contains(out.String(), introduction) { + t.Errorf("README output does not contain count-free introduction %q", introduction) + } + if strings.Contains(out.String(), "language ecosystems with") { + t.Errorf("README output contains numeric knowledge-base totals: %q", out.String()) + } +} diff --git a/cmd/brief/main.go b/cmd/brief/main.go index 875db85..1ffea09 100644 --- a/cmd/brief/main.go +++ b/cmd/brief/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "os" "reflect" "runtime/debug" @@ -232,6 +233,10 @@ func listTools(knowledgeBase *kb.KnowledgeBase) { } func listToolsReadme(knowledgeBase *kb.KnowledgeBase) { + writeToolsReadme(os.Stdout, knowledgeBase) +} + +func writeToolsReadme(out io.Writer, knowledgeBase *kb.KnowledgeBase) { // Group tools by category, deduplicating names. seen := make(map[string]map[string]bool) byCategory := make(map[string][]string) @@ -255,24 +260,17 @@ func listToolsReadme(knowledgeBase *kb.KnowledgeBase) { languages := byCategory["language"] - ecosystems := knowledgeBase.AllEcosystems() - totalTools := 0 - for _, names := range byCategory { - totalTools += len(names) - } - - _, _ = fmt.Fprintf(os.Stdout, "## What it detects\n\n") - _, _ = fmt.Fprintf(os.Stdout, "%d language ecosystems with %d tool definitions across %d categories.\n\n", - len(ecosystems), totalTools, len(byCategory)) + _, _ = fmt.Fprint(out, "## What it detects\n\n") + _, _ = fmt.Fprint(out, "Language ecosystems and development tools across multiple categories.\n\n") // Languages if len(languages) > 0 { - _, _ = fmt.Fprintf(os.Stdout, "**Languages:** %s.\n\n", strings.Join(languages, ", ")) + _, _ = fmt.Fprintf(out, "**Languages:** %s.\n\n", strings.Join(languages, ", ")) } // Package managers if pms := byCategory["package_manager"]; len(pms) > 0 { - _, _ = fmt.Fprintf(os.Stdout, "**Package Managers:** %s.\n\n", strings.Join(pms, ", ")) + _, _ = fmt.Fprintf(out, "**Package Managers:** %s.\n\n", strings.Join(pms, ", ")) } // Tool categories in display order. @@ -285,10 +283,10 @@ func listToolsReadme(knowledgeBase *kb.KnowledgeBase) { if label == "" { label = cat } - _, _ = fmt.Fprintf(os.Stdout, "**%s:** %s.\n\n", label, strings.Join(names, ", ")) + _, _ = fmt.Fprintf(out, "**%s:** %s.\n\n", label, strings.Join(names, ", ")) } - _, _ = fmt.Fprintf(os.Stdout, "Run `brief list tools` for the full list.\n") + _, _ = fmt.Fprint(out, "Run `brief list tools` for the full list.\n") } func listEcosystems(knowledgeBase *kb.KnowledgeBase) { diff --git a/detect/detect.go b/detect/detect.go index bbf990d..b803cd5 100644 --- a/detect/detect.go +++ b/detect/detect.go @@ -473,6 +473,12 @@ func (e *Engine) detectCategory(category string) []brief.Detection { // matchTool checks if a tool definition matches the project. // Returns the confidence level, or empty string if no match. func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence { + for _, pattern := range tool.Detect.ExcludeFiles { + if e.exists(pattern) { + return "" + } + } + best := brief.Confidence("") for _, pattern := range tool.Detect.Files { diff --git a/detect/detect_test.go b/detect/detect_test.go index 7ba5e09..e0a34cf 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -1257,6 +1257,158 @@ func TestGradleJavaKotlinDSL(t *testing.T) { } } +func TestExpandedToolFormatDetection(t *testing.T) { + tests := []struct { + name string + path string + content string + extraFiles map[string]string + category string + tool string + }{ + { + name: "Gradle version catalog", + path: "gradle/libs.versions.toml", + content: "[versions]\n", + extraFiles: map[string]string{"Main.java": "class Main {}\n"}, + category: "package_manager", + tool: "Gradle", + }, + { + name: "NuGet central packages", + path: "Directory.Packages.props", + content: "\n", + extraFiles: map[string]string{"Program.cs": "class Program {}\n"}, + category: "package_manager", + tool: "NuGet", + }, + { + name: "NuGet shared build properties", + path: "Directory.Build.props", + content: "\n", + extraFiles: map[string]string{"Program.cs": "class Program {}\n"}, + category: "package_manager", + tool: "NuGet", + }, + { + name: "Helm chart lock", + path: "Chart.lock", + content: "dependencies: []\n", + category: "infrastructure", + tool: "Helm", + }, + { + name: "Helm legacy requirements", + path: "requirements.yaml", + content: "dependencies: []\n", + category: "infrastructure", + tool: "Helm", + }, + { + name: "Helm legacy requirements lock", + path: "requirements.lock", + content: "dependencies: []\n", + category: "infrastructure", + tool: "Helm", + }, + { + name: "Yarn Plug'n'Play", + path: ".pnp.cjs", + content: "module.exports = {};\n", + extraFiles: map[string]string{"package.json": "{}\n"}, + category: "package_manager", + tool: "Yarn", + }, + { + name: "legacy pnpm lockfile", + path: "shrinkwrap.yaml", + content: "lockfileVersion: 3\n", + extraFiles: map[string]string{"package.json": "{}\n"}, + category: "package_manager", + tool: "pnpm", + }, + { + name: "Ansible Galaxy requirements yml", + path: "requirements.yml", + content: "roles: []\n", + category: "infrastructure", + tool: "Ansible", + }, + { + name: "Ansible Galaxy requirements yaml", + path: "requirements.yaml", + content: "roles: []\n", + category: "infrastructure", + tool: "Ansible", + }, + { + name: "Ansible Galaxy metadata yml", + path: "galaxy.yml", + content: "namespace: example\n", + category: "infrastructure", + tool: "Ansible", + }, + { + name: "Ansible Galaxy metadata yaml", + path: "galaxy.yaml", + content: "namespace: example\n", + category: "infrastructure", + tool: "Ansible", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, test.path, test.content) + for path, content := range test.extraFiles { + writeFile(t, dir, path, content) + } + + r, err := New(loadKB(t), dir).Run() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if test.category == "package_manager" { + if !slices.Contains(packageManagerNames(r), test.tool) { + t.Errorf("expected %s package manager, got %v", test.tool, packageManagerNames(r)) + } + return + } + assertToolDetected(t, r, test.category, test.tool) + }) + } +} + +func TestBazelBuildDoesNotConflictWithPants(t *testing.T) { + t.Run("Python Bazel project", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "BUILD", "py_library(name = \"example\")\n") + writeFile(t, dir, "example.py", "VALUE = 1\n") + + r, err := New(loadKB(t), dir).Run() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolDetected(t, r, "monorepo", "Bazel") + assertToolNotDetected(t, r, "monorepo", "Pants") + }) + + t.Run("Pants project", func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "pants.toml", "[GLOBAL]\n") + writeFile(t, dir, "BUILD", "python_sources()\n") + writeFile(t, dir, "example.py", "VALUE = 1\n") + + r, err := New(loadKB(t), dir).Run() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolDetected(t, r, "monorepo", "Pants") + assertToolNotDetected(t, r, "monorepo", "Bazel") + }) +} + func TestGradleJavaGroovyDSL(t *testing.T) { // Regression for #84: with build.gradle in app/ and source under // app/src/main/java//, both Java and Gradle must be detected. @@ -2019,6 +2171,16 @@ func assertToolDetected(t *testing.T, r *brief.Report, category, name string) { t.Errorf("expected %s in %s category", name, category) } +func assertToolNotDetected(t *testing.T, r *brief.Report, category, name string) { + t.Helper() + for _, tool := range r.Tools[category] { + if tool.Name == name { + t.Errorf("did not expect %s in %s category", name, category) + return + } + } +} + func assertHighConfidenceToolDetected( t *testing.T, r *brief.Report, diff --git a/kb/kb.go b/kb/kb.go index 345282a..c06ea2c 100644 --- a/kb/kb.go +++ b/kb/kb.go @@ -36,6 +36,7 @@ type ToolInfo struct { // DetectInfo holds the detection primitives for a tool. type DetectInfo struct { Files []string `toml:"files"` + ExcludeFiles []string `toml:"exclude_files"` Dependencies []string `toml:"dependencies"` DevDependencies []string `toml:"dev_dependencies"` FileContains map[string][]string `toml:"file_contains"` diff --git a/knowledge/_shared/ansible.toml b/knowledge/_shared/ansible.toml index 2cf969d..e211bbc 100644 --- a/knowledge/_shared/ansible.toml +++ b/knowledge/_shared/ansible.toml @@ -7,7 +7,7 @@ repo = "https://github.com/ansible/ansible" description = "IT automation and configuration management" [detect] -files = ["ansible.cfg", "playbooks/", "roles/", "inventory/"] +files = ["ansible.cfg", "requirements.yml", "requirements.yaml", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "inventory/"] [commands] run = "ansible-playbook" diff --git a/knowledge/_shared/bazel.toml b/knowledge/_shared/bazel.toml index d01ece0..f242b06 100644 --- a/knowledge/_shared/bazel.toml +++ b/knowledge/_shared/bazel.toml @@ -7,7 +7,8 @@ repo = "https://github.com/bazelbuild/bazel" description = "Build and test tool for multi-language monorepos" [detect] -files = ["WORKSPACE", "WORKSPACE.bazel", "MODULE.bazel", "BUILD.bazel"] +files = ["WORKSPACE", "WORKSPACE.bazel", "MODULE.bazel", "BUILD", "BUILD.bazel"] +exclude_files = ["pants.toml"] [commands] run = "bazel build //..." diff --git a/knowledge/_shared/helm.toml b/knowledge/_shared/helm.toml index a2005ec..005301d 100644 --- a/knowledge/_shared/helm.toml +++ b/knowledge/_shared/helm.toml @@ -7,7 +7,7 @@ repo = "https://github.com/helm/helm" description = "Kubernetes package manager" [detect] -files = ["Chart.yaml", "Chart.yml", "charts/"] +files = ["Chart.yaml", "Chart.yml", "Chart.lock", "requirements.yaml", "requirements.lock", "charts/"] [commands] run = "helm install" diff --git a/knowledge/csharp/nuget.toml b/knowledge/csharp/nuget.toml index 61382be..4f7fe72 100644 --- a/knowledge/csharp/nuget.toml +++ b/knowledge/csharp/nuget.toml @@ -6,7 +6,7 @@ docs = "https://learn.microsoft.com/en-us/nuget/" description = ".NET package manager" [detect] -files = ["*.csproj", "packages.config", "nuget.config"] +files = ["*.csproj", "packages.config", "nuget.config", "Directory.Packages.props", "Directory.Build.props"] ecosystems = ["csharp"] [commands] diff --git a/knowledge/java/gradle.toml b/knowledge/java/gradle.toml index 5dfa7c3..f53958a 100644 --- a/knowledge/java/gradle.toml +++ b/knowledge/java/gradle.toml @@ -7,7 +7,7 @@ repo = "https://github.com/gradle/gradle" description = "Build automation tool for Java and other JVM languages" [detect] -files = ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts"] +files = ["build.gradle", "build.gradle.kts", "settings.gradle", "settings.gradle.kts", "gradle/libs.versions.toml"] ecosystems = ["java", "groovy", "kotlin"] [commands] diff --git a/knowledge/node/pnpm.toml b/knowledge/node/pnpm.toml index a3fa077..6b2dc81 100644 --- a/knowledge/node/pnpm.toml +++ b/knowledge/node/pnpm.toml @@ -7,7 +7,7 @@ repo = "https://github.com/pnpm/pnpm" description = "Fast, disk space efficient package manager" [detect] -files = ["pnpm-lock.yaml"] +files = ["pnpm-lock.yaml", "shrinkwrap.yaml"] ecosystems = ["node"] [commands] diff --git a/knowledge/node/yarn.toml b/knowledge/node/yarn.toml index 6f38f43..ec89775 100644 --- a/knowledge/node/yarn.toml +++ b/knowledge/node/yarn.toml @@ -7,7 +7,7 @@ repo = "https://github.com/yarnpkg/berry" description = "Node.js package manager" [detect] -files = ["yarn.lock", ".yarnrc.yml", ".yarnrc"] +files = ["yarn.lock", ".yarnrc.yml", ".yarnrc", ".pnp.cjs"] ecosystems = ["node"] [commands] diff --git a/knowledge/python/pants.toml b/knowledge/python/pants.toml index 9d3d70a..4bf873e 100644 --- a/knowledge/python/pants.toml +++ b/knowledge/python/pants.toml @@ -7,7 +7,7 @@ repo = "https://github.com/pantsbuild/pants" description = "Build system for Python and polyglot monorepos" [detect] -files = ["pants.toml", "BUILD"] +files = ["pants.toml"] ecosystems = ["python"] [commands] From 0c546f655b7b8bf063d3fd48e09391226df6eeb4 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Mon, 24 Aug 2026 13:26:58 +0530 Subject: [PATCH 2/5] Disambiguate Helm and Ansible requirements --- detect/detect_test.go | 7 +++++++ knowledge/_shared/ansible.toml | 6 +++++- knowledge/_shared/helm.toml | 5 ++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/detect/detect_test.go b/detect/detect_test.go index e0a34cf..a8539b0 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -1265,6 +1265,7 @@ func TestExpandedToolFormatDetection(t *testing.T) { extraFiles map[string]string category string tool string + notTool string }{ { name: "Gradle version catalog", @@ -1303,6 +1304,7 @@ func TestExpandedToolFormatDetection(t *testing.T) { content: "dependencies: []\n", category: "infrastructure", tool: "Helm", + notTool: "Ansible", }, { name: "Helm legacy requirements lock", @@ -1333,6 +1335,7 @@ func TestExpandedToolFormatDetection(t *testing.T) { content: "roles: []\n", category: "infrastructure", tool: "Ansible", + notTool: "Helm", }, { name: "Ansible Galaxy requirements yaml", @@ -1340,6 +1343,7 @@ func TestExpandedToolFormatDetection(t *testing.T) { content: "roles: []\n", category: "infrastructure", tool: "Ansible", + notTool: "Helm", }, { name: "Ansible Galaxy metadata yml", @@ -1376,6 +1380,9 @@ func TestExpandedToolFormatDetection(t *testing.T) { return } assertToolDetected(t, r, test.category, test.tool) + if test.notTool != "" { + assertToolNotDetected(t, r, test.category, test.notTool) + } }) } } diff --git a/knowledge/_shared/ansible.toml b/knowledge/_shared/ansible.toml index e211bbc..0d66992 100644 --- a/knowledge/_shared/ansible.toml +++ b/knowledge/_shared/ansible.toml @@ -7,7 +7,11 @@ repo = "https://github.com/ansible/ansible" description = "IT automation and configuration management" [detect] -files = ["ansible.cfg", "requirements.yml", "requirements.yaml", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "inventory/"] +files = ["ansible.cfg", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "inventory/"] + +[detect.file_contains] +"requirements.yml" = ["roles:", "collections:", "- src:"] +"requirements.yaml" = ["roles:", "collections:", "- src:"] [commands] run = "ansible-playbook" diff --git a/knowledge/_shared/helm.toml b/knowledge/_shared/helm.toml index 005301d..4582472 100644 --- a/knowledge/_shared/helm.toml +++ b/knowledge/_shared/helm.toml @@ -7,7 +7,10 @@ repo = "https://github.com/helm/helm" description = "Kubernetes package manager" [detect] -files = ["Chart.yaml", "Chart.yml", "Chart.lock", "requirements.yaml", "requirements.lock", "charts/"] +files = ["Chart.yaml", "Chart.yml", "Chart.lock", "requirements.lock", "charts/"] + +[detect.file_contains] +"requirements.yaml" = ["dependencies:"] [commands] run = "helm install" From 45db3e841b894641f2873c7fc820ce88e8fd6e23 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Mon, 24 Aug 2026 14:31:15 +0530 Subject: [PATCH 3/5] Address format detection review feedback --- detect/detect_test.go | 16 ++++++++++++++++ detect/filter.go | 11 ++++++++--- detect/filter_test.go | 16 ++++++++++++++++ knowledge/_shared/ansible.toml | 4 ++-- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/detect/detect_test.go b/detect/detect_test.go index a8539b0..2e800d9 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -1345,6 +1345,22 @@ func TestExpandedToolFormatDetection(t *testing.T) { tool: "Ansible", notTool: "Helm", }, + { + name: "Ansible Galaxy named role requirement", + path: "requirements.yml", + content: "- name: geerlingguy.apache\n", + category: "infrastructure", + tool: "Ansible", + notTool: "Helm", + }, + { + name: "Ansible Galaxy included role requirements", + path: "requirements.yaml", + content: "- include: requirements/common.yml\n", + category: "infrastructure", + tool: "Ansible", + notTool: "Helm", + }, { name: "Ansible Galaxy metadata yml", path: "galaxy.yml", diff --git a/detect/filter.go b/detect/filter.go index e695379..673c842 100644 --- a/detect/filter.go +++ b/detect/filter.go @@ -268,8 +268,8 @@ func (fc *filterContext) filterPlatforms(plat *brief.PlatformInfo, changedFiles } // toolMatchesChangedFiles checks whether any changed file is relevant to a tool's -// detection signals: config files, lockfile, detection file patterns, or -// file_contains targets. +// detection signals: config files, lockfile, detection or exclusion file +// patterns, or file_contains targets. func toolMatchesChangedFiles(tool *kb.ToolDef, changed map[string]bool, changedExts map[string]bool) bool { if matchesConfigFiles(tool, changed) { return true @@ -290,7 +290,12 @@ func matchesConfigFiles(tool *kb.ToolDef, changed map[string]bool) bool { } func matchesDetectionPatterns(tool *kb.ToolDef, changed map[string]bool, changedExts map[string]bool) bool { - for _, pattern := range tool.Detect.Files { + return matchesPathPatterns(tool.Detect.Files, changed, changedExts) || + matchesPathPatterns(tool.Detect.ExcludeFiles, changed, changedExts) +} + +func matchesPathPatterns(patterns []string, changed map[string]bool, changedExts map[string]bool) bool { + for _, pattern := range patterns { if changed[pattern] { return true } diff --git a/detect/filter_test.go b/detect/filter_test.go index d85e177..e7ea26f 100644 --- a/detect/filter_test.go +++ b/detect/filter_test.go @@ -196,6 +196,22 @@ func TestToolMatchesChangedFiles_FileContainsGlob(t *testing.T) { } } +func TestFilterByChangedFiles_ExcludeFileRemoved(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "BUILD", "py_library(name = \"example\")\n") + writeFile(t, dir, "example.py", "VALUE = 1\n") + + knowledgeBase := loadKB(t) + r, err := New(knowledgeBase, dir).Run() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolDetected(t, r, "monorepo", "Bazel") + + filtered := FilterByChangedFiles(r, knowledgeBase, []string{"pants.toml"}) + assertToolDetected(t, filtered, "monorepo", "Bazel") +} + func TestFilterByChangedFiles_PackageManagers(t *testing.T) { knowledgeBase := loadKB(t) diff --git a/knowledge/_shared/ansible.toml b/knowledge/_shared/ansible.toml index 0d66992..9a9432f 100644 --- a/knowledge/_shared/ansible.toml +++ b/knowledge/_shared/ansible.toml @@ -10,8 +10,8 @@ description = "IT automation and configuration management" files = ["ansible.cfg", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "inventory/"] [detect.file_contains] -"requirements.yml" = ["roles:", "collections:", "- src:"] -"requirements.yaml" = ["roles:", "collections:", "- src:"] +"requirements.yml" = ["roles:", "collections:", "- src:", "- name:", "- include:"] +"requirements.yaml" = ["roles:", "collections:", "- src:", "- name:", "- include:"] [commands] run = "ansible-playbook" From 379cd9c1ff10c8137e41d876d284315ec6284842 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Mon, 24 Aug 2026 16:08:15 +0100 Subject: [PATCH 4/5] Fix Helm detection collision and README drift --- README.md | 18 +++++++++--------- cmd/brief/list_test.go | 29 +++++++++++++++++++++++++++++ detect/detect.go | 6 +++++- detect/detect_test.go | 15 ++++++++++++++- detect/filter.go | 20 ++++++++++++++------ detect/filter_test.go | 15 +++++++++++++++ kb/kb.go | 15 ++++++++------- knowledge/_shared/ansible.toml | 4 ++++ 8 files changed, 98 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 938e980..fa0e456 100644 --- a/README.md +++ b/README.md @@ -292,19 +292,19 @@ Language ecosystems and development tools across multiple categories. **Languages:** Ada, C, C#, C++, COBOL, Clojure, Common Lisp, Crystal, D, Dart, Deno, Elixir, Elm, Emacs Lisp, Erlang, F#, Fortran, GDScript, Gleam, Go, Groovy, Haskell, Haxe, Java, JavaScript, Julia, Kotlin, Lua, Mojo, Nim, Nix, OCaml, Objective-C, Odin, PHP, Perl, Prolog, Python, R, Racket, Roc, Ruby, Rust, Scala, Scheme, Solidity, Swift, Tcl, TypeScript, V, VHDL, Verilog, Zig. -**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Maven, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, sbt, setuptools, uv, vcpkg. +**Package Managers:** Alire, Bun, Bundler, Cabal, Cargo, CocoaPods, Composer, Conan, Conda, DUB, Deno Modules, Flit, Gleam Packages, Go Modules, Gradle, Hatch, Haxelib, Maven, Mix, Nix Flakes, NuGet, PDM, Pipenv, Pkg, Poetry, Quicklisp, Shards, Swift Package Manager, Yarn, cpanm, dotnet CLI, elm, fpm, npm, opam, pip, pnpm, pub, rebar3, renv, sbt, setuptools, uv, vcpkg. -**Test:** AVA, Alcotest, Artillery, Bats, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, pytest, pytest-benchmark, tape, testament, zig test. +**Test:** ASV, AVA, Alcotest, Artillery, Bats, BenchmarkTools.jl, Bruno, Catch2, Cucumber, Cypress, EUnit, ExUnit, Gatling, Ginkgo, Google Test, Hspec, Hurl, JUnit, Jasmine, Jest, Kotest, Lighthouse CI, Locust, MSW, Minitest, Mocha, Newman, PHPUnit, Pest, Playwright, REST Client, RSpec, ScalaTest, Selenium, Testify, Testing Library, Vitest, XCTest, axe-core, benchmark-ips, cargo test, clojure.test, criterion, crystal spec, dart test, deno test, dotnet test, gleam test, go test, hyperfine, k6, kotlin.test, nf-test, pytest, pytest-benchmark, tape, testament, testthat, tinytest, tox, vdiffr, zig test. -**Lint:** Ameba, Biome, Checkstyle, Clippy, Credo, ESLint, Flake8, HLint, Husky, Lefthook, Overcommit, PHP_CodeSniffer, PMD, Pylint, Revive, Roslyn Analyzers, RuboCop, Ruff, ShellCheck, SpotBugs, Stylelint, SwiftLint, Vale, WartRemover, actionlint, clang-tidy, clj-kondo, commitlint, cspell, dart analyze, deno lint, detekt, elvis, golangci-lint, hadolint, markdownlint, oxlint, pre-commit, typos. +**Lint:** Ameba, Biome, Checkstyle, Clippy, Credo, ESLint, Flake8, Fortitude, HLint, Husky, Lefthook, Overcommit, PHP_CodeSniffer, PMD, Pylint, Revive, Roslyn Analyzers, RuboCop, Ruff, ShellCheck, SpotBugs, Stylelint, SwiftLint, Vale, WartRemover, actionlint, clang-tidy, clj-kondo, commitlint, cspell, dart analyze, deno lint, detekt, elvis, golangci-lint, hadolint, lintr, markdownlint, oxlint, pre-commit, typos. -**Format:** Black, Ormolu, PHP CS Fixer, Prettier, Spotless, StandardRB, SwiftFormat, clang-format, cljfmt, crystal tool format, dart format, deno fmt, dotnet format, dprint, erlfmt, gleam format, gofmt, google-java-format, isort, ktlint, mix format, nimpretty, ocamlformat, rustfmt, scalafmt, yapf, zig fmt. +**Format:** Black, JuliaFormatter, Ormolu, PHP CS Fixer, Prettier, Runic, Spotless, StandardRB, SwiftFormat, clang-format, cljfmt, crystal tool format, dart format, deno fmt, dotnet format, dprint, erlfmt, gleam format, gofmt, google-java-format, isort, ktlint, mix format, nimpretty, ocamlformat, rustfmt, scalafmt, styler, yapf, zig fmt. **Typecheck:** Dialyxir, Dialyzer, Flow, PHPStan, Pyright, Sorbet, Steep, mypy, tsc. -**Docs:** Docsify, Docusaurus, Dokka, Doxygen, ExDoc, Hugo, Javadoc, Jekyll, MkDocs, Nextra, Read the Docs, Redoc, Sphinx, Starlight, Storybook, Swagger UI, TypeDoc, VitePress, Yard, Zensical, cargo doc, dart doc, deno doc, mdBook, phpDocumentor, pkgsite. +**Docs:** Docsify, Documenter.jl, Docusaurus, Dokka, Doxygen, ExDoc, Hugo, Javadoc, Jekyll, MkDocs, MultiQC, MyST-Parser, Nextra, Quarto, R Markdown, Read the Docs, Redoc, Sphinx, Starlight, Storybook, Swagger UI, TypeDoc, VitePress, Yard, Zensical, cargo doc, dart doc, deno doc, knitr, mdBook, phpDocumentor, pkgdown, pkgsite, roxygen2. -**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, NestJS, Next.js, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cross, esbuild, tsup. +**Build:** Actix Web, AdonisJS, Angular, Astro, Autotools, Axum, CMake, Django, Dune, Echo, Electron, Eleventy, Ember.js, Express, FastAPI, Fastify, Fiber, Flask, Flutter, Foundry, Gatsby, Gin, GoReleaser, Hardhat, Hono, Invoke, Just, Koa, Laravel, Less, Mage, Make, Meson, NestJS, Next.js, Nextflow, Nuxt, Parcel, Phoenix, PostCSS, Qwik, Rails, Rake, React Native, Remix, Rocket, Rollup, Rspack, SWC, Sass, Sinatra, Snakemake, Spin, Spring Boot, Svelte, SvelteKit, Symfony, Task, Tauri, Vite, Vue, Webpack, cibuildwheel, cross, esbuild, nf-core, targets, tsup. **Native Ext:** Maturin, Neon, Rustler, meson-python, mkmf, napi-rs, node-gyp, phpize, rb-sys, setuptools Extension, setuptools-rust. @@ -320,17 +320,17 @@ Language ecosystems and development tools across multiple categories. **Container:** Cloud Native Buildpacks, Dev Container, Docker, Docker Compose, Podman. -**Infra:** AWS CDK, Ansible, CloudFormation, Helm, Kubernetes, Kustomize, Packer, Pulumi, Serverless Framework, Terraform, Vagrant. +**Infra:** AWS CDK, Ansible, CloudFormation, DVC, Dockstore, Helm, Kubernetes, Kustomize, Packer, Pulumi, Serverless Framework, Terraform, Vagrant. **Monorepo:** Bazel, Cargo workspaces, Go workspace, Lerna, Moon, Nx, Pants, Rush, Turborepo, Yarn workspaces, pnpm workspaces. -**Environment:** Flipper, JetBrains IDE, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv. +**Environment:** Flipper, JetBrains IDE, Jupyter, LaunchDarkly, Mise, Pixi, Unleash, VS Code, Volta, asdf, direnv, dotenv, pyenv. **i18n:** Crowdin, Fluent, FormatJS, Lingui, Rails i18n, Transifex, gettext, i18next, vue-i18n. **Release:** Changesets, cargo-release, conventional-changelog, git-cliff, np, release-please, semantic-release, standard-version, twine. -**Coverage:** Codecov, Coveralls, Excoveralls, JaCoCo, Sentry, SimpleCov, c8, cargo-tarpaulin, coverage.py, go test -cover, nyc. +**Coverage:** Codecov, Coveralls, Excoveralls, JaCoCo, Sentry, SimpleCov, c8, cargo-tarpaulin, coverage.py, covr, go test -cover, nyc. **Dep Updates:** Dependabot, Git Submodules, Renovate. diff --git a/cmd/brief/list_test.go b/cmd/brief/list_test.go index 3f1c686..1e83bcd 100644 --- a/cmd/brief/list_test.go +++ b/cmd/brief/list_test.go @@ -1,9 +1,11 @@ package main import ( + "os" "strings" "testing" + "github.com/git-pkgs/brief" "github.com/git-pkgs/brief/kb" ) @@ -19,3 +21,30 @@ func TestWriteToolsReadmeOmitsKnowledgeBaseTotals(t *testing.T) { t.Errorf("README output contains numeric knowledge-base totals: %q", out.String()) } } + +func TestToolsReadmeMatchesGeneratedOutput(t *testing.T) { + knowledgeBase, err := kb.Load(brief.KnowledgeFS) + if err != nil { + t.Fatalf("load knowledge base: %v", err) + } + var generated strings.Builder + writeToolsReadme(&generated, knowledgeBase) + + readme, err := os.ReadFile("../../README.md") + if err != nil { + t.Fatalf("read README: %v", err) + } + const start = "\n" + const end = "" + _, afterStart, ok := strings.Cut(string(readme), start) + if !ok { + t.Fatal("README tools start marker is missing") + } + section, _, ok := strings.Cut(afterStart, end) + if !ok { + t.Fatal("README tools end marker is missing") + } + if section != generated.String() { + t.Error("README tools block differs from `brief list -readme tools` output") + } +} diff --git a/detect/detect.go b/detect/detect.go index b803cd5..811fe7a 100644 --- a/detect/detect.go +++ b/detect/detect.go @@ -494,7 +494,7 @@ func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence { } for file, patterns := range tool.Detect.FileContains { - if e.contains(file, patterns) { + if e.contentSignalMatches(file, patterns, tool.Detect.ExcludeFileContains[file]) { best = brief.ConfidenceHigh } } @@ -517,6 +517,10 @@ func (e *Engine) matchTool(tool *kb.ToolDef) brief.Confidence { return best } +func (e *Engine) contentSignalMatches(file string, patterns, excluded []string) bool { + return e.contains(file, patterns) && (len(excluded) == 0 || !e.contains(file, excluded)) +} + // exists checks if a file, directory, or glob pattern matches something at the project root. // A trailing "/" means the pattern must match a directory. Glob patterns without // a trailing "/" only match regular files so that a NEWS.d/ directory does not diff --git a/detect/detect_test.go b/detect/detect_test.go index 2e800d9..97cb067 100644 --- a/detect/detect_test.go +++ b/detect/detect_test.go @@ -1301,7 +1301,7 @@ func TestExpandedToolFormatDetection(t *testing.T) { { name: "Helm legacy requirements", path: "requirements.yaml", - content: "dependencies: []\n", + content: "dependencies:\n - name: example\n version: 1.0.0\n", category: "infrastructure", tool: "Helm", notTool: "Ansible", @@ -1432,6 +1432,19 @@ func TestBazelBuildDoesNotConflictWithPants(t *testing.T) { }) } +func TestHelmRequirementsDoesNotHideAnsibleConfig(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, "requirements.yaml", "dependencies:\n - name: example\n version: 1.0.0\n") + writeFile(t, dir, "ansible.cfg", "[defaults]\n") + + r, err := New(loadKB(t), dir).Run() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assertToolDetected(t, r, "infrastructure", "Helm") + assertToolDetected(t, r, "infrastructure", "Ansible") +} + func TestGradleJavaGroovyDSL(t *testing.T) { // Regression for #84: with build.gradle in app/ and source under // app/src/main/java//, both Java and Gradle must be detected. diff --git a/detect/filter.go b/detect/filter.go index 673c842..c250d6e 100644 --- a/detect/filter.go +++ b/detect/filter.go @@ -324,7 +324,20 @@ func matchesPathPatterns(patterns []string, changed map[string]bool, changedExts } func matchesContentTargets(tool *kb.ToolDef, changed map[string]bool) bool { - for pattern := range tool.Detect.FileContains { + if matchesContentPatterns(tool.Detect.FileContains, changed) || + matchesContentPatterns(tool.Detect.ExcludeFileContains, changed) { + return true + } + for file := range tool.Detect.KeyExists { + if changed[file] { + return true + } + } + return false +} + +func matchesContentPatterns(patterns map[string][]string, changed map[string]bool) bool { + for pattern := range patterns { if changed[pattern] { return true } @@ -336,10 +349,5 @@ func matchesContentTargets(tool *kb.ToolDef, changed map[string]bool) bool { } } } - for file := range tool.Detect.KeyExists { - if changed[file] { - return true - } - } return false } diff --git a/detect/filter_test.go b/detect/filter_test.go index e7ea26f..8079369 100644 --- a/detect/filter_test.go +++ b/detect/filter_test.go @@ -196,6 +196,21 @@ func TestToolMatchesChangedFiles_FileContainsGlob(t *testing.T) { } } +func TestToolMatchesChangedFiles_ExcludeFileContains(t *testing.T) { + tool := &kb.ToolDef{ + Detect: kb.DetectInfo{ + Files: []string{"BUILD"}, + ExcludeFileContains: map[string][]string{ + "requirements.yaml": {"dependencies:"}, + }, + }, + } + changed := map[string]bool{"requirements.yaml": true} + if !toolMatchesChangedFiles(tool, changed, nil) { + t.Error("expected exclusion content target to match changed file") + } +} + func TestFilterByChangedFiles_ExcludeFileRemoved(t *testing.T) { dir := t.TempDir() writeFile(t, dir, "BUILD", "py_library(name = \"example\")\n") diff --git a/kb/kb.go b/kb/kb.go index c06ea2c..6b98178 100644 --- a/kb/kb.go +++ b/kb/kb.go @@ -35,13 +35,14 @@ type ToolInfo struct { // DetectInfo holds the detection primitives for a tool. type DetectInfo struct { - Files []string `toml:"files"` - ExcludeFiles []string `toml:"exclude_files"` - Dependencies []string `toml:"dependencies"` - DevDependencies []string `toml:"dev_dependencies"` - FileContains map[string][]string `toml:"file_contains"` - KeyExists map[string][]string `toml:"key_exists"` - Ecosystems []string `toml:"ecosystems"` + Files []string `toml:"files"` + ExcludeFiles []string `toml:"exclude_files"` + Dependencies []string `toml:"dependencies"` + DevDependencies []string `toml:"dev_dependencies"` + FileContains map[string][]string `toml:"file_contains"` + ExcludeFileContains map[string][]string `toml:"exclude_file_contains"` + KeyExists map[string][]string `toml:"key_exists"` + Ecosystems []string `toml:"ecosystems"` } // CommandInfo holds the commands associated with a tool. diff --git a/knowledge/_shared/ansible.toml b/knowledge/_shared/ansible.toml index 9a9432f..24637d8 100644 --- a/knowledge/_shared/ansible.toml +++ b/knowledge/_shared/ansible.toml @@ -13,6 +13,10 @@ files = ["ansible.cfg", "galaxy.yml", "galaxy.yaml", "playbooks/", "roles/", "in "requirements.yml" = ["roles:", "collections:", "- src:", "- name:", "- include:"] "requirements.yaml" = ["roles:", "collections:", "- src:", "- name:", "- include:"] +[detect.exclude_file_contains] +"requirements.yml" = ["dependencies:"] +"requirements.yaml" = ["dependencies:"] + [commands] run = "ansible-playbook" From 5248d48c23750dd485f26db6147892a7f29cb2d2 Mon Sep 17 00:00:00 2001 From: abhinavgautam01 Date: Mon, 24 Aug 2026 21:05:06 +0530 Subject: [PATCH 5/5] Make README drift test CRLF-safe --- cmd/brief/list_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/brief/list_test.go b/cmd/brief/list_test.go index 1e83bcd..73d7969 100644 --- a/cmd/brief/list_test.go +++ b/cmd/brief/list_test.go @@ -34,9 +34,10 @@ func TestToolsReadmeMatchesGeneratedOutput(t *testing.T) { if err != nil { t.Fatalf("read README: %v", err) } + readmeText := strings.ReplaceAll(string(readme), "\r\n", "\n") const start = "\n" const end = "" - _, afterStart, ok := strings.Cut(string(readme), start) + _, afterStart, ok := strings.Cut(readmeText, start) if !ok { t.Fatal("README tools start marker is missing") }