open-code-review-toolkit 0.4.7__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/PKG-INFO +3 -3
  2. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/README.md +2 -2
  3. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/_version.py +2 -2
  4. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/collect.py +14 -7
  5. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/collectors.py +255 -38
  6. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/__init__.py +1 -0
  7. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/ansible/__init__.py +1 -0
  8. open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/go_manifests.py → open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/go.py +1 -1
  9. open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/javascript_manifests.py → open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/javascript.py +1 -1
  10. open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/composer_manifests.py → open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/php.py +1 -1
  11. open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/python_manifests.py → open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/python.py +1 -1
  12. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/__init__.py +42 -0
  13. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/contracts.py +81 -0
  14. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/detection.py +635 -0
  15. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/providers/__init__.py +8 -0
  16. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/providers/frontend.py +35 -0
  17. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/providers/go.py +22 -0
  18. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/providers/php.py +25 -0
  19. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/providers/python.py +19 -0
  20. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/registry.py +165 -0
  21. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/schema.py +275 -0
  22. open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/frameworks/templates.py +144 -0
  23. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/infrastructure.py +1 -1
  24. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/mcp.py +56 -19
  25. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/model.py +51 -2
  26. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/project.py +8 -1
  27. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/store.py +68 -5
  28. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/preflight.py +1 -1
  29. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/.gitignore +0 -0
  30. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/LICENSE +0 -0
  31. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/pyproject.toml +0 -0
  32. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/__init__.py +0 -0
  33. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/cli.py +0 -0
  34. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/common/__init__.py +0 -0
  35. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/common/git.py +0 -0
  36. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/common/language.py +0 -0
  37. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/common/markdown.py +0 -0
  38. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/common/redaction.py +0 -0
  39. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/config_writer.py +0 -0
  40. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/configure.py +0 -0
  41. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/__init__.py +0 -0
  42. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/__main__.py +0 -0
  43. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/artifacts.py +0 -0
  44. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/categorize.py +0 -0
  45. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/coverage.py +0 -0
  46. /open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/ansible_requirements.py → /open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/ansible/requirements.py +0 -0
  47. /open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/ansible.py → /open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/ansible/topology.py +0 -0
  48. /open_code_review_toolkit-0.4.7/src/ocr_toolkit/evidence/manifest_model.py → /open_code_review_toolkit-0.5.0/src/ocr_toolkit/evidence/ecosystems/contracts.py +0 -0
  49. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/invocation.py +0 -0
  50. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/evidence/repository.py +0 -0
  51. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/mcp_config.py +0 -0
  52. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/ocr_result.py +0 -0
  53. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/__init__.py +0 -0
  54. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/__main__.py +0 -0
  55. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/approval.py +0 -0
  56. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/comments.py +0 -0
  57. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/formatting.py +0 -0
  58. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/gitlab.py +0 -0
  59. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/gitlab_approval.py +0 -0
  60. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/markers.py +0 -0
  61. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/payloads.py +0 -0
  62. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/result.py +0 -0
  63. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/settings.py +0 -0
  64. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/snapshot.py +0 -0
  65. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/suggestions.py +0 -0
  66. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/posting/workflow.py +0 -0
  67. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/providers/__init__.py +0 -0
  68. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/providers/gitlab.py +0 -0
  69. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/py.typed +0 -0
  70. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/result_contract.py +0 -0
  71. {open_code_review_toolkit-0.4.7 → open_code_review_toolkit-0.5.0}/src/ocr_toolkit/review_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: open-code-review-toolkit
3
- Version: 0.4.7
3
+ Version: 0.5.0
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
@@ -243,7 +243,7 @@ ocr --version
243
243
  ocr-ci --help
244
244
  ```
245
245
 
246
- The current compatibility target is OCR `1.9.1`. CI should pin the release and verify its published checksum before execution.
246
+ The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
247
247
  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
248
  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
249
 
@@ -266,7 +266,7 @@ Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/a
266
266
 
267
267
  ## Project architecture
268
268
 
269
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed evidence through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
269
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
270
270
 
271
271
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
272
272
  - [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
@@ -17,7 +17,7 @@ ocr --version
17
17
  ocr-ci --help
18
18
  ```
19
19
 
