# Env-First Profile Redesign Implementation Plan

This ExecPlan is a living document. Keep `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` up to date as work proceeds.

Reference: `PLANS.md` at repository root. This plan must be maintained according to `PLANS.md`.

## Execution Protocol (Mandatory)

Execute subtasks strictly in sequence. Do not run multiple subtasks concurrently.

For each subtask, create a fresh subagent with non-inherited context. Provide only this plan, the target subtask, and minimum required repository context.

After each subtask finishes, the parent agent must review the patch, integrate it, run the subtask validation commands, commit that subtask, and then update `Progress` before starting the next subtask.

If a subtask fails validation, fix that subtask before moving forward. Never carry known failing behavior into a later subtask.

operator prompt template:

`Implement docs/2026-04-04-env-first-profile-redesign-plan.md using fresh non-inherited subagents per subtask, sequentially, with parent review/integration, one commit per subtask, and Progress updates after each subtask commit.`

## Purpose / Big Picture

After this change, `env` becomes the runtime top-level selector (`prod`, `staging`, `dev`), while `profile` becomes an optional layer under the selected env. A profile is always bound to one cluster and stores fixed cluster metadata (`cluster_id`, deploy type fields, org, parent, and cluster name), plus optional `range_start` and `range_end`.

Profile sets are isolated per env, and envs can contain independent profile namespaces. If no profile is selected in the chosen env, commands must rely on explicit flags or `CLINIC_*` environment variables; profile defaults are not applied.

`clinic cluster search-id` gains side-effect behavior: when the resolved cluster is not represented by any profile in the current env, it auto-creates one profile, does not switch to it, and keeps command output unchanged.

Compatibility with the previous global-profile schema is explicitly not required for this redesign.

## Progress

- [x] (2026-04-04 00:54 CST) T1. Replaced config schema with env-scoped profile model and hard-cut migration behavior; validated with `go test ./internal/config -v`.
- [x] (2026-04-04 00:59 CST) T2. Reworked root runtime selection to env-first (`--env` + optional `--profile`) with optional active profile; validated with `go test ./internal/cmd -run Root -v`.
- [x] (2026-04-04 01:08 CST) T3. Redesigned `profile` command family for env-scoped lifecycle and removed `profile cluster` commands; validated with `go test ./internal/cmd -run Profile -v`.
- [x] (2026-04-04 01:13 CST) T4. Added `cluster search-id` auto-create profile behavior with deterministic naming and no auto-switch; validated with `go test ./internal/cmd -run Cluster -v`.
- [x] (2026-04-04 01:21 CST) T5. Rewired defaults/metadata resolution to use optional selected profile and preserved `flag > env var > profile` precedence; validated with `go test ./internal/cmd -run 'EnvFallback|Loki|Events|Cluster' -v`.
- [x] (2026-04-04 01:25 CST) T6. Updated help and docs (`README`, command surface, architecture) to match new semantics; validated with `go test ./internal/cmd -run Help -v`.
- [x] (2026-04-04 01:26 CST) T7. Ran `go test ./internal/cmd -run 'EnvFallback|Loki|Events|Cluster' -v`, `make test`, and `make verify`; `make verify` initially failed the legibility check because this plan lacked the exact lowercase `operator prompt template` phrase, then passed after a plan-only wording fix.

## Surprises & Discoveries

- Observation: After T1 hard-cut schema, `internal/cmd` still referenced legacy config types; a temporary non-persisted compatibility shim (`json:"-"` fields/aliases) was needed to keep package compilation stable during T2.
  Evidence: `go test ./internal/cmd -run Root -v` passed after adding the shim, and `go test ./internal/config -v` still confirms persisted JSON top-level shape (`active_env`, `environments`, `env_profiles`) without legacy keys.

## Decision Log

- Decision: Runtime selection is env-first (`--env` override or `active_env`), then optional profile in that env.
  Rationale: user explicitly defined env as top-level and profile as optional env-local layer.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Profile names are unique within an env, not globally across envs.
  Rationale: same profile name in different envs is valid and expected.
  Date/Author: 2026-04-04 / User + Codex

- Decision: If no active/selected profile in env, profile defaults are not applied.
  Rationale: user requested explicit cluster/range usage without mandatory profile selection.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Keep fallback precedence `flag > CLINIC_* env var > active profile`.
  Rationale: existing env-var workflow remains useful and should be preserved.
  Date/Author: 2026-04-04 / User + Codex

