Files
hammer/crates/hammer-build
Sergio de036d3666 Fase 4 — provenance en hammer export (sidecar de receta en el store)
Hasta ahora `hammer export` emitía sólo `file_drop` con `content_b64`.
Eso reproducía byte-a-byte pero perdía la receta: el receptor obtenía
binarios opacos, sin manera de auditarlos ni de recompilarlos desde
fuente. Esta fase cierra el hueco para los artefactos producidos por
`hammer-build::build`.

Mecanismo:
- `Store` reserva `.hammer/recipe.toml` (`RECIPE_SIDECAR_REL`) dentro
  de cada artefacto. `hammer-build::build` lo escribe antes de sellar,
  así queda inmutable junto con el árbol.
- `Store::recipe_for_dir` / `recipe_for_hash` lo leen de vuelta. Si el
  artefacto es viejo y no trae sidecar, devuelven `None` sin fallar.
- `Recipe::to_toml` (nuevo) hace el roundtrip.

Lógica del export (función pura `build_export_mutations`):
1. Aplica semánticas de Delete: invalida estados previos del mismo path.
2. Particiona los eventos sobrevivientes en (a) los que tienen
   `artifact_hash` con receta sidecar y (b) el resto.
3. Por cada grupo de (a) emite UN `source_patch` con repo+commit+build
   de la receta y `expected_hash = artifact_hash`. El `target_bin` es
   el primer path alfabético del grupo; el receptor hidrata el árbol
   completo al aplicar.
4. Patches de la receta se concatenan inline en el `source_patch.patch`.
5. Los eventos del bucket (b) caen al fallback `file_drop` con
   `content_b64 + content_hash` (orden alfabético).

Limitaciones explícitas:
- `SourcePatch` sólo modela `Source::Git`. Recetas con `tarball` se
  reportan por stderr y caen a file_drop. Extender el SWM para
  tarballs es trabajo aparte.
- Si la receta tiene patches pero alguno no se puede leer, no se
  inlina; `expected_hash` sigue siendo el gate de integridad para
  detectar la divergencia.

CLI: el subcomando `Export` ahora recibe `--store` para resolver
artefactos. Defaults igual que antes (`/store`).

Tests (12 nuevos):
- hammer-core (5): `Recipe::to_toml` roundtrip; `Store::recipe_for_dir`
  sin/con sidecar; `Store::recipe_for_hash` por prefijo + hash inexistente.
- hammer-cli (7): export sin store; semánticas de Delete; hydrate con
  sidecar emite source_patch agrupando dos archivos; hydrate sin sidecar
  cae a file_drop contando `missing_recipe`; mix trazable + external;
  path ilegible sólo warning.
2026-06-10 16:49:09 +00:00
..