fix(ownership): update syncs when a same-date snapshot is rebuilt

A snapshot republished for the same as-of date (the TRUE ticker fix)
kept the date, latest release, and release count, so sync saw nothing
to do and existing installs kept the bad data without --force.

The manifest gains an optional snapshot.replaces_sqlite_sha256 listing
earlier builds for that date. Queries never write to a synced file, so
a local database byte-identical to a listed build is replaced; one that
matches nothing (local imports or edits) still needs --force. The
publisher fills the list on a same-date rebuild. Older clients ignore
the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 18:52:13 +00:00
commit ec17a1bd05
5 changed files with 191 additions and 2 deletions

View file

@ -396,6 +396,22 @@ if [[ "$old_asset" != "$new_asset" ]] && remote_asset_exists "$old_asset" && rem
else
fail "forced refresh keeps the previous content-addressed artifact (old=$old_asset new=$new_asset count=$(asset_count))"
fi
old_sha="${old_asset#ownership-snapshot-2026-08-31-}"
old_sha="${old_sha%.sqlite}"
if [[ "$(remote_manifest_field '.snapshot.replaces_sqlite_sha256 | join(",")')" == "$old_sha" ]]; then
pass "same-date rebuild lists the build it replaces"
else
fail "same-date rebuild lists the build it replaces (got $(remote_manifest_field '.snapshot.replaces_sqlite_sha256'))"
fi
mid_sha="$(remote_manifest_field '.snapshot.sqlite_sha256')"
export FAKE_BUILD_VARIANT="second-replacement"
run_publisher --force
unset FAKE_BUILD_VARIANT
if [[ "$(remote_manifest_field '.snapshot.replaces_sqlite_sha256 | join(",")')" == "$old_sha,$mid_sha" ]]; then
pass "a second rebuild carries earlier replaced builds forward"
else
fail "a second rebuild carries earlier replaced builds forward (got $(remote_manifest_field '.snapshot.replaces_sqlite_sha256'))"
fi
# ===========================================================================
# 5. a trustworthy-looking manifest with a missing asset forces a republish

View file

@ -461,6 +461,25 @@ cp "$STAGED_MANIFEST_PATH" "$OUTPUT_DIR/ownership-snapshot-manifest.json"
cp "$STAGED_SQLITE_PATH" "$OUTPUT_DIR/"
MANIFEST_PATH="$OUTPUT_DIR/ownership-snapshot-manifest.json"
# A rebuild for the published as-of date (e.g. after a parser fix) lists the
# builds it replaces, so clients still holding one update without --force.
if [[ "$BUILT_AS_OF" == "$PUBLISHED_AS_OF" && "$PUBLISHED_SHA256" =~ ^[0-9a-fA-F]{64}$ ]]; then
replaces_tmp="$PUBLISH_WORKDIR/manifest-replaces.json"
jq --arg published "$PUBLISHED_SHA256" \
--argjson previous "$(jq -c '.snapshot.replaces_sqlite_sha256 // []' <<< "$PUBLISHED_MANIFEST" 2>/dev/null || echo '[]')" '
.snapshot.sqlite_sha256 as $own
| .snapshot.replaces_sqlite_sha256 =
([$previous[], $published]
| map(ascii_downcase) | map(select(. != ($own | ascii_downcase)))
| reduce .[] as $sha ([]; if index([$sha]) then . else . + [$sha] end)
| .[-20:])
| if (.snapshot.replaces_sqlite_sha256 | length) == 0
then del(.snapshot.replaces_sqlite_sha256) else . end
' "$MANIFEST_PATH" > "$replaces_tmp"
mv "$replaces_tmp" "$MANIFEST_PATH"
fi
SQLITE_PATH="$OUTPUT_DIR/$(basename "$STAGED_SQLITE_PATH")"
SQLITE_ASSET_NAME="$(basename "$STAGED_SQLITE_PATH")"
LOCAL_SQLITE_BYTES="$(wc -c < "$SQLITE_PATH" | tr -d ' ')"