- Decision: `profile set` is current-env scoped and requires `--cluster-id`; metadata is fetched, not manually entered.
  Rationale: profile must bind to one real cluster and include fixed cluster metadata.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Auto-created profile name format is `<cluster-id>__<deploy-or-unknown>__<slug(cluster-name-or-unknown)>`.
  Rationale: user requested cluster ID + deploy type + cluster name, with deterministic fallback and slug normalization.
  Date/Author: 2026-04-04 / User + Codex

- Decision: `cluster search-id` auto-create is skipped if any profile in current env already has the same `cluster_id`, and existing profiles are not updated.
  Rationale: avoid unintended mutation; auto-create is only for first discovery in that env.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Delete of active profile is allowed and clears env-local active profile pointer.
  Rationale: user prefers no forced auto-switch or delete rejection.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Remove `profile cluster` command subtree.
  Rationale: cluster metadata ownership moves into profile records directly.
  Date/Author: 2026-04-04 / User + Codex

- Decision: Hard cut on old profile schema compatibility.
  Rationale: user explicitly said no compatibility issue handling is needed.
  Date/Author: 2026-04-04 / User + Codex

## Outcomes & Retrospective

Targeted command coverage and repo-wide verification both passed. The initial `make verify` run failed only the legibility check, which was resolved by adding the exact lowercase `operator prompt template` phrase to this plan; the rerun of `make verify` completed successfully.

## Context and Orientation

Current code is profile-centric globally:

- `internal/config/config.go` stores `active_profile`, global `profiles`, and `cluster_cache`.
- `internal/cmd/root.go` parses only global `--profile`, then resolves env from that profile.
- `internal/cmd/profile.go` has profile CRUD plus `profile cluster list/use/remove`.
- `internal/cmd/cluster.go` and helpers use `cluster_cache` for metadata shortcuts.

This redesign flips runtime orientation:

- env selection is first-class and independent (`active_env` / `--env`),
- profiles become env-local sets with env-local active pointer,
- profile selection is optional,
- cluster metadata lives inside profile entries (no separate cluster cache layer).

Terms used in this plan:

- "Selected env": env used for this invocation (global `--env` or `active_env`).
- "Selected profile": profile explicitly chosen by `--profile` or env-local `active_profile`; it may be absent.
- "Profile binding metadata": fixed cluster fields that should not drift unless user explicitly updates profile.

## Plan of Work

### Task T1: Config Schema Hard Cut To Env-Scoped Profiles

Files:

- Modify `internal/config/config.go`.
- Modify `internal/config/config_test.go`.

Required changes:

- Replace old profile/cache fields with env-scoped profile container model.
- Use exact JSON model:
  - top level:
    - `active_env`
    - `environments`
    - `env_profiles`
  - per env (`env_profiles.<env>`):
    - `active_profile` (optional string)
    - `profiles` (map from profile name to profile record)
- Profile record fields:
  - `cluster_id` (required for persisted profiles)
  - `cluster_name`
  - `org_id`
  - `parent_id`
  - `cluster_deploy_type_v2`
  - `range_start` (optional)
  - `range_end` (optional)
- `ensureDefaults()` must ensure `active_env` and env entries for `prod/staging/dev` plus initialized empty env-profile containers for each of those envs.
- Do not migrate old `profiles` or `cluster_cache` data.

Acceptance criteria:

- Fresh config has env-profile containers for `prod/staging/dev` with empty profile maps.
- Save/load roundtrip preserves env-profile records exactly.
- Existing permission guarantees remain (`0700` dir, `0600` file).

### Task T2: Root Env-First Runtime Resolution

Files:

- Modify `internal/cmd/root.go`.
- Modify `internal/cmd/root_test.go`.
- Modify `internal/cmd/help.go` (global usage text only in this task).

Required changes:

- Replace current global parser with parser for leading `--env` and `--profile` options.
- Support both options in either order before command family.
- Selected env resolution:
  - use `--env` when provided,
  - otherwise use `active_env`,
  - validate against configured environments.
- Selected profile resolution:
  - if `--profile` provided, resolve by name inside selected env only,
  - otherwise use selected env's `active_profile`,
  - if env active profile is empty, runtime profile is `nil` (no error).
- Update `RuntimeContext` fields to reflect optional profile:
  - `SelectedEnv string`
  - `SelectedProfileName string`
  - `SelectedProfile *config.ProfileRecord` (or equivalent pointer type)
  - `Config`, `Client` unchanged
- Root must no longer require a profile to exist just to run commands.

Acceptance criteria:

- `clinic --env staging <command>` runs without requiring profile state.
- `clinic --env staging --profile foo <command>` errors clearly when `foo` not found in staging.
- Invocation without `--env` uses `active_env`.

