arcgraph 0.1.0__py3-none-any.whl

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 (165) hide show
  1. arcgraph/__init__.py +41 -0
  2. arcgraph/_build_provenance.json +10 -0
  3. arcgraph/adapters/__init__.py +6 -0
  4. arcgraph/adapters/arq_adapter.py +369 -0
  5. arcgraph/adapters/celery_adapter.py +440 -0
  6. arcgraph/adapters/common.py +436 -0
  7. arcgraph/adapters/django_adapter.py +765 -0
  8. arcgraph/adapters/fastapi_adapter.py +415 -0
  9. arcgraph/adapters/logging_adapter.py +335 -0
  10. arcgraph/adapters/mcp_adapter.py +100 -0
  11. arcgraph/adapters/pydantic_adapter.py +290 -0
  12. arcgraph/adapters/pytest_adapter.py +221 -0
  13. arcgraph/adapters/pytest_collection.py +96 -0
  14. arcgraph/adapters/pytest_fixtures.py +371 -0
  15. arcgraph/adapters/pytest_types.py +251 -0
  16. arcgraph/adapters/registry.py +34 -0
  17. arcgraph/adapters/service_container_adapter.py +548 -0
  18. arcgraph/adapters/sqlalchemy_adapter.py +446 -0
  19. arcgraph/adapters/typer_click_adapter.py +292 -0
  20. arcgraph/analyzers/__init__.py +1 -0
  21. arcgraph/analyzers/bindings.py +782 -0
  22. arcgraph/analyzers/calls/__init__.py +6 -0
  23. arcgraph/analyzers/calls/analyzer.py +2921 -0
  24. arcgraph/analyzers/calls/constants.py +702 -0
  25. arcgraph/analyzers/calls/context.py +298 -0
  26. arcgraph/analyzers/calls/lexical.py +237 -0
  27. arcgraph/analyzers/coverage.py +826 -0
  28. arcgraph/analyzers/exports.py +46 -0
  29. arcgraph/analyzers/har_trace.py +323 -0
  30. arcgraph/analyzers/imports.py +190 -0
  31. arcgraph/analyzers/otel_trace.py +352 -0
  32. arcgraph/analyzers/precise_references.py +1199 -0
  33. arcgraph/analyzers/python_scopes.py +56 -0
  34. arcgraph/analyzers/runtime_trace.py +516 -0
  35. arcgraph/analyzers/similarity.py +430 -0
  36. arcgraph/analyzers/symbols.py +363 -0
  37. arcgraph/analyzers/type_unions.py +89 -0
  38. arcgraph/analyzers/types.py +2293 -0
  39. arcgraph/assets/__init__.py +1 -0
  40. arcgraph/assets/workbench/__init__.py +1 -0
  41. arcgraph/assets/workbench/index.html +2398 -0
  42. arcgraph/assets/workbench/vendor/LICENSE.d3.txt +15 -0
  43. arcgraph/assets/workbench/vendor/d3.v7.min.js +2 -0
  44. arcgraph/change/__init__.py +10 -0
  45. arcgraph/change/baseline.py +145 -0
  46. arcgraph/change/changed_regions.py +728 -0
  47. arcgraph/change/contracts.py +927 -0
  48. arcgraph/change/errors.py +88 -0
  49. arcgraph/change/evidence.py +289 -0
  50. arcgraph/change/graph_delta.py +1422 -0
  51. arcgraph/change/identities.py +1277 -0
  52. arcgraph/change/paths.py +234 -0
  53. arcgraph/change/pins.py +426 -0
  54. arcgraph/change/planner.py +504 -0
  55. arcgraph/change/protected_surfaces.py +328 -0
  56. arcgraph/change/provider.py +46 -0
  57. arcgraph/change/service.py +1030 -0
  58. arcgraph/change/source_provider.py +311 -0
  59. arcgraph/change/store.py +1138 -0
  60. arcgraph/change/trusted_runners.py +154 -0
  61. arcgraph/change/verdicts.py +141 -0
  62. arcgraph/change/verification_plan.py +113 -0
  63. arcgraph/change/verifier.py +579 -0
  64. arcgraph/core/__init__.py +1 -0
  65. arcgraph/core/assurance.py +343 -0
  66. arcgraph/core/binding_metrics.py +75 -0
  67. arcgraph/core/cleanup.py +701 -0
  68. arcgraph/core/confidence_profiles.py +108 -0
  69. arcgraph/core/evidence_manifest.py +862 -0
  70. arcgraph/core/expression_kinds.py +11 -0
  71. arcgraph/core/force_graph_export.py +1125 -0
  72. arcgraph/core/freshness.py +94 -0
  73. arcgraph/core/graph_store.py +1099 -0
  74. arcgraph/core/ids.py +206 -0
  75. arcgraph/core/language_tiers.py +150 -0
  76. arcgraph/core/merge.py +649 -0
  77. arcgraph/core/metadata.py +30 -0
  78. arcgraph/core/operation_lock.py +218 -0
  79. arcgraph/core/passes.py +142 -0
  80. arcgraph/core/payload_policy.py +708 -0
  81. arcgraph/core/precise_references.py +124 -0
  82. arcgraph/core/query_engine.py +3078 -0
  83. arcgraph/core/recovery.py +18 -0
  84. arcgraph/core/scanner.py +1454 -0
  85. arcgraph/core/schemas.py +700 -0
  86. arcgraph/core/semantic.py +263 -0
  87. arcgraph/core/semantic_metrics.py +619 -0
  88. arcgraph/core/sharing_retry.py +84 -0
  89. arcgraph/core/structural.py +260 -0
  90. arcgraph/core/target_resolver.py +486 -0
  91. arcgraph/core/type_metrics.py +67 -0
  92. arcgraph/core/unresolved_classification.py +471 -0
  93. arcgraph/core/utils.py +100 -0
  94. arcgraph/core/visual_contract.py +240 -0
  95. arcgraph/core/visual_slice_engine.py +933 -0
  96. arcgraph/interfaces/__init__.py +1 -0
  97. arcgraph/interfaces/agent_capabilities.py +493 -0
  98. arcgraph/interfaces/agent_help.py +313 -0
  99. arcgraph/interfaces/benchmark.py +548 -0
  100. arcgraph/interfaces/ci.py +1992 -0
  101. arcgraph/interfaces/cli.py +2799 -0
  102. arcgraph/interfaces/cli_change.py +690 -0
  103. arcgraph/interfaces/cli_feedback.py +176 -0
  104. arcgraph/interfaces/cli_ops.py +69 -0
  105. arcgraph/interfaces/cli_support.py +113 -0
  106. arcgraph/interfaces/cli_trial.py +42 -0
  107. arcgraph/interfaces/cli_visual.py +601 -0
  108. arcgraph/interfaces/cli_workspace.py +300 -0
  109. arcgraph/interfaces/client_setup.py +481 -0
  110. arcgraph/interfaces/docs.py +1255 -0
  111. arcgraph/interfaces/human_output.py +409 -0
  112. arcgraph/interfaces/local_state.py +577 -0
  113. arcgraph/interfaces/mcp_server.py +457 -0
  114. arcgraph/interfaces/mcp_tools.py +1209 -0
  115. arcgraph/interfaces/metrics.py +571 -0
  116. arcgraph/interfaces/ops.py +330 -0
  117. arcgraph/interfaces/precision.py +1670 -0
  118. arcgraph/interfaces/project_probe.py +75 -0
  119. arcgraph/interfaces/reports.py +1121 -0
  120. arcgraph/interfaces/stdio_encoding.py +29 -0
  121. arcgraph/interfaces/trace.py +592 -0
  122. arcgraph/interfaces/trial_feedback.py +519 -0
  123. arcgraph/interfaces/trial_setup.py +535 -0
  124. arcgraph/interfaces/visual_server.py +354 -0
  125. arcgraph/interfaces/visual_slice.py +168 -0
  126. arcgraph/interfaces/visual_smoke.py +666 -0
  127. arcgraph/interfaces/workbench.py +576 -0
  128. arcgraph/interfaces/workspace.py +1513 -0
  129. arcgraph/pipeline/__init__.py +1 -0
  130. arcgraph/pipeline/contracts.py +122 -0
  131. arcgraph/pipeline/external_frontend.py +451 -0
  132. arcgraph/pipeline/external_l3_frontends.py +368 -0
  133. arcgraph/pipeline/external_protocol.py +358 -0
  134. arcgraph/pipeline/frontends.py +437 -0
  135. arcgraph/pipeline/indexer.py +661 -0
  136. arcgraph/pipeline/openapi_frontend.py +708 -0
  137. arcgraph/pipeline/python_frontend.py +298 -0
  138. arcgraph/pipeline/reindexer.py +1333 -0
  139. arcgraph/pipeline/scip_frontend.py +726 -0
  140. arcgraph/pipeline/typescript_extractor/declarations.mjs +183 -0
  141. arcgraph/pipeline/typescript_extractor/edges.mjs +2794 -0
  142. arcgraph/pipeline/typescript_extractor/graph_primitives.mjs +189 -0
  143. arcgraph/pipeline/typescript_extractor/module_resolution.mjs +877 -0
  144. arcgraph/pipeline/typescript_extractor/runtime.mjs +56 -0
  145. arcgraph/pipeline/typescript_extractor/similarity.mjs +619 -0
  146. arcgraph/pipeline/typescript_extractor/similarity_runner.mjs +19 -0
  147. arcgraph/pipeline/typescript_extractor/sources.mjs +23 -0
  148. arcgraph/pipeline/typescript_extractor.mjs +177 -0
  149. arcgraph/pipeline/typescript_frameworks.py +947 -0
  150. arcgraph/pipeline/typescript_frontend.py +656 -0
  151. arcgraph/pipeline/typescript_references.mjs +167 -0
  152. arcgraph/providers/__init__.py +27 -0
  153. arcgraph/providers/context_provider.py +1258 -0
  154. arcgraph/providers/flow_payload.py +117 -0
  155. arcgraph/providers/memory_connector.py +216 -0
  156. arcgraph/providers/orchestrator.py +475 -0
  157. arcgraph/providers/relationship_payload.py +102 -0
  158. arcgraph/providers/risk_provider.py +843 -0
  159. arcgraph/providers/why_provider.py +349 -0
  160. arcgraph/version_info.py +246 -0
  161. arcgraph-0.1.0.dist-info/METADATA +676 -0
  162. arcgraph-0.1.0.dist-info/RECORD +165 -0
  163. arcgraph-0.1.0.dist-info/WHEEL +4 -0
  164. arcgraph-0.1.0.dist-info/entry_points.txt +2 -0
  165. arcgraph-0.1.0.dist-info/licenses/LICENSE +203 -0
