Feature 07: Releases, assets, stars, watches — internal/releases + internal/social #13

Closed
opened 2026-09-04 04:25:13 +00:00 by crueber · 4 comments
Owner

Feature 07: Releases, assets, stars, watches — internal/releases + internal/social

Spec: docs/features/07_releases_stars.md (normative) + P1/P5–P8 in docs/features/README.md.
Rollout: Wave D in docs/features/09_rollout.md §3. Depends on 01 (P6), 03 (merged-PR index for autodraft, fork counter), 06 (watch fan-out).

  • Two packages: internal/releases (releases + assets + autodraft) and internal/social (stars, watches, counters) — both api.RouteProvider (Seam 1, both lanes via api.Lanes). No task kinds (§9 — everything synchronous and bounded; name but don't build the release-asset-import Seam 5 hook).
  • Objects: releases/<tag.json> (tag percent-encoded one segment, NO lowercasing; CAS create-against-absent then CAS updates; tag/tag_sha immutable), releases/<tag.json>/assets/<name> (raw bytes, immutable Create), releases/latest.json (CAS'd pointer, monotonic created_at compare in-loop + bounded self-healing scan max 100, sync repair on delete), meta/social.json ({stars, watchers, forks} CAS'd, one canonical field-scoped loop), users/<principal>/{starred,watching}/<o>/<r>.json (Create/Delete only). CAS'd JSON joins the frozen overwritable list in the same change.
  • Tag rule: must resolve as refs/tags/<tag> at write (404 unknown revision); sha snapshotted; tag moves/deletes don't rewrite the release. Releases always bound to an existing tag (no tag-less drafts — draft hides from latest/list/notifications only).
  • Assets (§1.2, normative two-step): POST …/assets/{name} raw bytes + Content-Length + X-Walgit-Asset-Sha256 → spool-verify (sha256+size, cap releases.max_asset_bytes default 2 GiB → 413) → bytes Create (same-sha = idempotent success, clash = 409) → header CAS-append. Bytes-first always (orphans harmless, dangling entries impossible). Serving via static contract (strong ETag, Range, immutable CC) on GET|HEAD /{o}/{r}/releases/{tag}/assets/{name} registered in the static uncompressed group (14.3 routing note).
  • Autodraft (§3): sync, since defaults to latest tag; merged-PR source from 03's index; window test via merge-base --is-ancestor both sides under the per-repo git semaphore; ≤100 PRs, more flag.
  • Stars/watches (§§4–5): authenticated + repo-visible to star/watch; idempotent; unstar always allowed; counts via the shared social.json CAS loop; 03's fork completion CAS-increments forks (coordinate the call site with internal/pulls).
  • Publish fan-out (§3): after CAS, P8 notifications to watchers (read 06/07 watch records) + webhook enqueue + SSE release frame.
  • UI/SDK (§8): releases list/detail/new (tag picker, autodraft fill, client-side crypto.subtle hash upload), star/watch toggles with optimistic rollback, releases.js + social.js SDK. Dark + light themes.

Acceptance criteria

  • Endpoints in 07 §7 with P6 gates (create=write, delete=maintain); SWR+ETag on JSON GETs, static contract on bytes; plain-text errors.
  • Live proof: tag → release → asset upload/download (byte-identical, sha verified) → latest badge → delete repairs pointer.
  • make cover ≥ 95% on both packages; -race clean; concurrent asset-upload convergence test.
  • EVIDENCE.md entry: latest-pointer O(1) reads, autodraft git-subprocess bounds, asset streaming memory cap.
# Feature 07: Releases, assets, stars, watches — `internal/releases` + `internal/social` **Spec:** `docs/features/07_releases_stars.md` (normative) + P1/P5–P8 in `docs/features/README.md`. **Rollout:** Wave D in `docs/features/09_rollout.md` §3. Depends on 01 (P6), 03 (merged-PR index for autodraft, fork counter), 06 (watch fan-out). ## Recommended implementation (verified against the doc) - **Two packages:** `internal/releases` (releases + assets + autodraft) and `internal/social` (stars, watches, counters) — both `api.RouteProvider` (Seam 1, both lanes via `api.Lanes`). No task kinds (§9 — everything synchronous and bounded; name but don't build the `release-asset-import` Seam 5 hook). - **Objects:** `releases/<tag.json>` (tag percent-encoded one segment, NO lowercasing; CAS create-against-absent then CAS updates; tag/tag_sha immutable), `releases/<tag.json>/assets/<name>` (raw bytes, immutable Create), `releases/latest.json` (CAS'd pointer, monotonic `created_at` compare in-loop + bounded self-healing scan max 100, sync repair on delete), `meta/social.json` (`{stars, watchers, forks}` CAS'd, one canonical field-scoped loop), `users/<principal>/{starred,watching}/<o>/<r>.json` (Create/Delete only). CAS'd JSON joins the frozen overwritable list in the same change. - **Tag rule:** must resolve as `refs/tags/<tag>` at write (404 `unknown revision`); sha snapshotted; tag moves/deletes don't rewrite the release. Releases always bound to an existing tag (no tag-less drafts — draft hides from latest/list/notifications only). - **Assets (§1.2, normative two-step):** `POST …/assets/{name}` raw bytes + `Content-Length` + `X-Walgit-Asset-Sha256` → spool-verify (sha256+size, cap `releases.max_asset_bytes` default 2 GiB → 413) → bytes Create (same-sha = idempotent success, clash = 409) → header CAS-append. Bytes-first always (orphans harmless, dangling entries impossible). Serving via static contract (strong ETag, Range, immutable CC) on `GET|HEAD /{o}/{r}/releases/{tag}/assets/{name}` registered in the static uncompressed group (14.3 routing note). - **Autodraft (§3):** sync, `since` defaults to latest tag; merged-PR source from 03's index; window test via `merge-base --is-ancestor` both sides under the per-repo git semaphore; ≤100 PRs, `more` flag. - **Stars/watches (§§4–5):** authenticated + repo-visible to star/watch; idempotent; unstar always allowed; counts via the shared `social.json` CAS loop; 03's fork completion CAS-increments `forks` (coordinate the call site with internal/pulls). - **Publish fan-out (§3):** after CAS, P8 notifications to watchers (read 06/07 watch records) + webhook enqueue + SSE `release` frame. - **UI/SDK (§8):** releases list/detail/new (tag picker, autodraft fill, client-side `crypto.subtle` hash upload), star/watch toggles with optimistic rollback, `releases.js` + `social.js` SDK. Dark + light themes. ## Acceptance criteria - [ ] Endpoints in 07 §7 with P6 gates (create=write, delete=maintain); SWR+ETag on JSON GETs, static contract on bytes; plain-text errors. - [ ] Live proof: tag → release → asset upload/download (byte-identical, sha verified) → latest badge → delete repairs pointer. - [ ] `make cover` ≥ 95% on both packages; `-race` clean; concurrent asset-upload convergence test. - [ ] EVIDENCE.md entry: latest-pointer O(1) reads, autodraft git-subprocess bounds, asset streaming memory cap.
Author
Owner

Starting Feature 07 implementation (docs/features/07_releases_stars.md): internal/releases + internal/social from origin/main in an isolated worktree; dirty feat/issues worktree untouched. Plan: releases/CRUD+assets+latest-pointer+autodraft, social stars/watches/forks-counter, watch-route handover from internal/notify, repo-subpath bytes seam, UI+SDK, EVIDENCE entry, PR to follow.

Starting Feature 07 implementation (docs/features/07_releases_stars.md): internal/releases + internal/social from origin/main in an isolated worktree; dirty feat/issues worktree untouched. Plan: releases/CRUD+assets+latest-pointer+autodraft, social stars/watches/forks-counter, watch-route handover from internal/notify, repo-subpath bytes seam, UI+SDK, EVIDENCE entry, PR to follow.
Author
Owner

Feature 07 implementation ready for review: PR #18 (feat/releases → main) — internal/releases + internal/social, UI+SDK, EVIDENCE E8. Live proof on the compose rig (release/asset sha-verified/latest-repair/star/fork counters) plus releases 99.8% / social 99.7% coverage, -race, cover gate, e2e all green. Deviations from the 07 text are recorded in the doc's Decisions (watch routes stay in notify; shared-index autodraft; idempotent PUT; asset subtree layout). Do not merge per instructions.

Feature 07 implementation ready for review: PR #18 (feat/releases → main) — internal/releases + internal/social, UI+SDK, EVIDENCE E8. Live proof on the compose rig (release/asset sha-verified/latest-repair/star/fork counters) plus releases 99.8% / social 99.7% coverage, -race, cover gate, e2e all green. Deviations from the 07 text are recorded in the doc's Decisions (watch routes stay in notify; shared-index autodraft; idempotent PUT; asset subtree layout). Do not merge per instructions.
Author
Owner

PR #18 review (feat/releases, head d34c946 after fixes): 5 findings, all fixed and pushed to origin/feat/releases.

FINDINGS (file:line on d34c946, resolution):

  1. docs/go/14_extensibility.md:472 — Feature-07 amendment named the OLD asset layout releases/<tag.json>/assets/; code ships releases/assets//. FIXED: amendment corrected to the sibling subtree (+why: header path can't be file+dir on the filesystem backend). HTTP route was already unchanged; old layout fully gone (no references left); 07 doc table + code comments already matched.
  2. internal/releases/service.go:392 (repairLatestPointer) — read/delete repair overwrote the pointer unconditionally when the tag differed, NOT the section-2 monotonic CAS the spec promises; a truncated scan (>100 releases) could park the badge at an older release. FIXED: repair applies the same created_at compare as the publish path (shared pointerTargetCreated helper, :416); corrupt pointers still heal by overwrite. Verified by new TestRepairLatestPointerMonotonic (fails pre-fix, passes post-fix).
  3. internal/social/service.go:50 (Unstar) — concurrent double-unstar double-decremented: both saw the record, both unconditional Deletes succeeded (unconditional delete of an absent key returns nil on every backend — memory.go:164 confirmed), both bumped. FIXED: version-conditional Delete (records are Create/Delete-only, no ABA); loser recounts, no second bump. Verified by new TestUnstarConcurrentSingleDecrement with a deterministic overlap gate (fails pre-fix, passes post-fix, -race clean).
  4. internal/releases/http.go:550 (serveAsset) — Range was honored on HEAD (206 + wasted store GET); RFC 7233 3.1 defines range handling for GET only. FIXED: non-GET ignores Range (full 200 headers). Covered in TestAssetHTTPFlow.
  5. docs/features/07_releases_stars.md:300 — tags literally named latest/autodraft have their single-GET shadowed by the pointer/autodraft routes (forced by the section-7 route table itself; create/delete/list unaffected). Was code-comment-only. FIXED: recorded in Decisions.

VERIFIED CLEAN (no change needed): import budget (stdlib+internal only in both packages); seam direction (core/pulls never import releases/social; ForksCounter consumed as interface, wired in cmd/walhub/social.go); bytes-before-header with streaming spool + dual cap checks (declared + LimitReader) and failure-path-only verification; tag resolve-at-write with peeled rev-parse argv + snapshot semantics; asset name validation; P6 gates (write/maintain/read + 401-with-Bearer); watch mutation single-owned by notify with dual field-scoped CAS loops (notify watch.go preserves stars/forks; social preserves watcher_list — TestSocialPreservesWatcherList); fork exactly-once (Create-arbitrated target + (repo,kind) single-flight + single IncForks site; retries hit 412-conflict, never re-increment); PUT-upsert-vs-409 deviation recorded with rationale (retry-safe publishes; store-level absent-CAS still converges concurrent creators; 409 kept for sha clash + stale If-Match); cache classes (SWR on JSON GETs, immutable on bytes, no-store on starred twins); byte route via server.RepoRoutes consulted in repoDispatch (router.go:278,291) with no compress group wrapping it (verified mount at router.go:128 bare); dark+light via shared themed ui.css classes (+ new chip-draft/chip-prerelease/btn variants); E8 numbers check out against the code paths (publish 3G+2P, star 2G+2P, upload 2G+2P all confirmed in code); config releases.max_asset_bytes rides the reflective setup schema (ByteSize already handled); no task kinds registered.

FINAL RESULTS on d34c946: gofmt clean; go vet clean (releases/social/pulls/notify/server); go test -race green on all four (releases, social, pulls, notify); coverage 99.8% releases / 99.7% social (gate >=95%). Main worktree untouched (all work in /tmp/pr18fix, now on branch review/pr18-fixes, pushed as feat/releases).

MERGE RECOMMENDATION: ready to merge.

PR #18 review (feat/releases, head d34c946 after fixes): 5 findings, all fixed and pushed to origin/feat/releases. FINDINGS (file:line on d34c946, resolution): 1. docs/go/14_extensibility.md:472 — Feature-07 amendment named the OLD asset layout releases/<tag.json>/assets/<name>; code ships releases/assets/<tag>/<name>. FIXED: amendment corrected to the sibling subtree (+why: header path can't be file+dir on the filesystem backend). HTTP route was already unchanged; old layout fully gone (no references left); 07 doc table + code comments already matched. 2. internal/releases/service.go:392 (repairLatestPointer) — read/delete repair overwrote the pointer unconditionally when the tag differed, NOT the section-2 monotonic CAS the spec promises; a truncated scan (>100 releases) could park the badge at an older release. FIXED: repair applies the same created_at compare as the publish path (shared pointerTargetCreated helper, :416); corrupt pointers still heal by overwrite. Verified by new TestRepairLatestPointerMonotonic (fails pre-fix, passes post-fix). 3. internal/social/service.go:50 (Unstar) — concurrent double-unstar double-decremented: both saw the record, both unconditional Deletes succeeded (unconditional delete of an absent key returns nil on every backend — memory.go:164 confirmed), both bumped. FIXED: version-conditional Delete (records are Create/Delete-only, no ABA); loser recounts, no second bump. Verified by new TestUnstarConcurrentSingleDecrement with a deterministic overlap gate (fails pre-fix, passes post-fix, -race clean). 4. internal/releases/http.go:550 (serveAsset) — Range was honored on HEAD (206 + wasted store GET); RFC 7233 3.1 defines range handling for GET only. FIXED: non-GET ignores Range (full 200 headers). Covered in TestAssetHTTPFlow. 5. docs/features/07_releases_stars.md:300 — tags literally named latest/autodraft have their single-GET shadowed by the pointer/autodraft routes (forced by the section-7 route table itself; create/delete/list unaffected). Was code-comment-only. FIXED: recorded in Decisions. VERIFIED CLEAN (no change needed): import budget (stdlib+internal only in both packages); seam direction (core/pulls never import releases/social; ForksCounter consumed as interface, wired in cmd/walhub/social.go); bytes-before-header with streaming spool + dual cap checks (declared + LimitReader) and failure-path-only verification; tag resolve-at-write with peeled rev-parse argv + snapshot semantics; asset name validation; P6 gates (write/maintain/read + 401-with-Bearer); watch mutation single-owned by notify with dual field-scoped CAS loops (notify watch.go preserves stars/forks; social preserves watcher_list — TestSocialPreservesWatcherList); fork exactly-once (Create-arbitrated target + (repo,kind) single-flight + single IncForks site; retries hit 412-conflict, never re-increment); PUT-upsert-vs-409 deviation recorded with rationale (retry-safe publishes; store-level absent-CAS still converges concurrent creators; 409 kept for sha clash + stale If-Match); cache classes (SWR on JSON GETs, immutable on bytes, no-store on starred twins); byte route via server.RepoRoutes consulted in repoDispatch (router.go:278,291) with no compress group wrapping it (verified mount at router.go:128 bare); dark+light via shared themed ui.css classes (+ new chip-draft/chip-prerelease/btn variants); E8 numbers check out against the code paths (publish 3G+2P, star 2G+2P, upload 2G+2P all confirmed in code); config releases.max_asset_bytes rides the reflective setup schema (ByteSize already handled); no task kinds registered. FINAL RESULTS on d34c946: gofmt clean; go vet clean (releases/social/pulls/notify/server); go test -race green on all four (releases, social, pulls, notify); coverage 99.8% releases / 99.7% social (gate >=95%). Main worktree untouched (all work in /tmp/pr18fix, now on branch review/pr18-fixes, pushed as feat/releases). MERGE RECOMMENDATION: ready to merge.
Author
Owner

Feature 07 complete: PR #18 reviewed (5 findings fixed: doc layout, monotonic pointer repair, unstar double-decrement, HEAD range, route shadowing note), merged. releases 99.8% / social 99.7%, -race clean. Closing.

Feature 07 complete: PR #18 reviewed (5 findings fixed: doc layout, monotonic pointer repair, unstar double-decrement, HEAD range, route shadowing note), merged. releases 99.8% / social 99.7%, -race clean. Closing.
crueber added this to the v1 milestone 2026-09-10 22:20:51 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#13
No description provided.