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.
- devtime/__init__.py +9 -0
- devtime/ai/__init__.py +0 -0
- devtime/ai/local.py +11 -0
- devtime/ai/prompts.py +24 -0
- devtime/ai/providers.py +41 -0
- devtime/assets/devtimeignore.starter +23 -0
- devtime/cli.py +374 -0
- devtime/config.py +67 -0
- devtime/db/__init__.py +0 -0
- devtime/db/connection.py +16 -0
- devtime/db/migrations.py +114 -0
- devtime/db/repository.py +351 -0
- devtime/db/schema.sql +145 -0
- devtime/fixtures/__init__.py +0 -0
- devtime/fixtures/assertions.py +51 -0
- devtime/fixtures/loader.py +52 -0
- devtime/fixtures/runner.py +73 -0
- devtime/intelligence/__init__.py +0 -0
- devtime/intelligence/claims.py +235 -0
- devtime/intelligence/concepts.py +483 -0
- devtime/intelligence/context_pack.py +276 -0
- devtime/intelligence/evidence.py +127 -0
- devtime/intelligence/lineage.py +21 -0
- devtime/intelligence/risk.py +267 -0
- devtime/intelligence/scoring.py +99 -0
- devtime/mcp/__init__.py +0 -0
- devtime/mcp/schemas.py +39 -0
- devtime/mcp/server.py +35 -0
- devtime/mcp/tools.py +90 -0
- devtime/output/__init__.py +0 -0
- devtime/output/json_export.py +50 -0
- devtime/output/markdown.py +50 -0
- devtime/output/terminal.py +208 -0
- devtime/paths.py +40 -0
- devtime/privacy.py +96 -0
- devtime/scanner/__init__.py +0 -0
- devtime/scanner/extractors/__init__.py +0 -0
- devtime/scanner/extractors/base.py +83 -0
- devtime/scanner/extractors/config_files.py +41 -0
- devtime/scanner/extractors/docs.py +35 -0
- devtime/scanner/extractors/nextjs.py +82 -0
- devtime/scanner/extractors/python.py +81 -0
- devtime/scanner/extractors/tests.py +61 -0
- devtime/scanner/extractors/typescript.py +99 -0
- devtime/scanner/file_walker.py +96 -0
- devtime/scanner/ignore.py +96 -0
- devtime/scanner/language.py +36 -0
- devtime/scanner/signals.py +252 -0
- devtime_ei-0.1.0.dist-info/METADATA +289 -0
- devtime_ei-0.1.0.dist-info/RECORD +54 -0
- devtime_ei-0.1.0.dist-info/WHEEL +5 -0
- devtime_ei-0.1.0.dist-info/entry_points.txt +2 -0
- devtime_ei-0.1.0.dist-info/licenses/LICENSE +201 -0
- 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
|
+
)
|
devtime/mcp/__init__.py
ADDED
|
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()
|