Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8a9de0c
feat(api): resincroniza el backend con la plataforma de origen, rebra…
beyondnetPeru Aug 10, 2026
ea65c51
feat(sdk): resincroniza los SDK .NET/TypeScript/NestJS con la platafo…
beyondnetPeru Aug 10, 2026
1b2e723
feat(web): resincroniza el web-app con la plataforma de origen, conse…
beyondnetPeru Aug 10, 2026
a0cd4c2
feat(e2e,infra): resincroniza arnés RoboSoft, infraestructura local y…
beyondnetPeru Aug 10, 2026
fdc5ccb
docs: mapea la documentación de la plataforma de origen a la taxonomí…
beyondnetPeru Aug 10, 2026
e9423a0
fix(ci): cierra los dos fallos de CI que introduce la resincronización
beyondnetPeru Aug 10, 2026
8b541ca
ci: arregla cuatro gates que nunca habían pasado
beyondnetPeru Aug 10, 2026
64699c7
ci: corrige la subida de SARIF de Trivy y aplica el autofix de lint a…
beyondnetPeru Aug 10, 2026
c3faac1
refactor(web): elimina imports, bindings y asignaciones muertas del á…
beyondnetPeru Aug 10, 2026
27dd9d3
refactor(web): elimina `any`, console suelto, catch vacío y caídas de…
beyondnetPeru Aug 10, 2026
7954a2b
refactor(web): sustituye cascadas de ternarios por tablas de presenta…
beyondnetPeru Aug 10, 2026
c53ed69
refactor(web): justifica los patrones intencionales de set-state-in-e…
beyondnetPeru Aug 10, 2026
ceb4adc
refactor(web): unifica el estado visual de los campos y sigue con las…
beyondnetPeru Aug 10, 2026
dc7bbf6
refactor(web): elimina el último `any` del web-app
beyondnetPeru Aug 10, 2026
d27970c
refactor(web): cierra los grupos pequeños de lint
beyondnetPeru Aug 10, 2026
d2f28aa
refactor(web): convierte 10 cadenas de ternarios en JSX a bloques con…
beyondnetPeru Aug 10, 2026
babf879
refactor(web): resuelve a mano las cuatro cadenas JSX que el script n…
beyondnetPeru Aug 10, 2026
d10c887
refactor(web): sustituye las cascadas de ternarios que quedaban en ex…
beyondnetPeru Aug 10, 2026
c8d5078
refactor(web): saca de los bucles las funciones anidadas de más de cu…
beyondnetPeru Aug 10, 2026
e0fddbb
fix(web): corrige violaciones reales de las reglas de React
beyondnetPeru Aug 10, 2026
f9797b1
refactor(web): elimina el último ternario anidado del web-app
beyondnetPeru Aug 10, 2026
bd5064a
refactor(web): reduce complejidad extrayendo lo que estaba repetido
beyondnetPeru Aug 10, 2026
b267b1c
refactor(web): parte executeGraphQl y computeNodeState
beyondnetPeru Aug 10, 2026
54b2c7c
refactor(web): baja la complejidad de servicios, hooks y formularios
beyondnetPeru Aug 10, 2026
070780a
refactor(web): cierra los últimos cuatro puntos de complejidad cognitiva
beyondnetPeru Aug 10, 2026
4d5cae6
docs(deuda): registra TD-004 — el typecheck del web-app nunca ha comp…
beyondnetPeru Aug 10, 2026
fbb8750
docs(deuda): registra TD-005 — los cuerpos de notificación se registr…
beyondnetPeru Aug 10, 2026
cad3435
fix(api): revierte los resolvers de GraphQL a métodos de instancia — …
beyondnetPeru Aug 10, 2026
247ab73
fix(api): registra los tipos concretos de las factorías y corrige el …
beyondnetPeru Aug 10, 2026
0a7769b
docs(deuda): registra TD-006 y TD-007, hallados al verificar la aplic…
beyondnetPeru Aug 10, 2026
e7b75f6
docs(adr): redacta ADR-0090 y ADR-0164, y corrige el modelo de domini…
beyondnetPeru Aug 10, 2026
d369650
fix(web): los guards de menú y opción leían una forma del grafo que e…
beyondnetPeru Aug 10, 2026
75be986
docs(sdk,dominio): corrige el contrato del grafo de autorización al á…
beyondnetPeru Aug 10, 2026
d5a0147
docs: retira SQLite/SQL Server, el conmutador de transporte y documen…
beyondnetPeru Aug 10, 2026
a9fb071
docs: cierra las referencias al modelo de menús de cuatro niveles
beyondnetPeru Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 27 additions & 7 deletions .github/workflows/docs-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,29 @@ jobs:

