academia/index.html

1483 lines
643 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Academia - Documentación</title>
<meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
/* ═══════════════════════════════════════════════════════════
APPLE DESIGN SYSTEM - Inspired by apple.com & developer.apple.com
═══════════════════════════════════════════════════════════ */
/* ─── Design Tokens ─── */
:root {
/* Typography Scale (Apple's SF Pro) */
--font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
--font-mono: "SF Mono", ui-monospace, Menlo, Monaco, "Cascadia Code", monospace;
/* Spacing (8pt grid) */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 20px;
--space-6: 24px;
--space-8: 32px;
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
--space-20: 80px;
/* Radius (Apple's continuous corners) */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 18px;
--radius-xl: 22px;
/* Transitions (Apple's spring-like easing) */
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-spring: cubic-bezier(0.175, 0.885, 0.32, 1.1);
--duration-fast: 0.15s;
--duration-normal: 0.25s;
--duration-slow: 0.4s;
/* Layout */
--sidebar-width: 260px;
--header-height: 48px;
--content-max-width: 780px;
/* Light Mode Colors (Apple semantic) */
--color-bg: #ffffff;
--color-bg-secondary: #f5f5f7;
--color-bg-tertiary: #fbfbfd;
--color-bg-elevated: #ffffff;
--color-surface: rgba(255, 255, 255, 0.72);
--color-text: #1d1d1f;
--color-text-secondary: #86868b;
--color-text-tertiary: #6e6e73;
--color-accent: #0071e3;
--color-accent-hover: #0077ed;
--color-border: rgba(0, 0, 0, 0.08);
--color-border-strong: rgba(0, 0, 0, 0.12);
--color-separator: rgba(60, 60, 67, 0.12);
--color-fill: rgba(120, 120, 128, 0.08);
--color-fill-hover: rgba(120, 120, 128, 0.12);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.04), 0 1px 2px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04);
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.06);
--shadow-float: 0 22px 70px 4px rgba(0, 0, 0, 0.15);
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #000000;
--color-bg-secondary: #1c1c1e;
--color-bg-tertiary: #2c2c2e;
--color-bg-elevated: #1c1c1e;
--color-surface: rgba(28, 28, 30, 0.72);
--color-text: #f5f5f7;
--color-text-secondary: #98989d;
--color-text-tertiary: #8e8e93;
--color-accent: #2997ff;
--color-accent-hover: #40a9ff;
--color-border: rgba(255, 255, 255, 0.08);
--color-border-strong: rgba(255, 255, 255, 0.12);
--color-separator: rgba(84, 84, 88, 0.65);
--color-fill: rgba(120, 120, 128, 0.24);
--color-fill-hover: rgba(120, 120, 128, 0.32);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.5);
--shadow-float: 0 22px 70px 4px rgba(0, 0, 0, 0.6);
}
}
/* ─── Reset & Base ─── */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 17px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
scroll-behavior: smooth;
}
body {
font-family: var(--font-family);
background: var(--color-bg);
color: var(--color-text);
line-height: 1.47059;
letter-spacing: -0.022em;
overflow-x: hidden;
}
/* ─── Header (Apple Nav Bar) ─── */
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--header-height);
background: var(--color-surface);
backdrop-filter: saturate(180%) blur(20px);
-webkit-backdrop-filter: saturate(180%) blur(20px);
border-bottom: 0.5px solid var(--color-separator);
z-index: 1000;
display: flex;
align-items: center;
padding: 0 var(--space-5);
gap: var(--space-3);
}
.header-logo {
display: flex;
align-items: center;
gap: var(--space-2);
}
.header-logo svg {
width: 20px;
height: 20px;
fill: var(--color-accent);
}
.header-title {
font-size: 17px;
font-weight: 600;
letter-spacing: -0.022em;
color: var(--color-text);
}
.header-divider {
width: 1px;
height: 20px;
background: var(--color-separator);
margin: 0 var(--space-2);
}
.header-subtitle {
font-size: 13px;
font-weight: 400;
color: var(--color-text-secondary);
letter-spacing: -0.008em;
}
/* ─── Layout ─── */
.layout {
display: flex;
min-height: 100vh;
padding-top: var(--header-height);
}
/* ─── Sidebar (Apple Developer Docs style) ─── */
.sidebar {
width: var(--sidebar-width);
background: var(--color-bg-secondary);
border-right: 0.5px solid var(--color-separator);
height: calc(100vh - var(--header-height));
position: fixed;
left: 0;
top: var(--header-height);
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-4) 0;
}
.sidebar::-webkit-scrollbar {
width: 8px;
}
.sidebar::-webkit-scrollbar-track {
background: transparent;
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--color-fill);
border-radius: 4px;
border: 2px solid var(--color-bg-secondary);
}
.sidebar::-webkit-scrollbar-thumb:hover {
background: var(--color-fill-hover);
}
/* Search Box */
.search-wrap {
padding: 0 var(--space-4) var(--space-4);
}
.search-box {
position: relative;
}
.search-box input {
width: 100%;
height: 36px;
padding: 0 var(--space-3) 0 36px;
background: var(--color-fill);
border: none;
border-radius: var(--radius-sm);
font-size: 15px;
font-family: var(--font-family);
color: var(--color-text);
transition: background var(--duration-fast) var(--ease-out),
box-shadow var(--duration-fast) var(--ease-out);
}
.search-box input::placeholder {
color: var(--color-text-tertiary);
}
.search-box input:focus {
outline: none;
background: var(--color-bg);
box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.24);
}
.search-box::before {
content: '';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
width: 15px;
height: 15px;
background: var(--color-text-tertiary);
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E") center/contain no-repeat;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='currentColor'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E") center/contain no-repeat;
}
/* Navigation */
.nav-group {
margin-bottom: var(--space-2);
}
.nav-group-title {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-2) var(--space-4);
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
cursor: pointer;
user-select: none;
transition: color var(--duration-fast) var(--ease-out);
}
.nav-group-title:hover {
color: var(--color-text);
}
.nav-group-title .chevron {
width: 12px;
height: 12px;
transition: transform var(--duration-normal) var(--ease-spring);
opacity: 0.5;
}
.nav-group.open .nav-group-title .chevron {
transform: rotate(90deg);
}
.nav-list {
display: none;
padding: var(--space-1) var(--space-2);
}
.nav-group.open .nav-list {
display: block;
}
.nav-item {
display: block;
padding: var(--space-2) var(--space-3);
margin: 1px 0;
font-size: 13px;
font-weight: 400;
color: var(--color-text-secondary);
text-decoration: none;
border-radius: var(--radius-sm);
cursor: pointer;
transition: all var(--duration-fast) var(--ease-out);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.nav-item:hover {
background: var(--color-fill);
color: var(--color-text);
}
.nav-item.active {
background: var(--color-accent);
color: white;
font-weight: 500;
}
/* ─── Main Content ─── */
.main {
flex: 1;
margin-left: var(--sidebar-width);
min-height: calc(100vh - var(--header-height));
}
.content-wrapper {
max-width: var(--content-max-width);
margin: 0 auto;
padding: var(--space-12) var(--space-8) var(--space-20);
}
/* ─── Typography (Apple Style) ─── */
.content h1 {
font-size: 40px;
line-height: 1.1;
font-weight: 700;
letter-spacing: -0.015em;
color: var(--color-text);
margin: 0 0 var(--space-6);
}
.content h2 {
font-size: 28px;
line-height: 1.14286;
font-weight: 600;
letter-spacing: 0.007em;
color: var(--color-text);
margin: var(--space-12) 0 var(--space-4);
padding-top: var(--space-4);
}
.content h3 {
font-size: 21px;
line-height: 1.19048;
font-weight: 600;
letter-spacing: 0.011em;
color: var(--color-text);
margin: var(--space-8) 0 var(--space-3);
}
.content h4 {
font-size: 17px;
line-height: 1.23536;
font-weight: 600;
letter-spacing: -0.022em;
color: var(--color-text-secondary);
margin: var(--space-6) 0 var(--space-2);
}
.content p {
font-size: 17px;
line-height: 1.52941;
color: var(--color-text-tertiary);
margin: var(--space-4) 0;
}
.content strong {
font-weight: 600;
color: var(--color-text);
}
.content a {
color: var(--color-accent);
text-decoration: none;
transition: opacity var(--duration-fast) var(--ease-out);
}
.content a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
/* Lists */
.content ul, .content ol {
margin: var(--space-4) 0;
padding-left: var(--space-6);
}
.content li {
font-size: 17px;
line-height: 1.52941;
color: var(--color-text-tertiary);
margin: var(--space-2) 0;
}
.content li::marker {
color: var(--color-accent);
}
/* Code (Apple Developer style) */
.content code {
font-family: var(--font-mono);
font-size: 0.88em;
background: var(--color-fill);
padding: 2px 6px;
border-radius: 4px;
color: var(--color-text);
}
.content pre {
background: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
padding: var(--space-4) var(--space-5);
overflow-x: auto;
margin: var(--space-5) 0;
}
.content pre code {
background: none;
padding: 0;
font-size: 14px;
line-height: 1.6;
color: var(--color-text);
}
/* Blockquote (Apple callout style) */
.content blockquote {
background: linear-gradient(135deg, rgba(0, 113, 227, 0.08) 0%, rgba(88, 86, 214, 0.08) 100%);
border-left: 4px solid var(--color-accent);
border-radius: 0 var(--radius-md) var(--radius-md) 0;
padding: var(--space-4) var(--space-5);
margin: var(--space-5) 0;
}
.content blockquote p {
margin: 0;
color: var(--color-text-secondary);
font-style: normal;
}
/* Tables (Apple clean style) */
.content table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: var(--space-5) 0;
font-size: 15px;
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
}
.content th {
background: var(--color-bg-secondary);
font-weight: 600;
color: var(--color-text);
text-align: left;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-border);
}
.content td {
padding: var(--space-3) var(--space-4);
color: var(--color-text-tertiary);
border-bottom: 1px solid var(--color-border);
}
.content tr:last-child td {
border-bottom: none;
}
.content tbody tr:hover {
background: var(--color-fill);
}
/* Horizontal Rule */
.content hr {
border: none;
height: 1px;
background: var(--color-separator);
margin: var(--space-10) 0;
}
/* ─── Images & Diagrams (Apple Gallery style) ─── */
.content img {
display: block;
max-width: 100%;
height: auto;
margin: var(--space-6) auto;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
background: var(--color-bg);
}
.diagram-card {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border);
border-radius: var(--radius-xl);
padding: var(--space-6);
margin: var(--space-6) 0;
box-shadow: var(--shadow-sm);
transition: box-shadow var(--duration-normal) var(--ease-out),
transform var(--duration-normal) var(--ease-out);
}
.diagram-card:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
.diagram-card img {
margin: 0;
border-radius: var(--radius-md);
box-shadow: none;
width: 100%;
}
.diagram-title {
font-size: 15px;
font-weight: 600;
color: var(--color-text);
margin-top: var(--space-4);
text-align: center;
}
/* ─── Welcome Screen ─── */
.welcome {
text-align: center;
padding: var(--space-20) var(--space-8);
max-width: 480px;
margin: 0 auto;
}
.welcome-icon {
width: 96px;
height: 96px;
margin: 0 auto var(--space-6);
background: linear-gradient(135deg, var(--color-accent) 0%, #5856d6 100%);
border-radius: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 48px;
box-shadow: var(--shadow-lg);
}
.welcome h1 {
font-size: 32px;
font-weight: 700;
margin-bottom: var(--space-3);
}
.welcome p {
color: var(--color-text-secondary);
font-size: 17px;
line-height: 1.52941;
}
/* ─── Diagrams Gallery ─── */
.diagrams-section {
margin: var(--space-10) 0;
}
.diagrams-section h2 {
margin-bottom: var(--space-6);
}
.diagrams-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: var(--space-5);
}
/* ─── Lightbox Modal (Apple style with Zoom & Pan) ─── */
.lightbox {
position: fixed;
inset: 0;
z-index: 2000;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out);
}
.lightbox.active {
display: flex;
opacity: 1;
}
/* Image Container with Pan support */
.lightbox-viewport {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
}
.lightbox-viewport.dragging {
cursor: grabbing;
}
.lightbox-viewport.zoomed-out {
cursor: zoom-in;
}
.lightbox-content {
position: absolute;
transform-origin: center center;
transition: transform 0.3s var(--ease-spring);
will-change: transform;
animation: lightbox-in var(--duration-slow) var(--ease-spring);
}
.lightbox-content.no-transition {
transition: none;
}
@keyframes lightbox-in {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.lightbox-content img {
max-width: 90vw;
max-height: 85vh;
object-fit: contain;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-float);
background: white;
pointer-events: none;
user-select: none;
-webkit-user-drag: none;
}
/* Top Toolbar (Apple style pill) */
.lightbox-toolbar {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 2px;
padding: 6px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.1);
z-index: 10;
}
.lightbox-toolbar button {
width: 36px;
height: 36px;
border-radius: 50%;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
color: white;
}
.lightbox-toolbar button:hover {
background: rgba(255, 255, 255, 0.15);
}
.lightbox-toolbar button:active {
transform: scale(0.95);
}
.lightbox-toolbar button:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.lightbox-toolbar button svg {
width: 18px;
height: 18px;
stroke: white;
stroke-width: 2;
fill: none;
}
.lightbox-toolbar .divider {
width: 1px;
height: 24px;
background: rgba(255, 255, 255, 0.2);
margin: 0 4px;
}
.zoom-level {
font-size: 13px;
font-weight: 500;
color: white;
min-width: 50px;
text-align: center;
font-variant-numeric: tabular-nums;
}
/* Bottom Info Bar */
.lightbox-info {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.lightbox-title {
font-size: 15px;
font-weight: 500;
color: white;
white-space: nowrap;
}
.lightbox-counter {
font-size: 13px;
color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
}
/* Navigation Buttons */
.lightbox-close {
position: fixed;
top: 20px;
right: 20px;
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
z-index: 10;
}
.lightbox-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.lightbox-close:active {
transform: scale(0.95);
}
.lightbox-close svg {
width: 18px;
height: 18px;
stroke: white;
stroke-width: 2;
}
.lightbox-nav {
position: fixed;
top: 50%;
transform: translateY(-50%);
width: 50px;
height: 50px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.12);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--duration-fast) var(--ease-out);
z-index: 10;
}
.lightbox-nav:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-50%) scale(1.05);
}
.lightbox-nav:active {
transform: translateY(-50%) scale(0.95);
}
.lightbox-nav svg {
width: 22px;
height: 22px;
stroke: white;
stroke-width: 2;
}
.lightbox-prev { left: 20px; }
.lightbox-next { right: 20px; }
/* Zoom hint toast */
.zoom-hint {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
padding: 10px 16px;
background: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(10px);
border-radius: 10px;
font-size: 13px;
color: white;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
z-index: 20;
}
.zoom-hint.visible {
opacity: 1;
}
/* ─── Utilities ─── */
.hidden { display: none !important; }
/* ─── Mobile Responsive ─── */
@media (max-width: 960px) {
:root {
--sidebar-width: 240px;
}
.content-wrapper {
padding: var(--space-8) var(--space-5);
}
}
@media (max-width: 768px) {
.sidebar {
position: relative;
width: 100%;
height: auto;
border-right: none;
border-bottom: 1px solid var(--color-separator);
}
.main {
margin-left: 0;
}
.layout {
flex-direction: column;
}
.content h1 {
font-size: 28px;
}
.content h2 {
font-size: 22px;
}
.diagrams-grid {
grid-template-columns: 1fr;
}
}
/* ─── Print ─── */
@media print {
.header, .sidebar {
display: none;
}
.main {
margin-left: 0;
}
.content-wrapper {
max-width: none;
padding: 0;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-logo">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
<span class="header-title">Academia</span>
</div>
<div class="header-divider"></div>
<span class="header-subtitle">Documentación del Sistema</span>
</header>
<div class="layout">
<aside class="sidebar">
<div class="search-wrap">
<div class="search-box">
<input type="text" id="search" placeholder="Buscar documentos...">
</div>
</div>
<nav id="nav"></nav>
</aside>
<main class="main">
<div class="content-wrapper">
<article class="content" id="content">
<div class="welcome">
<div class="welcome-icon">📚</div>
<h1>Documentación</h1>
<p>Selecciona un documento del menú lateral para explorar la documentación del sistema.</p>
</div>
</article>
</div>
</main>
</div>
<!-- Lightbox Modal with Zoom & Pan -->
<div class="lightbox" id="lightbox">
<!-- Top Toolbar -->
<div class="lightbox-toolbar">
<button onclick="zoomOut()" id="btn-zoom-out" aria-label="Alejar">
<svg viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" stroke-linecap="round"/></svg>
</button>
<span class="zoom-level" id="zoom-level">100%</span>
<button onclick="zoomIn()" id="btn-zoom-in" aria-label="Acercar">
<svg viewBox="0 0 24 24"><path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" stroke-linecap="round"/></svg>
</button>
<div class="divider"></div>
<button onclick="resetZoom()" aria-label="Restablecer zoom">
<svg viewBox="0 0 24 24"><path d="M4 4v5h5M20 20v-5h-5M4 9a8 8 0 0114-5.3M20 15a8 8 0 01-14 5.3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
<button onclick="fitToScreen()" aria-label="Ajustar a pantalla">
<svg viewBox="0 0 24 24"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<!-- Close Button -->
<button class="lightbox-close" onclick="closeLightbox()" aria-label="Cerrar">
<svg viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round"/>
</svg>
</button>
<!-- Navigation -->
<button class="lightbox-nav lightbox-prev" onclick="navigateLightbox(-1)" aria-label="Anterior">
<svg viewBox="0 0 24 24" fill="none">
<path d="M15 18l-6-6 6-6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="lightbox-nav lightbox-next" onclick="navigateLightbox(1)" aria-label="Siguiente">
<svg viewBox="0 0 24 24" fill="none">
<path d="M9 18l6-6-6-6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<!-- Image Viewport (for pan & zoom) -->
<div class="lightbox-viewport" id="lightbox-viewport">
<div class="lightbox-content" id="lightbox-content">
<img id="lightbox-img" src="" alt="" draggable="false">
</div>
</div>
<!-- Bottom Info Bar -->
<div class="lightbox-info">
<span class="lightbox-title" id="lightbox-title"></span>
<span class="lightbox-counter" id="lightbox-counter"></span>
</div>
<!-- Zoom Hint Toast -->
<div class="zoom-hint" id="zoom-hint">Usa la rueda del ratón o pellizca para hacer zoom</div>
</div>
<script>
// ═══════════════════════════════════════════════════════════
// EMBEDDED DOCUMENTS & SVG DIAGRAMS
// ═══════════════════════════════════════════════════════════
const diagrams = {
"01-use-cases": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjQzN3B4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6MzM5NXB4O2hlaWdodDo0MzdweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMzk1IDQzNyIgd2lkdGg9IjMzOTVweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBEaWFncmFtYSBkZSBDYXNvcyBkZSBVc288L3RpdGxlPjxkZWZzLz48Zz48ZyBjbGFzcz0idGl0bGUiIGRhdGEtc291cmNlLWxpbmU9IjgiPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OTcuNjcxOSIgeD0iMTI5NC40NjE1IiB5PSIzNS40MjA5Ij5TaXN0ZW1hIGRlIFJlZ2lzdHJvIGRlIEVzdHVkaWFudGVzIC0gRGlhZ3JhbWEgZGUgQ2Fzb3MgZGUgVXNvPC90ZXh0PjwvZz48IS0tY2x1c3RlciBTaXN0ZW1hIGRlIEluc2NyaXBjaT9uIEFjYWQ/bWljYS0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iU2lzdGVtYSBkZSBJbnNjcmlwY2kubiBBY2FkLm1pY2EiIGRhdGEtc291cmNlLWxpbmU9IjEzIiBkYXRhLXVpZD0iZW50MDAwNCIgaWQ9ImNsdXN0ZXJfU2lzdGVtYSBkZSBJbnNjcmlwY2kubiBBY2FkLm1pY2EiPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iMjQ5LjcxIiByeD0iMi41IiByeT0iMi41IiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iMjUwMiIgeD0iMTIiIHk9IjE4MS45MDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjI2OS4wMjE1IiB4PSIxMTI4LjQ4OTMiIHk9IjE5Ni45MDQ1Ij5TaXN0ZW1hIGRlIEluc2NyaXBjaSYjMjQzO24gQWNhZCYjMjMzO21pY2E8L3RleHQ+PC9nPjwhLS1lbnRpdHkgVUNfTE9HSU4tLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLWVudGl0eT0iVUNfTE9HSU4iIGRhdGEtc291cmNlLWxpbmU9IjE1IiBkYXRhLXVpZD0iZW50MDAwNSIgaWQ9ImVudGl0eV9VQ19MT0dJTiI+PGVsbGlwc2UgY3g9IjE3NDUuMDAxMSIgY3k9IjI0MC41NTM2IiBmaWxsPSIjRjhGOUZBIiByeD0iNjQuMzcxMSIgcnk9IjE1LjI3NDIiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI5MS43OTMiIHg9IjE2OTkuMTA0NiIgeT0iMjQ1LjIwMiI+SW5pY2lhciBzZXNpJiMyNDM7bjwvdGV4dD48L2c+PCEtLWVudGl0eSBVQ19SRUNPVkVSLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1lbnRpdHk9IlVDX1JFQ09WRVIiIGRhdGEtc291cmNlLWxpbmU9IjE2IiBkYXRhLXVpZD0iZW50MDAwNiIgaWQ9ImVudGl0eV9VQ19SRUNPVkVSIj48ZWxsaXBzZSBjeD0iMTkzNC45OTY5IiBjeT0iMjQwLjU0NDciIGZpbGw9IiNGOEY5RkEiIHJ4PSI5MC42NzY5IiByeT0iMjAuNTM1NCIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE1NS4yNzE1IiB4PSIxODU3LjM2MTEiIHk9IjI0NS4xOTMyIj5SZWN1cGVyYXIgY29udHJhc2UmIzI0MTthPC90ZXh0PjwvZz48IS0tZW50aXR5IFVDX1JFR0lTVEVSLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1lbnRpdHk9IlVDX1JFR0lTVEVSIiBkYXRhLXNvdXJjZS1saW5lPSIxOSIgZGF0YS11aWQ9ImVudDAwMDciIGlkPSJlbnRpdHlfVUNfUkVHSVNURVIiPjxlbGxpcHNlIGN4PSI3MDkuOTk2NyIgY3k9IjI0MC41NTMiIGZpbGw9IiNGOEY5RkEiIHJ4PSI1OS40OTY3IiByeT0iMTQuNTIzNiIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc5Ljg5ODQiIHg9IjY3MC4wNDc1IiB5PSIyNDUuMjAxNCI+UmVnaXN0cmFyc2U8L3RleHQ+PC9nPjwhLS1lbnRpdHkgVUNfQUNUSVZBVEUtLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLWVudGl0eT0iVUNfQUNUSVZBVEUiIGRhdGEtc291cmNlLWxpbmU9IjIwIiBkYXRhLXVpZD0iZW50MDAwOCIgaWQ9ImVudGl0eV9VQ19BQ1RJVkFURSI+PGVsbGlwc2UgY3g9IjU0Ni45OTY3IiBjeT0iMjQwLjU1MjciIGZpbGw9IiNGOEY5RkEiIHJ4PSI2OC4yMTY3IiByeT0iMTYuMDQzMyIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEwMS44NDg2IiB4PSI0OTYuMDcyMyIgeT0iMjQ1LjIwMTEiPkFjdGl2YXIgY3VlbnRhPC90ZXh0PjwvZz48IS0tZW50aXR5IFVDX0RBU0hCT0FSRC0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtZW50aXR5PSJVQ19EQVNIQk9BUkQiIGRhdGEtc291cmNlLWxpbmU9IjIxIiBkYXRhLXVpZD0iZW50MDAwOSIgaWQ9ImVudGl0eV9VQ19EQVNIQk9BUkQiPjxlbGxpcHNlIGN4PSI5MDEuMDAzOSIgY3k9IjI0MC41NDYxIiBmaWxsPSIjRjhGOUZBIiByeD0iOTYuMTgzOSIgcnk9IjIxLjYzNjgiIHN0eWxl
"02-domain-model": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkNMQVNTIiBoZWlnaHQ9Ijk5MnB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6MTU3NnB4O2hlaWdodDo5OTJweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNTc2IDk5MiIgd2lkdGg9IjE1NzZweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBNb2RlbG8gZGUgRG9taW5pbzwvdGl0bGU+PGRlZnMvPjxnPjxnIGNsYXNzPSJ0aXRsZSIgZGF0YS1zb3VyY2UtbGluZT0iNyI+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMjIiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjcwNS45NDQzIiB4PSI0MzAuNjU4NiIgeT0iMzUuNDIwOSI+U2lzdGVtYSBkZSBSZWdpc3RybyBkZSBFc3R1ZGlhbnRlcyAtIE1vZGVsbyBkZSBEb21pbmlvPC90ZXh0PjwvZz48IS0tY2x1c3RlciBEb21haW4tLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1lbnRpdHk9IkRvbWFpbiIgZGF0YS1zb3VyY2UtbGluZT0iOSIgZGF0YS11aWQ9ImVudDAwMDIiIGlkPSJjbHVzdGVyX0RvbWFpbiI+PHBhdGggZD0iTTEzLjUsNTcuNjA5NCBMNzQuNTQsNTcuNjA5NCBBMy43NSwzLjc1IDAgMCAxIDc3LjA0LDYwLjEwOTQgTDg0LjA0LDc5LjkwNjMgTDExNDcuNSw3OS45MDYzIEEyLjUsMi41IDAgMCAxIDExNTAsODIuNDA2MyBMMTE1MCw5ODIuOTk5NCBBMi41LDIuNSAwIDAgMSAxMTQ3LjUsOTg1LjQ5OTQgTDEzLjUsOTg1LjQ5OTQgQTIuNSwyLjUgMCAwIDEgMTEsOTgyLjk5OTQgTDExLDYwLjEwOTQgQTIuNSwyLjUgMCAwIDEgMTMuNSw1Ny42MDk0IiBmaWxsPSIjRkZGRkZGIiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgeDE9IjExIiB4Mj0iODQuMDQiIHkxPSI3OS45MDYzIiB5Mj0iNzkuOTA2MyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI2MC4wNCIgeD0iMTUiIHk9IjcyLjYwNDUiPkRvbWFpbjwvdGV4dD48L2c+PCEtLWNsYXNzIFVzZXItLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLWVudGl0eT0iVXNlciIgZGF0YS1zb3VyY2UtbGluZT0iMTEiIGRhdGEtdWlkPSJlbnQwMDAzIiBpZD0iZW50aXR5X1VzZXIiPjxyZWN0IGZpbGw9IiNGOEY5RkEiIGhlaWdodD0iMjY4LjEyNSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjMyNS45MzM2IiB4PSIyNy4wMyIgeT0iOTIuNjA5NCIvPjxlbGxpcHNlIGN4PSIxNjMuMzIzOSIgY3k9IjExMi43NDIyIiBmaWxsPSIjRkZGRkZGIiByeD0iOSIgcnk9IjkiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiLz48cGF0aCBkPSJNMTY2LjI5MjcsMTE4LjM4MjggUTE2NS43MTQ2LDExOC42Nzk3IDE2NS4wNzM5LDExOC44MjAzIFExNjQuNDMzMywxMTguOTc2NiAxNjMuNzMwMiwxMTguOTc2NiBRMTYxLjIzMDIsMTE4Ljk3NjYgMTU5LjkwMjEsMTE3LjMzNTkgUTE1OC41ODk2LDExNS42Nzk3IDE1OC41ODk2LDExMi41NTQ3IFExNTguNTg5NiwxMDkuNDI5NyAxNTkuOTAyMSwxMDcuNzczNCBRMTYxLjIzMDIsMTA2LjExNzIgMTYzLjczMDIsMTA2LjExNzIgUTE2NC40MzMzLDEwNi4xMTcyIDE2NS4wNzM5LDEwNi4yNzM0IFExNjUuNzMwMiwxMDYuNDI5NyAxNjYuMjkyNywxMDYuNzI2NiBMMTY2LjI5MjcsMTA5LjQ0NTMgUTE2NS42Njc3LDEwOC44NjcyIDE2NS4wNzM5LDEwOC42MDE2IFExNjQuNDgwMiwxMDguMzIwMyAxNjMuODU1MiwxMDguMzIwMyBRMTYyLjUxMTQsMTA4LjMyMDMgMTYxLjgyMzksMTA5LjM5ODQgUTE2MS4xMzY0LDExMC40NjA5IDE2MS4xMzY0LDExMi41NTQ3IFExNjEuMTM2NCwxMTQuNjQ4NCAxNjEuODIzOSwxMTUuNzI2NiBRMTYyLjUxMTQsMTE2Ljc4OTEgMTYzLjg1NTIsMTE2Ljc4OTEgUTE2NC40ODAyLDExNi43ODkxIDE2NS4wNzM5LDExNi41MjM0IFExNjUuNjY3NywxMTYuMjQyMiAxNjYuMjkyNywxMTUuNjY0MSBMMTY2LjI5MjcsMTE4LjM4MjggWiAiIGZpbGw9IiMwMDAwMDAiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxMiIgZm9udC1zdHlsZT0iaXRhbGljIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjQ5Ljg0NTciIHg9IjE3OC44MjM5IiB5PSIxMDguNzQ4Ij4mIzE3MTtFbnRpdHkmIzE4Nzs8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM2LjEwMDYiIHg9IjE4NS42OTY1IiB5PSIxMjQuNTczMiI+VXNlcjwvdGV4dD48bGluZSBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iMjguMDMiIHgyPSIzNTEuOTYzNiIgeTE9IjEzMi44NzUiIHkyPSIxMzIuODc1Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNDUuMjQ3MSIgeD0iMzMuMDMiIHk9IjE0OS44NzAxIj4taWQ6IGludDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMjQuNjQ2NSIgeD0iMzMuMDMiIHk9I
"03-sequence-enrollment": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNFUVVFTkNFIiBoZWlnaHQ9IjE3OThweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjE5MDVweDtoZWlnaHQ6MTc5OHB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDE5MDUgMTc5OCIgd2lkdGg9IjE5MDVweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNlY3VlbmNpYTogSW5zY3JpcGNpJiMyNDM7biBkZSBFc3R1ZGlhbnRlIGVuIE1hdGVyaWEgKGNvbiBKV1QpPC90aXRsZT48ZGVmcy8+PGc+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMjIiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjcyNC45NTgiIHg9IjU4OC45NzA1IiB5PSIzNS40MjA5Ij5TZWN1ZW5jaWE6IEluc2NyaXBjaSYjMjQzO24gZGUgRXN0dWRpYW50ZSBlbiBNYXRlcmlhIChjb24gSldUKTwvdGV4dD48Zz48dGl0bGU+RnJvbnRlbmQ8L3RpdGxlPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iMTM4Ny4zNzUiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMCIgeD0iMjYzLjM3NCIgeT0iMzExLjcwMzEiLz48L2c+PGc+PHRpdGxlPkFQSSBHcmFwaFFMPC90aXRsZT48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjEyODMuNzEwOSIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjEwIiB4PSI0OTIuMjE3MyIgeT0iMzcxLjEwMTYiLz48L2c+PGc+PHRpdGxlPkpXVCBNaWRkbGV3YXJlPC90aXRsZT48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9Ijg2LjM5ODQiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMCIgeD0iNjQ0LjgwMjciIHk9IjQwMC4yMzQ0Ii8+PC9nPjxnPjx0aXRsZT5FbnJvbGxTdHVkZW50SGFuZGxlcjwvdGl0bGU+PHJlY3QgZmlsbD0iI0ZGRkZGRiIgaGVpZ2h0PSIxMTA5LjkxNDEiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMCIgeD0iODE5LjYzOTIiIHk9IjUxNS43NjU2Ii8+PC9nPjxnPjx0aXRsZT5FbnJvbGxtZW50RG9tYWluU2VydmljZTwvdGl0bGU+PHJlY3QgZmlsbD0iI0ZGRkZGRiIgaGVpZ2h0PSI0MzguOTkyMiIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjEwIiB4PSIxMTI3LjQxOTkiIHk9Ijk5Ny44OTA2Ii8+PC9nPjxnPjx0aXRsZT5TdHVkZW50UmVwb3NpdG9yeTwvdGl0bGU+PHJlY3QgZmlsbD0iI0ZGRkZGRiIgaGVpZ2h0PSI4Ny4zOTg0IiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iMTAiIHg9IjEzMzkuNTA5OCIgeT0iNTg4LjAzMTMiLz48L2c+PGc+PHRpdGxlPlN1YmplY3RSZXBvc2l0b3J5PC90aXRsZT48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9Ijg3LjM5ODQiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMCIgeD0iMTUxMi4zMDU3IiB5PSI4MzguMjI2NiIvPjwvZz48Zz48dGl0bGU+RW5yb2xsbWVudFJlcG9zaXRvcnk8L3RpdGxlPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iODcuMzk4NCIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjEwIiB4PSIxNjk1Ljc2MjIiIHk9IjE1MDkuMTQ4NCIvPjwvZz48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9IjExOS42NjQxIiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iOTExLjY2NyIgeD0iMTUiIHk9IjY5MC40Mjk3Ii8+PHJlY3QgZmlsbD0iI0ZGRkZGRiIgaGVpZ2h0PSIxMzMuNjY0MSIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjEyMzUuNjY5NCIgeD0iMTUiIHk9IjEwNzAuMTU2MyIvPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iMTMzLjY2NDEiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMjM1LjY2OTQiIHg9IjE1IiB5PSIxMjc1LjA4NTkiLz48Zz48dGl0bGU+RXN0dWRpYW50ZTwvdGl0bGU+PHJlY3QgZmlsbD0iIzAwMDAwMCIgZmlsbC1vcGFjaXR5PSIwLjAwMDAwIiBoZWlnaHQ9IjE1ODguMTcxOSIgd2lkdGg9IjgiIHg9IjYxLjQxNjUiIHk9IjEyOC45MDYzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxO3N0cm9rZS1kYXNoYXJyYXk6NSw1OyIgeDE9IjY1IiB4Mj0iNjUiIHkxPSIxMjguOTA2MyIgeTI9IjE3MTcuMDc4MSIvPjwvZz48Zz48dGl0bGU+RnJvbnRlbmQ8L3RpdGxlPjxyZWN0IGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC4wMDAwMCIgaGVpZ2h0PSIxNTg4LjE3MTkiIHdpZHRoPSI4IiB4PSIyNjQuMzc0IiB5PSIxMjguOTA2MyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTtzdHJva2UtZGFzaGFycmF5OjUsNTsiIHgxPSIyNjcuNjk0OCIgeDI9IjI2Ny42OTQ4IiB5MT0iMTI4LjkwNjMiIHkyPSIxNzE3LjA3ODEiLz48L2c+PGc+PHRpdGxlPkFQSSBHcmFwaFFMPC90aXRsZT48cmVjdCBmaWxsPSIjMDAwMDAwIiBmaWxsLW9wYWNpdHk9IjAuMDAwMDAiIGhlaWdodD0iMTU4OC4xNzE5IiB3aWR0aD0iOCIgeD0iNDkzLjIxNzMiIHk9IjEyOC45MDYzIi8+PGxpbmUgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxO3N0cm9rZS1kYXNoYXJyYXk6NSw1OyIgeDE9IjQ5Ni40NDA0IiB4Mj0iNDk2LjQ0MDQiIHkxPSIxMjguOTA2MyIgeTI9IjE3MTcuMDc4MSIvPjwvZz48Zz48dGl0bGU+SldUIE1pZGRsZXdhcmU8L3RpdGxlPjxyZWN0IGZpbGw9IiMwMDAwMDAiIGZpbGwtb3BhY2l0eT0iMC
"04-components": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9Ijk3NHB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6NjU3NnB4O2hlaWdodDo5NzRweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA2NTc2IDk3NCIgd2lkdGg9IjY1NzZweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBBcnF1aXRlY3R1cmEgZGUgQ29tcG9uZW50ZXM8L3RpdGxlPjxkZWZzLz48Zz48ZyBjbGFzcz0idGl0bGUiIGRhdGEtc291cmNlLWxpbmU9IjkiPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4NDAuNDU4IiB4PSIyODYzLjI3NzUiIHk9IjM1LjQyMDkiPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBBcnF1aXRlY3R1cmEgZGUgQ29tcG9uZW50ZXM8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIGZyb250ZW5kLS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtZW50aXR5PSJmcm9udGVuZCIgZGF0YS1zb3VyY2UtbGluZT0iMTEiIGRhdGEtdWlkPSJlbnQwMDAyIiBpZD0iY2x1c3Rlcl9mcm9udGVuZCI+PHJlY3QgZmlsbD0iI0ZGRkZGRiIgaGVpZ2h0PSI2MzAuMTkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6I0RFRTJFNjtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSI0MDg5IiB4PSIyMTU2IiB5PSIzMjIuMzI5NCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxNzYuMTk2MyIgeD0iNDExMi40MDE5IiB5PSIzMzcuMzI0NSI+RnJvbnRlbmQgKEFuZ3VsYXIgMjEpPC90ZXh0PjwvZz48IS0tY2x1c3RlciBGZWF0dXJlcy0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iRmVhdHVyZXMiIGRhdGEtc291cmNlLWxpbmU9IjE0IiBkYXRhLXVpZD0iZW50MDAwNCIgaWQ9ImNsdXN0ZXJfRmVhdHVyZXMiPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iMTY0LjMiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6I0RFRTJFNjtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIyOTI1IiB4PSIyNjg2IiB5PSI0OTIuNjI5NCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI2OS44OTc1IiB4PSI0MTEzLjU1MTMiIHk9IjUwNy42MjQ1Ij5GZWF0dXJlczwvdGV4dD48L2c+PCEtLWNsdXN0ZXIgQXV0aC0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iQXV0aCIgZGF0YS1zb3VyY2UtbGluZT0iMTUiIGRhdGEtdWlkPSJlbnQwMDA1IiBpZD0iY2x1c3Rlcl9BdXRoIj48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9Ijk3LjMiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6I0RFRTJFNjtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxMjg1IiB4PSIzNDYzIiB5PSI1MzUuNjI5NCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIzNy40NjA5IiB4PSI0MDg2Ljc2OTUiIHk9IjU1MC42MjQ1Ij5BdXRoPC90ZXh0PjwvZz48IS0tY2x1c3RlciBEYXNoYm9hcmQtLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1lbnRpdHk9IkRhc2hib2FyZCIgZGF0YS1zb3VyY2UtbGluZT0iMjEiIGRhdGEtdWlkPSJlbnQwMDEwIiBpZD0iY2x1c3Rlcl9EYXNoYm9hcmQiPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iOTcuMyIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojREVFMkU2O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjQwOSIgeD0iNDc3MiIgeT0iNTM1LjYyOTQiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iODUuMzgwOSIgeD0iNDkzMy44MDk2IiB5PSI1NTAuNjI0NSI+RGFzaGJvYXJkPC90ZXh0PjwvZz48IS0tY2x1c3RlciBTdHVkZW50cy0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iU3R1ZGVudHMiIGRhdGEtc291cmNlLWxpbmU9IjI1IiBkYXRhLXVpZD0iZW50MDAxMyIgaWQ9ImNsdXN0ZXJfU3R1ZGVudHMiPjxyZWN0IGZpbGw9IiNGRkZGRkYiIGhlaWdodD0iOTcuMyIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojREVFMkU2O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjcyOSIgeD0iMjcxMCIgeT0iNTM1LjYyOTQiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgZm9udC13ZWlnaHQ9ImJvbGQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNzEuMjUxIiB4PSIzMDM4Ljg3NDUiIHk9IjU1MC42MjQ1Ij5TdHVkZW50czwvdGV4dD48L2c+PCEtLWNsdXN0ZXIgRW5yb2xsbWVudC0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iRW5yb2xsbWVudCIgZGF0YS1zb3VyY2UtbGluZT0iMjkiIGRhdGEtdWlkPSJlbnQwMDE2IiBpZD0iY2x1c3Rlcl9FbnJvbGxtZW50Ij48cmVjdCBmaWxsPSIjRkZGRkZGIiBoZWlnaHQ9Ijk3LjMiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6I0RFRTJFNjtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIzODIiIHg9IjUyMDUiIHk9IjU
"05-entity-relationship": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkNMQVNTIiBoZWlnaHQ9Ijc4MnB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6OTM5cHg7aGVpZ2h0Ojc4MnB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDkzOSA3ODIiIHdpZHRoPSI5MzlweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBEaWFncmFtYSBFbnRpZGFkLVJlbGFjaSYjMjQzO248L3RpdGxlPjxkZWZzLz48Zz48ZyBjbGFzcz0idGl0bGUiIGRhdGEtc291cmNlLWxpbmU9IjYiPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4MDQuODE1NCIgeD0iNjIuODgzOCIgeT0iMzUuNDIwOSI+U2lzdGVtYSBkZSBSZWdpc3RybyBkZSBFc3R1ZGlhbnRlcyAtIERpYWdyYW1hIEVudGlkYWQtUmVsYWNpJiMyNDM7bjwvdGV4dD48L2c+PCEtLWNsYXNzIHVzZXJzLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1lbnRpdHk9InVzZXJzIiBkYXRhLXNvdXJjZS1saW5lPSI4IiBkYXRhLXVpZD0iZW50MDAwMiIgaWQ9ImVudGl0eV91c2VycyI+PHJlY3QgZmlsbD0iI0Y4RjlGQSIgaGVpZ2h0PSIxNzQuMzc1IiByeD0iMi41IiByeT0iMi41IiBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB3aWR0aD0iMjc0LjMxODQiIHg9IjEyIiB5PSI1OC42MDk0Ii8+PGVsbGlwc2UgY3g9IjEyNS44MDcxIiBjeT0iNzIuNjA5NCIgZmlsbD0iI0ZGRkZGRiIgcng9IjkiIHJ5PSI5IiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7Ii8+PHBhdGggZD0iTTEyOS45MTY1LDc4LjYwOTQgTDEyMi4xOTc4LDc4LjYwOTQgTDEyMi4xOTc4LDY2LjIxODggTDEyOS45MTY1LDY2LjIxODggTDEyOS45MTY1LDY4LjM3NSBMMTI0LjY1MDksNjguMzc1IEwxMjQuNjUwOSw3MS4wNDY5IEwxMjkuNDE2NSw3MS4wNDY5IEwxMjkuNDE2NSw3My4yMDMxIEwxMjQuNjUwOSw3My4yMDMxIEwxMjQuNjUwOSw3Ni40NTMxIEwxMjkuOTE2NSw3Ni40NTMxIEwxMjkuOTE2NSw3OC42MDk0IFogIiBmaWxsPSIjMDAwMDAwIi8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzkuMjA0MSIgeD0iMTQzLjMwNzEiIHk9Ijc3LjQ1NjEiPlVzZXJzPC90ZXh0PjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHgxPSIxMyIgeDI9IjI4NS4zMTg0IiB5MT0iODYuNjA5NCIgeTI9Ijg2LjYwOTQiLz48ZWxsaXBzZSBjeD0iMjMiIGN5PSIxMDAuMjU3OCIgZmlsbD0iI0ZGRkZGRiIgcng9IjMiIHJ5PSIzIiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE1LjIzMDUiIHg9IjMwIiB5PSIxMDMuNjA0NSI+SWQ8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNjYuNjIzIiB4PSI0OS42ODA3IiB5PSIxMDMuNjA0NSI+OiBpbnQgJiMxNzE7UEsmIzE4Nzs8L3RleHQ+PGxpbmUgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIgeDE9IjEzIiB4Mj0iMjg1LjMxODQiIHkxPSIxMTAuOTA2MyIgeTI9IjExMC45MDYzIi8+PGVsbGlwc2UgY3g9IjIzIiBjeT0iMTI0LjU1NDciIGZpbGw9IiNGRkZGRkYiIHJ4PSIzIiByeT0iMyIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjI0NS45NTciIHg9IjMwIiB5PSIxMjcuOTAxNCI+VXNlcm5hbWUgOiBudmFyY2hhcig1MCkgJiMxNzE7dW5pcXVlJiMxODc7PC90ZXh0PjxlbGxpcHNlIGN4PSIyMyIgY3k9IjE0MC44NTE2IiBmaWxsPSIjRkZGRkZGIiByeD0iMyIgcnk9IjMiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyMTUuMjAyMSIgeD0iMzAiIHk9IjE0NC4xOTgyIj5QYXNzd29yZEhhc2ggOiBudmFyY2hhcigyNTUpPC90ZXh0PjxlbGxpcHNlIGN4PSIyMyIgY3k9IjE1Ny4xNDg0IiBmaWxsPSIjRkZGRkZGIiByeD0iMyIgcnk9IjMiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyNTAuMzE4NCIgeD0iMzAiIHk9IjE2MC40OTUxIj5SZWNvdmVyeUNvZGVIYXNoIDogbnZhcmNoYXIoMjU1KTwvdGV4dD48ZWxsaXBzZSBjeD0iMjMiIGN5PSIxNzMuNDQ1MyIgZmlsbD0iI0ZGRkZGRiIgcng9IjMiIHJ5PSIzIiBzdHlsZT0ic3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjE7Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTM1LjU0OTgiIHg9IjMwIiB5PSIxNzYuNzkyIj5Sb2xlIDogbn
"06-state-enrollment": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IlNUQVRFIiBoZWlnaHQ9IjgxNnB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6MTQ2MnB4O2hlaWdodDo4MTZweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNDYyIDgxNiIgd2lkdGg9IjE0NjJweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPkVzdGFkb3MgZGVsIEVzdHVkaWFudGUgeSBzdXMgSW5zY3JpcGNpb25lczwvdGl0bGU+PGRlZnMvPjxnPjxnIGNsYXNzPSJ0aXRsZSIgZGF0YS1zb3VyY2UtbGluZT0iNSI+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMjIiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjUyNy4xNzI5IiB4PSI0NjMuMTA3OSIgeT0iMzUuNDIwOSI+RXN0YWRvcyBkZWwgRXN0dWRpYW50ZSB5IHN1cyBJbnNjcmlwY2lvbmVzPC90ZXh0PjwvZz48IS0tY2x1c3RlciBjdWVudGEtLT48ZyBjbGFzcz0iY2x1c3RlciIgZGF0YS1lbnRpdHk9ImN1ZW50YSIgZGF0YS1zb3VyY2UtbGluZT0iOSIgZGF0YS11aWQ9ImVudDAwMDIiIGlkPSJjbHVzdGVyX2N1ZW50YSI+PHBhdGggZD0iTTc0MS4wOCwxNjQuNzU5NCBMMTA3Ny4wOCwxNjQuNzU5NCBBMTIuNSwxMi41IDAgMCAxIDEwODkuNTgsMTc3LjI1OTQgTDEwODkuNTgsMTg2LjA1NjIgTDcyOC41OCwxODYuMDU2MiBMNzI4LjU4LDE3Ny4yNTk0IEExMi41LDEyLjUgMCAwIDEgNzQxLjA4LDE2NC43NTk0IiBmaWxsPSIjRjhGOUZBIi8+PHBhdGggZD0iTTcyOC41OCwxODYuMDU2MiBMMTA4OS41OCwxODYuMDU2MiBMMTA4OS41OCw0NDcuNDQ5NCBBMTIuNSwxMi41IDAgMCAxIDEwNzcuMDgsNDU5Ljk0OTQgTDc0MS4wOCw0NTkuOTQ5NCBBMTIuNSwxMi41IDAgMCAxIDcyOC41OCw0NDcuNDQ5NCBMNzI4LjU4LDE4Ni4wNTYyIiBmaWxsPSIjRkZGRkZGIi8+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyOTUuMTkiIHJ4PSIxMi41IiByeT0iMTIuNSIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjM2MSIgeD0iNzI4LjU4IiB5PSIxNjQuNzU5NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHgxPSI3MjguNTgiIHgyPSIxMDg5LjU4IiB5MT0iMTg2LjA1NjIiIHkyPSIxODYuMDU2MiIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjUwLjIwMzEiIHg9Ijg4My45Nzg0IiB5PSIxODEuNzU0NSI+Q3VlbnRhPC90ZXh0PjwvZz48IS0tY2x1c3RlciBpbnNjcmlwY2lvbmVzLS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtZW50aXR5PSJpbnNjcmlwY2lvbmVzIiBkYXRhLXNvdXJjZS1saW5lPSIxNyIgZGF0YS11aWQ9ImVudDAwMDUiIGlkPSJjbHVzdGVyX2luc2NyaXBjaW9uZXMiPjxwYXRoIGQ9Ik0yOTkuMDgsMTQwLjYwOTQgTDY5Mi4wOCwxNDAuNjA5NCBBMTIuNSwxMi41IDAgMCAxIDcwNC41OCwxNTMuMTA5NCBMNzA0LjU4LDE2MS45MDYzIEwyODYuNTgsMTYxLjkwNjMgTDI4Ni41OCwxNTMuMTA5NCBBMTIuNSwxMi41IDAgMCAxIDI5OS4wOCwxNDAuNjA5NCIgZmlsbD0iI0Y4RjlGQSIvPjxwYXRoIGQ9Ik0yODYuNTgsMTYxLjkwNjMgTDcwNC41OCwxNjEuOTA2MyBMNzA0LjU4LDY2My4xODk0IEExMi41LDEyLjUgMCAwIDEgNjkyLjA4LDY3NS42ODk0IEwyOTkuMDgsNjc1LjY4OTQgQTEyLjUsMTIuNSAwIDAgMSAyODYuNTgsNjYzLjE4OTQgTDI4Ni41OCwxNjEuOTA2MyIgZmlsbD0iI0ZGRkZGRiIvPjxyZWN0IGZpbGw9Im5vbmUiIGhlaWdodD0iNTM1LjA4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSI0MTgiIHg9IjI4Ni41OCIgeT0iMTQwLjYwOTQiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iMjg2LjU4IiB4Mj0iNzA0LjU4IiB5MT0iMTYxLjkwNjMiIHkyPSIxNjEuOTA2MyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjkxLjQ1OCIgeD0iNDQ5Ljg1MSIgeT0iMTU3LjYwNDUiPkluc2NyaXBjaW9uZXM8L3RleHQ+PC9nPjxnIGlkPSJjdWVudGEuUmVnaXN0cmFkbyI+PHJlY3QgZmlsbD0iI0Y4RjlGQSIgaGVpZ2h0PSI1Mi41OTM4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNzguMjEwOSIgeD0iNzQ0LjQ4IiB5PSIxOTkuNzU5NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHgxPSI3NDQuNDgiIHgyPSI5MjIuNjkwOSIgeTE9IjI0Mi4zNTMxIiB5Mj0iMjQyLjM1MzEiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3NS42ODc1IiB4PSI3NTQuNDgiIHk9IjIxNy43NTQ1Ij5SZWdpc3RyYWRvPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE1OC4yMTA5IiB4PSI3NTQuNDgiIHk9IjIzNC4wNTE0Ij4oUGVuZGllbnRlIEFjdGl2YWNpJiMyNDM7bik8L3RleHQ+PC9nPjxnIGlkPSJjdWVudGEuQWN0aXZvIj48cmVjdCBmaWxsPSIjRjhGOUZBIiBoZWlnaHQ9IjUwIiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6M
"07-deployment": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjE1MDhweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjEzNDhweDtoZWlnaHQ6MTUwOHB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDEzNDggMTUwOCIgd2lkdGg9IjEzNDhweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBEaWFncmFtYSBkZSBEZXNwbGllZ3VlPC90aXRsZT48ZGVmcy8+PGc+PGcgY2xhc3M9InRpdGxlIiBkYXRhLXNvdXJjZS1saW5lPSI2Ij48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIyMiIgZm9udC13ZWlnaHQ9ImJvbGQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNzcxLjY3NTgiIHg9IjI4My43NDkyIiB5PSIzNS40MjA5Ij5TaXN0ZW1hIGRlIFJlZ2lzdHJvIGRlIEVzdHVkaWFudGVzIC0gRGlhZ3JhbWEgZGUgRGVzcGxpZWd1ZTwvdGV4dD48L2c+PCEtLWNsdXN0ZXIgY2xpZW50LS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtZW50aXR5PSJjbGllbnQiIGRhdGEtc291cmNlLWxpbmU9IjgiIGRhdGEtdWlkPSJlbnQwMDAyIiBpZD0iY2x1c3Rlcl9jbGllbnQiPjxwb2x5Z29uIGZpbGw9IiNGOEY5RkEiIHBvaW50cz0iNjI5LDY3LjYwOTQsNjM5LDU3LjYwOTQsODg2LDU3LjYwOTQsODg2LDI0OC45MDk0LDg3NiwyNTguOTA5NCw2MjksMjU4LjkwOTQsNjI5LDY3LjYwOTQiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iODc2IiB4Mj0iODg2IiB5MT0iNjcuNjA5NCIgeTI9IjU3LjYwOTQiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iNjI5IiB4Mj0iODc2IiB5MT0iNjcuNjA5NCIgeTI9IjY3LjYwOTQiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiM0OTUwNTc7c3Ryb2tlLXdpZHRoOjE7IiB4MT0iODc2IiB4Mj0iODc2IiB5MT0iNjcuNjA5NCIgeTI9IjI1OC45MDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU1LjUyMTUiIHg9IjcyNS43MzkzIiB5PSI4My42MDQ1Ij5DbGllbnRlPC90ZXh0PjwvZz48IS0tY2x1c3RlciBicm93c2VyLS0+PGcgY2xhc3M9ImNsdXN0ZXIiIGRhdGEtZW50aXR5PSJicm93c2VyIiBkYXRhLXNvdXJjZS1saW5lPSI5IiBkYXRhLXVpZD0iZW50MDAwMyIgaWQ9ImNsdXN0ZXJfYnJvd3NlciI+PHJlY3QgZmlsbD0iI0U5RUNFRiIgaGVpZ2h0PSIxMTMuMyIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjE5MyIgeD0iNjYxIiB5PSIxMTMuNjA5NCIvPjxyZWN0IGZpbGw9IiNFOUVDRUYiIGhlaWdodD0iMTAiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNSIgeD0iODM0IiB5PSIxMTguNjA5NCIvPjxyZWN0IGZpbGw9IiNFOUVDRUYiIGhlaWdodD0iMiIgc3R5bGU9InN0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoxOyIgd2lkdGg9IjQiIHg9IjgzMiIgeT0iMTIwLjYwOTQiLz48cmVjdCBmaWxsPSIjRTlFQ0VGIiBoZWlnaHQ9IjIiIHN0eWxlPSJzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSI0IiB4PSI4MzIiIHk9IjEyNC42MDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEyNS42MzA5IiB4PSI2OTQuNjg0NiIgeT0iMTM5LjYwNDUiPk5hdmVnYWRvciBXZWI8L3RleHQ+PC9nPjwhLS1jbHVzdGVyIGszcy0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iazNzIiBkYXRhLXNvdXJjZS1saW5lPSIxNCIgZGF0YS11aWQ9ImVudDAwMDUiIGlkPSJjbHVzdGVyX2szcyI+PHBvbHlnb24gZmlsbD0iI0Y4RjlGQSIgcG9pbnRzPSIyMSw0NDkuMTk5NCwzMSw0MzkuMTk5NCw5MjAsNDM5LjE5OTQsOTIwLDE0ODEuNjM5NCw5MTAsMTQ5MS42Mzk0LDIxLDE0OTEuNjM5NCwyMSw0NDkuMTk5NCIgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHgxPSI5MTAiIHgyPSI5MjAiIHkxPSI0NDkuMTk5NCIgeTI9IjQzOS4xOTk0Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojNDk1MDU3O3N0cm9rZS13aWR0aDoxOyIgeDE9IjIxIiB4Mj0iOTEwIiB5MT0iNDQ5LjE5OTQiIHkyPSI0NDkuMTk5NCIvPjxsaW5lIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHgxPSI5MTAiIHgyPSI5MTAiIHkxPSI0NDkuMTk5NCIgeTI9IjE0OTEuNjM5NCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyODQuNzkyIiB4PSIzMjQuMTA0IiB5PSI0NjUuMTk0NSI+SzNzIENsdXN0ZXIgKE5hbWVzcGFjZTogYWNhZGVtaWEpPC90ZXh0PjwvZz48IS0tY2x1c3RlciBmcm9udGVuZFBvZC0tPjxnIGNsYXNzPSJjbHVzdGVyIiBkYXRhLWVudGl0eT0iZnJvbnRlbmRQb2QiIGRhdGEtc291cmNlLWxpbmU9IjE2IiBkYXRhLXVpZD0iZW50MDAwNiIgaWQ9ImNsdXN0ZXJfZnJvbnRlbmRQb2QiPjxwb2x5Z29uIGZpbGw9IiNGOEY5RkEiIHBvaW50cz0iNDQ1LDcwNC40OTk0LDQ
"08-c4-context": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgZGF0YS1kaWFncmFtLXR5cGU9IkRFU0NSSVBUSU9OIiBoZWlnaHQ9IjcwMnB4IiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJub25lIiBzdHlsZT0id2lkdGg6OTM0cHg7aGVpZ2h0OjcwMnB4O2JhY2tncm91bmQ6I0ZGRkZGRjsiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDkzNCA3MDIiIHdpZHRoPSI5MzRweCIgem9vbUFuZFBhbj0ibWFnbmlmeSI+PHRpdGxlPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBEaWFncmFtYSBkZSBDb250ZXh0byAoQzQgTGV2ZWwgMSk8L3RpdGxlPjxkZWZzLz48Zz48ZyBjbGFzcz0idGl0bGUiIGRhdGEtc291cmNlLWxpbmU9IjUiPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iYm9sZCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI5MDEuMDg2OSIgeD0iMTUiIHk9IjM1LjQyMDkiPlNpc3RlbWEgZGUgUmVnaXN0cm8gZGUgRXN0dWRpYW50ZXMgLSBEaWFncmFtYSBkZSBDb250ZXh0byAoQzQgTGV2ZWwgMSk8L3RleHQ+PC9nPjwhLS1lbnRpdHkgc3lzdGVtLS0+PGcgY2xhc3M9ImVudGl0eSIgZGF0YS1lbnRpdHk9InN5c3RlbSIgZGF0YS1zb3VyY2UtbGluZT0iMTQiIGRhdGEtdWlkPSJlbnQwMDA0IiBpZD0iZW50aXR5X3N5c3RlbSI+PHJlY3QgZmlsbD0iI0FERDhFNiIgaGVpZ2h0PSI2OC44OTA2IiByeD0iNSIgcnk9IjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNzguMzQwOCIgeD0iMzMzLjA2MDciIHk9IjM0MS44ODk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtc3R5bGU9Iml0YWxpYyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMzYuMjA2MSIgeD0iMzU0LjEyODEiIHk9IjM2NC44ODQ1Ij4mIzE3MTtTb2Z0d2FyZSBTeXN0ZW0mIzE4Nzs8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTU4LjM0MDgiIHg9IjM0My4wNjA3IiB5PSIzODEuMTgxNCI+U2lzdGVtYSBkZSBJbnNjcmlwY2kmIzI0MztuPC90ZXh0Pjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJWZXJkYW5hIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9Ijc3LjE1NzIiIHg9IjM0My4wNjA3IiB5PSIzOTcuNDc4MiI+QWNhZCYjMjMzO21pY2E8L3RleHQ+PC9nPjwhLS1lbnRpdHkgZGF0YWJhc2UtLT48ZyBjbGFzcz0iZW50aXR5IiBkYXRhLWVudGl0eT0iZGF0YWJhc2UiIGRhdGEtc291cmNlLWxpbmU9IjE3IiBkYXRhLXVpZD0iZW50MDAwNSIgaWQ9ImVudGl0eV9kYXRhYmFzZSI+PHJlY3QgZmlsbD0iI0QzRDNEMyIgaGVpZ2h0PSI2OC44OTA2IiByeD0iNSIgcnk9IjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNTQuMjY4NiIgeD0iMTYyLjEwMDciIHk9IjYwMi42NDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtc3R5bGU9Iml0YWxpYyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMzIuMjY4NiIgeD0iMTczLjEwMDciIHk9IjYyNS42NDQ1Ij4mIzE3MTtFeHRlcm5hbCBTeXN0ZW0mIzE4Nzs8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTAxLjE5OTIiIHg9IjE4OC42MzU0IiB5PSI2NDEuOTQxNCI+QmFzZSBkZSBEYXRvczwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI3OC4wNjY0IiB4PSIxODguNjM1NCIgeT0iNjU4LjIzODIiPlNRTCBTZXJ2ZXI8L3RleHQ+PC9nPjwhLS1lbnRpdHkgc210cC0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtZW50aXR5PSJzbXRwIiBkYXRhLXNvdXJjZS1saW5lPSIyMCIgZGF0YS11aWQ9ImVudDAwMDYiIGlkPSJlbnRpdHlfc210cCI+PHJlY3QgZmlsbD0iI0QzRDNEMyIgaGVpZ2h0PSI2OC44OTA2IiByeD0iNSIgcnk9IjUiIHN0eWxlPSJzdHJva2U6IzQ5NTA1NztzdHJva2Utd2lkdGg6MTsiIHdpZHRoPSIxNTQuMjY4NiIgeD0iNTMwLjEwMDciIHk9IjYwMi42NDk0Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGZvbnQtc3R5bGU9Iml0YWxpYyIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIxMzIuMjY4NiIgeD0iNTQxLjEwMDciIHk9IjYyNS42NDQ1Ij4mIzE3MTtFeHRlcm5hbCBTeXN0ZW0mIzE4Nzs8L3RleHQ+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9IlZlcmRhbmEiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMTAxLjA0ODgiIHg9IjU1Ni43MTA2IiB5PSI2NDEuOTQxNCI+U2Vydmlkb3IgU01UUDwvdGV4dD48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0iVmVyZGFuYSIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0OS43NjU2IiB4PSI1NTYuNzEwNiIgeT0iNjU4LjIzODIiPihFbWFpbCk8L3RleHQ+PC9nPjwhLS1lbnRpdHkgc3R1ZGVudC0tPjxnIGNsYXNzPSJlbnRpdHkiIGRhdGEtZW50aXR5PSJzdHVkZW50IiBkYXRhLXNvdXJjZS1saW5lPSIxMSIgZGF0YS11aWQ9ImVudDAwMDIiIGlkPSJlbnRpdHlfc3R1ZGVudCI+PGVsbGlwc2UgY3g9IjkwLjIzMjkiIGN
};
const diagramNames = {
"01-use-cases": "Casos de Uso",
"02-domain-model": "Modelo de Dominio",
"03-sequence-enrollment": "Secuencia: Inscripción",
"04-components": "Componentes",
"05-entity-relationship": "Entidad-Relación",
"06-state-enrollment": "Estados: Inscripción",
"07-deployment": "Despliegue",
"08-c4-context": "Contexto C4"
};
const diagramKeys = Object.keys(diagramNames);
let currentDiagramIndex = 0;
// ═══════════════════════════════════════════════════════════
// LIGHTBOX WITH ZOOM & PAN (Apple-style)
// ═══════════════════════════════════════════════════════════
// Zoom state
let currentZoom = 1;
let panX = 0;
let panY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
let lastPanX = 0;
let lastPanY = 0;
const MIN_ZOOM = 0.5;
const MAX_ZOOM = 5;
const ZOOM_STEP = 0.25;
function openLightbox(key) {
const lightbox = document.getElementById('lightbox');
const img = document.getElementById('lightbox-img');
const title = document.getElementById('lightbox-title');
const counter = document.getElementById('lightbox-counter');
currentDiagramIndex = diagramKeys.indexOf(key);
img.src = diagrams[key];
title.textContent = diagramNames[key] || key;
counter.textContent = `${currentDiagramIndex + 1} / ${diagramKeys.length}`;
// Reset zoom and pan
resetZoom();
lightbox.classList.add('active');
document.body.style.overflow = 'hidden';
// Show zoom hint briefly
showZoomHint();
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
document.body.style.overflow = '';
resetZoom();
}
function navigateLightbox(direction) {
currentDiagramIndex = (currentDiagramIndex + direction + diagramKeys.length) % diagramKeys.length;
const key = diagramKeys[currentDiagramIndex];
document.getElementById('lightbox-img').src = diagrams[key];
document.getElementById('lightbox-title').textContent = diagramNames[key] || key;
document.getElementById('lightbox-counter').textContent = `${currentDiagramIndex + 1} / ${diagramKeys.length}`;
resetZoom();
}
// Zoom functions
function setZoom(newZoom, centerX, centerY) {
const oldZoom = currentZoom;
currentZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, newZoom));
// Adjust pan to zoom towards cursor position
if (centerX !== undefined && centerY !== undefined) {
const viewport = document.getElementById('lightbox-viewport');
const rect = viewport.getBoundingClientRect();
const relX = centerX - rect.left - rect.width / 2;
const relY = centerY - rect.top - rect.height / 2;
const scale = currentZoom / oldZoom;
panX = relX - (relX - panX) * scale;
panY = relY - (relY - panY) * scale;
}
updateTransform();
updateZoomUI();
}
function zoomIn() {
setZoom(currentZoom + ZOOM_STEP);
}
function zoomOut() {
setZoom(currentZoom - ZOOM_STEP);
}
function resetZoom() {
currentZoom = 1;
panX = 0;
panY = 0;
updateTransform();
updateZoomUI();
}
function fitToScreen() {
currentZoom = 1;
panX = 0;
panY = 0;
updateTransform();
updateZoomUI();
}
function updateTransform() {
const content = document.getElementById('lightbox-content');
const viewport = document.getElementById('lightbox-viewport');
// Constrain pan when zoomed out
if (currentZoom <= 1) {
panX = 0;
panY = 0;
viewport.classList.add('zoomed-out');
} else {
viewport.classList.remove('zoomed-out');
}
content.style.transform = `translate(${panX}px, ${panY}px) scale(${currentZoom})`;
}
function updateZoomUI() {
document.getElementById('zoom-level').textContent = Math.round(currentZoom * 100) + '%';
document.getElementById('btn-zoom-in').disabled = currentZoom >= MAX_ZOOM;
document.getElementById('btn-zoom-out').disabled = currentZoom <= MIN_ZOOM;
}
function showZoomHint() {
const hint = document.getElementById('zoom-hint');
hint.classList.add('visible');
setTimeout(() => hint.classList.remove('visible'), 3000);
}
// Mouse wheel zoom
document.getElementById('lightbox-viewport')?.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
setZoom(currentZoom + delta, e.clientX, e.clientY);
}, { passive: false });
// Pan with mouse drag
const viewport = document.getElementById('lightbox-viewport');
viewport?.addEventListener('mousedown', (e) => {
if (currentZoom <= 1) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
lastPanX = panX;
lastPanY = panY;
viewport.classList.add('dragging');
document.getElementById('lightbox-content').classList.add('no-transition');
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
panX = lastPanX + (e.clientX - startX);
panY = lastPanY + (e.clientY - startY);
updateTransform();
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
viewport?.classList.remove('dragging');
document.getElementById('lightbox-content')?.classList.remove('no-transition');
}
});
// Touch support for pinch zoom and pan
let lastTouchDistance = 0;
let lastTouchCenter = { x: 0, y: 0 };
viewport?.addEventListener('touchstart', (e) => {
if (e.touches.length === 2) {
lastTouchDistance = getTouchDistance(e.touches);
lastTouchCenter = getTouchCenter(e.touches);
} else if (e.touches.length === 1 && currentZoom > 1) {
isDragging = true;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
lastPanX = panX;
lastPanY = panY;
document.getElementById('lightbox-content').classList.add('no-transition');
}
}, { passive: true });
viewport?.addEventListener('touchmove', (e) => {
if (e.touches.length === 2) {
e.preventDefault();
const newDistance = getTouchDistance(e.touches);
const newCenter = getTouchCenter(e.touches);
const scale = newDistance / lastTouchDistance;
setZoom(currentZoom * scale, newCenter.x, newCenter.y);
lastTouchDistance = newDistance;
lastTouchCenter = newCenter;
} else if (e.touches.length === 1 && isDragging) {
panX = lastPanX + (e.touches[0].clientX - startX);
panY = lastPanY + (e.touches[0].clientY - startY);
updateTransform();
}
}, { passive: false });
viewport?.addEventListener('touchend', () => {
isDragging = false;
lastTouchDistance = 0;
document.getElementById('lightbox-content')?.classList.remove('no-transition');
});
function getTouchDistance(touches) {
const dx = touches[0].clientX - touches[1].clientX;
const dy = touches[0].clientY - touches[1].clientY;
return Math.sqrt(dx * dx + dy * dy);
}
function getTouchCenter(touches) {
return {
x: (touches[0].clientX + touches[1].clientX) / 2,
y: (touches[0].clientY + touches[1].clientY) / 2
};
}
// Double-click to toggle zoom
viewport?.addEventListener('dblclick', (e) => {
if (currentZoom === 1) {
setZoom(2, e.clientX, e.clientY);
} else {
resetZoom();
}
});
// Keyboard navigation
document.addEventListener('keydown', (e) => {
const lightbox = document.getElementById('lightbox');
if (!lightbox.classList.contains('active')) return;
switch(e.key) {
case 'Escape': closeLightbox(); break;
case 'ArrowLeft': navigateLightbox(-1); break;
case 'ArrowRight': navigateLightbox(1); break;
case '+': case '=': zoomIn(); break;
case '-': zoomOut(); break;
case '0': resetZoom(); break;
}
});
const docs = {
"Inicio": {
"README": `# Sistema de Registro de Estudiantes\n\nSistema web para gestionar inscripciones de estudiantes en materias con restricciones de créditos y profesores.\n\n## Stack Tecnológico\n\n| Capa | Tecnología |\n|------|------------|\n| Backend | .NET 10, C# |\n| API | GraphQL (HotChocolate) |\n| Frontend | Angular 21, TypeScript |\n| Base de Datos | SQL Server 2022 |\n| ORM | Entity Framework Core |\n| UI | Angular Material |\n\n## Reglas de Negocio\n\n- 10 materias, cada una vale 3 créditos\n- 5 profesores, cada uno imparte 2 materias\n- Estudiantes pueden inscribir **máximo 3 materias** (9 créditos)\n- **Restricción:** Un estudiante NO puede tener materias con el mismo profesor\n\n## Características del Sistema\n\n### Funcionalidades\n- CRUD completo de estudiantes\n- Inscripción/cancelación de materias con validación de reglas\n- Visualización de compañeros de clase por materia\n- Interfaz responsive con Angular Material\n- **Sistema de autenticación con flujo de activación**\n- **Control de acceso por roles (Admin/Student)**\n\n### Calidad y Robustez\n- **Manejo de errores**: Mensajes amigables para usuarios + logging detallado para desarrolladores\n- **Monitoreo de conectividad**: Verificación cada 5 segundos con overlay bloqueante si se pierde conexión\n- **Validación**: FluentValidation en backend + validación reactiva en frontend\n- **Arquitectura**: Clean Architecture + CQRS + Ports & Adapters\n\n### DevOps\n- **Docker Compose**: Despliegue optimizado con un solo comando (\`./start.sh\`)\n- **Health Check**: Endpoint \`/health\` con verificación de base de datos\n- **Diagramas UML**: 8 diagramas PlantUML documentando la arquitectura\n\n## Requisitos Previos\n\n- [.NET 10 SDK](https://dotnet.microsoft.com/download)\n- [Node.js 22+](https://nodejs.org/)\n- [Docker](https://www.docker.com/) (para SQL Server)\n- Angular CLI 21: \`npm install -g @angular/cli\`\n\n## Inicio Rápido\n\n### Paso 1: Clonar e ir al directorio\n\n\`\`\`bash\ngit clone <repo-url>\ncd Interrapidisimo\n\`\`\`\n\n### Paso 2: Iniciar SQL Server con Docker\n\n\`\`\`bash\ndocker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=Asde71.4Asde71.4" \\\n -p 1433:1433 --name sqlserver -d mcr.microsoft.com/mssql/server:2022-latest\n\`\`\`\n\nEsperar ~10 segundos a que inicie completamente.\n\n### Paso 3: Configurar y ejecutar Backend\n\n\`\`\`bash\ncd src/backend\n\n# 1. Instalar herramienta EF Core (solo primera vez)\ndotnet tool install --global dotnet-ef\n# Si ya está instalada, ignorar el mensaje\n\n# 2. Restaurar dependencias\ndotnet restore\n\n# 3. Crear base de datos y aplicar migraciones\ndotnet ef database update -p Adapters/Driven/Persistence -s Host\n# NOTA: El mensaje "HostAbortedException" es NORMAL - indica que las migraciones se completaron\n\n# 4. Liberar puerto si está ocupado (opcional)\nfuser -k 5000/tcp 2>/dev/null\n\n# 5. Ejecutar API\ndotnet run --project Host\n\`\`\`\n\n**API GraphQL:** http://localhost:5000/graphql\n\n### Paso 4: Verificar que funciona\n\nEn otra terminal:\n\n\`\`\`bash\n# Consultar materias (debe retornar 10)\ncurl -s http://localhost:5000/graphql -X POST \\\n -H "Content-Type: application/json" \\\n -d '{"query":"{ subjects { id name credits } }"}' | jq .\n\n# Crear un estudiante\ncurl -s http://localhost:5000/graphql -X POST \\\n -H "Content-Type: application/json" \\\n -d '{"query":"mutation { createStudent(input: {name: \\"Test User\\", email: \\"test@example.com\\"}) { student { id name email } } }"}' | jq .\n\`\`\`\n\n### Paso 5: Frontend (opcional)\n\n\`\`\`bash\ncd src/frontend\n\n# Instalar dependencias (usar --legacy-peer-deps por compatibilidad)\nnpm install --legacy-peer-deps\n\n# Iniciar servidor de desarrollo\nnpx ng serve\n\`\`\`\n\n**App Angular:** http://localhost:4200\n\n> **Nota:** Si \`ng\` no está instalado globalmente, usar \`npx ng serve\`\n\n## Datos de Ejemplo\n\nLa migración incluye datos iniciales:\n\n| Profesor | Materias |\n|----------|----------|\n| Dr. García | Matemáticas I, Matemáticas II |\n| Dra. López | Física I, Física II |\n| Ing. Mar
"Entregables": `# Entregables - Prueba Técnica Senior .NET/Angular\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Empresa:** Inter Rapidísimo\n**Cargo:** Desarrollador Master .NET/Angular\n\n---\n\n## Resumen Ejecutivo\n\nSistema web completo para gestión de inscripciones de estudiantes con las siguientes características:\n\n- **Backend:** .NET 10, GraphQL (HotChocolate), Clean Architecture\n- **Frontend:** Angular 21, Standalone Components, Signals\n- **Base de Datos:** SQL Server con EF Core\n- **Seguridad:** OWASP compliant, rate limiting, query complexity\n- **Testing:** 145 tests automatizados\n- **Despliegue:** Docker + Kubernetes (k3s)\n\n---\n\n## Estructura del Proyecto\n\n\`\`\`\n/\n├── src/\n│ ├── backend/ # .NET 10 API GraphQL\n│ │ ├── Domain/ # Entidades, Value Objects, Ports\n│ │ ├── Application/ # Commands, Queries, DTOs\n│ │ ├── Adapters/ # GraphQL API, Persistence\n│ │ └── Host/ # Entry point, DI\n│ └── frontend/ # Angular 21 SPA\n├── tests/ # Tests automatizados\n├── docs/ # Documentación\n└── deploy/ # Docker + k3s\n\`\`\`\n\n---\n\n## Funcionalidades Implementadas\n\n### Requisitos Funcionales\n\n| # | Requisito | Estado |\n|---|-----------|--------|\n| 1 | CRUD completo de estudiantes | ✅ |\n| 2 | Inscripción en materias (max 3) | ✅ |\n| 3 | Visualización de compañeros de clase | ✅ |\n| 4 | Restricción de profesor único | ✅ |\n| 5 | 10 materias, 5 profesores | ✅ |\n| 6 | 3 créditos por materia | ✅ |\n| 7 | Validación de inscripciones | ✅ |\n| 8 | UI responsiva | ✅ |\n| 9 | Manejo de errores | ✅ |\n| 10 | Autenticación JWT | ✅ |\n| 11 | Flujo de activación de estudiantes | ✅ |\n| 12 | Control de acceso por roles (Admin/Student) | ✅ |\n\n### Reglas de Negocio\n\n- ✅ Máximo 3 materias por estudiante (9 créditos)\n- ✅ No repetir profesor en inscripciones\n- ✅ Validación en Domain Layer (pura, testeable)\n- ✅ Mensajes de error descriptivos\n\n### Sistema de Autenticación\n\n- ✅ **JWT** con HMAC-SHA256\n- ✅ **Flujo de Activación:** Admin crea estudiante → Código de activación → Estudiante activa cuenta\n- ✅ **Roles:** Admin (gestión completa) / Student (dashboard personal)\n- ✅ **Recuperación:** Código de recuperación generado en activación\n- ✅ **Seguridad:** PBKDF2-SHA256 para hashing de contraseñas\n\n---\n\n## Arquitectura\n\n### Clean Architecture\n\n\`\`\`\nHost → Adapters → Application → Domain\n\`\`\`\n\n- **Domain:** Entidades puras, sin dependencias\n- **Application:** Casos de uso con CQRS\n- **Adapters:** GraphQL API + EF Core\n- **Host:** Composición y DI\n\n### Patrones Implementados\n\n| Patrón | Uso |\n|--------|-----|\n| CQRS | Separación Commands/Queries |\n| Repository | Abstracción de persistencia |\n| Ports & Adapters | Inversión de dependencias |\n| DataLoader | Evitar N+1 en GraphQL |\n| Specification | Consultas reutilizables |\n\n### Diagramas de Arquitectura\n\nTodos los diagramas están disponibles en \`/docs/architecture/diagrams/\` en formatos PNG y SVG.\n\n| # | Diagrama | Archivo | Descripción |\n|---|----------|---------|-------------|\n| 1 | **Casos de Uso** | \`01-use-cases\` | Actores (Estudiante, Admin), funcionalidades del sistema, reglas de negocio |\n| 2 | **Modelo de Dominio** | \`02-domain-model\` | Entidades (User, Student, Professor, Subject, Enrollment), Value Objects, Domain Services |\n| 3 | **Secuencia Inscripción** | \`03-sequence-enrollment\` | Flujo completo de inscripción con JWT, validaciones y persistencia |\n| 4 | **Componentes** | \`04-components\` | Arquitectura Clean Architecture: Frontend Angular 21, Backend .NET 10, GraphQL |\n| 5 | **Entidad-Relación** | \`05-entity-relationship\` | Modelo de base de datos: tablas, relaciones, restricciones |\n| 6 | **Estados** | \`06-state-enrollment\` | Estados de cuenta (activación) e inscripciones (0-9 créditos) |\
"Prueba Técnica": `# PRUEBA TECNICA APLICACION WEB\n\n1. Los resultados deben ser enviados en un documento adjunto.\n2. Los entregables deben venir adjuntos.\n\n## El test consiste en:\n\nUna aplicación para registro de estudiantes:\n\n1. Realizar un CRUD que le permita a un usuario realizar un registro en línea.\n2. El estudiante se adhiere a un programa de créditos\n3. Existen 10 materias\n4. Cada materia equivale a 3 créditos.\n5. El estudiante sólo podrá seleccionar 3 materias.\n6. Hay 5 profesores que dictan 2 materias cada uno.\n7. El estudiante no podrá tener clases con el mismo profesor.\n8. Cada estudiante puede ver en línea los registros de otros estudiantes.\n9. El estudiante podrá ver sólo el nombre de los alumnos con quienes compartirá cada clase.\n\n## Entregables:\n\nUna aplicación web o Cliente servidor\nBase de datos o scripts para su creación en MySql / SQL\n`,
},
"Análisis": {
"AN-001 Requisitos Funcionales": `# AN-001: Análisis de Requisitos Funcionales\n\n**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo\n**Rol:** Analista de Sistemas\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Resumen Ejecutivo\n\nSistema web para gestión de inscripciones estudiantiles con programa de créditos académicos. Permite CRUD de estudiantes, inscripción en materias con restricciones de negocio, y visualización de compañeros de clase.\n\n---\n\n## 2. Requisitos Funcionales Identificados\n\n### RF-001: Registro de Estudiantes (CRUD)\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-001 |\n| **Nombre** | Gestión CRUD de Estudiantes |\n| **Descripción** | El sistema debe permitir crear, leer, actualizar y eliminar registros de estudiantes |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 1 |\n\n**Criterios de Aceptación:**\n- [ ] CA-001.1: Usuario puede crear estudiante con nombre y email válido\n- [ ] CA-001.2: Usuario puede consultar lista de estudiantes registrados\n- [ ] CA-001.3: Usuario puede actualizar datos de un estudiante existente\n- [ ] CA-001.4: Usuario puede eliminar un estudiante (cascade con inscripciones)\n- [ ] CA-001.5: Email debe ser único en el sistema\n\n---\n\n### RF-002: Programa de Créditos\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-002 |\n| **Nombre** | Adhesión a Programa de Créditos |\n| **Descripción** | Los estudiantes se inscriben en un programa basado en créditos académicos |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 2 |\n\n**Criterios de Aceptación:**\n- [ ] CA-002.1: Sistema muestra créditos totales del estudiante\n- [ ] CA-002.2: Créditos se calculan automáticamente según materias inscritas\n- [ ] CA-002.3: Máximo 9 créditos por estudiante (3 materias × 3 créditos)\n\n---\n\n### RF-003: Catálogo de Materias\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-003 |\n| **Nombre** | Gestión de 10 Materias |\n| **Descripción** | El sistema debe gestionar un catálogo fijo de 10 materias académicas |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 3 |\n\n**Criterios de Aceptación:**\n- [ ] CA-003.1: Sistema contiene exactamente 10 materias predefinidas\n- [ ] CA-003.2: Usuario puede consultar catálogo completo de materias\n- [ ] CA-003.3: Cada materia muestra nombre, créditos y profesor asignado\n\n---\n\n### RF-004: Valor de Créditos por Materia\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-004 |\n| **Nombre** | Asignación de 3 Créditos por Materia |\n| **Descripción** | Cada materia del catálogo equivale a exactamente 3 créditos |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 4 |\n\n**Criterios de Aceptación:**\n- [ ] CA-004.1: Todas las materias tienen valor fijo de 3 créditos\n- [ ] CA-004.2: Sistema muestra créditos en detalle de cada materia\n\n---\n\n### RF-005: Límite de Materias por Estudiante\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-005 |\n| **Nombre** | Máximo 3 Materias por Estudiante |\n| **Descripción** | Un estudiante solo puede inscribirse en máximo 3 materias |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 5 |\n\n**Criterios de Aceptación:**\n- [ ] CA-005.1: Sistema impide inscripción si estudiante ya tiene 3 materias\n- [ ] CA-005.2: Sistema muestra contador de materias inscritas\n- [ ] CA-005.3: Mensaje de error claro al intentar exceder límite\n\n---\n\n### RF-006: Asignación Profesores-Materias\n\n| Atributo | Descripción |\n|----------|-------------|\n| **ID** | RF-006 |\n| **Nombre** | 5 Profesores con 2 Materias c/u |\n| **Descripción** | El sistema tiene 5 profesores, cada uno dicta exactamente 2 materias |\n| **Prioridad** | Alta |\n| **Fuente** | Enunciado punto 6 |\n\n**Criterios de Aceptación:**\n- [ ] CA-006.1: Sistema contiene exactamente 5 profesores predefinidos\n- [ ] CA-006.2: Cada profesor tiene asignadas exactamente 2 materias\n- [ ] CA-006.3: Usuario puede ver qué profesor dict
"AN-002 Reglas de Negocio": `# AN-002: Reglas de Negocio\n\n**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo\n**Rol:** Analista de Sistemas\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Resumen\n\nEste documento define las reglas de negocio que gobiernan el sistema de registro de estudiantes. Estas reglas son invariantes y deben ser validadas en la capa de dominio.\n\n---\n\n## 2. Reglas de Negocio\n\n### RN-001: Estructura del Catálogo de Materias\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RN-001 |\n| **Nombre** | Catálogo Fijo de Materias |\n| **Descripción** | El sistema contiene exactamente 10 materias académicas |\n| **Tipo** | Restricción Estructural |\n| **Severidad** | Crítica |\n\n**Datos Iniciales (Seed):**\n\n| ID | Materia | Profesor Asignado |\n|----|---------|-------------------|\n| 1 | Matemáticas I | Profesor A |\n| 2 | Matemáticas II | Profesor A |\n| 3 | Física I | Profesor B |\n| 4 | Física II | Profesor B |\n| 5 | Programación I | Profesor C |\n| 6 | Programación II | Profesor C |\n| 7 | Base de Datos I | Profesor D |\n| 8 | Base de Datos II | Profesor D |\n| 9 | Redes I | Profesor E |\n| 10 | Redes II | Profesor E |\n\n**Validación:** \`COUNT(Subjects) == 10\`\n\n---\n\n### RN-002: Valor Uniforme de Créditos\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RN-002 |\n| **Nombre** | Créditos por Materia |\n| **Descripción** | Cada materia equivale exactamente a 3 créditos |\n| **Tipo** | Restricción de Valor |\n| **Severidad** | Crítica |\n\n**Fórmula:**\n\`\`\`\nCréditos_Materia = 3 (constante)\nCréditos_Estudiante = COUNT(Inscripciones) × 3\nCréditos_Máximos = 9 (3 materias × 3 créditos)\n\`\`\`\n\n**Validación:** \`Subject.Credits == 3\`\n\n---\n\n### RN-003: Límite de Inscripciones por Estudiante\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RN-003 |\n| **Nombre** | Máximo 3 Materias |\n| **Descripción** | Un estudiante solo puede inscribirse en máximo 3 materias |\n| **Tipo** | Restricción de Cardinalidad |\n| **Severidad** | Crítica |\n\n**Escenarios:**\n\n| Materias Actuales | Acción | Resultado |\n|-------------------|--------|-----------|\n| 0-2 | Inscribir | ✓ Permitido |\n| 3 | Inscribir | ✗ Rechazado |\n| 1-3 | Cancelar | ✓ Permitido |\n\n**Validación:** \`Student.Enrollments.Count <= 3\`\n\n**Mensaje de Error:** "El estudiante ya tiene el máximo de 3 materias inscritas"\n\n---\n\n### RN-004: Asignación Profesores-Materias\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RN-004 |\n| **Nombre** | Distribución de Profesores |\n| **Descripción** | Existen 5 profesores, cada uno dicta exactamente 2 materias |\n| **Tipo** | Restricción Estructural |\n| **Severidad** | Crítica |\n\n**Invariantes:**\n- \`COUNT(Professors) == 5\`\n- \`∀ Professor: COUNT(Professor.Subjects) == 2\`\n- \`∀ Subject: Subject.Professor != NULL\`\n\n**Validación:**\n\`\`\`csharp\nprofessors.All(p => p.Subjects.Count == 2)\nsubjects.All(s => s.ProfessorId != null)\n\`\`\`\n\n---\n\n### RN-005: Restricción de Profesor Único (CRÍTICA)\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RN-005 |\n| **Nombre** | Prohibición de Mismo Profesor |\n| **Descripción** | Un estudiante NO puede inscribirse en dos materias dictadas por el mismo profesor |\n| **Tipo** | Restricción de Integridad |\n| **Severidad** | **CRÍTICA** |\n\n**Lógica de Validación:**\n\n\`\`\`\nPARA inscribir(estudiante, nuevaMateria):\n profesorNuevaMateria = nuevaMateria.Profesor\n profesoresActuales = estudiante.Inscripciones.Select(i => i.Materia.Profesor)\n\n SI profesorNuevaMateria EN profesoresActuales:\n RECHAZAR "Ya tienes una materia con este profesor"\n SINO:\n PERMITIR inscripción\n\`\`\`\n\n**Casos de Prueba:**\n\n| Estudiante tiene | Intenta inscribir | Resultado |\n|------------------|-------------------|-----------|\n| Matemáticas I (Prof A) | Matemáticas II (Prof A) | ✗ Rechazado |\n| Matemáticas I (Prof A) | Física I (Prof B) | ✓ Permitido |\n| Física I (Prof B), Programac
"AN-003 Historias de Usuario": `# AN-003: Historias de Usuario\n\n**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo\n**Rol:** Product Owner\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Épica Principal\n\n**EP-001: Sistema de Inscripción de Estudiantes**\n\nComo institución educativa, necesito un sistema web que permita a los estudiantes registrarse e inscribirse en materias siguiendo las reglas del programa de créditos, para gestionar eficientemente el proceso de matrícula.\n\n---\n\n## 2. Historias de Usuario\n\n### US-001: Registro de Estudiante\n\n| Campo | Valor |\n|-------|-------|\n| **ID** | US-001 |\n| **Épica** | EP-001 |\n| **Prioridad** | Alta |\n| **Story Points** | 5 |\n| **Sprint** | 1 |\n\n**Historia:**\n> Como **estudiante nuevo**,\n> quiero **registrarme en el sistema con mi nombre y correo electrónico**,\n> para **poder acceder al programa de inscripción de materias**.\n\n**Criterios de Aceptación:**\n\n\`\`\`gherkin\nScenario: Registro exitoso de estudiante\n Given estoy en la página de registro\n When ingreso un nombre válido "Juan Pérez"\n And ingreso un email válido "juan@email.com"\n And presiono el botón "Registrar"\n Then el sistema crea mi cuenta\n And veo un mensaje de confirmación\n And soy redirigido a la página de inscripción\n\nScenario: Registro con email duplicado\n Given existe un estudiante con email "juan@email.com"\n When intento registrarme con el mismo email\n Then veo el mensaje "Ya existe un estudiante con este email"\n And el formulario no se envía\n\nScenario: Registro con email inválido\n Given estoy en la página de registro\n When ingreso un email inválido "juanemail.com"\n Then veo el mensaje "Formato de email no válido"\n And el botón de registro está deshabilitado\n\`\`\`\n\n**Notas Técnicas:**\n- Validación de email en frontend y backend\n- Email almacenado en minúsculas (normalización)\n\n---\n\n### US-002: Consulta de Materias Disponibles\n\n| Campo | Valor |\n|-------|-------|\n| **ID** | US-002 |\n| **Épica** | EP-001 |\n| **Prioridad** | Alta |\n| **Story Points** | 3 |\n| **Sprint** | 1 |\n\n**Historia:**\n> Como **estudiante registrado**,\n> quiero **ver el catálogo de las 10 materias disponibles**,\n> para **conocer mis opciones de inscripción**.\n\n**Criterios de Aceptación:**\n\n\`\`\`gherkin\nScenario: Ver catálogo completo\n Given estoy autenticado como estudiante\n When accedo a la sección de materias\n Then veo una lista de 10 materias\n And cada materia muestra nombre, créditos (3) y profesor\n\nScenario: Ver detalle de materia\n Given estoy viendo el catálogo de materias\n When selecciono "Matemáticas I"\n Then veo el nombre del profesor asignado\n And veo que equivale a 3 créditos\n\`\`\`\n\n---\n\n### US-003: Inscripción en Materia\n\n| Campo | Valor |\n|-------|-------|\n| **ID** | US-003 |\n| **Épica** | EP-001 |\n| **Prioridad** | Alta |\n| **Story Points** | 8 |\n| **Sprint** | 1 |\n\n**Historia:**\n> Como **estudiante registrado**,\n> quiero **inscribirme en una materia disponible**,\n> para **acumular créditos en mi programa académico**.\n\n**Criterios de Aceptación:**\n\n\`\`\`gherkin\nScenario: Inscripción exitosa\n Given tengo menos de 3 materias inscritas\n And la materia "Física I" está disponible para mí\n When presiono "Inscribir" en "Física I"\n Then la materia se agrega a mis inscripciones\n And mis créditos aumentan en 3\n And veo mensaje de confirmación\n\nScenario: Intento inscripción con máximo alcanzado\n Given ya tengo 3 materias inscritas\n When intento inscribir otra materia\n Then veo el mensaje "Ya tienes el máximo de 3 materias"\n And el botón de inscripción está deshabilitado\n\nScenario: Intento inscripción con mismo profesor\n Given estoy inscrito en "Matemáticas I" (Profesor A)\n When intento inscribir "Matemáticas II" (Profesor A)\n Then veo el mensaje "Ya tienes una materia con este profesor"\n And la inscripción no se procesa\n\`\`\`\n\n**Notas Técnicas:**\n- Validaciones RN-003 y RN-005 en capa de do
"AN-004 Requisitos No Funcionales": `# AN-004: Requisitos No Funcionales\n\n**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo\n**Rol:** Arquitecto de Software\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Resumen\n\nEste documento define los atributos de calidad (requisitos no funcionales) que el sistema debe cumplir, estableciendo métricas medibles y criterios de aceptación.\n\n---\n\n## 2. Requisitos No Funcionales\n\n### RNF-001: Rendimiento - Tiempo de Respuesta\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RNF-001 |\n| **Categoría** | Rendimiento |\n| **Prioridad** | Alta |\n| **Métrica** | Tiempo de respuesta < 200ms (P95) |\n\n**Descripción:**\nEl sistema debe responder a las solicitudes del usuario en menos de 200 milisegundos en el percentil 95.\n\n**Criterios de Aceptación:**\n\n| Operación | Tiempo Máximo |\n|-----------|---------------|\n| Consulta lista de estudiantes | < 150ms |\n| Consulta catálogo materias | < 100ms |\n| Inscripción en materia | < 200ms |\n| Crear/actualizar estudiante | < 200ms |\n| Consulta compañeros de clase | < 150ms |\n\n**Medición:**\n- Herramienta: Application Insights / Logs estructurados\n- Ambiente: Producción simulada\n- Carga: 100 usuarios concurrentes\n\n---\n\n### RNF-002: Rendimiento - Throughput\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RNF-002 |\n| **Categoría** | Rendimiento |\n| **Prioridad** | Media |\n| **Métrica** | >= 500 requests/segundo |\n\n**Descripción:**\nEl sistema debe soportar al menos 500 solicitudes por segundo sin degradación.\n\n**Criterios de Aceptación:**\n- [ ] API soporta 500 req/s con latencia < 200ms\n- [ ] Sin errores 5xx bajo carga normal\n- [ ] CPU < 80% bajo carga máxima\n\n---\n\n### RNF-003: Seguridad - OWASP Top 10\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RNF-003 |\n| **Categoría** | Seguridad |\n| **Prioridad** | Crítica |\n| **Estándar** | OWASP Top 10 2021 |\n\n**Descripción:**\nEl sistema debe estar protegido contra las 10 vulnerabilidades más críticas según OWASP.\n\n**Controles por Vulnerabilidad:**\n\n| # | Vulnerabilidad | Control Implementado |\n|---|----------------|----------------------|\n| A01 | Broken Access Control | Validación de permisos en cada endpoint |\n| A02 | Cryptographic Failures | HTTPS obligatorio, hashing de datos sensibles |\n| A03 | Injection | Consultas parametrizadas (EF Core), validación de entrada |\n| A04 | Insecure Design | Clean Architecture, validación en dominio |\n| A05 | Security Misconfiguration | Headers de seguridad, CORS restrictivo |\n| A06 | Vulnerable Components | Auditoría de dependencias (\`dotnet list package --vulnerable\`) |\n| A07 | Auth Failures | Rate limiting, validación de sesión |\n| A08 | Data Integrity Failures | Validación de DTOs, firma de respuestas |\n| A09 | Logging Failures | Logs estructurados sin datos sensibles |\n| A10 | SSRF | Validación de URLs, no fetch de recursos externos |\n\n**Criterios de Aceptación:**\n- [ ] Análisis SAST sin vulnerabilidades críticas\n- [ ] Headers de seguridad configurados (CSP, X-Frame-Options, etc.)\n- [ ] Sin secretos en código fuente\n- [ ] HTTPS en todos los endpoints\n\n---\n\n### RNF-004: Seguridad - Validación de Entrada\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RNF-004 |\n| **Categoría** | Seguridad |\n| **Prioridad** | Alta |\n\n**Descripción:**\nToda entrada del usuario debe ser validada tanto en frontend como en backend.\n\n**Reglas de Validación:**\n\n| Campo | Regla Frontend | Regla Backend |\n|-------|----------------|---------------|\n| Nombre | Required, MaxLength(100) | FluentValidation |\n| Email | Required, EmailFormat | ValueObject + Validator |\n| IDs | Numeric | Tipo fuerte (int) |\n\n**Criterios de Aceptación:**\n- [ ] Ningún input llega a la BD sin validación\n- [ ] Mensajes de error claros y sin información sensible\n- [ ] Sanitización de HTML en campos de texto\n\n---\n\n### RNF-005: Usabilidad - Responsive Design\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RNF-005 |\n| *
"AN-005 Riesgos Técnicos": `# AN-005: Análisis de Riesgos Técnicos\n\n**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo\n**Rol:** Líder Técnico\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Resumen\n\nIdentificación y evaluación de riesgos técnicos que podrían impactar el desarrollo, calidad o entrega del sistema. Cada riesgo incluye probabilidad, impacto y estrategia de mitigación.\n\n---\n\n## 2. Matriz de Evaluación\n\n| Probabilidad / Impacto | Bajo (1) | Medio (2) | Alto (3) |\n|------------------------|----------|-----------|----------|\n| **Alta (3)** | 3 | 6 | **9** |\n| **Media (2)** | 2 | 4 | 6 |\n| **Baja (1)** | 1 | 2 | 3 |\n\n**Clasificación:**\n- **Crítico:** 6-9 (acción inmediata)\n- **Moderado:** 3-5 (plan de mitigación)\n- **Bajo:** 1-2 (monitorear)\n\n---\n\n## 3. Riesgos Identificados\n\n### RT-001: Complejidad de Validaciones Cruzadas\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RT-001 |\n| **Categoría** | Desarrollo |\n| **Probabilidad** | Alta (3) |\n| **Impacto** | Alto (3) |\n| **Score** | **9 - Crítico** |\n\n**Descripción:**\nLas validaciones de negocio (máximo 3 materias + no repetir profesor) requieren consultas cruzadas entre entidades. Una implementación incorrecta puede generar condiciones de carrera o validaciones inconsistentes.\n\n**Escenarios de Riesgo:**\n1. Dos inscripciones concurrentes del mismo estudiante\n2. Validación en frontend que no se replica en backend\n3. Race condition al verificar profesor duplicado\n\n**Estrategia de Mitigación:**\n\n| Acción | Responsable | Plazo |\n|--------|-------------|-------|\n| Implementar validaciones en Domain Service (única fuente de verdad) | Dev Senior | Sprint 1 |\n| Usar transacciones con nivel de aislamiento Serializable | Dev Senior | Sprint 1 |\n| Tests de concurrencia con múltiples threads | QA | Sprint 1 |\n| Lock optimista con RowVersion en Enrollment | Dev | Sprint 1 |\n\n**Código de Mitigación:**\n\`\`\`csharp\n// Transacción con locking\nawait using var transaction = await _context.Database\n .BeginTransactionAsync(IsolationLevel.Serializable);\n\nvar student = await _context.Students\n .Include(s => s.Enrollments)\n .ThenInclude(e => e.Subject)\n .FirstOrDefaultAsync(s => s.Id == command.StudentId);\n\n// Validaciones en dominio\n_enrollmentService.ValidateEnrollment(student, subject);\n\nawait _context.SaveChangesAsync();\nawait transaction.CommitAsync();\n\`\`\`\n\n---\n\n### RT-002: Integración Frontend-Backend (GraphQL)\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RT-002 |\n| **Categoría** | Integración |\n| **Probabilidad** | Media (2) |\n| **Impacto** | Alto (3) |\n| **Score** | **6 - Crítico** |\n\n**Descripción:**\nGraphQL introduce complejidad adicional en la integración. Errores en el esquema, types incorrectos o problemas de N+1 queries pueden afectar rendimiento y desarrollo.\n\n**Escenarios de Riesgo:**\n1. Mismatch entre schema GraphQL y DTOs\n2. N+1 queries sin DataLoaders\n3. Errores de tipos en Apollo Angular\n4. Over-fetching o under-fetching de datos\n\n**Estrategia de Mitigación:**\n\n| Acción | Responsable | Plazo |\n|--------|-------------|-------|\n| Generar tipos TypeScript desde schema (codegen) | Dev Frontend | Sprint 1 |\n| Implementar DataLoaders para todas las relaciones | Dev Backend | Sprint 1 |\n| Configurar GraphQL Voyager para visualizar schema | Dev | Sprint 1 |\n| Tests de integración GraphQL con Banana Cake Pop | QA | Sprint 1 |\n\n**Configuración DataLoader:**\n\`\`\`csharp\n// DataLoader para evitar N+1\npublic class SubjectByIdDataLoader : BatchDataLoader<int, Subject>\n{\n protected override async Task<IReadOnlyDictionary<int, Subject>> LoadBatchAsync(\n IReadOnlyList<int> keys, CancellationToken ct)\n {\n return await _context.Subjects\n .Where(s => keys.Contains(s.Id))\n .ToDictionaryAsync(s => s.Id, ct);\n }\n}\n\`\`\`\n\n---\n\n### RT-003: Manejo de Concurrencia\n\n| Atributo | Valor |\n|----------|-------|\n| **ID** | RT-003 |\n| **Categor<6F>
},
"Diseño": {
"DI-001 Arquitectura Backend": `# DI-001: Arquitectura Backend\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Patrón: Clean Architecture + CQRS\n\n\`\`\`\n┌────────────────────────────────────────────────────────────┐\n│ HOST │\n│ (Program.cs, DI, Configuration) │\n└─────────────────────────┬──────────────────────────────────┘\n │\n┌─────────────────────────▼──────────────────────────────────┐\n│ ADAPTERS │\n│ ┌──────────────────────┬─────────────────────────────┐ │\n│ │ DRIVING (Primary) │ DRIVEN (Secondary) │ │\n│ │ ────────────────── │ ───────────────────── │ │\n│ │ GraphQL API │ Persistence (EF Core) │ │\n│ │ (HotChocolate) │ DataLoaders │ │\n│ └──────────────────────┴─────────────────────────────┘ │\n└─────────────────────────┬──────────────────────────────────┘\n │\n┌─────────────────────────▼──────────────────────────────────┐\n│ APPLICATION │\n│ Commands / Queries / Handlers / Validators │\n└─────────────────────────┬──────────────────────────────────┘\n │\n┌─────────────────────────▼──────────────────────────────────┐\n│ DOMAIN │\n│ Entities / Value Objects / Services / Ports │\n└────────────────────────────────────────────────────────────┘\n\`\`\`\n\n---\n\n## 2. Regla de Dependencia\n\n\`\`\`\nHost → Adapters → Application → Domain\n ↑\n Adapters implementa Ports\n\`\`\`\n\n**INVIOLABLE:** Domain NO depende de nada externo.\n\n---\n\n## 3. Responsabilidades por Capa\n\n| Capa | Responsabilidad | Tecnología |\n|------|-----------------|------------|\n| **Domain** | Entidades, Value Objects, reglas de negocio puras, Ports (interfaces) | C# puro |\n| **Application** | Casos de uso (Commands/Queries), DTOs, Validators, orquestación | MediatR, FluentValidation, Mapster |\n| **Adapters/Driving** | GraphQL API (Types, Resolvers, Mutations) | HotChocolate |\n| **Adapters/Driven** | Repositorios, DbContext, DataLoaders | EF Core, SQL Server |\n| **Host** | Composition Root, DI, Middleware | ASP.NET Core |\n\n---\n\n## 4. Estructura de Proyectos\n\n\`\`\`\nsrc/backend/\n├── Domain/\n│ ├── Entities/ # Student, Subject, Professor, Enrollment\n│ ├── ValueObjects/ # Email, Credits\n│ ├── Services/
"DI-002 Modelo de Dominio": `# DI-002: Modelo de Dominio\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Diagrama de Entidades\n\n\`\`\`\n┌─────────────────┐ ┌─────────────────────────┐\n│ PROFESSOR │ │ STUDENT │\n├─────────────────┤ ├─────────────────────────┤\n│ Id: int (PK) │ │ Id: int (PK) │\n│ Name: string │ │ Name: string │\n└────────┬────────┘ │ Email: Email │\n │ │ ActivationCodeHash? │ ← Nuevo\n │ 1:2 │ ActivationExpiresAt? │ ← Nuevo\n ▼ │ IsActivated (computed) │ ← Nuevo\n┌─────────────────┐ │ RowVersion │\n│ SUBJECT │ └────────┬────────────────┘\n├─────────────────┤ │\n│ Id: int (PK) │ │ 0..3\n│ Name: string │ ▼\n│ Credits: 3 │ ┌─────────────────┐\n│ ProfessorId: FK │◄──────│ ENROLLMENT │\n└─────────────────┘ 1:N ├─────────────────┤\n │ Id: int (PK) │\n┌─────────────────┐ │ StudentId: FK │\n│ USER │ │ SubjectId: FK │\n├─────────────────┤ │ EnrolledAt │\n│ Id: int (PK) │ └─────────────────┘\n│ Username │\n│ PasswordHash │\n│ RecoveryCodeHash│\n│ Role (Admin/ │\n│ Student) │\n│ StudentId?: FK │───────► 0..1 Student\n│ CreatedAt │\n│ LastLoginAt? │\n└─────────────────┘\n\`\`\`\n\n---\n\n## 2. Entidades\n\n### Student (Aggregate Root)\n\n\`\`\`csharp\npublic class Student\n{\n public const int MaxEnrollments = 3;\n\n public int Id { get; private set; }\n public string Name { get; private set; }\n public Email Email { get; private set; }\n\n // Campos de Activación (nuevo flujo)\n public string? ActivationCodeHash { get; private set; }\n public DateTime? ActivationExpiresAt { get; private set; }\n public bool IsActivated => ActivationCodeHash == null;\n\n private readonly List<Enrollment> _enrollments = new();\n public IReadOnlyCollection<Enrollment> Enrollments => _enrollments;\n\n public int TotalCredits => _enrollments.Count * 3;\n\n public void Enroll(Subject subject, IEnrollmentPolicy policy)\n {\n policy.Validate(this, subject);\n _enrollments.Add(new Enrollment(this, subject));\n }\n\n public void Unenroll(int subjectId)\n {\n var enrollment = _enrollments.FirstOrDefault(e => e.SubjectId == subjectId);\n if (enrollment != null) _enrollments.Remove(enrollment);\n }\n\n // Métodos de activación\n public void SetActivationCode(string codeHash, TimeSpan expiresIn)\n {\n ActivationCodeHash = codeHash;\n ActivationExpiresAt = DateTime.UtcNow.Add(expiresIn);\n }\n\n public void ClearActivationCode()\n {\n ActivationCodeHash = null;\n ActivationExpiresAt = null;\n }\n\n public bool IsActivationExpired() =>\n ActivationExpiresAt.HasValue && DateTime.UtcNow > ActivationExpiresAt.Value;\n}\n\`\`\`\n\n### User (Autenticación)\n\n\`\`\`csharp\npublic class User\n{\n public int Id { get; private set; }\n public string Username { get; private set; } // Almacenado en min<69>
"DI-003 Diseño Base de Datos": `# DI-003: Diseño de Base de Datos\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Modelo Entidad-Relación\n\n\`\`\`\n┌─────────────────────────────────────────────────────────────────┐\n│ PROFESSORS │\n├──────────────┬────────────────┬─────────────────────────────────┤\n│ Id │ INT │ PK, IDENTITY │\n│ Name │ NVARCHAR(100) │ NOT NULL │\n└──────────────┴────────────────┴─────────────────────────────────┘\n │\n │ 1:N (cada profesor → 2 materias)\n ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ SUBJECTS │\n├──────────────┬────────────────┬─────────────────────────────────┤\n│ Id │ INT │ PK, IDENTITY │\n│ Name │ NVARCHAR(100) │ NOT NULL │\n│ Credits │ INT │ DEFAULT 3, CHECK (Credits = 3) │\n│ ProfessorId │ INT │ FK → Professors.Id │\n└──────────────┴────────────────┴─────────────────────────────────┘\n │\n │ 1:N\n ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ ENROLLMENTS │\n├──────────────┬────────────────┬─────────────────────────────────┤\n│ Id │ INT │ PK, IDENTITY │\n│ StudentId │ INT │ FK → Students.Id │\n│ SubjectId │ INT │ FK → Subjects.Id │\n│ EnrolledAt │ DATETIME2 │ DEFAULT GETUTCDATE() │\n└──────────────┴────────────────┴─────────────────────────────────┘\n ▲\n │ 0..3:N\n │\n┌─────────────────────────────────────────────────────────────────┐\n│ STUDENTS │\n├──────────────┬────────────────┬─────────────────────────────────┤\n│ Id │ INT │ PK, IDENTITY │\n│ Name │ NVARCHAR(100) │ NOT NULL │\n│ Email │ NVARCHAR(255) │ NOT NULL, UNIQUE
"DI-004 Esquema GraphQL": `# DI-004: Esquema GraphQL\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Schema Completo\n\n\`\`\`graphql\n# ═══════════════════════════════════════════════════════════════\n# TYPES\n# ═══════════════════════════════════════════════════════════════\n\ntype Student {\n id: Int!\n name: String!\n email: String!\n totalCredits: Int!\n isActivated: Boolean!\n activationExpiresAt: DateTime\n enrollments: [Enrollment!]!\n}\n\ntype User {\n id: Int!\n username: String!\n role: String! # "Admin" | "Student"\n studentId: Int\n studentName: String\n}\n\ntype Subject {\n id: Int!\n name: String!\n credits: Int!\n professor: Professor!\n enrolledStudents: [Student!]!\n}\n\ntype Professor {\n id: Int!\n name: String!\n subjects: [Subject!]!\n}\n\ntype Enrollment {\n id: Int!\n student: Student!\n subject: Subject!\n enrolledAt: DateTime!\n}\n\ntype AvailableSubject {\n subject: Subject!\n isAvailable: Boolean!\n unavailableReason: String\n}\n\ntype Classmate {\n subjectName: String!\n students: [String!]!\n}\n\n# ═══════════════════════════════════════════════════════════════\n# QUERIES\n# ═══════════════════════════════════════════════════════════════\n\ntype Query {\n # Autenticación\n me: User # Usuario autenticado actual\n validateActivationCode(code: String!): ActivationValidation!\n\n # Estudiantes\n students: [Student!]!\n student(id: Int!): Student\n\n # Materias\n subjects: [Subject!]!\n subject(id: Int!): Subject\n availableSubjects(studentId: Int!): [AvailableSubject!]!\n\n # Profesores\n professors: [Professor!]!\n professor(id: Int!): Professor\n\n # Compañeros de clase\n classmates(studentId: Int!): [Classmate!]!\n}\n\ntype ActivationValidation {\n isValid: Boolean!\n studentName: String\n error: String\n}\n\n# ═══════════════════════════════════════════════════════════════\n# MUTATIONS\n# ═══════════════════════════════════════════════════════════════\n\ntype Mutation {\n # Autenticación\n login(input: LoginInput!): AuthPayload!\n activateAccount(input: ActivateAccountInput!): AuthPayload!\n resetPassword(input: ResetPasswordInput!): ResetPayload!\n\n # Estudiantes (Admin crea con código de activación)\n createStudent(input: CreateStudentInput!): CreateStudentPayload!\n updateStudent(id: Int!, input: UpdateStudentInput!): StudentPayload!\n deleteStudent(id: Int!): DeletePayload!\n regenerateActivationCode(studentId: Int!): ActivationCodePayload!\n\n # Inscripciones\n enrollStudent(input: EnrollInput!): EnrollmentPayload!\n unenrollStudent(enrollmentId: Int!): DeletePayload!\n}\n\n# ═══════════════════════════════════════════════════════════════\n# INPUTS\n# ═══════════════════════════════════════════════════════════════\n\n# Autenticación\ninput LoginInput {\n username: String!\n password: String!\n}\n\ninput ActivateAccountInput {\n activationCode: String!\n username: String!\n password: String!\n}\n\ninput ResetPasswordInput {\n username: String!\n
"DI-005 Arquitectura Frontend": `# DI-005: Arquitectura Frontend\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Stack Tecnológico\n\n| Tecnología | Propósito |\n|------------|-----------|\n| Angular 21 | Framework SPA |\n| Angular Material | UI Components |\n| Apollo Angular | Cliente GraphQL |\n| Signals | Estado reactivo |\n| TypeScript | Tipado estático |\n\n---\n\n## 2. Estructura de Carpetas\n\n\`\`\`\nsrc/frontend/src/app/\n├── core/ # Singleton services\n│ ├── services/\n│ │ ├── student.service.ts\n│ │ └── enrollment.service.ts\n│ ├── graphql/\n│ │ ├── generated/ # Tipos generados\n│ │ ├── queries/\n│ │ │ ├── students.graphql\n│ │ │ └── subjects.graphql\n│ │ └── mutations/\n│ │ ├── student.graphql\n│ │ └── enrollment.graphql\n│ └── interceptors/\n│ └── error.interceptor.ts\n│\n├── shared/ # Reutilizables\n│ ├── components/\n│ │ ├── confirm-dialog/\n│ │ └── loading-spinner/\n│ └── pipes/\n│ └── credits.pipe.ts\n│\n└── features/ # Módulos por funcionalidad\n ├── students/\n │ ├── pages/\n │ │ ├── student-list/\n │ │ └── student-form/\n │ └── components/\n │ └── student-card/\n │\n ├── enrollment/\n │ ├── pages/\n │ │ └── enrollment-page/\n │ └── components/\n │ ├── subject-selector/\n │ └── enrolled-subjects/\n │\n └── classmates/\n └── pages/\n └── classmates-page/\n\`\`\`\n\n---\n\n## 3. Configuración Apollo\n\n\`\`\`typescript\n// app.config.ts\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideHttpClient(),\n provideApollo(() => ({\n link: httpLink.create({ uri: environment.graphqlUrl }),\n cache: new InMemoryCache(),\n defaultOptions: {\n watchQuery: { fetchPolicy: 'cache-and-network' },\n mutate: { errorPolicy: 'all' }\n }\n }))\n ]\n};\n\`\`\`\n\n---\n\n## 4. Gestión de Estado\n\n\`\`\`\n┌─────────────────────────────────────────────┐\n│ Apollo Cache │\n│ (Estado del servidor: students, subjects) │\n└─────────────────────────────────────────────┘\n ▲\n │ watchQuery / mutate\n ▼\n┌─────────────────────────────────────────────┐\n│ Services │\n│ (Encapsulan operaciones GraphQL) │\n└─────────────────────────────────────────────┘\n ▲\n │ inject()\n ▼\n┌─────────────────────────────────────────────┐\n│ Components │\n│ (Signals para estado local/UI) │\n└─────────────────────────────────────────────┘\n\`\`\`\n\n---\n\n## 5. Ejemplo de Service\n\n\`\`\`typescript\n@Injectable({ providedIn: 'root' })\nexport class StudentService {\n private apollo = inject(Apollo);\n\n getStudents() {\n return this.apollo.watchQuery<{ students: Student[] }>({\n query: GET_STUDENTS,\n fetchPolicy: 'cache-and-network'
"DI-006 Componentes UI": `# DI-006: Diseño de Componentes UI\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Mapa de Pantallas\n\n\`\`\`\n┌──────────────────────────────────────────────────────────────┐\n│ APP LAYOUT │\n├──────────────────────────────────────────────────────────────┤\n│ [Logo] Sistema de Estudiantes [Estudiantes] [Mat] │\n├──────────────────────────────────────────────────────────────┤\n│ │\n│ <router-outlet> │\n│ │\n└──────────────────────────────────────────────────────────────┘\n\nRutas:\n├── /students → Lista de estudiantes\n├── /students/new → Formulario crear\n├── /students/:id/edit → Formulario editar\n├── /enrollment/:id → Inscripción de materias\n└── /classmates/:id → Compañeros de clase\n\`\`\`\n\n---\n\n## 2. Wireframes\n\n### 2.1 Lista de Estudiantes (\`/students\`)\n\n\`\`\`\n┌─────────────────────────────────────────────────────────────┐\n│ ESTUDIANTES [+ Nuevo Estudiante] │\n├─────────────────────────────────────────────────────────────┤\n│ ┌─────────────────────────────────────────────────────┐ │\n│ │ 🔍 Buscar estudiante... │ │\n│ └─────────────────────────────────────────────────────┘ │\n│ │\n│ ┌───────────────────────────────────────────────────────┐ │\n│ │ NOMBRE │ EMAIL │ CRÉDITOS │ ACCIONES│ │\n│ ├───────────────────────────────────────────────────────┤ │\n│ │ Juan Pérez │ juan@email.com │ 6/9 │ ✏️ 🗑️ 📚 │ │\n│ │ María García │ maria@email.com │ 9/9 │ ✏️ 🗑️ 📚 │ │\n│ │ Carlos López │ carlos@mail.com │ 3/9 │ ✏️ 🗑️ 📚 │ │\n│ └───────────────────────────────────────────────────────┘ │\n│ │\n│ [← Anterior] [Siguiente →] │\n└─────────────────────────────────────────────────────────────┘\n\nLeyenda: ✏️ Editar 🗑️ Eliminar 📚 Inscripción\n\`\`\`\n\n### 2.2 Formulario Estudiante (\`/students/new\`)\n\n\`\`\`\n┌──────────────────────
"DI-007 Contratos DTOs": `# DI-007: Contratos y DTOs\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. DTOs Backend (C#)\n\n### Request DTOs (Inputs)\n\n\`\`\`csharp\n// Students\npublic record CreateStudentInput(string Name, string Email);\npublic record UpdateStudentInput(string? Name, string? Email);\n\n// Enrollments\npublic record EnrollInput(int StudentId, int SubjectId);\n\`\`\`\n\n### Response DTOs\n\n\`\`\`csharp\n// Student\npublic record StudentDto(\n int Id,\n string Name,\n string Email,\n int TotalCredits,\n IEnumerable<EnrollmentDto> Enrollments);\n\n// Subject\npublic record SubjectDto(\n int Id,\n string Name,\n int Credits,\n ProfessorDto Professor);\n\n// Professor\npublic record ProfessorDto(\n int Id,\n string Name);\n\n// Enrollment\npublic record EnrollmentDto(\n int Id,\n SubjectDto Subject,\n DateTime EnrolledAt);\n\n// Available Subject (con disponibilidad)\npublic record AvailableSubjectDto(\n SubjectDto Subject,\n bool IsAvailable,\n string? UnavailableReason);\n\n// Classmates\npublic record ClassmateDto(\n string SubjectName,\n IEnumerable<string> StudentNames);\n\`\`\`\n\n### Payloads (Respuestas con errores)\n\n\`\`\`csharp\npublic record StudentPayload(StudentDto? Student, IEnumerable<string>? Errors);\npublic record EnrollmentPayload(EnrollmentDto? Enrollment, IEnumerable<string>? Errors);\npublic record DeletePayload(bool Success, IEnumerable<string>? Errors);\n\`\`\`\n\n---\n\n## 2. DTOs Frontend (TypeScript)\n\n### Tipos Generados (graphql-codegen)\n\n\`\`\`typescript\n// Student\nexport interface Student {\n id: number;\n name: string;\n email: string;\n totalCredits: number;\n enrollments: Enrollment[];\n}\n\n// Subject\nexport interface Subject {\n id: number;\n name: string;\n credits: number;\n professor: Professor;\n}\n\n// Professor\nexport interface Professor {\n id: number;\n name: string;\n subjects?: Subject[];\n}\n\n// Enrollment\nexport interface Enrollment {\n id: number;\n subject: Subject;\n enrolledAt: string;\n}\n\n// Available Subject\nexport interface AvailableSubject {\n subject: Subject;\n isAvailable: boolean;\n unavailableReason?: string;\n}\n\n// Classmate\nexport interface Classmate {\n subjectName: string;\n students: string[];\n}\n\`\`\`\n\n### Inputs\n\n\`\`\`typescript\nexport interface CreateStudentInput {\n name: string;\n email: string;\n}\n\nexport interface UpdateStudentInput {\n name?: string;\n email?: string;\n}\n\nexport interface EnrollInput {\n studentId: number;\n subjectId: number;\n}\n\`\`\`\n\n### Payloads\n\n\`\`\`typescript\nexport interface StudentPayload {\n student?: Student;\n errors?: string[];\n}\n\nexport interface EnrollmentPayload {\n enrollment?: Enrollment;\n errors?: string[];\n}\n\nexport interface DeletePayload {\n success: boolean;\n errors?: string[];\n}\n\`\`\`\n\n---\n\n## 3. Mapeos (Mapster)\n\n\`\`\`csharp\npublic class MappingConfig : IRegister\n{\n public void Register(TypeAdapterConfig config)\n {\n config.NewConfig<Student, StudentDto>()\n .Map(dest => dest.TotalCredits, src => src.Enrollments.Count * 3);\n\n config.NewConfig<Subject, SubjectDto>();\n config.NewConfig<Professor, ProfessorDto>();\n config.NewConfig<Enrollment, EnrollmentDto>();\n }\n}\n\`\`\`\n\n---\n\n## 4. Validadores (FluentValidation)\n\n\`\`\`csharp\npublic class CreateStudentInputValidator : AbstractValidator<CreateStudentInput>\n{\n public CreateStudentInputValidator()\n {\n RuleFor(x => x.Name)\n .NotEmpty().WithMessage("Nombre requerido")\n .MaximumLength(100).WithMessage("Máximo 100 caracteres");\n\n RuleFor(x => x.Email)\n .NotEmpty().WithMessage("Email requerido")\n .EmailAddress().WithMessage("Formato de email inválido")\n .MaximumLength(255).WithMessage("Máximo 255 caracteres");\n }\n}\n\npublic class EnrollInputValidator : AbstractValidator<EnrollInput>\n{\n public
"DI-008 Manejo de Errores": `# DI-008: Estrategia de Manejo de Errores\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Clasificación de Errores\n\n| Tipo | Origen | Manejo | HTTP Code (equiv) |\n|------|--------|--------|-------------------|\n| **Validación** | FluentValidation | Payload.errors | 400 |\n| **Dominio** | Domain Exceptions | Payload.errors | 422 |\n| **Not Found** | Repository | Payload.errors | 404 |\n| **Conflicto** | Concurrencia | Payload.errors | 409 |\n| **Sistema** | Excepciones no manejadas | Error GraphQL | 500 |\n\n---\n\n## 2. Excepciones de Dominio\n\n\`\`\`csharp\n// Base\npublic abstract class DomainException : Exception\n{\n public string Code { get; }\n protected DomainException(string code, string message) : base(message)\n => Code = code;\n}\n\n// Específicas\npublic class MaxEnrollmentsExceededException : DomainException\n{\n public MaxEnrollmentsExceededException()\n : base("MAX_ENROLLMENTS", "Máximo 3 materias permitidas") { }\n}\n\npublic class SameProfessorConstraintException : DomainException\n{\n public SameProfessorConstraintException(string professorName)\n : base("SAME_PROFESSOR", \$"Ya tienes una materia con {professorName}") { }\n}\n\npublic class DuplicateEmailException : DomainException\n{\n public DuplicateEmailException()\n : base("DUPLICATE_EMAIL", "Este email ya está registrado") { }\n}\n\npublic class StudentNotFoundException : DomainException\n{\n public StudentNotFoundException(int id)\n : base("NOT_FOUND", \$"Estudiante {id} no encontrado") { }\n}\n\`\`\`\n\n---\n\n## 3. Patrón Result\n\n\`\`\`csharp\npublic class Result\n{\n public bool IsSuccess { get; }\n public IEnumerable<string> Errors { get; }\n\n protected Result(bool success, IEnumerable<string>? errors = null)\n {\n IsSuccess = success;\n Errors = errors ?? Array.Empty<string>();\n }\n\n public static Result Success() => new(true);\n public static Result Failure(params string[] errors) => new(false, errors);\n}\n\npublic class Result<T> : Result\n{\n public T? Value { get; }\n\n private Result(T value) : base(true) => Value = value;\n private Result(IEnumerable<string> errors) : base(false, errors) { }\n\n public static Result<T> Success(T value) => new(value);\n public static new Result<T> Failure(params string[] errors) => new(errors);\n}\n\`\`\`\n\n---\n\n## 4. Handler con Manejo de Errores\n\n\`\`\`csharp\npublic class EnrollStudentHandler\n{\n public async Task<EnrollmentPayload> Handle(EnrollInput input)\n {\n try\n {\n var student = await _studentRepo.GetByIdWithEnrollmentsAsync(input.StudentId);\n if (student is null)\n return new EnrollmentPayload(null, ["Estudiante no encontrado"]);\n\n var subject = await _subjectRepo.GetByIdAsync(input.SubjectId);\n if (subject is null)\n return new EnrollmentPayload(null, ["Materia no encontrada"]);\n\n // Validación de dominio\n student.Enroll(subject, _enrollmentPolicy);\n\n await _unitOfWork.SaveChangesAsync();\n\n var dto = student.Enrollments.Last().Adapt<EnrollmentDto>();\n return new EnrollmentPayload(dto, null);\n }\n catch (DomainException ex)\n {\n return new EnrollmentPayload(null, [ex.Message]);\n }\n }\n}\n\`\`\`\n\n---\n\n## 5. Error Filter GraphQL (HotChocolate)\n\n\`\`\`csharp\npublic class ErrorFilter : IErrorFilter\n{\n public IError OnError(IError error)\n {\n return error.Exception switch\n {\n DomainException ex => error\n .WithMessage(ex.Message)\n .WithCode(ex.Code),\n\n ValidationException ex => error\n .WithMessage("Errores de validación")\n .WithCode("VALIDATION_ERROR")\n .SetExtension("errors", ex.Errors.Select(e => e.ErrorMessage)),\n\n DbUpdateConcurrencyEx
},
"Configuración": {
"DV-001 Configuración Repositorio": `# DV-001: Configuración del Repositorio\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Estructura de Carpetas\n\n\`\`\`\n/\n├── src/\n│ ├── backend/\n│ │ ├── Domain/\n│ │ ├── Application/\n│ │ ├── Adapters/\n│ │ │ ├── Driving/Api/\n│ │ │ └── Driven/Persistence/\n│ │ └── Host/\n│ └── frontend/\n│ └── src/app/\n│ ├── core/\n│ ├── shared/\n│ └── features/\n├── tests/\n│ ├── Domain.Tests/\n│ ├── Application.Tests/\n│ ├── Adapters.Tests/\n│ └── e2e/\n├── docs/\n│ └── entregables/\n├── database/\n│ ├── scripts/\n│ └── migrations/\n├── deploy/\n│ └── docker/\n├── DEV-GUIDE.md\n├── README.md\n└── .gitignore\n\`\`\`\n\n---\n\n## 2. .gitignore\n\n\`\`\`gitignore\n# .NET\nbin/\nobj/\n*.user\n*.suo\n.vs/\n*.csproj.user\n\n# Angular\nnode_modules/\ndist/\n.angular/\n.nx/\n\n# IDE\n.idea/\n.vscode/\n*.swp\n\n# Logs\n*.log\nlogs/\n\n# Environment\n.env\n.env.*\n!.env.example\nappsettings.*.json\n!appsettings.json\n!appsettings.Development.json.example\n\n# Database\n*.mdf\n*.ldf\n\n# OS\n.DS_Store\nThumbs.db\n\n# Test\ncoverage/\nTestResults/\n\n# Build\npublish/\n\`\`\`\n\n---\n\n## 3. Comandos de Inicialización\n\n\`\`\`bash\n# Crear repositorio\ngit init\ngit add .\ngit commit -m "chore: initial project structure"\n\n# Crear rama de desarrollo\ngit checkout -b develop\n\n# Estructura de ramas\n# main → producción\n# develop → integración\n# feature/ → nuevas funcionalidades\n# fix/ → correcciones\n\`\`\`\n\n---\n\n## 4. Convenciones de Commits\n\n\`\`\`\n<tipo>(<alcance>): <descripción>\n\nTipos:\n- feat: Nueva funcionalidad\n- fix: Corrección de bug\n- refactor: Refactorización\n- test: Tests\n- docs: Documentación\n- chore: Tareas de mantenimiento\n\nEjemplos:\nfeat(students): add create student mutation\nfix(enrollment): validate professor constraint\ntest(domain): add enrollment policy tests\n\`\`\`\n`,
"DV-002 Configuración .NET": `# DV-002: Configuración Solución .NET 10\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Crear Solución y Proyectos\n\n\`\`\`bash\ncd src/backend\n\n# Crear solución\ndotnet new sln -n StudentEnrollment\n\n# Crear proyectos\ndotnet new classlib -n Domain -f net10.0\ndotnet new classlib -n Application -f net10.0\ndotnet new classlib -n Adapters.Driving.Api -f net10.0\ndotnet new classlib -n Adapters.Driven.Persistence -f net10.0\ndotnet new web -n Host -f net10.0\n\n# Agregar a solución\ndotnet sln add Domain/Domain.csproj\ndotnet sln add Application/Application.csproj\ndotnet sln add Adapters.Driving.Api/Adapters.Driving.Api.csproj\ndotnet sln add Adapters.Driven.Persistence/Adapters.Driven.Persistence.csproj\ndotnet sln add Host/Host.csproj\n\`\`\`\n\n---\n\n## 2. Referencias entre Proyectos\n\n\`\`\`bash\n# Application → Domain\ndotnet add Application reference Domain\n\n# Adapters.Driving.Api → Application\ndotnet add Adapters.Driving.Api reference Application\n\n# Adapters.Driven.Persistence → Domain\ndotnet add Adapters.Driven.Persistence reference Domain\n\n# Host → Todos\ndotnet add Host reference Application\ndotnet add Host reference Adapters.Driving.Api\ndotnet add Host reference Adapters.Driven.Persistence\n\`\`\`\n\n\`\`\`\nHost\n├── Adapters.Driving.Api → Application → Domain\n└── Adapters.Driven.Persistence ────────→ Domain\n\`\`\`\n\n---\n\n## 3. Paquetes NuGet por Proyecto\n\n### Domain (sin dependencias externas)\n\`\`\`xml\n<!-- Solo C# puro -->\n\`\`\`\n\n### Application\n\`\`\`xml\n<PackageReference Include="FluentValidation" />\n<PackageReference Include="Mapster" />\n<PackageReference Include="MediatR" />\n\`\`\`\n\n### Adapters.Driven.Persistence\n\`\`\`xml\n<PackageReference Include="Microsoft.EntityFrameworkCore" />\n<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />\n<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />\n\`\`\`\n\n### Adapters.Driving.Api\n\`\`\`xml\n<PackageReference Include="HotChocolate.AspNetCore" />\n<PackageReference Include="HotChocolate.Data" />\n<PackageReference Include="HotChocolate.Data.EntityFramework" />\n\`\`\`\n\n### Host\n\`\`\`xml\n<PackageReference Include="Serilog.AspNetCore" />\n\`\`\`\n\n---\n\n## 4. Directory.Build.props (Raíz backend)\n\n\`\`\`xml\n<Project>\n <PropertyGroup>\n <TargetFramework>net10.0</TargetFramework>\n <Nullable>enable</Nullable>\n <ImplicitUsings>enable</ImplicitUsings>\n <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n </PropertyGroup>\n</Project>\n\`\`\`\n\n---\n\n## 5. Program.cs (Host)\n\n\`\`\`csharp\nvar builder = WebApplication.CreateBuilder(args);\n\n// Services\nbuilder.Services.AddApplication();\nbuilder.Services.AddPersistence(builder.Configuration);\nbuilder.Services.AddGraphQLApi();\n\n// CORS\nbuilder.Services.AddCors(options =>\n{\n options.AddDefaultPolicy(policy =>\n policy.WithOrigins("http://localhost:4200")\n .AllowAnyHeader()\n .AllowAnyMethod());\n});\n\nvar app = builder.Build();\n\napp.UseCors();\napp.MapGraphQL();\n\napp.Run();\n\`\`\`\n\n---\n\n## 6. Comandos de Desarrollo\n\n\`\`\`bash\n# Restaurar dependencias\ndotnet restore\n\n# Build\ndotnet build\n\n# Ejecutar\ndotnet run --project Host\n\n# Watch mode\ndotnet watch run --project Host\n\n# Tests\ndotnet test\n\n# Migraciones EF\ndotnet ef migrations add Initial -p Adapters.Driven.Persistence -s Host\ndotnet ef database update -p Adapters.Driven.Persistence -s Host\n\`\`\`\n`,
"DV-003 Configuración Angular": `# DV-003: Configuración Proyecto Angular 21\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Crear Proyecto\n\n\`\`\`bash\ncd src/frontend\n\n# Crear proyecto Angular 21\nng new student-enrollment \\\n --standalone \\\n --style=scss \\\n --routing \\\n --ssr=false\n\ncd student-enrollment\n\`\`\`\n\n---\n\n## 2. Instalar Dependencias\n\n\`\`\`bash\n# Angular Material\nng add @angular/material\n\n# Apollo GraphQL\nnpm install apollo-angular @apollo/client graphql\n\n# GraphQL Code Generator\nnpm install -D @graphql-codegen/cli \\\n @graphql-codegen/typescript \\\n @graphql-codegen/typescript-operations \\\n @graphql-codegen/typescript-apollo-angular\n\`\`\`\n\n---\n\n## 3. Estructura de Carpetas\n\n\`\`\`bash\n# Crear estructura\nmkdir -p src/app/core/{services,graphql/{queries,mutations},interceptors}\nmkdir -p src/app/shared/{components,pipes,directives}\nmkdir -p src/app/features/{students,enrollment,classmates}/{pages,components}\n\`\`\`\n\n---\n\n## 4. Configuración Apollo (app.config.ts)\n\n\`\`\`typescript\nimport { ApplicationConfig } from '@angular/core';\nimport { provideRouter } from '@angular/router';\nimport { provideHttpClient } from '@angular/common/http';\nimport { provideApollo } from 'apollo-angular';\nimport { HttpLink } from 'apollo-angular/http';\nimport { InMemoryCache } from '@apollo/client/core';\nimport { inject } from '@angular/core';\nimport { routes } from './app.routes';\nimport { environment } from '../environments/environment';\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideRouter(routes),\n provideHttpClient(),\n provideApollo(() => {\n const httpLink = inject(HttpLink);\n return {\n link: httpLink.create({ uri: environment.graphqlUrl }),\n cache: new InMemoryCache(),\n };\n }),\n ],\n};\n\`\`\`\n\n---\n\n## 5. Environment Files\n\n\`\`\`typescript\n// environments/environment.ts\nexport const environment = {\n production: false,\n graphqlUrl: 'https://localhost:5001/graphql',\n};\n\n// environments/environment.prod.ts\nexport const environment = {\n production: true,\n graphqlUrl: '/graphql',\n};\n\`\`\`\n\n---\n\n## 6. GraphQL Codegen (codegen.ts)\n\n\`\`\`typescript\nimport type { CodegenConfig } from '@graphql-codegen/cli';\n\nconst config: CodegenConfig = {\n schema: 'https://localhost:5001/graphql',\n documents: 'src/app/core/graphql/**/*.graphql',\n generates: {\n 'src/app/core/graphql/generated/types.ts': {\n plugins: [\n 'typescript',\n 'typescript-operations',\n 'typescript-apollo-angular',\n ],\n },\n },\n};\n\nexport default config;\n\`\`\`\n\n\`\`\`json\n// package.json scripts\n{\n "scripts": {\n "codegen": "graphql-codegen --config codegen.ts"\n }\n}\n\`\`\`\n\n---\n\n## 7. Path Aliases (tsconfig.json)\n\n\`\`\`json\n{\n "compilerOptions": {\n "paths": {\n "@core/*": ["src/app/core/*"],\n "@shared/*": ["src/app/shared/*"],\n "@features/*": ["src/app/features/*"],\n "@env/*": ["src/environments/*"]\n }\n }\n}\n\`\`\`\n\n---\n\n## 8. Comandos de Desarrollo\n\n\`\`\`bash\n# Desarrollo\nng serve\n\n# Build producción\nng build --configuration production\n\n# Generar tipos GraphQL\nnpm run codegen\n\n# Lint\nng lint\n\n# Tests\nng test\nng test --watch=false --code-coverage\n\n# Generar componente standalone\nng g c features/students/pages/student-list --standalone\n\`\`\`\n`,
"DV-004 Configuración Base Datos": `# DV-004: Configuración Base de Datos\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Docker Compose (SQL Server)\n\n\`\`\`yaml\n# deploy/docker/docker-compose.yml\nservices:\n sqlserver:\n image: mcr.microsoft.com/mssql/server\n container_name: sqlserver-students\n environment:\n - ACCEPT_EULA=Y\n - SA_PASSWORD=\${DB_PASSWORD:-Asde71.4Asde71.4}\n - MSSQL_PID=Developer\n ports:\n - "1433:1433"\n volumes:\n - sqlserver-data:/var/opt/mssql\n healthcheck:\n test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "\$\$SA_PASSWORD" -C -Q "SELECT 1"\n interval: 10s\n timeout: 5s\n retries: 5\n\nvolumes:\n sqlserver-data:\n\`\`\`\n\n---\n\n## 2. Comandos Docker\n\n\`\`\`bash\n# Iniciar SQL Server\ndocker-compose -f deploy/docker/docker-compose.yml up -d sqlserver\n\n# Ver logs\ndocker logs sqlserver-students\n\n# Conectar a SQL Server\ndocker exec -it sqlserver-students /opt/mssql-tools18/bin/sqlcmd \\\n -S localhost -U sa -P 'Asde71.4Asde71.4' -C\n\n# Detener\ndocker-compose -f deploy/docker/docker-compose.yml down\n\`\`\`\n\n---\n\n## 3. Connection String\n\n\`\`\`json\n// appsettings.Development.json\n{\n "ConnectionStrings": {\n "DefaultConnection": "Server=localhost;Database=StudentEnrollment;User Id=sa;Password=Asde71.4Asde71.4;TrustServerCertificate=True"\n }\n}\n\`\`\`\n\n---\n\n## 4. DbContext Configuration\n\n\`\`\`csharp\n// Adapters/Driven/Persistence/Context/AppDbContext.cs\npublic class AppDbContext : DbContext\n{\n public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }\n\n public DbSet<Student> Students => Set<Student>();\n public DbSet<Subject> Subjects => Set<Subject>();\n public DbSet<Professor> Professors => Set<Professor>();\n public DbSet<Enrollment> Enrollments => Set<Enrollment>();\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);\n }\n}\n\`\`\`\n\n---\n\n## 5. Registrar DbContext\n\n\`\`\`csharp\n// Adapters/Driven/Persistence/DependencyInjection.cs\npublic static class DependencyInjection\n{\n public static IServiceCollection AddPersistence(\n this IServiceCollection services,\n IConfiguration configuration)\n {\n services.AddDbContext<AppDbContext>(options =>\n options.UseSqlServer(\n configuration.GetConnectionString("DefaultConnection")));\n\n services.AddScoped<IStudentRepository, StudentRepository>();\n services.AddScoped<ISubjectRepository, SubjectRepository>();\n services.AddScoped<IUnitOfWork, UnitOfWork>();\n\n return services;\n }\n}\n\`\`\`\n\n---\n\n## 6. Migraciones\n\n\`\`\`bash\n# Crear migración inicial\ndotnet ef migrations add InitialCreate \\\n -p src/backend/Adapters.Driven.Persistence \\\n -s src/backend/Host\n\n# Aplicar migración\ndotnet ef database update \\\n -p src/backend/Adapters.Driven.Persistence \\\n -s src/backend/Host\n\n# Generar script SQL\ndotnet ef migrations script \\\n -p src/backend/Adapters.Driven.Persistence \\\n -s src/backend/Host \\\n -o database/scripts/create.sql\n\`\`\`\n\n---\n\n## 7. Seed Data\n\n\`\`\`csharp\n// Adapters/Driven/Persistence/Seeding/DataSeeder.cs\npublic static class DataSeeder\n{\n public static void Seed(ModelBuilder modelBuilder)\n {\n // Profesores\n modelBuilder.Entity<Professor>().HasData(\n new { Id = 1, Name = "Dr. García" },\n new { Id = 2, Name = "Dra. Martínez" },\n new { Id = 3, Name = "Dr. López" },\n new { Id = 4, Name = "Dra. Rodríguez" },\n new { Id = 5, Name = "Dr. Hernández" }\n );\n\n // Materias (2 por profesor)\n modelBuilder.Entity<Subject>().HasData(\n new { Id = 1, Name = "Matemáticas I", Credits = 3, ProfessorId = 1 },\n new { Id = 2, Name = "Matemáticas II", Credits = 3, Profess
"DV-005 Variables de Entorno": `# DV-005: Variables de Entorno\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Backend (.NET 10)\n\n### appsettings.json (Base)\n\n\`\`\`json\n{\n "Logging": {\n "LogLevel": {\n "Default": "Information",\n "Microsoft.AspNetCore": "Warning",\n "HotChocolate": "Warning"\n }\n },\n "AllowedHosts": "*",\n "GraphQL": {\n "MaxExecutionDepth": 5,\n "MaxComplexity": 100\n }\n}\n\`\`\`\n\n### appsettings.Development.json\n\n\`\`\`json\n{\n "ConnectionStrings": {\n "DefaultConnection": "Server=localhost;Database=StudentEnrollment;User Id=sa;Password=Asde71.4Asde71.4;TrustServerCertificate=True"\n },\n "GraphQL": {\n "EnableIntrospection": true\n }\n}\n\`\`\`\n\n### appsettings.Production.json.example\n\n\`\`\`json\n{\n "ConnectionStrings": {\n "DefaultConnection": "Server=PROD_SERVER;Database=StudentEnrollment;User Id=app_user;Password=CHANGE_ME;Encrypt=True"\n },\n "GraphQL": {\n "EnableIntrospection": false\n }\n}\n\`\`\`\n\n---\n\n## 2. User Secrets (Desarrollo Local)\n\n\`\`\`bash\n# Inicializar secrets\ncd src/backend/Host\ndotnet user-secrets init\n\n# Guardar connection string\ndotnet user-secrets set "ConnectionStrings:DefaultConnection" \\\n "Server=localhost;Database=StudentEnrollment;User Id=sa;Password=Asde71.4Asde71.4;TrustServerCertificate=True"\n\n# Listar secrets\ndotnet user-secrets list\n\n# Remover\ndotnet user-secrets remove "ConnectionStrings:DefaultConnection"\n\`\`\`\n\n---\n\n## 3. Frontend (Angular 21)\n\n### environment.ts (Desarrollo)\n\n\`\`\`typescript\nexport const environment = {\n production: false,\n graphqlUrl: 'https://localhost:5001/graphql',\n apiTimeout: 30000,\n};\n\`\`\`\n\n### environment.prod.ts (Producción)\n\n\`\`\`typescript\nexport const environment = {\n production: true,\n graphqlUrl: '/graphql',\n apiTimeout: 15000,\n};\n\`\`\`\n\n---\n\n## 4. Docker (.env)\n\n### .env.example\n\n\`\`\`env\n# Database\nDB_PASSWORD=Asde71.4Asde71.4\nDB_NAME=StudentEnrollment\n\n# API\nASPNETCORE_ENVIRONMENT=Development\nASPNETCORE_URLS=http://+:5000\n\n# Frontend\nFRONTEND_URL=http://localhost:4200\n\`\`\`\n\n### docker-compose.yml (usando .env)\n\n\`\`\`yaml\nservices:\n sqlserver:\n environment:\n - SA_PASSWORD=\${DB_PASSWORD}\n\n api:\n environment:\n - ASPNETCORE_ENVIRONMENT=\${ASPNETCORE_ENVIRONMENT}\n - ConnectionStrings__DefaultConnection=Server=sqlserver;Database=\${DB_NAME};User Id=sa;Password=\${DB_PASSWORD};TrustServerCertificate=True\n\`\`\`\n\n---\n\n## 5. Variables por Ambiente\n\n| Variable | Desarrollo | Producción |\n|----------|------------|------------|\n| ConnectionString | localhost | Servidor prod |\n| EnableIntrospection | true | false |\n| LogLevel | Debug | Warning |\n| CORS Origins | localhost:4200 | dominio.com |\n| MaxExecutionDepth | 10 | 5 |\n\n---\n\n## 6. Cargar Configuración\n\n\`\`\`csharp\n// Program.cs\nvar builder = WebApplication.CreateBuilder(args);\n\n// Orden de carga (último gana):\n// 1. appsettings.json\n// 2. appsettings.{Environment}.json\n// 3. User Secrets (solo Development)\n// 4. Variables de entorno\n// 5. Command line args\n\nvar connectionString = builder.Configuration\n .GetConnectionString("DefaultConnection");\n\`\`\`\n`,
"DV-006 Herramientas de Calidad": `# DV-006: Herramientas de Calidad\n\n**Proyecto:** Sistema de Registro de Estudiantes\n**Fecha:** 2026-01-07\n\n---\n\n## 1. Backend (.NET 10)\n\n### .editorconfig\n\n\`\`\`ini\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.{cs,csx}]\ncsharp_style_var_when_type_is_apparent = true:suggestion\ncsharp_prefer_braces = true:warning\ndotnet_sort_system_directives_first = true\n\`\`\`\n\n### Directory.Build.props (Analyzers)\n\n\`\`\`xml\n<Project>\n <PropertyGroup>\n <TargetFramework>net10.0</TargetFramework>\n <Nullable>enable</Nullable>\n <ImplicitUsings>enable</ImplicitUsings>\n <TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>\n <AnalysisLevel>latest</AnalysisLevel>\n </PropertyGroup>\n\n <ItemGroup>\n <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" PrivateAssets="all" />\n </ItemGroup>\n</Project>\n\`\`\`\n\n### Comandos\n\n\`\`\`bash\n# Formatear código\ndotnet format\n\n# Verificar sin cambiar\ndotnet format --verify-no-changes\n\n# Build con warnings\ndotnet build -warnaserror\n\`\`\`\n\n---\n\n## 2. Frontend (Angular 21)\n\n### ESLint\n\n\`\`\`bash\n# Instalar\nng add @angular-eslint/schematics\n\`\`\`\n\n\`\`\`json\n// .eslintrc.json\n{\n "root": true,\n "overrides": [\n {\n "files": ["*.ts"],\n "extends": [\n "eslint:recommended",\n "plugin:@typescript-eslint/recommended",\n "plugin:@angular-eslint/recommended"\n ],\n "rules": {\n "@angular-eslint/component-selector": ["error", {\n "prefix": "app",\n "style": "kebab-case",\n "type": "element"\n }],\n "@typescript-eslint/no-unused-vars": "error",\n "no-console": "warn"\n }\n }\n ]\n}\n\`\`\`\n\n### Prettier\n\n\`\`\`json\n// .prettierrc\n{\n "singleQuote": true,\n "trailingComma": "es5",\n "tabWidth": 2,\n "semi": true,\n "printWidth": 100\n}\n\`\`\`\n\n\`\`\`json\n// .prettierignore\nnode_modules\ndist\ncoverage\n.angular\n\`\`\`\n\n---\n\n## 3. Pre-commit Hooks (Husky)\n\n\`\`\`bash\n# Instalar\nnpm install -D husky lint-staged\nnpx husky init\n\`\`\`\n\n\`\`\`json\n// package.json\n{\n "lint-staged": {\n "*.ts": ["eslint --fix", "prettier --write"],\n "*.html": ["prettier --write"],\n "*.scss": ["prettier --write"]\n }\n}\n\`\`\`\n\n\`\`\`bash\n# .husky/pre-commit\nnpm run lint-staged\ncd ../backend && dotnet format --verify-no-changes\n\`\`\`\n\n---\n\n## 4. Scripts package.json\n\n\`\`\`json\n{\n "scripts": {\n "start": "ng serve",\n "build": "ng build",\n "test": "ng test",\n "lint": "ng lint",\n "format": "prettier --write \\"src/**/*.{ts,html,scss}\\"",\n "format:check": "prettier --check \\"src/**/*.{ts,html,scss}\\"",\n "codegen": "graphql-codegen",\n "prepare": "husky"\n }\n}\n\`\`\`\n\n---\n\n## 5. CI Checks\n\n\`\`\`yaml\n# .github/workflows/ci.yml (ejemplo)\njobs:\n backend:\n steps:\n - run: dotnet format --verify-no-changes\n - run: dotnet build -warnaserror\n - run: dotnet test\n\n frontend:\n steps:\n - run: npm ci\n - run: npm run lint\n - run: npm run format:check\n - run: npm run build\n - run: npm test -- --watch=false\n\`\`\`\n\n---\n\n## 6. Checklist de Calidad\n\n| Verificación | Backend | Frontend |\n|--------------|---------|----------|\n| Formato código | \`dotnet format\` | \`prettier\` |\n| Linting | Roslyn Analyzers | ESLint |\n| Tipos | Nullable enabled | TypeScript strict |\n| Tests | xUnit | Jasmine/Jest |\n| Pre-commit | dotnet format | lint-staged |\n`,
},
"Arquitectura": {
"ADR-001 Clean Architecture": `# ADR-001: Clean Architecture\n\n**Estado:** Aceptado\n**Fecha:** 2026-01-07\n\n## Contexto\n\nNecesitamos una arquitectura que permita:\n- Testabilidad de reglas de negocio\n- Independencia de frameworks\n- Mantenibilidad a largo plazo\n- Separación clara de responsabilidades\n\n## Decisión\n\nAdoptar **Clean Architecture** con 4 capas: Domain, Application, Adapters, Host.\n\n\`\`\`\nHost → Adapters → Application → Domain\n\`\`\`\n\n## Consecuencias\n\n### Positivas\n- Domain sin dependencias externas (puro C#)\n- Reglas de negocio testeables sin mocks de infraestructura\n- Fácil cambiar ORM o base de datos\n- Fácil cambiar de REST a GraphQL (o viceversa)\n\n### Negativas\n- Mayor cantidad de archivos/proyectos\n- Curva de aprendizaje inicial\n- Overhead para proyectos muy pequeños\n\n## Alternativas Consideradas\n\n| Alternativa | Razón de Descarte |\n|-------------|-------------------|\n| N-Layer tradicional | Alto acoplamiento, difícil testear |\n| Vertical Slices | Menor separación de concerns |\n| Monolítico simple | No escala con complejidad |\n\n## Referencias\n\n- [Clean Architecture - Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)\n`,
"ADR-002 GraphQL vs REST": `# ADR-002: GraphQL vs REST\n\n**Estado:** Aceptado\n**Fecha:** 2026-01-07\n\n## Contexto\n\nLa aplicación tiene relaciones complejas:\n- Estudiantes → Inscripciones → Materias → Profesores\n- Consultas como "materias disponibles" requieren múltiples joins\n- Frontend necesita flexibilidad en datos solicitados\n\n## Decisión\n\nUsar **GraphQL** con HotChocolate como API.\n\n## Consecuencias\n\n### Positivas\n- **No over-fetching:** Cliente pide solo campos necesarios\n- **No under-fetching:** Una query obtiene datos relacionados\n- **Schema tipado:** Contrato explícito frontend-backend\n- **Playground incluido:** Banana Cake Pop para testing\n- **DataLoaders:** Resuelve N+1 automáticamente\n\n### Negativas\n- Complejidad adicional vs REST simple\n- Curva de aprendizaje GraphQL\n- Requiere configurar query complexity limits\n- Cache más complejo que HTTP caching\n\n## Ejemplo Comparativo\n\n### REST (múltiples requests)\n\`\`\`\nGET /students/1\nGET /students/1/enrollments\nGET /subjects/1\nGET /professors/1\n\`\`\`\n\n### GraphQL (una query)\n\`\`\`graphql\nquery {\n student(id: 1) {\n name\n enrollments {\n subject {\n name\n professor { name }\n }\n }\n }\n}\n\`\`\`\n\n## Alternativas\n\n| Alternativa | Razón de Descarte |\n|-------------|-------------------|\n| REST | Over/under-fetching, múltiples endpoints |\n| OData | Menos flexible, menos ecosistema |\n| gRPC | No ideal para frontend web |\n\n## Referencias\n\n- [HotChocolate Docs](https://chillicream.com/docs/hotchocolate)\n`,
"ADR-003 Angular Signals": `# ADR-003: Signals vs RxJS para Estado Local\n\n**Estado:** Aceptado\n**Fecha:** 2026-01-07\n\n## Contexto\n\nAngular 21 introduce Signals como alternativa a RxJS para estado reactivo.\nNecesitamos decidir el enfoque para manejo de estado en el frontend.\n\n## Decisión\n\nUsar **Signals para estado local** + **Apollo Client para estado del servidor**.\n\n\`\`\`typescript\n// Estado local con Signals\nstudents = signal<Student[]>([]);\nloading = signal(true);\n\n// Estado del servidor con Apollo\nthis.apollo.watchQuery<GetStudentsQuery>({...})\n\`\`\`\n\n## Consecuencias\n\n### Positivas\n- **Simplicidad:** Signals más intuitivos que BehaviorSubject\n- **Performance:** Integración nativa con OnPush\n- **Menos boilerplate:** No requiere async pipe en templates\n- **Type-safe:** Mejor inferencia de tipos\n\n### Negativas\n- Tecnología relativamente nueva\n- Menos operadores que RxJS\n- Apollo aún usa Observables internamente\n\n## Patrón Adoptado\n\n\`\`\`typescript\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class StudentListComponent {\n // Estado local\n students = signal<Student[]>([]);\n loading = signal(true);\n\n // Suscripción a Apollo\n ngOnInit() {\n this.studentService.getStudents().subscribe(({ data, loading }) => {\n this.students.set(data);\n this.loading.set(loading);\n });\n }\n}\n\`\`\`\n\n## Alternativas\n\n| Alternativa | Razón de Descarte |\n|-------------|-------------------|\n| RxJS puro | Mayor complejidad, más boilerplate |\n| NgRx | Overkill para esta aplicación |\n| Akita | Dependencia adicional innecesaria |\n\n## Referencias\n\n- [Angular Signals](https://angular.dev/guide/signals)\n`,
"ADR-004 Validation Strategy": `# ADR-004: Estrategia de Validación en 3 Capas\n\n**Estado:** Aceptado\n**Fecha:** 2026-01-07\n\n## Contexto\n\nLas reglas de negocio críticas son:\n- Máximo 3 materias por estudiante (9 créditos)\n- No repetir profesor en inscripciones\n- Datos de entrada válidos (email, nombre)\n\n## Decisión\n\nValidar en **3 capas** con responsabilidades distintas:\n\n| Capa | Responsabilidad | Tecnología |\n|------|-----------------|------------|\n| Frontend | UX, feedback rápido | Reactive Forms |\n| Application | Estructura de datos, sanitización | FluentValidation |\n| Domain | Reglas de negocio puras | Domain Services |\n\n## Implementación\n\n### Frontend (UX)\n\`\`\`typescript\nthis.form = this.fb.group({\n name: ['', [Validators.required, Validators.minLength(3)]],\n email: ['', [Validators.required, Validators.email]],\n});\n\`\`\`\n\n### Application (Sanitización + XSS)\n\`\`\`csharp\nRuleFor(x => x.Name)\n .NotEmpty()\n .Must(NotContainDangerousContent); // Previene XSS\n\`\`\`\n\n### Domain (Negocio)\n\`\`\`csharp\npublic void ValidateEnrollment(Student student, Subject subject)\n{\n if (student.Enrollments.Count >= 3)\n throw new MaxEnrollmentsExceededException();\n\n if (student.HasProfessor(subject.ProfessorId))\n throw new SameProfessorConstraintException();\n}\n\`\`\`\n\n## Consecuencias\n\n### Positivas\n- Defensa en profundidad\n- Separación de responsabilidades\n- UX mejorada (errores rápidos)\n- Seguridad garantizada (backend siempre valida)\n\n### Negativas\n- Duplicación parcial de reglas\n- Mantener sincronizadas las validaciones\n\n## Regla de Oro\n\n> **Nunca confíes en el frontend.** El backend SIEMPRE debe validar.\n\n## Referencias\n\n- [OWASP Input Validation](https://owasp.org/www-community/Input_Validation)\n`,
},
"Despliegue": {
"Manual de Despliegue": `# Manual de Despliegue\n\n## Requisitos del Sistema\n\n| Componente | Versión Mínima |\n|------------|----------------|\n| .NET SDK | 10.0 |\n| Node.js | 22.x |\n| SQL Server | 2022 |\n| Docker | 24.x |\n| Docker Compose | 2.x |\n\n## Variables de Entorno\n\n### Backend (.NET)\n\n| Variable | Descripción | Ejemplo |\n|----------|-------------|---------|\n| \`ConnectionStrings__DefaultConnection\` | Connection string SQL Server | \`Server=db;Database=StudentEnrollment;...\` |\n| \`ASPNETCORE_ENVIRONMENT\` | Ambiente | \`Production\` |\n| \`ASPNETCORE_URLS\` | URLs de escucha | \`http://+:8080\` |\n| \`JWT_SECRET_KEY\` | **REQUERIDO** - Secret JWT (mín. 32 chars) | \`your-super-secret-key-minimum-32-chars\` |\n| \`JWT_ISSUER\` | Emisor JWT | \`StudentEnrollmentApi\` |\n| \`JWT_AUDIENCE\` | Audiencia JWT | \`StudentEnrollmentApp\` |\n| \`JWT_EXPIRATION_MINUTES\` | Expiración token | \`60\` |\n\n### Frontend (Angular)\n\n| Variable | Descripción | Ejemplo |\n|----------|-------------|---------|\n| \`API_URL\` | URL del backend GraphQL | \`https://api.example.com/graphql\` |\n\n### Desarrollo Local (SQLite)\n\n| Variable | Descripción | Ejemplo |\n|----------|-------------|---------|\n| \`USE_SQLITE\` | Usar SQLite en lugar de SQL Server | \`true\` |\n| \`ConnectionStrings__DefaultConnection\` | Path a archivo SQLite | \`Data Source=./data/dev.db\` |\n\n## Despliegue con Docker\n\n### 1. Estructura de Archivos\n\n\`\`\`\ndeploy/\n└── docker/\n ├── Dockerfile.api\n ├── Dockerfile.frontend\n ├── docker-compose.yml\n └── nginx.conf\n\`\`\`\n\n### 2. Dockerfile Backend\n\n\`\`\`dockerfile\n# deploy/docker/Dockerfile.api\nFROM mcr.microsoft.com/dotnet/sdk:10.0 AS build\nWORKDIR /src\n\nCOPY src/backend/ .\nRUN dotnet restore Host/Host.csproj\nRUN dotnet publish Host/Host.csproj -c Release -o /app\n\nFROM mcr.microsoft.com/dotnet/aspnet:10.0\nWORKDIR /app\nCOPY --from=build /app .\n\n# Non-root user\nRUN adduser --disabled-password --gecos '' appuser\nUSER appuser\n\nEXPOSE 5000\nHEALTHCHECK --interval=30s --timeout=3s \\\n CMD curl -f http://localhost:5000/health || exit 1\n\nENTRYPOINT ["dotnet", "Host.dll"]\n\`\`\`\n\n### 3. Dockerfile Frontend\n\n\`\`\`dockerfile\n# deploy/docker/Dockerfile.frontend\nFROM node:22-alpine AS build\nWORKDIR /app\n\nCOPY src/frontend/package*.json ./\nRUN npm ci\n\nCOPY src/frontend/ .\nRUN npm run build -- --configuration production\n\nFROM nginx:alpine\nCOPY --from=build /app/dist/student-enrollment/browser /usr/share/nginx/html\nCOPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf\n\nEXPOSE 80\nHEALTHCHECK --interval=30s --timeout=3s \\\n CMD curl -f http://localhost/ || exit 1\n\`\`\`\n\n### 4. Nginx Configuration\n\n\`\`\`nginx\n# deploy/docker/nginx.conf\nserver {\n listen 80;\n server_name localhost;\n root /usr/share/nginx/html;\n index index.html;\n\n # Gzip\n gzip on;\n gzip_types text/plain text/css application/json application/javascript;\n\n # SPA routing\n location / {\n try_files \$uri \$uri/ /index.html;\n }\n\n # Proxy GraphQL\n location /graphql {\n proxy_pass http://api:5000/graphql;\n proxy_http_version 1.1;\n proxy_set_header Upgrade \$http_upgrade;\n proxy_set_header Connection "upgrade";\n proxy_set_header Host \$host;\n }\n\n # Security headers\n add_header X-Frame-Options "DENY" always;\n add_header X-Content-Type-Options "nosniff" always;\n add_header X-XSS-Protection "1; mode=block" always;\n}\n\`\`\`\n\n### 5. Docker Compose\n\n\`\`\`yaml\n# deploy/docker/docker-compose.yml\nservices:\n db:\n image: mcr.microsoft.com/mssql/server:2022-latest\n environment:\n - ACCEPT_EULA=Y\n - SA_PASSWORD=\${DB_PASSWORD}\n ports:\n - "1433:1433"\n volumes:\n - sqlserver-data:/var/opt/mssql\n healthcheck:\n test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P \$\${SA_PASSWORD} -Q "SELECT 1" -C\n interval: 10s\n timeout: 3s\n retries: 10\n\n
"Plan de Actividades": `# Plan de Actividades - Prueba Técnica Senior .NET/Angular\n\n## Información del Proyecto\n- **Cargo:** Desarrollador Master .NET/Angular\n- **Empresa:** Inter Rapidísimo\n- **Proyecto:** Sistema de Registro de Estudiantes\n- **Stack:** .NET 10 + GraphQL (HotChocolate) + Angular 21 + SQL Server\n\n---\n\n## Procesos de Fábrica de Software\n\n| Código | Proceso | Descripción |\n|--------|---------|-------------|\n| **AN** | Análisis | Levantamiento de requisitos, historias de usuario |\n| **DI** | Diseño | Arquitectura, modelos, prototipos UI/UX |\n| **DE** | Desarrollo | Codificación, implementación |\n| **QA** | Quality Assurance | Testing, revisión de código |\n| **DV** | DevOps | CI/CD, containerización, despliegue |\n| **DO** | Documentación | Técnica, usuario, API |\n| **SE** | Seguridad | Validaciones, autenticación, OWASP |\n\n---\n\n## Tabla de Actividades\n\n### Fase 1: Análisis y Planificación (AN)\n\n| # | Actividad | Detalle | Rol | Proceso |\n|---|-----------|---------|-----|---------|\n| 1.1 | Análisis de requisitos funcionales | Identificar y documentar los 9 requisitos del enunciado, criterios de aceptación por cada uno | Analista | AN |\n| 1.2 | Identificación de reglas de negocio | Documentar restricciones: máx 3 materias, 3 créditos/materia, 5 profesores con 2 materias c/u, restricción de profesor único | Analista | AN |\n| 1.3 | Definición de historias de usuario | Crear historias con formato "Como [rol] quiero [acción] para [beneficio]" con criterios de aceptación | Product Owner | AN |\n| 1.4 | Análisis de requisitos no funcionales | Definir: rendimiento (<200ms respuesta), seguridad (OWASP Top 10), usabilidad (responsive), mantenibilidad | Arquitecto | AN |\n| 1.5 | Identificación de riesgos técnicos | Mapear riesgos: complejidad de validaciones, integración frontend-backend, manejo de concurrencia | Líder Técnico | AN |\n\n### Fase 2: Diseño de Arquitectura (DI)\n\n| # | Actividad | Detalle | Rol | Proceso |\n|---|-----------|---------|-----|---------|\n| 2.1 | Diseño de arquitectura backend | Definir Clean Architecture: Domain, Application, Infrastructure, GraphQL. Diagrama de capas y dependencias | Arquitecto Backend | DI |\n| 2.2 | Diseño del modelo de dominio | Crear diagrama de entidades: Student, Subject, Professor, Enrollment. Definir agregados y value objects | Arquitecto Backend | DI |\n| 2.3 | Diseño de base de datos | Modelo E-R normalizado (3FN), índices, constraints, scripts DDL con integridad referencial | DBA/Arquitecto | DI |\n| 2.4 | Diseño de esquema GraphQL | Definir Types, Queries, Mutations, Inputs, Payloads. Diseñar resolvers y DataLoaders para N+1 | Arquitecto Backend | DI |\n| 2.5 | Diseño de arquitectura frontend | Definir estructura Angular: standalone components, signals, lazy loading, Apollo Client para GraphQL | Arquitecto Frontend | DI |\n| 2.6 | Diseño de componentes UI | Wireframes de pantallas: listado estudiantes, formulario inscripción, selección materias, vista compañeros | UI/UX Designer | DI |\n| 2.7 | Definición de contratos GraphQL | Schema GraphQL completo, DTOs de request/response, interfaces de servicios, contratos entre capas | Arquitecto | DI |\n| 2.8 | Diseño de estrategia de manejo de errores | Definir excepciones de dominio, error handling en GraphQL (Union types para errores), respuestas estandarizadas | Arquitecto | DI |\n\n### Fase 3: Configuración del Entorno (DV)\n\n| # | Actividad | Detalle | Rol | Proceso |\n|---|-----------|---------|-----|---------|\n| 3.1 | Inicialización del repositorio | Crear estructura de carpetas, .gitignore, README, DEV-GUIDE.md con convenciones del proyecto | DevOps | DV |\n| 3.2 | Configuración solución .NET | Crear solución con 4 proyectos (Domain, Application, Infrastructure, GraphQL), referencias entre proyectos | Backend Dev | DV |\n| 3.3 | Configuración proyecto Angular | ng new con standalone, configurar ESLint, Prettier, paths aliases, Apollo Angular para GraphQL | Frontend Dev | DV |\n| 3.4 | Configuración de base de
},
"Calidad": {
"Code Review Checklist": `# Code Review Checklist - Sistema Registro Estudiantes\n\n## Estado: Validado\n\n### Arquitectura (Clean Architecture)\n\n| Criterio | Estado | Evidencia |\n|----------|--------|-----------|\n| Separación de capas | ✅ | Domain, Application, Adapters, Host |\n| Regla de dependencia | ✅ | Domain no depende de nada externo |\n| Ports & Adapters | ✅ | IStudentRepository, IEnrollmentRepository |\n| CQRS implementado | ✅ | Commands y Queries separados |\n\n### Principios SOLID\n\n| Principio | Estado | Evidencia |\n|-----------|--------|-----------|\n| **S**ingle Responsibility | ✅ | Cada clase tiene una responsabilidad |\n| **O**pen/Closed | ✅ | Extensible via interfaces |\n| **L**iskov Substitution | ✅ | Repositorios intercambiables |\n| **I**nterface Segregation | ✅ | Interfaces específicas por entidad |\n| **D**ependency Inversion | ✅ | Inyección de dependencias |\n\n### Clean Code\n\n| Criterio | Estado | Notas |\n|----------|--------|-------|\n| Naming conventions | ✅ | PascalCase clases, camelCase variables |\n| Métodos pequeños | ✅ | < 20 líneas promedio |\n| Sin código duplicado | | DRY aplicado |\n| Comentarios mínimos | | Código autodocumentado |\n| Archivos < 100 líneas | | Refactorizado donde necesario |\n\n### Seguridad\n\n| Criterio | Estado | Ubicación |\n|----------|--------|-----------|\n| Input validation | | FluentValidation + Regex |\n| SQL Injection prevention | | EF Core parametrizado |\n| XSS prevention | | Sanitización en validators |\n| Security headers | | Program.cs middleware |\n| Rate limiting | | 100 req/min |\n| Query complexity limits | | Depth 5, complexity 100 |\n\n### Testing\n\n| Tipo | Cantidad | Cobertura |\n|------|----------|-----------|\n| Domain Tests | 30 | Entidades, ValueObjects, Services |\n| Application Tests | 66 | Commands, Queries, Validators |\n| Integration Tests | 5 | GraphQL flujo completo |\n| Angular Unit Tests | 24 | Services, Pipes |\n| E2E Tests (Playwright) | 20 | Flujos principales |\n| **Total** | **145** | |\n\n### Convenciones de Código\n\n#### Backend (.NET)\n\n- [x] Async/await en operaciones I/O\n- [x] Records para DTOs inmutables\n- [x] Nullable habilitado\n- [x] Global usings configurados\n- [x] FluentValidation para validaciones\n\n#### Frontend (Angular)\n\n- [x] Standalone components\n- [x] Signals para estado reactivo\n- [x] Lazy loading por feature\n- [x] OnPush change detection\n- [x] Apollo Client para GraphQL\n\n### GraphQL\n\n| Criterio | Estado |\n|----------|--------|\n| Types bien definidos | |\n| DataLoaders para N+1 | |\n| Error handling | |\n| Payloads con errors | |\n| Depth limiting | |\n\n### Performance\n\n| Optimización | Implementada |\n|--------------|--------------|\n| Response compression | Brotli + Gzip |\n| Output caching | 5 min para subjects/professors |\n| Apollo cache | cache-and-network |\n| Lazy loading | Por feature module |\n| Bundle optimization | < 800KB initial |\n\n### Documentación\n\n| Documento | Estado |\n|-----------|--------|\n| README.md | |\n| DEV-GUIDE.md | |\n| OWASP_CHECKLIST.md | |\n| GraphQL Schema | (Banana Cake Pop) |\n\n---\n\n## Checklist de Revisión Manual\n\n### Antes de Merge\n\n- [ ] Todos los tests pasan\n- [ ] Build sin errores ni warnings\n- [ ] Código formateado\n- [ ] Sin TODOs pendientes críticos\n- [ ] Variables de entorno documentadas\n\n### Seguridad\n\n- [ ] Sin secrets hardcodeados\n- [ ] Sin console.log en producción\n- [ ] Validaciones en frontend Y backend\n- [ ] Error messages no exponen detalles internos\n\n### UX\n\n- [ ] Loading states implementados\n- [ ] Error messages claros\n- [ ] Responsive design funcional\n- [ ] Accesibilidad básica (a11y)\n`,
"OWASP Checklist": `# OWASP Top 10 Security Checklist\n\n## Estado: Validado\n\n| # | Vulnerabilidad | Mitigación Implementada | Ubicación |\n|---|---------------|-------------------------|-----------|\n| A01 | **Broken Access Control** | No aplica (sin autenticación requerida) | N/A |\n| A02 | **Cryptographic Failures** | HTTPS forzado en producción (HSTS) | \`Program.cs:127\` |\n| A03 | **Injection** | FluentValidation + Regex sanitization, EF Core parameterized queries | \`CreateStudentValidator.cs\`, \`EnrollStudentValidator.cs\` |\n| A04 | **Insecure Design** | Clean Architecture, input validation en todas las capas | Arquitectura por capas |\n| A05 | **Security Misconfiguration** | Security headers (CSP, X-Frame-Options, etc.), Exception details disabled in prod | \`Program.cs:106-130\`, \`GraphQLExtensions.cs:53\` |\n| A06 | **Vulnerable Components** | Dependencias actualizadas (.NET 10, Angular 21) | \`*.csproj\`, \`package.json\` |\n| A07 | **Auth Failures** | No aplica (sin autenticación en este MVP) | N/A |\n| A08 | **Data Integrity Failures** | Input validation, FluentValidation, GraphQL type safety | Validators |\n| A09 | **Security Logging Failures** | Serilog structured logging, sensitive data filtering | \`appsettings.json:38-45\` |\n| A10 | **Server-Side Request Forgery** | No endpoints que acepten URLs externas | N/A |\n\n## Medidas de Seguridad Implementadas\n\n### Backend (.NET)\n\n1. **Input Validation**\n - FluentValidation con regex patterns\n - Sanitización de HTML/scripts\n - Longitud máxima de campos\n - Validación de formato email\n\n2. **Security Headers**\n - \`Content-Security-Policy\`\n - \`X-Content-Type-Options: nosniff\`\n - \`X-Frame-Options: DENY\`\n - \`Referrer-Policy: strict-origin-when-cross-origin\`\n - \`Permissions-Policy\`\n - \`Strict-Transport-Security\` (producción)\n\n3. **Rate Limiting**\n - 100 requests/minuto para queries GraphQL\n - 30 mutations/minuto\n - Queue limit para prevenir acumulación\n\n4. **GraphQL Security**\n - Query depth limit: 5 niveles\n - Query complexity limit: 100\n - Execution timeout: 30 segundos\n - Pagination max: 50 items\n\n5. **Logging Seguro**\n - Filtrado de datos sensibles (passwords, tokens)\n - Structured logging con Serilog\n - Rotación de logs (7 días)\n\n### Frontend (Angular)\n\n1. **XSS Prevention**\n - Angular sanitization por defecto\n - Content Security Policy\n\n2. **CSRF Protection**\n - No cookies de sesión (stateless GraphQL)\n\n3. **Secure Communication**\n - Solo HTTPS en producción\n - GraphQL sobre HTTPS\n\n## Pruebas de Seguridad Recomendadas\n\n\`\`\`bash\n# Test security headers\ncurl -I http://localhost:5000/graphql\n\n# Test rate limiting (debe retornar 429 después de 100 requests)\nfor i in {1..150}; do curl -s -o /dev/null -w "%{http_code}\\n" http://localhost:5000/graphql; done\n\n# Test query depth (debe fallar con depth > 5)\ncurl -X POST http://localhost:5000/graphql \\\n -H "Content-Type: application/json" \\\n -d '{"query":"{ students { enrollments { subject { professor { subjects { name } } } } } }"}'\n\`\`\`\n`,
"Recomendaciones": `# Recomendaciones Finales\n\n**Fecha:** 2026-01-08\n**Proyecto:** Sistema de Inscripción de Estudiantes\n**Versión:** 1.0\n\n---\n\n## Resumen del Estado Actual\n\nEl sistema cumple con todos los requisitos funcionales de la prueba técnica:\n\n| Requisito | Estado |\n|-----------|--------|\n| CRUD de estudiantes | ✅ Implementado |\n| Programa de créditos (10 materias, 3 créditos c/u) | ✅ Implementado |\n| Máximo 3 materias por estudiante | ✅ Implementado |\n| 5 profesores con 2 materias c/u | ✅ Implementado |\n| Restricción de mismo profesor | ✅ Implementado |\n| Ver compañeros de clase (solo nombres) | ✅ Implementado |\n| Autenticación y autorización | ✅ Implementado |\n| Recuperación de contraseña | ✅ Implementado |\n\n---\n\n## Recomendaciones Técnicas\n\n### 1. Seguridad\n\n#### Alta Prioridad\n- **Rate Limiting:** Implementar limitación de solicitudes en endpoints de autenticación para prevenir ataques de fuerza bruta.\n- **Refresh Tokens:** Actualmente solo se usa un token JWT. Implementar refresh tokens para mejor seguridad.\n- **Logging de Auditoría:** Agregar logs para acciones sensibles (login fallido, cambio de contraseña, etc.).\n\n#### Media Prioridad\n- **CORS Restrictivo:** Revisar configuración de CORS para producción (actualmente permite localhost).\n- **Helmet Headers:** Agregar headers de seguridad HTTP en producción.\n\n### 2. Rendimiento\n\n#### Alta Prioridad\n- **Paginación:** La query \`students\` debería usar paginación para escalabilidad.\n- **DataLoaders:** Ya implementados, pero verificar N+1 queries en GraphQL.\n\n#### Media Prioridad\n- **Caché de Apollo:** Optimizar políticas de caché en frontend para reducir llamadas al servidor.\n- **Compression:** Habilitar Brotli/gzip en nginx para assets estáticos.\n\n### 3. Calidad de Código\n\n#### Alta Prioridad\n- **Tests E2E:** Los tests de Playwright existen pero deben ejecutarse en CI/CD.\n- **Cobertura de Tests:** Aumentar cobertura en Domain y Application layers.\n\n#### Media Prioridad\n- **Error Handling Centralizado:** Crear interceptor global para manejo de errores GraphQL.\n- **Typing Estricto:** Generar tipos TypeScript desde el schema GraphQL automáticamente.\n\n### 4. DevOps\n\n#### Alta Prioridad\n- **Health Checks:** Mejorar endpoint \`/health\` para incluir dependencias externas.\n- **Secrets Management:** No hardcodear credenciales en manifiestos de k8s (usar Sealed Secrets o Vault).\n\n#### Media Prioridad\n- **Monitoring:** Agregar métricas con Prometheus y dashboards en Grafana.\n- **Logging Centralizado:** Configurar stack ELK o Loki para logs.\n\n---\n\n## Mejoras Funcionales Sugeridas\n\n### Corto Plazo (Sprint actual)\n1. **Confirmación de Cancelación:** Agregar diálogo de confirmación antes de desinscribir materia.\n2. **Notificaciones Push:** Informar a estudiantes cuando un compañero se inscribe en su clase.\n3. **Validación de Email:** Agregar validación de formato de email en frontend.\n\n### Mediano Plazo (2-4 sprints)\n1. **Horarios:** Agregar horarios a materias para evitar conflictos.\n2. **Waitlist:** Implementar lista de espera para materias muy demandadas.\n3. **Reportes:** Dashboard administrativo con métricas de inscripciones.\n\n### Largo Plazo (Roadmap)\n1. **Multi-tenant:** Soporte para múltiples instituciones.\n2. **Integración LMS:** Conectar con sistemas de gestión de aprendizaje.\n3. **App Mobile:** Versión móvil nativa con Flutter/React Native.\n\n---\n\n## Arquitectura\n\n### Fortalezas Actuales\n- **Clean Architecture:** Separación clara de capas (Domain, Application, Adapters).\n- **CQRS:** Comandos y queries bien separados con MediatR.\n- **GraphQL:** API flexible con HotChocolate.\n- **Angular Signals:** Estado reactivo moderno y eficiente.\n\n### Áreas de Mejora\n1. **Event Sourcing:** Considerar para auditoría completa de inscripciones.\n2. **SAGA Pattern:** Para operaciones distribuidas (si se escala a microservicios).\n3. **API Gateway:** Si se agregan más servicios, usar Kong o Traefik.\n\n---\n\n##
"Defectos QA": `# Reporte de Pruebas Manuales QA\n\n**Fecha:** 2026-01-08\n**Tester:** QA Team\n**Ambiente:** Desarrollo Local (localhost:4200 / localhost:5000)\n\n---\n\n## Resumen Ejecutivo\n\n| Categoría | Total | Pasaron | Fallaron | Corregidos |\n|-----------|-------|---------|----------|------------|\n| CRUD Estudiantes | 4 | 4 | 0 | 1 |\n| Inscripciones | 5 | 5 | 0 | 0 |\n| Compañeros | 2 | 2 | 0 | 0 |\n| **Total** | **11** | **11** | **0** | **1** |\n\n---\n\n## Capturas de Pantalla\n\n| # | Archivo | Descripción |\n|---|---------|-------------|\n| 01 | \`01-inicio-cargando.png\` | Página inicial cargando |\n| 02 | \`02-formulario-nuevo-estudiante.png\` | Formulario de nuevo estudiante |\n| 03 | \`03-formulario-lleno.png\` | Formulario con datos |\n| 04 | \`04-estudiante-creado.png\` | Estudiante creado exitosamente |\n| 05 | \`05-DEFECTO-editar-estudiante-no-encontrado.png\` | **DEFECTO: Error al editar** |\n| 06 | \`06-pagina-inscripcion.png\` | Página de inscripción |\n| 07 | \`07-inscripcion-exitosa-regla-profesor.png\` | Regla de profesor funcionando |\n| 08 | \`08-maximo-3-materias.png\` | Límite de materias visible |\n| 09 | \`09-creditos-maximos-9-9.png\` | 9/9 créditos alcanzados |\n| 10 | \`10-dos-estudiantes.png\` | Lista con dos estudiantes |\n| 11 | \`11-companeros-de-clase.png\` | Vista de compañeros |\n| 12 | \`12-confirmar-eliminacion.png\` | Diálogo de confirmación |\n| 13 | \`13-estudiante-eliminado.png\` | Estudiante eliminado |\n| 14 | \`14-CORREGIDO-editar-estudiante-funciona.png\` | **CORREGIDO: Editar funciona** |\n\n---\n\n## Defectos Encontrados\n\n### DEFECTO #1: Error "Estudiante no encontrado" al editar\n\n**ID:** DEF-001\n**Severidad:** Alta\n**Prioridad:** P1\n**Estado:** RESUELTO\n\n#### Descripcion\nAl hacer clic en el boton de editar (icono de lapiz) en la lista de estudiantes, aparece un snackbar con el mensaje "Estudiante no encontrado" en lugar de abrir el formulario de edicion.\n\n#### Pasos para Reproducir\n1. Ir a http://localhost:4200/students\n2. Crear un nuevo estudiante\n3. En la tabla, hacer clic en el boton de editar (icono de lapiz)\n4. **Resultado (antes):** Aparecia mensaje "Estudiante no encontrado"\n5. **Resultado (despues del fix):** El formulario de edicion abre correctamente\n\n#### Capturas de Pantalla\n| Estado | Captura |\n|--------|---------|\n| Antes | ![Defecto](../.playwright-mcp/05-DEFECTO-editar-estudiante-no-encontrado.png) |\n| Despues | ![Corregido](../.playwright-mcp/14-CORREGIDO-editar-estudiante-funciona.png) |\n\n#### Causa Raiz Identificada\nSe encontraron **DOS problemas** combinados:\n\n1. **Problema de navegacion:** El boton de editar usaba \`<button [routerLink]>\` pero en Angular el \`routerLink\` funciona mejor con elementos \`<a>\`.\n\n2. **Problema de timing con signals:** El componente \`StudentFormComponent\` usaba \`ngOnInit\` para leer el parametro de ruta \`id\` via \`input()\` signal, pero el valor no estaba disponible en ese momento del ciclo de vida.\n\n#### Archivos Modificados\n- \`student-list.component.ts\`: Cambio \`<button>\` por \`<a>\` para el boton de editar\n- \`student-form.component.ts\`: Cambio \`ngOnInit\` por \`effect()\` para reaccionar cuando el input signal este disponible\n\n---\n\n## Analisis de Soluciones - DEFECTO #1\n\n### Soluciones Propuestas\n\n| Solucion | Descripcion | Ventajas | Desventajas |\n|----------|-------------|----------|-------------|\n| A | Verificar navegacion y parametros de ruta | Simple si el problema es routing | No resuelve si es backend |\n| B | Revisar servicio de estudiantes | Identifica problemas en servicios | Requiere debugging profundo |\n| C | Agregar logs de diagnostico | Ayuda a identificar donde falla | Es temporal |\n| D | Usar modal en lugar de navegacion | Evita problemas de nav, mejor UX | Cambio arquitectonico grande |\n\n### SOLUCION IMPLEMENTADA\n\nSe implemento una **combinacion de A + investigacion profunda** que revelo dos problemas:\n\n#### Fix 1: Cambiar \`<button>\` por \`<a>\` en student-list.component.ts\n\n\`\`\`typescript\n// ANTES
},
};
// ═══════════════════════════════════════════════════════════
// APP LOGIC
// ═══════════════════════════════════════════════════════════
marked.setOptions({ gfm: true, breaks: true });
// Build navigation
function buildNav() {
const nav = document.getElementById('nav');
let html = '';
// Add diagrams section first
html += `
<div class="nav-group open" data-group="Diagramas">
<div class="nav-group-title" onclick="toggleGroup(this.parentElement)">
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18l6-6-6-6"/>
</svg>
Diagramas UML
</div>
<div class="nav-list">
<div class="nav-item" data-key="diagrams:all" onclick="showDiagrams()">Ver todos los diagramas</div>
</div>
</div>
`;
// Add document sections
for (const [section, items] of Object.entries(docs)) {
if (typeof items !== 'object') continue;
html += `
<div class="nav-group" data-group="${section}">
<div class="nav-group-title" onclick="toggleGroup(this.parentElement)">
<svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M9 18l6-6-6-6"/>
</svg>
${section}
</div>
<div class="nav-list">
`;
for (const name of Object.keys(items)) {
html += `<div class="nav-item" data-key="${section}|${name}" onclick="loadDoc('${section}', '${name}', this)">${name}</div>`;
}
html += `</div></div>`;
}
nav.innerHTML = html;
}
function toggleGroup(el) {
el.classList.toggle('open');
}
// Show all diagrams in a gallery
function showDiagrams() {
const content = document.getElementById('content');
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.querySelector('[data-key="diagrams:all"]')?.classList.add('active');
let html = `
<h1>Diagramas de Arquitectura</h1>
<p>Diagramas UML que documentan la arquitectura y diseño del sistema.</p>
<div class="diagrams-grid">
`;
for (const [key, src] of Object.entries(diagrams)) {
if (!src) continue;
const name = diagramNames[key] || key;
html += `
<div class="diagram-card" onclick="showSingleDiagram('${key}')">
<img src="${src}" alt="${name}" loading="lazy">
<div class="diagram-title">${name}</div>
</div>
`;
}
html += '</div>';
content.innerHTML = html;
window.scrollTo(0, 0);
window.location.hash = 'diagrams';
}
// Show single diagram in lightbox (full size)
function showSingleDiagram(key) {
openLightbox(key);
}
// Load document
function loadDoc(section, name, navItem) {
const content = document.getElementById('content');
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
navItem?.classList.add('active');
try {
let markdown = docs[section]?.[name];
if (!markdown) throw new Error('Documento no encontrado');
// Replace SVG links with embedded images
markdown = markdown.replace(
/\[([^\]]+)\]\(docs\/architecture\/diagrams\/(\d+-[^)]+\.svg)\)/g,
(match, text, file) => {
const key = file.replace('.svg', '');
if (diagrams[key]) {
return `![${text}](${diagrams[key]})`;
}
return match;
}
);
content.innerHTML = marked.parse(markdown);
window.scrollTo(0, 0);
window.location.hash = encodeURIComponent(`${section}|${name}`);
} catch (error) {
content.innerHTML = `<div class="welcome"><h1>Error</h1><p>${error.message}</p></div>`;
}
}
// Search
document.getElementById('search').addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
document.querySelectorAll('.nav-item').forEach(item => {
const matches = item.textContent.toLowerCase().includes(query);
item.classList.toggle('hidden', !matches && query);
});
document.querySelectorAll('.nav-group').forEach(group => {
const hasVisible = group.querySelector('.nav-item:not(.hidden)');
group.classList.toggle('hidden', !hasVisible && query);
if (hasVisible && query) group.classList.add('open');
});
});
// Init
buildNav();
// Load from hash
if (window.location.hash) {
const hash = decodeURIComponent(window.location.hash.slice(1));
if (hash === 'diagrams') {
showDiagrams();
} else {
const [section, name] = hash.split('|');
if (docs[section]?.[name]) {
const navItem = document.querySelector(`[data-key="${hash}"]`);
navItem?.closest('.nav-group')?.classList.add('open');
loadDoc(section, name, navItem);
}
}
}
</script>
</body>
</html>