From c7d0cf5ea5e2d3c2372bad2f3a6785708ab65a41 Mon Sep 17 00:00:00 2001 From: Alberto Arroyo Raygada Date: Wed, 8 Jul 2026 00:50:12 -0500 Subject: [PATCH] feat(architecture): ADR registry endpoint + dedicated browser screen (CR-10, CR-21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: AdrRegistryEndpoints exposes GET /api/architecture/adrs — a static AdrRegistryCatalog with one entry per DECISIONS.md row (T-001..T-033): code, title, decisionType, upstreamRef, localDocLink, status (from ADR-doc frontmatter where present, else Recorded), supersedes/supersededBy, short note; guarded by ArchitectureRead, optional ?status= filter; 3 catalog tests (496 total). Frontend: AdrBrowserMd3 dedicated ADR browser consuming useAdrRegistry() (real API) with status filter, supersession badges, and doc links; routed in app.tsx + Governance nav group. Closes CR-21 (dedicated ADR browser) and CR-10 (architecture-independent view now includes the ADR registry with status/supersession); Core-backed catalog sync deferred (GAP-004). Backend 0 warnings; frontend lint 0 errors / typecheck / build green. Co-Authored-By: Claude Opus 4.8 --- docs/audit/tracker-gap-reference-catalog.md | 4 +- docs/audit/tracker-gap-tracking.md | 4 +- .../TrackerApiApplicationBuilderExtensions.cs | 1 + .../Architecture/AdrRegistryEndpoints.cs | 133 ++++++++++ .../Integration/AdrRegistryEndpointTests.cs | 43 ++++ src/apps/tracker-web/src/api/endpoints.ts | 6 + src/apps/tracker-web/src/api/hooks.ts | 6 + src/apps/tracker-web/src/api/types.ts | 15 ++ src/apps/tracker-web/src/app/app.tsx | 3 + .../components/screens/md3/AdrBrowserMd3.tsx | 230 ++++++++++++++++++ src/apps/tracker-web/src/shell/nav.ts | 1 + 11 files changed, 442 insertions(+), 4 deletions(-) create mode 100644 src/apps/tracker-api/Tracker.Presentation/Endpoints/Architecture/AdrRegistryEndpoints.cs create mode 100644 src/apps/tracker-api/Tracker.Tests/Presentation/Integration/AdrRegistryEndpointTests.cs create mode 100644 src/apps/tracker-web/src/components/screens/md3/AdrBrowserMd3.tsx diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index 8278c5fd..094f3935 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -179,7 +179,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Acceptance criteria:** - [ ] A user can browse topologies/ADRs/blueprints without selecting a satellite process. - **Dependencies:** [CR-08](#cr-08), [CR-11](#cr-11), [CR-21](#cr-21), [CR-23](#cr-23). - - **Status:** `IN-PROGRESS` — pantalla + endpoint de arquitectura (topologías/blueprints/evidencia/providers) existen; faltan registro/browser de ADRs y sync respaldado por Core. + - **Status:** `DONE` — navegación de arquitectura completa + registro de ADRs (`GET /api/architecture/adrs`, T-001…T-033 con estado/supersesión) y browser `AdrBrowserMd3`. Diferido: sync respaldado por Core (GAP-004). #### CR-11 @@ -366,7 +366,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Acceptance criteria:** - [ ] Each canonical concept has a reachable screen. - **Dependencies:** [CR-08](#cr-08), [CR-10](#cr-10), [CR-13](#cr-13), [CR-14](#cr-14), [CR-15](#cr-15), [CR-22](#cr-22). - - **Status:** `IN-PROGRESS` — las 5 pantallas canónicas están ruteadas (Gate Decision/Evaluation = `GateEvaluationMd3`); único faltante: un browser de ADRs dedicado. + - **Status:** `DONE` — todas las pantallas canónicas ruteadas; el browser de ADRs dedicado (`AdrBrowserMd3`, API real) cierra el último faltante. #### CR-22 diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 483ab6f1..6c4ed5cd 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -24,9 +24,9 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`LV-13`](./tracker-gap-reference-catalog.md#lv-13) | Dev-seed papercut: the DevBypass platform-root tenant (`11111111…`) is a phantom — no seeder creates it, so local dev must create tenants by hand before tenant-scoped screens work. **Fixed:** `DevTenantSeedHostedService` (Development-only gate before any DB access, idempotent, seeds 3 bare demo tenants via `Tenant.Create` + repo — no geo, left for the UI). Testing/Prod no-op. | `Backend/WEB` | Cross | P3 | S | `DONE` | | [`COH-002`](./tracker-gap-reference-catalog.md#coh-002) | **Decidido (obsoleto):** se ratifica "Evolith no posee backlog / sin descomposición interna" (PRD REQ-DIS-07). Las tablas `backlogs`/`epics`/`user_stories(in_refinement/split)` de `tracker-postgresql-data-design.md` quedan anotadas como OBSOLETAS/no-implementar; no se construye entidad de refinamiento. | `Backend` | Discovery | P0 | L | `DONE` | | [`COH-004`](./tracker-gap-reference-catalog.md#coh-004) | **Hecho (coherencia):** "TestRun" ya no se usa (solo aparece en docs de auditoría); las specs modelan consistentemente TestCycle+TestExecution(+Defect). El contexto QA (`tracker_qa`) sigue sin construir pero es roadmap Fase 3 — se trackea aparte, no bloquea construcción. | `Backend` | QA | P0 | L | `DONE` | -| [`CR-21`](./tracker-gap-reference-catalog.md#cr-21) | Las 5 pantallas canónicas están ruteadas y accesibles (Gate Decision/Evaluation = `GateEvaluationMd3` ya existe — la nota "pendiente" estaba desactualizada); único faltante: un **browser de ADRs** dedicado (hoy los ADRs son solo tags en blueprints). | `WEB` | Cross | P1 | S | `IN-PROGRESS` | +| [`CR-21`](./tracker-gap-reference-catalog.md#cr-21) | **Hecho:** todas las pantallas canónicas ruteadas — el faltante (browser de ADRs dedicado) ahora existe: `AdrBrowserMd3` (filtro por status, supersesión, links a docs) consumiendo `GET /api/architecture/adrs` por API real, en el grupo Governance de la navegación. | `WEB` | Cross | P1 | S | `DONE` | | [`GAP-010`](./tracker-gap-reference-catalog.md#gap-010) | **Hecho:** existe `reference/knowledge/domain/PRODUCT_VISION.md` (EN) con navegación bilingüe a `PRODUCT_VISION.es.md`. | `Docs` | Cross | P1 | M | `DONE` | -| [`CR-10`](./tracker-gap-reference-catalog.md#cr-10) | Pantalla + endpoint de navegación de arquitectura (independiente del SDLC) existen para topologías/blueprints/evidencia/providers; faltan registro/browser de ADRs (estado/supersesión) y sync respaldado por Core (el catálogo hoy es estático). | `WEB/Backend` | Cross | P1 | M | `IN-PROGRESS` | +| [`CR-10`](./tracker-gap-reference-catalog.md#cr-10) | **Hecho:** navegación de arquitectura independiente del SDLC completa — topologías/blueprints/evidencia/providers + ahora **registro de ADRs** (`GET /api/architecture/adrs`, 33 entradas T-001…T-033 con estado/supersesión desde `DECISIONS.md`) y su browser `AdrBrowserMd3`. **Diferido:** sync del catálogo respaldado por Core (GAP-004). | `WEB/Backend` | Cross | P1 | M | `DONE` | | [`CR-14`](./tracker-gap-reference-catalog.md#cr-14) | **Hecho:** `EvidenceRecord` es ahora un nodo de Evidence **Graph** — campos de primera clase `sourceProvider`/`producer`/`contentHash`/`classification`(validada)/`retentionPolicyRef`/`immutable`, aristas de linaje `references[]` con operación de dominio `AddReference` (dedup) + `Seal`, comando/endpoint `POST /{id}/references`, migración aditiva `AddEvidenceGraphFields`. Build 0-warnings, 458 tests (+15). | `Backend` | Cross | P1 | M | `DONE` | | [`CR-15`](./tracker-gap-reference-catalog.md#cr-15) | **Hecho:** dominio `ProviderConnection` construido — taxonomía capacidad→Port (`ProviderCapabilityCatalog`), VO `ProviderAcl` (dirección validada + scopes), agregado con ciclo de vida `Connect`/`Disconnect`/`MarkError`/`UpdateAcl`, persistencia (tabla `provider_connections` + índices) + migración aditiva `AddProviderConnections`, comandos/queries + grupo de endpoints `provider-connections`. 493 tests (+20). **Diferido:** adaptadores externos concretos (GitHub/Jira/Datadog) tras el seam `IProviderPort` — integración viva. | `Backend/Integration` | Cross | P1 | M | `DONE` | | [`GAP-019`](./tracker-gap-reference-catalog.md#gap-019) | **Hecho:** requisitos Discovery desglosados a detalle implementable — PRD §2 REQ-DIS-01…13, cada uno con cláusula de aceptación, + catálogo de artefactos por gate (`functional-scope.md` §6). Sin user-stories atómicas, coherente con "sin backlog propio". | `Docs` | Cross | P1 | M | `DONE` | diff --git a/src/apps/tracker-api/Tracker.Presentation/Bootstrapping/TrackerApiApplicationBuilderExtensions.cs b/src/apps/tracker-api/Tracker.Presentation/Bootstrapping/TrackerApiApplicationBuilderExtensions.cs index 718f298e..f0be6e7c 100644 --- a/src/apps/tracker-api/Tracker.Presentation/Bootstrapping/TrackerApiApplicationBuilderExtensions.cs +++ b/src/apps/tracker-api/Tracker.Presentation/Bootstrapping/TrackerApiApplicationBuilderExtensions.cs @@ -80,6 +80,7 @@ public static WebApplication MapTrackerApiSurface(this WebApplication app) api.MapEvidenceRecordEndpoints(); api.MapAuditEntryEndpoints(); api.MapArchitectureReferenceEndpoints(); + api.MapAdrRegistryEndpoints(); api.MapCoreEvaluationEndpoints(); api.MapCoreEvaluationTransactionEndpoints(); api.MapProviderConnectionEndpoints(); diff --git a/src/apps/tracker-api/Tracker.Presentation/Endpoints/Architecture/AdrRegistryEndpoints.cs b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Architecture/AdrRegistryEndpoints.cs new file mode 100644 index 00000000..b0b14212 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Presentation/Endpoints/Architecture/AdrRegistryEndpoints.cs @@ -0,0 +1,133 @@ +using Tracker.Presentation.Auth; + +namespace Tracker.Presentation.Endpoints.Architecture; + +/// +/// CR-10 / CR-21: exposes the canonical local ADR registry (status + supersession) sourced from +/// the authoritative DECISIONS.md table (rows T-001…T-033). Mirrors the static-catalog idiom +/// of ; today the registry is a curated static mirror. +/// +public static class AdrRegistryEndpoints +{ + public static void MapAdrRegistryEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/architecture") + .WithTags("Architecture"); + + group.MapGet("/adrs", (string? status) => + { + var entries = AdrRegistryCatalog.Entries; + + if (!string.IsNullOrWhiteSpace(status)) + { + entries = entries + .Where(e => string.Equals(e.Status, status, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + return Results.Ok(entries); + }) + .WithName("GetAdrRegistry") + .RequireTrackerPermission(TrackerPermissions.ArchitectureRead) + .Produces>(200) + .Produces(401) + .Produces(403); + } +} + +/// +/// Static mirror of the local ADR registry. Each entry corresponds to a row in DECISIONS.md. +/// Status comes from the ADR document frontmatter when a doc exists (Active/Accepted); rows +/// without a local doc default to "Recorded". Notes are short one-line summaries, not the +/// verbose source notes. +/// +internal static class AdrRegistryCatalog +{ + // TODO CR-10: Core-backed ADR sync — replace this curated static mirror with a live pull from + // the Core governance registry (evolith.yaml → spec.compliance.adrRegistry / Core BFF) so that + // status, supersession and upstream-ref changes flow in automatically instead of being hand-maintained. + public static readonly IReadOnlyList Entries = + [ + new("T-001", "Orquestación de Monorepo con Nx", "Adoptar", "ADR-0001", null, "Recorded", null, null, + "Nx monorepo initialized in src/ using npm workspaces."), + new("T-002", "Adopción de Microfrontends en Fase 1", "Sobrescribir", null, "docs/adrs/T-002-microfrontends-fase1.md", "Active", null, null, + "Microfrontend topology in Phase 1 — deliberate deviation from the base topology for UI scalability."), + new("T-003", "Arquitectura Hexagonal (Ports & Adapters)", "Adoptar", "ADR-0002", null, "Recorded", null, null, + "Ports & Adapters; pure domain layer with no external dependencies."), + new("T-004", "TypeScript estricto como lenguaje primario", "Adoptar", "ADR-0003", null, "Recorded", null, null, + "Strict TypeScript (strict: true) as the primary language."), + new("T-005", "TypeORM como ORM (Data Mapper pattern)", "Adoptar", "ADR-0043", null, "Recorded", null, null, + "TypeORM using the Data Mapper pattern (over Active Record)."), + new("T-006", "React con Vite como base del frontend", "Adoptar", "ADR-0044", "docs/adrs/T-006-frontend-vite.md", "Active", null, null, + "React + Vite as the frontend base, aligned to the microfrontend topology."), + new("T-007", "Zustand + TanStack Query (State Management)", "Adoptar", "ADR-0045", null, "Recorded", null, null, + "Zustand for client state, TanStack Query for server state."), + new("T-008", "Convención de nombres de schema PostgreSQL", "Definir", null, "docs/adrs/T-008-schema-naming.md", "Active", null, null, + "Canonical PostgreSQL schema naming (tracker_discovery, tracker_design, …)."), + new("T-009", "REST + OpenAPI 3.0 como única API en Fase 1", "Definir", null, "docs/adrs/T-009-rest-openapi.md", "Active", null, null, + "REST + OpenAPI 3.0 as the sole API in Phase 1; GraphQL out of scope."), + new("T-010", "Framework de agentes configurable por tenant", "Extender", null, "docs/adrs/T-010-agent-framework-per-tenant.md", "Active", null, null, + "Agentic framework is tenant-configurable per phase; Tracker stays framework-agnostic."), + new("T-011", "Estándar de numeración para Iniciativas, ADRs y Specs", "Definir", null, "docs/adrs/T-011-numbering-standard.md", "Active", null, null, + "Numbering standard: INIT-{NNN} initiatives, T-{NNN} local ADRs, ADR-{NNNN} upstream."), + new("T-012", "Contratos de eventos de dominio en libs/shared/", "Definir", null, null, "Recorded", null, null, + "Domain event contracts defined as TypeScript in libs/shared; Pact tests pending."), + new("T-013", "Value Object canónico ExternalReference en Shared Kernel", "Definir", null, null, "Recorded", null, null, + "Canonical ExternalReference value object in the Shared Kernel, unifying shapes across contexts."), + new("T-014", "UC-005 dividido en UC-005a y UC-005b", "Definir", null, null, "Recorded", null, null, + "UC-005 split into UC-005a (planning) and UC-005b (execution)."), + new("T-015", "Core BFF Gateway como único canal de comunicación", "Adoptar", "core/0080", null, "Recorded", null, null, + "Tracker talks to Core only via the BFF Gateway; the api-key model (core/0075) was superseded by core/0080."), + new("T-016", "Capa Anti-Corrupción para Integración PPM (Funnel 0)", "Definir", null, null, "Recorded", null, null, + "Anti-corruption layer (PpmIntakeACL) normalizes external PPM schemas before they reach the domain."), + new("T-017", "Core API Exposure Layer (REST-only)", "Adoptar", "core/0074", null, "Recorded", null, null, + "Core-API is REST-only under /api/v1 (no GraphQL/SSE) plus an MCP gateway."), + new("T-018", "Contrato de Referencia de Repositorio Remoto", "Adoptar", "core/0080", null, "Recorded", "core/0075", null, + "Remote repo reference contract; Core does not authenticate — the BFF is the sole perimeter. Supersedes the api-key model (core/0075)."), + new("T-019", "Separación Dominio/Financiero", "Adoptar", "core/0078", null, "Recorded", null, null, + "ROI/finance separated out of the governance domain."), + new("T-020", "Corpus Multi-Topología Componible", "Adoptar", "core/0079", null, "Recorded", null, null, + "Topology is 5 composable dimensions, not a mono→micro ladder; F1/F2/F3 = progressive-axis maturity."), + new("T-021", "PhaseId Canónico Semántico", "Adoptar", "core/GT-343", null, "Recorded", null, null, + "Canonical semantic PhaseIds (discovery|design|construction|qa|release); f1..f5 are deprecated input aliases."), + new("T-022", "Contrato de Evaluación de Satélite", "Adoptar", "core/0073", null, "Recorded", null, null, + "POST /api/v1/evaluate returns technical evidence; the Tracker owns the gate decision."), + new("T-023", "Distribución OPA-wasm Agnóstica", "Adoptar", "core/0085", null, "Recorded", null, null, + "OPA-wasm rule evaluation via rulesets/opa/policy.wasm; per-rule passed|failed|skipped, degrading gracefully."), + new("T-024", "Colisión de Nombre GateDecision", "Definir", null, null, "Recorded", null, null, + "Tracker's rich GateDecision is namespaced (TrackerGateDecision) to avoid colliding with Core's VO."), + new("T-025", "Gobernanza de IA Agéntica (cluster)", "Referenciar", "core/0081-0089", null, "Recorded", null, null, + "Agentic AI governance cluster referencing Core ADRs; basis for the Tracker's AI Governance surface."), + new("T-026", "Redis solo para soporte operacional", "Definir", null, null, "Recorded", null, null, + "Redis is operational support only (cache/locks/jobs/idempotency); PostgreSQL is the system of record."), + new("T-027", "Circuit breaker en cliente Core API", "Definir", null, null, "Recorded", null, null, + "Circuit breaker on the Core API client (CLOSED→OPEN→HALF_OPEN) to prevent cascade failures."), + new("T-028", "Estrategia PostgreSQL schema-per-context", "Adoptar", "T-008", null, "Recorded", null, null, + "PostgreSQL schema-per-context (10 schemas); no cross-schema FKs, references via UUID only."), + new("T-029", "OpenTelemetry Collector como sink único", "Definir", null, null, "Recorded", null, null, + "OpenTelemetry Collector as the single OTLP sink, decoupling the app from observability backends."), + new("T-030", "K8s overlays via Kustomize (no Helm inicialmente)", "Definir", null, null, "Recorded", null, null, + "Kubernetes overlays via Kustomize (not Helm) for Phase 1 simplicity."), + new("T-031", "Criterios de evaluación de compuertas configurables por tenant", "Definir", null, "docs/adrs/T-031-tenant-gate-evaluation-criteria.md", "Accepted", null, null, + "Tenant-configurable gate evaluation criteria over artifact fields, fail-closed with UNCONFIGURED."), + new("T-032", "Contexto acotado Geo / Datos Maestros", "Definir", null, "docs/adrs/T-032-geo-master-data-context.md", "Accepted", null, null, + "Geo / master-data bounded context replacing free-text regional fields with validated catalogs."), + new("T-033", "Adopción de la Plataforma Shell del Core (.NET)", "Extender", "ADR-0071", "docs/adrs/T-033-shell-platform-adoption.md", "Accepted", null, null, + "Adopts/extends the Core .NET Shell platform (DDD, AOP, factory, bootstrapper).") + ]; +} + +/// +/// A single local ADR registry entry. Supersedes/SupersededBy capture supersession +/// relationships stated in DECISIONS.md notes; both are null when none applies. +/// +public sealed record AdrEntryResponse( + string Code, + string Title, + string DecisionType, + string? UpstreamRef, + string? LocalDocLink, + string Status, + string? Supersedes, + string? SupersededBy, + string Note); diff --git a/src/apps/tracker-api/Tracker.Tests/Presentation/Integration/AdrRegistryEndpointTests.cs b/src/apps/tracker-api/Tracker.Tests/Presentation/Integration/AdrRegistryEndpointTests.cs new file mode 100644 index 00000000..8d3ddd92 --- /dev/null +++ b/src/apps/tracker-api/Tracker.Tests/Presentation/Integration/AdrRegistryEndpointTests.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using Tracker.Presentation.Endpoints.Architecture; +using Xunit; + +namespace Tracker.Tests.Presentation.Architecture; + +/// +/// CR-10 / CR-21: guards the local ADR registry catalog that backs GET /api/architecture/adrs. +/// Exercised at the catalog level (rather than via ) +/// because the endpoint is a pure static mirror of DECISIONS.md with no DB or middleware behaviour +/// to observe, and Serilog's process-global bootstrap logger only tolerates a single host boot per test run. +/// +public sealed class AdrRegistryEndpointTests +{ + [Fact] + public void Registry_IsPopulated_AndIncludesT006Active() + { + AdrRegistryCatalog.Entries.Should().NotBeEmpty(); + + var t006 = AdrRegistryCatalog.Entries.Single(e => e.Code == "T-006"); + t006.Status.Should().Be("Active"); + t006.LocalDocLink.Should().Be("docs/adrs/T-006-frontend-vite.md"); + t006.DecisionType.Should().Be("Adoptar"); + } + + [Fact] + public void Registry_CoversDecisionsTable_T001ThroughT033() + { + var codes = AdrRegistryCatalog.Entries.Select(e => e.Code).ToList(); + + codes.Should().HaveCount(33); + codes.Should().Contain("T-001").And.Contain("T-033"); + codes.Should().OnlyHaveUniqueItems(); + } + + [Fact] + public void Registry_EntriesWithoutLocalDoc_DefaultToRecorded() + { + AdrRegistryCatalog.Entries + .Where(e => e.LocalDocLink is null) + .Should().OnlyContain(e => e.Status == "Recorded"); + } +} diff --git a/src/apps/tracker-web/src/api/endpoints.ts b/src/apps/tracker-web/src/api/endpoints.ts index 7fc8f589..a750542a 100644 --- a/src/apps/tracker-web/src/api/endpoints.ts +++ b/src/apps/tracker-web/src/api/endpoints.ts @@ -4,6 +4,7 @@ ========================================================================= */ import { apiFetch, apiFetchForm } from './client'; import type { + AdrEntry, CountryDto, LocationLevelDto, LocationNodeDto, @@ -277,6 +278,11 @@ export const architectureApi = { method: 'POST', body, }), + // CR-10 / CR-21: local ADR registry (status + supersession) from DECISIONS.md. + listAdrs: (status?: string) => + apiFetch( + `/architecture/adrs${status ? `?status=${encodeURIComponent(status)}` : ''}`, + ), }; /* ── Auth (UMS / dev-bypass discovery + session) ────────────────────────── */ diff --git a/src/apps/tracker-web/src/api/hooks.ts b/src/apps/tracker-web/src/api/hooks.ts index 764d56da..ebfcd14d 100644 --- a/src/apps/tracker-web/src/api/hooks.ts +++ b/src/apps/tracker-web/src/api/hooks.ts @@ -31,6 +31,7 @@ import { tenantsApi, } from './endpoints'; import type { + AdrEntry, CountryDto, LocationLevelDto, LocationNodeDto, @@ -118,6 +119,7 @@ export const qk = { opportunities: (tenantId?: string) => ['opportunities', tenantId ?? 'all'] as const, opportunity: (id: string) => ['opportunities', 'detail', id] as const, topologies: ['architecture', 'topologies'] as const, + adrs: ['architecture', 'adrs'] as const, tenantIntelligence: (tenantId: string) => ['tenant-intelligence', tenantId] as const, intakeInitiatives: (tenantId?: string) => ['intake-initiatives', tenantId ?? 'all'] as const, intakeInitiative: (id: string) => ['intake-initiatives', 'detail', id] as const, @@ -552,6 +554,10 @@ export function useAssistantConverse() { export function useTopologies(): UseQueryResult { return useQuery({ queryKey: qk.topologies, queryFn: () => architectureApi.topologies() }); } +/** CR-10 / CR-21: local ADR registry (status + supersession) from DECISIONS.md. */ +export function useAdrRegistry(): UseQueryResult { + return useQuery({ queryKey: qk.adrs, queryFn: () => architectureApi.listAdrs() }); +} export function useRecommendTopology() { return useMutation({ mutationFn: (body: RecommendTopologyRequest) => architectureApi.recommendTopology(body), diff --git a/src/apps/tracker-web/src/api/types.ts b/src/apps/tracker-web/src/api/types.ts index 6e27c52f..0bf57cca 100644 --- a/src/apps/tracker-web/src/api/types.ts +++ b/src/apps/tracker-web/src/api/types.ts @@ -859,6 +859,21 @@ export interface EvaluatePhaseArtifactsResponse { [key: string]: unknown; } +/* ── ADR registry (CR-10 / CR-21) ─────────────────────────────────────────── + GET /api/architecture/adrs — the local Architecture Decision Record registry + mirrored from DECISIONS.md (status + supersession). */ +export interface AdrEntry { + code: string; + title: string; + decisionType: string; + upstreamRef: string | null; + localDocLink: string | null; + status: string; + supersedes: string | null; + supersededBy: string | null; + note: string; +} + /* ── Tenant intelligence registry ───────────────────────────────────────── */ export interface TenantIntelligenceDto { tenantId: string; diff --git a/src/apps/tracker-web/src/app/app.tsx b/src/apps/tracker-web/src/app/app.tsx index 91807e40..5f25128b 100644 --- a/src/apps/tracker-web/src/app/app.tsx +++ b/src/apps/tracker-web/src/app/app.tsx @@ -17,6 +17,7 @@ import { TenantRegistryMd3 } from '../components/screens/md3/TenantRegistryMd3'; import { MonitoringMd3 } from '../components/screens/md3/MonitoringMd3'; import { ScorecardsMd3 } from '../components/screens/md3/ScorecardsMd3'; import { DesignStudioMd3 } from '../components/screens/md3/DesignStudioMd3'; +import { AdrBrowserMd3 } from '../components/screens/md3/AdrBrowserMd3'; import { GateEvaluationMd3 } from '../components/screens/md3/GateEvaluationMd3'; import { GateGovernanceMd3 } from '../components/screens/md3/GateGovernanceMd3'; import { ArtifactsEvidenceMd3 } from '../components/screens/md3/ArtifactsEvidenceMd3'; @@ -113,6 +114,8 @@ export function App() { return ; case 'design': return ; + case 'adrs': + return ; case 'gate': return ; case 'gate-governance': diff --git a/src/apps/tracker-web/src/components/screens/md3/AdrBrowserMd3.tsx b/src/apps/tracker-web/src/components/screens/md3/AdrBrowserMd3.tsx new file mode 100644 index 00000000..ce21a8f4 --- /dev/null +++ b/src/apps/tracker-web/src/components/screens/md3/AdrBrowserMd3.tsx @@ -0,0 +1,230 @@ +import React from 'react'; +import { ApiError, useAdrRegistry } from '../../../api'; +import type { AdrEntry } from '../../../api'; +import { + Card, + CardHeader, + Chip, + Column, + DataTable, + EmptyState, + ErrorState, + Icon, + LoadingState, + PageHeader, +} from '../../../md3'; + +/* CR-10 / CR-21: dedicated browser for the local ADR registry (DECISIONS.md). + Sourced from GET /api/architecture/adrs via useAdrRegistry(). */ + +const REPO_DOC_BASE = 'https://github.com/beyondnetcode/evolith_tracker/blob/main/'; + +const STATUS_FILTERS = ['All', 'Active', 'Accepted', 'Recorded'] as const; +type StatusFilter = (typeof STATUS_FILTERS)[number]; + +function statusTone(status: string): 'pass' | 'info' | 'out' { + const s = (status ?? '').toLowerCase(); + if (s === 'active') return 'pass'; + if (s === 'accepted') return 'info'; + return 'out'; +} + +function typeTone(decisionType: string): 'eval' | 'out' { + const t = (decisionType ?? '').toLowerCase(); + return t === 'sobrescribir' || t === 'extender' ? 'eval' : 'out'; +} + +export const AdrBrowserMd3: React.FC = () => { + const query = useAdrRegistry(); + const [status, setStatus] = React.useState('All'); + + const entries: AdrEntry[] = query.data ?? []; + + const filtered = React.useMemo( + () => (status === 'All' ? entries : entries.filter((e) => e.status === status)), + [entries, status], + ); + + const columns: Column[] = [ + { + key: 'code', + header: 'Code', + width: 84, + render: (e) => ( + + {e.code} + + ), + }, + { + key: 'title', + header: 'Title · Título', + render: (e) => ( +
+ {e.title} + + {e.note} + +
+ ), + }, + { + key: 'type', + header: 'Type · Tipo', + width: 130, + render: (e) => {e.decisionType}, + }, + { + key: 'status', + header: 'Status · Estado', + width: 120, + render: (e) => + e.supersededBy ? ( + superseded + ) : ( + {e.status} + ), + }, + { + key: 'supersession', + header: 'Supersession', + width: 150, + render: (e) => { + if (!e.supersedes && !e.supersededBy) { + return ; + } + return ( +
+ {e.supersedes && ( + + supersedes + {e.supersedes} + + )} + {e.supersededBy && ( + + superseded by + {e.supersededBy} + + )} +
+ ); + }, + }, + { + key: 'upstream', + header: 'Upstream ref', + width: 120, + render: (e) => + e.upstreamRef ? ( + {e.upstreamRef} + ) : ( + + ), + }, + { + key: 'doc', + header: 'ADR doc', + width: 96, + render: (e) => + e.localDocLink ? ( + ev.stopPropagation()} + > + + open + + ) : ( + + ), + }, + ]; + + return ( + <> + + Governance / ADR registry + + } + title="ADR registry" + subtitle="Local Architecture Decision Records (T-###) mirrored from DECISIONS.md — decision type, status and supersession against the Core upstream." + actions={ + + } + /> + + + {filtered.length} ADRs} + /> + {query.isLoading ? ( + + ) : query.isError ? ( + query.refetch()} + /> + ) : filtered.length === 0 ? ( + + ) : ( +
+ + columns={columns} + rows={filtered} + rowKey={(e) => e.code} + /> +
+ )} +
+ + ); +}; diff --git a/src/apps/tracker-web/src/shell/nav.ts b/src/apps/tracker-web/src/shell/nav.ts index 9b172581..a5220847 100644 --- a/src/apps/tracker-web/src/shell/nav.ts +++ b/src/apps/tracker-web/src/shell/nav.ts @@ -42,6 +42,7 @@ export const NAV_GROUPS: NavGroup[] = [ label: 'Governance', items: [ { screen: 'design', label: 'Design studio', icon: 'dashboard_customize', perm: 'architecture' }, + { screen: 'adrs', label: 'ADR registry', icon: 'gavel', perm: 'architecture' }, { screen: 'gate-governance', label: 'Gate governance', icon: 'policy', perm: 'satellites' }, { screen: 'gate', label: 'Gate evaluation', icon: 'verified', perm: 'satellites' }, { screen: 'artifacts', label: 'Artifacts & evidence', icon: 'description', perm: 'satellites' },