fix(#195): narrow fix — committed_at_raw preserves commit identity across UTC normalization
committed_at is identity-bearing: hash_commit's preimage includes the exact ISO-8601 string the client used. musehub_commits.timestamp is a Postgres timestamptz, which normalizes any non-UTC offset to UTC on write. The wire-serve path (_to_wire_commit) reconstructs committed_at via `timestamp.isoformat()` -- for a commit originally authored with a non-UTC offset (e.g. every bridge-imported commit carrying its original author-local timezone), this produces a DIFFERENT string than the one baked into commit_id, so the client's own hash check correctly rejects the download. Per corrected counts: 890 of 1,308 knotwation commits affected (all bridge imports); musehub itself 0 of 1,617 (native commits are always server-generated in UTC).
This is an instance of the broader problem tracked in #63 (commits are DB-only, reconstructed from DB fields rather than served as stored canonical bytes). This is the deliberately narrow, additive alternative, per gabriel's decision: still correct after #63 eventually lands (#63 would make this column redundant, not wrong).
Added: - alembic/versions/0077_add_committed_at_raw.py + matching ORM column on MusehubCommit -- additive, nullable Text column storing the exact original committed_at string. - musehub/services/musehub_wire_shared.py::_to_wire_commit -- prefers committed_at_raw verbatim when present; unchanged fallback to timestamp.isoformat() when absent (legacy rows). - verify_commit_identity_or_raise() -- a standalone helper that recomputes a commit's id from its wire representation and raises ObjectHashMismatch on mismatch, for explicit/diagnostic use (same pattern wire_repair_commit already uses internally). - committed_at_raw populated at every commit write site: wire push ingest (musehub_wire_push.py -- captures the true client-original string before CommitRecord.from_dict discards it, the only point in the ingest path where it still exists), proposal merge + rebase replay (musehub_proposals.py), repo-init and sync synthetic commits (musehub_repository.py, musehub_sync.py -- populated for consistency, though neither of these two sites uses hash_commit-derived ids at all, see below).
Deliberately NOT done -- automatic fail-closed verification on every serve: tried wiring verify_commit_identity_or_raise into _commit_to_wire_s3 (the function wire_fetch_mpack actually uses) and confirmed experimentally it breaks real things: - wire_repair_commit's entire purpose is repairing a commit whose stored identity fields don't currently reproduce its id -- an automatic check on every serve broke that flow outright (test_repair_commit_endpoint.py failed with the exact ObjectHashMismatch this ticket is about, on a commit mid-repair). - musehub_repository.py's repo-init commit and musehub_sync.py's synthetic content commit both use non-hash_commit id schemes (blob_id-based), confirmed directly: hash_commit-recomputing either one never matches its stored id, regardless of committed_at accuracy. This is a real, pre-existing, unrelated defect (filed separately, see below) that a blanket automatic check would have conflated with this ticket's actual bug. Legacy non-UTC rows with committed_at_raw=NULL therefore still serve a mismatched committed_at exactly as before this fix -- not newly broken, not silently fixed either. Fixing already-affected historical data is out of scope for this narrow fix.
Tests (TDD, red confirmed before implementation): - tests/test_committed_at_raw.py (7 tests): _to_wire_commit prefers raw when present (constructed so it actually discriminates old vs new behavior, not a self-referential no-op check) and falls back correctly when absent; verify_commit_identity_or_raise passes/raises correctly in isolation; wire_fetch_mpack end-to-end serves the exact original non-UTC string when raw is present (the actual regression closed); a legacy non-UTC row missing raw is explicitly proven unchanged (not worsened), documenting the deliberate scope boundary above.
Full regression: 345/345 across test_wire_fetch_mpack.py, test_repair_commit_endpoint.py, test_merge_commit_id_parity.py, test_musehub_repos.py, test_wire_push_external_parent_reconstruction.py, test_schema_parity.py, test_schema_check.py, test_committed_at_raw.py, test_musehub_proposals.py, test_merge_proposals.py, test_musehub_collaborators.py, test_collaborators.py -- no regressions.
Along the way: found the local dev Postgres container's musehub DB had 17 duplicate (repo_id, number) issue rows blocking migration past #184's own unique constraint (0076) -- pre-existing local dev-data cruft, cleaned up (kept one row per group) so migrations could progress; also found and fixed a missing `zstandard` package in this repo's .venv (installed) that was masking real test signal with an unrelated ModuleNotFoundError.
Related: - #63 -- the structural fix this stays correct under. - musehub_repository.py/musehub_sync.py's non-hash_commit id schemes -- new finding, filed as its own follow-up ticket.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Semantic Changes
50 symbols
Files Changed
+2
~6
1075 in snapshot
0 comments
muse hub commit comment sha256:06cdc3de1e7f8091954906cf91f22c941373474c3322392a79300adc19b33197 --body "your comment"
No comments yet. Be the first to start the discussion.