defsort 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. defsort-0.1.0/.gitignore +46 -0
  2. defsort-0.1.0/LICENSE +21 -0
  3. defsort-0.1.0/PKG-INFO +150 -0
  4. defsort-0.1.0/README.md +131 -0
  5. defsort-0.1.0/_build.py +30 -0
  6. defsort-0.1.0/plugins/agy/_defsort/__init__.py +1 -0
  7. defsort-0.1.0/plugins/agy/_defsort/_adapter.py +121 -0
  8. defsort-0.1.0/plugins/agy/adapter.toml +2 -0
  9. defsort-0.1.0/plugins/agy/hooks.json +15 -0
  10. defsort-0.1.0/plugins/agy/plugin.json +5 -0
  11. defsort-0.1.0/plugins/agy/tests/test_agy_adapter.py +140 -0
  12. defsort-0.1.0/plugins/claude/.claude-plugin/plugin.json +10 -0
  13. defsort-0.1.0/plugins/claude/_defsort/__init__.py +1 -0
  14. defsort-0.1.0/plugins/claude/_defsort/_adapter.py +131 -0
  15. defsort-0.1.0/plugins/claude/adapter.toml +2 -0
  16. defsort-0.1.0/plugins/claude/hooks/hooks.json +15 -0
  17. defsort-0.1.0/plugins/claude/tests/test_claude_adapter.py +210 -0
  18. defsort-0.1.0/plugins/codex/.codex-plugin/plugin.json +20 -0
  19. defsort-0.1.0/plugins/codex/_defsort/__init__.py +1 -0
  20. defsort-0.1.0/plugins/codex/_defsort/_adapter.py +142 -0
  21. defsort-0.1.0/plugins/codex/adapter.toml +2 -0
  22. defsort-0.1.0/plugins/codex/hooks/hooks.json +15 -0
  23. defsort-0.1.0/plugins/codex/tests/test_codex_adapter.py +166 -0
  24. defsort-0.1.0/pyproject.toml +167 -0
  25. defsort-0.1.0/src/defsort/__init__.py +332 -0
  26. defsort-0.1.0/src/defsort/__main__.py +8 -0
  27. defsort-0.1.0/src/defsort/_classify.py +56 -0
  28. defsort-0.1.0/src/defsort/_cli.py +1186 -0
  29. defsort-0.1.0/src/defsort/_color.py +86 -0
  30. defsort-0.1.0/src/defsort/_config.py +576 -0
  31. defsort-0.1.0/src/defsort/_core/__init__.py +5 -0
  32. defsort-0.1.0/src/defsort/_core/_analysis.py +1237 -0
  33. defsort-0.1.0/src/defsort/_core/_api.py +373 -0
  34. defsort-0.1.0/src/defsort/_core/_banners.py +563 -0
  35. defsort-0.1.0/src/defsort/_core/_full.py +407 -0
  36. defsort-0.1.0/src/defsort/_core/_future.py +233 -0
  37. defsort-0.1.0/src/defsort/_core/_guard.py +151 -0
  38. defsort-0.1.0/src/defsort/_core/_hoist.py +238 -0
  39. defsort-0.1.0/src/defsort/_core/_incremental.py +741 -0
  40. defsort-0.1.0/src/defsort/_core/_layout.py +496 -0
  41. defsort-0.1.0/src/defsort/_core/_merge.py +123 -0
  42. defsort-0.1.0/src/defsort/_core/_methods.py +1283 -0
  43. defsort-0.1.0/src/defsort/_core/_ordering.py +394 -0
  44. defsort-0.1.0/src/defsort/_core/_parse.py +763 -0
  45. defsort-0.1.0/src/defsort/_core/_reassemble.py +42 -0
  46. defsort-0.1.0/src/defsort/_core/_safety.py +411 -0
  47. defsort-0.1.0/src/defsort/_detector.py +152 -0
  48. defsort-0.1.0/src/defsort/_display/__init__.py +16 -0
  49. defsort-0.1.0/src/defsort/_display/_constraints.py +60 -0
  50. defsort-0.1.0/src/defsort/_display/_detect.py +62 -0
  51. defsort-0.1.0/src/defsort/_display/_diff.py +105 -0
  52. defsort-0.1.0/src/defsort/_display/_json.py +138 -0
  53. defsort-0.1.0/src/defsort/_display/_movement.py +282 -0
  54. defsort-0.1.0/src/defsort/_display/_skipped.py +130 -0
  55. defsort-0.1.0/src/defsort/_display/_summary.py +211 -0
  56. defsort-0.1.0/src/defsort/_display/_terminal.py +24 -0
  57. defsort-0.1.0/src/defsort/_display/_types.py +166 -0
  58. defsort-0.1.0/src/defsort/_display/_width.py +34 -0
  59. defsort-0.1.0/src/defsort/_downstream.py +181 -0
  60. defsort-0.1.0/src/defsort/_files.py +272 -0
  61. defsort-0.1.0/src/defsort/_git.py +1021 -0
  62. defsort-0.1.0/src/defsort/_git_executable.py +51 -0
  63. defsort-0.1.0/src/defsort/_grammar.py +785 -0
  64. defsort-0.1.0/src/defsort/_hook.py +1564 -0
  65. defsort-0.1.0/src/defsort/_hosts/__init__.py +50 -0
  66. defsort-0.1.0/src/defsort/_shell.py +66 -0
  67. defsort-0.1.0/src/defsort/_shellparse.py +595 -0
  68. defsort-0.1.0/src/defsort/_source.py +140 -0
  69. defsort-0.1.0/src/defsort/_types.py +840 -0
  70. defsort-0.1.0/src/defsort/hooks/__init__.py +242 -0
  71. defsort-0.1.0/src/defsort/py.typed +0 -0
