feat(entitlement): has_all_breakdown + endpoint — per-axis blocking breakdown - #5134
Conversation
…reakdown Boolean-fold twin of min_tier_for_all_breakdown. Where the reverse-lookup breakdown identifies which axis is *binding* the aggregate min-tier floor, this identifies which axis (or axes) is *blocking* the LIVE aggregate grant — so a paywall diagnostics tile can render "denied here BECAUSE of channels (you asked for 8)" off ONE round-trip alongside "cheapest tier that would grant it is Pro BECAUSE of channels" from the reverse-lookup companion. Closes the boolean-fold breakdown gap: has_all folds five axes to ONE bool, min_tier_for_all_breakdown surfaces the per-axis binding story for the reverse-lookup family, but no per-axis blocking story existed for has_all. A UI wanting the pair had to fan out five singular has_* scalars plus a client-side which-axis-is-false walk. Ships in GRACE: while ent.grace is True every fully-known bundle folds to has_all=True with empty blocking_axes (matches has_all byte-for-byte), so wiring this into a paywall diagnostics tile today changes NO current behaviour. Unknown grant tokens, empty grant axes, and non-int capacity axes still collapse the fold to False even in grace (matches the singular scalars' strict-False typo posture), and the corresponding axis is surfaced in blocking_axes with the typo detail (unknown[] on grant axes / raw value on capacity axes) so a tooltip can flag it. Endpoint: GET /api/entitlement/has-all-breakdown?features=…&runtimes=… &channels=N&retention_days=K&nodes=M — mirrors the required-tier- breakdown 400/200 posture, never 5xxs (OSS-fallback envelope on resolver failure), preserves bad capacity input in axis rows instead of dropping it (so the tooltip can flag typos). Tests: tests/test_entitlement_has_all_breakdown.py (35 tests) — envelope shape, has_all-parity across the five axes, blocking-axis identification (single-axis, multi-axis-ordered, empty-on-True, empty-on-nothing-supplied), per-axis row shape (kind/supplied/has/blocking + items+unknown for grants, value for capacities), grace-still-denies posture on unknowns / empties / non-int, axes-echo parity with min_tier_for_all_breakdown for pair-invariant paywall rendering, never-raise contract, full HTTP envelope + 400/200 matrix. - 35/35 passing - Broader entitlement suite: 10967 passed, no regressions - ruff, py39-annotation guard, and AC ratchet all green Draft PR — merge, [RELEASE] tag, and cloud rollout are the operator's. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual diffComparing 22 of 66 comparison(s) flagged (>1% pixel diff).
Folder: 08b89d7c39da. Full PNGs also attached as a workflow artefact. Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem. |
|
Stale E2E Gate cancellation — infra, not a code problem CI on The one red mark is Effect: the sole context branch protection names is stale-cancelled, so this PR can't merge as-is until the gate re-runs green — but the actual tests pass, so a re-run alone should clear it. Autonomous fire cannot re-trigger the workflow itself (403 Generated by Claude Code |
|
E2E Gate cancelled again on the merge-main head — same 18-min pattern, not a one-off Updating my earlier note. The gate was cancelled a second time on
Identical duration to the second, on two different head shas. All the checks the gate polls for landed green well before the cancellation on both runs (on this one, Entitlement API tests — the longest — completed at 18:52:52Z, 5 min before the gate died). That's reproducible, not a one-off runner-shutdown flake, and it's inside the gate's own 30-min Not proposing a fix here since this touches Ideas worth checking:
Generated by Claude Code |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
3-hour open-core fire tick: no new draft PR opened this cycle. Roadmap state on the public repo:
Nothing new I can usefully author here without duplicating the two open PRs already sitting in your queue. Leaving those to the local operator verification checklist in each PR body. Generated by Claude Code |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
CI block (transient): E2E Gate (required) was cancelled after ~18 min (run 32658723046) — the Action needed: manually rerun the cancelled E2E Gate job on that run to unblock the PR. Generated by Claude Code |
|
✨ auto-fixed: branch was behind main (base Generated by Claude Code |
Summary
Boolean-fold twin of
min_tier_for_all_breakdown. Closes the last per-axis-breakdown gap in the has_/min_tier_for_ pair: the reverse-lookup family already surfaces which axis is binding the aggregate required-tier floor; the boolean-fold family had no counterpart for which axis is blocking the LIVE aggregate grant. A UI wanting the pair had to fan out five singularhas_*scalars plus a client-side which-axis-is-false walk.What lands
One helper in
clawmetry/entitlements.py, sitting immediately aftermin_tier_for_all_breakdownso the pair reads side by side:Grant-axis row:
{kind, supplied, items, unknown, has, blocking}—itemsholds normalised known ids the fold saw,unknownholds typo tokens the singularhas_features/has_runtimesscalars would collapse the axis on (so a tooltip can rendertypo: *Fleeet*instead of a bare denial). Capacity-axis row:{kind, supplied, value, has, blocking}—valueechoes the parsed int, or the raw input if it did not parse, mirroringmin_tier_for_all_breakdown.Endpoint in
routes/entitlement.py, mirroring/api/entitlement/required-tier-breakdown's posture:GET /api/entitlement/has-all-breakdown?features=…&runtimes=…&channels=N&retention_days=K&nodes=M400 when nothing is supplied; 200 with the per-axis envelope otherwise. A blank or non-int capacity value still surfaces the axis in the response (with
has=falseand the raw input invalue) instead of 400-walling — matches the never-crash posture of the singular/has-*endpoints.retention_days=is unset, NOT unlimited (matcheshas_all); asking about the unlimited-retention live grant stays the singular/api/entitlement/has-retention-windowcall's job.Envelope adds
current_tier{,_rank},grace,enforcedalongside the breakdown fields so a caller wiring this next to the singular/has-*endpoints sees the same shape and can adopt the breakdown without reshaping its diagnostics payload. Never 5xxs — resolver failure returns the OSS-fallback envelope (has_all=false, emptyblocking_axes, every axisnull).Grace / open-core posture
Ships in GRACE like every other entitlement helper. Zero current behaviour change: while
ent.graceisTrueevery fully-known bundle folds tohas_all=Truewith emptyblocking_axes(matcheshas_allbyte-for-byte). Unknown grant tokens, empty grant axes, and non-int capacity axes still collapse the fold toFalseeven in grace (matches the singular scalars' strict-Falsetypo posture), and the corresponding axis is surfaced inblocking_axeswith the typo detail so a tooltip can flag it. No version bump, no release tag.Pairing invariants
axesechoes matchmin_tier_for_all_breakdownonkind,supplied,items(grants), andvalue(capacities) so a paywall tooltip that pairs the two envelopes renders coherently: "denied here BECAUSE of axis Y" (this helper'sblocking_axes) alongside "cheapest tier that would grant it is Z BECAUSE of axis W" (reverse-lookup breakdown'sbinding_axes). One test pins that invariant so a future breakdown-shape change on either side breaks loudly.Tests
New
tests/test_entitlement_has_all_breakdown.py(35 tests):has_all/axes/blocking_axesonly;axeskeys are the five axes)has_all-parity across the five axes plus the mixed bundlehas_all=Falsewith the axis surfaced as blocking (grace-independent typo posture)blocking_axesidentification: empty onhas_all=True, single-axis denial, multi-axis denial in envelope order, empty on the nothing-supplied edgekind/supplied/has/blocking+items+unknownfor grants,valuefor capacities)min_tier_for_all_breakdownfor pair-invariant paywall renderinggrace/enforced/current_tierdefaultsLocal operator verification checklist
The autonomous fire can't touch the local daemon, the browser, or the live cloud snapshot. Once CI is green, please verify locally:
launchctl kickstart -k gui/$(id -u)/com.clawmetry.synchas_all=true,grace=true,enforced=false,blocking_axes=[], and each of the five axes surfaces withhas=true,blocking=false. The second returnshas_all=falsewithblocking_axes=["features"]andaxes.features.unknown=["bogus-feature"]. The third returnshas_all=falsewithblocking_axescontainingchannelsandaxes.channels.value="five". The fourth returns 400 with anerrorfield naming the five accepted axes./api/entitlement,/api/entitlement/required-tier-breakdown, or any singular/api/entitlement/has-*endpoint (none of them were touched).Draft only — leaving merge,
[RELEASE]PR, tag, and cloud rollout to the operator.Generated by Claude Code