open-code-review-toolkit 0.3.1__tar.gz → 0.4.1__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 (66) hide show
  1. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/PKG-INFO +8 -12
  2. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/README.md +6 -8
  3. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/pyproject.toml +41 -21
  4. open_code_review_toolkit-0.4.1/src/ocr_toolkit/__init__.py +10 -0
  5. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/_version.py +2 -2
  6. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/cli.py +1 -13
  7. open_code_review_toolkit-0.4.1/src/ocr_toolkit/common/git.py +53 -0
  8. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/config_writer.py +32 -4
  9. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/__init__.py +35 -0
  10. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/__main__.py +14 -0
  11. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/ansible.py +193 -0
  12. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/ansible_requirements.py +280 -0
  13. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/artifacts.py +74 -0
  14. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/categorize.py +107 -0
  15. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/collect.py +119 -0
  16. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/collectors.py +764 -0
  17. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/composer_manifests.py +327 -0
  18. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/go_manifests.py +309 -0
  19. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/infrastructure.py +274 -0
  20. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/invocation.py +85 -0
  21. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/javascript_manifests.py +446 -0
  22. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/manifest_model.py +28 -0
  23. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/mcp.py +338 -0
  24. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/model.py +293 -0
  25. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/project.py +115 -0
  26. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/python_manifests.py +500 -0
  27. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/repository.py +521 -0
  28. open_code_review_toolkit-0.4.1/src/ocr_toolkit/evidence/store.py +398 -0
  29. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/mcp_config.py +152 -65
  30. open_code_review_toolkit-0.4.1/src/ocr_toolkit/ocr_result.py +317 -0
  31. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/formatting.py +112 -21
  32. open_code_review_toolkit-0.4.1/src/ocr_toolkit/posting/result.py +78 -0
  33. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/settings.py +21 -37
  34. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/workflow.py +104 -20
  35. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/preflight.py +1 -1
  36. open_code_review_toolkit-0.4.1/src/ocr_toolkit/providers/__init__.py +1 -0
  37. open_code_review_toolkit-0.4.1/src/ocr_toolkit/providers/gitlab.py +38 -0
  38. open_code_review_toolkit-0.4.1/src/ocr_toolkit/review_runner.py +402 -0
  39. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/__init__.py +0 -1
  40. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/__main__.py +0 -8
  41. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/ansible.py +0 -561
  42. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/categorize.py +0 -162
  43. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/instructions.py +0 -269
  44. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/manifests.py +0 -471
  45. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/planner.py +0 -227
  46. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/render.py +0 -955
  47. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/repo.py +0 -674
  48. open_code_review_toolkit-0.3.1/src/ocr_toolkit/context/settings.py +0 -97
  49. open_code_review_toolkit-0.3.1/src/ocr_toolkit/posting/__init__.py +0 -1
  50. open_code_review_toolkit-0.3.1/src/ocr_toolkit/posting/result.py +0 -116
  51. open_code_review_toolkit-0.3.1/src/ocr_toolkit/review_runner.py +0 -117
  52. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/.gitignore +0 -0
  53. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/LICENSE +0 -0
  54. {open_code_review_toolkit-0.3.1/src/ocr_toolkit → open_code_review_toolkit-0.4.1/src/ocr_toolkit/common}/__init__.py +0 -0
  55. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/common/language.py +0 -0
  56. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/common/markdown.py +0 -0
  57. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/common/redaction.py +0 -0
  58. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/configure.py +0 -0
  59. {open_code_review_toolkit-0.3.1/src/ocr_toolkit/common → open_code_review_toolkit-0.4.1/src/ocr_toolkit/posting}/__init__.py +0 -0
  60. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/__main__.py +0 -0
  61. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/comments.py +0 -0
  62. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/gitlab.py +0 -0
  63. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/markers.py +0 -0
  64. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/payloads.py +0 -0
  65. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/posting/snapshot.py +0 -0
  66. {open_code_review_toolkit-0.3.1 → open_code_review_toolkit-0.4.1}/src/ocr_toolkit/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: Unofficial GitLab CI integration layer for Open Code Review
5
5
  Project-URL: Homepage, https://github.com/xeonvs/open-code-review-toolkit
