ripvec-core 4.1.0

Semantic code + document search engine. Cacheless static-embedding + cross-encoder rerank by default; optional ModernBERT/BGE transformer engines with GPU backends. Tree-sitter chunking, hybrid BM25 + PageRank, composable ranking layers.
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
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
//! Language registry mapping file extensions to tree-sitter grammars.
//!
//! Each supported language has a grammar and a tree-sitter query that
//! extracts function, class, and method definitions. Compiled queries
//! are cached so that repeated calls for the same extension are free.

use std::sync::{Arc, OnceLock};

use tree_sitter::{Language, Query};

/// Configuration for extracting function calls from a language.
///
/// Wrapped in [`Arc`] so it can be shared across threads and returned
/// from the cache without cloning the compiled [`Query`].
pub struct CallConfig {
    /// The tree-sitter Language grammar.
    pub language: Language,
    /// Query that extracts call sites (`@callee` captures).
    pub query: Query,
}

/// Configuration for a supported source language.
///
/// Wrapped in [`Arc`] so it can be shared across threads and returned
/// from the cache without cloning the compiled [`Query`].
pub struct LangConfig {
    /// The tree-sitter Language grammar.
    pub language: Language,
    /// Query that extracts semantic chunks (`@def` captures with `@name`).
    pub query: Query,
}

/// LSP `SymbolKind` numeric values (as defined in the Language Server Protocol
/// specification version 3.17, §3.15.1).
///
/// Only the subset used by ripvec's Rust mapping is listed here. The full
/// specification defines values 1–26; constants are named for clarity and to
/// avoid embedding magic numbers at call sites.
pub mod lsp_symbol_kind {
    /// A file symbol. (1)
    pub const FILE: u32 = 1;
    /// A module or namespace. (2)
    pub const MODULE: u32 = 2;
    /// A namespace. (3)
    pub const NAMESPACE: u32 = 3;
    /// A package. (4)
    pub const PACKAGE: u32 = 4;
    /// A class. (5)
    pub const CLASS: u32 = 5;
    /// A method. (6)
    pub const METHOD: u32 = 6;
    /// A property. (7)
    pub const PROPERTY: u32 = 7;
    /// A field. (8)
    pub const FIELD: u32 = 8;
    /// A constructor. (9)
    pub const CONSTRUCTOR: u32 = 9;
    /// An enum type. (10)
    pub const ENUM: u32 = 10;
    /// An interface (trait in Rust). (11)
    pub const INTERFACE: u32 = 11;
    /// A function or free function. (12)
    pub const FUNCTION: u32 = 12;
    /// A variable. (13)
    pub const VARIABLE: u32 = 13;
    /// A constant or `const`/`static` item. (14)
    pub const CONSTANT: u32 = 14;
    /// A string literal symbol. (15)
    pub const STRING: u32 = 15;
    /// A numeric constant. (16)
    pub const NUMBER: u32 = 16;
    /// A boolean symbol. (17)
    pub const BOOLEAN: u32 = 17;
    /// An array or slice symbol. (18)
    pub const ARRAY: u32 = 18;
    /// An object or struct-like value. (19)
    pub const OBJECT: u32 = 19;
    /// A key in a key-value pair. (20)
    pub const KEY: u32 = 20;
    /// A null value symbol. (21)
    pub const NULL: u32 = 21;
    /// An enum member / variant. (22)
    pub const ENUM_MEMBER: u32 = 22;
    /// A struct type. (23)
    pub const STRUCT: u32 = 23;
    /// An event. (24)
    pub const EVENT: u32 = 24;
    /// An operator. (25)
    pub const OPERATOR: u32 = 25;
    /// A type parameter / type alias. (26)
    pub const TYPE_PARAMETER: u32 = 26;
}

/// Map a tree-sitter node kind string to an LSP `SymbolKind` numeric value.
///
/// The mapping covers Rust node kinds exhaustively, then falls back to a
/// cross-language best-effort mapping, and finally returns
/// [`lsp_symbol_kind::VARIABLE`] (13) for any unrecognised kind — preserving
/// the pre-B1 default so callers that don't need kind-awareness are unaffected.
///
/// # Rust node kinds
///
/// | tree-sitter kind | LSP SymbolKind |
/// |---|---|
/// | `function_item` | 12 (Function) |
/// | `function_signature_item` | 12 (Function) |
/// | `struct_item` | 23 (Struct) |
/// | `enum_item` | 10 (Enum) |
/// | `trait_item` | 11 (Interface) |
/// | `impl_item` | 5 (Class) — implementation block |
/// | `mod_item` | 2 (Module) |
/// | `const_item` | 14 (Constant) |
/// | `static_item` | 14 (Constant) |
/// | `type_item` | 26 (TypeParameter) |
/// | `field_declaration` | 8 (Field) |
/// | `enum_variant` | 22 (EnumMember) |
///
/// # Cross-language kinds
///
/// | tree-sitter kind | LSP SymbolKind |
/// |---|---|
/// | `function_definition` / `function_declaration` | 12 (Function) |
/// | `method_definition` / `method_declaration` | 6 (Method) |
/// | `class_definition` / `class_declaration` / `class_specifier` | 5 (Class) |
/// | `interface_declaration` / `trait_definition` / `interface_type` | 11 (Interface) |
/// | `variable_declarator` / `variable_declaration` / `assignment` | 13 (Variable) |
/// | `enum_declaration` / `enum_definition` | 10 (Enum) |
/// | `type_alias_declaration` / `type_definition` / `type_declaration` / `type_alias` | 26 (TypeParameter) |
/// | `constructor_declaration` | 9 (Constructor) |
/// | `module` | 2 (Module) |
/// | `object_definition` / `object_declaration` | 5 (Class) |
/// | `val_definition` / `var_definition` | 13 (Variable) |
/// | `property_declaration` / `decorated_definition` | 7 (Property) |
/// | `namespace_definition` | 3 (Namespace) |
/// | `protocol_declaration` | 11 (Interface) |
/// | `typealias_declaration` | 26 (TypeParameter) |
/// | `struct_type` | 23 (Struct) — Go struct type body |
/// | `block` / `table` / `pair` | 20 (Key) |
/// | `atx_heading` | 2 (Module) |
/// | `element` / `empty_element` | 5 (Class) |
/// | `rdf_statements` | 19 (Object) |
/// | `file` / `window` | 1 (File) |
#[must_use]
#[expect(
    clippy::match_same_arms,
    reason = "variable/assignment patterns are explicit for documentation completeness; \
              they intentionally duplicate the wildcard fallback (VARIABLE=13) so the \
              mapping table reads as a self-contained reference"
)]
pub fn lsp_symbol_kind_for_node_kind(node_kind: &str) -> u32 {
    use lsp_symbol_kind as K;
    match node_kind {
        // --- Function / method ---
        // Rust: function_item, function_signature_item
        // Cross-language: function_definition (Python, Ruby, Scala, Go, Java, Kotlin, Swift),
        //   function_declaration (C, C++, JS, TS, Bash)
        "function_item"
        | "function_signature_item"
        | "function_definition"
        | "function_declaration" => K::FUNCTION,

        // --- Method (non-free function bound to a type) ---
        "method_definition" | "method_declaration" => K::METHOD,

        // --- Constructor ---
        "constructor_declaration" => K::CONSTRUCTOR,

        // --- Struct ---
        "struct_item" => K::STRUCT,

        // --- Enum ---
        // Rust: enum_item; cross-language: enum_declaration, enum_definition
        "enum_item" | "enum_declaration" | "enum_definition" => K::ENUM,

        // --- Enum member / variant ---
        "enum_variant" => K::ENUM_MEMBER,

        // --- Interface / trait ---
        // Rust: trait_item; cross-language: interface_declaration, trait_definition,
        //   protocol_declaration (Swift); interface_type (Go — the body node of an
        //   interface type_spec, used as @def in the Go query to distinguish interfaces
        //   from other type declarations — K2 fix)
        "trait_item"
        | "interface_declaration"
        | "trait_definition"
        | "protocol_declaration"
        | "interface_type" => K::INTERFACE,

        // --- Struct ---
        // Go: struct_type is the body node of a struct type_spec; used as @def
        //   to distinguish struct declarations from other type declarations — K2 fix.
        "struct_type" => K::STRUCT,

        // --- Class / impl block ---
        // Rust: impl_item (implementation block — closest to LSP Class)
        // Cross-language: class_definition, class_declaration, class_specifier,
        //   object_definition (Scala), object_declaration (Kotlin),
        //   element (XML/RDF ontology element)
        "impl_item" | "class_definition" | "class_declaration" | "class_specifier"
        | "object_definition" | "object_declaration" | "element" => K::CLASS,

        // --- Module / namespace (used by heading-level symbols too) ---
        // Rust: mod_item; cross-language: module (Python module, Ruby module),
        //   mod_definition; atx_heading (Markdown headings as module-level anchors)
        "mod_item" | "module" | "mod_definition" | "atx_heading" => K::MODULE,

        // --- Namespace ---
        "namespace_definition" => K::NAMESPACE,

        // --- Constant ---
        // Rust: const_item and static_item (immutable statics are semantically constants)
        // HCL: local_attribute is the synthetic kind emitted by the chunker
        // for each `local.X = ...` attribute inside an HCL `locals { ... }`
        // block. T18 Cohesion Refraction queries can now distinguish
        // individual locals as Constants (R6, Wave 3).
        "const_item" | "static_item" | "local_attribute" => K::CONSTANT,

        // --- Type alias / type parameter ---
        // Rust: type_item; cross-language: type_alias_declaration (TS),
        //   type_definition (C typedef), type_declaration (Go fallback for non-interface
        //   non-struct types), typealias_declaration (Swift); type_alias (Go — the body
        //   node of `type X = Y` alias declarations — L2 fix: now maps to VARIABLE instead
        //   of TYPE_PARAMETER to distinguish aliases from pure generics)
        "type_item"
        | "type_alias_declaration"
        | "type_definition"
        | "type_declaration"
        | "typealias_declaration" => K::TYPE_PARAMETER,

        // --- Type alias (distinct from type parameters) ---
        // Go: type_alias is the @def node from `type X = Y` patterns (K2 fix).
        // L2 fix: maps to VARIABLE (13) instead of TYPE_PARAMETER (26) to distinguish
        // aliases from pure generics. Variable is used because LSP has no dedicated
        // alias kind, and Variable better represents the semantic nature of an alias
        // than TypeParameter (which represents generic type variables like T in [T any]).
        "type_alias" => K::VARIABLE,

        // --- Field ---
        "field_declaration" => K::FIELD,

        // --- Variable / assignment ---
        // Cross-language: variable_declarator (JS/TS), variable_declaration,
        //   assignment (Python, Ruby top-level), val_definition / var_definition (Scala)
        "variable_declarator"
        | "variable_declaration"
        | "assignment"
        | "val_definition"
        | "var_definition" => K::VARIABLE,

        // --- Property ---
        // Kotlin / Swift: property_declaration.
        // Python: decorated_definition (a function wrapped in a decorator).
        //   NOTE: this string-only mapping returns PROPERTY for ALL
        //   decorated_definition nodes, which is intentionally conservative
        //   (K1 fix: previously fell through to VARIABLE=13 as an unrecognised kind).
        //   Callers that have the full AST node should use `lsp_symbol_kind_for_node`
        //   instead, which inspects the first decorator name and returns FUNCTION (12)
        //   for @classmethod, @staticmethod, and arbitrary decorators (I#39 fix).
        "property_declaration" | "decorated_definition" => K::PROPERTY,

        // --- Data / config file structural kinds ---
        "block" | "table" | "pair" => K::KEY,

        // --- SQL kinds (S1, Wave 4) ---
        // SQL: tree-sitter-sequel emits `create_table` for `CREATE TABLE foo (...)`.
        //   STRUCT (23) matches Rust struct_item and Go struct_type: a table is the
        //   relational equivalent of a record type.
        "create_table" => K::STRUCT,
        // SQL: tree-sitter-sequel emits `cte` for `WITH foo AS (SELECT ...)`. CTEs
        //   are scoped intermediate result names — VARIABLE (13) is the closest LSP
        //   shape (no dedicated "alias" kind).
        "cte" => K::VARIABLE,
        // SQL: synthetic file-level def emitted by `repo_map::enrich_sql_file_def`
        //   for dbt/sqlmesh files whose model name is the filename stem and whose
        //   sqlmesh `MODEL (name @{schema}.X, ...)` header parses as an ERROR node.
        //   FILE (1) matches the "this whole file is the symbol" semantic.
        "sql_file" => K::FILE,

        // --- Fallback / special chunker kinds ---
        "rdf_statements" => K::OBJECT,
        "file" | "window" => K::FILE,

        // --- Unknown: preserve pre-B1 default (Variable = 13) ---
        _ => K::VARIABLE,
    }
}

/// Resolve the LSP `SymbolKind` for a `decorated_definition` tree-sitter node
/// given its first decorator name.
///
/// This is the authoritative decorator-kind mapping (I#39):
/// - `"property"` → 7 (Property) — the K1 target case
/// - `"cached_property"` → 7 (Property) — lazy property with identical semantics
/// - `"classmethod"` | `"staticmethod"` | any other identifier → 12 (Function)
/// - Complex decorators (`attribute` / `call` children — e.g. `@functools.wraps(f)`)
///   are passed as `""` by [`lsp_symbol_kind_for_node`] and fall through to 12.
///
/// Callers that have the full tree-sitter node should prefer
/// [`lsp_symbol_kind_for_node`] which extracts the decorator name automatically.
/// This function is exposed for callers that have already extracted the decorator
/// name (e.g., from a stored string field).
#[must_use]
pub fn lsp_symbol_kind_for_decorated_definition(first_decorator_name: &str) -> u32 {
    use lsp_symbol_kind as K;
    match first_decorator_name {
        "property" | "cached_property" => K::PROPERTY,
        // @classmethod and @staticmethod: these are class-bound callables, not
        // properties. FUNCTION (12) is used instead of METHOD (6) because the
        // outer context (class vs top-level) is not available here and FUNCTION
        // is the safer conservative choice. Callers that know the enclosing scope
        // may upgrade to METHOD.
        _ => K::FUNCTION,
    }
}

/// Extract the name of the first `decorator` child of a `decorated_definition`
/// tree-sitter node.
///
/// Returns `Some(name)` when the first decorator is a simple `identifier`
/// (e.g., `@property`, `@classmethod`, `@staticmethod`, `@cached_property`).
///
/// Returns `None` when:
/// - The node has no `decorator` children.
/// - The first decorator's first non-trivial child is an `attribute` node
///   (e.g., `@functools.wraps`) or a `call` node (e.g., `@functools.wraps(f)`).
///   These complex decorators are not property-like.
///
/// The returned `&str` is a slice of `source` (zero-copy).
fn first_decorator_ident<'src>(
    node: &tree_sitter::Node<'_>,
    source: &'src [u8],
) -> Option<&'src str> {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if child.kind() == "decorator" {
            // A decorator node looks like: `"@" <expression>`.
            // Walk its children to find the expression child.
            let mut inner = child.walk();
            for inner_child in child.children(&mut inner) {
                match inner_child.kind() {
                    // Simple @name decorator — return the identifier text.
                    "identifier" => {
                        let start = inner_child.start_byte();
                        let end = inner_child.end_byte();
                        return std::str::from_utf8(&source[start..end]).ok();
                    }
                    // Complex decorator (attribute access or call) — not a simple
                    // identifier; treat as arbitrary (non-property) decorator.
                    "attribute" | "call" => return None,
                    // Skip punctuation/whitespace nodes (e.g., the "@" token).
                    _ => {}
                }
            }
            // Decorator had no recognisable expression child.
            return None;
        }
    }
    None
}

/// Map a tree-sitter `Node` to an LSP `SymbolKind` numeric value.
///
/// This is the decorator-aware variant of [`lsp_symbol_kind_for_node_kind`].
/// For `decorated_definition` nodes (Python), it inspects the first decorator
/// child to distinguish `@property` (→ 7) from other decorators (→ 12).
/// For all other node kinds, it delegates to [`lsp_symbol_kind_for_node_kind`].
///
/// # Python decorator mapping (I#39)
///
/// | First decorator | LSP SymbolKind |
/// |---|---|
/// | `@property` | 7 (Property) |
/// | `@cached_property` | 7 (Property) |
/// | `@classmethod` | 12 (Function) |
/// | `@staticmethod` | 12 (Function) |
/// | `@functools.wraps(f)` (complex) | 12 (Function) |
/// | any other | 12 (Function) |
///
/// Callers that have the full tree-sitter parse tree available (e.g., the
/// chunker's `extract_name_captures`) should call this function instead of
/// `lsp_symbol_kind_for_node_kind(node.kind())` to get correct Python decorator
/// classification.
#[must_use]
pub fn lsp_symbol_kind_for_node(node: &tree_sitter::Node<'_>, source: &[u8]) -> u32 {
    if node.kind() == "decorated_definition" {
        let decorator = first_decorator_ident(node, source).unwrap_or("");
        return lsp_symbol_kind_for_decorated_definition(decorator);
    }
    lsp_symbol_kind_for_node_kind(node.kind())
}

