dotscope 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.
- dotscope/.scope +63 -0
- dotscope/__init__.py +3 -0
- dotscope/absorber.py +390 -0
- dotscope/assertions.py +128 -0
- dotscope/ast_analyzer.py +2 -0
- dotscope/backtest.py +2 -0
- dotscope/bench.py +141 -0
- dotscope/budget.py +3 -0
- dotscope/cache.py +2 -0
- dotscope/check/__init__.py +1 -0
- dotscope/check/acknowledge.py +2 -0
- dotscope/check/checker.py +3 -0
- dotscope/check/checks/__init__.py +1 -0
- dotscope/check/checks/antipattern.py +2 -0
- dotscope/check/checks/boundary.py +2 -0
- dotscope/check/checks/contracts.py +3 -0
- dotscope/check/checks/direction.py +2 -0
- dotscope/check/checks/intent.py +2 -0
- dotscope/check/checks/stability.py +2 -0
- dotscope/check/constraints.py +2 -0
- dotscope/check/models.py +15 -0
- dotscope/cli.py +1447 -0
- dotscope/composer.py +147 -0
- dotscope/constants.py +45 -0
- dotscope/context.py +60 -0
- dotscope/counterfactual.py +180 -0
- dotscope/debug.py +220 -0
- dotscope/discovery.py +104 -0
- dotscope/formatter.py +157 -0
- dotscope/graph.py +3 -0
- dotscope/health.py +212 -0
- dotscope/help.py +204 -0
- dotscope/history.py +6 -0
- dotscope/hooks.py +2 -0
- dotscope/ingest.py +858 -0
- dotscope/intent.py +618 -0
- dotscope/lessons.py +223 -0
- dotscope/matcher.py +104 -0
- dotscope/mcp_server.py +1081 -0
- dotscope/models/.scope +45 -0
- dotscope/models/__init__.py +7 -0
- dotscope/models/core.py +288 -0
- dotscope/models/history.py +73 -0
- dotscope/models/intent.py +213 -0
- dotscope/models/passes.py +58 -0
- dotscope/models/state.py +250 -0
- dotscope/models.py +9 -0
- dotscope/near_miss.py +3 -0
- dotscope/onboarding.py +2 -0
- dotscope/parser.py +387 -0
- dotscope/passes/.scope +105 -0
- dotscope/passes/__init__.py +1 -0
- dotscope/passes/ast_analyzer.py +508 -0
- dotscope/passes/backtest.py +198 -0
- dotscope/passes/budget_allocator.py +164 -0
- dotscope/passes/convention_compliance.py +40 -0
- dotscope/passes/convention_discovery.py +247 -0
- dotscope/passes/convention_parser.py +223 -0
- dotscope/passes/graph_builder.py +299 -0
- dotscope/passes/history_miner.py +336 -0
- dotscope/passes/incremental.py +149 -0
- dotscope/passes/lang/__init__.py +38 -0
- dotscope/passes/lang/_base.py +20 -0
- dotscope/passes/lang/_treesitter.py +93 -0
- dotscope/passes/lang/go.py +333 -0
- dotscope/passes/lang/javascript.py +348 -0
- dotscope/passes/lazy.py +152 -0
- dotscope/passes/semantic_diff.py +160 -0
- dotscope/passes/sentinel/__init__.py +1 -0
- dotscope/passes/sentinel/acknowledge.py +222 -0
- dotscope/passes/sentinel/checker.py +383 -0
- dotscope/passes/sentinel/checks/__init__.py +1 -0
- dotscope/passes/sentinel/checks/antipattern.py +84 -0
- dotscope/passes/sentinel/checks/boundary.py +46 -0
- dotscope/passes/sentinel/checks/contracts.py +148 -0
- dotscope/passes/sentinel/checks/convention.py +54 -0
- dotscope/passes/sentinel/checks/direction.py +71 -0
- dotscope/passes/sentinel/checks/intent.py +207 -0
- dotscope/passes/sentinel/checks/stability.py +66 -0
- dotscope/passes/sentinel/checks/voice.py +108 -0
- dotscope/passes/sentinel/constraints.py +472 -0
- dotscope/passes/sentinel/line_filter.py +88 -0
- dotscope/passes/sentinel/models.py +15 -0
- dotscope/passes/virtual.py +239 -0
- dotscope/passes/voice.py +162 -0
- dotscope/passes/voice_defaults.py +28 -0
- dotscope/passes/voice_discovery.py +245 -0
- dotscope/paths.py +32 -0
- dotscope/progress.py +44 -0
- dotscope/regression.py +147 -0
- dotscope/resolver.py +203 -0
- dotscope/scanner.py +246 -0
- dotscope/sessions.py +2 -0
- dotscope/storage/.scope +64 -0
- dotscope/storage/__init__.py +1 -0
- dotscope/storage/cache.py +114 -0
- dotscope/storage/claude_hooks.py +119 -0
- dotscope/storage/git_hooks.py +277 -0
- dotscope/storage/incremental_state.py +61 -0
- dotscope/storage/mcp_config.py +98 -0
- dotscope/storage/near_miss.py +183 -0
- dotscope/storage/onboarding.py +150 -0
- dotscope/storage/session_manager.py +195 -0
- dotscope/storage/timing.py +84 -0
- dotscope/timing.py +2 -0
- dotscope/tokens.py +53 -0
- dotscope/utility.py +123 -0
- dotscope/virtual.py +3 -0
- dotscope/visibility.py +664 -0
- dotscope-0.1.0.dist-info/METADATA +50 -0
- dotscope-0.1.0.dist-info/RECORD +114 -0
- dotscope-0.1.0.dist-info/WHEEL +4 -0
- dotscope-0.1.0.dist-info/entry_points.txt +3 -0
- dotscope-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Lightweight timing instrumentation for core operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class TimingEntry:
|
|
12
|
+
operation: str # "resolve", "check", "ingest"
|
|
13
|
+
duration_ms: float
|
|
14
|
+
timestamp: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_MAX_TIMING_LINES = 5000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def record_timing(repo_root: str, operation: str, duration_ms: float) -> None:
|
|
21
|
+
"""Append a timing entry to .dotscope/timings.jsonl. Truncates at 5000 lines."""
|
|
22
|
+
dot_dir = os.path.join(repo_root, ".dotscope")
|
|
23
|
+
if not os.path.isdir(dot_dir):
|
|
24
|
+
return # No .dotscope dir — skip silently
|
|
25
|
+
|
|
26
|
+
path = os.path.join(dot_dir, "timings.jsonl")
|
|
27
|
+
entry = {
|
|
28
|
+
"operation": operation,
|
|
29
|
+
"duration_ms": round(duration_ms, 2),
|
|
30
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
31
|
+
}
|
|
32
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
33
|
+
f.write(json.dumps(entry) + "\n")
|
|
34
|
+
|
|
35
|
+
# Truncate if too large (keep most recent half)
|
|
36
|
+
try:
|
|
37
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
38
|
+
lines = f.readlines()
|
|
39
|
+
if len(lines) > _MAX_TIMING_LINES:
|
|
40
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
41
|
+
f.writelines(lines[len(lines) // 2:])
|
|
42
|
+
except (IOError, OSError):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_timings(repo_root: str) -> List[TimingEntry]:
|
|
47
|
+
"""Load all timing entries."""
|
|
48
|
+
path = os.path.join(repo_root, ".dotscope", "timings.jsonl")
|
|
49
|
+
if not os.path.exists(path):
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
entries = []
|
|
53
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
54
|
+
for line in f:
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if line:
|
|
57
|
+
try:
|
|
58
|
+
d = json.loads(line)
|
|
59
|
+
entries.append(TimingEntry(
|
|
60
|
+
operation=d["operation"],
|
|
61
|
+
duration_ms=d["duration_ms"],
|
|
62
|
+
timestamp=d.get("timestamp", ""),
|
|
63
|
+
))
|
|
64
|
+
except (json.JSONDecodeError, KeyError):
|
|
65
|
+
continue
|
|
66
|
+
return entries
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def median(values: List[float]) -> float:
|
|
70
|
+
if not values:
|
|
71
|
+
return 0.0
|
|
72
|
+
s = sorted(values)
|
|
73
|
+
n = len(s)
|
|
74
|
+
if n % 2 == 0:
|
|
75
|
+
return (s[n // 2 - 1] + s[n // 2]) / 2
|
|
76
|
+
return s[n // 2]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def percentile(values: List[float], p: int) -> float:
|
|
80
|
+
if not values:
|
|
81
|
+
return 0.0
|
|
82
|
+
s = sorted(values)
|
|
83
|
+
idx = int(len(s) * p / 100)
|
|
84
|
+
return s[min(idx, len(s) - 1)]
|
dotscope/timing.py
ADDED
dotscope/tokens.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Token estimation for scope files and resolved contexts.
|
|
2
|
+
|
|
3
|
+
Uses tiktoken if available, otherwise falls back to len(text) // 4.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
# Try tiktoken for accurate counts, fall back to approximation
|
|
11
|
+
try:
|
|
12
|
+
import tiktoken
|
|
13
|
+
|
|
14
|
+
_encoder = tiktoken.encoding_for_model("gpt-4")
|
|
15
|
+
|
|
16
|
+
def estimate_tokens(text: str) -> int:
|
|
17
|
+
"""Estimate token count using tiktoken."""
|
|
18
|
+
return len(_encoder.encode(text))
|
|
19
|
+
|
|
20
|
+
except ImportError:
|
|
21
|
+
|
|
22
|
+
def estimate_tokens(text: str) -> int:
|
|
23
|
+
"""Estimate token count (~4 chars per token for English)."""
|
|
24
|
+
return len(text) // 4
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def estimate_file_tokens(path: str) -> int:
|
|
28
|
+
"""Read a file and estimate its token count."""
|
|
29
|
+
try:
|
|
30
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
31
|
+
return estimate_tokens(f.read())
|
|
32
|
+
except (OSError, IOError):
|
|
33
|
+
return 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def estimate_scope_tokens(files: List[str]) -> int:
|
|
37
|
+
"""Estimate total tokens across a list of files."""
|
|
38
|
+
return sum(estimate_file_tokens(f) for f in files)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def estimate_context_tokens(context: str) -> int:
|
|
42
|
+
"""Estimate tokens for a context string."""
|
|
43
|
+
if not context:
|
|
44
|
+
return 0
|
|
45
|
+
return estimate_tokens(context)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def file_size_bytes(path: str) -> int:
|
|
49
|
+
"""Get file size in bytes, 0 if not accessible."""
|
|
50
|
+
try:
|
|
51
|
+
return os.path.getsize(path)
|
|
52
|
+
except OSError:
|
|
53
|
+
return 0
|
dotscope/utility.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Utility scoring: historical file relevance from observation data.
|
|
2
|
+
|
|
3
|
+
Computes per-file utility ratios from session + observation logs.
|
|
4
|
+
Files agents actually touch get higher scores. Budget allocation
|
|
5
|
+
uses these scores instead of static heuristics.
|
|
6
|
+
|
|
7
|
+
The utility floor prevents a death spiral: core abstractions that are
|
|
8
|
+
rarely edited but frequently read always retain a base weight.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
from .models.state import FileUtilityScore, ObservationLog, SessionLog # noqa: F401
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
BASE_UTILITY_WEIGHT = 0.5
|
|
20
|
+
MAX_UTILITY_BONUS = 1.0
|
|
21
|
+
MIN_SAMPLE_SIZE = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compute_utility_scores(
|
|
25
|
+
sessions: List[SessionLog],
|
|
26
|
+
observations: List[ObservationLog],
|
|
27
|
+
) -> Dict[str, FileUtilityScore]:
|
|
28
|
+
"""Build utility scores from session + observation logs."""
|
|
29
|
+
scores: Dict[str, FileUtilityScore] = {}
|
|
30
|
+
|
|
31
|
+
# Count resolves per file
|
|
32
|
+
for session in sessions:
|
|
33
|
+
for f in session.predicted_files:
|
|
34
|
+
if f not in scores:
|
|
35
|
+
scores[f] = FileUtilityScore(path=f)
|
|
36
|
+
scores[f].resolve_count += 1
|
|
37
|
+
scores[f].last_resolved = max(scores[f].last_resolved, session.timestamp)
|
|
38
|
+
|
|
39
|
+
# Count touches per file
|
|
40
|
+
obs_by_session: Dict[str, ObservationLog] = {
|
|
41
|
+
obs.session_id: obs for obs in observations
|
|
42
|
+
}
|
|
43
|
+
for session in sessions:
|
|
44
|
+
obs = obs_by_session.get(session.session_id)
|
|
45
|
+
if not obs:
|
|
46
|
+
continue
|
|
47
|
+
for f in obs.actual_files_modified:
|
|
48
|
+
if f not in scores:
|
|
49
|
+
scores[f] = FileUtilityScore(path=f)
|
|
50
|
+
scores[f].touch_count += 1
|
|
51
|
+
scores[f].last_touched = max(scores[f].last_touched, obs.timestamp)
|
|
52
|
+
|
|
53
|
+
# Compute ratios
|
|
54
|
+
for score in scores.values():
|
|
55
|
+
if score.resolve_count > 0:
|
|
56
|
+
score.utility_ratio = round(score.touch_count / score.resolve_count, 3)
|
|
57
|
+
|
|
58
|
+
return scores
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def effective_score(
|
|
62
|
+
base_score: float,
|
|
63
|
+
utility: Optional[FileUtilityScore],
|
|
64
|
+
is_explicit_include: bool,
|
|
65
|
+
) -> float:
|
|
66
|
+
"""Compute effective score with utility floor protection.
|
|
67
|
+
|
|
68
|
+
Explicit includes always get a base weight. Utility observations
|
|
69
|
+
add on top, never subtract below the floor.
|
|
70
|
+
"""
|
|
71
|
+
floor = BASE_UTILITY_WEIGHT if is_explicit_include else 0.0
|
|
72
|
+
|
|
73
|
+
bonus = 0.0
|
|
74
|
+
if utility and utility.resolve_count >= MIN_SAMPLE_SIZE:
|
|
75
|
+
bonus = utility.utility_ratio * MAX_UTILITY_BONUS
|
|
76
|
+
# Recency bonus: touched in last 30 days
|
|
77
|
+
if utility.last_touched > time.time() - (30 * 86400):
|
|
78
|
+
bonus *= 1.1
|
|
79
|
+
|
|
80
|
+
return base_score * max(floor + bonus, floor) if floor else base_score * (1.0 + bonus)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def save_utility_scores(dot_dir: Path, scores: Dict[str, FileUtilityScore]) -> None:
|
|
84
|
+
"""Write utility scores to .dotscope/utility/file_scores.json."""
|
|
85
|
+
utility_dir = dot_dir / "utility"
|
|
86
|
+
utility_dir.mkdir(parents=True, exist_ok=True)
|
|
87
|
+
|
|
88
|
+
data = {
|
|
89
|
+
path: {
|
|
90
|
+
"resolve_count": s.resolve_count,
|
|
91
|
+
"touch_count": s.touch_count,
|
|
92
|
+
"utility_ratio": s.utility_ratio,
|
|
93
|
+
"last_touched": s.last_touched,
|
|
94
|
+
"last_resolved": s.last_resolved,
|
|
95
|
+
}
|
|
96
|
+
for path, s in scores.items()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
(utility_dir / "file_scores.json").write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_utility_scores(dot_dir: Path) -> Dict[str, FileUtilityScore]:
|
|
103
|
+
"""Load utility scores from .dotscope/utility/file_scores.json."""
|
|
104
|
+
path = dot_dir / "utility" / "file_scores.json"
|
|
105
|
+
if not path.exists():
|
|
106
|
+
return {}
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
110
|
+
return {
|
|
111
|
+
k: FileUtilityScore(path=k, **v)
|
|
112
|
+
for k, v in data.items()
|
|
113
|
+
}
|
|
114
|
+
except (json.JSONDecodeError, TypeError):
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rebuild_utility(dot_dir: Path, sessions: List[SessionLog],
|
|
119
|
+
observations: List[ObservationLog]) -> Dict[str, FileUtilityScore]:
|
|
120
|
+
"""Rebuild and save utility scores from event logs."""
|
|
121
|
+
scores = compute_utility_scores(sessions, observations)
|
|
122
|
+
save_utility_scores(dot_dir, scores)
|
|
123
|
+
return scores
|
dotscope/virtual.py
ADDED