subedit 0.3.2__tar.gz → 0.5.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 (163) hide show
  1. {subedit-0.3.2 → subedit-0.5.0}/.claude-plugin/marketplace.json +1 -1
  2. {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/ci.yml +20 -10
  3. {subedit-0.3.2 → subedit-0.5.0}/.gitignore +6 -1
  4. subedit-0.5.0/.vale.ini +16 -0
  5. subedit-0.5.0/PKG-INFO +400 -0
  6. subedit-0.5.0/README.md +362 -0
  7. subedit-0.5.0/docs/assets/cover.svg +1 -0
  8. subedit-0.5.0/docs/assets/icon.svg +4 -0
  9. subedit-0.5.0/docs/assets/layers.excalidraw +1700 -0
  10. subedit-0.5.0/docs/assets/layers.svg +2 -0
  11. subedit-0.5.0/docs/assets/logo.svg +4 -0
  12. subedit-0.5.0/docs/assets/pipeline.excalidraw +1230 -0
  13. subedit-0.5.0/docs/assets/pipeline.svg +2 -0
  14. subedit-0.5.0/docs/assets/wordmark.svg +4 -0
  15. subedit-0.5.0/docs/rules.md +105 -0
  16. {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/.claude-plugin/plugin.json +2 -2
  17. {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/hooks/hooks.json +1 -1
  18. {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/scripts/run-subedit-hook.sh +2 -2
  19. subedit-0.5.0/plugins/subedit/skills/judging-findings/SKILL.md +48 -0
  20. {subedit-0.3.2 → subedit-0.5.0}/pyproject.toml +24 -4
  21. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/__init__.py +1 -1
  22. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/__init__.py +2 -1
  23. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/corpus_meter.py +9 -3
  24. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/fixer.py +6 -2
  25. subedit-0.5.0/src/subedit/application/linter.py +117 -0
  26. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/report_aggregator.py +22 -6
  27. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/__init__.py +36 -1
  28. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/__init__.py +18 -0
  29. subedit-0.5.0/src/subedit/domain/models/abbreviations.py +64 -0
  30. subedit-0.5.0/src/subedit/domain/models/cldr_sentence_suppressions.txt +194 -0
  31. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/comment.py +94 -98
  32. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/diagnostic.py +11 -0
  33. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/document.py +2 -0
  34. subedit-0.5.0/src/subedit/domain/models/embedded.py +71 -0
  35. subedit-0.5.0/src/subedit/domain/models/file_type.py +27 -0
  36. subedit-0.5.0/src/subedit/domain/models/markdown.py +132 -0
  37. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/position.py +4 -0
  38. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/prose.py +68 -214
  39. subedit-0.5.0/src/subedit/domain/models/prose_map.py +49 -0
  40. subedit-0.5.0/src/subedit/domain/models/prose_unit.py +80 -0
  41. subedit-0.5.0/src/subedit/domain/models/reflow.py +311 -0
  42. subedit-0.5.0/src/subedit/domain/models/rule_option.py +107 -0
  43. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/rule_set.py +13 -3
  44. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/text.py +3 -0
  45. subedit-0.5.0/src/subedit/domain/models/title_case.py +115 -0
  46. subedit-0.5.0/src/subedit/domain/models/word_classes.py +212 -0
  47. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/__init__.py +2 -1
  48. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/external_linter.py +2 -0
  49. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/language_conventions.py +7 -2
  50. subedit-0.5.0/src/subedit/domain/ports/rule.py +39 -0
  51. subedit-0.5.0/src/subedit/domain/ports/source_parser.py +56 -0
  52. subedit-0.5.0/src/subedit/domain/rules/__init__.py +44 -0
  53. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/rules/layout.py +18 -21
  54. subedit-0.5.0/src/subedit/domain/rules/markdown.py +226 -0
  55. subedit-0.3.2/src/subedit/domain/rules/rewrite.py → subedit-0.5.0/src/subedit/domain/rules/sentence.py +90 -66
  56. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/rules/structure.py +56 -59
  57. subedit-0.5.0/src/subedit/domain/rules/style.py +364 -0
  58. subedit-0.5.0/src/subedit/infrastructure/__init__.py +128 -0
  59. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/__init__.py +2 -1
  60. subedit-0.5.0/src/subedit/infrastructure/config_sources/config_schema.py +86 -0
  61. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/toml_config_source.py +74 -16
  62. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/autocorrect_linter.py +6 -4
  63. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/vale_linter.py +12 -10
  64. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/__init__.py +13 -0
  65. subedit-0.5.0/src/subedit/infrastructure/parsers/composite_parser.py +27 -0
  66. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/__init__.py +6 -1
  67. subedit-0.5.0/src/subedit/infrastructure/parsers/languages/comment_only_profile.py +101 -0
  68. subedit-0.5.0/src/subedit/infrastructure/parsers/languages/language_profile.py +110 -0
  69. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/python_profile.py +7 -14
  70. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/typescript_profile.py +38 -20
  71. subedit-0.5.0/src/subedit/infrastructure/parsers/markdown_parser.py +237 -0
  72. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +27 -23
  73. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/cli.py +51 -27
  74. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/agent_harness.py +2 -0
  75. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/claude_code_harness.py +4 -4
  76. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/codex_harness.py +2 -2
  77. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/hook.py +25 -8
  78. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/report.py +7 -2
  79. subedit-0.5.0/src/subedit/presentation/rule_reference.py +43 -0
  80. subedit-0.5.0/src/subedit/vale/styles/subedit/EditHistory.yml +16 -0
  81. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/Semicolon.yml +1 -0
  82. subedit-0.5.0/src/subedit/vale/vale.ini +14 -0
  83. {subedit-0.3.2 → subedit-0.5.0}/subedit.toml +6 -3
  84. {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_corpus_meter.py +3 -3
  85. {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_fixer.py +24 -22
  86. {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_linter.py +30 -38
  87. {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_report_aggregator.py +46 -26
  88. subedit-0.5.0/tests/application/test_surface_ocp.py +149 -0
  89. subedit-0.5.0/tests/conftest.py +9 -0
  90. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_comment.py +5 -0
  91. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_diagnostic.py +3 -6
  92. subedit-0.5.0/tests/domain/test_embedded.py +61 -0
  93. subedit-0.5.0/tests/domain/test_markdown.py +240 -0
  94. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_prose.py +106 -9
  95. subedit-0.5.0/tests/domain/test_rule_option.py +52 -0
  96. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_rules_layout.py +10 -13
  97. subedit-0.3.2/tests/test_rules_rewrite.py → subedit-0.5.0/tests/domain/test_rules_sentence.py +86 -18
  98. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_rules_structure.py +16 -15
  99. subedit-0.5.0/tests/domain/test_style.py +448 -0
  100. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_text.py +9 -12
  101. subedit-0.5.0/tests/factories.py +111 -0
  102. {subedit-0.3.2 → subedit-0.5.0}/tests/fixtures/vale/violations.py +0 -2
  103. subedit-0.5.0/tests/infrastructure/test_config.py +230 -0
  104. subedit-0.5.0/tests/infrastructure/test_data_formats.py +79 -0
  105. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_language_parity.py +6 -15
  106. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser.py +33 -37
  107. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser_ocp.py +31 -8
  108. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser_oracle.py +5 -6
  109. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_schema.py +16 -19
  110. {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_typescript.py +61 -39
  111. subedit-0.5.0/tests/infrastructure/test_vale_style.py +103 -0
  112. {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_cli.py +69 -71
  113. {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_harnesses.py +7 -9
  114. {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_hook.py +48 -43
  115. {subedit-0.3.2/tests → subedit-0.5.0/tests/repository}/test_plugin.py +18 -18
  116. {subedit-0.3.2/tests → subedit-0.5.0/tests/repository}/test_self_hosting.py +39 -14
  117. subedit-0.5.0/uv.lock +701 -0
  118. subedit-0.3.2/.vale.ini +0 -13
  119. subedit-0.3.2/PKG-INFO +0 -227
  120. subedit-0.3.2/README.md +0 -193
  121. subedit-0.3.2/docs/rules.md +0 -18
  122. subedit-0.3.2/examples/README.md +0 -47
  123. subedit-0.3.2/examples/probe.py +0 -5
  124. subedit-0.3.2/examples/subedit.toml +0 -2
  125. subedit-0.3.2/examples/walkthrough.py +0 -71
  126. subedit-0.3.2/plugins/subedit/skills/judging-findings/SKILL.md +0 -45
  127. subedit-0.3.2/src/subedit/application/linter.py +0 -74
  128. subedit-0.3.2/src/subedit/domain/models/prose_map.py +0 -44
  129. subedit-0.3.2/src/subedit/domain/ports/rule.py +0 -28
  130. subedit-0.3.2/src/subedit/domain/ports/source_parser.py +0 -17
  131. subedit-0.3.2/src/subedit/domain/rules/__init__.py +0 -15
  132. subedit-0.3.2/src/subedit/infrastructure/__init__.py +0 -77
  133. subedit-0.3.2/src/subedit/infrastructure/config_sources/config_schema.py +0 -44
  134. subedit-0.3.2/src/subedit/infrastructure/parsers/languages/language_profile.py +0 -67
  135. subedit-0.3.2/src/subedit/presentation/rule_reference.py +0 -24
  136. subedit-0.3.2/src/subedit/vale/styles/subedit/EditHistory.yml +0 -8
  137. subedit-0.3.2/src/subedit/vale/styles/subedit/Ellipsis.yml +0 -10
  138. subedit-0.3.2/src/subedit/vale/styles/subedit/MidSentenceColon.yml +0 -9
  139. subedit-0.3.2/src/subedit/vale/styles/subedit/MidSentenceColonMarkup.yml +0 -8
  140. subedit-0.3.2/src/subedit/vale/styles/subedit/SignatureTag.yml +0 -7
  141. subedit-0.3.2/src/subedit/vale/vale.ini +0 -11
  142. subedit-0.3.2/tests/test_config.py +0 -155
  143. subedit-0.3.2/tests/test_vale_style.py +0 -132
  144. subedit-0.3.2/uv.lock +0 -502
  145. {subedit-0.3.2 → subedit-0.5.0}/.claude/settings.json +0 -0
  146. {subedit-0.3.2 → subedit-0.5.0}/.github/dependabot.yml +0 -0
  147. {subedit-0.3.2 → subedit-0.5.0}/.github/release.yml +0 -0
  148. {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/pr.yml +0 -0
  149. {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/release.yml +0 -0
  150. {subedit-0.3.2 → subedit-0.5.0}/LICENSE +0 -0
  151. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/comment_layout.py +0 -0
  152. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/formatter_width.py +5 -5
  153. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/__init__.py +0 -0
  154. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/source_path.py +0 -0
  155. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/code_line.py +0 -0
  156. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/__init__.py +0 -0
  157. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/__init__.py +0 -0
  158. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/DateRef.yml +0 -0
  159. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/EmDash.yml +0 -0
  160. {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/ExternalRef.yml +0 -0
  161. {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_position.py +0 -0
  162. {subedit-0.3.2 → subedit-0.5.0}/tests/fixtures/autocorrect/unspaced.py +0 -0
  163. {subedit-0.3.2 → subedit-0.5.0}/tests/fixtures/vale/clean.md +0 -0
@@ -6,7 +6,7 @@
6
6
  {
7
7
  "name": "subedit",
8
8
  "source": "./plugins/subedit",
9
- "description": "Repairs comment layout silently on write, and returns only what needs rewriting."
9
+ "description": "Repairs prose layout silently on write, and returns only what needs rewriting."
10
10
  }
11
11
  ]
12
12
  }
@@ -51,7 +51,7 @@ jobs:
51
51
  enable-cache: true
52
52
  - run: uv sync --extra dev
53
53
 
54
- # Without Vale, every test against it skips and the self-hosting check never runs it.
54
+ # Without Vale, every test against it skips.
55
55
  # AutoCorrect needs no step, since it arrives as a Python dependency.
56
56
  - name: Install Vale
57
57
  env:
@@ -64,22 +64,32 @@ jobs:
64
64
  - name: Confirm Vale
65
65
  run: vale --version
66
66
 
67
- - run: uv run pytest tests/ -v --tb=short
68
-
69
- - name: Self-hosting
70
- run: uv run subedit check src/ tests/
67
+ - run: uv run pytest tests/ -v --tb=short --cov
71
68
 
69
+ # subedit checks its own comments and Markdown with every rule it ships, failing only on an error.
72
70
  prose:
73
71
  name: Prose
74
72
  runs-on: ubuntu-latest
75
73
  steps:
76
74
  - uses: actions/checkout@v4
77
- - uses: errata-ai/vale-action@reviewdog
75
+ - uses: astral-sh/setup-uv@v6
78
76
  with:
79
- version: 3.18.0
80
- files: src tests README.md
81
- fail_on_error: true
82
- reporter: github-check
77
+ enable-cache: true
78
+ - run: uv sync --extra dev
79
+
80
+ # Without Vale, the check skips the rules the bundled style holds.
81
+ - name: Install Vale
82
+ env:
83
+ VALE_VERSION: 3.18.0
84
+ run: |
85
+ curl -sSfL "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/vale_${VALE_VERSION}_Linux_64-bit.tar.gz" \
86
+ | tar -xz -C "$RUNNER_TEMP" vale
87
+ echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
88
+
89
+ - name: Confirm Vale
90
+ run: vale --version
91
+
92
+ - run: uv run subedit check src/ tests/ README.md docs/
83
93
 
84
94
  build:
85
95
  name: Build
@@ -1,6 +1,8 @@
1
- # Design notes stay local. The generated rule reference is tracked.
1
+ # Design notes stay local. The generated rule reference and README assets are tracked.
2
2
  docs/*
3
3
  !docs/rules.md
4
+ !docs/assets/
5
+
4
6
 
5
7
  # Python
6
8
  __pycache__/
@@ -18,3 +20,6 @@ htmlcov/
18
20
 
19
21
  # macOS
20
22
  .DS_Store
23
+
24
+ # Internal docs, linked in from project-docs
25
+ .internal
@@ -0,0 +1,16 @@
1
+ StylesPath = src/subedit/vale/styles
2
+ MinAlertLevel = warning
3
+
4
+ [*.{py,pyi,ts,mts,cts,tsx,js,go,rs}]
5
+ BasedOnStyles = subedit
6
+
7
+ [*.md]
8
+ BasedOnStyles = subedit
9
+ # These guard a comment against going stale. A Markdown note may tell a history,
10
+ # cite an issue, date a release, or explain a tag on purpose.
11
+ subedit.EditHistory = NO
12
+ subedit.DateRef = NO
13
+ subedit.ExternalRef = NO
14
+
15
+ [tests/fixtures/vale/*]
16
+ BasedOnStyles =
subedit-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.5
2
+ Name: subedit
3
+ Version: 0.5.0
4
+ Summary: A subeditor for AI agents, keeping every sentence they write to your house style.
5
+ Project-URL: Homepage, https://github.com/mroops0111/subedit
6
+ Project-URL: Repository, https://github.com/mroops0111/subedit
7
+ Project-URL: Issues, https://github.com/mroops0111/subedit/issues
8
+ Author: Yun-Tai Yang
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: comments,formatter,javascript,json,linter,lsp,prose,python,tree-sitter,typescript,yaml
12
+ Classifier: Development Status :: 1 - Planning
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: autocorrect-py>=2.14.0
22
+ Requires-Dist: pydantic>=2.13.5
23
+ Requires-Dist: tree-sitter-json>=0.24.0
24
+ Requires-Dist: tree-sitter-markdown>=0.5.0
25
+ Requires-Dist: tree-sitter-python>=0.25.0
26
+ Requires-Dist: tree-sitter-typescript>=0.23.2
27
+ Requires-Dist: tree-sitter-yaml>=0.7.0
28
+ Requires-Dist: tree-sitter>=0.26.0
29
+ Requires-Dist: typer>=0.27.2
30
+ Provides-Extra: dev
31
+ Requires-Dist: basedpyright>=1.40.1; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
33
+ Requires-Dist: pytest>=9.0.0; extra == 'dev'
34
+ Requires-Dist: ruff>=0.16.7; extra == 'dev'
35
+ Provides-Extra: vale
36
+ Requires-Dist: vale>=3.22.0.0; extra == 'vale'
37
+ Description-Content-Type: text/markdown
38
+
39
+ ![subedit. Your house style, enforced on every sentence your agents write.](docs/assets/cover.svg)
40
+
41
+ <p align="center">
42
+ <a href="https://github.com/mroops0111/subedit/actions/workflows/ci.yml"><img src="https://github.com/mroops0111/subedit/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
43
+ <a href="https://pypi.org/project/subedit/"><img src="https://img.shields.io/pypi/v/subedit.svg" alt="PyPI version"></a>
44
+ <a href="https://pepy.tech/projects/subedit"><img src="https://static.pepy.tech/badge/subedit/month" alt="PyPI downloads"></a>
45
+ <a href="https://pypi.org/project/subedit/"><img src="https://img.shields.io/pypi/pyversions/subedit.svg" alt="Python versions"></a>
46
+ <a href="#agent-integration"><img src="https://img.shields.io/badge/Claude%20Code-plugin-d97757" alt="Claude Code plugin"></a>
47
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
48
+ </p>
49
+
50
+ Linters keep an agent's code in shape. Almost nothing does the same for its sentences. `subedit` is a subeditor for AI agents. It checks every sentence an agent writes into your repository, autofixes the layout without a word, and hands back to the model only what needs rewriting, with the fix stated. [Surfaces](#surfaces) lists the kinds of file it reads.
51
+
52
+ - **Built for Agents**: a post-edit hook for Claude Code and Codex checks each file an agent writes, as it lands.
53
+ - **Silent Autofix, Stated Rewrites**: layout and spacing are autofixed without a message, and only a finding that needs the sentence's meaning goes back, with its fix stated.
54
+ - **Sentence-Aware Layout**: a comment breaks only at a comma that ends a clause, and a Markdown paragraph stays on one line.
55
+ - **Never Touches Code**: tree-sitter finds the prose, so code, string literals, and commented-out code are left as they are.
56
+ - **One Report, Three Engines**: native layout and structure rules, a bundled Vale style, and AutoCorrect share one config and one report.
57
+
58
+ ---
59
+
60
+ ## Example
61
+
62
+ An editor wraps a comment at whatever column it reaches, so sentences break mid clause.
63
+
64
+ ```python
65
+ def reflow(comment: Comment, width: int) -> Comment:
66
+ # A sentence that fits the width stays on one line of the comment, and a longer
67
+ # one breaks only at a comma that ends a clause.
68
+ return comment.reflow(width)
69
+ ```
70
+
71
+ `subedit fix --write` autofixes the layout and says nothing. The sentence is too long for one line, so it breaks at the comma where its first clause ends.
72
+
73
+ ```python
74
+ def reflow(comment: Comment, width: int) -> Comment:
75
+ # A sentence that fits the width stays on one line of the comment,
76
+ # and a longer one breaks only at a comma that ends a clause.
77
+ return comment.reflow(width)
78
+ ```
79
+
80
+ Markdown has no column limit, yet an agent often wraps a paragraph by hand, and GitHub, Notion, and Heptabase may show each of those newlines as a break.
81
+
82
+ ```markdown
83
+ ## cache layout
84
+
85
+ The cache keeps one entry per file,
86
+ and drops it when the file changes.
87
+ ```
88
+
89
+ The same command joins the paragraph into one line and puts the heading in Title Case, again without a word.
90
+
91
+ ```markdown
92
+ ## Cache Layout
93
+
94
+ The cache keeps one entry per file, and drops it when the file changes.
95
+ ```
96
+
97
+ A sentence in a comment that runs past the width with no comma has nowhere legal to break, and only its author knows where the meaning divides, so it goes back to the agent that wrote it.
98
+
99
+ ```text
100
+ 1 finding needs rewriting in src/cache.py.
101
+ Each states the fix. Apply it in place, do not add a suppression comment.
102
+
103
+ line 2 [unbreakable-sentence]
104
+ This sentence runs to 116 characters against a budget of 114 characters and contains no comma, so there is no legal place to break it. Add a comma at a clause boundary, or split it into two sentences.
105
+ ```
106
+
107
+ ## Getting Started
108
+
109
+ `subedit` runs inside an AI agent as a plugin, and on its own from the command line.
110
+
111
+ ### Claude Code Plugin
112
+
113
+ The plugin needs only [uv](https://docs.astral.sh/uv/). It runs `subedit` from PyPI with Vale included, and warms that environment when a session starts.
114
+
115
+ ```text
116
+ /plugin marketplace add mroops0111/subedit
117
+ /plugin install subedit@mroops
118
+ ```
119
+
120
+ The hook then acts in every project that opts in with a `subedit.toml`, which may be empty. [Agent Integration](#agent-integration) covers Codex and registering the hook by hand.
121
+
122
+ ### Command Line
123
+
124
+ The command line tool needs Python 3.11 or newer, and installs from PyPI.
125
+
126
+ ```bash
127
+ uv tool install 'subedit[vale]' # recommended, fetches Vale for you
128
+ uv tool install subedit # without Vale, the native rules and AutoCorrect still run
129
+ ```
130
+
131
+ `pipx install subedit` and `pip install subedit` work as well. To try it without installing, prefix any command with `uvx`.
132
+
133
+ ### Quick Start
134
+
135
+ Every command takes files or directories, and a directory walk skips dotted and vendor directories such as `.venv` and `node_modules`.
136
+
137
+ | Command | What It Does |
138
+ | --- | --- |
139
+ | `subedit check src/` | Reports every finding |
140
+ | `subedit check src/ --output=actionable` | Reports only the findings that need rewriting |
141
+ | `subedit check src/ --output=json` | Reports every finding as JSON, for other tools |
142
+ | `subedit fix src/` | Shows the autofixes as a diff |
143
+ | `subedit fix src/ --write` | Applies the autofixes in place |
144
+ | `subedit stats src/` | Measures comment lengths and widths, to set thresholds from data |
145
+ | `subedit rules` | Lists every native rule with its fix kind and default level |
146
+
147
+ To adopt `subedit`, add an empty `subedit.toml` at the project root, run `fix --write` once, and read what `check` still reports. `check` exits 1 on any `error` finding, and `fix` exits 1 while an `error` rewrite remains, so both drop into CI or a pre-commit hook. `--no-delegate` skips Vale and AutoCorrect.
148
+
149
+ <details>
150
+ <summary><b>CI Workflow</b>: a GitHub Actions job</summary>
151
+
152
+ <br>
153
+
154
+ ```yaml
155
+ # .github/workflows/prose.yml
156
+ jobs:
157
+ subedit:
158
+ runs-on: ubuntu-latest
159
+ steps:
160
+ - uses: actions/checkout@v4
161
+ - uses: astral-sh/setup-uv@v6
162
+ - run: uvx --from 'subedit[vale]' subedit check src/
163
+ ```
164
+
165
+ </details>
166
+
167
+ ## Background
168
+
169
+ A subeditor is the editor at a newspaper who makes every article follow the paper's style guide. `subedit` does that for the text AI agents write in your repository.
170
+
171
+ Agents write a lot of comments and docs, and rarely go back to check them. Code formatters skip comments, and prose linters don't understand source files, so three kinds of problems slip through:
172
+
173
+ - **Layout**: comments are wrapped at a fixed column, so sentences break in the middle of a phrase.
174
+ - **Placement**: a public function gets a plain comment instead of a docstring, so it never shows up on hover.
175
+ - **Wording**: change history and ticket numbers end up in comments and go out of date, and punctuation drifts from the house style.
176
+
177
+ Telling the agent isn't enough. Even with a style guide or a skill, agents get layout rules wrong and forget them as the conversation grows. A tool that checks every edit doesn't forget, and by fixing what it can itself, it keeps fifty layout complaints out of the agent's context.
178
+
179
+ ## Design
180
+
181
+ Every finding says who can resolve it, and the whole design turns on that answer.
182
+
183
+ ### Fix Kinds
184
+
185
+ A rule declares one of three fix kinds, which decides what changes and who makes the change.
186
+
187
+ | Kind | What Changes | Resolved By | Example |
188
+ | --- | --- | --- | --- |
189
+ | `TARGETED` | A few characters, in place | The tool, silently | A missing space between CJK and Latin text |
190
+ | `REFLOW` | Where lines break | The tool, silently | A sentence broken mid clause |
191
+ | `REWRITE` | The wording itself | The author | A sentence too long to break anywhere legal |
192
+
193
+ Targeted and reflow fixes are autofixes, applied without a message. Only a rewrite reaches the author, because changing the wording needs the sentence's meaning.
194
+
195
+ ### Severity
196
+
197
+ A level decides whether a rewrite blocks. Every rule has a default, and every rule can be moved between levels or switched off.
198
+
199
+ | Level | `check` and `fix` | Agent Hook |
200
+ | --- | --- | --- |
201
+ | `error` | Fails with exit code 1 | Blocks the edit and sends the message to the model |
202
+ | `warning` | Shows the message as advice | Sends the message as context, without blocking |
203
+ | `off` | Skips the rule | Skips the rule |
204
+
205
+ Judgment rules, such as a long block or a date in a comment, default to `warning`.
206
+
207
+ ### Layout Model
208
+
209
+ Reflow works on sentences rather than lines, so the result reads the way the author would have broken it.
210
+
211
+ - **One Line When It Fits**: a sentence that fits takes one line, and a wrapped one that would fit is joined.
212
+ - **Clause Commas Only**: a longer sentence breaks only at a comma that ends a clause, never inside brackets or a code span, and preferably not inside a list.
213
+ - **No Short Lines**: no break leaves a line shorter than the `line-too-short` floor, 24 characters by default.
214
+ - **Verbatim Blocks**: code examples, docstring sections, and JSDoc tags are left as written.
215
+ - **Markdown Paragraphs**: Markdown has no column limit, so a paragraph takes one source line, and a hand-wrapped one is joined.
216
+
217
+ ### Pipeline
218
+
219
+ A file takes the same path through `subedit` whether the CLI or the agent hook started it.
220
+
221
+ ![subedit pipeline. An author or agent runs the CLI or the agent hook, which reads subedit.toml. subedit parses the comments, docstrings, and Markdown, lints them with its own rules, Vale, and AutoCorrect, then triages each finding by fix kind and level. An autofix is written back silently, and a rewrite is sent back to the author with its fix stated.](docs/assets/pipeline.svg)
222
+
223
+ 1. **Resolve**: the closest `subedit.toml`, or `[tool.subedit]` in `pyproject.toml`, gives the rules and the width.
224
+ 2. **Parse**: tree-sitter finds the prose, such as comments, docstrings, headings, and paragraphs, and maps where code sits. A Markdown code block is read as its own language when the project sets `code-blocks`.
225
+ 3. **Lint**: each native rule reads its own surface, and a Vale or AutoCorrect finding is kept only inside prose.
226
+ 4. **Autofix**: targeted edits, then reflow, rerun until nothing mechanical is left, for up to ten rounds.
227
+ 5. **Report**: what remains is a rewrite, reported as a blocking error or as advice.
228
+
229
+ ### Architecture
230
+
231
+ The package follows a layered design, so a new language, rule, or delegated tool is one class behind an existing port.
232
+
233
+ ![subedit layers. presentation holds the CLI, the agent hook, and the reports, and calls application, which holds the Linter, the Fixer, the report aggregator, and the corpus meter. application uses domain, which holds the models, the rules, and the ports. infrastructure holds the tree-sitter parser, the config source, and the Vale and AutoCorrect adapters. presentation wires infrastructure in, and infrastructure implements the domain ports.](docs/assets/layers.svg)
234
+
235
+ Rules never see a syntax tree. A parser turns the file into prose units, such as a comment group with the symbol it documents or a Markdown paragraph, and each rule reads the units of its own surface. A new surface is a `ProseUnit` with its `SourceParser`, a new language is a `LanguageProfile`, a format whose only prose is its line comments is one more `CommentOnlyProfile`, and a new agent is an `AgentHarness`.
236
+
237
+ ## Reference
238
+
239
+ ### Agent Integration
240
+
241
+ `subedit-hook` runs after every file an agent edits. It applies the autofixes in place, and exits 2 only when a sentence needs rewriting, which hands the message back to the model. In Claude Code a warning reaches the model too, as context beside the tool result, without blocking the edit.
242
+
243
+ The hook acts only in a project that opts in, with a `subedit.toml` or a `[tool.subedit]` table in `pyproject.toml`, in the edited file's directory or a parent. An empty table is enough. Elsewhere it exits 0 and changes nothing. The opt-in covers every surface, and a single rule is switched off by name under `[rules]`.
244
+
245
+ #### Claude Code
246
+
247
+ The [plugin](#claude-code-plugin) registers the hook for you. It also ships the `subedit:judging-findings` skill, which gives the model the criteria for every finding a tool cannot fix, such as when a spec identifier stays and a ticket number goes. Criteria in your `CLAUDE.md`, `AGENTS.md`, or a skill of your own come first.
248
+
249
+ <details>
250
+ <summary><b>Manual Hook Registration</b>: without the plugin</summary>
251
+
252
+ <br>
253
+
254
+ With `subedit` installed as a tool, add this to `.claude/settings.json`.
255
+
256
+ ```json
257
+ {
258
+ "hooks": {
259
+ "PostToolUse": [
260
+ {
261
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
262
+ "hooks": [{ "type": "command", "command": "subedit-hook" }]
263
+ }
264
+ ]
265
+ }
266
+ }
267
+ ```
268
+
269
+ </details>
270
+
271
+ #### Codex
272
+
273
+ Add this to `~/.codex/hooks.json`.
274
+
275
+ ```json
276
+ {
277
+ "hooks": {
278
+ "PostToolUse": [{ "command": "subedit-hook" }]
279
+ }
280
+ }
281
+ ```
282
+
283
+ Registering the hook with both agents is fine, since each payload identifies its sender and anything else exits 0 untouched. `subedit-hook --help` prints the payload shapes it accepts.
284
+
285
+ ### Configuration
286
+
287
+ Settings come from `subedit.toml`, or from `[tool.subedit]` in `pyproject.toml`, whichever sits closest above the file being checked. Without either, `check` and `fix` run with the defaults. A config with a misspelled key, level, or option stops subedit with an error that names the file and the key, so a typo never leaves a project on defaults it did not choose.
288
+
289
+ A rule takes one of two forms, depending on whether it has options to set.
290
+
291
+ - **Level Only**: a string, `"error"`, `"warning"`, or `"off"`. Every rule takes this form, including the delegated Vale and AutoCorrect rules.
292
+ - **Level and Options**: a `[rules.<name>]` table, holding `severity` and the rule's options. Leave out `severity` to keep the default level. The [rule reference](docs/rules.md) lists each rule's options with their defaults.
293
+
294
+ ```toml
295
+ [subedit]
296
+ line-width = 100
297
+ exclude = ["tests/fixtures/**"]
298
+
299
+ [rules]
300
+ mid-clause-break = "warning"
301
+ file-header-comment = "off"
302
+ "subedit.DateRef" = "off"
303
+
304
+ [rules.comment-too-long]
305
+ severity = "error"
306
+ max-plain-lines = 3
307
+
308
+ [rules.line-too-short]
309
+ min-line-characters = 30
310
+
311
+ [rules.heading-title-case]
312
+ exceptions = ["npm"]
313
+ ```
314
+
315
+ A name Title Case must leave as written, such as `npm`, goes under `exceptions`, which headings, table headers, and list labels all follow.
316
+
317
+ In `pyproject.toml`, the same keys sit under `[tool.subedit]`, `[tool.subedit.rules]`, and `[tool.subedit.rules.<name>]`.
318
+
319
+ <details>
320
+ <summary><b>Settings</b>: the width, excluded paths, code blocks, and abbreviations</summary>
321
+
322
+ <br>
323
+
324
+ | Key | Type | Default | Meaning |
325
+ | --- | --- | --- | --- |
326
+ | `line-width` | Integer | The formatter's width, else 120 | The column every comment line must fit, counting indent and delimiter |
327
+ | `exclude` | List of globs | `[]` | Paths a directory walk skips, relative to the config file |
328
+ | `code-blocks` | Boolean | `false` | Whether the comments in a Markdown code block are read, in a language subedit supports |
329
+ | `abbreviations` | List of strings | `[]` | Abbreviations whose period ends no sentence, each written with its period and its case, such as `"Dept."` |
330
+
331
+ Left unset, `line-width` follows the formatter that would format the file, read from its own config.
332
+
333
+ | Language | Width Read From |
334
+ | --- | --- |
335
+ | Python | `line-length` for ruff or black |
336
+ | TypeScript, JavaScript | `printWidth` for Prettier, `lineWidth` for Biome |
337
+
338
+ </details>
339
+
340
+ ### Rules
341
+
342
+ `subedit` ships 21 native rules, a bundled Vale style of 5 rules, and AutoCorrect. Every rule is set by name under `[rules]` to `"error"`, `"warning"`, or `"off"`, and its fix kind follows [Fix Kinds](#fix-kinds).
343
+
344
+ Comments and Markdown share one house style, and differ where their forms differ. A comment has a column limit and sits beside the code it explains, while Markdown has no limit and is built from paragraphs and headings.
345
+
346
+ | Concern | Comments | Markdown | Rules |
347
+ | --- | --- | --- | --- |
348
+ | Punctuation | No mid-sentence colon, semicolon, dash, or arrow | Same | [Style](docs/rules.md#style), [Wording](docs/rules.md#wording) |
349
+ | Parentheses | Half-width, spaced outside, flush inside | Same | [Style](docs/rules.md#style) |
350
+ | List Labels | Title Case, and bold only in docstrings and JSDoc | Title Case and bold | [Style](docs/rules.md#style) |
351
+ | Word Choice | The house verb for *include*, and no *not A, but B* framing | Same | [Style](docs/rules.md#style) |
352
+ | CJK Spacing | A space between CJK and Latin text | Same | [Spacing](docs/rules.md#spacing) |
353
+ | Line Breaks | Within the width, only at clause commas | One source line per paragraph or list item | [Layout](docs/rules.md#layout), [Markdown](docs/rules.md#markdown) |
354
+ | Long Sentences | Rewritten when no clause comma can carry the break | Not checked, since no line breaks | [Sentence](docs/rules.md#sentence) |
355
+ | Placement | A docstring on each public symbol, no banner, no restated signature, and a short block | Not checked | [Structure](docs/rules.md#structure) |
356
+ | Stale References | No change narration, ticket number, or date | Not checked, since a note may tell a history | [Wording](docs/rules.md#wording) |
357
+ | Headings | None | Title Case, not bold, a noun phrase, and opened by a lead sentence | [Markdown](docs/rules.md#markdown) |
358
+
359
+ The [rule reference](docs/rules.md) lists every rule by group, with what it catches, its default level, and its options.
360
+
361
+ ### Surfaces
362
+
363
+ A surface is a kind of file where prose lives. Every surface goes through the same pipeline.
364
+
365
+ | Surface | Status | Focus |
366
+ | --- | --- | --- |
367
+ | Code comments and docstrings | Available | Layout, because a comment must fit a column limit |
368
+ | Markdown | Available | One line per paragraph and the shape of headings, because Markdown has no column limit |
369
+
370
+ Markdown is read from `.md` and `.markdown` files, leaving front matter, code, and HTML blocks alone, and reading only the header of a table. With `code-blocks = true`, the comments in a fenced block are read too, when its info string names a language subedit supports, such as `python`, `ts`, or `yaml`. Each block is checked as a file of that language would be, by Vale and AutoCorrect as well, except for the rules on a whole file. A code block is often an example, so this stays off unless the project asks. Editor diagnostics over LSP are planned, so the same findings show as you type.
371
+
372
+ #### Languages
373
+
374
+ Each language is a profile that knows its comment forms, its doc comment, and what counts as public. A data format has no doc comment or public symbol, so only the rules on wording and layout apply to its comments.
375
+
376
+ | Language | Files | Doc Comment | Public Symbol |
377
+ | --- | --- | --- | --- |
378
+ | Python | `.py`, `.pyi` | Docstring, `"""` | A name without a leading underscore |
379
+ | TypeScript | `.ts`, `.mts`, `.cts`, `.tsx` | JSDoc, `/** */` | An exported symbol, and its members unless `private`, `protected`, or `#named` |
380
+ | JavaScript | `.js`, `.mjs`, `.cjs`, `.jsx` | JSDoc, `/** */` | A symbol exported with `export`, and its members unless `#named` |
381
+ | YAML | `.yml`, `.yaml` | None | None |
382
+ | JSON | `.json`, `.jsonc` | None | None |
383
+
384
+ <details>
385
+ <summary><b>Content Left Alone</b>: what every profile skips</summary>
386
+
387
+ <br>
388
+
389
+ - **Commented-Out Code**: a comment line the language's own grammar reads as code, such as `// return result` or `# client.send(`, is left as written. In YAML and JSON, a setting commented out, such as `# timeout: 30` or `// "semi": false,`, is too.
390
+ - **Doc Sections**: a docstring's sections from `Args:`, `Parameters`, or `:param` onward, and a JSDoc block's tags from the first one such as `@throws` onward, are left as written. A JSDoc block keeps its frame when reflowed.
391
+ - **Directives**: a directive such as `// eslint-disable-next-line`, `# type: ignore`, or `# yaml-language-server:` carries no prose, so nothing reads it.
392
+ - **Plain Blocks**: a plain `/* */` block is usually a license header or code switched off, so nothing reads it.
393
+ - **Code and Values**: Vale and AutoCorrect findings are kept only inside comment prose, so a string in code or a value in YAML is never changed.
394
+ - **Group Headings**: a plain comment opening a paragraph over several declarations heads them as a group, so it documents none of them.
395
+
396
+ </details>
397
+
398
+ ## License
399
+
400
+ [MIT](LICENSE). The English abbreviations that end no sentence come from the [Unicode CLDR](https://cldr.unicode.org/) under the Unicode License v3, whose notice ships with the [data file](src/subedit/domain/models/cldr_sentence_suppressions.txt).