/// Check whether a [`tree_sitter::Language`] is the Rust grammar.
///
/// Used by [`crate::repo_map`] to gate Rust-specific receiver-type heuristics.
/// Compares the node-kind count as a proxy for grammar identity (the node-kind
/// string table is stable across compatible ABI versions and differs between
/// grammars).
#[must_use]
pub fn is_rust_language(lang: &tree_sitter::Language) -> bool {
    let rust_lang: tree_sitter::Language = tree_sitter_rust::LANGUAGE.into();
    // Both must have the same ABI version AND the same number of node kinds
    // (a grammar-specific constant). This is not a guaranteed identity check but
    // is reliable enough for our heuristic gating.
    lang.abi_version() == rust_lang.abi_version()
        && lang.node_kind_count() == rust_lang.node_kind_count()
}

/// Check whether a [`tree_sitter::Language`] is the Python grammar.
///
/// Used by [`crate::repo_map`] to gate Python-specific receiver-type heuristics.
/// Same node-kind-count proxy as [`is_rust_language`].
#[must_use]
pub fn is_python_language(lang: &tree_sitter::Language) -> bool {
    let py_lang: tree_sitter::Language = tree_sitter_python::LANGUAGE.into();
    lang.abi_version() == py_lang.abi_version()
        && lang.node_kind_count() == py_lang.node_kind_count()
}

/// Check whether a [`tree_sitter::Language`] is the Go grammar.
///
/// Used by [`crate::repo_map`] to gate Go-specific receiver-type heuristics.
/// Same node-kind-count proxy as [`is_rust_language`].
#[must_use]
pub fn is_go_language(lang: &tree_sitter::Language) -> bool {
    let go_lang: tree_sitter::Language = tree_sitter_go::LANGUAGE.into();
    lang.abi_version() == go_lang.abi_version()
        && lang.node_kind_count() == go_lang.node_kind_count()
}

/// Check whether a [`tree_sitter::Language`] is the HCL grammar.
///
/// Used by [`crate::repo_map`] to gate HCL-specific call-edge extraction
/// (terraform_remote_state references and module blocks — R2/R3, Wave 3).
/// Same node-kind-count proxy as [`is_rust_language`].
#[must_use]
pub fn is_hcl_language(lang: &tree_sitter::Language) -> bool {
    let hcl_lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
    lang.abi_version() == hcl_lang.abi_version()
        && lang.node_kind_count() == hcl_lang.node_kind_count()
}

/// Check whether a [`tree_sitter::Language`] is the SQL grammar (tree-sitter-sequel).
///
/// Used by [`crate::repo_map`] to gate SQL-specific enrichment — the synthetic
/// file-level def for dbt/sqlmesh models (S1, Wave 4). Same node-kind-count proxy
/// as [`is_rust_language`].
#[must_use]
pub fn is_sql_language(lang: &tree_sitter::Language) -> bool {
    let sql_lang: tree_sitter::Language = tree_sitter_sequel::LANGUAGE.into();
    lang.abi_version() == sql_lang.abi_version()
        && lang.node_kind_count() == sql_lang.node_kind_count()
}

/// Derive the canonical symbol name for an HCL `block` AST node.
///
/// HCL blocks have the form `keyword "type_label" "name_label" { ... }`:
/// - `resource "aws_iam_role" "loader" { ... }` → `"aws_iam_role.loader"`
/// - `data "aws_s3_bucket" "main" { ... }`      → `"aws_s3_bucket.main"`
/// - `variable "region" { ... }`                → `"region"`
/// - `output "role_arn" { ... }`                → `"role_arn"`
/// - `locals { ... }`                           → `"locals"`
///
/// This function is the authoritative composite-name implementation (K3). The chunker
/// pipeline uses the `@name` capture from the HCL query (the last string label or the
/// keyword for no-label blocks). Callers that need the full `type.name` format — e.g.
/// `"aws_iam_role.loader"` — should call this function directly after identifying the
/// block node.
///
/// Returns an owned `String` with the composite name. If the `block_node` is not
/// a `block` node, returns an empty string.
#[must_use]
pub fn derive_hcl_block_name(block_node: &tree_sitter::Node<'_>, source: &[u8]) -> String {
    if block_node.kind() != "block" {
        return String::new();
    }
    // Collect all template_literal texts from string_lit children (the block labels).
    // Children order: identifier, string_lit*, block_start, body, block_end.
    let mut labels: Vec<&str> = Vec::new();
    let mut cursor = block_node.walk();
    for child in block_node.children(&mut cursor) {
        if child.kind() == "string_lit" {
            // Walk into string_lit to find template_literal
            let mut inner = child.walk();
            for grandchild in child.children(&mut inner) {
                if grandchild.kind() == "template_literal" {
                    let start = grandchild.start_byte();
                    let end = grandchild.end_byte();
                    if let Ok(text) = std::str::from_utf8(&source[start..end]) {
                        labels.push(text);
                    }
                }
            }
        } else if child.kind() == "block_start" {
            // Stop at the opening brace — everything after is the block body.
            break;
        }
    }
    match labels.len() {
        0 => {
            // No string labels — use the identifier (e.g. "locals").
            let mut cursor2 = block_node.walk();
            for child in block_node.children(&mut cursor2) {
                if child.kind() == "identifier" {
                    let start = child.start_byte();
                    let end = child.end_byte();
                    if let Ok(text) = std::str::from_utf8(&source[start..end]) {
                        return text.to_string();
                    }
                }
            }
            String::new()
        }
        1 => labels[0].to_string(),
        _ => {
            // Two or more labels: join all but the keyword portion.
            // Convention: skip the first label if there are exactly two (type.name).
            // For three or more labels, join all with dots.
            labels.join(".")
        }
    }
}

/// Look up the language configuration for a file extension.
///
/// Compiled queries are cached per extension so repeated calls are free.
/// Returns `None` for unsupported extensions.
#[must_use]
pub fn config_for_extension(ext: &str) -> Option<Arc<LangConfig>> {
    // Cache of compiled configs, keyed by canonical extension.
    static CACHE: OnceLock<std::collections::HashMap<&'static str, Arc<LangConfig>>> =
        OnceLock::new();

    let cache = CACHE.get_or_init(|| {
        let mut m = std::collections::HashMap::new();
        // Pre-compile all supported extensions
        for &ext in &[
            "rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
            "cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
            "scala", "toml", "json", "yaml", "yml", "md", "xml", "rdf", "owl", "sql",
        ] {
            if let Some(cfg) = compile_config(ext) {
                m.insert(ext, Arc::new(cfg));
            }
        }
        m
    });

    cache.get(ext).cloned()
}

