//! Per-category catalog key schemas (ADR-0019 §8.3).
//!
//! Every catalog key the friendly-error layer references at
//! runtime is enumerated here together with its expected
//! placeholder set. The validator
//! (`tests::keys_validate_against_catalog`) walks this list and
//! asserts:
//!
//! - the key exists in the catalog;
//! - every placeholder declared here appears at least once in
//!   the template;
//! - no placeholder appears in the template that isn't declared
//!   here (catches typos in either direction);
//! - every catalog key (outside the `_test.*` sanity group) is
//!   declared here (catches dead YAML entries).
//!
//! Adding a new translation site is a two-step change: add the
//! key + placeholders here, add the YAML entry. Either alone
//! fails the validator.
//!
//! ## Convention
//!
//! Each error entry in the catalog has:
//!
//! - a `.headline` template — used in both short and verbose
//!   modes;
//! - optionally a `.hint` template — surfaced only in verbose
//!   mode.
//!
//! Single-line errors (object-not-found, already-exists,
//! invalid-value) have no hint; the headline carries the whole
//! message.
//!
//! Other categories (`help.*`, `ok.*`, `client_side.*`,
//! `replay.*`, `parse.*`, modal labels, …) get added to this
//! list as the migration sweep (ADR-0019 §9) lands them.

