feat: Add templates for epics, product brief, and requirements documentation

- Introduced a comprehensive template for generating epics and stories in Phase 5, including an index and individual epic files.
- Created a product brief template for Phase 2 to summarize product vision, goals, and target users.
- Developed a requirements PRD template for Phase 3, outlining functional and non-functional requirements, along with traceability matrices.

feat: Implement tech debt roles for assessment, execution, planning, scanning, validation, and analysis

- Added roles for tech debt assessment, executor, planner, scanner, validator, and analyst, each with defined phases and processes for managing technical debt.
- Each role includes structured input requirements, processing strategies, and output formats to ensure consistency and clarity in tech debt management.
This commit is contained in:
catlog22
2026-03-07 13:32:04 +08:00
parent 7ee9b579fa
commit 29a1fea467
255 changed files with 14407 additions and 21120 deletions

View File

@@ -7,7 +7,7 @@ for the DeepWiki documentation generation system.
from __future__ import annotations
from datetime import datetime
from typing import List, Optional, Tuple
from typing import Any, List, Optional, Tuple
from pydantic import BaseModel, Field, field_validator
@@ -30,6 +30,10 @@ class DeepWikiSymbol(BaseModel):
)
created_at: Optional[datetime] = Field(default=None, description="Record creation timestamp")
updated_at: Optional[datetime] = Field(default=None, description="Record update timestamp")
staleness_score: float = Field(default=0.0, ge=0.0, le=1.0, description="Staleness score (0.0=fresh, 1.0=stale)")
last_checked_commit: Optional[str] = Field(default=None, description="Git commit hash at last freshness check")
last_checked_at: Optional[float] = Field(default=None, description="Timestamp of last freshness check")
staleness_factors: Optional[dict[str, Any]] = Field(default=None, description="JSON factors contributing to staleness score")
@field_validator("line_range")
@classmethod
@@ -101,6 +105,10 @@ class DeepWikiFile(BaseModel):
)
symbols_count: int = Field(default=0, ge=0, description="Number of symbols indexed from this file")
docs_generated: bool = Field(default=False, description="Whether documentation has been generated")
staleness_score: float = Field(default=0.0, ge=0.0, le=1.0, description="Staleness score (0.0=fresh, 1.0=stale)")
last_checked_commit: Optional[str] = Field(default=None, description="Git commit hash at last freshness check")
last_checked_at: Optional[float] = Field(default=None, description="Timestamp of last freshness check")
staleness_factors: Optional[dict[str, Any]] = Field(default=None, description="JSON factors contributing to staleness score")
@field_validator("path", "content_hash")
@classmethod

View File

