Implement storage layer for MVP-1 Personal Tracker CLI

Add storage layer with FileStorage, MarkdownReader, and MarkdownWriter classes.
Add data models (Project, Session, Note, Change).
This commit is contained in:
2026-03-23 08:54:00 -03:00
parent 525996f60c
commit 4547c492da
16 changed files with 1013 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
"""Markdown writer utility."""
import re
from datetime import datetime
from typing import Optional
from tracker.models.session import Session
class MarkdownWriter:
"""Escritura de archivos Markdown del proyecto."""
def format_log_entry(self, session: Session, summary: str) -> str:
"""Formatea una entrada para LOG.md.
Formato:
## 2026-03-23 10:0011:20
**Objetivo**
...
**Trabajo realizado**
- ...
**Cambios relevantes**
- ...
**Bloqueos**
- ...
**Decisiones**
- ...
**Próximos pasos**
- ...
**Resumen**
...
Returns string formateado.
"""
started = session.started_at.strftime("%Y-%m-%d %H:%M")
ended = session.ended_at.strftime("%H:%M") if session.ended_at else "En Curso"
date_range = f"{started}{ended}"
lines = [
f"## {date_range}",
"",
"**Objetivo**",
f"{session.objective or 'No especificado'}",
"",
"**Trabajo realizado**",
]
if session.work_done:
for item in session.work_done:
lines.append(f"- {item}")
else:
lines.append("- Sin trabajo registrado")
lines.extend(["", "**Cambios relevantes**"])
if session.changes:
for item in session.changes:
lines.append(f"- {item}")
else:
lines.append("- Sin cambios")
lines.extend(["", "**Bloqueos**"])
if session.blockers:
for item in session.blockers:
lines.append(f"- {item}")
else:
lines.append("- Sin bloqueos")
lines.extend(["", "**Decisiones**"])
if session.decisions:
for item in session.decisions:
lines.append(f"- {item}")
else:
lines.append("- Sin decisiones")
lines.extend(["", "**Próximos pasos**"])
if session.next_steps:
for item in session.next_steps:
lines.append(f"- {item}")
else:
lines.append("- Sin pasos definidos")
lines.extend(["", "**Resumen**", summary])
return "\n".join(lines) + "\n\n"
def format_session_file(self, session: Session) -> str:
"""Formatea archivo de sesion detalle en sessions/YYYY-MM-DD_HHMM.md.
Formato:
# Sesion: 2026-03-23 10:0011:20
## Objetivo
...
## Notas
...
## Trabajo realizado
...
## Cambios
...
## Decisiones
...
## Bloqueos
...
## Proximos pasos
...
## Referencias
...
## Duracion
X minutos
"""
started = session.started_at.strftime("%Y-%m-%d %H:%M")
ended = session.ended_at.strftime("%H:%M") if session.ended_at else "En Curso"
lines = [
f"# Sesion: {started}{ended}",
"",
"## Objetivo",
f"{session.objective or 'No especificado'}",
"",
"## Notas",
]
if session.raw_notes:
for note in session.raw_notes:
note_type = note.get("type", "work")
note_text = note.get("text", "")
lines.append(f"- [{note_type}] {note_text}")
else:
lines.append("- Sin notas")
lines.extend([
"",
"## Trabajo realizado",
])
if session.work_done:
for item in session.work_done:
lines.append(f"- {item}")
else:
lines.append("- Sin trabajo realizado")
lines.extend([
"",
"## Cambios",
])
if session.changes:
for item in session.changes:
lines.append(f"- {item}")
else:
lines.append("- Sin cambios")
lines.extend([
"",
"## Decisiones",
])
if session.decisions:
for item in session.decisions:
lines.append(f"- {item}")
else:
lines.append("- Sin decisiones")
lines.extend([
"",
"## Bloqueos",
])
if session.blockers:
for item in session.blockers:
lines.append(f"- {item}")
else:
lines.append("- Sin bloqueos")
lines.extend([
"",
"## Proximos pasos",
])
if session.next_steps:
for item in session.next_steps:
lines.append(f"- {item}")
else:
lines.append("- Sin pasos definidos")
lines.extend([
"",
"## Referencias",
])
if session.references:
for item in session.references:
lines.append(f"- {item}")
else:
lines.append("- Sin referencias")
lines.extend([
"",
"## Duracion",
f"{session.duration_minutes} minutos",
])
return "\n".join(lines) + "\n"
def format_autogen_section(self, content: str, section: str, new_content: str) -> str:
"""Reemplaza o inserta una seccion AUTOGEN en contenido Markdown.
Busca <!-- AUTOGEN:{section}_START --> ... <!-- AUTOGEN:{section}_END -->
Si existe, reemplaza el contenido entre los marcadores.
Si no existe, inserta la seccion al final.
Returns el contenido modificado.
"""
start_marker = f"<!-- AUTOGEN:{section}_START -->"
end_marker = f"<!-- AUTOGEN:{section}_END -->"
full_marker = f"{start_marker}\n{new_content}\n{end_marker}"
# Buscar si existe la seccion
pattern = rf"{re.escape(start_marker)}.*?{re.escape(end_marker)}"
if re.search(pattern, content, re.DOTALL):
# Reemplazar seccion existente
return re.sub(pattern, full_marker, content, flags=re.DOTALL)
else:
# Insertar al final
return content + "\n" + full_marker + "\n"
def format_readme_section(self, section: str, content: str) -> str:
"""Formatea una seccion de README.md.
Para usar con format_autogen_section.
"""
return content