// Copyright 2026 AlphaOne LLC
// SPDX-License-Identifier: Apache-2.0

//! v0.6.4-001 — `Profile` resolution for the MCP tool surface.
//!
//! A profile is a set of tool *families* (`Family`) that the MCP server
//! advertises in its `tools/list` response. v0.6.4 collapses the default
//! surface from 43 tools (full) to 5 (core) so eager-loading harnesses
//! stop pre-paying ~6,000 input tokens of tool schemas per request. The
//! 38 tools outside `core` remain reachable via runtime expansion through
//! `memory_capabilities --include-schema family=<name>` (Track C —
//! v0.6.4-006), so no functionality is lost; only the eager prefix cost
//! goes away.
//!
//! ## Resolution order
//!
//! `CLI flag > AI_MEMORY_PROFILE env > [mcp].profile config > "core"`.
//!
//! `clap` natively handles "CLI > env" with `#[arg(env = "...")]`, so
//! the daemon-runtime side only needs to call
//! [`AppConfig::effective_profile`] with the resolved CLI/env value
//! (already merged by clap) plus the config-file value (read by
//! `serde`).
//!
//! ## Profile vocabulary
//!
//! - `core` — 7 tools, the new v0.6.4 default (v0.7 B1 added
//!   `memory_load_family`; v0.7 B2 added `memory_smart_load`).
//!   Always loaded.
//! - `graph` — adds the 11 KG/entity/replay/verify/find_paths tools. ~18 tools.
//! - `admin` — adds lifecycle (6) + governance (8). ~21 tools.
//! - `power` — adds the 8 LLM-augmented + operator tools (consolidate,
//!   auto_tag, …, plus the v0.7 K7 subscription-reliability pair).
//!   ~15 tools.
//! - `full` — every family. **74 advertised entries at v0.7.0**
//!   (73 callable "memory tools" + the always-on `memory_capabilities`
//!   bootstrap; `Profile::full().expected_tool_count()` is the
//!   canonical assertion).
//! - `custom` — comma-separated family list (`core,graph,archive` …).
//!   `core` is implicitly added if missing — there's no profile that
//!   ships *less than* the 7 core tools at v0.7.0 (the original 5 +
//!   `memory_load_family` + `memory_smart_load`).
//!
//! ## Custom-profile parsing edge cases
//!
//! Documented in this RFC + pinned by unit tests:
//!
//! - empty string → `Profile::core()` (default)
//! - `core,core` → dedupe silently
//! - `core,xyz` → `ProfileParseError::UnknownFamily("xyz")` listing
//!   every valid family name
//! - mixed-case (`Core`) → `ProfileParseError::CaseMismatch`. Profiles
//!   are case-sensitive lowercase. Rejecting mixed case prevents
//!   `Profile` vs `profile` config-file divergence from creating two
//!   different surfaces in production.
//! - whitespace-only token (`core, ,graph`) → silently skipped
//! - `core,full` → `Profile::full()` (full subsumes everything; not an
//!   error)
//! - duplicates across the named-then-custom path (`full,core`) → also
//!   resolves to full.

use std::str::FromStr;

/// A tool family. Source-anchored at `crate::mcp::registry::tool_definitions()`
/// 2026-05-05. Counts must sum to 51 (the v0.6.3.1 baseline of 43 +
/// v0.7.0 I4 `memory_replay` + v0.7 H4 `memory_verify` (both in
/// `Family::Graph`) + v0.7 B1 `memory_load_family` and v0.7 B2
/// `memory_smart_load` in `Family::Core` +
/// v0.7 K7 `memory_subscription_replay` and `memory_subscription_dlq_list`
/// in `Family::Power` + v0.7 J7 `memory_find_paths` in `Family::Graph` +
/// v0.7 K8 `memory_quota_status` in `Family::Power`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Family {
    /// store, recall, list, get, search, load_family, smart_load — 7
    /// (load_family added in v0.7 B1 — always-on family loader that
    /// returns the top-k recent + high-priority memories whose
    /// `metadata.family` matches one of the eight enum names;
    /// smart_load added in v0.7 B2 — intent-routed front door that
    /// picks the best Family from a free-text intent and forwards to
    /// `memory_load_family`.)
    Core,
    /// update, delete, forget, gc, promote — 5
    Lifecycle,
    /// kg_query, kg_timeline, kg_invalidate, link, get_links,
    /// entity_register, entity_get_by_alias, get_taxonomy, replay,
    /// verify, find_paths — 11 (replay added in v0.7.0 I4 — joins to the I2
    /// transcript-link substrate to reconstruct a memory's source
    /// transcript chain; verify added in v0.7 H4 — re-checks the
    /// Ed25519 signature on a stored memory_links row.)
    Graph,
    /// pending_list/approve/reject, namespace_set/get/clear_standard,
    /// subscribe, unsubscribe — 8
    Governance,
    /// consolidate, detect_contradiction, check_duplicate, auto_tag,
    /// expand_query, inbox, subscription_replay, subscription_dlq_list,
    /// quota_status — 9 (v0.7 K7 added the two
    /// operator/governance subscription-reliability tools — replay
    /// events from the audit log + inspect the DLQ; v0.7 K8 added
    /// `memory_quota_status` for the per-agent rate-limit + storage-cap
    /// substrate.)
    Power,
    /// capabilities, agent_register, agent_list, session_start, stats — 5
    Meta,
    /// archive_list, archive_purge, archive_restore, archive_stats — 4
    Archive,
    /// list_subscriptions, notify — 2
    Other,
}

/// Tool names that are loaded in every profile, regardless of which
/// families it includes. v0.6.4 reserves `memory_capabilities` as the
/// always-on bootstrap so the runtime-discovery dance works out of the
/// box on `--profile core`. Per RFC S27 and the v0.6.4-002 acceptance
/// criteria.
///
/// v0.7.x (issue #1174 PR1 — pm-v3.1 MCP tool name sweep): the
/// literal references the canonical const so this slice cannot drift
/// from the dispatch table.
///
/// DOC-7 (med/low review batch) — semantic note: this is a sentinel
/// list with `len == 1` at v0.7.0 (only `memory_capabilities`). The
/// `&[…]` slice shape is intentionally extensible: future profile
/// work may promote additional bootstrap-class tools (e.g.
/// `memory_load_family`, `memory_smart_load`) into the always-on set,
/// at which point [`Profile::core`] would need to subtract them from
/// the per-family count to avoid double-counting. Today the family
/// counts treat `memory_load_family` and `memory_smart_load` as core
/// family members, NOT as always-on; promotion would require updating
/// `Profile::expected_tool_count` arithmetic accordingly.
pub const ALWAYS_ON_TOOLS: &[&str] = &[crate::mcp::registry::tool_names::MEMORY_CAPABILITIES];

