Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

## Restricciones

- Funcionalidad nueva solo dentro del veredicto vigente (CONDITIONAL-CLEAR hasta MVP-5: catálogo de matriz, UI de cinco pasos de esa matriz, paridad Tauri/WASM); el rail de Forge sigue en seis pasos y no se elimina ninguna pantalla. Runner, imagen real, root, discos y shell siguen cerrados hasta un alcance `accepted` adicional y una reevaluación del veredicto.
- Funcionalidad nueva solo dentro del veredicto vigente (CONDITIONAL-CLEAR hasta MVP-5, más el slice ISO-01 de imagen viva x86_64 UEFI: perfil archiso, una corrida `mkarchiso` en proceso separado y arranque vivo). El recorrido de la cara es Sistema base → Entorno → Apps → Identidad y seguridad → Revisión. El paso Disco y particiones sale de ese recorrido por orden del operador (2026-10-18). Siguen cerrados el runner de instalación, root, discos (A6), shell arbitraria y aarch64. Sin la línea ISO-01 de `go-no-go.md` no hay exporter ni `mkarchiso`.
- No ejecutar strings heredados `cmd`, hooks, `pacstrap` ni shell.
- Rust es la única autoridad semántica; TypeScript no duplica reglas.
- MVP no usa root, discos, shell, sidecars ni instalación real.
Expand All @@ -37,7 +37,7 @@
- La siguiente fase se especifica desde el último plan implementado, el roadmap de `docs/10-delivery/` y el trabajo pendiente, sin ampliar el veredicto vigente.
- No incluir en un commit cambios de `.gitignore` ni artefactos de fugas salvo petición explícita.
- La GUI se llama Forge (la interfaz gráfica de ArchMaker); el proyecto y el motor se llaman ArchMaker. Neubat queda fuera de la nomenclatura activa y se conserva solo como historia. No inventar pantallas: conectar los botones y controles que ya trae la cara.
- El flujo de Forge es Disco y particiones → Sistema base → Entorno → Apps → Identidad y seguridad → Revisión (experiencia única; sin Sencillo/Avanzado).
- El flujo de Forge es Sistema base → Entorno → Apps → Identidad y seguridad → Revisión (experiencia única; sin Sencillo/Avanzado). En Revisión se pide la imagen. No hay paso de disco, particiones, GPT, ESP, LUKS ni formatear. El SSD lo instala archinstall en la sesión viva.
- En la cara UEFI el arranque preseleccionado es systemd-boot; GRUB y Limine siguen seleccionables.
- Título visible de ventana/documento/cabecera: ArchMaker Forge; productName Forge e identifier intactos.
- Protección de `main` sin checks bloqueantes para el dueño y sí ante colaboradores externos.
Expand All @@ -46,12 +46,12 @@