@@ -0,0 +1,46 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ dist/
13
+ .eggs/
14
+ *.egg-info/
15
+ *.egg
16
+
17
+ # Virtual environments
18
+ .venv/
19
+ venv/
20
+ ENV/
21
+
22
+ # PyInstaller
23
+ *.manifest
24
+ *.spec
25
+
26
+ # Unit test / coverage reports
27
+ htmlcov/
28
+ .tox/
29
+ .nox/
30
+ .coverage*
31
+ .pytest_cache/
32
+ .cache/
33
+
34
+ # Type checker / linter caches
35
+ .pyright/
36
+ .mypy_cache/
37
+ .ruff_cache/
38
+ pyrightconfig.json
39
+
40
+ # Editors
41
+ .vscode/
42
+ .idea/
43
+
44
+ # OS
45
+ .DS_Store
46
+ Thumbs.db
defsort-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the defsort authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
defsort-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.5
2
+ Name: defsort
3
+ Version: 0.1.0
4
+ Summary: Sort Python definitions by dependency or alphabetically
5
+ Project-URL: Documentation, https://github.com/dactylo/defsort/tree/main/docs
6
+ Project-URL: Homepage, https://github.com/dactylo/defsort
7
+ Project-URL: Issues, https://github.com/dactylo/defsort/issues
8
+ Project-URL: Repository, https://github.com/dactylo/defsort
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ast,definitions,dependency,formatter,sort
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: 3.15
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.13
18
+ Description-Content-Type: text/markdown
19
+
20
+ # defsort
21
+
22
+ [![CI](https://github.com/dactylo/defsort/actions/workflows/ci.yml/badge.svg)](https://github.com/dactylo/defsort/actions/workflows/ci.yml)
23
+
24
+ Defsort applies a configurable ordering policy to Python functions, classes, and methods. It makes
25
+ definition order deterministic, so placement follows a project convention rather than accumulating
26
+ from successive edits.
27
+
28
+ Run `defsort` to see how much would change across your project:
29
+
30
+ ![defsort summary of six Python files, with 15 of 26 definitions and methods moving](images/defsort-summary.png)
31
+
32
+ Then use `defsort --preview` to see where definitions move. Colors track definitions between the
33
+ Current and Proposed columns. For example:
34
+
35
+ ![defsort preview showing function movement in catalog.py and method movement in delivery.py](images/defsort-preview.png)
36
+
37
+ Use the CLI to preview and apply changes, an agent hook to sort before commits, or the Python
38
+ library to integrate sorting into your own tools.
39
+
40
+ ## Installation
41
+
42
+ defsort requires CPython 3.13 or later. Install the current checkout from the repository root with
43
+ [uv](https://docs.astral.sh/uv/):
44
+
45
+ ```bash
46
+ uv tool install .
47
+ ```
48
+
49
+ To update an existing installation after pulling changes:
50
+
51
+ ```bash
52
+ uv tool install --reinstall .
53
+ ```
54
+
55
+ Git 2.42 or newer is required for agent hooks, incremental mode, and a configured reorder guard.
56
+ Ordinary CLI sorting and the library do not require Git. See
57
+ [platform compatibility](docs/platform.md) for runtime requirements and file-handling details.
58
+
59
+ ## Quick start
60
+
61
+ Start with a summary, preview movement, inspect the diff, then apply the changes:
62
+
63
+ ```bash
64
+ defsort
65
+ defsort --preview
66
+ defsort --diff
67
+ defsort --write
68
+ ```
69
+
70
+ The first three commands leave files unchanged. They exit 1 when changes or skipped files need
71
+ attention; exit 2 indicates an error. A successful write exits 0 unless files were skipped.
72
+
73
+ Choose dependency or alphabetical ordering, with separate policies for top-level definitions and
74
+ class methods. Automatic sorting trades control over individual placement for consistency.
75
+ Dependency changes can produce large reorders; alphabetical order can separate related code. Use
76
+ comment banners to preserve sections. See
77
+ [ordering policies](docs/design.md#choose-an-ordering-policy) for the choices and tradeoffs.
78
+
79
+ The defaults use dependency ordering, alphabetical ties, and lifecycle methods first. No
80
+ configuration is required. To keep class methods in their authored order while sorting top-level
81
+ definitions, add:
82
+
83
+ ```toml
84
+ [tool.defsort]
85
+ method-order = "none"
86
+ ```
87
+
88
+ Defsort checks for changes that could break definition-time references and preserves decorators,
89
+ comments, and source layout. Its checks are bounded: classes whose behavior depends on method order
90
+ should use `method-order = "none"`. Read the [safety model](docs/design.md#safety-model) before
91
+ adopting it in order-sensitive code.
92
+
93
+ For an existing codebase, the [incremental guide](docs/incremental.md) explains how to establish
94
+ sorted baselines file by file and limit later reordering.
95
+
96
+ ## Integrations
97
+
98
+ - **Coding agents:** the recommended PreToolUse hook sorts staged Python content before supported
99
+ commits and preserves partial staging. Follow the [hook setup guide](docs/hook.md#configuration)
100
+ for Claude Code, Codex, or Antigravity. Choose either plugin installation or manual registration.
101
+ - **Multiple formatting hooks:** use the
102
+ [composition guide](docs/hook.md#composing-with-other-hooks) to run modifying hooks in order. It
103
+ includes the current Antigravity limitation.
104
+ - **pre-commit:** use a local registration with an installed CLI as described in the
105
+ [pre-commit guide](docs/hook.md#pre-commit).
106
+
107
+ ## Library API
108
+
109
+ ```python
110
+ import defsort
111
+
112
+ source = 'def zebra(): pass\n\ndef alpha(): pass\n'
113
+ result = defsort.check(source)
114
+ if result.status == 'changed':
115
+ source = defsort.sort(source)
116
+ ```
117
+
118
+ Use `check()` to inspect skipped or invalid source before deciding what to do. The
119
+ [library reference](docs/library.md) describes configuration, results, and the public adapter API.
120
+
121
+ ## Documentation
122
+
123
+ | If you want to... | Read |
124
+ | --------------------------------------------------- | ------------------------------------------ |
125
+ | Choose an ordering policy and understand its limits | [Concepts and safety](docs/design.md) |
126
+ | Preview, apply, or automate a sort | [CLI reference](docs/cli.md) |
127
+ | Configure agent hooks or pre-commit | [Hook guide](docs/hook.md) |
128
+ | Control reordering during adoption | [Incremental mode](docs/incremental.md) |
129
+ | Call defsort from Python | [Library reference](docs/library.md) |
130
+ | Check runtime, encoding, or path behavior | [Platform compatibility](docs/platform.md) |
131
+
132
+ ## Development
133
+
134
+ See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
135
+
136
+ Provide the tools in `[dependency-groups].dev` through your environment or package manager.
137
+ `make install` and `make reinstall` install only the CLI through uv.
138
+
139
+ ```bash
140
+ make quality # read-only quality checks
141
+ make check # quality checks and one full test run
142
+ make format # apply repository formatters
143
+ ```
144
+
145
+ To contribute an agent integration, see
146
+ [adding a bundled agent](docs/hook.md#adding-a-bundled-agent).
147
+
148
+ ## License
149
+
150
+ defsort is distributed under the [MIT License](LICENSE).
@@ -0,0 +1,131 @@
1
+ # defsort
2
+
3
+ [![CI](https://github.com/dactylo/defsort/actions/workflows/ci.yml/badge.svg)](https://github.com/dactylo/defsort/actions/workflows/ci.yml)
4
+
5
+ Defsort applies a configurable ordering policy to Python functions, classes, and methods. It makes
6
+ definition order deterministic, so placement follows a project convention rather than accumulating
7
+ from successive edits.
8
+
9
+ Run `defsort` to see how much would change across your project:
10
+
11
+ ![defsort summary of six Python files, with 15 of 26 definitions and methods moving](images/defsort-summary.png)
12
+
13
+ Then use `defsort --preview` to see where definitions move. Colors track definitions between the
14
+ Current and Proposed columns. For example:
15
+
16
+ ![defsort preview showing function movement in catalog.py and method movement in delivery.py](images/defsort-preview.png)
17
+
18
+ Use the CLI to preview and apply changes, an agent hook to sort before commits, or the Python
19
+ library to integrate sorting into your own tools.
20
+
21
+ ## Installation
22
+
23
+ defsort requires CPython 3.13 or later. Install the current checkout from the repository root with
24
+ [uv](https://docs.astral.sh/uv/):
25
+
26
+ ```bash
27
+ uv tool install .
28
+ ```
29
+
30
+ To update an existing installation after pulling changes:
31
+
32
+ ```bash
33
+ uv tool install --reinstall .
34
+ ```
35
+
36
+ Git 2.42 or newer is required for agent hooks, incremental mode, and a configured reorder guard.
37
+ Ordinary CLI sorting and the library do not require Git. See
38
+ [platform compatibility](docs/platform.md) for runtime requirements and file-handling details.
39
+
40
+ ## Quick start
41
+
42
+ Start with a summary, preview movement, inspect the diff, then apply the changes:
43
+
44
+ ```bash
45
+ defsort
46
+ defsort --preview
47
+ defsort --diff
48
+ defsort --write
49
+ ```
50
+
51
+ The first three commands leave files unchanged. They exit 1 when changes or skipped files need
52
+ attention; exit 2 indicates an error. A successful write exits 0 unless files were skipped.
53
+
54
+ Choose dependency or alphabetical ordering, with separate policies for top-level definitions and
55
+ class methods. Automatic sorting trades control over individual placement for consistency.
56
+ Dependency changes can produce large reorders; alphabetical order can separate related code. Use
57
+ comment banners to preserve sections. See
58
+ [ordering policies](docs/design.md#choose-an-ordering-policy) for the choices and tradeoffs.
59
+
60
+ The defaults use dependency ordering, alphabetical ties, and lifecycle methods first. No
61
+ configuration is required. To keep class methods in their authored order while sorting top-level
62
+ definitions, add:
63
+
64
+ ```toml
65
+ [tool.defsort]
66
+ method-order = "none"
67
+ ```
68
+
69
+ Defsort checks for changes that could break definition-time references and preserves decorators,
70
+ comments, and source layout. Its checks are bounded: classes whose behavior depends on method order
71
+ should use `method-order = "none"`. Read the [safety model](docs/design.md#safety-model) before
72
+ adopting it in order-sensitive code.
73
+
74
+ For an existing codebase, the [incremental guide](docs/incremental.md) explains how to establish
75
+ sorted baselines file by file and limit later reordering.
76
+
77
+ ## Integrations
78
+
79
+ - **Coding agents:** the recommended PreToolUse hook sorts staged Python content before supported
80
+ commits and preserves partial staging. Follow the [hook setup guide](docs/hook.md#configuration)
81
+ for Claude Code, Codex, or Antigravity. Choose either plugin installation or manual registration.
82
+ - **Multiple formatting hooks:** use the
83
+ [composition guide](docs/hook.md#composing-with-other-hooks) to run modifying hooks in order. It
84
+ includes the current Antigravity limitation.
85
+ - **pre-commit:** use a local registration with an installed CLI as described in the
86
+ [pre-commit guide](docs/hook.md#pre-commit).
87
+
88
+ ## Library API
89
+
90
+ ```python
91
+ import defsort
92
+
93
+ source = 'def zebra(): pass\n\ndef alpha(): pass\n'
94
+ result = defsort.check(source)
95
+ if result.status == 'changed':
96
+ source = defsort.sort(source)
97
+ ```
98
+
99
+ Use `check()` to inspect skipped or invalid source before deciding what to do. The
100
+ [library reference](docs/library.md) describes configuration, results, and the public adapter API.
101
+
102
+ ## Documentation
103
+
104
+ | If you want to... | Read |
105
+ | --------------------------------------------------- | ------------------------------------------ |
106
+ | Choose an ordering policy and understand its limits | [Concepts and safety](docs/design.md) |
107
+ | Preview, apply, or automate a sort | [CLI reference](docs/cli.md) |
108
+ | Configure agent hooks or pre-commit | [Hook guide](docs/hook.md) |
109
+ | Control reordering during adoption | [Incremental mode](docs/incremental.md) |
110
+ | Call defsort from Python | [Library reference](docs/library.md) |
111
+ | Check runtime, encoding, or path behavior | [Platform compatibility](docs/platform.md) |
112
+
113
+ ## Development
114
+
115
+ See the [development guide](docs/development.md) for pinned CI dependencies and lock updates.
116
+
117
+ Provide the tools in `[dependency-groups].dev` through your environment or package manager.
118
+ `make install` and `make reinstall` install only the CLI through uv.
119
+
120
+ ```bash
121
+ make quality # read-only quality checks
122
+ make check # quality checks and one full test run
123
+ make format # apply repository formatters
124
+ ```
125
+
126
+ To contribute an agent integration, see
127
+ [adding a bundled agent](docs/hook.md#adding-a-bundled-agent).
128
+
129
+ ## License
130
+
131
+ defsort is distributed under the [MIT License](LICENSE).
@@ -0,0 +1,30 @@
1
+ """Collect bundled agent entry points for distribution metadata."""
2
+
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import override
6
+
7
+ from hatchling.metadata.plugin.interface import MetadataHookInterface
8
+
9
+
10
+ class PluginMetadataHook(MetadataHookInterface):
11
+ """Include each bundled plugin's registration in the containing distribution."""
12
+
13
+ @override
14
+ def update(self, metadata: dict[str, object]) -> None:
15
+ metadata['entry-points'] = _plugin_entry_points(Path(self.root))
16
+
17
+
18
+ def _plugin_entry_points(root: Path) -> dict[str, dict[str, str]]:
19
+ """Read entry points declared by the root's agent plugin directories."""
20
+ groups: dict[str, dict[str, str]] = {}
21
+ for manifest in sorted((root / 'plugins').glob('*/adapter.toml')):
22
+ with manifest.open('rb') as stream:
23
+ entries = tomllib.load(stream)['entry-points']
24
+ for group, targets in entries.items():
25
+ registered = groups.setdefault(group, {})
26
+ for name, target in targets.items():
27
+ if name in registered:
28
+ raise ValueError(f'Duplicate entry point {group}:{name} in {manifest}')
29
+ registered[name] = target
30
+ return groups
@@ -0,0 +1 @@
1
+ """Antigravity adapter package for defsort."""
@@ -0,0 +1,121 @@
1
+ """Antigravity hook adapter."""
2
+
3
+ import json
4
+ from collections.abc import Mapping
5
+ from typing import cast
6
+
7
+ from defsort.hooks import (
8
+ API_VERSION,
9
+ HookResult,
10
+ HostInput,
11
+ HostResponse,
12
+ absolute_directory,
13
+ notes_text,
14
+ object_field,
15
+ payload_cwd,
16
+ )
17
+
18
+ _COMMAND_TOOL = 'run_command'
19
+ _EDIT_TOOLS = frozenset({'replace_file_content', 'write_to_file'})
20
+ _EVENTS = {'PreToolUse': 'pre_tool', 'PostToolUse': 'post_tool'}
21
+
22
+
23
+ def _allow_response(result: HookResult) -> HostResponse:
24
+ """Render Antigravity allow feedback."""
25
+ stdout = f'{json.dumps({"systemMessage": result.reason})}\n' if result.reason else ''
26
+ return HostResponse(stdout, notes_text(result), 0)
27
+
28
+
29
+ def _base_cwd(payload: Mapping[str, object], args: Mapping[str, object]) -> str:
30
+ """Resolve the target directory from Cwd, workspace paths, or the process."""
31
+ cwd = args.get('Cwd')
32
+ if isinstance(cwd, str) and cwd:
33
+ return cwd
34
+ workspaces = payload.get('workspacePaths')
35
+ if isinstance(workspaces, list):
36
+ first = cast('list[object]', workspaces)[:1]
37
+ if first and isinstance(first[0], str) and first[0]:
38
+ return first[0]
39
+ return payload_cwd(payload)
40
+
41
+
42
+ def _deny(reason: str, stderr_prefix: str = '') -> HostResponse:
43
+ """Render Antigravity's top-level deny object."""
44
+ stdout = json.dumps({'decision': 'deny', 'reason': reason})
45
+ return HostResponse(f'{stdout}\n', f'{stderr_prefix}{reason}\n', 0)
46
+
47
+
48
+ class AntigravityAdapter:
49
+ """Translate Antigravity tool-call payloads, which carry no event name."""
50
+
51
+ name = 'agy'
52
+ api_version = API_VERSION
53
+ requires_event = True
54
+
55
+ def parse(self, payload: Mapping[str, object], event: str | None) -> HostInput | None:
56
+ """Read the tool-call name and arguments under the given event.
57
+
58
+ For `run_command`, pre-tool `command_cwd` is `toolCall.args.Cwd` when
59
+ it is an absolute path; `workspacePaths` and the process directory are
60
+ never invocation evidence. Other tools report `None`.
61
+
62
+ Args:
63
+ payload: Decoded Antigravity hook payload.
64
+ event: PreToolUse or PostToolUse from the command line.
65
+
66
+ Returns:
67
+ Neutral input. An unrelated tool yields input with nothing to act
68
+ on.
69
+
70
+ Raises:
71
+ ValueError: If event is not an Antigravity hook event.
72
+ """
73
+ neutral_event = _EVENTS.get(event or '')
74
+ if neutral_event is None:
75
+ raise ValueError(
76
+ f'unknown Antigravity event {event!r}; expected PreToolUse or PostToolUse'
77
+ )
78
+ tool_call = object_field(payload, 'toolCall')
79
+ args = object_field(tool_call, 'args')
80
+ cwd = _base_cwd(payload, args)
81
+ name = tool_call.get('name')
82
+ if neutral_event == 'pre_tool':
83
+ command = args.get('CommandLine') if name == _COMMAND_TOOL else None
84
+ command_cwd = absolute_directory(args, 'Cwd') if name == _COMMAND_TOOL else None
85
+ return HostInput(
86
+ 'pre_tool',
87
+ command if isinstance(command, str) else None,
88
+ cwd,
89
+ command_cwd,
90
+ )
91
+ editing = isinstance(name, str) and name in _EDIT_TOOLS
92
+ target = args.get('TargetFile') if editing else None
93
+ paths = (target,) if isinstance(target, str) and target else ()
94
+ return HostInput('post_tool', None, cwd, None, paths)
95
+
96
+ def render(self, result: HookResult) -> HostResponse:
97
+ """Render allow feedback or the top-level deny object.
98
+
99
+ Args:
100
+ result: Decision, reason, and notes from defsort.
101
+
102
+ Returns:
103
+ Empty stdout for a silent allow, or one system message for allow
104
+ feedback; notes on stderr and exit 0. For deny, the deny object on
105
+ stdout, the reason on stderr, and exit 0.
106
+ """
107
+ notes = notes_text(result)
108
+ if result.decision == 'deny':
109
+ return _deny(result.reason or 'defsort denied the operation', notes)
110
+ return _allow_response(result)
111
+
112
+ def render_failure(self, reason: str) -> HostResponse:
113
+ """Render the top-level deny object.
114
+
115
+ Args:
116
+ reason: Reason for the failure.
117
+
118
+ Returns:
119
+ The deny object on stdout, the reason on stderr, and exit 0.
120
+ """
121
+ return _deny(reason)
@@ -0,0 +1,2 @@
1
+ [entry-points."defsort.hook_adapters"]
2
+ agy = "plugins.agy._defsort._adapter:AntigravityAdapter"
@@ -0,0 +1,15 @@
1
+ {
2
+ "defsort": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "run_command",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "defsort hook --agent agy --event PreToolUse"
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "$schema": "https://antigravity.google/schemas/v1/plugin.json",
3
+ "name": "defsort-agy",
4
+ "description": "Install the defsort commit-time definition sorting hook for Antigravity."
5
+ }
@@ -0,0 +1,140 @@
1
+ # pyright: reportPrivateUsage=false
2
+
3
+ import json
4
+ import shlex
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Literal
8
+
9
+ import pytest
10
+
11
+ from defsort._cli import _build_hook_parser
12
+ from defsort.hooks import API_VERSION, HookAdapter, HookResult, HostInput, HostResponse
13
+ from plugins.agy._defsort._adapter import AntigravityAdapter
14
+
15
+ PLUGIN_ROOT = Path(__file__).resolve().parents[1]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class _Result:
20
+ decision: Literal['allow', 'deny']
21
+ reason: str | None = None
22
+ notes: tuple[str, ...] = ()
23
+
24
+
25
+ def _payload(tool: str, args: dict[str, object]) -> dict[str, object]:
26
+ return {'toolCall': {'name': tool, 'args': args}}
27
+
28
+
29
+ def _result(
30
+ decision: Literal['allow', 'deny'],
31
+ reason: str | None = None,
32
+ notes: tuple[str, ...] = (),
33
+ ) -> HookResult:
34
+ return _Result(decision, reason, notes)
35
+
36
+
37
+ def test_contract() -> None:
38
+ adapter: HookAdapter = AntigravityAdapter()
39
+ assert (adapter.name, adapter.api_version, adapter.requires_event) == (
40
+ 'agy',
41
+ API_VERSION,
42
+ True,
43
+ )
44
+
45
+
46
+ def test_manifest_registers_only_pre_tool_use() -> None:
47
+ manifest = PLUGIN_ROOT / 'hooks.json'
48
+ assert json.loads(manifest.read_text(encoding='utf-8')) == {
49
+ 'defsort': {
50
+ 'PreToolUse': [
51
+ {
52
+ 'matcher': 'run_command',
53
+ 'hooks': [
54
+ {
55
+ 'type': 'command',
56
+ 'command': 'defsort hook --agent agy --event PreToolUse',
57
+ }
58
+ ],
59
+ }
60
+ ]
61
+ }
62
+ }
63
+ argv = shlex.split('defsort hook --agent agy --event PreToolUse')
64
+ args = _build_hook_parser().parse_args(argv[2:])
65
+ assert args.agent == 'agy'
66
+ assert (args.event is not None) == AntigravityAdapter.requires_event
67
+
68
+
69
+ @pytest.mark.parametrize('tool', ['write_to_file', 'replace_file_content'])
70
+ def test_parse_post_tool_file_writers(tool: str) -> None:
71
+ payload = _payload(tool, {'TargetFile': '/t/a.py', 'Cwd': '/t'})
72
+ assert AntigravityAdapter().parse(payload, 'PostToolUse') == HostInput(
73
+ 'post_tool', None, '/t', None, ('/t/a.py',)
74
+ )
75
+
76
+
77
+ def test_parse_pre_tool_other_tool_has_no_command() -> None:
78
+ payload = _payload('write_to_file', {'TargetFile': 'a.py', 'Cwd': '/t'})
79
+ assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
80
+ 'pre_tool', None, '/t', None
81
+ )
82
+
83
+
84
+ def test_parse_rejects_unknown_event() -> None:
85
+ with pytest.raises(ValueError, match='unknown Antigravity event'):
86
+ AntigravityAdapter().parse({}, 'Stop')
87
+
88
+
89
+ @pytest.mark.parametrize('cwd', ['rel', '', 7])
90
+ def test_parse_run_command_non_absolute_cwd_is_unknown(cwd: object) -> None:
91
+ payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
92
+ payload['workspacePaths'] = ['/ws']
93
+ parsed = AntigravityAdapter().parse(payload, 'PreToolUse')
94
+ assert parsed is not None
95
+ assert parsed.command_cwd is None
96
+
97
+
98
+ def test_parse_run_command_prefers_cwd_over_workspace(tmp_path: Path) -> None:
99
+ cwd = str(tmp_path / 'command')
100
+ payload = _payload('run_command', {'CommandLine': 'git commit -m x', 'Cwd': cwd})
101
+ payload['workspacePaths'] = [str(tmp_path / 'workspace')]
102
+ assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
103
+ 'pre_tool', 'git commit -m x', cwd, cwd
104
+ )
105
+
106
+
107
+ def test_parse_run_command_without_cwd_is_unknown() -> None:
108
+ payload = _payload('run_command', {'CommandLine': 'git commit -m x'})
109
+ payload['workspacePaths'] = ['/ws']
110
+ assert AntigravityAdapter().parse(payload, 'PreToolUse') == HostInput(
111
+ 'pre_tool', 'git commit -m x', '/ws', None
112
+ )
113
+
114
+
115
+ def test_plugin_manifest_uses_defsort_identity() -> None:
116
+ manifest = json.loads((PLUGIN_ROOT / 'plugin.json').read_text(encoding='utf-8'))
117
+ assert manifest == {
118
+ '$schema': 'https://antigravity.google/schemas/v1/plugin.json',
119
+ 'name': 'defsort-agy',
120
+ 'description': 'Install the defsort commit-time definition sorting hook for Antigravity.',
121
+ }
122
+
123
+
124
+ def test_render_allow_and_failure() -> None:
125
+ assert AntigravityAdapter().render(_result('allow')) == HostResponse('', '', 0)
126
+ failure = AntigravityAdapter().render_failure('defsort: reason')
127
+ assert json.loads(failure.stdout) == {'decision': 'deny', 'reason': 'defsort: reason'}
128
+
129
+
130
+ def test_render_allow_feedback_is_one_system_message() -> None:
131
+ response = AntigravityAdapter().render(_result('allow', 'first\nsecond', ('diagnostic',)))
132
+ assert json.loads(response.stdout) == {'systemMessage': 'first\nsecond'}
133
+ assert response.stderr == 'diagnostic\n'
134
+ assert response.exit_code == 0
135
+
136
+
137
+ def test_render_deny_is_top_level_json() -> None:
138
+ response = AntigravityAdapter().render(_result('deny', 'defsort: reason', ('note',)))
139
+ assert json.loads(response.stdout) == {'decision': 'deny', 'reason': 'defsort: reason'}
140
+ assert (response.stderr, response.exit_code) == ('note\ndefsort: reason\n', 0)
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "defsort",
3
+ "version": "0.1.0",
4
+ "description": "Install the defsort commit-time definition sorting hook for Claude Code.",
5
+ "author": {
6
+ "name": "defsort contributors"
7
+ },
8
+ "license": "MIT",
9
+ "keywords": ["definitions", "dependency", "hooks", "python", "sorting"]
10
+ }