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
dotscope/models/.scope
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
description: Data models — the single source of truth for all types
|
|
2
|
+
includes:
|
|
3
|
+
- core.py
|
|
4
|
+
- history.py
|
|
5
|
+
- intent.py
|
|
6
|
+
- state.py
|
|
7
|
+
- passes.py
|
|
8
|
+
context: |
|
|
9
|
+
Five domain files. Each owns a distinct category of data.
|
|
10
|
+
Models NEVER import from functional modules — they are the foundation.
|
|
11
|
+
|
|
12
|
+
## core.py — Static Architecture
|
|
13
|
+
ScopeConfig, FileAnalysis, DependencyGraph, FileNode, ResolvedImport,
|
|
14
|
+
ExportedSymbol, ClassInfo, FunctionInfo, ModuleBoundary.
|
|
15
|
+
What the codebase IS right now.
|
|
16
|
+
|
|
17
|
+
## history.py — Empirical Behavior
|
|
18
|
+
ImplicitContract, FileHistory, ChangeCoupling, HistoryAnalysis.
|
|
19
|
+
How the codebase behaves over time. Mined from git.
|
|
20
|
+
|
|
21
|
+
## intent.py — Human Rulebook
|
|
22
|
+
IntentDirective, Assertion, ContextExhaustionError, CheckResult,
|
|
23
|
+
ProposedFix, Severity, CheckCategory, Constraint, WarningPair, NearMiss.
|
|
24
|
+
How the codebase MUST be treated. Enforcement targets.
|
|
25
|
+
|
|
26
|
+
## state.py — Persistent Memory
|
|
27
|
+
SessionLog, ObservationLog, BenchReport, RegressionCase, BisectionResult,
|
|
28
|
+
FileUtilityScore, Lesson, ObservedInvariant, Counterfactual, SessionStats.
|
|
29
|
+
Schemas for .dotscope/ JSON event logs.
|
|
30
|
+
|
|
31
|
+
## passes.py — Transient Outputs
|
|
32
|
+
IngestPlan, PlannedScope, VirtualScope.
|
|
33
|
+
DTOs that live only during a single operation.
|
|
34
|
+
|
|
35
|
+
## Gotchas
|
|
36
|
+
The old dotscope/models.py is a backward-compat re-export facade.
|
|
37
|
+
New code should import from dotscope.models (the package), not the file.
|
|
38
|
+
related:
|
|
39
|
+
- dotscope/passes/.scope
|
|
40
|
+
- dotscope/storage/.scope
|
|
41
|
+
tags:
|
|
42
|
+
- models
|
|
43
|
+
- dataclasses
|
|
44
|
+
- types
|
|
45
|
+
tokens_estimate: 4500
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Unified model facade: re-exports all data models from sub-modules."""
|
|
2
|
+
|
|
3
|
+
from .core import * # noqa: F401,F403
|
|
4
|
+
from .history import * # noqa: F401,F403
|
|
5
|
+
from .intent import * # noqa: F401,F403
|
|
6
|
+
from .state import * # noqa: F401,F403
|
|
7
|
+
from .passes import * # noqa: F401,F403
|
dotscope/models/core.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Core data models: the static architecture of a codebase."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class StructuredContext:
|
|
9
|
+
"""Context with optional named sections (## headers within the context block)."""
|
|
10
|
+
|
|
11
|
+
raw: str
|
|
12
|
+
sections: Dict[str, str] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
def query(self, section: Optional[str] = None) -> str:
|
|
15
|
+
if section is None:
|
|
16
|
+
return self.raw
|
|
17
|
+
key = section.lower().strip()
|
|
18
|
+
for name, content in self.sections.items():
|
|
19
|
+
if name.lower() == key:
|
|
20
|
+
return content
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
return self.raw
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ScopeConfig:
|
|
29
|
+
"""Parsed .scope file."""
|
|
30
|
+
|
|
31
|
+
path: str # Absolute path to the .scope file
|
|
32
|
+
description: str
|
|
33
|
+
includes: List[str] = field(default_factory=list)
|
|
34
|
+
excludes: List[str] = field(default_factory=list)
|
|
35
|
+
context: Optional[StructuredContext] = None
|
|
36
|
+
related: List[str] = field(default_factory=list)
|
|
37
|
+
owners: List[str] = field(default_factory=list)
|
|
38
|
+
tags: List[str] = field(default_factory=list)
|
|
39
|
+
tokens_estimate: Optional[int] = None
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def context_str(self) -> str:
|
|
43
|
+
if self.context is None:
|
|
44
|
+
return ""
|
|
45
|
+
return self.context.raw
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def directory(self) -> str:
|
|
49
|
+
"""Directory containing this .scope file."""
|
|
50
|
+
import os
|
|
51
|
+
|
|
52
|
+
return os.path.dirname(self.path)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ScopeEntry:
|
|
57
|
+
"""Entry in the .scopes index file."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
path: str
|
|
61
|
+
keywords: List[str] = field(default_factory=list)
|
|
62
|
+
description: Optional[str] = None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ScopesIndex:
|
|
67
|
+
"""Parsed .scopes index file (repo root)."""
|
|
68
|
+
|
|
69
|
+
version: int = 1
|
|
70
|
+
scopes: Dict[str, ScopeEntry] = field(default_factory=dict)
|
|
71
|
+
defaults: Dict[str, object] = field(default_factory=dict)
|
|
72
|
+
total_repo_tokens: int = 0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def max_tokens(self) -> int:
|
|
76
|
+
return int(self.defaults.get("max_tokens", 8000))
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def include_related(self) -> bool:
|
|
80
|
+
return bool(self.defaults.get("include_related", False))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ResolvedScope:
|
|
85
|
+
"""Result of resolving a scope to concrete files."""
|
|
86
|
+
|
|
87
|
+
files: List[str] = field(default_factory=list)
|
|
88
|
+
context: str = ""
|
|
89
|
+
token_estimate: int = 0
|
|
90
|
+
scope_chain: List[str] = field(default_factory=list)
|
|
91
|
+
truncated: bool = False
|
|
92
|
+
excluded_files: List[str] = field(default_factory=list)
|
|
93
|
+
|
|
94
|
+
def merge(self, other: "ResolvedScope") -> "ResolvedScope":
|
|
95
|
+
"""Merge two resolved scopes (union)."""
|
|
96
|
+
seen = set(self.files)
|
|
97
|
+
merged_files = list(self.files)
|
|
98
|
+
for f in other.files:
|
|
99
|
+
if f not in seen:
|
|
100
|
+
merged_files.append(f)
|
|
101
|
+
seen.add(f)
|
|
102
|
+
|
|
103
|
+
ctx_parts = [p for p in [self.context, other.context] if p]
|
|
104
|
+
return ResolvedScope(
|
|
105
|
+
files=merged_files,
|
|
106
|
+
context="\n\n".join(ctx_parts),
|
|
107
|
+
token_estimate=self.token_estimate + other.token_estimate,
|
|
108
|
+
scope_chain=list(dict.fromkeys(self.scope_chain + other.scope_chain)),
|
|
109
|
+
truncated=self.truncated or other.truncated,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def subtract(self, other: "ResolvedScope") -> "ResolvedScope":
|
|
113
|
+
"""Remove files present in other scope."""
|
|
114
|
+
other_set = set(other.files)
|
|
115
|
+
return ResolvedScope(
|
|
116
|
+
files=[f for f in self.files if f not in other_set],
|
|
117
|
+
context=self.context,
|
|
118
|
+
token_estimate=0, # recalculated after
|
|
119
|
+
scope_chain=self.scope_chain,
|
|
120
|
+
truncated=self.truncated,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def intersect(self, other: "ResolvedScope") -> "ResolvedScope":
|
|
124
|
+
"""Keep only files present in both scopes."""
|
|
125
|
+
other_set = set(other.files)
|
|
126
|
+
ctx_parts = [p for p in [self.context, other.context] if p]
|
|
127
|
+
return ResolvedScope(
|
|
128
|
+
files=[f for f in self.files if f in other_set],
|
|
129
|
+
context="\n\n".join(ctx_parts),
|
|
130
|
+
token_estimate=0,
|
|
131
|
+
scope_chain=list(dict.fromkeys(self.scope_chain + other.scope_chain)),
|
|
132
|
+
truncated=self.truncated or other.truncated,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class TokenBudget:
|
|
138
|
+
"""Token budget for progressive file loading."""
|
|
139
|
+
|
|
140
|
+
max_tokens: int
|
|
141
|
+
context_reserved: int = 0
|
|
142
|
+
remaining: int = 0
|
|
143
|
+
|
|
144
|
+
def __post_init__(self):
|
|
145
|
+
self.remaining = self.max_tokens - self.context_reserved
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# AST analysis models
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
@dataclass
|
|
153
|
+
class ResolvedImport:
|
|
154
|
+
"""A single import statement, resolved to its structural meaning."""
|
|
155
|
+
raw: str
|
|
156
|
+
module: str = "" # Top-level module (e.g., "auth")
|
|
157
|
+
resolved_path: Optional[str] = None # Target file path, or None if external
|
|
158
|
+
names: List[str] = field(default_factory=list)
|
|
159
|
+
is_relative: bool = False
|
|
160
|
+
is_star: bool = False
|
|
161
|
+
is_conditional: bool = False
|
|
162
|
+
is_type_only: bool = False # Inside TYPE_CHECKING block
|
|
163
|
+
line: int = 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class ExportedSymbol:
|
|
168
|
+
"""A symbol exported by a module."""
|
|
169
|
+
name: str
|
|
170
|
+
kind: str # "function", "class", "constant", "variable"
|
|
171
|
+
is_public: bool = True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class ClassInfo:
|
|
176
|
+
"""Structural summary of a class definition."""
|
|
177
|
+
name: str
|
|
178
|
+
bases: List[str] = field(default_factory=list)
|
|
179
|
+
methods: List[str] = field(default_factory=list)
|
|
180
|
+
method_count: int = 0
|
|
181
|
+
decorators: List[str] = field(default_factory=list)
|
|
182
|
+
is_abstract: bool = False
|
|
183
|
+
is_public: bool = True
|
|
184
|
+
line: int = 0
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class FunctionInfo:
|
|
189
|
+
"""Structural summary of a function definition."""
|
|
190
|
+
name: str
|
|
191
|
+
params: List[str] = field(default_factory=list)
|
|
192
|
+
arg_count: int = 0
|
|
193
|
+
return_type: Optional[str] = None
|
|
194
|
+
decorators: List[str] = field(default_factory=list)
|
|
195
|
+
is_public: bool = True
|
|
196
|
+
is_async: bool = False
|
|
197
|
+
complexity: int = 0 # Count of if/for/while/try in body
|
|
198
|
+
line: int = 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class FileAnalysis:
|
|
203
|
+
"""Complete structural analysis of a single source file."""
|
|
204
|
+
path: str
|
|
205
|
+
language: str
|
|
206
|
+
imports: List[ResolvedImport] = field(default_factory=list)
|
|
207
|
+
exports: List[ExportedSymbol] = field(default_factory=list)
|
|
208
|
+
classes: List[ClassInfo] = field(default_factory=list)
|
|
209
|
+
functions: List[FunctionInfo] = field(default_factory=list)
|
|
210
|
+
decorators_used: List[str] = field(default_factory=list) # All unique decorators
|
|
211
|
+
is_init: bool = False # True for __init__.py
|
|
212
|
+
reexports: List[str] = field(default_factory=list) # Imported then re-exported
|
|
213
|
+
node_count: int = 0 # Total AST nodes (complexity proxy)
|
|
214
|
+
docstring: Optional[str] = None
|
|
215
|
+
all_list: Optional[List[str]] = None
|
|
216
|
+
is_entry_point: bool = False
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def public_api(self) -> List[str]:
|
|
220
|
+
if self.all_list is not None:
|
|
221
|
+
return self.all_list
|
|
222
|
+
names = []
|
|
223
|
+
for cls in self.classes:
|
|
224
|
+
if cls.is_public:
|
|
225
|
+
names.append(cls.name)
|
|
226
|
+
for fn in self.functions:
|
|
227
|
+
if fn.is_public:
|
|
228
|
+
names.append(fn.name)
|
|
229
|
+
for exp in self.exports:
|
|
230
|
+
if exp.is_public and exp.name not in names:
|
|
231
|
+
names.append(exp.name)
|
|
232
|
+
return names
|
|
233
|
+
|
|
234
|
+
@property
|
|
235
|
+
def import_paths(self) -> List[str]:
|
|
236
|
+
return [i.resolved_path for i in self.imports if i.resolved_path]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# Keep ModuleAPI as alias for backward compatibility during migration
|
|
240
|
+
ModuleAPI = FileAnalysis
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Graph dataclasses (data only, no builder functions)
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
@dataclass
|
|
248
|
+
class FileNode:
|
|
249
|
+
"""A file in the dependency graph."""
|
|
250
|
+
path: str # Relative to root
|
|
251
|
+
language: str
|
|
252
|
+
imports: List[str] = field(default_factory=list)
|
|
253
|
+
imported_by: List[str] = field(default_factory=list)
|
|
254
|
+
api: Optional[ModuleAPI] = None
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass
|
|
258
|
+
class ModuleBoundary:
|
|
259
|
+
"""A detected module boundary (candidate scope)."""
|
|
260
|
+
directory: str
|
|
261
|
+
files: List[str] = field(default_factory=list)
|
|
262
|
+
internal_edges: int = 0
|
|
263
|
+
external_edges: int = 0
|
|
264
|
+
external_deps: List[str] = field(default_factory=list)
|
|
265
|
+
depended_on_by: List[str] = field(default_factory=list)
|
|
266
|
+
cohesion: float = 0.0
|
|
267
|
+
churn: int = 0
|
|
268
|
+
hotspot_files: List[str] = field(default_factory=list)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@dataclass
|
|
272
|
+
class DependencyGraph:
|
|
273
|
+
"""Full dependency graph of a codebase."""
|
|
274
|
+
root: str
|
|
275
|
+
files: Dict[str, FileNode] = field(default_factory=dict)
|
|
276
|
+
edges: List[tuple] = field(default_factory=list)
|
|
277
|
+
modules: List[ModuleBoundary] = field(default_factory=list)
|
|
278
|
+
apis: Dict[str, ModuleAPI] = field(default_factory=dict)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@dataclass
|
|
282
|
+
class ConventionNode:
|
|
283
|
+
"""A file's membership in a convention, with any rule violations."""
|
|
284
|
+
name: str # Convention name, e.g. "REST Controller"
|
|
285
|
+
file_path: str
|
|
286
|
+
target_name: str # Class or function name
|
|
287
|
+
violations: List[str] = field(default_factory=list)
|
|
288
|
+
matched_by: List[str] = field(default_factory=list) # Which criteria matched
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""History data models: the empirical ledger from git mining."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class FileChange:
|
|
9
|
+
"""A file changed in a commit, with line counts."""
|
|
10
|
+
path: str
|
|
11
|
+
insertions: int = 0
|
|
12
|
+
deletions: int = 0
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def magnitude(self) -> int:
|
|
16
|
+
return self.insertions + self.deletions
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CommitInfo:
|
|
21
|
+
"""A single git commit."""
|
|
22
|
+
hash: str
|
|
23
|
+
timestamp: str
|
|
24
|
+
message: str
|
|
25
|
+
files: List[str] = field(default_factory=list)
|
|
26
|
+
changes: List[FileChange] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def total_lines(self) -> int:
|
|
30
|
+
return sum(c.magnitude for c in self.changes)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FileHistory:
|
|
35
|
+
"""History stats for a single file."""
|
|
36
|
+
path: str
|
|
37
|
+
commit_count: int = 0
|
|
38
|
+
total_lines_changed: int = 0
|
|
39
|
+
last_modified: str = ""
|
|
40
|
+
stability: str = "" # "stable", "volatile", "tweaked"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ChangeCoupling:
|
|
45
|
+
"""Two files that frequently change together."""
|
|
46
|
+
file_a: str
|
|
47
|
+
file_b: str
|
|
48
|
+
co_changes: int # How many commits touch both
|
|
49
|
+
total_a: int # Total commits touching A
|
|
50
|
+
total_b: int # Total commits touching B
|
|
51
|
+
coupling_strength: float = 0.0 # co_changes / min(total_a, total_b)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ImplicitContract:
|
|
56
|
+
"""An observed pattern: when X changes, Y always changes too."""
|
|
57
|
+
trigger_file: str
|
|
58
|
+
coupled_file: str
|
|
59
|
+
confidence: float # How often Y changes when X changes
|
|
60
|
+
occurrences: int
|
|
61
|
+
description: str = ""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class HistoryAnalysis:
|
|
66
|
+
"""Full git history analysis results."""
|
|
67
|
+
commits_analyzed: int = 0
|
|
68
|
+
file_histories: Dict[str, FileHistory] = field(default_factory=dict)
|
|
69
|
+
hotspots: List[Tuple[str, int]] = field(default_factory=list) # (file, churn)
|
|
70
|
+
change_couplings: List[ChangeCoupling] = field(default_factory=list)
|
|
71
|
+
implicit_contracts: List[ImplicitContract] = field(default_factory=list)
|
|
72
|
+
recent_summaries: Dict[str, List[str]] = field(default_factory=dict) # module -> commit messages
|
|
73
|
+
module_churn: Dict[str, int] = field(default_factory=dict) # module -> total changes
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Intent data models: the human rulebook for enforcement."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Severity(Enum):
|
|
9
|
+
GUARD = "guard" # Blocks commit. Protective wall.
|
|
10
|
+
NUDGE = "nudge" # Prints warning. Does not block. Course correction.
|
|
11
|
+
NOTE = "note" # Informational.
|
|
12
|
+
HOLD = "hold" # Backwards compat — treated same as GUARD
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def blocks_commit(self) -> bool:
|
|
16
|
+
return self.value in ("guard", "hold")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CheckCategory(Enum):
|
|
20
|
+
BOUNDARY = "boundary_violation"
|
|
21
|
+
CONTRACT = "implicit_contract"
|
|
22
|
+
ANTIPATTERN = "anti_pattern"
|
|
23
|
+
DIRECTION = "dependency_direction"
|
|
24
|
+
STABILITY = "stability_concern"
|
|
25
|
+
INTENT = "architectural_intent"
|
|
26
|
+
CONVENTION = "convention_violation"
|
|
27
|
+
VOICE = "voice_violation"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class IntentDirective:
|
|
32
|
+
"""A declared architectural intent."""
|
|
33
|
+
directive: str # "decouple", "deprecate", "freeze", "consolidate"
|
|
34
|
+
modules: List[str] = field(default_factory=list)
|
|
35
|
+
files: List[str] = field(default_factory=list)
|
|
36
|
+
reason: str = ""
|
|
37
|
+
replacement: Optional[str] = None
|
|
38
|
+
target: Optional[str] = None
|
|
39
|
+
set_by: str = "developer"
|
|
40
|
+
set_at: str = ""
|
|
41
|
+
id: str = "" # Auto-generated slug
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Constraint:
|
|
46
|
+
"""A single constraint surfaced during resolve_scope (prophylactic mode)."""
|
|
47
|
+
category: str # "contract", "anti_pattern", "dependency_boundary", "stability", "intent"
|
|
48
|
+
message: str
|
|
49
|
+
file: Optional[str] = None
|
|
50
|
+
confidence: float = 1.0
|
|
51
|
+
metadata: Dict[str, object] = field(default_factory=dict)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class ProposedFix:
|
|
56
|
+
"""A machine-generated fix proposal the agent can apply."""
|
|
57
|
+
file: str
|
|
58
|
+
reason: str
|
|
59
|
+
predicted_sections: List[str] = field(default_factory=list)
|
|
60
|
+
proposed_diff: Optional[str] = None # Unified diff
|
|
61
|
+
confidence: float = 0.5
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class CheckResult:
|
|
66
|
+
"""A single check finding."""
|
|
67
|
+
passed: bool
|
|
68
|
+
category: CheckCategory
|
|
69
|
+
severity: Severity
|
|
70
|
+
message: str
|
|
71
|
+
detail: str = ""
|
|
72
|
+
file: Optional[str] = None
|
|
73
|
+
suggestion: Optional[str] = None
|
|
74
|
+
proposed_fix: Optional[ProposedFix] = None
|
|
75
|
+
can_acknowledge: bool = False
|
|
76
|
+
acknowledge_id: Optional[str] = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class CheckReport:
|
|
81
|
+
"""Aggregate report from all checks against a diff."""
|
|
82
|
+
passed: bool
|
|
83
|
+
results: List[CheckResult] = field(default_factory=list)
|
|
84
|
+
files_checked: int = 0
|
|
85
|
+
checks_run: int = 0
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def guards(self) -> List[CheckResult]:
|
|
89
|
+
return [r for r in self.results if not r.passed and r.severity.blocks_commit]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def nudges(self) -> List[CheckResult]:
|
|
93
|
+
return [r for r in self.results if not r.passed and r.severity == Severity.NUDGE]
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def notes(self) -> List[CheckResult]:
|
|
97
|
+
return [r for r in self.results if not r.passed and r.severity == Severity.NOTE]
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def holds(self) -> List[CheckResult]:
|
|
101
|
+
"""Backwards compat alias for guards."""
|
|
102
|
+
return self.guards
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Assertions
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
class ContextExhaustionError(Exception):
|
|
110
|
+
"""Token budget cannot satisfy required assertions."""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
assertion_type: str,
|
|
115
|
+
detail: str,
|
|
116
|
+
file: Optional[str] = None,
|
|
117
|
+
file_tokens: int = 0,
|
|
118
|
+
budget: int = 0,
|
|
119
|
+
tokens_used: int = 0,
|
|
120
|
+
reason: str = "",
|
|
121
|
+
suggestion: str = "",
|
|
122
|
+
):
|
|
123
|
+
self.assertion_type = assertion_type
|
|
124
|
+
self.detail = detail
|
|
125
|
+
self.file = file
|
|
126
|
+
self.file_tokens = file_tokens
|
|
127
|
+
self.budget = budget
|
|
128
|
+
self.tokens_used = tokens_used
|
|
129
|
+
self.reason = reason
|
|
130
|
+
self.suggestion = suggestion
|
|
131
|
+
super().__init__(detail)
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict:
|
|
134
|
+
return {
|
|
135
|
+
"error": "context_exhaustion",
|
|
136
|
+
"assertion_failed": {
|
|
137
|
+
"type": self.assertion_type,
|
|
138
|
+
"detail": self.detail,
|
|
139
|
+
"file": self.file,
|
|
140
|
+
"file_tokens": self.file_tokens,
|
|
141
|
+
"budget": self.budget,
|
|
142
|
+
"reason": self.reason,
|
|
143
|
+
},
|
|
144
|
+
"suggestion": self.suggestion,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class Assertion:
|
|
150
|
+
"""A single architectural assertion."""
|
|
151
|
+
scope: str = "*" # Scope name or "*" for all
|
|
152
|
+
ensure_includes: List[str] = field(default_factory=list)
|
|
153
|
+
ensure_context_contains: List[str] = field(default_factory=list)
|
|
154
|
+
ensure_constraints: bool = False
|
|
155
|
+
reason: str = ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
# Near-miss dataclasses
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class WarningPair:
|
|
164
|
+
"""An extracted (anti_pattern, safe_pattern) pair from scope context."""
|
|
165
|
+
anti_pattern: str
|
|
166
|
+
safe_pattern: str
|
|
167
|
+
context_line: str
|
|
168
|
+
scope: str
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class NearMiss:
|
|
173
|
+
"""A detected near-miss event."""
|
|
174
|
+
scope: str
|
|
175
|
+
event: str
|
|
176
|
+
context_used: str
|
|
177
|
+
potential_impact: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Convention dataclasses
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class ConventionRule:
|
|
186
|
+
"""A structural convention: discovered or hand-authored."""
|
|
187
|
+
name: str
|
|
188
|
+
source: str = "discovered" # "discovered" | "hand_authored"
|
|
189
|
+
match_criteria: Dict[str, list] = field(default_factory=dict) # {"any_of": [...], "all_of": [...]}
|
|
190
|
+
rules: Dict[str, object] = field(default_factory=dict) # {"prohibited_imports": [...], ...}
|
|
191
|
+
description: str = ""
|
|
192
|
+
compliance: float = 1.0
|
|
193
|
+
last_checked: Optional[str] = None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# Voice dataclasses
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class DiscoveredVoice:
|
|
202
|
+
"""Coding style profile: discovered from codebase or prescriptive defaults."""
|
|
203
|
+
mode: str = "adaptive" # "prescriptive" | "adaptive"
|
|
204
|
+
rules: Dict[str, str] = field(default_factory=dict)
|
|
205
|
+
stats: Dict[str, float] = field(default_factory=dict)
|
|
206
|
+
enforce: Dict[str, object] = field(default_factory=dict)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@dataclass
|
|
210
|
+
class CanonicalExample:
|
|
211
|
+
"""A representative file for a convention's coding style."""
|
|
212
|
+
file_path: str = ""
|
|
213
|
+
snippet: Optional[str] = None
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Pass data models: ephemeral outputs from analysis passes."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from .core import ConventionNode, DependencyGraph, ScopeConfig, ScopesIndex, ScopeEntry
|
|
7
|
+
from .history import HistoryAnalysis
|
|
8
|
+
from .intent import ConventionRule
|
|
9
|
+
from .state import BacktestReport
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class IngestPlan:
|
|
14
|
+
"""Plan for .scope files to be created."""
|
|
15
|
+
root: str
|
|
16
|
+
scopes: List["PlannedScope"] = field(default_factory=list)
|
|
17
|
+
index: Optional[ScopesIndex] = None
|
|
18
|
+
graph_summary: str = ""
|
|
19
|
+
history_summary: str = ""
|
|
20
|
+
backtest_summary: str = ""
|
|
21
|
+
# Structured data for discovery rendering
|
|
22
|
+
graph: Optional[DependencyGraph] = None
|
|
23
|
+
history: Optional[HistoryAnalysis] = None
|
|
24
|
+
backtest_report: Optional[BacktestReport] = None
|
|
25
|
+
virtual_scopes: List[ScopeConfig] = field(default_factory=list)
|
|
26
|
+
discovered_conventions: List[ConventionRule] = field(default_factory=list)
|
|
27
|
+
total_repo_files: int = 0
|
|
28
|
+
total_repo_tokens: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class PlannedScope:
|
|
33
|
+
"""A .scope file to be created."""
|
|
34
|
+
directory: str # Relative to root
|
|
35
|
+
config: ScopeConfig
|
|
36
|
+
confidence: float # How confident we are in this scope boundary
|
|
37
|
+
signals: List[str] # What signals contributed to this scope
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class VirtualScope:
|
|
42
|
+
"""A detected cross-cutting scope."""
|
|
43
|
+
name: str
|
|
44
|
+
hub_file: str
|
|
45
|
+
files: List[str]
|
|
46
|
+
cohesion: float
|
|
47
|
+
directories_spanned: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class SemanticDiffReport:
|
|
52
|
+
"""Structural diff translated to convention-level changes."""
|
|
53
|
+
added: List[ConventionNode] = field(default_factory=list)
|
|
54
|
+
removed: List[ConventionNode] = field(default_factory=list)
|
|
55
|
+
modified: List[tuple] = field(default_factory=list) # (before, after) pairs
|
|
56
|
+
dependency_changes: List[str] = field(default_factory=list)
|
|
57
|
+
all_conventions_upheld: bool = True
|
|
58
|
+
counterfactual: Optional[str] = None
|