chronicle-devkit 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.
- chronicle/__init__.py +1 -0
- chronicle/ai/__init__.py +0 -0
- chronicle/ai/base.py +7 -0
- chronicle/ai/factory.py +23 -0
- chronicle/ai/google_ai.py +14 -0
- chronicle/ai/openai.py +14 -0
- chronicle/analysis/__init__.py +0 -0
- chronicle/analysis/analyzers/__init__.py +0 -0
- chronicle/analysis/analyzers/base.py +15 -0
- chronicle/analysis/analyzers/django_migrations.py +74 -0
- chronicle/analysis/analyzers/git.py +37 -0
- chronicle/analysis/context.py +12 -0
- chronicle/analysis/engine.py +18 -0
- chronicle/cli/__init__.py +1 -0
- chronicle/cli/commands/__init__.py +0 -0
- chronicle/cli/commands/analyze.py +73 -0
- chronicle/cli/commands/config.py +79 -0
- chronicle/cli/commands/init.py +47 -0
- chronicle/cli/commands/interpret.py +48 -0
- chronicle/cli/commands/scan.py +87 -0
- chronicle/cli/commands/show.py +106 -0
- chronicle/cli/commands/status.py +26 -0
- chronicle/cli/commands/version.py +6 -0
- chronicle/cli/main.py +30 -0
- chronicle/config/__init__.py +0 -0
- chronicle/config/credentials.py +17 -0
- chronicle/config/loader.py +21 -0
- chronicle/config/manager.py +52 -0
- chronicle/integrations/__init__.py +0 -0
- chronicle/integrations/git.py +16 -0
- chronicle/interpretation/__init__.py +0 -0
- chronicle/interpretation/context.py +8 -0
- chronicle/interpretation/context_builder.py +147 -0
- chronicle/interpretation/interpreter.py +12 -0
- chronicle/interpretation/prompts.py +51 -0
- chronicle/project/__init__.py +0 -0
- chronicle/project/discovery.py +9 -0
- chronicle/project/initializer.py +45 -0
- chronicle/project/status.py +25 -0
- chronicle/scanning/__init__.py +0 -0
- chronicle/scanning/context.py +10 -0
- chronicle/scanning/engine.py +16 -0
- chronicle/scanning/scanners/__init__.py +0 -0
- chronicle/scanning/scanners/base.py +17 -0
- chronicle/scanning/scanners/django_migrations.py +156 -0
- chronicle/scanning/scanners/django_model.py +12 -0
- chronicle/scanning/scanners/git.py +94 -0
- chronicle/scanning/scanners/git_models.py +6 -0
- chronicle/storage/__init__.py +0 -0
- chronicle/storage/analysis_state.py +44 -0
- chronicle/storage/database.py +9 -0
- chronicle/storage/findings.py +53 -0
- chronicle/storage/models.py +20 -0
- chronicle/storage/observations.py +93 -0
- chronicle/storage/scan_state.py +44 -0
- chronicle/storage/schema.py +47 -0
- chronicle_devkit-0.1.0.dist-info/METADATA +233 -0
- chronicle_devkit-0.1.0.dist-info/RECORD +61 -0
- chronicle_devkit-0.1.0.dist-info/WHEEL +5 -0
- chronicle_devkit-0.1.0.dist-info/entry_points.txt +2 -0
- chronicle_devkit-0.1.0.dist-info/top_level.txt +1 -0
chronicle/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
chronicle/ai/__init__.py
ADDED
|
File without changes
|
chronicle/ai/base.py
ADDED
chronicle/ai/factory.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from chronicle.ai.base import AIProvider
|
|
2
|
+
from chronicle.ai.openai import OpenAIProvider
|
|
3
|
+
from chronicle.ai.google_ai import GenAIProvider
|
|
4
|
+
|
|
5
|
+
def create_provider(
|
|
6
|
+
provider: str,
|
|
7
|
+
model:str,
|
|
8
|
+
api_key:str
|
|
9
|
+
) -> AIProvider:
|
|
10
|
+
if provider == "openai":
|
|
11
|
+
return OpenAIProvider(
|
|
12
|
+
api_key=api_key,
|
|
13
|
+
model=model,
|
|
14
|
+
)
|
|
15
|
+
elif provider == "gemini":
|
|
16
|
+
return GenAIProvider(
|
|
17
|
+
api_key=api_key,
|
|
18
|
+
model=model,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"Unsupported AI provider: {provider}"
|
|
23
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from google import genai
|
|
2
|
+
from chronicle.ai.base import AIProvider
|
|
3
|
+
|
|
4
|
+
class GenAIProvider(AIProvider):
|
|
5
|
+
def __init__(self,model:str,api_key:str) -> None:
|
|
6
|
+
self.client = genai.Client(api_key=api_key)
|
|
7
|
+
self.model = model
|
|
8
|
+
|
|
9
|
+
def generate(self, prompt: str) -> str:
|
|
10
|
+
response = self.client.models.generate_content(
|
|
11
|
+
model=self.model,
|
|
12
|
+
contents = prompt,
|
|
13
|
+
)
|
|
14
|
+
return response.text if response.text else "Error: Model did not respond"
|
chronicle/ai/openai.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from openai import OpenAI
|
|
2
|
+
from chronicle.ai.base import AIProvider
|
|
3
|
+
|
|
4
|
+
class OpenAIProvider(AIProvider):
|
|
5
|
+
def __init__(self,api_key:str,model:str) -> None:
|
|
6
|
+
self.client = OpenAI(api_key=api_key)
|
|
7
|
+
self.model = model
|
|
8
|
+
|
|
9
|
+
def generate(self, prompt: str) -> str:
|
|
10
|
+
response = self.client.responses.create(
|
|
11
|
+
model=self.model,
|
|
12
|
+
input=prompt,
|
|
13
|
+
)
|
|
14
|
+
return response.output_text
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from abc import ABC,abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from chronicle.storage.models import Observation,Findings
|
|
4
|
+
from chronicle.analysis.context import AnalysisContext
|
|
5
|
+
|
|
6
|
+
class BaseAnalyzer(ABC):
|
|
7
|
+
"""Base class for all chronicle analyzers."""
|
|
8
|
+
|
|
9
|
+
def __init__(self,project_root:Path) -> None:
|
|
10
|
+
self.project_root = project_root
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def analyze(self, context:AnalysisContext) -> list[Findings]:
|
|
14
|
+
"""Analyze the project and return findings."""
|
|
15
|
+
pass
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from chronicle.analysis.analyzers.base import BaseAnalyzer
|
|
2
|
+
from chronicle.storage.models import Findings
|
|
3
|
+
import typer
|
|
4
|
+
from chronicle.storage.observations import Observation
|
|
5
|
+
from chronicle.analysis.context import AnalysisContext
|
|
6
|
+
|
|
7
|
+
class DjangoMigrationAnalyzer(BaseAnalyzer):
|
|
8
|
+
|
|
9
|
+
def analyze(
|
|
10
|
+
self,
|
|
11
|
+
context: AnalysisContext
|
|
12
|
+
) -> list[Findings]:
|
|
13
|
+
|
|
14
|
+
findings = []
|
|
15
|
+
|
|
16
|
+
for observation in context.observations:
|
|
17
|
+
|
|
18
|
+
if observation.source != "django":
|
|
19
|
+
continue
|
|
20
|
+
|
|
21
|
+
if observation.type != "migration":
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
operations = observation.data.get(
|
|
25
|
+
"operations",
|
|
26
|
+
[]
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
for operation in operations:
|
|
30
|
+
|
|
31
|
+
operation_name = operation.get(
|
|
32
|
+
"operation"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if operation_name == "RemoveField":
|
|
36
|
+
typer.echo(
|
|
37
|
+
f"Generating finding: "
|
|
38
|
+
f"observation={observation.id}, "
|
|
39
|
+
f"data={operation}"
|
|
40
|
+
)
|
|
41
|
+
findings.append(
|
|
42
|
+
Findings(
|
|
43
|
+
analyzer="django-migrations",
|
|
44
|
+
severity="warning",
|
|
45
|
+
title="Field removed",
|
|
46
|
+
message=(
|
|
47
|
+
"A database field is being "
|
|
48
|
+
"removed by this migration."
|
|
49
|
+
),
|
|
50
|
+
observation_id= observation.id, #type: ignore
|
|
51
|
+
data=operation,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
if operation_name == "AddField":
|
|
55
|
+
typer.echo(
|
|
56
|
+
f"Generating finding: "
|
|
57
|
+
f"observation={observation.id}, "
|
|
58
|
+
f"data={operation}"
|
|
59
|
+
)
|
|
60
|
+
findings.append(
|
|
61
|
+
Findings(
|
|
62
|
+
analyzer="django-migrations",
|
|
63
|
+
severity="info",
|
|
64
|
+
title="Field added",
|
|
65
|
+
message=(
|
|
66
|
+
"A database field is being "
|
|
67
|
+
"added by this migration."
|
|
68
|
+
),
|
|
69
|
+
observation_id= observation.id, #type: ignore
|
|
70
|
+
data=operation,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return findings
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from chronicle.storage.models import Observation,Findings
|
|
4
|
+
from .base import BaseAnalyzer
|
|
5
|
+
from chronicle.analysis.context import AnalysisContext
|
|
6
|
+
|
|
7
|
+
class GitAnalyzer(BaseAnalyzer):
|
|
8
|
+
def __init__(self, project_root: Path) -> None:
|
|
9
|
+
super().__init__(project_root)
|
|
10
|
+
|
|
11
|
+
def analyze(self,context:AnalysisContext) -> list[Findings]:
|
|
12
|
+
findings = []
|
|
13
|
+
for observation in context.observations:
|
|
14
|
+
if observation.type != "commit":
|
|
15
|
+
continue
|
|
16
|
+
if observation.source != "git":
|
|
17
|
+
continue
|
|
18
|
+
|
|
19
|
+
message = observation.data.get("message","Not given")
|
|
20
|
+
author = observation.data.get("author","unknown")
|
|
21
|
+
parsed_message = author + ":" + message
|
|
22
|
+
changes = observation.data.get("changes",[])
|
|
23
|
+
|
|
24
|
+
for change in changes:
|
|
25
|
+
file_status = change.get("status")
|
|
26
|
+
if file_status == "A":
|
|
27
|
+
findings.append(
|
|
28
|
+
Findings(
|
|
29
|
+
analyzer="git_analyzer",
|
|
30
|
+
severity="info",
|
|
31
|
+
title="New File created",
|
|
32
|
+
message=parsed_message,
|
|
33
|
+
observation_id=observation.id, #type: ignore
|
|
34
|
+
data=change
|
|
35
|
+
))
|
|
36
|
+
|
|
37
|
+
return findings
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dataclasses import dataclass,field
|
|
2
|
+
from chronicle.storage.observations import Observation
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class AnalysisContext:
|
|
6
|
+
observations: list[Observation]
|
|
7
|
+
state: dict[str,str | None] = field(
|
|
8
|
+
default_factory=dict
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
def get_state(self,analyzer:str,key:str) -> str |None:
|
|
12
|
+
return self.state.get(f"{analyzer}.{key}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from chronicle.analysis.context import AnalysisContext
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from chronicle.analysis.analyzers.base import BaseAnalyzer
|
|
6
|
+
from chronicle.storage.models import Observation,Findings
|
|
7
|
+
|
|
8
|
+
class AnalyzeEngine:
|
|
9
|
+
def __init__(self, analyzers:list[BaseAnalyzer]) -> None:
|
|
10
|
+
self.analyzers = analyzers
|
|
11
|
+
|
|
12
|
+
def analyze(self,context:AnalysisContext) -> list[Findings]:
|
|
13
|
+
findings = []
|
|
14
|
+
for analyzer in self.analyzers:
|
|
15
|
+
analyzer_findings = analyzer.analyze(context)
|
|
16
|
+
findings.extend(analyzer_findings)
|
|
17
|
+
|
|
18
|
+
return findings
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.1.0'
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from chronicle.analysis.engine import AnalyzeEngine
|
|
3
|
+
from chronicle.analysis.analyzers.git import GitAnalyzer
|
|
4
|
+
from chronicle.analysis.analyzers.django_migrations import DjangoMigrationAnalyzer
|
|
5
|
+
from chronicle.config.loader import get_chronicle_directory
|
|
6
|
+
from chronicle.project.discovery import find_project_root
|
|
7
|
+
from chronicle.storage.database import connect
|
|
8
|
+
from chronicle.storage.observations import ObservationRepo
|
|
9
|
+
from chronicle.storage.findings import FindingsRepo
|
|
10
|
+
from chronicle.analysis.context import AnalysisContext
|
|
11
|
+
from chronicle.storage.analysis_state import AnalysisStateRepo
|
|
12
|
+
|
|
13
|
+
def analyze():
|
|
14
|
+
"""Analyzes all observations done by chronicle."""
|
|
15
|
+
|
|
16
|
+
project_root = find_project_root()
|
|
17
|
+
if project_root:
|
|
18
|
+
db_path = (get_chronicle_directory(project_root) / "chronicle.db")
|
|
19
|
+
conn = connect(db_path)
|
|
20
|
+
observations = ObservationRepo(conn).list_all()
|
|
21
|
+
analysis_state = AnalysisStateRepo(conn)
|
|
22
|
+
last_obs_id = analysis_state.get(
|
|
23
|
+
"last_observation_id",
|
|
24
|
+
"analysis"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if last_obs_id is not None:
|
|
28
|
+
observations = [
|
|
29
|
+
observation for observation in observations if observation.id > int(last_obs_id) #type: ignore
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
context = AnalysisContext(
|
|
33
|
+
observations=observations,
|
|
34
|
+
state={
|
|
35
|
+
"analysis.last_observation_id":last_obs_id,
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
repo = FindingsRepo(conn)
|
|
40
|
+
analyzers = [
|
|
41
|
+
GitAnalyzer(project_root),
|
|
42
|
+
DjangoMigrationAnalyzer(project_root),
|
|
43
|
+
]
|
|
44
|
+
engine = AnalyzeEngine(analyzers) #type: ignore
|
|
45
|
+
findings = engine.analyze(context)
|
|
46
|
+
|
|
47
|
+
finding_count = 0
|
|
48
|
+
|
|
49
|
+
with conn:
|
|
50
|
+
for finding in findings:
|
|
51
|
+
if repo.save(finding):
|
|
52
|
+
finding_count +=1
|
|
53
|
+
|
|
54
|
+
if observations:
|
|
55
|
+
newest_obs_id = max(
|
|
56
|
+
observation.id for observation in observations #type: ignore
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
analysis_state.set(
|
|
60
|
+
"last_observation_id",
|
|
61
|
+
"analysis",
|
|
62
|
+
str(newest_obs_id)
|
|
63
|
+
)
|
|
64
|
+
typer.echo(f"Analyzed and stored {finding_count} findings.")
|
|
65
|
+
typer.echo(
|
|
66
|
+
f"Last analyzed observation: {last_obs_id}"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
typer.echo(
|
|
70
|
+
f"Observations being analyzed: "
|
|
71
|
+
f"{[observation.id for observation in observations]}"
|
|
72
|
+
)
|
|
73
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import typer
|
|
3
|
+
from chronicle.project.discovery import find_project_root
|
|
4
|
+
from chronicle.config.loader import *
|
|
5
|
+
from chronicle.config.manager import *
|
|
6
|
+
from chronicle.config.credentials import has_api_key,set_api_key
|
|
7
|
+
|
|
8
|
+
config_app = typer.Typer(
|
|
9
|
+
help="Manage Chronicle configuration."
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
@config_app.command("set")
|
|
13
|
+
def set_config(
|
|
14
|
+
key: str = typer.Argument(...),
|
|
15
|
+
value: str = typer.Argument(...),
|
|
16
|
+
):
|
|
17
|
+
"""Set a Chronicle config value."""
|
|
18
|
+
project_root = find_project_root()
|
|
19
|
+
if project_root:
|
|
20
|
+
config_path = get_config_path(project_root)
|
|
21
|
+
config = load_config(config_path)
|
|
22
|
+
if key == "provider":
|
|
23
|
+
update_ai_config(
|
|
24
|
+
config_path,
|
|
25
|
+
provider=value,
|
|
26
|
+
)
|
|
27
|
+
elif key == "model":
|
|
28
|
+
update_ai_config(
|
|
29
|
+
config_path,
|
|
30
|
+
model=value,
|
|
31
|
+
)
|
|
32
|
+
elif key == "api_key":
|
|
33
|
+
if config["ai"]["provider"] != "":
|
|
34
|
+
set_api_key(provider=config["ai"]["provider"],api_key=value)
|
|
35
|
+
else:
|
|
36
|
+
typer.echo(f"Unknown config key: {key}")
|
|
37
|
+
typer.echo("Available key: provider, model, api_key")
|
|
38
|
+
raise typer.Exit(1)
|
|
39
|
+
|
|
40
|
+
typer.echo(
|
|
41
|
+
f"Updated {key} = {value}"
|
|
42
|
+
)
|
|
43
|
+
else:
|
|
44
|
+
typer.echo("Chronicle not initialized.")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
@config_app.callback(invoke_without_command=True)
|
|
48
|
+
def config(ctx: typer.Context):
|
|
49
|
+
"""View Chronicle configurations."""
|
|
50
|
+
if ctx.invoked_subcommand is not None:
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
project_root = find_project_root()
|
|
54
|
+
if project_root:
|
|
55
|
+
config_path = get_config_path(project_root)
|
|
56
|
+
configuration = load_config(config_path)
|
|
57
|
+
if not configuration:
|
|
58
|
+
typer.echo("Chronicle config is empty.")
|
|
59
|
+
return
|
|
60
|
+
ai = configuration.get("ai",{})
|
|
61
|
+
|
|
62
|
+
provider = ai.get("provider","")
|
|
63
|
+
model = ai.get('model', '')
|
|
64
|
+
|
|
65
|
+
if provider:
|
|
66
|
+
api_status = (
|
|
67
|
+
"configured" if has_api_key(provider) else "not configured"
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
api_status = "not configured"
|
|
71
|
+
|
|
72
|
+
typer.echo("Chronicle Configuration")
|
|
73
|
+
typer.echo("=======================")
|
|
74
|
+
typer.echo(f"Provider : {provider}")
|
|
75
|
+
typer.echo(f"Model : {model}")
|
|
76
|
+
typer.echo(f"API Key : {api_status}")
|
|
77
|
+
else:
|
|
78
|
+
typer.echo("Chronicle not initialized.")
|
|
79
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from chronicle.project.discovery import find_project_root
|
|
3
|
+
from chronicle.project.initializer import initialize_chronicle,setup_ai_config
|
|
4
|
+
|
|
5
|
+
def init():
|
|
6
|
+
"""Initialize chronicle in a project"""
|
|
7
|
+
|
|
8
|
+
project_root = find_project_root()
|
|
9
|
+
|
|
10
|
+
if project_root is None:
|
|
11
|
+
typer.echo(f"Error: No .git files found in the current working directory",err=True)
|
|
12
|
+
raise typer.Exit(code=1)
|
|
13
|
+
|
|
14
|
+
config_path,created = initialize_chronicle(project_root)
|
|
15
|
+
|
|
16
|
+
if created:
|
|
17
|
+
typer.echo()
|
|
18
|
+
typer.echo(" ╔══════════════════════════════════════╗")
|
|
19
|
+
typer.echo(" ║ C H R O N I C L E ║")
|
|
20
|
+
typer.echo(" ╚══════════════════════════════════════╝")
|
|
21
|
+
typer.echo()
|
|
22
|
+
|
|
23
|
+
typer.echo(" ✦ Initializing Chronicle...")
|
|
24
|
+
typer.echo()
|
|
25
|
+
|
|
26
|
+
typer.echo(" ✓ Project detected")
|
|
27
|
+
typer.echo(" ✓ Chronicle directory created")
|
|
28
|
+
typer.echo(" ✓ Local database initialized")
|
|
29
|
+
typer.echo(" ✓ Initialized NLTK tokenizer")
|
|
30
|
+
typer.echo()
|
|
31
|
+
|
|
32
|
+
choice = typer.prompt(" Configure AI now?[Y/n] ").lower()
|
|
33
|
+
if choice == "y":
|
|
34
|
+
setup_ai_config(config_path=config_path)
|
|
35
|
+
typer.echo(" ✓ AI successfully configured.")
|
|
36
|
+
typer.echo()
|
|
37
|
+
else:
|
|
38
|
+
typer.echo(" AI not configured.")
|
|
39
|
+
typer.echo(" Use chronicle set command to configure AI.")
|
|
40
|
+
typer.echo()
|
|
41
|
+
|
|
42
|
+
typer.echo(" Chronicle is ready.")
|
|
43
|
+
typer.echo()
|
|
44
|
+
typer.echo(" Run `chronicle scan` to begin collecting observations.")
|
|
45
|
+
typer.echo()
|
|
46
|
+
else:
|
|
47
|
+
typer.echo(f"Chronicle already initiated at: {project_root}")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from chronicle.project.discovery import find_project_root
|
|
3
|
+
from chronicle.config.loader import get_chronicle_directory, load_config, get_config_path
|
|
4
|
+
from chronicle.interpretation.context_builder import *
|
|
5
|
+
from chronicle.storage.database import connect
|
|
6
|
+
from chronicle.storage.findings import FindingsRepo
|
|
7
|
+
from chronicle.storage.observations import ObservationRepo
|
|
8
|
+
from chronicle.interpretation.interpreter import Interpreter
|
|
9
|
+
from chronicle.ai.factory import GenAIProvider
|
|
10
|
+
from chronicle.config.credentials import get_api_key
|
|
11
|
+
|
|
12
|
+
def interpret(query: str = typer.Option(...,"--question","-q",help="Question to ask the project history.")):
|
|
13
|
+
"""Interpret Chronicle's project history using AI."""
|
|
14
|
+
project_root = find_project_root()
|
|
15
|
+
if project_root is None:
|
|
16
|
+
typer.echo("Chronicle project not found.")
|
|
17
|
+
raise typer.Exit(1)
|
|
18
|
+
|
|
19
|
+
db_path = (get_chronicle_directory(project_root) / "chronicle.db")
|
|
20
|
+
|
|
21
|
+
conn = connect(db_path)
|
|
22
|
+
|
|
23
|
+
findings_repo = FindingsRepo(conn)
|
|
24
|
+
observation_repo = ObservationRepo(conn)
|
|
25
|
+
|
|
26
|
+
findings = findings_repo.list_all()
|
|
27
|
+
observations = observation_repo.list_all()
|
|
28
|
+
|
|
29
|
+
context = build_context(
|
|
30
|
+
question=query,
|
|
31
|
+
findings=findings,
|
|
32
|
+
observations=observations)
|
|
33
|
+
|
|
34
|
+
config_path = get_config_path(project_root)
|
|
35
|
+
config = load_config(config_path)
|
|
36
|
+
ai = config.get("ai",{})
|
|
37
|
+
model = ai.get("model")
|
|
38
|
+
provider_name = ai.get("provider")
|
|
39
|
+
api_key = get_api_key(provider=provider_name)
|
|
40
|
+
if not api_key:
|
|
41
|
+
typer.echo(f"Couldn't find an api key for {provider_name} provider")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
provider = GenAIProvider(model=model,api_key=api_key)
|
|
44
|
+
|
|
45
|
+
interpreter = Interpreter(provider=provider)
|
|
46
|
+
response = interpreter.interpret(context)
|
|
47
|
+
|
|
48
|
+
typer.echo(response)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from chronicle.project.discovery import find_project_root
|
|
3
|
+
from chronicle.scanning.scanners.git import GitScanner
|
|
4
|
+
from chronicle.scanning.scanners.django_migrations import DjangoMigrationScanner
|
|
5
|
+
from chronicle.scanning.engine import ScanEngine
|
|
6
|
+
from chronicle.config.loader import get_chronicle_directory
|
|
7
|
+
from chronicle.storage.database import connect
|
|
8
|
+
from chronicle.storage.observations import ObservationRepo
|
|
9
|
+
from chronicle.storage.scan_state import ScanStateRepo
|
|
10
|
+
from chronicle.scanning.context import ScanContext
|
|
11
|
+
|
|
12
|
+
def scan():
|
|
13
|
+
"""Scan the project and collect observation"""
|
|
14
|
+
project_root = find_project_root()
|
|
15
|
+
if project_root is None:
|
|
16
|
+
typer.echo("Error: could not find git repo.",err=True)
|
|
17
|
+
raise typer.Exit(code=1)
|
|
18
|
+
|
|
19
|
+
db_path = (get_chronicle_directory(project_root) / "chronicle.db")
|
|
20
|
+
connection = connect(database_path=db_path)
|
|
21
|
+
scan_state = ScanStateRepo(connection)
|
|
22
|
+
repo = ObservationRepo(connection)
|
|
23
|
+
last_commit = scan_state.get(
|
|
24
|
+
"last_commit",
|
|
25
|
+
"git",
|
|
26
|
+
)
|
|
27
|
+
last_migration = scan_state.get(
|
|
28
|
+
"last_migration",
|
|
29
|
+
"django_migrations",
|
|
30
|
+
)
|
|
31
|
+
git_context = ScanContext(
|
|
32
|
+
state={
|
|
33
|
+
"git.last_commit":last_commit,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
django_context = ScanContext(
|
|
37
|
+
state={
|
|
38
|
+
"django_migrations.last_migration":last_migration,
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
contexts = [git_context,django_context]
|
|
42
|
+
|
|
43
|
+
scanners = [
|
|
44
|
+
GitScanner(project_root),
|
|
45
|
+
DjangoMigrationScanner(project_root)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
engine = ScanEngine(
|
|
49
|
+
project_root=project_root,
|
|
50
|
+
scanners=scanners, #type: ignore
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
observations = engine.scan(contexts=contexts)
|
|
54
|
+
|
|
55
|
+
created_count = 0
|
|
56
|
+
|
|
57
|
+
git_observations = [ observation for observation in observations if observation.source == "git"]
|
|
58
|
+
|
|
59
|
+
django_observations = [ observation for observation in observations if observation.source == "django"]
|
|
60
|
+
|
|
61
|
+
with connection:
|
|
62
|
+
for observation in observations:
|
|
63
|
+
if repo.save(observation=observation):
|
|
64
|
+
created_count+=1
|
|
65
|
+
|
|
66
|
+
if git_observations:
|
|
67
|
+
newest_commit = git_observations[0].external_id
|
|
68
|
+
scan_state.set(
|
|
69
|
+
"last_commit",
|
|
70
|
+
"git",
|
|
71
|
+
newest_commit
|
|
72
|
+
)
|
|
73
|
+
if django_observations:
|
|
74
|
+
newest_migration = django_observations[-1].external_id
|
|
75
|
+
scan_state.set(
|
|
76
|
+
"last_migration",
|
|
77
|
+
"django_migrations",
|
|
78
|
+
newest_migration
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
connection.close()
|
|
82
|
+
|
|
83
|
+
if created_count > 0:
|
|
84
|
+
typer.echo(f"Collected {len(observations)} observation(s).")
|
|
85
|
+
typer.echo(f"Stored {created_count} new observation(s)")
|
|
86
|
+
else:
|
|
87
|
+
typer.echo(f"All observation(s) already stored.")
|