Skip to content

feat(entitlement): preview(tier) + GET /api/entitlement/preview#3221

Merged
vivekchand merged 1 commit into
mainfrom
feat/entitlement-preview
Jun 20, 2026
Merged

feat(entitlement): preview(tier) + GET /api/entitlement/preview#3221
vivekchand merged 1 commit into
mainfrom
feat/entitlement-preview

Conversation

@vivekchand

Copy link
Copy Markdown
Owner

Summary

upgrade_diff() answers "what changes"; preview() answers "what does the resulting Entitlement look like" — the full denormalised to_dict() shape rendered for an arbitrary purchasable tier so the upgrade-CTA card can render concrete numbers ("Cloud Pro: 90-day retention, unlimited channels, claude_code + codex + … unlocked") without re-deriving per-tier capacity tables in JS.

  • The previewed Entitlement is built with grace=False so per-tier limits (channel_limit, retention_days) actually surface — a grace-mode preview zeroes those out and defeats the purpose.
  • source="preview" tags the synthetic shape so the UI can never mistake it for a live entitlement.
  • GET /api/entitlement/preview?tier=cloud_pro → the to_dict() body. 400 on missing tier, 404 on unknown tier.
  • preview() itself returns None for unknown / empty / non-string input and never raises.

Behaviour change

None. This is a pure-read primitive; nothing in the existing gates calls it. It composes with upgrade_diff / downgrade_diff (delta surface) and tier_catalog (per-tier static metadata) — preview is the third leg: "the resolved shape at tier X".

Files

  • clawmetry/entitlements.pypreview(target_tier) module-level helper (~40 lines, follows the _build() pattern).
  • routes/entitlement.pyGET /api/entitlement/preview on bp_entitlement + docstring update.
  • tests/test_entitlement_preview.py — 19 tests covering shape, per-tier limits (OSS / Starter / Pro / Enterprise), case-insensitivity, source="preview" invariant, grace-independence, mutation-free, and the four HTTP status paths.

Tests

$ python -m pytest tests/test_entitlement_preview.py -q
19 passed in 0.29s
$ python -m pytest tests/test_entitlements.py tests/test_entitlement_upgrade_diff.py tests/test_entitlement_tier_diff.py tests/test_entitlement_api.py tests/test_entitlements_table_conformance.py -q
154 passed in 1.33s
$ ruff check clawmetry/entitlements.py routes/entitlement.py tests/test_entitlement_preview.py
All checks passed!

Local operator verification checklist

Since this PR is opened from a remote container, the local-daemon + live-cloud verification has to happen on your box:

  • Copy changed files into ~/.clawmetry/lib/python3.11/site-packages/clawmetry/ (and routes/):
    - clawmetry/entitlements.py
    - routes/entitlement.py
  • Clear __pycache__ under the installed clawmetry/ + routes/ trees.
  • launchctl kickstart -k gui/$(id -u)/com.clawmetry.sync to restart the daemon.
  • curl -s http://localhost:8900/api/entitlement/preview?tier=cloud_pro | jq .tier,.source,.retention_days,.channel_limit — expect "cloud_pro", "preview", 90, null.
  • curl -i http://localhost:8900/api/entitlement/preview — expect 400.
  • curl -i http://localhost:8900/api/entitlement/preview?tier=nope — expect 404.
  • Decrypt the live cloud snapshot, confirm the dashboard tab still loads with no new console errors.
  • Browser-check /api/entitlement is unchanged.

🤖 Generated with Claude Code


Generated by Claude Code

upgrade_diff() answers "what changes"; preview() answers "what does the
resulting Entitlement look like" -- the full denormalised to_dict() shape
rendered for an arbitrary purchasable tier so the upgrade-CTA card can
render concrete numbers ("Cloud Pro: 90-day retention, unlimited channels,
claude_code + codex + ... unlocked") without re-deriving per-tier capacity
tables in JS.

The previewed Entitlement is built with grace=False so the per-tier limits
(channel_limit, retention_days) actually surface -- a grace-mode preview
zeroes those out and defeats the purpose. source="preview" tags the
synthetic shape so the UI can never mistake it for a live entitlement.

GET /api/entitlement/preview?tier=cloud_pro -> the to_dict() body, with
400 on missing tier and 404 on unknown tier. preview() itself returns
None for unknown / empty / non-string input and never raises.

No behaviour change: this is a pure-read primitive; nothing in the
existing gates calls it.

19 new tests pin the shape, per-tier limits (OSS/Starter/Pro/Enterprise),
case-insensitivity, fallback, and that calling preview never mutates the
live cached entitlement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jun 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Visual diff

Comparing d770df140132 (head) against the PR base branch.

No flagged differences (running with ALWAYS_POST=1).

View Before After Diff
desktop overview before after diff · 0.19%
desktop flow before after diff · 0.01%
desktop brain before after diff · 0.00%
desktop usage before after diff · 0.00%
desktop crons before after diff · 0.00%
desktop memory before after diff · 0.00%
desktop security before after diff · 0.02%
desktop subagents before after diff · 0.00%
desktop transcripts before after diff · 0.00%
desktop logs before after diff · 0.00%
desktop skills before after diff · 0.00%
desktop models before after diff · 0.00%
desktop approvals before after diff · 0.02%
desktop alerts before after diff · 0.00%
desktop notifications before after diff · 0.00%
desktop context before after diff · 0.00%
desktop limits before after diff · 0.00%
desktop clusters before after diff · 0.00%
desktop history before after diff · 0.00%
desktop channels before after diff · 0.00%
desktop dives before after diff · 0.00%
desktop harness before after diff · 0.00%
desktop inventory before after diff · 0.00%
desktop nemoclaw before after diff · 0.00%
desktop policy before after diff · 0.00%
desktop selfevolve before after diff · 0.00%
desktop swimlane before after diff · 0.00%
desktop tool-catalog before after diff · 0.00%
desktop tracing before after diff · 0.00%
desktop turn-anatomy before after diff · 0.02%
desktop version-impact before after diff · 0.00%
desktop context-economics before after diff · 0.00%
mobile overview before after diff · 0.17%
mobile flow before after diff · 0.00%
mobile brain before after diff · 0.01%
mobile usage before after diff · 0.00%
mobile crons before after diff · 0.00%
mobile memory before after diff · 0.00%
mobile security before after diff · 0.01%
mobile subagents before after diff · 0.00%
mobile transcripts before after diff · 0.00%
mobile logs before after diff · 0.00%
mobile skills before after diff · 0.00%
mobile models before after diff · 0.00%
mobile approvals before after diff · 0.00%
mobile alerts before after diff · 0.00%
mobile notifications before after diff · 0.00%
mobile context before after diff · 0.00%
mobile limits before after diff · 0.00%
mobile clusters before after diff · 0.00%
mobile history before after diff · 0.00%
mobile channels before after diff · 0.00%
mobile dives before after diff · 0.00%
mobile harness before after diff · 0.00%
mobile inventory before after diff · 0.00%
mobile nemoclaw before after diff · 0.00%
mobile policy before after diff · 0.00%
mobile selfevolve before after diff · 0.00%
mobile swimlane before after diff · 0.00%
mobile tool-catalog before after diff · 0.00%
mobile tracing before after diff · 0.00%
mobile turn-anatomy before after diff · 0.04%
mobile version-impact before after diff · 0.00%
mobile context-economics before after diff · 0.00%

Folder: pr/3221/d770df140132. 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.

@vivekchand vivekchand marked this pull request as ready for review June 20, 2026 12:05
@vivekchand vivekchand merged commit 3af72a2 into main Jun 20, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants