razeri-plugin-api 0.1.0

Extension traits and registries for Razeri plugins
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
// Copyright (C) 2026 Gabriel Shea <gshea@ideapivot.com> - All Rights Reserved

//! **Every obligation a dialect owes, reported at once.**
//!
//! Two people built dialects against Razeri's authoring guide without help.
//! Between them they met twenty-seven obligations one `cargo test` at a time —
//! a roster fails, you add a row, the next roster fails. Not one of those
//! failures was discoverable before it fired. This module is the report that
//! answers all of them in one pass.
//!
//! # Running it
//!
//! From your own crate's tests, where your dialect is linked and Razeri's
//! registry can see it:
//!
//! ```no_run
//! # use razeri_plugin_api::conformance;
//! # fn example(my_dialect: &dyn razeri_plugin_api::PlatformDialect) {
//! let report = conformance::check(my_dialect);
//! assert!(report.is_clean(), "{report}");
//! # }
//! ```
//!
//! Inside Razeri's own repository, `razeri plugin conform <name>` prints the
//! same report for any registered platform, followed by the repository-side
//! obligations a command cannot run for you.
//!
//! # What it can and cannot see
//!
//! Everything here is read off the dialect object, so it works wherever your
//! plugin compiles. What it cannot do is run Razeri's frozen matrices or
//! regenerate its language bindings — those are commands, and
//! [`repository_obligations`] lists them in the order they are owed.

use crate::PlatformDialect;
use razeri_plugin_contract::{
    ConstraintEnforcement, DataType, DdlRenderer, DialectCapabilities, DialectFamily, MergeShape,
    ProcedureForm, SqlRenderer, UNKNOWN_PLATFORM,
};
use std::fmt;

/// How much a finding matters.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
    /// **This dialect will produce SQL its engine rejects.** Fix it before
    /// anything else.
    Blocking,
    /// **A question this dialect has not answered.** Razeri has a default and
    /// will use it; the default was written for a different engine.
    Unanswered,
    /// Worth a look. Nothing is broken.
    Note,
}

impl fmt::Display for Severity {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::Blocking => "must fix",
            Self::Unanswered => "unanswered",
            Self::Note => "worth a look",
        })
    }
}

/// One thing to fix, in the order a reader wants it: what is wrong, then what
/// to do about it.
#[derive(Debug, Clone)]
pub struct Finding {
    /// How much it matters.
    pub severity: Severity,
    /// One line naming what is wrong.
    pub what: String,
    /// One line naming what to do.
    pub fix: String,
}

/// A command a plugin author runs inside Razeri's repository, and why.
#[derive(Debug, Clone, Copy)]
pub struct Obligation {
    /// What this step is for.
    pub what: &'static str,
    /// The command, exactly as typed.
    pub command: &'static str,
}

/// The whole report for one dialect.
#[derive(Debug, Clone)]
pub struct Report {
    /// The dialect's registry name.
    pub dialect: String,
    /// Everything found, worst first.
    pub findings: Vec<Finding>,
}

impl Report {
    /// Whether nothing blocking or unanswered was found.
    ///
    /// Notes do not fail a report: they are observations, not obligations.
    #[must_use]
    pub fn is_clean(&self) -> bool {
        !self.findings.iter().any(|f| f.severity != Severity::Note)
    }

    /// How many findings of one severity.
    #[must_use]
    pub fn count(&self, severity: Severity) -> usize {
        self.findings.iter().filter(|f| f.severity == severity).count()
    }
}

impl fmt::Display for Report {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = &self.dialect;
        if self.findings.is_empty() {
            return writeln!(
                f,
                "{name}: nothing to fix. Every question Razeri asks a dialect, this one answers."
            );
        }
        let blocking = self.count(Severity::Blocking);
        let unanswered = self.count(Severity::Unanswered);
        let notes = self.count(Severity::Note);
        writeln!(f, "{name}: {blocking} to fix, {unanswered} unanswered, {notes} worth a look.")?;
        for finding in &self.findings {
            writeln!(f)?;
            writeln!(f, "  [{}] {}", finding.severity, finding.what)?;
            writeln!(f, "      → {}", finding.fix)?;
        }
        Ok(())
    }
}