@@ -0,0 +1,311 @@
1
+ """Read-only baseline and current source providers for deterministic diffs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from dataclasses import dataclass
7
+ import hashlib
8
+ from pathlib import Path
9
+ import re
10
+ import subprocess
11
+ from typing import Any
12
+
13
+ from arcgraph.change.contracts import (
14
+ SOURCE_NORMALIZATION_PROJECTION_VERSION,
15
+ SOURCE_PROVIDER_VERSION,
16
+ BaselineReference,
17
+ canonical_digest,
18
+ )
19
+ from arcgraph.change.errors import (
20
+ BaselineGitObjectMissing,
21
+ BaselineSourceIdentityMismatch,
22
+ BaselineSourceUnavailable,
23
+ )
24
+ from arcgraph.change.paths import NormalizedRepositoryPath, normalize_repository_path
25
+
26
+ _COMMIT = re.compile(r"^[0-9a-fA-F]{7,64}$")
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class SourceNormalization:
31
+ raw_source_digest: str
32
+ normalized_implementation_digest: str | None
33
+ status: str
34
+ projection_version: str = SOURCE_NORMALIZATION_PROJECTION_VERSION
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class SourceDocument:
39
+ path: NormalizedRepositoryPath
40
+ content: bytes
41
+ normalization: SourceNormalization
42
+ source_identity: dict[str, Any]
43
+
44
+
45
+ class BaselineSourceProvider:
46
+ """Read exact old-source blobs from the baseline Git commit only."""
47
+
48
+ def __init__(self, repo_root: Path, baseline: BaselineReference) -> None:
49
+ self.repo_root = repo_root.resolve()
50
+ self.baseline = baseline
51
+ commit_sha = baseline.working_tree_identity.head_sha
52
+ if not baseline.working_tree_identity.clean or not commit_sha:
53
+ raise BaselineSourceUnavailable(
54
+ "baseline source provider requires a clean baseline Git commit"
55
+ )
56
+ if not _COMMIT.fullmatch(commit_sha):
57
+ raise BaselineSourceIdentityMismatch(
58
+ "baseline commit SHA has invalid syntax"
59
+ )
60
+ if baseline.build_identity.commit_sha != commit_sha:
61
+ raise BaselineSourceIdentityMismatch(
62
+ "baseline build commit does not match baseline source commit"
63
+ )
64
+ self.commit_sha = commit_sha
65
+ self.git_tree_sha = _git_tree_or_raise(self.repo_root, commit_sha)
66
+ if self.git_tree_sha != baseline.build_identity.git_tree_sha:
67
+ raise BaselineSourceIdentityMismatch(
68
+ "baseline source tree does not match baseline build tree"
69
+ )
70
+ expected = {
71
+ "repo_id": baseline.repo_id,
72
+ "commit_sha": commit_sha,
73
+ "git_tree_sha": self.git_tree_sha,
74
+ "source_provider_version": SOURCE_PROVIDER_VERSION,
75
+ }
76
+ if canonical_digest(expected) != canonical_digest(
77
+ baseline.baseline_source_identity
78
+ ):
79
+ raise BaselineSourceIdentityMismatch(
80
+ "baseline source provider identity differs"
81
+ )
82
+
83
+ def read(self, path: str | Path) -> SourceDocument:
84
+ normalized = normalize_repository_path(path, self.repo_root)
85
+ spec = f"{self.commit_sha}:{normalized.display_path}"
86
+ content = _git_bytes(self.repo_root, ["show", "--no-ext-diff", spec])
87
+ return SourceDocument(
88
+ path=normalized,
89
+ content=content,
90
+ normalization=normalize_implementation_source(
91
+ normalized.display_path, content
92
+ ),
93
+ source_identity={
94
+ "repo_id": self.baseline.repo_id,
95
+ "mode": "baseline_git_commit",
96
+ "commit_sha": self.commit_sha,
97
+ "git_tree_sha": self.git_tree_sha,
98
+ "source_provider_version": SOURCE_PROVIDER_VERSION,
99
+ },
100
+ )
101
+
102
+
103
+ class CurrentSourceProvider:
104
+ """Read either a fixed current commit or the explicit current working tree."""
105
+
106
+ def __init__(
107
+ self,
108
+ repo_root: Path,
109
+ *,
110
+ repo_id: str,
111
+ commit_sha: str | None = None,
112
+ git_tree_sha: str | None = None,
113
+ ) -> None:
114
+ self.repo_root = repo_root.resolve()
115
+ self.repo_id = repo_id
116
+ self.commit_sha = commit_sha
117
+ self.git_tree_sha = git_tree_sha
118
+ if commit_sha is not None and not _COMMIT.fullmatch(commit_sha):
119
+ raise BaselineSourceIdentityMismatch(
120
+ "current commit SHA has invalid syntax"
121
+ )
122
+ if commit_sha is not None and git_tree_sha is None:
123
+ self.git_tree_sha = _git_tree_or_raise(self.repo_root, commit_sha)
124
+
125
+ def read(self, path: str | Path) -> SourceDocument:
126
+ normalized = normalize_repository_path(path, self.repo_root)
127
+ if self.commit_sha:
128
+ content = _git_bytes(
129
+ self.repo_root,
130
+ [
131
+ "show",
132
+ "--no-ext-diff",
133
+ f"{self.commit_sha}:{normalized.display_path}",
134
+ ],
135
+ )
136
+ mode = "current_git_commit"
137
+ else:
138
+ disk_path = self.repo_root / Path(*normalized.display_path.split("/"))
139
+ if disk_path.is_symlink() or not disk_path.is_file():
140
+ raise BaselineSourceUnavailable(
141
+ f"current source path cannot be read: {normalized.display_path}"
142
+ )
143
+ content = disk_path.read_bytes()
144
+ mode = "current_working_tree"
145
+ return SourceDocument(
146
+ path=normalized,
147
+ content=content,
148
+ normalization=normalize_implementation_source(
149
+ normalized.display_path, content
150
+ ),
151
+ source_identity={
152
+ "repo_id": self.repo_id,
153
+ "mode": mode,
154
+ "commit_sha": self.commit_sha,
155
+ "git_tree_sha": self.git_tree_sha,
156
+ "source_provider_version": SOURCE_PROVIDER_VERSION,
157
+ },
158
+ )
159
+
160
+
161
+ def normalize_implementation_source(path: str, content: bytes) -> SourceNormalization:
162
+ """Produce a conservative, non-LLM implementation projection.
163
+
164
+ Python uses the standard AST with attributes omitted. This intentionally
165
+ retains literals, operators, control-flow nodes, calls, returns, and
166
+ expression structure while removing comments, whitespace, and line-ending
167
+ noise. Unsupported languages and invalid syntax are ``unknown`` rather
168
+ than falsely classified as a no-op.
169
+ """
170
+
171
+ return _normalize_python_projection(path, content, content)
172
+
173
+
174
+ def normalize_implementation_region(
175
+ path: str,
176
+ content: bytes,
177
+ *,
178
+ start_line: int,
179
+ end_line: int,
180
+ ) -> SourceNormalization:
181
+ """Project one graph-mapped source region, rather than its whole file.
182
+
183
+ A ChangedRegion is authorized at Symbol granularity. Its source evidence
184
+ must therefore be derived from the mapped Symbol's exact raw line range and
185
+ from the smallest enclosing Python AST node. Parsing the complete document
186
+ preserves context while the selected AST node keeps unrelated symbols out
187
+ of the comparison projection.
188
+ """
189
+
190
+ region = _source_region_bytes(content, start_line, end_line)
191
+ raw_content = region if region is not None else content
192
+ return _normalize_python_projection(
193
+ path,
194
+ content,
195
+ raw_content,
196
+ start_line=start_line,
197
+ end_line=end_line,
198
+ )
199
+
200
+
201
+ def _normalize_python_projection(
202
+ path: str,
203
+ content: bytes,
204
+ raw_content: bytes,
205
+ *,
206
+ start_line: int | None = None,
207
+ end_line: int | None = None,
208
+ ) -> SourceNormalization:
209
+ raw_digest = hashlib.sha256(raw_content).hexdigest()
210
+ if Path(path).suffix.lower() not in {".py", ".pyi"}:
211
+ return SourceNormalization(raw_digest, None, "unknown")
212
+ try:
213
+ source = content.decode("utf-8")
214
+ tree = ast.parse(source, filename=path)
215
+ except (SyntaxError, UnicodeDecodeError):
216
+ return SourceNormalization(raw_digest, None, "unknown")
217
+ if start_line is None and end_line is None:
218
+ projection_node: ast.AST | None = tree
219
+ elif start_line is not None and end_line is not None:
220
+ projection_node = _smallest_ast_region(
221
+ tree,
222
+ start_line,
223
+ end_line,
224
+ source_line_count=max(1, len(content.splitlines())),
225
+ )
226
+ else:
227
+ projection_node = None
228
+ if projection_node is None:
229
+ return SourceNormalization(raw_digest, None, "unknown")
230
+ projection = ast.dump(
231
+ projection_node,
232
+ annotate_fields=True,
233
+ include_attributes=False,
234
+ )
235
+ return SourceNormalization(
236
+ raw_digest,
237
+ hashlib.sha256(projection.encode("utf-8")).hexdigest(),
238
+ "normalized",
239
+ )
240
+
241
+
242
+ def _source_region_bytes(
243
+ content: bytes,
244
+ start_line: int,
245
+ end_line: int,
246
+ ) -> bytes | None:
247
+ lines = content.splitlines(keepends=True)
248
+ if start_line < 1 or end_line < start_line or end_line > len(lines):
249
+ return None
250
+ return b"".join(lines[start_line - 1 : end_line])
251
+
252
+
253
+ def _smallest_ast_region(
254
+ tree: ast.Module,
255
+ start_line: int,
256
+ end_line: int,
257
+ *,
258
+ source_line_count: int,
259
+ ) -> ast.AST | None:
260
+ if start_line < 1 or end_line < start_line:
261
+ return None
262
+ candidates = [
263
+ node
264
+ for node in ast.walk(tree)
265
+ if isinstance(getattr(node, "lineno", None), int)
266
+ and isinstance(getattr(node, "end_lineno", None), int)
267
+ and node.lineno <= start_line
268
+ and node.end_lineno >= end_line
269
+ ]
270
+ if candidates:
271
+ return min(
272
+ candidates,
273
+ key=lambda node: (
274
+ int(getattr(node, "end_lineno")) - int(getattr(node, "lineno")),
275
+ type(node).__name__,
276
+ ),
277
+ )
278
+ if start_line == 1 and end_line >= source_line_count:
279
+ return tree
280
+ return None
281
+
282
+
283
+ def _git_tree_or_raise(repo_root: Path, commit_sha: str) -> str:
284
+ value = _git_bytes(repo_root, ["rev-parse", f"{commit_sha}^{{tree}}"])
285
+ tree = value.decode("ascii", errors="strict").strip()
286
+ if not _COMMIT.fullmatch(tree):
287
+ raise BaselineGitObjectMissing("Git did not return a valid tree object id")
288
+ return tree
289
+
290
+
291
+ def _git_bytes(repo_root: Path, args: list[str]) -> bytes:
292
+ result = subprocess.run(
293
+ ["git", *args],
294
+ cwd=repo_root,
295
+ check=False,
296
+ capture_output=True,
297
+ text=False,
298
+ )
299
+ if result.returncode != 0:
300
+ detail = result.stderr.decode("utf-8", errors="replace").strip()
301
+ lowered = detail.lower()
302
+ if (
303
+ "bad object" in lowered
304
+ or "path '" in lowered
305
+ or "unknown revision" in lowered
306
+ or "ambiguous argument" in lowered
307
+ or "invalid object name" in lowered
308
+ ):
309
+ raise BaselineGitObjectMissing(detail or "Git object is unavailable")
310
+ raise BaselineSourceUnavailable(detail or "Git source provider is unavailable")
311
+ return result.stdout