doc-code 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.
- doc_code-0.1.0/LICENSE +21 -0
- doc_code-0.1.0/PKG-INFO +138 -0
- doc_code-0.1.0/README.md +99 -0
- doc_code-0.1.0/doc_code/__init__.py +3 -0
- doc_code-0.1.0/doc_code/ai.py +206 -0
- doc_code-0.1.0/doc_code/cli.py +593 -0
- doc_code-0.1.0/doc_code/config.py +362 -0
- doc_code-0.1.0/doc_code/editor.py +496 -0
- doc_code-0.1.0/doc_code/errors.py +21 -0
- doc_code-0.1.0/doc_code/git.py +74 -0
- doc_code-0.1.0/doc_code/py.typed +1 -0
- doc_code-0.1.0/doc_code/scope.py +105 -0
- doc_code-0.1.0/doc_code/symbols.py +594 -0
- doc_code-0.1.0/doc_code.egg-info/PKG-INFO +138 -0
- doc_code-0.1.0/doc_code.egg-info/SOURCES.txt +24 -0
- doc_code-0.1.0/doc_code.egg-info/dependency_links.txt +1 -0
- doc_code-0.1.0/doc_code.egg-info/entry_points.txt +2 -0
- doc_code-0.1.0/doc_code.egg-info/requires.txt +14 -0
- doc_code-0.1.0/doc_code.egg-info/top_level.txt +1 -0
- doc_code-0.1.0/pyproject.toml +101 -0
- doc_code-0.1.0/setup.cfg +4 -0
- doc_code-0.1.0/tests/test_cli.py +814 -0
- doc_code-0.1.0/tests/test_config.py +409 -0
- doc_code-0.1.0/tests/test_editor.py +751 -0
- doc_code-0.1.0/tests/test_git.py +108 -0
- doc_code-0.1.0/tests/test_scope.py +188 -0
doc_code-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 André Argôlo (argolo.dev)
|
|
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.
|
doc_code-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: doc-code
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate and safely apply AI-assisted source documentation.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://argolo.dev
|
|
7
|
+
Project-URL: Documentation, https://argolo.dev
|
|
8
|
+
Keywords: documentation,docstrings,developer-tools,generative-ai
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
20
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: tree-sitter<0.26,>=0.25
|
|
26
|
+
Requires-Dist: tree-sitter-javascript<0.24,>=0.23
|
|
27
|
+
Requires-Dist: tree-sitter-typescript<0.24,>=0.23
|
|
28
|
+
Requires-Dist: typer<1,>=0.12
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build<2,>=1.2; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy<2,>=1.11; extra == "dev"
|
|
32
|
+
Requires-Dist: pip-audit<3,>=2.7; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest<10,>=8; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov<8,>=5; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff<1,>=0.6; extra == "dev"
|
|
36
|
+
Requires-Dist: commitar<2,>=1.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: setuptools<84,>=83; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Doc Code
|
|
41
|
+
|
|
42
|
+
[Leia em português](README-PTBR.md)
|
|
43
|
+
|
|
44
|
+
`doc-code` generates documentation for Python, JavaScript, and TypeScript files using OpenAI, Gemini, or Ollama.
|
|
45
|
+
|
|
46
|
+
By default, it displays a reviewable diff of the generated documentation. Use `--no-show-diff` to keep only the compact summary. To write the changes, run:
|
|
47
|
+
|
|
48
|
+
```shell
|
|
49
|
+
doc-code --output apply
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For automation, combine `--yes` with `--output apply`. Each file is written as soon as its generation and validation complete; a later failure does not roll back files that were already applied. Immediately before writing, the file is compared with the preview. If it changed, `doc-code` does not overwrite it. With `confirm = true`, Doc Code shows and confirms each formatted docstring before applying it; with `confirm = false`, it reviews the complete generated file diff before applying it.
|
|
53
|
+
|
|
54
|
+
## Installation and requirements
|
|
55
|
+
|
|
56
|
+
`doc-code` requires Python 3.11 or later and Git on your `PATH`. To process JavaScript, install Node.js. For TypeScript/TSX, also install the `tsc` compiler (for example, `npm install --global typescript`). These executables validate generated syntax before any file is changed.
|
|
57
|
+
|
|
58
|
+
For development, run `uv sync --locked --all-extras`. To install the published package, run `pip install doc-code`.
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
```shell
|
|
63
|
+
doc-code # Git changes: staged, unstaged, and untracked files
|
|
64
|
+
doc-code src/a.py src/b.ts # one or more specific files or directories
|
|
65
|
+
doc-code --selection repository # every eligible file
|
|
66
|
+
doc-code --coverage all --format numpy
|
|
67
|
+
doc-code --language Portuguese
|
|
68
|
+
doc-code --request-scope symbol
|
|
69
|
+
doc-code --no-show-diff # compact preview without the unified diff
|
|
70
|
+
doc-code --check # fail CI when symbols are undocumented
|
|
71
|
+
doc-code --continue-on-error # return status 0 when a file is skipped
|
|
72
|
+
doc-code config init
|
|
73
|
+
doc-code config show
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Configuration uses this precedence: flags, `--config`, `DOC_CODE_*` environment variables, `.doc-code.toml`, user configuration, and defaults. Set `language = "Portuguese"` in `[documentation]`, use `DOC_CODE_LANGUAGE`, or pass `--language Portuguese` to control the generated language. Credentials use `OPENAI_API_KEY` or `GEMINI_API_KEY`; Ollama needs no credential.
|
|
77
|
+
|
|
78
|
+
## Configuration (`.doc-code.toml`)
|
|
79
|
+
|
|
80
|
+
Create a starter file with `doc-code config init`. The `[ai]`, `[documentation]`, and `[limits]` sections are merged into one configuration. The following are all accepted keys.
|
|
81
|
+
|
|
82
|
+
### `[ai]`
|
|
83
|
+
|
|
84
|
+
| Option | Values / default | Purpose |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `provider` | `openai`, `gemini`, or `ollama` · `ollama` | Selects the provider that generates descriptions. |
|
|
87
|
+
| `model` | string · provider-dependent | Single model used when `models` is empty: `qwen2.5-coder:14b`, `gpt-5.6-sol`, or `gemini-3.6-flash`. |
|
|
88
|
+
| `models` | list of up to 3 strings · `qwen2.5-coder:14b`, `gemma4:e4b` | Candidates rotated across attempts; takes precedence over `model`. An empty list uses only `model`. |
|
|
89
|
+
| `endpoint` | URL or absent · provider default endpoint | Overrides the provider endpoint. Authenticated providers require HTTPS outside loopback. |
|
|
90
|
+
| `max_input_tokens` | positive integer · `12000` | Estimated limit for the prompt sent to the model. |
|
|
91
|
+
| `context_window_tokens` | positive integer · `32768` | Total model context window; must fit input and output. |
|
|
92
|
+
| `max_output_tokens` | positive integer · `800` | Limit for generated response tokens. |
|
|
93
|
+
| `temperature` | number · `0.2` | Controls variation; must be non-negative and is between 0 and 2 for OpenAI and Gemini. |
|
|
94
|
+
| `timeout_seconds` | positive integer · `60` | Maximum duration of each provider call. |
|
|
95
|
+
|
|
96
|
+
`max_input_tokens + max_output_tokens` cannot exceed `context_window_tokens`.
|
|
97
|
+
|
|
98
|
+
### `[documentation]`
|
|
99
|
+
|
|
100
|
+
| Option | Values / default | Purpose |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `selection` | `changes`, `repository` · `changes` | Chooses Git changes or every eligible file. |
|
|
103
|
+
| `coverage` | `missing`, `minimal`, `all` · `missing` | `missing` includes every undocumented symbol; `minimal` includes only undocumented modules and top-level public APIs; `all` generates or replaces documentation for every symbol. |
|
|
104
|
+
| `request_scope` | `file`, `symbol` · `file` | Chooses the context for each call: a complete file or one symbol. It does not change which symbols are eligible. |
|
|
105
|
+
| `language` | non-empty string · `English` | Language requested for generated descriptions. |
|
|
106
|
+
| `python_format` | `google`, `numpy`, `sphinx` · `google` | Parameter-section style for Python docstrings. |
|
|
107
|
+
| `javascript_format` | `jsdoc` · `jsdoc` | Annotation style for JavaScript and TypeScript. |
|
|
108
|
+
| `output` | `preview`, `apply` · `preview` | Shows a preview or writes changes. |
|
|
109
|
+
| `confirm` | boolean · `true` | `true` shows and confirms each formatted docstring before applying it; `false` shows the generated file diff and confirms it before applying it. |
|
|
110
|
+
|
|
111
|
+
### `[limits]`
|
|
112
|
+
|
|
113
|
+
| Option | Values / default | Purpose |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| `max_files_per_request` | positive integer · `50` | Maximum number of files in a scope. |
|
|
116
|
+
| `max_file_bytes` | positive integer · `100000` | Maximum size of each processed file. |
|
|
117
|
+
| `exclude` | list of glob patterns | Removes files from scope. Defaults exclude dependencies, build artifacts, minified files, and `package-lock.json`. |
|
|
118
|
+
| `include` | list of glob patterns · empty list | When provided, keeps only files matching at least one pattern. |
|
|
119
|
+
|
|
120
|
+
## Python docstring style
|
|
121
|
+
|
|
122
|
+
Generated Python docstrings follow PEP 257: summaries end in a period; multiline docstrings have a blank line before and after sections, with closing quotes on their own line. Module and class docstrings are separated from the next declaration by one blank line. Empty lines have no indentation whitespace.
|
|
123
|
+
|
|
124
|
+
Line length also follows the target project. For Python, `doc-code` uses `tool.ruff.line-length` and `tool.ruff.lint.pydocstyle.convention` from the nearest `pyproject.toml` (or 88 and the configured `doc-code` format when absent). Long summaries are separated from the description by a blank line according to D205. For JavaScript and TypeScript, it uses `max-len` from the nearest `eslint.config.js`, `eslint.config.mjs`, or `eslint.config.cjs` (or 100 when no rule exists). Descriptions and JSDoc lines are wrapped while accounting for indentation and delimiters.
|
|
125
|
+
|
|
126
|
+
The `google`, `numpy`, and `sphinx` styles affect only the parameter section. To rewrite existing docstrings using one of these styles, run:
|
|
127
|
+
|
|
128
|
+
```shell
|
|
129
|
+
doc-code --coverage all --output apply --yes
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
For functions and methods, `doc-code` asks the model for a distinct description for every parameter and inserts it in the corresponding parameter section. The structured response must include every requested symbol and parameter; incomplete responses are rejected and generated again. Legacy integrations retain a fallback text for compatibility.
|
|
133
|
+
|
|
134
|
+
By default, `request_scope = "file"` sends the file and all symbols that require generation in a single request. Use `request_scope = "symbol"` (or `--request-scope symbol`) to send each symbol in a separate request with only its code scope. This reduces context in large files but can increase latency and the number of calls. For modules, it sends a structural index — opening docstring, imports, constants, and public signatures — without bodies. With `output = "apply"` and `confirm = true`, each formatted docstring is reviewed and confirmed individually; a rejected docstring is reported and skipped while the remaining symbols continue. `--yes` applies without prompts. With `confirm = false`, the full file diff is reviewed before any of its changes are written.
|
|
135
|
+
|
|
136
|
+
Regardless of `request_scope`, `coverage` decides which symbols are eligible and `request_scope` only changes the context unit sent to the model. Use `coverage = "all"` to rewrite existing documentation.
|
|
137
|
+
|
|
138
|
+
Partial failures return status 1 after all remaining eligible files have been processed. Use `--continue-on-error` only when automation explicitly accepts partial results.
|
doc_code-0.1.0/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Doc Code
|
|
2
|
+
|
|
3
|
+
[Leia em português](README-PTBR.md)
|
|
4
|
+
|
|
5
|
+
`doc-code` generates documentation for Python, JavaScript, and TypeScript files using OpenAI, Gemini, or Ollama.
|
|
6
|
+
|
|
7
|
+
By default, it displays a reviewable diff of the generated documentation. Use `--no-show-diff` to keep only the compact summary. To write the changes, run:
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
doc-code --output apply
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For automation, combine `--yes` with `--output apply`. Each file is written as soon as its generation and validation complete; a later failure does not roll back files that were already applied. Immediately before writing, the file is compared with the preview. If it changed, `doc-code` does not overwrite it. With `confirm = true`, Doc Code shows and confirms each formatted docstring before applying it; with `confirm = false`, it reviews the complete generated file diff before applying it.
|
|
14
|
+
|
|
15
|
+
## Installation and requirements
|
|
16
|
+
|
|
17
|
+
`doc-code` requires Python 3.11 or later and Git on your `PATH`. To process JavaScript, install Node.js. For TypeScript/TSX, also install the `tsc` compiler (for example, `npm install --global typescript`). These executables validate generated syntax before any file is changed.
|
|
18
|
+
|
|
19
|
+
For development, run `uv sync --locked --all-extras`. To install the published package, run `pip install doc-code`.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
doc-code # Git changes: staged, unstaged, and untracked files
|
|
25
|
+
doc-code src/a.py src/b.ts # one or more specific files or directories
|
|
26
|
+
doc-code --selection repository # every eligible file
|
|
27
|
+
doc-code --coverage all --format numpy
|
|
28
|
+
doc-code --language Portuguese
|
|
29
|
+
doc-code --request-scope symbol
|
|
30
|
+
doc-code --no-show-diff # compact preview without the unified diff
|
|
31
|
+
doc-code --check # fail CI when symbols are undocumented
|
|
32
|
+
doc-code --continue-on-error # return status 0 when a file is skipped
|
|
33
|
+
doc-code config init
|
|
34
|
+
doc-code config show
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Configuration uses this precedence: flags, `--config`, `DOC_CODE_*` environment variables, `.doc-code.toml`, user configuration, and defaults. Set `language = "Portuguese"` in `[documentation]`, use `DOC_CODE_LANGUAGE`, or pass `--language Portuguese` to control the generated language. Credentials use `OPENAI_API_KEY` or `GEMINI_API_KEY`; Ollama needs no credential.
|
|
38
|
+
|
|
39
|
+
## Configuration (`.doc-code.toml`)
|
|
40
|
+
|
|
41
|
+
Create a starter file with `doc-code config init`. The `[ai]`, `[documentation]`, and `[limits]` sections are merged into one configuration. The following are all accepted keys.
|
|
42
|
+
|
|
43
|
+
### `[ai]`
|
|
44
|
+
|
|
45
|
+
| Option | Values / default | Purpose |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `provider` | `openai`, `gemini`, or `ollama` · `ollama` | Selects the provider that generates descriptions. |
|
|
48
|
+
| `model` | string · provider-dependent | Single model used when `models` is empty: `qwen2.5-coder:14b`, `gpt-5.6-sol`, or `gemini-3.6-flash`. |
|
|
49
|
+
| `models` | list of up to 3 strings · `qwen2.5-coder:14b`, `gemma4:e4b` | Candidates rotated across attempts; takes precedence over `model`. An empty list uses only `model`. |
|
|
50
|
+
| `endpoint` | URL or absent · provider default endpoint | Overrides the provider endpoint. Authenticated providers require HTTPS outside loopback. |
|
|
51
|
+
| `max_input_tokens` | positive integer · `12000` | Estimated limit for the prompt sent to the model. |
|
|
52
|
+
| `context_window_tokens` | positive integer · `32768` | Total model context window; must fit input and output. |
|
|
53
|
+
| `max_output_tokens` | positive integer · `800` | Limit for generated response tokens. |
|
|
54
|
+
| `temperature` | number · `0.2` | Controls variation; must be non-negative and is between 0 and 2 for OpenAI and Gemini. |
|
|
55
|
+
| `timeout_seconds` | positive integer · `60` | Maximum duration of each provider call. |
|
|
56
|
+
|
|
57
|
+
`max_input_tokens + max_output_tokens` cannot exceed `context_window_tokens`.
|
|
58
|
+
|
|
59
|
+
### `[documentation]`
|
|
60
|
+
|
|
61
|
+
| Option | Values / default | Purpose |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `selection` | `changes`, `repository` · `changes` | Chooses Git changes or every eligible file. |
|
|
64
|
+
| `coverage` | `missing`, `minimal`, `all` · `missing` | `missing` includes every undocumented symbol; `minimal` includes only undocumented modules and top-level public APIs; `all` generates or replaces documentation for every symbol. |
|
|
65
|
+
| `request_scope` | `file`, `symbol` · `file` | Chooses the context for each call: a complete file or one symbol. It does not change which symbols are eligible. |
|
|
66
|
+
| `language` | non-empty string · `English` | Language requested for generated descriptions. |
|
|
67
|
+
| `python_format` | `google`, `numpy`, `sphinx` · `google` | Parameter-section style for Python docstrings. |
|
|
68
|
+
| `javascript_format` | `jsdoc` · `jsdoc` | Annotation style for JavaScript and TypeScript. |
|
|
69
|
+
| `output` | `preview`, `apply` · `preview` | Shows a preview or writes changes. |
|
|
70
|
+
| `confirm` | boolean · `true` | `true` shows and confirms each formatted docstring before applying it; `false` shows the generated file diff and confirms it before applying it. |
|
|
71
|
+
|
|
72
|
+
### `[limits]`
|
|
73
|
+
|
|
74
|
+
| Option | Values / default | Purpose |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| `max_files_per_request` | positive integer · `50` | Maximum number of files in a scope. |
|
|
77
|
+
| `max_file_bytes` | positive integer · `100000` | Maximum size of each processed file. |
|
|
78
|
+
| `exclude` | list of glob patterns | Removes files from scope. Defaults exclude dependencies, build artifacts, minified files, and `package-lock.json`. |
|
|
79
|
+
| `include` | list of glob patterns · empty list | When provided, keeps only files matching at least one pattern. |
|
|
80
|
+
|
|
81
|
+
## Python docstring style
|
|
82
|
+
|
|
83
|
+
Generated Python docstrings follow PEP 257: summaries end in a period; multiline docstrings have a blank line before and after sections, with closing quotes on their own line. Module and class docstrings are separated from the next declaration by one blank line. Empty lines have no indentation whitespace.
|
|
84
|
+
|
|
85
|
+
Line length also follows the target project. For Python, `doc-code` uses `tool.ruff.line-length` and `tool.ruff.lint.pydocstyle.convention` from the nearest `pyproject.toml` (or 88 and the configured `doc-code` format when absent). Long summaries are separated from the description by a blank line according to D205. For JavaScript and TypeScript, it uses `max-len` from the nearest `eslint.config.js`, `eslint.config.mjs`, or `eslint.config.cjs` (or 100 when no rule exists). Descriptions and JSDoc lines are wrapped while accounting for indentation and delimiters.
|
|
86
|
+
|
|
87
|
+
The `google`, `numpy`, and `sphinx` styles affect only the parameter section. To rewrite existing docstrings using one of these styles, run:
|
|
88
|
+
|
|
89
|
+
```shell
|
|
90
|
+
doc-code --coverage all --output apply --yes
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For functions and methods, `doc-code` asks the model for a distinct description for every parameter and inserts it in the corresponding parameter section. The structured response must include every requested symbol and parameter; incomplete responses are rejected and generated again. Legacy integrations retain a fallback text for compatibility.
|
|
94
|
+
|
|
95
|
+
By default, `request_scope = "file"` sends the file and all symbols that require generation in a single request. Use `request_scope = "symbol"` (or `--request-scope symbol`) to send each symbol in a separate request with only its code scope. This reduces context in large files but can increase latency and the number of calls. For modules, it sends a structural index — opening docstring, imports, constants, and public signatures — without bodies. With `output = "apply"` and `confirm = true`, each formatted docstring is reviewed and confirmed individually; a rejected docstring is reported and skipped while the remaining symbols continue. `--yes` applies without prompts. With `confirm = false`, the full file diff is reviewed before any of its changes are written.
|
|
96
|
+
|
|
97
|
+
Regardless of `request_scope`, `coverage` decides which symbols are eligible and `request_scope` only changes the context unit sent to the model. Use `coverage = "all"` to rewrite existing documentation.
|
|
98
|
+
|
|
99
|
+
Partial failures return status 1 after all remaining eligible files have been processed. Use `--continue-on-error` only when automation explicitly accepts partial results.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Provider calls and strict JSON response validation for documentation descriptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from math import ceil
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
from urllib.error import URLError
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from urllib.request import Request, urlopen
|
|
12
|
+
|
|
13
|
+
from .config import Settings
|
|
14
|
+
from .errors import AIProviderError, AITimeoutError, DocGubError, InvalidAIResponseError
|
|
15
|
+
from .symbols import Documentation, Symbol
|
|
16
|
+
|
|
17
|
+
_MAX_PROVIDER_RESPONSE_BYTES = 1_000_000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def estimate_tokens(text: str) -> int:
|
|
21
|
+
"""Estimate tokens conservatively from the UTF-8 byte length."""
|
|
22
|
+
return ceil(len(text.encode("utf-8")) / 3)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def prompt(content: str, symbols: list[Symbol], language: str = "English") -> str:
|
|
26
|
+
"""Build the strict JSON prompt sent to a provider."""
|
|
27
|
+
names = [{"symbol": item.name, "kind": item.kind, "arguments": item.args} for item in symbols]
|
|
28
|
+
return (
|
|
29
|
+
"Return only a JSON object mapping each requested symbol to an object with "
|
|
30
|
+
"`description` (a concise factual description) and `arguments` (an object mapping "
|
|
31
|
+
"every requested argument name to its specific description). Use an empty `arguments` "
|
|
32
|
+
"object when a symbol has no arguments. Do not include Markdown or code fences. Write "
|
|
33
|
+
f"all documentation text in {json.dumps(language, ensure_ascii=False)}. Requested symbols: "
|
|
34
|
+
+ json.dumps(names)
|
|
35
|
+
+ "\n\nSOURCE:\n"
|
|
36
|
+
+ content
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _post(
|
|
41
|
+
url: str, payload: dict[str, Any], headers: dict[str, str], timeout: int
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Post JSON and convert transport or decoding failures to domain errors."""
|
|
44
|
+
parsed = urlparse(url)
|
|
45
|
+
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
46
|
+
raise AIProviderError("The AI provider endpoint must be an absolute HTTP(S) URL.")
|
|
47
|
+
try:
|
|
48
|
+
with urlopen(
|
|
49
|
+
Request(url, data=json.dumps(payload).encode(), headers=headers, method="POST"),
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
) as response: # nosec B310 - user-configured endpoint
|
|
52
|
+
body = response.read(_MAX_PROVIDER_RESPONSE_BYTES + 1)
|
|
53
|
+
if len(body) > _MAX_PROVIDER_RESPONSE_BYTES:
|
|
54
|
+
raise AIProviderError("The AI provider response exceeds the supported size.")
|
|
55
|
+
decoded = json.loads(body)
|
|
56
|
+
if not isinstance(decoded, dict):
|
|
57
|
+
raise TypeError("The AI provider response must be a JSON object.")
|
|
58
|
+
return cast(dict[str, Any], decoded)
|
|
59
|
+
except TimeoutError as exc:
|
|
60
|
+
raise AITimeoutError(f"Unable to contact the AI provider: {exc}") from exc
|
|
61
|
+
except URLError as exc:
|
|
62
|
+
if isinstance(exc.reason, TimeoutError):
|
|
63
|
+
raise AITimeoutError(f"Unable to contact the AI provider: {exc.reason}") from exc
|
|
64
|
+
raise AIProviderError(f"Unable to contact the AI provider: {exc}") from exc
|
|
65
|
+
except (json.JSONDecodeError, KeyError, IndexError, TypeError) as exc:
|
|
66
|
+
raise AIProviderError("The AI provider returned an invalid response.") from exc
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def documentation_for(
|
|
70
|
+
content: str, symbols: list[Symbol], settings: Settings
|
|
71
|
+
) -> dict[str, Documentation]:
|
|
72
|
+
"""Generate validated documentation for the requested symbols."""
|
|
73
|
+
body = prompt(content, symbols, settings.language)
|
|
74
|
+
tokens = estimate_tokens(body)
|
|
75
|
+
if (
|
|
76
|
+
tokens > settings.max_input_tokens
|
|
77
|
+
or tokens + settings.max_output_tokens > settings.context_window_tokens
|
|
78
|
+
):
|
|
79
|
+
raise DocGubError(
|
|
80
|
+
"AI input exceeds configured token limits; narrow the scope or increase limits."
|
|
81
|
+
)
|
|
82
|
+
if settings.provider == "openai":
|
|
83
|
+
key = os.getenv("OPENAI_API_KEY")
|
|
84
|
+
if not key:
|
|
85
|
+
raise DocGubError("OPENAI_API_KEY is not configured.")
|
|
86
|
+
data = _post(
|
|
87
|
+
settings.endpoint or "https://api.openai.com/v1/chat/completions",
|
|
88
|
+
{
|
|
89
|
+
"model": settings.model,
|
|
90
|
+
"messages": [{"role": "user", "content": body}],
|
|
91
|
+
"max_completion_tokens": settings.max_output_tokens,
|
|
92
|
+
"temperature": settings.temperature,
|
|
93
|
+
},
|
|
94
|
+
{"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
|
|
95
|
+
settings.timeout_seconds,
|
|
96
|
+
)
|
|
97
|
+
answer = _provider_answer(data, "openai")
|
|
98
|
+
elif settings.provider == "gemini":
|
|
99
|
+
key = os.getenv("GEMINI_API_KEY")
|
|
100
|
+
if not key:
|
|
101
|
+
raise DocGubError("GEMINI_API_KEY is not configured.")
|
|
102
|
+
endpoint = settings.endpoint or (
|
|
103
|
+
"https://generativelanguage.googleapis.com/v1beta/models/"
|
|
104
|
+
f"{settings.model}:generateContent"
|
|
105
|
+
)
|
|
106
|
+
data = _post(
|
|
107
|
+
endpoint,
|
|
108
|
+
{"contents": [{"parts": [{"text": body}]}]},
|
|
109
|
+
{"Content-Type": "application/json", "x-goog-api-key": key},
|
|
110
|
+
settings.timeout_seconds,
|
|
111
|
+
)
|
|
112
|
+
answer = _provider_answer(data, "gemini")
|
|
113
|
+
else:
|
|
114
|
+
data = _post(
|
|
115
|
+
settings.endpoint or "http://localhost:11434/api/generate",
|
|
116
|
+
{
|
|
117
|
+
"model": settings.model,
|
|
118
|
+
"prompt": body,
|
|
119
|
+
"stream": False,
|
|
120
|
+
"options": {
|
|
121
|
+
"temperature": settings.temperature,
|
|
122
|
+
"num_ctx": settings.context_window_tokens,
|
|
123
|
+
"num_predict": settings.max_output_tokens,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{"Content-Type": "application/json"},
|
|
127
|
+
settings.timeout_seconds,
|
|
128
|
+
)
|
|
129
|
+
answer = _provider_answer(data, "ollama")
|
|
130
|
+
return _documentation_response(answer, symbols)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _provider_answer(data: dict[str, Any], provider: str) -> str:
|
|
134
|
+
"""Extract provider text while keeping malformed envelopes inside the domain boundary."""
|
|
135
|
+
try:
|
|
136
|
+
if provider == "openai":
|
|
137
|
+
answer = data["choices"][0]["message"]["content"]
|
|
138
|
+
elif provider == "gemini":
|
|
139
|
+
answer = data["candidates"][0]["content"]["parts"][0]["text"]
|
|
140
|
+
else:
|
|
141
|
+
answer = data["response"]
|
|
142
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
143
|
+
raise AIProviderError(f"The {provider} provider returned an invalid response.") from exc
|
|
144
|
+
if not isinstance(answer, str):
|
|
145
|
+
raise AIProviderError(f"The {provider} provider returned an invalid response.")
|
|
146
|
+
return answer
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _documentation_response(answer: str, symbols: list[Symbol]) -> dict[str, Documentation]:
|
|
150
|
+
"""Validate and normalize the structured documentation returned by a provider."""
|
|
151
|
+
if not isinstance(answer, str):
|
|
152
|
+
raise InvalidAIResponseError("The AI returned documentation in an invalid format.")
|
|
153
|
+
try:
|
|
154
|
+
parsed = json.loads(answer)
|
|
155
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
156
|
+
raise InvalidAIResponseError("The AI returned invalid documentation JSON.") from exc
|
|
157
|
+
if not isinstance(parsed, dict):
|
|
158
|
+
raise InvalidAIResponseError("The AI response must be a JSON object of documentation.")
|
|
159
|
+
|
|
160
|
+
requested = {symbol.name: symbol for symbol in symbols}
|
|
161
|
+
if set(parsed) != set(requested):
|
|
162
|
+
raise InvalidAIResponseError("The AI response must document every requested symbol.")
|
|
163
|
+
normalized: dict[str, Documentation] = {}
|
|
164
|
+
for name, value in parsed.items():
|
|
165
|
+
if not isinstance(name, str) or name not in requested:
|
|
166
|
+
raise InvalidAIResponseError("The AI response contains an unexpected symbol.")
|
|
167
|
+
normalized[name] = _normalize_documentation(name, value, requested[name])
|
|
168
|
+
return normalized
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _normalize_documentation(name: str, value: Any, symbol: Symbol) -> Documentation:
|
|
172
|
+
"""Validate one symbol's generated description and argument mapping."""
|
|
173
|
+
if isinstance(value, str):
|
|
174
|
+
legacy_description = value.strip()
|
|
175
|
+
if not legacy_description:
|
|
176
|
+
raise InvalidAIResponseError("Each symbol must contain a non-empty description.")
|
|
177
|
+
return Documentation(legacy_description)
|
|
178
|
+
if not isinstance(value, dict):
|
|
179
|
+
raise InvalidAIResponseError("Each symbol must contain documentation details.")
|
|
180
|
+
structured_description = value.get("description")
|
|
181
|
+
arguments = value.get("arguments", {})
|
|
182
|
+
valid_arguments = isinstance(arguments, dict) and all(
|
|
183
|
+
isinstance(argument, str)
|
|
184
|
+
and isinstance(argument_description, str)
|
|
185
|
+
and bool(argument_description.strip())
|
|
186
|
+
for argument, argument_description in arguments.items()
|
|
187
|
+
)
|
|
188
|
+
if (
|
|
189
|
+
not isinstance(structured_description, str)
|
|
190
|
+
or not structured_description.strip()
|
|
191
|
+
or not valid_arguments
|
|
192
|
+
):
|
|
193
|
+
raise InvalidAIResponseError(
|
|
194
|
+
"Each symbol must contain a non-empty description and argument descriptions."
|
|
195
|
+
)
|
|
196
|
+
if set(arguments) != set(symbol.args):
|
|
197
|
+
raise InvalidAIResponseError(
|
|
198
|
+
f"Documentation for `{name}` must describe every requested argument."
|
|
199
|
+
)
|
|
200
|
+
return Documentation(
|
|
201
|
+
structured_description.strip(),
|
|
202
|
+
{
|
|
203
|
+
argument: argument_description.strip()
|
|
204
|
+
for argument, argument_description in arguments.items()
|
|
205
|
+
},
|
|
206
|
+
)
|