@@ -2,6 +2,68 @@ |
| 2 | 2 | |
| 3 | 3 | ## Unreleased |
| 4 | 4 | |
| 5 | +### Sprint 26 — X3 sway pack / unpack (sway-side half of the cross-repo X1+X3 pair) |
| 6 | + |
| 7 | +Closes the X3 half of Audit 03's "make a sway run reproducible by a |
| 8 | +coworker without recreating their environment" goal. The X1 half |
| 9 | +(`dlm export --to sway-json`) lands in the dlm repo via a separate |
| 10 | +PR; this changelog block ships the sway-only deliverable. |
| 11 | + |
| 12 | +**New CLI commands.** |
| 13 | + |
| 14 | +- **`sway pack <spec> [-o OUT] [--include-golden PATH] |
| 15 | + [--include-null-cache/--no-include-null-cache] [--max-size-mb MB]`** — |
| 16 | + bundles the spec + its `dlm_source` (when set) + cached null-stats |
| 17 | + entries + an optional last-known-good JSON report into a single |
| 18 | + `*.swaypack.tar.gz`. Default cap 50 MB; refuses to overwrite. |
| 19 | +- **`sway unpack <pack> [-o DIR]`** — extracts into `<DIR>/swaypack/`, |
| 20 | + validates the manifest, and prints the ready-to-run `sway run` |
| 21 | + invocation including the `SWAY_NULL_CACHE_DIR=...` env var that |
| 22 | + redirects null-stats lookups at the bundled cache. |
| 23 | + |
| 24 | +**Pack format (`swaypack_version=1`).** |
| 25 | + |
| 26 | +``` |
| 27 | +swaypack/ |
| 28 | + manifest.json # version + counts + packed_at + sway_version |
| 29 | + sway.yaml # the spec, verbatim |
| 30 | + source.dlm # spec.dlm_source content (when present) |
| 31 | + null-stats/ # one .json per cache key |
| 32 | + golden.json # known-good run report (when --include-golden) |
| 33 | +``` |
| 34 | + |
| 35 | +Implementation modules: |
| 36 | +- **`cli/_pack.py`** — builds the tarball in-memory first so the |
| 37 | + size cap can refuse cleanly *before* writing a half-built pack |
| 38 | + to disk. Path-traversal-safe by construction (we author every |
| 39 | + arcname). |
| 40 | +- **`cli/_unpack.py`** — uses `tarfile.extractall(filter='data')` |
| 41 | + to reject absolute paths / `../` escapes / device files (3.11 |
| 42 | + compat). Validates `swaypack_version`; rejects mismatches with |
| 43 | + a clear message. |
| 44 | + |
| 45 | +**Null-cache override env var.** |
| 46 | + |
| 47 | +- **`probes/_null_cache._cache_root`** now honors |
| 48 | + `$SWAY_NULL_CACHE_DIR` if set (used verbatim — no |
| 49 | + `dlm-sway/null-stats` suffix). Order: env override → XDG cache → |
| 50 | + `~/.dlm-sway/null-stats`. The `sway unpack` CLI prints the exact |
| 51 | + invocation that wires this env at the bundled cache. |
| 52 | + |
| 53 | +**Tests.** |
| 54 | + |
| 55 | +- **17 unit tests** in `tests/unit/test_pack_unpack.py`: round-trip |
| 56 | + identity, manifest fields, dlm_source bundling (present + missing |
| 57 | + + warning emit), golden bundling, every PackError + UnpackError |
| 58 | + branch (overwrite refusal, size cap, missing file, corrupt |
| 59 | + tarball, missing manifest, version mismatch, malformed root). |
| 60 | +- **2 slow+online integration tests** in |
| 61 | + `tests/integration/test_pack_run_roundtrip.py`: pack → unpack → |
| 62 | + `sway run` produces an identical band + per-probe verdict + |
| 63 | + score; null-cache packing populates the unpack report's |
| 64 | + `null_stats_dir` pointer. |
| 65 | +- 708 unit tests pass; mypy + ruff + format clean. |
| 66 | + |
| 5 | 67 | ### Sprint 25 — P3 gradient_ghost probe (pre-run, cross-repo) |
| 6 | 68 | |
| 7 | 69 | New zero-forward-pass diagnostic probe that loads dlm's |