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/discovery.py ADDED
@@ -0,0 +1,104 @@
1
+ """Discover repo roots, .scope files, and .scopes index files."""
2
+
3
+
4
+ import os
5
+ from typing import List, Optional
6
+
7
+ from .constants import SKIP_DIRS
8
+ from .models import ScopeConfig, ScopesIndex
9
+
10
+
11
+ def find_repo_root(start_dir: Optional[str] = None) -> Optional[str]:
12
+ """Walk up from start_dir looking for .scopes, .git, or a .scope file.
13
+
14
+ Returns the directory containing the marker, or None.
15
+ """
16
+ current = os.path.abspath(start_dir or os.getcwd())
17
+
18
+ while True:
19
+ if os.path.isfile(os.path.join(current, ".scopes")):
20
+ return current
21
+ if os.path.isdir(os.path.join(current, ".git")):
22
+ return current
23
+ if os.path.isfile(os.path.join(current, ".scope")):
24
+ return current
25
+
26
+ parent = os.path.dirname(current)
27
+ if parent == current:
28
+ break
29
+ current = parent
30
+
31
+ return None
32
+
33
+
34
+ def find_all_scopes(root: str) -> List[str]:
35
+ """Find all .scope files under root, returning absolute paths.
36
+
37
+ Skips common directories that should never be scanned.
38
+ """
39
+ scope_files = []
40
+ for dirpath, dirnames, filenames in os.walk(root):
41
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
42
+
43
+ if ".scope" in filenames:
44
+ scope_files.append(os.path.join(dirpath, ".scope"))
45
+
46
+ return sorted(scope_files)
47
+
48
+
49
+ def load_index(root: str) -> Optional[ScopesIndex]:
50
+ """Load .scopes index from repo root, if it exists."""
51
+ from .parser import parse_scopes_index
52
+
53
+ index_path = os.path.join(root, ".scopes")
54
+ if os.path.isfile(index_path):
55
+ return parse_scopes_index(index_path)
56
+ return None
57
+
58
+
59
+ def find_scope(name_or_path: str, root: Optional[str] = None) -> Optional[ScopeConfig]:
60
+ """Resolve a scope by name (from index), path, or directory.
61
+
62
+ Resolution order:
63
+ 1. If name_or_path is a file path ending in .scope, parse it directly
64
+ 2. If name_or_path is a directory containing .scope, parse that
65
+ 3. Look up in .scopes index by name
66
+ 4. Look for name_or_path/.scope relative to root
67
+ """
68
+ from .parser import parse_scope_file
69
+
70
+ # Direct .scope file path
71
+ if name_or_path.endswith(".scope") and os.path.isfile(name_or_path):
72
+ return parse_scope_file(name_or_path)
73
+
74
+ # Absolute directory containing .scope
75
+ if os.path.isdir(name_or_path):
76
+ scope_path = os.path.join(name_or_path, ".scope")
77
+ if os.path.isfile(scope_path):
78
+ return parse_scope_file(scope_path)
79
+
80
+ # Need root for index and relative lookups
81
+ if root is None:
82
+ root = find_repo_root()
83
+ if root is None:
84
+ return None
85
+
86
+ # Check .scopes index
87
+ index = load_index(root)
88
+ if index and name_or_path in index.scopes:
89
+ entry = index.scopes[name_or_path]
90
+ scope_path = os.path.join(root, entry.path)
91
+ if os.path.isfile(scope_path):
92
+ return parse_scope_file(scope_path)
93
+
94
+ # Try as relative directory
95
+ candidate = os.path.join(root, name_or_path, ".scope")
96
+ if os.path.isfile(candidate):
97
+ return parse_scope_file(candidate)
98
+
99
+ # Try as relative path to .scope
100
+ candidate = os.path.join(root, name_or_path)
101
+ if candidate.endswith(".scope") and os.path.isfile(candidate):
102
+ return parse_scope_file(candidate)
103
+
104
+ return None
dotscope/formatter.py ADDED
@@ -0,0 +1,157 @@
1
+ """Output formatting: plain, json, cursor."""
2
+
3
+ import json
4
+ import os
5
+ from typing import Optional
6
+
7
+ from .models import ResolvedScope
8
+ from .paths import make_relative
9
+
10
+
11
+ def format_resolved(
12
+ resolved: ResolvedScope,
13
+ fmt: str = "plain",
14
+ root: Optional[str] = None,
15
+ show_tokens: bool = False,
16
+ ) -> str:
17
+ """Format a resolved scope for output.
18
+
19
+ Formats:
20
+ plain — One file path per line, with comments for context/excludes
21
+ json — Full JSON object
22
+ cursor — .cursorrules-style: context + file list for pasting into agent prompts
23
+ """
24
+ if fmt == "json":
25
+ return _format_json(resolved, root)
26
+ elif fmt == "cursor":
27
+ return _format_cursor(resolved, root)
28
+ else:
29
+ return _format_plain(resolved, root, show_tokens)
30
+
31
+
32
+ def _format_plain(resolved: ResolvedScope, root: Optional[str], show_tokens: bool) -> str:
33
+ """Plain format: one file per line."""
34
+ lines = []
35
+
36
+ for f in resolved.files:
37
+ path = make_relative(f, root)
38
+ if show_tokens:
39
+ from .tokens import estimate_file_tokens
40
+ tokens = estimate_file_tokens(f)
41
+ lines.append(f"{path} # {tokens} tokens")
42
+ else:
43
+ lines.append(path)
44
+
45
+ if resolved.excluded_files:
46
+ lines.append("")
47
+ lines.append(f"# Excluded: {len(resolved.excluded_files)} files")
48
+
49
+ if resolved.truncated:
50
+ lines.append(f"# Truncated to fit token budget ({resolved.token_estimate} tokens)")
51
+
52
+ if resolved.context:
53
+ lines.append("")
54
+ lines.append(f"# Context: {len(resolved.context)} chars, from {len(resolved.scope_chain)} scope(s)")
55
+
56
+ return "\n".join(lines)
57
+
58
+
59
+ def _format_json(resolved: ResolvedScope, root: Optional[str]) -> str:
60
+ """JSON format: full object."""
61
+ data = {
62
+ "files": [make_relative(f, root) for f in resolved.files],
63
+ "context": resolved.context,
64
+ "token_estimate": resolved.token_estimate,
65
+ "scope_chain": [make_relative(s, root) for s in resolved.scope_chain],
66
+ "truncated": resolved.truncated,
67
+ "file_count": len(resolved.files),
68
+ }
69
+ if resolved.excluded_files:
70
+ data["excluded_count"] = len(resolved.excluded_files)
71
+
72
+ return json.dumps(data, indent=2)
73
+
74
+
75
+ def _format_cursor(resolved: ResolvedScope, root: Optional[str]) -> str:
76
+ """Cursor-style format for pasting into agent prompts or .cursorrules."""
77
+ parts = []
78
+
79
+ if resolved.context:
80
+ parts.append("# Scope Context")
81
+ parts.append("")
82
+ parts.append(resolved.context)
83
+ parts.append("")
84
+
85
+ if resolved.files:
86
+ parts.append("# Relevant Files")
87
+ parts.append("")
88
+ for f in resolved.files:
89
+ parts.append(f"- {make_relative(f, root)}")
90
+
91
+ if resolved.truncated:
92
+ parts.append("")
93
+ parts.append(f"# Note: file list truncated to {resolved.token_estimate} tokens")
94
+
95
+ return "\n".join(parts)
96
+
97
+
98
+ def format_stats(
99
+ scope_stats: list,
100
+ total_files: int,
101
+ total_tokens: int,
102
+ ) -> str:
103
+ """Format the stats report."""
104
+ lines = [
105
+ f"Repository: {total_files} files, ~{total_tokens:,} tokens",
106
+ "",
107
+ f"{'Scope':<20} {'Files':>6} {'Tokens':>8} {'Savings':>8}",
108
+ ]
109
+
110
+ for name, file_count, token_count in scope_stats:
111
+ if total_tokens > 0:
112
+ savings = (1 - token_count / total_tokens) * 100
113
+ lines.append(f"{name:<20} {file_count:>6} {token_count:>8,} {savings:>7.1f}%")
114
+ else:
115
+ lines.append(f"{name:<20} {file_count:>6} {token_count:>8,} N/A")
116
+
117
+ if scope_stats and total_tokens > 0:
118
+ avg_savings = sum(
119
+ (1 - tc / total_tokens) * 100 for _, _, tc in scope_stats
120
+ ) / len(scope_stats)
121
+ lines.append("")
122
+ lines.append(f"Average context reduction: {avg_savings:.1f}%")
123
+
124
+ avg_tokens = sum(tc for _, _, tc in scope_stats) / len(scope_stats)
125
+ cost_per_call = (total_tokens - avg_tokens) / 1_000_000 * 3
126
+ lines.append(f"Estimated cost savings at $3/M tokens: ${cost_per_call:.2f} per agent call")
127
+
128
+ return "\n".join(lines)
129
+
130
+
131
+ def format_tree(scopes: list, root: str) -> str:
132
+ """Format a visual tree of scopes and relationships."""
133
+ if not scopes:
134
+ return "No .scope files found."
135
+
136
+ lines = [os.path.basename(root) + "/"]
137
+
138
+ for i, (scope_path, config) in enumerate(scopes):
139
+ is_last = i == len(scopes) - 1
140
+ prefix = "└── " if is_last else "├── "
141
+ rel = os.path.relpath(scope_path, root)
142
+ desc = config.description if config else "?"
143
+
144
+ lines.append(f"{prefix}{rel}")
145
+ lines.append(f"{' ' if is_last else '│ '} {desc}")
146
+
147
+ if config and config.related:
148
+ for j, related in enumerate(config.related):
149
+ r_is_last = j == len(config.related) - 1
150
+ r_prefix = " └─→ " if r_is_last else " ├─→ "
151
+ if not is_last:
152
+ r_prefix = "│ " + r_prefix[4:]
153
+ lines.append(f"{r_prefix}{related}")
154
+
155
+ return "\n".join(lines)
156
+
157
+
dotscope/graph.py ADDED
@@ -0,0 +1,3 @@
1
+ """Backward-compatibility stub. Moved to dotscope.passes.graph_builder."""
2
+ from .passes.graph_builder import * # noqa: F401,F403
3
+ from .models.core import DependencyGraph, FileNode, ModuleBoundary # noqa: F401
dotscope/health.py ADDED
@@ -0,0 +1,212 @@
1
+ """Scope health monitoring: staleness, coverage gaps, import drift."""
2
+
3
+
4
+ import os
5
+ import re
6
+ from typing import List, Optional, Set
7
+
8
+ from .constants import SKIP_DIRS, SOURCE_EXTS
9
+ from .models import HealthIssue, HealthReport, ScopeConfig
10
+
11
+
12
+ def full_health_report(root: str) -> HealthReport:
13
+ """Run all health checks and return a combined report."""
14
+ from .discovery import find_all_scopes
15
+ from .parser import parse_scope_file
16
+
17
+ scope_files = find_all_scopes(root)
18
+ issues: List[HealthIssue] = []
19
+ scoped_dirs: Set[str] = set()
20
+
21
+ for sf in scope_files:
22
+ try:
23
+ config = parse_scope_file(sf)
24
+ except (ValueError, IOError) as e:
25
+ issues.append(HealthIssue(
26
+ scope_path=sf, severity="error",
27
+ category="parse", message=str(e),
28
+ ))
29
+ continue
30
+
31
+ scoped_dirs.add(os.path.dirname(sf))
32
+
33
+ issues.extend(check_staleness(config, root))
34
+ issues.extend(check_broken_paths(config, root))
35
+ issues.extend(check_import_drift(config))
36
+
37
+ # Coverage check
38
+ all_dirs = _find_source_dirs(root)
39
+ uncovered = all_dirs - scoped_dirs
40
+ coverage_issues = check_coverage(uncovered, root)
41
+ issues.extend(coverage_issues)
42
+
43
+ return HealthReport(
44
+ issues=issues,
45
+ scopes_checked=len(scope_files),
46
+ directories_total=len(all_dirs),
47
+ directories_covered=len(scoped_dirs),
48
+ )
49
+
50
+
51
+ def check_staleness(config: ScopeConfig, root: str) -> List[HealthIssue]:
52
+ """Check if files in scope have been modified more recently than the .scope file."""
53
+ issues = []
54
+ scope_mtime = _get_mtime(config.path)
55
+ if scope_mtime is None:
56
+ return issues
57
+
58
+ stale_files = []
59
+
60
+ for inc in config.includes:
61
+ full = os.path.normpath(os.path.join(root, inc))
62
+ if inc.endswith("/") or os.path.isdir(full.rstrip("/")):
63
+ dir_path = full.rstrip("/")
64
+ if os.path.isdir(dir_path):
65
+ for dirpath, _, filenames in os.walk(dir_path):
66
+ for fn in filenames:
67
+ fp = os.path.join(dirpath, fn)
68
+ fmtime = _get_mtime(fp)
69
+ if fmtime and fmtime > scope_mtime:
70
+ stale_files.append(os.path.relpath(fp, root))
71
+ elif os.path.isfile(full):
72
+ fmtime = _get_mtime(full)
73
+ if fmtime and fmtime > scope_mtime:
74
+ stale_files.append(os.path.relpath(full, root))
75
+
76
+ if stale_files:
77
+ count = len(stale_files)
78
+ sample = stale_files[:3]
79
+ msg = f"{count} file(s) modified since .scope was last updated: {', '.join(sample)}"
80
+ if count > 3:
81
+ msg += f" (+{count - 3} more)"
82
+ issues.append(HealthIssue(
83
+ scope_path=config.path, severity="warning",
84
+ category="staleness", message=msg,
85
+ ))
86
+
87
+ return issues
88
+
89
+
90
+ def check_broken_paths(config: ScopeConfig, root: str = "") -> List[HealthIssue]:
91
+ """Check for include/related paths that don't exist."""
92
+ from .paths import path_exists, strip_inline_comment
93
+
94
+ issues = []
95
+ scope_dir = config.directory
96
+ base = root or scope_dir
97
+
98
+ for inc in config.includes:
99
+ if not path_exists(base, inc):
100
+ issues.append(HealthIssue(
101
+ scope_path=config.path, severity="error",
102
+ category="broken_path", message=f"include not found: {inc}",
103
+ ))
104
+
105
+ for rel in config.related:
106
+ clean = strip_inline_comment(rel)
107
+ # Related paths are repo-root-relative; try root first, then scope dir
108
+ if not path_exists(base, clean) and not path_exists(scope_dir, clean):
109
+ issues.append(HealthIssue(
110
+ scope_path=config.path, severity="warning",
111
+ category="broken_path", message=f"related scope not found: {clean}",
112
+ ))
113
+
114
+ return issues
115
+
116
+
117
+ def check_import_drift(config: ScopeConfig) -> List[HealthIssue]:
118
+ """Check if imports in scoped files reference modules not in includes."""
119
+ issues = []
120
+ scope_dir = config.directory
121
+ included_dirs = set()
122
+
123
+ for inc in config.includes:
124
+ full = os.path.normpath(os.path.join(scope_dir, inc))
125
+ if inc.endswith("/") or os.path.isdir(full.rstrip("/")):
126
+ included_dirs.add(os.path.basename(full.rstrip("/")))
127
+ else:
128
+ included_dirs.add(os.path.dirname(inc).split("/")[0] if "/" in inc else "")
129
+
130
+ # Find Python files in includes and check their imports
131
+ drifted = set()
132
+ parent = os.path.dirname(scope_dir)
133
+
134
+ for inc in config.includes:
135
+ full = os.path.normpath(os.path.join(scope_dir, inc))
136
+ files = []
137
+ if os.path.isdir(full.rstrip("/")):
138
+ for dp, _, fns in os.walk(full.rstrip("/")):
139
+ for fn in fns:
140
+ if fn.endswith(".py"):
141
+ files.append(os.path.join(dp, fn))
142
+ elif full.endswith(".py") and os.path.isfile(full):
143
+ files.append(full)
144
+
145
+ for f in files:
146
+ try:
147
+ with open(f, "r", encoding="utf-8", errors="replace") as fh:
148
+ for line in fh:
149
+ m = re.match(r"from\s+([\w.]+)\s+import", line.strip())
150
+ if m:
151
+ module = m.group(1).split(".")[0]
152
+ candidate = os.path.join(parent, module)
153
+ if (
154
+ os.path.isdir(candidate)
155
+ and module not in included_dirs
156
+ and candidate != scope_dir
157
+ and module not in SKIP_DIRS
158
+ ):
159
+ drifted.add(module)
160
+ except (IOError, OSError):
161
+ continue
162
+
163
+ if drifted:
164
+ msg = f"imports reference modules not in includes: {', '.join(sorted(drifted))}"
165
+ issues.append(HealthIssue(
166
+ scope_path=config.path, severity="info",
167
+ category="drift", message=msg,
168
+ ))
169
+
170
+ return issues
171
+
172
+
173
+ def check_coverage(uncovered: Set[str], root: str) -> List[HealthIssue]:
174
+ """Report directories that have source files but no .scope."""
175
+ issues = []
176
+ for d in sorted(uncovered):
177
+ rel = os.path.relpath(d, root)
178
+ # Only report if directory has source files
179
+ has_source = any(
180
+ fn.endswith((".py", ".js", ".ts", ".go", ".rs", ".rb", ".java"))
181
+ for fn in os.listdir(d)
182
+ if os.path.isfile(os.path.join(d, fn))
183
+ )
184
+ if has_source:
185
+ issues.append(HealthIssue(
186
+ scope_path="", severity="info",
187
+ category="coverage", message=f"no .scope file: {rel}/",
188
+ ))
189
+
190
+ return issues
191
+
192
+
193
+ def _find_source_dirs(root: str) -> Set[str]:
194
+ """Find all directories under root that contain source files."""
195
+ source_exts = SOURCE_EXTS
196
+ dirs = set()
197
+
198
+ for dirpath, dirnames, filenames in os.walk(root):
199
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
200
+
201
+ if any(os.path.splitext(f)[1] in source_exts for f in filenames):
202
+ dirs.add(dirpath)
203
+
204
+ return dirs
205
+
206
+
207
+ def _get_mtime(path: str) -> Optional[float]:
208
+ """Get file modification time, None if not accessible."""
209
+ try:
210
+ return os.path.getmtime(path)
211
+ except OSError:
212
+ return None
dotscope/help.py ADDED
@@ -0,0 +1,204 @@
1
+ """Hand-written help text following the voice spec.
2
+
3
+ Dense, no filler, examples before options. argparse handles parsing;
4
+ this module handles display.
5
+ """
6
+
7
+ HELP_ROOT = """\
8
+ Usage: dotscope <command>
9
+
10
+ init One command: ingest, hooks, MCP config
11
+ resolve <scope> Serve context to an agent
12
+ check Verify routing (guards block, nudges guide)
13
+ conventions View and manage conventions
14
+ voice View and manage code style
15
+ health Scope staleness and drift
16
+
17
+ ingest . Re-ingest (full scan)
18
+ intent Declare architectural direction
19
+ diff --staged Semantic diff
20
+ hook install Re-install hooks
21
+ bench Performance metrics
22
+ test-compiler Regression suite
23
+ debug --last Diagnose a bad session
24
+
25
+ Run dotscope <command> --help for details."""
26
+
27
+ HELP_INGEST = """\
28
+ Usage: dotscope ingest <path> [options]
29
+
30
+ dotscope ingest . Full codebase ingest
31
+ dotscope ingest auth/ Single module
32
+ dotscope ingest . --max-commits 500 Deeper history mining
33
+ dotscope ingest . --quiet Suppress progress (for CI)
34
+
35
+ Options:
36
+ --max-commits <n> Git commits to mine (default: 500)
37
+ --no-history Skip git history mining
38
+ --no-docs Skip doc absorption
39
+ --quiet Suppress progress output
40
+ --dry-run Show what would be generated without writing"""
41
+
42
+ HELP_RESOLVE = """\
43
+ Usage: dotscope resolve <scope> [options]
44
+
45
+ dotscope resolve auth Files and context for auth/
46
+ dotscope resolve auth --budget 4000 Best 4K tokens
47
+ dotscope resolve auth+payments Union of two scopes
48
+ dotscope resolve auth@context Context only, no files
49
+
50
+ Options:
51
+ --budget <tokens> Token limit
52
+ --task <description> Filter constraints by relevance
53
+ --json Machine-readable output
54
+ --no-related Don't follow related scopes"""
55
+
56
+ HELP_CHECK = """\
57
+ Usage: dotscope check [options]
58
+
59
+ dotscope check Validate staged changes
60
+ dotscope check --diff changes.patch Check arbitrary diff
61
+ dotscope check --backtest --commits 10 Replay history
62
+ dotscope check --acknowledge <id> Acknowledge a guard
63
+
64
+ Three severities:
65
+ GUARD Blocks commit. Frozen modules, deprecated imports.
66
+ NUDGE Prints guidance. Contracts, conventions, anti-patterns.
67
+ NOTE Informational. Direction reversals, stability.
68
+
69
+ Options:
70
+ --staged Check staged changes (default)
71
+ --diff <file> Check arbitrary diff file
72
+ --backtest Replay commits instead of checking staged
73
+ --commits <n> Commits to replay (with --backtest)
74
+ --acknowledge <id> Acknowledge a guard and proceed
75
+ --json Machine-readable output"""
76
+
77
+ HELP_INTENT = """\
78
+ Usage: dotscope intent <action> [options]
79
+
80
+ dotscope intent list Show all intents
81
+ dotscope intent add decouple auth/ payments/ Decouple modules
82
+ dotscope intent add freeze core/ Freeze a module
83
+ dotscope intent add deprecate old.py --replacement new.py
84
+ dotscope intent remove <id> Remove an intent
85
+
86
+ Directives:
87
+ decouple <mod> <mod> Discourage new coupling between modules
88
+ deprecate <file> Flag new usage as a hold
89
+ freeze <module> Require acknowledgment for any change
90
+ consolidate <m> <m> Encourage merging toward a target"""
91
+
92
+ HELP_CONVENTIONS = """\
93
+ Usage: dotscope conventions [options]
94
+
95
+ dotscope conventions List all conventions + compliance
96
+ dotscope conventions --discover Re-run discovery against codebase
97
+ dotscope conventions --accept Accept all discovered conventions
98
+ dotscope conventions --review Interactive review
99
+
100
+ Options:
101
+ --discover Re-run convention discovery
102
+ --accept Accept discovered conventions
103
+ --review Review discovered conventions interactively"""
104
+
105
+ HELP_VOICE = """\
106
+ Usage: dotscope voice [options]
107
+
108
+ dotscope voice Show current voice config
109
+ dotscope voice --upgrade typing Upgrade enforcement level
110
+ dotscope voice --json Machine-readable output
111
+
112
+ Options:
113
+ --upgrade <rule> Upgrade enforcement (typing, bare_excepts)
114
+ --json Machine-readable output"""
115
+
116
+ HELP_DIFF = """\
117
+ Usage: dotscope diff [options]
118
+
119
+ dotscope diff --staged Semantic diff of staged changes
120
+ dotscope diff HEAD~1 Semantic diff against last commit
121
+ dotscope diff HEAD~5..HEAD Range of commits
122
+
123
+ Options:
124
+ --staged Diff staged changes (default)
125
+ --json Machine-readable output"""
126
+
127
+ HELP_BENCH = """\
128
+ Usage: dotscope bench [options]
129
+
130
+ dotscope bench Full benchmark report
131
+ dotscope bench --json Machine-readable output
132
+
133
+ Options:
134
+ --json Machine-readable output"""
135
+
136
+ HELP_TEST_COMPILER = """\
137
+ Usage: dotscope test-compiler [options]
138
+
139
+ dotscope test-compiler Replay all regression cases
140
+ dotscope test-compiler --scope auth Replay regressions for auth only
141
+
142
+ Options:
143
+ --scope <name> Filter to a specific scope"""
144
+
145
+ HELP_DEBUG = """\
146
+ Usage: dotscope debug [options]
147
+
148
+ dotscope debug --last Debug most recent bad session
149
+ dotscope debug <session_id> Debug a specific session
150
+ dotscope debug --list List sessions with low recall
151
+
152
+ Options:
153
+ --last Debug most recent session with low recall
154
+ --list List debuggable sessions
155
+ --json Machine-readable output"""
156
+
157
+ HELP_HEALTH = """\
158
+ Usage: dotscope health [options]
159
+
160
+ dotscope health All scopes
161
+ dotscope health auth Single scope
162
+ dotscope health --json Machine-readable output
163
+
164
+ Options:
165
+ --json Machine-readable output"""
166
+
167
+ HELP_HOOK = """\
168
+ Usage: dotscope hook <action>
169
+
170
+ dotscope hook install Install pre-commit + post-commit hooks
171
+ dotscope hook claude Install Claude Code pre-commit enforcement
172
+ dotscope hook uninstall Remove all dotscope hooks
173
+ dotscope hook status Check what's installed
174
+
175
+ Pre-commit blocks on GUARDs only. NUDGEs and NOTEs pass through.
176
+ Post-commit records observations for the feedback loop."""
177
+
178
+ HELP_COMMANDS = {
179
+ "ingest": HELP_INGEST,
180
+ "resolve": HELP_RESOLVE,
181
+ "check": HELP_CHECK,
182
+ "intent": HELP_INTENT,
183
+ "conventions": HELP_CONVENTIONS,
184
+ "diff": HELP_DIFF,
185
+ "voice": HELP_VOICE,
186
+ "bench": HELP_BENCH,
187
+ "test-compiler": HELP_TEST_COMPILER,
188
+ "debug": HELP_DEBUG,
189
+ "health": HELP_HEALTH,
190
+ "hook": HELP_HOOK,
191
+ }
192
+
193
+
194
+ def print_help(command=None):
195
+ """Print help for a command, or root help if no command given."""
196
+ if command is None:
197
+ print(HELP_ROOT)
198
+ elif command in HELP_COMMANDS:
199
+ print(HELP_COMMANDS[command])
200
+ else:
201
+ import sys
202
+ print(f"Unknown command: {command}\n")
203
+ print("Run dotscope --help to see available commands.")
204
+ sys.exit(1)
dotscope/history.py ADDED
@@ -0,0 +1,6 @@
1
+ """Backward-compatibility stub. Moved to dotscope.passes.history_miner."""
2
+ from .passes.history_miner import * # noqa: F401,F403
3
+ from .models.history import ( # noqa: F401
4
+ FileChange, CommitInfo, FileHistory,
5
+ ChangeCoupling, ImplicitContract, HistoryAnalysis,
6
+ )
dotscope/hooks.py ADDED
@@ -0,0 +1,2 @@
1
+ """Backward-compatibility stub. Moved to dotscope.storage.git_hooks."""
2
+ from .storage.git_hooks import * # noqa: F401,F403