6
6
  Project-URL: Repository, https://github.com/xeonvs/open-code-review-toolkit
@@ -217,21 +217,19 @@ Classifier: License :: OSI Approved :: Apache Software License
217
217
  Classifier: Operating System :: MacOS
218
218
  Classifier: Operating System :: POSIX :: Linux
219
219
  Classifier: Programming Language :: Python :: 3 :: Only
220
- Classifier: Programming Language :: Python :: 3.10
221
- Classifier: Programming Language :: Python :: 3.11
222
220
  Classifier: Programming Language :: Python :: 3.12
223
221
  Classifier: Programming Language :: Python :: 3.13
224
222
  Classifier: Programming Language :: Python :: 3.14
225
223
  Classifier: Typing :: Typed
226
- Requires-Python: <3.15,>=3.10
224
+ Requires-Python: <3.15,>=3.12
227
225
  Description-Content-Type: text/markdown
228
226
 
229
227
  # Open Code Review Toolkit
230
228
 
231
- Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review). It provides bounded repository context generation, environment-driven OCR configuration, preflight validation, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
229
+ Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review). It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
232
230
 
233
231
  > [!NOTE]
234
- > The project is under active development. It currently targets Python 3.10-3.14 on Linux and macOS; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
232
+ > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
235
233
 
236
234
  ## Install
237
235
 
@@ -243,9 +241,9 @@ ocr --version
243
241
  ocr-ci --help
244
242
  ```
245
243
 
246
- The current compatibility target is OCR `1.8.0`. CI should pin the release and verify its published checksum before execution.
244
+ The current compatibility target is OCR `1.8.2`. CI should pin the release and verify its published checksum before execution.
247
245
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
248
- Review output defaults to English. Set `OCR_REVIEW_LANGUAGE=Russian` to use Russian consistently in both OCR configuration and generated review context.
246
+ Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
249
247
 
250
248
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
251
249
 
@@ -255,7 +253,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
255
253
 
256
254
  Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
257
255
 
258
- Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the context generator supplies them to OCR only when the current merge request is not changing that file. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
256
+ Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the evidence collector supplies target-ref decisions to OCR and never lets a source change self-authorize its own review. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
259
257
 
260
258
  ## Project development
261
259
 
@@ -270,13 +268,11 @@ The project is evolving from bounded background generation toward a shared Repos
270
268
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
271
269
  2. Pin and checksum the OCR binary.
272
270
  3. Install this package.
273
- 4. Run the five helper stages around `ocr review`:
271
+ 4. Run the four public helper stages around `ocr review`:
274
272
 
275
273
  ```console
276
274
  ocr-ci preflight
277
275
  ocr-ci configure
278
- ocr-ci mcp-config
279
- ocr-ci context --output .review-context/dependencies.md
280
276
  ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
281
277
  ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
282
278
  ```
@@ -1,9 +1,9 @@
1
1
  # Open Code Review Toolkit
2
2
 
3
- Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review). It provides bounded repository context generation, environment-driven OCR configuration, preflight validation, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
3
+ Open Code Review Toolkit is an unofficial GitLab CI integration layer for [Alibaba Open Code Review](https://github.com/alibaba/open-code-review). It provides bounded repository evidence, a compact review bootstrap, a built-in read-only MCP server, environment-driven OCR configuration, preflight validation, and safe GitLab merge-request posting. It does **not** bundle or download the `ocr` binary.
4
4
 
5
5
  > [!NOTE]
6
- > The project is under active development. It currently targets Python 3.10-3.14 on Linux and macOS; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
6
+ > The project is under active development; the public API, CLI, environment contract, and generated schemas may evolve before 1.0.
7
7
 
8
8
  ## Install
9
9
 
@@ -15,9 +15,9 @@ ocr --version
15
15
  ocr-ci --help
16
16
  ```
17
17
 
18
- The current compatibility target is OCR `1.8.0`. CI should pin the release and verify its published checksum before execution.
18
+ The current compatibility target is OCR `1.8.2`. CI should pin the release and verify its published checksum before execution.
19
19
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
20
- Review output defaults to English. Set `OCR_REVIEW_LANGUAGE=Russian` to use Russian consistently in both OCR configuration and generated review context.
20
+ Review output defaults to English. `OCR_REVIEW_LANGUAGE` accepts another explicit language name when a project needs localized review output; for example, `OCR_REVIEW_LANGUAGE=Russian`.
21
21
 