/// **The report.** Everything about this dialect that Razeri can check by
/// asking it.
#[must_use]
pub fn check(dialect: &dyn PlatformDialect) -> Report {
    let info = dialect.info();
    let mut findings = Vec::new();

    check_registration(info.name, &mut findings);
    check_family(info.dialect_family, &mut findings);
    check_required_rendering(dialect, &mut findings);
    check_decisions(dialect, &mut findings);
    check_formula_renderer(dialect, &mut findings);
    check_paired_facts(dialect, &mut findings);

    findings.sort_by_key(|f| f.severity);
    Report { dialect: info.name.to_owned(), findings }
}

/// The name has to resolve, and it has to resolve to exactly one platform.
fn check_registration(name: &str, findings: &mut Vec<Finding>) {
    if crate::resolve_dialect_by_name(name).is_err() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!(
                "the name `{name}` does not resolve — nothing in this program registered it"
            ),
            fix: "add `inventory::submit! { DialectRegistration::new(\"<name>\", || \
                  Arc::new(MyDialect)) }` to your crate, and a `pub fn force_link() {}` beside \
                  it that the program using your dialect calls once. A crate nothing links is a \
                  crate the registry cannot see."
                .to_owned(),
        });
        return;
    }
    if !crate::resolve::alias_spellings()
        .iter()
        .any(|(spelling, canonical)| *spelling == name && *canonical == name)
    {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!(
                "`{name}` is registered but the resolver's spelling table does not carry it, so \
                 a design naming this platform is refused at the door"
            ),
            fix: "add your platform's accepted spellings to the resolver's alias table, including its \
                  own \
                  name, and add the same spellings to `schemas/razeri-design.schema.json`'s \
                  platform list — a test holds the two set-equal."
                .to_owned(),
        });
    }
}

/// A family is a claim about spelling, so an unmeasured engine claiming a
/// measured family's name licenses SQL it may reject.
fn check_family(family: DialectFamily, findings: &mut Vec<Finding>) {
    if matches!(family, DialectFamily::Custom(name) if name.trim().is_empty()) {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "this dialect's family is a custom one with no name".to_owned(),
            fix: "give `DialectFamily::Custom` your engine's own name — it is what a report \
                  prints and what a reader looks up."
                .to_owned(),
        });
    }
}

/// The four required methods must answer for every type a design can declare.
fn check_required_rendering(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
    let declarable = declarable_types();
    let empty_column: Vec<String> = declarable
        .iter()
        .filter(|dt| SqlRenderer::render_data_type(dialect, dt).trim().is_empty())
        .map(|dt| format!("{dt:?}"))
        .collect();
    if !empty_column.is_empty() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!(
                "`render_data_type` answers nothing for {} of the types a design can declare",
                empty_column.len()
            ),
            fix: "every declarable type needs a column spelling on this engine, even if the \
                  spelling is a wider type that stores it. A blank answer becomes a column \
                  declaration with no type in it."
                .to_owned(),
        });
    }
    let empty_cast: Vec<String> = declarable
        .iter()
        .filter(|dt| SqlRenderer::render_cast_type(dialect, dt).trim().is_empty())
        .map(|dt| format!("{dt:?}"))
        .collect();
    if !empty_cast.is_empty() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!(
                "`render_cast_type` answers nothing for {} declarable types",
                empty_cast.len()
            ),
            fix: "the generated load opens by casting every source column to its declared type, \
                  so a blank answer fails the load on its first statement."
                .to_owned(),
        });
    }
    let quoted = SqlRenderer::quote_identifier(dialect, "order");
    if quoted == "order" {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "`quote_identifier` returns the identifier unchanged, so a reserved word \
                   reaches this engine bare"
                .to_owned(),
            fix: "wrap the identifier in whatever this engine quotes with — double quotes, \
                  backticks, or brackets."
                .to_owned(),
        });
    }
    if SqlRenderer::render_current_timestamp(dialect).trim().is_empty() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "`render_current_timestamp` answers nothing, and every generated load stamps \
                   a row with it"
                .to_owned(),
            fix: "return this engine's own expression for the current moment.".to_owned(),
        });
    }
}

