raidlock 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raidlock/__init__.py +11 -0
- raidlock/__main__.py +5 -0
- raidlock/_python.py +58 -0
- raidlock/achievements.py +156 -0
- raidlock/approvals.py +237 -0
- raidlock/bench.py +286 -0
- raidlock/bridge/__init__.py +186 -0
- raidlock/budgets.py +178 -0
- raidlock/cli.py +2153 -0
- raidlock/commands_install.py +114 -0
- raidlock/core/__init__.py +26 -0
- raidlock/core/advisories.py +16 -0
- raidlock/core/billing.py +54 -0
- raidlock/core/board.py +2251 -0
- raidlock/core/brief.py +111 -0
- raidlock/core/clock.py +40 -0
- raidlock/core/cost.py +198 -0
- raidlock/core/db.py +608 -0
- raidlock/core/errors.py +52 -0
- raidlock/core/exe.py +6 -0
- raidlock/core/hints.py +32 -0
- raidlock/core/identity.py +285 -0
- raidlock/core/lead.py +758 -0
- raidlock/core/lead_env.py +23 -0
- raidlock/core/lead_hook.py +42 -0
- raidlock/core/leases.py +19 -0
- raidlock/core/lock.py +99 -0
- raidlock/core/messages.py +98 -0
- raidlock/core/migrate.py +579 -0
- raidlock/core/model.py +126 -0
- raidlock/core/models.py +109 -0
- raidlock/core/paths.py +61 -0
- raidlock/core/portable.py +53 -0
- raidlock/core/receipts.py +73 -0
- raidlock/core/registry.py +39 -0
- raidlock/core/repos.py +250 -0
- raidlock/core/safeio.py +201 -0
- raidlock/core/status.py +52 -0
- raidlock/core/store.py +107 -0
- raidlock/core/tokens.py +77 -0
- raidlock/core/transcript.py +208 -0
- raidlock/duel.py +371 -0
- raidlock/foresight.py +191 -0
- raidlock/frames.py +162 -0
- raidlock/gitguard.py +753 -0
- raidlock/guard.py +239 -0
- raidlock/hooks.py +1526 -0
- raidlock/infer.py +205 -0
- raidlock/limits.py +685 -0
- raidlock/lore/__init__.py +22 -0
- raidlock/lore/approval.py +145 -0
- raidlock/lore/capture.py +146 -0
- raidlock/lore/cli.py +109 -0
- raidlock/lore/distill.py +17 -0
- raidlock/lore/frontmatter.py +56 -0
- raidlock/lore/inject.py +127 -0
- raidlock/lore/vault.py +264 -0
- raidlock/mcp/__init__.py +34 -0
- raidlock/mcp/resources.py +72 -0
- raidlock/mcp/server.py +184 -0
- raidlock/mcp/tools.py +178 -0
- raidlock/notify.py +144 -0
- raidlock/party.py +239 -0
- raidlock/pr.py +575 -0
- raidlock/prsync.py +318 -0
- raidlock/recipes/README.md +34 -0
- raidlock/recipes/__init__.py +105 -0
- raidlock/recipes/docs-refresh.md +15 -0
- raidlock/recipes/multi-repo-refactor.md +15 -0
- raidlock/recipes/security-audit.md +27 -0
- raidlock/recipes/upgrade-deps.md +15 -0
- raidlock/reputation.py +194 -0
- raidlock/resume.py +329 -0
- raidlock/resurrect.py +419 -0
- raidlock/routing.py +419 -0
- raidlock/runners/__init__.py +16 -0
- raidlock/runners/base.py +341 -0
- raidlock/runners/brief.py +176 -0
- raidlock/runners/claude_code.py +121 -0
- raidlock/runners/cli.py +110 -0
- raidlock/runners/codex.py +114 -0
- raidlock/runners/gemini.py +64 -0
- raidlock/runners/isolation.py +391 -0
- raidlock/runners/policy.py +123 -0
- raidlock/runners/runner.py +921 -0
- raidlock/runners/shell.py +35 -0
- raidlock/runners/verifycache.py +93 -0
- raidlock/runners/workspace.py +376 -0
- raidlock/scan.py +357 -0
- raidlock/snapshot.py +151 -0
- raidlock/treasury.py +210 -0
- raidlock/tui.py +223 -0
- raidlock/web/__init__.py +5 -0
- raidlock/web/achievements/view.css +19 -0
- raidlock/web/achievements/view.js +58 -0
- raidlock/web/bosses/view.css +19 -0
- raidlock/web/bosses/view.js +46 -0
- raidlock/web/cli.py +90 -0
- raidlock/web/data.py +687 -0
- raidlock/web/export.py +153 -0
- raidlock/web/frames/view.css +25 -0
- raidlock/web/frames/view.js +63 -0
- raidlock/web/graph/__init__.py +142 -0
- raidlock/web/graph/view.css +33 -0
- raidlock/web/graph/view.js +136 -0
- raidlock/web/raidmap/__init__.py +205 -0
- raidlock/web/raidmap/view.css +15 -0
- raidlock/web/raidmap/view.js +47 -0
- raidlock/web/replay/__init__.py +258 -0
- raidlock/web/replay/view.css +13 -0
- raidlock/web/replay/view.js +127 -0
- raidlock/web/server.py +292 -0
- raidlock/web/static/app.css +851 -0
- raidlock/web/static/app.js +2102 -0
- raidlock/web/static/fonts/OFL.txt +93 -0
- raidlock/web/static/fonts/cinzel-600.woff2 +0 -0
- raidlock/web/static/fonts/cinzel-700.woff2 +0 -0
- raidlock/web/static/icon-32.png +0 -0
- raidlock/web/static/icon-raid.svg +1 -0
- raidlock/web/static/icon.svg +1 -0
- raidlock/web/static/index.html +122 -0
- raidlock-0.1.0.dist-info/METADATA +186 -0
- raidlock-0.1.0.dist-info/RECORD +126 -0
- raidlock-0.1.0.dist-info/WHEEL +4 -0
- raidlock-0.1.0.dist-info/entry_points.txt +34 -0
- raidlock-0.1.0.dist-info/licenses/LICENSE +21 -0
raidlock/infer.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Read-only lock suggestions from tracked paths, recent history and CODEOWNERS."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from collections import Counter, defaultdict
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
|
|
9
|
+
from .core import canonical
|
|
10
|
+
from .core.errors import Precondition
|
|
11
|
+
from .core.repos import Resolver
|
|
12
|
+
from .runners.policy import denied
|
|
13
|
+
from .runners.workspace import git_run
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _git(root, *args):
|
|
17
|
+
try:
|
|
18
|
+
result = git_run(root, *args, timeout=30)
|
|
19
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
20
|
+
return ""
|
|
21
|
+
return result.stdout if result.returncode == 0 else ""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _tokens(text):
|
|
25
|
+
result = set()
|
|
26
|
+
for token in re.findall(r"[\w./\\-]+", text.replace("\\", "/")):
|
|
27
|
+
token = token.rstrip(".")
|
|
28
|
+
while token.startswith("./"):
|
|
29
|
+
token = token[2:]
|
|
30
|
+
if token and ".." not in PurePosixPath(token).parts and not token.startswith("/"):
|
|
31
|
+
result.add(token)
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _glob(parts, pattern):
|
|
36
|
+
if not pattern:
|
|
37
|
+
return not parts
|
|
38
|
+
if pattern[0] == "**":
|
|
39
|
+
return _glob(parts, pattern[1:]) or bool(parts) and _glob(parts[1:], pattern)
|
|
40
|
+
return bool(parts) and fnmatch.fnmatchcase(parts[0], pattern[0]) and _glob(parts[1:], pattern[1:])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _matches(pattern, path):
|
|
44
|
+
if "[" in pattern or "]" in pattern:
|
|
45
|
+
return False
|
|
46
|
+
anchored = pattern.startswith("/") or "/" in pattern.rstrip("/")
|
|
47
|
+
directory = pattern.endswith("/")
|
|
48
|
+
pattern = pattern.strip("/")
|
|
49
|
+
if not pattern:
|
|
50
|
+
return False
|
|
51
|
+
parts = path.split("/")
|
|
52
|
+
if anchored:
|
|
53
|
+
ends = range(1, len(parts) + (0 if directory else 1))
|
|
54
|
+
return any(_glob(parts[:end], pattern.split("/")) for end in ends)
|
|
55
|
+
candidates = parts[:-1] if directory else parts
|
|
56
|
+
return any(fnmatch.fnmatchcase(part, pattern) for part in candidates)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _tracked_file(root, file):
|
|
60
|
+
try:
|
|
61
|
+
canonical(file)
|
|
62
|
+
if denied(file):
|
|
63
|
+
return False
|
|
64
|
+
parts = PurePosixPath(file).parts
|
|
65
|
+
if any(
|
|
66
|
+
part.lower() in (".git", ".board", ".claude", "secret", "secrets")
|
|
67
|
+
or part.lower().startswith(".env")
|
|
68
|
+
or part.lower().endswith(".pem")
|
|
69
|
+
for part in parts
|
|
70
|
+
) or parts[:2] == (".github", "workflows"):
|
|
71
|
+
return False
|
|
72
|
+
path = Path(root, file)
|
|
73
|
+
root = Path(root).resolve()
|
|
74
|
+
if not path.is_file() or path.is_symlink() or root not in path.resolve().parents:
|
|
75
|
+
return False
|
|
76
|
+
return not any(parent.is_symlink() for parent in path.parents if parent != root and root in parent.parents)
|
|
77
|
+
except (OSError, RuntimeError, Precondition):
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _owners(root, files):
|
|
82
|
+
for location in (".github/CODEOWNERS", "CODEOWNERS", "docs/CODEOWNERS"):
|
|
83
|
+
path = Path(root, location)
|
|
84
|
+
if path.is_file() and not path.is_symlink() and Path(root).resolve() in path.resolve().parents:
|
|
85
|
+
try:
|
|
86
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
87
|
+
except (OSError, UnicodeError):
|
|
88
|
+
return {}
|
|
89
|
+
break
|
|
90
|
+
else:
|
|
91
|
+
return {}
|
|
92
|
+
assignments = {}
|
|
93
|
+
for line in lines:
|
|
94
|
+
line = line.strip()
|
|
95
|
+
if not line or line.startswith("#"):
|
|
96
|
+
continue
|
|
97
|
+
if line.startswith("[") or line.startswith("^["):
|
|
98
|
+
continue
|
|
99
|
+
fields = line.split("#", 1)[0].split()
|
|
100
|
+
if not fields or fields[0].startswith("!"):
|
|
101
|
+
continue
|
|
102
|
+
for file in files:
|
|
103
|
+
if _matches(fields[0], file):
|
|
104
|
+
owners = tuple(
|
|
105
|
+
sorted({owner for owner in fields[1:] if re.fullmatch(r"@[A-Za-z0-9_/-]+|[^\s@]+@[^\s@]+", owner)})
|
|
106
|
+
)
|
|
107
|
+
assignments[file] = (owners, ", ".join(owners)) if owners else None
|
|
108
|
+
return assignments
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def suggest_locks(board, text, repos=None, limit=5):
|
|
112
|
+
"""Return ranked {path, score, reason} dictionaries without changing board state.
|
|
113
|
+
|
|
114
|
+
repos optionally selects repository names from the board's resolver. Scores
|
|
115
|
+
prioritize explicit paths, then co-change frequency, then shared CODEOWNERS owners.
|
|
116
|
+
Bulk commits over 50 paths, protected files and symlinks are excluded. Directory
|
|
117
|
+
collapse never covers additional tracked files; suggestions never mutate the board.
|
|
118
|
+
"""
|
|
119
|
+
if not isinstance(text, str):
|
|
120
|
+
raise Precondition("suggestion text must be text")
|
|
121
|
+
if not isinstance(limit, int) or isinstance(limit, bool) or limit < 0:
|
|
122
|
+
raise Precondition("limit must be a non-negative integer")
|
|
123
|
+
if not text.strip() or limit == 0:
|
|
124
|
+
return []
|
|
125
|
+
resolver = Resolver(board.workspace, board.dir)
|
|
126
|
+
names = sorted(resolver.repos) if repos is None else ([repos] if isinstance(repos, str) else list(repos))
|
|
127
|
+
if any(not isinstance(name, str) for name in names):
|
|
128
|
+
raise Precondition("repository names must be text")
|
|
129
|
+
unknown = set(names) - resolver.repos.keys()
|
|
130
|
+
if unknown:
|
|
131
|
+
raise Precondition("unknown repositories: " + ", ".join(sorted(unknown)))
|
|
132
|
+
tokens = _tokens(text)
|
|
133
|
+
suggestions = []
|
|
134
|
+
for name in sorted(set(names)):
|
|
135
|
+
root = resolver.repos[name]
|
|
136
|
+
tracked_files = set(filter(None, _git(root, "ls-files", "-z").split("\0")))
|
|
137
|
+
files = {file for file in tracked_files if _tracked_file(root, file)}
|
|
138
|
+
scores, reasons = {}, {}
|
|
139
|
+
for file in sorted(files):
|
|
140
|
+
path = PurePosixPath(file)
|
|
141
|
+
aliases = {file, name + "/" + file, path.name}
|
|
142
|
+
directories = {str(parent) for parent in path.parents if str(parent) != "."}
|
|
143
|
+
if tokens & aliases:
|
|
144
|
+
scores[file], reasons[file] = 100, "Path mentioned in text"
|
|
145
|
+
elif tokens & (directories | {name + "/" + d for d in directories}):
|
|
146
|
+
scores[file], reasons[file] = 90, "Inside directory mentioned in text"
|
|
147
|
+
seeds = set(scores)
|
|
148
|
+
if not seeds:
|
|
149
|
+
continue
|
|
150
|
+
counts = Counter()
|
|
151
|
+
history = _git(root, "log", "-200", "--format=%x00", "--name-only", "-z", "--no-renames")
|
|
152
|
+
for commit in history.split("\0\0"):
|
|
153
|
+
raw_changed = {item.lstrip("\n") for item in commit.split("\0")} - {""}
|
|
154
|
+
changed = raw_changed & files
|
|
155
|
+
if changed & seeds and len(raw_changed) <= 50:
|
|
156
|
+
counts.update(changed - seeds)
|
|
157
|
+
for file, count in counts.items():
|
|
158
|
+
scores[file] = 40 + min(count, 39)
|
|
159
|
+
reasons[file] = f"Changed with mentioned files in {count} recent commit(s)"
|
|
160
|
+
owners = _owners(root, files)
|
|
161
|
+
groups = {owners[file] for file in seeds if owners.get(file)}
|
|
162
|
+
for file in sorted(files - seeds):
|
|
163
|
+
if owners.get(file) in groups:
|
|
164
|
+
scores[file] = scores.get(file, 0) + 10
|
|
165
|
+
reason = "Same CODEOWNERS ownership group"
|
|
166
|
+
reasons[file] = reasons[file] + "; " + reason if file in reasons else reason
|
|
167
|
+
siblings = defaultdict(list)
|
|
168
|
+
for file in scores:
|
|
169
|
+
siblings[str(PurePosixPath(file).parent)].append(file)
|
|
170
|
+
for directory, children in sorted(siblings.items()):
|
|
171
|
+
named = directory in tokens or name + "/" + directory in tokens
|
|
172
|
+
if len(children) < 3 or directory == "." or ("/" not in directory and not named):
|
|
173
|
+
continue
|
|
174
|
+
if any(file.startswith(directory + "/") and file not in children for file in tracked_files):
|
|
175
|
+
continue
|
|
176
|
+
scores[directory] = max(scores[child] for child in children)
|
|
177
|
+
reasons[directory] = (
|
|
178
|
+
f"{len(children)} sibling files; " + reasons[max(children, key=lambda child: (scores[child], child))]
|
|
179
|
+
)
|
|
180
|
+
for child in children:
|
|
181
|
+
del scores[child]
|
|
182
|
+
for file, score in scores.items():
|
|
183
|
+
try:
|
|
184
|
+
path = canonical(name + "/" + file)
|
|
185
|
+
except Precondition:
|
|
186
|
+
continue
|
|
187
|
+
suggestions.append({"path": path, "score": score, "reason": reasons[file]})
|
|
188
|
+
return sorted(suggestions, key=lambda item: (-item["score"], item["path"]))[:limit]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def register_cli(add_command):
|
|
192
|
+
"""Register with the raidlock.commands extension interface."""
|
|
193
|
+
|
|
194
|
+
def setup(parser):
|
|
195
|
+
parser.add_argument("text")
|
|
196
|
+
parser.add_argument("--repos", nargs="+")
|
|
197
|
+
parser.add_argument("--limit", type=int, default=5)
|
|
198
|
+
|
|
199
|
+
def handler(ctx, args):
|
|
200
|
+
suggestions = suggest_locks(ctx.board, args.text, args.repos, args.limit)
|
|
201
|
+
return {"suggestions": suggestions}, "\n".join(
|
|
202
|
+
item["path"] + ": " + item["reason"] for item in suggestions
|
|
203
|
+
) or "No lock suggestions."
|
|
204
|
+
|
|
205
|
+
add_command("suggest-locks", "suggest narrow lock paths from card text", setup, handler)
|