devtime-ei 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 (54) hide show
  1. devtime/__init__.py +9 -0
  2. devtime/ai/__init__.py +0 -0
  3. devtime/ai/local.py +11 -0
  4. devtime/ai/prompts.py +24 -0
  5. devtime/ai/providers.py +41 -0
  6. devtime/assets/devtimeignore.starter +23 -0
  7. devtime/cli.py +374 -0
  8. devtime/config.py +67 -0
  9. devtime/db/__init__.py +0 -0
  10. devtime/db/connection.py +16 -0
  11. devtime/db/migrations.py +114 -0
  12. devtime/db/repository.py +351 -0
  13. devtime/db/schema.sql +145 -0
  14. devtime/fixtures/__init__.py +0 -0
  15. devtime/fixtures/assertions.py +51 -0
  16. devtime/fixtures/loader.py +52 -0
  17. devtime/fixtures/runner.py +73 -0
  18. devtime/intelligence/__init__.py +0 -0
  19. devtime/intelligence/claims.py +235 -0
  20. devtime/intelligence/concepts.py +483 -0
  21. devtime/intelligence/context_pack.py +276 -0
  22. devtime/intelligence/evidence.py +127 -0
  23. devtime/intelligence/lineage.py +21 -0
  24. devtime/intelligence/risk.py +267 -0
  25. devtime/intelligence/scoring.py +99 -0
  26. devtime/mcp/__init__.py +0 -0
  27. devtime/mcp/schemas.py +39 -0
  28. devtime/mcp/server.py +35 -0
  29. devtime/mcp/tools.py +90 -0
  30. devtime/output/__init__.py +0 -0
  31. devtime/output/json_export.py +50 -0
  32. devtime/output/markdown.py +50 -0
  33. devtime/output/terminal.py +208 -0
  34. devtime/paths.py +40 -0
  35. devtime/privacy.py +96 -0
  36. devtime/scanner/__init__.py +0 -0
  37. devtime/scanner/extractors/__init__.py +0 -0
  38. devtime/scanner/extractors/base.py +83 -0
  39. devtime/scanner/extractors/config_files.py +41 -0
  40. devtime/scanner/extractors/docs.py +35 -0
  41. devtime/scanner/extractors/nextjs.py +82 -0
  42. devtime/scanner/extractors/python.py +81 -0
  43. devtime/scanner/extractors/tests.py +61 -0
  44. devtime/scanner/extractors/typescript.py +99 -0
  45. devtime/scanner/file_walker.py +96 -0
  46. devtime/scanner/ignore.py +96 -0
  47. devtime/scanner/language.py +36 -0
  48. devtime/scanner/signals.py +252 -0
  49. devtime_ei-0.1.0.dist-info/METADATA +289 -0
  50. devtime_ei-0.1.0.dist-info/RECORD +54 -0
  51. devtime_ei-0.1.0.dist-info/WHEEL +5 -0
  52. devtime_ei-0.1.0.dist-info/entry_points.txt +2 -0
  53. devtime_ei-0.1.0.dist-info/licenses/LICENSE +201 -0
  54. devtime_ei-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,99 @@
