diff --git a/crates/archmaker-core/src/core.rs b/crates/archmaker-core/src/core.rs index 323e35b..0256de5 100644 --- a/crates/archmaker-core/src/core.rs +++ b/crates/archmaker-core/src/core.rs @@ -718,40 +718,93 @@ fn resolve_internal( }) } -pub fn render_artifact_bytes(manifest: &Manifest, target: &TargetRef) -> Result<(Vec, String)> { +/// Bytes + media type + adapter payload for a sealed manifest (ADR-0003 / DEC-001). +#[derive(Debug, Clone, PartialEq)] +pub struct RenderedArtifact { + pub bytes: Vec, + pub media_type: String, + pub content: serde_json::Value, +} + +fn ordered_effective_selections(manifest: &Manifest) -> Vec { + let mut rows = manifest.effective_selections.clone(); + rows.sort_by(|a, b| (&a.step_id, &a.option_id).cmp(&(&b.step_id, &b.option_id))); + rows +} + +fn render_archmaker_profile(manifest: &Manifest) -> Result { let source = CoreOp::ExportArtifact.source(); - match target.id.as_str() { - "archmaker-profile" | "archinstall-profile" => { - let value = - serde_json::to_value(manifest).map_err(|_| CoreError::doc_canonical(source))?; - Ok(( - archmaker_canonicalization::canonicalize(&value), - "application/json".to_string(), - )) - } - "report" => { - let mut text = String::from("# ArchMaker report\n\n"); - text.push_str(&format!("domain: {MANIFEST_DOMAIN_LABEL}\n")); - text.push_str(&format!("target: {} {}\n", target.id, target.version)); - text.push_str(&format!( - "contentDigest: sha256:{}\n\n", - manifest.content_digest.value - )); - text.push_str("## Effective selections\n\n"); - let mut rows = manifest.effective_selections.clone(); - rows.sort_by(|a, b| (&a.step_id, &a.option_id).cmp(&(&b.step_id, &b.option_id))); - for selection in &rows { - let origin = match selection.origin { - SelectionOrigin::Manual => "manual", - SelectionOrigin::Derived => "derived", - }; - text.push_str(&format!( - "- {} = {} ({})\n", - selection.step_id, selection.option_id, origin - )); + let value = serde_json::to_value(manifest).map_err(|_| CoreError::doc_canonical(source))?; + Ok(RenderedArtifact { + bytes: archmaker_canonicalization::canonicalize(&value), + media_type: "application/json".to_string(), + content: value, + }) +} + +/// Experimental archinstall adapter: versioned envelope, never the raw manifest (ADR-0003). +fn render_archinstall_profile(manifest: &Manifest, target: &TargetRef) -> Result { + let source = CoreOp::ExportArtifact.source(); + let selections = ordered_effective_selections(manifest); + let envelope = serde_json::json!({ + "adapter": "archinstall-profile", + "version": target.version, + "manifestRef": { + "contentDigest": { + "algorithm": "sha256", + "value": manifest.content_digest.value, + "kind": "content", + } + }, + "effectiveSelections": selections, + }); + // Reject executable / shell-shaped keys if they ever appear in the envelope. + if let Some(obj) = envelope.as_object() { + for banned in ["cmd", "hooks", "pacstrap", "shell", "script", "commands"] { + if obj.contains_key(banned) { + return Err(CoreError::tgt_unsupported(source)); } - Ok((text.into_bytes(), "text/markdown".to_string())) } + } + Ok(RenderedArtifact { + bytes: archmaker_canonicalization::canonicalize(&envelope), + media_type: "application/json".to_string(), + content: envelope, + }) +} + +fn render_report(manifest: &Manifest, target: &TargetRef) -> Result { + let mut text = String::from("# ArchMaker report\n\n"); + text.push_str(&format!("domain: {MANIFEST_DOMAIN_LABEL}\n")); + text.push_str(&format!("target: {} {}\n", target.id, target.version)); + text.push_str(&format!( + "contentDigest: sha256:{}\n\n", + manifest.content_digest.value + )); + text.push_str("## Effective selections\n\n"); + for selection in &ordered_effective_selections(manifest) { + let origin = match selection.origin { + SelectionOrigin::Manual => "manual", + SelectionOrigin::Derived => "derived", + }; + text.push_str(&format!( + "- {} = {} ({})\n", + selection.step_id, selection.option_id, origin + )); + } + Ok(RenderedArtifact { + bytes: text.as_bytes().to_vec(), + media_type: "text/markdown".to_string(), + content: serde_json::Value::String(text), + }) +} + +pub fn render_artifact_bytes(manifest: &Manifest, target: &TargetRef) -> Result { + let source = CoreOp::ExportArtifact.source(); + match target.id.as_str() { + "archmaker-profile" => render_archmaker_profile(manifest), + "archinstall-profile" => render_archinstall_profile(manifest, target), + "report" => render_report(manifest, target), _ => Err(CoreError::tgt_unsupported(source)), } } @@ -999,10 +1052,10 @@ impl CorePort for RealCore { if recomputed.value != input.manifest.content_digest.value { return Err(CoreError::doc_canonical(source)); } - let (bytes, media_type) = render_artifact_bytes(&input.manifest, &input.target_ref)?; + let rendered = render_artifact_bytes(&input.manifest, &input.target_ref)?; let binary = BinaryDigest { algorithm: DigestAlgorithm::Sha256, - value: sha256_hex(&bytes), + value: sha256_hex(&rendered.bytes), kind: Some(BinaryKind::Binary), }; let dir = self @@ -1012,27 +1065,32 @@ impl CorePort for RealCore { atomic_write_bytes( CoreOp::ExportArtifact, &dir.join(input.destination.token()), - &bytes, + &rendered.bytes, )?; + let experimental = if input.target_ref.id == "archinstall-profile" { + Some(true) + } else { + None + }; Ok(Artifact { document_version: input.manifest.document_version.clone(), schema_version: input.manifest.schema_version.clone(), artifact_ref: ArtifactRef { binary_digest: binary.clone(), - media_type: media_type.clone(), + media_type: rendered.media_type.clone(), }, target_ref: input.target_ref, producer: Producer { id: PRODUCER_ID.to_string(), version: PRODUCER_VERSION.to_string(), }, - media_type, + media_type: rendered.media_type, binary_digest: binary, manifest_ref: ManifestRef { content_digest: input.manifest.content_digest.clone(), }, - experimental: Some(input.manifest.target_ref.id == "archinstall-profile"), - content: None, + experimental, + content: Some(rendered.content), }) } diff --git a/crates/archmaker-core/src/lib.rs b/crates/archmaker-core/src/lib.rs index 4290833..e160cbd 100644 --- a/crates/archmaker-core/src/lib.rs +++ b/crates/archmaker-core/src/lib.rs @@ -23,7 +23,7 @@ pub use archmaker_domain::{ pub use archmaker_rules::Diagnostic as RuleDiagnostic; pub use core::{ catalog_content_digest, draft_content_digest, is_uuid, manifest_content_digest, new_uuid_v4, - render_artifact_bytes, RealCore, EMBEDDED_CATALOG_JSON, PIPELINE_VERSION, + render_artifact_bytes, RealCore, RenderedArtifact, EMBEDDED_CATALOG_JSON, PIPELINE_VERSION, }; pub use error::{CoreError, ErrorCategory, ErrorFamily, Result, Severity}; pub use inputs::{ diff --git a/crates/archmaker-core/tests/core_contract.rs b/crates/archmaker-core/tests/core_contract.rs index f74c25a..fd3cc9d 100644 --- a/crates/archmaker-core/tests/core_contract.rs +++ b/crates/archmaker-core/tests/core_contract.rs @@ -542,11 +542,128 @@ fn export_artifact_byte_idempotent() { .expect("exports again"); assert_eq!(first.binary_digest, second.binary_digest); assert_eq!(first.manifest_ref.content_digest, manifest.content_digest); + assert!(first.content.is_some(), "adapter content filled"); let bytes_a = std::fs::read(dir.join("artifact-a.json")).expect("reads a"); let bytes_b = std::fs::read(dir.join("artifact-b.json")).expect("reads b"); assert_eq!(bytes_a, bytes_b); - let (rendered, _) = render_artifact_bytes(&manifest, &export_target()).expect("renders"); - assert_eq!(rendered, bytes_a); + let rendered = render_artifact_bytes(&manifest, &export_target()).expect("renders"); + assert_eq!(rendered.bytes, bytes_a); + assert_eq!(rendered.media_type, "application/json"); + let _ = std::fs::remove_dir_all(&dir); +} + +fn export_target_archinstall() -> TargetRef { + TargetRef { + id: "archinstall-profile".to_string(), + version: "0.1.0".to_string(), + } +} + +fn export_target_report() -> TargetRef { + TargetRef { + id: "report".to_string(), + version: "0.1.0".to_string(), + } +} + +#[test] +fn export_adapters_diverge_bytes_and_fill_content() { + let (core, dir) = core_with_store("export-adapters"); + let draft = valid_draft(&core); + let manifest_archmaker = core + .build_manifest(BuildManifestInput { + draft: draft.clone(), + catalog_ref: catalog_ref(), + target_ref: export_target(), + expected_resolution_digest: None, + request_id: None, + }) + .expect("builds archmaker"); + let manifest_archinstall = core + .build_manifest(BuildManifestInput { + draft: draft.clone(), + catalog_ref: catalog_ref(), + target_ref: export_target_archinstall(), + expected_resolution_digest: None, + request_id: None, + }) + .expect("builds archinstall"); + let manifest_report = core + .build_manifest(BuildManifestInput { + draft, + catalog_ref: catalog_ref(), + target_ref: export_target_report(), + expected_resolution_digest: None, + request_id: None, + }) + .expect("builds report"); + + let archmaker = core + .export_artifact(ExportArtifactInput { + manifest: manifest_archmaker.clone(), + target_ref: export_target(), + destination: DestinationHandle::new("a.json".to_string()).expect("handle"), + request_id: None, + }) + .expect("exports archmaker"); + let archinstall = core + .export_artifact(ExportArtifactInput { + manifest: manifest_archinstall.clone(), + target_ref: export_target_archinstall(), + destination: DestinationHandle::new("b.json".to_string()).expect("handle"), + request_id: None, + }) + .expect("exports archinstall"); + let report = core + .export_artifact(ExportArtifactInput { + manifest: manifest_report, + target_ref: export_target_report(), + destination: DestinationHandle::new("c.md".to_string()).expect("handle"), + request_id: None, + }) + .expect("exports report"); + + assert_ne!( + archmaker.binary_digest.value, + archinstall.binary_digest.value + ); + assert_ne!(archmaker.binary_digest.value, report.binary_digest.value); + assert_ne!(archinstall.binary_digest.value, report.binary_digest.value); + assert_eq!( + archmaker.manifest_ref.content_digest.value, + manifest_archmaker.content_digest.value + ); + assert_eq!( + archinstall.manifest_ref.content_digest.value, + manifest_archinstall.content_digest.value + ); + assert_eq!(archinstall.experimental, Some(true)); + assert!(archmaker.experimental.is_none()); + assert_eq!(report.media_type, "text/markdown"); + assert!(archmaker.content.is_some()); + assert!(report.content.as_ref().is_some_and(|v| v.is_string())); + + let envelope = archinstall.content.as_ref().expect("archinstall content"); + assert_eq!(envelope["adapter"], "archinstall-profile"); + assert_eq!(envelope["version"], "0.1.0"); + assert_eq!( + envelope["manifestRef"]["contentDigest"]["value"], + manifest_archinstall.content_digest.value + ); + assert!(envelope["effectiveSelections"].is_array()); + for banned in ["cmd", "hooks", "pacstrap", "shell", "script", "commands"] { + assert!( + envelope.get(banned).is_none(), + "envelope must not carry executable key {banned}" + ); + } + let bytes_a = std::fs::read(dir.join("a.json")).expect("a"); + let bytes_b = std::fs::read(dir.join("b.json")).expect("b"); + assert_ne!(bytes_a, bytes_b); + // archinstall envelope must not be a raw re-serialization of the domain Manifest. + let parsed_b: serde_json::Value = serde_json::from_slice(&bytes_b).expect("json"); + assert!(parsed_b.get("domainLabel").is_none()); + assert_eq!(parsed_b["adapter"], "archinstall-profile"); let _ = std::fs::remove_dir_all(&dir); } diff --git a/docs/00-governance/go-no-go.md b/docs/00-governance/go-no-go.md index 74675c5..b5e7d38 100644 --- a/docs/00-governance/go-no-go.md +++ b/docs/00-governance/go-no-go.md @@ -24,10 +24,14 @@ reviewers: - Reevaluación slice MVP-2 (MVP-2-07): **CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2** (reglas de catálogo, resolución de dominio, validación que bloquea manifiesto). Baseline emitida: `implementation-baseline-mvp2` (tags previos intactos). +- Reevaluación slice MVP-3 (MVP-3-06): **CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2/MVP-3** + (adapters de exportación versionados; ADR-0003 parcial). Baseline emitida: + `implementation-baseline-mvp3` (tags previos intactos). - Alcance autorizado: `docs/10-delivery/walking-skeleton.md` (DOC-DEL-WSK-001), - `docs/10-delivery/mvp1-scope.md` (DOC-DEL-MVP1-001) y `docs/10-delivery/mvp2-scope.md` - (DOC-DEL-MVP2-001). Bloqueados: runner real, root, discos, shell arbitraria, catálogos remotos, - firma criptográfica de catálogo en carga, Enterprise. + `docs/10-delivery/mvp1-scope.md` (DOC-DEL-MVP1-001), `docs/10-delivery/mvp2-scope.md` + (DOC-DEL-MVP2-001) y `docs/10-delivery/mvp3-scope.md` (DOC-DEL-MVP3-001). Bloqueados: runner real, + root, discos, shell arbitraria, catálogos remotos, firma criptográfica de catálogo en carga, + Enterprise. ## Motivo del resultado mecánico @@ -94,10 +98,10 @@ Resultado derivado: **Clear** — todos los gates G0–G10 están `complete`. ## Consecuencia inmediata Autorizado: el walking skeleton MVP-0 definido en `docs/10-delivery/walking-skeleton.md`, -el slice **MVP-1** (`mvp1-scope.md`) y el slice **MVP-2** (reglas de catálogo / resolución / -validación) según `docs/10-delivery/mvp2-scope.md`. **No** autorizado: runner real, root, -discos, shell arbitraria, catálogos remotos, firma criptográfica de catálogo en carga, ni -Enterprise. +el slice **MVP-1** (`mvp1-scope.md`), el slice **MVP-2** (reglas / resolución / validación) +según `docs/10-delivery/mvp2-scope.md` y el slice **MVP-3** (adapters de exportación) según +`docs/10-delivery/mvp3-scope.md`. **No** autorizado: runner real, root, discos, shell +arbitraria, catálogos remotos, firma criptográfica de catálogo en carga, ni Enterprise. ## Decisiones de alcance (C3 — G1-C03) diff --git a/docs/00-governance/plan-status.md b/docs/00-governance/plan-status.md index 1004325..877eb80 100644 --- a/docs/00-governance/plan-status.md +++ b/docs/00-governance/plan-status.md @@ -16,8 +16,8 @@ reviewers: # Estado de planificación - Estado: in-review -- Gate global: CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2 (sin runner) -- Baseline vigente: `implementation-baseline-mvp2` (predecesores mvp0.1 y mvp1 intactos) +- Gate global: CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2/MVP-3 (sin runner) +- Baseline vigente: `implementation-baseline-mvp3` (predecesores mvp0.1, mvp1 y mvp2 intactos) - Fecha de corte: 2026-10-12 ## Bloqueos P0 @@ -28,8 +28,8 @@ documento no los repite. ## Regla de avance -Se autorizan el walking skeleton MVP-0 y el alcance MVP-1 de -`docs/10-delivery/mvp1-scope.md` (dominio, catálogo local, migración). El runner v1 y los +Se autorizan el walking skeleton MVP-0 y los slices MVP-1 (`mvp1-scope.md`), MVP-2 +(`mvp2-scope.md`) y MVP-3 (`mvp3-scope.md`, adapters de exportación). El runner v1 y los privilegios se autorizan únicamente cuando G0–G10 tengan evidencia específica de esas fases, no existan riesgos P0 sin tratamiento y todos los contratos P0 posean fixtures válidos e inválidos. diff --git a/docs/02-architecture/adr/ADR-0003-modelo-canonico-separado-del-target.md b/docs/02-architecture/adr/ADR-0003-modelo-canonico-separado-del-target.md index 8d4a0a6..1b399ba 100644 --- a/docs/02-architecture/adr/ADR-0003-modelo-canonico-separado-del-target.md +++ b/docs/02-architecture/adr/ADR-0003-modelo-canonico-separado-del-target.md @@ -4,8 +4,8 @@ phase: MVP priority: P0 documentStatus: accepted approvalStatus: approved -implementationStatus: not-started -verificationStatus: not-verified +implementationStatus: partial +verificationStatus: partial releaseStatus: ineligible owners: - architecture diff --git a/docs/10-delivery/backlog.md b/docs/10-delivery/backlog.md index 9cd8580..1d646b6 100644 --- a/docs/10-delivery/backlog.md +++ b/docs/10-delivery/backlog.md @@ -125,6 +125,19 @@ Detalle y exclusiones: [`mvp1-scope.md`](mvp1-scope.md). Detalle y exclusiones: [`mvp2-scope.md`](mvp2-scope.md). +## Issues MVP-3 + +| Issue | Título | Dueño | Estado | Gate | +| -------- | ------------------------------------------------ | ------------ | ------ | ------ | +| MVP-3-01 | Aprobar alcance DOC-DEL-MVP3-001 | Product | closed | G2,G10 | +| MVP-3-02 | Adapter `archmaker-profile` + `report` + content | Architecture | closed | G4,G5 | +| MVP-3-03 | Sobre experimental `archinstall-profile` | Architecture | closed | G3,G5 | +| MVP-3-04 | Tests divergencia / idempotencia / rechazo | Quality | closed | G5,G9 | +| MVP-3-05 | Trazabilidad ampliada MVP-3 | Quality | closed | G2,G9 | +| MVP-3-06 | Tag `implementation-baseline-mvp3` | Release | closed | G10 | + +Detalle y exclusiones: [`mvp3-scope.md`](mvp3-scope.md). + ## Reglas - Cada issue P0 tiene owner, fecha objetivo y criterio de cierre (ver `status-model.md`). diff --git a/docs/10-delivery/mvp3-scope.md b/docs/10-delivery/mvp3-scope.md new file mode 100644 index 0000000..0e2bf9f --- /dev/null +++ b/docs/10-delivery/mvp3-scope.md @@ -0,0 +1,95 @@ +--- +id: DOC-DEL-MVP3-001 +phase: MVP +priority: P0 +documentStatus: accepted +approvalStatus: approved +implementationStatus: complete +verificationStatus: partial +releaseStatus: ineligible +owners: + - architecture + - data +reviewers: + - independent-reviewer +dependsOn: + - id: DOC-DEL-MVP2-001 + expectedDocumentStatus: accepted + - id: DOC-GOV-GNG-001 + - id: ADR-0003 + expectedDocumentStatus: accepted +--- + +# Alcance MVP-3 — manifest / exporters + +- Documento: DOC-DEL-MVP3-001 · Estado: accepted · Propietario: Arquitectura/Datos · Fecha: 2026-10-12 +- Entrada: baseline `implementation-baseline-mvp2` + gates G0–G10 `complete` (slice). +- Alcance **accepted** (MVP-3-01). Implementación de adapters de exportación versionados + autorizada bajo las exclusiones de este documento; runner y privilegios siguen fuera. + +## Objetivo + +Hacer que `exportArtifact` cumpla ADR-0003 y FR-EXPORT-001: cada `ExportTarget` de DEC-001 es +un adapter versionado que traduce un `Manifest` sellado a un `Artifact` con bytes propios, +sin alterar el draft, sin shell y sin cambiar la superficie de CorePort. + +## Incluido + +| Área | Contenido | Anclas | +| --------------------- | ----------------------------------------------------------------------------------------------------- | -------------------------------- | +| Perfil ArchMaker | Bytes = manifest canónico JSON; `mediaType` `application/json`; `content` = payload del perfil | FR-EXPORT-001, DEC-001 | +| Reporte | Markdown determinista ordenado; `mediaType` `text/markdown` | FR-EXPORT-001, DEC-001 | +| Sobre archinstall | Envelope JSON experimental distinto del manifest; `experimental: true`; sin cmd/hooks/shell/rutas | FR-EXPORT-001, ADR-0003, DEC-001 | +| Compatibilidad target | Etapa 12 del pipeline: target soportado, alineado al manifest, digest recomputable | VAL-TARGET, pipeline.md etapa 12 | +| Determinismo | Reexportar el mismo manifest sellado ⇒ mismos bytes/`binaryDigest`; adapters producen bytes distintos | NFR-DET-001, TST-EXP-001 | + +## Excluido (explícito) + +- Runner v1, root, discos reales, shell arbitraria, sidecars. +- Catálogos remotos / firma criptográfica de catálogo en carga (etapa 11). +- Schema upstream de `archinstall` ni config ejecutable. +- Etapa 10 como schema nuevo de hardware (el draft ya fija `arch-x86_64`, DEC-008). +- Nuevas operaciones CorePort (`listExportTargets`, etc.). +- Presets, Enterprise, telemetría, auto-updater, rediseño visual. + +## Inventario de gaps (vs MVP-2) + +| Gap | Estado en MVP-2 | Trabajo MVP-3 | +| ------------------------------------------- | ----------------------------------------------- | ----------------------------------------------- | +| `archmaker-profile` ≡ `archinstall-profile` | Mismos bytes canónicos del manifest | Sobre experimental distinto + `content` relleno | +| `Artifact.content` | Siempre `None` | Payload del adapter por target | +| ADR-0003 | `accepted`, `implementationStatus: not-started` | Evidencia de adapters versionados | +| Etapa 12 exporter compatibility | Diferida explícitamente a MVP-3 | Rechazo tipado + tests VAL-TARGET / TST-EXP-001 | + +## Issues trazables (orden por dependencia) + +| # | Issue | Depende de | Gate | Owner | Criterio Done | +| -------- | ------------------------------------------------------------- | ---------- | ------ | ------------ | ------------------------------------------------ | +| MVP-3-01 | Aprobar alcance DOC-DEL-MVP3-001 + actualizar roadmap/backlog | — | G2,G10 | Product | Documento `accepted`; exclusiones firmadas | +| MVP-3-02 | Adapter `archmaker-profile` + `report` con `Artifact.content` | 01 | G4,G5 | Architecture | Bytes canónicos / markdown; content relleno | +| MVP-3-03 | Sobre experimental `archinstall-profile` (ADR-0003) | 01,02 | G3,G5 | Architecture | Bytes ≠ perfil ArchMaker; sin campos ejecutables | +| MVP-3-04 | Tests divergencia / idempotencia / rechazo (TST-EXP-001) | 02,03 | G5,G9 | Quality | core_contract verde; VAL-TARGET evidenciado | +| MVP-3-05 | Trazabilidad FR-EXPORT-001 / NFR-DET-001 / ADR-0003 ampliada | 04 | G2,G9 | Quality | Matriz sin huérfanos del slice MVP-3 | +| MVP-3-06 | Tag `implementation-baseline-mvp3` tras review | 01–05 | G10 | Release | Tag anotado; CI verde; go-no-go reevaluado | + +## Trazabilidad + +- Requisitos: FR-EXPORT-001, FR-MANIFEST-001 (entrada), NFR-DET-001, NFR-PORT-001. +- Riesgos: RSK-005 (adapter obsoleto — residual aceptado para experimental). +- Amenazas: sin superficie nueva de shell/red/disco (CON-005/006/012 siguen). +- Validadores: VAL-TARGET, corpus schema `artifact`. +- Pruebas: TST-EXP-001 (ampliación), paridad export si el golden lo exige. +- Decisiones/ADR: DEC-001, DEC-008, ADR-0003. + +## Criterio de entrada + +1. Baseline `implementation-baseline-mvp2` presente; tags previos intactos. +2. `python3 tools/gates.py` → Clear; validadores locales en verde. +3. Aprobación explícita de este alcance antes de código de producto MVP-3. + +## Criterio de salida + +- Issues MVP-3-01..06 cerrados o con residual fechado. +- Tag `implementation-baseline-mvp3` emitido; tags MVP-0.1, MVP-1 y MVP-2 intactos. +- `archinstall-profile` y `archmaker-profile` no son byte-idénticos. +- Reexportar el mismo manifest produce los mismos bytes y el mismo `binaryDigest`. diff --git a/docs/10-delivery/next-issues.md b/docs/10-delivery/next-issues.md index 99c8244..83661cf 100644 --- a/docs/10-delivery/next-issues.md +++ b/docs/10-delivery/next-issues.md @@ -78,6 +78,19 @@ Fuente canónica de alcance: [`mvp2-scope.md`](mvp2-scope.md). | MVP-2-06 | Trazabilidad MVP-2 sin huérfanos | 04,05 | G2,G9 | Quality | Matriz verde | closed | | MVP-2-07 | Tag `implementation-baseline-mvp2` | 01–06 | G10 | Release | Tag + CI verde | closed | +## Issues MVP-3 + +Fuente canónica de alcance: [`mvp3-scope.md`](mvp3-scope.md). + +| # | Issue | Depende de | Gate | Owner | Criterio Done | Estado | +| -------- | ------------------------------------------------ | ---------- | ------ | ------------ | ---------------------------------- | ------ | +| MVP-3-01 | Aprobar alcance DOC-DEL-MVP3-001 | — | G2,G10 | Product | Documento `accepted` | closed | +| MVP-3-02 | Adapter `archmaker-profile` + `report` + content | 01 | G4,G5 | Architecture | Content relleno; bytes canónicos | closed | +| MVP-3-03 | Sobre experimental `archinstall-profile` | 01,02 | G3,G5 | Architecture | Bytes ≠ ArchMaker; sin ejecutables | closed | +| MVP-3-04 | Tests divergencia / idempotencia / rechazo | 02,03 | G5,G9 | Quality | TST-EXP-001 / VAL-TARGET | closed | +| MVP-3-05 | Trazabilidad MVP-3 sin huérfanos | 04 | G2,G9 | Quality | Matriz verde | closed | +| MVP-3-06 | Tag `implementation-baseline-mvp3` | 01–05 | G10 | Release | Tag + CI verde | closed | + ## Trazabilidad de los issues - Requisitos: ISSUE-003 (`FR-*`, `NFR-*`), ISSUE-006 (FR-DRAFT/EXPORT, NFR-MIG), ISSUE-007 (FR-VALIDATE/NFR-DET), ISSUE-008 (NFR-PORT/SEC/OBS). @@ -88,7 +101,8 @@ Fuente canónica de alcance: [`mvp2-scope.md`](mvp2-scope.md). ## Regla de cierre del plan -El veredicto vigente es **CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2** +El veredicto vigente es **CONDITIONAL-CLEAR — walking skeleton MVP-0 + slices MVP-1/MVP-2/MVP-3** (`docs/00-governance/go-no-go.md`), con gates G0–G10 `complete`. MVP-1-01..08 están `closed`; -MVP-2-01..07 están `closed`; baseline `implementation-baseline-mvp2` emitida (tags previos intactos). -La autorización del MVP completo y del runner exige su propio gate. +MVP-2-01..07 están `closed`; MVP-3-01..06 están `closed`; baseline `implementation-baseline-mvp3` +emitida (tags previos intactos). La autorización del MVP completo y del runner exige su propio +gate. diff --git a/docs/10-delivery/roadmap.md b/docs/10-delivery/roadmap.md index 9b63962..2338cfe 100644 --- a/docs/10-delivery/roadmap.md +++ b/docs/10-delivery/roadmap.md @@ -24,7 +24,7 @@ PD-0 fuentes; PD-1 producto; PD-2 arquitectura; PD-3 datos; PD-4 interfaces; PD- - MVP-0.1 walking skeleton stabilizado (baseline `implementation-baseline-mvp0.1`): CorePort, adapters Tauri/WASM, canonicalización, reglas, resolución, manifest, export, security, CI, supply chain mínimo. Sin runner ni privilegios. - MVP-1 domain/schema/catalog/migrations — baseline `implementation-baseline-mvp1` ([`mvp1-scope.md`](mvp1-scope.md)). - MVP-2 rules/resolver/validation — baseline `implementation-baseline-mvp2` ([`mvp2-scope.md`](mvp2-scope.md)). -- MVP-3 manifest/exporters. +- MVP-3 manifest/exporters — baseline `implementation-baseline-mvp3` ([`mvp3-scope.md`](mvp3-scope.md)). - MVP-4 React + MockCorePort + design system. - MVP-5 Tauri/WASM/persistencia/paridad. - MVP-6 packaging/hardening/beta/stable. diff --git a/docs/10-delivery/tests-inventory.json b/docs/10-delivery/tests-inventory.json index 10a5c88..66f9165 100644 --- a/docs/10-delivery/tests-inventory.json +++ b/docs/10-delivery/tests-inventory.json @@ -1,6 +1,6 @@ { - "generated_at": "1791822953", - "git_sha": "773487e554710eb1e536d824f0a1002bae1c2688", + "generated_at": "1791827390", + "git_sha": "9cef12060127a9a76520c1be4cca1635ab057d0c", "tests": [ { "id": "TST-ARCHMAKER-CANONICALIZATION-VECTORS-GOLDEN_CORPUS_BYTE_IDENTITY_AND_DIGEST", @@ -436,6 +436,15 @@ "covers": ["FR-EXPORT-001"], "description": "core_contract::export_artifact_byte_idempotent" }, + { + "id": "TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ADAPTERS_DIVERGE_BYTES_AND_FILL_CONTENT", + "type": "integration", + "location": "crates/archmaker-core/tests/core_contract.rs", + "command": "cargo test -p archmaker-core --test core_contract export_adapters_diverge_bytes_and_fill_content", + "workflow": "ci", + "covers": ["FR-EXPORT-001"], + "description": "core_contract::export_adapters_diverge_bytes_and_fill_content" + }, { "id": "TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_REJECTS_BAD_TARGET_AND_HANDLE", "type": "integration", diff --git a/docs/10-delivery/traceability-matrix.json b/docs/10-delivery/traceability-matrix.json index f8d1daa..ba2e8e1 100644 --- a/docs/10-delivery/traceability-matrix.json +++ b/docs/10-delivery/traceability-matrix.json @@ -1,7 +1,7 @@ { "metadata": { - "generated_at": "1791822953", - "git_sha": "773487e554710eb1e536d824f0a1002bae1c2688", + "generated_at": "1791827390", + "git_sha": "9cef12060127a9a76520c1be4cca1635ab057d0c", "requirements_file": "docs/01-product/requirements.md", "inventory_file": "docs/10-delivery/tests-inventory.json", "validators_file": "docs/10-delivery/validators.json" @@ -70,7 +70,7 @@ "evidence_count": 34, "has_executable_evidence": true, "passes_ci": true, - "evidence_types": ["negative", "unit", "schema", "integration"], + "evidence_types": ["schema", "unit", "integration", "negative"], "coverage_status": "covered" }, { @@ -126,7 +126,7 @@ "evidence_count": 41, "has_executable_evidence": true, "passes_ci": true, - "evidence_types": ["negative", "unit", "schema", "integration"], + "evidence_types": ["negative", "schema", "unit", "integration"], "coverage_status": "covered" }, { @@ -155,11 +155,12 @@ "pending": false, "tests": [ "TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_BYTE_IDEMPOTENT", + "TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ADAPTERS_DIVERGE_BYTES_AND_FILL_CONTENT", "TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_REJECTS_BAD_TARGET_AND_HANDLE", "TST-ARCHMAKER-CORE-CORE_CONTRACT-END_TO_END_CREATE_SAVE_RESOLVE_VALIDATE_BUILD_EXPORT" ], "validators": [], - "evidence_count": 3, + "evidence_count": 4, "has_executable_evidence": true, "passes_ci": true, "evidence_types": ["integration"], @@ -232,11 +233,11 @@ "has_executable_evidence": true, "passes_ci": true, "evidence_types": [ - "unit", - "integration", - "negative", + "canonicalize", "schema", - "canonicalize" + "negative", + "integration", + "unit" ], "coverage_status": "covered" }, @@ -276,7 +277,7 @@ "evidence_count": 6, "has_executable_evidence": true, "passes_ci": true, - "evidence_types": ["negative", "integration"], + "evidence_types": ["integration", "negative"], "coverage_status": "covered" }, { @@ -357,7 +358,7 @@ "evidence_count": 13, "has_executable_evidence": true, "passes_ci": true, - "evidence_types": ["negative", "unit", "integration", "canonicalize"], + "evidence_types": ["negative", "canonicalize", "unit", "integration"], "coverage_status": "covered" }, { @@ -474,7 +475,7 @@ "evidence_count": 28, "has_executable_evidence": true, "passes_ci": true, - "evidence_types": ["negative", "unit"], + "evidence_types": ["unit", "negative"], "coverage_status": "covered" } ], diff --git a/docs/10-delivery/traceability-matrix.md b/docs/10-delivery/traceability-matrix.md index d84fad9..6e11d9f 100644 --- a/docs/10-delivery/traceability-matrix.md +++ b/docs/10-delivery/traceability-matrix.md @@ -17,7 +17,7 @@ reviewers: # Matriz de Trazabilidad FR/NFR -> TST/VAL -- **Generado**: 773487e5 +- **Generado**: 9cef1206 - **Total requisitos**: 17 - **En alcance (mvp0.1)**: 13 - **Diferidos**: 3 @@ -44,25 +44,25 @@ reviewers: ## Matriz de Requisitos -| Req | Tipo | Nombre | Scope | Tests | Validators | Evidencia | CI | Estado | -| --------------- | ---- | ------------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------- | --- | -------- | -| FR-CAT-001 | FR | Cargar catálogo versionado. | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-ALLOWED_INVOKE_PASSES_CAPABILITY_GATE... | VAL-SCHEMA-REFS, VAL-NEG-CAPABILITIES | 34 | OK | covered | -| FR-DRAFT-001 | FR | Crear y guardar drafts locales | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-PARITY-PARITY_CREATE_DRAFT_DETERMINISTIC_BYTES, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001... | VAL-SCHEMA-REFS, VAL-NEG-CAPABILITIES | 41 | OK | covered | -| FR-ENT-001 | FR | Aplicar perfiles/policies por | deferred | -- | -- | 0 | OK | deferred | -| FR-EXPORT-001 | FR | Exportar artefacto y reporte. | mvp0.1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_BYTE_IDEMPOTENT, TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_REJECTS_BAD_TARGET_AND_HANDLE, TST-ARCHMAKER-CORE-CORE_CONTRACT-END_TO_END_CREATE_SAVE_RESOLVE_VALIDATE_BUILD_EXPORT | -- | 3 | OK | covered | -| FR-IMPORT-001 | FR | Importar y migrar v5.1. | mvp1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-IMPORT_DRAFT_MIGRATES_V51_SAMPLE | VAL-MIG | 2 | OK | covered | -| FR-MANIFEST-001 | FR | Construir manifest canónico. | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-PARITY-PARITY_RESOLVE_AND_MANIFEST, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001... | VAL-CANONICALIZE, VAL-SCHEMA-REFS... | 33 | OK | covered | -| FR-PRESET-001 | FR | Aplicar presets como patches. | deferred | -- | -- | 0 | OK | deferred | -| FR-RESOLVE-001 | FR | Resolver capacidades/conflicto | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_RESOLVE_AND_MANIFEST, TST-ARCHMAKER-CORE-CAPABILITIES_NEGATIVE-NAMED_ALLOWED_INVOKE_RESOLVES_OP, TST-ARCHMAKER-CORE-CORE_CONTRACT-RESOLVE_DRAFT_VALID_AND_DETERMINISTIC... | -- | 6 | OK | covered | -| FR-RUN-001 | FR | Ejecutar plan aprobado. | deferred | -- | -- | 0 | OK | deferred | -| FR-VALIDATE-001 | FR | Ejecutar pipeline de validació | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_VALIDATE_DIAGNOSTICS, TST-ARCHMAKER-CORE-CORE_CONTRACT-VALIDATE_DRAFT_CLEAN_AND_BLOCKING, TST-ARCHMAKER-CORE-CORE_CONTRACT-END_TO_END_CREATE_SAVE_RESOLVE_VALIDATE_BUILD_EXPORT | -- | 3 | OK | covered | -| NFR-ACC-001 | NFR | WCAG 2.2 AA. | mvp0.1 | TST-WEB-A11Y-PLAYWRIGHT, TST-WEB-VISUAL-REGRESSION | -- | 2 | OK | covered | -| NFR-DET-001 | NFR | Mismo input/versiones → mismo | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-GOLDEN_CORPUS_BYTE_IDENTITY_AND_DIGEST, TST-ARCHMAKER-CANONICALIZATION-VECTORS-M_KEY_ORDER_BYTE_IDENTITY, TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY... | VAL-CANONICALIZE | 13 | OK | covered | -| NFR-MIG-001 | NFR | Migraciones explícitas y no de | mvp1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-IMPORT_DRAFT_MIGRATES_V51_SAMPLE | VAL-MIG | 2 | OK | covered | -| NFR-OBS-001 | NFR | Errores/eventos estructurados | pending | -- | -- | 0 | OK | pending | -| NFR-OFF-001 | NFR | MVP usable sin CDN ni servidor | mvp0.1 | TST-WEB-VISUAL-REGRESSION | -- | 1 | OK | covered | -| NFR-PORT-001 | NFR | Dominio independiente de adapt | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_MAPS_VECTORS, TST-ARCHMAKER-WASM-PARITY-PARITY_DOMAIN_VECTORS, TST-ARCHMAKER-WASM-PARITY-PARITY_UNICODE_VECTORS... | -- | 6 | OK | covered | -| NFR-SEC-001 | NFR | Deny-by-default y mínimo privi | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-ALLOWED_INVOKE_PASSES_CAPABILITY_GATE... | VAL-NEG-CAPABILITIES | 28 | OK | covered | +| Req | Tipo | Nombre | Scope | Tests | Validators | Evidencia | CI | Estado | +| --------------- | ---- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | --------- | --- | -------- | +| FR-CAT-001 | FR | Cargar catálogo versionado. | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-ALLOWED_INVOKE_PASSES_CAPABILITY_GATE... | VAL-SCHEMA-REFS, VAL-NEG-CAPABILITIES | 34 | OK | covered | +| FR-DRAFT-001 | FR | Crear y guardar drafts locales | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-PARITY-PARITY_CREATE_DRAFT_DETERMINISTIC_BYTES, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001... | VAL-SCHEMA-REFS, VAL-NEG-CAPABILITIES | 41 | OK | covered | +| FR-ENT-001 | FR | Aplicar perfiles/policies por | deferred | -- | -- | 0 | OK | deferred | +| FR-EXPORT-001 | FR | Exportar artefacto y reporte. | mvp0.1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_BYTE_IDEMPOTENT, TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ADAPTERS_DIVERGE_BYTES_AND_FILL_CONTENT, TST-ARCHMAKER-CORE-CORE_CONTRACT-EXPORT_ARTIFACT_REJECTS_BAD_TARGET_AND_HANDLE... | -- | 4 | OK | covered | +| FR-IMPORT-001 | FR | Importar y migrar v5.1. | mvp1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-IMPORT_DRAFT_MIGRATES_V51_SAMPLE | VAL-MIG | 2 | OK | covered | +| FR-MANIFEST-001 | FR | Construir manifest canónico. | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-PARITY-PARITY_RESOLVE_AND_MANIFEST, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001... | VAL-CANONICALIZE, VAL-SCHEMA-REFS... | 33 | OK | covered | +| FR-PRESET-001 | FR | Aplicar presets como patches. | deferred | -- | -- | 0 | OK | deferred | +| FR-RESOLVE-001 | FR | Resolver capacidades/conflicto | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_RESOLVE_AND_MANIFEST, TST-ARCHMAKER-CORE-CAPABILITIES_NEGATIVE-NAMED_ALLOWED_INVOKE_RESOLVES_OP, TST-ARCHMAKER-CORE-CORE_CONTRACT-RESOLVE_DRAFT_VALID_AND_DETERMINISTIC... | -- | 6 | OK | covered | +| FR-RUN-001 | FR | Ejecutar plan aprobado. | deferred | -- | -- | 0 | OK | deferred | +| FR-VALIDATE-001 | FR | Ejecutar pipeline de validació | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_VALIDATE_DIAGNOSTICS, TST-ARCHMAKER-CORE-CORE_CONTRACT-VALIDATE_DRAFT_CLEAN_AND_BLOCKING, TST-ARCHMAKER-CORE-CORE_CONTRACT-END_TO_END_CREATE_SAVE_RESOLVE_VALIDATE_BUILD_EXPORT | -- | 3 | OK | covered | +| NFR-ACC-001 | NFR | WCAG 2.2 AA. | mvp0.1 | TST-WEB-A11Y-PLAYWRIGHT, TST-WEB-VISUAL-REGRESSION | -- | 2 | OK | covered | +| NFR-DET-001 | NFR | Mismo input/versiones → mismo | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-GOLDEN_CORPUS_BYTE_IDENTITY_AND_DIGEST, TST-ARCHMAKER-CANONICALIZATION-VECTORS-M_KEY_ORDER_BYTE_IDENTITY, TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY... | VAL-CANONICALIZE | 13 | OK | covered | +| NFR-MIG-001 | NFR | Migraciones explícitas y no de | mvp1 | TST-ARCHMAKER-CORE-CORE_CONTRACT-IMPORT_DRAFT_MIGRATES_V51_SAMPLE | VAL-MIG | 2 | OK | covered | +| NFR-OBS-001 | NFR | Errores/eventos estructurados | pending | -- | -- | 0 | OK | pending | +| NFR-OFF-001 | NFR | MVP usable sin CDN ni servidor | mvp0.1 | TST-WEB-VISUAL-REGRESSION | -- | 1 | OK | covered | +| NFR-PORT-001 | NFR | Dominio independiente de adapt | mvp0.1 | TST-ARCHMAKER-WASM-PARITY-PARITY_MAPS_VECTORS, TST-ARCHMAKER-WASM-PARITY-PARITY_DOMAIN_VECTORS, TST-ARCHMAKER-WASM-PARITY-PARITY_UNICODE_VECTORS... | -- | 6 | OK | covered | +| NFR-SEC-001 | NFR | Deny-by-default y mínimo privi | mvp0.1 | TST-ARCHMAKER-CANONICALIZATION-VECTORS-N_NEGATIVE_ZERO_BYTE_IDENTITY, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-DENIED_INVOKE_RETURNS_TYPED_PROTO_001, TST-ARCHMAKER-WASM-INVOKE_NEGATIVE-ALLOWED_INVOKE_PASSES_CAPABILITY_GATE... | VAL-NEG-CAPABILITIES | 28 | OK | covered | ## Requisitos Diferidos (fuera de slice MVP-0.1)