20
- The current compatibility target is OCR `1.9.1`. CI should pin the release and verify its published checksum before execution.
20
+ The exact recommended OCR release and its verified asset checksums live in the [versioned compatibility manifest](compatibility/ocr-support.json). CI should pin that release and checksum before execution.
21
21
  The [versioned compatibility policy](docs/compatibility.md) records tested assets and evidence and describes the conservative Dependabot-like qualification workflow for later upstream releases.
22
22
  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`.
23
23
 
@@ -40,7 +40,7 @@ Project-wide accepted tradeoffs can be recorded separately in `.opencodereview/a
40
40
 
41
41
  ## Project architecture
42
42
 
43
- The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed evidence through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
43
+ The shipped Repository Evidence Engine reads immutable base/head Git objects, stores bounded typed facts and deltas, creates the compact bootstrap used by OCR, and exposes detailed facts, scoped completeness, and base/head changes through the mandatory built-in read-only MCP server. Reviewed external stdio or native HTTPS MCP servers compose alongside it without replacing the built-in evidence boundary.
44
44
 
45
45
  - [Toolkit strategy](docs/engineering/toolkit_strategy.md) - durable product boundaries, architecture, invariants, and non-goals.
46
46
  - [Roadmap](ROADMAP.md) - milestone status, dependencies, outcomes, and completion signals.