impl Family {
    /// Lookup the family that owns a given tool name. Source-anchored
    /// at `crate::mcp::registry::tool_definitions()` 2026-05-04. Every name listed
    /// in the v0.6.3.1 baseline is covered; `None` means the tool is
    /// either unknown to this enumeration or moved out of bounds (which
    /// should make `tool_definitions_returns_43_tools` red and force a
    /// reconciliation).
    #[must_use]
    pub fn for_tool(name: &str) -> Option<Self> {
        // v0.7.x (issue #1174 PR1 — pm-v3.1 MCP tool name sweep) — every
        // match arm references a const from
        // [`crate::mcp::registry::tool_names`] so the family routing
        // table and the dispatch table cannot drift in name spelling.
        use crate::mcp::registry::tool_names as tn;
        match name {
            // core (7 — v0.7 B1 added memory_load_family as the always-on
            // alternative to memory_recall when the agent already knows
            // which family taxonomy it wants; v0.7 B2 added
            // memory_smart_load as the intent-routed front door that
            // picks the best family for the caller).
            tn::MEMORY_STORE | tn::MEMORY_RECALL | tn::MEMORY_LIST | tn::MEMORY_GET
            | tn::MEMORY_SEARCH | tn::MEMORY_LOAD_FAMILY | tn::MEMORY_SMART_LOAD => {
                Some(Self::Core)
            }
            // lifecycle (6 — v0.7.0 #1389 L4 added memory_capture_turn, the
            // host-volunteered idempotent turn-capture substrate primitive).
            tn::MEMORY_UPDATE | tn::MEMORY_DELETE | tn::MEMORY_FORGET | tn::MEMORY_GC
            | tn::MEMORY_PROMOTE | tn::MEMORY_CAPTURE_TURN => Some(Self::Lifecycle),
            // graph (11 — v0.7.0 I4 added memory_replay; v0.7 H4 added memory_verify;
            // v0.7 J7 added memory_find_paths)
            tn::MEMORY_KG_QUERY
            | tn::MEMORY_KG_TIMELINE
            | tn::MEMORY_KG_INVALIDATE
            | tn::MEMORY_LINK
            | tn::MEMORY_GET_LINKS
            | tn::MEMORY_ENTITY_REGISTER
            | tn::MEMORY_ENTITY_GET_BY_ALIAS
            | tn::MEMORY_GET_TAXONOMY
            | tn::MEMORY_REPLAY
            | tn::MEMORY_VERIFY
            | tn::MEMORY_FIND_PATHS => Some(Self::Graph),
            // governance (8)
            tn::MEMORY_PENDING_LIST
            | tn::MEMORY_PENDING_APPROVE
            | tn::MEMORY_PENDING_REJECT
            | tn::MEMORY_NAMESPACE_SET_STANDARD
            | tn::MEMORY_NAMESPACE_GET_STANDARD
            | tn::MEMORY_NAMESPACE_CLEAR_STANDARD
            | tn::MEMORY_SUBSCRIBE
            | tn::MEMORY_UNSUBSCRIBE => Some(Self::Governance),
            // power (23 — the actual count; v0.7 K7 added the
            // subscription-reliability pair (`memory_subscription_replay`
            // + `memory_subscription_dlq_list`); v0.7 K8 added
            // `memory_quota_status`; v0.7.0 Task 4/8 added
            // `memory_reflect`; v0.7.0 L2-2/L2-3 added
            // `memory_reflection_origin` + `memory_dependents_of_invalidated`;
            // v0.7.0 QW-1 added `memory_export_reflection`; v0.7.0 QW-2
            // added `memory_persona` + `memory_persona_generate`; v0.7.0
            // QW-3 follow-up added `memory_offload` + `memory_deref`;
            // v0.7.0 WT-1-C added `memory_atomise`; v0.7.0 Form 3 added
            // `memory_ingest_multistep`; v0.7.0 Form 5 added
            // `memory_calibrate_confidence`; v0.7.0 #691 added
            // `memory_check_agent_action` + `memory_rule_list`; v0.7.0
            // #224/#311 added `memory_share`. All operator/governance,
            // not data-plane. Pinned by `Family::expected_tool_count`
            // (derived from this slice) and the
            // `family_tool_names_cover_registry_all` test.
            tn::MEMORY_CONSOLIDATE
            | tn::MEMORY_DETECT_CONTRADICTION
            | tn::MEMORY_CHECK_DUPLICATE
            | tn::MEMORY_AUTO_TAG
            | tn::MEMORY_EXPAND_QUERY
            | tn::MEMORY_INBOX
            | tn::MEMORY_SUBSCRIPTION_REPLAY
            | tn::MEMORY_SUBSCRIPTION_DLQ_LIST
            | tn::MEMORY_QUOTA_STATUS
            | tn::MEMORY_REFLECT
            | tn::MEMORY_REFLECTION_ORIGIN
            // v0.7.0 QW-1 — file-backed reflection chain export.
            // Operator-facing; substrate returns rendered content,
            // agent harness owns the disk write.
            | tn::MEMORY_EXPORT_REFLECTION
            // v0.7.0 QW-2 — Persona-as-artifact. The read-only
            // `memory_persona` lookup and the write-side
            // `memory_persona_generate` regeneration both sit under
            // Power. Tier-gating in the MCP dispatcher refuses the
            // write-side surface unless smart+autonomous is enabled.
            | tn::MEMORY_PERSONA
            | tn::MEMORY_PERSONA_GENERATE
            // v0.7.0 Form 5 (issue #758) — calibration sweep over the
            // shadow-mode observation table. Operator-callable
            // equivalent of `ai-memory calibrate confidence
            // --from-shadow`. Lives in Power alongside the other
            // operator-facing observability tools.
            | tn::MEMORY_CALIBRATE_CONFIDENCE
            // v0.7.0 L2-3 (issue #668) — read-side surface for the
            // reflection invalidation propagation walker. Operator-
            // facing inspector for the per-reflection dependent set
            // that gets notified on Reflection→Reflection supersedes.
            | tn::MEMORY_DEPENDENTS_OF_INVALIDATED
            // v0.7.0 (issue #691) — substrate-level agent-action rules
            // engine. Both tools live in Family::Power (governance /
            // operator-facing, not data-plane). Mutation tools are
            // explicitly NOT registered over MCP per design revision
            // 2026-05-13 — operator uses CLI / HTTP with signed key.
            | tn::MEMORY_CHECK_AGENT_ACTION
            | tn::MEMORY_RULE_LIST
            // v0.7.0 QW-3 follow-up — context-offload substrate primitive.
            // The pair lives in Family::Power so the `power` (and `full`)
            // profile surfaces them while keeping the keyword-tier
            // `core` surface unchanged (semantic-tier+ exposure per the
            // QW-3 brief).
            | tn::MEMORY_OFFLOAD
            | tn::MEMORY_DEREF
            // v0.7.0 WT-1-C — curator-pass atomisation tool. Lives in
            // the same family/profile group as memory_consolidate and
            // memory_reflect (semantic+ tier; the keyword tier short-
            // circuits with a tier-locked advisory envelope).
            | tn::MEMORY_ATOMISE
            // v0.7.0 Form 3 (issue #756) — multi-step ingest
            // orchestrator. Lives at Family::Power alongside the other
            // LLM-driven write-side tools; tier-gated to smart+ with
            // the standard tier-locked advisory on keyword.
            | tn::MEMORY_INGEST_MULTISTEP
            // v0.7.0 (issues #224 + #311) — Phase 3 Memory Sharing &
            // Sync RFC pulled forward per operator directive
            // `28860423-d12c-4959-bc8b-8fa9a94a33d9`. Substrate-level
            // point-to-point copy into `_shared/<from>→<to>/`.
            | tn::MEMORY_SHARE
            // v0.8.0 Pillar 1 (#1709) — coordination-action create/get.
            // Operator/substrate surface over the action DAG; lives in
            // Power alongside the other write-side substrate tools.
            | tn::MEMORY_ACTION_CREATE
            | tn::MEMORY_ACTION_GET
            | tn::MEMORY_ACTION_TRANSITION
            | tn::MEMORY_ACTION_LIST
            | tn::MEMORY_ACTION_ADD_EDGE
            | tn::MEMORY_ACTION_EDGES
            // v0.8.0 Pillar 1 (#1709 §11.4) — FRONTIER surface.
            | tn::MEMORY_ACTION_FRONTIER
            | tn::MEMORY_ACTION_NEXT
            // v0.8.0 Pillar 1 (#1709) — coordination LEASE surface
            // (acquire/renew/release/get) over the action DAG.
            | tn::MEMORY_LEASE_ACQUIRE
            | tn::MEMORY_LEASE_RENEW
            | tn::MEMORY_LEASE_RELEASE
            | tn::MEMORY_LEASE_GET
            // v0.8.0 Pillar 1 (#1709) — signed-signal coordination surface
            // (send/read/inbox/thread/ack) over the `crate::signals` substrate.
            | tn::MEMORY_SIGNAL_SEND
            | tn::MEMORY_SIGNAL_READ
            | tn::MEMORY_SIGNAL_INBOX
            | tn::MEMORY_SIGNAL_THREAD
            | tn::MEMORY_SIGNAL_ACK
            // v0.8.0 Pillar 1 (#1709) — attested-checkpoint coordination surface
            // (create/resolve/query/verify) over the `crate::checkpoints` substrate.
            | tn::MEMORY_CHECKPOINT_CREATE
            | tn::MEMORY_CHECKPOINT_RESOLVE
            | tn::MEMORY_CHECKPOINT_QUERY
            | tn::MEMORY_CHECKPOINT_VERIFY
            // v0.8.0 Pillar 1 (#1709) — routine coordination surface
            // (create/freeze/run/status/list) over the `crate::routines` substrate.
            | tn::MEMORY_ROUTINE_CREATE
            | tn::MEMORY_ROUTINE_FREEZE
            | tn::MEMORY_ROUTINE_RUN
            | tn::MEMORY_ROUTINE_STATUS
            | tn::MEMORY_ROUTINE_LIST => Some(Self::Power),
            // meta (6 — 5 baseline + v0.7.0 Gap 3 (#886)
            // memory_recall_observations).
            tn::MEMORY_CAPABILITIES
            | tn::MEMORY_AGENT_REGISTER
            | tn::MEMORY_AGENT_LIST
            | tn::MEMORY_SESSION_START
            | tn::MEMORY_STATS
            | tn::MEMORY_RECALL_OBSERVATIONS => Some(Self::Meta),
            // archive (4)
            tn::MEMORY_ARCHIVE_LIST
            | tn::MEMORY_ARCHIVE_PURGE
            | tn::MEMORY_ARCHIVE_RESTORE
            | tn::MEMORY_ARCHIVE_STATS => Some(Self::Archive),
            // other (9 — 2 baseline + v0.7.0 L1-5 5 skill tools +
            // v0.7.0 L2-6 memory_skill_promote_from_reflection (#671) +
            // v0.7.0 L2-7 memory_skill_compositional_context (#672))
            tn::MEMORY_LIST_SUBSCRIPTIONS
            | tn::MEMORY_NOTIFY
            | tn::MEMORY_SKILL_REGISTER
            | tn::MEMORY_SKILL_LIST
            | tn::MEMORY_SKILL_GET
            | tn::MEMORY_SKILL_RESOURCE
            | tn::MEMORY_SKILL_EXPORT
            | tn::MEMORY_SKILL_PROMOTE_FROM_REFLECTION
            | tn::MEMORY_SKILL_COMPOSITIONAL_CONTEXT => Some(Self::Other),
            _ => None,
        }
    }