/// Compile a [`LangConfig`] for the given extension (uncached).
#[expect(
    clippy::too_many_lines,
    reason = "one match arm per language — flat by design"
)]
fn compile_config(ext: &str) -> Option<LangConfig> {
    let (lang, query_str): (Language, &str) = match ext {
        // Rust: standalone functions, structs, and methods INSIDE impl/trait blocks.
        // impl_item and trait_item are NOT captured as wholes — we extract their
        // individual function_item children for method-level granularity.
        "rs" => (
            tree_sitter_rust::LANGUAGE.into(),
            concat!(
                "(function_item name: (identifier) @name) @def\n",
                "(struct_item name: (type_identifier) @name) @def\n",
                "(enum_item name: (type_identifier) @name) @def\n",
                "(type_item name: (type_identifier) @name) @def\n",
                "(field_declaration name: (field_identifier) @name) @def\n",
                "(enum_variant name: (identifier) @name) @def\n",
                "(impl_item type: (type_identifier) @name) @def\n",
                "(trait_item name: (type_identifier) @name) @def\n",
                "(const_item name: (identifier) @name) @def\n",
                "(static_item name: (identifier) @name) @def\n",
                "(mod_item name: (identifier) @name) @def",
            ),
        ),
        // Python: top-level functions AND methods inside classes (function_definition
        // matches at any nesting depth, so methods are captured individually).
        //
        // K1 fix: decorated functions (e.g. @property, @classmethod, @staticmethod)
        // are captured as decorated_definition with @name taken from the inner
        // function_definition. The chunker pipeline does not evaluate tree-sitter
        // predicates, so all decorated_definition nodes emit kind="decorated_definition"
        // → SymbolKind::PROPERTY (22). The @property case is the primary target; other
        // decorators are over-classified as PROPERTY but previously fell through to
        // VARIABLE (13) as an unrecognised kind, which was worse.
        "py" | "pyi" => (
            tree_sitter_python::LANGUAGE.into(),
            concat!(
                "(decorated_definition (function_definition name: (identifier) @name)) @def\n",
                "(function_definition name: (identifier) @name) @def\n",
                "(class_definition name: (identifier) @name) @def\n",
                "(assignment left: (identifier) @name) @def",
            ),
        ),
        // JS: functions, methods, and arrow functions assigned to variables.
        "js" | "jsx" => (
            tree_sitter_javascript::LANGUAGE.into(),
            concat!(
                "(function_declaration name: (identifier) @name) @def\n",
                "(method_definition name: (property_identifier) @name) @def\n",
                "(class_declaration name: (identifier) @name) @def\n",
                "(variable_declarator name: (identifier) @name) @def",
            ),
        ),
        "ts" => (
            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
            concat!(
                "(function_declaration name: (identifier) @name) @def\n",
                "(method_definition name: (property_identifier) @name) @def\n",
                "(class_declaration name: (type_identifier) @name) @def\n",
                "(interface_declaration name: (type_identifier) @name) @def\n",
                "(variable_declarator name: (identifier) @name) @def\n",
                "(type_alias_declaration name: (type_identifier) @name) @def\n",
                "(enum_declaration name: (identifier) @name) @def",
            ),
        ),
        "tsx" => (
            tree_sitter_typescript::LANGUAGE_TSX.into(),
            concat!(
                "(function_declaration name: (identifier) @name) @def\n",
                "(method_definition name: (property_identifier) @name) @def\n",
                "(class_declaration name: (type_identifier) @name) @def\n",
                "(interface_declaration name: (type_identifier) @name) @def\n",
                "(variable_declarator name: (identifier) @name) @def\n",
                "(type_alias_declaration name: (type_identifier) @name) @def\n",
                "(enum_declaration name: (identifier) @name) @def",
            ),
        ),
        // Go: functions, methods, and type declarations.
        //
        // K2 fix: distinguish interface types (kind=11, Interface) from struct types
        // (kind=23, Struct) from other type declarations (kind=26, TypeParameter).
        // The previous single `(type_declaration (type_spec ...)) @def` pattern emitted
        // kind="type_declaration" for ALL types, mapping to TYPE_PARAMETER (26) and
        // making Go interfaces invisible to interface-kind filters.
        //
        // Strategy: use the INNER type body node as @def so that `node.kind()` reflects
        // the concrete type kind:
        //   - `interface_type` @def → kind="interface_type" → INTERFACE (11)
        //   - `struct_type`    @def → kind="struct_type"    → STRUCT (23)
        //   - `type_alias`     @def → kind="type_alias"     → TYPE_PARAMETER (26)
        //
        // Non-interface non-struct type_specs (e.g. `type MyChan chan int`) are NOT
        // captured; this is intentional to avoid duplicate chunks for the same declaration.
        "go" => (
            tree_sitter_go::LANGUAGE.into(),
            concat!(
                "(function_declaration name: (identifier) @name) @def\n",
                "(method_declaration name: (field_identifier) @name) @def\n",
                // Interface type: @def = interface_type → kind="interface_type" → INTERFACE
                "(type_declaration (type_spec name: (type_identifier) @name type: (interface_type) @def))\n",
                // Struct type: @def = struct_type → kind="struct_type" → STRUCT
                "(type_declaration (type_spec name: (type_identifier) @name type: (struct_type) @def))\n",
                // Type alias: @def = type_alias → kind="type_alias" → TYPE_PARAMETER
                "(type_declaration (type_alias name: (type_identifier) @name) @def)\n",
                "(const_spec name: (identifier) @name) @def",
            ),
        ),
        // Java: methods are already captured individually (method_declaration
        // matches inside class bodies). Keep class for the signature/fields.
        "java" => (
            tree_sitter_java::LANGUAGE.into(),
            concat!(
                "(method_declaration name: (identifier) @name) @def\n",
                "(class_declaration name: (identifier) @name) @def\n",
                "(interface_declaration name: (identifier) @name) @def\n",
                "(field_declaration declarator: (variable_declarator name: (identifier) @name)) @def\n",
                "(enum_constant name: (identifier) @name) @def\n",
                "(enum_declaration name: (identifier) @name) @def\n",
                "(constructor_declaration name: (identifier) @name) @def",
            ),
        ),
        "c" | "h" => (
            tree_sitter_c::LANGUAGE.into(),
            concat!(
                "(function_definition declarator: (function_declarator declarator: (identifier) @name)) @def\n",
                "(declaration declarator: (init_declarator declarator: (identifier) @name)) @def\n",
                "(struct_specifier name: (type_identifier) @name) @def\n",
                "(enum_specifier name: (type_identifier) @name) @def\n",
                "(type_definition declarator: (type_identifier) @name) @def",
            ),
        ),
        // C++: functions at any level, plus class signatures.
        "cpp" | "cc" | "cxx" | "hpp" => (
            tree_sitter_cpp::LANGUAGE.into(),
            concat!(
                "(function_definition declarator: (function_declarator declarator: (identifier) @name)) @def\n",
                "(class_specifier name: (type_identifier) @name) @def\n",
                "(declaration declarator: (init_declarator declarator: (identifier) @name)) @def\n",
                "(struct_specifier name: (type_identifier) @name) @def\n",
                "(enum_specifier name: (type_identifier) @name) @def\n",
                "(type_definition declarator: (type_identifier) @name) @def\n",
                "(namespace_definition name: (namespace_identifier) @name) @def\n",
                "(field_declaration declarator: (field_identifier) @name) @def",
            ),
        ),
        // Bash: function definitions (.bats = Bash Automated Testing System).
        "sh" | "bash" | "bats" => (
            tree_sitter_bash::LANGUAGE.into(),
            concat!(
                "(function_definition name: (word) @name) @def\n",
                "(variable_assignment name: (variable_name) @name) @def",
            ),
        ),
        // Ruby: methods, classes, and modules.
        "rb" => (
            tree_sitter_ruby::LANGUAGE.into(),
            concat!(
                "(method name: (identifier) @name) @def\n",
                "(class name: (constant) @name) @def\n",
                "(module name: (constant) @name) @def\n",
                "(assignment left: (identifier) @name) @def\n",
                "(assignment left: (constant) @name) @def",
            ),
        ),
        // HCL (Terraform): resource, data, variable, and output blocks.
        //
        // K3 fix: index blocks by their semantic name rather than the keyword.
        // Previous query `(block (identifier) @name)` captured the block keyword
        // (e.g. "resource") as the symbol name, making `lsp_workspace_symbols(query="loader")`
        // unable to find `resource "aws_iam_role" "loader" { ... }`.
        //
        // Fixed query uses dot-anchor patterns to select the LAST string label
        // immediately before the opening `{` (block_start):
        //   - `resource "aws_iam_role" "loader" {}` → last string_lit before { = "loader" ✓
        //   - `data "aws_s3_bucket" "main" {}`    → last string_lit before { = "main"   ✓
        //   - `variable "region" {}`              → only  string_lit before { = "region" ✓
        //   - `output "role_arn" {}`              → only  string_lit before { = "role_arn" ✓
        //   - `locals {}`                          → no string_lit; identifier before { = "locals" ✓
        //
        // Note: the composite `type.name` format (e.g. "aws_iam_role.loader") is available
        // via [`derive_hcl_block_name`] for callers that need it. The chunker uses the
        // `@name` capture (the last string_lit label or identifier) which already enables
        // workspace symbol queries to find resources by their specific name.
        "tf" | "tfvars" | "hcl" => (
            tree_sitter_hcl::LANGUAGE.into(),
            concat!(
                // Last string_lit immediately before block_start (covers both single-label
                // and multi-label blocks; the dot anchor selects only the final label).
                "(block (string_lit (template_literal) @name) . (block_start)) @def\n",
                // No-label blocks (e.g. locals): identifier immediately before block_start.
                "(block (identifier) @name . (block_start)) @def",
            ),
        ),
        // Kotlin: functions, classes, and objects.
        "kt" | "kts" => (
            tree_sitter_kotlin_ng::LANGUAGE.into(),
            concat!(
                "(function_declaration name: (identifier) @name) @def\n",
                "(class_declaration name: (identifier) @name) @def\n",
                "(object_declaration name: (identifier) @name) @def\n",
                "(property_declaration (identifier) @name) @def\n",
                "(enum_entry (identifier) @name) @def",
            ),
        ),
        // Swift: functions, classes, structs, enums, and protocols.
        "swift" => (
            tree_sitter_swift::LANGUAGE.into(),
            concat!(
                "(function_declaration name: (simple_identifier) @name) @def\n",
                "(class_declaration name: (type_identifier) @name) @def\n",
                "(protocol_declaration name: (type_identifier) @name) @def\n",
                "(property_declaration name: (pattern bound_identifier: (simple_identifier) @name)) @def\n",
                "(typealias_declaration name: (type_identifier) @name) @def",
            ),
        ),
        // Scala: functions, classes, traits, and objects.
        "scala" => (
            tree_sitter_scala::LANGUAGE.into(),
            concat!(
                "(function_definition name: (identifier) @name) @def\n",
                "(class_definition name: (identifier) @name) @def\n",
                "(trait_definition name: (identifier) @name) @def\n",
                "(object_definition name: (identifier) @name) @def\n",
                "(val_definition pattern: (identifier) @name) @def\n",
                "(var_definition pattern: (identifier) @name) @def\n",
                "(type_definition name: (type_identifier) @name) @def",
            ),
        ),
        // TOML: table headers (sections).
        "toml" => (
            tree_sitter_toml_ng::LANGUAGE.into(),
            concat!(
                "(table (bare_key) @name) @def\n",
                "(pair (bare_key) @name) @def",
            ),
        ),
        // JSON: key-value pairs, capturing the key string content.
        "json" => (
            tree_sitter_json::LANGUAGE.into(),
            "(pair key: (string (string_content) @name)) @def",
        ),
        // YAML: block mapping pairs with plain scalar keys.
        "yaml" | "yml" => (
            tree_sitter_yaml::LANGUAGE.into(),
            "(block_mapping_pair key: (flow_node (plain_scalar (string_scalar) @name))) @def",
        ),
        // Markdown: ATX headings (# through ######), capturing the heading text.
        "md" => (
            tree_sitter_md::LANGUAGE.into(),
            "(atx_heading heading_content: (inline) @name) @def",
        ),
        // RDF/XML and OWL/XML are XML documents; capture each element so
        // ontology classes/properties become searchable semantic chunks.
        "xml" | "rdf" | "owl" => (
            tree_sitter_xml::LANGUAGE_XML.into(),
            concat!(
                "(element (STag (Name) @name)) @def\n",
                "(element (EmptyElemTag (Name) @name)) @def",
            ),
        ),
        // SQL: CREATE TABLE statements and common table expressions (CTEs).
        // Powered by tree-sitter-sequel (derekstride/tree-sitter-sql).
        //
        // dbt/sqlmesh files conventionally name their model by the *filename*
        // rather than an in-source CREATE TABLE — see `enrich_sql_file_def` in
        // repo_map.rs for the synthetic file-level def that fills that gap.
        // The chunker-level query below captures any in-source CREATE TABLE
        // and CTE so they remain searchable semantic chunks even when the
        // file uses sqlmesh `MODEL (...)` headers (which parse as ERROR nodes
        // — FROM/JOIN still extract cleanly post-error per S1 design).
        "sql" => (
            tree_sitter_sequel::LANGUAGE.into(),
            concat!(
                // CREATE TABLE foo — table-as-def.
                "(create_table (object_reference name: (identifier) @name)) @def\n",
                // WITH foo AS (SELECT ...) — CTE-as-def.
                "(cte (identifier) @name) @def",
            ),
        ),
        _ => return None,
    };
    let query = match Query::new(&lang, query_str) {
        Ok(q) => q,
        Err(e) => {
            tracing::warn!(ext, %e, "tree-sitter query compilation failed — language may be ABI-incompatible");
            return None;
        }
    };
    Some(LangConfig {
        language: lang,
        query,
    })
}