@@ -14,13 +14,13 @@ from __future__ import annotations
import hashlib
import json
import logging
import platform
import math # noqa: F401 - used in calculate_staleness_score
import sqlite3
import threading
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional
from codexlens.errors import StorageError
from codexlens.storage.deepwiki_models import DeepWikiDoc, DeepWikiFile, DeepWikiSymbol
@@ -40,7 +40,7 @@ class DeepWikiStore:
"""
DEFAULT_DB_PATH = Path.home() / ".codexlens" / "deepwiki_index.db"
SCHEMA_VERSION = 1
SCHEMA_VERSION = 2
def __init__(self, db_path: Path | None = None) -> None:
"""Initialize DeepWiki store.
@@ -130,7 +130,11 @@ class DeepWikiStore:
content_hash TEXT NOT NULL,
last_indexed REAL NOT NULL,
symbols_count INTEGER DEFAULT 0,
docs_generated INTEGER DEFAULT 0
docs_generated INTEGER DEFAULT 0,
staleness_score REAL DEFAULT 0.0,
last_checked_commit TEXT,
last_checked_at REAL,
staleness_factors TEXT
)
"""
)
@@ -172,6 +176,10 @@ class DeepWikiStore:
end_line INTEGER NOT NULL,
created_at REAL,
updated_at REAL,
staleness_score REAL DEFAULT 0.0,
last_checked_commit TEXT,
last_checked_at REAL,
staleness_factors TEXT,
UNIQUE(name, source_file)
)
"""
@@ -226,6 +234,23 @@ class DeepWikiStore:
(self.SCHEMA_VERSION, time.time()),
)
# Schema v2 migration: add staleness columns
staleness_columns = [
("deepwiki_files", "staleness_score", "REAL DEFAULT 0.0"),
("deepwiki_files", "last_checked_commit", "TEXT"),
("deepwiki_files", "last_checked_at", "REAL"),
("deepwiki_files", "staleness_factors", "TEXT"),
("deepwiki_symbols", "staleness_score", "REAL DEFAULT 0.0"),
("deepwiki_symbols", "last_checked_commit", "TEXT"),
("deepwiki_symbols", "last_checked_at", "REAL"),
("deepwiki_symbols", "staleness_factors", "TEXT"),
]
for table, col, col_type in staleness_columns:
try:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {col} {col_type}")
except sqlite3.OperationalError:
pass # Column already exists
conn.commit()
except sqlite3.DatabaseError as exc:
raise StorageError(
@@ -355,6 +380,70 @@ class DeepWikiStore:
)
conn.commit()
def update_file_staleness(
self,
file_path: str | Path,
staleness_score: float,
commit: str | None = None,
factors: Dict[str, Any] | None = None,
) -> None:
"""Update staleness data for a tracked file.
Args:
file_path: Path to the source file.
staleness_score: Staleness score (0.0-1.0).
commit: Git commit hash at check time.
factors: Dict of factors contributing to the score.
"""
with self._lock:
conn = self._get_connection()
path_str = self._normalize_path(file_path)
now = time.time()
factors_json = json.dumps(factors) if factors else None
conn.execute(
"""
UPDATE deepwiki_files
SET staleness_score=?, last_checked_commit=?, last_checked_at=?, staleness_factors=?
WHERE path=?
""",
(staleness_score, commit, now, factors_json, path_str),
)
conn.commit()
def update_symbol_staleness(
self,
name: str,
source_file: str | Path,
staleness_score: float,
commit: str | None = None,
factors: Dict[str, Any] | None = None,
) -> None:
"""Update staleness data for a symbol.
Args:
name: Symbol name.
source_file: Path to the source file.
staleness_score: Staleness score (0.0-1.0).
commit: Git commit hash at check time.
factors: Dict of factors contributing to the score.
"""
with self._lock:
conn = self._get_connection()
path_str = self._normalize_path(source_file)
now = time.time()
factors_json = json.dumps(factors) if factors else None
conn.execute(
"""
UPDATE deepwiki_symbols
SET staleness_score=?, last_checked_commit=?, last_checked_at=?, staleness_factors=?
WHERE name=? AND source_file=?
""",
(staleness_score, commit, now, factors_json, name, path_str),
)
conn.commit()
def remove_file(self, file_path: str | Path) -> bool:
"""Remove a tracked file and its associated symbols.
@@ -722,6 +811,121 @@ class DeepWikiStore:
return sha256.hexdigest()
@staticmethod
def calculate_staleness_score(
days_since_update: float,
commits_since: int = 0,
files_changed: int = 0,
lines_changed: int = 0,
proportion_changed: float = 0.0,
is_deleted: bool = False,
weights: tuple[float, float, float] = (0.1, 0.4, 0.5),
decay_k: float = 0.05,
) -> float:
"""Calculate staleness score using three-factor formula.
S = min(1.0, w_t * T + w_c * C + w_s * M)
Args:
days_since_update: Days since last documentation update.
commits_since: Number of commits since last check.
files_changed: Number of files changed.
lines_changed: Total lines changed.
proportion_changed: Proportion of symbol body changed (0.0-1.0).
is_deleted: Whether the symbol was deleted.
weights: (w_t, w_c, w_s) weights for time, churn, symbol factors.
decay_k: Time decay constant (default 0.05, ~14 days to 50%).
Returns:
Staleness score between 0.0 and 1.0.
"""
w_t, w_c, w_s = weights
# T: Time decay factor
T = 1 - math.exp(-decay_k * max(0, days_since_update))
# C: Code churn factor (sigmoid normalization)
churn_raw = (
math.log1p(commits_since)
+ math.log1p(files_changed)
+ math.log1p(lines_changed)
)
C = 1 / (1 + math.exp(-churn_raw + 3)) # sigmoid centered at 3
# M: Symbol modification factor
if is_deleted:
M = 1.0
else:
M = min(1.0, max(0.0, proportion_changed))
return min(1.0, w_t * T + w_c * C + w_s * M)
def get_stale_files(
self, files: list[dict[str, str]]
) -> list[dict[str, str]]:
"""Check which files have stale documentation by comparing hashes.
Args:
files: List of dicts with 'path' and 'hash' keys.
Returns:
List of file dicts where stored hash differs from provided hash.
"""
with self._lock:
conn = self._get_connection()
stale = []
for f in files:
path_str = self._normalize_path(f["path"])
row = conn.execute(
"SELECT content_hash FROM deepwiki_files WHERE path=?",
(path_str,),
).fetchone()
if row and row["content_hash"] != f["hash"]:
stale.append({
"path": f["path"],
"stored_hash": row["content_hash"],
"current_hash": f["hash"],
})
elif not row:
stale.append({
"path": f["path"],
"stored_hash": None,
"current_hash": f["hash"],
})
return stale
def get_symbols_for_paths(
self, paths: list[str | Path]
) -> dict[str, list[DeepWikiSymbol]]:
"""Get all symbols for multiple source files.
Args:
paths: List of source file paths.
Returns:
Dict mapping normalized path to list of DeepWikiSymbol records.
"""
with self._lock:
conn = self._get_connection()
result: dict[str, list[DeepWikiSymbol]] = {}
for path in paths:
path_str = self._normalize_path(path)
rows = conn.execute(
"""
SELECT * FROM deepwiki_symbols
WHERE source_file=?
ORDER BY start_line
""",
(path_str,),
).fetchall()
if rows:
result[path_str] = [
self._row_to_deepwiki_symbol(row) for row in rows
]
return result
def stats(self) -> Dict[str, Any]:
"""Get storage statistics.
@@ -914,6 +1118,14 @@ class DeepWikiStore:
def _row_to_deepwiki_file(self, row: sqlite3.Row) -> DeepWikiFile:
"""Convert database row to DeepWikiFile."""
staleness_factors = None
try:
factors_str = row["staleness_factors"]
if factors_str:
staleness_factors = json.loads(factors_str)
except (KeyError, IndexError):
pass
return DeepWikiFile(
id=int(row["id"]),
path=row["path"],
@@ -923,6 +1135,10 @@ class DeepWikiStore:
else datetime.utcnow(),
symbols_count=int(row["symbols_count"]) if row["symbols_count"] else 0,
docs_generated=bool(row["docs_generated"]),
staleness_score=float(row["staleness_score"]) if row["staleness_score"] else 0.0,
last_checked_commit=row["last_checked_commit"] if "last_checked_commit" in row.keys() else None,
last_checked_at=row["last_checked_at"] if "last_checked_at" in row.keys() else None,
staleness_factors=staleness_factors,
)
def _row_to_deepwiki_symbol(self, row: sqlite3.Row) -> DeepWikiSymbol:
@@ -935,6 +1151,14 @@ class DeepWikiStore:
if row["updated_at"]:
updated_at = datetime.fromtimestamp(row["updated_at"])
staleness_factors = None
try:
factors_str = row["staleness_factors"]
if factors_str:
staleness_factors = json.loads(factors_str)
except (KeyError, IndexError):
pass
return DeepWikiSymbol(
id=int(row["id"]),
name=row["name"],
@@ -945,6 +1169,10 @@ class DeepWikiStore:
line_range=(int(row["start_line"]), int(row["end_line"])),
created_at=created_at,
updated_at=updated_at,
staleness_score=float(row["staleness_score"]) if row["staleness_score"] else 0.0,
last_checked_commit=row["last_checked_commit"] if "last_checked_commit" in row.keys() else None,
last_checked_at=row["last_checked_at"] if "last_checked_at" in row.keys() else None,
staleness_factors=staleness_factors,
)
def _row_to_deepwiki_doc(self, row: sqlite3.Row) -> DeepWikiDoc:

View File

@@ -605,7 +605,12 @@ Keep it minimal. Format as clean Markdown."""
symbol=symbol,
)
def should_regenerate(self, symbol: DeepWikiSymbol, source_code: str) -> bool:
def should_regenerate(
self,
symbol: DeepWikiSymbol,
source_code: str,
staleness_threshold: float = 0.7,
) -> bool:
"""Check if symbol needs regeneration.
Conditions for regeneration:
@@ -613,10 +618,12 @@ Keep it minimal. Format as clean Markdown."""
2. Symbol not in database (new)
3. Source code hash changed
4. Previous generation failed
5. Staleness score exceeds threshold
Args:
symbol: Symbol to check.
source_code: Source code of the symbol.
staleness_threshold: Score above which regeneration is triggered.
Returns:
True if regeneration needed, False otherwise.
@@ -639,6 +646,11 @@ Keep it minimal. Format as clean Markdown."""
if progress.get("status") == "failed":
return True # Retry failed
# Check staleness score from DeepWiki index
db_symbol = self.db.get_symbol(symbol.name, symbol.source_file)
if db_symbol and db_symbol.staleness_score >= staleness_threshold:
return True # Stale documentation
return False # Skip
def _fallback_generate(