- name: Check for TODO/demo references
run: |
TODO_COUNT=$(grep -r "TODO\|FIXME\|DEMO\|HACK" docs/ --include="*.md" | wc -l)
if [ $TODO_COUNT -gt 0 ]; then
echo "Found $TODO_COUNT TODO/FIXME/DEMO/HACK references"
grep -r "TODO\|FIXME\|DEMO\|HACK" docs/ --include="*.md"
# Se busca el MARCADOR (`TODO:`, `TODO(`, `FIXME`…), no la palabra suelta: en prosa
# española «TODOS los perfiles» no es deuda técnica, y con la coincidencia laxa el
# gate disparaba sobre texto corriente.
#
# Quedan fuera los documentos que hablan DE los marcadores en vez de tenerlos: el
# registro de deuda técnica, el TODO del proyecto (que es su inventario), las actas
# de release y la especificación que cita sus propios TD-xxx. Gatearlos obligaba a
# borrar el inventario de deuda para poder publicar documentación.
# El `sed` vacía los code spans antes de buscar: citar `TODO(G-069)` para explicar
# dónde está la deuda no es tener deuda.
MATCHES=$(grep -rn "" docs/ --include="*.md" \
--exclude-dir=releases \
--exclude="TODO.md" --exclude="TODO.es.md" \
--exclude="technical-debt*.md" \
--exclude="*parameterization-system-spec.md" \
| sed 's/`[^`]*`//g' \
| grep -E "(TODO[:(]|FIXME|HACK[:(]|\bDEMO\b)" || true)
if [ -n "$MATCHES" ]; then
echo "Found $(echo "$MATCHES" | wc -l) TODO/FIXME/DEMO/HACK markers"
echo "$MATCHES"
exit 1
fi
echo "No TODO/FIXME/DEMO/HACK references found"
echo "No TODO/FIXME/DEMO/HACK markers found"

link-validation:
name: Internal & External Link Validation
Expand Down Expand Up @@ -100,8 +116,12 @@ jobs:

- name: Extract mermaid diagrams
run: |
grep -r "```mermaid" docs/ --include="*.md" -A 50 | grep -v "^--$" > mermaid-diagrams.md
echo "Found $(grep -c "graph\|flowchart\|sequence\|class\|state\|er\|gantt" mermaid-diagrams.md || echo 0) potential mermaid blocks"
# Comillas SIMPLES: entre dobles, los tres backticks del patrón abren una sustitución
# de comandos y el paso moría con `unexpected EOF while looking for matching`.
# El `|| true` cubre el caso sin coincidencias, que en grep es salida 1 y bajo `bash -e`
# tumbaba el paso igual.
grep -r '```mermaid' docs/ --include="*.md" -A 50 | grep -v "^--$" > mermaid-diagrams.md || true
echo "Found $(grep -c 'graph\|flowchart\|sequence\|class\|state\|er\|gantt' mermaid-diagrams.md || echo 0) potential mermaid blocks"

- name: Validate Mermaid syntax
run: |
Expand Down
39 changes: 28 additions & 11 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,31 @@ jobs:
name: npm Vulnerability Audit
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
# El workspace npm/nx vive bajo src/, no en la raíz: sin esto `npm ci` moría con ENOENT
# buscando un package.json que nunca existió ahí. Mismo patrón que ci.yml.
defaults:
run:
working-directory: src
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
# React Router v8 exige node >= 22; el NODE_VERSION global (20) no basta.
node-version: 22
cache: 'npm'
cache-dependency-path: src/package-lock.json

- name: Install dependencies
run: npm ci

- name: Audit for vulnerabilities
run: npm audit --audit-level=high
# `--omit=dev` por la misma razón que en ci.yml: se audita lo que se DESPLIEGA. Las
# high restantes son de tooling de build y no llegan al runtime; auditarlas aquí
# dejaría el gate en rojo permanente sin señalar riesgo real de lo entregado.
run: npm audit --omit=dev --audit-level=high

dockerfile-scan:
name: Docker Image Security Scan
Expand All @@ -193,17 +203,20 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

# `@v3` no existe como tag en hadolint-action: el job moría en «Set up job» sin llegar a
# ejecutar nada. v3.3.0 es el release real de esa línea.
- name: Hadolint Lint
uses: hadolint/hadolint-action@v3
uses: hadolint/hadolint-action@v3.3.0
with:
dockerfile: Dockerfile
dockerfile: src/apps/ums.api/Dockerfile
continue-on-error: true

