devtorch-core 3.0.1__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.
- devtorch_core/__init__.py +158 -0
- devtorch_core/aggphi_textual.py +275 -0
- devtorch_core/alerts/__init__.py +23 -0
- devtorch_core/alerts/base.py +46 -0
- devtorch_core/alerts/config.py +60 -0
- devtorch_core/alerts/dispatcher.py +110 -0
- devtorch_core/alerts/jira.py +96 -0
- devtorch_core/alerts/linear.py +72 -0
- devtorch_core/alerts/pagerduty.py +66 -0
- devtorch_core/alerts/slack.py +81 -0
- devtorch_core/alerts/teams.py +70 -0
- devtorch_core/audit/__init__.py +43 -0
- devtorch_core/audit/exporter.py +297 -0
- devtorch_core/audit/privacy.py +101 -0
- devtorch_core/audit/scrubber.py +149 -0
- devtorch_core/audit/service.py +67 -0
- devtorch_core/audit/signing.py +127 -0
- devtorch_core/broadcast/__init__.py +4 -0
- devtorch_core/broadcast/broadcaster.py +100 -0
- devtorch_core/broadcast/watcher.py +71 -0
- devtorch_core/capability.py +639 -0
- devtorch_core/cloud/__init__.py +1 -0
- devtorch_core/cloud/client_config.py +472 -0
- devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
- devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
- devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
- devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
- devtorch_core/cloud/client_configs/.opencode.json +15 -0
- devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
- devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
- devtorch_core/cloud/mcp_client.py +229 -0
- devtorch_core/cloud/setup.py +144 -0
- devtorch_core/cloud/sync.py +143 -0
- devtorch_core/cloud/sync_bundle.py +603 -0
- devtorch_core/cloud/sync_conflicts.py +159 -0
- devtorch_core/cloud/sync_state.py +159 -0
- devtorch_core/cloud/team_sync.py +283 -0
- devtorch_core/codex/__init__.py +9 -0
- devtorch_core/codex/__main__.py +97 -0
- devtorch_core/codex/capture.py +208 -0
- devtorch_core/codex/proxy.py +412 -0
- devtorch_core/concept_catalog.py +209 -0
- devtorch_core/consolidation/__init__.py +3 -0
- devtorch_core/consolidation/synthesizer.py +87 -0
- devtorch_core/consolidation/workflow.py +175 -0
- devtorch_core/daemon/__init__.py +27 -0
- devtorch_core/daemon/supervisor.py +293 -0
- devtorch_core/daemon/watcher.py +244 -0
- devtorch_core/dashboard_api.py +2012 -0
- devtorch_core/deltaf.py +97 -0
- devtorch_core/disclosure.py +50 -0
- devtorch_core/divergence/__init__.py +3 -0
- devtorch_core/divergence/detector.py +166 -0
- devtorch_core/gateway/__init__.py +32 -0
- devtorch_core/gateway/key_manager.py +124 -0
- devtorch_core/gateway/metrics_webhook.py +252 -0
- devtorch_core/gateway/policy.py +262 -0
- devtorch_core/gateway/server.py +727 -0
- devtorch_core/gateway/sso.py +233 -0
- devtorch_core/gcc.py +1246 -0
- devtorch_core/github/__init__.py +35 -0
- devtorch_core/github/app.py +240 -0
- devtorch_core/github/comment_builder.py +113 -0
- devtorch_core/github/pat.py +76 -0
- devtorch_core/github/pr_parser.py +82 -0
- devtorch_core/github/pr_reporter.py +555 -0
- devtorch_core/gitlab/__init__.py +177 -0
- devtorch_core/hitl/__init__.py +4 -0
- devtorch_core/hitl/channels.py +129 -0
- devtorch_core/hitl/orchestrator.py +95 -0
- devtorch_core/hooks/__init__.py +17 -0
- devtorch_core/hooks/claude_code.py +228 -0
- devtorch_core/hooks/git_capture.py +341 -0
- devtorch_core/hooks/git_commit.py +182 -0
- devtorch_core/hooks/installer.py +733 -0
- devtorch_core/hooks/pre_commit.py +157 -0
- devtorch_core/hooks/runner.py +344 -0
- devtorch_core/identity/__init__.py +4 -0
- devtorch_core/identity/agent.py +86 -0
- devtorch_core/identity/providers.py +85 -0
- devtorch_core/invariants.py +182 -0
- devtorch_core/mcp/__init__.py +10 -0
- devtorch_core/mcp/auth.py +177 -0
- devtorch_core/mcp/server.py +1049 -0
- devtorch_core/metrics/__init__.py +35 -0
- devtorch_core/metrics/aggregate.py +215 -0
- devtorch_core/metrics/calibrate.py +198 -0
- devtorch_core/metrics/calibration.py +125 -0
- devtorch_core/metrics/credibility.py +288 -0
- devtorch_core/metrics/delivery_time.py +70 -0
- devtorch_core/metrics/dhs.py +126 -0
- devtorch_core/metrics/mcs.py +96 -0
- devtorch_core/metrics/roi.py +88 -0
- devtorch_core/metrics/session_writer.py +81 -0
- devtorch_core/metrics/shadow_ai.py +117 -0
- devtorch_core/metrics/sprint_writer.py +243 -0
- devtorch_core/observability/__init__.py +78 -0
- devtorch_core/observability/datadog.py +157 -0
- devtorch_core/observability/formatter.py +119 -0
- devtorch_core/observability/report.py +264 -0
- devtorch_core/observability/servicenow.py +147 -0
- devtorch_core/observability/splunk.py +218 -0
- devtorch_core/observability/webhook.py +227 -0
- devtorch_core/parser/__init__.py +30 -0
- devtorch_core/parser/blocks.py +216 -0
- devtorch_core/parser/inference.py +159 -0
- devtorch_core/parser/thinking.py +112 -0
- devtorch_core/projects.py +169 -0
- devtorch_core/prompt_artifact.py +76 -0
- devtorch_core/proxy/__init__.py +9 -0
- devtorch_core/proxy/routes/__init__.py +1 -0
- devtorch_core/proxy/routes/anthropic.py +264 -0
- devtorch_core/proxy/routes/azure_openai.py +336 -0
- devtorch_core/proxy/routes/gemini.py +331 -0
- devtorch_core/proxy/routes/groq.py +284 -0
- devtorch_core/proxy/routes/ollama.py +279 -0
- devtorch_core/proxy/routes/openai.py +287 -0
- devtorch_core/proxy/server.py +356 -0
- devtorch_core/query/__init__.py +15 -0
- devtorch_core/query/grep.py +181 -0
- devtorch_core/query/hybrid.py +86 -0
- devtorch_core/query/semantic.py +157 -0
- devtorch_core/rdp.py +105 -0
- devtorch_core/reasoning/__init__.py +4 -0
- devtorch_core/reasoning/entry.py +31 -0
- devtorch_core/reasoning/store.py +122 -0
- devtorch_core/reasoning_plus/__init__.py +70 -0
- devtorch_core/reasoning_plus/augmenter.py +326 -0
- devtorch_core/reasoning_plus/capture.py +51 -0
- devtorch_core/reasoning_plus/config.py +256 -0
- devtorch_core/reasoning_plus/context.py +262 -0
- devtorch_core/reasoning_plus/learning/__init__.py +72 -0
- devtorch_core/reasoning_plus/learning/analytics.py +141 -0
- devtorch_core/reasoning_plus/learning/api.py +313 -0
- devtorch_core/reasoning_plus/learning/chain.py +285 -0
- devtorch_core/reasoning_plus/learning/composer.py +74 -0
- devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
- devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
- devtorch_core/reasoning_plus/learning/extractor.py +207 -0
- devtorch_core/reasoning_plus/learning/models.py +116 -0
- devtorch_core/reasoning_plus/learning/provenance.py +126 -0
- devtorch_core/reasoning_plus/learning/recorder.py +81 -0
- devtorch_core/reasoning_plus/learning/relevance.py +122 -0
- devtorch_core/reasoning_plus/learning/state.py +86 -0
- devtorch_core/reasoning_plus/learning/store.py +160 -0
- devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
- devtorch_core/reasoning_plus/prompt.py +90 -0
- devtorch_core/rep.py +134 -0
- devtorch_core/rep_network/__init__.py +25 -0
- devtorch_core/rep_network/merge.py +70 -0
- devtorch_core/rep_network/node.py +137 -0
- devtorch_core/rep_network/server.py +140 -0
- devtorch_core/rep_network/sync.py +207 -0
- devtorch_core/sensitivity.py +182 -0
- devtorch_core/serve.py +258 -0
- devtorch_core/session/__init__.py +39 -0
- devtorch_core/session/disagreement.py +188 -0
- devtorch_core/session/models.py +114 -0
- devtorch_core/session/orchestrator.py +182 -0
- devtorch_core/session/planner.py +169 -0
- devtorch_core/session/simulator.py +132 -0
- devtorch_core/signing.py +290 -0
- devtorch_core/sis.py +197 -0
- devtorch_core/storage.py +308 -0
- devtorch_core/templates/__init__.py +6 -0
- devtorch_core/templates/engine.py +122 -0
- devtorch_core/templates/go.py +18 -0
- devtorch_core/templates/infra.py +19 -0
- devtorch_core/templates/library/__init__.py +18 -0
- devtorch_core/templates/library/api_design.md +27 -0
- devtorch_core/templates/library/bug_fix.md +27 -0
- devtorch_core/templates/library/decision_record.md +27 -0
- devtorch_core/templates/library/engine.py +228 -0
- devtorch_core/templates/library/security_review.md +30 -0
- devtorch_core/templates/python.py +19 -0
- devtorch_core/templates/react.py +18 -0
- devtorch_core/templates/typescript.py +18 -0
- devtorch_core/theta.py +221 -0
- devtorch_core/theta_synthesis.py +268 -0
- devtorch_core/topics.py +320 -0
- devtorch_core/variance.py +219 -0
- devtorch_core/wrapper/__init__.py +52 -0
- devtorch_core/wrapper/anthropic.py +487 -0
- devtorch_core/wrapper/base.py +562 -0
- devtorch_core/wrapper/bedrock.py +342 -0
- devtorch_core/wrapper/gemini.py +422 -0
- devtorch_core/wrapper/ollama.py +527 -0
- devtorch_core/wrapper/openai.py +461 -0
- devtorch_core-3.0.1.dist-info/METADATA +867 -0
- devtorch_core-3.0.1.dist-info/RECORD +193 -0
- devtorch_core-3.0.1.dist-info/WHEEL +5 -0
- devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
- devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
devtorch_core/storage.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fcntl
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@runtime_checkable
|
|
9
|
+
class StorageBackend(Protocol):
|
|
10
|
+
def read_bytes(self, key: str) -> bytes: ...
|
|
11
|
+
def write_bytes(self, key: str, data: bytes) -> None: ...
|
|
12
|
+
def append_bytes_atomic(self, key: str, data: bytes) -> None: ...
|
|
13
|
+
def read_and_append_atomic(self, key: str, transform: Callable[[bytes], bytes]) -> None: ...
|
|
14
|
+
def exists(self, key: str) -> bool: ...
|
|
15
|
+
def list_keys(self, prefix: str) -> list[str]: ...
|
|
16
|
+
def delete(self, key: str) -> None: ...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class LocalFileBackend:
|
|
20
|
+
"""StorageBackend backed by the local filesystem. Thread-safe via fcntl.flock."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, root: Path) -> None:
|
|
23
|
+
self._root = root
|
|
24
|
+
|
|
25
|
+
def _path(self, key: str) -> Path:
|
|
26
|
+
p = (self._root / key).resolve()
|
|
27
|
+
if not str(p).startswith(str(self._root.resolve())):
|
|
28
|
+
raise ValueError(f"Key escapes root: {key!r}")
|
|
29
|
+
return p
|
|
30
|
+
|
|
31
|
+
def read_bytes(self, key: str) -> bytes:
|
|
32
|
+
return self._path(key).read_bytes()
|
|
33
|
+
|
|
34
|
+
def write_bytes(self, key: str, data: bytes) -> None:
|
|
35
|
+
p = self._path(key)
|
|
36
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
tmp = p.with_suffix(p.suffix + ".tmp")
|
|
38
|
+
tmp.write_bytes(data)
|
|
39
|
+
tmp.replace(p)
|
|
40
|
+
|
|
41
|
+
def append_bytes_atomic(self, key: str, data: bytes) -> None:
|
|
42
|
+
p = self._path(key)
|
|
43
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
with p.open("ab") as f:
|
|
45
|
+
fcntl.flock(f, fcntl.LOCK_EX)
|
|
46
|
+
try:
|
|
47
|
+
f.write(data)
|
|
48
|
+
f.flush()
|
|
49
|
+
finally:
|
|
50
|
+
fcntl.flock(f, fcntl.LOCK_UN)
|
|
51
|
+
|
|
52
|
+
def read_and_append_atomic(self, key: str, transform: Callable[[bytes], bytes]) -> None:
|
|
53
|
+
"""Read current content, call transform(content) to get bytes to append, write atomically."""
|
|
54
|
+
p = self._path(key)
|
|
55
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
with p.open("a+b") as f:
|
|
57
|
+
fcntl.flock(f, fcntl.LOCK_EX)
|
|
58
|
+
try:
|
|
59
|
+
f.seek(0)
|
|
60
|
+
current = f.read()
|
|
61
|
+
new_data = transform(current)
|
|
62
|
+
f.seek(0, 2)
|
|
63
|
+
f.write(new_data)
|
|
64
|
+
f.flush()
|
|
65
|
+
finally:
|
|
66
|
+
fcntl.flock(f, fcntl.LOCK_UN)
|
|
67
|
+
|
|
68
|
+
def exists(self, key: str) -> bool:
|
|
69
|
+
return self._path(key).exists()
|
|
70
|
+
|
|
71
|
+
def list_keys(self, prefix: str) -> list[str]:
|
|
72
|
+
base = self._root / prefix
|
|
73
|
+
if not base.exists():
|
|
74
|
+
return []
|
|
75
|
+
return sorted(
|
|
76
|
+
str(p.relative_to(self._root))
|
|
77
|
+
for p in base.rglob("*")
|
|
78
|
+
if p.is_file()
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def delete(self, key: str) -> None:
|
|
82
|
+
self._path(key).unlink(missing_ok=True)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class R2Backend:
|
|
86
|
+
"""StorageBackend backed by Cloudflare R2 (S3-compatible). Uses conditional PUTs for atomic append."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, client: Any, bucket: str, prefix: str) -> None:
|
|
89
|
+
# prefix = "{org_id}/{repo_id}/.GCC" (no trailing slash)
|
|
90
|
+
self._s3 = client
|
|
91
|
+
self._bucket = bucket
|
|
92
|
+
self._prefix = prefix.rstrip("/")
|
|
93
|
+
|
|
94
|
+
def _key(self, relative: str) -> str:
|
|
95
|
+
return f"{self._prefix}/{relative}"
|
|
96
|
+
|
|
97
|
+
def read_bytes(self, key: str) -> bytes:
|
|
98
|
+
try:
|
|
99
|
+
resp = self._s3.get_object(Bucket=self._bucket, Key=self._key(key))
|
|
100
|
+
return resp["Body"].read()
|
|
101
|
+
except self._s3.exceptions.ClientError as e:
|
|
102
|
+
if e.response["Error"]["Code"] in ("NoSuchKey", "404"):
|
|
103
|
+
raise FileNotFoundError(key)
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
def write_bytes(self, key: str, data: bytes) -> None:
|
|
107
|
+
self._s3.put_object(Bucket=self._bucket, Key=self._key(key), Body=data)
|
|
108
|
+
|
|
109
|
+
def append_bytes_atomic(self, key: str, data: bytes) -> None:
|
|
110
|
+
self.read_and_append_atomic(key, lambda current: data)
|
|
111
|
+
|
|
112
|
+
def read_and_append_atomic(self, key: str, transform: Callable[[bytes], bytes]) -> None:
|
|
113
|
+
full_key = self._key(key)
|
|
114
|
+
for attempt in range(10):
|
|
115
|
+
try:
|
|
116
|
+
resp = self._s3.get_object(Bucket=self._bucket, Key=full_key)
|
|
117
|
+
current = resp["Body"].read()
|
|
118
|
+
etag = resp["ETag"].strip('"')
|
|
119
|
+
except self._s3.exceptions.ClientError as e:
|
|
120
|
+
if e.response["Error"]["Code"] in ("NoSuchKey", "404"):
|
|
121
|
+
current, etag = b"", None
|
|
122
|
+
else:
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
new_data = transform(current)
|
|
126
|
+
new_content = current + new_data
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
kwargs: dict = {"Bucket": self._bucket, "Key": full_key, "Body": new_content}
|
|
130
|
+
if etag:
|
|
131
|
+
kwargs["IfMatch"] = f'"{etag}"'
|
|
132
|
+
else:
|
|
133
|
+
# Blob did not exist — use If-None-Match: * to guard concurrent creates.
|
|
134
|
+
kwargs["IfNoneMatch"] = "*"
|
|
135
|
+
self._s3.put_object(**kwargs)
|
|
136
|
+
return
|
|
137
|
+
except self._s3.exceptions.ClientError as e:
|
|
138
|
+
if e.response["Error"]["Code"] in ("PreconditionFailed", "ConditionalRequestConflict"):
|
|
139
|
+
continue
|
|
140
|
+
raise
|
|
141
|
+
raise RuntimeError(f"R2Backend: failed to atomically update {key} after 10 retries")
|
|
142
|
+
|
|
143
|
+
def exists(self, key: str) -> bool:
|
|
144
|
+
try:
|
|
145
|
+
self.read_bytes(key)
|
|
146
|
+
return True
|
|
147
|
+
except FileNotFoundError:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def list_keys(self, prefix: str) -> list[str]:
|
|
151
|
+
full_prefix = self._key(prefix)
|
|
152
|
+
resp = self._s3.list_objects_v2(Bucket=self._bucket, Prefix=full_prefix)
|
|
153
|
+
contents = resp.get("Contents", [])
|
|
154
|
+
strip = self._prefix + "/"
|
|
155
|
+
return sorted(item["Key"].removeprefix(strip) for item in contents)
|
|
156
|
+
|
|
157
|
+
def delete(self, key: str) -> None:
|
|
158
|
+
self._s3.delete_object(Bucket=self._bucket, Key=self._key(key))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class AzureBlobBackend:
|
|
162
|
+
"""StorageBackend backed by Azure Blob Storage. Uses ETag conditional PUTs for atomic append."""
|
|
163
|
+
|
|
164
|
+
def __init__(self, client: Any, container: str, prefix: str) -> None:
|
|
165
|
+
# prefix = "{org_id}/{repo_id}/.GCC" (no trailing slash)
|
|
166
|
+
self._service = client
|
|
167
|
+
self._container = container
|
|
168
|
+
self._prefix = prefix.rstrip("/")
|
|
169
|
+
self._container_client = client.get_container_client(container)
|
|
170
|
+
|
|
171
|
+
def _blob_name(self, relative: str) -> str:
|
|
172
|
+
return f"{self._prefix}/{relative}" if self._prefix else relative
|
|
173
|
+
|
|
174
|
+
def read_bytes(self, key: str) -> bytes:
|
|
175
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
176
|
+
try:
|
|
177
|
+
blob_client = self._container_client.get_blob_client(self._blob_name(key))
|
|
178
|
+
downloader = blob_client.download_blob()
|
|
179
|
+
return downloader.readall()
|
|
180
|
+
except ResourceNotFoundError as exc:
|
|
181
|
+
raise FileNotFoundError(key) from exc
|
|
182
|
+
|
|
183
|
+
def write_bytes(self, key: str, data: bytes) -> None:
|
|
184
|
+
blob_client = self._container_client.get_blob_client(self._blob_name(key))
|
|
185
|
+
blob_client.upload_blob(data, overwrite=True)
|
|
186
|
+
|
|
187
|
+
def append_bytes_atomic(self, key: str, data: bytes) -> None:
|
|
188
|
+
self.read_and_append_atomic(key, lambda _current: data)
|
|
189
|
+
|
|
190
|
+
def read_and_append_atomic(self, key: str, transform: Callable[[bytes], bytes]) -> None:
|
|
191
|
+
from azure.core.exceptions import ResourceExistsError, ResourceModifiedError, ResourceNotFoundError
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
from azure.core import MatchConditions
|
|
195
|
+
except ImportError:
|
|
196
|
+
from azure.core.conditions import MatchConditions # type: ignore[no-redef]
|
|
197
|
+
blob_client = self._container_client.get_blob_client(self._blob_name(key))
|
|
198
|
+
|
|
199
|
+
for _attempt in range(10):
|
|
200
|
+
try:
|
|
201
|
+
downloader = blob_client.download_blob()
|
|
202
|
+
current = downloader.readall()
|
|
203
|
+
etag = downloader.properties["etag"].strip('"')
|
|
204
|
+
except ResourceNotFoundError:
|
|
205
|
+
current, etag = b"", None
|
|
206
|
+
|
|
207
|
+
new_data = transform(current)
|
|
208
|
+
new_content = current + new_data
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
if etag:
|
|
212
|
+
blob_client.upload_blob(
|
|
213
|
+
new_content,
|
|
214
|
+
overwrite=True,
|
|
215
|
+
etag=etag,
|
|
216
|
+
match_condition=MatchConditions.IfNotModified,
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
# Blob did not exist — use overwrite=False to guard concurrent creates.
|
|
220
|
+
blob_client.upload_blob(new_content, overwrite=False)
|
|
221
|
+
return
|
|
222
|
+
except (ResourceModifiedError, ResourceExistsError):
|
|
223
|
+
continue
|
|
224
|
+
raise RuntimeError(f"AzureBlobBackend: failed to atomically update {key} after 10 retries")
|
|
225
|
+
|
|
226
|
+
def exists(self, key: str) -> bool:
|
|
227
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
228
|
+
try:
|
|
229
|
+
self._container_client.get_blob_client(self._blob_name(key)).get_blob_properties()
|
|
230
|
+
return True
|
|
231
|
+
except ResourceNotFoundError:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
def list_keys(self, prefix: str) -> list[str]:
|
|
235
|
+
if self._prefix:
|
|
236
|
+
full_prefix = f"{self._prefix}/{prefix}" if prefix else self._prefix + "/"
|
|
237
|
+
else:
|
|
238
|
+
full_prefix = prefix
|
|
239
|
+
blobs = self._container_client.list_blobs(name_starts_with=full_prefix)
|
|
240
|
+
strip = self._prefix + "/" if self._prefix else ""
|
|
241
|
+
return sorted(b.name.removeprefix(strip) for b in blobs)
|
|
242
|
+
|
|
243
|
+
def delete(self, key: str) -> None:
|
|
244
|
+
from azure.core.exceptions import ResourceNotFoundError
|
|
245
|
+
blob_client = self._container_client.get_blob_client(self._blob_name(key))
|
|
246
|
+
try:
|
|
247
|
+
blob_client.delete_blob()
|
|
248
|
+
except ResourceNotFoundError:
|
|
249
|
+
pass
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_backend_from_env(prefix: str = "") -> "StorageBackend":
|
|
253
|
+
"""
|
|
254
|
+
Create a StorageBackend from environment variables.
|
|
255
|
+
|
|
256
|
+
DEVTORCH_STORAGE controls which backend to use:
|
|
257
|
+
r2 — Cloudflare R2 (boto3, requires R2_ACCOUNT_ID, R2_BUCKET, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY)
|
|
258
|
+
s3 — Native AWS S3 (boto3, requires AWS_S3_BUCKET; uses IAM/env auth automatically)
|
|
259
|
+
azure — Azure Blob Storage (requires DEVTORCH_AZURE_CONTAINER + AZURE_STORAGE_CONNECTION_STRING
|
|
260
|
+
or AZURE_STORAGE_ACCOUNT_URL)
|
|
261
|
+
local — Local filesystem (uses DEVTORCH_GCC_PATH or current directory)
|
|
262
|
+
|
|
263
|
+
prefix is prepended to all storage keys (e.g. "{org_id}" for auth backends).
|
|
264
|
+
"""
|
|
265
|
+
import os
|
|
266
|
+
|
|
267
|
+
storage = os.environ.get("DEVTORCH_STORAGE", "r2").lower()
|
|
268
|
+
|
|
269
|
+
if storage == "r2":
|
|
270
|
+
import boto3
|
|
271
|
+
bucket = os.environ.get("DEVTORCH_BUCKET") or os.environ["R2_BUCKET"]
|
|
272
|
+
account_id = os.environ["R2_ACCOUNT_ID"]
|
|
273
|
+
access_key = os.environ.get("DEVTORCH_ACCESS_KEY_ID") or os.environ["R2_ACCESS_KEY_ID"]
|
|
274
|
+
secret_key = os.environ.get("DEVTORCH_SECRET_ACCESS_KEY") or os.environ["R2_SECRET_ACCESS_KEY"]
|
|
275
|
+
s3 = boto3.client(
|
|
276
|
+
"s3",
|
|
277
|
+
endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
|
|
278
|
+
aws_access_key_id=access_key,
|
|
279
|
+
aws_secret_access_key=secret_key,
|
|
280
|
+
region_name="auto",
|
|
281
|
+
)
|
|
282
|
+
return R2Backend(client=s3, bucket=bucket, prefix=prefix)
|
|
283
|
+
|
|
284
|
+
elif storage == "s3":
|
|
285
|
+
import boto3
|
|
286
|
+
bucket = os.environ.get("DEVTORCH_BUCKET") or os.environ["AWS_S3_BUCKET"]
|
|
287
|
+
s3 = boto3.client("s3") # uses IAM role, env vars, or ~/.aws/credentials
|
|
288
|
+
return R2Backend(client=s3, bucket=bucket, prefix=prefix)
|
|
289
|
+
|
|
290
|
+
elif storage == "azure":
|
|
291
|
+
from azure.storage.blob import BlobServiceClient
|
|
292
|
+
container = os.environ["DEVTORCH_AZURE_CONTAINER"]
|
|
293
|
+
conn_str = os.environ.get("AZURE_STORAGE_CONNECTION_STRING")
|
|
294
|
+
if conn_str:
|
|
295
|
+
service = BlobServiceClient.from_connection_string(conn_str)
|
|
296
|
+
else:
|
|
297
|
+
account_url = os.environ["AZURE_STORAGE_ACCOUNT_URL"]
|
|
298
|
+
service = BlobServiceClient(account_url=account_url)
|
|
299
|
+
return AzureBlobBackend(client=service, container=container, prefix=prefix)
|
|
300
|
+
|
|
301
|
+
elif storage == "local":
|
|
302
|
+
gcc_path = Path(os.environ.get("DEVTORCH_GCC_PATH", "."))
|
|
303
|
+
return LocalFileBackend(gcc_path)
|
|
304
|
+
|
|
305
|
+
else:
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"Unknown DEVTORCH_STORAGE: {storage!r}. Must be one of: r2, s3, azure, local."
|
|
308
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Built-in project templates for `devtorch init --template`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from devtorch_core import GCCRepository
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Template:
|
|
14
|
+
"""A project template defines default concepts and a governance note."""
|
|
15
|
+
|
|
16
|
+
name: str
|
|
17
|
+
display_name: str
|
|
18
|
+
project_type: str
|
|
19
|
+
concepts: list[tuple[str, str]]
|
|
20
|
+
governance_note: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Built-in template registry keyed by template name.
|
|
24
|
+
_TEMPLATES: dict[str, Template] = {}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register_template(template: Template) -> None:
|
|
28
|
+
"""Register a built-in or custom template."""
|
|
29
|
+
_TEMPLATES[template.name] = template
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_builtin_templates() -> None:
|
|
33
|
+
"""Lazily import and register built-in templates."""
|
|
34
|
+
if _TEMPLATES:
|
|
35
|
+
return
|
|
36
|
+
from devtorch_core.templates import python, typescript, react, go, infra
|
|
37
|
+
|
|
38
|
+
for module in (python, typescript, react, go, infra):
|
|
39
|
+
register_template(module.TEMPLATE)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def list_templates() -> list[dict[str, Any]]:
|
|
43
|
+
"""Return a list of available templates with metadata."""
|
|
44
|
+
_load_builtin_templates()
|
|
45
|
+
return [
|
|
46
|
+
{
|
|
47
|
+
"name": t.name,
|
|
48
|
+
"display_name": t.display_name,
|
|
49
|
+
"project_type": t.project_type,
|
|
50
|
+
}
|
|
51
|
+
for t in _TEMPLATES.values()
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_template(name: str) -> Template:
|
|
56
|
+
"""Look up a template by name; raise ValueError if unknown."""
|
|
57
|
+
_load_builtin_templates()
|
|
58
|
+
try:
|
|
59
|
+
return _TEMPLATES[name]
|
|
60
|
+
except KeyError as exc:
|
|
61
|
+
raise ValueError(f"Unknown template: {name}") from exc
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def apply_template(
|
|
65
|
+
gcc_dir: Path | str,
|
|
66
|
+
template_name: str,
|
|
67
|
+
project_type: str | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Apply a project template to a GCC repository.
|
|
71
|
+
|
|
72
|
+
The repository at `gcc_dir` is initialized if it has not been already.
|
|
73
|
+
Default concepts are written to `.GCC/concepts/` and the template's
|
|
74
|
+
governance note is persisted to `.GCC/templates/policy.md`.
|
|
75
|
+
"""
|
|
76
|
+
template = _get_template(template_name)
|
|
77
|
+
repo = GCCRepository.at(Path(gcc_dir).resolve())
|
|
78
|
+
|
|
79
|
+
if not repo.is_initialized():
|
|
80
|
+
repo.init()
|
|
81
|
+
|
|
82
|
+
for concept_name, definition in template.concepts:
|
|
83
|
+
repo.concept_add(concept_name, definition)
|
|
84
|
+
|
|
85
|
+
policy_dir = repo.gcc_dir / "templates"
|
|
86
|
+
policy_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
policy_path = policy_dir / "policy.md"
|
|
88
|
+
|
|
89
|
+
effective_type = project_type or template.project_type
|
|
90
|
+
note = f"""# Template: {template.display_name}
|
|
91
|
+
|
|
92
|
+
Project type: {effective_type}
|
|
93
|
+
Default governance focus: {template.governance_note}
|
|
94
|
+
|
|
95
|
+
Applied concepts: {', '.join(name for name, _ in template.concepts)}
|
|
96
|
+
"""
|
|
97
|
+
policy_path.write_text(note, encoding="utf-8")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cmd_init_template(args: Any) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Helper for CLI integration with `devtorch init --template <type>`.
|
|
103
|
+
|
|
104
|
+
`args` is a Namespace-like object with at least `path` and `template`.
|
|
105
|
+
Optional `project_type` customizes the template's declared project type.
|
|
106
|
+
"""
|
|
107
|
+
template_name = getattr(args, "template", None)
|
|
108
|
+
if not template_name:
|
|
109
|
+
print("error: --template is required", file=sys.stderr)
|
|
110
|
+
return 1
|
|
111
|
+
|
|
112
|
+
project_root = Path(getattr(args, "path", ".")).resolve()
|
|
113
|
+
project_type = getattr(args, "project_type", None)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
apply_template(project_root, template_name, project_type)
|
|
117
|
+
except ValueError as e:
|
|
118
|
+
print(f"error: {e}", file=sys.stderr)
|
|
119
|
+
return 1
|
|
120
|
+
|
|
121
|
+
print(f"Applied template '{template_name}' to {project_root}")
|
|
122
|
+
return 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Built-in Go project template."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from devtorch_core.templates.engine import Template
|
|
5
|
+
|
|
6
|
+
TEMPLATE = Template(
|
|
7
|
+
name="go",
|
|
8
|
+
display_name="Go",
|
|
9
|
+
project_type="go",
|
|
10
|
+
concepts=[
|
|
11
|
+
("auth", "Authentication, authorization, and identity management."),
|
|
12
|
+
("schema", "Structs, validation, and serialization contracts."),
|
|
13
|
+
("api", "HTTP handlers, gRPC services, and public interfaces."),
|
|
14
|
+
("data", "Storage, repositories, and data access patterns."),
|
|
15
|
+
("cmd", "Command-line entry points and service binaries."),
|
|
16
|
+
],
|
|
17
|
+
governance_note="Go projects default to explicit interfaces, small command entry points, and clear separation between API handlers, business logic, and data repositories.",
|
|
18
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Built-in infrastructure project template."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from devtorch_core.templates.engine import Template
|
|
5
|
+
|
|
6
|
+
TEMPLATE = Template(
|
|
7
|
+
name="infra",
|
|
8
|
+
display_name="Infrastructure",
|
|
9
|
+
project_type="infra",
|
|
10
|
+
concepts=[
|
|
11
|
+
("auth", "Identity, access control, and least-privilege policies."),
|
|
12
|
+
("schema", "Resource definitions, configuration schemas, and versioning."),
|
|
13
|
+
("api", "Gateway, load balancer, and service mesh configuration."),
|
|
14
|
+
("data", "Databases, object storage, and backup policies."),
|
|
15
|
+
("networking", "VPCs, subnets, DNS, and firewall rules."),
|
|
16
|
+
("secrets", "Secret management, rotation, and encryption at rest."),
|
|
17
|
+
],
|
|
18
|
+
governance_note="Infrastructure projects default to declarative resource definitions, least-privilege access, explicit networking boundaries, and auditable secret management.",
|
|
19
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Reasoning template library package."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from devtorch_core.templates.library.engine import (
|
|
5
|
+
apply_template,
|
|
6
|
+
discover_custom_templates,
|
|
7
|
+
list_builtin_templates,
|
|
8
|
+
load_builtin_template,
|
|
9
|
+
render_template,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"apply_template",
|
|
14
|
+
"discover_custom_templates",
|
|
15
|
+
"list_builtin_templates",
|
|
16
|
+
"load_builtin_template",
|
|
17
|
+
"render_template",
|
|
18
|
+
]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api_design
|
|
3
|
+
description: Design rationale for an API or interface change
|
|
4
|
+
tags: [reasoning, api, design]
|
|
5
|
+
---
|
|
6
|
+
# API Design: {{title}}
|
|
7
|
+
|
|
8
|
+
## Goal
|
|
9
|
+
{{goal}}
|
|
10
|
+
|
|
11
|
+
## Constraints
|
|
12
|
+
{{constraints}}
|
|
13
|
+
|
|
14
|
+
## Proposed interface
|
|
15
|
+
{{interface}}
|
|
16
|
+
|
|
17
|
+
## Design rationale
|
|
18
|
+
{{reasoning}}
|
|
19
|
+
|
|
20
|
+
## Alternatives considered
|
|
21
|
+
{{alternatives}}
|
|
22
|
+
|
|
23
|
+
## Risks
|
|
24
|
+
{{risks}}
|
|
25
|
+
|
|
26
|
+
## Migration plan
|
|
27
|
+
{{migration}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bug_fix
|
|
3
|
+
description: Capture the investigation, root cause, and fix for a bug
|
|
4
|
+
tags: [reasoning, bug, debugging]
|
|
5
|
+
---
|
|
6
|
+
# Bug Fix: {{title}}
|
|
7
|
+
|
|
8
|
+
## Observed behaviour
|
|
9
|
+
{{observed}}
|
|
10
|
+
|
|
11
|
+
## Expected behaviour
|
|
12
|
+
{{expected}}
|
|
13
|
+
|
|
14
|
+
## Reproduction steps
|
|
15
|
+
{{repro_steps}}
|
|
16
|
+
|
|
17
|
+
## Root cause analysis
|
|
18
|
+
{{reasoning}}
|
|
19
|
+
|
|
20
|
+
## Fix
|
|
21
|
+
{{fix}}
|
|
22
|
+
|
|
23
|
+
## Verification
|
|
24
|
+
{{verification}}
|
|
25
|
+
|
|
26
|
+
## Prevention
|
|
27
|
+
{{prevention}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: decision_record
|
|
3
|
+
description: Structured reasoning record for architectural or product decisions
|
|
4
|
+
tags: [reasoning, decision, architecture]
|
|
5
|
+
---
|
|
6
|
+
# Decision Record: {{title}}
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
{{context}}
|
|
10
|
+
|
|
11
|
+
## Problem statement
|
|
12
|
+
{{problem}}
|
|
13
|
+
|
|
14
|
+
## Options considered
|
|
15
|
+
{{options}}
|
|
16
|
+
|
|
17
|
+
## Decision
|
|
18
|
+
{{decision}}
|
|
19
|
+
|
|
20
|
+
## Rationale
|
|
21
|
+
{{reasoning}}
|
|
22
|
+
|
|
23
|
+
## Consequences
|
|
24
|
+
{{consequences}}
|
|
25
|
+
|
|
26
|
+
## Related concepts
|
|
27
|
+
{{related_concepts}}
|