refactor(python): rename package from 'contexa' to 'cortexa'
Swadhin Biswas committed 7 months ago
3fa717d
1 parent 7cbf4e3
commit 3fa717d
13 files changed
+699 -455
Showing 13 changed files+699-455
▼PYTHON/README.md
+21-21
| @@ -1,11 +1,11 @@ | ||
1 | -# contexa | |
1 | +# cortexa | |
2 | 2 | |
3 | -[](https://pypi.org/project/contexa/) | |
4 | -[](https://www.python.org/downloads/) | |
5 | -[](https://github.com/swadhinbiswas/contexa/blob/main/LICENSE) | |
6 | -[](https://github.com/swadhinbiswas/contexa) | |
3 | +[](https://pypi.org/project/cortexa/) | |
4 | +[](https://www.python.org/downloads/) | |
5 | +[](https://github.com/swadhinbiswas/Cortexa/blob/main/LICENSE) | |
6 | +[](https://github.com/swadhinbiswas/Cortexa) | |
7 | 7 | |
8 | -A Python implementation of the **contexa** framework -- Git-inspired context management for LLM agents. | |
8 | +A Python implementation of the **cortexa** framework -- Git-inspired context management for LLM agents. | |
9 | 9 | |
10 | 10 | Based on: [arXiv:2508.00031](https://arxiv.org/abs/2508.00031) -- *"Git Context Controller: Manage the Context of LLM-based Agents like Git"* (Junde Wu et al., 2025) |
11 | 11 | |
| @@ -73,13 +73,13 @@ The key insight from the paper: by controlling **how much history** the agent se | ||
73 | 73 | ## Installation |
74 | 74 | |
75 | 75 | ```bash |
76 | -pip install contexa | |
76 | +pip install cortexa | |
77 | 77 | ``` |
78 | 78 | |
79 | 79 | Or with [uv](https://docs.astral.sh/uv/): |
80 | 80 | |
81 | 81 | ```bash |
82 | -uv add contexa | |
82 | +uv add cortexa | |
83 | 83 | ``` |
84 | 84 | |
85 | 85 | --- |
| @@ -87,7 +87,7 @@ uv add contexa | ||
87 | 87 | ## Quick Start |
88 | 88 | |
89 | 89 | ```python |
90 | -from contexa import GCCWorkspace | |
90 | +from cortexa import GCCWorkspace | |
91 | 91 | |
92 | 92 | # 1. Initialize a workspace |
93 | 93 | ws = GCCWorkspace("/path/to/project") |
| @@ -284,7 +284,7 @@ All data is stored as **human-readable Markdown and YAML** -- you can inspect an | ||
284 | 284 | All models support serialization: |
285 | 285 | |
286 | 286 | ```python |
287 | -from contexa import OTARecord, BranchMetadata | |
287 | +from cortexa import OTARecord, BranchMetadata | |
288 | 288 | |
289 | 289 | # OTARecord <-> dict |
290 | 290 | record = OTARecord.from_dict({"timestamp": "...", "observation": "...", ...}) |
| @@ -302,10 +302,10 @@ print(record.to_markdown()) | ||
302 | 302 | |
303 | 303 | ## Real-World Example |
304 | 304 | |
305 | -Here's how an autonomous coding agent might use contexa to manage its memory while building a web application: | |
305 | +Here's how an autonomous coding agent might use cortexa to manage its memory while building a web application: | |
306 | 306 | |
307 | 307 | ```python |
308 | -from contexa import GCCWorkspace | |
308 | +from cortexa import GCCWorkspace | |
309 | 309 | |
310 | 310 | ws = GCCWorkspace("./my-webapp") |
311 | 311 | ws.init("Build a Flask web app with user auth, blog posts, and admin panel") |
| @@ -348,8 +348,8 @@ ws.commit("Blog post CRUD with auth-protected routes") | ||
348 | 348 | |
349 | 349 | ```bash |
350 | 350 | # Clone the repository |
351 | -git clone https://github.com/swadhinbiswas/contexa.git | |
352 | -cd contexa | |
351 | +git clone https://github.com/swadhinbiswas/Cortexa.git | |
352 | +cd cortexa | |
353 | 353 | |
354 | 354 | # Install dev dependencies and run tests |
355 | 355 | uv sync |
| @@ -380,19 +380,19 @@ test_ota_step_increments # Step auto-increment | ||
380 | 380 | |
381 | 381 | Contributions are welcome! Here's how to get started: |
382 | 382 | |
383 | -1. Fork the repository: [https://github.com/swadhinbiswas/contexa](https://github.com/swadhinbiswas/contexa) | |
383 | +1. Fork the repository: [https://github.com/swadhinbiswas/Cortexa](https://github.com/swadhinbiswas/Cortexa) | |
384 | 384 | 2. Create a feature branch: `git checkout -b feature/my-feature` |
385 | 385 | 3. Make your changes and add tests |
386 | 386 | 4. Run the test suite: `uv run pytest -v` |
387 | 387 | 5. Submit a pull request |
388 | 388 | |
389 | -Please open an [issue](https://github.com/swadhinbiswas/contexa/issues) first for major changes to discuss the approach. | |
389 | +Please open an [issue](https://github.com/swadhinbiswas/Cortexa/issues) first for major changes to discuss the approach. | |
390 | 390 | |
391 | 391 | --- |
392 | 392 | |
393 | 393 | ## Requirements |
394 | 394 | |
395 | -- **Python** >= 3.12 | |
395 | +- **Python** >= 3.10 | |
396 | 396 | - **PyYAML** >= 6.0 |
397 | 397 | |
398 | 398 | No other dependencies. The entire implementation uses Python's standard library (`dataclasses`, `pathlib`, `uuid`, `datetime`) plus PyYAML for metadata serialization. |
| @@ -401,7 +401,7 @@ No other dependencies. The entire implementation uses Python's standard library | ||
401 | 401 | |
402 | 402 | ## License |
403 | 403 | |
404 | -This project is licensed under the MIT License. See the [LICENSE](https://github.com/swadhinbiswas/contexa/blob/main/LICENSE) file for details. | |
404 | +This project is licensed under the MIT License. See the [LICENSE](https://github.com/swadhinbiswas/Cortexa/blob/main/LICENSE) file for details. | |
405 | 405 | |
406 | 406 | --- |
407 | 407 | |
| @@ -422,8 +422,8 @@ If you use this in research, please cite the original paper: | ||
422 | 422 | |
423 | 423 | ## Links |
424 | 424 | |
425 | -- **GitHub Repository**: [https://github.com/swadhinbiswas/contexa](https://github.com/swadhinbiswas/contexa) | |
426 | -- **PyPI Package**: [https://pypi.org/project/contexa/](https://pypi.org/project/contexa/) | |
427 | -- **Issue Tracker**: [https://github.com/swadhinbiswas/contexa/issues](https://github.com/swadhinbiswas/contexa/issues) | |
425 | +- **GitHub Repository**: [https://github.com/swadhinbiswas/Cortexa](https://github.com/swadhinbiswas/Cortexa) | |
426 | +- **PyPI Package**: [https://pypi.org/project/cortexa/](https://pypi.org/project/cortexa/) | |
427 | +- **Issue Tracker**: [https://github.com/swadhinbiswas/Cortexa/issues](https://github.com/swadhinbiswas/Cortexa/issues) | |
428 | 428 | - **Original Paper**: [arXiv:2508.00031v2](https://arxiv.org/abs/2508.00031v2) |
429 | 429 | - **Author**: [Swadhin Biswas](https://github.com/swadhinbiswas) |
430 | 430 | |
▼PYTHON/contexa/workspace.py
+0-407
| @@ -1,407 +0,0 @@ | ||
1 | -""" | |
2 | -GCC Workspace — core file-system layer. | |
3 | -Based on arXiv:2508.00031v2. | |
4 | - | |
5 | -The paper specifies a .GCC/ root directory containing: | |
6 | - main.md — global roadmap / planning artifact | |
7 | - branches/ | |
8 | - <branch_name>/ | |
9 | - log.md — continuous OTA (Observation-Thought-Action) trace | |
10 | - commit.md — milestone-level commit summaries | |
11 | - metadata.yaml — branch intent, status, creation info | |
12 | - | |
13 | -All four commands (COMMIT, BRANCH, MERGE, CONTEXT) operate on this structure. | |
14 | -""" | |
15 | - | |
16 | -import os | |
17 | -import uuid | |
18 | -from datetime import datetime, timezone | |
19 | -from pathlib import Path | |
20 | -from typing import Optional | |
21 | - | |
22 | -from .models import ( | |
23 | - BranchMetadata, | |
24 | - CommitRecord, | |
25 | - ContextResult, | |
26 | - OTARecord, | |
27 | -) | |
28 | - | |
29 | - | |
30 | -MAIN_BRANCH = "main" | |
31 | -GCC_DIR = ".GCC" | |
32 | - | |
33 | - | |
34 | -def _now() -> str: | |
35 | - return datetime.now(timezone.utc).isoformat() | |
36 | - | |
37 | - | |
38 | -def _short_id() -> str: | |
39 | - return str(uuid.uuid4())[:8] | |
40 | - | |
41 | - | |
42 | -class GCCWorkspace: | |
43 | - """ | |
44 | - Manages the .GCC/ directory structure for one agent project. | |
45 | - | |
46 | - Usage | |
47 | - ----- | |
48 | - ws = GCCWorkspace("/path/to/project") | |
49 | - ws.init("Build a REST API service") | |
50 | - | |
51 | - # agent logs OTA steps | |
52 | - ws.log_ota("file list returned", "need to read main.py", "read_file main.py") | |
53 | - | |
54 | - # agent commits a milestone | |
55 | - ws.commit("Implement GET /users endpoint") | |
56 | - | |
57 | - # agent branches to explore alternative | |
58 | - ws.branch("auth-jwt", "Explore JWT-based auth vs session auth") | |
59 | - | |
60 | - # agent merges branch back | |
61 | - ws.merge("auth-jwt") | |
62 | - | |
63 | - # retrieve context (K=1 by default, per paper experiments) | |
64 | - ctx = ws.context(k=1) | |
65 | - print(ctx.summary()) | |
66 | - """ | |
67 | - | |
68 | - def __init__(self, project_root: str): | |
69 | - self.root = Path(project_root) | |
70 | - self.gcc_dir = self.root / GCC_DIR | |
71 | - self._current_branch: str = MAIN_BRANCH | |
72 | - | |
73 | - def _branch_dir(self, branch: str) -> Path: | |
74 | - return self.gcc_dir / "branches" / branch | |
75 | - | |
76 | - def _log_path(self, branch: str) -> Path: | |
77 | - return self._branch_dir(branch) / "log.md" | |
78 | - | |
79 | - def _commit_path(self, branch: str) -> Path: | |
80 | - return self._branch_dir(branch) / "commit.md" | |
81 | - | |
82 | - def _meta_path(self, branch: str) -> Path: | |
83 | - return self._branch_dir(branch) / "metadata.yaml" | |
84 | - | |
85 | - def _main_md(self) -> Path: | |
86 | - return self.gcc_dir / "main.md" | |
87 | - | |
88 | - def _read(self, path: Path) -> str: | |
89 | - if path.exists(): | |
90 | - return path.read_text(encoding="utf-8") | |
91 | - return "" | |
92 | - | |
93 | - def _append(self, path: Path, text: str) -> None: | |
94 | - with open(path, "a", encoding="utf-8") as f: | |
95 | - f.write(text) | |
96 | - | |
97 | - def _write(self, path: Path, text: str) -> None: | |
98 | - path.parent.mkdir(parents=True, exist_ok=True) | |
99 | - path.write_text(text, encoding="utf-8") | |
100 | - | |
101 | - def _read_commits(self, branch: str) -> list[CommitRecord]: | |
102 | - """Parse commit.md into a list of CommitRecord objects.""" | |
103 | - text = self._read(self._commit_path(branch)) | |
104 | - records: list[CommitRecord] = [] | |
105 | - if not text: | |
106 | - return records | |
107 | - for block in text.split("---\n"): | |
108 | - block = block.strip() | |
109 | - if not block: | |
110 | - continue | |
111 | - lines = block.splitlines() | |
112 | - commit_id = branch_purpose = prev_summary = contribution = ts = "" | |
113 | - for line in lines: | |
114 | - if line.startswith("## Commit"): | |
115 | - commit_id = line.split("`")[1] if "`" in line else "" | |
116 | - elif line.startswith("**Timestamp:**"): | |
117 | - ts = line.split("**Timestamp:**")[-1].strip() | |
118 | - elif line.startswith("**Branch Purpose:**"): | |
119 | - branch_purpose = line.split("**Branch Purpose:**")[-1].strip() | |
120 | - elif line.startswith("**Previous Progress Summary:**"): | |
121 | - prev_summary = line.split("**Previous Progress Summary:**")[ | |
122 | - -1 | |
123 | - ].strip() | |
124 | - elif line.startswith("**This Commit's Contribution:**"): | |
125 | - contribution = line.split("**This Commit's Contribution:**")[ | |
126 | - -1 | |
127 | - ].strip() | |
128 | - | |
129 | - if commit_id: | |
130 | - records.append( | |
131 | - CommitRecord( | |
132 | - commit_id=commit_id, | |
133 | - branch_name=branch, | |
134 | - branch_purpose=branch_purpose, | |
135 | - previous_progress_summary=prev_summary, | |
136 | - this_commit_contribution=contribution, | |
137 | - timestamp=ts, | |
138 | - ) | |
139 | - ) | |
140 | - | |
141 | - return records | |
142 | - | |
143 | - def _read_ota(self, branch: str) -> list[OTARecord]: | |
144 | - text = self._read(self._log_path(branch)) | |
145 | - records: list[OTARecord] = [] | |
146 | - if not text: | |
147 | - return records | |
148 | - for block in text.split("--------\n"): | |
149 | - block = block.strip() | |
150 | - if not block: | |
151 | - continue | |
152 | - lines = block.splitlines() | |
153 | - step = 0 | |
154 | - ts = obs = thought = action = "" | |
155 | - for line in lines: | |
156 | - if line.startswith("### Step"): | |
157 | - header = line.replace("### Step", "").strip() | |
158 | - parts = header.split("-", 1) | |
159 | - try: | |
160 | - step = int(parts[0].strip()) | |
161 | - except ValueError: | |
162 | - step = 0 | |
163 | - ts = parts[1].strip() if len(parts) > 1 else "" | |
164 | - elif line.startswith("**Observation:**"): | |
165 | - obs = line.split("**Observation:**")[-1].strip() | |
166 | - elif line.startswith("**Thought:**"): | |
167 | - thought = line.split("**Thought:**")[-1].strip() | |
168 | - elif line.startswith("**Action:**"): | |
169 | - action = line.split("**Action:**")[-1].strip() | |
170 | - if obs or thought or action: | |
171 | - records.append(OTARecord(ts, obs, thought, action, step)) | |
172 | - | |
173 | - return records | |
174 | - | |
175 | - def init(self, project_roadmap: str = "") -> None: | |
176 | - """ | |
177 | - Initialise a new GCC workspace. Creates .GCC/ structure with a | |
178 | - 'main' branch, main.md roadmap, and initial metadata. | |
179 | - """ | |
180 | - if self.gcc_dir.exists(): | |
181 | - raise FileExistsError(f"GCC workspace already exists at {self.gcc_dir}") | |
182 | - main_branch_dir = self._branch_dir(MAIN_BRANCH) | |
183 | - main_branch_dir.mkdir(parents=True) | |
184 | - roadmap_content = ( | |
185 | - f"# Project Roadmap\n\n**Initialized:** {_now()}\n\n{project_roadmap}\n" | |
186 | - ) | |
187 | - self._write(self._main_md(), roadmap_content) | |
188 | - self._write( | |
189 | - self._log_path(MAIN_BRANCH), f"# OTA Log — branch `{MAIN_BRANCH}`\n\n" | |
190 | - ) | |
191 | - self._write( | |
192 | - self._commit_path(MAIN_BRANCH), | |
193 | - f"# Commit History — branch `{MAIN_BRANCH}`\n\n", | |
194 | - ) | |
195 | - | |
196 | - meta = BranchMetadata( | |
197 | - name=MAIN_BRANCH, | |
198 | - purpose="Primary reasoning trajectory", | |
199 | - created_from="", | |
200 | - created_at=_now(), | |
201 | - status="active", | |
202 | - ) | |
203 | - self._write(self._meta_path(MAIN_BRANCH), meta.to_yaml()) | |
204 | - | |
205 | - self._current_branch = MAIN_BRANCH | |
206 | - | |
207 | - def load(self) -> None: | |
208 | - """Load an existing GCC workspace (no-op if already in memory).""" | |
209 | - if not self.gcc_dir.exists(): | |
210 | - raise FileNotFoundError(f"No GCC workspace found at {self.gcc_dir}") | |
211 | - # Determine current branch from active metadata | |
212 | - self._current_branch = MAIN_BRANCH | |
213 | - | |
214 | - def log_ota(self, observation: str, thought: str, action: str) -> OTARecord: | |
215 | - """ | |
216 | - Append an OTA step to the current branch's log.md. | |
217 | - The paper logs continuous OTA cycles: Observation-Thought-Action. | |
218 | - """ | |
219 | - existing = self._read_ota(self._current_branch) | |
220 | - step = len(existing) + 1 | |
221 | - record = OTARecord( | |
222 | - timestamp=_now(), | |
223 | - observation=observation, | |
224 | - thought=thought, | |
225 | - action=action, | |
226 | - step=step, | |
227 | - ) | |
228 | - self._append(self._log_path(self._current_branch), record.to_markdown()) | |
229 | - return record | |
230 | - | |
231 | - def commit( | |
232 | - self, | |
233 | - contribution: str, | |
234 | - previous_summary: Optional[str] = None, | |
235 | - update_roadmap: Optional[str] = None, | |
236 | - ) -> CommitRecord: | |
237 | - """ | |
238 | - COMMIT command (paper S3.2). | |
239 | - | |
240 | - Persists a milestone checkpoint to commit.md. Fields per paper: | |
241 | - - Branch Purpose (from metadata) | |
242 | - - Previous Progress Summary | |
243 | - - This Commit's Contribution | |
244 | - | |
245 | - Optionally updates main.md global roadmap. | |
246 | - """ | |
247 | - meta_text = self._read(self._meta_path(self._current_branch)) | |
248 | - meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
249 | - branch_purpose = meta.purpose if meta else "" | |
250 | - | |
251 | - # Auto-summarise previous progress from last commit if not provided | |
252 | - if previous_summary is None: | |
253 | - commits = self._read_commits(self._current_branch) | |
254 | - if commits: | |
255 | - previous_summary = commits[-1].this_commit_contribution | |
256 | - else: | |
257 | - previous_summary = "Initial state — no prior commits." | |
258 | - | |
259 | - record = CommitRecord( | |
260 | - commit_id=_short_id(), | |
261 | - branch_name=self._current_branch, | |
262 | - branch_purpose=branch_purpose, | |
263 | - previous_progress_summary=previous_summary, | |
264 | - this_commit_contribution=contribution, | |
265 | - timestamp=_now(), | |
266 | - ) | |
267 | - self._append(self._commit_path(self._current_branch), record.to_markdown()) | |
268 | - | |
269 | - # Optionally update the global roadmap in main.md | |
270 | - if update_roadmap: | |
271 | - self._append( | |
272 | - self._main_md(), | |
273 | - f"\n## Update ({record.timestamp})\n{update_roadmap}\n", | |
274 | - ) | |
275 | - | |
276 | - return record | |
277 | - | |
278 | - def branch(self, name: str, purpose: str) -> "GCCWorkspace": | |
279 | - """ | |
280 | - BRANCH command (paper S3.3). | |
281 | - | |
282 | - Creates an isolated workspace for alternative plans or experimental | |
283 | - reasoning. Mathematically: B_t^(name) = BRANCH(M_{t-1}). | |
284 | - | |
285 | - Initialises empty OTA trace (log.md) and commit.md for the branch, | |
286 | - carrying forward the parent's last committed state as starting context. | |
287 | - """ | |
288 | - branch_dir = self._branch_dir(name) | |
289 | - if branch_dir.exists(): | |
290 | - raise FileExistsError(f"Branch '{name}' already exists.") | |
291 | - | |
292 | - branch_dir.mkdir(parents=True) | |
293 | - | |
294 | - # Initialise empty OTA log (fresh execution trace) | |
295 | - self._write(self._log_path(name), f"# OTA Log — branch `{name}`\n\n") | |
296 | - self._write(self._commit_path(name), f"# Commit History — branch `{name}`\n\n") | |
297 | - | |
298 | - # metadata.yaml records intent and motivation (paper S3.3) | |
299 | - meta = BranchMetadata( | |
300 | - name=name, | |
301 | - purpose=purpose, | |
302 | - created_from=self._current_branch, | |
303 | - created_at=_now(), | |
304 | - status="active", | |
305 | - ) | |
306 | - self._write(self._meta_path(name), meta.to_yaml()) | |
307 | - | |
308 | - # Switch current branch | |
309 | - self._current_branch = name | |
310 | - return self | |
311 | - | |
312 | - def merge( | |
313 | - self, | |
314 | - branch_name: str, | |
315 | - summary: Optional[str] = None, | |
316 | - target: str = MAIN_BRANCH, | |
317 | - ) -> CommitRecord: | |
318 | - """ | |
319 | - MERGE command (paper S3.4). | |
320 | - | |
321 | - Integrates a completed branch back into the target (default: main), | |
322 | - merging summaries and execution traces to produce a unified, | |
323 | - consistent state. Creates a merge commit on the target branch. | |
324 | - """ | |
325 | - # Gather branch commits and synthesise | |
326 | - branch_commits = self._read_commits(branch_name) | |
327 | - branch_ota = self._read_ota(branch_name) | |
328 | - meta_text = self._read(self._meta_path(branch_name)) | |
329 | - meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
330 | - | |
331 | - if not summary: | |
332 | - contributions = " | ".join( | |
333 | - c.this_commit_contribution for c in branch_commits | |
334 | - ) | |
335 | - summary = f"Merged branch `{branch_name}` ({len(branch_commits)} commits). Contributions: {contributions}" | |
336 | - | |
337 | - # Append branch OTA to target log (merge trace) | |
338 | - if branch_ota: | |
339 | - merge_header = f"\n## Merged from `{branch_name}` ({_now()})\n\n" | |
340 | - self._append(self._log_path(target), merge_header) | |
341 | - for rec in branch_ota: | |
342 | - self._append(self._log_path(target), rec.to_markdown()) | |
343 | - | |
344 | - # Create merge commit on target | |
345 | - self._current_branch = target | |
346 | - merge_commit = self.commit( | |
347 | - contribution=summary, | |
348 | - previous_summary=f"Merging branch `{branch_name}` with purpose: {meta.purpose if meta else ''}", | |
349 | - update_roadmap=f"Merged `{branch_name}`: {summary}", | |
350 | - ) | |
351 | - | |
352 | - # Mark branch as merged in metadata | |
353 | - if meta: | |
354 | - meta.status = "merged" | |
355 | - meta.merged_into = target | |
356 | - meta.merged_at = _now() | |
357 | - self._write(self._meta_path(branch_name), meta.to_yaml()) | |
358 | - | |
359 | - return merge_commit | |
360 | - | |
361 | - def context(self, branch: Optional[str] = None, k: int = 1) -> ContextResult: | |
362 | - """ | |
363 | - CONTEXT command (paper S3.5). | |
364 | - | |
365 | - Retrieves historical context at varying resolutions. The paper fixes | |
366 | - K=1 in experiments (only the most recent commit record is revealed). | |
367 | - | |
368 | - Parameters | |
369 | - ---------- | |
370 | - branch : branch to retrieve context from (default: current branch) | |
371 | - k : number of recent commit records to surface (paper default K=1) | |
372 | - """ | |
373 | - target = branch or self._current_branch | |
374 | - commits = self._read_commits(target) | |
375 | - ota = self._read_ota(target) | |
376 | - roadmap = self._read(self._main_md()) | |
377 | - meta_text = self._read(self._meta_path(target)) | |
378 | - meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
379 | - | |
380 | - return ContextResult( | |
381 | - branch_name=target, | |
382 | - k=k, | |
383 | - commits=commits[-k:] if commits else [], | |
384 | - ota_records=ota, | |
385 | - main_roadmap=roadmap, | |
386 | - metadata=meta, | |
387 | - ) | |
388 | - | |
389 | - @property | |
390 | - def current_branch(self) -> str: | |
391 | - return self._current_branch | |
392 | - | |
393 | - def switch_branch(self, name: str) -> None: | |
394 | - """Switch the active branch without creating a new one.""" | |
395 | - if not self._branch_dir(name).exists(): | |
396 | - raise FileNotFoundError(f"Branch '{name}' does not exist.") | |
397 | - self._current_branch = name | |
398 | - | |
399 | - def list_branches(self) -> list[str]: | |
400 | - branches_root = self.gcc_dir / "branches" | |
401 | - if not branches_root.exists(): | |
402 | - return [] | |
403 | - return [d.name for d in branches_root.iterdir() if d.is_dir()] | |
404 | - | |
405 | - def update_roadmap(self, content: str) -> None: | |
406 | - """Overwrite or append to main.md global roadmap.""" | |
407 | - self._append(self._main_md(), f"\n{content}\n") | |
408 | 0 | |
▼PYTHON/contexa/.gitignore → PYTHON/cortexa/.gitignore
+0-0
0 | 0 |
▼PYTHON/contexa/.python-version → PYTHON/cortexa/.python-version
+0-0
0 | 0 |
▼PYTHON/contexa/README.md → PYTHON/cortexa/README.md
+0-0
0 | 0 |
▼PYTHON/contexa/__init__.py → PYTHON/cortexa/__init__.py
+1-1
| @@ -1,5 +1,5 @@ | ||
1 | 1 | """ |
2 | -gcc-context — Python implementation of Git-Context-Controller (GCC) | |
2 | +cortexa — Python implementation of Git-Context-Controller (GCC) | |
3 | 3 | |
4 | 4 | Paper: "Git Context Controller: Manage the Context of LLM-based Agents like Git" |
5 | 5 | arXiv:2508.00031v2, Junde Wu et al., 2025 |
6 | 6 | |
▼PYTHON/contexa/main.py → PYTHON/cortexa/main.py
+1-1
| @@ -1,5 +1,5 @@ | ||
1 | 1 | def main(): |
2 | - print("Hello from contexa!") | |
2 | + print("Hello from cortexa!") | |
3 | 3 | |
4 | 4 | |
5 | 5 | if __name__ == "__main__": |
6 | 6 | |
▼PYTHON/contexa/models.py → PYTHON/cortexa/models.py
+46-8
| @@ -9,12 +9,50 @@ four core commands: COMMIT, BRANCH, MERGE, and CONTEXT. | ||
9 | 9 | |
10 | 10 | """ |
11 | 11 | |
12 | +from __future__ import annotations | |
13 | + | |
12 | 14 | from dataclasses import dataclass, field |
13 | 15 | from datetime import datetime |
14 | 16 | from typing import Dict, Optional |
15 | 17 | import yaml |
16 | 18 | |
17 | 19 | |
20 | +def _sanitize(text: str) -> str: | |
21 | + """Escape separator sequences in user-provided content. | |
22 | + | |
23 | + Lines that are exactly ``---`` (the markdown/OTA block separator) | |
24 | + would break all parsers that split on ``---\\n``. We prepend a | |
25 | + backslash so the separator is preserved in the stored markdown | |
26 | + without confusing the splitter. | |
27 | + """ | |
28 | + return text.replace("\n---\n", "\n\\---\n") | |
29 | + | |
30 | + | |
31 | +def _desanitize(text: str) -> str: | |
32 | + """Reverse the escaping applied by :func:`_sanitize`.""" | |
33 | + return text.replace("\n\\---\n", "\n---\n") | |
34 | + | |
35 | + | |
36 | +def _split_blocks(text: str) -> list[str]: | |
37 | + """Split markdown text on ``---\\n`` while respecting escaped separators. | |
38 | + | |
39 | + After naive splitting on ``---\\n``, blocks whose predecessor ends | |
40 | + with ``\\`` are rejoined (the backslash means ``\\---\\n`` was an | |
41 | + escaped separator produced by :func:`_sanitize`). | |
42 | + """ | |
43 | + raw = text.split("---\n") | |
44 | + blocks: list[str] = [] | |
45 | + i = 0 | |
46 | + while i < len(raw): | |
47 | + block = raw[i] | |
48 | + while block.endswith("\\") and i + 1 < len(raw): | |
49 | + i += 1 | |
50 | + block = block + "---\n" + raw[i] | |
51 | + blocks.append(block) | |
52 | + i += 1 | |
53 | + return blocks | |
54 | + | |
55 | + | |
18 | 56 | @dataclass |
19 | 57 | class OTARecord: |
20 | 58 | """ |
| @@ -30,11 +68,11 @@ class OTARecord: | ||
30 | 68 | |
31 | 69 | def to_markdown(self) -> str: |
32 | 70 | return ( |
33 | - f"### Step {self.step}-{self.timestamp}\n" | |
34 | - f"**Observation:** {self.observation}\n\n" | |
35 | - f"**Thought:** {self.thought}\n\n" | |
36 | - f"**Action:** {self.action}\n\n" | |
37 | - "--------\n" | |
71 | + f"### Step {self.step} — {self.timestamp}\n" | |
72 | + f"**Observation:** {_sanitize(self.observation)}\n\n" | |
73 | + f"**Thought:** {_sanitize(self.thought)}\n\n" | |
74 | + f"**Action:** {_sanitize(self.action)}\n\n" | |
75 | + "---\n" | |
38 | 76 | ) |
39 | 77 | |
40 | 78 | @staticmethod |
| @@ -61,9 +99,9 @@ class CommitRecord: | ||
61 | 99 | return ( |
62 | 100 | f"## Commit `{self.commit_id}`\n" |
63 | 101 | f"**Timestamp:** {self.timestamp}\n\n" |
64 | - f"**Branch Purpose:** {self.branch_purpose}\n\n" | |
65 | - f"**Previous Progress Summary:** {self.previous_progress_summary}\n\n" | |
66 | - f"**This Commit's Contribution:** {self.this_commit_contribution}\n\n" | |
102 | + f"**Branch Purpose:** {_sanitize(self.branch_purpose)}\n\n" | |
103 | + f"**Previous Progress Summary:** {_sanitize(self.previous_progress_summary)}\n\n" | |
104 | + f"**This Commit's Contribution:** {_sanitize(self.this_commit_contribution)}\n\n" | |
67 | 105 | "---\n" |
68 | 106 | ) |
69 | 107 | |
70 | 108 | |
▼PYTHON/cortexa/workspace.py
+521-0
| @@ -0,0 +1,521 @@ | ||
1 | +""" | |
2 | +GCC Workspace — core file-system layer. | |
3 | +Based on arXiv:2508.00031v2. | |
4 | + | |
5 | +The paper specifies a .GCC/ root directory containing: | |
6 | + main.md — global roadmap / planning artifact | |
7 | + branches/ | |
8 | + <branch_name>/ | |
9 | + log.md — continuous OTA (Observation-Thought-Action) trace | |
10 | + commit.md — milestone-level commit summaries | |
11 | + metadata.yaml — branch intent, status, creation info | |
12 | + | |
13 | +All four commands (COMMIT, BRANCH, MERGE, CONTEXT) operate on this structure. | |
14 | +""" | |
15 | + | |
16 | +from __future__ import annotations | |
17 | + | |
18 | +import fcntl | |
19 | +import os | |
20 | +import uuid | |
21 | +from contextlib import contextmanager | |
22 | +from datetime import datetime, timezone | |
23 | +from pathlib import Path | |
24 | +from typing import Iterator, Optional | |
25 | + | |
26 | +from .models import ( | |
27 | + BranchMetadata, | |
28 | + CommitRecord, | |
29 | + ContextResult, | |
30 | + OTARecord, | |
31 | + _desanitize, | |
32 | + _split_blocks, | |
33 | +) | |
34 | + | |
35 | + | |
36 | +MAIN_BRANCH = "main" | |
37 | +GCC_DIR = ".GCC" | |
38 | + | |
39 | + | |
40 | +def _validate_not_empty(value: str, field: str) -> None: | |
41 | + """Raise ValueError if *value* is empty or whitespace-only.""" | |
42 | + if not value or not value.strip(): | |
43 | + raise ValueError(f"{field} must not be empty.") | |
44 | + | |
45 | + | |
46 | +def _validate_branch_name(name: str) -> None: | |
47 | + """Raise ValueError if *name* is not a valid branch identifier.""" | |
48 | + _validate_not_empty(name, "Branch name") | |
49 | + if "/" in name or "\\" in name: | |
50 | + raise ValueError(f"Branch name must not contain path separators: {name!r}") | |
51 | + if name in (".", ".."): | |
52 | + raise ValueError(f"Branch name must not be '.' or '..': {name!r}") | |
53 | + | |
54 | + | |
55 | +def _now() -> str: | |
56 | + return datetime.now(timezone.utc).isoformat() | |
57 | + | |
58 | + | |
59 | +def _short_id() -> str: | |
60 | + return str(uuid.uuid4())[:8] | |
61 | + | |
62 | + | |
63 | +class GCCWorkspace: | |
64 | + """ | |
65 | + Manages the .GCC/ directory structure for one agent project. | |
66 | + | |
67 | + Usage | |
68 | + ----- | |
69 | + ws = GCCWorkspace("/path/to/project") | |
70 | + ws.init("Build a REST API service") | |
71 | + | |
72 | + # agent logs OTA steps | |
73 | + ws.log_ota("file list returned", "need to read main.py", "read_file main.py") | |
74 | + | |
75 | + # agent commits a milestone | |
76 | + ws.commit("Implement GET /users endpoint") | |
77 | + | |
78 | + # agent branches to explore alternative | |
79 | + ws.branch("auth-jwt", "Explore JWT-based auth vs session auth") | |
80 | + | |
81 | + # agent merges branch back | |
82 | + ws.merge("auth-jwt") | |
83 | + | |
84 | + # retrieve context (K=1 by default, per paper experiments) | |
85 | + ctx = ws.context(k=1) | |
86 | + print(ctx.summary()) | |
87 | + """ | |
88 | + | |
89 | + def __init__(self, project_root: str): | |
90 | + self.root = Path(project_root) | |
91 | + self.gcc_dir = self.root / GCC_DIR | |
92 | + self._current_branch: str = MAIN_BRANCH | |
93 | + | |
94 | + # ------------------------------------------------------------------ # | |
95 | + # File locking # | |
96 | + # ------------------------------------------------------------------ # | |
97 | + | |
98 | + @contextmanager | |
99 | + def _lock(self) -> Iterator[None]: | |
100 | + """Acquire an exclusive file lock on .GCC/.lock for concurrency safety. | |
101 | + | |
102 | + Uses POSIX ``fcntl.flock`` so multiple processes (or threads) | |
103 | + operating on the same workspace are serialised. The lock is | |
104 | + automatically released when the ``with`` block exits. | |
105 | + """ | |
106 | + lock_path = self.gcc_dir / ".lock" | |
107 | + lock_path.parent.mkdir(parents=True, exist_ok=True) | |
108 | + fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR) | |
109 | + try: | |
110 | + fcntl.flock(fd, fcntl.LOCK_EX) | |
111 | + yield | |
112 | + finally: | |
113 | + fcntl.flock(fd, fcntl.LOCK_UN) | |
114 | + os.close(fd) | |
115 | + | |
116 | + def _branch_dir(self, branch: str) -> Path: | |
117 | + return self.gcc_dir / "branches" / branch | |
118 | + | |
119 | + def _log_path(self, branch: str) -> Path: | |
120 | + return self._branch_dir(branch) / "log.md" | |
121 | + | |
122 | + def _commit_path(self, branch: str) -> Path: | |
123 | + return self._branch_dir(branch) / "commit.md" | |
124 | + | |
125 | + def _meta_path(self, branch: str) -> Path: | |
126 | + return self._branch_dir(branch) / "metadata.yaml" | |
127 | + | |
128 | + def _main_md(self) -> Path: | |
129 | + return self.gcc_dir / "main.md" | |
130 | + | |
131 | + def _read(self, path: Path) -> str: | |
132 | + if path.exists(): | |
133 | + return path.read_text(encoding="utf-8") | |
134 | + return "" | |
135 | + | |
136 | + def _append(self, path: Path, text: str) -> None: | |
137 | + with open(path, "a", encoding="utf-8") as f: | |
138 | + f.write(text) | |
139 | + | |
140 | + def _write(self, path: Path, text: str) -> None: | |
141 | + path.parent.mkdir(parents=True, exist_ok=True) | |
142 | + path.write_text(text, encoding="utf-8") | |
143 | + | |
144 | + def _read_commits(self, branch: str) -> list[CommitRecord]: | |
145 | + """Parse commit.md into a list of CommitRecord objects.""" | |
146 | + text = self._read(self._commit_path(branch)) | |
147 | + records: list[CommitRecord] = [] | |
148 | + if not text: | |
149 | + return records | |
150 | + for block in _split_blocks(text): | |
151 | + block = block.strip() | |
152 | + if not block: | |
153 | + continue | |
154 | + lines = block.splitlines() | |
155 | + commit_id = ts = "" | |
156 | + fields: dict[str, list[str]] = {} | |
157 | + current: str | None = None | |
158 | + for line in lines: | |
159 | + if line.startswith("## Commit"): | |
160 | + commit_id = line.split("`")[1] if "`" in line else "" | |
161 | + current = None | |
162 | + elif line.startswith("**Timestamp:**"): | |
163 | + ts = line.split("**Timestamp:**")[-1].strip() | |
164 | + current = None | |
165 | + elif line.startswith("**Branch Purpose:**"): | |
166 | + val = line.split("**Branch Purpose:**")[-1].strip() | |
167 | + fields["branch_purpose"] = [val] | |
168 | + current = "branch_purpose" | |
169 | + elif line.startswith("**Previous Progress Summary:**"): | |
170 | + val = line.split("**Previous Progress Summary:**")[-1].strip() | |
171 | + fields["prev_summary"] = [val] | |
172 | + current = "prev_summary" | |
173 | + elif line.startswith("**This Commit's Contribution:**"): | |
174 | + val = line.split("**This Commit's Contribution:**")[-1].strip() | |
175 | + fields["contribution"] = [val] | |
176 | + current = "contribution" | |
177 | + elif current is not None and line.strip(): | |
178 | + fields[current].append(line.strip()) | |
179 | + | |
180 | + if commit_id: | |
181 | + | |
182 | + def _get(key: str) -> str: | |
183 | + return _desanitize("\n".join(fields.get(key, [""])).strip()) | |
184 | + | |
185 | + records.append( | |
186 | + CommitRecord( | |
187 | + commit_id=commit_id, | |
188 | + branch_name=branch, | |
189 | + branch_purpose=_get("branch_purpose"), | |
190 | + previous_progress_summary=_get("prev_summary"), | |
191 | + this_commit_contribution=_get("contribution"), | |
192 | + timestamp=ts, | |
193 | + ) | |
194 | + ) | |
195 | + | |
196 | + return records | |
197 | + | |
198 | + def _read_ota(self, branch: str) -> list[OTARecord]: | |
199 | + text = self._read(self._log_path(branch)) | |
200 | + records: list[OTARecord] = [] | |
201 | + if not text: | |
202 | + return records | |
203 | + for block in _split_blocks(text): | |
204 | + block = block.strip() | |
205 | + if not block: | |
206 | + continue | |
207 | + lines = block.splitlines() | |
208 | + step = 0 | |
209 | + ts = "" | |
210 | + fields: dict[str, list[str]] = {} | |
211 | + current: str | None = None | |
212 | + for line in lines: | |
213 | + if line.startswith("### Step"): | |
214 | + header = line.replace("### Step", "").strip() | |
215 | + # Support both " — " (standard) and "-" (legacy) separators | |
216 | + if " — " in header: | |
217 | + parts = header.split(" — ", 1) | |
218 | + else: | |
219 | + parts = header.split("-", 1) | |
220 | + try: | |
221 | + step = int(parts[0].strip()) | |
222 | + except ValueError: | |
223 | + step = 0 | |
224 | + ts = parts[1].strip() if len(parts) > 1 else "" | |
225 | + current = None | |
226 | + elif line.startswith("**Observation:**"): | |
227 | + val = line.split("**Observation:**")[-1].strip() | |
228 | + fields["obs"] = [val] | |
229 | + current = "obs" | |
230 | + elif line.startswith("**Thought:**"): | |
231 | + val = line.split("**Thought:**")[-1].strip() | |
232 | + fields["thought"] = [val] | |
233 | + current = "thought" | |
234 | + elif line.startswith("**Action:**"): | |
235 | + val = line.split("**Action:**")[-1].strip() | |
236 | + fields["action"] = [val] | |
237 | + current = "action" | |
238 | + elif current is not None and line.strip(): | |
239 | + fields[current].append(line.strip()) | |
240 | + | |
241 | + def _get(key: str) -> str: | |
242 | + return _desanitize("\n".join(fields.get(key, [""])).strip()) | |
243 | + | |
244 | + obs = _get("obs") | |
245 | + thought = _get("thought") | |
246 | + action = _get("action") | |
247 | + if obs or thought or action: | |
248 | + records.append( | |
249 | + OTARecord( | |
250 | + ts, | |
251 | + obs, | |
252 | + thought, | |
253 | + action, | |
254 | + step, | |
255 | + ) | |
256 | + ) | |
257 | + | |
258 | + return records | |
259 | + | |
260 | + def init(self, project_roadmap: str = "") -> None: | |
261 | + """ | |
262 | + Initialise a new GCC workspace. Creates .GCC/ structure with a | |
263 | + 'main' branch, main.md roadmap, and initial metadata. | |
264 | + """ | |
265 | + if self.gcc_dir.exists(): | |
266 | + raise FileExistsError(f"GCC workspace already exists at {self.gcc_dir}") | |
267 | + main_branch_dir = self._branch_dir(MAIN_BRANCH) | |
268 | + main_branch_dir.mkdir(parents=True) | |
269 | + | |
270 | + with self._lock(): | |
271 | + roadmap_content = ( | |
272 | + f"# Project Roadmap\n\n**Initialized:** {_now()}\n\n{project_roadmap}\n" | |
273 | + ) | |
274 | + self._write(self._main_md(), roadmap_content) | |
275 | + self._write( | |
276 | + self._log_path(MAIN_BRANCH), f"# OTA Log — branch `{MAIN_BRANCH}`\n\n" | |
277 | + ) | |
278 | + self._write( | |
279 | + self._commit_path(MAIN_BRANCH), | |
280 | + f"# Commit History — branch `{MAIN_BRANCH}`\n\n", | |
281 | + ) | |
282 | + | |
283 | + meta = BranchMetadata( | |
284 | + name=MAIN_BRANCH, | |
285 | + purpose="Primary reasoning trajectory", | |
286 | + created_from="", | |
287 | + created_at=_now(), | |
288 | + status="active", | |
289 | + ) | |
290 | + self._write(self._meta_path(MAIN_BRANCH), meta.to_yaml()) | |
291 | + | |
292 | + self._current_branch = MAIN_BRANCH | |
293 | + | |
294 | + def load(self) -> None: | |
295 | + """Load an existing GCC workspace (no-op if already in memory).""" | |
296 | + if not self.gcc_dir.exists(): | |
297 | + raise FileNotFoundError(f"No GCC workspace found at {self.gcc_dir}") | |
298 | + # Determine current branch from active metadata | |
299 | + self._current_branch = MAIN_BRANCH | |
300 | + | |
301 | + def log_ota(self, observation: str, thought: str, action: str) -> OTARecord: | |
302 | + """ | |
303 | + Append an OTA step to the current branch's log.md. | |
304 | + The paper logs continuous OTA cycles: Observation-Thought-Action. | |
305 | + """ | |
306 | + if ( | |
307 | + not (observation and observation.strip()) | |
308 | + and not (thought and thought.strip()) | |
309 | + and not (action and action.strip()) | |
310 | + ): | |
311 | + raise ValueError( | |
312 | + "At least one of observation, thought, or action must be non-empty." | |
313 | + ) | |
314 | + with self._lock(): | |
315 | + existing = self._read_ota(self._current_branch) | |
316 | + step = len(existing) + 1 | |
317 | + record = OTARecord( | |
318 | + timestamp=_now(), | |
319 | + observation=observation, | |
320 | + thought=thought, | |
321 | + action=action, | |
322 | + step=step, | |
323 | + ) | |
324 | + self._append(self._log_path(self._current_branch), record.to_markdown()) | |
325 | + return record | |
326 | + | |
327 | + def commit( | |
328 | + self, | |
329 | + contribution: str, | |
330 | + previous_summary: Optional[str] = None, | |
331 | + update_roadmap: Optional[str] = None, | |
332 | + ) -> CommitRecord: | |
333 | + """ | |
334 | + COMMIT command (paper S3.2). | |
335 | + | |
336 | + Persists a milestone checkpoint to commit.md. Fields per paper: | |
337 | + - Branch Purpose (from metadata) | |
338 | + - Previous Progress Summary | |
339 | + - This Commit's Contribution | |
340 | + | |
341 | + Optionally updates main.md global roadmap. | |
342 | + """ | |
343 | + _validate_not_empty(contribution, "Contribution") | |
344 | + with self._lock(): | |
345 | + meta_text = self._read(self._meta_path(self._current_branch)) | |
346 | + meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
347 | + branch_purpose = meta.purpose if meta else "" | |
348 | + | |
349 | + # Auto-summarise previous progress from last commit if not provided | |
350 | + if previous_summary is None: | |
351 | + commits = self._read_commits(self._current_branch) | |
352 | + if commits: | |
353 | + previous_summary = commits[-1].this_commit_contribution | |
354 | + else: | |
355 | + previous_summary = "Initial state — no prior commits." | |
356 | + | |
357 | + record = CommitRecord( | |
358 | + commit_id=_short_id(), | |
359 | + branch_name=self._current_branch, | |
360 | + branch_purpose=branch_purpose, | |
361 | + previous_progress_summary=previous_summary, | |
362 | + this_commit_contribution=contribution, | |
363 | + timestamp=_now(), | |
364 | + ) | |
365 | + self._append(self._commit_path(self._current_branch), record.to_markdown()) | |
366 | + | |
367 | + # Optionally update the global roadmap in main.md | |
368 | + if update_roadmap: | |
369 | + self._append( | |
370 | + self._main_md(), | |
371 | + f"\n## Update ({record.timestamp})\n{update_roadmap}\n", | |
372 | + ) | |
373 | + | |
374 | + return record | |
375 | + | |
376 | + def branch(self, name: str, purpose: str) -> "GCCWorkspace": | |
377 | + """ | |
378 | + BRANCH command (paper S3.3). | |
379 | + | |
380 | + Creates an isolated workspace for alternative plans or experimental | |
381 | + reasoning. Mathematically: B_t^(name) = BRANCH(M_{t-1}). | |
382 | + | |
383 | + Initialises empty OTA trace (log.md) and commit.md for the branch, | |
384 | + carrying forward the parent's last committed state as starting context. | |
385 | + """ | |
386 | + _validate_branch_name(name) | |
387 | + _validate_not_empty(purpose, "Branch purpose") | |
388 | + branch_dir = self._branch_dir(name) | |
389 | + if branch_dir.exists(): | |
390 | + raise FileExistsError(f"Branch '{name}' already exists.") | |
391 | + | |
392 | + branch_dir.mkdir(parents=True) | |
393 | + | |
394 | + with self._lock(): | |
395 | + # Initialise empty OTA log (fresh execution trace) | |
396 | + self._write(self._log_path(name), f"# OTA Log — branch `{name}`\n\n") | |
397 | + self._write( | |
398 | + self._commit_path(name), f"# Commit History — branch `{name}`\n\n" | |
399 | + ) | |
400 | + | |
401 | + # metadata.yaml records intent and motivation (paper S3.3) | |
402 | + meta = BranchMetadata( | |
403 | + name=name, | |
404 | + purpose=purpose, | |
405 | + created_from=self._current_branch, | |
406 | + created_at=_now(), | |
407 | + status="active", | |
408 | + ) | |
409 | + self._write(self._meta_path(name), meta.to_yaml()) | |
410 | + | |
411 | + # Switch current branch | |
412 | + self._current_branch = name | |
413 | + return self | |
414 | + | |
415 | + def merge( | |
416 | + self, | |
417 | + branch_name: str, | |
418 | + summary: Optional[str] = None, | |
419 | + target: str = MAIN_BRANCH, | |
420 | + ) -> CommitRecord: | |
421 | + """ | |
422 | + MERGE command (paper S3.4). | |
423 | + | |
424 | + Integrates a completed branch back into the target (default: main), | |
425 | + merging summaries and execution traces to produce a unified, | |
426 | + consistent state. Creates a merge commit on the target branch. | |
427 | + """ | |
428 | + _validate_branch_name(branch_name) | |
429 | + _validate_branch_name(target) | |
430 | + with self._lock(): | |
431 | + # Gather branch commits and synthesise | |
432 | + branch_commits = self._read_commits(branch_name) | |
433 | + branch_ota = self._read_ota(branch_name) | |
434 | + meta_text = self._read(self._meta_path(branch_name)) | |
435 | + meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
436 | + | |
437 | + if not summary: | |
438 | + contributions = " | ".join( | |
439 | + c.this_commit_contribution for c in branch_commits | |
440 | + ) | |
441 | + summary = f"Merged branch `{branch_name}` ({len(branch_commits)} commits). Contributions: {contributions}" | |
442 | + | |
443 | + # Append branch OTA to target log (merge trace) | |
444 | + if branch_ota: | |
445 | + merge_header = f"\n## Merged from `{branch_name}` ({_now()})\n\n" | |
446 | + self._append(self._log_path(target), merge_header) | |
447 | + for rec in branch_ota: | |
448 | + self._append(self._log_path(target), rec.to_markdown()) | |
449 | + | |
450 | + # Create merge commit on target | |
451 | + self._current_branch = target | |
452 | + | |
453 | + # commit() acquires the lock internally | |
454 | + merge_commit = self.commit( | |
455 | + contribution=summary, | |
456 | + previous_summary=f"Merging branch `{branch_name}` with purpose: {meta.purpose if meta else ''}", | |
457 | + update_roadmap=f"Merged `{branch_name}`: {summary}", | |
458 | + ) | |
459 | + | |
460 | + # Mark branch as merged in metadata | |
461 | + with self._lock(): | |
462 | + if meta: | |
463 | + meta.status = "merged" | |
464 | + meta.merged_into = target | |
465 | + meta.merged_at = _now() | |
466 | + self._write(self._meta_path(branch_name), meta.to_yaml()) | |
467 | + | |
468 | + return merge_commit | |
469 | + | |
470 | + def context(self, branch: Optional[str] = None, k: int = 1) -> ContextResult: | |
471 | + """ | |
472 | + CONTEXT command (paper S3.5). | |
473 | + | |
474 | + Retrieves historical context at varying resolutions. The paper fixes | |
475 | + K=1 in experiments (only the most recent commit record is revealed). | |
476 | + | |
477 | + Parameters | |
478 | + ---------- | |
479 | + branch : branch to retrieve context from (default: current branch) | |
480 | + k : number of recent commit records to surface (paper default K=1) | |
481 | + """ | |
482 | + if k < 1: | |
483 | + raise ValueError(f"k must be >= 1, got {k}") | |
484 | + target = branch or self._current_branch | |
485 | + with self._lock(): | |
486 | + commits = self._read_commits(target) | |
487 | + ota = self._read_ota(target) | |
488 | + roadmap = self._read(self._main_md()) | |
489 | + meta_text = self._read(self._meta_path(target)) | |
490 | + meta = BranchMetadata.from_yaml(meta_text) if meta_text else None | |
491 | + | |
492 | + return ContextResult( | |
493 | + branch_name=target, | |
494 | + k=k, | |
495 | + commits=commits[-k:] if commits else [], | |
496 | + ota_records=ota, | |
497 | + main_roadmap=roadmap, | |
498 | + metadata=meta, | |
499 | + ) | |
500 | + | |
501 | + @property | |
502 | + def current_branch(self) -> str: | |
503 | + return self._current_branch | |
504 | + | |
505 | + def switch_branch(self, name: str) -> None: | |
506 | + """Switch the active branch without creating a new one.""" | |
507 | + _validate_branch_name(name) | |
508 | + if not self._branch_dir(name).exists(): | |
509 | + raise FileNotFoundError(f"Branch '{name}' does not exist.") | |
510 | + self._current_branch = name | |
511 | + | |
512 | + def list_branches(self) -> list[str]: | |
513 | + branches_root = self.gcc_dir / "branches" | |
514 | + if not branches_root.exists(): | |
515 | + return [] | |
516 | + return [d.name for d in branches_root.iterdir() if d.is_dir()] | |
517 | + | |
518 | + def update_roadmap(self, content: str) -> None: | |
519 | + """Overwrite or append to main.md global roadmap.""" | |
520 | + with self._lock(): | |
521 | + self._append(self._main_md(), f"\n{content}\n") | |
0 | 522 | |
▼PYTHON/main.py
+0-6
| @@ -1,6 +0,0 @@ | ||
1 | -def main(): | |
2 | - print("Hello from python!") | |
3 | - | |
4 | - | |
5 | -if __name__ == "__main__": | |
6 | - main() | |
7 | 0 | |
▼PYTHON/pyproject.toml
+9-6
| @@ -1,9 +1,9 @@ | ||
1 | 1 | [project] |
2 | -name = "contexa" | |
2 | +name = "cortexa" | |
3 | 3 | version = "0.1.1" |
4 | 4 | description = "Python implementation of Git-Context-Controller (GCC)" |
5 | 5 | readme = "README.md" |
6 | -requires-python = ">=3.12" | |
6 | +requires-python = ">=3.10" | |
7 | 7 | license = "MIT" |
8 | 8 | authors = [ |
9 | 9 | { name = "Swadhin Biswas", email = "[email protected]" }, |
| @@ -14,7 +14,10 @@ classifiers = [ | ||
14 | 14 | "Intended Audience :: Developers", |
15 | 15 | "License :: OSI Approved :: MIT License", |
16 | 16 | "Programming Language :: Python :: 3", |
17 | + "Programming Language :: Python :: 3.10", | |
18 | + "Programming Language :: Python :: 3.11", | |
17 | 19 | "Programming Language :: Python :: 3.12", |
20 | + "Programming Language :: Python :: 3.13", | |
18 | 21 | "Topic :: Scientific/Engineering :: Artificial Intelligence", |
19 | 22 | ] |
20 | 23 | dependencies = [ |
| @@ -22,16 +25,16 @@ dependencies = [ | ||
22 | 25 | ] |
23 | 26 | |
24 | 27 | [project.urls] |
25 | -Homepage = "https://github.com/swadhinbiswas/contexa" | |
26 | -Repository = "https://github.com/swadhinbiswas/contexa" | |
27 | -Issues = "https://github.com/swadhinbiswas/contexa/issues" | |
28 | +Homepage = "https://github.com/swadhinbiswas/Cortexa" | |
29 | +Repository = "https://github.com/swadhinbiswas/Cortexa" | |
30 | +Issues = "https://github.com/swadhinbiswas/Cortexa/issues" | |
28 | 31 | |
29 | 32 | [build-system] |
30 | 33 | requires = ["hatchling>=1.21"] |
31 | 34 | build-backend = "hatchling.build" |
32 | 35 | |
33 | 36 | [tool.hatch.build.targets.wheel] |
34 | -packages = ["contexa"] | |
37 | +packages = ["cortexa"] | |
35 | 38 | |
36 | 39 | [dependency-groups] |
37 | 40 | dev = [ |
38 | 41 | |
▼PYTHON/test/test.py
+3-3
| @@ -1,7 +1,7 @@ | ||
1 | 1 | """Tests for gcc-context Python package.""" |
2 | 2 | |
3 | 3 | import pytest |
4 | -from contexa import GCCWorkspace | |
4 | +from cortexa import GCCWorkspace | |
5 | 5 | |
6 | 6 | |
7 | 7 | @pytest.fixture |
| @@ -84,7 +84,7 @@ def test_context_includes_roadmap(ws): | ||
84 | 84 | |
85 | 85 | def test_branch_metadata_records_purpose(ws): |
86 | 86 | ws.branch("jwt-branch", "Test JWT auth approach") |
87 | - from contexa.models import BranchMetadata | |
87 | + from cortexa.models import BranchMetadata | |
88 | 88 | |
89 | 89 | meta_text = ws._read(ws._meta_path("jwt-branch")) |
90 | 90 | meta = BranchMetadata.from_yaml(meta_text) |
| @@ -97,7 +97,7 @@ def test_merge_marks_branch_as_merged(ws): | ||
97 | 97 | ws.branch("to-merge", "Will be merged") |
98 | 98 | ws.commit("Branch work done") |
99 | 99 | ws.merge("to-merge", target="main") |
100 | - from contexa.models import BranchMetadata | |
100 | + from cortexa.models import BranchMetadata | |
101 | 101 | |
102 | 102 | meta_text = ws._read(ws._meta_path("to-merge")) |
103 | 103 | meta = BranchMetadata.from_yaml(meta_text) |
104 | 104 | |
▼PYTHON/uv.lock
+97-2
| @@ -1,6 +1,6 @@ | ||
1 | 1 | version = 1 |
2 | 2 | revision = 3 |
3 | -requires-python = ">=3.12" | |
3 | +requires-python = ">=3.10" | |
4 | 4 | |
5 | 5 | [[package]] |
6 | 6 | name = "colorama" |
| @@ -12,7 +12,7 @@ wheels = [ | ||
12 | 12 | ] |
13 | 13 | |
14 | 14 | [[package]] |
15 | -name = "contexa" | |
15 | +name = "cortexa" | |
16 | 16 | version = "0.1.1" |
17 | 17 | source = { editable = "." } |
18 | 18 | dependencies = [ |
| @@ -30,6 +30,18 @@ requires-dist = [{ name = "pyyaml", specifier = ">=6.0" }] | ||
30 | 30 | [package.metadata.requires-dev] |
31 | 31 | dev = [{ name = "pytest", specifier = ">=9.0.2" }] |
32 | 32 | |
33 | +[[package]] | |
34 | +name = "exceptiongroup" | |
35 | +version = "1.3.1" | |
36 | +source = { registry = "https://pypi.org/simple" } | |
37 | +dependencies = [ | |
38 | + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, | |
39 | +] | |
40 | +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } | |
41 | +wheels = [ | |
42 | + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, | |
43 | +] | |
44 | + | |
33 | 45 | [[package]] |
34 | 46 | name = "iniconfig" |
35 | 47 | version = "2.3.0" |
| @@ -72,10 +84,12 @@ version = "9.0.2" | ||
72 | 84 | source = { registry = "https://pypi.org/simple" } |
73 | 85 | dependencies = [ |
74 | 86 | { name = "colorama", marker = "sys_platform == 'win32'" }, |
87 | + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, | |
75 | 88 | { name = "iniconfig" }, |
76 | 89 | { name = "packaging" }, |
77 | 90 | { name = "pluggy" }, |
78 | 91 | { name = "pygments" }, |
92 | + { name = "tomli", marker = "python_full_version < '3.11'" }, | |
79 | 93 | ] |
80 | 94 | sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } |
81 | 95 | wheels = [ |
| @@ -88,6 +102,24 @@ version = "6.0.3" | ||
88 | 102 | source = { registry = "https://pypi.org/simple" } |
89 | 103 | sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } |
90 | 104 | wheels = [ |
105 | + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, | |
106 | + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, | |
107 | + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, | |
108 | + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, | |
109 | + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, | |
110 | + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, | |
111 | + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, | |
112 | + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, | |
113 | + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, | |
114 | + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, | |
115 | + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, | |
116 | + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, | |
117 | + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, | |
118 | + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, | |
119 | + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, | |
120 | + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, | |
121 | + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, | |
122 | + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, | |
91 | 123 | { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, |
92 | 124 | { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, |
93 | 125 | { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, |
| @@ -127,3 +159,66 @@ wheels = [ | ||
127 | 159 | { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, |
128 | 160 | { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, |
129 | 161 | ] |
162 | + | |
163 | +[[package]] | |
164 | +name = "tomli" | |
165 | +version = "2.4.0" | |
166 | +source = { registry = "https://pypi.org/simple" } | |
167 | +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } | |
168 | +wheels = [ | |
169 | + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, | |
170 | + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, | |
171 | + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, | |
172 | + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, | |
173 | + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, | |
174 | + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, | |
175 | + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, | |
176 | + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, | |
177 | + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, | |
178 | + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, | |
179 | + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, | |
180 | + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, | |
181 | + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, | |
182 | + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, | |
183 | + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, | |
184 | + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, | |
185 | + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, | |
186 | + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, | |
187 | + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, | |
188 | + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, | |
189 | + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, | |
190 | + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, | |
191 | + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, | |
192 | + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, | |
193 | + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, | |
194 | + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, | |
195 | + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, | |
196 | + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, | |
197 | + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, | |
198 | + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, | |
199 | + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, | |
200 | + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, | |
201 | + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, | |
202 | + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, | |
203 | + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, | |
204 | + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, | |
205 | + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, | |
206 | + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, | |
207 | + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, | |
208 | + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, | |
209 | + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, | |
210 | + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, | |
211 | + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, | |
212 | + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, | |
213 | + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, | |
214 | + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, | |
215 | +] | |
216 | + | |
217 | +[[package]] | |
218 | +name = "typing-extensions" | |
219 | +version = "4.15.0" | |
220 | +source = { registry = "https://pypi.org/simple" } | |
221 | +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } | |
222 | +wheels = [ | |
223 | + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, | |
224 | +] | |
130 | 225 | |