    /// Lowercase canonical name as used in CLI/env/config.
    #[must_use]
    pub const fn name(self) -> &'static str {
        match self {
            Self::Core => "core",
            Self::Lifecycle => "lifecycle",
            Self::Graph => "graph",
            Self::Governance => crate::models::field_names::GOVERNANCE,
            Self::Power => "power",
            Self::Meta => "meta",
            Self::Archive => "archive",
            Self::Other => "other",
        }
    }

    /// All eight families in declaration order. Useful for `--profile full`
    /// and for the `ProfileParseError::UnknownFamily` diagnostic.
    #[must_use]
    pub const fn all() -> &'static [Family] {
        &[
            Self::Core,
            Self::Lifecycle,
            Self::Graph,
            Self::Governance,
            Self::Power,
            Self::Meta,
            Self::Archive,
            Self::Other,
        ]
    }

    /// Number of MCP tools advertised by this family.
    ///
    /// Derived from [`Family::tool_names`] — that slice is the single
    /// source of truth for both the names AND the count. Adding a tool
    /// to a family is therefore exactly one edit (append a `tn::*`
    /// entry to the slice arm); every count that depends on it —
    /// per-profile expectations, the full-profile total, the registry
    /// lockstep — recomputes automatically. There are NO hand-maintained
    /// per-family magic numbers here by design (the historical
    /// `match self { Core => 7, … }` form drifted whenever a tool landed
    /// without the matching count bump).
    #[must_use]
    pub const fn expected_tool_count(self) -> usize {
        self.tool_names().len()
    }

    /// v0.7.0 A2 — tool names belonging to this family. Forward of the
    /// `Family::for_tool` reverse map; source-anchored at
    /// `crate::mcp::registry::tool_definitions()` 2026-05-04 (same anchor as
    /// [`Family::for_tool`] and [`Family::expected_tool_count`]).
    /// Order is the order each tool appears in
    /// `tool_definitions_for_profile`'s registration walk, so an
    /// LLM-facing preview ("the first three tools loaded") aligns with
    /// the actual `tools/list` output.
    ///
    /// This slice is the single source of truth for the family's tool
    /// set. [`Family::expected_tool_count`] derives its return value
    /// from `self.tool_names().len()`, and the
    /// `family_tool_names_cover_registry_all` unit test pins the union
    /// of all families against the canonical registry set.
    #[must_use]
    pub const fn tool_names(self) -> &'static [&'static str] {
        // v0.7.x (issue #1174 PR1 — pm-v3.1 MCP tool name sweep) — every
        // entry references a `pub const` from
        // [`crate::mcp::registry::tool_names`] so the per-family lists,
        // the dispatch table, and the registry iterator cannot drift
        // in name spelling.
        use crate::mcp::registry::tool_names as tn;
        match self {
            Self::Core => &[
                tn::MEMORY_STORE,
                tn::MEMORY_RECALL,
                tn::MEMORY_LIST,
                tn::MEMORY_GET,
                tn::MEMORY_SEARCH,
                // v0.7 B1 — always-on alternative to memory_recall when
                // the agent already knows the Family taxonomy it wants.
                tn::MEMORY_LOAD_FAMILY,
                // v0.7 B2 — intent-routed front door. Caller passes a
                // free-text intent; the handler picks the best family
                // from the cached descriptors and forwards to
                // `memory_load_family`.
                tn::MEMORY_SMART_LOAD,
            ],
            Self::Lifecycle => &[
                tn::MEMORY_UPDATE,
                tn::MEMORY_DELETE,
                tn::MEMORY_FORGET,
                tn::MEMORY_GC,
                tn::MEMORY_PROMOTE,
                // v0.7.0 #1389 L4 — host-volunteered idempotent turn
                // capture (RFC-0001). One memory row + one
                // transcript_line_dedup row per host turn.
                tn::MEMORY_CAPTURE_TURN,
            ],
            Self::Graph => &[
                tn::MEMORY_KG_QUERY,
                tn::MEMORY_KG_TIMELINE,
                tn::MEMORY_KG_INVALIDATE,
                tn::MEMORY_LINK,
                tn::MEMORY_GET_LINKS,
                tn::MEMORY_ENTITY_REGISTER,
                tn::MEMORY_ENTITY_GET_BY_ALIAS,
                tn::MEMORY_GET_TAXONOMY,
                // v0.7.0 I4 — traverses memory_transcript_links (I2) to
                // reconstruct the source-transcript chain for a memory.
                tn::MEMORY_REPLAY,
                // v0.7 H4 — re-verifies a stored link's Ed25519
                // signature on demand, returning attest_level.
                tn::MEMORY_VERIFY,
                // v0.7 J7 — enumerate up to N paths between two memories
                // (BFS with cycle detection over memory_links).
                tn::MEMORY_FIND_PATHS,
            ],
            Self::Governance => &[
                tn::MEMORY_PENDING_LIST,
                tn::MEMORY_PENDING_APPROVE,
                tn::MEMORY_PENDING_REJECT,
                tn::MEMORY_NAMESPACE_SET_STANDARD,
                tn::MEMORY_NAMESPACE_GET_STANDARD,
                tn::MEMORY_NAMESPACE_CLEAR_STANDARD,
                tn::MEMORY_SUBSCRIBE,
                tn::MEMORY_UNSUBSCRIBE,
            ],
            Self::Power => &[
                tn::MEMORY_CONSOLIDATE,
                tn::MEMORY_DETECT_CONTRADICTION,
                tn::MEMORY_CHECK_DUPLICATE,
                tn::MEMORY_AUTO_TAG,
                tn::MEMORY_EXPAND_QUERY,
                tn::MEMORY_INBOX,
                // v0.7 K7 — operator/governance subscription-reliability
                // tools. Replay reads back the audit row series for one
                // subscription since an RFC3339 cursor; dlq_list inspects
                // payloads that exhausted the [200ms, 1s, 5s] retry ladder.
                tn::MEMORY_SUBSCRIPTION_REPLAY,
                tn::MEMORY_SUBSCRIPTION_DLQ_LIST,
                // v0.7 K8 — per-agent quota status (memories/day, storage
                // bytes, links/day). Operator-facing inspector for the K8
                // rate-limit substrate.
                tn::MEMORY_QUOTA_STATUS,
                // v0.7.0 Task 4/8 (recursive learning, issue #655) —
                // substrate-native reflection primitive. Inserts a
                // reflection memory plus N `reflects_on` provenance
                // links in a single atomic transaction.
                tn::MEMORY_REFLECT,
                // v0.7.0 L2-2 (S6-M1) — cross-peer reflection origin
                // inspector. Returns peer_origin / signing_agent /
                // original_depth / local_depth_at_arrival for a row.
                tn::MEMORY_REFLECTION_ORIGIN,
                // v0.7.0 L2-3 (issue #668) — invalidation propagation
                // read-side inspector. Lists dependents flagged by
                // the walker on Reflection→Reflection supersedes.
                tn::MEMORY_DEPENDENTS_OF_INVALIDATED,
                // v0.7.0 (issue #691) — substrate-level agent-action
                // rules engine. Read-side surface; mutation tools are
                // NOT registered over MCP (operator uses CLI / HTTP).
                tn::MEMORY_CHECK_AGENT_ACTION,
                tn::MEMORY_RULE_LIST,
                // v0.7.0 QW-1 — file-backed reflection chain export
                // companion. Renders the markdown / JSON envelope;
                // does NOT write to disk (agent harness owns disk I/O).
                tn::MEMORY_EXPORT_REFLECTION,
                // v0.7.0 QW-3 follow-up — context-offload substrate
                // primitive (offload + deref). Power-family registration
                // gives semantic-tier+ exposure per the QW-3 brief; the
                // handlers themselves live at src/mcp/tools/offload.rs.
                tn::MEMORY_OFFLOAD,
                tn::MEMORY_DEREF,
                // v0.7.0 WT-1-C — curator-pass atomisation tool.
                // Decomposes a coarse memory into 2-10 atomic
                // propositions; archives the source. Lives in Power
                // alongside memory_consolidate / memory_reflect.
                tn::MEMORY_ATOMISE,
                // v0.7.0 QW-2 — Persona-as-artifact. Read-only lookup
                // + smart+ regeneration. Substrate writes the SQL row
                // (and optionally the filesystem export via namespace
                // policy); the agent never holds the keypair.
                tn::MEMORY_PERSONA,
                tn::MEMORY_PERSONA_GENERATE,
                // v0.7.0 Form 3 (issue #756) — multi-step ingest
                // orchestrator. Deterministic helpers + LLM stages
                // with explicit-trust slots and prompt-cache reuse.
                tn::MEMORY_INGEST_MULTISTEP,
                // v0.7.0 Form 5 (issue #758) — calibration sweep over
                // the shadow-mode observation table. Operator surface
                // for tuning per-(namespace, source) confidence
                // baselines.
                tn::MEMORY_CALIBRATE_CONFIDENCE,
                // v0.7.0 (issues #224 + #311) — Phase 3 Memory Sharing &
                // Sync RFC pulled forward per operator directive
                // `28860423-d12c-4959-bc8b-8fa9a94a33d9`. Substrate-
                // level point-to-point copy into `_shared/<from>→<to>/`.
                tn::MEMORY_SHARE,
                // v0.8.0 Pillar 1 (#1709) — coordination-action DAG surface
                // (create/get + transition/list/add_edge/edges) over the
                // `crate::actions` substrate.
                tn::MEMORY_ACTION_CREATE,
                tn::MEMORY_ACTION_GET,
                tn::MEMORY_ACTION_TRANSITION,
                tn::MEMORY_ACTION_LIST,
                tn::MEMORY_ACTION_ADD_EDGE,
                tn::MEMORY_ACTION_EDGES,
                // v0.8.0 Pillar 1 (#1709 §11.4) — FRONTIER surface
                // (ranked unblocked actions + the next action to do).
                tn::MEMORY_ACTION_FRONTIER,
                tn::MEMORY_ACTION_NEXT,
                // v0.8.0 Pillar 1 (#1709) — coordination LEASE surface
                // (acquire/renew/release/get) over the action DAG.
                tn::MEMORY_LEASE_ACQUIRE,
                tn::MEMORY_LEASE_RENEW,
                tn::MEMORY_LEASE_RELEASE,
                tn::MEMORY_LEASE_GET,
                // v0.8.0 Pillar 1 (#1709) — signed-signal coordination surface
                // (send/read/inbox/thread/ack) over the `crate::signals` substrate.
                tn::MEMORY_SIGNAL_SEND,
                tn::MEMORY_SIGNAL_READ,
                tn::MEMORY_SIGNAL_INBOX,
                tn::MEMORY_SIGNAL_THREAD,
                tn::MEMORY_SIGNAL_ACK,
                // v0.8.0 Pillar 1 (#1709) — attested-checkpoint coordination surface
                // (create/resolve/query/verify) over the `crate::checkpoints` substrate.
                tn::MEMORY_CHECKPOINT_CREATE,
                tn::MEMORY_CHECKPOINT_RESOLVE,
                tn::MEMORY_CHECKPOINT_QUERY,
                tn::MEMORY_CHECKPOINT_VERIFY,
                // v0.8.0 Pillar 1 (#1709) — routine coordination surface
                // (create/freeze/run/status/list) over the
                // `crate::routines` parameterised-template substrate.
                tn::MEMORY_ROUTINE_CREATE,
                tn::MEMORY_ROUTINE_FREEZE,
                tn::MEMORY_ROUTINE_RUN,
                tn::MEMORY_ROUTINE_STATUS,
                tn::MEMORY_ROUTINE_LIST,
            ],
            Self::Meta => &[
                tn::MEMORY_CAPABILITIES,
                tn::MEMORY_AGENT_REGISTER,
                tn::MEMORY_AGENT_LIST,
                tn::MEMORY_SESSION_START,
                tn::MEMORY_STATS,
                // v0.7.0 Gap 3 (#886) — read-side surface for the
                // `recall_observations` ledger.
                tn::MEMORY_RECALL_OBSERVATIONS,
            ],
            Self::Archive => &[
                tn::MEMORY_ARCHIVE_LIST,
                tn::MEMORY_ARCHIVE_PURGE,
                tn::MEMORY_ARCHIVE_RESTORE,
                tn::MEMORY_ARCHIVE_STATS,
            ],
            Self::Other => &[
                tn::MEMORY_LIST_SUBSCRIPTIONS,
                tn::MEMORY_NOTIFY,
                // v0.7.0 L1-5 — Agent Skills ingestion substrate (Pillar 1.5).
                tn::MEMORY_SKILL_REGISTER,
                tn::MEMORY_SKILL_LIST,
                tn::MEMORY_SKILL_GET,
                tn::MEMORY_SKILL_RESOURCE,
                tn::MEMORY_SKILL_EXPORT,
                // v0.7.0 L2-6 (issue #671) — closing the recursive-learning loop.
                tn::MEMORY_SKILL_PROMOTE_FROM_REFLECTION,
                // v0.7.0 L2-7 (issue #672) — reflection-skill composition.
                tn::MEMORY_SKILL_COMPOSITIONAL_CONTEXT,
            ],
        }
    }
}