- Remoto canónico: `https://github.com/Alexendros/archmaker`. Licencia Apache-2.0.
- Baseline vigente: `implementation-baseline-mvp5`. En el árbol están el walking skeleton MVP-0 y los slices MVP-1 (dominio, catálogo y migración v5.1), MVP-2 (reglas, resolución y validación), MVP-3 (adapters de exportación), MVP-4 (React + MockCorePort + cara Forge en `apps/web`) y MVP-5 (catálogo de matriz, UI cinco pasos, paridad Tauri/WASM, remediación operativa + Prove-It).
- Alcance MVP-5 cerrado (`mvp5-scope.md`, MVP-5-01..05). Hueco residual histórico: `docs/10-delivery/mvp5-gap-spec.md` (draft; cara supersedida por incremento Forge 2026-10-17). Remediación operativa: `docs/10-delivery/remediation/`. MVP-6 **accepted** solo AppImage (histórico Neubat `v0.1.0`; nombre visible vigente Forge); release [`v0.1.0`](https://github.com/Alexendros/archmaker/releases/tag/v0.1.0) publicada (verificación parcial: sin cosign local, sin 3× GUI, F7 sin `RC_SHA_FUNCIONAL`). `iso-scope.md` sigue sin `accepted`.
- Alcance MVP-5 cerrado (`mvp5-scope.md`, MVP-5-01..05). Hueco residual histórico: `docs/10-delivery/mvp5-gap-spec.md` (draft; cara supersedida por incremento Forge 2026-10-17). Remediación operativa: `docs/10-delivery/remediation/`. MVP-6 **accepted** solo AppImage (histórico Neubat `v0.1.0`; nombre visible vigente Forge); release [`v0.1.0`](https://github.com/Alexendros/archmaker/releases/tag/v0.1.0) publicada (verificación parcial: sin cosign local, sin 3× GUI, F7 sin `RC_SHA_FUNCIONAL`). `iso-scope.md` **accepted** el 2026-10-18 solo para imagen viva x86_64 UEFI (ISO-01). El código del perfil y de `mkarchiso` empieza en ISO-02.
- `reference/v5.1/` es evidencia inmutable verificada por SHA-256. La UI React vive en `apps/web/` (`forge-face.ts`); el dominio está en `crates/archmaker-core` y se expone por CorePort.
- Arranque de adaptadores vía `apps/web/src/create-core-port.ts`: Desktop → TauriCorePort (`invoke` de `@tauri-apps/api/core`); navegador → WasmCorePort (`tools/build-wasm.sh`); MockCorePort solo con `?mock=1` u opt-in explícito, nunca por fallo silencioso de Tauri/WASM.
- ADR-0011 (propuesto, `in-review`): la selección del configurador produce un contrato JSON de aplicación; cada cláusula tiene schema en `contracts/json-schema/`, issue `kind:clause` y tests verificados en Rust. No añade target de imagen.
- Alcance declarativo Forge (draft): `docs/10-delivery/forge-declarative-scope.md`. ADR-0013…0018 (UKI ESP, estados ortogonales, handles, digests v2, ONLYOFFICE/navegadores, A6 excluida). No reescribe `mvp5-scope.md`. Mantener `executionStatus=not-executed` en ese alcance.
- Cadena de integridad Forge (selección → documento persistido → motor → resolución/validación → UI efectiva → export → reimportación). Registro: `docs/10-delivery/closure/forge-integrity-findings-2026-10-18.md`; matriz: `docs/10-delivery/closure/forge-acceptance-matrix.md`. `StorageIntent` y campos v2 deben llegar íntegros al borrador/`draftForCore`/export; TypeScript no inventa semántica.
- Cadena de integridad Forge (selección → documento persistido → motor → resolución/validación → UI efectiva → export → reimportación). Registro: `docs/10-delivery/closure/forge-integrity-findings-2026-10-18.md`; matriz: `docs/10-delivery/closure/forge-acceptance-matrix.md`. Desde el 2026-10-18 la cara no autora `storagePlan`, filesystem ni cifrado de disco; un borrador importado con `storagePlan` no reabre el paso de disco. `executionStatus` de planes de disco permanece `not-executed`. TypeScript no inventa paquetes ni el perfil.
- En `apps/web` con Vite, la CSP (`style-src 'self'`) exige CSS por `<link>` a `/forge-face.css` (plugin Vite); inyectar `<style>` inline deja la cara sin estilos.
- La revisión auditada de arranque (UEFI / systemd-boot) vive en `docs/12-research/bootloader-review.md`.
- La referencia visual histórica de la cara vive fuera del repo, en `/home/alexendros/Aplicaciones/Repertorio/archmaker_neubat.html` (no en Descargas); el nombre de ese archivo se conserva.
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"crates/archmaker-core",
"crates/archmaker-wasm",
"crates/archmaker-hardware",
"crates/archmaker-constitute",
"src-tauri",
]
resolver = "2"
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ Configurador local para diseñar, validar y exportar un perfil. Rust es la autor

Tres capas distintas (detalle: [`docs/10-delivery/closure/f8-governance.md`](docs/10-delivery/closure/f8-governance.md)):

| Capa | Estado |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Baseline de código | **passed** — tag `implementation-baseline-mvp5`; F0–F8 fusionados en `main` |
| Aprobación funcional | **pending** — matriz F7 ejecutable; sin `RC_SHA_FUNCIONAL` ni aceptación independiente (`f7-candidate.md`) |
| Capa | Estado |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Baseline de código | **passed** — tag `implementation-baseline-mvp5`; F0–F8 fusionados en `main` |
| Aprobación funcional | **pending** — matriz F7 ejecutable; sin `RC_SHA_FUNCIONAL` ni aceptación independiente (`f7-candidate.md`) |
| Release binaria | **published** — [Neubat v0.1.0](https://github.com/Alexendros/archmaker/releases/tag/v0.1.0) AppImage x86_64; verificación parcial |

- Código actual de `main` y el AppImage publicado son identidades distintas: el tip de `main`
Expand All @@ -23,9 +23,9 @@ Tres capas distintas (detalle: [`docs/10-delivery/closure/f8-governance.md`](doc
- Diagramas Archify (Forge as-built): [`docs/02-architecture/diagrams/`](docs/02-architecture/diagrams/).
Archify es herramienta de diagramación; no confundir con Forge.
- Gate global (mecánico): **Clear** — todos los gates G0–G10 están `complete`. Autoridad: [`docs/00-governance/go-no-go.md`](docs/00-governance/go-no-go.md) y [`docs/10-delivery/gates.md`](docs/10-delivery/gates.md).
- Veredicto de gobierno: **CONDITIONAL-CLEAR** hasta MVP-5 (sin runner). Autoriza el walking skeleton, los slices MVP-1..MVP-5 y el canal AppImage de MVP-6; runner, privilegios, ISO y Enterprise siguen bloqueados.
- Veredicto de gobierno: **CONDITIONAL-CLEAR** hasta MVP-5, más el slice ISO-01 de imagen viva x86_64 UEFI. Autoriza el walking skeleton, los slices MVP-1..MVP-5 y el canal AppImage de MVP-6. Siguen bloqueados el runner de instalación, la escritura de discos, la shell arbitraria y Enterprise.
- Baseline vigente: `implementation-baseline-mvp5` (commit anotado `b13c200…`; tags previos intactos). Remediación operativa: [`docs/10-delivery/remediation/`](docs/10-delivery/remediation/).
- Alcance MVP-5 **accepted** y cerrado (MVP-5-01..05): [`docs/10-delivery/mvp5-scope.md`](docs/10-delivery/mvp5-scope.md). Hueco residual: [`mvp5-gap-spec.md`](docs/10-delivery/mvp5-gap-spec.md) (draft). Empaquetado MVP-6 **accepted solo AppImage** ([`mvp6-scope.md`](docs/10-delivery/mvp6-scope.md)); ISO sigue sin `accepted`.
- Alcance MVP-5 **accepted** y cerrado (MVP-5-01..05): [`docs/10-delivery/mvp5-scope.md`](docs/10-delivery/mvp5-scope.md). Hueco residual: [`mvp5-gap-spec.md`](docs/10-delivery/mvp5-gap-spec.md) (draft). Empaquetado MVP-6 **accepted solo AppImage** ([`mvp6-scope.md`](docs/10-delivery/mvp6-scope.md)); imagen viva [`iso-scope.md`](docs/10-delivery/iso-scope.md) **accepted** (ISO-01). El target `archmaker-live-profile` está en el árbol; la cara no invoca `mkarchiso`.
- Fuentes históricas: `reference/v5.1/`, conservadas por hash; walking skeleton en [`docs/10-delivery/walking-skeleton.md`](docs/10-delivery/walking-skeleton.md).
- Rust: autoridad de dominio y casos de uso (nativo + WASM).
- React: cara Forge; escritorio → `TauriCorePort` (`invoke` de `@tauri-apps/api/core`); navegador → `WasmCorePort`. `MockCorePort` solo con `?mock=1` en desarrollo.
Expand Down
2 changes: 1 addition & 1 deletion apps/web/a11y-playwright.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ test.describe("F6 viewports y foco", () => {
await page.setViewportSize({ width, height: 800 });
await page.goto(BASE_URL);
await expect(
page.getByRole("heading", { name: /Disco y particiones/i }),
page.getByRole("heading", { name: /Sistema base/i }),
).toBeVisible({ timeout: 30_000 });
// Rail y stepper coexisten; solo uno es visible por viewport (CSS).
await expect(
Expand Down
109 changes: 27 additions & 82 deletions apps/web/e2e-fidelity-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
/**
* Helpers compartidos para el ejercicio E2E de cierre:
* selección Forge → Generar → Exportar → SHA-256(archivo) == binaryDigest.
* Fuera de alcance: imagen Arch, ISO, root, discos, pacstrap, instalación real.
* Helpers del recorrido Forge de cinco pasos:
* selección → Generar → Pedir imagen (error tipado mientras la constitución
* no está cableada).
*/
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { expect, type Page } from "@playwright/test";

/** Hostname del ejercicio de ejemplo (F3). */
export const E2E_HOSTNAME = "arch-e2e-audit";

/** Seis pasos del rail Forge (RAIL_STEPS). */
/** Cinco pasos del rail Forge (RAIL_STEPS). */
export const FORGE_RAIL_STEPS = [
"Disco y particiones",
"Sistema base",
"Entorno",
"Apps",
Expand All @@ -25,12 +22,6 @@ export const FORGE_RAIL_STEPS = [
* Cada fila debe poder ejercitarse o quedar cubierta por un test dedicado.
*/
export const ESSENTIAL_CONTROLS = [
{
id: "rail-storage",
route: "rail",
step: "Disco y particiones",
control: "button",
},
{ id: "rail-base", route: "rail", step: "Sistema base", control: "button" },
{ id: "rail-env", route: "rail", step: "Entorno", control: "button" },
{ id: "rail-apps", route: "rail", step: "Apps", control: "button" },
Expand Down Expand Up @@ -60,12 +51,6 @@ export const ESSENTIAL_CONTROLS = [
control: "Oscuro|Claro",
},
{ id: "header-cmdk", route: "header", step: "*", control: "Ctrl+K" },
{
id: "nav-to-base",
route: "storage",
step: "Disco y particiones",
control: "Continuar → Sistema base",
},
{
id: "nav-to-env",
route: "base",
Expand Down Expand Up @@ -97,16 +82,10 @@ export const ESSENTIAL_CONTROLS = [
control: "hostname text",
},
{
id: "identity-encrypt",
route: "identity",
step: "Identidad y seguridad",
control: "Activar cifrado",
},
{
id: "review-export",
id: "review-image",
route: "review",
step: "Revisión",
control: "Exportar",
control: "Pedir imagen",
},
{
id: "review-import",
Expand All @@ -116,30 +95,25 @@ export const ESSENTIAL_CONTROLS = [
},
] as const;

export function sha256FileHex(path: string): string {
return createHash("sha256").update(readFileSync(path)).digest("hex");
/** Texto visible: sin disco, partición, tabla, ESP, LUKS, cifrado ni formateo. */
export async function assertNoDiskVocabulary(page: Page): Promise<void> {
const body = await page.locator("body").innerText();
expect(body).not.toMatch(/\bdiscos?\b/i);
expect(body).not.toMatch(/partici[oó]n/i);
expect(body).not.toMatch(/\bGPT\b/);
expect(body).not.toMatch(/\bMBR\b/);
expect(body).not.toMatch(/\bESP\b/);
expect(body).not.toMatch(/\bLUKS\b/i);
expect(body).not.toMatch(/cifrado/i);
expect(body).not.toMatch(/formatear/i);
}

/** Declara storage mínimo (modo + acks) sin fingir detección. */
export async function completeStorageDeclaration(page: Page): Promise<void> {
await page
.getByRole("radio", { name: /Disco nuevo o sin datos a conservar/i })
.click();
await page
.getByRole("checkbox", { name: /declaración de intención/i })
.check();
}

/** Recorrido razonable: storage → base Ext4 → GNOME → identidad. */
/** Recorrido: base → GNOME → identidad. Sin disco ni cifrado. */
export async function completeExampleSelection(page: Page): Promise<void> {
await completeStorageDeclaration(page);
await page
.getByRole("button", { name: /Continuar → Sistema base/i })
.click();

await page.getByRole("radio", { name: /Linux Zen/i }).click();
await expect(page.getByRole("radio", { name: /systemd-boot/i })).toBeChecked();
await page.getByRole("radio", { name: /^Ext4$/i }).click();
await expect(
page.getByRole("radio", { name: /systemd-boot/i }),
).toBeChecked();

await page.getByRole("button", { name: /Continuar → Entorno/i }).click();
await expect(page.getByRole("radio", { name: /GNOME/i })).toBeChecked();
Expand All @@ -151,57 +125,28 @@ export async function completeExampleSelection(page: Page): Promise<void> {

await page.getByRole("button", { name: /Continuar → Identidad/i }).click();
await page.getByPlaceholder("mi-equipo").fill(E2E_HOSTNAME);
await page.getByRole("checkbox", { name: /Activar cifrado/i }).check();

await page.getByRole("button", { name: /Continuar → Revisión/i }).click();
await expect(page.getByRole("heading", { name: "Revisión" })).toBeVisible();
await expect(page.getByText(`Hostname: ${E2E_HOSTNAME}`)).toBeVisible();
await expect(
page.getByText(/Cifrado: intención marcada \(no aplicada al disco\)/i),
).toBeVisible();
}

/**
* Generar + Exportar: comprueba SHA-256 del archivo == binaryDigest (toast)
* y fidelidad hostname/cifrado en el JSON (F3).
* Generar deja el manifiesto listo. Pedir imagen llama a la operación tipada
* y no descarga un archivo: en este runtime no hay mkarchiso.
*/
export async function generateExportAndAssertFidelity(
page: Page,
): Promise<{ digest: string; filename: string }> {
export async function generateAndAskForImage(page: Page): Promise<void> {
await page.getByRole("button", { name: "Generar" }).click();
await expect(page.getByRole("heading", { name: "Revisión" })).toBeVisible();
await expect(page.locator("#toast-region")).toContainText(
/Manifest listo|Validación/i,
{ timeout: 60_000 },
);

const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Exportar" }).click();
const download = await downloadPromise;
const filename = download.suggestedFilename();
expect(filename).toMatch(/archmaker-profile-.*\.json/);

const path = await download.path();
expect(path).toBeTruthy();
const digest = sha256FileHex(path!);
expect(digest).toMatch(/^[0-9a-f]{64}$/);

const digestShort = digest.slice(0, 12);
await page.getByRole("button", { name: "Pedir imagen" }).click();
await expect(page.locator("#toast-region")).toContainText(
new RegExp(`Descarga iniciada \\(${digestShort}`),
/constitution-mkarchiso-absent/,
{ timeout: 15_000 },
);

const text = readFileSync(path!, "utf8");
expect(text).toContain("archmaker.face.hostname");
expect(text).toContain(E2E_HOSTNAME);
expect(text).toContain("archmaker.face.encrypt");
expect(text.toLowerCase()).not.toMatch(/cifrado aplicado|disk encrypted/);

// Paridad selección → export: arranque y filesystem del ejercicio.
expect(text).toMatch(/systemd-boot|archmaker\.boot\.systemd-boot/);
expect(text).toMatch(/ext4|archmaker\.filesystem\.ext4/i);
expect(text).toMatch(/zen|archmaker\.kernel\.zen/i);

return { digest, filename };
await expect(page.getByText(/Archivo guardado/i)).toHaveCount(0);
}
Loading
Loading