symbol_anchor.py
python
sha256:c3910cc561368d2b40576c1fbb0841b5d3abefd0a65c96c85114a7238222c77c
fix: root-of-push snapshots were never hash-verified before…
Sonnet 5
patch
5 days ago
| 1 | """Parse and resolve symbol anchor strings. |
| 2 | |
| 3 | Anchor grammar |
| 4 | -------------- |
| 5 | anchor := cross_anchor | same_anchor |
| 6 | cross_anchor := owner '/' repo '::' file_path ('::' symbol)? ('@' ref)? |
| 7 | same_anchor := file_path ('::' symbol)? ('@' ref)? |
| 8 | |
| 9 | owner/repo — exactly one '/', no dots in either segment (not a file path) |
| 10 | file_path — slash-separated path; distinguished from owner/repo by having |
| 11 | a file extension (dot in last segment) or more than one slash |
| 12 | symbol — function/class name; may contain dots (e.g. 'GitExporter.fix_file_modes') |
| 13 | ref — branch name OR 'sha256:<64hex>' |
| 14 | |
| 15 | Examples |
| 16 | -------- |
| 17 | same-repo: |
| 18 | muse/cli/commands/bridge.py::GitExporter.fix_file_modes |
| 19 | src/main.py::Fn@dev |
| 20 | src/main.py@sha256:fa002... |
| 21 | |
| 22 | cross-repo: |
| 23 | gabriel/muse::muse/cli/commands/bridge.py::GitExporter.fix_file_modes |
| 24 | gabriel/muse::muse/cli/commands/bridge.py::GitExporter._has_shebang@dev |
| 25 | acme/infra::deploy/scripts/install.sh@sha256:fa002... |
| 26 | """ |
| 27 | from __future__ import annotations |
| 28 | |
| 29 | from dataclasses import dataclass |
| 30 | |
| 31 | |
| 32 | @dataclass(frozen=True) |
| 33 | class SymbolAnchorRef: |
| 34 | owner: str | None # None → same repo as the issue |
| 35 | repo: str | None # None → same repo as the issue |
| 36 | file_path: str |
| 37 | symbol_name: str | None # None → file-level anchor |
| 38 | ref: str | None # None → caller supplies default (usually "main") |
| 39 | |
| 40 | @property |
| 41 | def is_cross_repo(self) -> bool: |
| 42 | return self.owner is not None |
| 43 | |
| 44 | def blob_url( |
| 45 | self, |
| 46 | base_url: str, |
| 47 | default_owner: str, |
| 48 | default_repo: str, |
| 49 | default_ref: str = "main", |
| 50 | ) -> str: |
| 51 | owner = self.owner or default_owner |
| 52 | repo = self.repo or default_repo |
| 53 | ref = self.ref or default_ref |
| 54 | url = f"{base_url}/{owner}/{repo}/blob/{ref}/{self.file_path}" |
| 55 | if self.symbol_name: |
| 56 | url += f"#S:{self.symbol_name}" |
| 57 | return url |
| 58 | |
| 59 | def display_label(self) -> str: |
| 60 | parts = [] |
| 61 | if self.is_cross_repo: |
| 62 | parts.append(f"{self.owner}/{self.repo}::") |
| 63 | parts.append(self.file_path) |
| 64 | if self.symbol_name: |
| 65 | parts.append(f"::{self.symbol_name}") |
| 66 | if self.ref: |
| 67 | parts.append(f"@{self.ref}") |
| 68 | return "".join(parts) |
| 69 | |
| 70 | |
| 71 | def _looks_like_owner_repo(segment: str) -> bool: |
| 72 | """Return True iff segment is 'owner/repo' — exactly one slash, no dots in either part.""" |
| 73 | slash_count = segment.count("/") |
| 74 | if slash_count != 1: |
| 75 | return False |
| 76 | owner, repo = segment.split("/", 1) |
| 77 | return ( |
| 78 | bool(owner) and bool(repo) |
| 79 | and "." not in owner |
| 80 | and "." not in repo |
| 81 | ) |
| 82 | |
| 83 | |
| 84 | def _strip_ref(s: str) -> tuple[str, str | None]: |
| 85 | """Split 'name@ref' into (name, ref). Returns (s, None) if no '@'.""" |
| 86 | at = s.rfind("@") |
| 87 | if at == -1: |
| 88 | return s, None |
| 89 | return s[:at], s[at + 1:] or None |
| 90 | |
| 91 | |
| 92 | def parse_symbol_anchor(raw: str) -> SymbolAnchorRef: |
| 93 | """Parse a symbol anchor string into a SymbolAnchorRef. |
| 94 | |
| 95 | Handles same-repo and cross-repo anchors, with optional ref pins. |
| 96 | """ |
| 97 | parts = raw.split("::") |
| 98 | |
| 99 | owner: str | None = None |
| 100 | repo: str | None = None |
| 101 | ref: str | None = None |
| 102 | |
| 103 | if len(parts) >= 2 and _looks_like_owner_repo(parts[0]): |
| 104 | # Cross-repo: owner/repo :: file (:: symbol)? |
| 105 | owner, repo = parts[0].split("/", 1) |
| 106 | file_path = parts[1] |
| 107 | symbol_raw = parts[2] if len(parts) >= 3 else None |
| 108 | else: |
| 109 | # Same-repo: file (:: symbol)? |
| 110 | file_path = parts[0] |
| 111 | symbol_raw = parts[1] if len(parts) >= 2 else None |
| 112 | |
| 113 | # Strip @ref from whichever is the last token. |
| 114 | if symbol_raw is not None: |
| 115 | symbol_raw, ref = _strip_ref(symbol_raw) |
| 116 | symbol_name: str | None = symbol_raw or None |
| 117 | else: |
| 118 | file_path, ref = _strip_ref(file_path) |
| 119 | symbol_name = None |
| 120 | |
| 121 | return SymbolAnchorRef( |
| 122 | owner=owner, |
| 123 | repo=repo, |
| 124 | file_path=file_path, |
| 125 | symbol_name=symbol_name, |
| 126 | ref=ref, |
| 127 | ) |
File History
1 commit
sha256:c3910cc561368d2b40576c1fbb0841b5d3abefd0a65c96c85114a7238222c77c
fix: root-of-push snapshots were never hash-verified before…
Sonnet 5
patch
5 days ago