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.
Files changed (114) hide show
  1. dotscope/.scope +63 -0
  2. dotscope/__init__.py +3 -0
  3. dotscope/absorber.py +390 -0
  4. dotscope/assertions.py +128 -0
  5. dotscope/ast_analyzer.py +2 -0
  6. dotscope/backtest.py +2 -0
  7. dotscope/bench.py +141 -0
  8. dotscope/budget.py +3 -0
  9. dotscope/cache.py +2 -0
  10. dotscope/check/__init__.py +1 -0
  11. dotscope/check/acknowledge.py +2 -0
  12. dotscope/check/checker.py +3 -0
  13. dotscope/check/checks/__init__.py +1 -0
  14. dotscope/check/checks/antipattern.py +2 -0
  15. dotscope/check/checks/boundary.py +2 -0
  16. dotscope/check/checks/contracts.py +3 -0
  17. dotscope/check/checks/direction.py +2 -0
  18. dotscope/check/checks/intent.py +2 -0
  19. dotscope/check/checks/stability.py +2 -0
  20. dotscope/check/constraints.py +2 -0
  21. dotscope/check/models.py +15 -0
  22. dotscope/cli.py +1447 -0
  23. dotscope/composer.py +147 -0
  24. dotscope/constants.py +45 -0
  25. dotscope/context.py +60 -0
  26. dotscope/counterfactual.py +180 -0
  27. dotscope/debug.py +220 -0
  28. dotscope/discovery.py +104 -0
  29. dotscope/formatter.py +157 -0
  30. dotscope/graph.py +3 -0
  31. dotscope/health.py +212 -0
  32. dotscope/help.py +204 -0
  33. dotscope/history.py +6 -0
  34. dotscope/hooks.py +2 -0
  35. dotscope/ingest.py +858 -0
  36. dotscope/intent.py +618 -0
  37. dotscope/lessons.py +223 -0
  38. dotscope/matcher.py +104 -0
  39. dotscope/mcp_server.py +1081 -0
  40. dotscope/models/.scope +45 -0
  41. dotscope/models/__init__.py +7 -0
  42. dotscope/models/core.py +288 -0
  43. dotscope/models/history.py +73 -0
  44. dotscope/models/intent.py +213 -0
  45. dotscope/models/passes.py +58 -0
  46. dotscope/models/state.py +250 -0
  47. dotscope/models.py +9 -0
  48. dotscope/near_miss.py +3 -0
  49. dotscope/onboarding.py +2 -0
  50. dotscope/parser.py +387 -0
  51. dotscope/passes/.scope +105 -0
  52. dotscope/passes/__init__.py +1 -0
  53. dotscope/passes/ast_analyzer.py +508 -0
  54. dotscope/passes/backtest.py +198 -0
  55. dotscope/passes/budget_allocator.py +164 -0
  56. dotscope/passes/convention_compliance.py +40 -0
  57. dotscope/passes/convention_discovery.py +247 -0
  58. dotscope/passes/convention_parser.py +223 -0
  59. dotscope/passes/graph_builder.py +299 -0
  60. dotscope/passes/history_miner.py +336 -0
  61. dotscope/passes/incremental.py +149 -0
  62. dotscope/passes/lang/__init__.py +38 -0
  63. dotscope/passes/lang/_base.py +20 -0
  64. dotscope/passes/lang/_treesitter.py +93 -0
  65. dotscope/passes/lang/go.py +333 -0
  66. dotscope/passes/lang/javascript.py +348 -0
  67. dotscope/passes/lazy.py +152 -0
  68. dotscope/passes/semantic_diff.py +160 -0
  69. dotscope/passes/sentinel/__init__.py +1 -0
  70. dotscope/passes/sentinel/acknowledge.py +222 -0
  71. dotscope/passes/sentinel/checker.py +383 -0
  72. dotscope/passes/sentinel/checks/__init__.py +1 -0
  73. dotscope/passes/sentinel/checks/antipattern.py +84 -0
  74. dotscope/passes/sentinel/checks/boundary.py +46 -0
  75. dotscope/passes/sentinel/checks/contracts.py +148 -0
  76. dotscope/passes/sentinel/checks/convention.py +54 -0
  77. dotscope/passes/sentinel/checks/direction.py +71 -0
  78. dotscope/passes/sentinel/checks/intent.py +207 -0
  79. dotscope/passes/sentinel/checks/stability.py +66 -0
  80. dotscope/passes/sentinel/checks/voice.py +108 -0
  81. dotscope/passes/sentinel/constraints.py +472 -0
  82. dotscope/passes/sentinel/line_filter.py +88 -0
  83. dotscope/passes/sentinel/models.py +15 -0
  84. dotscope/passes/virtual.py +239 -0
  85. dotscope/passes/voice.py +162 -0
  86. dotscope/passes/voice_defaults.py +28 -0
  87. dotscope/passes/voice_discovery.py +245 -0
  88. dotscope/paths.py +32 -0
  89. dotscope/progress.py +44 -0
  90. dotscope/regression.py +147 -0
  91. dotscope/resolver.py +203 -0
  92. dotscope/scanner.py +246 -0
  93. dotscope/sessions.py +2 -0
  94. dotscope/storage/.scope +64 -0
  95. dotscope/storage/__init__.py +1 -0
  96. dotscope/storage/cache.py +114 -0
  97. dotscope/storage/claude_hooks.py +119 -0
  98. dotscope/storage/git_hooks.py +277 -0
  99. dotscope/storage/incremental_state.py +61 -0
  100. dotscope/storage/mcp_config.py +98 -0
  101. dotscope/storage/near_miss.py +183 -0
  102. dotscope/storage/onboarding.py +150 -0
  103. dotscope/storage/session_manager.py +195 -0
  104. dotscope/storage/timing.py +84 -0
  105. dotscope/timing.py +2 -0
  106. dotscope/tokens.py +53 -0
  107. dotscope/utility.py +123 -0
  108. dotscope/virtual.py +3 -0
  109. dotscope/visibility.py +664 -0
  110. dotscope-0.1.0.dist-info/METADATA +50 -0
  111. dotscope-0.1.0.dist-info/RECORD +114 -0
  112. dotscope-0.1.0.dist-info/WHEEL +4 -0
  113. dotscope-0.1.0.dist-info/entry_points.txt +3 -0
  114. 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
@@ -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