# No hay Dockerfile en la raíz, así que el `if` de antes nunca se cumplía: no se construía
# imagen alguna y Trivy escaneaba una referencia inexistente (lo tapaba su
# continue-on-error). El contexto de build es src/, porque el grafo de proyectos alcanza
# Ums.ReadModels y libs/sdk/dotnet, fuera de apps/ums.api.
- name: Build Docker image for scan
run: |
if [ -f "Dockerfile" ]; then
docker build -t ums-app:test . --quiet
fi
run: docker build -f src/apps/ums.api/Dockerfile -t ums-app:test src/ --quiet

- name: Run Trivy scanner
uses: aquasecurity/trivy-action@master
Expand All @@ -214,11 +227,15 @@ jobs:
severity: 'CRITICAL,HIGH'
continue-on-error: true

# `upload-security-results` no existe en codeql-action —de ahí que el job muriera en
# «Set up job», antes de ejecutar nada—. La acción real es `upload-sarif`, y v2 está
# retirada. El `if` cubre que Trivy no llegue a escribir el SARIF: sube solo si hay
# fichero, en vez de fallar el paso.
- name: Upload Trivy results
uses: github/codeql-action/upload-security-results@v2
if: always()
uses: github/codeql-action/upload-sarif@v3
if: always() && hashFiles('trivy-results.sarif') != ''
with:
tool_name: 'Trivy'
category: 'trivy'
sarif_file: 'trivy-results.sarif'

tenant-isolation-tests:
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/adrs/0071-auth-graph-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ AuthorizationGraph
├── context — user, tenant, systemSuite, role, profile, branch
├── authentication — method (Local|IDP), provider, mfaRequired, expiry
├── actions[] — all registered actions in the SystemSuite
├── menuAccess[] — Module→Menu→SubMenu→Option tree with AccessEffect per option
├── menuAccess[] — Module→MenuNode recursive tree (ADR-0090), AccessEffect per node action
├── domainPermissions[] — domain resources with effect per action (Aggregate/Entity)
├── featureFlags[] — flags evaluated against user context at auth-time
├── effectiveConfig — tenant-resolved parameters (session timeout, MFA, etc.)
Expand Down
97 changes: 97 additions & 0 deletions docs/architecture/adrs/0090-recursive-menu-node-tree.es.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# ADR-0090: El Árbol Recursivo `MenuNode` Sustituye a `Menu` / `SubMenu` / `Option`

**Estado:** Aceptado
**Fecha:** 2026-08-10
**Responsable de Decisión:** Arquitectura
**Reemplaza:** La jerarquía rígida de cuatro niveles Suite → Módulo → Menú → Submenú → Opción
**Relacionado:** [ADR-0071](./0071-auth-graph-engine.es.md) · gap G-029 · decisión D-009

---

> **Registro retroactivo.** La decisión se implementó antes de escribirse. Este ADR se reconstruye
> a partir del código en producción (`Ums.Domain/Authorization/SystemSuite/MenuNode/`) y de los más
> de quince documentos que ya la citan como aceptada. Documenta lo que el sistema hace hoy; no
> propone un cambio.

## Contexto

La topología de navegación de una suite se modelaba como una **cadena fija de cuatro niveles**:

```
SystemSuite → Module → Menu → SubMenu → Option
```

Tres entidades propias distintas —`Menu`, `SubMenu`, `Option`— expresaban tres niveles de la misma
idea: *un sitio en un árbol*. Esa rigidez traía problemas concretos:

1. **Un nivel obligatorio que nadie quería.** Colgar una opción directamente de un menú era
imposible: había que inventar un submenú de relleno. Esos rellenos existen en los datos sembrados
y en las suites de clientes, y en la interfaz se ven como ramas vacías.
2. **La relación funcionalidad↔opción era 1:1 y débil.** El vínculo era un `ActionCode` en texto
sobre la opción, sin clave ajena. Una misma funcionalidad no podía alcanzarse desde dos sitios
del menú, y nada impedía que una opción nombrara una acción inexistente.
3. **Sin metadatos de gobernanza por nodo.** `Status` existía en la suite y en el módulo. El nodo
—lo que una persona navega, y por lo que pregunta una auditoría— no llevaba responsable, ni
criticidad, ni trazabilidad al artefacto SDLC que lo justifica.
4. **Tres de todo.** Tres agregados de comandos, manejadores, validadores, configuraciones EF y
registros para expresar un único concepto recursivo. Añadir una regla obligaba a añadirla tres
veces, y las tres copias derivaban.

## Decisión

Modelar la topología de navegación como una **única entidad recursiva**, `MenuNode`, propiedad de
`Module`.