22
22
  Stable distributions are published to [PyPI](https://pypi.org/project/open-code-review-toolkit/) and mirrored as checksum-listed, provenance-attested assets in the corresponding [GitHub Release](https://github.com/xeonvs/open-code-review-toolkit/releases). Development snapshots are published only to TestPyPI.
23
23
 
@@ -27,7 +27,7 @@ On a successful rerun, the toolkit replaces untouched OCR-only notes instead of
27
27
 
28
28
  Suppression uses both the GitLab diff position and a stable finding fingerprint, so ordinary line shifts do not normally bring the same bug back. A materially changed finding can still receive a new discussion. See [GitLab review operations](docs/operations.md) for the complete lifecycle, posting modes, permissions, failure behavior, and Mermaid state diagram.
29
29
 
30
- Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the context generator supplies them to OCR only when the current merge request is not changing that file. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
30
+ Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/accepted-decisions.md`; the evidence collector supplies target-ref decisions to OCR and never lets a source change self-authorize its own review. See [Accepted project decisions](docs/configuration.md#accepted-project-decisions) for the entry format, inline marker convention, security boundary, and limitations.
31
31
 
32
32
  ## Project development
33
33
 
@@ -42,13 +42,11 @@ The project is evolving from bounded background generation toward a shared Repos
42
42
  1. Configure protected/masked `GITLAB_API_TOKEN` and LLM variables in GitLab.
43
43
  2. Pin and checksum the OCR binary.
44
44
  3. Install this package.
45
- 4. Run the five helper stages around `ocr review`:
45
+ 4. Run the four public helper stages around `ocr review`:
46
46
 
47
47
  ```console
48
48
  ocr-ci preflight
49
49
  ocr-ci configure
50
- ocr-ci mcp-config
51
- ocr-ci context --output .review-context/dependencies.md
52
50
  ocr-ci review --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log -- ... --format json
53
51
  ocr-ci post --result /tmp/ocr-result.json --stderr /tmp/ocr-stderr.log
54
52
  ```
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling>=1.27", "hatch-vcs>=0.4"]
2
+ requires = ["hatchling>=1.31.0", "hatch-vcs>=0.5.0"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -7,7 +7,7 @@ name = "open-code-review-toolkit"
7
7
  dynamic = ["version"]
8
8
  description = "Unofficial GitLab CI integration layer for Open Code Review"
9
9
  readme = "README.md"
10
- requires-python = ">=3.10,<3.15"
10
+ requires-python = ">=3.12,<3.15"
11
11
  license = { file = "LICENSE" }
12
12
  authors = [{ name = "Open Code Review Toolkit contributors" }]
13
13
  keywords = ["code-review", "gitlab", "ci", "open-code-review"]
@@ -18,8 +18,6 @@ classifiers = [
18
18
  "Operating System :: MacOS",
19
19
  "Operating System :: POSIX :: Linux",
20
20
  "Programming Language :: Python :: 3 :: Only",
21
- "Programming Language :: Python :: 3.10",
22
- "Programming Language :: Python :: 3.11",
23
21
  "Programming Language :: Python :: 3.12",
24
22
  "Programming Language :: Python :: 3.13",
25
23
  "Programming Language :: Python :: 3.14",
@@ -39,16 +37,16 @@ ocr-ci = "ocr_toolkit.cli:main"
39
37
  [dependency-groups]
40
38
  dev = [
41
39
  "bandit==1.9.4",
42
- "build>=1.2.2",
43
- "coverage[toml]>=7.6",
44
- "hatch-vcs>=0.4",
45
- "mypy>=1.15",
46
- "pip-audit>=2.7",
47
- "pytest>=8.3",
48
- "pytest-cov>=6.0",
49
- "ruff>=0.11",
50
- "towncrier>=24.8",
51
- "twine>=6.1",
40
+ "build>=1.5.0",
41
+ "coverage[toml]>=7.15.2",
42
+ "hatch-vcs>=0.5.0",
43
+ "mypy>=2.3.0",
44
+ "pip-audit>=2.10.1",
45
+ "pytest>=9.1.1",
46
+ "pytest-cov>=7.1.0",
47
+ "ruff>=0.16.0",
48
+ "towncrier>=25.8.0",
49
+ "twine>=7.0.0",
52
50
  ]
53
51
 
54
52
  [tool.hatch.version]
@@ -67,13 +65,15 @@ only-include = ["src", "README.md", "LICENSE", "pyproject.toml"]
67
65
  fallback_version = "0.3.0.dev0"
68
66
 
69
67
  [tool.ruff]
70
- target-version = "py310"
68
+ target-version = "py312"
71
69
  line-length = 100
72
70
  src = ["src", "tests"]
73
71
 
74
72
  [tool.ruff.lint]
75
73
  select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
76
- ignore = ["E402", "E501", "E731", "B007", "B023", "B905", "SIM103", "SIM105", "SIM117", "RUF002", "RUF012", "UP022"]
74
+ # These upgrade rules describe a separate future modernization of the existing
75
+ # codebase; changing the minimum runtime must not silently rewrite public types.
76
+ ignore = ["E402", "E501", "E731", "B007", "B023", "B905", "SIM103", "SIM105", "SIM117", "RUF002", "RUF012", "UP017", "UP022", "UP040", "UP042", "UP045", "UP047"]
77
77
 
78
78
  [tool.ruff.lint.per-file-ignores]
79
79
  "tests/**/*.py" = ["S101"]
@@ -83,7 +83,7 @@ ignore = ["E402", "E501", "E731", "B007", "B023", "B905", "SIM103", "SIM105", "S
83
83
  exclude = ["src/ocr_toolkit/_version.py"]
84
84
 
85
85
  [tool.mypy]
86
- python_version = "3.10"
86
+ python_version = "3.12"
87
87
  strict = true
88
88
  files = ["src/ocr_toolkit"]
89
89
  exclude = ["src/ocr_toolkit/_version.py"]
@@ -124,12 +124,27 @@ underlines = ["", "", ""]
124
124
 
125
125
  [[tool.towncrier.type]]
126
126
  directory = "feature"
127
- name = "Features"
127
+ name = "🚀 Features"
128
128
  showcontent = true
129
129
 
130
130
  [[tool.towncrier.type]]
131
131
  directory = "bugfix"
132
- name = "Bug fixes"
132
+ name = "🐛 Bug Fixes"
133
+ showcontent = true
134
+
135
+ [[tool.towncrier.type]]
136
+ directory = "refactor"
137
+ name = "🔧 Refactoring"
138
+ showcontent = true
139
+
140
+ [[tool.towncrier.type]]
141
+ directory = "doc"
142
+ name = "📖 Documentation"
143
+ showcontent = true
144
+
145
+ [[tool.towncrier.type]]
146
+ directory = "rules"
147
+ name = "🧩 Rules"
133
148
  showcontent = true
134
149
 
135
150
  [[tool.towncrier.type]]
@@ -138,6 +153,11 @@ name = "Security"
138
153
  showcontent = true
139
154
 
140
155
  [[tool.towncrier.type]]
141
- directory = "doc"
142
- name = "Documentation"
156
+ directory = "deprecation"
157
+ name = "Deprecations"
158
+ showcontent = true
159
+
160
+ [[tool.towncrier.type]]
161
+ directory = "removal"
162
+ name = "Removals"
143
163
  showcontent = true
@@ -0,0 +1,10 @@
1
+ """Open Code Review CI helper package and centralized runtime metadata."""
2
+
3
+ try:
4
+ from ocr_toolkit._version import __version__
5
+ except ModuleNotFoundError:
6
+ # Hatch-vcs creates ``_version.py`` for editable/build installs. A raw source
7
+ # checkout must remain importable without introducing another release version.
8
+ __version__ = "0+unknown"
9
+
10
+ __all__ = ["__version__"]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.3.1'
22
- __version_tuple__ = version_tuple = (0, 3, 1)
21
+ __version__ = version = '0.4.1'
22
+ __version_tuple__ = version_tuple = (0, 4, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -8,7 +8,6 @@ from collections.abc import Sequence
8
8
  from pathlib import Path
9
9
 
10
10
  from ocr_toolkit import configure, mcp_config, preflight, review_runner
11
- from ocr_toolkit.context import render as context_render
12
11
  from ocr_toolkit.posting.workflow import main as posting_main
13
12
 
14
13
 
@@ -32,15 +31,6 @@ def build_parser() -> argparse.ArgumentParser:
32
31
  "ocr_args", nargs=argparse.REMAINDER, help="OCR review arguments after --."
33
32
  )
34
33
 
35
- context_parser = subparsers.add_parser(
36
- "context", help="Generate bounded repository review context."
37
- )
38
- context_parser.add_argument(
39
- "--output",
40
- default=".review-context/dependencies.md",
41
- help="Output markdown path.",
42
- )
43
-
44
34
  post_parser = subparsers.add_parser("post", help="Publish an OCR result artifact to GitLab.")
45
35
  post_parser.add_argument(
46
36
  # The public GitLab example writes this fixed path inside an isolated CI job.
@@ -70,12 +60,10 @@ def main(argv: Sequence[str] | None = None) -> int:
70
60
  if args.command == "review":
71
61
  ocr_args = args.ocr_args[1:] if args.ocr_args[:1] == ["--"] else args.ocr_args
72
62
  try:
73
- return review_runner.run_review(Path(args.result), Path(args.stderr), ocr_args)
63
+ return review_runner.run_evidence_review(Path(args.result), Path(args.stderr), ocr_args)
74
64
  except review_runner.ReviewRunnerError as exc:
75
65
  print(f"Cannot run Open Code Review: {exc}", file=sys.stderr)
76
66
  return 2
77
- if args.command == "context":
78
- return context_render.main(["--output", args.output])
79
67
  if args.command == "post":
80
68
  return posting_main([args.result, args.stderr])
81
69
  raise AssertionError(f"unhandled command: {args.command}")
@@ -0,0 +1,53 @@
1
+ """Shared trust-boundary controls for read-only Git plumbing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+ _PROCESS_OVERRIDE_KEYS = frozenset(
9
+ {
10
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
11
+ "GIT_COMMON_DIR",
12
+ "GIT_CONFIG_COUNT",
13
+ "GIT_CONFIG_PARAMETERS",
14
+ "GIT_DIR",
15
+ "GIT_INDEX_FILE",
16
+ "GIT_OBJECT_DIRECTORY",
17
+ "GIT_REPLACE_REF_BASE",
18
+ "GIT_WORK_TREE",
19
+ }
20
+ )
21
+
22
+
23
+ def isolated_git_environment() -> dict[str, str]:
24
+ """Return an environment isolated from caller-controlled Git state."""
25
+
26
+ environment = {
27
+ key: value
28
+ for key, value in os.environ.items()
29
+ if key not in _PROCESS_OVERRIDE_KEYS
30
+ and not key.startswith(("GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_"))
31
+ }
32
+ environment.update(
33
+ {
34
+ "GIT_CONFIG_GLOBAL": os.devnull,
35
+ "GIT_CONFIG_NOSYSTEM": "1",
36
+ "GIT_CONFIG_SYSTEM": os.devnull,
37
+ "GIT_NO_REPLACE_OBJECTS": "1",
38
+ "GIT_OPTIONAL_LOCKS": "0",
39
+ "GIT_PAGER": "cat",
40
+ "GIT_TERMINAL_PROMPT": "0",
41
+ "LC_ALL": "C",
42
+ }
43
+ )
44
+ return environment
45
+
46
+
47
+ def read_only_git_prefix(root: Path | None = None) -> list[str]:
48
+ """Return Git arguments that bind a root and disable repository hooks."""
49
+
50
+ prefix = ["git", "-c", "core.hooksPath=/dev/null"]
51
+ if root is not None:
52
+ prefix.extend(["-C", str(root)])
53
+ return prefix
@@ -10,6 +10,8 @@ from collections.abc import Mapping
10
10
  from pathlib import Path
11
11
  from typing import Any
12
12
 
13
+ MAX_OCR_CONFIG_BYTES = 1_000_000
14
+
13
15
 
14
16
  class OCRConfigError(Exception):
15
17
  """OCR configuration could not be read or written safely."""
@@ -25,16 +27,40 @@ def read_ocr_config(path: Path | None = None) -> dict[str, Any]:
25
27
  """Read OCR config JSON, returning an empty config when it is absent."""
26
28
 
27
29
  config_path = path or ocr_config_path()
28
- if not config_path.exists():
30
+ flags = os.O_RDONLY
31
+ if hasattr(os, "O_NOFOLLOW"):
32
+ flags |= os.O_NOFOLLOW
33
+ descriptor = -1
34
+ try:
35
+ descriptor = os.open(config_path, flags)
36
+ except FileNotFoundError:
29
37
  return {}
38
+ except OSError as exc:
39
+ raise OCRConfigError(f"cannot read OCR config: {exc}") from exc
30
40
  try:
31
- data = json.loads(config_path.read_text(encoding="utf-8"))
41
+ metadata = os.fstat(descriptor)
42
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1:
43
+ raise OCRConfigError("OCR config must be a private regular file with one link")
44
+ if metadata.st_size > MAX_OCR_CONFIG_BYTES:
45
+ raise OCRConfigError("OCR config exceeds the bounded byte limit")
46
+ chunks: list[bytes] = []
47
+ remaining = MAX_OCR_CONFIG_BYTES + 1
48
+ while remaining:
49
+ chunk = os.read(descriptor, min(64 * 1024, remaining))
50
+ if not chunk:
51
+ break
52
+ chunks.append(chunk)
53
+ remaining -= len(chunk)
54
+ raw = b"".join(chunks)
55
+ if len(raw) > MAX_OCR_CONFIG_BYTES:
56
+ raise OCRConfigError("OCR config exceeds the bounded byte limit")
57
+ data = json.loads(raw)
32
58
  except json.JSONDecodeError as exc:
33
59
  raise OCRConfigError(f"OCR config is not valid JSON: {exc}") from exc
34
60
  except UnicodeDecodeError as exc:
35
61
  raise OCRConfigError(f"OCR config is not valid UTF-8: {exc}") from exc
36
- except OSError as exc:
37
- raise OCRConfigError(f"cannot read OCR config: {exc}") from exc
62
+ finally:
63
+ os.close(descriptor)
38
64
  if not isinstance(data, dict):
39
65
  raise OCRConfigError("OCR config top-level value is not an object")
40
66
  return data
@@ -51,6 +77,8 @@ def write_ocr_config(config: Mapping[str, Any], path: Path | None = None) -> Non
51
77
  raise OCRConfigError(f"cannot prepare OCR config directory: {exc}") from exc
52
78
 
53
79
  payload = json.dumps(config, ensure_ascii=False, indent=4, sort_keys=True) + "\n"
80
+ if len(payload.encode("utf-8")) > MAX_OCR_CONFIG_BYTES:
81
+ raise OCRConfigError("OCR config exceeds the bounded byte limit")
54
82
  tmp_name = ""
55
83
  try:
56
84
  with tempfile.NamedTemporaryFile(
@@ -0,0 +1,35 @@
1
+ """Schema-versioned repository evidence contracts and projections."""
2
+
3
+ from ocr_toolkit.evidence.model import (
4
+ Confidence,
5
+ EvidenceDelta,
6
+ EvidenceRecord,
7
+ EvidenceSnapshot,
8
+ RefRole,
9
+ Sensitivity,
10
+ TrustClass,
11
+ )
12
+ from ocr_toolkit.evidence.repository import (
13
+ GitRepositoryReader,
14
+ RepositoryEvidenceError,
15
+ build_file_snapshot,
16
+ file_deltas,
17
+ )
18
+ from ocr_toolkit.evidence.store import EvidenceStore, EvidenceStoreError, EvidenceStoreLimits
19
+
20
+ __all__ = [
21
+ "Confidence",
22
+ "EvidenceDelta",
23
+ "EvidenceRecord",
24
+ "EvidenceSnapshot",
25
+ "EvidenceStore",
26
+ "EvidenceStoreError",
27
+ "EvidenceStoreLimits",
28
+ "GitRepositoryReader",
29
+ "RefRole",
30
+ "RepositoryEvidenceError",
31
+ "Sensitivity",
32
+ "TrustClass",
33
+ "build_file_snapshot",
34
+ "file_deltas",
35
+ ]
@@ -0,0 +1,14 @@
1
+ """Run the toolkit-owned repository evidence MCP server."""
2
+
3
+ from ocr_toolkit.evidence.artifacts import repository_artifacts
4
+ from ocr_toolkit.evidence.mcp import serve
5
+
6
+
7
+ def main() -> int:
8
+ """Serve the fixed private evidence store for the current repository."""
9
+
10
+ return serve(repository_artifacts().store)
11
+
12
+
13
+ if __name__ == "__main__":
14
+ raise SystemExit(main())
@@ -0,0 +1,193 @@
1
+ """Derive bounded Ansible topology facts from immutable repository blobs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+ from pathlib import PurePosixPath
8
+
9
+ MAX_TOPOLOGY_ITEMS = 256
10
+ PLAYBOOK_KEY_RE = re.compile(r"^(?P<indent>\s*)[\"']?(?P<key>hosts|import_playbook)[\"']?\s*:")
11
+ INI_GROUP_RE = re.compile(r"^\s*\[(?P<group>[^]#:]+)(?::children|:vars)?]\s*$")
12
+ YAML_GROUP_RE = re.compile(r"^(?P<indent>\s*)(?P<group>[A-Za-z0-9_.:-]+):(?:\s*\{\s*})?\s*$")
13
+ YAML_CHILDREN_RE = re.compile(r"^(?P<indent>\s*)children:\s*$")
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class AnsibleTopologyFact:
18
+ """Describe one normalized Ansible path or inventory-group fact."""
19
+
20
+ kind: str
21
+ identity: str
22
+ value: dict[str, str]
23
+
24
+
25
+ def topology_candidate(path: str) -> bool:
26
+ """Return whether a normalized path can contribute Ansible topology."""
27
+
28
+ parts = PurePosixPath(path).parts
29
+ if not parts:
30
+ return False
31
+ name = parts[-1].casefold()
32
+ return (
33
+ (len(parts) == 1 and name.endswith((".yml", ".yaml")))
34
+ or _role_surface(path) is not None
35
+ or _is_inventory_path(path)
36
+ )
37
+
38
+
39
+ def _role_surface(path: str) -> tuple[str, str] | None:
40
+ """Return role name and supported topology surface for a canonical role path."""
41
+
42
+ parts = PurePosixPath(path).parts
43
+ folded = tuple(part.casefold() for part in parts)
44
+ if (
45
+ len(parts) == 4
46
+ and folded[0] == "roles"
47
+ and folded[2] in {"meta", "defaults", "vars"}
48
+ and folded[3] in {"main.yml", "main.yaml"}
49
+ ):
50
+ surface = "metadata" if folded[2] == "meta" else folded[2]
51
+ return parts[1], surface
52
+ return None
53
+
54
+
55
+ def _is_inventory_path(path: str) -> bool:
56
+ """Recognize conventional inventory paths while excluding variable payloads."""
57
+
58
+ pure = PurePosixPath(path)
59
+ parts = pure.parts
60
+ if not parts:
61
+ return False
62
+ name = parts[-1].casefold()
63
+ folded = tuple(part.casefold() for part in parts)
64
+ if "group_vars" in folded or "host_vars" in folded:
65
+ return False
66
+ return name in {
67
+ "hosts",
68
+ "hosts.ini",
69
+ "hosts.yml",
70
+ "hosts.yaml",
71
+ "inventory",
72
+ "inventory.ini",
73
+ "inventory.yml",
74
+ "inventory.yaml",
75
+ } or (
76
+ ("inventory" in folded or "inventories" in folded)
77
+ and pure.suffix.casefold() in {"", ".ini", ".yml", ".yaml"}
78
+ )
79
+
80
+
81
+ def _is_root_playbook(path: str, text: str) -> bool:
82
+ """Recognize a root-level playbook from bounded YAML structure signals."""
83
+
84
+ pure = PurePosixPath(path)
85
+ if len(pure.parts) != 1 or pure.suffix.casefold() not in {".yml", ".yaml"}:
86
+ return False
87
+ top_level_list_item = False
88
+ play_child_indent: int | None = None
89
+ for raw_line in text.splitlines():
90
+ line = raw_line.rstrip()
91
+ stripped = line.strip()
92
+ if not stripped or stripped.startswith("#") or stripped in {"---", "..."}:
93
+ continue
94
+ indent = len(line) - len(line.lstrip(" "))
95
+ if indent == 0 and stripped.startswith("-"):
96
+ top_level_list_item = True
97
+ play_child_indent = None
98
+ if PLAYBOOK_KEY_RE.match(stripped[1:].lstrip()):
99
+ return True
100
+ continue
101
+ if indent == 0:
102
+ top_level_list_item = False
103
+ play_child_indent = None
104
+ continue
105
+ if not top_level_list_item:
106
+ continue
107
+ if play_child_indent is None:
108
+ play_child_indent = indent
109
+ if indent == play_child_indent and PLAYBOOK_KEY_RE.match(stripped):
110
+ return True
111
+ return False
112
+
113
+
114
+ def _inventory_groups(text: str) -> tuple[str, ...]:
115
+ """Extract bounded INI and YAML inventory group names without a YAML runtime."""
116
+
117
+ groups: set[str] = set()
118
+ children_indent: int | None = None
119
+ group_indent: int | None = None
120
+ top_level_group: str | None = None
121
+ for raw_line in text.splitlines():
122
+ ini = INI_GROUP_RE.match(raw_line)
123
+ if ini:
124
+ groups.add(ini.group("group").strip())
125
+ if len(groups) >= MAX_TOPOLOGY_ITEMS:
126
+ break
127
+ continue
128
+ children = YAML_CHILDREN_RE.match(raw_line)
129
+ if children:
130
+ children_indent = len(children.group("indent"))
131
+ group_indent = None
132
+ continue
133
+ yaml_group = YAML_GROUP_RE.match(raw_line)
134
+ if yaml_group and len(yaml_group.group("indent")) == 0:
135
+ group = yaml_group.group("group")
136
+ top_level_group = group if group not in {"all", "ungrouped"} else None
137
+ children_indent = None
138
+ group_indent = None
139
+ continue
140
+ if (
141
+ yaml_group
142
+ and top_level_group is not None
143
+ and yaml_group.group("group") in {"hosts", "children", "vars"}
144
+ ):
145
+ groups.add(top_level_group)
146
+ if yaml_group.group("group") == "children":
147
+ children_indent = len(yaml_group.group("indent"))
148
+ group_indent = None
149
+ continue
150
+ if yaml_group and children_indent is not None:
151
+ indent = len(yaml_group.group("indent"))
152
+ if indent <= children_indent:
153
+ children_indent = None
154
+ group_indent = None
155
+ else:
156
+ # YAML indentation width is repository-defined; the first key
157
+ # below children establishes the sibling group depth.
158
+ if group_indent is None:
159
+ group_indent = indent
160
+ if indent == group_indent:
161
+ groups.add(yaml_group.group("group"))
162
+ if len(groups) >= MAX_TOPOLOGY_ITEMS:
163
+ break
164
+ return tuple(sorted(groups))
165
+
166
+
167
+ def collect_topology(path: str, text: str) -> tuple[AnsibleTopologyFact, ...]:
168
+ """Collect typed Ansible topology facts for one immutable path and blob."""
169
+
170
+ facts = []
171
+ if _is_root_playbook(path, text):
172
+ facts.append(AnsibleTopologyFact("ansible.playbook", path, {"path": path, "scope": "root"}))
173
+ role_surface = _role_surface(path)
174
+ if role_surface is not None:
175
+ role, surface = role_surface
176
+ facts.append(
177
+ AnsibleTopologyFact(
178
+ f"ansible.role_{surface}",
179
+ path,
180
+ {"role": role, "path": path},
181
+ )
182
+ )
183
+ if _is_inventory_path(path):
184
+ facts.append(AnsibleTopologyFact("ansible.inventory", path, {"path": path}))
185
+ facts.extend(
186
+ AnsibleTopologyFact(
187
+ "ansible.inventory_group",
188
+ f"{path}:{group}",
189
+ {"path": path, "group": group},
190
+ )
191
+ for group in _inventory_groups(text)
192
+ )
193
+ return tuple(facts)