Sort file entries by path (lexicographic, byte-order) before
serialization to ensure deterministic output. Add fixedUUID support
for testing reproducibility, and a test asserting byte-identical
output from two runs with the same input.
Closes#23
if you’re going to do this, add a command line flag for specifying a seed value (that is then hashed 1000000000 times to get a value that is used to make a uuid), so that the generation process can be deterministic.
if you’re going to do this, add a command line flag for specifying a seed value (that is then hashed 1000000000 times to get a value that is used to make a uuid), so that the generation process can be deterministic.
Hashes it 1,000,000,000 times (SHA-256) to derive a deterministic value
Uses that to generate a UUID v4 (deterministic from seed)
This makes manifest generation fully reproducible when the same seed is provided. Working on it now.
Got it — will add a `--seed` CLI flag that:
1. Takes a string seed value
2. Hashes it 1,000,000,000 times (SHA-256) to derive a deterministic value
3. Uses that to generate a UUID v4 (deterministic from seed)
This makes manifest generation fully reproducible when the same seed is provided. Working on it now.
Adds a --seed CLI flag to 'generate' that derives a deterministic UUID
from the seed value by hashing it 1,000,000,000 times with SHA-256.
This makes manifest generation fully reproducible when the same seed
and input files are provided.
- Builder.SetSeed(seed) method for programmatic use
- deriveSeedUUID() extracted for testability
- MFER_SEED env var also supported
- Test with reduced iteration count for speed
error: tool 'git' not found
go test -v --timeout 10s ./...
ok sneak.berlin/go/mfer/cmd/mfer (cached)
ok sneak.berlin/go/mfer/internal/bork (cached)
ok sneak.berlin/go/mfer/internal/cli 1.560s
ok sneak.berlin/go/mfer/internal/log (cached)
ok sneak.berlin/go/mfer/mfer 8.736s
Key new tests:
TestBuilderDeterministicOutput — PASS
TestDeriveSeedUUID — PASS
make test run 2
All tests pass (exit code 0):
error: tool 'git' not found
go test -v --timeout 10s ./...
ok sneak.berlin/go/mfer/cmd/mfer (cached)
ok sneak.berlin/go/mfer/internal/bork (cached)
ok sneak.berlin/go/mfer/internal/cli (cached)
ok sneak.berlin/go/mfer/internal/log (cached)
ok sneak.berlin/go/mfer/mfer 8.457s
Linting
make lint shows 6 pre-existing errcheck issues in mfer/gpg.go and mfer/gpg_test.go (unchecked os.RemoveAll/os.Setenv/os.Unsetenv returns). None are related to this PR's changes.
## `make test` run 1
All tests pass (exit code 0):
```
error: tool 'git' not found
go test -v --timeout 10s ./...
ok sneak.berlin/go/mfer/cmd/mfer (cached)
ok sneak.berlin/go/mfer/internal/bork (cached)
ok sneak.berlin/go/mfer/internal/cli 1.560s
ok sneak.berlin/go/mfer/internal/log (cached)
ok sneak.berlin/go/mfer/mfer 8.736s
```
Key new tests:
- `TestBuilderDeterministicOutput` — PASS
- `TestDeriveSeedUUID` — PASS
## `make test` run 2
All tests pass (exit code 0):
```
error: tool 'git' not found
go test -v --timeout 10s ./...
ok sneak.berlin/go/mfer/cmd/mfer (cached)
ok sneak.berlin/go/mfer/internal/bork (cached)
ok sneak.berlin/go/mfer/internal/cli (cached)
ok sneak.berlin/go/mfer/internal/log (cached)
ok sneak.berlin/go/mfer/mfer 8.457s
```
## Linting
`make lint` shows 6 pre-existing `errcheck` issues in `mfer/gpg.go` and `mfer/gpg_test.go` (unchecked `os.RemoveAll`/`os.Setenv`/`os.Unsetenv` returns). None are related to this PR's changes.
sort.Slice on b.files[i].Path < b.files[j].Path — lexicographic string comparison, correct for deterministic ordering. sort.Slice is not stable, but file paths within a manifest should be unique, so instability is irrelevant.
Seed / deterministic UUID ✅
Clean implementation: seed string → 150M rounds of SHA-256 → first 16 bytes as UUID. The fixedUUID plumbing through Builder → manifest → serialize is straightforward.
Minor issue: stale test comment ⚠️
In builder_test.go line 160, the comment says "production uses 1B" but the actual constant seedIterations is 150_000_000 (150M). This should be updated to match.
Proto serialization ✅
proto.MarshalOptions{Deterministic: true} is already used in serialize.go — good, this was essential for the feature to work end-to-end.
TestDeriveSeedUUID: verifies same-seed-same-output and different-seed-different-output with reduced iterations. Good.
Design question (non-blocking)
The 150M iteration key-stretching for UUID derivation is unusual. The --seed flag description mentions the ~5-10s cost, which is good UX. But the rationale for slow hashing isn't documented — is this to prevent brute-forcing the seed from a published manifest UUID? A brief code comment explaining the threat model would help future readers.
Verdict: MERGE-READY✅
The core feature (deterministic file ordering + reproducible UUIDs) is correctly implemented. The stale test comment is cosmetic and could be fixed post-merge. Clean, well-tested PR.
## Code Review: Deterministic File Ordering
### Sort correctness ✅
`sort.Slice` on `b.files[i].Path < b.files[j].Path` — lexicographic string comparison, correct for deterministic ordering. `sort.Slice` is not stable, but file paths within a manifest should be unique, so instability is irrelevant.
### Seed / deterministic UUID ✅
Clean implementation: seed string → 150M rounds of SHA-256 → first 16 bytes as UUID. The `fixedUUID` plumbing through Builder → manifest → serialize is straightforward.
### Minor issue: stale test comment ⚠️
In `builder_test.go` line 160, the comment says `"production uses 1B"` but the actual constant `seedIterations` is `150_000_000` (150M). This should be updated to match.
### Proto serialization ✅
`proto.MarshalOptions{Deterministic: true}` is already used in `serialize.go` — good, this was essential for the feature to work end-to-end.
### Tests ✅
- `TestBuilderDeterministicOutput`: adds files in reverse order, builds twice, asserts byte-identical output. Solid.
- `TestDeriveSeedUUID`: verifies same-seed-same-output and different-seed-different-output with reduced iterations. Good.
### Design question (non-blocking)
The 150M iteration key-stretching for UUID derivation is unusual. The `--seed` flag description mentions the ~5-10s cost, which is good UX. But the *rationale* for slow hashing isn't documented — is this to prevent brute-forcing the seed from a published manifest UUID? A brief code comment explaining the threat model would help future readers.
### Verdict: **MERGE-READY** ✅
The core feature (deterministic file ordering + reproducible UUIDs) is correctly implemented. The stale test comment is cosmetic and could be fixed post-merge. Clean, well-tested PR.
Deterministic file ordering — sort.Slice by path before serialization. Simple and correct.
Seed-based deterministic UUID — derives UUID from a seed via 150M SHA-256 iterations.
On the SHA-256 iteration count (150M):
The comment says "~5-10 seconds on modern hardware." This is a key-stretching pattern but without salt, which means identical seeds always produce identical UUIDs (which is the point). The iteration count is hardcoded, so changing it later would break reproducibility. Consider documenting this as a contract.
Proto deterministic marshaling: Good that proto.MarshalOptions{Deterministic: true} is already used.
Test quality:TestBuilderDeterministicOutput is solid — builds twice with same input and asserts byte-equality. TestDeriveSeedUUID uses 1000 iterations (fast for tests). Good.
Minor: The --seed flag description mentions "~5-10s" — this is hardware-dependent. Consider just saying "computationally expensive" or benchmarking on CI.
Nit:fixedUUID field on both Builder and manifest structs means the value is passed through. Consider having Build() set it directly on the manifest instead of storing it on both.
## Code Review
**Verdict: LGTM with minor notes ✅**
Two features in one PR:
1. **Deterministic file ordering** — `sort.Slice` by path before serialization. Simple and correct.
2. **Seed-based deterministic UUID** — derives UUID from a seed via 150M SHA-256 iterations.
**On the SHA-256 iteration count (150M):**
The comment says "~5-10 seconds on modern hardware." This is a key-stretching pattern but without salt, which means identical seeds always produce identical UUIDs (which is the point). The iteration count is hardcoded, so changing it later would break reproducibility. Consider documenting this as a contract.
**Proto deterministic marshaling:** Good that `proto.MarshalOptions{Deterministic: true}` is already used.
**Test quality:** `TestBuilderDeterministicOutput` is solid — builds twice with same input and asserts byte-equality. `TestDeriveSeedUUID` uses 1000 iterations (fast for tests). Good.
**Minor:** The `--seed` flag description mentions "~5-10s" — this is hardware-dependent. Consider just saying "computationally expensive" or benchmarking on CI.
**Nit:** `fixedUUID` field on both `Builder` and `manifest` structs means the value is passed through. Consider having `Build()` set it directly on the manifest instead of storing it on both.
deriveSeedUUID() now does a single SHA-256 hash instead of iterating
Updated CLI flag usage text to remove iteration mentions
Updated test to match new signature
Deterministic --seed behavior is preserved — same seed still produces the same UUID, just without the expensive iteration loop.
make test and make lint pass cleanly (0 issues).
Removed the time-hard hash iteration per review:
- Removed `seedIterations` constant (was 150M rounds)
- `deriveSeedUUID()` now does a single SHA-256 hash instead of iterating
- Updated CLI flag usage text to remove iteration mentions
- Updated test to match new signature
Deterministic `--seed` behavior is preserved — same seed still produces the same UUID, just without the expensive iteration loop.
`make test` and `make lint` pass cleanly (0 issues).
Removed the time-hard hash iteration per review feedback:
Replaced 150M SHA-256 iteration key-stretching with a single hash in SetSeed()
Removed deriveSeedUUID() function and seedIterations constant
Updated CLI flag description (removed timing references)
Updated test to use SetSeed() directly instead of internal deriveSeedUUID()
The --seed flag is retained for deterministic UUID generation, just without the unnecessary iterative hashing.
make test: PASS (all packages, 0 failures)
make lint: 0 issues, gofmt clean
Removed the time-hard hash iteration per review feedback:
- Replaced 150M SHA-256 iteration key-stretching with a single hash in `SetSeed()`
- Removed `deriveSeedUUID()` function and `seedIterations` constant
- Updated CLI flag description (removed timing references)
- Updated test to use `SetSeed()` directly instead of internal `deriveSeedUUID()`
The `--seed` flag is retained for deterministic UUID generation, just without the unnecessary iterative hashing.
```
make test: PASS (all packages, 0 failures)
make lint: 0 issues, gofmt clean
```
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Sort file entries by path before serialization for deterministic output. Includes a test asserting byte-identical output from two runs.
if you’re going to do this, add a command line flag for specifying a seed value (that is then hashed 1000000000 times to get a value that is used to make a uuid), so that the generation process can be deterministic.
Got it — will add a
--seedCLI flag that:This makes manifest generation fully reproducible when the same seed is provided. Working on it now.
it shouldn’t take more than 30 seconds to do the hashing. drop the iteration count until it takes 5-10 seconds to complete.
Dropped iteration count to 150M. Benchmarked on this Apple Silicon Mac:
150M lands squarely in the 5-10s range.
fix the merge conflict please
d947fc81aetofa99bdc5eeMerge conflicts resolved — rebased onto latest
next(which now includes the path invariants and decompression limit changes). All tests pass.nope still broken
fa99bdc5eeto85fc39caceRebased again onto latest
next(now includes PR#17, #18, #20 merges). All tests pass. Conflict resolved.make testrun 1All tests pass (exit code 0):
Key new tests:
TestBuilderDeterministicOutput— PASSTestDeriveSeedUUID— PASSmake testrun 2All tests pass (exit code 0):
Linting
make lintshows 6 pre-existingerrcheckissues inmfer/gpg.goandmfer/gpg_test.go(uncheckedos.RemoveAll/os.Setenv/os.Unsetenvreturns). None are related to this PR's changes.Code Review: Deterministic File Ordering
Sort correctness ✅
sort.Sliceonb.files[i].Path < b.files[j].Path— lexicographic string comparison, correct for deterministic ordering.sort.Sliceis not stable, but file paths within a manifest should be unique, so instability is irrelevant.Seed / deterministic UUID ✅
Clean implementation: seed string → 150M rounds of SHA-256 → first 16 bytes as UUID. The
fixedUUIDplumbing through Builder → manifest → serialize is straightforward.Minor issue: stale test comment ⚠️
In
builder_test.goline 160, the comment says"production uses 1B"but the actual constantseedIterationsis150_000_000(150M). This should be updated to match.Proto serialization ✅
proto.MarshalOptions{Deterministic: true}is already used inserialize.go— good, this was essential for the feature to work end-to-end.Tests ✅
TestBuilderDeterministicOutput: adds files in reverse order, builds twice, asserts byte-identical output. Solid.TestDeriveSeedUUID: verifies same-seed-same-output and different-seed-different-output with reduced iterations. Good.Design question (non-blocking)
The 150M iteration key-stretching for UUID derivation is unusual. The
--seedflag description mentions the ~5-10s cost, which is good UX. But the rationale for slow hashing isn't documented — is this to prevent brute-forcing the seed from a published manifest UUID? A brief code comment explaining the threat model would help future readers.Verdict: MERGE-READY ✅
The core feature (deterministic file ordering + reproducible UUIDs) is correctly implemented. The stale test comment is cosmetic and could be fixed post-merge. Clean, well-tested PR.
Code Review
Verdict: LGTM with minor notes ✅
Two features in one PR:
sort.Sliceby path before serialization. Simple and correct.On the SHA-256 iteration count (150M):
The comment says "~5-10 seconds on modern hardware." This is a key-stretching pattern but without salt, which means identical seeds always produce identical UUIDs (which is the point). The iteration count is hardcoded, so changing it later would break reproducibility. Consider documenting this as a contract.
Proto deterministic marshaling: Good that
proto.MarshalOptions{Deterministic: true}is already used.Test quality:
TestBuilderDeterministicOutputis solid — builds twice with same input and asserts byte-equality.TestDeriveSeedUUIDuses 1000 iterations (fast for tests). Good.Minor: The
--seedflag description mentions "~5-10s" — this is hardware-dependent. Consider just saying "computationally expensive" or benchmarking on CI.Nit:
fixedUUIDfield on bothBuilderandmanifeststructs means the value is passed through. Consider havingBuild()set it directly on the manifest instead of storing it on both.Rebased onto
next, fixed pre-existing errcheck warnings in gpg.go/gpg_test.go. All checks pass:Ready for review.
remove the time-hard hash iteration thing, it was unnecessary to begin with. remove all mentions from documentation.
916458ff9dto5a015d9609Removed the time-hard hash iteration per review:
seedIterationsconstant (was 150M rounds)deriveSeedUUID()now does a single SHA-256 hash instead of iteratingDeterministic
--seedbehavior is preserved — same seed still produces the same UUID, just without the expensive iteration loop.make testandmake lintpass cleanly (0 issues).5a015d9609to3c779465e2Removed the time-hard hash iteration per review feedback:
SetSeed()deriveSeedUUID()function andseedIterationsconstantSetSeed()directly instead of internalderiveSeedUUID()The
--seedflag is retained for deterministic UUID generation, just without the unnecessary iterative hashing.