impl FromStr for Family {
    type Err = ProfileParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // Reject mixed case explicitly. Lowercase form below.
        if s.chars().any(|c| c.is_ascii_uppercase()) {
            return Err(ProfileParseError::CaseMismatch(s.to_string()));
        }
        match s {
            "core" => Ok(Self::Core),
            "lifecycle" => Ok(Self::Lifecycle),
            "graph" => Ok(Self::Graph),
            crate::models::field_names::GOVERNANCE => Ok(Self::Governance),
            "power" => Ok(Self::Power),
            "meta" => Ok(Self::Meta),
            "archive" => Ok(Self::Archive),
            "other" => Ok(Self::Other),
            unknown => Err(ProfileParseError::UnknownFamily(unknown.to_string())),
        }
    }
}

/// A resolved tool profile — the set of families to register on the
/// MCP server.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Profile {
    families: Vec<Family>,
}

impl Profile {
    /// `core` — 7 tools (`store, recall, list, get, search,
    /// load_family, smart_load`). The new v0.6.4 default; v0.7 B1
    /// added `memory_load_family` as the always-on family loader and
    /// v0.7 B2 added `memory_smart_load` as the intent-routed front
    /// door. Registers exactly the `Core` family.
    ///
    /// **Design note (v0.6.4-002 hook):** `memory_capabilities` is
    /// **always-on** regardless of profile per RFC scenario S27. It is
    /// NOT in this family list because the registration filter
    /// (v0.6.4-002) injects it as a bootstrap tool outside the
    /// profile-driven path. That keeps the "core profile = 7 tools at
    /// v0.7.0" claim accurate (5 original + memory_load_family +
    /// memory_smart_load) while still making the runtime-discovery
    /// dance reachable. Cross-check with
    /// `Profile::core().expected_tool_count()`.
    #[must_use]
    pub fn core() -> Self {
        Self {
            families: vec![Family::Core],
        }
    }