- Un `Module` posee una colección de **nodos raíz**; cada nodo puede anidar hijos recursivamente
(lista de adyacencia mediante `ParentNodeId`).
- El papel del nodo se clasifica con `NodeKind` —`Menu`, `SubMenu`, `Option`— **sin fijar la
profundidad**. `Menu` y `SubMenu` actúan como rama y `Option` como hoja. Los nombres sobreviven
como *roles*, no como tipos, porque es el vocabulario que el negocio ya usa.
- El vínculo con la funcionalidad pasa a ser **N:M** mediante la tabla puente
`SystemSuiteNodeActions`, de modo que una funcionalidad se alcanza desde varios sitios y todo
vínculo apunta a una acción que existe.
- Cada nodo lleva `MenuNodeMetadata`, un objeto de valor con campos de gobernanza SDLC
(responsable, criticidad, producto impactado, componente técnico, dependencias, evidencias,
trazabilidad SDLC). Todos opcionales; se reemplazan de forma atómica.

| Dimensión | Modelo rígido (retirado) | Árbol `MenuNode` |
|---|---|---|
| Profundidad | Fija de 4 niveles, submenú obligatorio | Variable; submenú opcional |
| Funcionalidad↔opción | 1:1 débil (`ActionCode` sin FK) | **N:M** vía `SystemSuiteNodeActions` |
| Metadatos de gobernanza | Solo `Status` en suite/módulo | **Metadatos SDLC por nodo** (`MenuNodeMetadata`) |
| Entidades | `Menu`, `SubMenu`, `Option` | Un único `MenuNode` recursivo |

Las operaciones siguen en la raíz de agregado `SystemSuite`, que delega en `Module`/`MenuNode`:
`AddModuleRootNode`, `AddModuleChildNode`, `UpdateModuleNode`, `RemoveModuleNode` (el nodo **y su
subárbol**), `ActivateModuleNode` / `DeactivateModuleNode`, `LinkModuleNodeAction` /
`UnlinkModuleNodeAction`, `SetModuleNodeMetadata`.

## Consecuencias

**Se gana.** Una entidad, un juego de reglas. La profundidad la marca el producto y no el esquema.
Una funcionalidad alcanzable desde dos menús es expresable. Cada nodo puede responder «quién es su
responsable y por qué existe».

**Se paga.** El árbol se guarda plano y se reconstruye en memoria
(`AuthorizationAggregateFactory.RehydrateNode`, agrupando por `ParentNodeId`); una suite profunda
cuesta un recorrido de sus nodos al cargar. La recursión admite ciclos que una cadena fija no
permitía, así que la invariante «un nodo no es su propio ancestro» pasa a exigir vigilancia en vez
de darse por supuesta.

**Se retira.** `Menu`, `SubMenu` y `Option`, con sus comandos, manejadores, validadores,
configuraciones EF y registros, **ya no existen en el código**. Tampoco `SystemSuiteMenuRecord`,
`SystemSuiteSubMenuRecord` ni `SystemSuiteOptionRecord`. La documentación que siga describiendo la
cadena de cuatro niveles como vigente está obsoleta, no describe una alternativa.

## Persistencia

- `ums_authorization.SystemSuiteNodes` — lista de adyacencia (`ParentNodeId`), con columnas de
metadatos SDLC.
- `ums_authorization.SystemSuiteNodeActions` — puente N:M, nodo ↔ `ActionCode`.
- Migración `20260715165202_AddSystemSuiteNodes`.

## Referencias

- Ficha de dominio: [`domain-es/authorization/menu-node.md`](../../domain-es/authorization/menu-node.md)
- Agregado: [`domain-es/authorization/system-suite.md`](../../domain-es/authorization/system-suite.md)
- Código: `src/apps/ums.api/Ums.Domain/Authorization/SystemSuite/MenuNode/`
102 changes: 102 additions & 0 deletions docs/architecture/adrs/0090-recursive-menu-node-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
adr: 0090
title: Recursive MenuNode tree replaces the rigid Menu/SubMenu/Option hierarchy
status: Accepted
date: 2026-08-10
tags: [EvolithSatellite, authorization, system-suite, navigation, domain-model]
supersedes: none
relates: [ADR-0071 auth graph engine, ADR-0081 semantic auth graph client contract]
gap: G-029
decision: D-009
---

# ADR-0090 — A Recursive `MenuNode` Tree Replaces `Menu` / `SubMenu` / `Option`

> **Retroactive record.** The decision was implemented before it was written down. This ADR is
> reconstructed from the shipped code (`Ums.Domain/Authorization/SystemSuite/MenuNode/`) and from
> the fifteen-plus documents that already cite it as accepted. It documents what the system does
> today; it does not propose a change.

## Status

