1# SDD 24 — El árbol de fuentes: caché sellada + workspace efímero (v2)
2
3> **Resuelve ADR 0012.** Estado propuesto: ACEPTADO al cerrar F2.5.
4> **v2, 2026-08-28:** incorpora la verificación contra el código (§8): R1 e IF-1/IF-2 corregidos
5> por medición, segunda carrera nombrada, Q1 cerrada por precedente, tensión NG-4/Q3 resuelta.
6
7---
8
9## 0. El problema, medido — son DOS carreras
10
11`work/sources/…` cumple dos papeles que se contradicen bajo concurrencia:
12
131. **Caché direccionada por contenido**: dos builds de la misma dep resuelven al MISMO directorio.
14 (El nombre es `<nombre>-<sha16>` para tarballs y `<nombre>-<commit-completo>` para git —
15 `fetch.rs:234` / `fetch.rs:73`.)
162. **Workspace mutable de build**: patches, `cargo vendor`, `go mod vendor` (minutos, decenas de
17 GB), y los residuos que las recetas dejan EN EL ÁRBOL — `.zwrap/cc` lo escriben **131 recetas**
18 *(medido)*, casi todas de la cola KDE.
19
20Las dos carreras concretas:
21
22- **Carrera A — fetch vs build**: un `remove_dir_all` de re-materialización barre un árbol que un
23 `bwrap` está compilando. Medida a escala: al invalidar `libdrm`, ~93/205 recetas KDE murieron con
24 `/src/.zwrap/cc is not a full path…`.
25- **Carrera B — build vs build**: dos recetas DISTINTAS que comparten una dep transitiva no sellada
26 la construyen en el MISMO `sources/<dep>/output` ⇒ `Some other Meson process is already using
27 this build directory`. Documentada en `build-farm.sh:82-99`; hoy se concilia con un **reintento
28 serial** (Fase 1b) que existe sólo por esto. Un lock por árbol (opción A del ADR) NO la resuelve
29 sin serializar.
30
31Mitigación vigente: `flock` global + `JOBS=1` ⇒ la granja entera corre serializada.
32
33**La tesis:** los dos papeles se **separan por capa**, con el corte puesto para que lo caro
34(vendor) caiga del lado inmutable. Es el mismo mecanismo que el sandbox ya usa para las build-deps
35(`--overlay-src` RO apiladas), extendido a `/src`. Mata las dos carreras: A porque lo sellado es
36inmutable y la re-materialización es imposible (mismo `fuente_id` ⇒ ya existe); B porque cada
37constructor tiene su propio `output/` en su upper privado.
38
39---
40
41## 1. Diseño
42
43### 1.1 Las dos capas
44
45```
46MATERIALIZACIÓN (host, una vez por fuente_id, lock corto por-hash)
47 fetch (red) → patch → vendor (cargo/go) ← YA ocurre todo en host hoy (lib.rs:224,248,258);
48 → sellar árbol en work/sources-cas/<fuente_id>-<nombre>/ este SDD lo nombra como capa
49 fuente_id = blake3( source_id ⧺ Σ blake3(patch_i) ⧺ marca_vendor )
50 source_id cubre ambos modos: "git:<commit>" XOR "tarball:<sha256>"
51
52BUILD (sandbox, N en paralelo, cero locks)
53 bwrap monta /src = overlay( lower: fuente sellada, upper+work: work/ws/<uuid>/ )
54 configure → compile → install → seal artefacto → destruir ws/<uuid>
55```
56
57- **El vendoreo se paga una vez** y queda del lado inmutable: `marca_vendor` deriva de los
58 lockfiles que ya viven en el `source_id` ⇒ la fuente sellada es determinista dado el pin.
59- **`.zwrap` y todo residuo viven en el upper privado.** Las 131 recetas quedan estructuralmente
60 a salvo.
61- **La fase `patch` NO migra: ya corre en host** desde el día 1. Este SDD nombra como contrato lo
62 que hoy es de facto. La tabla de fases del **SDD 02 §4 está desactualizada** (dice "patch en
63 sandbox") y corregirla es entregable de F1.
64
65### 1.2 Gotchas de bwrap verificados (citar al implementar)
66
67- `--overlay` exige ≥1 `--overlay-src` previo, y el acumulador **se consume por operación**. Como
68 `sandbox.rs` ya emite `--overlay-src <rootfs> [deps…] --tmp-overlay /`, el grupo de `/src` va
69 **después** y no debe filtrar srcs al overlay del root. Orden de argumentos, no diseño.
70 **Verificado end-to-end** (§8.2): la composición real de los dos grupos funciona y no filtra.
71- El límite de una página del `lowerdir` (`sandbox.rs:347-357`, el pre-merge `cp -al`) **no
72 aplica**: `/src` es un mount aparte con su propia lowerdir.
73- `--overlay RWSRC WORKDIR DEST`: exige `upper` **y** `work` vacíos en el mismo filesystem.
74
75### 1.3 Por qué no las otras opciones del ADR
76
77- **(A)** lock por árbol durante todo el build: serializa el diamante qtbase y **no resuelve la
78 carrera B**.
79- **(B)** árbol privado por constructor: paga el vendor completo en cada build.
80- **(C)** caché inmutable + copia: paga la copia por build. Este diseño es (C) con overlay en vez
81 de copia y el vendor movido al lado sellado.
82
83---
84
85## 2. Reglas
86
87- **R1** — La fuente sellada es inmutable y direccionada: `fuente_id = blake3(source_id ⧺
88 Σ blake3(patch) ⧺ marca_vendor)`. Vive en `work/sources-cas/`. **La inmutabilidad la da el
89 mount, no los bits de modo**: a través del overlay es estructuralmente imposible escribir el
90 lower. Los modos del árbol se dejan **tal como los trae upstream** — sellarlos a `a-w` los
91 hereda el copy-up y **rompe el build** (medido: el sandbox corre uid 1001 sin
92 `CAP_DAC_OVERRIDE`; EACCES al reescribir un fichero existente y al crear dentro de un subdir;
93 el fallo es parcial-por-profundidad porque el modo del upper root gana en la raíz). Blindaje
94 contra procesos del host: proteger el **directorio contenedor** `sources-cas/`, nunca el árbol.
95 La detección de corrupción es `blake3_arbol` (R10).
96- **R2** — Sellado atómico: materializar en `tmp/`, escribir marker, `rename`. Jamás visible un
97 árbol parcial bajo su nombre final.
98- **R3** — Marker `.sellada` = `{fuente_id, blake3_arbol, ts}`. Directorio sin marker = no sellado
99 (lección del vacío-como-cache-hit: *un ausente falla ruidosamente; un vacío llega hasta el final
100 diciendo que todo fue bien* — CLAUDE.md regla 3).
101- **R4** — Nada escribe en una fuente sellada. El build la recibe sólo como lower del overlay.
102- **R5** — Workspace efímero por constructor: `work/ws/<uuid>/{upper,work}` (los dos directorios
103 que `--overlay` exige, mismo filesystem), destruido tras el seal. `--keep-ws` lo conserva.
104- **R6** — Lock **sólo** durante la materialización, por `fuente_id`
105 (`flock work/locks/<fuente_id>`). El segundo llegador espera y encuentra sellado. Los builds no
106 toman locks.
107- **R7** — Fallo o kill durante materialización ⇒ sólo queda `tmp/`; la corrida siguiente lo barre
108 y re-materializa. Nunca se sella parcial.
109- **R8** — La adopción **no mueve ningún `artifact_hash`**: `fuente_id` no entra en `hash_inputs`.
110 Verificación pre-merge: `hammer hash --check` sobre el corpus, cero movidos.
111- **R9** — GC de `sources-cas` por alcanzabilidad desde recetas activas; lo no alcanzable es
112 podable (regenerable desde el mirror ADR 0013). Medir por contenido, nunca con `du`.
113- **R10** — Re-materializar un `fuente_id` existente debe reproducir el `blake3_arbol` del marker
114 o fallar ruidoso (detecta vendor no determinista o mirror envenenado).
115- **R11** — El `flock` global (CLAUDE.md regla 1) y el `JOBS=1` se retiran **sólo** tras F2.5
116 verde, en el mismo commit.
117
118## 3. Invariantes (mecánicamente verificables)
119
120- **IF-1** — Tras N builds concurrentes sobre la misma fuente, el `blake3_arbol` del árbol sellado
121 casa con el del marker. Verificable **sin fanotify** (que exige `CAP_SYS_ADMIN` y complica la
122 granja): hashear antes/después es más barato y más fuerte.
123- **IF-2** — Canario deliberado (estilo D9 de harkaq): un build de test **escribe** en `/src`
124 (debe FUNCIONAR, vía copy-up al upper) y el árbol sellado **sigue idéntico** por `blake3_arbol`.
125 Si el hash se movió, alguien montó el lower como bind RW. No se cree: se gana.
126- **IF-3** — `hammer hash --check` corpus completo: hash vigente idéntico pre/post migración.
127- **IF-4** — `kill -9` en medio de la materialización ⇒ no existe marker; la corrida siguiente
128 sella limpio y `blake3_arbol` casa con una materialización sin interrupción.
129- **IF-5** — Artefacto sobre overlay ≡ bit-a-bit al del modelo viejo, **una receta por clase y
130 por build-system**: autotools-en-raíz (zlib), **meson y cmake (escriben en subdirs — la clase
131 que un spike sólo-zlib dejaría sin cubrir)**, cargo, go.
132- **IF-6** — Tras F3, `work/sources/` no existe, la **Fase 1b de `build-farm.sh` está borrada**, y
133 ningún script referencia el esquema viejo — verificado reconstruyendo, no con `grep` (en este
134 repo `grep` sobre ficheros con comentarios densos no es una medición).
135
136## 4. Fases
137
138- **F0 — gate residual**: el mecanismo overlay ya está verde (verificado 2026-08-28 con bwrap
139 0.11.2: lower legible, escritura al upper, lower intacto, y la composición con el
140 `--tmp-overlay /` del root — §8). Queda **sólo IF-5**, y con el corpus de clases completo — el
141 caso que muerde es meson/cmake en subdirs, no zlib.
142- **F1 — materialización**: `fetch.rs` produce fuentes selladas (R1–R3, R6–R7, R10). El build
143 sigue en el modelo viejo (puente). **Incluye corregir la tabla de fases del SDD 02 §4.**
144- **F2 — build sobre overlay**: `/src` = lower sellado + `ws/<uuid>/{upper,work}` (R4–R5),
145 respetando §1.2. Detrás de `SOURCES_CAS=1`, **inerte sin él**. IF-3 + IF-5.
146- **F2.5 — vertical slice**: worker real con `JOBS=4` drena una onda KDE que atraviesa el diamante
147 qtbase. Todo `✓ REPRODUCIBLE`, IF-1 e IF-2 corridos. **Gate del ADR:** 0012 pasa a ACEPTADO aquí
148 o el diseño vuelve.
149- **F3 — migración**: borrar `work/sources/`; **borrar la Fase 1b de `build-farm.sh`**
150 (entregable medible: la carrera B ya no existe); **reescribir — no re-apuntar — el watchdog de
151 disco de `farm-worker-loop.sh:43`** (queda más simple: lo sellado es inmutable, cada `ws/<uuid>`
152 tiene un solo dueño); actualizar `reconstruir-corpus.sh:206`; retirar flock global + `JOBS=1`
153 (R11); GC (R9) al latido.
154- **F4 — cosecha**: medir el speedup real de la campaña de reconstrucción con JOBS=N. El número
155 cierra el informe del ADR.
156
157## 5. No-objetivos
158
159- **NG-1** — No cambia el sandbox de build ni sus fases internas.
160- **NG-2** — No toca `hash_inputs` ni re-hashea nada (R8 lo verifica).
161- **NG-3** — No resuelve la red del vendor de Go en host (→ D1).
162- **NG-4** — `sources-cas` NO es procedencia ni parte del store: es caché **podable**. Q3 cita
163 `fuente_id` en el log de transparencia, y eso es compatible **porque la cita es a un hash cuya
164 materialización es regenerable desde el mirror (ADR 0013)**: podar no rompe la cita, la vuelve
165 un puntero frío. Lo que NUNCA se cita es una ruta de `sources-cas`.
166- **NG-5** — No introduce dependencia de reflink ni de overlayfs del host: el mecanismo es el
167 `--overlay` de bwrap ya en uso para deps.
168
169## 6. Deudas nombradas
170
171- **D1** — `go mod vendor` sigue en host con red. Acotado a materialización; R10 lo hace
172 detectable. Cierre futuro: materialización en sandbox con red controlada.
173- **D2** — Confirmar en F2.5 que los kernels propios de granja (SDD 22) montan el `--overlay` de
174 bwrap con lower en `sources-cas`. Fallback: copia completa — correcta, lenta, **ruidosa en el
175 log**, nunca silenciosa.
176- **D3** — `--keep-ws` acumula workspaces; podarlos en el GC de R9.
177- **D4** — El snapshot golden debe hornear `sources-cas` o los primeros builds pagan
178 materialización fría. **Choca con el test de `hammer-bootstrap` (`lib.rs:2659`) que afirma que
179 `work/sources/` no viaja en el builder rootfs**: la decisión (¿el builder hereda `sources-cas`?)
180 se toma en F2.5 con el dato del slice, y si es sí, ese test se actualiza a propósito — no se
181 deja fallar por sorpresa.
182
183## 7. Preguntas — estado
184
185- **Q1 — CERRADA por precedente**: fetch+patch+vendor corren en host desde el día 1
186 (`lib.rs:224,248,258`). El riesgo ya se corre hoy; este SDD sólo lo pone bajo contrato (R10 lo
187 hace además detectable). No es una apuesta nueva.
188- **Q2 — abierta**: retención de `sources-cas` con vendors grandes: ¿tope GB con evicción LRU o
189 sólo alcanzabilidad? Decide el operador con el dato de F4.
190- **Q3 — DECIDIDA: sí** — `fuente_id` entra en `bootstrap.json` como campo opcional. Ver NG-4
191 para por qué no contradice la podabilidad.
192
193---
194
195*Cruces: ADR 0012 (resuelve) · ADR 0013 (regenerabilidad que sostiene NG-4/Q3) · SDD 02 §4
196(overlay-src de deps: el mecanismo reutilizado; tabla de fases a corregir en F1) · SDD 16 (patrón
197"inerte sin flag"; canario D9 → IF-2) · CLAUDE.md regla 3 (marker contra vacíos) · CLAUDE.md
198regla 1 (retiro del flock en R11) · `build-farm.sh` Fase 1b (carrera B; se borra en F3) ·
199`docs/BRIEFING-hammer.md` §14.4 (los modos de fallo estructurales que R3 e IF-6 citan).*
200
201---
202
203# 8. Registro de evidencia — mediciones contra el código (2026-08-28)
204
205> Las correcciones que salieron de aquí **ya están incorporadas en el cuerpo de la v2**. Esta
206> sección queda como registro reproducible: qué se midió, con qué comando y con qué salida, para
207> que nadie tenga que volver a descubrirlo. Método del SDD 22: un diseño entrante se contesta con
208> evidencia, y si un dato de hammer lo contradice, manda hammer.
209
210## 8.1 El `chmod a-w` recursivo rompe el build (origen de R1)
211
212El sandbox corre como **uid 1001 sin `CAP_DAC_OVERRIDE`** (`id` dentro de bwrap lo confirma) y
213**overlayfs preserva el modo del lower al hacer copy-up**. Con el árbol sellado a `a-w`
214(dirs 0555, ficheros 0444), sobre patrones reales de fase:
215
216| patrón de build | resultado |
217|---|---|
218| `echo >> /mnt/configure` (reescribir fichero existente) | **FALLA** — `Permission denied` |
219| `echo y > /mnt/sub/nuevo.o` (crear dentro de un subdir del lower) | **FALLA** — `Permission denied` |
220| `sed -i` en la **raíz** del overlay | OK |
221| `mkdir /mnt/output` (patrón meson) en la **raíz** | OK |
222
223Las dos últimas pasan porque el upper root ya existe con modo 0755 y su modo gana sobre el del
224lower root ⇒ **el fallo es parcial y por profundidad**: funciona arriba y falla adentro. Un spike
225sobre `zlib` (autotools que configura en la raíz) habría pasado en verde y el diseño habría
226reventado después en meson/cmake/cargo. Es la forma del fallo *sellado ≠ arranca*. De ahí R1
227(inmutabilidad por mount, modos upstream) e IF-5 (una receta por build-system, no sólo zlib).
228
229## 8.2 La composición real con el sandbox actual funciona (origen de §1.2 y del gate de F0)
230
231Reproduciendo el orden de argumentos que `sandbox.rs` ya emite, más el grupo de `/src`:
232
233```sh
234bwrap --unshare-all \
235 --overlay-src .dev-fs/alpine --tmp-overlay / \
236 --overlay-src $S/lower --overlay $S/upper $S/work /src \
237 --proc /proc --dev /dev -- /bin/sh -c '…'
238```
239```
240root = rootfs alpine: bin dev etc home lib media mnt musl256 opt proc root run sbi
241/src = fuente sellada: SOY-LA-FUENTE-SELLADA
242escritura en SUBDIR de /src: OK ← /src/sub/output/build.ninja (el caso meson)
243raiz sigue escribible (tmp-overlay): OK
244lower intacto: marca.txt, sub/f.c ← el árbol sellado no se tocó
245upper del /src: sub/output/build.ninja ← el residuo quedó aislado
246```
247
248Cuatro hechos de una sola corrida: (1) el acumulador de `--overlay-src` **se consume por operación
249y no filtra** — el root sigue siendo el rootfs Alpine y `/src` la fuente sellada; (2) el
250`--tmp-overlay /` del root sigue funcionando junto al `/src`; (3) con modos upstream, **escribir en
251un subdirectorio de `/src` funciona** — exactamente el caso que §8.1 rompía; (4) el lower queda
252byte-intacto y el residuo aislado en el upper.
253
254⇒ El gate de *mecanismo* de F0 está verde. Lo que queda de F0 es IF-5 (bit-a-bit real).
255
256## 8.3 Referencias de código verificadas
257
258| Afirmación del SDD | Dónde | Estado |
259|---|---|---|
260| `<nombre>-<sha16>` (tarball) / `<nombre>-<commit>` (git) | `fetch.rs:234` / `fetch.rs:73` | ✅ |
261| fetch+patch+vendor corren en host, antes de bwrap | `lib.rs:224,248,258` | ✅ (cierra Q1) |
262| Límite de página del `lowerdir` y pre-merge `cp -al` | `sandbox.rs:347-357` | ✅ |
263| Carrera B y su reintento serial (Fase 1b) | `build-farm.sh:82-99` | ✅ |
264| Watchdog acoplado al esquema de rutas | `farm-worker-loop.sh:43` | ✅ |
265| Purga del árbol viejo | `reconstruir-corpus.sh:206` | ✅ |
266| `work/sources/` no viaja en el builder rootfs (test) | `hammer-bootstrap/src/lib.rs:2659` | ✅ (origen de D4) |
267| `.zwrap` escrito en el árbol de fuentes | 131 recetas *(medido)* | ✅ |
268| `--overlay RWSRC WORKDIR DEST` disponible | bubblewrap 0.11.2 | ✅ |
269| La tabla de fases dice "patch en sandbox" | `docs/02-build-lab.md` §4 | ⚠️ desactualizada — F1 |
24-arbol-de-fuentes.md
SDD 24 — El árbol de fuentes: caché sellada + workspace efímero (v2)
Resuelve ADR 0012. Estado propuesto: ACEPTADO al cerrar F2.5.
v2, 2026-08-28: incorpora la verificación contra el código (§8): R1 e IF-1/IF-2 corregidos
por medición, segunda carrera nombrada, Q1 cerrada por precedente, tensión NG-4/Q3 resuelta.
0. El problema, medido — son DOS carreras
work/sources/… cumple dos papeles que se contradicen bajo concurrencia:
- Caché direccionada por contenido: dos builds de la misma dep resuelven al MISMO directorio.
(El nombre es
<nombre>-<sha16> para tarballs y <nombre>-<commit-completo> para git —
fetch.rs:234 / fetch.rs:73.)
- Workspace mutable de build: patches,
cargo vendor, go mod vendor (minutos, decenas de
GB), y los residuos que las recetas dejan EN EL ÁRBOL — .zwrap/cc lo escriben 131 recetas
(medido), casi todas de la cola KDE.
Las dos carreras concretas:
- Carrera A — fetch vs build: un
remove_dir_all de re-materialización barre un árbol que un
bwrap está compilando. Medida a escala: al invalidar libdrm, ~93/205 recetas KDE murieron con
/src/.zwrap/cc is not a full path….
- Carrera B — build vs build: dos recetas DISTINTAS que comparten una dep transitiva no sellada
la construyen en el MISMO
sources/<dep>/output ⇒ Some other Meson process is already using this build directory. Documentada en build-farm.sh:82-99; hoy se concilia con un reintento
serial (Fase 1b) que existe sólo por esto. Un lock por árbol (opción A del ADR) NO la resuelve
sin serializar.
Mitigación vigente: flock global + JOBS=1 ⇒ la granja entera corre serializada.
La tesis: los dos papeles se separan por capa, con el corte puesto para que lo caro
(vendor) caiga del lado inmutable. Es el mismo mecanismo que el sandbox ya usa para las build-deps
(--overlay-src RO apiladas), extendido a /src. Mata las dos carreras: A porque lo sellado es
inmutable y la re-materialización es imposible (mismo fuente_id ⇒ ya existe); B porque cada
constructor tiene su propio output/ en su upper privado.
1. Diseño
1.1 Las dos capas
MATERIALIZACIÓN (host, una vez por fuente_id, lock corto por-hash)
fetch (red) → patch → vendor (cargo/go) ← YA ocurre todo en host hoy (lib.rs:224,248,258);
→ sellar árbol en work/sources-cas/<fuente_id>-<nombre>/ este SDD lo nombra como capa
fuente_id = blake3( source_id ⧺ Σ blake3(patch_i) ⧺ marca_vendor )
source_id cubre ambos modos: "git:<commit>" XOR "tarball:<sha256>"
BUILD (sandbox, N en paralelo, cero locks)
bwrap monta /src = overlay( lower: fuente sellada, upper+work: work/ws/<uuid>/ )
configure → compile → install → seal artefacto → destruir ws/<uuid>
- El vendoreo se paga una vez y queda del lado inmutable:
marca_vendor deriva de los
lockfiles que ya viven en el source_id ⇒ la fuente sellada es determinista dado el pin.
.zwrap y todo residuo viven en el upper privado. Las 131 recetas quedan estructuralmente
a salvo.
- La fase
patch NO migra: ya corre en host desde el día 1. Este SDD nombra como contrato lo
que hoy es de facto. La tabla de fases del SDD 02 §4 está desactualizada (dice "patch en
sandbox") y corregirla es entregable de F1.
1.2 Gotchas de bwrap verificados (citar al implementar)
--overlay exige ≥1 --overlay-src previo, y el acumulador se consume por operación. Como
sandbox.rs ya emite --overlay-src <rootfs> [deps…] --tmp-overlay /, el grupo de /src va
después y no debe filtrar srcs al overlay del root. Orden de argumentos, no diseño.
Verificado end-to-end (§8.2): la composición real de los dos grupos funciona y no filtra.
- El límite de una página del
lowerdir (sandbox.rs:347-357, el pre-merge cp -al) no
aplica: /src es un mount aparte con su propia lowerdir.
--overlay RWSRC WORKDIR DEST: exige upper y work vacíos en el mismo filesystem.
1.3 Por qué no las otras opciones del ADR
- (A) lock por árbol durante todo el build: serializa el diamante qtbase y no resuelve la
carrera B.
- (B) árbol privado por constructor: paga el vendor completo en cada build.
- (C) caché inmutable + copia: paga la copia por build. Este diseño es (C) con overlay en vez
de copia y el vendor movido al lado sellado.
2. Reglas
- R1 — La fuente sellada es inmutable y direccionada:
fuente_id = blake3(source_id ⧺ Σ blake3(patch) ⧺ marca_vendor). Vive en work/sources-cas/. La inmutabilidad la da el
mount, no los bits de modo: a través del overlay es estructuralmente imposible escribir el
lower. Los modos del árbol se dejan tal como los trae upstream — sellarlos a a-w los
hereda el copy-up y rompe el build (medido: el sandbox corre uid 1001 sin
CAP_DAC_OVERRIDE; EACCES al reescribir un fichero existente y al crear dentro de un subdir;
el fallo es parcial-por-profundidad porque el modo del upper root gana en la raíz). Blindaje
contra procesos del host: proteger el directorio contenedor sources-cas/, nunca el árbol.
La detección de corrupción es blake3_arbol (R10).
- R2 — Sellado atómico: materializar en
tmp/, escribir marker, rename. Jamás visible un
árbol parcial bajo su nombre final.
- R3 — Marker
.sellada = {fuente_id, blake3_arbol, ts}. Directorio sin marker = no sellado
(lección del vacío-como-cache-hit: un ausente falla ruidosamente; un vacío llega hasta el final
diciendo que todo fue bien — CLAUDE.md regla 3).
- R4 — Nada escribe en una fuente sellada. El build la recibe sólo como lower del overlay.
- R5 — Workspace efímero por constructor:
work/ws/<uuid>/{upper,work} (los dos directorios
que --overlay exige, mismo filesystem), destruido tras el seal. --keep-ws lo conserva.
- R6 — Lock sólo durante la materialización, por
fuente_id
(flock work/locks/<fuente_id>). El segundo llegador espera y encuentra sellado. Los builds no
toman locks.
- R7 — Fallo o kill durante materialización ⇒ sólo queda
tmp/; la corrida siguiente lo barre
y re-materializa. Nunca se sella parcial.
- R8 — La adopción no mueve ningún
artifact_hash: fuente_id no entra en hash_inputs.
Verificación pre-merge: hammer hash --check sobre el corpus, cero movidos.
- R9 — GC de
sources-cas por alcanzabilidad desde recetas activas; lo no alcanzable es
podable (regenerable desde el mirror ADR 0013). Medir por contenido, nunca con du.
- R10 — Re-materializar un
fuente_id existente debe reproducir el blake3_arbol del marker
o fallar ruidoso (detecta vendor no determinista o mirror envenenado).
- R11 — El
flock global (CLAUDE.md regla 1) y el JOBS=1 se retiran sólo tras F2.5
verde, en el mismo commit.
3. Invariantes (mecánicamente verificables)
- IF-1 — Tras N builds concurrentes sobre la misma fuente, el
blake3_arbol del árbol sellado
casa con el del marker. Verificable sin fanotify (que exige CAP_SYS_ADMIN y complica la
granja): hashear antes/después es más barato y más fuerte.
- IF-2 — Canario deliberado (estilo D9 de harkaq): un build de test escribe en
/src
(debe FUNCIONAR, vía copy-up al upper) y el árbol sellado sigue idéntico por blake3_arbol.
Si el hash se movió, alguien montó el lower como bind RW. No se cree: se gana.
- IF-3 —
hammer hash --check corpus completo: hash vigente idéntico pre/post migración.
- IF-4 —
kill -9 en medio de la materialización ⇒ no existe marker; la corrida siguiente
sella limpio y blake3_arbol casa con una materialización sin interrupción.
- IF-5 — Artefacto sobre overlay ≡ bit-a-bit al del modelo viejo, una receta por clase y
por build-system: autotools-en-raíz (zlib), meson y cmake (escriben en subdirs — la clase
que un spike sólo-zlib dejaría sin cubrir), cargo, go.
- IF-6 — Tras F3,
work/sources/ no existe, la Fase 1b de build-farm.sh está borrada, y
ningún script referencia el esquema viejo — verificado reconstruyendo, no con grep (en este
repo grep sobre ficheros con comentarios densos no es una medición).
4. Fases
- F0 — gate residual: el mecanismo overlay ya está verde (verificado 2026-08-28 con bwrap
0.11.2: lower legible, escritura al upper, lower intacto, y la composición con el
--tmp-overlay / del root — §8). Queda sólo IF-5, y con el corpus de clases completo — el
caso que muerde es meson/cmake en subdirs, no zlib.
- F1 — materialización:
fetch.rs produce fuentes selladas (R1–R3, R6–R7, R10). El build
sigue en el modelo viejo (puente). Incluye corregir la tabla de fases del SDD 02 §4.
- F2 — build sobre overlay:
/src = lower sellado + ws/<uuid>/{upper,work} (R4–R5),
respetando §1.2. Detrás de SOURCES_CAS=1, inerte sin él. IF-3 + IF-5.
- F2.5 — vertical slice: worker real con
JOBS=4 drena una onda KDE que atraviesa el diamante
qtbase. Todo ✓ REPRODUCIBLE, IF-1 e IF-2 corridos. Gate del ADR: 0012 pasa a ACEPTADO aquí
o el diseño vuelve.
- F3 — migración: borrar
work/sources/; borrar la Fase 1b de build-farm.sh
(entregable medible: la carrera B ya no existe); reescribir — no re-apuntar — el watchdog de
disco de farm-worker-loop.sh:43 (queda más simple: lo sellado es inmutable, cada ws/<uuid>
tiene un solo dueño); actualizar reconstruir-corpus.sh:206; retirar flock global + JOBS=1
(R11); GC (R9) al latido.
- F4 — cosecha: medir el speedup real de la campaña de reconstrucción con JOBS=N. El número
cierra el informe del ADR.
5. No-objetivos
- NG-1 — No cambia el sandbox de build ni sus fases internas.
- NG-2 — No toca
hash_inputs ni re-hashea nada (R8 lo verifica).
- NG-3 — No resuelve la red del vendor de Go en host (→ D1).
- NG-4 —
sources-cas NO es procedencia ni parte del store: es caché podable. Q3 cita
fuente_id en el log de transparencia, y eso es compatible porque la cita es a un hash cuya
materialización es regenerable desde el mirror (ADR 0013): podar no rompe la cita, la vuelve
un puntero frío. Lo que NUNCA se cita es una ruta de sources-cas.
- NG-5 — No introduce dependencia de reflink ni de overlayfs del host: el mecanismo es el
--overlay de bwrap ya en uso para deps.
6. Deudas nombradas
- D1 —
go mod vendor sigue en host con red. Acotado a materialización; R10 lo hace
detectable. Cierre futuro: materialización en sandbox con red controlada.
- D2 — Confirmar en F2.5 que los kernels propios de granja (SDD 22) montan el
--overlay de
bwrap con lower en sources-cas. Fallback: copia completa — correcta, lenta, ruidosa en el
log, nunca silenciosa.
- D3 —
--keep-ws acumula workspaces; podarlos en el GC de R9.
- D4 — El snapshot golden debe hornear
sources-cas o los primeros builds pagan
materialización fría. Choca con el test de hammer-bootstrap (lib.rs:2659) que afirma que
work/sources/ no viaja en el builder rootfs: la decisión (¿el builder hereda sources-cas?)
se toma en F2.5 con el dato del slice, y si es sí, ese test se actualiza a propósito — no se
deja fallar por sorpresa.
7. Preguntas — estado
- Q1 — CERRADA por precedente: fetch+patch+vendor corren en host desde el día 1
(
lib.rs:224,248,258). El riesgo ya se corre hoy; este SDD sólo lo pone bajo contrato (R10 lo
hace además detectable). No es una apuesta nueva.
- Q2 — abierta: retención de
sources-cas con vendors grandes: ¿tope GB con evicción LRU o
sólo alcanzabilidad? Decide el operador con el dato de F4.
- Q3 — DECIDIDA: sí —
fuente_id entra en bootstrap.json como campo opcional. Ver NG-4
para por qué no contradice la podabilidad.
Cruces: ADR 0012 (resuelve) · ADR 0013 (regenerabilidad que sostiene NG-4/Q3) · SDD 02 §4
(overlay-src de deps: el mecanismo reutilizado; tabla de fases a corregir en F1) · SDD 16 (patrón
"inerte sin flag"; canario D9 → IF-2) · CLAUDE.md regla 3 (marker contra vacíos) · CLAUDE.md
regla 1 (retiro del flock en R11) · build-farm.sh Fase 1b (carrera B; se borra en F3) ·
docs/BRIEFING-hammer.md §14.4 (los modos de fallo estructurales que R3 e IF-6 citan).
8. Registro de evidencia — mediciones contra el código (2026-08-28)
Las correcciones que salieron de aquí ya están incorporadas en el cuerpo de la v2. Esta
sección queda como registro reproducible: qué se midió, con qué comando y con qué salida, para
que nadie tenga que volver a descubrirlo. Método del SDD 22: un diseño entrante se contesta con
evidencia, y si un dato de hammer lo contradice, manda hammer.
8.1 El chmod a-w recursivo rompe el build (origen de R1)
El sandbox corre como uid 1001 sin CAP_DAC_OVERRIDE (id dentro de bwrap lo confirma) y
overlayfs preserva el modo del lower al hacer copy-up. Con el árbol sellado a a-w
(dirs 0555, ficheros 0444), sobre patrones reales de fase:
| patrón de build | resultado |
echo >> /mnt/configure (reescribir fichero existente) | FALLA — Permission denied |
echo y > /mnt/sub/nuevo.o (crear dentro de un subdir del lower) | FALLA — Permission denied |
sed -i en la raíz del overlay | OK |
mkdir /mnt/output (patrón meson) en la raíz | OK |
Las dos últimas pasan porque el upper root ya existe con modo 0755 y su modo gana sobre el del
lower root ⇒ el fallo es parcial y por profundidad: funciona arriba y falla adentro. Un spike
sobre zlib (autotools que configura en la raíz) habría pasado en verde y el diseño habría
reventado después en meson/cmake/cargo. Es la forma del fallo sellado ≠ arranca. De ahí R1
(inmutabilidad por mount, modos upstream) e IF-5 (una receta por build-system, no sólo zlib).
8.2 La composición real con el sandbox actual funciona (origen de §1.2 y del gate de F0)
Reproduciendo el orden de argumentos que sandbox.rs ya emite, más el grupo de /src:
bwrap --unshare-all \
--overlay-src .dev-fs/alpine --tmp-overlay / \
--overlay-src $S/lower --overlay $S/upper $S/work /src \
--proc /proc --dev /dev -- /bin/sh -c '…'
root = rootfs alpine: bin dev etc home lib media mnt musl256 opt proc root run sbi
/src = fuente sellada: SOY-LA-FUENTE-SELLADA
escritura en SUBDIR de /src: OK ← /src/sub/output/build.ninja (el caso meson)
raiz sigue escribible (tmp-overlay): OK
lower intacto: marca.txt, sub/f.c ← el árbol sellado no se tocó
upper del /src: sub/output/build.ninja ← el residuo quedó aislado
Cuatro hechos de una sola corrida: (1) el acumulador de --overlay-src se consume por operación
y no filtra — el root sigue siendo el rootfs Alpine y /src la fuente sellada; (2) el
--tmp-overlay / del root sigue funcionando junto al /src; (3) con modos upstream, escribir en
un subdirectorio de /src funciona — exactamente el caso que §8.1 rompía; (4) el lower queda
byte-intacto y el residuo aislado en el upper.
⇒ El gate de mecanismo de F0 está verde. Lo que queda de F0 es IF-5 (bit-a-bit real).
8.3 Referencias de código verificadas
| Afirmación del SDD | Dónde | Estado |
<nombre>-<sha16> (tarball) / <nombre>-<commit> (git) | fetch.rs:234 / fetch.rs:73 | ✅ |
| fetch+patch+vendor corren en host, antes de bwrap | lib.rs:224,248,258 | ✅ (cierra Q1) |
Límite de página del lowerdir y pre-merge cp -al | sandbox.rs:347-357 | ✅ |
| Carrera B y su reintento serial (Fase 1b) | build-farm.sh:82-99 | ✅ |
| Watchdog acoplado al esquema de rutas | farm-worker-loop.sh:43 | ✅ |
| Purga del árbol viejo | reconstruir-corpus.sh:206 | ✅ |
work/sources/ no viaja en el builder rootfs (test) | hammer-bootstrap/src/lib.rs:2659 | ✅ (origen de D4) |
.zwrap escrito en el árbol de fuentes | 131 recetas (medido) | ✅ |
--overlay RWSRC WORKDIR DEST disponible | bubblewrap 0.11.2 | ✅ |
| La tabla de fases dice "patch en sandbox" | docs/02-build-lab.md §4 | ⚠️ desactualizada — F1 |