docs(gating): ADR 0017 + architecture + guide + CHANGELOG #308

Closed
maximus wants to merge 2 commits from issue-302-docs-gating into issue-301-rust-entitlements
Owner

Documentation of the edition-gating work (#297-#301), written against the actual code of the stack.

  • ADR 0017 (accepted): tier->features matrix, signed features[] override fail-closed in Free (CWE-863), UI-only enforcement as an assumed GPL soft-paywall (server-enforced price fetching stays the only hard gate), non-destructive downgrade, dev-override Cargo feature (CWE-489), rejected alternatives. Spec links point to the force-added root specs (checked: tracked in git).
  • architecture.md: new "Gating par édition" section (matrix, LicenseContext, useEntitlement, RequireFeature/UpsellGate, NavLock, profileGate, Rust side), rewritten entitlements.rs section (auto-update now Base+, stale "open to free" note removed), gated routes listed in routing, hooks table updated (useLicense removed in #297 -> useEntitlement/useIsPremium), ADR index + header refreshed.
  • guide-utilisateur.md + docs.editions.* (FR/EN): new "Éditions" section with the Free/Base/Premium table, unlock flow, non-destructive locking tips. The keys are wired into DocsContent.tsx (new SECTIONS entry — without it the keys would be dead).
  • CHANGELOG.md + CHANGELOG.fr.md: one global entry under [Unreleased]/### Changed listing modules now Base (Budget, Adjustments, 4 advanced reports, multi-profile, auto-update) and Premium (Balance), the visible-but-locked upsell with disabled "coming soon" purchase CTA, and data preservation.

Validation: 871 vitest green, npm run build (tsc + vite) clean. No Rust change.

Resolves #302
Generated autonomously by /autopilot run of 2026-07-20

Documentation of the edition-gating work (#297-#301), written against the actual code of the stack. - **ADR 0017** (accepted): tier->features matrix, signed `features[]` override fail-closed in Free (CWE-863), UI-only enforcement as an assumed GPL soft-paywall (server-enforced price fetching stays the only hard gate), non-destructive downgrade, `dev-override` Cargo feature (CWE-489), rejected alternatives. Spec links point to the force-added root specs (checked: tracked in git). - **architecture.md**: new "Gating par édition" section (matrix, LicenseContext, useEntitlement, RequireFeature/UpsellGate, NavLock, profileGate, Rust side), rewritten `entitlements.rs` section (auto-update now Base+, stale "open to free" note removed), gated routes listed in routing, hooks table updated (`useLicense` removed in #297 -> `useEntitlement`/`useIsPremium`), ADR index + header refreshed. - **guide-utilisateur.md + `docs.editions.*` (FR/EN)**: new "Éditions" section with the Free/Base/Premium table, unlock flow, non-destructive locking tips. The keys are wired into `DocsContent.tsx` (new SECTIONS entry — without it the keys would be dead). - **CHANGELOG.md + CHANGELOG.fr.md**: one global entry under [Unreleased]/### Changed listing modules now Base (Budget, Adjustments, 4 advanced reports, multi-profile, auto-update) and Premium (Balance), the visible-but-locked upsell with disabled "coming soon" purchase CTA, and data preservation. Validation: 871 vitest green, `npm run build` (tsc + vite) clean. No Rust change. Resolves #302 Generated autonomously by /autopilot run of 2026-07-20
maximus added 1 commit 2026-07-21 02:49:16 +00:00
Document the edition-gating work (#297-#301):

- ADR 0017 (accepted): tier->features matrix, signed features[] override
  fail-closed in Free (CWE-863), UI-only enforcement as an assumed GPL
  soft-paywall (server-enforced price fetching stays the only hard gate),
  non-destructive downgrade, dev-override behind an explicit Cargo
  feature (CWE-489), rejected alternatives.
- architecture.md: new 'Gating par edition' section (entitlements matrix,
  LicenseContext, useEntitlement, RequireFeature/UpsellGate, NavLock,
  profileGate, Rust side), rewritten entitlements.rs section (auto-update
  now Base+, stale 'open to free' note removed), gated routes listed in
  the routing section, hooks table updated (useLicense removed in #297 ->
  useEntitlement/useIsPremium), ADR index + header refreshed.
- guide-utilisateur.md + docs.editions.* i18n keys (FR/EN) wired into
  DocsContent: new 'Editions' section with the Free/Base/Premium table,
  unlock flow and non-destructive locking tips.
- CHANGELOG.md + CHANGELOG.fr.md: one global [Unreleased] entry listing
  the modules now gated Base (Budget, Adjustments, advanced reports,
  multi-profile, auto-update) and Premium (Balance), the visible-but-
  locked upsell with disabled 'coming soon' purchase CTA, and the
  data-preserving behaviour.

Resolves #302

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
maximus added the
status:review
type:feature
source:human
autopilot:pending-human
labels 2026-07-21 02:49:26 +00:00
Author
Owner

Review adversariale — APPROVE

Résumé : PR de documentation vérifiée claim-par-claim contre le code réel de la branche (pile #297-#301 incluse). Chaque affirmation de l'ADR 0017, de la section « Gating par édition » d'architecture.md et du guide utilisateur correspond au code.

Vérifications effectuées (lecture git show sur issue-302-docs-gating, sans checkout) :

  • src/shared/entitlements.ts : FeatureKey = les 5 clés documentées, matrice conforme (balance → premium seul), court-circuit if (edition === "free") return false AVANT l'override features[] (CWE-863), requiredTierFor dérivé de l'appartenance — tout matche l'ADR.
  • src-tauri/src/commands/entitlements.rs : FEATURE_TIERS réduit à auto-update → [base, premium] (#271 absorbé), is_entitled avec le même court-circuit Free ; license_commands::current_entitlements est bien pub(crate) (« interne, pas une commande ») et retourne ("free", []) sur tout échec ; le chemin Compte Maximus ne porte pas de features[] — conforme au point « Neutre » de l'ADR.
  • Cargo feature dev-override : hors default, #[cfg(feature = "dev-override")] sur dev_override_edition() / SR_DEV_EDITION — conforme (CWE-489).
  • LicenseContext.tsx : RETRY_BASE_MS = 1000 / RETRY_MAX_MS = 30_000, retry uniquement sur status === "error", validationError orthogonal (ne touche jamais status) — conforme.
  • useEntitlement retourne { allowed, ready } ; useLicense.ts absent de la branche ; useIsPremium.ts subsiste — la table des hooks est exacte.
  • main.tsx : LicenseProvider > ProfileProvider > ErrorBoundary (le BrowserRouter key={refreshKey} est dans App.tsx) — le claim « survit au remount » est exact.
  • App.tsx : les 4 groupes RequireFeature couvrent exactement les routes listées ; /reports et /reports/trends hors gate.
  • Sidebar : NavLock rend null si !ready || allowed (badge seulement si ready && !allowed) ; l'« invariant testé : reports n'est jamais gaté » existe réellement (src/shared/constants/index.test.ts:20).
  • profileGate.ts exporte isProfileSwitchLocked / isProfileCreationLocked ; ProfileFormModal porte le mode upsell (point unique de création) ; LicenseCard est dans UsersSettingsPage → « Paramètres → Utilisateurs » exact.
  • UpsellGate : CTA disabled + clé « bientôt » (ctaGetSoon), « J'ai déjà une clé » → /settings/users + onNavigate? — conforme.
  • Les 2 specs spec-*-feature-gating.md sont trackées à la racine → les liens ../../ de l'ADR résolvent (le piège qui avait bloqué #295 est évité). ADR 0009/0011/0016 : noms de fichiers exacts. Index ADR mis à jour.
  • Guide : sections ## 11. Paramètres## 12. Éditions, numérotation correcte ; matrice du guide = matrice du code.
  • DocsContent.tsx : le rendu consomme docs.{key}.title/overview/features/steps/tips via returnObjects — la section editions fournit exactement ces 5 clés, symétriques FR/EN (4/4/3 items). Sans l'entrée SECTIONS les clés seraient mortes ; elle est là.
  • CHANGELOG : entrée dans les deux fichiers, ### Added avant ### Changed (ordre Keep a Changelog), références #297-#301.
  • Aucune migration SQL, aucun secret, commit conventionnel docs(gating): ..., Resolves #302 présent.

Suggestions (non bloquantes) :

  1. src/i18n/locales/fr.jsondocs.editions.features[1] et [2] : « tout la Gratuite » / « tout la Base » est agrammatical → « tout de la Gratuite » / « tout de la Base » (chaîne user-facing dans la page Docs). Corrigeable au merge ou dans un maillon suivant.
  2. Note mineure : le pseudo-code isEntitled de l'ADR omet la branche défensive !tiers (feature hors matrice → override signé seul) présente dans le code réel — la sémantique documentée reste exacte, rien à corriger.

Review adversariale /pr-review — vérifiée contre le code de la pile, base issue-301-rust-entitlements.

## Review adversariale — APPROVE **Résumé** : PR de documentation vérifiée claim-par-claim contre le code réel de la branche (pile #297-#301 incluse). Chaque affirmation de l'ADR 0017, de la section « Gating par édition » d'architecture.md et du guide utilisateur correspond au code. **Vérifications effectuées** (lecture `git show` sur `issue-302-docs-gating`, sans checkout) : - `src/shared/entitlements.ts` : `FeatureKey` = les 5 clés documentées, matrice conforme (balance → premium seul), court-circuit `if (edition === "free") return false` AVANT l'override `features[]` (CWE-863), `requiredTierFor` dérivé de l'appartenance — tout matche l'ADR. - `src-tauri/src/commands/entitlements.rs` : `FEATURE_TIERS` réduit à `auto-update → [base, premium]` (#271 absorbé), `is_entitled` avec le même court-circuit Free ; `license_commands::current_entitlements` est bien `pub(crate)` (« interne, pas une commande ») et retourne `("free", [])` sur tout échec ; le chemin Compte Maximus ne porte pas de `features[]` — conforme au point « Neutre » de l'ADR. - Cargo feature `dev-override` : hors `default`, `#[cfg(feature = "dev-override")]` sur `dev_override_edition()` / `SR_DEV_EDITION` — conforme (CWE-489). - `LicenseContext.tsx` : `RETRY_BASE_MS = 1000` / `RETRY_MAX_MS = 30_000`, retry uniquement sur `status === "error"`, `validationError` orthogonal (ne touche jamais `status`) — conforme. - `useEntitlement` retourne `{ allowed, ready }` ; `useLicense.ts` absent de la branche ; `useIsPremium.ts` subsiste — la table des hooks est exacte. - `main.tsx` : `LicenseProvider` > `ProfileProvider` > `ErrorBoundary` (le `BrowserRouter key={refreshKey}` est dans App.tsx) — le claim « survit au remount » est exact. - App.tsx : les 4 groupes `RequireFeature` couvrent exactement les routes listées ; `/reports` et `/reports/trends` hors gate. - Sidebar : `NavLock` rend `null` si `!ready || allowed` (badge seulement si `ready && !allowed`) ; l'« invariant testé : reports n'est jamais gaté » existe réellement (`src/shared/constants/index.test.ts:20`). - `profileGate.ts` exporte `isProfileSwitchLocked` / `isProfileCreationLocked` ; `ProfileFormModal` porte le mode `upsell` (point unique de création) ; `LicenseCard` est dans `UsersSettingsPage` → « Paramètres → Utilisateurs » exact. - `UpsellGate` : CTA `disabled` + clé « bientôt » (`ctaGetSoon`), « J'ai déjà une clé » → `/settings/users` + `onNavigate?` — conforme. - Les 2 specs `spec-*-feature-gating.md` sont trackées à la racine → les liens `../../` de l'ADR résolvent (le piège qui avait bloqué #295 est évité). ADR 0009/0011/0016 : noms de fichiers exacts. Index ADR mis à jour. - Guide : sections `## 11. Paramètres` → `## 12. Éditions`, numérotation correcte ; matrice du guide = matrice du code. - `DocsContent.tsx` : le rendu consomme `docs.{key}.title/overview/features/steps/tips` via `returnObjects` — la section `editions` fournit exactement ces 5 clés, symétriques FR/EN (4/4/3 items). Sans l'entrée SECTIONS les clés seraient mortes ; elle est là. - CHANGELOG : entrée dans les deux fichiers, `### Added` avant `### Changed` (ordre Keep a Changelog), références #297-#301. - Aucune migration SQL, aucun secret, commit conventionnel `docs(gating): ...`, `Resolves #302` présent. **Suggestions (non bloquantes)** : 1. `src/i18n/locales/fr.json` — `docs.editions.features[1]` et `[2]` : « tout **la** Gratuite » / « tout **la** Base » est agrammatical → « tout **de** la Gratuite » / « tout **de** la Base » (chaîne user-facing dans la page Docs). Corrigeable au merge ou dans un maillon suivant. 2. Note mineure : le pseudo-code `isEntitled` de l'ADR omet la branche défensive `!tiers` (feature hors matrice → override signé seul) présente dans le code réel — la sémantique documentée reste exacte, rien à corriger. *Review adversariale /pr-review — vérifiée contre le code de la pile, base `issue-301-rust-entitlements`.*
defenseur-auto-bot added 1 commit 2026-07-22 00:20:14 +00:00
"tout la Gratuite/Base" -> "tout de la Gratuite/Base", flagged as the
one user-facing correction in the /pr-review pass on PR #308.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Author
Owner

Mergé en fast-forward sur main (pile #304→#308), voir commit 01da65c + 6de9617 (fix i18n). Verdict /pr-review : APPROVE. Fermeture manuelle : le merge local ff n est pas détecté comme merged par Forgejo.

Mergé en fast-forward sur main (pile #304→#308), voir commit 01da65c + 6de9617 (fix i18n). Verdict /pr-review : APPROVE. Fermeture manuelle : le merge local ff n est pas détecté comme *merged* par Forgejo.
maximus closed this pull request 2026-07-22 00:23:45 +00:00

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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: maximus/Simpl-Resultat#308
No description provided.