/// The decisions a dialect states rather than inherits.
fn check_decisions(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
    let spelling = SqlRenderer::sid_spelling(dialect);
    if spelling.platform == UNKNOWN_PLATFORM {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "this dialect states no surrogate-key spelling, so every key it renders calls \
                   an `xxhash64` its engine very likely does not have"
                .to_owned(),
            fix: "override `SqlRenderer::sid_spelling`. Say what this engine calls LOWER and \
                  TRIM, how it joins strings, how it casts to text, and how it spells a 64-bit \
                  hash. If it has no portable hash, name an artifact your prerequisites install \
                  — never the nearest-looking built-in, which keys a warehouse to a number \
                  nothing else can reproduce."
                .to_owned(),
        });
    }

    match SqlRenderer::procedure_form(dialect) {
        // Razeri's own agnostic renderer answers `Agnostic` on purpose: the
        // annotated form IS its output. Every other dialect that answers it has
        // simply not said.
        ProcedureForm::Agnostic if SqlRenderer::is_agnostic_dialect(dialect) => {}
        ProcedureForm::Agnostic => findings.push(Finding {
            severity: Severity::Unanswered,
            what: "this dialect states no stored-procedure grammar, so Razeri emits its own \
                   annotated form — which no engine parses"
                .to_owned(),
            fix: "override `SqlRenderer::procedure_form` with the grammar this engine speaks, \
                  or with `ProcedureForm::Unsupported(\"…\")` and a sentence saying what it \
                  offers instead."
                .to_owned(),
        }),
        ProcedureForm::Unsupported(orientation) => {
            check_refusal_copy("the stored-procedure refusal", orientation, findings);
        }
        _ => {}
    }

    if let Some(orientation) = SqlRenderer::table_function_form(dialect).refusal() {
        check_refusal_copy("the table-function refusal", orientation, findings);
    }

    // The note is what a reader of the DDL learns from, and on an engine with
    // no keyword of its own it is the ONLY place they can learn it.
    if let ConstraintEnforcement::DeclaredOnly(declared) =
        DdlRenderer::constraint_enforcement(dialect)
        && declared.note.split_whitespace().count() < 5
    {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "this dialect says its engine accepts constraints it does not enforce, and \
                   the note explaining that says almost nothing"
                .to_owned(),
            fix: "a reader of your rendered DDL sees a PRIMARY KEY that looks like a \
                  guarantee. Say what this engine really does with the declaration, and where \
                  the guarantee has to come from instead."
                .to_owned(),
        });
    }

    // **The two refusals are different findings, and that is the point of the
    // second variant.** `None` is silence, and silence is unanswered. A stated
    // `Unsupported` is a measured claim; what is left to grade there is whether
    // the sentence is worth reading, because it is the only thing an operator
    // gets where the load would have been.
    match DialectCapabilities::merge_shape(dialect) {
        MergeShape::None => findings.push(Finding {
            severity: Severity::Unanswered,
            what: "this dialect has not stated its upsert shape, so Razeri writes no generated \
                   load for it"
                .to_owned(),
            fix: "if this engine has a MERGE or an upsert statement, say which in \
                  `DialectCapabilities::merge_shape`. If it genuinely has neither, answer \
                  `MergeShape::Unsupported` with your engine's own sentence — a refusal an \
                  operator can act on, rather than a silence."
                .to_owned(),
        }),
        MergeShape::Unsupported(orientation) => {
            check_refusal_copy("the upsert refusal", orientation, findings);
        }
        _ => {}
    }
}

