Edition map and named feature guards

Task 10758 · Done · Coder · created · updated

Epic 10745 "Close the identified gaps in Jaah CE". Original request (verbatim): "Close the identified gaps in Jaah CE". Settled inputs: PRD (Task 10746) and Architecture (Task 10747). Requirement and AC ids below are the PRD's; section refs (§) are the Architecture's. Read them with task_read on 10746/10747 if you need more than is quoted here. ## Requirement (verbatim) R2 — Named feature guards and one edition map (PRD, verbatim). Every cloud-only capability (everything CE leaves out today — billing, cloud node launch, node groups, Multi-Node Hosts, NodeLink, MicroVM, GHA runners, knowledge base, project memory, SSO, SCIM, SIEM export, tenant mail, data residency, org transfer, sign-up/users, … — plus R1's enterprise features) has a stable name, and ONE file maps each name to the editions that include it. Editions in the map today: `cloud` and `ce`; the map must accept a further edition (e.g. `onprem`, `enterprise`) by adding a column/entry only. Code guards a feature by its name, not by `edition.IS_CE`. Operator's words (Q2): "Let's guard each feature with its name and maintain a map of what is included in CE. that will give us a way to create other types of distribution, e.g. for on-premises usage or enterprise, etc. Do a deep research for the best practice" → map scope: "Every cloud-only feature". R4 (PRD) context: gitleaks over the built tree stays the scan of record; check_tree must keep reporting 0 findings. ## Scope and approach Child C1 of Architecture §7. Build the seam every other child uses (Architecture §2, §3). **The edition map, `packages/editions/features.toml`.** - It holds `editions = ["cloud", "ce"]` and one `[feature.<slug>]` row per cloud-only capability. - §2.1 rules: the slug matches `^[a-z][a-z0-9_]*$` and names a capability, not a tier. `label` is the only text a person sees. `cloud` is on every row. `paths`, `route_prefixes` and `console_routes` are optional. - Move EVERY current cloud-only `exclude` path and every `removed_route_prefixes` entry of `tools/community_edition/manifest.toml` into exactly one row. - Seed list (§3; C1 owns the final classification): billing, hostings, cloud_nodes, node_groups, multi_node_hosts, nodelink, microvm, gha_runners, apt_self_upgrade, tailnet, knowledge_base, support_assistant, site_chat, project_memory, jaah_ai_credit, sso, scim, siem_export, tenant_mail, system_mail, data_residency, org_transfer, multi_org_tenancy, signup, users_invites, oauth_signin, slack, telegram, legal_acceptance, customers, control_plane_view. - The `sso`, `scim`, `jaah_ai_credit`, `tenant_mail`, `multi_org_tenancy` and `org_transfer` rows may start with only what the manifest already excludes; C2a and C2b add the enterprise module paths. - If C5 has already merged, move `portfolio/fleet/memory-on.md` from the manifest into the `project_memory` row's `paths`. **The renderer, `tools/editions/render.py --edition <name> --out <root>`.** It writes the three import-free modules of §2.2, each with the GENERATED header: - `apps/api/app/edition_features.py`, which also renders `REMOVED_ROUTE_PREFIXES` and replaces `edition_removed_routes.py`. - `apps/jaahd/edition_features.py`. - `apps/web/src/lib/editionFeatures.ts`, with the `FeatureName` union, `ENABLED`, `isEnabled`, `LABELS` and `REMOVED_CONSOLE_ROUTES`. - Commit the `cloud` rendering. **The `EDITION` constant.** - Widen its annotation to `str` (TS `string`) in both `edition.py` files and `edition.ts`. - Add a test asserting `EDITION ∈ editions`. - Replace the manifest's fixed `[[rewrite]]` EDITION flips with `build_tree.py` writing `[mirror] edition` into those three lines. Keep the exactly-once check. **`build_tree.py` derivation (§2.4).** - Read `[mirror] edition`. - exclude := manifest.exclude ∪ the paths of features the edition lacks. - `removed_route_prefixes` leaves the manifest. - Render the three modules for the edition into the tree. **`check_tree.py`.** - Honour the §2.3 guard forms in the import check. The body of `enabled("f")` with f ∉ E is never-run, and so is the rest of a block after `if not enabled("f"): return|raise`. - New `features` check: every `enabled("x")`/`isEnabled('x')` literal names a map feature, and every path of a feature E lacks is absent. - New orphan check with `[check] entrypoints` (§2.5). - The `IS_CE`/`TYPE_CHECKING` forms stay. The "no knob" check is untouched. **`edition_fixture(edition)` helpers** in the api `conftest`, the jaahd tests and web `test/edition.ts`. **`tools/tests/test_editions.py`, with every §2.5 test:** - Freshness. - Each `paths` glob matches ≥1 file, and no two features' globs overlap. - Every guard literal names a feature. - Manifest `exclude` entries belong to no feature. - The api's private cloud-router test pins the `routers_cloud.py` route table against the map's `route_prefixes`, both directions. - The AC2.3 allowlist test has two sections. `EDITION_WIDE` is final: one entry per site key (file + enclosing function), each with a one-line reason. Pre-list the site key `apps/jaahd/agents/agent_layer.py::place_subagents` there for C5. The stale-entry check covers `PENDING_SWEEP` only, so this pre-listed key passes before C5 lands. `PENDING_SWEEP` holds every current per-feature `IS_CE` site, each tagged with its owning child (C2a/C2b/C3a/C3b/C3c/C3d), and fails on a stale entry. - A test forbidding a bare `IS_CE` monkeypatch in new tests. **Docs.** - Rewrite the manifest comments that call the manifest the single home of exclusions (line 1), and the "carries inert" list, to point at the map. - Add an "Add an edition" section (≤10 steps) to the mechanism's README. Route it from `tools/community_edition/README.md` so `check_context.py` reachability stays green. **The AC2.4 dry run goes in the PR body.** Take a temp copy of the map with an `onprem` edition that includes `sso` and `scim`, plus a temp manifest with `[mirror] edition = "onprem"`. It builds with `build_tree.py --manifest <tmp>`. That tree contains `org_sso.py` and the CE tree does not. Nothing is committed. No call-site rewrite beyond what the tests need: the sweeps are C2a–C3d. Credentials & reach: (Architecture §10 rows, verbatim) - Build, check (incl. gitleaks), tooling + app test suites | worktree only; gitleaks at `~/.local/bin/gitleaks` on node h2 | verified (binary present on h2) - `mirrors.yml` `jaah-ce` hand run, no publish (AC1.3) | `jaah-forge` (task-authenticated) on GitHub `jaah-ai/jaah` | verified (from h2: `api get repos/jaah-ai/jaah/actions/workflows/mirrors.yml` returned the active workflow, and `api get repos/jaah-ai/jaah` reports `permissions.push` and `admin` true — the identity may dispatch; the dispatch itself is C6's write) - Jaah Deploy watch per child PR | `jaah-forge` read verbs (`pr checks`, `ci log`) | verified (same forge identity and read probe as above) Out of scope: (PRD §3, verbatim — do NOT touch) - MUST NOT make `jaah-ai/jaah-ce` public, change repo visibility, or run a `ce-mirror` publish. - MUST NOT edit LICENSE text or any legal page (legal review is a human item). - MUST NOT edit `docs/pricing.md` or the marketing site (deferred to the flip; not in this repo). - MUST NOT add a second secret scanner (trufflehog etc.). - MUST NOT add an env var, setting, build arg or runtime flag that selects an edition, enables a feature or moves a cap. - MUST NOT change cloud behaviour or cloud plan entitlements/pricing; the R2 guard rewrite is behaviour-preserving for `cloud`. - MUST NOT ship `.claude/skills/` (translate, marketing-copy) — named a deferred follow-up below. - MUST NOT build an on-prem or enterprise edition — only make the map able to describe one. Architecture boundaries: - D4, D5: no monorepo file moves. `portfolio/fleet/*.md`, `docs/policies/context-architecture.md`, `.claude/agents/*.md`, hub pointers, `tools/check_context.py`, `tools/portable_standard.py`, `infra/local/Dockerfile.api` and `infra/host/fleet-harness/reconcile.py` stay untouched. - D1: keep schema, strip logic. The enterprise ORM models (the `models/orgs.py` SSO/SCIM classes, `models/metering.py`, `models/tenant_email.py`) and the squashed baseline migration stay. No edition-forked schema, no new migration. Done means: - AC2.1 → verify: open the map file: every feature row names its editions; `cloud` includes every feature. - AC2.2 (single home) → verify: the manifest's cloud-only `exclude` entries and `removed_route_prefixes` are derived from (or checked against) the map — a test fails if a path/prefix belongs to no feature, or a feature excluded from `ce` has a shipped path. Run the CE tooling tests (`./dc check full` stage that runs them) and observe green; then delete one feature's map row locally and observe the test red. (Tightened: delete two rows, and both must go red. One is a row with `route_prefixes`, red via the `routers_cloud.py` route-table pin. The other is a paths-only row, red via the orphan check.) - AC2.4 (extensibility) → verify: the README of the mechanism documents, in under ten steps, how to add an edition; the architect's spec includes a dry-run demonstrating a third edition builds with the existing build tool and no code change outside the map and manifest. (Here: the dry-run output is in the PR body.) - AC2.5 (no knob) → the edition and features remain build-time literals: no env var, setting, build arg or flag turns a feature on (existing CE invariant, PRD §7 of the CE build). Verify: the existing "no knob" check in `check_tree.py` stays green. - AC2.6 (copy) → every feature name a person sees (upgrade message, docs) is a human label, never the slug. (Here: every row has a `label` written per docs/standards/ui-copy-standards.md — a real product name such as "Single sign-on", not a slug restated in Title Case.) - Freshness: `python3 tools/editions/render.py --edition cloud --out .` leaves `git status` clean. - The built CE tree has the same file set as a develop build (diff `find $T -type f | sort`), except that the three rendered edition modules are added and `edition_removed_routes.py` is dropped if folded in. No other path is added or removed. - `manifest.toml` has no `removed_route_prefixes` key. Its `exclude` holds only non-feature carve-outs (tests, fixtures, tooling, docs, `infra/jaah`, `infra/host`, `apps/site-widget`). Every entry removed from the manifest appears in exactly one map row. - State in the PR body whether `packages/editions/**` and `tools/editions/**` ship in the tree. If `check_tree.py` reads the map in the public tree, the manifest `include` carries them and the built tree's own `check_tree.py $T` run stays green. - Quick gate `./dc check quick` green. - `python3 tools/community_edition/build_tree.py $T && python3 tools/community_edition/check_tree.py $T` exits 0 (AC1.2: import closure intact). - The PR's Jaah Deploy on GitHub succeeds. If not, fix it and re-merge; leave a cancelled or superseded deploy alone.

Outcome

Built the edition map (packages/editions/features.toml, 31 rows), rendered feature guards for api/jaahd/web, derived CE build + features/orphans checks, AC2.3 allowlist tests. Quick gate green; PR #7169 merged (eff16b06c), Jaah Deploy succeeded. Deferred: splitting the 4 aggregation seams, web orphan check. Memory: none applied.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)