    /// `graph` — core + graph. 18 tools (v0.7.0 I4 added `memory_replay`;
    /// v0.7 H4 added `memory_verify`; v0.7 B1 added `memory_load_family`
    /// to core; v0.7 B2 added `memory_smart_load` to core; v0.7 J7
    /// added `memory_find_paths`).
    #[must_use]
    pub fn graph() -> Self {
        Self {
            families: vec![Family::Core, Family::Graph],
        }
    }

    /// `admin` — core + lifecycle + governance. 21 tools
    /// (core 7 + lifecycle 6 + governance 8; v0.7 B1 added
    /// `memory_load_family` to core; v0.7 B2 added
    /// `memory_smart_load` to core; #1389 L4 added
    /// `memory_capture_turn` to lifecycle, 20 → 21).
    #[must_use]
    pub fn admin() -> Self {
        Self {
            families: vec![Family::Core, Family::Lifecycle, Family::Governance],
        }
    }

    /// `power` — core + power. 30 tools (core 7 + power 23; v0.7 B1
    /// added `memory_load_family` to core; v0.7 B2 added `memory_smart_load`
    /// to core; v0.7 K7 added the two subscription-reliability tools
    /// to `Family::Power`).
    #[must_use]
    pub fn power() -> Self {
        Self {
            families: vec![Family::Core, Family::Power],
        }
    }

