diff --git a/plugins/caveman/skills/caveman-compress/scripts/detect.py b/plugins/caveman/skills/caveman-compress/scripts/detect.py index 8d5f6d7..5114d04 100644 --- a/plugins/caveman/skills/caveman-compress/scripts/detect.py +++ b/plugins/caveman/skills/caveman-compress/scripts/detect.py @@ -17,6 +17,16 @@ SKIP_EXTENSIONS = { ".dockerfile", ".makefile", ".csv", ".ini", ".cfg", } +# Well-known build/config files that carry no (or a misleading) extension — +# `Dockerfile` has no suffix so `.dockerfile` above never matches it, and +# `CMakeLists.txt` would ride the compressible `.txt` rule. Checked by +# basename before any extension rule. +KNOWN_CODE_FILENAMES = { + "dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile", + "rakefile", "gemfile", "justfile", "procfile", "brewfile", + "cmakelists.txt", +} + # Patterns that indicate a line is code CODE_PATTERNS = [ re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"), @@ -67,6 +77,10 @@ def detect_file_type(filepath: Path) -> str: """ ext = filepath.suffix.lower() + # Known code filenames win over any extension rule + if filepath.name.lower() in KNOWN_CODE_FILENAMES: + return "code" + # Extension-based classification if ext in COMPRESSIBLE_EXTENSIONS: return "natural_language" @@ -82,6 +96,10 @@ def detect_file_type(filepath: Path) -> str: lines = text.splitlines()[:50] + # Shebang means executable script, never prose + if text.startswith("#!"): + return "code" + if _is_json_content(text[:10000]): return "config" if _is_yaml_content(lines): diff --git a/skills/caveman-compress/scripts/detect.py b/skills/caveman-compress/scripts/detect.py index 8d5f6d7..5114d04 100644 --- a/skills/caveman-compress/scripts/detect.py +++ b/skills/caveman-compress/scripts/detect.py @@ -17,6 +17,16 @@ SKIP_EXTENSIONS = { ".dockerfile", ".makefile", ".csv", ".ini", ".cfg", } +# Well-known build/config files that carry no (or a misleading) extension — +# `Dockerfile` has no suffix so `.dockerfile` above never matches it, and +# `CMakeLists.txt` would ride the compressible `.txt` rule. Checked by +# basename before any extension rule. +KNOWN_CODE_FILENAMES = { + "dockerfile", "makefile", "gnumakefile", "jenkinsfile", "vagrantfile", + "rakefile", "gemfile", "justfile", "procfile", "brewfile", + "cmakelists.txt", +} + # Patterns that indicate a line is code CODE_PATTERNS = [ re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"), @@ -67,6 +77,10 @@ def detect_file_type(filepath: Path) -> str: """ ext = filepath.suffix.lower() + # Known code filenames win over any extension rule + if filepath.name.lower() in KNOWN_CODE_FILENAMES: + return "code" + # Extension-based classification if ext in COMPRESSIBLE_EXTENSIONS: return "natural_language" @@ -82,6 +96,10 @@ def detect_file_type(filepath: Path) -> str: lines = text.splitlines()[:50] + # Shebang means executable script, never prose + if text.startswith("#!"): + return "code" + if _is_json_content(text[:10000]): return "config" if _is_yaml_content(lines): diff --git a/tests/test_detect.py b/tests/test_detect.py new file mode 100644 index 0000000..a690711 --- /dev/null +++ b/tests/test_detect.py @@ -0,0 +1,100 @@ +"""Tests for detect.py file-type classification (issue #600). + +Extensionless build files (`Dockerfile`, `Makefile`) and shebang scripts +used to fall through to the content heuristic and come back as +compressible natural language — so `/caveman-compress Dockerfile` would +overwrite a Dockerfile with caveman prose. These tests pin the basename +and shebang guards. +""" + +import sys +import tempfile +import unittest +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO_ROOT / "skills" / "caveman-compress")) + +from scripts.detect import detect_file_type, should_compress # noqa: E402 + + +DOCKERFILE_BODY = """FROM node:20-bookworm +WORKDIR /app +COPY package.json . +RUN npm install +CMD ["node", "index.js"] +""" + +MAKEFILE_BODY = """all: build + +build: +\tgo build -o bin/app ./cmd/app + +clean: +\trm -rf bin +""" + +SHEBANG_BODY = """#!/usr/bin/env bash +set -euo pipefail +echo "deploying" +""" + +PROSE_BODY = """This project collects notes about our deployment process. + +The main goal is to keep the steps simple enough that anyone on the +team can run a release without asking for help. Start by reading the +overview, then follow the checklist in order. +""" + + +class DetectFileTypeTests(unittest.TestCase): + def _write(self, dirpath, name, body): + p = Path(dirpath) / name + p.write_text(body, encoding="utf-8") + return p + + def test_dockerfile_is_code(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "Dockerfile", DOCKERFILE_BODY) + self.assertEqual(detect_file_type(p), "code") + self.assertFalse(should_compress(p)) + + def test_makefile_is_code(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "Makefile", MAKEFILE_BODY) + self.assertEqual(detect_file_type(p), "code") + self.assertFalse(should_compress(p)) + + def test_known_names_case_insensitive(self): + with tempfile.TemporaryDirectory() as tmp: + for name in ("dockerfile", "MAKEFILE", "Jenkinsfile", "Vagrantfile"): + p = self._write(tmp, name, "irrelevant body\n") + self.assertEqual(detect_file_type(p), "code", name) + + def test_cmakelists_txt_not_compressible_despite_txt_extension(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "CMakeLists.txt", "add_executable(app main.c)\n") + self.assertEqual(detect_file_type(p), "code") + self.assertFalse(should_compress(p)) + + def test_shebang_script_is_code(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "deploy", SHEBANG_BODY) + self.assertEqual(detect_file_type(p), "code") + self.assertFalse(should_compress(p)) + + def test_extensionless_prose_still_compressible(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "NOTES", PROSE_BODY) + self.assertEqual(detect_file_type(p), "natural_language") + self.assertTrue(should_compress(p)) + + def test_markdown_still_compressible(self): + with tempfile.TemporaryDirectory() as tmp: + p = self._write(tmp, "README.md", PROSE_BODY) + self.assertEqual(detect_file_type(p), "natural_language") + self.assertTrue(should_compress(p)) + + +if __name__ == "__main__": + unittest.main()