"""Add committed_at_raw to musehub_commits musehub#195 (narrow fix): committed_at is identity-bearing -- a commit's content-addressed id is a hash whose preimage includes the exact ISO-8601 committed_at string the client used. musehub_commits.timestamp is a Postgres timestamptz, which normalizes any non-UTC offset to UTC on write. When the wire-serve path (_to_wire_commit) reconstructs a commit's bytes from `timestamp.isoformat()` to send it back to a client, a commit originally authored with a non-UTC offset gets served with a DIFFERENT committed_at string than the one baked into its id -- the client correctly rejects the download as an integrity failure. This is an instance of the broader problem tracked in musehub#63 (commits are DB-only, reconstructed from DB fields rather than served as stored canonical bytes). This migration is the deliberately narrow, additive fix: capture the exact original string alongside the normalized timestamptz, so the serve path can use it verbatim instead of re-deriving a lossy one. Still correct after #63 eventually lands (canonical-bytes storage would make this column redundant, not wrong). Additive and nullable -- existing rows get NULL. The serve path (musehub/services/musehub_wire_shared.py::_to_wire_commit) falls back to `timestamp.isoformat()` for NULL rows and verifies the reconstructed bytes actually reproduce the stored commit_id, refusing to serve (fail closed) rather than silently serving a mismatched reconstruction if they don't. Revision ID: 0077 Revises: 0076 """ from __future__ import annotations from typing import Sequence, Union import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. revision: str = '0077' down_revision: Union[str, None] = '0076' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.add_column( 'musehub_commits', sa.Column('committed_at_raw', sa.Text(), nullable=True), ) def downgrade() -> None: op.drop_column('musehub_commits', 'committed_at_raw')