fix(compress): never classify build files or scripts as prose

detect.py listed .dockerfile/.makefile in SKIP_EXTENSIONS, but real
files are named Dockerfile/Makefile with no extension, so they fell
through to the content heuristic and came back compressible —
/caveman-compress Dockerfile overwrote a Dockerfile with caveman prose.

Add a basename guard (Dockerfile, Makefile, Jenkinsfile, Vagrantfile,
CMakeLists.txt, ...) checked before any extension rule — CMakeLists.txt
would otherwise ride the compressible .txt rule — and a shebang check
in the extensionless branch so executable scripts are always code.

Mirror synced to plugins/caveman (sync workflow only triggers on
SKILL.md changes, so scripts/ must ride along).

Fixes #600
This commit is contained in:
AmirF194
2026-07-01 23:36:17 -06:00
parent 25d22f864a
commit 4dad1afe84
3 changed files with 136 additions and 0 deletions
@@ -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):
+18
View File
@@ -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):
+100
View File
@@ -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()