/// The one override nothing tells an author about, and every formula in every
/// design goes through it.
fn check_formula_renderer(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
    let formula = SqlRenderer::to_formula_renderer(dialect);
    let dialect_name = SqlRenderer::dialect_name(dialect);
    if formula.dialect_name() != dialect_name && dialect_name != "ansi" {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: "`to_formula_renderer` was not overridden, so every formula in every design \
                   renders through Razeri's ANSI defaults instead of this dialect"
                .to_owned(),
            fix: "return `Box::new(Self)`. It is one line, nothing fails without it, and every \
                  formula this engine sees is silently somebody else's."
                .to_owned(),
        });
    }
}

/// A refusal that does not say what to do instead is a wall.
fn check_refusal_copy(subject: &str, copy: &str, findings: &mut Vec<Finding>) {
    if copy.trim().is_empty() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!("{subject} has no words in it"),
            fix: "say what this engine offers instead. A reader who hits a refusal needs the \
                  next thing to try, and it has to be about their engine."
                .to_owned(),
        });
        return;
    }
    if copy.split_whitespace().count() < 5 {
        findings.push(Finding {
            severity: Severity::Note,
            what: format!("{subject} is very short"),
            fix: "a refusal earns its place by naming the alternative — what this engine has \
                  instead of the thing it cannot do."
                .to_owned(),
        });
    }
}

/// Fourteen facts are declared twice and must agree.
fn check_paired_facts(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
    let caps: &dyn DialectCapabilities = dialect;
    let ren: &dyn SqlRenderer = dialect;
    let ddl: &dyn DdlRenderer = dialect;
    let mut disagreements: Vec<&'static str> = Vec::new();

    macro_rules! pair {
        ($m:ident, $renderer_value:expr) => {
            if caps.$m() != $renderer_value {
                disagreements.push(stringify!($m));
            }
        };
    }
    pair!(supports_on_conflict, ren.supports_on_conflict());
    pair!(supports_insert_overwrite, ren.supports_insert_overwrite());
    pair!(supports_update_from, ren.supports_update_from());
    pair!(supports_named_windows, ren.supports_named_windows());
    pair!(supports_compound_assignment, ren.supports_compound_assignment());
    pair!(supports_window_frame_exclude, ren.supports_window_frame_exclude());
    pair!(supports_merge_not_matched_by_source, ren.supports_merge_not_matched_by_source());
    pair!(merge_not_matched_keyword, ren.merge_not_matched_keyword());
    pair!(uses_alter_table_update, ren.uses_alter_table_update());
    pair!(supports_multi_table_insert, ren.supports_multi_table_insert());
    pair!(supports_cascade_drop, ddl.supports_cascade_drop());
    pair!(supports_sync_drop, ddl.supports_sync_drop());
    pair!(supports_force_drop, ddl.supports_force_drop());

    // **The fourteenth, whose two sides are different types.** The capability
    // is a boolean; the renderer answer is a form that either renders or
    // carries a refusal. Both are read — the build door reads the form, and
    // registry-walking rosters read the capability — so a dialect claiming a
    // table function on one side and refusing on the other has an engine that
    // both does and does not have one. `SQLite` was exactly that on the day
    // this check was written: `false` since the capability existed, and a
    // renderer still inheriting PostgreSQL's `RETURNS TABLE`.
    if caps.supports_table_functions() != ren.table_function_form().refusal().is_none() {
        disagreements.push("supports_table_functions");
    }

    if !disagreements.is_empty() {
        findings.push(Finding {
            severity: Severity::Blocking,
            what: format!(
                "this dialect declares {} fact(s) two ways and the two disagree: {}",
                disagreements.len(),
                disagreements.join(", ")
            ),
            fix: "each of these is declared once on the capability trait and once in the \
                  renderer hierarchy, and Razeri reads both. Decide which is true of your engine \
                  and move the other one to match."
                .to_owned(),
        });
    }
}