    /// `full` — every family. The advertised entry count (callable
    /// "memory tools" + the always-on `memory_capabilities` bootstrap)
    /// is whatever `Profile::full().expected_tool_count()` returns —
    /// that accessor, derived from the per-family `tool_names` slices,
    /// is the canonical SSOT; no literal is restated here.
    #[must_use]
    pub fn full() -> Self {
        Self {
            families: Family::all().to_vec(),
        }
    }

    /// Family list, sorted in declaration order, deduplicated.
    #[must_use]
    pub fn families(&self) -> &[Family] {
        &self.families
    }

    /// `true` if this profile would register tools from `family`.
    #[must_use]
    pub fn includes(&self, family: Family) -> bool {
        self.families.contains(&family)
    }

    /// Sum of expected tool counts. v0.6.4-002 will assert that the
    /// runtime registration matches.
    #[must_use]
    pub fn expected_tool_count(&self) -> usize {
        self.families.iter().map(|f| f.expected_tool_count()).sum()
    }

    /// `true` if a tool with this name is loaded under this profile.
    /// Treats every name in [`ALWAYS_ON_TOOLS`] as loaded regardless of
    /// the family map (per RFC S27 — `memory_capabilities` is the
    /// bootstrap tool for runtime discovery).
    #[must_use]
    pub fn loads(&self, tool_name: &str) -> bool {
        if ALWAYS_ON_TOOLS.contains(&tool_name) {
            return true;
        }
        Family::for_tool(tool_name).is_some_and(|f| self.includes(f))
    }

    /// Parse a profile name. Accepts the named profiles plus
    /// comma-separated family lists. Empty or whitespace-only input
    /// resolves to [`Profile::core`]. See module docs for full edge-case
    /// matrix.
    ///
    /// # Errors
    ///
    /// - [`ProfileParseError::UnknownFamily`] if a comma-separated
    ///   token is neither a known profile nor a known family.
    /// - [`ProfileParseError::CaseMismatch`] if any token contains an
    ///   uppercase letter.
    pub fn parse(s: &str) -> Result<Self, ProfileParseError> {
        let trimmed = s.trim();
        if trimmed.is_empty() {
            return Ok(Self::core());
        }

        // Reject mixed case at the whole-string level so `Core` doesn't
        // sneak past as a family (Family::from_str would also catch it,
        // but the diagnostic is clearer here).
        if trimmed.chars().any(|c| c.is_ascii_uppercase()) {
            return Err(ProfileParseError::CaseMismatch(trimmed.to_string()));
        }

        // Single named profile?
        match trimmed {
            "core" => return Ok(Self::core()),
            "graph" => return Ok(Self::graph()),
            "admin" => return Ok(Self::admin()),
            "power" => return Ok(Self::power()),
            "full" => return Ok(Self::full()),
            _ => {}
        }

        // Comma-separated. Could mix profile names and family names.
        // `core,graph` registers core+meta (from `core`) plus graph
        // (from the family). `core,full` is full because full subsumes.
        let mut families = Vec::with_capacity(8);
        for raw_token in trimmed.split(',') {
            let token = raw_token.trim();
            if token.is_empty() {
                continue;
            }
            // Each token is either a profile or a family.
            match token {
                "core" => merge(&mut families, Self::core().families()),
                "graph" => merge(&mut families, Self::graph().families()),
                "admin" => merge(&mut families, Self::admin().families()),
                "power" => merge(&mut families, Self::power().families()),
                "full" => return Ok(Self::full()),
                _ => {
                    let f = Family::from_str(token)?;
                    if !families.contains(&f) {
                        families.push(f);
                    }
                }
            }
        }

        // Every profile implicitly includes `core` — there is no
        // legitimate use case for a profile smaller than the 5
        // core tools.
        if !families.contains(&Family::Core) {
            families.insert(0, Family::Core);
        }

        // Sort into declaration order so two equivalent profile
        // strings (`graph,core` vs `core,graph`) resolve to the same
        // value.
        families.sort_unstable();
        families.dedup();

        Ok(Self { families })
    }
}

impl Default for Profile {
    fn default() -> Self {
        Self::core()
    }
}

fn merge(dst: &mut Vec<Family>, src: &[Family]) {
    for f in src {
        if !dst.contains(f) {
            dst.push(*f);
        }
    }
}

/// Errors produced by [`Profile::parse`] / [`Family::from_str`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProfileParseError {
    /// A custom-profile token was neither a known profile nor a family.
    UnknownFamily(String),
    /// A token contained an uppercase letter. Profile vocabulary is
    /// case-sensitive lowercase.
    CaseMismatch(String),
}

impl std::fmt::Display for ProfileParseError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnknownFamily(name) => {
                let valid: Vec<&str> = Family::all().iter().map(|f| f.name()).collect();
                let profiles = "core, graph, admin, power, full";
                write!(
                    f,
                    "unknown profile or family '{name}'. \
                     Valid profiles: {profiles}. \
                     Valid families: {valid}.",
                    valid = valid.join(", ")
                )
            }
            Self::CaseMismatch(s) => {
                write!(
                    f,
                    "profile '{s}' contains uppercase letters; \
                     profile vocabulary is case-sensitive lowercase \
                     (e.g. 'core', not 'Core')"
                )
            }
        }
    }
}

impl std::error::Error for ProfileParseError {}

#[cfg(test)]
mod tests {
    use super::*;

    // ---------- Family ----------

    #[test]
    fn family_all_has_eight_entries() {
        assert_eq!(Family::all().len(), 8);
    }

    #[test]
    fn family_tool_names_cover_registry_all() {
        // Cross-module SSOT invariant (no magic number): the union of
        // every family's `tool_names` slice must be exactly the
        // canonical registry set `tool_names::ALL`. Both sides are
        // hand-maintained name lists in different modules; pinning
        // their lengths against each other catches a tool added to one
        // side but not the other. The aggregate `--profile full` count
        // is whatever this union holds — it is never asserted as a
        // literal anywhere.
        let family_total: usize = Family::all().iter().map(|f| f.tool_names().len()).sum();
        assert_eq!(
            family_total,
            crate::mcp::registry::tool_names::ALL.len(),
            "per-family tool_names slices must cover exactly the registry ALL set; \
             a tool was added to one side but not the other"
        );
    }

