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.
- {subedit-0.3.2 → subedit-0.5.0}/.claude-plugin/marketplace.json +1 -1
- {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/ci.yml +20 -10
- {subedit-0.3.2 → subedit-0.5.0}/.gitignore +6 -1
- subedit-0.5.0/.vale.ini +16 -0
- subedit-0.5.0/PKG-INFO +400 -0
- subedit-0.5.0/README.md +362 -0
- subedit-0.5.0/docs/assets/cover.svg +1 -0
- subedit-0.5.0/docs/assets/icon.svg +4 -0
- subedit-0.5.0/docs/assets/layers.excalidraw +1700 -0
- subedit-0.5.0/docs/assets/layers.svg +2 -0
- subedit-0.5.0/docs/assets/logo.svg +4 -0
- subedit-0.5.0/docs/assets/pipeline.excalidraw +1230 -0
- subedit-0.5.0/docs/assets/pipeline.svg +2 -0
- subedit-0.5.0/docs/assets/wordmark.svg +4 -0
- subedit-0.5.0/docs/rules.md +105 -0
- {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/.claude-plugin/plugin.json +2 -2
- {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/hooks/hooks.json +1 -1
- {subedit-0.3.2 → subedit-0.5.0}/plugins/subedit/scripts/run-subedit-hook.sh +2 -2
- subedit-0.5.0/plugins/subedit/skills/judging-findings/SKILL.md +48 -0
- {subedit-0.3.2 → subedit-0.5.0}/pyproject.toml +24 -4
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/__init__.py +1 -1
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/__init__.py +2 -1
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/corpus_meter.py +9 -3
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/fixer.py +6 -2
- subedit-0.5.0/src/subedit/application/linter.py +117 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/application/report_aggregator.py +22 -6
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/__init__.py +36 -1
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/__init__.py +18 -0
- subedit-0.5.0/src/subedit/domain/models/abbreviations.py +64 -0
- subedit-0.5.0/src/subedit/domain/models/cldr_sentence_suppressions.txt +194 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/comment.py +94 -98
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/diagnostic.py +11 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/document.py +2 -0
- subedit-0.5.0/src/subedit/domain/models/embedded.py +71 -0
- subedit-0.5.0/src/subedit/domain/models/file_type.py +27 -0
- subedit-0.5.0/src/subedit/domain/models/markdown.py +132 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/position.py +4 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/prose.py +68 -214
- subedit-0.5.0/src/subedit/domain/models/prose_map.py +49 -0
- subedit-0.5.0/src/subedit/domain/models/prose_unit.py +80 -0
- subedit-0.5.0/src/subedit/domain/models/reflow.py +311 -0
- subedit-0.5.0/src/subedit/domain/models/rule_option.py +107 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/rule_set.py +13 -3
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/text.py +3 -0
- subedit-0.5.0/src/subedit/domain/models/title_case.py +115 -0
- subedit-0.5.0/src/subedit/domain/models/word_classes.py +212 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/__init__.py +2 -1
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/external_linter.py +2 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/ports/language_conventions.py +7 -2
- subedit-0.5.0/src/subedit/domain/ports/rule.py +39 -0
- subedit-0.5.0/src/subedit/domain/ports/source_parser.py +56 -0
- subedit-0.5.0/src/subedit/domain/rules/__init__.py +44 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/rules/layout.py +18 -21
- subedit-0.5.0/src/subedit/domain/rules/markdown.py +226 -0
- subedit-0.3.2/src/subedit/domain/rules/rewrite.py → subedit-0.5.0/src/subedit/domain/rules/sentence.py +90 -66
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/rules/structure.py +56 -59
- subedit-0.5.0/src/subedit/domain/rules/style.py +364 -0
- subedit-0.5.0/src/subedit/infrastructure/__init__.py +128 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/__init__.py +2 -1
- subedit-0.5.0/src/subedit/infrastructure/config_sources/config_schema.py +86 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/toml_config_source.py +74 -16
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/autocorrect_linter.py +6 -4
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/vale_linter.py +12 -10
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/__init__.py +13 -0
- subedit-0.5.0/src/subedit/infrastructure/parsers/composite_parser.py +27 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/__init__.py +6 -1
- subedit-0.5.0/src/subedit/infrastructure/parsers/languages/comment_only_profile.py +101 -0
- subedit-0.5.0/src/subedit/infrastructure/parsers/languages/language_profile.py +110 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/python_profile.py +7 -14
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/typescript_profile.py +38 -20
- subedit-0.5.0/src/subedit/infrastructure/parsers/markdown_parser.py +237 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/tree_sitter_parser.py +27 -23
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/cli.py +51 -27
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/agent_harness.py +2 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/claude_code_harness.py +4 -4
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/codex_harness.py +2 -2
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/hook.py +25 -8
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/report.py +7 -2
- subedit-0.5.0/src/subedit/presentation/rule_reference.py +43 -0
- subedit-0.5.0/src/subedit/vale/styles/subedit/EditHistory.yml +16 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/Semicolon.yml +1 -0
- subedit-0.5.0/src/subedit/vale/vale.ini +14 -0
- {subedit-0.3.2 → subedit-0.5.0}/subedit.toml +6 -3
- {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_corpus_meter.py +3 -3
- {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_fixer.py +24 -22
- {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_linter.py +30 -38
- {subedit-0.3.2/tests → subedit-0.5.0/tests/application}/test_report_aggregator.py +46 -26
- subedit-0.5.0/tests/application/test_surface_ocp.py +149 -0
- subedit-0.5.0/tests/conftest.py +9 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_comment.py +5 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_diagnostic.py +3 -6
- subedit-0.5.0/tests/domain/test_embedded.py +61 -0
- subedit-0.5.0/tests/domain/test_markdown.py +240 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_prose.py +106 -9
- subedit-0.5.0/tests/domain/test_rule_option.py +52 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_rules_layout.py +10 -13
- subedit-0.3.2/tests/test_rules_rewrite.py → subedit-0.5.0/tests/domain/test_rules_sentence.py +86 -18
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_rules_structure.py +16 -15
- subedit-0.5.0/tests/domain/test_style.py +448 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_text.py +9 -12
- subedit-0.5.0/tests/factories.py +111 -0
- {subedit-0.3.2 → subedit-0.5.0}/tests/fixtures/vale/violations.py +0 -2
- subedit-0.5.0/tests/infrastructure/test_config.py +230 -0
- subedit-0.5.0/tests/infrastructure/test_data_formats.py +79 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_language_parity.py +6 -15
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser.py +33 -37
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser_ocp.py +31 -8
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_parser_oracle.py +5 -6
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_schema.py +16 -19
- {subedit-0.3.2/tests → subedit-0.5.0/tests/infrastructure}/test_typescript.py +61 -39
- subedit-0.5.0/tests/infrastructure/test_vale_style.py +103 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_cli.py +69 -71
- {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_harnesses.py +7 -9
- {subedit-0.3.2/tests → subedit-0.5.0/tests/presentation}/test_hook.py +48 -43
- {subedit-0.3.2/tests → subedit-0.5.0/tests/repository}/test_plugin.py +18 -18
- {subedit-0.3.2/tests → subedit-0.5.0/tests/repository}/test_self_hosting.py +39 -14
- subedit-0.5.0/uv.lock +701 -0
- subedit-0.3.2/.vale.ini +0 -13
- subedit-0.3.2/PKG-INFO +0 -227
- subedit-0.3.2/README.md +0 -193
- subedit-0.3.2/docs/rules.md +0 -18
- subedit-0.3.2/examples/README.md +0 -47
- subedit-0.3.2/examples/probe.py +0 -5
- subedit-0.3.2/examples/subedit.toml +0 -2
- subedit-0.3.2/examples/walkthrough.py +0 -71
- subedit-0.3.2/plugins/subedit/skills/judging-findings/SKILL.md +0 -45
- subedit-0.3.2/src/subedit/application/linter.py +0 -74
- subedit-0.3.2/src/subedit/domain/models/prose_map.py +0 -44
- subedit-0.3.2/src/subedit/domain/ports/rule.py +0 -28
- subedit-0.3.2/src/subedit/domain/ports/source_parser.py +0 -17
- subedit-0.3.2/src/subedit/domain/rules/__init__.py +0 -15
- subedit-0.3.2/src/subedit/infrastructure/__init__.py +0 -77
- subedit-0.3.2/src/subedit/infrastructure/config_sources/config_schema.py +0 -44
- subedit-0.3.2/src/subedit/infrastructure/parsers/languages/language_profile.py +0 -67
- subedit-0.3.2/src/subedit/presentation/rule_reference.py +0 -24
- subedit-0.3.2/src/subedit/vale/styles/subedit/EditHistory.yml +0 -8
- subedit-0.3.2/src/subedit/vale/styles/subedit/Ellipsis.yml +0 -10
- subedit-0.3.2/src/subedit/vale/styles/subedit/MidSentenceColon.yml +0 -9
- subedit-0.3.2/src/subedit/vale/styles/subedit/MidSentenceColonMarkup.yml +0 -8
- subedit-0.3.2/src/subedit/vale/styles/subedit/SignatureTag.yml +0 -7
- subedit-0.3.2/src/subedit/vale/vale.ini +0 -11
- subedit-0.3.2/tests/test_config.py +0 -155
- subedit-0.3.2/tests/test_vale_style.py +0 -132
- subedit-0.3.2/uv.lock +0 -502
- {subedit-0.3.2 → subedit-0.5.0}/.claude/settings.json +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/.github/dependabot.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/.github/release.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/pr.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/.github/workflows/release.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/LICENSE +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/domain/models/comment_layout.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/config_sources/formatter_width.py +5 -5
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/__init__.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/linters/source_path.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/infrastructure/parsers/languages/code_line.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/__init__.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/presentation/harnesses/__init__.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/DateRef.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/EmDash.yml +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/src/subedit/vale/styles/subedit/ExternalRef.yml +0 -0
- {subedit-0.3.2/tests → subedit-0.5.0/tests/domain}/test_position.py +0 -0
- {subedit-0.3.2 → subedit-0.5.0}/tests/fixtures/autocorrect/unspaced.py +0 -0
- {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
|
|
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
|
|
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:
|
|
75
|
+
- uses: astral-sh/setup-uv@v6
|
|
78
76
|
with:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
subedit-0.5.0/.vale.ini
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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).
|