/// **What a command has to do for you**, in the order it is owed.
///
/// These are Razeri's repository obligations: frozen matrices, generated
/// bindings, and the allowlist a new crate joins. A dialect built outside this
/// repository owes none of them; a dialect built inside it owes all of them,
/// and each has an excellent failure message that nobody can read before it
/// fires.
#[must_use]
pub const fn repository_obligations() -> &'static [Obligation] {
    &[
        Obligation {
            what: "classify the new crate — every workspace member is named exactly once, and \
                   dialect crates are tier 1",
            command: "scripts/check-tier-boundary.sh",
        },
        Obligation {
            what: "freeze the DDL your platform renders for all eleven corpus designs",
            command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test golden_ddl",
        },
        Obligation {
            what: "freeze the derived-column projections your platform renders",
            command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test expression_matrix",
        },
        Obligation {
            what: "freeze the transaction envelope your platform renders",
            command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize \
                      --test load_transaction_matrix",
        },
        Obligation {
            what: "regenerate the typed platform constants for Python, TypeScript, Go, C# and \
                   Java",
            command: "cargo run -p razeri-dialects --bin gen-platform-constants",
        },
        Obligation {
            what: "give every registry-walking roster a row for your platform — they fail one \
                   at a time otherwise",
            command: "cargo test -p razeri-materialize -p razeri-sql",
        },
        Obligation {
            what: "prove the artifacts deploy and run: an execution leg per routine kind and \
                   per generated load is what finished means here",
            command: "cargo test -p integration-tests",
        },
    ]
}

