# Plan de pruebas: límites de la API .NET de Revit 2027

**Objetivo:** llevar al límite la API .NET de Revit 2027 y registrar, capacidad por capacidad, qué **se puede**, qué **no se puede** y bajo qué **condiciones**. El laboratorio no usa nube, ni pyRevit, ni Dynamo, ni servidores MCP de terceros: solo C# contra `RevitAPI.dll`/`RevitAPIUI.dll`.

**Entorno fijo** (no actualizar a mitad del laboratorio): Revit **2027.1** (build 27.1.0.45), .NET 10 (SDK 10.0.402), Visual Studio Community 2026, interfaz en inglés (ENU), licencia educativa.

Además de la API, se mide **qué tan difícil le resulta a la IA** programar contra ella (modelo, tokens, iteraciones, causas de fallo): ver [Métricas de dificultad IA](metricas-detalladas.md).

Contexto y fuentes: [informe de investigación](#), en especial los apartados "Una sola API .NET" y "Hot Reload desactivado".

---

## 1. Cómo se verifica (el "Playwright" de Revit)

En web, un navegador automatizado deja que el agente compruebe solo si algo funciona. En Revit no existe un equivalente oficial: la API solo vive **dentro** de un `Revit.exe` abierto, con licencia y en su hilo principal. La solución es poner un **ejecutor de pruebas dentro de Revit** que reciba órdenes desde fuera y devuelva resultados en un formato que Claude pueda leer. Así el ciclo queda casi igual que en web:

```
Claude escribe el test (C#) → dotnet build → el ejecutor dentro de Revit carga la DLL
→ ejecuta en el contexto de API válido → escribe resultados JSON + imágenes
→ Claude lee los resultados, mira las imágenes y actualiza este checklist
```

### 1.1 Niveles de verificación

| Nivel | Código | Quién verifica | Cómo | Ejemplos |
|---|---|---|---|---|
| **Estático** | `S` | Claude, sin Revit | Reflexión sobre `RevitAPI.dll` con `MetadataLoadContext`: inventario de clases, métodos y enums. Consolas .NET que intentan usar la API fuera de Revit | "¿Existe una API para crear leyendas?", lista de `PostableCommand`, la prueba sin interfaz |
| **Automático** | `A` | Claude | Un test C# con aserciones se ejecuta dentro de Revit y devuelve OK, FALLO o EXCEPCIÓN (tipo y mensaje) en JSON | Crear un muro y comprobar longitud, tipo y nivel |
| **Visual** | `V` | Claude | El test exporta una imagen de la vista (`Document.ExportImage`) o un PDF; Claude la abre y la juzga. Si hace falta, una captura de la ventana de Revit con PowerShell | Que un filtro de vista colorea lo esperado; que una cota se dibuja bien |
| **Manual** | `M` | Tú | Lo que necesita interacción humana o percepción de la interfaz. Claude prepara el test y tú anotas el resultado | Cinta, paneles acoplables, diálogos no modales, si la interfaz responde |
| **Medición** | `P` | Claude | `Stopwatch`, memoria del proceso (`Process.WorkingSet64`), número de elementos; resultados en JSON | Crear 10.000 muros: tiempo y memoria |
| **Journal** | `J` | Claude | Leer `%LOCALAPPDATA%\Autodesk\Revit\Autodesk Revit 2027\Journals\journal.*.txt` tras cada sesión: registra excepciones de API, avisos y cuelgues | Diagnosticar un crash o un diálogo inesperado |

### 1.2 El ejecutor de pruebas: dos opciones

**Opción 1 (recomendada): `RevitLab.Runner`, un ejecutor propio.** Es un add-in mínimo que hace tres cosas:

1. abre un *named pipe* local (`\\.\pipe\revitlab`) restringido a tu usuario;
2. al recibir `run <dll> <filtro>`, carga la DLL de tests en un `AssemblyLoadContext` descartable, igual que el cargador del falso hot reload;
3. ejecuta los tests mediante un `ExternalEvent`, en el contexto de API válido, y responde con JSON. Las imágenes se guardan en `pruebas/resultados/<fecha>/`.

Del lado de Claude basta una pequeña CLI (`labctl run --filter ELM-*`) que habla con el pipe.

- **Por qué es la recomendada:** es **el mismo componente que el falso hot reload** (fase 1b), así que se resuelven dos problemas con una sola pieza. No añade dependencias cerradas ni capas de terceros, corre en tu sesión normal de Revit (lo que permite combinar tests `A` con verificación `V` y `M` en la misma sesión) y sirve para probar cosas de interfaz que un runner desatendido no ve.
- **Costo:** hay que escribirlo, unas 300–500 líneas. Es la primera tarea del laboratorio.
- **No es un "MCP":** no hay ningún LLM en medio; es un canal de órdenes de pruebas, como el que usa Playwright.

**Opción 2 (para comparar): `Nice3point.TUnit.Revit` 2027.0.7** ([NuGet](https://www.nuget.org/packages/Nice3point.TUnit.Revit)).

- **Qué ofrece:** tests TUnit con las clases base `RevitApiTest` y `RevitApiUiTest`, ejecutados con `dotnet run -c Release.R27` o `dotnet test`. Apunta a `net10.0-windows`. Según su página, "TUnit initializes Revit" a partir de la ruta de instalación, y depende de `PolyHook2.NET` (*hooking* de funciones).
- **Lo atractivo:** si de verdad funciona sin intervención, es lo más parecido a Playwright, porque Claude ejecuta `dotnet run` y lee el resultado.
- **Dudas:** no es código abierto ("not currently planned due to Autodesk export regulations"), no se sabe si funciona con licencia educativa y no está claro cómo encaja el *hooking* con los términos de uso de Autodesk. Probarlo es en sí un experimento del checklist (ENT-12).
- Para comparar: `ricaun.RevitTest` (NUnit, MIT, [GitHub](https://github.com/ricaun-io/RevitTest)) solo documenta hasta Revit 2025, y su última versión es 1.11.1. Queda descartado mientras no confirme 2027.

### 1.3 Plantilla de test (opción 1)

Cada test lleva el ID del checklist para que el resultado se vuelque solo:

```csharp
[LabTest("ELM-01", "Crear muro recto por línea")]
public static LabResult CrearMuro(Document doc)
{
    using var t = new Transaction(doc, "ELM-01");
    t.Start();
    var nivel = new FilteredElementCollector(doc).OfClass(typeof(Level)).Cast<Level>().First();
    var muro = Wall.Create(doc, Line.CreateBound(XYZ.Zero, new XYZ(10, 0, 0)), nivel.Id, false);
    t.Commit();
    return LabResult.Assert(Math.Abs(muro.get_Parameter(BuiltInParameter.CURVE_ELEM_LENGTH).AsDouble() - 10) < 1e-9,
                            obs: $"WallType={muro.WallType.Name}");
}
```

El JSON de salida por test incluye `id`, `estado` (`ok | fallo | excepcion | crash | timeout`), `excepcion` (tipo y mensaje), `ms`, `memMB`, `obs` y `evidencias` (rutas de imágenes).

### 1.4 Modelos de prueba

- **Nunca** se trabaja sobre proyectos reales. Todo modelo se crea desde plantilla o se genera por API dentro de `modelos/`.
- `M0-vacio.rvt`: desde `C:\ProgramData\Autodesk\RVT 2027\Templates\` (plantilla métrica).
- `M1-basico.rvt`: lo genera un test (niveles, rejillas, muros, puertas, habitaciones). Es la base de casi todo.
- `M2-grande.rvt`: también generado por API, con 50.000 o más elementos, para las pruebas de estrés. La instalación solo trae contenido mínimo, así que el modelo grande hay que fabricarlo.
- Cada test que modifica parte de una copia o de un documento nuevo y lo cierra sin guardar (`doc.Close(false)`).

---

## 2. Convenciones del checklist

- `- [ ]` pendiente. `- [x]` probado (con el resultado ya anotado).
- **Hipótesis** (antes de probar, según la documentación y la investigación): `✅` se espera que se pueda · `❌` se espera que no · `⚠️` se espera que solo con condiciones · `❓` sin información.
- **Resultado** (tras probar): `✅ se puede` · `❌ no se puede` (no hay API o lanza excepción) · `⚠️ se puede con condiciones` · `💥 tumba o cuelga Revit` · `⏭ omitido`.
- **Método:** `S A V M P J` (ver 1.1).
- **Obs:** condiciones, excepción exacta, workaround, tiempo medido, enlace a evidencia.

Formato de cada línea:

```
- [ ] **ID** Capacidad · H: ✅ · Método: A · R: — · Obs: —
```

> Las hipótesis son puntos de partida, no hechos. Si un resultado contradice la hipótesis, ese es precisamente un hallazgo que vale la pena explicar en Obs.

---

## 3. Checklist

### DEV · Ciclo de desarrollo y falso hot reload (prioridad 1)

Detalle de cada prueba en el informe, apartado "Hot Reload desactivado".

- [x] **DEV-00** Hot Reload / Edit and Continue nativos desde VS 2026 con Revit como programa externo · H: ❌ · Método: M · R: ❌ · Obs: **No funciona de ninguna forma.** (1) VS 2026 adjuntado a Revit + Recarga activa (Alt+F10): "Los cambios no se pueden aplicar debido a un error inesperado: ."; Salida → "Hot Reload is not supported in this session due to the lack of a managed language service" y "An unexpected error has occurred, any pending updates have been discarded" (además: "[XAML] … La Recarga activa incremental es deshabilitado"). (2) Editar y continuar detenido en un breakpoint y F10: **ningún mensaje**, y el cambio no se aplica (el resultado siguió siendo 'marca 2'): falla en silencio. (3) VS **lanzando** Revit (perfil `Executable` en launchSettings.json, F5) + Recarga activa: el mismo "error inesperado" y el cambio no se aplica ('marca 3'). Causa: los **89** `*.runtimeconfig.json` de Revit 2027 fijan `System.Reflection.Metadata.MetadataUpdater.IsSupported=false`. Alternativa que sí funciona: el falso hot reload del runner (contexto descargable, ~4 s de guardar a resultado con DEV-09)
- [x] **DEV-01** Línea base: ciclo de cerrar Revit, compilar, abrir Revit y el modelo y ejecutar · H: — · Método: P · R: ✅ · Obs: **Ciclo completo con reinicio: 37 s**, automatizado sin intervención (SALIR = `PostCommand(ExitRevit)` → esperar fin del proceso → `lab build` → `Start-Process Revit.exe` → esperar `lab ping` → abrir el modelo → ejecutar la prueba): cerrar Revit 10,9 s, compilar 1,1 s, arrancar hasta que el pipe responde 11,9 s y hasta que la API atiende +3,2 s, abrir el modelo 8,3 s, ejecutar 1,8 s. Arranques posteriores sin modelo: 11–42 s hasta el runner (42 s con 2 avisos de seguridad pendientes). Frente a **~4 s** del falso hot reload (DEV-09): reiniciar cuesta ~9 veces más con un Revit vacío y un modelo pequeño. Además se pierde el estado de la sesión y salen avisos de seguridad si cambió la DLL de un add-in
- [x] **DEV-02** Add-In Manager del SDK: recompilar y reejecutar sin reiniciar · H: ⚠️ · Método: M/P · R: ✅ · Obs: Add-In Manager del **SDK 2027.3** (`C:\Revit 2027.3 SDK\Add-In Manager`, con Revit 2027.1) instalado copiando su manifiesto con la ruta absoluta a la DLL. Carga `RevitLab.Aim.dll`, ejecuta el comando ('versión 1'); se recompila con Revit abierto (la DLL original no se bloquea) y **Run sin volver a cargar ejecuta el código nuevo** ('versión 2'). Cómo lo hace: copia la DLL a `%TEMP%\…\RevitAddins\RevitLab.Aim-Parsing-<sello>` (para listar) y `…-Executing-<sello>` (para ejecutar) y la carga con `Assembly.LoadFile`, que **no es descargable**: en la ventana Módulos de VS había **4 copias** de RevitLab.Aim.dll cargadas tras 2 ejecuciones (frente a 1 sola RevitLab.Tests.dll del runner tras muchas recargas). Solo comandos, manual (clics). Hallazgo lateral: su manifiesto usa `<ManifestSettings><UseRevitContext>False</UseRevitContext><ContextName>AIMInternal</ContextName></ManifestSettings>`, el interruptor del aislamiento de dependencias (ver ENT-08..11). Además, Revit trae su propio Add-Ins Manager (activar/desactivar add-ins) en `Revit 2027\AddInsManager\RevitAddInsManager.exe`
- [x] **DEV-03** Cargador propio con `AssemblyLoadContext` coleccionable: cambio visible en menos de 5 s · H: ✅ · Método: A/P · R: ✅ · Obs: Cargador propio (`RevitLab.Runner`, copia sombra + `AssemblyLoadContext` coleccionable): **3,8 s** desde guardar el cambio hasta ver el resultado (3,5 s de compilación, 35 ms de carga y ejecución). Contexto descargado en 2 ciclos de GC
- [x] **DEV-04** 50 recargas seguidas sin que la memoria crezca de forma descontrolada · H: ⚠️ · Método: P · R: ✅ · Obs: 51 recargas seguidas: 51/51 descargadas (`WeakReference` muerta), memoria de `Revit.exe` 941 → 948 MB (≈0,14 MB por recarga, con un `IExternalCommand` trivial). Pendiente: limpiar las copias sombra que se acumulan en el TEMP propio de Revit
- [x] **DEV-05** Dependencia NuGet en el payload recargable · H: ✅ · Método: A · R: ⚠️ · Obs: Las dependencias NuGet **administradas** funcionan en el código recargable (con `CopyLocalLockFileAssemblies`); las **nativas** no, hasta que el runner copie `runtimes/` y sobrescriba `LoadUnmanagedDll` (pendiente para el próximo reinicio)
- [x] **DEV-06** Ventana WPF no modal dentro de un contexto descargable · H: ⚠️ · Método: M/A · R: ✅ · Obs: Ventana WPF **con XAML compilado (BAML)** y manejador de clic en el XAML, creada desde el payload recargable (ALC `RevitLab.Pruebas.<sello>`) y mostrada no modal: `InitializeComponent` OK y el botón Cerrar funciona. **Cada recarga usa el XAML nuevo:** versión A → (XAML con la codificación estropeada por un error propio) → versión B, con 11, 12 y 13 copias de RevitLab.Tests cargadas en el proceso. El riesgo de que el BAML o el pack URI se resuelvan contra una copia vieja no se materializó (sin recursos `pack://` de imágenes, que no se probaron)
- [x] **DEV-07** Payload con `DocumentChanged` e `IUpdater` que se descarga limpio tras `Shutdown()` · H: ⚠️ · Método: A/P · R: ✅ · Obs: **Corrige la observación anterior, que era errónea.** Con el nuevo `[LabShutdown]` (el runner lo llama antes de `Unload()`), cada variante en su propio run: (a) control sin eventos → descargado; (b) `DocumentChanged` dado de baja en Shutdown → **descargado**; (c) `IUpdater` de aplicación dado de baja en Shutdown → **descargado**; (e) `DocumentChanged` dado de baja dentro de la prueba → **descargado**; (d) control negativo, evento que nunca se da de baja → **retenido** (10 ciclos de GC), lo que confirma que la medición es sensible; (f) solo `Tx.Ejecutar` → **retenido**. Conclusión: los eventos y updaters liberan el contexto si se dan de baja. El que retiene es el **`IFailuresPreprocessor`** de la transacción (Revit guarda la referencia). La afirmación de 2026-10-18 ("aunque se den de baja") mezclaba ambos factores, porque todas aquellas pruebas usaban `Tx`. Sin verificar de nuevo: `FailuresProcessing` y `ExternalEvent.Create`. Runs 20261027-1628xx–1631xx
- [x] **DEV-08** Breakpoints de VS en el payload recargado · H: ⚠️ · Método: M · R: ✅ · Obs: VS 2026 → Depurar → Asociar al proceso (Revit.exe, código administrado .NET Core) → breakpoint en `Recarga.cs` → `lab run`: **VS se detiene** en el código de la copia sombra (`%TEMP%\…\RevitLab\sombra\<sello>\RevitLab.Tests.dll`, 'Símbolos cargados', el PDB se copia junto a la DLL) y muestra los valores; Revit queda congelado hasta F5 (406 s en la prueba, sin efectos secundarios). La ventana Módulos confirma que las recargas anteriores del runner se descargaron (una sola RevitLab.Tests.dll en memoria)
- [x] **DEV-09** Recarga automática con `dotnet watch` + `FileSystemWatcher` + `Idling` · H: ✅ · Método: M/P · R: ✅ · Obs: Nuevo `lab vigilar <id> --filtro X` (`FileSystemWatcher` sobre la DLL de pruebas, espera de 1,5 s sin cambios y luego `run`) + `dotnet watch --project … build` en otra consola: guardar `Recarga.cs` ('marca 1' → 'marca 2') a las 00:31:43.955 → DLL compilada a las 00:31:45.984 (+2,0 s) → detectada a las 00:31:47.650 (+1,7 s, la espera de seguridad) → **resultado 'marca 2' en Revit a las 00:31:48.125**: **~4,2 s de guardar a resultado**, sin tocar nada ni reiniciar Revit. No hizo falta `Idling`: la orden llega por el pipe del runner
- [x] **DEV-10** Excepción no controlada en el payload: Revit sobrevive · H: ✅ · Método: A · R: ✅ · Obs: La excepción llega al runner con traza y línea y Revit sigue funcionando. Desde la versión 2 del runner, `[LabTest(…, EsperaExcepcion = true)]` la cuenta como ok
- [x] **DEV-11** `StackOverflowException` o bucle infinito en el payload · H: 💥 · Método: M/J · R: 💥 · Obs: Una **recursión infinita en el payload mata Revit al instante**: ni el runner (`try/catch` alrededor de la prueba) ni Revit pueden capturar un `StackOverflowException` en .NET. Windows registra el evento 1000: `Revit.exe` 27.1.0.45, módulo "unknown", **código de excepción 0xc00000fd**; WER registra un APPCRASH. Se pierde todo lo no guardado. Defensa posible: limitar la profundidad de recursión en el código, o ejecutar el código peligroso en otro proceso

### ENT · Carga de add-ins y entorno .NET 10

- [x] **ENT-01** Add-in cargado desde `%APPDATA%\Autodesk\Revit\Addins\2027` · H: ✅ · Método: A/J · R: ✅ · Obs: Runner cargado desde `%APPDATA%\Autodesk\Revit\Addins\2027` (2026-10-18). Sin `UseRevitContext` en el manifiesto, el add-in se carga en el `AssemblyLoadContext` **Default** (dato útil para ENT-09)
- [x] **ENT-02** Manifiesto en `C:\ProgramData\Autodesk\Revit\Addins\2027` (ruta antigua) · H: ❌ · Método: J · R: ❌ · Obs: Revit 2027 **ignora** los manifiestos de la ruta antigua `C:\ProgramData\Autodesk\Revit\Addins\2027` (que además admitió escribir sin permisos de administrador). Sin diálogo; solo el journal lo explica: "Add-in manifest file from: C:\ProgramData\Autodesk\Revit\Addins\2027\RevitLab.Ent.pd.addin, won't be loaded. All-users Add-in manifest files must be installed to: C:\Program Files\Autodesk\Revit\Addins\2027". Arranque A del 2026-10-28, journal.0016
- [x] **ENT-03** Manifiesto en `C:\Program Files\Autodesk\Revit\Addins\2027` (requiere administrador) · H: ✅ · Método: J · R: ✅ · Obs: Un manifiesto en **`C:\Program Files\Autodesk\Revit\Addins\2027`** (ruta "para todos los usuarios") se carga: journal `API_SUCCESS { Starting External Application: RevitLab ENT-03 Program Files … }` y el add-in escribió en ent.log al arrancar (ALC 'Default'). La carpeta **no existía** en esta instalación (ni `C:\Program Files\Autodesk\Revit`): hubo que crearla y copiar el manifiesto con **PowerShell como administrador** (lo hizo el usuario; sin elevación no se puede escribir ahí). Como cualquier add-in nuevo sin firma, Revit pidió confirmación (Always Load) y detuvo el arranque hasta que el usuario respondió. Es la ruta que indica el propio journal cuando ignora la antigua de ProgramData (ENT-02). 2026-10-28, journal.0028
- [x] **ENT-04** DLL compilada para `net8.0-windows` (estilo Revit 2025/2026) cargada en 2027 · H: ⚠️ · Método: A/J · R: ⚠️ · Obs: Un add-in compilado para **`net8.0-windows` contra la API de Revit 2026** (Nice3point.Revit.Api 2026.4.10) **carga y funciona** en Revit 2027 (`OnStartup` corre en .NET 10.0.14). El journal registra `API_ERROR` ("Assembly version conflict … System.Runtime 8.0.0.0 conflicts with same preloaded module of version 10.0.0.0; RevitAPI 26.4.10.0 … 27.1.0.0") pero arranca. Funciona mientras use API que siga existiendo; Autodesk recomienda recompilar
- [x] **ENT-05** DLL compilada para .NET Framework 4.8 · H: ❌ · Método: J · R: ⚠️ · Obs: **Sorpresa: un add-in compilado para .NET Framework 4.8 contra la API de Revit 2024** (Nice3point.Revit.Api 2024.3.60) **también carga y ejecuta `OnStartup`** en Revit 2027, sobre .NET 10.0.14. El journal registra `API_ERROR` por el conflicto de versiones de RevitAPI/RevitAPIUI (24.3.60 frente a 27.1.0) pero arranca. Un add-in sencillo de 2024 puede funcionar sin recompilar; uno que use API eliminadas, `BinaryFormatter` (ENT-15) u otras librerías de .NET Framework fallará en tiempo de ejecución
- [x] **ENT-06** Add-in sin firma digital: aviso de seguridad y comportamiento · H: ⚠️ · Método: M · R: ✅ · Obs: Texto exacto (del journal): **"The publisher of this add-in could not be verified. What do you want to do?"**, Id `TaskDialog_Security_Unsigned_File_Loading`, botones **1001 Always Load / 1002 Load Once / 1003 Do Not Load**, botón por defecto **1003 (Do Not Load)**. Sale una vez por add-in nuevo sin firma (9 en el arranque A, para 8 manifiestos, uno de ellos con 2 add-ins), antes de cargar ninguno; también al recompilar la DLL de un add-in ya aceptado y al aparecer un manifiesto nuevo con Revit abierto. Tras Always Load no vuelve a preguntar para esa DLL. Al ser `TaskDialog` con Id, en principio se podría responder con `DialogBoxShowing`, pero sale antes de que cargue ningún add-in
- [x] **ENT-07** Excepción en `OnStartup`: qué ve el usuario y si Revit sigue · H: ⚠️ · Método: M/J · R: ⚠️ · Obs: Una **excepción no controlada en `OnStartup`** no detiene Revit: el resto de add-ins carga y, al final del arranque ("start up warnings for AddIns"), Revit muestra **"External Tools - External Tool Failure: Revit cannot run the external application \"RevitLab ENT-07 excepción\". Contact the provider for assistance. Information they provided to Revit about their identity: …"** (Id `TaskDialog_External_Tools_External_Tool_Failure`, con "Show details" y "Do not show me this message again"). Un `OnStartup` que devuelve **`Result.Failed` no muestra ningún diálogo**: solo `API_ERROR` en el journal
- [x] **ENT-08** Dos add-ins con versiones distintas de la misma dependencia (p. ej. Newtonsoft.Json 12 y 13) · H: ⚠️ · Método: A · R: ⚠️ · Obs: Dos add-ins en el contexto por defecto, uno con Newtonsoft.Json **12.0.3** y otro con **13.0.3** en su carpeta: **los dos reciben el Newtonsoft.Json 13.0.0.0 (archivo 13.0.1) que Revit ya tiene cargado** desde `C:\Program Files\Autodesk\Revit 2027`; sus copias se ignoran. El que pedía la 12 recibe la 13 (journal: "Addin's module Newtonsoft.Json of version 12.0.0.0 conflicts with same preloaded module of version 13.0.0.0"), que funcionó aquí por compatibilidad hacia atrás. En el contexto por defecto, quien carga primero (aquí Revit) decide la versión para todos
- [x] **ENT-09** Lo mismo que ENT-08 con `UseRevitContext=false` y `ContextName` (aislamiento 2026+) · H: ✅ · Método: A · R: ✅ · Obs: **El aislamiento de 2027 funciona.** Con `<ManifestSettings><UseRevitContext>False</UseRevitContext><ContextName>…</ContextName></ManifestSettings>`, cada add-in se carga en su propio `AssemblyLoadContext` (`REVITLAB.ISO12`, `REVITLAB.ISO13`) y usa **su propia** Newtonsoft (12.0.3 y 13.0.3, desde su carpeta), no la de Revit (journal: "The requested assembly 'Newtonsoft.Json, Version=12.0.0.0…' was loaded to the context 'REVITLAB.ISO12'"). Contradice el informe del foro para 2026.1 ("no aislaba"). El ContextName aparece en mayúsculas
- [x] **ENT-10** Dependencias explícitas entre add-ins de 2027 (`Dependencies`, `PublicAssemblies`) · H: ✅ · Método: A · R: ⚠️ · Obs: **Funciona, con tres trampas no documentadas** (6 arranques; ver addins/ent/instalar.py). Montaje: el contexto aislado `RevitLab.Iso13` lleva y publica `Newtonsoft.Json.dll` 13.0.3 y `RevitLab.Compartida.dll` (`<PublicAssemblies>`); el contexto `RevitLab.Dep` no los lleva y declara `<Dependencies><DependsOnContext ContextName="RevitLab.Iso13" /></Dependencies>`. **Resultado final:** Dep recibe las dos DLL desde ISO13 (journal: "The context `REVITLAB.DEP` resolved assembly 'RevitLab.Compartida…' as a PublicAssembly from the context 'REVITLAB.ISO13'"; Newtonsoft 13.0.3 de ISO13, no la 13.0.1 de Revit). **Trampas:** (1) `<PublicAssemblies><Assembly>` es una **ruta de archivo** (con .dll) que se resuelve contra el **directorio de trabajo de Revit**, no contra la carpeta del add-in ni la del manifiesto (DBG_WARN "Attempt to register the public assembly 'D:\…\revitlab-2027\Newtonsoft.Json.dll' that can't be found"); con el manifiesto en %APPDATA% hay que usar ruta absoluta (Autodesk pone el manifiesto junto a sus DLL). (2) `DependsOnContext` con `<AssemblyName>RevitLab.Compartida</AssemblyName>` se registra ("for 2 assembly/assemblies") pero **no resuelve** (FileNotFoundException); **sin** `<AssemblyName>` ("for all public assemblies"), como en los add-ins de Autodesk, sí. (3) Los manifiestos se procesan en **orden alfabético**: el proveedor debe cargar antes que el dependiente. Mientras la dependencia no resuelve, el dependiente cae en silencio a la versión que tenga Revit en el contexto por defecto (Newtonsoft 13.0.1)
- [x] **ENT-11** Dependencia que choca con una DLL que ya carga Revit (versión distinta) · H: ⚠️ · Método: A/J · R: ⚠️ · Obs: Una dependencia que ya carga Revit (Newtonsoft.Json, 13.0.1 en la carpeta de Revit) **gana siempre en el contexto por defecto**: el add-in recibe la de Revit aunque traiga otra versión (ver ENT-08), incluida la misma versión de ensamblado (13.0.0.0) pero otra de archivo (13.0.3): se queda con 13.0.1. Solución: aislar el add-in (ENT-09)
- [x] **ENT-12** `Nice3point.TUnit.Revit` 2027.0.7: ejecución desatendida con `dotnet run` y licencia educativa · H: ❓ · Método: P/J · R: ⚠️ · Obs: `Nice3point.TUnit.Revit` 2027.0.7 (TUnit + Microsoft Testing Platform; depende de PolyHook2.NET): un proyecto de pruebas con `RevitApiTest` compila a la primera y `dotnet run` **pasa la prueba de API en 16 s (27 s con el arranque), sin ventana y sin lanzar Revit.exe**: el motor de Revit se carga **dentro del propio proceso de pruebas** (`RevitLab.Ent12`, 537 módulos; build 27.1.0.45, idioma ENU), con *detours* de PolyHook (cadenas de la DLL: `Nice3point.Revit.Injector`, `detour`, `InRevitProcess`; para las pruebas de UI, `CreateProcessW` + `RevitUiConnection`, es decir, lanza un Revit.exe aparte). **Hallazgo delicado:** en ese proceso **no se carga ningún módulo de licencias de Autodesk**, mientras que un Revit.exe normal carga `AdskLicensingSDK_10.dll` y `AdskIdentitySDK.dll` (937 módulos). Es decir, la API corre sin la comprobación de licencia que hace Revit. El README solo dice "You must have a licensed copy of Autodesk Revit installed". Por coherencia con SHL-02 (no eludir el licenciamiento), **se detuvo la prueba aquí**: no se ejecutó la prueba de UI ni se adopta como alternativa al runner; usarla es decisión del usuario tras revisar los términos de Autodesk. Ejemplo en addins/ent12
- [x] **ENT-13** Arrancar Revit por línea de comandos con un journal que abre un modelo y ejecuta un comando del add-in · H: ⚠️ · Método: M/J · R: ✅ · Obs: **Lote local sin nube, desatendido:** `Revit.exe "addins\ent\ent13-journal.txt"` arranca Revit, abre `interactivo.rvt`, ejecuta un comando de add-in instalado por manifiesto (`Type="Command"`) y **se cierra solo: 29 s, código de salida 0**, sin intervención. El comando vio `IsJournalPlaying()=true`. Sintaxis del journal, copiada de journals reales de esta instalación: abrir con `Jrn.Command "Ribbon", "Open an existing project , ID_REVIT_FILE_OPEN"` + `Jrn.Data "File Name", "IDOK", "<ruta>"`; comando por manifiesto con `Jrn.RibbonEvent "Execute external command:<ClientId>:<FullClassName>"` (independiente de la cinta; los botones creados por código usan `CustomCtrl_%…%<pestaña>%<panel>%<botón>:<clase>`); salir con `Jrn.Command "Internal", "Quit the application; prompts to save projects , ID_APP_EXIT"`. Requisito: el add-in debe estar aceptado antes (un aviso de seguridad en la reproducción la detendría), y el comando no debe abrir diálogos. Observación sin explicar: antes de esa prueba, cerrar Revit con `ExitRevit` tardó ~2,7 min (otras veces, ~11-22 s)
- [x] **ENT-14** Uso de librerías modernas de .NET 10 (`System.Text.Json`, `HttpClient`, `Channels`) dentro del add-in · H: ✅ · Método: A · R: ✅ · Obs: `System.Text.Json`, `HttpClient`, PLINQ/`Task.Run` y NuGet administrado (Microsoft.Data.Sqlite) funcionan dentro de Revit en .NET 10 (DAT-06, DAT-07, CTX-08)
- [x] **ENT-15** `BinaryFormatter` (Revit lo habilita con `EnableUnsafeBinaryFormatterSerialization=true`) · H: ✅ · Método: A · R: ❌ · Obs: Revit 2027 pone `EnableUnsafeBinaryFormatterSerialization=true`, pero en .NET 10 `BinaryFormatter` lanza siempre `PlatformNotSupportedException` ("have been removed"): la opción ya no tiene efecto. Los add-ins antiguos que serializaban con `BinaryFormatter` se rompen en 2027

### CTX · Contexto de API e hilos (el límite estructural)

- [x] **CTX-01** Llamar a la API desde un hilo de fondo (`Task.Run`) · H: ❌ · Método: A/J · R: ⚠️ · Obs: No tumba Revit. Desde `Task.Run` (hilo 11, sin SynchronizationContext), mientras el hilo de Revit espera: **la lectura funciona sin ninguna protección** (`FilteredElementCollector` devolvió 2 niveles) y **la escritura se bloquea** con `InvalidOperationException` "Cannot modify the document for either a read-only external command is being executed, or changes to the document are temporarily disabled". Revit protege las escrituras, no las lecturas: una lectura concurrente puede devolver datos inconsistentes sin dar error
- [x] **CTX-02** `async/await` dentro de `Execute`: código después del `await` · H: ❌ · Método: A/J · R: ❌ · Obs: El hilo de Revit tiene un `WindowsFormsSynchronizationContext`: después del `await`, el código vuelve al **mismo hilo principal (2)**, pero **fuera del contexto de API**. La lectura funciona; la escritura se rechaza con `InvalidOperationException` "Starting a transaction from an external application running outside of API context is not allowed". El mensaje es explícito: Revit detecta el contexto, no el hilo
- [x] **CTX-03** Guardar `Document` o `UIApplication` en un campo estático y usarlo desde un temporizador · H: ❌ · Método: A/J · R: ⚠️ · Obs: Desde un `System.Threading.Timer` (hilo 67) con el `Document` capturado: **la lectura funciona sin protección** y la escritura se bloquea con `InvalidOperationException` "Cannot modify the document … temporarily disabled". Mismo comportamiento que CTX-01
- [x] **CTX-04** Ventana WPF no modal que modifica el modelo mediante `ExternalEvent` · H: ✅ · Método: M/A · R: ✅ · Obs: Ventana WPF no modal (`Window.Show()` con `Owner` = `MainWindowHandle`) abierta desde un botón: Revit sigue siendo utilizable con ella abierta. Su botón "Crear muro (ExternalEvent)" crea el muro (`Committed`, en el hilo principal); el botón que llama a la API **directamente** desde el clic de WPF lanza `InvalidOperationException` ("Starting a transaction from an external application running outside of API context is not allowed"). Confirma la regla: todo cambio desde WPF no modal tiene que pasar por `ExternalEvent`
- [x] **CTX-05** `ExternalEvent.Raise()` mientras hay un diálogo modal abierto en Revit · H: ⚠️ · Método: M · R: ⚠️ · Obs: **El `ExternalEvent` espera a que se cierre el diálogo modal.** Medido por la propia prueba con `IsWindowEnabled(MainWindowHandle)` (un modal deshabilita la ventana principal): el diálogo se abrió a las 18:49:29; `Raise()` a las 18:49:58 → `Accepted` con el modal abierto; el handler se ejecutó a las 18:50:26, al cerrarse el diálogo (28 s de espera). Explica por qué el runner queda bloqueado con un diálogo abierto (TRX-07). Los dos primeros intentos fueron ambiguos porque dependían de que el usuario abriera el diálogo a tiempo; la medición automática los sustituye. Run 20261027-184913-079
- [x] **CTX-06** `ExternalEvent.Raise()` mil veces seguidas: cuántas ejecuciones se producen · H: ⚠️ · Método: P · R: ⚠️ · Obs: 1000 `Raise()` seguidos desde un hilo de fondo → `Accepted`=1 y `Pending`=999 → **una sola ejecución** del handler, en el hilo principal (2). Las llamadas se fusionan mientras hay una pendiente: el patrón correcto es una cola propia más un `Raise()`, como hace el runner. Además: `ExternalEvent.Create` funciona dentro de otro `ExternalEvent`, y el evento creado impide descargar el contexto (ver DEV-07)
- [x] **CTX-07** `Idling` con `SetRaiseWithoutDelay`: frecuencia real y consumo de CPU · H: ⚠️ · Método: P · R: ⚠️ · Obs: `UIApplication.Idling` + `SetRaiseWithoutDelay` depende por completo del foco: con Revit en **segundo plano**, 1 evento en 8,9 s; con Revit en **primer plano** y sin tocar nada, **68.396 eventos en 10 s (6.840/s)**: un bucle continuo que consume CPU. Idling no sirve como temporizador para automatización desatendida, y con `SetRaiseWithoutDelay` en primer plano conviene limitar el trabajo por evento. Runs del 2026-10-18 y 20261027-184352-363
- [x] **CTX-08** Cálculo pesado en un hilo de fondo sin API y escritura del resultado mediante `ExternalEvent` · H: ✅ · Método: A/P · R: ✅ · Obs: **Patrón correcto:** `ExternalEvent` creado en contexto de API → cálculo pesado en `Task.Run` + PLINQ (216.816 primos en 0,7 s, con Revit libre) → `Raise()` desde el hilo de fondo → el handler escribe en el hilo principal (2). **Error típico:** `ExternalEvent.Create` desde un hilo de fondo lanza "Attempting to create an ExternalEvent outside of a standard API execution", y dentro de un `Task` sin `try/catch` el fallo pasa **en silencio**
- [x] **CTX-09** Si la interfaz de Revit responde durante una operación de API de 60 s · H: ❌ · Método: M · R: ⚠️ · Obs: **Una operación de API larga congela Revit** (20 s creando muros, 74.286 en una transacción: el usuario no pudo resaltar la cinta, hacer zoom ni mover la ventana). **Bombeando mensajes cada 100 ms** (`Dispatcher.PushFrame`, el equivalente a `DoEvents`) la interfaz **responde** (confirmado por el usuario), a cambio de un −27 % de rendimiento (54.141 muros) y de un riesgo de reentrada (el usuario podría lanzar comandos en mitad de la transacción). Además, crear y deshacer 74.000 muros dejó +1,2 GB de memoria que no se liberó (1,15 → 2,39 GB). Runs 20261027-184051-898 y 184121-544
- [x] **CTX-10** Barra de progreso propia durante una operación larga · H: ⚠️ · Método: M · R: ✅ · Obs: No hay API para una barra de progreso del add-in (solo `Application.ProgressChanged` para la de Revit), pero una **ventana WPF propia no modal con `ProgressBar`**, actualizada cada 100 ms y bombeando mensajes, avanza con fluidez durante una operación de API (confirmado por el usuario en 2 ejecuciones). Sin bombear no se repintaría (ver CTX-09)
- [x] **CTX-11** Cancelar una operación larga (Esc o botón propio) · H: ⚠️ · Método: M · R: ✅ · Obs: Cancelar una operación larga: con mensajes bombeados, el botón **Cancelar** de la ventana WPF (a los 6,0 s, 19 %) y la tecla **Esc** sondeada con `GetAsyncKeyState` (a los 5,8 s, 18 %) interrumpen el bucle; la transacción se deshace (`RollBack`) y el modelo queda intacto. Runs 20261027-184220-121 y 184229-241
- [x] **CTX-12** Dos instancias de Revit a la vez, cada una con su runner (paralelismo por procesos) · H: ✅ · Método: P · R: ✅ · Obs: **La licencia educativa permite dos Revit a la vez**, cada uno con su runner. El runner se cambió (RUNNER, 2026-10-28): cada instancia escucha en `revitlab-<pid>` y solo la primera también en `revitlab` (si dos procesos sirven el mismo nombre, el cliente se conecta a cualquiera al azar); `lab` admite `--pid N` y `lab instancias` lista los pipes. La segunda instancia volvió a pedir Always Load para el runner recompilado, aunque ya se había aceptado en la primera. **Paralelismo por procesos:** crear y cerrar 30 documentos tarda ~14 s en una instancia; dos instancias con 30 cada una a la vez, **18 s para 60 (×1,5 de rendimiento)**, con cada una un 19 % más lenta (16,4 s) y ~1 GB de memoria por proceso. El estado compartido entre instancias (por ejemplo, un archivo de registro común) choca: el primer intento falló por acceso concurrente al mismo archivo
- [x] **CTX-13** Servidor HTTP o pipe propio (el runner) aceptando peticiones mientras Revit está ocupado · H: ⚠️ · Método: A · R: ✅ · Obs: **Antes del arreglo (run 20261027-154422-784):** con el hilo de Revit ocupado 12 s, 0/4 `eco` conectaron y `lab eco`/`lab ping` fallaron con un mensaje engañoso, porque el `PipeServer` tenía una sola instancia y atendía una orden cada vez. **Después (RUNNER-2: `MaxAllowedServerInstances` + una tarea por conexión, run 20261027-162813-531):** 4/4 `eco` responden en 0 ms con Revit ocupado; `lab eco` externo en 72 ms; `lab ping` externo espera su turno en la cola y responde a los 9,6 s, cuando Revit queda libre. Las órdenes que necesitan Revit se encolan (`ConcurrentQueue` + `ExternalEvent.Raise`) y las que no lo necesitan responden siempre

### SHL · Fuera de Revit (sin interfaz)

- [x] **SHL-01** App de consola .NET 10 que referencia `RevitAPI.dll` y crea `XYZ` (tipo de valor sencillo) · H: ⚠️ · Método: S · R: ⚠️ · Obs: Funciona en consola .NET 10 sin Revit: `XYZ`, `Line.CreateBound`, `GeometryCreationUtilities.CreateExtrusionGeometry` (volumen 2,0 correcto). Requiere `SetDllDirectory` a la carpeta de Revit y resolver ensamblados desde ahí. No soportado por Autodesk. Evidencia: `tools/ShlProbe` (2026-10-18)
- [x] **SHL-02** App de consola que intenta abrir un `.rvt` (`Application` sin Revit) · H: ❌ · Método: S · R: ❌ · Obs: `Application` no tiene constructor público (solo 3 no públicos: `()`, `(IntPtr)`, `(Object)`), así que no hay vía soportada para abrir un RVT fuera de Revit. No se intentó invocar los constructores internos: sería eludir el licenciamiento
- [x] **SHL-03** Leer metadatos de un `.rvt` sin Revit (`BasicFileInfo.Extract`) · H: ❓ · Método: S · R: ✅ · Obs: `BasicFileInfo.Extract` funciona fuera de Revit sobre .rfa y .rte (`Format=2027`, `IsWorkshared=False`). Útil para clasificar archivos antes de un lote
- [x] **SHL-04** Leer la vista previa o el OLE del `.rvt` sin Revit (formato de almacenamiento estructurado) · H: ✅ · Método: S · R: ✅ · Obs: `tools/ShlProbe ole <archivo>` abre el RVT/RFA con `StgOpenStorage` de ole32 (sin RevitAPI ni paquetes) en 75–140 ms. Contiene 18 entradas: `Global/` (Latest, History, ElemTable, PartitionTable, ContentDocuments, DocumentIncrementTable), `Formats/Latest`, `Contents`, `Partitions/<n>`, `BasicFileInfo`, `RevitPreview4.0`, `TransmissionData` y `ProjectInformation` (o `PartAtom` en un RFA). **Vista previa:** un PNG de 128×128 a partir del byte 235 de `RevitPreview4.0`, extraído a pruebas/resultados/shl04. **`BasicFileInfo`** es texto UTF-16 *little-endian* seguido de un resumen en *big-endian*: versión (Format 2027), build, worksharing, ruta del último guardado, GUID del central y configuración regional. Ojo, privacidad: la ruta del último guardado viaja dentro del archivo (el RFA de muestra revela la carpeta Downloads de un usuario de Autodesk)
- [x] **SHL-05** Inventario completo de la API por reflexión (clases, métodos, enums) en `pruebas/api-surface-2027.json` · H: ✅ · Método: S · R: ✅ · Obs: RevitAPI 27.1 (2.556 tipos, 29.792 miembros), RevitAPIUI (155 / 1.401), RevitAPIIFC (42 / 430); 85 miembros `[Obsolete]`. Salida: `pruebas/resultados/api-surface-2027.{json,tsv}`; herramienta: `tools/ApiInventory`

### TRX · Transacciones, deshacer y fallos

- [x] **TRX-01** Modificar el modelo sin transacción · H: ❌ · Método: A · R: ❌ · Obs: Esperado: `Autodesk.Revit.Exceptions.ModificationOutsideTransactionException` ("Modifying is forbidden because the document has no open transaction"). Es una excepción capturable, no tumba Revit
- [x] **TRX-02** Dos `Transaction` anidadas · H: ❌ · Método: A · R: ❌ · Obs: No se puede (esperado): `Autodesk.Revit.Exceptions.InvalidOperationException` "Starting a new transaction is not permitted...". Para anidar hay que usar `SubTransaction` o `TransactionGroup`
- [x] **TRX-03** `SubTransaction` dentro de una transacción, con rollback parcial · H: ✅ · Método: A · R: ✅ · Obs: El muro creado dentro de la `SubTransaction` deshecha queda inválido y el de la transacción principal se conserva (1 muro tras el commit)
- [x] **TRX-04** `TransactionGroup.Assimilate()`: N transacciones en un solo deshacer · H: ✅ · Método: A/M · R: ✅ · Obs: A: `Assimilate()` devuelve `Committed`. **M (confirmado en la lista de Deshacer):** el grupo asimilado aparece como **una sola entrada con el nombre del grupo** ("RevitLab TRX-04: grupo ASIMILADO de 3"), sin los nombres internos. En cambio, un `TransactionGroup` con `Commit()` **no aparece**: sus transacciones internas salen como entradas separadas ("interna sin asimilar 2", "interna sin asimilar 1")
- [x] **TRX-05** `RollBack()` restaura exactamente el estado (comparar el recuento y un hash de parámetros) · H: ✅ · Método: A · R: ✅ · Obs: La huella (ids, puntos finales y alturas de 10 muros) es idéntica tras borrar 5, mover 5, cambiar alturas y hacer `RollBack`
- [x] **TRX-06** Comando con `TransactionMode.ReadOnly` que intenta escribir · H: ❌ · Método: A · R: ✅ · Obs: Un comando con `[Transaction(TransactionMode.ReadOnly)]` lanzado desde un botón: `Transaction.Start` lanza `Autodesk.Revit.Exceptions.InvalidOperationException` ("Cannot modify the document for either a read-only external command is being executed, or changes to the document are temporarily disabled"). Repetido 3 veces con el mismo resultado
- [x] **TRX-07** Advertencias suprimidas con `IFailuresPreprocessor` (p. ej. muros superpuestos) · H: ✅ · Método: A · R: ⚠️ · Obs: **Hallazgo doble.** (1) Sin preprocesador, un aviso de una transacción de API abre el diálogo **modal** `Dialog_Revit_DocWarnDialog`, que **bloqueó la API 146 s** hasta que se cerró a mano. (2) Con `IFailuresPreprocessor` + `DeleteWarning` no hay diálogo (0,3 s), pero **el aviso queda guardado en el documento** (`GetWarnings()` = 1): solo se quita del procesamiento de la transacción. Además, ver DEV-07: el preprocesador retiene el contexto de carga
- [x] **TRX-08** Errores graves resueltos por código (`FailureResolution`, `DeleteElements`) · H: ⚠️ · Método: A · R: ✅ · Obs: **Errores graves resueltos por código.** El runner registra en `OnStartup` un error propio (`FailureDefinition.CreateFailureDefinition(id, FailureSeverity.Error, …)` + `AddResolutionType(DeleteElements, …, typeof(DeleteElements))`; solo se puede hacer durante el arranque, así que hubo que cambiar el runner y reiniciar Revit). En una transacción que además cambia el comentario de otro muro, `doc.PostFailure(new FailureMessage(id).SetFailingElement(muro).AddResolution(DeleteElements, DeleteElements.Create(doc, muro)))`. Con un `IFailuresPreprocessor` que hace `SetCurrentResolutionType(DeleteElements)` + `ResolveFailure` + `ProceedWithCommit`: **`Committed`, el muro con el error se borra y el resto de la transacción se conserva** (el comentario del otro muro queda cambiado). Control (TRX-08b): sin resolver y `ProceedWithRollBack` → `RolledBack`, el muro sigue y se deshace también el comentario. El preprocesador ve la severidad Error, 1 elemento, 1 resolución y su texto ('Borrar el elemento'). Ojo: después de la resolución, leer `.Id` del elemento borrado lanza "The referenced object is not valid" (guardar los `ElementId` antes). Runs del 2026-10-28
- [x] **TRX-09** Transacción gigante: 100.000 cambios en un `Commit` (tiempo y memoria) · H: ⚠️ · Método: P · R: ✅ · Obs: 20.000 líneas de modelo en una transacción: 1,0 s creando (51 µs por elemento) + **3,7 s de Commit** + 203 MB. El Commit cuesta 3,6 veces más que crear
- [x] **TRX-10** Coste de `doc.Regenerate()` en bucle frente a una sola regeneración · H: — · Método: P · R: ⚠️ · Obs: 200 movimientos con `Regenerate()` en cada paso: 373 ms; con una sola regeneración: 374 ms. Sin diferencia: `MoveElement` parece regenerar por su cuenta, o `Regenerate()` sin cambios pendientes no hace nada. Repetir con cambios de parámetros, que no regeneran solos
- [x] **TRX-11** Nombre de la transacción visible en Deshacer de Revit · H: ✅ · Método: M · R: ✅ · Obs: El nombre de la `Transaction` aparece tal cual en la lista de Deshacer ("RevitLab TRX-11: un muro"), confirmado por el usuario. También las de `Tx.Ejecutar` ("AVF estilo")
- [x] **TRX-12** Deshacer o rehacer programático (sin API; prueba con `PostCommand` Undo) · H: ⚠️ · Método: A/M · R: ✅ · Obs: No hay método `Undo`, pero `PostCommand(PostableCommand.Undo)` funciona: `CanPostCommand`=true y, al terminar la orden, deshace la última entrada (de 11 a 10 muros, comprobado en el run siguiente). Asíncrono: se ejecuta después de que el código de la API devuelve el control

### ELM · Creación y edición de elementos

- [x] **ELM-01** Muro recto por línea (`Wall.Create`) · H: ✅ · Método: A · R: ✅ · Obs: `Wall.Create` en un proyecto nuevo desde `Default_M_ENU.rte`: 10,000000 m exactos (conversión con `UnitUtils`), tipo 'Generic - 200mm', 1,2 s incluida la creación del documento
- [x] **ELM-02** Muro por perfil (no rectangular) · H: ✅ · Método: A/V · R: ✅ · Obs: `Wall.Create(doc, IList<Curve> perfil, tipo, nivel, false)` con un trapecio vertical: área 24,000 m² exacta
- [x] **ELM-03** Suelo por contorno (`Floor.Create`) con hueco · H: ✅ · Método: A/V · R: ✅ · Obs: `Floor.Create` con dos `CurveLoop` (exterior + hueco): 96,000 m² exactos
- [x] **ELM-04** Cubierta por perímetro (`NewFootPrintRoof`) con pendientes · H: ✅ · Método: A/V · R: ⚠️ · Obs: `NewFootPrintRoof` + `set_DefinesSlope`/`set_SlopeAngle` en los 4 lados: cumbrera a 2,34 m. **Rareza:** el parámetro `out ModelCurveArray` debe pasarse **ya inicializado** (`new ModelCurveArray()`); con `out var` lanza `ArgumentNullException`
- [x] **ELM-05** Techo (`Ceiling.Create`) · H: ✅ · Método: A · R: ✅ · Obs: `Ceiling.Create`: 24,000 m² exactos
- [x] **ELM-06** Puerta y ventana alojadas en un muro (`NewFamilyInstance` con anfitrión) · H: ✅ · Método: A/V · R: ✅ · Obs: `NewFamilyInstance(XYZ, símbolo, muro anfitrión, nivel, NonStructural)` con la puerta M_Single-Flush y la ventana M_Fixed de la plantilla; posición x=3,000 m exacta y antepecho fijado a 0,900 m. Hay que `Activate()` el símbolo antes. Fue la prueba más lenta (4,4 s, carga inicial de la familia)
- [x] **ELM-07** Pilares y vigas estructurales · H: ✅ · Método: A · R: ✅ · Obs: 2 pilares W250 con nivel superior fijado por parámetro + viga W310 de 6,000 m mediante `NewFamilyInstance(Curve, …, StructuralType.Beam)`
- [x] **ELM-08** Muro cortina: rejillas, cambio de panel y montantes · H: ⚠️ · Método: A/V · R: ⚠️ · Obs: Muro cortina 8×3 m: 3 líneas verticales + 1 horizontal, 10 montantes (`CurtainGridLine.AddMullions`), 8 paneles y uno cambiado a 'Solid' (desanclarlo antes). V: rejilla 4×2 visible. **Semántica contraintuitiva (ELM-08b):** `AddGridLine(isU=true)` crea una línea **horizontal** a la Z del punto (ignora X), y `isU=false` una **vertical**. Si la línea queda duplicada devuelve **null** sin excepción
- [x] **ELM-09** Escalera por componentes (`StairsEditScope`) y barandilla · H: ⚠️ · Método: A/V · R: ⚠️ · Obs: **Benchmark #11 (dificultad 5), resuelto en 3 ejecuciones.** `StairsEditScope.Start(nivel1, nivel2)` + 2 `StairsRun.CreateStraightRun` + `StairsLanding.CreateAutomaticLanding` + `scope.Commit(IFailuresPreprocessor)`: escalera en U con 23/23 contrahuellas de 173,9 mm. **Rarezas C5:** (1) Revit **crea barandillas automáticamente** al confirmar, así que `Railing.Create` lanza "already has associated railings" (usar `GetAssociatedRailings`); (2) un recorrido de longitud L·huella da **L+1 contrahuellas**: para k contrahuellas hay que usar (k−1)·huella. V: tramo y descansillo con barandilla visibles
- [x] **ELM-10** Niveles, rejillas y planos de referencia · H: ✅ · Método: A · R: ✅ · Obs: `Level.Create`, `Grid.Create` (recta y arco) y `NewReferencePlane` (requiere una vista); se pueden renombrar todos
- [x] **ELM-11** Habitaciones, líneas de separación y cálculo de área y volumen · H: ✅ · Método: A · R: ✅ · Obs: `NewRoom(nivel, UV)` tras regenerar los muros (en una transacción aparte): 27,840 m² (interior exacto de los muros de 200 mm), 67,885 m³. Detalle: `Room.Name` devuelve "Laboratorio 1" (nombre + número)
- [x] **ELM-12** Áreas y planos de área · H: ✅ · Método: A · R: ✅ · Obs: `ViewPlan.CreateAreaPlan` ('Gross Building') + 4 `NewAreaBoundaryLine` + `NewArea`: 60,000 m² exactos
- [x] **ELM-13** Toposólido (`Toposolid.Create`) · H: ✅ · Método: A/V · R: ✅ · Obs: `Toposolid.Create` con 121 puntos (malla 30×30 m con relieve senoidal)
- [x] **ELM-14** `DirectShape` con geometría arbitraria (sólido o malla) · H: ✅ · Método: A/V · R: ✅ · Obs: Sólido booleano (`BooleanOperationsUtils` diferencia) → `DirectShape` en Generic Models: 9,0000 m³ exactos
- [x] **ELM-15** Copiar, mover, rotar, reflejar y matrices (`ElementTransformUtils`, `LinearArray`) · H: ✅ · Método: A · R: ✅ · Obs: Move, Copy, Rotate (45°), Mirror con copia y `LinearArray` ×5: 7 muros, como se esperaba
- [x] **ELM-16** Copiar elementos entre dos documentos abiertos · H: ✅ · Método: A · R: ✅ · Obs: `ElementTransformUtils.CopyElements(docOrigen, ids, docDestino, Transform, CopyPasteOptions)`: 4 muros copiados con traslación
- [x] **ELM-17** Borrar un elemento y ver qué dependientes se van (`doc.Delete` devuelve IDs) · H: ✅ · Método: A · R: ✅ · Obs: `doc.Delete(muro)` devuelve 3 ids: el muro, la puerta y la ventana alojadas
- [x] **ELM-18** Grupos: crear, desagrupar, colocar instancias, editar un grupo · H: ⚠️ · Método: A · R: ✅ · Obs: `NewGroup` (4 muros + puerta), renombrar `GroupType`, `PlaceGroup` ×2 y `UngroupMembers` de una copia: 2 grupos y 5 elementos sueltos, 12 muros. Contar después del commit
- [x] **ELM-19** Conjuntos (assemblies) y partes (`PartUtils`) · H: ✅ · Método: A · R: ✅ · Obs: `PartUtils.CreateParts` sobre 2 muros → 2 partes; `AssemblyInstance.Create` con 2 muros → conjunto 'Walls 001'
- [x] **ELM-20** Opciones de diseño: crear un conjunto u opción · H: ❌ · Método: S/A · R: ❌ · Obs: S: `DesignOption` solo expone `GetActiveDesignOptionId` e `IsPrimary`. No hay ningún método que cree opciones o conjuntos de opciones
- [x] **ELM-21** Fases: crear una fase nueva · H: ❓ · Método: S/A · R: ❌ · Obs: No hay forma de crear fases: `doc.Phases.Append` lanza `InvalidOperationException` "Collection is read-only" y `Phase` tiene 0 constructores públicos
- [x] **ELM-22** Scope boxes: crear · H: ❌ · Método: S · R: ❌ · Obs: S: no hay método de creación; solo `PostableCommand.ScopeBox` (herramienta interactiva). El workaround `ElementTransformUtils.CopyElements` de una caja existente queda por probar con A
- [x] **ELM-23** Familias in situ: crear · H: ❌ · Método: S · R: ❌ · Obs: S: solo `PostableCommand.ModelInPlace`/`InPlaceMass` (interactivo) y la lectura `Family.IsInPlace`
- [x] **ELM-24** Worksets: crear, renombrar y asignar elementos (con worksharing activo) · H: ✅ · Método: A · R: ✅ · Obs: `EnableWorksharing` funciona en un proyecto **nuevo sin guardar** (fuera de transacción); `Workset.Create` y asignación por `ELEM_PARTITION_PARAM`
- [x] **ELM-25** Anclar o desanclar y unir geometría (`JoinGeometryUtils`) · H: ✅ · Método: A · R: ✅ · Obs: `Pinned = true` y `JoinGeometryUtils.JoinGeometry(suelo, muro)`: unidos y el suelo corta al muro
- [x] **ELM-26** Unión o recorte de extremos de muros (`WallUtils`) · H: ⚠️ · Método: A/V · R: ✅ · Obs: `WallUtils.DisallowWallJoinAtEnd(muro, 1)`: solo ese extremo deja de unirse
- [x] **ELM-27** Línea de propiedad (`PropertyLine.Create`, nuevo en 2027) · H: ✅ · Método: A · R: ✅ · Obs: `PropertyLine.Create(doc, [CurveLoop])`: área 600,00 m² exacta en `PROPERTY_AREA`
- [x] **ELM-28** Renumeración por reglas (`NumberingSchema.Create`, `ScopeDefiningFilter`, nuevo en 2027) · H: ✅ · Método: A · R: ✅ · Obs: **Numeración por reglas de 2027:** `NumberingSchema.Create(doc, nombre, ALL_MODEL_MARK, ParameterFilterElement de Doors, 0)` numera **automáticamente** las 6 puertas (Mark 1–6). **Corrección a la investigación:** `ScopeDefiningFilter` **no es una clase**; es la propiedad `NumberingSchema.ScopeDefiningFilter` (el id del filtro)

### ANO · Anotación y documentación 2D

- [x] **ANO-01** Notas de texto con formato · H: ✅ · Método: A/V · R: ✅ · Obs: `TextNote.Create` + `FormattedText` (`SetBoldStatus`/`SetUnderlineStatus` por `TextRange`): el formato parcial se confirma al releer
- [x] **ANO-02** Cotas lineales entre caras de muros (`NewDimension` con `Reference`) · H: ⚠️ · Método: A/V · R: ⚠️ · Obs: **Benchmark #9 (dificultad 4), primer intento.** Referencias con `HostObjectUtils.GetSideFaces` (exterior e interior), ordenadas por la proyección de `PlanarFace.Origin`, y `NewDimension` encadenada: 0,2 + 7,8 + 0,2 m y 0,2 + 4,8 + 0,2 + 6,8 + 0,2 m exactos. **V:** las dos cadenas se cruzan dentro del edificio y los textos '4800' y '7800' se superponen. Es correcto numéricamente pero mal resuelto gráficamente, y ninguna aserción lo detectaría
- [x] **ANO-03** Etiquetas (`IndependentTag.Create`) con directriz; control del inicio de la directriz (nuevo en 2027) · H: ✅ · Método: A/V · R: ✅ · Obs: `IndependentTag.Create(…, TM_ADDBY_CATEGORY, …)` con directriz sobre una puerta; `LeaderStartCondition` (2027): Attached y Free asignables
- [x] **ANO-04** Etiquetas de habitación y de puerta en masa (1.000 o más) · H: ✅ · Método: P/V · R: ⚠️ · Obs: **Hallazgo de rendimiento:** 1.000 `IndependentTag` de muro + 100 `NewRoomTag` = **980 s (890 ms por etiqueta)**, en un modelo con una rejilla de muros que se cruzan, 100 habitaciones y 978 muros sueltos. Son órdenes de magnitud más que cualquier otra operación medida. Pendiente de aislar en LIM (¿regeneración de la vista o de las habitaciones por etiqueta?)
- [x] **ANO-05** Regiones rellenas y patrones de relleno nuevos (`FillPatternElement`) · H: ✅ · Método: A/V · R: ✅ · Obs: `FillPatternElement.Create(new FillPattern(…, Drafting, ToView, 45°, 2 mm))` + `FilledRegionType.Duplicate` con color + `FilledRegion.Create` con hueco
- [x] **ANO-06** Patrones de línea y estilos de línea nuevos · H: ✅ · Método: A · R: ✅ · Obs: `LinePattern.SetSegments` (raya-espacio-punto-espacio) + `LinePatternElement.Create` + `Categories.NewSubcategory(OST_Lines)` con color, patrón y grosor → `DetailCurve.LineStyle`
- [x] **ANO-07** Nubes de revisión y revisiones · H: ✅ · Método: A · R: ✅ · Obs: `Revision.Create` (descripción y fecha) + `RevisionCloud.Create(doc, planta, revisionId, curvas)`
- [x] **ANO-08** Cotas de elevación (spot elevations) · H: ⚠️ · Método: A/V · R: ⚠️ · Obs: `NewSpotElevation` sobre `HostObjectUtils.GetTopFaces(suelo)` crea la cota, y el usuario la ve en pantalla con un valor numérico. **`SpotDimension.ValueString` devuelve null siempre** (al crear, tras `Regenerate` y tras `Commit`): no sirve para las cotas de elevación. El valor se lee con el parámetro **`SPOT_ELEV_SINGLE_OR_UPPER_VALUE`** ("0" para un suelo a 0 mm). Run 20261027-183950-726
- [x] **ANO-09** Líneas de detalle y componentes de detalle · H: ✅ · Método: A · R: ✅ · Obs: `NewDetailCurve` con estilo propio (ANO-06) y componente de detalle con `NewFamilyInstance(XYZ, símbolo, vista)` (familia 'Brick - UK Standard' de la plantilla)

### PAR · Parámetros

- [x] **PAR-01** Leer y escribir parámetros integrados, de instancia y de tipo · H: ✅ · Método: A · R: ✅ · Obs: Instancia (Comments, Mark, altura) y tipo (Type Comments), por `BuiltInParameter` y por `LookupParameter("Comments")`. `AsValueString()` formatea con las unidades del proyecto ('3250', en mm)
- [x] **PAR-02** Escribir un parámetro de solo lectura · H: ❌ · Método: A · R: ❌ · Obs: Esperado: `IsReadOnly=True` y `Set()` lanza `InvalidOperationException` "The parameter is read-only" (no devuelve false)
- [x] **PAR-03** Crear un archivo de parámetros compartidos y vincular un parámetro de proyecto a categorías · H: ✅ · Método: A · R: ✅ · Obs: Archivo de parámetros compartidos creado desde cero (`SharedParametersFilename` → `OpenSharedParameterFile` → grupo → `ExternalDefinitionCreationOptions`), vinculado a Walls con `NewInstanceBinding` + `ParameterBindings.Insert(def, binding, GroupTypeId.Data)`, con valor en el muro. Se restauró la configuración original del usuario. Ojo: lee 0,05000000000000001 m (redondeo de coma flotante pies↔m)
- [x] **PAR-04** Parámetros globales con fórmula y asociación a cotas · H: ✅ · Método: A · R: ✅ · Obs: `GlobalParameter.Create` + `SetValue(DoubleParameterValue)` y otro con `SetFormula("Ancho_Modulo * 2")` → 6 m. Falta asociarlos a cotas (requiere ANO-02)
- [x] **PAR-05** Conversión de unidades con `ForgeTypeId` / `UnitUtils` · H: ✅ · Método: A · R: ✅ · Obs: Conversión con `UnitUtils` (`ForgeTypeId`) y formato del proyecto con `UnitFormatUtils.Format` ('1000' para 3,2808 pies → el proyecto está en mm, `autodesk.unit.unit:millimeters-1.0.1`)
- [x] **PAR-06** Parámetros de familia con fórmula · H: ✅ · Método: A · R: ✅ · Obs: `FamilyManager.SetFormula(Alto, "Ancho * 2")` + `AssociateElementParameterToFamilyParameter` (cubierto en FAM-04)
- [x] **PAR-07** Escribir 100.000 valores de parámetro (tiempo) · H: — · Método: P · R: ✅ · Obs: 100.000 `Set()` en 1.000 muros: 1.475 ms (**14,8 µs por Set**) + 279 ms de commit
- [x] **PAR-08** Filtros por valor de parámetro (`ParameterFilterElement`, `ElementParameterFilter`) · H: ✅ · Método: A · R: ✅ · Obs: `ParameterFilterRuleFactory.CreateContainsRule(ElementId, string)` (la sobrecarga de 2027 ya no lleva `caseSensitive`) + `ElementParameterFilter` en el colector (4/4) + `ParameterFilterElement.Create`
- [x] **PAR-09** Borrar un parámetro de proyecto · H: ✅ · Método: A · R: ✅ · Obs: `doc.ParameterBindings.Remove(def)` → true; el parámetro desaparece del muro
- [ ] **PAR-10** Parámetros del servicio de parámetros de Autodesk (en la nube) · H: ⏭ · Método: — · R: ⏭ · Obs: fuera de alcance (nube)

### FAM · Familias

- [x] **FAM-01** Crear una familia nueva desde `.rft` (`NewFamilyDocument`) · H: ✅ · Método: A · R: ✅ · Obs: `Application.NewFamilyDocument("Metric Generic Model.rft")` + extrusión + `SaveAs(.rfa)` (444 KB). El documento de familia es invisible, como los proyectos de prueba
- [x] **FAM-02** Geometría de familia: extrusión, revolución, barrido y fundido · H: ✅ · Método: A/V · R: ⚠️ · Obs: Extrusión 1,000 m³, revolución 2,356 m³, fundido 0,624 m³ y barrido 0,640 m³: los 4 volúmenes coinciden con el cálculo manual. V: las 4 formas se ven en la imagen. **Rareza C5 (FAM-02b):** el perfil de `NewSweep` (`CurveLoopsProfile`) debe darse en **coordenadas locales** (plano XY centrado en el origen); Revit lo coloca perpendicular al recorrido. En coordenadas de modelo, aunque esté en el plano correcto, lanza `ArgumentException` genérica
- [x] **FAM-03** Tipos de familia y parámetros con `FamilyManager` · H: ✅ · Método: A · R: ✅ · Obs: `FamilyManager.NewType` ×3 (600/1000/1500) con `Set` del parámetro por tipo (cubierto en FAM-04)
- [x] **FAM-04** Restricciones: cotas etiquetadas con parámetros en la familia · H: ⚠️ · Método: A · R: ✅ · Obs: **Benchmark #10 (dificultad 4), primer intento.** Planos de referencia + `NewDimension` con `FamilyLabel = Ancho` + `NewAlignment` bloqueado entre los planos y las caras (obtenidas con `Options.ComputeReferences`) + `Alto` con fórmula `Ancho * 2` asociado a `EXTRUSION_END_PARAM`. Por tipo: 0,600×1,200, 1,000×2,000 y 1,500×3,000 m exactos
- [x] **FAM-05** Familias anidadas · H: ✅ · Método: A · R: ✅ · Obs: Caja paramétrica anidada 3 veces en otra familia (`famA.LoadFamily(famB)`) y cargada al proyecto: solo aparece la familia anfitriona; la anidada no compartida no es accesible desde la instancia (`GetSubComponentIds` = 0)
- [x] **FAM-06** `EditFamily` desde el proyecto, modificar y `LoadFamily` con `IFamilyLoadOptions` · H: ✅ · Método: A · R: ✅ · Obs: `LoadFamily(proyecto, IFamilyLoadOptions)` → instancia; `EditFamily` → cambiar Ancho en todos los tipos → `LoadFamily` con overwrite: la instancia pasa de 1,000 a 2,500 m. `EditFamily` y `LoadFamily` se llaman fuera de transacción
- [x] **FAM-07** Cargar 200 familias `.rfa` en lote (tiempo) · H: ✅ · Método: P · R: ⚠️ · Obs: 200 familias .rfa (copias de la caja paramétrica, 3 tipos cada una) cargadas con `LoadFamily(ruta)` en una transacción: **55,9 s (279,5 ms por familia)**. Es una de las operaciones más lentas medidas hasta ahora
- [x] **FAM-08** Duplicar un tipo de sistema (`WallType.Duplicate`) y editar sus capas (`CompoundStructure`) · H: ✅ · Método: A · R: ✅ · Obs: `WallType.Duplicate` + `CompoundStructure.CreateSimpleCompoundStructure` (15+200+15 mm con material) + capas de acabado: espesor total 230 mm
- [x] **FAM-09** Materiales: crear, cambiar color y editar el asset de apariencia (`AppearanceAssetEditScope`) · H: ⚠️ · Método: A/V · R: ✅ · Obs: `Material.Create` + color + duplicado de un `AppearanceAssetElement` + `AppearanceAssetEditScope` → `generic_diffuse.SetValueAsColor`: leído (200,30,30)
- [x] **FAM-10** Asset de carbono incorporado (`EmbodiedCarbonAsset`, nuevo en 2027) · H: ✅ · Método: A · R: ⚠️ · Obs: `PropertySetElement.Create(doc, new EmbodiedCarbonAsset(nombre))` + `Material.EmbodiedCarbonAssetId`: el enlace funciona, pero **`CarbonCoefficient`, la unidad, el proveedor y la categoría son de solo lectura**: un asset creado por API tiene coeficiente 0 (Undefined). La API de 2027 permite enlazar y leer, no definir los datos de carbono

### VIS · Vistas, planos y tablas

- [x] **VIS-01** Crear planta, sección, alzado, vista 3D, vista de dibujo y llamada · H: ✅ · Método: A · R: ✅ · Obs: Planta, plano de techo, sección (`BoundingBoxXYZ` con `Transform`: X a lo largo del corte, Y arriba, Z hacia el observador), 3D isométrica, vista de dibujo, alzado (`ElevationMarker.CreateElevationMarker` + `CreateElevation`) y llamada (`ViewSection.CreateCallout`)
- [x] **VIS-02** Crear una leyenda nueva desde cero · H: ❌ · Método: S/A · R: ❌ · Obs: Sin API de creación (confirmado por S) y la plantilla `Default_M_ENU.rte` no trae ninguna leyenda que duplicar. El workaround `View.Duplicate` solo sirve si el proyecto ya tiene una leyenda
- [x] **VIS-03** Plantillas de vista: crear y aplicar · H: ✅ · Método: A · R: ✅ · Obs: `View.CreateViewTemplate()` desde una planta (escala 1:50, detalle Fine) y aplicación con `ViewTemplateId`: la otra vista hereda escala y detalle
- [x] **VIS-04** Sobrescrituras gráficas por elemento y por categoría (`OverrideGraphicSettings`) · H: ✅ · Método: A/V · R: ✅ · Obs: V: el muro sur sale azul (`SetElementOverrides` con relleno sólido de corte y grosor de línea 8). Evidencia `VIS-04 - Floor Plan - Level 1.png`. **Observación:** las imágenes exportadas salen con el fondo oscuro del tema de lienzo de la interfaz
- [x] **VIS-05** Filtros de vista con colores · H: ✅ · Método: A/V · R: ✅ · Obs: V: el tabique con Comments='rojo' sale rojo mediante `ParameterFilterElement` + `AddFilter` + `SetFilterOverrides` (misma imagen que VIS-04)
- [x] **VIS-06** Rango de vista, recorte y caja de sección 3D · H: ✅ · Método: A/V · R: ✅ · Obs: `PlanViewRange.SetOffset(CutPlane, 0,6 m)`, `CropBox` activo y caja de sección 3D (`SetSectionBox`). Imágenes de la planta y del 3D exportadas
- [x] **VIS-07** Ocultar o aislar temporalmente · H: ✅ · Método: A/V · R: ✅ · Obs: `IsolateElementsTemporary` funciona desde la API sin la vista abierta en la interfaz (`IsTemporaryHideIsolateActive()`=True)
- [x] **VIS-08** Planos con cajetín y viewports colocados con precisión · H: ✅ · Método: A/V · R: ⚠️ · Obs: `ViewSheet.Create` (A0) + 4 `Viewport.Create` en cuadrícula 2×2: posición exacta (error 0,0000 mm). V: correcto, pero hay dos detalles que una aserción no ve: el nombre largo del plano se superpone con "Project Name" en el cajetín, y el 3D incluye las líneas de nivel extendidas. Evidencia: `VIS-08-plano - Sheet - A-101 ….png`
- [x] **VIS-09** Generar 500 planos con viewports (tiempo) · H: ✅ · Método: P · R: ✅ · Obs: 500 planos, cada uno con una vista de dibujo nueva y su viewport, en una transacción: 15,2 s (**30,4 ms por plano**)
- [x] **VIS-10** Tablas: campos, filtros, ordenación, agrupación, formato y fórmulas · H: ⚠️ · Método: A/V · R: ⚠️ · Obs: Campos, filtro (`ScheduleFilter` > 5 m), orden y agrupación con encabezado y pie, totales y gran total: OK. **No se pueden crear campos calculados:** `AddField(ScheduleFieldType.Formula)` lanza `ArgumentException` "fieldType is not a schedulable field type" y no existe API para escribir la fórmula (solo se lee `IsCalculatedField`)
- [x] **VIS-11** Leer celdas de una tabla (`TableSectionData`) · H: ✅ · Método: A · R: ✅ · Obs: `GetTableData().GetSectionData(Body)` + `GetCellText`: 10 filas × 4 columnas (incluye encabezados de grupo y totales: 186 m²). `ViewSchedule.Export` a CSV
- [x] **VIS-12** Esquemas de color (color fill) · H: ⚠️ · Método: A/V · R: ✅ · Obs: No hay API para crear un esquema de color desde cero, pero la plantilla trae uno ('Name') → `ColorFillScheme.Duplicate` + `ParameterDefinition = ROOM_NAME` + `AddEntry`/`UpdateEntry` con colores + `View.SetColorFillSchemeId` + `ColorFillLegend.Create`
- [x] **VIS-13** Vistas dependientes y regiones de recorte no rectangulares · H: ✅ · Método: A · R: ✅ · Obs: Vista dependiente (`Duplicate(AsDependent)`) con recorte en L (`GetCropRegionShapeManager().SetCropShape`). V: el recorte en L se ve en la imagen
- [x] **VIS-14** Cámara de una vista 3D en perspectiva (posición y objetivo) · H: ✅ · Método: A/V · R: ✅ · Obs: `View3D.CreatePerspective` + `SetOrientation(ViewOrientation3D)` + `ShadingWithEdges`. V: perspectiva correcta. **Rareza (C5):** las vistas sombreadas se exportan según `ShadowViewsFileType` (JPEG por defecto), no según `HLRandWFViewsFileType`
- [x] **VIS-15** Render fotorrealista · H: ❌ · Método: S · R: ❌ · Obs: S: no hay ningún método de render. Lo más cercano es `IDirectContext3DServer.RenderScene` (dibujo propio, no render fotorrealista)

### EXP · Exportación, importación, vínculos y lotes locales

- [x] **EXP-01** Exportar a PDF (`PDFExportOptions`), varios planos combinados en un solo PDF · H: ✅ · Método: A/V · R: ✅ · Obs: `Document.Export(carpeta, idsPlanos, PDFExportOptions{Combine=true})`: 1 PDF de 2 páginas (65 KB). **Sin marca educativa visible** y sin las cadenas 'student'/'educational' dentro del archivo. Fondo blanco (a diferencia de `ExportImage`, que sigue el tema oscuro). V: planos correctos
- [x] **EXP-02** Exportar a DWG y DXF con configuración · H: ✅ · Método: A · R: ✅ · Obs: DWG (16 KB) y DXF (97 KB) de una planta con las opciones por defecto; se genera además un `.pcp`. El DXF no contiene marcadores educativos en texto
- [x] **EXP-03** Exportar a IFC (`IFCExportOptions`, IFC4) · H: ✅ · Método: A · R: ⚠️ · Obs: IFC4: **requiere una transacción abierta** (sin ella lanza `ModificationOutsideTransactionException`); con una transacción y `RollBack` posterior exporta 65 KB sin dejar cambios. El IFC no tiene marca educativa (`IFCAPPLICATION 'Autodesk Revit 2027 (ENU)'`)
- [x] **EXP-04** Exportar a NWC · H: ❓ · Método: A · R: ❌ · Obs: `OptionalFunctionalityUtils.IsNavisworksExporterAvailable()` = false: la API existe, pero el exportador NWC no viene con Revit 2027; es un complemento aparte
- [x] **EXP-05** Exportar a FBX, gbXML y DGN · H: ⚠️ · Método: A · R: ⚠️ · Obs: FBX ✅ (131 KB, requiere un `ViewSet` con una vista 3D). gbXML: requiere transacción y un `EnergyAnalysisDetailModel` previo ("There is no EnergyAnalysisDetailModel in the document"). DGN: `Export` devuelve **false sin excepción ni explicación** con las opciones por defecto
- [x] **EXP-06** Exportar imágenes de vistas a PNG a alta resolución · H: ✅ · Método: A/V · R: ✅ · Obs: `Document.ExportImage` con `ImageExportOptions` (`SetOfViews`, 1400–2400 px, 150 DPI, PNG). Revit añade al nombre de archivo el tipo y el nombre de la vista. El fondo sigue el tema de lienzo de la interfaz (oscuro en este equipo); se comprobará con UI-08
- [x] **EXP-07** Importar o vincular DWG · H: ✅ · Método: A/V · R: ✅ · Obs: `Document.Import` y `Document.Link` de un DWG con `DWGImportOptions`: 2 `ImportInstance` (importado y vinculado)
- [x] **EXP-08** Vincular otro RVT y leer los elementos del vínculo · H: ✅ · Método: A · R: ✅ · Obs: `RevitLinkType.Create` + `RevitLinkInstance.Create` + `MoveElement` del vínculo: 5 muros legibles con `GetLinkDocument()` y transformación correcta (0,0 → 100,50 m). **Condición:** el RVT no puede estar abierto en la misma sesión; si lo está, `Create` no lanza excepción, devuelve un `LinkLoadResult` fallido y el error aparece después en `RevitLinkInstance.Create`
- [x] **EXP-09** Abrir o vincular IFC · H: ⚠️ · Método: A · R: ⚠️ · Obs: `Application.OpenIFCDocument`: 5 muros y 2 ventanas en 2,5 s. **Abre un diálogo modal de aviso** (botones OK/Cancel) que bloquea la API. `UIApplication.DialogBoxShowing` + `OverrideResult(1)` lo cierra, pero **aborta la importación** ("Transaction committing failed"). La solución que funciona es `Application.FailuresProcessing` + `DeleteWarning`: sin diálogo. Aviso interceptado: "IFC versions 4 and above are only partially supported by Open IFC"
- [x] **EXP-10** Lote: abrir 20 RVT en segundo plano (`OpenDocumentFile`), procesarlos, exportar y cerrarlos sin guardar en una misma sesión · H: ✅ · Método: P/J · R: 💥 · Obs: **Crash reproducible de Revit, ligado a la CARPETA de destino.** Bucle `NewProjectDocument` → muros → `SaveAs` → `Close` (~1 s por ciclo): `0xc0000005` en un hilo no principal (2026-10-18: misma dirección 00007FFC7FD01B98, tras 17 y 9 ciclos). **Aislamiento del 2026-10-28** (Crash.cs, variantes EXP-10a…h, cada ciclo anotado en %LOCALAPPDATA%\RevitLab\crash): guardando en `C:\Users\…\AppData\Local\RevitLab\crash` → **120 ciclos sin crash** (4 tandas); en `D:\RevitLabCrash` (misma unidad que el repositorio, fuera de él) → **30 sin crash**; en la carpeta del run **dentro del repositorio** (`D:\…\revitlab-2027\pruebas\resultados\runs\…`) → 💥 **tras 9** (la prueba EXP-10 original, re-ejecutada en un Revit recién arrancado) y **tras 8** (la réplica guardando ahí), con Visual Studio abierto con el proyecto; con VS cerrado → 30 sin crash y luego 💥 **tras 28 más** (~88 ciclos en la sesión). El ciclo anterior al crash tarda **~6 s** en lugar de ~1 s (las 2 veces medidas). El journal termina mientras Revit abre la plantilla del siguiente documento. Ambas unidades son NTFS locales. **Hipótesis (no demostrada):** otro proceso que vigila la carpeta del repositorio (VS, Claude Code, indexador) abre los archivos recién guardados y Revit no maneja bien ese acceso concurrente (VS lo acelera mucho). Un vigilante sintético que abría cada .rvt nuevo en la carpeta de C: hizo los ciclos más lentos (35 s frente a 29 s) pero **no** provocó el crash en 30 ciclos. **Consecuencia práctica:** en un lote, guardar los RVT en una carpeta que no vigile ningún IDE ni sincronizador. Tras el crash aparece "Revit 2027.1 Error Report" (CER). Los RVT de lote se abren bien: ver EXP-13
- [ ] **EXP-11** Abrir un archivo de una versión anterior (actualización automática) en el lote · H: ✅ · Método: A/P · R: — · Obs:
- [x] **EXP-12** Diálogos que aparecen durante el lote, resueltos por `DialogBoxShowing` · H: ⚠️ · Método: A/J · R: ⚠️ · Obs: Lote de 7 documentos con muros superpuestos en una transacción sin preprocesador (`--avisos mostrar`): `UIApplication.DialogBoxShowing` recibe `Dialog_Revit_DocWarnDialog` en todos y `OverrideResult` lo cierra (devuelve true), así que **el lote no se bloquea** (7 docs en 3,7 s). Pero **todos los códigos probados (1, 2, 6, 7, 8, 1001 y 1002) dejan la transacción en `RolledBack`** (0 muros): ningún código "acepta" el aviso. `DialogBoxShowing` sirve para que el lote no se cuelgue, no para aceptar avisos; para eso hacen falta `FailuresProcessing` o `IFailuresPreprocessor` + `DeleteWarning` (TRX-07, EXP-09). Run 20261027-154444-998
- [x] **EXP-13** Memoria tras abrir y cerrar 100 documentos (fugas) · H: ⚠️ · Método: P · R: ✅ · Obs: **100 aperturas y cierres** (`OpenDocumentFile` + contar muros + `Close(false)`) de RVT existentes en una misma sesión: **47,7 s (476 ms/archivo)**, sin crash, memoria 1.015 → 1.073 MB (**+58 MB**, sin fuga apreciable). Detalle: `SaveAs` con `OverwriteExistingFile` deja **copias de seguridad** `modelo_NN.0001.rvt`… junto al archivo (120 archivos para 30 modelos guardados 4 veces). Run EXP-10f del 2026-10-28
- [x] **EXP-14** Purgar elementos no usados por API (`GetUnusedElements`) · H: ❓ · Método: S/A · R: ✅ · Obs: `GetUnusedElements(conjunto vacío = todas las categorías)`: 309 no usados en una plantilla nueva (FamilySymbol 120, Family 48, WallType 22…). `doc.Delete` eliminó 1.698 ids (en cascada); tras una pasada quedan 27, así que la purga completa requiere iterar

### GEO · Geometría y análisis espacial

- [x] **GEO-01** Extraer sólidos, caras y aristas (`get_Geometry`, `Options`) con volumen y área · H: ✅ · Método: A · R: ✅ · Obs: `get_Geometry(Options{Fine})` de un muro 10×3 m: 1 sólido, 6 caras, 12 aristas, 6,0000 m³ exactos
- [x] **GEO-02** Booleanas entre sólidos (`BooleanOperationsUtils`) · H: ✅ · Método: A · R: ✅ · Obs: `BooleanOperationsUtils` diferencia (ELM-14, 9 m³ exactos) y unión (GEO-03)
- [x] **GEO-03** Crear sólidos (`GeometryCreationUtilities`: extrusión, barrido, revolución) · H: ✅ · Método: A · R: ✅ · Obs: `CreateRevolvedGeometry` 9,4248 m³ (3π, error relativo 5e-7) y `CreateSweptGeometry` 5,0000 m³, sin documento ni transacción. Aquí el perfil del barrido va en **coordenadas de modelo**, al contrario que en `NewSweep` de familias (FAM-02b). Para comparar volúmenes de revolución hace falta una tolerancia relativa
- [x] **GEO-04** Lanzar rayos (`ReferenceIntersector`) para encontrar el elemento más cercano · H: ✅ · Método: A · R: ✅ · Obs: `ReferenceIntersector(ElementClassFilter(Wall), Face, View3D).FindNearest`: muro sur a 3,900 m, tabique a 2,400 m y muro norte a 3,900 m (distancias exactas a las caras interiores). Requiere una vista 3D
- [x] **GEO-05** Detección de choques con `ElementIntersectsSolidFilter` en un modelo grande (tiempo) · H: ✅ · Método: P · R: ✅ · Obs: 200 sólidos contra 2.000 muros con `ElementIntersectsSolidFilter`: 57 choques en 12,0 s (**60 ms por sólido**)
- [x] **GEO-06** Geometría de habitaciones (`SpatialElementGeometryCalculator`) · H: ✅ · Método: A · R: ✅ · Obs: `SpatialElementGeometryCalculator`: sólido de 6 caras con 91,294 m³ (igual que `Room.Volume`) y 5 elementos delimitadores mediante `GetBoundaryFaceInfo`
- [x] **GEO-07** Teselar caras a malla y exportarla (por ejemplo a OBJ) por cuenta propia · H: ✅ · Método: A · R: ✅ · Obs: Exportador OBJ propio: `Face.Triangulate()` de muros y suelo → 248 vértices y 152 triángulos (8 KB). Evidencia `edificio.obj`
- [x] **GEO-08** Geometría de elementos en vínculos con transformación · H: ✅ · Método: A · R: ✅ · Obs: `RevitLinkInstance.GetTotalTransform().OfPoint` convierte coordenadas del vínculo al anfitrión (EXP-08)
- [x] **GEO-09** Recolectar todos los elementos de un modelo con 50.000 (`FilteredElementCollector`, filtros rápidos frente a lentos) · H: — · Método: P · R: ✅ · Obs: Modelo de 24.225 elementos: todos 3,8 ms · `OfClass(Wall)` 2,6 ms · `OfClass` + `ElementParameterFilter` 6,9 ms · filtro de parámetro sin `OfClass` 31 ms · **LINQ con `get_Parameter` 79 ms (×11 frente al filtro nativo)**. Conviene filtrar primero por clase o categoría y después por parámetro, siempre con filtros nativos

### VIZ · Visualización propia

- [x] **VIZ-01** Gráficos temporales (`TemporaryGraphicsManager`) · H: ✅ · Método: M/V · R: ✅ · Obs: Repetida en dos partes, sin ningún diálogo modal mientras el usuario usa el ratón (run 20261027-175755-283 + 175851-311). `TemporaryGraphicsManager.AddControl` con un BMP propio de 32×32: un control en todas las vistas (`InvalidElementId`) y otro solo en la vista activa; **`SetTooltip` funciona** (el usuario vio el texto al pasar el ratón); `SetVisibility(false)` oculta uno; `Clear()` los borra todos. Las cuatro comprobaciones del usuario, SÍ. No necesita transacción. Con un `TaskDialog` abierto la vista no recibe el ratón: el tooltip solo se puede comprobar sin diálogo
- [x] **VIZ-02** `DirectContext3D`: dibujar geometría propia que no es un elemento · H: ✅ · Método: M/V · R: ✅ · Obs: Servidor `IDirectContext3DServer` registrado en caliente (`MultiServerService.AddServer` + `SetActiveServers`): dibuja una caja de 12 triángulos con color por vértice (`VertexBuffer`/`IndexBuffer`/`FlushBuffer`) flotando sobre el edificio en las vistas 3D. El usuario la vio; se mueve con el modelo al girar y **no se puede seleccionar** (no es un elemento). `RemoveServer` la quita
- [x] **VIZ-03** Analysis Visualization Framework (mapa de colores sobre caras) · H: ✅ · Método: V · R: ✅ · Obs: AVF sobre la cara superior del suelo en una vista 3D: `SpatialFieldManager` + `AnalysisResultSchema` + `FieldDomainPointsByUV` (13×13 puntos) + `UpdateSpatialFieldPrimitive`, **sin transacción**; solo `AnalysisDisplayStyle.CreateAnalysisDisplayStyle` y asignarlo a la vista la necesitan. El usuario confirmó el degradado y la leyenda en pantalla. **`ExportImage` incluye el mapa de colores pero no la leyenda**
- [x] **VIZ-04** `PreviewControl` (vista de Revit dentro de una ventana WPF propia) · H: ✅ · Método: M · R: ✅ · Obs: `new PreviewControl(doc, viewId)` dentro de una ventana WPF propia no modal: muestra el 3D, se puede girar y hacer zoom, pero no seleccionar. **Hallazgo:** no es una copia independiente. Al girar en el `PreviewControl` **cambia la orientación de la vista real** del proyecto ("RevitLab 3D": dirección de vista (0.037,0.586,-0.809) → (0.764,-0.070,-0.642), medido con `View3D.GetOrientation` antes y después); "{3D}" no cambió. Además, con el foco en la ventana del `PreviewControl`, `UIDocument.ActiveView` devolvió una vista llamada 'Project View' que no existe en el proyecto (observación sin verificar). Hay que llamar a `Dispose()` al cerrar

### EVT · Eventos y automatización reactiva

- [x] **EVT-01** `DocumentChanged`: detalle de elementos añadidos, modificados y borrados · H: ✅ · Método: A · R: ✅ · Obs: `Application.DocumentChanged` informa por transacción: [crear 3] +3, [modificar 1] ~1, [borrar 1] −1, con los nombres de las transacciones
- [x] **EVT-02** Modificar el modelo dentro de `DocumentChanged` · H: ❌ · Método: A · R: ❌ · Obs: Dentro de `DocumentChanged` no se puede iniciar una transacción: `InvalidOperationException` "Cannot modify the document … changes to the document are temporarily disabled". Para reaccionar modificando hay que usar `IUpdater` (EVT-03)
- [x] **EVT-03** `IUpdater` que valida o corrige al cambiar parámetros · H: ✅ · Método: A · R: ✅ · Obs: `IUpdater` registrado por documento + `AddTrigger(ElementClassFilter(Wall), GetChangeTypeParameter(WALL_USER_HEIGHT_PARAM))`: escribe Comments dentro de la **misma transacción** ('ok 4.50 m', 'REVISAR: altura 7.50 m'). Detalle: si el valor no cambia (altura por defecto = 4,00 m) no hay disparo
- [x] **EVT-04** Dos `IUpdater` que se disparan mutuamente (bucle) · H: ⚠️ · Método: A/J · R: ⚠️ · Obs: Dos updaters que se disparan mutuamente: Revit **detecta el bucle** tras unas 25 ejecuciones de cada uno, cancela con el error "Third party updater … has caused an infinite loop and its action had to be canceled" y la transacción termina en RolledBack (129 ms). Sin preprocesador de errores, ese error se mostraría en un diálogo
- [x] **EVT-05** Cancelar un guardado desde `DocumentSaving` · H: ✅ · Método: A/M · R: ✅ · Obs: `DocumentSavingAs` + `e.Cancel()`: `SaveAs` lanza `InvalidOperationException` "Saving failed." y no se crea el archivo. Ojo: `SaveAs` dispara `DocumentSavingAs`, no `DocumentSaving`
- [x] **EVT-06** Responder diálogos automáticamente con `DialogBoxShowing` · H: ⚠️ · Método: A/M · R: ⚠️ · Obs: `UIApplication.DialogBoxShowing` permite responder diálogos (`OverrideResult`), pero **el código correcto no es deducible**: para `Dialog_Revit_DocWarnDialog` (OK/Cancel), `OverrideResult(1)` cerró el diálogo y abortó la operación. Para los avisos es mejor `Application.FailuresProcessing` (EXP-09)
- [x] **EVT-07** Sustituir un comando nativo (por ejemplo Delete) con `AddInCommandBinding` · H: ✅ · Método: M · R: ✅ · Obs: `UIApplication.CreateAddInCommandBinding(Delete)` en caliente desde el runner + `Executed`: la tecla Supr **y** el botón Delete de la cinta quedan interceptados (diálogo propio, no se borra nada; confirmado por el usuario y el registro). `RemoveAddInCommandBinding` devuelve el comportamiento nativo (el usuario borró un muro con Supr después)
- [x] **EVT-08** `SelectionChanged` · H: ✅ · Método: M/A · R: ✅ · Obs: `UIApplication.SelectionChanged`: cada cambio de selección del usuario dispara un evento con la selección completa (1 → 2 con Ctrl+clic → 0 → 2 con ventana → 0 con Esc). `Selection.SetElementIds` desde la API **no lo dispara durante la llamada**: llega un único evento después, con el estado final (dos `SetElementIds` seguidos → 1 evento con 0 elementos). El manejador puede darse de baja a sí mismo con `((UIApplication)sender).SelectionChanged -= …`
- [x] **EVT-09** `ViewActivated`, `DocumentOpened` y `ApplicationInitialized` · H: ✅ · Método: A · R: ✅ · Obs: `DocumentOpened` (Status Succeeded) y `DocumentClosing` se disparan al abrir o cerrar por API con `OpenDocumentFile`/`Close`. Falta `ViewActivated` (requiere interfaz)
- [x] **EVT-10** Coste de rendimiento de un `DocumentChanged` pesado durante una edición masiva · H: — · Método: P · R: ✅ · Obs: Un `DocumentChanged` que lee los 74.000 parámetros de 2.000 muros nuevos no añade un coste medible (36,6 s frente a 37,9 s). **Hallazgo aparte:** crear 2.000 muros paralelos separados 0,5 m tardó ~37 s (**19 ms por muro**), frente a unos 3 ms por muro en una cuadrícula separada 3 m (PAR-07): la proximidad entre muros dispara el coste (¿uniones o cálculo de límites?). Pendiente de aislar en LIM

### UI · Interfaz de usuario

- [x] **UI-01** Pestaña, paneles y botones propios: pulldown, split, combobox, cuadro de texto · H: ✅ · Método: M · R: ✅ · Obs: **La cinta se crea en caliente desde el runner** (`UIApplication.CreateRibbonTab`/`CreateRibbonPanel`, no solo en `OnStartup`), sin reiniciar Revit. Panel con 11 elementos: botón con iconos generados con WPF (`RenderTargetBitmap`) y tooltip; desplegable con separador; botón dividido (`IsSynchronizedWithCurrentItem`: el principal pasa a ser el último usado); apilados (combo, cuadro de texto y botón pequeño); separadores; panel extensible (`AddSlideOut`). Todo verificado por el usuario y por el registro de los comandos: `ComboBox.CurrentChanged` (Rojo→Verde→Azul…) y `TextBox.EnterPressed` (valor recibido) funcionan con manejadores en el payload. `PushButtonData` apunta a una **copia** de la DLL de comandos en una carpeta única: Revit la carga por ruta al pulsar, en el **ALC 'Default'**, igual que el runner y que un add-in cargado por manifiesto (UI-03): en esta instalación ningún add-in recibe un contexto aislado (corregido el 2026-10-27; ver ENT-08..11) y la bloquea. Las pestañas y paneles no se pueden quitar por API: se acumulan hasta cerrar Revit. Runs 20261027-180256-362 y siguientes
- [x] **UI-02** Disponibilidad de botones según contexto (`IExternalCommandAvailability`) · H: ✅ · Método: M · R: ✅ · Obs: `PushButtonData.AvailabilityClassName` + `IExternalCommandAvailability`: sin selección, `IsCommandAvailable` recibe `CategorySet` vacío y devuelve false (botón gris; más de 100 llamadas en ~2 min, al mover el ratón o cambiar el estado); con un muro seleccionado se habilita y el comando se ejecuta (seleccionados=1). Verificado por el usuario y el registro
- [x] **UI-03** Panel acoplable WPF (`DockablePane`) · H: ✅ · Método: M · R: ⚠️ · Obs: **Funciona solo si se registra en el arranque.** (1) Desde un botón, en caliente, `UIApplication.RegisterDockablePane` no da error, llama a `SetupDockablePane` y `PaneIsRegistered` pasa a true, pero el panel **nunca se crea** (`GetDockablePane` → `ArgumentException` "has not been created yet", 11 intentos): fallo silencioso. (2) Desde `IExternalApplication.OnStartup` (manifiesto propio `RevitLab.Panel.addin`) el panel WPF construido en código aparece acoplado a la derecha al arrancar; `PaneExists`=true, `GetTitle`/`IsShown`/`Show` funcionan desde el runner, y su botón modifica el modelo por `ExternalEvent` (`Committed`). El add-in cargado por manifiesto también corre en el ALC 'Default'. Para desarrollar un panel acoplable hace falta reiniciar Revit en cada cambio (o separar el contenido del panel en una DLL recargable). Runs 20261027-1805xx (en caliente) y 20261027-183300-526 (arranque)
- [x] **UI-04** `TaskDialog` con todas sus variantes · H: ✅ · Método: M · R: ✅ · Obs: Repetida con el usuario atento y preguntas de control (run 20261027-174809-432 y UI-04c 20261027-174929-371). Desde el runner (`ExternalEvent`): `TaskDialog.Show` estático → `No`; diálogo completo con 3 enlaces, icono, contenido expandible, pie con hipervínculo y casilla de verificación → `CommandLink2` (el usuario confirmó que todo se veía bien). **`WasVerificationChecked()` es fiable en todos los casos**, contrastado con lo que dijo el usuario: sin marcar + enlace → False; marcada + OK → True; marcada + enlace → True. **C5:** `VerificationText` y `ExtraCheckBoxText` **no pueden ir juntos**: `Show()` lanza `InvalidOperationException` ("not allowed in a TaskDialog at the same time"), y la firma no lo anuncia. La casilla extra sola funciona (`WasExtraCheckBoxChecked`=True). `EnableDoNotShowAgain` funciona: la segunda vez el diálogo no aparece y devuelve `Ok` en 7 ms, **y Revit lo recuerda entre ejecuciones**: hay que usar un identificador nuevo para volver a probarlo. En `DialogBoxShowing`, el `DialogId` de un `TaskDialog` está **vacío**, salvo con `EnableDoNotShowAgain`, que usa ese identificador (útil para reconocer los diálogos propios)
- [x] **UI-05** Selección interactiva con filtro (`PickObject`, `PickObjects`, `PickPoint`, `ISelectionFilter`) · H: ✅ · Método: M · R: ✅ · Obs: Repetida completa con el usuario atento (run 20261027-175653-236): `PickObject` con `ISelectionFilter` (solo muros; el usuario confirmó que el suelo y la puerta no se resaltaban), `PickObjects` + Finish (3 elementos), `PickObject(Face)`, `PickPoint` y `PickBox` arrastrando. Esc en `PickElementsByRectangle` → `Autodesk.Revit.Exceptions.OperationCanceledException`, con pregunta de control ("¿pulsaste Esc sin hacer clic?" → SÍ); lo mismo con `PickObject` (UI-05b). Los "0 elementos" de pasadas anteriores fueron Esc olvidados (un clic suelto termina la selección vacía), no una rareza. **C5:** `PickedBox.Min`/`Max` **no están normalizados**: son la esquina inicial y la final del arrastre (Min.Y > Max.Y en 3 ejecuciones). `PickBox` termina al soltar el botón (un clic sin arrastrar da una caja de tamaño cero). Para el usuario, las instrucciones en la barra de estado pasan desapercibidas: hizo falta un `TaskDialog` antes de cada paso
- [x] **UI-06** `PostCommand` de un comando nativo; dos `PostCommand` seguidos · H: ⚠️ · Método: M/A · R: ⚠️ · Obs: **La hipótesis era falsa:** no se ejecuta "solo el último". El **segundo** `PostCommand` seguido lanza `InvalidOperationException` ("Revit does not support more than one command are posted") y se ejecuta el **primero** (`Default3DView`), después de que termina la orden de la API (vista activa al salir='Level 1'; en el siguiente run='{3D}'). `CanPostCommand` = true para ambos. Para encadenar comandos hay que esperar a que se ejecute el anterior (por ejemplo, en `Idling` o en otra orden)
- [x] **UI-07** Abrir o cambiar la vista activa y hacer zoom a elementos (`UIView.ZoomAndCenterRectangle`, `ShowElements`) · H: ✅ · Método: M · R: ✅ · Obs: Desde el runner: asignar `UIDocument.ActiveView` funciona dentro de un `ExternalEvent`. `UIView.ZoomAndCenterRectangle`, `UIDocument.ShowElements`, `ZoomToFit` y `Zoom(2)` con `RefreshActiveView()`: las tres comprobaciones visuales del usuario, SÍ; `GetZoomCorners` lo confirma numéricamente. `ZoomToFit` encuadra ~97×58 m para un edificio de 12×8 m porque incluye las marcas de alzado. `OpenAndActivateDocument(String)` también funciona desde un `ExternalEvent` (INT-00)
- [x] **UI-08** Detectar el tema claro u oscuro (`UIThemeManager`) y adaptar iconos · H: ✅ · Método: M · R: ✅ · Obs: `UIThemeManager.CurrentTheme`/`CurrentCanvasTheme`/`FollowSystemColorTheme` se escriben por API y la interfaz cambia al momento (el usuario lo vio en claro). `UIApplication.ThemeChanged` se dispara una vez por cada propiedad cambiada (2 al cambiar y 2 al restaurar). **`ExportImage` sigue al tema del lienzo:** fondo (255,255,255) en claro y (34,41,51) en oscuro. Para exportar con fondo blanco basta con poner el lienzo en claro un momento. El tema original quedó restaurado
- [x] **UI-09** Texto en la barra de estado de Revit · H: ❌ · Método: S/M · R: ❌ · Obs: S: no hay API de barra de estado (solo `PostableCommand.StatusBar*`, que la muestra u oculta)
- [x] **UI-10** Atajos de teclado asignados por API · H: ❌ · Método: S · R: ❌ · Obs: S: solo `PostableCommand.KeyboardShortcuts`, que abre el diálogo
- [x] **UI-11** Modificar pestañas nativas de Revit (vía `AdWindows`, sin soporte) · H: ⚠️ · Método: M · R: ⚠️ · Obs: Funciona, pero sin soporte: `Autodesk.Windows.ComponentManager.Ribbon` (AdWindows.dll) permite **renombrar** una pestaña nativa (Collaborate → 'Colaborar (RevitLab)'), **ocultarla** (Massing & Site, `IsVisible=false`) y **añadir un botón** a un panel nativo (Manage › Select); confirmado por el usuario y restaurado al terminar. Lista de pestañas con sus Id internos (Architecture, MassingSite, Add-Ins, Modify, Home_Family…, ocultas según el contexto). AdWindows no está en la API documentada: puede romperse en cualquier actualización. Inventario de AdWindows en pruebas/resultados/api-surface-adwindows-2027.tsv
- [x] **UI-12** Cerrar Revit desde el add-in (`PostableCommand.ExitRevit`) · H: ✅ · Método: M · R: ✅ · Obs: `PostCommand(PostableCommand.ExitRevit)` (CanPostCommand=true) cierra Revit como si lo hiciera el usuario, al terminar la orden de la API. Con un documento visible modificado sin guardar ('interactivo', IsModified=True): a los 1,6 s Revit muestra **`TaskDialog_Save_File`** (registrado por `DialogBoxShowing`); el usuario respondió No y Revit se cerró **limpiamente** (el runner recibió `OnShutdown` a las 01:12:39). Para un cierre desatendido hay que guardar o cerrar antes los documentos, o responder `TaskDialog_Save_File` por código. Run 20261028-011152-276
- [x] **UI-13** Cambiar opciones de Revit (rutas de archivos, unidades por defecto) · H: ⚠️ · Método: S/A · R: ⚠️ · Obs: Se cambian y se leen al momento (y se restauran): `SetLibraryPaths` (**se escribe en Revit.ini en el acto**), `SharedParametersFilename` (acepta incluso una ruta que no existe, y se puede volver a ''), `BackgroundColor`, `DefaultViewDiscipline` (la vista nueva sale con la disciplina elegida), `SelectionUIOptions` (seleccionar vínculos, subyacentes, fijados, caras, arrastrar) y el tema con `UIThemeManager` (solo leído: Dark, siguiendo al sistema). **Sin API:** las unidades por defecto de la aplicación (solo `Document.SetUnits`, por documento), y `DefaultProjectTemplate`, `FamilyTemplatePath` y `PointCloudsRootPath`, que son de solo lectura. Efecto secundario: al guardar, Revit reescribió `DataLibraryLocations` de ENU en Revit.ini con rutas **relativas** a Revit.exe (`..\..\..\ProgramData\…`), que son equivalentes. Run 20261027-154351-453

### DAT · Datos propios y comunicación externa

- [x] **DAT-01** Extensible Storage: crear un esquema, escribir y leer una entidad en un elemento · H: ✅ · Método: A · R: ✅ · Obs: `SchemaBuilder` con campo simple, campo con unidad (`SetSpec(Length)`), lista (`AddArrayField`) y mapa (`AddMapField`) → `Entity` en un muro; todo se relee (Longitud 3,4999999999999996 m por la conversión de coma flotante)
- [x] **DAT-02** Cambiar los campos de un esquema existente sin cambiar el GUID · H: ❌ · Método: A · R: ❌ · Obs: Esperado: redefinir un esquema con el mismo GUID lanza `InvalidOperationException` "A different Schema with the same identity already exists". El esquema vive en la sesión, así que cambiar sus campos exige un GUID nuevo
- [x] **DAT-03** Niveles de acceso de un esquema (vendor) · H: ✅ · Método: A · R: ✅ · Obs: `AccessLevel.Vendor` se respeta: con VendorId 'OTROVENDOR', `SetEntity` lanza "Writing of Entities of this Schema is not allowed to the current add-in"; con 'REVITLAB' (el VendorId del manifiesto) escribe sin problema
- [x] **DAT-04** Qué pasa con la entidad al copiar o pegar el elemento y al copiar entre documentos · H: ❓ · Método: A · R: ✅ · Obs: La entidad **viaja con el elemento** al copiarlo, tanto en el mismo documento (`CopyElement`) como a otro (`CopyElements` entre documentos)
- [x] **DAT-05** `DataStorage`: datos de proyecto sin elemento anfitrión; tamaño máximo práctico · H: ⚠️ · Método: P · R: ✅ · Obs: `DataStorage.Create` + entidad con 1 MB y 10 MB de texto: 7–10 ms; el RVT crece solo 12 KB y 116 KB (comprime el texto repetitivo). No apareció ningún límite práctico hasta 10 MB
- [x] **DAT-06** Llamar a una API HTTP externa desde el add-in (`HttpClient`) · H: ✅ · Método: A · R: ✅ · Obs: `HttpClient.GetStringAsync().GetAwaiter().GetResult()` desde el hilo de Revit: 321 ms, **con la interfaz bloqueada** todo ese tiempo. Para no congelar: llamada en segundo plano + `ExternalEvent` para escribir el resultado
- [x] **DAT-07** SQLite o JSON locales como base de datos del add-in · H: ✅ · Método: A · R: ⚠️ · Obs: JSON con `System.Text.Json`: OK. **SQLite (NuGet `Microsoft.Data.Sqlite` 10.0.13)**: (1) un proyecto de biblioteca no copia sus DLL NuGet; hace falta `CopyLocalLockFileAssemblies=true`; (2) con eso cargan las DLL administradas, pero la **nativa `e_sqlite3`** falla (`TypeInitializationException`) dentro del contexto recargable del runner, que no copia `runtimes/` ni resuelve DLL nativas. Es una limitación del runner, no de Revit
- [ ] **DAT-08** External Data Manager (nuevo en 2027): fuente de datos propia en Manage Links · H: ✅ · Método: M · R: — · Obs: S ✔ `Autodesk.Revit.UI.ExternalDataManagerCommand` (`RegisterCommand`) y la familia `ExternalResourceServer*`
- [x] **DAT-09** Extended Properties vinculadas al modelo (nuevo en 2027) · H: ❓ · Método: A · R: ⚠️ · Obs: S: `ExtendedPropertiesLink.Create(doc, ExternalResourceReference)` y `CreateFromCloudDataset(doc, datasetId)`: requiere un servidor de recursos externo o un dataset en la nube (AEC Data Model), así que no es comprobable en local sin nube

### WSH · Worksharing local (sin nube)

- [x] **WSH-01** Activar worksharing en un modelo (`EnableWorksharing`) · H: ✅ · Método: A · R: ✅ · Obs: `EnableWorksharing` en un proyecto nuevo (también en ELM-24)
- [x] **WSH-02** Guardar como central en disco local y crear una copia local · H: ✅ · Método: A · R: ✅ · Obs: `SaveAs` con `WorksharingSaveAsOptions{SaveAsCentral=true}` → central local de 3,7 MB; `WorksharingUtils.CreateNewLocal` → copia local
- [x] **WSH-03** Sincronizar con el central (`SynchronizeWithCentral`) · H: ✅ · Método: A · R: ✅ · Obs: `SynchronizeWithCentral(TransactWithCentralOptions, SynchronizeWithCentralOptions{Comment, RelinquishOptions(true)})`: 1,0 s; después el elemento queda `NotOwned`
- [x] **WSH-04** Propiedad de elementos y préstamos (`WorksharingUtils`) · H: ✅ · Método: A · R: ✅ · Obs: `WorksharingUtils.CheckoutElements` → `GetCheckoutStatus` = OwnedByCurrentUser (el propietario es el usuario de Autodesk)
- [x] **WSH-05** Abrir separado del central (detach) en un lote · H: ✅ · Método: A · R: ✅ · Obs: `OpenOptions{DetachFromCentralOption = DetachAndPreserveWorksets}`: `IsDetached=True` con los 2 muros sincronizados

### DIS · Disciplinas (muestra, no exhaustivo)

- [x] **DIS-01** MEP: conductos y tuberías con conexiones; crear un sistema · H: ✅ · Método: A/V · R: ⚠️ · Obs: `Duct.Create(sistema, tipo, nivel, p1, p2)` crea conductos y su sistema ('Mechanical Supply Air 1'), pero `NewElbowFitting` lanza "failed to insert elbow": la plantilla arquitectónica **no trae accesorios** (preferencias de enrutamiento). Hace falta una plantilla MEP
- [x] **DIS-02** MEP: calculadoras de dimensionado (nuevo en 2027) · H: ❓ · Método: A · R: ⚠️ · Obs: `SizingCalculator(doc).CalculateSizeByVelocity(caudal, velocidad, SizingOptions(conducto))` se ejecuta sin excepción, pero no se comprobó qué devuelve o modifica (método `void`)
- [x] **DIS-03** Estructura: armadura (`Rebar.CreateFromCurves`), espaciado personalizado (nuevo en 2027) · H: ✅ · Método: A/V · R: ⚠️ · Obs: **Firma 2027:** `Rebar.CreateFromCurves(…, BarTerminationsData, …)` sustituye a la de ganchos y orientaciones (deriva frente al código de ejemplo antiguo); el muro estructural es host válido y `CreateDefaultRebarBarType` crea el tipo de barra. **Sintaxis de espaciado** (exploración de 11 candidatas): solo `3@200;2@400` es válida. Aplicarla con `SetLayoutAsCustomSpacing` lanzó `InternalException` "An internal error has occurred" sin más detalle
- [x] **DIS-04** Modelo analítico y condiciones de contorno como elementos (nuevo en 2027) · H: ✅ · Método: A · R: ✅ · Obs: `AnalyticalMember.Create(doc, Line)`: 6,000 m, `StructuralRole=Unset`
- [x] **DIS-05** Energía o gbXML: análisis energético por API · H: ⚠️ · Método: S/A · R: ❌ · Obs: `EnergyAnalysisDetailModel.Create` (deprecado en 2027) lanza "Failed to create the energy analysis detail model" en un proyecto sin espacios ni configuración energética. gbXML también lo exige (EXP-05)

### LIM · Estrés y límites duros

- [x] **LIM-01** Crear 10.000 muros y 100.000 elementos `DirectShape`: tiempo, memoria y tamaño del RVT · H: — · Método: P · R: ✅ · Obs: 10.000 muros en rejilla dispersa: 21,7 s (**2,17 ms/muro**, sin crecer con la cantidad) y +20 MB · 50.000 `DirectShape` (caja, `SolidUtils.CreateTransformed`): 30 s (**0,6 ms/elemento**) pero **+647 MB** · `SaveAs`: 5,4 s y RVT de 22,6 MB · Revit llegó a 2,5 GB de memoria. Los DirectShape son baratos de crear pero caros en memoria
- [ ] **LIM-02** Sesión larga: 8 horas de ejecución continua del runner (fugas) · H: ⚠️ · Método: P/J · R: — · Obs:
- [ ] **LIM-03** Archivo RVT de más de 1 GB: apertura, recolección y guardado · H: — · Método: P · R: — · Obs:
- [x] **LIM-04** Excepciones nativas de Revit (`InternalException`) frente a excepciones de .NET: cuáles se pueden capturar · H: ⚠️ · Método: A/J · R: ⚠️ · Obs: De lo observado en todo el laboratorio: las excepciones de la API son `Autodesk.Revit.Exceptions.*` (InvalidOperation, Argument, ModificationOutsideTransaction, FileArgumentNotFound) y son capturables; existe además `InternalException` ("An internal error has occurred", "Transaction committing failed") sin detalle (DIS-03, EXP-09), también capturable. Los fallos nativos `0xc0000005` (EXP-10) **no** se pueden capturar: matan el proceso
- [x] **LIM-05** Recuperación tras un crash: qué queda en el journal y en las copias de seguridad · H: — · Método: J · R: ⚠️ · Obs: **Tras un crash** (5 provocados el 2026-10-28): (1) el **journal** registra `ExceptionCode` (0xc0000005), `ExceptionAddress` y que el filtro de excepciones corrió en un hilo no principal; la dirección cambia entre sesiones (00007FFC7FD01B98 el 18-oct, 00007FFE4AC028F8 el 28-oct); (2) Revit deja un **dump** de 10-13 MB junto al journal (`journal.NNNN.0001.dmp`, "dump file left intact") y una copia `*.txt.copy.txt`; (3) aparece **"Revit 2027.1 Error Report"** (CER) para enviar el informe; (4) el siguiente arranque no ofrece recuperar nada (no hay autoguardado; lo no guardado se pierde) y tarda lo normal (~12-15 s hasta el runner); (5) **el archivo que se estaba guardando queda corrupto** (`modelo_08.rvt`: 16 KB frente a 3,7 MB, OLE con 0 entradas, `BasicFileInfo` lo rechaza) y los anteriores quedan íntegros (`modelo_07`: Format 2027, 15 entradas OLE), verificado sin Revit con ShlProbe; (6) la excepción se registra **~5 s antes** de que muera el proceso: Revit la captura ("crash protection") y sigue a medias, que es el "ciclo lento" observado; (7) los resultados parciales del runner (`results.partial.jsonl`) y los registros por ciclo de las pruebas sobreviven. Para lotes: guardar con nombre temporal y renombrar al terminar, y comprobar cada archivo con `BasicFileInfo`
- [x] **LIM-06** Inventario: lo que la interfaz hace y la API no expone (lista a partir de los hallazgos ❌) · H: — · Método: S · R: ✅ · Obs: Entregable final: **reports/Límites de la API de Revit 2027.md**. Clasifica lo que la interfaz hace y la API no (fases, opciones de diseño, scope boxes, familias in situ, leyendas, campos calculados, render, barra de estado, atajos, unidades por defecto, datos de carbono, NWC, Hot Reload…, con su atajo cuando existe), lo que la API permite con condiciones (diálogos y contexto, interfaz, modelado y exportación, carga de add-ins), la estabilidad y lo que funcionó mejor de lo esperado, con el ID de cada prueba. Complemento: **reports/Resumen de métricas de la IA.md** (26 tareas, 1 alucinación en 6.626 líneas, veredicto de las hipótesis H1-H7)
- [x] **LIM-07** Aislar el coste de `IndependentTag.Create` (añadida tras ANO-04) · H: — · Método: P · R: ⚠️ · Obs: `IndependentTag.Create` cuesta **~300 ms por etiqueta** con muros sueltos (321 ms), con una rejilla de muros cruzados (303 ms) y con 100 habitaciones (330 ms): el coste **no depende** de esos factores. Con 300 etiquetas promedia 0,32 s y con 1.100 (ANO-04) 0,89 s: **indicio de coste cuadrático** respecto al número de etiquetas en la vista
- [x] **LIM-08** Aislar el coste de crear muros paralelos según la separación (añadida tras EVT-10) · H: — · Método: P · R: ⚠️ · Obs: 1.000 muros paralelos de 5 m: 3,6 (separación 3 m), 4,1 (1 m), 4,9 (0,5 m) y 4,7 (0,3 m) ms/muro; con 2.000 muros a 0,5 m (EVT-10) fueron 19 ms/muro, mientras que 10.000 muros en rejilla dispersa (LIM-01) cuestan 2,2 ms/muro. **Conclusión:** el coste crece con la cantidad de muros paralelos **con los extremos alineados** (¿búsqueda de uniones entre extremos?), no con la cantidad total ni con la separación

---

## 4. Registro de sesiones

Una entrada por sesión de pruebas:

| Fecha | Build de Revit | IDs probados | ✅ | ❌ | ⚠️ | 💥 | Notas o journal |
|---|---|---|---|---|---|---|---|
| 2026-10-18 | 27.1.0.45 (sin abrir Revit) | SHL-01/02/03/05 + evidencia estática (S) en otros 31 IDs | 2 | 8 | 1 | 0 | Inventario por reflexión + `tools/ShlProbe`. Hallazgo: la geometría y `BasicFileInfo` funcionan fuera de Revit |
| 2026-10-18 | 27.1.0.45 | ENT-01, ELM-01, DEV-10, TRX-01, DEV-03, DEV-04 | 5 | 1 | 0 | 0 | Primera sesión con `RevitLab.Runner`. Falso hot reload: 3,8 s por iteración, 51/51 descargas, +7 MB en 51 recargas |
| 2026-10-18 | 27.1.0.45 | TRX-02/03/04/05/07/09/10, CTX-01/02/06, DEV-07 (parcial) | 4 | 2 | 4 | 0 | Diálogo modal de aviso bloquea la API (146 s). Async vuelve al hilo principal pero fuera de contexto. Lecturas desde otro hilo no están protegidas |
| 2026-10-18 | 27.1.0.45 | INV-01, ELM (20), PAR (8), VIS (14), EXP-06 | 37 | 4 | 4 | 0 | Verificación visual por imagen exportada. Sin API para campos calculados de tablas, fases ni leyendas. Rarezas: `out` inicializado (NewFootPrintRoof), JPG en vistas sombreadas |
| 2026-10-27 | 27.1.0.45 | CTX-13, UI-13, EXP-12, SHL-04 (S) | 1 | 0 | 3 | 0 | El pipe del runner rechaza conexiones (incluso `eco`) mientras atiende una orden. Ningún código de `OverrideResult` acepta un aviso: todos hacen rollback. Opciones de Revit cambiables por API salvo las unidades y las rutas de plantillas. Vista previa PNG y `BasicFileInfo` leídos con ole32 sin Revit |
| 2026-10-27 | 27.1.0.45 | Runner nuevo (pipe con varias instancias, `[LabShutdown]`): CTX-13 (repetida), DEV-07 (a–f) | 2 | 0 | 0 | 0 | `eco` responde con Revit ocupado y `ping` se encola. DEV-07: los eventos y updaters dados de baja liberan el contexto; `IFailuresPreprocessor` lo retiene (corrige la observación del 2026-10-18). Revit pidió confirmación ("Always Load") para la DLL del runner recompilada |
| 2026-10-27 | 27.1.0.45 | Sesión interactiva, bloque 1: UI-04/05/06/07/08, VIZ-01/03, TRX-04/11/12, EVT-08 | 10 | 0 | 1 | 0 | Runner sobre un documento visible y comprobaciones del usuario con un `TaskDialog` Sí/No registrado en el resultado. El 2.º `PostCommand` lanza excepción; `PickedBox` no normaliza Min/Max; `ExportImage` sigue al tema del lienzo; un grupo asimilado es una sola entrada de Deshacer |
| 2026-10-27 | 27.1.0.45 | Sesión interactiva, bloque 2: UI-01/02/03, TRX-06, EVT-07, CTX-04, DEV-06, VIZ-02/04 | 8 | 1 | 0 | 0 | Cinta, combo, cuadro de texto, disponibilidad, sustitución de Delete y `DirectContext3D` funcionan **en caliente** desde el runner; un panel acoplable no (falla en silencio). El XAML se recarga bien en el contexto descargable. `PreviewControl` modifica la cámara de la vista real |
| 2026-10-27 | 27.1.0.45 | UI-03 (panel acoplable en `OnStartup`, tras reiniciar Revit) | 0 | 0 | 1 | 0 | En caliente el panel nunca se crea; en el arranque funciona. Los add-ins por manifiesto también corren en el ALC 'Default'. Revit avisó del manifiesto nuevo con Revit abierto |
| 2026-10-27 | 27.1.0.45 | Sesión interactiva, bloque 3a: CTX-05/07/09/10/11, UI-11, ANO-08 | 2 | 0 | 5 | 0 | El `ExternalEvent` espera a que se cierre el modal (medido con `IsWindowEnabled`). Idling: 6.840/s en primer plano. Una operación larga congela Revit salvo bombeando mensajes. La barra de progreso WPF y cancelar con botón o Esc funcionan. AdWindows modifica pestañas nativas |
| 2026-10-28 | 27.1.0.45 | Bloque 3b con Visual Studio 2026 y el SDK 2027.3: DEV-00, DEV-02, DEV-08, DEV-09 | 3 | 1 | 0 | 0 | Hot Reload y Editar y continuar imposibles (`MetadataUpdater.IsSupported=false` en los 89 runtimeconfig de Revit); Editar y continuar falla en silencio. Breakpoints en la copia sombra: sí. Add-In Manager del SDK: recarga, pero acumula copias. `lab vigilar`: ~4,2 s de guardar a resultado |
| 2026-10-28 | 27.1.0.45 | UI-12 | 1 | 0 | 0 | 0 | `PostCommand(ExitRevit)` con un documento modificado: `TaskDialog_Save_File`; tras responder No, Revit se cierra limpiamente (`OnShutdown`) |
| 2026-10-28 | 27.1.0.45 | Sesión de reinicios (6 arranques, 4 automáticos): ENT-02/04/05/06/07/08/09/10/11, DEV-01 | 3 | 1 | 6 | 0 | ProgramData ignorado; add-ins net8 y net48 cargan; el aislamiento de 2027 funciona; dependencias entre contextos con trampas (ruta absoluta, sin AssemblyName, orden alfabético); el texto del aviso de seguridad y los errores, en el journal; reinicio completo 37 s |
| 2026-10-28 | 27.1.0.45 | Variantes del crash: EXP-10 (a, g, h + original), EXP-13, DEV-11 | 1 | 0 | 0 | 2 | El crash de EXP-10 depende de la carpeta de destino (dentro del repositorio cae; en C:\AppData o D: fuera del repositorio, no). 100 aperturas: 476 ms/archivo, +58 MB. StackOverflow: Revit muere con 0xc00000fd. 5 crashes provocados y reinicios automáticos |
| 2026-10-28 | 27.1.0.45 | ENT-13, CTX-12 (runner con pipe por PID) | 2 | 0 | 0 | 0 | Revit + journal: abre, ejecuta un comando de add-in y sale solo en 29 s. Dos instancias simultáneas con la licencia educativa: ×1,5 de rendimiento |
| 2026-10-28 | 27.1.0.45 (sin Revit) | LIM-05, LIM-06 y resumen de métricas | 1 | 0 | 1 | 0 | Entregables finales en `reports/`. Tras un crash, el archivo que se estaba guardando queda corrupto (16 KB, OLE vacío) y los anteriores íntegros |
| 2026-10-28 | 27.1.0.45 | ENT-03 | 1 | 0 | 0 | 0 | El manifiesto en C:\Program Files\Autodesk\Revit\Addins\2027 se carga (el usuario creó la carpeta como administrador) |
| 2026-10-28 | 27.1.0.45 | ENT-12 | 0 | 0 | 1 | 0 | Nice3point.TUnit.Revit ejecuta la API dentro del proceso de pruebas, sin Revit.exe y sin los módulos de licencias de Autodesk; detenida a propósito |
| 2026-10-28 | 27.1.0.45 | TRX-08 (runner con FailureDefinition propio) | 1 | 0 | 0 | 0 | Un error propio se resuelve por código con DeleteElements: Committed, el elemento se borra y el resto de la transacción se conserva |

## 5. Orden sugerido

1. **SHL-05**: inventario de la API por reflexión. Claude lo puede hacer ya, sin Revit, y responde de antemano muchas hipótesis ❓.
2. **Construir `RevitLab.Runner`** y validarlo con **DEV-03, DEV-10, ELM-01** y **ENT-01**.
3. **DEV-00 a DEV-11** completos: definen la forma de trabajar.
4. **ENT-12** (TUnit) como comparación del runner.
5. **CTX** y **TRX**: los límites estructurales.
6. El resto por áreas, empezando por **ELM**, **PAR** y **VIS** (sobre el modelo M1). Las de estrés **LIM** van al final, con el modelo M2 generado.