/// `(key, expected_placeholders)`. Sorted by key for grep-ability.
pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
    // ---- Pre-submit diagnostics (ADR-0027) ----
    ("diagnostic.alias_used_as_column", &["name"]),
    ("diagnostic.ambiguous_column", &["column", "qualifiers"]),
    ("diagnostic.auto_column_overridden", &["column", "type"]),
    (
        "diagnostic.compound_arity_mismatch",
        &["op", "left_n", "right_n"],
    ),
    (
        "diagnostic.cte_arity_mismatch",
        &["cte", "declared", "actual"],
    ),
    ("diagnostic.duplicate_cte", &["name"]),
    ("diagnostic.eq_null", &[]),
    ("diagnostic.insert_arity_mismatch", &["expected", "actual"]),
    // ADR-0033 §8.1 / Amendment 5: Form B (no column list) variant.
    // Cited from issue #1 — every column must be supplied positionally.
    (
        "diagnostic.insert_arity_mismatch_form_b",
        &["expected", "actual"],
    ),
    // ADR-0036 Amendment 1 / issue #17: simple-mode Form B variants.
    (
        "diagnostic.insert_arity_mismatch_form_b_simple",
        &["expected", "columns", "skipped", "actual"],
    ),
    (
        "diagnostic.insert_arity_mismatch_all_auto",
        &["table", "actual"],
    ),
    ("diagnostic.not_null_missing", &["column"]),
    ("diagnostic.like_numeric", &["column", "type"]),
    (
        "diagnostic.projection_alias_misplaced",
        &["alias", "clause"],
    ),
    ("diagnostic.table_used_as_column", &["name"]),
    ("diagnostic.type_mismatch", &["column", "type"]),
    ("diagnostic.unknown_column", &["name", "table"]),
    ("diagnostic.unknown_qualifier", &["qualifier"]),
    ("diagnostic.unknown_table", &["name"]),
    // ---- Friendly-error translations of engine messages
    // ---- (ADR-0019; ADR-0032 §11.5).
    ("engine.aggregate_misuse", &["name"]),
    ("engine.ambiguous_column", &["column"]),
    ("engine.compound_arity_mismatch", &["op"]),
    ("engine.group_by_required", &[]),
    ("engine.no_such_column", &["name"]),
    ("engine.no_such_table", &["name"]),
    ("engine.recursive_cte_malformed", &[]),
    ("engine.scalar_subquery_too_many_rows", &[]),
    // ---- Already-exists collisions (anchor: "already exists") ----
    ("error.already_exists.column.headline", &["table", "column"]),
    ("error.already_exists.relationship.headline", &["name"]),
    ("error.already_exists.table.headline", &["name"]),
    // ---- CHECK violations ----
    ("error.check.insert.headline", &["table", "column"]),
    ("error.check.insert.hint", &["column"]),
    (
        "error.check.insert.hint_with_rule",
        &["value", "rule", "column"],
    ),
    ("error.check.update.headline", &["table", "column"]),
    ("error.check.update.hint", &["column"]),
    (
        "error.check.update.hint_with_rule",
        &["value", "rule", "column"],
    ),
    // ---- FK violations (anchor: "referenced by") ----
    (
        "error.foreign_key.child_side.insert.headline",
        &["parent_table", "parent_column", "value"],
    ),
    (
        "error.foreign_key.child_side.insert.hint",
        &["parent_table", "parent_column"],
    ),
    (
        "error.foreign_key.child_side.update.headline",
        &["parent_table", "parent_column", "value"],
    ),
    (
        "error.foreign_key.child_side.update.hint",
        &["parent_table", "parent_column"],
    ),
    (
        "error.foreign_key.parent_side.delete.headline",
        &["table", "child_table"],
    ),
    ("error.foreign_key.parent_side.delete.hint", &[]),
    (
        "error.foreign_key.parent_side.update.headline",
        &["table", "child_table"],
    ),
    ("error.foreign_key.parent_side.update.hint", &[]),
    // ---- Generic engine refusal ----
    ("error.generic.headline", &["operation"]),
    ("error.generic.hint", &["table"]),
    ("error.generic.hint_no_table", &[]),
    // ---- Invalid-value errors (pre-engine, single-line) ----
    (
        "error.invalid_value.arity.headline",
        &["expected", "actual"],
    ),
    ("error.invalid_value.empty_insert.headline", &[]),
    ("error.invalid_value.empty_update.headline", &[]),
    // ---- Not-found errors (anchor: "no such ...") ----
    ("error.not_found.column.headline", &["table", "column"]),
    ("error.not_found.column_unqualified.headline", &["column"]),
    ("error.not_found.relationship.headline", &["name"]),
    ("error.not_found.table.headline", &["name"]),
    // ---- NOT NULL violations ----
    ("error.not_null.insert.headline", &["table", "column"]),
    ("error.not_null.insert.hint", &["column"]),
    ("error.not_null.update.headline", &["table", "column"]),
    ("error.not_null.update.hint", &["column"]),
    // ---- Type mismatch ----
    (
        "error.type_mismatch.change_column.headline",
        &["table", "column", "src_type", "target_type"],
    ),
    ("error.type_mismatch.change_column.hint", &["target_type"]),
    (
        "error.type_mismatch.insert.headline",
        &["value", "expected_type"],
    ),
    (
        "error.type_mismatch.insert.hint",
        &["table", "column", "expected_type"],
    ),
    (
        "error.type_mismatch.update.headline",
        &["value", "expected_type"],
    ),
    (
        "error.type_mismatch.update.hint",
        &["table", "column", "expected_type"],
    ),
    // ---- Help text ----
    ("help.cli_banner", &[]),
    // In-app `help` — framing + per-command entries keyed by
    // each CommandNode's `help_id` (ADR-0024 §help_id).
    ("help.intro", &[]),
    ("help.simple_section", &[]),
    ("help.advanced_section", &[]),
    ("help.types_reference", &[]),
    ("help.detail_hint", &[]),
    ("help.unknown_topic", &["topic"]),
    ("help.app.quit", &[]),
    ("help.app.help", &[]),
    ("help.app.hint", &[]),
    ("help.app.version", &[]),
    ("help.app.rebuild", &[]),
    ("help.app.save", &[]),
    ("help.app.new", &[]),
    ("help.app.load", &[]),
    ("help.app.export", &[]),
    ("help.app.import", &[]),
    ("help.app.mode", &[]),
    ("help.app.messages", &[]),
    ("help.app.undo", &[]),
    ("help.app.redo", &[]),
    ("help.app.copy", &[]),
    ("help.ddl.create", &[]),
    ("help.ddl.create_m2n", &[]),
    ("help.ddl.sql_create_table", &[]),
    ("help.ddl.sql_drop_table", &[]),
    ("help.ddl.sql_create_index", &[]),
    ("help.ddl.sql_drop_index", &[]),
    ("help.ddl.sql_alter_table", &[]),
    // Advanced-mode SQL CREATE TABLE / DROP TABLE no-op notes (ADR-0035 §4).
    ("ddl.create_skipped_exists", &["name"]),
    ("ddl.drop_skipped_absent", &["name"]),
    // Advanced-mode SQL CREATE INDEX / DROP INDEX no-op notes (ADR-0035 §4d).
    ("ddl.create_index_skipped_exists", &["name"]),
    ("ddl.drop_index_skipped_absent", &["name"]),
    ("help.ddl.drop", &[]),
    ("help.ddl.add", &[]),
    ("help.ddl.rename", &[]),
    ("help.ddl.change", &[]),
    ("help.data.show", &[]),
    ("help.data.seed", &[]),
    ("help.data.insert", &[]),
    ("help.data.update", &[]),
    ("help.data.delete", &[]),
    ("help.data.replay", &[]),
    ("help.data.explain", &[]),
    // Issue #36: advanced-mode (SQL) help pages.
    ("help.data.select", &[]),
    ("help.data.with", &[]),
    ("help.data.sql_insert", &[]),
    ("help.data.sql_update", &[]),
    ("help.data.sql_delete", &[]),
    ("help.data.explain_sql", &[]),
    // ---- Hint panel ambient typing assistance (ADR-0022 §6) ----
    ("hint.ambient_complete", &[]),
    ("hint.ambient_error_with_usage", &["message", "usage"]),
    ("hint.ambient_expected", &["expected"]),
    ("hint.getting_started", &[]),
    ("hint.block.heading", &[]),
    ("hint.block.concept_heading", &[]),
    ("hint.block.what", &[]),
    ("hint.block.example", &[]),
    ("hint.block.concept", &[]),
    // Clause-concept blocks (issue #37 / clause-concept-hints D5).
    // Both-mode topics carry a mode-keyed `example` (simple + advanced);
    // single-mode topics carry a plain `example`. The comprehensiveness
    // test (CONCEPT_TOPICS) enforces this shape per topic.
    ("hint.concept.referential_actions.what", &[]),
    ("hint.concept.referential_actions.example.simple", &[]),
    ("hint.concept.referential_actions.example.advanced", &[]),
    ("hint.concept.referential_actions.concept", &[]),
    ("hint.concept.cardinality_one_to_many.what", &[]),
    ("hint.concept.cardinality_one_to_many.example", &[]),
    ("hint.concept.cardinality_one_to_many.concept", &[]),
    ("hint.concept.cardinality_many_to_many.what", &[]),
    ("hint.concept.cardinality_many_to_many.example", &[]),
    ("hint.concept.cardinality_many_to_many.concept", &[]),
    ("hint.concept.primary_key.what", &[]),
    ("hint.concept.primary_key.example.simple", &[]),
    ("hint.concept.primary_key.example.advanced", &[]),
    ("hint.concept.primary_key.concept", &[]),
    ("hint.concept.unique.what", &[]),
    ("hint.concept.unique.example.simple", &[]),
    ("hint.concept.unique.example.advanced", &[]),
    ("hint.concept.unique.concept", &[]),
    ("hint.concept.check.what", &[]),
    ("hint.concept.check.example.simple", &[]),
    ("hint.concept.check.example.advanced", &[]),
    ("hint.concept.check.concept", &[]),
    ("hint.concept.foreign_key.what", &[]),
    ("hint.concept.foreign_key.example", &[]),
    ("hint.concept.foreign_key.concept", &[]),
    // Tier-3 teaching blocks (ADR-0053 D3) — Phase-B exemplars.
    ("hint.cmd.insert.what", &[]),
    ("hint.cmd.insert.example", &[]),
    ("hint.cmd.insert.concept", &[]),
    ("hint.cmd.add_relationship.what", &[]),
    ("hint.cmd.add_relationship.example", &[]),
    ("hint.cmd.add_relationship.concept", &[]),
    ("hint.err.foreign_key.child_side.what", &[]),
    ("hint.err.foreign_key.child_side.example", &[]),
    ("hint.err.foreign_key.child_side.concept", &[]),
    // Phase C batch 5 — runtime error-class hints.
    ("hint.err.foreign_key.parent_side.what", &[]),
    ("hint.err.foreign_key.parent_side.example", &[]),
    ("hint.err.foreign_key.parent_side.concept", &[]),
    ("hint.err.unique.what", &[]),
    ("hint.err.unique.example", &[]),
    ("hint.err.unique.concept", &[]),
    ("hint.err.not_null.what", &[]),
    ("hint.err.not_null.example", &[]),
    ("hint.err.not_null.concept", &[]),
    ("hint.err.check.what", &[]),
    ("hint.err.check.example", &[]),
    ("hint.err.check.concept", &[]),
    ("hint.err.type_mismatch.what", &[]),
    ("hint.err.type_mismatch.example", &[]),
    ("hint.err.type_mismatch.concept", &[]),
    ("hint.err.not_found.what", &[]),
    ("hint.err.not_found.example", &[]),
    ("hint.err.not_found.concept", &[]),
    ("hint.err.already_exists.what", &[]),
    ("hint.err.already_exists.example", &[]),
    ("hint.err.already_exists.concept", &[]),
    ("hint.err.generic.what", &[]),
    ("hint.err.generic.example", &[]),
    ("hint.err.invalid_value.what", &[]),
    ("hint.err.invalid_value.example", &[]),
    // Phase C batch 1 — app-lifecycle command hints.
    ("hint.cmd.quit.what", &[]),
    ("hint.cmd.quit.example", &[]),
    ("hint.cmd.help.what", &[]),
    ("hint.cmd.help.example", &[]),
    ("hint.cmd.help.concept", &[]),
    ("hint.cmd.hint.what", &[]),
    ("hint.cmd.hint.example", &[]),
    ("hint.cmd.version.what", &[]),
    ("hint.cmd.version.example", &[]),
    ("hint.cmd.rebuild.what", &[]),
    ("hint.cmd.rebuild.example", &[]),
    ("hint.cmd.rebuild.concept", &[]),
    ("hint.cmd.save.what", &[]),
    ("hint.cmd.save.example", &[]),
    ("hint.cmd.save.concept", &[]),
    ("hint.cmd.new.what", &[]),
    ("hint.cmd.new.example", &[]),
    ("hint.cmd.load.what", &[]),
    ("hint.cmd.load.example", &[]),
    ("hint.cmd.export.what", &[]),
    ("hint.cmd.export.example", &[]),
    ("hint.cmd.export.concept", &[]),
    ("hint.cmd.import.what", &[]),
    ("hint.cmd.import.example", &[]),
    ("hint.cmd.mode.what", &[]),
    ("hint.cmd.mode.example", &[]),
    ("hint.cmd.mode.concept", &[]),
    ("hint.cmd.messages.what", &[]),
    ("hint.cmd.messages.example", &[]),
    ("hint.cmd.messages.concept", &[]),
    ("hint.cmd.undo.what", &[]),
    ("hint.cmd.undo.example", &[]),
    ("hint.cmd.undo.concept", &[]),
    ("hint.cmd.redo.what", &[]),
    ("hint.cmd.redo.example", &[]),
    ("hint.cmd.copy.what", &[]),
    ("hint.cmd.copy.example", &[]),
    // Phase C batch 2 — DDL command hints.
    ("hint.cmd.create_table.what", &[]),
    ("hint.cmd.create_table.example", &[]),
    ("hint.cmd.create_table.concept", &[]),
    ("hint.cmd.create_m2n.what", &[]),
    ("hint.cmd.create_m2n.example", &[]),
    ("hint.cmd.create_m2n.concept", &[]),
    ("hint.cmd.add_column.what", &[]),
    ("hint.cmd.add_column.example", &[]),
    ("hint.cmd.add_column.concept", &[]),
    ("hint.cmd.add_index.what", &[]),
    ("hint.cmd.add_index.example", &[]),
    ("hint.cmd.add_index.concept", &[]),
    ("hint.cmd.add_constraint.what", &[]),
    ("hint.cmd.add_constraint.example", &[]),
    ("hint.cmd.add_constraint.concept", &[]),
    ("hint.cmd.drop_table.what", &[]),
    ("hint.cmd.drop_table.example", &[]),
    ("hint.cmd.drop_table.concept", &[]),
    ("hint.cmd.drop_column.what", &[]),
    ("hint.cmd.drop_column.example", &[]),
    ("hint.cmd.drop_column.concept", &[]),
    ("hint.cmd.drop_relationship.what", &[]),
    ("hint.cmd.drop_relationship.example", &[]),
    ("hint.cmd.drop_relationship.concept", &[]),
    ("hint.cmd.drop_index.what", &[]),
    ("hint.cmd.drop_index.example", &[]),
    ("hint.cmd.drop_index.concept", &[]),
    ("hint.cmd.drop_constraint.what", &[]),
    ("hint.cmd.drop_constraint.example", &[]),
    ("hint.cmd.drop_constraint.concept", &[]),
    ("hint.cmd.rename_column.what", &[]),
    ("hint.cmd.rename_column.example", &[]),
    ("hint.cmd.rename_column.concept", &[]),
    ("hint.cmd.change_column.what", &[]),
    ("hint.cmd.change_column.example", &[]),
    ("hint.cmd.change_column.concept", &[]),
    // Phase C batch 3 — DML command hints.
    ("hint.cmd.update.what", &[]),
    ("hint.cmd.update.example", &[]),
    ("hint.cmd.update.concept", &[]),
    ("hint.cmd.delete.what", &[]),
    ("hint.cmd.delete.example", &[]),
    ("hint.cmd.delete.concept", &[]),
    ("hint.cmd.show_data.what", &[]),
    ("hint.cmd.show_data.example", &[]),
    ("hint.cmd.show_data.concept", &[]),
    ("hint.cmd.show_table.what", &[]),
    ("hint.cmd.show_table.example", &[]),
    ("hint.cmd.show_table.concept", &[]),
    ("hint.cmd.show_tables.what", &[]),
    ("hint.cmd.show_tables.example", &[]),
    ("hint.cmd.show_relationships.what", &[]),
    ("hint.cmd.show_relationships.example", &[]),
    ("hint.cmd.show_relationships.concept", &[]),
    ("hint.cmd.show_indexes.what", &[]),
    ("hint.cmd.show_indexes.example", &[]),
    ("hint.cmd.show_indexes.concept", &[]),
    ("hint.cmd.seed.what", &[]),
    ("hint.cmd.seed.example", &[]),
    ("hint.cmd.seed.concept", &[]),
    ("hint.cmd.explain.what", &[]),
    ("hint.cmd.explain.example", &[]),
    ("hint.cmd.explain.concept", &[]),
    ("hint.cmd.replay.what", &[]),
    ("hint.cmd.replay.example", &[]),
    ("hint.cmd.replay.concept", &[]),
    // Phase C batch 4 — advanced-mode SQL command hints.
    ("hint.cmd.sql_create_table.what", &[]),
    ("hint.cmd.sql_create_table.example", &[]),
    ("hint.cmd.sql_create_table.concept", &[]),
    ("hint.cmd.sql_alter_table.what", &[]),
    ("hint.cmd.sql_alter_table.example", &[]),
    ("hint.cmd.sql_alter_table.concept", &[]),
    ("hint.cmd.sql_create_index.what", &[]),
    ("hint.cmd.sql_create_index.example", &[]),
    ("hint.cmd.sql_create_index.concept", &[]),
    ("hint.cmd.sql_drop_index.what", &[]),
    ("hint.cmd.sql_drop_index.example", &[]),
    ("hint.cmd.sql_drop_index.concept", &[]),
    ("hint.cmd.sql_drop_table.what", &[]),
    ("hint.cmd.sql_drop_table.example", &[]),
    ("hint.cmd.sql_drop_table.concept", &[]),
    ("hint.cmd.sql_insert.what", &[]),
    ("hint.cmd.sql_insert.example", &[]),
    ("hint.cmd.sql_insert.concept", &[]),
    ("hint.cmd.sql_update.what", &[]),
    ("hint.cmd.sql_update.example", &[]),
    ("hint.cmd.sql_update.concept", &[]),
    ("hint.cmd.sql_delete.what", &[]),
    ("hint.cmd.sql_delete.example", &[]),
    ("hint.cmd.sql_delete.concept", &[]),
    ("hint.cmd.select.what", &[]),
    ("hint.cmd.select.example", &[]),
    ("hint.cmd.select.concept", &[]),
    ("hint.cmd.with.what", &[]),
    ("hint.cmd.with.example", &[]),
    ("hint.cmd.with.concept", &[]),
    ("hint.cmd.explain_sql.what", &[]),
    ("hint.cmd.explain_sql.example", &[]),
    ("hint.cmd.explain_sql.concept", &[]),
    ("hint.ambient_invalid_ident", &["kind", "found"]),
    ("hint.ambient_typing_name", &[]),
    // Issue #4: introduce the advanced-mode CREATE TABLE element
    // slot (`create table T (`) so the otherwise-invisible
    // column-name role reads as the dominant first move.
    ("hint.create_table_element", &[]),
    ("hint.seed_count", &[]),
    ("hint.value_literal_slot", &[]),
    ("hint.ambient_typing_name_then", &["next"]),
    // Per-column-type value-slot hints (ADR-0024 §Phase D).
    ("hint.value_slot_bool", &[]),
    ("hint.value_slot_date", &[]),
    ("hint.value_slot_datetime", &[]),
    ("hint.value_slot_decimal", &[]),
    ("hint.value_slot_int", &[]),
    ("hint.value_slot_real", &[]),
    ("hint.value_slot_serial", &[]),
    ("hint.value_slot_shortid", &[]),
    ("hint.value_slot_text", &[]),
    ("hint.value_slot_for_column", &["column", "detail"]),
    ("hint.value_slot_autogen_skipped", &["columns"]),
    // ---- Parse error rendering ----
    ("parse.available_commands", &["commands"]),
    ("parse.caret", &["padding"]),
    // Custom (try_map / source-slice) error messages raised
    // by the DSL parser. See `parse.custom.*` in the catalog.
    ("parse.custom.alter_add_primary_key", &[]),
    ("parse.custom.alter_named_unique", &[]),
    ("parse.custom.bind_type_mismatch", &["found", "expected"]),
    ("parse.custom.change_column_flags_exclusive", &[]),
    (
        "parse.custom.constraint_redundant_on_pk",
        &["column", "constraint"],
    ),
    ("parse.custom.create_table_needs_pk", &[]),
    ("parse.custom.expression_too_deep", &[]),
    ("parse.custom.insert_form_a_missing_values", &["columns"]),
    ("parse.custom.on_action_specified_twice", &["target"]),
    ("parse.custom.replay_path_expected", &[]),
    ("parse.custom.unknown_action", &["found", "expected"]),
    ("parse.custom.unknown_type", &["found", "expected"]),
    ("parse.empty", &[]),
    ("parse.error", &["detail"]),
    ("parse.error_wrapper", &["detail"]),
    // Per-command usage templates (ADR-0021 §1). One key per
    // command. Multi-entry families (`add`, `drop`, `show`)
    // each have multiple keys. Templates are pure prose with
    // no placeholders — the renderer prepends "usage: " in
    // code, not the catalog, because spacing is alignment-
    // sensitive in the multi-entry case.
    ("parse.usage.add_column", &[]),
    ("parse.usage.add_constraint", &[]),
    ("parse.usage.add_index", &[]),
    ("parse.usage.add_relationship", &[]),
    ("parse.usage.change_column", &[]),
    ("parse.usage.create_table", &[]),
    ("parse.usage.create_m2n", &[]),
    ("parse.usage.sql_create_table", &[]),
    ("parse.usage.sql_drop_table", &[]),
    ("parse.usage.sql_create_index", &[]),
    ("parse.usage.sql_drop_index", &[]),
    ("parse.usage.sql_alter_table", &[]),
    ("parse.usage.delete", &[]),
    ("parse.usage.drop_column", &[]),
    ("parse.usage.drop_constraint", &[]),
    ("parse.usage.drop_index", &[]),
    ("parse.usage.drop_relationship", &[]),
    ("parse.usage.drop_table", &[]),
    ("parse.usage.explain", &[]),
    ("parse.usage.insert", &[]),
    ("parse.usage.rename_column", &[]),
    ("parse.usage.export", &[]),
    ("parse.usage.help", &[]),
    ("parse.usage.hint", &[]),
    ("parse.usage.import", &[]),
    ("parse.usage.copy", &[]),
    ("parse.usage.load", &[]),
    ("parse.usage.messages", &[]),
    ("parse.usage.mode", &[]),
    ("parse.usage.new", &[]),
    ("parse.usage.quit", &[]),
    ("parse.usage.version", &[]),
    ("parse.usage.rebuild", &[]),
    ("parse.usage.redo", &[]),
    ("parse.usage.replay", &[]),
    ("parse.usage.undo", &[]),
    ("parse.usage.save", &[]),
    ("parse.usage.select", &[]),
    ("parse.usage.seed", &[]),
    ("parse.usage.show_data", &[]),
    ("parse.usage.show_table", &[]),
    ("parse.usage.show_tables", &[]),
    ("parse.usage.show_relationships", &[]),
    ("parse.usage.show_indexes", &[]),
    ("parse.usage.show_relationship", &[]),
    ("parse.usage.show_index", &[]),
    ("parse.usage.update", &[]),
    ("parse.usage.with", &[]),
    ("parse.expect.select_projection", &[]),
    ("parse.cross_join_no_on", &[]),
    // ---- Project lifecycle event notes ----
    ("project.export_failed", &["error"]),
    ("project.export_ok", &["path"]),
    ("project.export_usage", &[]),
    ("project.import_empty_target", &[]),
    ("project.import_usage", &[]),
    ("project.import_zip_missing", &["path"]),
    ("persistence.csv.empty", &[]),
    ("persistence.csv.invalid_utf8", &[]),
    ("persistence.csv.unterminated_quote", &[]),
    ("persistence.encode", &["kind", "path", "message"]),
    ("persistence.io", &["operation", "path", "source"]),
    ("persistence.migrate.bad_output", &["detail"]),
    ("persistence.migrate.io", &["path", "source"]),
    (
        "persistence.migrate.newer_than_supported",
        &["file", "latest"],
    ),
    ("persistence.migrate.no_migrator", &["version"]),
    ("persistence.migrate.step_failed", &["from", "to", "source"]),
    ("persistence.migrate.version_parse", &["detail"]),
    ("persistence.yaml.syntax", &["detail"]),
    ("persistence.yaml.unknown_action", &["raw"]),
    ("persistence.yaml.unknown_type", &["table", "column", "raw"]),
    ("persistence.yaml.unsupported_version", &["version"]),
    ("project.already_exists", &["path"]),
    ("project.data_root_unavailable", &[]),
    ("project.io", &["path", "source"]),
    ("project.load_path_missing", &["path"]),
    ("project.lock.already_held", &["pid", "hostname", "path"]),
    ("project.lock.read", &["path", "source"]),
    ("project.lock.write", &["path", "source"]),
    ("project.naming.too_many_collisions", &["attempts"]),
    ("project.naming.wordlist_too_small", &["count"]),
    ("project.not_a_project", &["path"]),
    ("project.path_not_found", &["path"]),
    ("project.safe_delete.io", &["path", "source"]),
    ("project.safe_delete.refused", &["path", "reason"]),
    ("project.user_name.empty", &[]),
    ("project.user_name.invalid_char", &["ch"]),
    ("project.user_name.leading_dot", &[]),
    ("project.resume_no_previous", &["data_root"]),
    ("project.resume_recorded_missing", &["path"]),
    ("project.saveas_target_exists", &["path"]),
    ("project.rebuild_failed", &["error"]),
    ("project.rebuild_ok", &["summary"]),
    ("project.switch_failed", &["error"]),
    ("project.switched_ok", &["display_name"]),
    // ---- Advanced-mode placeholder ----
    ("advanced_mode.not_implemented", &["input"]),
    // ---- Advanced-mode SQL surface (ADR-0030) ----
    ("advanced_mode.sql_in_simple", &["command"]),
    ("advanced_mode.also_valid_sql", &[]),
    // Education note appended to a simple-mode INSERT Form B
    // parse error when the user supplied more values than the
    // non-auto-generated columns expect (issue #1 sub-task 2).
    (
        "insert.form_b_extra_values_note",
        &["table", "expected_phrase", "auto_phrase", "all_cols"],
    ),
    // Pre-flight teaching note for advanced-mode positional
    // `INSERT INTO T VALUES (…)` (no column list) when the value
    // count doesn't match the column count (issue #1 sub-task 3).
    (
        "insert.form_b_positional_count_mismatch_note",
        &["table", "col_count", "col_list", "supplied", "non_auto_csv"],
    ),
    ("select.internal_table", &["table"]),
    ("cli.invalid_value", &["flag", "value", "expected"]),
    ("cli.missing_value", &["flag"]),
    ("cli.multiple_paths", &["first", "second"]),
    ("cli.resume_with_path", &[]),
    ("cli.unknown_argument", &["arg"]),
    ("cli.version_line", &["version"]),
    (
        "archive.export_sequence_exhausted",
        &["project", "target_dir", "limit"],
    ),
    ("archive.import_collision_exhausted", &["path", "limit"]),
    ("archive.invalid_zip", &["detail"]),
    ("archive.io", &["path", "source"]),
    ("archive.multiple_top_folders", &[]),
    ("archive.not_a_project_archive", &[]),
    ("archive.unsafe_entry", &["entry"]),
    ("archive.zip", &["path", "message"]),
    // ---- DSL failure wrapper / running echo ----
    ("dsl.failed", &["rendered"]),
    ("dsl.running", &["input"]),
    // ---- Persistence-fatal banner ----
    ("fatal.persistence", &["operation", "path", "message"]),
    // ---- Modal labels ----
    ("modal.generic_cancelled", &["title"]),
    ("modal.load_cancelled", &[]),
    ("modal.load_picker_empty", &[]),
    ("modal.load_picker_nothing", &[]),
    ("modal.load_picker_path_prompt", &[]),
    ("modal.load_picker_title", &[]),
    ("modal.path_entry_empty_name", &[]),
    ("modal.path_entry_empty_path", &[]),
    ("modal.rebuild_cancelled", &[]),
    ("modal.rebuild_confirm_prompt", &[]),
    ("modal.rebuild_confirm_title", &[]),
    ("modal.redo_cancelled", &[]),
    ("modal.redo_confirm_command", &[]),
    ("modal.redo_confirm_title", &[]),
    ("modal.undo_cancelled", &[]),
    ("modal.undo_confirm_command", &[]),
    ("modal.undo_confirm_prompt", &[]),
    ("modal.undo_confirm_title", &[]),
    ("modal.undo_confirm_when", &["timestamp"]),
    // ---- Undo / redo command surfaces (ADR-0006 Amendment 1) ----
    ("undo.disabled", &[]),
    ("undo.nothing_to_undo", &[]),
    ("undo.nothing_to_redo", &[]),
    ("undo.undone_ok", &["command"]),
    ("undo.redone_ok", &["command"]),
    ("undo.undo_failed", &["error"]),
    ("undo.redo_failed", &["error"]),
    // ---- Status bar + panels ----
    ("panel.hint_empty", &[]),
    ("panel.hint_mode_advanced", &[]),
    ("panel.hint_title", &[]),
    ("panel.output_title", &[]),
    ("panel.relationships_empty", &[]),
    ("panel.relationships_title", &[]),
    ("panel.tables_empty", &[]),
    ("panel.tables_title", &[]),
    ("status.no_project", &[]),
    ("status.project_label", &[]),
    ("value.format", &["column", "message"]),
    ("value.type_mismatch", &["column", "expected_human", "got"]),
    // ---- Save / save-as surfaces ----
    ("save.already_saved", &[]),
    ("save.path_prompt", &[]),
    ("save.title_as", &[]),
    ("save.title_save", &[]),
    // ---- Shortcut hint labels ----
    ("shortcut.back_to_list", &[]),
    ("shortcut.browse", &[]),
    ("shortcut.browse_path", &[]),
    ("shortcut.cancel", &[]),
    ("shortcut.clear", &[]),
    ("shortcut.complete", &[]),
    ("shortcut.confirm", &[]),
    ("shortcut.cycle", &[]),
    ("shortcut.del_word", &[]),
    ("shortcut.hint", &[]),
    ("shortcut.history", &[]),
    ("shortcut.home_end", &[]),
    ("shortcut.load", &[]),
    ("shortcut.nav", &[]),
    ("shortcut.next_pane", &[]),
    ("shortcut.no", &[]),
    ("shortcut.run", &[]),
    ("shortcut.scroll", &[]),
    ("shortcut.select", &[]),
    ("shortcut.to_input", &[]),
    ("shortcut.yes", &[]),
    // ---- mode / messages banners ----
    ("messages.set_short", &[]),
    ("messages.set_verbose", &[]),
    ("messages.show", &["current"]),
    ("messages.unknown", &["value"]),
    ("mode.label_advanced", &[]),
    ("mode.label_advanced_one_shot", &[]),
    ("mode.label_simple", &[]),
    ("mode.set_advanced", &[]),
    ("mode.set_simple", &[]),
    ("mode.show_advanced", &[]),
    ("mode.show_simple", &[]),
    ("mode.unknown", &["value"]),
    ("mode.usage", &[]),
    // ---- copy (ADR-0041) ----
    ("copy.done", &["count"]),
    ("copy.nothing", &[]),
    ("copy.unknown", &["value"]),
    // ---- DbError Display fallback ----
    ("db.error.invalid_value", &["detail"]),
    ("db.error.io", &["detail"]),
    (
        "db.error.persistence_fatal",
        &["operation", "path", "message"],
    ),
    (
        "db.error.rebuild_row_failed",
        &["row_number", "csv_path", "table", "detail"],
    ),
    ("db.error.sqlite", &["message"]),
    ("db.error.unsupported", &["detail"]),
    ("db.error.worker_gone", &[]),
    // ---- Cascade-effect summaries (per ADR-0014) ----
    ("db.cascade.action_blocked", &[]),
    ("db.cascade.action_deleted", &[]),
    ("db.cascade.action_set_null", &[]),
    (
        "db.cascade.summary",
        &["count", "action", "child_table", "rel", "on_delete"],
    ),
    // ---- change-column dry-run diagnostics (per ADR-0017) ----
    // ---- add-constraint dry-run diagnostics (per ADR-0029 §5) ----
    (
        "db.diagnostic.add_check_summary",
        &["table", "column", "total", "rule"],
    ),
    (
        "db.diagnostic.add_not_null_summary",
        &["table", "column", "total"],
    ),
    (
        "db.diagnostic.add_unique_summary",
        &["table", "column", "total"],
    ),
    ("db.diagnostic.force_conversion_hint", &[]),
    ("db.diagnostic.header_becomes", &[]),
    ("db.diagnostic.header_from", &[]),
    ("db.diagnostic.header_reason", &[]),
    ("db.diagnostic.header_source_rows", &["pk_label"]),
    ("db.diagnostic.header_source_values", &[]),
    ("db.diagnostic.header_to", &[]),
    ("db.diagnostic.header_value", &[]),
    (
        "db.diagnostic.incompatible_summary",
        &["table", "column", "src_ty", "target_ty", "total"],
    ),
    (
        "db.diagnostic.lossy_summary",
        &["table", "column", "src_ty", "target_ty", "total"],
    ),
    (
        "db.diagnostic.uniqueness_summary",
        &["table", "column", "src_ty", "target_ty", "total"],
    ),
    // ---- DSL command success summaries (ADR-0019 §9 sweep) ----
    ("ok.index_dropped_with_column", &["index"]),
    ("ok.rows_deleted", &["count"]),
    ("ok.rows_inserted", &["count"]),
    ("ok.rows_seeded", &["count", "table"]),
    ("ok.rows_updated", &["count"]),
    ("seed.capped", &["requested"]),
    ("seed.advisory_generic", &["columns", "column", "table"]),
    // ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ----
    ("client_side.auto_fill_add_serial", &["count"]),
    ("client_side.auto_fill_add_shortid", &["count"]),
    ("client_side.auto_fill_transition", &["count", "kind"]),
    ("client_side.dont_convert_caveat", &[]),
    ("client_side.transformed", &["count"]),
    ("client_side.transformed_lossy", &["count", "lossy"]),
    // ---- Replay command surfaces (ADR-0019 §9 sweep) ----
    ("replay.command_echo", &["command"]),
    ("replay.completed", &["path", "count"]),
    ("replay.error_could_not_open", &["path", "detail"]),
    ("replay.error_parse", &["detail"]),
    ("replay.failed_at_line", &["path", "line_number", "error"]),
    ("replay.failed_open", &["path", "error"]),
    ("replay.skipped_import", &["line", "command"]),
    ("replay.skipped_replay", &["line", "command"]),
    // ---- UNIQUE violations (anchor: "already has the value") ----
    (
        "error.unique.insert.headline",
        &["table", "column", "value"],
    ),
    ("error.unique.insert.hint", &["table", "column"]),
    ("error.unique.pk.insert.headline", &["table", "value"]),
    ("error.unique.pk.insert.hint", &[]),
    ("error.unique.pk.update.headline", &["table", "value"]),
    ("error.unique.pk.update.hint", &[]),
    (
        "error.unique.update.headline",
        &["table", "column", "value"],
    ),
    ("error.unique.update.hint", &["table", "column"]),
];