/// Every type a design can declare, which is the set the required methods owe
/// an answer for.
fn declarable_types() -> Vec<DataType> {
    vec![
        DataType::tinyint(),
        DataType::smallint(),
        DataType::int(),
        DataType::bigint(),
        DataType::double(),
        DataType::boolean(),
        DataType::text(),
        DataType::varchar(200),
        DataType::uuid(),
        DataType::json(),
        DataType::date(),
        DataType::timestamp(),
        DataType::timestamp_tz(),
        DataType::decimal(18, 2),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dialect::DialectInfo;
    use razeri_plugin_contract::{
        DmlRenderer, ExpressionRenderer, FormulaRenderer, MergeRestrictions, ProcedureRenderer,
        ReplicationRenderer, ScalarFunctionForm, SidSpelling, StorageRenderer, TaskRenderer,
        TextCast,
    };

    // ── The broken fixture ────────────────────────────────────────────
    //
    // A plugin that compiles, registers nothing it should, and answers every
    // question with somebody else's default. This is what the kit exists to
    // catch, and it is deliberately the plugin an author gets by implementing
    // only the four required methods and stopping — which the guide, until
    // now, told them was enough.

    struct BrokenPlugin;

    impl SqlRenderer for BrokenPlugin {
        fn render_data_type(&self, _dt: &DataType) -> String {
            String::new()
        }
        fn quote_identifier(&self, name: &str) -> String {
            name.to_owned()
        }
        fn render_cast_type(&self, _dt: &DataType) -> String {
            String::new()
        }
        fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
            name.to_owned()
        }
        fn render_current_timestamp(&self) -> String {
            String::new()
        }
        fn dialect_name(&self) -> &'static str {
            "brokendb"
        }
        // No `sid_spelling`, no `procedure_form`, no `to_formula_renderer` —
        // and one fact declared twice, two ways.
        fn supports_on_conflict(&self) -> bool {
            true
        }
    }
    impl ExpressionRenderer for BrokenPlugin {}
    impl DdlRenderer for BrokenPlugin {}
    impl DmlRenderer for BrokenPlugin {}
    impl ProcedureRenderer for BrokenPlugin {}
    impl FormulaRenderer for BrokenPlugin {}
    impl TaskRenderer for BrokenPlugin {}
    impl ReplicationRenderer for BrokenPlugin {}
    impl StorageRenderer for BrokenPlugin {}
    impl DialectCapabilities for BrokenPlugin {
        fn supports_on_conflict(&self) -> bool {
            false
        }
    }
    impl PlatformDialect for BrokenPlugin {
        fn info(&self) -> DialectInfo {
            DialectInfo {
                name: "brokendb",
                display_name: "Broken DB",
                dialect_family: DialectFamily::Custom(""),
                family: "Broken",
                is_cloud_native: false,
                is_olap: false,
                is_postgres_compatible: false,
            }
        }
    }

    // ── The healthy fixture ───────────────────────────────────────────
    //
    // The same plugin with every question answered. It is not registered
    // either, so its report still carries the registration finding — which is
    // the honest report for a dialect nothing has linked, and the one
    // difference between "run me from your own crate's tests" and "run me
    // from Razeri's CLI".

    struct HealthyPlugin;

    impl SqlRenderer for HealthyPlugin {
        fn render_data_type(&self, dt: &DataType) -> String {
            razeri_plugin_contract::agnostic::render_data_type_ansi(dt)
        }
        fn quote_identifier(&self, name: &str) -> String {
            format!("\"{name}\"")
        }
        fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
            schema.map_or_else(
                || self.quote_identifier(name),
                |s| format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
            )
        }
        fn render_current_timestamp(&self) -> String {
            "NOW()".to_owned()
        }
        fn dialect_name(&self) -> &'static str {
            "healthydb"
        }
        fn sid_spelling(&self) -> SidSpelling {
            SidSpelling {
                platform: "healthydb",
                hash: ("healthy_xxhash64(", ")"),
                cast_type: TextCast::Direct("STRING"),
                through_helper: false,
                ..razeri_plugin_contract::ANSI_SID_SPELLING
            }
        }
        fn procedure_form(&self) -> ProcedureForm {
            ProcedureForm::Unsupported(
                "Healthy DB has no stored procedures; write the load as a scheduled query \
                 instead.",
            )
        }
        fn scalar_function_form(&self) -> ScalarFunctionForm {
            ScalarFunctionForm::PostgreSql
        }
        fn to_formula_renderer(&self) -> Box<dyn FormulaRenderer> {
            Box::new(Self)
        }
    }
    impl ExpressionRenderer for HealthyPlugin {}
    impl DdlRenderer for HealthyPlugin {}
    impl DmlRenderer for HealthyPlugin {}
    impl ProcedureRenderer for HealthyPlugin {}
    impl FormulaRenderer for HealthyPlugin {}
    impl TaskRenderer for HealthyPlugin {}
    impl ReplicationRenderer for HealthyPlugin {}
    impl StorageRenderer for HealthyPlugin {}
    impl DialectCapabilities for HealthyPlugin {
        fn merge_shape(&self) -> MergeShape {
            MergeShape::Merge(MergeRestrictions::none())
        }
    }
    impl PlatformDialect for HealthyPlugin {
        fn info(&self) -> DialectInfo {
            DialectInfo {
                name: "healthydb",
                display_name: "Healthy DB",
                dialect_family: DialectFamily::Custom("healthydb"),
                family: "Healthy",
                is_cloud_native: false,
                is_olap: false,
                is_postgres_compatible: false,
            }
        }
    }

    /// Every finding whose `what` line mentions one of these subjects.
    fn mentions(report: &Report, needle: &str) -> bool {
        report.findings.iter().any(|f| f.what.contains(needle) || f.fix.contains(needle))
    }

    /// **The kit's own red.** A plugin that implements the four required
    /// methods and stops is exactly what the guide used to produce, and every
    /// one of these findings is a defect a shipped dialect actually had.
    #[test]
    fn the_kit_catches_a_deliberately_broken_plugin() {
        let report = check(&BrokenPlugin);
        assert!(!report.is_clean(), "the broken plugin reported clean:\n{report}");

        assert!(mentions(&report, "does not resolve"), "unregistered:\n{report}");
        assert!(mentions(&report, "custom one with no name"), "unnamed family:\n{report}");
        assert!(mentions(&report, "render_data_type"), "empty column types:\n{report}");
        assert!(mentions(&report, "render_cast_type"), "empty cast types:\n{report}");
        assert!(mentions(&report, "identifier unchanged"), "no quoting:\n{report}");
        assert!(mentions(&report, "render_current_timestamp"), "no now():\n{report}");
        assert!(mentions(&report, "surrogate-key spelling"), "unspelled key:\n{report}");
        assert!(mentions(&report, "stored-procedure grammar"), "no procedure form:\n{report}");
        assert!(mentions(&report, "upsert shape"), "no merge shape:\n{report}");
        assert!(mentions(&report, "to_formula_renderer"), "formula renderer:\n{report}");
        assert!(mentions(&report, "supports_on_conflict"), "paired fact:\n{report}");

        assert!(
            report.count(Severity::Blocking) >= 8,
            "the broken plugin owes more than {} blocking fixes:\n{report}",
            report.count(Severity::Blocking)
        );
    }

    /// **The kit does not cry wolf.** The same plugin with its questions
    /// answered reports only what is actually true of it: it is not
    /// registered in this test binary, and nothing else.
    #[test]
    fn the_kit_reports_only_registration_on_a_plugin_that_answered_everything() {
        let report = check(&HealthyPlugin);
        let unregistered: Vec<&Finding> =
            report.findings.iter().filter(|f| f.what.contains("does not resolve")).collect();
        assert_eq!(unregistered.len(), 1, "the registration finding is the honest one here");
        assert_eq!(
            report.findings.len(),
            1,
            "a plugin that answered every question owes nothing else:\n{report}"
        );
    }

    /// **A stated refusal is a claim, and a claim owes the reader words** — the
    /// same grading every other refusal gets, because these words are the only
    /// thing an operator finds where the load would have been.
    #[test]
    fn a_stated_upsert_refusal_is_graded_as_copy_and_a_silent_one_as_unanswered() {
        let mut mute = Vec::new();
        check_refusal_copy("the upsert refusal", "no merge", &mut mute);
        assert_eq!(mute.len(), 1, "a three-word refusal is a wall: {mute:?}");
        assert!(mute[0].what.contains("the upsert refusal"), "{mute:?}");

        let silence = check(&BrokenPlugin);
        assert!(
            silence
                .findings
                .iter()
                .any(|f| f.severity == Severity::Unanswered && f.what.contains("upsert shape")),
            "a dialect that never answered is unanswered, not wrong:\n{silence}"
        );
        assert!(
            silence.findings.iter().any(|f| f.fix.contains("MergeShape::Unsupported")),
            "the fix line names the way to state a measured refusal:\n{silence}"
        );
    }

    /// The report leads with what to fix and says it in words a reader can
    /// act on — no identifiers from Razeri's own record-keeping, and a
    /// one-line count before the detail.
    #[test]
    fn the_report_reads_as_a_summary_then_a_fix_list() {
        let text = check(&BrokenPlugin).to_string();
        let first = text.lines().next().expect("a summary line");
        assert!(first.starts_with("brokendb:"), "{first}");
        assert!(first.contains("to fix"), "{first}");
        assert!(text.contains("      → "), "every finding names the fix:\n{text}");
    }

    /// The repository obligations are commands, in the order they are owed,
    /// and every one of them says what it is for.
    #[test]
    fn every_repository_obligation_names_a_command_and_a_reason() {
        let obligations = repository_obligations();
        assert!(obligations.len() >= 5);
        for o in obligations {
            assert!(!o.command.trim().is_empty(), "{o:?}");
            assert!(o.what.split_whitespace().count() >= 5, "{o:?}");
        }
    }
}