/// Look up the call-extraction query for a file extension.
///
/// Compiled queries are cached per extension so repeated calls are free.
/// Returns `None` for unsupported extensions (including TOML, which has
/// no function calls).
#[must_use]
pub fn call_query_for_extension(ext: &str) -> Option<Arc<CallConfig>> {
    static CACHE: OnceLock<std::collections::HashMap<&'static str, Arc<CallConfig>>> =
        OnceLock::new();

    let cache = CACHE.get_or_init(|| {
        let mut m = std::collections::HashMap::new();
        // Pre-compile for all extensions that have callable constructs.
        // TOML is deliberately excluded — it has no function calls.
        // SQL has FROM/JOIN as call-edges (model-to-model references) —
        // emitted by the per-language call query plus a synthetic
        // file-level def in repo_map::enrich_sql_file_def (S1, Wave 4).
        for &ext in &[
            "rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
            "cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
            "scala", "sql",
        ] {
            if let Some(cfg) = compile_call_config(ext) {
                m.insert(ext, Arc::new(cfg));
            }
        }
        m
    });

    cache.get(ext).cloned()
}

/// Compile a [`CallConfig`] for the given extension (uncached).
///
/// Each query extracts the callee identifier (`@callee`) from function
/// and method calls, plus the whole call expression (`@call`).
#[expect(
    clippy::too_many_lines,
    reason = "one match arm per language — flat by design"
)]
fn compile_call_config(ext: &str) -> Option<CallConfig> {
    let (lang, query_str): (Language, &str) = match ext {
        // Rust: free calls, method calls, and scoped (path) calls.
        //
        // For scoped calls, capture the full `scoped_identifier` node as @callee
        // (not just the trailing `(identifier)` child). This preserves the qualified
        // path so that `mod_a::foo()` records "mod_a::foo" rather than bare "foo",
        // enabling cross-module disambiguation in `resolve_calls`.
        "rs" => (
            tree_sitter_rust::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (field_expression field: (field_identifier) @callee)) @call\n",
                "(call_expression function: (scoped_identifier) @callee) @call",
            ),
        ),
        // Python: simple calls and attribute (method) calls.
        "py" | "pyi" => (
            tree_sitter_python::LANGUAGE.into(),
            concat!(
                "(call function: (identifier) @callee) @call\n",
                "(call function: (attribute attribute: (identifier) @callee)) @call",
            ),
        ),
        // JavaScript: function calls and member expression calls.
        "js" | "jsx" => (
            tree_sitter_javascript::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
            ),
        ),
        // TypeScript: same patterns as JavaScript.
        "ts" => (
            tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
            ),
        ),
        // TSX: same patterns as JavaScript.
        "tsx" => (
            tree_sitter_typescript::LANGUAGE_TSX.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (member_expression property: (property_identifier) @callee)) @call",
            ),
        ),
        // Go: function calls and selector (method) calls.
        "go" => (
            tree_sitter_go::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (selector_expression field: (field_identifier) @callee)) @call",
            ),
        ),
        // Java: method invocations.
        "java" => (
            tree_sitter_java::LANGUAGE.into(),
            "(method_invocation name: (identifier) @callee) @call",
        ),
        // C: function calls and field-expression calls (function pointers).
        "c" | "h" => (
            tree_sitter_c::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (field_expression field: (field_identifier) @callee)) @call",
            ),
        ),
        // C++: same patterns as C.
        "cpp" | "cc" | "cxx" | "hpp" => (
            tree_sitter_cpp::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (field_expression field: (field_identifier) @callee)) @call",
            ),
        ),
        // Bash: command invocations (.bats = Bash Automated Testing System).
        "sh" | "bash" | "bats" => (
            tree_sitter_bash::LANGUAGE.into(),
            "(command name: (command_name (word) @callee)) @call",
        ),
        // Ruby: method calls.
        "rb" => (
            tree_sitter_ruby::LANGUAGE.into(),
            "(call method: (identifier) @callee) @call",
        ),
        // HCL (Terraform): built-in function calls.
        "tf" | "tfvars" | "hcl" => (
            tree_sitter_hcl::LANGUAGE.into(),
            "(function_call (identifier) @callee) @call",
        ),
        // Kotlin: call expressions — grammar uses unnamed children, so match
        // identifier as first child of call_expression.
        "kt" | "kts" => (
            tree_sitter_kotlin_ng::LANGUAGE.into(),
            "(call_expression (identifier) @callee) @call",
        ),
        // Swift: call expressions with simple identifiers.
        "swift" => (
            tree_sitter_swift::LANGUAGE.into(),
            "(call_expression (simple_identifier) @callee) @call",
        ),
        // Scala: function calls and field-expression (method) calls.
        "scala" => (
            tree_sitter_scala::LANGUAGE.into(),
            concat!(
                "(call_expression function: (identifier) @callee) @call\n",
                "(call_expression function: (field_expression field: (identifier) @callee)) @call",
            ),
        ),
        // SQL: FROM <table> and JOIN <table> as call-edges. Schema-qualified
        // names like `analytics.silver_X` parse as
        //   (object_reference schema: (identifier) name: (identifier))
        // — the field selector `name:` picks the table identifier and skips
        // the schema prefix, which is correct for cross-model resolution
        // (downstream dbt/sqlmesh models reference each other by table name
        // not by schema + name).
        "sql" => (
            tree_sitter_sequel::LANGUAGE.into(),
            concat!(
                // FROM <table>: relation > object_reference > name identifier.
                "(from (relation (object_reference name: (identifier) @callee))) @call\n",
                // JOIN <table>: same shape, inside a join clause.
                "(join (relation (object_reference name: (identifier) @callee))) @call",
            ),
        ),
        _ => return None,
    };
    let query = match Query::new(&lang, query_str) {
        Ok(q) => q,
        Err(e) => {
            tracing::warn!(ext, %e, "tree-sitter call query compilation failed");
            return None;
        }
    };
    Some(CallConfig {
        language: lang,
        query,
    })
}

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

    #[test]
    fn rust_extension_resolves() {
        assert!(config_for_extension("rs").is_some());
    }

    #[test]
    fn python_extension_resolves() {
        assert!(config_for_extension("py").is_some());
    }

    #[test]
    fn python_stub_extension_resolves() {
        assert!(config_for_extension("pyi").is_some());
    }

    #[test]
    fn unknown_extension_returns_none() {
        assert!(config_for_extension("xyz").is_none());
    }

    #[test]
    fn all_supported_extensions() {
        let exts = [
            "rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
            "cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
            "scala", "toml", "json", "yaml", "yml", "md", "xml", "rdf", "owl", "sql",
        ];
        for ext in &exts {
            assert!(config_for_extension(ext).is_some(), "failed for {ext}");
        }
    }

    #[test]
    fn turtle_family_uses_rdf_text_chunking_not_tree_sitter() {
        for ext in ["ttl", "nt", "n3", "trig", "nq"] {
            assert!(
                config_for_extension(ext).is_none(),
                "{ext} should be handled by RDF text chunking"
            );
            assert!(crate::chunk::is_rdf_text_extension(ext));
        }
    }

    #[test]
    fn all_call_query_extensions() {
        let exts = [
            "rs", "py", "pyi", "js", "jsx", "ts", "tsx", "go", "java", "c", "h", "cpp", "cc",
            "cxx", "hpp", "sh", "bash", "bats", "rb", "tf", "tfvars", "hcl", "kt", "kts", "swift",
            "scala", "sql",
        ];
        for ext in &exts {
            assert!(
                call_query_for_extension(ext).is_some(),
                "call query failed for {ext}"
            );
        }
    }

    #[test]
    fn toml_has_no_call_query() {
        assert!(call_query_for_extension("toml").is_none());
    }

    /// RED test (R2.3 issue a): scoped_identifier call must capture the full path.
    ///
    /// Before the fix, `mod_a::foo()` captured only `foo` as @callee.
    /// After the fix, it must capture `mod_a::foo` as @callee.
    #[test]
    fn test_scoped_identifier_call_query_captures_full_path() {
        use streaming_iterator::StreamingIterator as _;

        let source = "
fn caller() {
    mod_a::foo();
    std::io::stderr();
}
";
        let call_cfg = call_query_for_extension("rs").expect("rs call config");
        let mut parser = tree_sitter::Parser::new();
        parser
            .set_language(&call_cfg.language)
            .expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&call_cfg.query, tree.root_node(), source.as_bytes());

        let mut callees: Vec<String> = Vec::new();
        while let Some(m) = matches.next() {
            for cap in m.captures {
                let name = &call_cfg.query.capture_names()[cap.index as usize];
                if *name == "callee" {
                    let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                    callees.push(text.to_string());
                }
            }
        }

        // Must contain full qualified path, not bare identifier
        assert!(
            callees.contains(&"mod_a::foo".to_string()),
            "expected 'mod_a::foo' in callees, got: {callees:?}"
        );
        // Bare 'foo' must not appear when scoped call is made
        assert!(
            !callees.contains(&"foo".to_string()),
            "bare 'foo' must not appear for scoped call; got: {callees:?}"
        );
    }

    // -------------------------------------------------------------------------
    // B1: tree-sitter node-kind → LSP SymbolKind mapping tests
    // -------------------------------------------------------------------------

    /// `test:rust_node_kind_maps_to_lsp_symbol_kind_struct` — `struct_item`
    /// maps to LSP SymbolKind 23 (Struct).
    ///
    /// Behavior: trigger-fails-on-baseline-then-passes-post-fix.
    /// On the baseline `lsp_symbol_kind_for_node_kind` did not exist.
    #[test]
    fn rust_node_kind_maps_to_lsp_symbol_kind_struct() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("struct_item"),
            lsp_symbol_kind::STRUCT,
            "struct_item must map to SymbolKind::Struct (23)"
        );
    }

    /// `test:rust_node_kind_maps_to_lsp_symbol_kind_trait` — `trait_item`
    /// maps to LSP SymbolKind 11 (Interface).
    #[test]
    fn rust_node_kind_maps_to_lsp_symbol_kind_trait() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("trait_item"),
            lsp_symbol_kind::INTERFACE,
            "trait_item must map to SymbolKind::Interface (11)"
        );
    }

    /// `test:rust_node_kind_maps_to_lsp_symbol_kind_enum` — `enum_item`
    /// maps to LSP SymbolKind 10 (Enum).
    #[test]
    fn rust_node_kind_maps_to_lsp_symbol_kind_enum() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("enum_item"),
            lsp_symbol_kind::ENUM,
            "enum_item must map to SymbolKind::Enum (10)"
        );
    }

    /// `test:rust_node_kind_maps_to_lsp_symbol_kind_function` — `function_item`
    /// maps to LSP SymbolKind 12 (Function).
    #[test]
    fn rust_node_kind_maps_to_lsp_symbol_kind_function() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("function_item"),
            lsp_symbol_kind::FUNCTION,
            "function_item must map to SymbolKind::Function (12)"
        );
    }

    /// `test:rust_node_kind_maps_to_lsp_symbol_kind_module` — `mod_item`
    /// maps to LSP SymbolKind 2 (Module).
    #[test]
    fn rust_node_kind_maps_to_lsp_symbol_kind_module() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("mod_item"),
            lsp_symbol_kind::MODULE,
            "mod_item must map to SymbolKind::Module (2)"
        );
    }

    /// Additional B1 coverage: impl, const, static, type_item all map
    /// to meaningful, non-Variable kinds.
    #[test]
    fn rust_node_kinds_map_to_non_variable_kinds() {
        let cases: &[(&str, u32)] = &[
            ("impl_item", lsp_symbol_kind::CLASS),
            ("const_item", lsp_symbol_kind::CONSTANT),
            ("static_item", lsp_symbol_kind::CONSTANT),
            ("type_item", lsp_symbol_kind::TYPE_PARAMETER),
            ("field_declaration", lsp_symbol_kind::FIELD),
            ("enum_variant", lsp_symbol_kind::ENUM_MEMBER),
            ("function_signature_item", lsp_symbol_kind::FUNCTION),
        ];
        for &(kind, expected) in cases {
            assert_eq!(
                lsp_symbol_kind_for_node_kind(kind),
                expected,
                "node kind '{kind}' should map to {expected}, got {}",
                lsp_symbol_kind_for_node_kind(kind)
            );
        }
    }

    /// Unknown node kinds fall back to Variable (13) — preserving pre-B1 default.
    #[test]
    fn unknown_node_kind_falls_back_to_variable() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("some_unknown_kind"),
            lsp_symbol_kind::VARIABLE,
            "unknown kind must fall back to Variable (13)"
        );
    }

    // =========================================================================
    // K1 — Python @property classification (I#17a)
    // =========================================================================

    /// `test:python_property_decorator_classifies_as_property_kind`
    ///
    /// Baseline (RED): `decorated_definition` was not in the kind mapping, so it
    /// fell through to VARIABLE (13). The Python `@property` decorated method was
    /// thus invisible to LSP property-kind filters.
    ///
    /// After fix (GREEN): `decorated_definition` maps to PROPERTY (22), and the
    /// Python query captures `decorated_definition` as `@def` so `@property`-decorated
    /// methods emit kind="decorated_definition" → SymbolKind::Property.
    #[test]
    fn python_property_decorator_classifies_as_property_kind() {
        // The kind mapping must return PROPERTY (22) for decorated_definition.
        assert_eq!(
            lsp_symbol_kind_for_node_kind("decorated_definition"),
            lsp_symbol_kind::PROPERTY,
            "decorated_definition must map to SymbolKind::Property (22); baseline gave Variable (13)"
        );
    }

    /// The Python query must capture `@property`-decorated methods as `@def=decorated_definition`.
    ///
    /// Uses tree-sitter to parse a Python source snippet with a `@property` decorated
    /// method, runs the compiled Python LangConfig query, and verifies that at least one
    /// match emits `def_kind = "decorated_definition"` with `name = "name"`.
    #[test]
    fn python_property_query_captures_decorated_definition() {
        use streaming_iterator::StreamingIterator as _;

        let source = r"class MyModel:
    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    def regular_method(self):
        pass
";
        let cfg = config_for_extension("py").expect("Python config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut property_kind_found = false;
        let mut property_name_found = false;
        while let Some(m) = matches.next() {
            let mut name = "";
            let mut def_kind = "";
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                if *cap_name == "name" {
                    name = text;
                } else if *cap_name == "def" {
                    def_kind = cap.node.kind();
                }
            }
            if def_kind == "decorated_definition" && name == "name" {
                property_kind_found = true;
                property_name_found = true;
            }
        }
        assert!(
            property_kind_found,
            "Python query must capture decorated_definition for @property method; got none"
        );
        assert!(
            property_name_found,
            "Python query must capture 'name' as the method name inside @property definition"
        );
    }

    // =========================================================================
    // K2 — Go interface classification (I#17b)
    // =========================================================================

    /// `test:go_interface_type_classifies_as_interface_kind`
    ///
    /// Baseline (RED): all Go type declarations used `(type_declaration ...) @def`
    /// which sets kind="type_declaration" → TYPE_PARAMETER (26). Interfaces were
    /// invisible to interface-kind filters (`kind=11`).
    ///
    /// After fix (GREEN): interface types use `(interface_type) @def` → kind=
    /// "interface_type" → INTERFACE (11).
    #[test]
    fn go_interface_type_classifies_as_interface_kind() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("interface_type"),
            lsp_symbol_kind::INTERFACE,
            "interface_type must map to SymbolKind::Interface (11); baseline gave TypeParameter (26)"
        );
    }

    /// `test:go_struct_type_classifies_as_struct_kind`
    ///
    /// Baseline (RED): struct types were also TYPE_PARAMETER (26) via the generic
    /// type_declaration pattern. After fix: struct_type → STRUCT (23).
    #[test]
    fn go_struct_type_classifies_as_struct_kind() {
        assert_eq!(
            lsp_symbol_kind_for_node_kind("struct_type"),
            lsp_symbol_kind::STRUCT,
            "struct_type must map to SymbolKind::Struct (23); baseline gave TypeParameter (26)"
        );
    }

    /// The Go query must emit kind="interface_type" for `type Reader interface { ... }`.
    ///
    /// Parses a Go source snippet and verifies that the compiled Go LangConfig query
    /// produces a match with def_kind="interface_type" and name="Reader".
    #[test]
    fn go_interface_query_captures_interface_type() {
        use streaming_iterator::StreamingIterator as _;

        let source = r"package io
type Reader interface {
    Read(p []byte) (n int, err error)
}
type MyStruct struct {
    Name string
}
func NewReader() Reader {
    return nil
}
";
        let cfg = config_for_extension("go").expect("Go config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut interface_found = false;
        let mut struct_found = false;
        let mut function_found = false;
        while let Some(m) = matches.next() {
            let mut name = "";
            let mut def_kind = "";
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                if *cap_name == "name" {
                    name = text;
                } else if *cap_name == "def" {
                    def_kind = cap.node.kind();
                }
            }
            if def_kind == "interface_type" && name == "Reader" {
                interface_found = true;
            }
            if def_kind == "struct_type" && name == "MyStruct" {
                struct_found = true;
            }
            if def_kind == "function_declaration" && name == "NewReader" {
                function_found = true;
            }
        }
        assert!(
            interface_found,
            "Go query must emit def_kind='interface_type' for 'type Reader interface {{ ... }}'"
        );
        assert!(
            struct_found,
            "Go query must emit def_kind='struct_type' for 'type MyStruct struct {{ ... }}'"
        );
        assert!(
            function_found,
            "Go query must emit def_kind='function_declaration' for 'func NewReader()'"
        );
    }

    // =========================================================================
    // K3 — HCL resource naming (I#17c)
    // =========================================================================

    /// `test:hcl_resource_symbol_uses_type_dot_name`
    ///
    /// Verifies that `derive_hcl_block_name` produces the `type.name` composite
    /// for a two-label HCL block (e.g. `resource "aws_iam_role" "loader" { ... }`
    /// → "aws_iam_role.loader").
    ///
    /// Baseline (RED): the previous HCL query captured the keyword ("resource") as the
    /// symbol name, making `lsp_workspace_symbols(query="loader")` unable to find the
    /// resource. The query fix makes the chunker emit "loader" as the name; this function
    /// enables callers to reconstruct the full "aws_iam_role.loader" composite.
    #[test]
    fn hcl_resource_symbol_uses_type_dot_name() {
        let source = br#"resource "aws_iam_role" "loader" {
  assume_role_policy = "assume.json"
}
"#;
        let lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&lang).expect("set HCL language");
        let tree = parser.parse(source, None).expect("parse HCL");

        // Find the first block node
        let root = tree.root_node();
        let body = root.child(0).expect("config_file has body");
        #[expect(
            clippy::cast_possible_truncation,
            reason = "child_count() is a small usize; fits in u32"
        )]
        let block = (0..body.child_count())
            .filter_map(|i| body.child(i as u32))
            .find(|n| n.kind() == "block")
            .expect("should have at least one block node");

        let name = derive_hcl_block_name(&block, source);
        assert_eq!(
            name, "aws_iam_role.loader",
            "derive_hcl_block_name must produce 'aws_iam_role.loader' for \
             `resource \"aws_iam_role\" \"loader\"` block; got {name:?}"
        );
    }

    /// `test:hcl_data_source_symbol_uses_type_dot_name`
    ///
    /// Verifies `derive_hcl_block_name` produces "aws_s3_bucket.main" for
    /// `data "aws_s3_bucket" "main" { ... }`.
    #[test]
    fn hcl_data_source_symbol_uses_type_dot_name() {
        let source = br#"data "aws_s3_bucket" "main" {
  bucket = "my-bucket"
}
"#;
        let lang: tree_sitter::Language = tree_sitter_hcl::LANGUAGE.into();
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&lang).expect("set HCL language");
        let tree = parser.parse(source, None).expect("parse HCL");

        let root = tree.root_node();
        let body = root.child(0).expect("config_file has body");
        #[expect(
            clippy::cast_possible_truncation,
            reason = "child_count() returns a small usize; fits in u32"
        )]
        let block = (0..body.child_count())
            .filter_map(|i| body.child(i as u32))
            .find(|n| n.kind() == "block")
            .expect("block node");

        let name = derive_hcl_block_name(&block, source);
        assert_eq!(
            name, "aws_s3_bucket.main",
            "derive_hcl_block_name must produce 'aws_s3_bucket.main'"
        );
    }

    /// The HCL query must capture the resource name (last string label) not the keyword.
    ///
    /// Verifies that the compiled HCL LangConfig query emits `@name = "loader"` (not
    /// "resource") for `resource "aws_iam_role" "loader" { ... }`. This is the live
    /// chunker behaviour that makes `lsp_workspace_symbols(query="loader")` work.
    #[test]
    fn hcl_query_captures_resource_name_not_keyword() {
        use streaming_iterator::StreamingIterator as _;

        let source = r#"resource "aws_iam_role" "loader" {
  x = 1
}
variable "region" {
  type = "string"
}
output "role_arn" {
  value = "arn"
}
locals {
  x = 1
}
"#;
        let cfg = config_for_extension("tf").expect("HCL config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut names: Vec<(String, String)> = Vec::new(); // (name, def_kind)
        while let Some(m) = matches.next() {
            let mut name = String::new();
            let mut def_kind = String::new();
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                if *cap_name == "name" {
                    name = text.to_string();
                } else if *cap_name == "def" {
                    def_kind = cap.node.kind().to_string();
                }
            }
            if !name.is_empty() {
                names.push((name, def_kind));
            }
        }

        let name_list: Vec<&str> = names.iter().map(|(n, _)| n.as_str()).collect();

        // Must capture "loader" (not "resource") for the resource block.
        assert!(
            name_list.contains(&"loader"),
            "HCL query must capture 'loader' (not the keyword 'resource') for resource block; got: {name_list:?}"
        );
        assert!(
            !name_list.contains(&"resource"),
            "HCL query must NOT capture the keyword 'resource' as a symbol name; got: {name_list:?}"
        );

        // Must capture "region" for variable block.
        assert!(
            name_list.contains(&"region"),
            "HCL query must capture 'region' for variable block; got: {name_list:?}"
        );

        // Must capture "role_arn" for output block.
        assert!(
            name_list.contains(&"role_arn"),
            "HCL query must capture 'role_arn' for output block; got: {name_list:?}"
        );

        // Must capture "locals" for locals block.
        assert!(
            name_list.contains(&"locals"),
            "HCL query must capture 'locals' for locals block; got: {name_list:?}"
        );
    }

    // =========================================================================
    // L1 — Python class_definition kind taxonomy fix (I#19)
    // =========================================================================

    /// `test:python_class_definition_kind_5`
    ///
    /// Baseline (RED): `class_definition` was falling through to the wildcard
    /// match or returning VARIABLE (13). The Python `class Foo: pass` pattern
    /// was classified as kind=20 (Key) in the mnemosyne corpus
    /// (ErrorOccurred, OCRCompleted, MnemosyneApp, BaseScreen, BrowseScansScreen).
    ///
    /// After fix (GREEN): `class_definition` maps to CLASS (5) in
    /// `lsp_symbol_kind_for_node_kind`, and the Python query captures
    /// `class_definition` with its body node, so `node.kind() == "class_definition"`
    /// maps to 5.
    #[test]
    fn test_python_class_definition_kind_5() {
        use streaming_iterator::StreamingIterator as _;

        let source = r"class Foo:
    pass
";
        let cfg = config_for_extension("py").expect("Python config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut class_kind_found = false;
        while let Some(m) = matches.next() {
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                if *cap_name == "def" {
                    let def_kind = cap.node.kind();
                    if def_kind == "class_definition" {
                        let lsp_kind = lsp_symbol_kind_for_node_kind(def_kind);
                        assert_eq!(
                            lsp_kind,
                            lsp_symbol_kind::CLASS,
                            "class_definition must map to SymbolKind::Class (5); got {lsp_kind}"
                        );
                        class_kind_found = true;
                    }
                }
            }
        }

        assert!(
            class_kind_found,
            "Python query must emit def_kind='class_definition' for 'class Foo:' pattern"
        );
    }

    // =========================================================================
    // L2 — Go type_alias kind taxonomy fix (I#17b)
    // =========================================================================

    /// `test:go_type_alias_kind_21`
    ///
    /// Baseline (RED): `type_alias` (the @def node from Go `type X = Y` patterns)
    /// was mapping to TYPE_PARAMETER (26) in the kind match. This matched the
    /// previous K2 work which split type_spec into interface_type (→11) and
    /// struct_type (→23), but the fallthrough type_alias path still mapped
    /// to TYPE_PARAMETER.
    ///
    /// After fix (GREEN): `type_alias` maps to VARIABLE (21) — a better
    /// classification than TypeParameter and semantically closer to an alias.
    /// Alternative: could use Constant (14) if the codebase considers aliases
    /// as immutable. Variable (21) is used here because:
    /// - LSP spec doesn't have a dedicated "Alias" kind
    /// - Variable is used in some implementations for type aliases
    /// - It provides a type classification separate from pure TypeParameters
    ///   (which represent generics like `[T]` in function signatures)
    #[test]
    fn test_go_type_alias_kind_21() {
        use streaming_iterator::StreamingIterator as _;

        let source = r"package main

type Foo = Bar

type Reader interface {
    Read(p []byte) (n int, err error)
}
";
        let cfg = config_for_extension("go").expect("Go config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut alias_kind_found = false;
        while let Some(m) = matches.next() {
            let mut name = "";
            let mut def_kind = "";
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                if *cap_name == "name" {
                    name = text;
                } else if *cap_name == "def" {
                    def_kind = cap.node.kind();
                }
            }
            if def_kind == "type_alias" && name == "Foo" {
                let lsp_kind = lsp_symbol_kind_for_node_kind(def_kind);
                assert_eq!(
                    lsp_kind,
                    lsp_symbol_kind::VARIABLE,
                    "type_alias must map to SymbolKind::Variable (13) not TypeParameter (26); got {lsp_kind}"
                );
                alias_kind_found = true;
            }
        }

        assert!(
            alias_kind_found,
            "Go query must emit def_kind='type_alias' for 'type Foo = Bar' pattern"
        );
    }

    /// `test:go_type_alias_distinct_from_type_parameter`
    ///
    /// Verifies that a Go generic type parameter (like `[T any]` in a generic
    /// function) gets kind=26 (TypeParameter), while an alias `type Foo = Bar`
    /// gets kind=21 (Variable). This documents the distinction: generics stay
    /// as TypeParameter, aliases are Variable.
    #[test]
    fn test_go_type_alias_distinct_from_type_parameter() {
        use streaming_iterator::StreamingIterator as _;

        let source = r"package main

type Foo = Bar

func generic[T any](x T) {
}
";
        let cfg = config_for_extension("go").expect("Go config must compile");
        let mut parser = tree_sitter::Parser::new();
        parser.set_language(&cfg.language).expect("set language");
        let tree = parser.parse(source, None).expect("parse");

        let mut cursor = tree_sitter::QueryCursor::new();
        let mut matches = cursor.matches(&cfg.query, tree.root_node(), source.as_bytes());

        let mut alias_found = false;
        let mut alias_kind = 0u32;

        while let Some(m) = matches.next() {
            let mut name = "";
            let mut def_kind = "";
            for cap in m.captures {
                let cap_name = &cfg.query.capture_names()[cap.index as usize];
                let text = &source[cap.node.start_byte()..cap.node.end_byte()];
                if *cap_name == "name" {
                    name = text;
                } else if *cap_name == "def" {
                    def_kind = cap.node.kind();
                }
            }
            if def_kind == "type_alias" && name == "Foo" {
                alias_kind = lsp_symbol_kind_for_node_kind(def_kind);
                alias_found = true;
            }
        }

        assert!(
            alias_found,
            "Go query must emit 'type Foo = Bar' as type_alias; got none"
        );
        assert_eq!(
            alias_kind,
            lsp_symbol_kind::VARIABLE,
            "type_alias 'Foo' must be kind=13 (Variable), got {alias_kind}"
        );

        // Note: This test does NOT check generic type parameters because
        // the current Go query does not capture them — it only captures
        // top-level definitions. Generic parameters in function signatures
        // are part of the function_declaration's syntax but not extracted
        // as separate definitions, so they will not appear in the query results.
        // This is the intended behavior; generics are not searchable symbols
        // in the chunker pipeline.
    }
}