#[cfg(test)]
mod tests {
    use super::KEYS_AND_PLACEHOLDERS;
    use crate::friendly::format::catalog;
    use std::collections::HashSet;

    // The pre-Phase-F `keyword_and_punct_have_complete_token_vocabulary`
    // test cross-checked the `Keyword` / `Punct` enums against
    // `parse.token.keyword.*` / `parse.token.punct.*` catalog
    // keys. With those enums deleted (ADR-0024 §migration Phase F)
    // and the walker rendering keyword wording via
    // `format!("`{word}`")`, the catalog entries survive only as
    // historic vocabulary; the `keys_validate_against_catalog`
    // test below still asserts every key in `KEYS_AND_PLACEHOLDERS`
    // resolves and vice versa, which keeps the catalog itself
    // honest. The dead entries collapse in ADR-0024 §cleanup-pass.

    /// Walks `KEYS_AND_PLACEHOLDERS` and verifies every entry
    /// matches the catalog. ADR-0019 §8.6.
    ///
    /// Checks:
    ///   1. every declared key exists in the catalog;
    ///   2. every declared placeholder appears in the template;
    ///   3. every placeholder used is declared (catches typos);
    ///   4. every catalog key (outside `_test.*`) is declared
    ///      (catches dead YAML entries);
    ///   5. no template contains a format specifier
    ///      (`{name:...}`); ADR-0019 §8.4 forbids these;
    ///   6. no template contains forbidden engine vocabulary
    ///      (ADR-0002 user-facing posture; same forbidden list
    ///      as `tests/engine_vocabulary_audit.rs`).
    #[test]
    fn keys_validate_against_catalog() {
        let cat = catalog();
        let mut errors: Vec<String> = Vec::new();

        for (key, expected) in KEYS_AND_PLACEHOLDERS {
            let Some(template) = cat.get(key) else {
                errors.push(format!("catalog missing key `{key}`"));
                continue;
            };

            // Placeholder set check (declared ↔ used).
            let actual = collect_placeholders(template);
            let expected_set: HashSet<&str> = expected.iter().copied().collect();
            for name in &expected_set {
                if !actual.contains(*name) {
                    errors.push(format!(
                        "key `{key}`: declared placeholder `{{{name}}}` is not used in template:\n{template}"
                    ));
                }
            }
            for name in &actual {
                if !expected_set.contains(name.as_str()) {
                    errors.push(format!(
                        "key `{key}`: template uses `{{{name}}}` but it isn't declared in keys.rs:\n{template}"
                    ));
                }
            }

            // Format-specifier check (ADR-0019 §8.4). Look for
            // `{name:...}` shapes — the substitute helper would
            // panic at runtime, but catching it at test time
            // means we never ship a binary that can hit that
            // panic.
            if has_format_specifier(template) {
                errors.push(format!(
                    "key `{key}`: template contains a `{{name:...}}` format specifier:\n{template}"
                ));
            }

            // Engine-vocabulary check (ADR-0002 user-facing
            // posture, regression-tested in
            // tests/engine_vocabulary_audit.rs).
            for needle in FORBIDDEN_ENGINE_VOCABULARY {
                if template.contains(needle) {
                    errors.push(format!(
                        "key `{key}`: template contains forbidden token `{needle}`:\n{template}"
                    ));
                }
            }
        }

        let declared: HashSet<&str> = KEYS_AND_PLACEHOLDERS.iter().map(|(k, _)| *k).collect();
        for key in cat.keys() {
            if key.starts_with("_test.") {
                continue;
            }
            if !declared.contains(key) {
                errors.push(format!(
                    "catalog has key `{key}` but it isn't declared in keys::KEYS_AND_PLACEHOLDERS"
                ));
            }
        }

        assert!(
            errors.is_empty(),
            "catalog validation failed:\n  {}",
            errors.join("\n  ")
        );
    }