@@ -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.4.7'
22
- __version_tuple__ = version_tuple = (0, 4, 7)
21
+ __version__ = version = '0.5.0'
22
+ __version_tuple__ = version_tuple = (0, 5, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -86,14 +86,9 @@ def collect_repository_evidence(
86
86
  coverage=tuple(head_coverage),
87
87
  )
88
88
  all_coverage = tuple((*base.coverage, *head.coverage))
89
- store = EvidenceStore(base=base, head=head, deltas=file_deltas(base, head))
89
+ snapshot_deltas = file_deltas(base, head)
90
+ store = EvidenceStore(base=base, head=head)
90
91
  typed_facts = [*base_facts, *head_facts]
91
- store.deltas = tuple(
92
- sorted(
93
- (*store.deltas, *fact_deltas(typed_facts), *coverage_deltas(all_coverage)),
94
- key=lambda item: (item.kind, item.component, item.identity),
95
- )
96
- )
97
92
  rejected_snapshot_records = [
98
93
  record for record in (*base.records, *head.records) if not store.add(record)
99
94
  ]
@@ -125,6 +120,18 @@ def collect_repository_evidence(
125
120
  )
126
121
  store.add_diagnostic("typed evidence was truncated by store limits")
127
122
  break
123
+ # Deltas are projections of canonical accepted store records, never raw facts
124
+ # or references to values that redaction, deduplication, or a budget omitted.
125
+ store.deltas = tuple(
126
+ sorted(
127
+ (
128
+ *snapshot_deltas,
129
+ *fact_deltas(store.records),
130
+ *coverage_deltas(all_coverage),
131
+ ),
132
+ key=lambda item: (item.kind, item.component, item.identity),
133
+ )
134
+ )
128
135
  categories = categorize_paths(list(changed))
129
136
  categories_truncated = False
130
137
  for category, paths in sorted(categories.items()):
@@ -8,7 +8,9 @@ from collections.abc import Callable, Iterable, Mapping
8
8
  from dataclasses import dataclass
9
9
  from pathlib import PurePosixPath
10
10
 
11
- from ocr_toolkit.evidence.ansible import (
11
+ from ocr_toolkit.evidence.coverage import CoverageObservation, compose_coverage
12
+ from ocr_toolkit.evidence.ecosystems.ansible.requirements import parse_galaxy_requirements
13
+ from ocr_toolkit.evidence.ecosystems.ansible.topology import (
12
14
  collect_topology,
13
15
  inventory_scope,
14
16
  role_coverage_scope,
@@ -16,22 +18,36 @@ from ocr_toolkit.evidence.ansible import (
16
18
  topology_candidate,
17
19
  topology_coverage,
18
20
  )
19
- from ocr_toolkit.evidence.ansible_requirements import parse_galaxy_requirements
20
- from ocr_toolkit.evidence.composer_manifests import parse_composer_json, parse_composer_lock
21
- from ocr_toolkit.evidence.coverage import CoverageObservation, compose_coverage
22
- from ocr_toolkit.evidence.go_manifests import parse_go_mod, parse_go_sum
23
- from ocr_toolkit.evidence.infrastructure import infrastructure_candidate, parse_infrastructure_pins
24
- from ocr_toolkit.evidence.javascript_manifests import (
21
+ from ocr_toolkit.evidence.ecosystems.contracts import (
22
+ MAX_MANIFEST_ITEMS,
23
+ ManifestFact,
24
+ ManifestParseResult,
25
+ )
26
+ from ocr_toolkit.evidence.ecosystems.go import parse_go_mod, parse_go_sum
27
+ from ocr_toolkit.evidence.ecosystems.javascript import (
25
28
  parse_package_json,
26
29
  parse_package_lock,
27
30
  parse_pnpm_lock,
28
31
  parse_yarn_lock,
29
32
  )
30
- from ocr_toolkit.evidence.manifest_model import (
31
- MAX_MANIFEST_ITEMS,
32
- ManifestFact,
33
- ManifestParseResult,
33
+ from ocr_toolkit.evidence.ecosystems.php import parse_composer_json, parse_composer_lock
34
+ from ocr_toolkit.evidence.ecosystems.python import (
35
+ parse_pipfile_lock,
36
+ parse_poetry_lock,
37
+ parse_pylock,
38
+ parse_pyproject,
39
+ parse_requirements,
40
+ parse_uv_lock,
41
+ )
42
+ from ocr_toolkit.evidence.frameworks import (
43
+ FrameworkPluginContext,
44
+ PluginCoverage,
45
+ PluginFact,
46
+ PluginSourceStatus,
47
+ collect_framework_plugins,
48
+ collect_template_files,
34
49
  )
50
+ from ocr_toolkit.evidence.infrastructure import infrastructure_candidate, parse_infrastructure_pins
35
51
  from ocr_toolkit.evidence.model import (
36
52
  Confidence,
37
53
  CoverageRecord,
@@ -42,14 +58,6 @@ from ocr_toolkit.evidence.model import (
42
58
  RefRole,
43
59
  TrustClass,
44
60
  )
45
- from ocr_toolkit.evidence.python_manifests import (
46
- parse_pipfile_lock,
47
- parse_poetry_lock,
48
- parse_pylock,
49
- parse_pyproject,
50
- parse_requirements,
51
- parse_uv_lock,
52
- )
53
61
  from ocr_toolkit.evidence.repository import (
54
62
  GitRepositoryReader,
55
63
  RepositoryEvidenceError,
@@ -82,29 +90,32 @@ CONTEXT_YAML_DIRECTORIES = (
82
90
 
83
91
  @dataclass(frozen=True, slots=True)
84
92
  class ManifestCollector:
85
- """Bind manifest path matching, ecosystem metadata, and a bounded parser."""
93
+ """Bind manifest path matching, ecosystem metadata, role, and bounded parser."""
86
94
 
87
95
  ecosystem: str
96
+ source_roles: tuple[str, ...]
88
97
  matches: Callable[[str], bool]
89
98
  parse: Callable[[str], ManifestParseResult]
90
99
 
91
100
 
92
101
  @dataclass(frozen=True, slots=True)
93
102
  class ManifestBlobSet:
94
- """Return immutable Galaxy blobs and explicit graph-read diagnostics."""
103
+ """Return immutable Galaxy blobs, diagnostics, and affected graph roots."""
95
104
 
96
105
  blobs: dict[str, bytes]
97
106
  galaxy_paths: tuple[str, ...]
98
107
  diagnostics: tuple[str, ...]
108
+ degraded_roots: tuple[tuple[str, str], ...] = ()
99
109
 
100
110
 
101
111
  @dataclass(frozen=True, slots=True)
102
112
  class PythonRequirementBlobSet:
103
- """Return immutable requirements blobs and graph-read diagnostics."""
113
+ """Return immutable requirements blobs, diagnostics, and affected roots."""
104
114
 
105
115
  blobs: dict[str, bytes]
106
116
  requirement_paths: tuple[str, ...]
107
117
  diagnostics: tuple[str, ...]
118
+ degraded_roots: tuple[tuple[str, str], ...] = ()
108
119
 
109
120
 
110
121
  def _parse_ansible_requirements(text: str) -> ManifestParseResult:
@@ -155,26 +166,28 @@ def _is_pylock(path: str) -> bool:
155
166
 
156
167
 
157
168
  MANIFEST_COLLECTORS = (
158
- ManifestCollector("python", _name_is("pyproject.toml"), parse_pyproject),
159
- ManifestCollector("python", _is_python_requirements, parse_requirements),
160
- ManifestCollector("python", _name_is("uv.lock"), parse_uv_lock),
161
- ManifestCollector("python", _name_is("poetry.lock"), parse_poetry_lock),
162
- ManifestCollector("python", _name_is("Pipfile.lock"), parse_pipfile_lock),
163
- ManifestCollector("python", _is_pylock, parse_pylock),
164
- ManifestCollector("javascript", _name_is("package.json"), parse_package_json),
169
+ ManifestCollector("python", ("declaration",), _name_is("pyproject.toml"), parse_pyproject),
170
+ ManifestCollector("python", ("declaration",), _is_python_requirements, parse_requirements),
171
+ ManifestCollector("python", ("resolution",), _name_is("uv.lock"), parse_uv_lock),
172
+ ManifestCollector("python", ("resolution",), _name_is("poetry.lock"), parse_poetry_lock),
173
+ ManifestCollector("python", ("resolution",), _name_is("Pipfile.lock"), parse_pipfile_lock),
174
+ ManifestCollector("python", ("resolution",), _is_pylock, parse_pylock),
175
+ ManifestCollector("javascript", ("declaration",), _name_is("package.json"), parse_package_json),
165
176
  ManifestCollector(
166
177
  "javascript",
178
+ ("resolution",),
167
179
  _name_is("package-lock.json"),
168
180
  parse_package_lock,
169
181
  ),
170
- ManifestCollector("javascript", _name_is("yarn.lock"), parse_yarn_lock),
171
- ManifestCollector("javascript", _name_is("pnpm-lock.yaml"), parse_pnpm_lock),
172
- ManifestCollector("go", _name_is("go.mod"), parse_go_mod),
173
- ManifestCollector("go", _name_is("go.sum"), parse_go_sum),
174
- ManifestCollector("php", _name_is("composer.json"), parse_composer_json),
175
- ManifestCollector("php", _name_is("composer.lock"), parse_composer_lock),
182
+ ManifestCollector("javascript", ("resolution",), _name_is("yarn.lock"), parse_yarn_lock),
183
+ ManifestCollector("javascript", ("resolution",), _name_is("pnpm-lock.yaml"), parse_pnpm_lock),
184
+ ManifestCollector("go", ("declaration", "resolution"), _name_is("go.mod"), parse_go_mod),
185
+ ManifestCollector("go", ("checksum",), _name_is("go.sum"), parse_go_sum),
186
+ ManifestCollector("php", ("declaration",), _name_is("composer.json"), parse_composer_json),
187
+ ManifestCollector("php", ("resolution",), _name_is("composer.lock"), parse_composer_lock),
176
188
  ManifestCollector(
177
189
  "ansible",
190
+ ("declaration",),
178
191
  _name_is("requirements.yml", "requirements.yaml"),
179
192
  _parse_ansible_requirements,
180
193
  ),
@@ -366,6 +379,42 @@ def _bound_include_diagnostics(
366
379
  )
367
380
 
368
381
 
382
+ def _roots_reaching_graph_degradation(
383
+ roots: tuple[str, ...],
384
+ edges: Mapping[str, tuple[str, ...]],
385
+ degraded_paths: Mapping[str, str],
386
+ ) -> tuple[tuple[str, str], ...]:
387
+ """Return roots whose accepted graph reaches a bounded degraded source."""
388
+
389
+ supported_reasons = {"bounded-source-omission", "include-graph-truncation"}
390
+ if any(reason not in supported_reasons for reason in degraded_paths.values()):
391
+ raise ValueError("include graph has an unsupported degradation reason")
392
+ affected: list[tuple[str, str]] = []
393
+ for root in sorted(set(roots)):
394
+ pending = [root]
395
+ visited: set[str] = set()
396
+ reasons: set[str] = set()
397
+ while pending:
398
+ path = pending.pop()
399
+ if path in visited:
400
+ continue
401
+ visited.add(path)
402
+ reason = degraded_paths.get(path)
403
+ if reason is not None:
404
+ reasons.add(reason)
405
+ pending.extend(reversed(edges.get(path, ())))
406
+ if reasons:
407
+ # A bounded omission is stronger than a traversal/item limit because
408
+ # the source itself was never parsed.
409
+ reason = (
410
+ "bounded-source-omission"
411
+ if "bounded-source-omission" in reasons
412
+ else "include-graph-truncation"
413
+ )
414
+ affected.append((root, reason))
415
+ return tuple(affected)
416
+
417
+
369
418
  def _read_manifest_graph(
370
419
  reader: GitRepositoryReader,
371
420
  entries_by_path: Mapping[str, RepositoryObject],
@@ -380,6 +429,7 @@ def _read_manifest_graph(
380
429
  admitted = set(initial_paths)
381
430
  root_paths = set(initial_paths)
382
431
  edges: dict[str, list[str]] = {}
432
+ degraded_paths: dict[str, str] = {}
383
433
  pending = [(path, "") for path in initial_paths]
384
434
  included_files = 0
385
435
  file_limit_reported = False
@@ -415,6 +465,7 @@ def _read_manifest_graph(
415
465
  f"{MAX_MANIFEST_INCLUDE_FILES} files"
416
466
  )
417
467
  file_limit_reported = True
468
+ degraded_paths[path] = "include-graph-truncation"
418
469
  continue
419
470
  admitted.add(path)
420
471
  included_files += 1
@@ -425,6 +476,8 @@ def _read_manifest_graph(
425
476
  read = reader.read_candidate_blobs(tuple(to_read))
426
477
  blobs.update(read.blobs)
427
478
  diagnostics.extend(read.diagnostics)
479
+ for path in (entry.path for entry in to_read if entry.path not in read.blobs):
480
+ degraded_paths[path] = "bounded-source-omission"
428
481
  for path in process_paths:
429
482
  visited.add(path)
430
483
  blob = blobs.get(path)
@@ -432,6 +485,8 @@ def _read_manifest_graph(
432
485
  continue
433
486
  try:
434
487
  parsed = parse_galaxy_requirements(blob.decode("utf-8"))
488
+ if any("truncated" in notice for notice in parsed.notices):
489
+ degraded_paths[path] = "include-graph-truncation"
435
490
  except UnicodeDecodeError:
436
491
  diagnostics.append(f"{path}: Ansible Galaxy include is not UTF-8")
437
492
  continue
@@ -447,6 +502,7 @@ def _read_manifest_graph(
447
502
  f"{MAX_MANIFEST_INCLUDE_EDGES} edges"
448
503
  )
449
504
  edge_limit_reported = True
505
+ degraded_paths[path] = "include-graph-truncation"
450
506
  continue
451
507
  included_edges += 1
452
508
  edges.setdefault(path, []).append(resolved)
@@ -454,12 +510,18 @@ def _read_manifest_graph(
454
510
  diagnostics.append(
455
511
  f"{path}: Ansible Galaxy include depth exceeded at {resolved}"
456
512
  )
513
+ degraded_paths[path] = "include-graph-truncation"
457
514
  else:
458
515
  pending.append((resolved, path))
459
516
  normalized_edges = {path: tuple(dict.fromkeys(targets)) for path, targets in edges.items()}
460
517
  diagnostics.extend(_include_cycle_diagnostics(normalized_edges))
461
518
  galaxy_paths = tuple(sorted(path for path in visited if path in blobs))
462
- return ManifestBlobSet(blobs, galaxy_paths, _bound_include_diagnostics(diagnostics))
519
+ return ManifestBlobSet(
520
+ blobs,
521
+ galaxy_paths,
522
+ _bound_include_diagnostics(diagnostics),
523
+ _roots_reaching_graph_degradation(initial_paths, normalized_edges, degraded_paths),
524
+ )
463
525
 
464
526
 
465
527
  def _read_python_requirement_graph(
@@ -474,6 +536,8 @@ def _read_python_requirement_graph(
474
536
  diagnostics: list[str] = []
475
537
  visited: set[str] = set()
476
538
  admitted = set(initial_paths)
539
+ edges: dict[str, list[str]] = {}
540
+ degraded_paths: dict[str, str] = {}
477
541
  pending = [(path, "") for path in initial_paths]
478
542
  included_files = 0
479
543
  included_edges = 0
@@ -506,6 +570,7 @@ def _read_python_requirement_graph(
506
570
  f"{MAX_MANIFEST_INCLUDE_FILES} files"
507
571
  )
508
572
  file_limit_reported = True
573
+ degraded_paths[path] = "include-graph-truncation"
509
574
  continue
510
575
  admitted.add(path)
511
576
  included_files += 1
@@ -515,12 +580,16 @@ def _read_python_requirement_graph(
515
580
  read = reader.read_candidate_blobs(tuple(sorted(to_read, key=lambda item: item.path)))
516
581
  blobs.update(read.blobs)
517
582
  diagnostics.extend(read.diagnostics)
583
+ for path in (entry.path for entry in to_read if entry.path not in read.blobs):
584
+ degraded_paths[path] = "bounded-source-omission"
518
585
  for path in process_paths:
519
586
  visited.add(path)
520
587
  if path not in blobs:
521
588
  continue
522
589
  try:
523
590
  parsed = parse_requirements(blobs[path].decode("utf-8"))
591
+ if any("truncated" in notice for notice in parsed.notices):
592
+ degraded_paths[path] = "include-graph-truncation"
524
593
  except UnicodeDecodeError:
525
594
  diagnostics.append(f"{path}: Python requirements include is not UTF-8")
526
595
  continue
@@ -538,15 +607,19 @@ def _read_python_requirement_graph(
538
607
  f"{MAX_MANIFEST_INCLUDE_EDGES} edges"
539
608
  )
540
609
  edge_limit_reported = True
610
+ degraded_paths[path] = "include-graph-truncation"
541
611
  continue
542
612
  included_edges += 1
613
+ edges.setdefault(path, []).append(resolved)
543
614
  if depth == MAX_MANIFEST_INCLUDE_DEPTH:
544
615
  diagnostics.append(
545
616
  f"{path}: Python requirements include depth exceeded at {resolved}"
546
617
  )
618
+ degraded_paths[path] = "include-graph-truncation"
547
619
  else:
548
620
  pending.append((resolved, path))
549
621
  requirement_paths = tuple(sorted(path for path in visited if path in blobs))
622
+ normalized_edges = {path: tuple(dict.fromkeys(targets)) for path, targets in edges.items()}
550
623
  return PythonRequirementBlobSet(
551
624
  blobs,
552
625
  requirement_paths,
@@ -554,9 +627,56 @@ def _read_python_requirement_graph(
554
627
  diagnostics,
555
628
  truncation_notice="Python requirements include diagnostics were truncated",
556
629
  ),
630
+ _roots_reaching_graph_degradation(initial_paths, normalized_edges, degraded_paths),
557
631
  )
558
632
 
559
633
 
634
+ def _plugin_records(
635
+ facts: tuple[PluginFact, ...],
636
+ *,
637
+ ref: RefRole,
638
+ commit_sha: str,
639
+ trust: TrustClass,
640
+ ) -> list[EvidenceRecord]:
641
+ """Attach immutable ref provenance to validated static plugin facts."""
642
+
643
+ return [
644
+ EvidenceRecord(
645
+ kind=fact.kind,
646
+ value={"identity": fact.identity, "fact": fact.value},
647
+ source_path=fact.source_path,
648
+ ref=ref,
649
+ commit_sha=commit_sha,
650
+ component=fact.component,
651
+ provenance=f"framework plugin:{fact.value['plugin']}",
652
+ confidence=Confidence.EXACT,
653
+ trust=trust,
654
+ )
655
+ for fact in facts
656
+ ]
657
+
658
+
659
+ def _plugin_coverage(
660
+ observations: tuple[PluginCoverage, ...], *, ref: RefRole, commit_sha: str
661
+ ) -> list[CoverageRecord]:
662
+ """Compose plugin coverage by semantic component/domain/scope identity."""
663
+
664
+ grouped: dict[tuple[str, str, str], list[CoverageObservation]] = {}
665
+ for item in observations:
666
+ grouped.setdefault((item.component, item.domain, item.scope), []).append(item.observation)
667
+ return [
668
+ compose_coverage(
669
+ component=component,
670
+ domain=domain,
671
+ scope=scope,
672
+ observations=tuple(values),
673
+ ref=ref,
674
+ commit_sha=commit_sha,
675
+ )
676
+ for (component, domain, scope), values in sorted(grouped.items())
677
+ ]
678
+
679
+
560
680
  def collect_ref_facts(
561
681
  reader: GitRepositoryReader,
562
682
  commit_sha: str,
@@ -615,6 +735,16 @@ def collect_ref_facts(
615
735
  and not entry.is_submodule
616
736
  and entry.object_type == "blob"
617
737
  )
738
+ source_statuses: dict[str, PluginSourceStatus] = {
739
+ entry.path: PluginSourceStatus(
740
+ entry.path,
741
+ collector.ecosystem,
742
+ collector.source_roles,
743
+ "pending",
744
+ )
745
+ for entry in candidates
746
+ if (collector := manifest_collector(entry.path)) is not None
747
+ }
618
748
  try:
619
749
  read = reader.read_candidate_blobs(candidates)
620
750
  except RepositoryEvidenceError as exc:
@@ -635,6 +765,13 @@ def collect_ref_facts(
635
765
  )
636
766
  return records, diagnostics
637
767
  blobs = read.blobs
768
+ for path, status in tuple(source_statuses.items()):
769
+ source_statuses[path] = PluginSourceStatus(
770
+ status.path,
771
+ status.ecosystem,
772
+ status.roles,
773
+ "accepted" if path in blobs else "omitted",
774
+ )
638
775
  diagnostics.extend(f"{ref.value}:{message}" for message in read.diagnostics)
639
776
  galaxy_roots = tuple(
640
777
  entry.path
@@ -656,9 +793,39 @@ def collect_ref_facts(
656
793
  except RepositoryEvidenceError as exc:
657
794
  diagnostics.append(f"Python requirements include batch read failed: {exc}")
658
795
  python_graph = PythonRequirementBlobSet(graph.blobs, python_roots, ())
796
+ # Included requirements may use arbitrary .txt/.in names that the initial
797
+ # manifest registry intentionally does not match. They still feed framework
798
+ # declarations, so register their exact source state before graph/parser
799
+ # degradation is projected into completeness.
800
+ python_declaration = manifest_collector("requirements.txt")
801
+ if python_declaration is None: # pragma: no cover - static registry invariant
802
+ raise ValueError("Python requirements collector is unavailable")
803
+ for path in python_graph.requirement_paths:
804
+ source_statuses.setdefault(
805
+ path,
806
+ PluginSourceStatus(
807
+ path,
808
+ python_declaration.ecosystem,
809
+ python_declaration.source_roles,
810
+ "accepted",
811
+ ),
812
+ )
813
+ degraded_roots = dict((*graph.degraded_roots, *python_graph.degraded_roots))
814
+ for path, reason in sorted(degraded_roots.items()):
815
+ status = source_statuses.get(path)
816
+ if status is not None and status.state not in {"omitted", "unavailable"}:
817
+ source_statuses[path] = PluginSourceStatus(
818
+ status.path,
819
+ status.ecosystem,
820
+ status.roles,
821
+ "partial",
822
+ reason,
823
+ )
659
824
  blobs = python_graph.blobs
660
- diagnostics.extend(f"{ref.value}:{message}" for message in graph.diagnostics)
661
- diagnostics.extend(f"{ref.value}:{message}" for message in python_graph.diagnostics)
825
+ for message in (*graph.diagnostics, *python_graph.diagnostics):
826
+ qualified = f"{ref.value}:{message}"
827
+ if qualified not in diagnostics:
828
+ diagnostics.append(qualified)
662
829
  paths = dict.fromkeys(
663
830
  (
664
831
  *tuple(entry.path for entry in candidates),
@@ -724,6 +891,23 @@ def collect_ref_facts(
724
891
  raise ValueError("supported manifest has no collector")
725
892
  parsed = collector.parse(text)
726
893
  diagnostics.extend(f"{ref.value}:{path}: {notice}" for notice in parsed.notices)
894
+ source_status = source_statuses.get(path)
895
+ if any("truncated" in notice for notice in parsed.notices):
896
+ if source_status is not None and source_status.state != "partial":
897
+ source_statuses[path] = PluginSourceStatus(
898
+ source_status.path,
899
+ source_status.ecosystem,
900
+ source_status.roles,
901
+ "partial",
902
+ "source-item-limit",
903
+ )
904
+ elif source_status is not None and source_status.state != "partial":
905
+ source_statuses[path] = PluginSourceStatus(
906
+ source_status.path,
907
+ source_status.ecosystem,
908
+ source_status.roles,
909
+ "complete",
910
+ )
727
911
  facts = [
728
912
  ManifestFact(
729
913
  "repository.manifest",
@@ -781,6 +965,14 @@ def collect_ref_facts(
781
965
  )
782
966
  if topology_source and entry is not None:
783
967
  unavailable_topology(entry, "parse-unavailable")
968
+ source_status = source_statuses.get(path)
969
+ if source_status is not None:
970
+ source_statuses[path] = PluginSourceStatus(
971
+ source_status.path,
972
+ source_status.ecosystem,
973
+ source_status.roles,
974
+ "unavailable",
975
+ )
784
976
  continue
785
977
  for fact in facts:
786
978
  if fact.kind.startswith("ansible.") and fact.kind != "dependency.declared":
@@ -820,6 +1012,24 @@ def collect_ref_facts(
820
1012
  trust=trust,
821
1013
  )
822
1014
  )
1015
+ plugin_context = FrameworkPluginContext(
1016
+ records=tuple(records),
1017
+ entries=entries,
1018
+ source_statuses=tuple(sorted(source_statuses.values(), key=lambda item: item.path)),
1019
+ ref=ref,
1020
+ commit_sha=commit_sha,
1021
+ )
1022
+ plugin_facts, plugin_observations, plugin_notices = collect_framework_plugins(plugin_context)
1023
+ template_facts, template_observations, template_notices = collect_template_files(plugin_context)
1024
+ records.extend(
1025
+ _plugin_records(
1026
+ (*plugin_facts, *template_facts),
1027
+ ref=ref,
1028
+ commit_sha=commit_sha,
1029
+ trust=trust,
1030
+ )
1031
+ )
1032
+ diagnostics.extend(f"{ref.value}:{notice}" for notice in (*plugin_notices, *template_notices))
823
1033
  if coverage_sink is not None:
824
1034
  for (domain, scope), observations in sorted(coverage_observations.items()):
825
1035
  coverage_sink.append(
@@ -832,6 +1042,13 @@ def collect_ref_facts(
832
1042
  commit_sha=commit_sha,
833
1043
  )
834
1044
  )
1045
+ coverage_sink.extend(
1046
+ _plugin_coverage(
1047
+ (*plugin_observations, *template_observations),
1048
+ ref=ref,
1049
+ commit_sha=commit_sha,
1050
+ )
1051
+ )
835
1052
  return records, diagnostics
836
1053
 
837
1054
 
@@ -0,0 +1 @@
1
+ """Bounded source adapters that normalize supported repository ecosystems."""
@@ -0,0 +1 @@
1
+ """Bounded Ansible Galaxy and topology source adapters."""
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from ocr_toolkit.evidence.manifest_model import (
5
+ from ocr_toolkit.evidence.ecosystems.contracts import (
6
6
  MAX_MANIFEST_ITEMS,
7
7
  ManifestFact,
8
8
  ManifestParseResult,
@@ -7,7 +7,7 @@ from collections.abc import Mapping
7
7
  from urllib.parse import urlsplit
8
8
 
9
9
  from ocr_toolkit.common.redaction import redact_url_userinfo
10
- from ocr_toolkit.evidence.manifest_model import (
10
+ from ocr_toolkit.evidence.ecosystems.contracts import (
11
11
  MAX_MANIFEST_ITEMS,
12
12
  ManifestFact,
13
13
  ManifestParseResult,
@@ -7,7 +7,7 @@ from collections.abc import Mapping
7
7
  from urllib.parse import urlsplit
8
8
 
9
9
  from ocr_toolkit.common.redaction import redact_url_userinfo
10
- from ocr_toolkit.evidence.manifest_model import (
10
+ from ocr_toolkit.evidence.ecosystems.contracts import (
11
11
  MAX_MANIFEST_ITEMS,
12
12
  ManifestFact,
13
13
  ManifestParseResult,
@@ -9,7 +9,7 @@ import shlex
9
9
  import tomllib # type: ignore[import-untyped]
10
10
 
11
11
  from ocr_toolkit.common.redaction import redact_url_userinfo
12
- from ocr_toolkit.evidence.manifest_model import (
12
+ from ocr_toolkit.evidence.ecosystems.contracts import (
13
13
  MAX_MANIFEST_ITEMS,
14
14
  ManifestFact,
15
15
  ManifestParseResult,