aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-26 01:56:14 -0500
committerCraig Jennings <c@cjennings.net>2026-05-26 01:56:14 -0500
commit98382929852b213f8dc8b1ba720cc0d1861159b6 (patch)
tree004d12d6ddfc21af0c999a6faca8f00332ed4f8d
parent8abcc1adc40b0609d3af58aedf003344209e3ad2 (diff)
downloadrulesets-98382929852b213f8dc8b1ba720cc0d1861159b6.tar.gz
rulesets-98382929852b213f8dc8b1ba720cc0d1861159b6.zip
fix(inbox-send): preserve dots in copied filenames
send_file ran filenames through slugify(), which flattens dots to hyphens. That corrupts the engine.plugin.org plugin-namespace convention: triage-intake.personal-gmail.org arrived as triage-intake-personal-gmail.org, which breaks the engine's triage-intake.*.org glob and the routing that depends on the first dot. I added slugify_filename() for filename stems. It keeps dots, hyphens, underscores, and case, collapses only whitespace runs to hyphens, and truncates on a separator boundary. The prose --text path still uses slugify().
-rw-r--r--.ai/scripts/inbox-send.py28
-rw-r--r--.ai/scripts/tests/test_inbox_send.py28
-rw-r--r--claude-templates/.ai/scripts/inbox-send.py28
-rw-r--r--claude-templates/.ai/scripts/tests/test_inbox_send.py28
4 files changed, 110 insertions, 2 deletions
diff --git a/.ai/scripts/inbox-send.py b/.ai/scripts/inbox-send.py
index 8e650ff..5373bd4 100644
--- a/.ai/scripts/inbox-send.py
+++ b/.ai/scripts/inbox-send.py
@@ -110,6 +110,32 @@ def slugify(text: str, max_length: int = MAX_SLUG_LENGTH) -> str:
return truncated.strip().replace(" ", "-")
+def slugify_filename(stem: str, max_length: int = MAX_SLUG_LENGTH) -> str:
+ """Slugify a filename stem while preserving its structure.
+
+ Unlike slugify() (for freeform prose), a filename stem is already
+ filename-safe and carries meaning in its separators — dots especially.
+ The engine.plugin.org plugin-namespace convention encodes the
+ engine/plugin boundary in the first dot, so flattening dots to hyphens
+ corrupts the name. Keep [A-Za-z0-9._-] and case (e.g. the TOOLARGE-
+ prefix convention), turn whitespace runs into single hyphens, drop
+ anything else.
+ """
+ stem = re.sub(r"\s+", "-", stem.strip())
+ stem = re.sub(r"[^A-Za-z0-9._-]+", "", stem)
+ stem = re.sub(r"-{2,}", "-", stem).strip("-._")
+ if not stem:
+ return ""
+ if len(stem) <= max_length:
+ return stem
+ truncated = stem[:max_length]
+ # Walk back to the last separator so truncation doesn't cut mid-segment.
+ last_sep = max(truncated.rfind("-"), truncated.rfind("."), truncated.rfind("_"))
+ if last_sep > 0:
+ truncated = truncated[:last_sep]
+ return truncated.strip("-._")
+
+
def find_target(target_name: str, projects: list[Path]) -> Path | None:
"""Resolve `target_name` against the project list (basename or numeric index)."""
if target_name.isdigit():
@@ -163,7 +189,7 @@ def send_file(
"""Copy src_path into target_inbox with a dated, source-tagged name."""
if not src_path.is_file():
raise FileNotFoundError(f"source file not found: {src_path}")
- slug = custom_name or slugify(src_path.stem)
+ slug = custom_name or slugify_filename(src_path.stem)
if not slug:
raise ValueError(f"could not derive a slug from file: {src_path}")
ext = src_path.suffix
diff --git a/.ai/scripts/tests/test_inbox_send.py b/.ai/scripts/tests/test_inbox_send.py
index 597a7e9..a0094dc 100644
--- a/.ai/scripts/tests/test_inbox_send.py
+++ b/.ai/scripts/tests/test_inbox_send.py
@@ -248,6 +248,34 @@ class TestInboxSendFile:
files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
assert "branching-strategy-notes" in files[0].name
+ def test_inbox_send_file_preserves_dots_in_basename(self, project_root, run_script, tmp_path):
+ """Boundary: a dotted stem keeps its dots — the engine.plugin.org plugin namespace must survive transit."""
+ project_root("target")
+ cwd = project_root("source")
+ src = tmp_path / "triage-intake.personal-gmail.org"
+ src.write_text("plugin")
+ run_script(
+ ["target", "--file", str(src)],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
+ assert len(files) == 1
+ assert _slug_from(files, "source") == "triage-intake.personal-gmail"
+ assert files[0].suffix == ".org"
+
+ def test_inbox_send_file_spaces_become_hyphens(self, project_root, run_script, tmp_path):
+ """Normal: whitespace in a filename stem still collapses to hyphens; dots are what's preserved, not spaces."""
+ project_root("target")
+ cwd = project_root("source")
+ src = tmp_path / "meeting notes draft.org"
+ src.write_text("x")
+ run_script(
+ ["target", "--file", str(src)],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
+ assert _slug_from(files, "source") == "meeting-notes-draft"
+
def test_inbox_send_file_name_override(self, project_root, run_script, tmp_path):
"""Normal: --name overrides the basename-derived slug; extension preserved."""
project_root("target")
diff --git a/claude-templates/.ai/scripts/inbox-send.py b/claude-templates/.ai/scripts/inbox-send.py
index 8e650ff..5373bd4 100644
--- a/claude-templates/.ai/scripts/inbox-send.py
+++ b/claude-templates/.ai/scripts/inbox-send.py
@@ -110,6 +110,32 @@ def slugify(text: str, max_length: int = MAX_SLUG_LENGTH) -> str:
return truncated.strip().replace(" ", "-")
+def slugify_filename(stem: str, max_length: int = MAX_SLUG_LENGTH) -> str:
+ """Slugify a filename stem while preserving its structure.
+
+ Unlike slugify() (for freeform prose), a filename stem is already
+ filename-safe and carries meaning in its separators — dots especially.
+ The engine.plugin.org plugin-namespace convention encodes the
+ engine/plugin boundary in the first dot, so flattening dots to hyphens
+ corrupts the name. Keep [A-Za-z0-9._-] and case (e.g. the TOOLARGE-
+ prefix convention), turn whitespace runs into single hyphens, drop
+ anything else.
+ """
+ stem = re.sub(r"\s+", "-", stem.strip())
+ stem = re.sub(r"[^A-Za-z0-9._-]+", "", stem)
+ stem = re.sub(r"-{2,}", "-", stem).strip("-._")
+ if not stem:
+ return ""
+ if len(stem) <= max_length:
+ return stem
+ truncated = stem[:max_length]
+ # Walk back to the last separator so truncation doesn't cut mid-segment.
+ last_sep = max(truncated.rfind("-"), truncated.rfind("."), truncated.rfind("_"))
+ if last_sep > 0:
+ truncated = truncated[:last_sep]
+ return truncated.strip("-._")
+
+
def find_target(target_name: str, projects: list[Path]) -> Path | None:
"""Resolve `target_name` against the project list (basename or numeric index)."""
if target_name.isdigit():
@@ -163,7 +189,7 @@ def send_file(
"""Copy src_path into target_inbox with a dated, source-tagged name."""
if not src_path.is_file():
raise FileNotFoundError(f"source file not found: {src_path}")
- slug = custom_name or slugify(src_path.stem)
+ slug = custom_name or slugify_filename(src_path.stem)
if not slug:
raise ValueError(f"could not derive a slug from file: {src_path}")
ext = src_path.suffix
diff --git a/claude-templates/.ai/scripts/tests/test_inbox_send.py b/claude-templates/.ai/scripts/tests/test_inbox_send.py
index 597a7e9..a0094dc 100644
--- a/claude-templates/.ai/scripts/tests/test_inbox_send.py
+++ b/claude-templates/.ai/scripts/tests/test_inbox_send.py
@@ -248,6 +248,34 @@ class TestInboxSendFile:
files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
assert "branching-strategy-notes" in files[0].name
+ def test_inbox_send_file_preserves_dots_in_basename(self, project_root, run_script, tmp_path):
+ """Boundary: a dotted stem keeps its dots — the engine.plugin.org plugin namespace must survive transit."""
+ project_root("target")
+ cwd = project_root("source")
+ src = tmp_path / "triage-intake.personal-gmail.org"
+ src.write_text("plugin")
+ run_script(
+ ["target", "--file", str(src)],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
+ assert len(files) == 1
+ assert _slug_from(files, "source") == "triage-intake.personal-gmail"
+ assert files[0].suffix == ".org"
+
+ def test_inbox_send_file_spaces_become_hyphens(self, project_root, run_script, tmp_path):
+ """Normal: whitespace in a filename stem still collapses to hyphens; dots are what's preserved, not spaces."""
+ project_root("target")
+ cwd = project_root("source")
+ src = tmp_path / "meeting notes draft.org"
+ src.write_text("x")
+ run_script(
+ ["target", "--file", str(src)],
+ cwd=cwd, roots=[tmp_path / "projects"],
+ )
+ files = list((tmp_path / "projects" / "target" / "inbox").iterdir())
+ assert _slug_from(files, "source") == "meeting-notes-draft"
+
def test_inbox_send_file_name_override(self, project_root, run_script, tmp_path):
"""Normal: --name overrides the basename-derived slug; extension preserved."""
project_root("target")