### Task T3: Env-Scoped `profile` Command Family Redesign

Files:

- Modify `internal/cmd/profile.go`.
- Modify `internal/cmd/profile_test.go`.
- Modify `internal/cmd/help.go` (profile command help text).
- Modify `internal/cmd/help_test.go` (profile help assertions).

Required changes:

- `profile list`:
  - scope to selected env only,
  - response includes `env`, `active_profile`, and env-local profiles.
- `profile current`:
  - response includes `env`, `active_profile`,
  - if no active profile in env, return `"profile": null` with success.
- `profile use <name>`:
  - resolve `<name>` in selected env only,
  - update selected env's `active_profile`.
- `profile set <name> --cluster-id <id> [--range-start ...] [--range-end ...]`:
  - current-env scoped; remove `--env` flag,
  - require `--cluster-id`,
  - fetch metadata by exact cluster id and write full binding metadata fields,
  - optional range fields update when flag provided,
  - no auto-switch to active profile.
- `profile delete <name>`:
  - remove profile from selected env,
  - if deleted name equals selected env active profile, clear active pointer,
  - no rejection for deleting active profile.
- Remove `profile cluster` subtree (`list/use/remove`) and all related code paths.

Acceptance criteria:

- Same profile name can exist independently in different envs.
- Deleting an active profile leaves env with no active profile and does not break command dispatch.
- `profile set` always produces profile records with fixed cluster metadata fields populated from lookup.

### Task T4: `cluster search-id` Auto-Creates Missing Profile In Current Env

Files:

- Modify `internal/cmd/cluster.go`.
- Modify `internal/cmd/cluster_test.go`.

Required changes:

- Preserve existing search-id request/response behavior for stdout.
- After successful search-id response:
  - find exact cluster-id match in returned items,
  - if no exact match, stop (no side effect),
  - if exact match exists and no profile in selected env has that `cluster_id`, auto-create profile.
- Auto profile creation rules:
  - profile range is empty by default,
  - does not modify env `active_profile`,
  - profile name:
    - base: `<cluster-id>__<deploy-or-unknown>__<slug(cluster-name-or-unknown)>`
    - `deploy-or-unknown` uses `clusterDeployTypeV2`, fallback `unknown`, then slugified,
    - cluster name slugification: lowercase; non `[a-z0-9]` collapsed to single `-`; trim `-`; empty becomes `unknown`,
    - if generated name already exists in env for a different profile, append `__2`, `__3`, ... until unique.
- Skip creation and skip updates when cluster id already exists in any profile in current env.

Acceptance criteria:

- First `search-id` discovery in env creates one profile.
- Repeated `search-id` for same cluster in same env does not create or mutate profiles.
- Active profile stays unchanged after auto-create.

### Task T5: Defaults and Metadata Resolution With Optional Profile

Files:

- Modify `internal/cmd/flag_env.go`.
- Modify `internal/cmd/defaults.go` (if needed).
- Modify `internal/cmd/env_fallback_test.go`.
- Modify metadata resolution helpers in `internal/cmd/cluster.go` and affected tests (`loki`, `events`, `cluster`).

Required changes:

- Keep required/optional resolution order: `flag > CLINIC_* env var > selected profile`.
- If selected profile is nil, profile tier is empty.
- Remove dependency on old `cluster_cache` helpers.
- For topology/loki/events metadata lookups:
  - if selected profile exists and its `cluster_id` matches requested id and required metadata is present, use it,
  - otherwise fallback to API lookup path (current behavior).

Acceptance criteria:

- Existing env-var fallback scenarios keep passing.
- Commands work when no active profile is selected, provided required values come from flag/env var.
- Topology/loki/events remain behaviorally correct when selected profile metadata is absent/incomplete.

### Task T6: Help and Documentation Alignment

Files:

- Modify `internal/cmd/help.go`.
- Modify `internal/cmd/help_test.go`.
- Modify `README.md`.
- Modify `docs/ARCHITECTURE.md`.
- Modify `docs/COMMAND_SURFACE.md`.

Required changes:

- Update global usage to include env-first selector and optional profile override.
- Remove documentation for `profile cluster` commands.
- Document env-scoped profile behavior and no-profile runtime behavior.
- Document `cluster search-id` auto-create side effect and "no auto-switch" rule.
- Keep examples aligned with implemented flags and outputs.

Acceptance criteria:

- Help tests pass with new command surface.
- README and docs reflect actual CLI behavior and precedence rules.

### Task T7: Verification and Evidence

Files:

- No required source file edits unless fixing failures.

Required changes:

