New projects created: - michangarrito (marketplace mobile) - template-saas (SaaS template) - clinica-dental (dental ERP) - clinica-veterinaria (veterinary ERP) Architecture updates: - Move catalog from core/ to shared/ - Add MCP servers structure and templates - Add git management scripts - Update SUBREPOSITORIOS.md with 15 new repos - Update .gitignore for new projects Repository infrastructure: - 4 main repositories - 11 subrepositorios - Gitea remotes configured 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
216 lines
7.1 KiB
Python
216 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Script para agregar YAML front-matter a archivos markdown que no lo tienen.
|
|
Soporta múltiples proyectos del workspace.
|
|
"""
|
|
import os
|
|
import re
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
def extract_title_from_content(content: str, filename: str) -> str:
|
|
"""Extrae título del primer H1 o usa el nombre del archivo."""
|
|
match = re.search(r'^#\s+(.+)$', content, re.MULTILINE)
|
|
if match:
|
|
return match.group(1).strip()
|
|
# Fallback: usar nombre de archivo
|
|
name = Path(filename).stem
|
|
return name.replace('-', ' ').replace('_', ' ').title()
|
|
|
|
def determine_file_type(filepath: str, filename: str) -> str:
|
|
"""Determina el tipo de documento basado en path y nombre."""
|
|
filepath_lower = filepath.lower()
|
|
filename_lower = filename.lower()
|
|
|
|
# Por prefijo de archivo
|
|
if filename_lower.startswith('rf-'):
|
|
return 'Requirement'
|
|
elif filename_lower.startswith('us-'):
|
|
return 'User Story'
|
|
elif filename_lower.startswith('et-'):
|
|
return 'Technical Specification'
|
|
elif filename_lower.startswith('adr-'):
|
|
return 'ADR'
|
|
elif filename_lower.startswith('epic-'):
|
|
return 'Epic'
|
|
elif filename_lower.startswith('pmc-') or filename_lower.startswith('oqi-'):
|
|
return 'Module Definition'
|
|
|
|
# Por nombre específico
|
|
if '_map' in filename_lower or '_index' in filename_lower:
|
|
return 'Index'
|
|
elif 'readme' in filename_lower:
|
|
return 'README'
|
|
elif 'vision' in filename_lower:
|
|
return 'Vision'
|
|
elif 'arquitectura' in filename_lower or 'architecture' in filename_lower:
|
|
return 'Architecture'
|
|
elif 'roadmap' in filename_lower:
|
|
return 'Roadmap'
|
|
elif 'guia' in filename_lower or 'guide' in filename_lower:
|
|
return 'Guide'
|
|
elif 'glosario' in filename_lower:
|
|
return 'Glossary'
|
|
elif 'board' in filename_lower:
|
|
return 'Planning'
|
|
elif 'definition-of' in filename_lower:
|
|
return 'Process'
|
|
elif 'auditoria' in filename_lower:
|
|
return 'Audit'
|
|
elif 'analisis' in filename_lower:
|
|
return 'Analysis'
|
|
elif 'modelo' in filename_lower or 'esquema' in filename_lower:
|
|
return 'Model'
|
|
elif 'api' in filename_lower:
|
|
return 'API Documentation'
|
|
|
|
# Por carpeta
|
|
if '/requerimientos/' in filepath_lower or '/requirements/' in filepath_lower:
|
|
return 'Requirement'
|
|
elif '/historias-usuario/' in filepath_lower or '/user-stories/' in filepath_lower:
|
|
return 'User Story'
|
|
elif '/especificaciones/' in filepath_lower:
|
|
return 'Technical Specification'
|
|
elif '/adr/' in filepath_lower or '97-adr' in filepath_lower:
|
|
return 'ADR'
|
|
elif '/planning/' in filepath_lower:
|
|
return 'Planning'
|
|
elif '/guias/' in filepath_lower or '/guides/' in filepath_lower:
|
|
return 'Guide'
|
|
|
|
return 'Documentation'
|
|
|
|
def extract_epic_from_path(filepath: str) -> str:
|
|
"""Extrae el epic/módulo del path si existe."""
|
|
# Buscar patrones como OQI-001, PMC-001, EPIC-001, etc.
|
|
match = re.search(r'(OQI-\d+|PMC-\d+|EPIC-\d+|BA-\d+|IA-\d+)', filepath, re.IGNORECASE)
|
|
if match:
|
|
return match.group(1).upper()
|
|
return ''
|
|
|
|
def generate_id(filename: str) -> str:
|
|
"""Genera un ID único basado en el nombre del archivo."""
|
|
name = Path(filename).stem
|
|
return name.upper().replace(' ', '-')
|
|
|
|
def determine_status(doc_type: str) -> str:
|
|
"""Determina el status por defecto según el tipo."""
|
|
if doc_type in ['Requirement', 'User Story', 'Technical Specification']:
|
|
return 'Draft'
|
|
elif doc_type in ['Vision', 'Architecture', 'Guide']:
|
|
return 'Active'
|
|
return 'Draft'
|
|
|
|
def has_yaml_frontmatter(content: str) -> bool:
|
|
"""Verifica si el contenido ya tiene YAML front-matter."""
|
|
return content.strip().startswith('---')
|
|
|
|
def add_yaml_frontmatter(filepath: str, project_name: str) -> bool:
|
|
"""Agrega YAML front-matter a un archivo si no lo tiene."""
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
if has_yaml_frontmatter(content):
|
|
return False
|
|
|
|
filename = os.path.basename(filepath)
|
|
title = extract_title_from_content(content, filename)
|
|
doc_type = determine_file_type(filepath, filename)
|
|
doc_id = generate_id(filename)
|
|
epic = extract_epic_from_path(filepath)
|
|
status = determine_status(doc_type)
|
|
today = datetime.now().strftime('%Y-%m-%d')
|
|
|
|
# Construir YAML
|
|
yaml_lines = [
|
|
'---',
|
|
f'id: "{doc_id}"',
|
|
f'title: "{title}"',
|
|
f'type: "{doc_type}"',
|
|
]
|
|
|
|
if epic:
|
|
yaml_lines.append(f'epic: "{epic}"')
|
|
|
|
yaml_lines.extend([
|
|
f'status: "{status}"',
|
|
f'project: "{project_name}"',
|
|
f'version: "1.0.0"',
|
|
f'created_date: "{today}"',
|
|
f'updated_date: "{today}"',
|
|
'---',
|
|
''
|
|
])
|
|
|
|
yaml_header = '\n'.join(yaml_lines)
|
|
new_content = yaml_header + content
|
|
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write(new_content)
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f"Error procesando {filepath}: {e}")
|
|
return False
|
|
|
|
def process_project(docs_path: str, project_name: str):
|
|
"""Procesa todos los archivos .md de un proyecto."""
|
|
processed = 0
|
|
skipped = 0
|
|
errors = 0
|
|
|
|
for root, dirs, files in os.walk(docs_path):
|
|
for filename in files:
|
|
if filename.endswith('.md'):
|
|
filepath = os.path.join(root, filename)
|
|
try:
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
if has_yaml_frontmatter(content):
|
|
skipped += 1
|
|
else:
|
|
if add_yaml_frontmatter(filepath, project_name):
|
|
processed += 1
|
|
print(f"✓ {filepath}")
|
|
else:
|
|
errors += 1
|
|
except Exception as e:
|
|
errors += 1
|
|
print(f"✗ {filepath}: {e}")
|
|
|
|
return processed, skipped, errors
|
|
|
|
def main():
|
|
base_path = '/home/isem/workspace-v1/projects'
|
|
|
|
projects = [
|
|
('platform_marketing_content', 'platform_marketing_content'),
|
|
('trading-platform', 'trading-platform'),
|
|
]
|
|
|
|
total_processed = 0
|
|
total_skipped = 0
|
|
total_errors = 0
|
|
|
|
for folder, project_name in projects:
|
|
docs_path = os.path.join(base_path, folder, 'docs')
|
|
if os.path.exists(docs_path):
|
|
print(f"\n=== Procesando: {project_name} ===")
|
|
processed, skipped, errors = process_project(docs_path, project_name)
|
|
print(f" Processed: {processed}, Skipped: {skipped}, Errors: {errors}")
|
|
total_processed += processed
|
|
total_skipped += skipped
|
|
total_errors += errors
|
|
else:
|
|
print(f"No existe: {docs_path}")
|
|
|
|
print(f"\n=== TOTAL ===")
|
|
print(f"Processed: {total_processed}")
|
|
print(f"Skipped: {total_skipped}")
|
|
print(f"Errors: {total_errors}")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|