From db2e4011407d273329375c0bbe50dc714400695c Mon Sep 17 00:00:00 2001 From: Pringled Date: Thu, 15 Oct 2026 20:49:37 +0200 Subject: [PATCH 1/6] feat: Add file-types command listing indexed extensions per content type --- README.md | 2 ++ src/semble/cli.py | 28 +++++++++++++++++++++++++++- src/semble/index/files.py | 5 +++++ tests/test_cli.py | 18 ++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 545f378e..ac6526d3 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,8 @@ generated/ # exclude generated dir !*.cob # include COBOL files ``` +Run `semble file-types` to list the extensions indexed per content type, plus data extensions (e.g. `.json`, `.csv`) that are excluded unless force-included this way. + Semble also always skips a set of well-known non-source directories regardless of ignore files (e.g. `node_modules/`, `.venv/`, `dist/`, `build/`, `__pycache__/`, and similar). diff --git a/src/semble/cli.py b/src/semble/cli.py index 94e7123c..da505a4a 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -18,6 +18,7 @@ from semble.cache import cache_key, resolve_cache_folder, save_index_to_cache from semble.index import SembleIndex +from semble.index.files import get_excluded_extensions, get_extensions from semble.index.types import PersistencePath from semble.installer.agents import AGENTS, IntegrationType from semble.stats import format_savings_report @@ -26,7 +27,19 @@ from semble.version import __version__ _CLI_DISPATCH_ARGS = frozenset( - {"search", "find-related", "install", "uninstall", "savings", "-h", "--help", "clear", "--version", "-V"} + { + "search", + "find-related", + "file-types", + "install", + "uninstall", + "savings", + "-h", + "--help", + "clear", + "--version", + "-V", + } ) _CLEAR_CHOICE = Literal["all", "index", "savings", "orphans"] @@ -292,6 +305,16 @@ def _cli_main() -> None: related_p.add_argument("line", type=int, help="Line number (1-indexed).") _add_query_args(related_p) + file_types_p = sub.add_parser("file-types", help="List the file extensions indexed per content type.") + file_types_p.add_argument( + "--content", + nargs="+", + default=["all"], + choices=[ct.value for ct in ContentType] + ["all"], + metavar="TYPE", + help="Content types to list (space-separated). Choices: code, docs, config, all. Default: all.", + ) + sub.add_parser("savings", help="Show token savings and usage stats.") install_p = sub.add_parser("install", help="Configure semble across coding agents.") @@ -332,6 +355,9 @@ def _cli_main() -> None: run(args.command, agent_ids=args.agent, integration_ids=integration_ids, yes=args.yes) elif args.command == "clear": _run_clear(args.type) + elif args.command == "file-types": + out = {ct.value: get_extensions([ct]) for ct in _resolve_content(args.content, False)} + print(json.dumps({**out, "excluded": get_excluded_extensions()})) elif args.command == "search": _run_search( args.path, diff --git a/src/semble/index/files.py b/src/semble/index/files.py index 1a7a78c5..6ce2e5d3 100644 --- a/src/semble/index/files.py +++ b/src/semble/index/files.py @@ -471,6 +471,11 @@ def get_extensions(types: Sequence[ContentType]) -> list[str]: ) +def get_excluded_extensions() -> list[str]: + """Returns the data file extensions that are not indexed under any content type.""" + return sorted(ext for lang in _DATA_LANGUAGES for ext in _LANGUAGE_TO_EXTENSIONS[lang]) + + class FileStatus(str, Enum): TOO_LARGE = "too_large" EMPTY = "empty" diff --git a/tests/test_cli.py b/tests/test_cli.py index 3a581ed1..5bb93d07 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -478,3 +478,21 @@ def test_cli_clear_command( assert not (tmp_path / sha).exists() if setup_savings: assert not savings_file.exists() + + +@pytest.mark.parametrize( + ("content", "expected_keys"), + [ + ([], ["code", "docs", "config", "excluded"]), + (["--content", "docs"], ["docs", "excluded"]), + ], +) +def test_cli_file_types( + content: list[str], expected_keys: list[str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] +) -> None: + """`semble file-types` prints extensions per requested content type plus the excluded data extensions.""" + monkeypatch.setattr(sys, "argv", ["semble", "file-types", *content]) + main() + out = json.loads(capsys.readouterr().out) + assert list(out) == expected_keys + assert ".json" in out["excluded"] From 80741483e7d5c1f28e77f73281958370061e1c1b Mon Sep 17 00:00:00 2001 From: Pringled Date: Fri, 16 Oct 2026 08:19:41 +0200 Subject: [PATCH 2/6] Rename excluded key to data in file-types output --- src/semble/cli.py | 4 ++-- src/semble/index/files.py | 2 +- tests/test_cli.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/semble/cli.py b/src/semble/cli.py index da505a4a..65d37441 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -18,7 +18,7 @@ from semble.cache import cache_key, resolve_cache_folder, save_index_to_cache from semble.index import SembleIndex -from semble.index.files import get_excluded_extensions, get_extensions +from semble.index.files import get_data_extensions, get_extensions from semble.index.types import PersistencePath from semble.installer.agents import AGENTS, IntegrationType from semble.stats import format_savings_report @@ -357,7 +357,7 @@ def _cli_main() -> None: _run_clear(args.type) elif args.command == "file-types": out = {ct.value: get_extensions([ct]) for ct in _resolve_content(args.content, False)} - print(json.dumps({**out, "excluded": get_excluded_extensions()})) + print(json.dumps({**out, "data": get_data_extensions()})) elif args.command == "search": _run_search( args.path, diff --git a/src/semble/index/files.py b/src/semble/index/files.py index 6ce2e5d3..87bc7f63 100644 --- a/src/semble/index/files.py +++ b/src/semble/index/files.py @@ -471,7 +471,7 @@ def get_extensions(types: Sequence[ContentType]) -> list[str]: ) -def get_excluded_extensions() -> list[str]: +def get_data_extensions() -> list[str]: """Returns the data file extensions that are not indexed under any content type.""" return sorted(ext for lang in _DATA_LANGUAGES for ext in _LANGUAGE_TO_EXTENSIONS[lang]) diff --git a/tests/test_cli.py b/tests/test_cli.py index 5bb93d07..853d35e9 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -483,16 +483,16 @@ def test_cli_clear_command( @pytest.mark.parametrize( ("content", "expected_keys"), [ - ([], ["code", "docs", "config", "excluded"]), - (["--content", "docs"], ["docs", "excluded"]), + ([], ["code", "docs", "config", "data"]), + (["--content", "docs"], ["docs", "data"]), ], ) def test_cli_file_types( content: list[str], expected_keys: list[str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: - """`semble file-types` prints extensions per requested content type plus the excluded data extensions.""" + """`semble file-types` prints extensions per requested content type plus the data extensions.""" monkeypatch.setattr(sys, "argv", ["semble", "file-types", *content]) main() out = json.loads(capsys.readouterr().out) assert list(out) == expected_keys - assert ".json" in out["excluded"] + assert ".json" in out["data"] From 78bc2dd818f75e7deec37e86c74ad98aa1dcf70e Mon Sep 17 00:00:00 2001 From: Pringled Date: Fri, 16 Oct 2026 08:21:49 +0200 Subject: [PATCH 3/6] Clarify file-types README note --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac6526d3..70e59450 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ generated/ # exclude generated dir !*.cob # include COBOL files ``` -Run `semble file-types` to list the extensions indexed per content type, plus data extensions (e.g. `.json`, `.csv`) that are excluded unless force-included this way. +Run `semble file-types` to see which extensions are indexed per content type. Extensions not listed are not indexed by default; data files (e.g. `.json`, `.csv`) are listed separately. Either can be force-included with a `!` pattern as above. Semble also always skips a set of well-known non-source directories regardless of ignore files (e.g. `node_modules/`, `.venv/`, `dist/`, `build/`, `__pycache__/`, and similar). From f7dc72eae6ef42074fd0acd5d1bf68306c69fe64 Mon Sep 17 00:00:00 2001 From: Pringled Date: Fri, 16 Oct 2026 08:25:12 +0200 Subject: [PATCH 4/6] Drop data extensions from file-types output --- README.md | 2 +- src/semble/cli.py | 5 ++--- src/semble/index/files.py | 5 ----- tests/test_cli.py | 8 ++++---- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 70e59450..154bbce0 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ generated/ # exclude generated dir !*.cob # include COBOL files ``` -Run `semble file-types` to see which extensions are indexed per content type. Extensions not listed are not indexed by default; data files (e.g. `.json`, `.csv`) are listed separately. Either can be force-included with a `!` pattern as above. +Run `semble file-types` to see which extensions are indexed per content type. Extensions not listed (including data files such as `.json` and `.csv`) are not indexed by default and can be force-included with a `!` pattern as above. Semble also always skips a set of well-known non-source directories regardless of ignore files (e.g. `node_modules/`, `.venv/`, `dist/`, `build/`, `__pycache__/`, and similar). diff --git a/src/semble/cli.py b/src/semble/cli.py index 65d37441..60e7e714 100644 --- a/src/semble/cli.py +++ b/src/semble/cli.py @@ -18,7 +18,7 @@ from semble.cache import cache_key, resolve_cache_folder, save_index_to_cache from semble.index import SembleIndex -from semble.index.files import get_data_extensions, get_extensions +from semble.index.files import get_extensions from semble.index.types import PersistencePath from semble.installer.agents import AGENTS, IntegrationType from semble.stats import format_savings_report @@ -356,8 +356,7 @@ def _cli_main() -> None: elif args.command == "clear": _run_clear(args.type) elif args.command == "file-types": - out = {ct.value: get_extensions([ct]) for ct in _resolve_content(args.content, False)} - print(json.dumps({**out, "data": get_data_extensions()})) + print(json.dumps({ct.value: get_extensions([ct]) for ct in _resolve_content(args.content, False)})) elif args.command == "search": _run_search( args.path, diff --git a/src/semble/index/files.py b/src/semble/index/files.py index 87bc7f63..1a7a78c5 100644 --- a/src/semble/index/files.py +++ b/src/semble/index/files.py @@ -471,11 +471,6 @@ def get_extensions(types: Sequence[ContentType]) -> list[str]: ) -def get_data_extensions() -> list[str]: - """Returns the data file extensions that are not indexed under any content type.""" - return sorted(ext for lang in _DATA_LANGUAGES for ext in _LANGUAGE_TO_EXTENSIONS[lang]) - - class FileStatus(str, Enum): TOO_LARGE = "too_large" EMPTY = "empty" diff --git a/tests/test_cli.py b/tests/test_cli.py index 853d35e9..522006b8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -483,16 +483,16 @@ def test_cli_clear_command( @pytest.mark.parametrize( ("content", "expected_keys"), [ - ([], ["code", "docs", "config", "data"]), - (["--content", "docs"], ["docs", "data"]), + ([], ["code", "docs", "config"]), + (["--content", "docs"], ["docs"]), ], ) def test_cli_file_types( content: list[str], expected_keys: list[str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: - """`semble file-types` prints extensions per requested content type plus the data extensions.""" + """`semble file-types` prints the indexed extensions per requested content type.""" monkeypatch.setattr(sys, "argv", ["semble", "file-types", *content]) main() out = json.loads(capsys.readouterr().out) assert list(out) == expected_keys - assert ".json" in out["data"] + assert all(".json" not in extensions for extensions in out.values()) From 5d415dd3a2d733171e372a773302885d05ba12f0 Mon Sep 17 00:00:00 2001 From: Pringled Date: Fri, 16 Oct 2026 08:33:33 +0200 Subject: [PATCH 5/6] Assert representative extensions in file-types test --- tests/test_cli.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 522006b8..9161a2f3 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -481,18 +481,19 @@ def test_cli_clear_command( @pytest.mark.parametrize( - ("content", "expected_keys"), + ("content", "expected"), [ - ([], ["code", "docs", "config"]), - (["--content", "docs"], ["docs"]), + ([], {"code": ".py", "docs": ".md", "config": ".toml"}), + (["--content", "docs"], {"docs": ".md"}), ], ) def test_cli_file_types( - content: list[str], expected_keys: list[str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] + content: list[str], expected: dict[str, str], monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] ) -> None: """`semble file-types` prints the indexed extensions per requested content type.""" monkeypatch.setattr(sys, "argv", ["semble", "file-types", *content]) main() out = json.loads(capsys.readouterr().out) - assert list(out) == expected_keys + assert list(out) == list(expected) + assert all(extension in out[content_type] for content_type, extension in expected.items()) assert all(".json" not in extensions for extensions in out.values()) From 437e44182bd3943cfb4f7fc8582899786caae800 Mon Sep 17 00:00:00 2001 From: Pringled Date: Fri, 16 Oct 2026 12:32:06 +0200 Subject: [PATCH 6/6] Bump version to 0.7.0 --- src/semble/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semble/version.py b/src/semble/version.py index 0a1957c5..1f5fd03f 100644 --- a/src/semble/version.py +++ b/src/semble/version.py @@ -1,2 +1,2 @@ -__version_triple__ = (0, 6, 2) +__version_triple__ = (0, 7, 0) __version__ = ".".join(map(str, __version_triple__))