NEW LAW + systemic guard: cache-class is decided by MUTABILITY, not addressability — shared policy definition, contract test, and an AGENTS.md law so stale-serve flip-flops (#259/#280/#381) cannot recur #382

Closed
opened 2026-09-12 15:26:25 +00:00 by crueber · 3 comments
Owner

Supersedes #259 (same root cause, wider blast radius, permanent fix proposed). #259 scoped this to the issue view; the same stale-serve behavior exists on stars/watches (/{o}/{r}/api/social), releases, pulls, and identity surfaces. Closing #259 in favor of this one is appropriate.

The systemic problem

Mutating collaborative state — reactions, stars, watches, release edits, issue patches — then refreshing can serve the pre-mutation state, and the next refresh shows the post-mutation state. It is intermittent by construction and affects every surface served with the SWR cache class:

Surface ETag basis File
Issue/PR thread (reactions, comments, patches) v<Thread.Version> internal/issues/http.go:459
Social (stars, watches, forks) store version token internal/social/http.go:248
Release single + list store version internal/releases/http.go:355,371,380
Pull view HeadLive internal/pulls/http.go:493
Identity (profile, orgs, teams, invites) doc version internal/identity/http.go (7 sites)

All use ccSWR = "private, max-age=0, stale-while-revalidate=60" (api/env.go:582, duplicated per package in issues/http.go:175, social/http.go:185).

Mechanics of the flip-flop: stale-while-revalidate=60 permits the browser to answer a refresh from its cache — the stale pre-mutation body — while revalidating in the background. Refresh 1 paints old state (reaction "gone", star "off"); the background revalidation lands; refresh 2 paints new state. Server state is correct at all times (every mutation is a versioned CAS put and the ETag is version-keyed) — the header contract is what licenses the staleness.

Why the client makes it worse: the SPA's useData layer (web/src/lib/data.js) has its own TTL revalidation (5–30 s by surface), and mutations invalidate only the mutation's own cache entry — a hard refresh re-requests through the browser cache, where SWR serves stale before the client TTL logic even runs.

Where the design went wrong

The repo's own spec drew the line in the right place and the implementation crossed it:

  • docs/go/07_api.md §4 ("the central design rule") defines exactly two cache classes, both for git content: sha-addressed → immutable; ref-dependent (name-addressed git views) → SWR-60 + sha ETag. The rationale is sound: git content changes only via refs, so a ref-state ETag makes SWR safe.
  • Collaborative state is not ref-dependent — threads, social counters, releases, and identity docs mutate independently of any ref move. Their specs then borrowed the class anyway: docs/features/07_releases_stars.md:235 explicitly says "single release / list / social GETs are ref-dependent class (SWR 60 s + ETag version token)" and docs/features/02_issues.md:258 says "GET-by-num is ref-class SWR keyed on the header version." That was the wrong call: a version-keyed ETag gives correct revalidation, but SWR's stale-serve window is a correctness concession that git content can afford (refs move rarely, stale ≈ seconds old) and mutable collab state cannot (the user just clicked the button).

Proposed permanent solution

Principle: the SWR stale-serve window is for content whose staleness is bounded by ref movement, not for user-mutable state. Any GET whose resource can change via a direct user action must not serve stale.

  1. Introduce a third cache class — ccNoCache = "private, no-cache" — for mutation-sensitive GETs: Cache-Control: private, no-cache + the existing version ETag. no-cache requires revalidation before every use (the browser still caches; the ETag still makes unchanged responses 304 with zero body), so the economics stay cheap — you lose only the stale-serve window, which is exactly the bug. This is a one-line class + per-site swap; writeCached already implements the If-None-Match → 304 path.
  2. Apply it to every version-keyed collab GET: issues/PRs thread + events, social, releases single/list/latest, pulls view, identity profile/org/team/invite surfaces (the 20 call sites in the table above). Leave git-content routes (summary/refs/resolve/tree/blob/commits) on SWR per §4 — those are correct as spec'd.
  3. Fix the spec where it encodes the wrong call: 07_releases_stars.md:235 and 02_issues.md:258 change "ref-dependent class (SWR 60 s)" to the new class; add a sentence to 07_api.md §4 defining the third class and the rule for when it applies (mutability, not addressability, decides). Note 07_api.md §4 currently says "the two cache classes (the central design rule)" — this amendment touches a stated design rule, which is why this issue is flagged for the maintainer rather than just implemented.
  4. Client-side belt-and-suspenders: on any successful mutation, invalidate() the affected useData entries and force a browser revalidation for the mutated resource (the SDK mutation helpers can issue the follow-up GET with If-None-Match stripped — a conditional GET the browser must forward under no-cache). This covers same-view updates without a reload; the header contract covers the reload case.
  5. Audit for stragglers: a contract test asserting that any handler whose ETag is derived from a store/doc version (not a git sha) serves no-cache, not SWR — the same test proposed in #272's registry work would be a natural home.

Laws impact (decision requested)

  • Not a dependency-law issue (no new modules).
  • It amends the §9.2/§4 "two cache classes" design rule in 07_api.md — adding a third class for mutable collab state. The rule's intent ("SWR is safe because staleness is bounded by ref movement") is preserved; the amendment narrows where the class applies. Per repo law 12 (existing keys never change meaning), git-content routes keep byte-identical headers; only collab GETs change.
  • Alternative if the two-class rule is sacred: drop SWR to max-age=0 only (no stale window) for collab GETs — same user-visible fix, keeps the class count at two, but loses the ETag-mandated revalidation semantics clarity of no-cache. Both are one-line header changes; the difference is documentation framing.

Acceptance criteria

  • Add reaction / star / watch / release-edit → hard refresh → post-mutation state on the first refresh, every time (the flip-flop is gone).
  • Unchanged collab resources still revalidate to 304 (no bandwidth regression; verify If-None-Match handling on the new class).
  • Git-content routes (summary, refs, resolve, tree, blob, commits by name) still serve SWR-60 unchanged (byte-identical headers).
  • 07_api.md §4 and the two feature specs document the class assignment rule; the per-package duplicated ccSWR constants are consolidated or reference the shared definition.
  • Mutation helpers invalidate + revalidate the client cache so same-view UI updates without reload (existing behavior preserved, now backed by the header contract).
  • Contract test: version-ETag GETs serve no-cache; sha-ETag GETs serve SWR/immutable as spec'd.

AMENDMENT (2026-09-12, after #381): the systemic scope widens — this is now the authoritative fix ticket

#381 reproduced the identical flip-flop on the repo summary — the one surface this ticket's original scope did not include — proving the per-endpoint fix approach cannot close the class. The summary was left on ccSWR because its ETag covers the mutable fields (~d/~m/~c/~v suffixes all present at internal/api/summary.go:181-187), which fixes revalidation but not stale-serve: SWR permits the browser to paint the pre-mutation body before revalidation runs, whatever the ETag says.

The rule that actually holds (and the amendment to the spec)

SWR's stale-serve window is only safe for content whose changes are bounded by ref movement (git content). Any endpoint whose ETag is derived from a version/counter of user-mutable state must never serve stale — regardless of ETag quality. The ETag fixes revalidation; only the cache class fixes stale-serve. This amends the §4 "two cache classes" framing in docs/go/07_api.md as planned above, and adds the decision rule: addressability does not decide the class — mutability does.

Systemic changes required (elevated from "fix direction" to binding)

  1. One cache-policy source. Consolidate the per-package ccSWR/ccNoStore/ccMutable constants (issues, social, releases, pulls, identity each redeclare them; api has its own) into one shared definition that carries the rule in its documentation. New packages import it; nothing redeclares.
  2. Contract test = the permanent guard. A test that enumerates every served route (the #272 route registry work provides the enumeration) and asserts: version-derived ETag ⇒ Cache-Control: private, no-cache; sha-derived ETag ⇒ immutable/SWR per §4. This fails CI the moment a new feature package copies ccSWR onto mutable state — which is precisely how #259 → #280 → #381 recurred.
  3. Immediate call-site sweep: summary (internal/api/summary.go:187 — #381), plus re-audit every remaining ccSWR site against the mutability rule; git-content routes stay byte-identical.
  4. ETag simplification on converted routes: once mutable endpoints are no-cache, the accumulating ETag suffix hacks (~d/~m/~c/~v) become unnecessary there — a single version-based ETag suffices since the browser always revalidates. Optional cleanup, not required for correctness.

Updated acceptance criteria (superset of the originals)

  • Summary route converted (fixes #381's bounce) and verified per #381's criteria.
  • Shared cache-policy definition exists; per-package redeclarations removed or aliased to it.
  • Contract test enumerates served routes and enforces the class rule (verified by deliberately breaking one route).
  • docs/go/07_api.md §4 documents the three classes and the mutability rule; DEVIATIONS.md records the amendment.
**Supersedes #259** (same root cause, wider blast radius, permanent fix proposed). #259 scoped this to the issue view; the same stale-serve behavior exists on stars/watches (`/{o}/{r}/api/social`), releases, pulls, and identity surfaces. Closing #259 in favor of this one is appropriate. ## The systemic problem Mutating collaborative state — reactions, stars, watches, release edits, issue patches — then refreshing can serve the **pre-mutation state**, and the next refresh shows the post-mutation state. It is intermittent by construction and affects every surface served with the SWR cache class: | Surface | ETag basis | File | |---|---|---| | Issue/PR thread (reactions, comments, patches) | `v<Thread.Version>` | `internal/issues/http.go:459` | | Social (stars, watches, forks) | store version token | `internal/social/http.go:248` | | Release single + list | store version | `internal/releases/http.go:355,371,380` | | Pull view | `HeadLive` | `internal/pulls/http.go:493` | | Identity (profile, orgs, teams, invites) | doc version | `internal/identity/http.go` (7 sites) | All use `ccSWR = "private, max-age=0, stale-while-revalidate=60"` (`api/env.go:582`, duplicated per package in `issues/http.go:175`, `social/http.go:185`). **Mechanics of the flip-flop:** `stale-while-revalidate=60` permits the browser to answer a refresh from its cache — the stale pre-mutation body — while revalidating in the background. Refresh 1 paints old state (reaction "gone", star "off"); the background revalidation lands; refresh 2 paints new state. Server state is correct at all times (every mutation is a versioned CAS put and the ETag *is* version-keyed) — the header contract is what licenses the staleness. **Why the client makes it worse:** the SPA's `useData` layer (`web/src/lib/data.js`) has its own TTL revalidation (5–30 s by surface), and mutations invalidate only the mutation's own cache entry — a hard refresh re-requests through the browser cache, where SWR serves stale *before* the client TTL logic even runs. ## Where the design went wrong The repo's own spec drew the line in the right place and the implementation crossed it: - `docs/go/07_api.md` §4 ("the central design rule") defines exactly **two cache classes**, both for **git content**: sha-addressed → immutable; **ref-dependent** (name-addressed git views) → SWR-60 + sha ETag. The rationale is sound: git content changes only via refs, so a ref-state ETag makes SWR safe. - Collaborative state is *not* ref-dependent — threads, social counters, releases, and identity docs mutate independently of any ref move. Their specs then borrowed the class anyway: `docs/features/07_releases_stars.md:235` explicitly says "single release / list / social GETs are ref-dependent class (SWR 60 s + ETag version token)" and `docs/features/02_issues.md:258` says "GET-by-num is ref-class SWR keyed on the header version." That was the wrong call: a version-keyed ETag gives correct **revalidation**, but SWR's stale-serve window is a **correctness** concession that git content can afford (refs move rarely, stale ≈ seconds old) and mutable collab state cannot (the user just clicked the button). ## Proposed permanent solution **Principle: the SWR stale-serve window is for content whose staleness is bounded by ref movement, not for user-mutable state. Any GET whose resource can change via a direct user action must not serve stale.** 1. **Introduce a third cache class** — `ccNoCache = "private, no-cache"` — for mutation-sensitive GETs: `Cache-Control: private, no-cache` + the existing version ETag. `no-cache` *requires* revalidation before every use (the browser still caches; the ETag still makes unchanged responses 304 with zero body), so the economics stay cheap — you lose only the stale-serve window, which is exactly the bug. This is a one-line class + per-site swap; `writeCached` already implements the If-None-Match → 304 path. 2. **Apply it to every version-keyed collab GET**: issues/PRs thread + events, social, releases single/list/latest, pulls view, identity profile/org/team/invite surfaces (the 20 call sites in the table above). Leave git-content routes (summary/refs/resolve/tree/blob/commits) on SWR per §4 — those are correct as spec'd. 3. **Fix the spec where it encodes the wrong call**: `07_releases_stars.md:235` and `02_issues.md:258` change "ref-dependent class (SWR 60 s)" to the new class; add a sentence to `07_api.md` §4 defining the third class and the rule for when it applies (mutability, not addressability, decides). Note `07_api.md` §4 currently says "the two cache classes (the central design rule)" — this amendment touches a stated design rule, which is why this issue is flagged for the maintainer rather than just implemented. 4. **Client-side belt-and-suspenders:** on any successful mutation, `invalidate()` the affected `useData` entries **and** force a browser revalidation for the mutated resource (the SDK mutation helpers can issue the follow-up GET with `If-None-Match` stripped — a conditional GET the browser must forward under `no-cache`). This covers same-view updates without a reload; the header contract covers the reload case. 5. **Audit for stragglers:** a contract test asserting that any handler whose ETag is derived from a store/doc version (not a git sha) serves `no-cache`, not SWR — the same test proposed in #272's registry work would be a natural home. ## Laws impact (decision requested) - Not a dependency-law issue (no new modules). - It **amends the §9.2/§4 "two cache classes" design rule** in `07_api.md` — adding a third class for mutable collab state. The rule's intent ("SWR is safe because staleness is bounded by ref movement") is preserved; the amendment narrows where the class applies. Per repo law 12 (existing keys never change meaning), git-content routes keep byte-identical headers; only collab GETs change. - Alternative if the two-class rule is sacred: drop SWR to `max-age=0` only (no stale window) for collab GETs — same user-visible fix, keeps the class count at two, but loses the ETag-mandated revalidation semantics clarity of `no-cache`. Both are one-line header changes; the difference is documentation framing. ## Acceptance criteria - [ ] Add reaction / star / watch / release-edit → hard refresh → post-mutation state on the **first** refresh, every time (the flip-flop is gone). - [ ] Unchanged collab resources still revalidate to 304 (no bandwidth regression; verify `If-None-Match` handling on the new class). - [ ] Git-content routes (summary, refs, resolve, tree, blob, commits by name) still serve SWR-60 unchanged (byte-identical headers). - [ ] `07_api.md` §4 and the two feature specs document the class assignment rule; the per-package duplicated `ccSWR` constants are consolidated or reference the shared definition. - [ ] Mutation helpers invalidate + revalidate the client cache so same-view UI updates without reload (existing behavior preserved, now backed by the header contract). - [ ] Contract test: version-ETag GETs serve `no-cache`; sha-ETag GETs serve SWR/immutable as spec'd. ## AMENDMENT (2026-09-12, after #381): the systemic scope widens — this is now the authoritative fix ticket #381 reproduced the identical flip-flop on the **repo summary** — the one surface this ticket's original scope did not include — proving the per-endpoint fix approach cannot close the class. The summary was left on `ccSWR` because its ETag covers the mutable fields (`~d`/`~m`/`~c`/`~v` suffixes all present at `internal/api/summary.go:181-187`), which fixes *revalidation* but not *stale-serve*: SWR permits the browser to paint the pre-mutation body before revalidation runs, whatever the ETag says. ### The rule that actually holds (and the amendment to the spec) **SWR's stale-serve window is only safe for content whose changes are bounded by ref movement (git content). Any endpoint whose ETag is derived from a version/counter of user-mutable state must never serve stale — regardless of ETag quality.** The ETag fixes revalidation; only the cache class fixes stale-serve. This amends the §4 "two cache classes" framing in `docs/go/07_api.md` as planned above, and adds the decision rule: *addressability does not decide the class — mutability does.* ### Systemic changes required (elevated from "fix direction" to binding) 1. **One cache-policy source.** Consolidate the per-package `ccSWR`/`ccNoStore`/`ccMutable` constants (issues, social, releases, pulls, identity each redeclare them; api has its own) into one shared definition that carries the rule in its documentation. New packages import it; nothing redeclares. 2. **Contract test = the permanent guard.** A test that enumerates every served route (the #272 route registry work provides the enumeration) and asserts: version-derived ETag ⇒ `Cache-Control: private, no-cache`; sha-derived ETag ⇒ immutable/SWR per §4. This fails CI the moment a new feature package copies `ccSWR` onto mutable state — which is precisely how #259 → #280 → #381 recurred. 3. **Immediate call-site sweep:** summary (`internal/api/summary.go:187` — #381), plus re-audit every remaining `ccSWR` site against the mutability rule; git-content routes stay byte-identical. 4. **ETag simplification on converted routes:** once mutable endpoints are `no-cache`, the accumulating ETag suffix hacks (`~d`/`~m`/`~c`/`~v`) become unnecessary there — a single version-based ETag suffices since the browser always revalidates. Optional cleanup, not required for correctness. ### Updated acceptance criteria (superset of the originals) - [ ] Summary route converted (fixes #381's bounce) and verified per #381's criteria. - [ ] Shared cache-policy definition exists; per-package redeclarations removed or aliased to it. - [ ] Contract test enumerates served routes and enforces the class rule (verified by deliberately breaking one route). - [ ] `docs/go/07_api.md` §4 documents the three classes and the mutability rule; `DEVIATIONS.md` records the amendment.
crueber added this to the v1 milestone 2026-09-12 15:26:25 +00:00
Author
Owner

Systemic fix up in PR #389 (#389), branch fix/issue-382, no conflicts with main. Shared internal/cachepolicy definition, per-package cache-class contract tests with ExposedTemplates coverage (break-verified red-then-green), re-audit sweep (only SWR outside git content: pull diff + unversioned listings, boundary-documented), 07_api section 4 law + D-API-3. ETag-suffix simplification deferred per the amendment. Not merging — awaiting review.

Systemic fix up in PR #389 (https://git.packden.us/crueber/walhub/pulls/389), branch fix/issue-382, no conflicts with main. Shared internal/cachepolicy definition, per-package cache-class contract tests with ExposedTemplates coverage (break-verified red-then-green), re-audit sweep (only SWR outside git content: pull diff + unversioned listings, boundary-documented), 07_api section 4 law + D-API-3. ETag-suffix simplification deferred per the amendment. Not merging — awaiting review.
Author
Owner

REVIEW PR #389 (fix/issue-382) — verified in scratch worktree, all checks re-run. No browser (per instructions; header-contract change, covered by httptest + reasoning).

(1) Shared definition SOUND — internal/cachepolicy/cachepolicy.go: five values byte-exact (Immutable/SWR/Mutable/NoStore/NoCache), mutability rule in package doc. Check logic correct: SWR+version-ETag fails (table pins v12, store-version, ~suffix, content-hash, folded-stamp, non-hex-40 all red), SWR+sha40/sha64-weak/bare-sha/empty passes, all non-SWR classes pass unconditionally. isSHA lowercase-only is fail-safe direction. HeaderValuesExact test pins wire strings.

(2) Placement CLEAN — cachepolicy imports stdlib only (errors, strings; confirmed via go list -deps). Direction: api/issues/social/releases/pulls/identity import the leaf; nothing imports upward. No 14_extensibility surface touched.

(3) Aliasing — FIXED 3 stragglers, pushed as 0690b48: internal/issues/attachments.go:450 literal private-immutable == Immutable (now aliased), internal/api/sse.go:43,253 literal no-store x2 (now cachepolicy.NoStore). Remaining literals are distinct public-immutable variants (identity/http.go:359,580 avatar 86400; releases/http.go:566 asset bytes; server/static.go, health.go) — different values, immutable (never stale), out of the five-value scope; doc claim now true for all five shared values. server/auth.go:914 private,max-age=300 is a pre-existing auth-check value in an untouched package.

(4) Contract tests — all 6 pin exact class + ETag presence per row + Check per pair. ExposedTemplates coverage loop present in all 5 feature packages (mutation-only sets look right: issues POST/DELETE-only, pulls POST/DELETE-only, identity team-member/invite/transfer, releases asset-template carve-out with separate immutable byte-row, social star PUT/DELETE). internal/api has no ExposedTemplates registry (only ExposedTemplatesCreate), so its fixed 15-row table is the best available enumeration — acceptable. Break-verification RE-RUN myself: summary ccMutable->ccSWR goes RED (exact-class pin fires), revert goes green.

(5) Re-audit boundary — pull diff SWR+no-ETag: genuinely ref-derived (patch body moves only on head/base push; comments/patches hit the Mutable view), Check passes via empty-ETag arm. Listings (owners/ownerRepos/owners-detailed) SWR+no-ETag: CONFIRMED different from #384's repos_detailed (per-owner visibility projections -> Mutable) vs owners/detailed (global counts+activity rollups, internal/api/owners_activity.go:142, no version token, no sibling under another class). One honest caveat for maintainer: the strict issue wording ('any GET whose resource can change via user action must never serve stale') would also cover these listings (create-repo -> stale owners list <=60s); the PR's 'no mutable projections / no sibling' rationale is a deliberate, documented tolerance, consistent with merged #384. Non-blocking, but the exception belongs to the maintainer to bless.

(6) Git-content routes byte-identical — all http.go/env.go diffs are const-alias + import + comment only (verified: no logic lines changed). Summary change is Mutable-for-Mutable (was already no-cache since #280).

(7) ETag-suffix deferral noted in 07_api §4 + Decisions (e). (8) Law 6: no store-trip changes (const swaps only).

TESTS (scratch worktree, post-fix commit): -race green all 7 pkgs; coverage cachepolicy 100.0, api 95.3, issues 96.3, social 99.5, releases 99.8, pulls 97.7, identity 95.6 (all >=95 gate); gofmt/vet clean; go build ./... clean.

DOCS: 07_api §3 summary line fixed (was stale SWR text post-#381), §4 law + shared-def + listings-boundary + suffix bullets, Decisions entry, D-API-3, 01_overview row. Nit (non-blocking): §4 heading still says 'three cache classes' while defining five values — pre-existing #280 framing, understandable (no-store/no-cache are non-cache classes), but a one-line heading touch-up would remove the ambiguity.

MERGE RECOMMENDATION: ready to merge (boundary caveat in (5) is maintainer-judgment, documented in the PR body and §4; my pushed commit 0690b48 needs no re-review beyond the 6 aliased lines).

REVIEW PR #389 (fix/issue-382) — verified in scratch worktree, all checks re-run. No browser (per instructions; header-contract change, covered by httptest + reasoning). (1) Shared definition SOUND — internal/cachepolicy/cachepolicy.go: five values byte-exact (Immutable/SWR/Mutable/NoStore/NoCache), mutability rule in package doc. Check logic correct: SWR+version-ETag fails (table pins v12, store-version, ~suffix, content-hash, folded-stamp, non-hex-40 all red), SWR+sha40/sha64-weak/bare-sha/empty passes, all non-SWR classes pass unconditionally. isSHA lowercase-only is fail-safe direction. HeaderValuesExact test pins wire strings. (2) Placement CLEAN — cachepolicy imports stdlib only (errors, strings; confirmed via go list -deps). Direction: api/issues/social/releases/pulls/identity import the leaf; nothing imports upward. No 14_extensibility surface touched. (3) Aliasing — FIXED 3 stragglers, pushed as 0690b48: internal/issues/attachments.go:450 literal private-immutable == Immutable (now aliased), internal/api/sse.go:43,253 literal no-store x2 (now cachepolicy.NoStore). Remaining literals are distinct public-immutable variants (identity/http.go:359,580 avatar 86400; releases/http.go:566 asset bytes; server/static.go, health.go) — different values, immutable (never stale), out of the five-value scope; doc claim now true for all five shared values. server/auth.go:914 private,max-age=300 is a pre-existing auth-check value in an untouched package. (4) Contract tests — all 6 pin exact class + ETag presence per row + Check per pair. ExposedTemplates coverage loop present in all 5 feature packages (mutation-only sets look right: issues POST/DELETE-only, pulls POST/DELETE-only, identity team-member/invite/transfer, releases asset-template carve-out with separate immutable byte-row, social star PUT/DELETE). internal/api has no ExposedTemplates registry (only ExposedTemplatesCreate), so its fixed 15-row table is the best available enumeration — acceptable. Break-verification RE-RUN myself: summary ccMutable->ccSWR goes RED (exact-class pin fires), revert goes green. (5) Re-audit boundary — pull diff SWR+no-ETag: genuinely ref-derived (patch body moves only on head/base push; comments/patches hit the Mutable view), Check passes via empty-ETag arm. Listings (owners/ownerRepos/owners-detailed) SWR+no-ETag: CONFIRMED different from #384's repos_detailed (per-owner visibility projections -> Mutable) vs owners/detailed (global counts+activity rollups, internal/api/owners_activity.go:142, no version token, no sibling under another class). One honest caveat for maintainer: the strict issue wording ('any GET whose resource can change via user action must never serve stale') would also cover these listings (create-repo -> stale owners list <=60s); the PR's 'no mutable projections / no sibling' rationale is a deliberate, documented tolerance, consistent with merged #384. Non-blocking, but the exception belongs to the maintainer to bless. (6) Git-content routes byte-identical — all http.go/env.go diffs are const-alias + import + comment only (verified: no logic lines changed). Summary change is Mutable-for-Mutable (was already no-cache since #280). (7) ETag-suffix deferral noted in 07_api §4 + Decisions (e). (8) Law 6: no store-trip changes (const swaps only). TESTS (scratch worktree, post-fix commit): -race green all 7 pkgs; coverage cachepolicy 100.0, api 95.3, issues 96.3, social 99.5, releases 99.8, pulls 97.7, identity 95.6 (all >=95 gate); gofmt/vet clean; go build ./... clean. DOCS: 07_api §3 summary line fixed (was stale SWR text post-#381), §4 law + shared-def + listings-boundary + suffix bullets, Decisions entry, D-API-3, 01_overview row. Nit (non-blocking): §4 heading still says 'three cache classes' while defining five values — pre-existing #280 framing, understandable (no-store/no-cache are non-cache classes), but a one-line heading touch-up would remove the ambiguity. MERGE RECOMMENDATION: ready to merge (boundary caveat in (5) is maintainer-judgment, documented in the PR body and §4; my pushed commit 0690b48 needs no re-review beyond the 6 aliased lines).
Author
Owner

Fixed by PR #389 (review clean + 3 literal stragglers fixed by reviewer; guard break-verified, boundaries ruled, all gates green), merged. Closing.

Fixed by PR #389 (review clean + 3 literal stragglers fixed by reviewer; guard break-verified, boundaries ruled, all gates green), merged. Closing.
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#382
No description provided.