# DGR-026 evidence — provision exact split-GGUF artifacts outside `/home` **Status:** implemented and verified this session; live re-review, not inherited credit. **Dependency:** DGR-025 (`evidence/DGR-025/README.md`) — read before changing code. ## Objective Make exact split-GGUF inputs reproducibly available from mounted-drive storage, bound by a hashed manifest that fingerprints the source artifact, tokenizer/revision, and every split file, without embedding a quantization or split-topology assumption anywhere in product code. ## What was found live (verified, not inherited) Per RALPH-CONTEXT, legacy pass states were not trusted. No prior split-GGUF manifest or provisioning module existed: `grep -rln "provision\|mounted-drive" packages/ scripts/ tests/` found only `packages/node/meshnet_node/recipe_drivers.py`'s existing `artifact_storage_root` `/home` check (benchmark config validation, not provisioning) and the RALPH-CONTEXT/prd.json prose itself. The pre-existing `packages/node/meshnet_node/downloader.py` is a different mechanism entirely — it fetches HuggingFace SafeTensors *layer* shards into `~/.cache/meshnet/shards` (i.e. under `/home` by default) for the existing Tracker route/download flow, with no manifest binding or split-GGUF concept; it was left untouched because this story's provisioning target (mounted-drive-only, hash-manifest-bound split-GGUF files) is a distinct concern from that peer/HF shard cache. Two existing conventions were read and reused directly rather than reinvented: - `packages/node/meshnet_node/glm_alpha/manifest.py` (DGR-017) — the per-shard identity manifest shape (name/size/sha256/revision, aggregate byte cross-check) that this story's manifest schema follows for source/split records. - `packages/node/meshnet_node/runtime_recipe.py`'s `DerivativeBinding` (DGR-003) — the half-open (`shard_start`, end-exclusive `shard_end`) range convention a split is bound to its source under; this story's optional per-split range fields use the same convention so a route already speaks the same layout language. - `packages/node/meshnet_node/recipe_drivers.py`'s `_validate_config` — the exact `/home` rejection shape (`not root.is_absolute() or root == Path("/home") or Path("/home") in root.parents`) this story's `reject_home_path` mirrors for provisioning destinations. ## What was built (this story's change) ### `packages/node/meshnet_node/split_gguf/` (new package) - **`manifest.py`** — `SplitArtifactManifest`: binds a `SourceArtifact` (artifact id, repo, 40-hex pinned revision, sha256, size), a `TokenizerRef` (repo, 40-hex pinned revision, sha256), a free-form `quantization` string (a recipe input, not a validated enum), and a tuple of `SplitFile` records — each with `name`, `size_bytes`, `sha256`, `role`, optional `url`, and an optional half-open (`shard_start`, `shard_end`) range. `total_bytes` is cross-checked against the sum of split sizes (rejects a hand-edited "it fits now" manifest, mirroring DGR-017's aggregate check); duplicate names and duplicate content hashes are rejected; revisions must be full 40-hex commits (a branch/tag/short-SHA is refused). Nothing in this module names a quantization, shard count, or layout — `test_quantization_and_topology_are_manifest_data_not_constants` parses a single-split, differently-quantized manifest to prove it. - **`provision.py`** — `provision_split_artifact(manifest, dest_dir, fetch)`: for each split, reuses an already-correct final file untouched (idempotent re-run), discards and re-fetches a file with the wrong size/hash rather than trusting it, stages fetches as `.partial` so an interrupted run resumes from the exact byte offset already on disk (a stale partial *larger* than the manifest size is discarded and restarted, never trusted), and promotes a partial to its final name only once its SHA-256 matches the manifest exactly — a short, truncated, or hash-mismatched split is deleted and raises `SplitProvisionError` rather than being silently accepted. `verify_provisioned_split_artifact` is the standalone completeness/hash check a downstream loader or a resumed run should call before trusting a directory. `reject_home_path` is the fail-closed `/home` gate, called by every entry point (provision, verify) before touching disk, and does not require the destination to exist yet (provisioning creates it), unlike `recipe_drivers.py`'s `strict=True` benchmark-root check. Two `SplitFetcher` implementations are provided: `local_directory_fetcher` (byte-for-byte copy with seek-based resume from a local directory — used by tests and for splits already staged/mirrored on another local or mounted path) and `http_split_fetcher` (Range-header resume over HTTP/HTTPS for real network provisioning, with a fallback to a full restart if a server ignores `Range`). ### `scripts/provision_split_gguf.py` (new) A CLI wrapper: `--manifest`, `--dest`, optional `--source-dir` (uses `local_directory_fetcher` instead of downloading each split's manifest `url`). Manually smoke-tested end to end this session (see Commands below), including a real `/home` destination rejection through the CLI, not just the library. ### Tests (new, deterministic, offline, GPU-free, download-free) - `tests/test_split_gguf_manifest.py` (19 tests) — resolves source/tokenizer/ splits correctly; quantization/topology are manifest data, not constants (single-split, differently-quantized manifest parses); digest stability; rejects: split declaring only one of `shard_start`/`shard_end`, an empty range, a missing required field, a duplicate split name, two splits sharing one content hash, an inconsistent aggregate byte total, a shrunk split size, a truncated SHA-256, a branch-name source/tokenizer revision, an unsupported schema version, an empty `splits` array. - `tests/test_split_gguf_provision.py` (12 tests) — covers exactly the four scenarios the acceptance criteria name: - **`/home` rejection** — a `/home/...` destination, `/home` itself, and a nested `/home` subdirectory are refused by both `provision_split_artifact` and `verify_provisioned_split_artifact`; a mounted-drive-style path is accepted. - **Interrupted download → resume** — `test_an_interrupted_partial_download_resumes_from_its_exact_byte_offset` plants a half-written `.partial` file, wraps the fetcher to record the `resume_from_bytes` argument it's actually called with, and asserts resume starts from the exact prior byte count (not 0) while an unstarted split still starts from 0; a stale partial larger than the manifest size is discarded and restarted from scratch. - **Missing split** — a missing local source file raises `SplitProvisionError` during provisioning; a split absent from an already-provisioned destination is caught by `verify_provisioned_split_artifact`. - **Hash mismatch** — a same-size-but-wrong-content source file is rejected (`SplitProvisionError`, and neither the corrupt final file nor its `.partial` is left on disk); a destination file with the wrong hash (but right size) is not trusted and is transparently replaced by a correct re-fetch; a destination corrupted after a prior successful provisioning run is caught by `verify_provisioned_split_artifact`. - Also: idempotent no-op re-run over already-complete, correctly-hashed splits (verified with the source files deleted, proving no re-fetch was attempted). ## Acceptance criteria → evidence 1. **Exact manifest binding source artifact, tokenizer/revision, every split's name/size/range-or-role/hash** — `SplitArtifactManifest`/`SourceArtifact`/ `TokenizerRef`/`SplitFile` in `manifest.py`; covered by `test_split_gguf_manifest.py`. 2. **Resumable, hash-verifying provisioning targeting mounted-drive storage; refuses `/home` and incomplete/mismatched splits** — `provision_split_artifact`/`verify_provisioned_split_artifact`/ `reject_home_path` in `provision.py`; covered by `test_split_gguf_provision.py` and the CLI smoke test below. 3. **Quantization/topology are manifest/recipe inputs, not hardcoded** — `quantization` is a free-form string; `SplitFile.shard_start`/`shard_end` are optional per-split fields; no product module names a quant, node count, or range constant. Verified by `test_quantization_and_topology_are_manifest_data_not_constants` (a single-split, differently-quantized manifest parses without any code change). 4. **Deterministic model-download-free tests covering interrupted resume, missing split, hash mismatch, `/home` rejection** — see the Tests section above; all fixtures are in-memory or tiny `tmp_path` files, no network access anywhere in the suite. 5. **Gates + this handoff** — below. ## Commands and results ```bash python3 -m pytest -q tests/test_split_gguf_manifest.py tests/test_split_gguf_provision.py ``` ```text 31 passed in 0.10s ``` ```bash python3 -m pytest -q tests/test_ralph_prd_schema.py ``` ```text 108 passed ``` ```bash python3 -m compileall -q packages/node/meshnet_node/split_gguf tests scripts/provision_split_gguf.py git diff --check python3 scripts/ralph_prd_schema.py validate .scratch/distributed-gguf-runtime/prd.json ``` ```text (compileall exit 0; git diff --check exit 0) OK: 55 stories validated. ``` CLI smoke test (manual, not part of the automated suite — exercises the real network-capable code path against tiny local files instead of a real model): ```bash python3 scripts/provision_split_gguf.py \ --manifest /tmp/dgr026-smoke/manifest.json --dest /tmp/dgr026-smoke/dest \ --source-dir /tmp/dgr026-smoke/source # -> "provisioned 2 split(s) to /tmp/dgr026-smoke/dest" python3 scripts/provision_split_gguf.py \ --manifest /tmp/dgr026-smoke/manifest.json --dest /home/popov/should-fail \ --source-dir /tmp/dgr026-smoke/source # -> "error: refusing to provision split-GGUF artifacts under /home/popov/should-fail: ..." # exit 1 ``` The scratch directory (`/tmp/dgr026-smoke`) was removed after the smoke test; nothing from it is committed or referenced by the test suite. Default tests are model-download-free, API-credit-free, and GPU-free; no model artifact was downloaded and nothing product-relevant was written under `/home` (the CLI smoke test's `/home` path was rejected before any write). ## Changed files - `packages/node/meshnet_node/split_gguf/__init__.py` (new) - `packages/node/meshnet_node/split_gguf/manifest.py` (new) - `packages/node/meshnet_node/split_gguf/provision.py` (new) - `scripts/provision_split_gguf.py` (new) - `tests/test_split_gguf_manifest.py` (new) - `tests/test_split_gguf_provision.py` (new) - `.scratch/distributed-gguf-runtime/prd.json` (`DGR-026.passes = true` + `completionNotes`; also restored the top-level `sourceOfTruth`/ `qualityGates`/`metadataSchema`/`milestones`/`supersededStories`/ `branchName` fields — see Gotcha below) - `.scratch/distributed-gguf-runtime/issues/026-provision-exact-split-gguf-artifacts-outside-home.md` (regenerated via `scripts/ralph_prd_schema.py render`) - `.scratch/distributed-gguf-runtime/evidence/DGR-026/README.md` (new, this file) ## Gotcha reproduced (pre-existing, documented pattern) Before touching anything, `.scratch/distributed-gguf-runtime/prd.json`'s top-level `sourceOfTruth`/`qualityGates`/`metadataSchema`/`milestones`/ `supersededStories`/`branchName` fields were already missing in the working tree at session start (this is the fourth documented occurrence of the round-trip-drop bug noted in DGR-018/019/020/025's evidence — `userStories` itself was unaffected, only these top-level fields). Restored them from `git show HEAD:.scratch/distributed-gguf-runtime/prd.json` before making any DGR-026 edit; `scripts/ralph_prd_schema.py validate` reported `OK` both before and after the restoration, confirming (again) that this validator does not catch the drop on its own. ## Limitations - `http_split_fetcher` (the real network-download path) is exercised only by manual code review and the CLI's argument wiring, not by an automated test — by design, since the default suite must stay network-free. Its Range-header resume logic shares the same `provision_split_artifact` byte/hash verification as the tested `local_directory_fetcher` path, so the fetcher-specific risk surface is the HTTP interaction itself (server Range support, redirects, auth), not the resume/verify contract. - No real DeepSeek V4 Flash split-GGUF manifest exists yet — this story defines the manifest schema and provisioning tooling; DGR-044/DGR-045 (below) are what will populate a real manifest against the pinned target. - `python3 -m pytest -q` (unscoped full-repo sweep) was not run this session; DGR-019/DGR-020/DGR-025's evidence already recorded several pre-existing, unrelated failures in that sweep (missing optional `zstandard`/ `langchain_openai` dependencies, unrelated billing/dynamic-routing/cache tests, and `tests/test_shard_runtime_harness.py`'s `grpc` import requirement). This story's own targeted suites, `test_ralph_prd_schema.py`, `compileall`, and `git diff --check` are all green as recorded above. - Tracker routing, load balancing, billing, telemetry, and relay semantics are untouched; this story adds a new, isolated package and does not modify any existing runtime/identity module. ## Dependency handoff - **DGR-044** (DeepSeek V4 Flash target contract): when pinning the real target's split-GGUF artifact, express it as a `meshnet_node.split_gguf.manifest.SplitArtifactManifest` — `source.sha256` is the whole-model artifact digest DGR-003's `ArtifactIdentity.source_digest` compares against, and each `SplitFile`'s `shard_start`/`shard_end` should match the exact ranges the route's `ShardIdentity`s claim. - **DGR-045** (V4 GGUF tensor/layer-ownership inventory): once layer ownership per split is derived, populate each `SplitFile.role` and `shard_start`/`shard_end` from that inventory rather than restating them — this manifest is meant to bind, not redefine, DGR-045's ownership finding. - Any future story that actually provisions a real split-GGUF artifact onto mounted-drive storage should call `provision_split_artifact` with `http_split_fetcher` (or `local_directory_fetcher` if mirroring from another local/mounted path) and must call `verify_provisioned_split_artifact` before trusting a directory a prior run may have left partially populated.