{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://skill-map.ai/spec/v1/user-settings.schema.json",
  "title": "UserSettings",
  "description": "Per-user, per-machine settings file persisted at `~/.skill-map/settings.json`. Holds the small set of preferences that genuinely belong to the operator (not to a project) plus the bookkeeping each one needs. The file is NOT part of the project config layer system (no merge, no PROJECT_LOCAL_ONLY_KEYS interaction); it is read directly by the few modules that own a user-scope feature. See `spec/cli-contract.md` §Scope is always project-local for the broader principle: skill-map never reads `$HOME` by default, this file is the narrow, documented exception. There is intentionally no `.local` partner; values here are already per-machine, so the project / project-local split would have no meaning.",
  "type": "object",
  "required": ["schemaVersion"],
  "additionalProperties": false,
  "properties": {
    "schemaVersion": {
      "type": "integer",
      "const": 1,
      "description": "Shape version of this file. Bumped only on breaking changes to the on-disk shape. Pre-1.0 the value stays `1`; future migrations land alongside the bump."
    },
    "updateCheck": {
      "type": "object",
      "additionalProperties": false,
      "description": "User toggle + boot-time throttle bookkeeping for the once-per-day 'new version available' probe. The toggle is a real preference; the timestamps are opaque bookkeeping the CLI maintains so it does not spam the user. Both live in the same sub-object because they belong to the same feature.",
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Operator opt-out toggle. Default `true` when absent. The CLI / Settings UI persists changes through `PATCH /api/preferences`."
        },
        "latestVersion": {
          "type": ["string", "null"],
          "description": "Latest @skill-map/cli version observed at the last npm-registry probe. `null` (or absent) when never probed."
        },
        "checkedAt": {
          "type": ["integer", "null"],
          "description": "Unix milliseconds of the last npm-registry probe. `null` (or absent) when never probed. Used to throttle the daily probe."
        },
        "shownAt": {
          "type": ["integer", "null"],
          "description": "Unix milliseconds of the last banner emission to stderr. `null` (or absent) when never shown. Used so a single probe does not re-emit the banner across back-to-back `sm` invocations."
        }
      }
    },
    "githubStars": {
      "type": "object",
      "additionalProperties": false,
      "description": "User toggle for the repository star count the UI surfaces (topbar + Settings → About). Lives here rather than in the project config because it is a preference of the operator, not of the project, and because it governs an outbound network read, the same reasoning that put `updateCheck` here. No bookkeeping fields: the count is machine output with no human judgment in it, so it is cached in the running server's memory and re-read on demand rather than persisted (see `cli-contract.md` §`GET /api/github-stars`).",
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Operator opt-out toggle. Default `true` when absent. When `false`, the server performs no request to api.github.com and the endpoint answers with a null count, which renders nothing at all. Persisted through `PATCH /api/preferences`."
        }
      }
    },
    "ui": {
      "type": "object",
      "additionalProperties": false,
      "description": "Per-machine UI bookkeeping. `dismissedNotes` holds the ids of one-time informational notes the operator closed (e.g. the Sessions tab's recording intro), so a note dismissed once stays dismissed on every project this machine opens.",
      "properties": {
        "dismissedNotes": {
          "type": "array",
          "items": { "type": "string", "minLength": 1 },
          "maxItems": 64
        }
      }
    },
    "telemetry": {
      "type": "object",
      "additionalProperties": false,
      "description": "User consent + prompt bookkeeping for the opt-in, anonymous telemetry surfaces (see `spec/telemetry.md`). All surfaces default OFF and run only after explicit consent. Three independent toggles (`errorsEnabled` for Sentry error reporting, `usageCliEnabled` / `usageUiEnabled` for PostHog usage analytics) plus the usage `distinct_id` (`anonymousId`) and the shared prompt bookkeeping (`firstRunAt`, `promptedAt`) the CLI maintains so it asks once, at the right time.",
      "properties": {
        "errorsEnabled": {
          "type": "boolean",
          "description": "Operator opt-in toggle for error reporting (Sentry). **Default OFF**: when absent or `false`, no Sentry SDK is initialised and no event leaves the machine. Set to `true` only after explicit consent (the consent prompt or Settings UI). The `SKILL_MAP_TELEMETRY=0` env var forces OFF regardless of this value."
        },
        "usageCliEnabled": {
          "type": "boolean",
          "description": "Operator opt-in toggle for CLI usage analytics (PostHog). **Default OFF**: when absent or `false`, no PostHog SDK is initialised in the CLI and no usage event leaves the machine. Independent of `errorsEnabled` and `usageUiEnabled`; the `SKILL_MAP_TELEMETRY=0` env var forces OFF regardless."
        },
        "usageUiEnabled": {
          "type": "boolean",
          "description": "Operator opt-in toggle for UI usage analytics (PostHog). **Default OFF**: when absent or `false`, the browser never loads the PostHog SDK. Independent of `errorsEnabled` and `usageCliEnabled`; the `SKILL_MAP_TELEMETRY=0` env var forces OFF regardless."
        },
        "anonymousId": {
          "type": ["string", "null"],
          "description": "Random UUID v4 used as the PostHog `distinct_id` for the usage surface, shared by the CLI and UI so both are attributed to one install. Carries no personal data. Minted exactly once, the first time any usage toggle becomes `true`, and never regenerated. The single anonymous correlation id the contract permits, scoped to usage only; the BFF exposes it read-only and it is never writable over the wire. `null` (or absent) until usage is first enabled."
        },
        "firstRunAt": {
          "type": ["integer", "null"],
          "description": "Unix milliseconds of the first run on which the consent prompt was eligible to appear (interactive TTY, at least one carrier configured, not opted-out by env, not yet answered). The prompt is intentionally deferred to the NEXT eligible run so it does not stack on top of the first-run provider-lens prompt. `null` (or absent) before any eligible run."
        },
        "promptedAt": {
          "type": ["integer", "null"],
          "description": "Unix milliseconds of the moment the shared consent prompt was shown. `null` (or absent) when the user has never been prompted. Once set, the prompt is never shown again, the persisted toggles are authoritative."
        }
      }
    }
  }
}