    #[test]
    fn family_from_str_lowercase_canonical() {
        assert_eq!(Family::from_str("core").unwrap(), Family::Core);
        assert_eq!(Family::from_str("meta").unwrap(), Family::Meta);
        assert_eq!(Family::from_str("graph").unwrap(), Family::Graph);
    }

    #[test]
    fn family_from_str_rejects_mixed_case() {
        assert!(matches!(
            Family::from_str("Core"),
            Err(ProfileParseError::CaseMismatch(_))
        ));
        assert!(matches!(
            Family::from_str("CORE"),
            Err(ProfileParseError::CaseMismatch(_))
        ));
    }

    #[test]
    fn family_from_str_unknown_returns_diagnostic() {
        let err = Family::from_str("xyz").unwrap_err();
        match err {
            ProfileParseError::UnknownFamily(s) => assert_eq!(s, "xyz"),
            _ => panic!("expected UnknownFamily, got {err:?}"),
        }
    }

    // ---------- Profile named ----------

    #[test]
    fn profile_core_loads_only_core_family() {
        let p = Profile::core();
        // Core advertises exactly the Core family's tools — derived
        // from the SSOT slice, never a literal.
        assert_eq!(p.expected_tool_count(), Family::Core.tool_names().len());
        assert!(p.includes(Family::Core));
        // meta is NOT in core's family list — `memory_capabilities`
        // is bootstrapped separately as always-on per RFC S27. The
        // other meta tools (agent_register/list/session_start/stats)
        // are NOT advertised by the core profile.
        assert!(!p.includes(Family::Meta));
        assert!(!p.includes(Family::Lifecycle));
    }

    #[test]
    fn profile_graph_loads_core_plus_graph() {
        let p = Profile::graph();
        // Graph = Core + Graph families; count derived from the SSOT
        // slices so the v0.7 surface additions can't drift this test.
        assert_eq!(
            p.expected_tool_count(),
            Family::Core.tool_names().len() + Family::Graph.tool_names().len()
        );
        assert!(p.includes(Family::Graph));
    }

    #[test]
    fn profile_admin_loads_core_lifecycle_governance() {
        let p = Profile::admin();
        // admin = Core + Lifecycle + Governance families; count derived
        // from the SSOT slices. Graph isn't in admin, and the #1389 L4
        // memory_capture_turn addition to Lifecycle flows through
        // automatically rather than needing a literal bump here.
        assert_eq!(
            p.expected_tool_count(),
            Family::Core.tool_names().len()
                + Family::Lifecycle.tool_names().len()
                + Family::Governance.tool_names().len()
        );
    }

    #[test]
    fn profile_power_loads_core_plus_power() {
        let p = Profile::power();
        // Power = Core + Power families; count derived from the SSOT
        // slices so every Power-family addition flows through here
        // without a literal bump.
        assert_eq!(
            p.expected_tool_count(),
            Family::Core.tool_names().len() + Family::Power.tool_names().len()
        );
    }

    #[test]
    fn profile_full_matches_registry_all() {
        let p = Profile::full();
        // `--profile full` advertises every family. Its count is the
        // canonical registry set `tool_names::ALL` — anchored on the
        // SSOT, never a literal. This is the test that the #1389 L4
        // memory_capture_turn addition flows through automatically.
        assert_eq!(
            p.expected_tool_count(),
            crate::mcp::registry::tool_names::ALL.len()
        );

        // The `power` profile is Core + Power; same SSOT derivation.
        assert_eq!(
            Profile::power().expected_tool_count(),
            Family::Core.tool_names().len() + Family::Power.tool_names().len()
        );
    }

    // ---------- Profile::parse ----------

    #[test]
    fn parse_empty_returns_core() {
        assert_eq!(Profile::parse("").unwrap(), Profile::core());
        assert_eq!(Profile::parse("   ").unwrap(), Profile::core());
    }

    #[test]
    fn parse_named_profiles() {
        assert_eq!(Profile::parse("core").unwrap(), Profile::core());
        assert_eq!(Profile::parse("graph").unwrap(), Profile::graph());
        assert_eq!(Profile::parse("admin").unwrap(), Profile::admin());
        assert_eq!(Profile::parse("power").unwrap(), Profile::power());
        assert_eq!(Profile::parse("full").unwrap(), Profile::full());
    }

    #[test]
    fn parse_custom_comma_list_dedup() {
        // `core,graph` → Core + Graph families. Meta is NOT included —
        // `memory_capabilities` is always-on bootstrapped outside the
        // family map (v0.6.4-002). Count derived from the SSOT slices.
        let p = Profile::parse("core,graph").unwrap();
        assert!(p.includes(Family::Core));
        assert!(!p.includes(Family::Meta));
        assert!(p.includes(Family::Graph));
        assert_eq!(
            p.expected_tool_count(),
            Family::Core.tool_names().len() + Family::Graph.tool_names().len()
        );
    }

    #[test]
    fn parse_custom_dedupes_repeated_token() {
        let p = Profile::parse("core,core").unwrap();
        assert_eq!(p, Profile::core());
    }

    #[test]
    fn parse_custom_with_full_subsumes() {
        let p = Profile::parse("graph,full").unwrap();
        assert_eq!(p, Profile::full());
    }

    #[test]
    fn parse_custom_implicitly_includes_core() {
        // Asking for just `archive` should still load core because
        // there is no legitimate profile smaller than the 7 core tools at v0.7.0.
        let p = Profile::parse("archive").unwrap();
        assert!(p.includes(Family::Core));
        assert!(p.includes(Family::Archive));
    }

    #[test]
    fn parse_custom_unknown_family_errors() {
        let err = Profile::parse("core,xyz").unwrap_err();
        match err {
            ProfileParseError::UnknownFamily(s) => assert_eq!(s, "xyz"),
            _ => panic!("expected UnknownFamily, got {err:?}"),
        }
    }

    #[test]
    fn parse_rejects_mixed_case() {
        assert!(matches!(
            Profile::parse("Core"),
            Err(ProfileParseError::CaseMismatch(_))
        ));
        assert!(matches!(
            Profile::parse("core,Graph"),
            Err(ProfileParseError::CaseMismatch(_))
        ));
    }

    #[test]
    fn parse_skips_whitespace_only_tokens() {
        // `core, ,graph` should resolve to graph not error.
        let p = Profile::parse("core, ,graph").unwrap();
        assert_eq!(p, Profile::graph());
    }

    #[test]
    fn parse_order_independence() {
        // `graph,core` resolves identically to `core,graph`.
        let a = Profile::parse("core,graph").unwrap();
        let b = Profile::parse("graph,core").unwrap();
        assert_eq!(a, b);
    }

