feat(swm): source_patch modela tarballs además de git (Fase 4)

Mutation::SourcePatch y RecipeInline ganan campos repo/commit XOR tarball/sha256
(Option, serde-default ⇒ compat con .swm git existentes), resueltos por
swm::swm_source_kind con la misma regla que recipe::Source::kind. swm_bridge
sintetiza la receta según el modo; hammer export reconstruye fuentes tarball
como source_patch en vez de caer a file_drop (provenance fina recuperada).
Prompt del traductor y roadmap actualizados. Tests nuevos para tarball + XOR.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-14 03:06:35 +00:00
co-authored by Claude Opus 4.8
parent 7c099afe4f
commit 0f42c38106
10 changed files with 280 additions and 83 deletions
+8 -2
View File
@@ -154,6 +154,8 @@ impl<T: IntentTranslator> Orchestrator<T> {
Mutation::SourcePatch { Mutation::SourcePatch {
repo, repo,
commit, commit,
tarball,
sha256,
patch, patch,
patch_url: _, patch_url: _,
build, build,
@@ -176,6 +178,8 @@ impl<T: IntentTranslator> Orchestrator<T> {
name: name.clone(), name: name.clone(),
repo: repo.clone(), repo: repo.clone(),
commit: commit.clone(), commit: commit.clone(),
tarball: tarball.clone(),
sha256: sha256.clone(),
patch: patch.clone(), patch: patch.clone(),
compiler: build.compiler.clone(), compiler: build.compiler.clone(),
target: build.target.clone(), target: build.target.clone(),
@@ -567,8 +571,10 @@ mod tests {
swm_version: 1, swm_version: 1,
base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() }, base: Base { distro_version: "2026-06-06".into(), pins: BTreeMap::new() },
mutations: vec![Mutation::SourcePatch { mutations: vec![Mutation::SourcePatch {
repo: "git://x".into(), repo: Some("git://x".into()),
commit: "abc".into(), commit: Some("abc".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
patch_url: None, patch_url: None,
build: hammer_core::swm::SwmBuild { build: hammer_core::swm::SwmBuild {
@@ -146,6 +146,8 @@ const SYSTEM_PROMPT: &str = r#"Eres un traductor que convierte intenciones human
3. source_patch — compila desde fuente y la inyecta. 3. source_patch — compila desde fuente y la inyecta.
{"type":"source_patch","repo":"git://...","commit":"<sha>","build":{"compiler":"zig-cc","target":"x86_64-linux-musl","link":"static","flags":[]},"target_bin":"/usr/bin/foo"} {"type":"source_patch","repo":"git://...","commit":"<sha>","build":{"compiler":"zig-cc","target":"x86_64-linux-musl","link":"static","flags":[]},"target_bin":"/usr/bin/foo"}
- Origen git (repo+commit) O tarball (tarball+sha256), exactamente uno; p. ej.
{"type":"source_patch","tarball":"https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz","sha256":"<hex>","build":{...},"target_bin":"/usr/bin/grep"}
- `target_bin` ruta absoluta del binario producido. - `target_bin` ruta absoluta del binario producido.
4. init_rule — registra un servicio. 4. init_rule — registra un servicio.
@@ -84,7 +84,10 @@ fn stub_serve(stream: UnixStream, rx_async: std::sync::mpsc::Receiver<Event>) {
let response = match cmd { let response = match cmd {
Command::Compile { recipe } => Event::BuildReady { Command::Compile { recipe } => Event::BuildReady {
recipe: recipe.name.clone(), recipe: recipe.name.clone(),
artifact: format!("b3:stub-{}", recipe.commit), artifact: format!(
"b3:stub-{}",
recipe.commit.as_deref().or(recipe.sha256.as_deref()).unwrap_or("nosrc")
),
}, },
Command::Query { what, .. } => Event::QueryResult { Command::Query { what, .. } => Event::QueryResult {
what, what,
@@ -133,8 +136,10 @@ fn client_handshake_compile_and_async_modified() {
let recipe = RecipeInline { let recipe = RecipeInline {
name: "grep".into(), name: "grep".into(),
repo: "git://example/grep.git".into(), repo: Some("git://example/grep.git".into()),
commit: "abc123".into(), commit: Some("abc123".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
compiler: "zig-cc".into(), compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(), target: "x86_64-linux-musl".into(),
+91 -40
View File
@@ -27,33 +27,54 @@ pub fn build_source_patch(
store: &Store, store: &Store,
scratch_root: Option<&Path>, scratch_root: Option<&Path>,
) -> hammer_core::Result<ArtifactHash> { ) -> hammer_core::Result<ArtifactHash> {
let (repo, commit, patch, patch_url, build_cfg, target_bin, expected) = match mutation { let (repo, commit, tarball, sha256, patch, patch_url, build_cfg, target_bin, expected) =
Mutation::SourcePatch { match mutation {
repo, Mutation::SourcePatch {
commit, repo,
patch, commit,
patch_url, tarball,
build, sha256,
target_bin, patch,
expected_hash, patch_url,
} => (repo, commit, patch, patch_url, build, target_bin, expected_hash), build,
_ => { target_bin,
return Err(hammer_core::Error::Recipe( expected_hash,
"build_source_patch: la mutación no es 'source_patch'".into(), } => (
)); repo, commit, tarball, sha256, patch, patch_url, build, target_bin, expected_hash,
} ),
}; _ => {
return Err(hammer_core::Error::Recipe(
"build_source_patch: la mutación no es 'source_patch'".into(),
));
}
};
// git xor tarball — misma regla que el schema (`swm_source_kind`).
let source_kind = hammer_core::swm::swm_source_kind(
repo.as_deref(),
commit.as_deref(),
tarball.as_deref(),
sha256.as_deref(),
)
.map_err(hammer_core::Error::Recipe)?;
let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf(); let scratch = scratch_root.unwrap_or(&cfg.work_root).to_path_buf();
let recipe_dir = scratch.join("swm-recipes"); let recipe_dir = scratch.join("swm-recipes");
std::fs::create_dir_all(&recipe_dir)?; std::fs::create_dir_all(&recipe_dir)?;
// Materializamos el patch a disco bajo un nombre derivado del commit (estable: si el .swm // Clave estable del origen para nombrar artefactos a disco: el commit (git) o el
// se reaplica, reusamos el mismo archivo y el hash de la receta no depende de // sha256 (tarball). Idéntica reaplicación ⇒ mismo nombre ⇒ hash de receta determinista.
let source_key: &str = match &source_kind {
hammer_core::SourceKind::Git { commit, .. } => commit,
hammer_core::SourceKind::Tarball { sha256, .. } => sha256,
};
// Materializamos el patch a disco bajo un nombre derivado de la clave de origen (estable:
// si el .swm se reaplica, reusamos el mismo archivo y el hash de la receta no depende de
// aleatoriedad). Origen: inline (`patch`) o remoto (`patch_url`); el schema garantiza que // aleatoriedad). Origen: inline (`patch`) o remoto (`patch_url`); el schema garantiza que
// no vengan ambos. // no vengan ambos.
let mut patches: Vec<String> = Vec::new(); let mut patches: Vec<String> = Vec::new();
let patch_path = recipe_dir.join(format!("{commit}.patch")); let patch_path = recipe_dir.join(format!("{source_key}.patch"));
if let Some(text) = patch { if let Some(text) = patch {
std::fs::write(&patch_path, text.as_bytes())?; std::fs::write(&patch_path, text.as_bytes())?;
patches.push(patch_path.to_string_lossy().into_owned()); patches.push(patch_path.to_string_lossy().into_owned());
@@ -64,14 +85,7 @@ pub fn build_source_patch(
} }
let name = derive_name(target_bin); let name = derive_name(target_bin);
let recipe = synthesize_recipe( let recipe = synthesize_recipe(&name, &source_kind, build_cfg, patches, &recipe_dir)?;
&name,
commit,
repo,
build_cfg,
patches,
&recipe_dir,
)?;
let hash = build(&recipe, cfg, store)?; let hash = build(&recipe, cfg, store)?;
if let Some(want) = expected { if let Some(want) = expected {
@@ -102,24 +116,33 @@ fn derive_name(target_bin: &str) -> String {
fn synthesize_recipe( fn synthesize_recipe(
name: &str, name: &str,
commit: &str, source: &hammer_core::SourceKind<'_>,
repo: &str,
build_cfg: &SwmBuild, build_cfg: &SwmBuild,
patches: Vec<String>, patches: Vec<String>,
base_dir: &Path, base_dir: &Path,
) -> hammer_core::Result<Recipe> { ) -> hammer_core::Result<Recipe> {
let compiler = parse_compiler(&build_cfg.compiler)?; let compiler = parse_compiler(&build_cfg.compiler)?;
let link = parse_link(&build_cfg.link)?; let link = parse_link(&build_cfg.link)?;
// Sección `[source]` y sufijo de versión según el modo de origen. El validador de
// `Source::kind` ya garantiza git xor tarball; acá sólo emitimos el TOML correspondiente.
let (version_key, source_block) = match source {
hammer_core::SourceKind::Git { repo, commit } => (
*commit,
format!("repo = \"{repo}\"\ncommit = \"{commit}\"\n"),
),
hammer_core::SourceKind::Tarball { url, sha256 } => (
*sha256,
format!("tarball = \"{url}\"\nsha256 = \"{sha256}\"\n"),
),
};
// Construimos la receta vía TOML para reusar las defaults y el validador de `Source`. // Construimos la receta vía TOML para reusar las defaults y el validador de `Source`.
let toml_text = format!( let toml_text = format!(
r#" r#"
name = "{name}" name = "{name}"
version = "swm-{commit_short}" version = "swm-{version_short}"
[source] [source]
repo = "{repo}" {source_block}
commit = "{commit}"
[build] [build]
compiler = "{compiler}" compiler = "{compiler}"
target = "{target}" target = "{target}"
@@ -127,9 +150,8 @@ link = "{link}"
flags = [] flags = []
"#, "#,
name = name, name = name,
commit_short = &commit[..commit.len().min(12)], version_short = &version_key[..version_key.len().min(12)],
repo = repo, source_block = source_block,
commit = commit,
compiler = compiler.as_str(), compiler = compiler.as_str(),
target = build_cfg.target, target = build_cfg.target,
link = link.as_str(), link = link.as_str(),
@@ -199,8 +221,10 @@ mod tests {
let d = tempfile::tempdir().unwrap(); let d = tempfile::tempdir().unwrap();
let r = synthesize_recipe( let r = synthesize_recipe(
"grep", "grep",
"a1b2c3d4e5f6a7b8c9", &hammer_core::SourceKind::Git {
"git://example/grep.git", repo: "git://example/grep.git",
commit: "a1b2c3d4e5f6a7b8c9",
},
&fake_swm_build(), &fake_swm_build(),
vec![], vec![],
d.path(), d.path(),
@@ -218,6 +242,31 @@ mod tests {
} }
} }
#[test]
fn synthesize_recipe_tarball() {
let d = tempfile::tempdir().unwrap();
let r = synthesize_recipe(
"grep",
&hammer_core::SourceKind::Tarball {
url: "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz",
sha256: "abcdef0123456789",
},
&fake_swm_build(),
vec![],
d.path(),
)
.unwrap();
assert_eq!(r.name, "grep");
assert!(r.version.starts_with("swm-abcdef012345"));
match r.source.kind().unwrap() {
hammer_core::SourceKind::Tarball { url, sha256 } => {
assert_eq!(url, "https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz");
assert_eq!(sha256, "abcdef0123456789");
}
_ => panic!("modo tarball esperado"),
}
}
#[test] #[test]
fn parse_compiler_and_link() { fn parse_compiler_and_link() {
assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc); assert_eq!(parse_compiler("zig-cc").unwrap(), Compiler::ZigCc);
@@ -249,8 +298,10 @@ mod tests {
std::fs::write(&patch_src, b"--- a\n+++ b\n").unwrap(); std::fs::write(&patch_src, b"--- a\n+++ b\n").unwrap();
let commit = "deadbeefcafe"; let commit = "deadbeefcafe";
let m = Mutation::SourcePatch { let m = Mutation::SourcePatch {
repo: "git://x".into(), repo: Some("git://x".into()),
commit: commit.into(), commit: Some(commit.into()),
tarball: None,
sha256: None,
patch: None, patch: None,
patch_url: Some(format!("file://{}", patch_src.display())), patch_url: Some(format!("file://{}", patch_src.display())),
build: fake_swm_build(), build: fake_swm_build(),
+9 -18
View File
@@ -1054,25 +1054,14 @@ fn build_export_mutations(
.map(|p| p.display().to_string()) .map(|p| p.display().to_string())
.unwrap_or_else(|| "/".into()); .unwrap_or_else(|| "/".into());
// Convertimos la Recipe a los campos que SourcePatch necesita. // Convertimos la Recipe a los campos de origen que SourcePatch necesita
let (repo, commit) = match recipe.source.kind() { // (git xor tarball — ambos se modelan ya, sin caer a file_drop).
let (repo, commit, tarball, sha256) = match recipe.source.kind() {
Ok(hammer_core::SourceKind::Git { repo, commit }) => { Ok(hammer_core::SourceKind::Git { repo, commit }) => {
(repo.to_string(), commit.to_string()) (Some(repo.to_string()), Some(commit.to_string()), None, None)
} }
Ok(hammer_core::SourceKind::Tarball { url, sha256 }) => { Ok(hammer_core::SourceKind::Tarball { url, sha256 }) => {
// SourcePatch sólo modela el modo git. Para tarballs caemos al (None, None, Some(url.to_string()), Some(sha256.to_string()))
// fallback file_drop por evento — no perdemos completeness, sólo
// provenance fina. El humano lo ve por stderr.
eprintln!(
"warning: artefacto {art_hash} proviene de tarball ({url} sha256={sha256}); \
`source_patch` aún no modela tarballs, hago file_drop"
);
for p in &paths {
if let Some(ev) = last_by_path.get(p) {
fallback.push((p.clone(), *ev));
}
}
continue;
} }
Err(_) => { Err(_) => {
stats.warnings += 1; stats.warnings += 1;
@@ -1105,6 +1094,8 @@ fn build_export_mutations(
mutations.push(hammer_core::Mutation::SourcePatch { mutations.push(hammer_core::Mutation::SourcePatch {
repo, repo,
commit, commit,
tarball,
sha256,
patch: patch_inline, patch: patch_inline,
patch_url: None, patch_url: None,
build: hammer_core::SwmBuild { build: hammer_core::SwmBuild {
@@ -1613,8 +1604,8 @@ flags = ["--enable-foo"]
expected_hash, expected_hash,
.. ..
} => { } => {
assert_eq!(repo, "git://example/grep.git"); assert_eq!(repo.as_deref(), Some("git://example/grep.git"));
assert_eq!(commit, "deadbeef"); assert_eq!(commit.as_deref(), Some("deadbeef"));
assert_eq!(build.compiler, "zig-cc"); assert_eq!(build.compiler, "zig-cc");
assert_eq!(build.link, "static"); assert_eq!(build.link, "static");
assert_eq!(build.flags, vec!["--enable-foo".to_string()]); assert_eq!(build.flags, vec!["--enable-foo".to_string()]);
+15 -4
View File
@@ -63,8 +63,17 @@ pub enum Command {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecipeInline { pub struct RecipeInline {
pub name: String, pub name: String,
pub repo: String, // Origen git (modo histórico) **o** tarball — exactamente uno, igual que
pub commit: String, // `swm::Mutation::SourcePatch`. `Option` con `serde(default)` para compat con clientes
// que sólo mandan `repo`+`commit`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tarball: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub patch: Option<String>, pub patch: Option<String>,
#[serde(default = "default_compiler")] #[serde(default = "default_compiler")]
@@ -208,8 +217,10 @@ mod tests {
let c = Command::Compile { let c = Command::Compile {
recipe: RecipeInline { recipe: RecipeInline {
name: "grep".into(), name: "grep".into(),
repo: "git://x/grep.git".into(), repo: Some("git://x/grep.git".into()),
commit: "abc".into(), commit: Some("abc".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
compiler: "zig-cc".into(), compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(), target: "x86_64-linux-musl".into(),
+133 -10
View File
@@ -70,8 +70,19 @@ pub struct PinDiff {
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum Mutation { pub enum Mutation {
SourcePatch { SourcePatch {
repo: String, // Origen git (modo histórico) **o** tarball — exactamente uno, igual que
commit: String, // [`crate::recipe::Source`]. Antes sólo se modelaba git y los tarballs caían a
// `file_drop`; ahora `hammer export` los reconstruye como source_patch. Los campos
// son `Option` (con `serde(default)`) para que los `.swm` git existentes —que sólo
// traen `repo`+`commit`— sigan parseando sin cambios.
#[serde(default, skip_serializing_if = "Option::is_none")]
repo: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
commit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tarball: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha256: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
patch: Option<String>, patch: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -200,18 +211,58 @@ impl Swm {
} }
} }
/// Resuelve el modo de origen de un `source_patch` (git **xor** tarball) desde sus campos
/// opcionales, con la misma regla que [`crate::recipe::Source::kind`]. Centraliza la
/// validación para que `verify_schema`, el lab y el export compartan una sola fuente de verdad.
pub fn swm_source_kind<'a>(
repo: Option<&'a str>,
commit: Option<&'a str>,
tarball: Option<&'a str>,
sha256: Option<&'a str>,
) -> Result<crate::recipe::SourceKind<'a>, String> {
use crate::recipe::SourceKind;
match (repo, commit, tarball, sha256) {
(Some(repo), Some(commit), None, None) => Ok(SourceKind::Git { repo, commit }),
(None, None, Some(url), Some(sha256)) => Ok(SourceKind::Tarball { url, sha256 }),
(Some(_), Some(_), Some(_), _) | (Some(_), Some(_), _, Some(_)) => {
Err("source_patch: usa repo+commit O tarball+sha256, no ambos".into())
}
_ => Err("source_patch: faltan campos; necesito (repo+commit) o (tarball+sha256)".into()),
}
}
impl Mutation { impl Mutation {
/// Sanity por mutación. Cada `type` tiene precondiciones distintas; la validación cruzada /// Sanity por mutación. Cada `type` tiene precondiciones distintas; la validación cruzada
/// (p. ej. "el `target_bin` apunta a un dir gestionado") la decide quien aplica, no el /// (p. ej. "el `target_bin` apunta a un dir gestionado") la decide quien aplica, no el
/// schema. /// schema.
pub fn verify_schema(&self) -> Result<(), String> { pub fn verify_schema(&self) -> Result<(), String> {
use crate::recipe::SourceKind;
match self { match self {
Mutation::SourcePatch { repo, commit, patch, patch_url, target_bin, .. } => { Mutation::SourcePatch {
if repo.is_empty() { repo, commit, tarball, sha256, patch, patch_url, target_bin, ..
return Err("source_patch: 'repo' vacío".into()); } => {
} match swm_source_kind(
if commit.is_empty() { repo.as_deref(),
return Err("source_patch: 'commit' vacío".into()); commit.as_deref(),
tarball.as_deref(),
sha256.as_deref(),
)? {
SourceKind::Git { repo, commit } => {
if repo.is_empty() {
return Err("source_patch: 'repo' vacío".into());
}
if commit.is_empty() {
return Err("source_patch: 'commit' vacío".into());
}
}
SourceKind::Tarball { url, sha256 } => {
if url.is_empty() {
return Err("source_patch: 'tarball' vacío".into());
}
if sha256.is_empty() {
return Err("source_patch: 'sha256' vacío".into());
}
}
} }
if patch.is_some() && patch_url.is_some() { if patch.is_some() && patch_url.is_some() {
return Err( return Err(
@@ -422,8 +473,10 @@ mutations:
swm_version: 1, swm_version: 1,
base: base_v("x", &[]), base: base_v("x", &[]),
mutations: vec![Mutation::SourcePatch { mutations: vec![Mutation::SourcePatch {
repo: "git://x".into(), repo: Some("git://x".into()),
commit: "abc".into(), commit: Some("abc".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
patch_url: None, patch_url: None,
build: SwmBuild { build: SwmBuild {
@@ -439,4 +492,74 @@ mutations:
}; };
swm.verify_schema().unwrap(); swm.verify_schema().unwrap();
} }
#[test]
fn verify_schema_source_patch_tarball() {
let swm = Swm {
swm_version: 1,
base: base_v("x", &[]),
mutations: vec![Mutation::SourcePatch {
repo: None,
commit: None,
tarball: Some("https://ftp.gnu.org/gnu/grep/grep-3.11.tar.gz".into()),
sha256: Some("deadbeef".into()),
patch: None,
patch_url: None,
build: SwmBuild {
compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(),
link: "static".into(),
flags: vec![],
},
target_bin: "/bin/grep".into(),
expected_hash: None,
}],
signature: None,
};
swm.verify_schema().unwrap();
}
#[test]
fn verify_schema_source_patch_rechaza_git_y_tarball_mezclados() {
let m = Mutation::SourcePatch {
repo: Some("git://x".into()),
commit: Some("abc".into()),
tarball: Some("https://x/a.tar.gz".into()),
sha256: Some("deadbeef".into()),
patch: None,
patch_url: None,
build: SwmBuild {
compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(),
link: "static".into(),
flags: vec![],
},
target_bin: "/bin/x".into(),
expected_hash: None,
};
let err = m.verify_schema().unwrap_err();
assert!(err.contains("no ambos"), "mensaje inesperado: {err}");
}
#[test]
fn verify_schema_source_patch_rechaza_sin_origen() {
let m = Mutation::SourcePatch {
repo: None,
commit: None,
tarball: None,
sha256: None,
patch: None,
patch_url: None,
build: SwmBuild {
compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(),
link: "static".into(),
flags: vec![],
},
target_bin: "/bin/x".into(),
expected_hash: None,
};
let err = m.verify_schema().unwrap_err();
assert!(err.contains("faltan campos"), "mensaje inesperado: {err}");
}
} }
+6 -2
View File
@@ -362,6 +362,8 @@ fn run_compile(recipe: RecipeInline, store_root: PathBuf, tx: Sender<Event>) {
let mutation = hammer_core::swm::Mutation::SourcePatch { let mutation = hammer_core::swm::Mutation::SourcePatch {
repo: recipe.repo, repo: recipe.repo,
commit: recipe.commit, commit: recipe.commit,
tarball: recipe.tarball,
sha256: recipe.sha256,
patch: recipe.patch, patch: recipe.patch,
patch_url: None, patch_url: None,
build: hammer_core::swm::SwmBuild { build: hammer_core::swm::SwmBuild {
@@ -493,8 +495,10 @@ caps = ["query", "compile", "inject", "inject-real", "init"]
t_of(&Command::Compile { t_of(&Command::Compile {
recipe: RecipeInline { recipe: RecipeInline {
name: "x".into(), name: "x".into(),
repo: "g".into(), repo: Some("g".into()),
commit: "c".into(), commit: Some("c".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
compiler: "zig-cc".into(), compiler: "zig-cc".into(),
target: "t".into(), target: "t".into(),
+4 -2
View File
@@ -138,8 +138,10 @@ fn missing_cap_returns_error_no_cap() {
&Command::Compile { &Command::Compile {
recipe: RecipeInline { recipe: RecipeInline {
name: "x".into(), name: "x".into(),
repo: "git://x".into(), repo: Some("git://x".into()),
commit: "abc".into(), commit: Some("abc".into()),
tarball: None,
sha256: None,
patch: None, patch: None,
compiler: "zig-cc".into(), compiler: "zig-cc".into(),
target: "x86_64-linux-musl".into(), target: "x86_64-linux-musl".into(),
+4 -2
View File
@@ -94,8 +94,10 @@ pre-requisito de validación.
- [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que - [x] Provenance en `export`: mapa artefacto→receta vía sidecar `.hammer/recipe.toml` que
`hammer-build::build` escribe dentro del artefacto antes de sellar. `hammer export` `hammer-build::build` escribe dentro del artefacto antes de sellar. `hammer export`
agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta agrupa eventos por `artifact_hash` y emite UN `source_patch` por grupo cuya receta
sea recuperable; lo demás cae al fallback `file_drop`. Source `git` modelado; sea recuperable; lo demás cae al fallback `file_drop`. Source `git` **y `tarball`**
`tarball` cae a file_drop con warning (pendiente extender SourcePatch). modelados: `Mutation::SourcePatch` (y `RecipeInline`) llevan campos `repo`/`commit`
**xor** `tarball`/`sha256` (resueltos por `swm::swm_source_kind`, misma regla que
`recipe::Source::kind`); `hammer export` reconstruye ambos sin caer a `file_drop`.
- [x] Firma `signature` (ed25519) y `TrustStore` local. `hammer_core::sign`: `KeyPair` - [x] Firma `signature` (ed25519) y `TrustStore` local. `hammer_core::sign`: `KeyPair`
(genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`), (genera/carga/escribe claves), `TrustStore::load(dir)` (lee `*.ed25519.pub`),
`Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/ `Swm::verify_signature(&trust) → SigStatus` (`trusted`/`unknown-key`/`bad-sig`/