Accepted. Implemented and in production.

## Context

The navigation topology of a system suite was modelled as a **fixed four-level chain**:

```
SystemSuite → Module → Menu → SubMenu → Option
```

Three separate owned entities — `Menu`, `SubMenu`, `Option` — expressed three levels of the same
idea: *a place in a tree*. That rigidity produced concrete problems:

1. **A mandatory level nobody wanted.** Anchoring an option directly under a menu was impossible;
a filler submenu had to be invented for it. Those fillers exist in seeded data and in customer
suites, and they show up in the UI as empty branches.
2. **Functionality ↔ option was 1:1 and weak.** The link was an `ActionCode` string on the option,
with no foreign key. The same functionality could not be reached from two places in the menu,
and nothing stopped an option from naming an action that did not exist.
3. **No governance metadata per node.** `Status` existed on the suite and the module. A node — the
thing a person actually navigates to, and the thing an auditor asks about — carried no owner,
no criticality, no traceability to the SDLC artefact that justifies it.
4. **Three of everything.** Three aggregates' worth of commands, handlers, validators, EF
configurations and records to express one recursive concept. Adding a rule meant adding it
three times, and the three copies drifted.

## Decision

Model the navigation topology as a **single recursive entity**, `MenuNode`, owned by `Module`.

- A `Module` owns a collection of **root nodes**; every node may nest children recursively
(adjacency list via `ParentNodeId`).
- The node's role is classified by `NodeKind` — `Menu`, `SubMenu`, `Option` — **without fixing the
depth**. `Menu` and `SubMenu` behave as branches, `Option` as a leaf. The names survive as
*roles*, not as types, because that is the vocabulary the business already uses.
- Functionality binding becomes **N:M** through the `SystemSuiteNodeActions` bridge table, so one
functionality can be reached from several places and every link points at an action that exists.
- Every node carries `MenuNodeMetadata`, a value object with SDLC governance fields (owner,
criticality, impacted product, technical component, dependencies, evidence, SDLC traceability).
All optional; replaced atomically.

| Dimension | Rigid model (withdrawn) | `MenuNode` tree |
|---|---|---|
| Depth | Fixed, 4 levels, submenu mandatory | Variable; submenu optional |
| Functionality ↔ option | 1:1, weak (`ActionCode` string, no FK) | **N:M** via `SystemSuiteNodeActions` |
| Governance metadata | `Status` on suite/module only | **Per-node SDLC metadata** (`MenuNodeMetadata`) |
| Entities | `Menu`, `SubMenu`, `Option` | One recursive `MenuNode` |

Operations stay on the `SystemSuite` aggregate root, which delegates to `Module`/`MenuNode`:
`AddModuleRootNode`, `AddModuleChildNode`, `UpdateModuleNode`, `RemoveModuleNode` (node **and its
subtree**), `ActivateModuleNode` / `DeactivateModuleNode`, `LinkModuleNodeAction` /
`UnlinkModuleNodeAction`, `SetModuleNodeMetadata`.

## Consequences

**Gained.** One entity, one set of rules. Depth follows the product instead of the schema. A
functionality reachable from two menus is expressible. Every node can answer "who owns this and
why does it exist".

**Paid.** The tree is stored flat and rebuilt in memory
(`AuthorizationAggregateFactory.RehydrateNode`, grouping by `ParentNodeId`); a deep suite costs one
pass over its nodes on load. Recursion admits cycles that a fixed chain could not, so the invariant
"a node is not its own ancestor" now has to be enforced rather than assumed.

**Migrated away.** `Menu`, `SubMenu` and `Option` and their commands, handlers, validators, EF
configurations and records are **gone from the code**. `SystemSuiteMenuRecord`,
`SystemSuiteSubMenuRecord` and `SystemSuiteOptionRecord` no longer exist. Documentation still
describing the four-level chain as current is stale, not describing an alternative.

## Persistence

- `ums_authorization.SystemSuiteNodes` — adjacency list (`ParentNodeId`), with SDLC metadata columns.
- `ums_authorization.SystemSuiteNodeActions` — N:M bridge, node ↔ `ActionCode`.
- Migration `20260715165202_AddSystemSuiteNodes`.

## References

- Domain sheet: [`domain-es/authorization/menu-node.md`](../../domain-es/authorization/menu-node.md) ·
[`domain/authorization/menu-node.md`](../../domain/authorization/menu-node.md)
- Aggregate: [`domain-es/authorization/system-suite.md`](../../domain-es/authorization/system-suite.md)
- Code: `src/apps/ums.api/Ums.Domain/Authorization/SystemSuite/MenuNode/`
Loading
Loading