- Run targeted tests first, then full verification.
- Record key pass/fail evidence in plan `Outcomes & Retrospective`.

Acceptance criteria:

- All changed package tests pass.
- `make verify` passes from repo root.

## Concrete Steps

All commands run from repository root: `/Users/jujiajia/code/tidbcloud/clinic-cli`.

1. Implement and validate T1.

    go test ./internal/config -v

Expected: config tests pass with new env-scoped profile schema assertions.

2. Implement and validate T2.

    go test ./internal/cmd -run Root -v

Expected: root parsing/runtime tests pass for `--env` and env-scoped `--profile`.

3. Implement and validate T3.

    go test ./internal/cmd -run Profile -v

Expected: profile CRUD tests pass without `profile cluster` command family.

4. Implement and validate T4.

    go test ./internal/cmd -run Cluster -v

Expected: cluster tests include search-id auto-create cases and pass.

5. Implement and validate T5.

    go test ./internal/cmd -run 'EnvFallback|Loki|Events|Cluster' -v

Expected: fallback and metadata-resolution tests pass with optional profile context.

6. Implement and validate T6.

    go test ./internal/cmd -run Help -v

Expected: help output assertions match new command surface.

7. Run final verification ladder.

    make test
    make verify

Expected: both commands exit 0.

## Validation and Acceptance

Behavioral acceptance checks after tests pass:

1. Env-first runtime:

    ./bin/clinic --env staging env current

Expected JSON indicates `active_env` state remains valid and command executes without requiring profile.

2. No active profile in env:

    ./bin/clinic --env dev profile current

Expected JSON includes `"env":"dev"` and `"profile":null`.

3. Manual profile creation:

    ./bin/clinic --env dev profile set my-dev --cluster-id <cluster-id>
    ./bin/clinic --env dev profile list

Expected: profile appears under dev with cluster metadata fields and no auto-activation.

4. Search-id auto-create:

    ./bin/clinic --env staging cluster search-id --cluster-id <cluster-id>
    ./bin/clinic --env staging profile list

Expected: new profile appears only if cluster id was previously absent in staging; active profile unchanged.

5. Precedence:

    CLINIC_CLUSTER_ID=<id> ./bin/clinic --env prod metrics query --query 'up'

Expected: command succeeds without `--cluster-id` when no profile is active, proving env-var tier remains functional.

## Idempotence and Recovery

- Safe to rerun tests and `make verify` repeatedly.
- Re-running `cluster search-id` for the same cluster in the same env is idempotent for profile creation (no duplicates by cluster-id condition).
- If a subtask introduces failing tests, revert only that subtask changes and reapply incrementally; do not continue with unresolved failures.
- If help/docs drift appears, regenerate expected assertions by reading current `help.go` strings rather than weakening tests.

## Artifacts and Notes

- Keep commit boundaries aligned to T1-T7.
- After each task commit, update `Progress` with timestamp and short evidence.
- Capture any API field-shape discrepancy (for example missing `clusterName`) in `Surprises & Discoveries` and adjust naming fallback tests accordingly.

## Interfaces and Dependencies

Core types to define or update in `internal/config/config.go`:

    type ProfileRecord struct {
        ClusterID           string `json:"cluster_id"`
        ClusterName         string `json:"cluster_name,omitempty"`
        OrgID               string `json:"org_id,omitempty"`
        ParentID            string `json:"parent_id,omitempty"`
        ClusterDeployTypeV2 string `json:"cluster_deploy_type_v2,omitempty"`
        RangeStart          string `json:"range_start,omitempty"`
        RangeEnd            string `json:"range_end,omitempty"`
    }

    type EnvProfileSet struct {
        ActiveProfile string                   `json:"active_profile,omitempty"`
        Profiles      map[string]ProfileRecord `json:"profiles,omitempty"`
    }

    type Config struct {
        ActiveEnv     string                     `json:"active_env"`
        Environments  map[string]Environment     `json:"environments"`
        EnvProfiles   map[string]EnvProfileSet   `json:"env_profiles,omitempty"`
    }

Runtime context contract in `internal/cmd/root.go`:

    type RuntimeContext struct {
        SelectedEnv         string
        SelectedProfileName string
        SelectedProfile     *config.ProfileRecord
        Config              *config.Config
        Client              Client
    }

Command contracts:

- `profile set` must call cluster lookup helper to hydrate metadata from API, not from user-supplied metadata flags.
- `cluster search-id` must keep response rendering path stable while applying optional profile auto-create side effect.

Plan update note (2026-04-04): Initial version authored from approved redesign decisions in planning session and saved for execution handoff.