    /// Mirror of `tests/engine_vocabulary_audit.rs::FORBIDDEN`,
    /// duplicated here so the catalog validator is self-contained
    /// (no dependency on the integration-test binary).
    const FORBIDDEN_ENGINE_VOCABULARY: &[&str] =
        &["SQLite", "sqlite", "rusqlite", "STRICT", "PRAGMA"];

    /// Detect a `{name:...}` format-specifier placeholder.
    /// Doubled braces `{{` / `}}` are escapes — must skip them.
    fn has_format_specifier(template: &str) -> bool {
        let mut chars = template.chars().peekable();
        while let Some(c) = chars.next() {
            if c == '{' {
                if chars.peek() == Some(&'{') {
                    chars.next();
                    continue;
                }
                while let Some(&nc) = chars.peek() {
                    if nc == '}' {
                        break;
                    }
                    if nc == ':' {
                        return true;
                    }
                    chars.next();
                }
            } else if c == '}' && chars.peek() == Some(&'}') {
                chars.next();
            }
        }
        false
    }

    /// Walk `template` and pull out every `{name}` placeholder.
    /// Mirrors the substitution helper's parse — if the helper
    /// accepts a placeholder, this collects it.
    fn collect_placeholders(template: &str) -> HashSet<String> {
        let mut out = HashSet::new();
        let mut chars = template.chars().peekable();
        while let Some(c) = chars.next() {
            if c == '{' {
                if chars.peek() == Some(&'{') {
                    chars.next();
                    continue;
                }
                let mut name = String::new();
                while let Some(&nc) = chars.peek() {
                    if nc == '}' {
                        chars.next();
                        break;
                    }
                    chars.next();
                    name.push(nc);
                }
                if !name.is_empty() && !name.contains(':') {
                    out.insert(name);
                }
            } else if c == '}' && chars.peek() == Some(&'}') {
                chars.next();
            }
        }
        out
    }
}