    #[test]
    fn parse_diagnostic_error_lists_valid_options() {
        let err = Profile::parse("xyz").unwrap_err();
        let msg = err.to_string();
        // The diagnostic must mention the valid profiles and families
        // so a confused operator can self-correct.
        assert!(msg.contains("core"));
        assert!(msg.contains("graph"));
        assert!(msg.contains("full"));
        assert!(msg.contains("xyz"));
    }

    #[test]
    fn default_is_core() {
        assert_eq!(Profile::default(), Profile::core());
    }

    // ---------- Tool name → family / loads ----------

    #[test]
    fn family_for_tool_resolves_every_baseline_name() {
        // Source-anchored at crate::mcp::registry::tool_definitions() — if any
        // tool here is missing from `for_tool`, the family map is
        // out of sync and `--profile <family>` would silently miss it.
        let baseline = [
            // core
            "memory_store",
            "memory_recall",
            "memory_list",
            "memory_get",
            "memory_search",
            // core (v0.7 B1 addition)
            "memory_load_family",
            // core (v0.7 B2 addition)
            "memory_smart_load",
            // lifecycle
            "memory_update",
            "memory_delete",
            "memory_forget",
            "memory_gc",
            "memory_promote",
            // graph
            "memory_kg_query",
            "memory_kg_timeline",
            "memory_kg_invalidate",
            "memory_link",
            "memory_get_links",
            "memory_entity_register",
            "memory_entity_get_by_alias",
            "memory_get_taxonomy",
            // graph (v0.7.0 I4 addition)
            "memory_replay",
            // graph (v0.7 H4 addition)
            "memory_verify",
            // graph (v0.7 J7 addition)
            "memory_find_paths",
            // governance
            "memory_pending_list",
            "memory_pending_approve",
            "memory_pending_reject",
            "memory_namespace_set_standard",
            "memory_namespace_get_standard",
            "memory_namespace_clear_standard",
            "memory_subscribe",
            "memory_unsubscribe",
            // power
            "memory_consolidate",
            "memory_detect_contradiction",
            "memory_check_duplicate",
            "memory_auto_tag",
            "memory_expand_query",
            "memory_inbox",
            // power (v0.7 K7 additions — subscription reliability)
            "memory_subscription_replay",
            "memory_subscription_dlq_list",
            // power (v0.7 K8 addition — per-agent quota status)
            "memory_quota_status",
            // power (v0.7.0 Task 4/8 — substrate-native reflection primitive)
            "memory_reflect",
            // meta
            "memory_capabilities",
            "memory_agent_register",
            "memory_agent_list",
            "memory_session_start",
            "memory_stats",
            // archive
            "memory_archive_list",
            "memory_archive_purge",
            "memory_archive_restore",
            "memory_archive_stats",
            // other
            "memory_list_subscriptions",
            "memory_notify",
            // other (v0.7.0 L1-5 — Agent Skills substrate)
            "memory_skill_register",
            "memory_skill_list",
            "memory_skill_get",
            "memory_skill_resource",
            "memory_skill_export",
            // other (v0.7.0 L2-6 — issue #671: reflections become skills)
            "memory_skill_promote_from_reflection",
            // v0.7.0 L2-7 (issue #672) — reflection-skill composition.
            "memory_skill_compositional_context",
            // v0.7.0 QW-1 — file-backed reflection chain export (Family::Power).
            "memory_export_reflection",
            // v0.7.0 QW-3 follow-up — context-offload substrate (Family::Power).
            "memory_offload",
            "memory_deref",
            // v0.7.0 WT-1-C (curator-pass atomisation) — Family::Power.
            "memory_atomise",
            // v0.7.0 Form 3 (#756) — multi-step ingest orchestrator.
            "memory_ingest_multistep",
            // v0.7.0 Form 5 (issue #758) — calibration sweep over the
            // shadow-mode observation table (Family::Power).
            "memory_calibrate_confidence",
            // v0.7.0 (issues #224 + #311) — Phase 3 Memory Sharing &
            // Sync RFC pulled forward per operator directive
            // `28860423-d12c-4959-bc8b-8fa9a94a33d9`.
            "memory_share",
        ];
        assert_eq!(
            baseline.len(),
            66,
            "baseline list = 43 (v0.6.3.1) + 1 (v0.7.0 I4 memory_replay) + \
             1 (v0.7 H4 memory_verify) + 1 (v0.7 B1 memory_load_family) + \
             1 (v0.7 B2 memory_smart_load) + \
             2 (v0.7 K7 memory_subscription_replay + memory_subscription_dlq_list) + \
             1 (v0.7 J7 memory_find_paths) + 1 (v0.7 K8 memory_quota_status) + \
             1 (v0.7.0 Task 4/8 memory_reflect) + \
             5 (v0.7.0 L1-5 skill tools) + \
             1 (v0.7.0 L2-6 memory_skill_promote_from_reflection) + \
             1 (v0.7.0 L2-7 memory_skill_compositional_context) + \
             1 (v0.7.0 QW-1 memory_export_reflection) + \
             2 (v0.7.0 QW-3 follow-up memory_offload + memory_deref) + \
             1 (v0.7.0 WT-1-C memory_atomise) + \
             1 (v0.7.0 Form 3 memory_ingest_multistep) + \
             1 (v0.7.0 Form 5 memory_calibrate_confidence) + \
             1 (v0.7.0 issues #224 + #311 memory_share) = 66"
        );
        for name in baseline {
            assert!(
                Family::for_tool(name).is_some(),
                "Family::for_tool({name}) returned None — update the family map"
            );
        }
    }

    #[test]
    fn family_for_tool_returns_none_for_unknown() {
        assert!(Family::for_tool("memory_does_not_exist").is_none());
        assert!(Family::for_tool("").is_none());
    }

    #[test]
    fn loads_includes_core_tools_under_core_profile() {
        let p = Profile::core();
        assert!(p.loads("memory_store"));
        assert!(p.loads("memory_recall"));
        assert!(!p.loads("memory_kg_query"));
        // memory_capabilities is always-on bootstrap.
        assert!(p.loads("memory_capabilities"));
    }

    #[test]
    fn loads_full_profile_includes_every_tool() {
        let p = Profile::full();
        // Every tool in the baseline must load under full.
        for name in [
            "memory_store",
            "memory_kg_query",
            "memory_consolidate",
            "memory_archive_list",
            "memory_notify",
            "memory_capabilities",
        ] {
            assert!(p.loads(name), "full profile should load {name}");
        }
    }

    #[test]
    fn loads_unknown_tool_returns_false() {
        let p = Profile::full();
        assert!(!p.loads("memory_does_not_exist"));
    }

    #[test]
    fn always_on_tools_loaded_in_every_profile() {
        for p in [
            Profile::core(),
            Profile::graph(),
            Profile::admin(),
            Profile::power(),
            Profile::full(),
        ] {
            for name in ALWAYS_ON_TOOLS {
                assert!(p.loads(name), "{name} must load in every profile");
            }
        }
    }
}