1
+ """Understanding Score and Understanding Debt (Builder Edition, Chapter 12).
2
+
3
+ Scores must explain themselves or they are theater.
4
+
5
+ Trust Repair (v0.0.6):
6
+ - Higher Understanding Score = better understanding. Understanding Debt is a
7
+ *label* (low/medium/high), never the same number as the score.
8
+ - Freshness is NOT measured in V0 (no git-history). It contributes zero points
9
+ and is reported as "not measured" rather than silently adding score.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+
16
+ from devtime.intelligence.claims import ConceptIntelligence
17
+ from devtime.intelligence.evidence import EvidenceItem
18
+
19
+
20
+ @dataclass
21
+ class UnderstandingScore:
22
+ score: int
23
+ debt_label: str
24
+ causes: list[str] = field(default_factory=list)
25
+ how_to_reduce: list[str] = field(default_factory=list)
26
+ dimensions: dict[str, object] = field(default_factory=dict)
27
+
28
+
29
+ def _clamp(value: float, low: float, high: float) -> float:
30
+ return max(low, min(high, value))
31
+
32
+
33
+ def _evidence_quality(evidence: list[EvidenceItem]) -> float:
34
+ if not evidence:
35
+ return 0.0
36
+ weights = {"strong": 1.0, "medium": 0.6, "weak": 0.3, "contradictory": 0.0}
37
+ return sum(weights.get(e.strength, 0.0) for e in evidence) / len(evidence)
38
+
39
+
40
+ def compute_understanding(ci: ConceptIntelligence) -> UnderstandingScore:
41
+ evidence = ci.evidence
42
+ kinds = {e.signal.kind for e in evidence}
43
+
44
+ concept_confidence = ci.concept.confidence
45
+ evidence_quality = _evidence_quality(evidence)
46
+ # Only corroborated decisions count toward understanding (see repository load).
47
+ decision_coverage = 1.0 if any(e.kind == "decision" for e in evidence) else 0.0
48
+ test_coverage_signal = (
49
+ 1.0 if any(e.signal.kind == "test" and e.strength == "strong" for e in evidence)
50
+ else (0.4 if "test" in kinds else 0.0)
51
+ )
52
+ ownership_clarity = 0.0 # V0 has no confirmed owners yet.
53
+ contradiction_penalty = (
54
+ 1.0 if any(e.strength == "contradictory" for e in evidence) else 0.0
55
+ )
56
+
57
+ # Weights sum to 100. Freshness is intentionally absent (not measured in V0).
58
+ score = 0.0
59
+ score += concept_confidence * 30
60
+ score += evidence_quality * 25
61
+ score += decision_coverage * 20
62
+ score += test_coverage_signal * 15
63
+ score += ownership_clarity * 10
64
+ score -= contradiction_penalty * 15
65
+ score = int(round(_clamp(score, 0, 100)))
66
+
67
+ causes: list[str] = []
68
+ how: list[str] = []
69
+ if decision_coverage == 0.0:
70
+ causes.append("missing or uncorroborated decision evidence")
71
+ how.append("Record a decision that matches the scanned implementation.")
72
+ if test_coverage_signal < 1.0:
73
+ causes.append("weak or missing behavior-specific tests")
74
+ how.append("Add or link a behavior-specific test.")
75
+ if ownership_clarity == 0.0:
76
+ causes.append("no confirmed owner")
77
+ how.append("Confirm a suggested reviewer or owner.")
78
+ if contradiction_penalty > 0.0:
79
+ causes.append("contradictory docs and code")
80
+ how.append("Resolve the conflict between documentation and implementation.")
81
+
82
+ # Debt is the inverse *label* of the score, never the numeric score itself.
83
+ debt_label = "low" if score >= 75 else "medium" if score >= 50 else "high"
84
+
85
+ return UnderstandingScore(
86
+ score=score,
87
+ debt_label=debt_label,
88
+ causes=causes,
89
+ how_to_reduce=how,
90
+ dimensions={
91
+ "concept_confidence": round(concept_confidence, 2),
92
+ "evidence_quality": round(evidence_quality, 2),
93
+ "decision_coverage": decision_coverage,
94
+ "test_coverage_signal": test_coverage_signal,
95
+ "ownership_clarity": ownership_clarity,
96
+ "freshness": "not measured in V0",
97
+ "contradiction_penalty": contradiction_penalty,
98
+ },
99
+ )
File without changes
devtime/mcp/schemas.py ADDED
@@ -0,0 +1,39 @@
1
+ """MCP tool schemas (Full Book Appendix C; Builder Edition Chapter 16)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ TOOLS = {
6
+ "read": [
7
+ "list_concepts",
8
+ "explain_concept",
9
+ "show_evidence",
10
+ "get_claims",
11
+ "get_decisions",
12
+ "get_understanding_debt",
13
+ ],
14
+ "context": [
15
+ "get_context_pack",
16
+ "get_onboarding_pack",
17
+ "get_risk_context",
18
+ ],
19
+ "review": [
20
+ "review_diff",
21
+ "check_sensitive_concepts",
22
+ ],
23
+ "write_later": [
24
+ "add_decision",
25
+ "challenge_claim",
26
+ "confirm_claim",
27
+ ],
28
+ }
29
+
30
+ DEFAULT_PERMISSIONS = {
31
+ "read_memory": True,
32
+ "read_evidence_metadata": True,
33
+ "read_source": False,
34
+ "generate_context_pack": True,
35
+ "review_diff": True,
36
+ "write_decisions": False,
37
+ "challenge_claims": False,
38
+ "export_memory": False,
39
+ }
devtime/mcp/server.py ADDED
@@ -0,0 +1,35 @@
1
+ """MCP server description (Builder Edition, Chapter 16).
2
+
3
+ V0 ships the tool surface, schemas, and permission model. The actual transport
4
+ (stdio/socket via an MCP SDK) is wired in a later milestone; this module makes
5
+ the read-only contract inspectable today.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ from devtime.intelligence.context_pack import generate_context_pack # noqa: F401
13
+ from devtime.mcp import schemas
14
+
15
+
16
+ def describe_server() -> str:
17
+ """Honest preview output (Trust Repair v0.0.6): no server is started, and no
18
+ bind address is shown, because the MCP transport is not implemented in V0."""
19
+ lines = [
20
+ "MCP transport is not implemented in V0.",
21
+ "No server was started.",
22
+ "This command is a preview of planned read-only MCP tools.",
23
+ "",
24
+ "Planned read tools:",
25
+ ]
26
+ lines += [f" - {t}" for t in schemas.TOOLS["read"]]
27
+ lines += ["Planned context tools:"]
28
+ lines += [f" - {t}" for t in schemas.TOOLS["context"]]
29
+ lines += ["Planned review tools:"]
30
+ lines += [f" - {t}" for t in schemas.TOOLS["review"]]
31
+ return "\n".join(lines)
32
+
33
+
34
+ def describe_permissions() -> str:
35
+ return json.dumps(schemas.DEFAULT_PERMISSIONS, indent=2)
devtime/mcp/tools.py ADDED
@@ -0,0 +1,90 @@
1
+ """MCP tool implementations (Builder Edition, Chapter 16).
2
+
3
+ Read-only by default. Evidence ids and uncertainty are always included; full
4
+ source is never returned.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from devtime.db import connection, repository
10
+ from devtime.intelligence import concepts as concepts_mod
11
+ from devtime.intelligence.context_pack import generate_context_pack
12
+ from devtime.intelligence.scoring import compute_understanding
13
+
14
+
15
+ def list_concepts(limit: int = 50) -> list[dict]:
16
+ conn = connection.connect()
17
+ try:
18
+ out = []
19
+ for ci in repository.load_all_concepts(conn)[:limit]:
20
+ out.append(
21
+ {
22
+ "concept": ci.concept.name,
23
+ "slug": ci.concept.slug,
24
+ "confidence": concepts_mod.confidence_label(ci.concept.confidence),
25
+ }
26
+ )
27
+ return out
28
+ finally:
29
+ conn.close()
30
+
31
+
32
+ def explain_concept(concept: str) -> dict:
33
+ conn = connection.connect()
34
+ try:
35
+ ci = repository.load_concept(conn, concept)
36
+ if ci is None:
37
+ return {"error": "concept_not_found", "suggested_tool": "list_concepts"}
38
+ us = compute_understanding(ci)
39
+ return {
40
+ "concept": ci.concept.name,
41
+ "confidence": {
42
+ "concept": concepts_mod.confidence_label(ci.concept.confidence),
43
+ "decision": "low" if not any(e.kind == "decision" for e in ci.evidence) else "high",
44
+ },
45
+ "claims": [
46
+ {
47
+ "type": c.type,
48
+ "text": c.text,
49
+ "state": c.state,
50
+ "confidence": round(c.confidence, 2),
51
+ "evidence": [e.path for e in c.evidence if e.path],
52
+ }
53
+ for c in ci.claims
54
+ if c.type != "uncertainty"
55
+ ],
56
+ "uncertainty": [u.text for u in ci.uncertainties],
57
+ "understanding_score": us.score,
58
+ "human_review_required": bool(ci.uncertainties),
59
+ }
60
+ finally:
61
+ conn.close()
62
+
63
+
64
+ def get_context_pack(concept: str, mode: str = "risk") -> dict:
65
+ conn = connection.connect()
66
+ try:
67
+ ci = repository.load_concept(conn, concept)
68
+ if ci is None:
69
+ return {"error": "concept_not_found", "suggested_tool": "list_concepts"}
70
+ pack = generate_context_pack(ci, mode=mode)
71
+ return {
72
+ "concept": pack.concept,
73
+ "mode": pack.mode,
74
+ "supported_claims": pack.supported_claims,
75
+ "decisions": pack.decisions,
76
+ "uncertainty": pack.uncertainty,
77
+ "do_not_change_without_review": pack.do_not_change,
78
+ "limited": pack.limited,
79
+ "tests_to_run": [
80
+ {"path": t.path, "reason": t.reason} for t in pack.tests_to_run
81
+ ],
82
+ "agent_guidance": pack.agent_guidance,
83
+ "privacy": {
84
+ "contains_source_excerpts": False,
85
+ "contains_file_paths": True,
86
+ "review_before_sharing": True,
87
+ },
88
+ }
89
+ finally:
90
+ conn.close()
File without changes
@@ -0,0 +1,50 @@
1
+ """JSON / Markdown export of repository memory (Builder Edition, Chapter 4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from devtime.db import connection, repository
9
+ from devtime.intelligence.scoring import compute_understanding
10
+
11
+
12
+ def export_memory(fmt: str = "json", root: Path | None = None) -> str:
13
+ conn = connection.connect(root)
14
+ try:
15
+ items = repository.load_all_concepts(conn)
16
+ data = []
17
+ for ci in items:
18
+ us = compute_understanding(ci)
19
+ data.append(
20
+ {
21
+ "concept": ci.concept.name,
22
+ "slug": ci.concept.slug,
23
+ "confidence": round(ci.concept.confidence, 2),
24
+ "understanding_score": us.score,
25
+ "debt": us.debt_label,
26
+ "claims": [
27
+ {"type": c.type, "text": c.text, "confidence": round(c.confidence, 2)}
28
+ for c in ci.claims
29
+ ],
30
+ "uncertainty": [u.text for u in ci.uncertainties],
31
+ "evidence": [
32
+ {"kind": e.kind, "strength": e.strength, "path": e.path}
33
+ for e in ci.evidence
34
+ ],
35
+ }
36
+ )
37
+ finally:
38
+ conn.close()
39
+
40
+ if fmt == "markdown":
41
+ lines = ["# DevTime Repository Memory Export", ""]
42
+ for d in data:
43
+ lines.append(f"## {d['concept']} ({d['understanding_score']}/100, debt: {d['debt']})")
44
+ lines.append("Claims:")
45
+ lines += [f"- [{c['type']}] {c['text']}" for c in d["claims"]]
46
+ lines.append("Uncertainty:")
47
+ lines += [f"- {u}" for u in d["uncertainty"]] or ["- None"]
48
+ lines.append("")
49
+ return "\n".join(lines)
50
+ return json.dumps(data, indent=2)
@@ -0,0 +1,50 @@
1
+ """Rendering for risk review results (Trust Repair v0.0.6)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from devtime.intelligence.risk import (
6
+ STATE_FINDING,
7
+ STATE_NO_FINDINGS,
8
+ STATE_REVIEW_FAILED,
9
+ STATE_UNSUPPORTED,
10
+ RiskReview,
11
+ )
12
+
13
+
14
+ def render_risk_review(review: RiskReview) -> str:
15
+ if review.state == STATE_REVIEW_FAILED:
16
+ return (
17
+ "Risk review failed: Git could not read the diff.\n"
18
+ f"Reason: {review.reason or 'unknown'}"
19
+ )
20
+
21
+ if review.state == STATE_NO_FINDINGS:
22
+ return (
23
+ "DevTime Risk Review\n\n"
24
+ "No memory-aware risk findings for this diff.\n"
25
+ "Supported checks completed."
26
+ )
27
+
28
+ if review.state == STATE_UNSUPPORTED:
29
+ concepts = ", ".join(review.affected_concepts) or "a known concept"
30
+ return (
31
+ "DevTime Risk Review\n\n"
32
+ "Manual review required:\n"
33
+ f"Changed files are linked to {concepts}, but V0 has no specific rule "
34
+ "for this change class.\n"
35
+ "No supported risk rule evaluated this diff."
36
+ )
37
+
38
+ # STATE_FINDING
39
+ blocks = ["DevTime Risk Review", ""]
40
+ for f in review.findings:
41
+ blocks.append(f"Affected concept:\n {f.concept}")
42
+ blocks.append(f"Finding ({f.severity}):\n {f.text}")
43
+ if f.why_it_matters:
44
+ blocks.append(f"Why this matters:\n {f.why_it_matters}")
45
+ if f.missing:
46
+ blocks.append("Missing:")
47
+ blocks += [f" - {m}" for m in f.missing]
48
+ blocks.append(f"Suggested action:\n {f.suggested_action}")
49
+ blocks.append("")
50
+ return "\n".join(blocks)
@@ -0,0 +1,208 @@
1
+ """Terminal output (Builder Edition, Chapter 4).
2
+
3
+ The CLI is the first proof surface. Output shows what was learned, what evidence
4
+ supports it, what is uncertain, and what to do next.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ from rich.console import Console
12
+ from rich.markup import escape
13
+
14
+ from devtime import config, paths
15
+ from devtime.db import connection, repository
16
+ from devtime.intelligence import concepts as concepts_mod
17
+ from devtime.intelligence.context_pack import generate_context_pack, render_markdown
18
+ from devtime.intelligence.scoring import compute_understanding
19
+
20
+ console = Console()
21
+
22
+
23
+ def _require_init(root: Path | None = None) -> bool:
24
+ if not paths.is_initialized(root):
25
+ console.print("[red]DevTime is not initialized.[/red] Run [bold]dtc init[/bold] first.")
26
+ return False
27
+ return True
28
+
29
+
30
+ def print_concepts(root: Path | None = None) -> None:
31
+ if not _require_init(root):
32
+ return
33
+ conn = connection.connect(root)
34
+ try:
35
+ items = repository.load_all_concepts(conn)
36
+ if not items:
37
+ console.print("No concepts detected yet. Run [bold]dtc scan[/bold].")
38
+ return
39
+ console.print("[bold]Detected concepts[/bold]\n")
40
+ for i, ci in enumerate(items, 1):
41
+ us = compute_understanding(ci)
42
+ ev_kinds = sorted({e.kind for e in ci.evidence})
43
+ console.print(f"{i}. [bold]{ci.concept.name}[/bold]")
44
+ console.print(
45
+ f" confidence: {concepts_mod.confidence_label(ci.concept.confidence)}"
46
+ )
47
+ console.print(f" evidence: {', '.join(ev_kinds) or 'none'}")
48
+ console.print(f" debt: {us.debt_label}\n")
49
+ finally:
50
+ conn.close()
51
+
52
+
53
+ def print_explanation(concept: str, root: Path | None = None) -> None:
54
+ if not _require_init(root):
55
+ return
56
+ conn = connection.connect(root)
57
+ try:
58
+ ci = repository.load_concept(conn, concept)
59
+ if ci is None:
60
+ console.print(f"[yellow]No concept found matching[/yellow] '{concept}'.")
61
+ console.print("Run [bold]dtc concepts[/bold] to see detected concepts.")
62
+ return
63
+ us = compute_understanding(ci)
64
+ console.print(f"[bold]Concept:[/bold] {ci.concept.name}\n")
65
+
66
+ console.print("[bold]Supported claims:[/bold]")
67
+ supported = [c for c in ci.claims if c.type != "uncertainty"]
68
+ if not supported:
69
+ console.print(" (none)")
70
+ for c in supported:
71
+ paths_str = ", ".join(dict.fromkeys(e.path for e in c.evidence if e.path)) or "n/a"
72
+ console.print(f" - {escape(c.text)}")
73
+ console.print(
74
+ f" [dim]type: {c.type} confidence: {c.confidence:.2f} "
75
+ f"evidence: {escape(paths_str)}[/dim]"
76
+ )
77
+
78
+ console.print("\n[bold]Uncertainty:[/bold]")
79
+ if not ci.uncertainties:
80
+ console.print(" (none)")
81
+ for u in ci.uncertainties:
82
+ console.print(f" - {escape(u.text)}")
83
+
84
+ # Trust Repair: Understanding Score (higher = better); Debt is a label.
85
+ console.print(
86
+ f"\n[bold]Understanding Score:[/bold] {us.score} / 100"
87
+ )
88
+ console.print(f"[bold]Understanding Debt:[/bold] {us.debt_label}")
89
+ if us.causes:
90
+ console.print("causes:")
91
+ for cause in us.causes:
92
+ console.print(f" - {cause}")
93
+ if us.how_to_reduce:
94
+ console.print("suggested next steps:")
95
+ for step in us.how_to_reduce:
96
+ console.print(f" - {step}")
97
+ finally:
98
+ conn.close()
99
+
100
+
101
+ def print_evidence(concept: str, root: Path | None = None) -> None:
102
+ if not _require_init(root):
103
+ return
104
+ conn = connection.connect(root)
105
+ try:
106
+ ci = repository.load_concept(conn, concept)
107
+ if ci is None:
108
+ console.print(f"[yellow]No concept found matching[/yellow] '{concept}'.")
109
+ return
110
+ console.print(f"[bold]Evidence for {ci.concept.name}[/bold]\n")
111
+ for e in ci.evidence:
112
+ loc = f" ({e.path})" if e.path else ""
113
+ console.print(f"- [{e.strength}] {escape(e.summary + loc)}")
114
+ finally:
115
+ conn.close()
116
+
117
+
118
+ def print_debt(root: Path | None = None) -> None:
119
+ if not _require_init(root):
120
+ return
121
+ conn = connection.connect(root)
122
+ try:
123
+ items = repository.load_all_concepts(conn)
124
+ scored = sorted(
125
+ ((ci, compute_understanding(ci)) for ci in items),
126
+ key=lambda pair: pair[1].score,
127
+ )
128
+ console.print("[bold]Understanding Score and Debt[/bold]\n")
129
+ for ci, us in scored:
130
+ console.print(
131
+ f"[bold]{ci.concept.name}[/bold] - score {us.score}/100, debt {us.debt_label}"
132
+ )
133
+ for cause in us.causes:
134
+ console.print(f" - {cause}")
135
+ console.print("")
136
+ finally:
137
+ conn.close()
138
+
139
+
140
+ def print_context(concept: str, mode: str, root: Path | None = None) -> str | None:
141
+ if not _require_init(root):
142
+ return None
143
+ conn = connection.connect(root)
144
+ try:
145
+ ci = repository.load_concept(conn, concept)
146
+ if ci is None:
147
+ console.print(f"[yellow]No concept found matching[/yellow] '{concept}'.")
148
+ return None
149
+ pack = generate_context_pack(ci, mode=mode)
150
+ md = render_markdown(pack)
151
+ # markup=False so filesystem paths like calendar/[provider]/route.ts render literally.
152
+ console.print(md, markup=False)
153
+ # Persist the generated pack to memory.
154
+ repository_save_context_pack(conn, ci.concept.slug, mode, md)
155
+ return md
156
+ finally:
157
+ conn.close()
158
+
159
+
160
+ def repository_save_context_pack(conn, concept_slug: str, mode: str, body: str) -> None:
161
+ import uuid
162
+ from datetime import datetime, timezone
163
+
164
+ conn.execute(
165
+ "INSERT INTO context_packs(id, concept_id, mode, body_markdown, metadata_json, created_at) "
166
+ "VALUES (?,?,?,?,?,?)",
167
+ (
168
+ f"CP-{uuid.uuid4().hex[:10]}",
169
+ concept_slug,
170
+ mode,
171
+ body,
172
+ "{}",
173
+ datetime.now(timezone.utc).isoformat(),
174
+ ),
175
+ )
176
+ conn.commit()
177
+
178
+
179
+ def print_status(root: Path | None = None) -> None:
180
+ root = root or paths.repo_root()
181
+ initialized = paths.is_initialized(root)
182
+ cfg = config.load_config(root)
183
+ priv = cfg["privacy"]
184
+ mcp = cfg["mcp"]
185
+
186
+ last_scan = None
187
+ if initialized:
188
+ conn = connection.connect(root)
189
+ try:
190
+ last_scan = repository.last_scan_time(conn)
191
+ finally:
192
+ conn.close()
193
+
194
+ console.print("[bold]Repository:[/bold]")
195
+ console.print(f" Initialized: {'yes' if initialized else 'no'}")
196
+ console.print(f" Root: {root.resolve()}")
197
+ console.print("[bold]Storage:[/bold]")
198
+ console.print(f" Local SQLite: {paths.db_path(root)}")
199
+ console.print("[bold]AI:[/bold]")
200
+ console.print(f" {'Enabled' if priv['ai_enabled'] else 'Disabled'}")
201
+ console.print("[bold]Cloud:[/bold]")
202
+ console.print(f" {'Enabled' if priv['cloud_enabled'] else 'Disabled'}")
203
+ console.print("[bold]Telemetry:[/bold]")
204
+ console.print(f" {'On' if priv['telemetry_enabled'] else 'Off'}")
205
+ console.print("[bold]MCP:[/bold]")
206
+ console.print(f" {'Enabled' if mcp['enabled'] else 'Stopped'}")
207
+ console.print("[bold]Last scan:[/bold]")
208
+ console.print(f" {last_scan or 'never'}")
devtime/paths.py ADDED
@@ -0,0 +1,40 @@
1
+ """Local path helpers (Builder Edition, Chapter 5)."""
2
+
3
+ from pathlib import Path
4
+
5
+ DEV_DIR = ".devtime"
6
+ DB_NAME = "devtime.sqlite"
7
+ CONFIG_NAME = "config.yaml"
8
+ IGNORE_NAME = ".devtimeignore"
9
+
10
+
11
+ def repo_root() -> Path:
12
+ return Path.cwd()
13
+
14
+
15
+ def devtime_dir(root: Path | None = None) -> Path:
16
+ return (root or repo_root()) / DEV_DIR
17
+
18
+
19
+ def db_path(root: Path | None = None) -> Path:
20
+ return devtime_dir(root) / DB_NAME
21
+
22
+
23
+ def config_path(root: Path | None = None) -> Path:
24
+ return devtime_dir(root) / CONFIG_NAME
25
+
26
+
27
+ def ignore_path(root: Path | None = None) -> Path:
28
+ return (root or repo_root()) / IGNORE_NAME
29
+
30
+
31
+ def backups_dir(root: Path | None = None) -> Path:
32
+ return devtime_dir(root) / "backups"
33
+
34
+
35
+ def logs_dir(root: Path | None = None) -> Path:
36
+ return devtime_dir(root) / "logs"
37
+
38
+
39
+ def is_initialized(root: Path | None = None) -> bool:
40
+ return db_path(root).exists()