conproxy 0.2.1

Cache proxy server for heterogeneous RAG/vector search backends with cascade, scope, and MCP
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
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
# Makefile for conproxy (cache proxy server)

# Use bash + pipefail so pipelines like `cargo bench | tee file` propagate
# cargo's real exit code. Without it, `tee` masks build/bench failures and the
# perf_summarize step reads stale data and reports a false PASS.
SHELL := /bin/bash
.SHELLFLAGS := -eu -o pipefail -c

.PHONY: all build build-release test test-unit test-integration test-integration-experimental lint fmt clean help build-embed build-persistence build-pgvector build-mcp build-all build-profiling bench bench-save bench-compare profile-dhat profile-pgo profile-pgo-clean profile-flamegraph profile-tokio-console profile-heaptrack profile-metrics-snap perf-tuning-quick perf-tuning-default perf-tuning-full e2e-all e2e-dirty e2e-qdrant e2e-elastic e2e-meili e2e-mixed e2e-filter e2e-smoke e2e-smoke-core e2e-cascade e2e-federated e2e-bench e2e-services-up e2e-services-down e2e-wait e2e-load-data e2e-results e2e-report e2e-bench-compare e2e-proxy-clean llm-server-check  eval-llamacpp eval-all eval-quick eval-vertical eval-queries eval-cheap eval-results eval-clean uat uat-quick e2e-profile eval-profile test-all test-all-prebuild test-all-lint test-all-unit test-all-bench test-all-coverage test-all-e2e test-all-eval test-all-quality test-all-perf test-all-security test-coverage-check proxy-start proxy-stop proxy-status audit security-deny lint-security audit-known-gaps e2e-security sbom unsafe-audit mutant-security fuzz-query fuzz-config fuzz-all security-quick security-full profile-pgo profile-pgo-clean cov-scope-tune proof-cascade sdk-smoke perf-tuning-clean bench-hitrate bench-hitrate-mem bench-hitrate-pressure bench-hitrate-sem bench-hitrate-onnx bench-hitrate-live bench-hitrate-replay perf-publish fmt-check test-one test-verbose test-coverage-quick e2e-generate-embeddings e2e-k8s docker-build docker-push dev-up dev-down dev-restart devex devex-attach devex-status devex-new devex-banner t test-fast test-nextest test-slow test-filter target-prune nextest-install docker-buildx

# E2E infra directory (docker-compose lives here). Override on the command
# line, e.g. `make e2e-services-up E2E_PROXY_DIR=/path/to/compose`. The
# default assumes tests/e2e/docker-compose.yml next to the Makefile.
E2E_PROXY_DIR ?= $(CURDIR)/tests/e2e

# Default target
all: fmt lint test build

# ============================================================================
# Build
# ============================================================================

build:
	cargo build

build-release:
	cargo build --release --features release --bin conproxy

# Feature-specific builds
build-embed:
	cargo build --release --features embed

build-persistence:
	cargo build --features persistence

build-pgvector:
	cargo build --features pgvector

build-mcp:
	cargo build --release --features mcp

# Release build with all standard features (excludes embed, pgvector, persistence, linux-sandbox which need external deps)
build-all:
	cargo build --release

# Build with profiling profile (release + debug symbols for perf/bpftrace + dhat heap profiling)
build-profiling:
	cargo build --profile profiling --features dhat-heap

# Binary path for test-all pipeline (dev profile = debug symbols for perf + dhat-heap)
TEST_BIN = $(CURDIR)/target/debug/conproxy
TEST_RUNNER_BIN = $(CURDIR)/target/debug/test_runner

# Feature set for test-all pipeline. 'test' = load-test + dhat-heap, so all
# dev-profile targets share one feature set (no feature-switch recompilation between stages).
TEST_ALL_FEATURES ?=

# Resource profile for cgroup v2 scoping (default = 1 CPU, 512 MB)
RESOURCE_PROFILE ?= default
_PROFILE_NAME = $(or $(RESOURCE_PROFILE),default)

# ============================================================================
# Test
# ============================================================================

# Run all tests (unit)
test:
	cargo clippy -- -D warnings
	cargo test

# Local iteration loop (see docs/dev-loop.md).
#
# t            - run the unit tests; uses cargo-nextest with the `dev`
#                profile when installed, falls back to `cargo test --lib`
#                otherwise. Default features only. Designed for the
#                save-and-run loop. Warm wall ~3-4s on a 16-core host.
# test-fast    - same as t; alias used in some docs.
# test-nextest - force cargo-nextest with the dev profile (no fallback).
# test-slow    - list the top slow tests via nextest.
# test-filter  - run tests matching $PAT; uses nextest if available.
# target-prune - drop the conproxy-only build artifacts; safe to re-run.
# nextest-install - one-time install of cargo-nextest.
#
# Cargo.toml [profile.dev] already sets debug = "line-tables-only" and
# split-debuginfo = "unpacked" to shrink test binaries. .cargo/config.toml
# forces lld on Linux for faster links. See docs/dev-loop.md for the
# rationale + measurements.
NEXTEST ?= $(shell command -v cargo-nextest 2>/dev/null)

t:
	@if [ -n "$(NEXTEST)" ]; then \
		cargo nextest run --profile dev --lib 2>&1 | tail -n 30; \
	else \
		echo "cargo-nextest not found; falling back to cargo test --lib"; \
		echo "Tip: run 'make nextest-install' for the fast path"; \
		cargo test --lib -q 2>&1 | tail -n 30; \
	fi

test-fast: t

test-nextest:
	@if [ -z "$(NEXTEST)" ]; then \
		echo "cargo-nextest not installed. Run: make nextest-install" >&2; \
		exit 1; \
	fi
	cargo nextest run --profile dev --lib

test-slow:
	@if [ -z "$(NEXTEST)" ]; then \
		echo "cargo-nextest not installed. Run: make nextest-install" >&2; \
		exit 1; \
	fi
	# nextest 0.9 doesn't have --show-slowest N; status-level=slow + final summary
	# surfaces the slowest tests after the run.
	cargo nextest run --profile dev --lib --status-level slow --final-status-level slow 2>&1 | tail -n 40

# Usage: make test-filter PAT=foo
test-filter:
	@if [ -z "$(PAT)" ]; then \
		echo "Usage: make test-filter PAT=<substring>" >&2; \
		exit 1; \
	fi
	@if [ -n "$(NEXTEST)" ]; then \
		cargo nextest run --profile dev --lib --filter-expr 'test(=PAT)' 2>/dev/null \
			|| cargo nextest run --profile dev --lib "$(PAT)"; \
	else \
		cargo test --lib -- "$(PAT)"; \
	fi

target-prune:
	@echo "Pruning conproxy-only build artifacts..."
	@rm -rf target/debug/deps target/debug/incremental target/.rustc_info.json 2>/dev/null || true
	@find target/debug -maxdepth 1 -type f -name 'conproxy-*' -newer Cargo.toml -delete 2>/dev/null || true
	@echo "Done. Use 'cargo clean' for a full wipe."

nextest-install:
	@if [ -n "$(NEXTEST)" ]; then \
		echo "cargo-nextest already installed: $(NEXTEST)"; \
	else \
		cargo install cargo-nextest --locked --version '^0.9'; \
	fi

# Unit tests only (fast)
test-unit:
	cargo test --lib

# Backend integration tests (requires Docker daemon + testcontainers).
# Plan 09: full non-experimental matrix. PR: optional label run-integration;
# nightly: full target. Experimental backends: test-integration-experimental.
test-integration:
	cargo test --features integration-tests --test integration_qdrant
	cargo test --features integration-tests --test integration_elasticsearch
	cargo test --features integration-tests --test integration_opensearch
	cargo test --features integration-tests --test integration_meilisearch
	cargo test --features integration-tests --test integration_engine
	cargo test --features "integration-tests,pgvector" --test integration_pgvector
	cargo test --features integration-tests --test integration_cascade
	cargo test --features integration-tests --test integration_peer
	cargo test --features integration-tests --test integration_circuit
	cargo test --features integration-tests --test integration_batch
	cargo test --features integration-tests --test integration_metrics
	cargo test --features integration-tests --test integration_context_config
	cargo test --features integration-tests --test integration_singleflight

# Mock / non-live experimental backends (Pinecone, Milvus) — not in default gate.
test-integration-experimental:
	cargo test --features integration-tests --test integration_pinecone
	cargo test --features integration-tests --test integration_milvus

# Plan 09 T4: fast e2e smoke (Docker services + data + proxy). Health + query + cache hit.
e2e-smoke-core: build-release
	@echo "=== E2E smoke core ==="
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=qdrant E2E_FILTER=smoke,health,query \
		E2E_OUTPUT_DIR="$(E2E_PROXY_DIR)/results/smoke-$(shell date +%Y%m%d-%H%M%S)" \
		$(E2E_CARGO_CMD) e2e_proxy_suite
	$(MAKE) e2e-services-down

# Plan 09 T4/T5: cascade-focused e2e (multi-upstream / cascade metrics).
e2e-cascade: build-release
	@echo "=== E2E cascade ==="
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=mixed E2E_FILTER=cascade \
		E2E_OUTPUT_DIR="$(E2E_PROXY_DIR)/results/cascade-$(shell date +%Y%m%d-%H%M%S)" \
		$(E2E_CARGO_CMD) e2e_proxy_suite
	$(MAKE) e2e-services-down

# Plan 09 T4: federated category (self-hosts mock upstream inside suite).
e2e-federated: build-release
	@echo "=== E2E federated (mock upstream) ==="
	E2E_FILTER=federated_search \
		E2E_OUTPUT_DIR="$(E2E_PROXY_DIR)/results/federated-$(shell date +%Y%m%d-%H%M%S)" \
		$(E2E_CARGO_CMD) e2e_proxy_suite

test-verbose:
	cargo test -- --nocapture --test-threads=1

test-coverage:
	cargo tarpaulin --out Html

test-coverage-quick:
	cargo tarpaulin --lib --skip-clean --out Stdout

# Check per-file coverage threshold (requires tarpaulin JSON report)
test-coverage-check:
	@echo "=== Coverage Threshold Check (80% per file) ==="
	@echo "NOTE: check_coverage binary lives in parent repo. Run from parent: make test-coverage-check"

# Plan 05: coverage gate for scope + tune modules (≥80% line floor).
# Runs tarpaulin on the two modules and asserts both meet the threshold.
# Outputs per-file percentages + final pass/fail. Exit 0 = pass, 1 = fail.
# Wire into CI via `make cov-scope-tune` once baseline is met.
cov-scope-tune:
	@echo "=== Scope + Tune Coverage Gate (≥80% line) ==="
	@_FAIL=0; \
	for _pat in 'src/proxy/scope.rs' 'src/proxy/tune/*'; do \
		_out=$$(cargo tarpaulin --lib --skip-clean --include-files "$$_pat" --out Stdout 2>/dev/null | grep -E '^/.*: [0-9]+/[0-9]+|coverage, ' | tail -2); \
		echo ""; \
		echo "--- Pattern: $$_pat ---"; \
		echo "$$_out"; \
		_pct=$$(echo "$$_out" | grep -oE '[0-9]+\.[0-9]+% coverage' | tail -1 | sed 's/%.*//'); \
		if [ -n "$$_pct" ] && awk "BEGIN {exit !($${_pct} >= 80.0)}"; then \
			echo "  PASS: $${_pct}% ≥ 80%"; \
		else \
			echo "  FAIL: $${_pct:-<unknown>}% < 80%"; \
			_FAIL=1; \
		fi; \
	done; \
	echo ""; \
	if [ "$$_FAIL" = "0" ]; then \
		echo "cov-scope-tune: PASS"; \
	else \
		echo "cov-scope-tune: FAIL"; \
	fi; \
	exit $$_FAIL

# Plan 05: Python SDK smoke — build wheel via maturin and import the module.
# Requires maturin (pip install maturin) + python3-dev (header) for pyo3 build.
# Optional: also tests instantiation if CONPROXY_SDK_SMOKE_CLIENT=1 and a
# conproxy process is reachable; default is import-only to stay offline.

# Plan 08 W2: lead-story unit proofs (cascade + federated) — no Docker.
proof-cascade:
	@echo "=== Cascade + Federated unit proofs ==="
	cargo test --lib cascade -- --nocapture
	cargo test --lib federated -- --nocapture
	cargo test --lib test_handler_query_with_cascade -- --nocapture
	cargo test --lib test_grpc_federated -- --nocapture
	@echo "proof-cascade: PASS"

sdk-smoke:
	@echo "=== Python SDK Smoke (maturin build + import) ==="
	@if ! command -v maturin >/dev/null 2>&1; then \
		echo "FAIL: maturin not found. Install via: pip install maturin"; \
		exit 1; \
	fi
	@rm -rf /tmp/conproxy-sdk-smoke
	@cd sdk/python && maturin build --release 2>&1 | tail -3
	@_WHL=$$(ls -t target/wheels/conproxy-*.whl 2>/dev/null | head -1); \
	if [ -z "$$_WHL" ]; then echo "FAIL: no wheel found in target/wheels/"; exit 1; fi; \
	echo "Built wheel: $$_WHL"; \
	python3 -m venv /tmp/conproxy-sdk-smoke && \
	/tmp/conproxy-sdk-smoke/bin/pip install --quiet "$$_WHL" 2>&1 | tail -2 && \
	/tmp/conproxy-sdk-smoke/bin/python -c \
		"import conproxy; print('import: OK'); print('client:', conproxy.ConproxyClient); print('engine:', conproxy.Engine)"
	@/tmp/conproxy-sdk-smoke/bin/python scripts/sdk-smoke.py
	@echo "sdk-smoke: PASS"

# Run specific test
test-one:
	cargo test $(TEST) -- --nocapture

# ============================================================================
# Lint & Format
# ============================================================================

fmt:
	cargo fmt

fmt-check:
	cargo fmt -- --check

# Build the conproxy Docker image (used by Tilt/kind dev loop).
#
# Local dev (default):   make docker-build                 -> conproxy:dev
# Versioned release:     make docker-build VERSION=0.1.0
#                        make docker-push  VERSION=0.1.0 REGISTRY=ghcr.io/me
# Multi-arch smoke:      make docker-buildx VERSION=0.1.0 PLATFORMS=linux/amd64,linux/arm64
VERSION ?=
REGISTRY ?=
IMAGE = $(if $(REGISTRY),$(REGISTRY)/conproxy,conproxy)
DOCKER_TAGS = $(if $(VERSION),-t $(IMAGE):$(VERSION) -t $(IMAGE):v$(VERSION),-t $(IMAGE):dev)

docker-build:
	docker build $(DOCKER_TAGS) .

docker-push: docker-build
	@if [ -z "$(VERSION)" ] || [ -z "$(REGISTRY)" ]; then \
		echo "VERSION and REGISTRY are required for docker-push (e.g. make docker-push VERSION=0.1.0 REGISTRY=ghcr.io/me)" >&2; \
		exit 1; \
	fi
	docker push $(IMAGE):$(VERSION)
	docker push $(IMAGE):v$(VERSION)

# Local multi-arch build via buildx (smoke; release job does this on GH).
docker-buildx:
	@command -v docker >/dev/null 2>&1 || { echo "docker required"; exit 1; }
	docker buildx build --platform $(PLATFORMS) $(DOCKER_TAGS) --load .

lint:
	cargo clippy -- -D warnings

lint-fix:
	cargo clippy --fix --allow-dirty

# ============================================================================
# Proxy Operations
# ============================================================================

proxy-start:
	./target/release/conproxy start

proxy-stop:
	./target/release/conproxy stop

proxy-status:
	./target/release/conproxy status

# ============================================================================
# Benchmarking
# ============================================================================

bench:
	cargo bench --bench core_ops
	cargo bench --bench cascade_scope

bench-save:
	@if [ -z "$(FORCE)" ] && [ -n "$$(git status --porcelain 2>/dev/null)" ]; then \
		echo "ERROR: git tree is dirty. Use FORCE=1 to override."; \
		exit 1; \
	fi
	cargo bench --bench core_ops -- --save-baseline main
	cargo bench --bench cascade_scope -- --save-baseline main
	@_N=$$(find target/criterion -path '*/main/estimates.json' 2>/dev/null | wc -l); \
	if [ "$$_N" -lt 10 ]; then \
		echo "ERROR: only $$_N baseline files found (expected ≥ 10)"; \
		exit 1; \
	fi; \
	_SHA=$$(git rev-parse --short HEAD 2>/dev/null || echo unknown); \
	_DIRTY=$$(git diff --quiet 2>/dev/null && echo false || echo true); \
	_RUSTC=$$(rustc --version 2>/dev/null || echo unknown); \
	_DATE=$$(date -u +%Y-%m-%dT%H:%M:%SZ); \
	printf '{\n  "schema_version": 1,\n  "saved_at": "%s",\n  "git_sha": "%s",\n  "git_dirty": %s,\n  "rustc": "%s",\n  "bench_count": %d\n}\n' \
		"$$_DATE" "$$_SHA" "$$_DIRTY" "$$_RUSTC" "$$_N" \
		> target/criterion/.baseline_meta.json; \
	echo "Baseline saved: $$_N files, sha=$$_SHA, dirty=$$_DIRTY, rustc=$$_RUSTC"

# Cache hit-rate benchmark (docs/strategy-assessment.md §3). Synthetic
# agentic + Zipf traces against the real CacheStore. Exit 2 (FAIL-CORE) if
# agentic exact hit rate misses the gate. Results: tests/results/hitrate/<ts>/
bench-hitrate:
	@_RD="tests/results/hitrate/$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	echo "=== Hit-Rate Benchmark → $$_RD ==="; \
	_RC=0; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD" || _RC=$$?; \
	cargo run --bin test_runner -- index "$$_RD" || echo "WARN: index generation failed"; \
	echo "  Index: $$_RD/index.html"; \
	exit $$_RC

# Memory-pressure A/B. Same traces + zipf payloads.
# A: count-cap 1000. B: byte-cap = A's live mem, S3-FIFO.
MEM_CAP ?= 1351648
bench-hitrate-mem:
	@_RD="tests/results/hitrate/mem-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD/count" "$$_RD/fifo"; \
	echo "=== Hit-Rate memory A/B → $$_RD (MEM_CAP=$(MEM_CAP)) ==="; \
	echo "--- arm A: count-cap 1000, zipf payloads ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/count" \
	  --cache-size 1000 --payload-zipf-max 4096 --no-fail; \
	echo "--- arm B: mem-cap $(MEM_CAP), FIFO ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/fifo" \
	  --cache-size 1000000 --max-memory-bytes $(MEM_CAP) --payload-zipf-max 4096 \
	  --no-fail; \
	echo "  A count: $$_RD/count"; \
	echo "  B fifo:  $$_RD/fifo"

# Sustained memory-pressure suite: starve / scan / flip / elephants / agentic-tight.
PRESS_MEM ?= 400000
PRESS_ELE_MEM ?= 2097152
bench-hitrate-pressure:
	@_RD="tests/results/hitrate/pressure-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	echo "=== Sustained pressure suite → $$_RD ==="; \
	echo "--- starve ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/starve" \
	  --workload zipf --unique 30000 --queries 80000 --paraphrase-rate 0 \
	  --cache-size 1000000 --max-memory-bytes $(PRESS_MEM) --payload-zipf-max 4096 \
	  --warmup-frac 0.25 --series-every 5000 --hot-set 50 --no-fail; \
	echo "--- scan ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/scan" \
	  --workload scan --unique 10000 --queries 40000 --scan-n 8000 --paraphrase-rate 0 \
	  --cache-size 1000000 --max-memory-bytes $(PRESS_MEM) --payload-zipf-max 4096 \
	  --warmup-frac 0.25 --series-every 5000 --hot-set 50 --no-fail; \
	echo "--- flip ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/flip" \
	  --workload flip --unique 10000 --queries 80000 --paraphrase-rate 0 \
	  --cache-size 1000000 --max-memory-bytes $(PRESS_MEM) --payload-zipf-max 4096 \
	  --warmup-frac 0.25 --series-every 5000 --hot-set 50 --no-fail; \
	echo "--- elephants ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/elephants" \
	  --workload zipf --unique 20000 --queries 60000 --paraphrase-rate 0 --elephants \
	  --cache-size 1000000 --max-memory-bytes $(PRESS_ELE_MEM) --payload-bytes 64 \
	  --warmup-frac 0.25 --series-every 5000 --hot-set 50 --no-fail; \
	echo "--- agentic-tight ---"; \
	cargo run --bin hitrate_bench -- --results-dir "$$_RD/agentic" \
	  --workload agentic --pool 2000 --tasks 200 --paraphrase-rate 0 \
	  --cache-size 1000000 --max-memory-bytes $(PRESS_MEM) --payload-zipf-max 4096 \
	  --warmup-frac 0.25 --series-every 5000 --no-fail; \
	echo "  Results: $$_RD"

# Semantic hit-rate mode (v2): real SemanticCache tier + synthetic orthogonal
# embedder. Requires embed-api feature. Sweeps τ, gates false-hit ≤ 1%.
# Exit 3 (FAIL-TRUST) if no τ clears the false-hit gate with uplift.
# Runtime ~6 min default suite (linear scan per lookup; scales with
# sem-cache-size). Trim with --sem-max-queries / --queries for iteration.
bench-hitrate-sem:
	@_RD="tests/results/hitrate/sem-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	echo "=== Hit-Rate Benchmark (semantic) → $$_RD ==="; \
	_RC=0; \
	cargo run --features embed-api --bin hitrate_bench -- \
	  --semantic --results-dir "$$_RD" || _RC=$$?; \
	cargo run --bin test_runner -- index "$$_RD" || echo "WARN: index generation failed"; \
	echo "  Index: $$_RD/index.html"; \
	exit $$_RC

# Live ONNX embedder semantic mode (v3): real all-MiniLM-L6-v2 embeddings
# instead of the synthetic orthogonal embedder. Requires: embed feature
# (ONNX runtime system libs) + model installed under ~/.conproxy/models/.
# ort-sys links the system lib: point ORT_LIB_LOCATION at it and prefer
# dynamic linking (override both if your setup differs).
# Embeddings are memoized across τ/cache sizes/workloads, so the ONNX cost is
# paid once per unique query. Semantic lookup is a linear scan at 384 dims —
# default caps keep the suite in the ~10 min range; raise for full runs:
#   make bench-hitrate-onnx SEM_MAX=100000
bench-hitrate-onnx:
	@_RD="tests/results/hitrate/onnx-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	echo "=== Hit-Rate Benchmark (semantic, ONNX embedder) → $$_RD ==="; \
	_RC=0; \
	ORT_LIB_LOCATION=$${ORT_LIB_LOCATION:-/usr/local/lib} \
	ORT_PREFER_DYNAMIC_LINK=$${ORT_PREFER_DYNAMIC_LINK:-1} \
	cargo run --features embed --bin hitrate_bench -- \
	  --semantic --embedder onnx --results-dir "$$_RD" \
	  --sem-max-queries $${SEM_MAX:-20000} || _RC=$$?; \
	cargo run --bin test_runner -- index "$$_RD" || echo "WARN: index generation failed"; \
	echo "  Index: $$_RD/index.html"; \
	exit $$_RC

# Live wire mode (v4): replay against a REAL proxy + qdrant (docker). Seeds
# qdrant with MiniLM-embedded docs, replays traces through the proxy HTTP API,
# measures real hit rate + wall latency. Optional mutation stream bumps doc
# versions in qdrant (stale-content accounting); LIVE_EVICT=1 adds a
# /cache/evict call after each mutation (simulated external CDC).
# Requires: docker, embed feature (ONNX runtime libs), model installed.
# Tunables: LIVE_QUERIES (zipf events), LIVE_TASKS (agentic tasks),
# LIVE_MUTATE (per-event mutation prob), LIVE_EVICT (0|1).
LIVE_QUERIES ?= 5000
LIVE_TASKS ?= 100
LIVE_MUTATE ?= 0.001
LIVE_EVICT ?= 0
bench-hitrate-live:
	@command -v docker >/dev/null 2>&1 || { echo "ERROR: docker required for bench-hitrate-live"; exit 1; }
	@_WD=/tmp/conproxy-hitrate-live; \
	_RD="tests/results/hitrate/live-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_WD" "$$_RD"; \
	export ORT_LIB_LOCATION=$${ORT_LIB_LOCATION:-/usr/local/lib} \
	  ORT_PREFER_DYNAMIC_LINK=$${ORT_PREFER_DYNAMIC_LINK:-1}; \
	cargo build --profile profiling --features embed --bin conproxy --bin hitrate_bench || exit 1; \
	docker rm -f conproxy-hitrate-qdrant >/dev/null 2>&1 || true; \
	docker run -d --name conproxy-hitrate-qdrant -p 16333:6333 qdrant/qdrant >/dev/null || exit 1; \
	_QOK=0; for _i in $$(seq 1 60); do \
	  curl -sf http://localhost:16333/healthz >/dev/null 2>&1 && { _QOK=1; break; }; sleep 1; done; \
	if [ $$_QOK != 1 ]; then echo "ERROR: qdrant not ready"; docker rm -f conproxy-hitrate-qdrant >/dev/null; exit 1; fi; \
	printf '[proxy]\nlisten = "127.0.0.1:8098"\n\n[proxy.embedding]\nprovider = "onnx"\n\n[upstreams.qdrant]\nurl = "http://localhost:16333"\ntype = "qdrant"\ncollection = "conproxy_hitrate"\n\n[contexts.default]\ndefault = true\n\n[[contexts.default.upstreams]]\nref = "qdrant"\npriority = 0\n' > "$$_WD/conproxy.toml"; \
	$(CURDIR)/target/profiling/conproxy start --config "$$_WD/conproxy.toml" >"$$_WD/proxy.log" 2>&1 & \
	_PID=$$!; \
	trap 'kill -INT $$_PID 2>/dev/null; docker rm -f conproxy-hitrate-qdrant >/dev/null 2>&1 || true' EXIT; \
	_READY=0; for _i in $$(seq 1 90); do \
	  kill -0 $$_PID 2>/dev/null || break; \
	  curl -sf http://127.0.0.1:8099/health >/dev/null 2>&1 && { _READY=1; break; }; sleep 1; done; \
	if [ $$_READY != 1 ]; then echo "ERROR: proxy not ready (see $$_WD/conproxy.toml)"; exit 1; fi; \
	echo "=== Hit-Rate Benchmark (LIVE) → $$_RD ==="; \
	_RC=0; \
	_EVICT=""; [ "$(LIVE_EVICT)" = "1" ] && _EVICT="--live-evict"; \
	$(CURDIR)/target/profiling/hitrate_bench \
	  --live http://127.0.0.1:8099 --live-seed http://localhost:16333 \
	  --queries $(LIVE_QUERIES) --tasks $(LIVE_TASKS) \
	  --live-mutate $(LIVE_MUTATE) $$_EVICT \
	  --results-dir "$(CURDIR)/$$_RD" || _RC=$$?; \
	cargo run --bin test_runner -- index "$$_RD" || echo "WARN: index generation failed"; \
	echo "  Index: $$_RD/index.html"; \
	exit $$_RC

# Bring-your-own-trace replay. Use a real query log from your corpus to
# measure hit rate on YOUR data — the only de-risking step before going
# to prod (see docs/strategy-assessment.md §3). One query per line, no
# other columns. File size cap is hitrate_bench's own load_queries_file.
# Tunables: QUERIES, POOL (number of unique queries for the pool),
# CACHE_SIZE, MODE (exact | semantic), TTL.
QUERIES ?=
POOL ?= 1000
CACHE_SIZE ?= 10000
MODE ?= exact
TTL ?= 600
bench-hitrate-replay:
	@test -n "$(QUERIES)" || { echo "ERROR: set QUERIES=path/to/trace.txt"; exit 1; }; \
	test -f "$(QUERIES)" || { echo "ERROR: $(QUERIES) not found"; exit 1; }; \
	_RD="tests/results/hitrate/replay-$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	echo "=== Hit-Rate Benchmark (replay) → $$_RD ==="; \
	_RC=0; \
	_FEAT=""; [ "$(MODE)" = "semantic" ] && _FEAT="--features embed-api"; \
	cargo run $$_FEAT --bin hitrate_bench -- \
	  --queries-file "$(QUERIES)" --pool "$(POOL)" \
	  --cache-size "$(CACHE_SIZE)" --ttl "$(TTL)" \
	  --results-dir "$$_RD" || _RC=$$?; \
	cargo run --bin test_runner -- index "$$_RD" || echo "WARN: index generation failed"; \
	echo "  Index: $$_RD/index.html"; \
	exit $$_RC

bench-compare:
	cargo bench --bench core_ops --
	cargo bench --bench cascade_scope --

# ============================================================================
# Profiling
# ============================================================================

# DHAT heap profile (standalone — start proxy, run brief workload, collect dhat-heap.json)
profile-dhat:
	@echo "=== DHAT Heap Profile ==="
	@cargo build --profile profiling --features dhat-heap
	@mkdir -p /tmp/conproxy-dhat
	@printf '[proxy]\nlisten = "127.0.0.1:9096"\n\n[[proxy.upstreams]]\nid = "test"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' > /tmp/conproxy-dhat/conproxy.toml
	@echo "Starting proxy with dhat instrumentation on :9096/:9097..."
	@cd /tmp/conproxy-dhat && CONPROXY_DHAT=1 $(CURDIR)/target/profiling/conproxy start --config conproxy.toml & \
	_PID=$$!; \
	trap 'kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null' EXIT; \
	_READY=0; \
	for _i in $$(seq 1 40); do \
		if ! kill -0 $$_PID 2>/dev/null; then break; fi; \
		if curl -sf http://127.0.0.1:9097/health >/dev/null 2>&1; then _READY=1; break; fi; \
		sleep 0.25; \
	done; \
	if [ "$$_READY" = "1" ]; then \
		echo "Proxy ready, running brief workload..."; \
		for _q in "rust async" "error handling" "database" "concurrency" "testing"; do \
			curl -sf -X POST http://127.0.0.1:9097/query -H 'Content-Type: application/json' \
				-d "{\"query\":\"$$_q\"}" >/dev/null 2>&1 || true; \
		done; \
		echo "Stopping proxy..."; \
	fi; \
	kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; trap - EXIT; \
	if [ -f /tmp/conproxy-dhat/dhat-heap.json ]; then \
		echo ""; \
		echo "DHAT profile written to: /tmp/conproxy-dhat/dhat-heap.json"; \
		ls -lh /tmp/conproxy-dhat/dhat-heap.json | awk '{print "  Size: " $$5}'; \
		echo "  View: https://nnethercote.github.io/dh_view/dh_view.html"; \
	else \
		echo "ERROR: dhat-heap.json not generated"; \
	fi

# PGO (Profile-Guided Optimization) build.
#
# PGO is a 2-pass optimization: first build an instrumented binary, run a
# representative workload to record branch coverage, then rebuild using
# that data for better optimization. Yields 5-20% speedups on hot paths.
#
# Requires `rustup component add llvm-tools-preview` for `llvm-profdata`.
# Requires Docker to be running for the Qdrant workload (uses existing
# `cqdrant` container or starts one).
profile-pgo:
	@rustup component add llvm-tools-preview >/dev/null 2>&1 || true
	@rm -rf /tmp/conproxy-pgo-profraw /tmp/conproxy-pgo-profdata /tmp/conproxy-pgo-wd
	@mkdir -p /tmp/conproxy-pgo-profraw /tmp/conproxy-pgo-wd
	@echo "=== PGO step 1: instrument build ==="
	@RUSTFLAGS="-Cprofile-generate=/tmp/conproxy-pgo-profraw" \
		cargo build --release --features release --bin conproxy
	@echo "=== PGO step 2: drive workload ==="
	@if curl -sf http://localhost:6333/readyz >/dev/null 2>&1; then \
		mkdir -p /tmp/conproxy-pgo-wd/.conproxy/cache; \
		printf '[proxy]\nlisten = "127.0.0.1:9090"\n\n[[proxy.upstreams]]\nid = "pgo-qdrant"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' \
			> /tmp/conproxy-pgo-wd/.conproxy/conproxy.toml; \
		cd /tmp/conproxy-pgo-wd && $(CURDIR)/target/release/conproxy start --daemon; \
		_PID=$$(lsof -ti:9090 2>/dev/null); \
		_READY=0; \
		for _i in $$(seq 1 40); do \
			kill -0 $$_PID 2>/dev/null || break; \
			if curl -sf http://127.0.0.1:9091/health >/dev/null 2>&1; then _READY=1; break; fi; \
			sleep 0.25; \
		done; \
		if [ "$$_READY" = "1" ]; then \
			for _q in "rust async" "error handling" "caching" "vector search" "tokio"; do \
				curl -sf -X POST http://127.0.0.1:9091/query \
					-H 'Content-Type: application/json' \
					-d "{\"query\":\"$$_q\",\"top_k\":5}" >/dev/null 2>&1 || true; \
			done; \
			cd /tmp/conproxy-pgo-wd && $(CURDIR)/target/release/conproxy stop; \
		else \
			echo "WARNING: proxy not ready on :9091; using fallback (cargo bench)"; \
			cargo bench --bench core_ops -- --quick >/dev/null 2>&1 || true; \
		fi; \
	else \
		echo "WARNING: Qdrant not reachable at localhost:6333; using fallback workload (cargo bench)"; \
		cargo bench --bench core_ops -- --quick >/dev/null 2>&1 || true; \
	fi
	@echo "=== PGO step 3: merge profraw ==="
	@LLVMPROFDATA=$$(rustc --print sysroot)/lib/rustlib/$$(rustc -vV | sed -n 's/host: //p')/bin/llvm-profdata; \
	if [ ! -x "$$LLVMPROFDATA" ]; then \
		echo "ERROR: llvm-profdata not found at $$LLVMPROFDATA"; \
		echo "  Install via: rustup component add llvm-tools-preview"; \
		exit 1; \
	fi; \
	$$LLVMPROFDATA merge -o /tmp/conproxy-pgo-profdata /tmp/conproxy-pgo-profraw/*.profraw
	@echo "=== PGO step 4: rebuild with profile data ==="
	@RUSTFLAGS="-Cprofile-use=/tmp/conproxy-pgo-profdata -Copt-level=2 -Clto=fat" \
		cargo build --release --features release --bin conproxy
	@echo ""
	@echo "PGO-optimized binary at: target/release/conproxy"
	@ls -lh target/release/conproxy | awk '{print "  Size: " $$5}'

profile-pgo-clean:
	@rm -rf /tmp/conproxy-pgo-profraw /tmp/conproxy-pgo-profdata /tmp/conproxy-pgo-wd

# CPU flamegraph under load. Prefers perf+inferno (real SVG, attach mode).
# samply also supported (emits Firefox Profiler profile.json.gz — open in
# https://profiler.firefox.com, not an SVG).
# Prerequisite: cargo install inferno-flamegraph samply
# (system `perf` required; perf_event_paranoid ≤ 1)
profile-flamegraph:
	@echo "=== CPU Flamegraph ==="
	@mkdir -p /tmp/conproxy-flame
	@printf '[proxy]\nlisten = "127.0.0.1:9090"\n\n[[proxy.upstreams]]\nid = "qdrant"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' > /tmp/conproxy-flame/conproxy.toml
	@cargo build --profile profiling --features release --bin conproxy
	@cd /tmp/conproxy-flame && $(CURDIR)/target/profiling/conproxy start --config conproxy.toml & \
	_PID=$$!; \
	trap 'kill -INT $$_PID 2>/dev/null; rm -rf /tmp/conproxy-flame' EXIT; \
	_READY=0; \
	for _i in $$(seq 1 40); do \
		kill -0 $$_PID 2>/dev/null || break; \
		if curl -sf http://127.0.0.1:9091/health >/dev/null 2>&1; then _READY=1; break; fi; \
		sleep 0.25; \
	done; \
	if [ "$$_READY" != "1" ]; then echo "ERROR: proxy not ready on :9091"; exit 1; fi; \
	if command -v perf >/dev/null 2>&1 && command -v inferno-flamegraph >/dev/null 2>&1; then \
		echo "Using perf + inferno (real SVG)..."; \
		perf record -g -F 99 -p $$_PID -o /tmp/conproxy-flame/perf.data -- sleep 12 & \
		_SAMP_PID=$$!; \
	elif command -v samply >/dev/null 2>&1; then \
		echo "Using samply (Firefox Profiler format — open in https://profiler.firefox.com)..."; \
		samply record -p $$_PID -o $(CURDIR)/profile.json.gz --save-only --duration 12 & \
		_SAMP_PID=$$!; \
	else \
		echo "ERROR: need (perf + inferno-flamegraph) OR samply."; \
		echo "Install with: cargo install inferno-flamegraph samply"; \
		exit 1; \
	fi; \
	for _n in $$(seq 1 30); do \
		for _q in "performance profiling sample" "rust async patterns" "vector search optimization"; do \
			curl -sf -X POST http://127.0.0.1:9091/query -H 'Content-Type: application/json' \
				-d "{\"query\":\"$$_q\",\"top_k\":5}" >/dev/null 2>&1 || true; \
		done; \
	done; \
	wait $$_SAMP_PID 2>/dev/null; \
	if [ -f /tmp/conproxy-flame/perf.data ]; then \
		perf script -i /tmp/conproxy-flame/perf.data | inferno-collapse-perf | \
			inferno-flamegraph > $(CURDIR)/flamegraph.svg; \
	fi; \
	if [ ! -s $(CURDIR)/flamegraph.svg ] && [ ! -s $(CURDIR)/profile.json.gz ]; then \
		echo "ERROR: empty flamegraph output"; exit 1; \
	fi; \
	if [ -s $(CURDIR)/flamegraph.svg ]; then \
		echo "Flamegraph output: $(CURDIR)/flamegraph.svg"; \
	else \
		echo "Profile output:  $(CURDIR)/profile.json.gz (open in profiler.firefox.com)"; \
	fi; \
	kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; \
	trap - EXIT; rm -rf /tmp/conproxy-flame

# ============================================================================
# Tokio Console + Metrics
# ============================================================================

# Tokio-console async runtime diagnosis (dev only).
# Requires: RUSTFLAGS="--cfg tokio_unstable" + tokio-console feature.
# Default bind: 127.0.0.1:6669 (overridable via TOKIO_CONSOLE_BIND env).
# Inspect with the `tokio-console` CLI client (not a browser):
#   cargo install tokio-console
#   tokio-console               # connects to default 127.0.0.1:6669
profile-tokio-console:
	@echo "=== Tokio Console (async runtime diagnosis) ==="
	@mkdir -p /tmp/conproxy-console
	@printf '[proxy]\nlisten = "127.0.0.1:8080"\n\n[[proxy.upstreams]]\nid = "qdrant"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' > /tmp/conproxy-console/conproxy.toml
	RUSTFLAGS="--cfg tokio_unstable" cargo build --profile profiling --features tokio-console --bin conproxy
	@echo "Starting proxy with tokio-console (default bind 127.0.0.1:6669) ..."
	@echo "In another terminal, run: tokio-console"
	cd /tmp/conproxy-console && $(CURDIR)/target/profiling/conproxy start --config conproxy.toml

# Prometheus metrics snapshot under load.
# Heap timeline profile (optional tool; deeper than DHAT for finding peak RSS leaks).
# Needs the `heaptrack` binary (https://github.com/KDE/heaptrack). Mirrors
# flamegraph's attach-under-load model: start proxy, attach heaptrack to the
# running PID, send curl load for ~12s, then summarize. The .gz file is
# portable; view with `heaptrack_print` or heaptrack_gui.
profile-heaptrack:
	@echo "=== Heaptrack heap timeline ==="
	@command -v heaptrack >/dev/null 2>&1 || { \
		echo "ERROR: 'heaptrack' not found."; \
		echo "Install: https://github.com/KDE/heaptrack (Debian/Ubuntu: apt install heaptrack)"; \
		exit 1; \
	}
	@mkdir -p /tmp/conproxy-heap
	@printf '[proxy]\nlisten = "127.0.0.1:9094"\n\n[[proxy.upstreams]]\nid = "qdrant"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' > /tmp/conproxy-heap/conproxy.toml
	cargo build --profile profiling --bin conproxy
	@cd /tmp/conproxy-heap && $(CURDIR)/target/profiling/conproxy start --config conproxy.toml & \
	_PID=$$!; \
	trap 'kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null' EXIT; \
	_READY=0; \
	for _i in $$(seq 1 40); do \
		if ! kill -0 $$_PID 2>/dev/null; then break; fi; \
		if curl -sf http://127.0.0.1:9095/health >/dev/null 2>&1; then _READY=1; break; fi; \
		sleep 0.25; \
	done; \
	if [ "$$_READY" != "1" ]; then echo "ERROR: proxy not ready on :9095"; exit 1; fi; \
	echo "Attaching heaptrack to PID $$_PID (12s)..."; \
	heaptrack -o /tmp/conproxy-heap/conproxy.heap -p $$_PID &
	_HT_PID=$$!; \
	sleep 1; \
	for _n in $$(seq 1 30); do \
		for _q in "performance profiling sample" "rust async patterns" "vector search optimization"; do \
			curl -sf -X POST http://127.0.0.1:9095/query -H 'Content-Type: application/json' \
				-d "{\"query\":\"$$_q\",\"top_k\":5}" >/dev/null 2>&1 || true; \
		done; \
	done; \
	wait $$_HT_PID 2>/dev/null; \
	kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; \
	trap - EXIT; \
	if ls /tmp/conproxy-heap/conproxy.heap.*.gz >/dev/null 2>&1; then \
		cp /tmp/conproxy-heap/conproxy.heap.*.gz $(CURDIR)/heaptrack-$$(date +%Y%m%d-%H%M%S).gz; \
		ls -lh $(CURDIR)/heaptrack-*.gz | tail -1 | awk '{print "Heaptrack output: " $$9 " (" $$5 ")"}'; \
		echo "View: heaptrack_print <file> | heaptrack_gui <file>"; \
	else \
		echo "ERROR: heaptrack output not generated"; \
		exit 1; \
	fi; \
	rm -rf /tmp/conproxy-heap

# Prometheus metrics snapshot under load.
# Starts proxy on :9092/:9093, scrapes /metrics/prometheus BEFORE and AFTER a
# curl workload, writes both to /tmp/conproxy-metrics/. perf_summarize can
# compute counter deltas when both files are passed via --metrics-file
# (after) + --metrics-before-file (before).
profile-metrics-snap:
	@echo "=== Prometheus Metrics Snapshot ==="
	@mkdir -p /tmp/conproxy-metrics
	@printf '[proxy]\nlisten = "127.0.0.1:9092"\n\n[[proxy.upstreams]]\nid = "qdrant"\nurl = "http://localhost:6333"\nupstream_type = "qdrant"\n' > /tmp/conproxy-metrics/conproxy.toml
	@: > /tmp/conproxy-metrics/metrics-prometheus.txt  # ensure file exists; perf-tuning copies it
	@: > /tmp/conproxy-metrics/metrics-before.txt
	cargo build --profile profiling --bin conproxy
	@cd /tmp/conproxy-metrics && $(CURDIR)/target/profiling/conproxy start --config conproxy.toml & \
	_PID=$$!; \
	trap 'kill -INT $$_PID 2>/dev/null' EXIT; \
	_READY=0; \
	for _i in $$(seq 1 40); do \
		kill -0 $$_PID 2>/dev/null || break; \
		if curl -sf http://127.0.0.1:9093/health >/dev/null 2>&1; then _READY=1; break; fi; \
		sleep 0.25; \
	done; \
	if [ "$$_READY" != "1" ]; then echo "ERROR: proxy not ready on :9093"; exit 1; fi; \
	echo "Scraping BEFORE workload..."; \
	curl -sf http://127.0.0.1:9093/metrics/prometheus -o /tmp/conproxy-metrics/metrics-before.txt \
		|| echo "(no /metrics/prometheus endpoint)"; \
	echo "Sending load..."; \
	for _n in $$(seq 1 30); do \
		for _q in "performance profiling sample" "rust async patterns" "vector search optimization"; do \
			curl -sf -X POST http://127.0.0.1:9093/query -H 'Content-Type: application/json' \
				-d "{\"query\":\"$$_q\",\"top_k\":5}" >/dev/null 2>&1 || true; \
		done; \
	done; \
	echo ""; \
	echo "--- /metrics/prometheus (AFTER) ---"; \
	curl -sf http://127.0.0.1:9093/metrics/prometheus -o /tmp/conproxy-metrics/metrics-prometheus.txt && \
		head -80 /tmp/conproxy-metrics/metrics-prometheus.txt || echo "(no /metrics/prometheus endpoint)"; \
	echo ""; \
	echo "--- /metrics (JSON summary) ---"; \
	curl -sf http://127.0.0.1:9093/metrics 2>/dev/null | head -50 || echo "(no /metrics endpoint)"; \
	echo ""; \
	echo "--- /debug/vars ---"; \
	curl -sf http://127.0.0.1:9093/debug/vars 2>/dev/null | head -50 || echo "(no /debug/vars)"; \
	kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; \
	trap - EXIT

# ============================================================================
# Perf Tuning (structured measure → summarize → plan)
# ============================================================================

# Quick: run both bench targets with (CI-aware verdict),
# summarize, write to results dir. PIPESTATUS propagates real cargo exit.
# Touches .bench_start so perf_summarize --since filters stale bench data.
# After summarize, report_criterion.json is mirrored to bench/ so test_runner
# index can pick it up via build_bench_digest (looks for report_<name>.json).
perf-tuning-quick:
	@_RD="tests/results/perf-tuning/$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	touch "$$_RD/.bench_start"; \
	echo "=== Perf Tuning (quick) → $$_RD ==="; \
	echo "--- core_ops (vs main baseline) ---"; \
	cargo bench --bench core_ops -- --quick 2>&1 | tee "$$_RD/criterion_core_ops.txt"; \
	echo "--- cascade_scope (vs main baseline) ---"; \
	cargo bench --bench cascade_scope -- --quick 2>&1 | tee "$$_RD/criterion_cascade_scope.txt"; \
	echo "--- summarize ---"; \
	cargo run --bin perf_summarize -- --results-dir "$$_RD" --threshold 15 --mode quick \
		--since "$$_RD/.bench_start" --fail-on-regression; \
	mkdir -p "$$_RD/bench"; \
	cp "$$_RD/report_criterion.json" "$$_RD/bench/report_criterion.json"; \
	echo ""; \
	echo "=== Results: $$_RD ==="; \
	cat "$$_RD/SUMMARY.md"

# Default: quick + metrics-snap (scrapes /metrics/prometheus → metrics-prometheus.txt)
perf-tuning-default:
	@_RD="tests/results/perf-tuning/$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	touch "$$_RD/.bench_start"; \
	echo "=== Perf Tuning (default) → $$_RD ==="; \
	echo "--- core_ops (vs main baseline) ---"; \
	cargo bench --bench core_ops -- --quick 2>&1 | tee "$$_RD/criterion_core_ops.txt"; \
	echo "--- cascade_scope (vs main baseline) ---"; \
	cargo bench --bench cascade_scope -- --quick 2>&1 | tee "$$_RD/criterion_cascade_scope.txt"; \
	echo "--- metrics-snap (prom scrape) ---"; \
	$(MAKE) profile-metrics-snap 2>&1 | tee "$$_RD/metrics-snap.txt" || true; \
	cp /tmp/conproxy-metrics/metrics-prometheus.txt "$$_RD/" 2>/dev/null || true; \
	cp /tmp/conproxy-metrics/metrics-before.txt "$$_RD/" 2>/dev/null || true; \
	echo "--- summarize ---"; \
	cargo run --bin perf_summarize -- --results-dir "$$_RD" --threshold 15 --mode default \
		--since "$$_RD/.bench_start" --fail-on-regression \
		--metrics-file "$$_RD/metrics-prometheus.txt" \
		--metrics-before-file "$$_RD/metrics-before.txt"; \
	mkdir -p "$$_RD/bench"; \
	cp "$$_RD/report_criterion.json" "$$_RD/bench/report_criterion.json"; \
	echo ""; \
	echo "=== Results: $$_RD ==="; \
	cat "$$_RD/SUMMARY.md"

# Full: default + flamegraph (+ DHAT if DHAT=1). PGO is intentionally NOT included (separate workflow).
# FLAMEGRAPH=0 to skip flamegraph; DHAT=1 to additionally capture heap profile.
perf-tuning-full:
	@_RD="tests/results/perf-tuning/$$(date +%Y%m%d-%H%M%S)-$$$$"; \
	mkdir -p "$$_RD"; \
	touch "$$_RD/.bench_start"; \
	echo "=== Perf Tuning (full) → $$_RD ==="; \
	echo "--- core_ops (full) ---"; \
	cargo bench --bench core_ops -- 2>&1 | tee "$$_RD/criterion_core_ops.txt"; \
	echo "--- cascade_scope (full) ---"; \
	cargo bench --bench cascade_scope -- 2>&1 | tee "$$_RD/criterion_cascade_scope.txt"; \
	echo "--- metrics-snap (prom scrape) ---"; \
	$(MAKE) profile-metrics-snap 2>&1 | tee "$$_RD/metrics-snap.txt" || true; \
	cp /tmp/conproxy-metrics/metrics-prometheus.txt "$$_RD/" 2>/dev/null || true; \
	cp /tmp/conproxy-metrics/metrics-before.txt "$$_RD/" 2>/dev/null || true; \
	if [ "$${FLAMEGRAPH:-1}" != "0" ]; then \
		echo "--- flamegraph ---"; \
		$(MAKE) profile-flamegraph 2>&1 | tee "$$_RD/flamegraph.txt" || true; \
	else \
		echo "--- flamegraph: SKIPPED (FLAMEGRAPH=0) ---"; \
	fi; \
	if [ "$${DHAT:-0}" = "1" ]; then \
		echo "--- dhat (DHAT=1) ---"; \
		$(MAKE) profile-dhat 2>&1 | tee "$$_RD/dhat.txt" || true; \
		cp /tmp/conproxy-dhat/dhat-heap.json "$$_RD/" 2>/dev/null || true; \
	else \
		echo "--- dhat: SKIPPED (set DHAT=1 to enable; needs Qdrant) ---"; \
	fi; \
	if [ "$${HITRATE:-1}" != "0" ]; then \
		echo "--- hitrate (exact, default workloads) ---"; \
		mkdir -p "$$_RD/hitrate"; \
		cargo run --bin hitrate_bench -- --results-dir "$$_RD/hitrate" --no-fail 2>&1 | tee "$$_RD/hitrate.txt" || true; \
	else \
		echo "--- hitrate: SKIPPED (HITRATE=0) ---"; \
	fi; \
	if [ "$${HEAPTRACK:-1}" != "0" ]; then \
		echo "--- heaptrack (graceful if tool missing) ---"; \
		$(MAKE) profile-heaptrack 2>&1 | tee "$$_RD/heaptrack.txt" || echo "(heaptrack unavailable; skipped)" > "$$_RD/heaptrack.txt"; \
	else \
		echo "--- heaptrack: SKIPPED (HEAPTRACK=0) ---"; \
	fi; \
	if [ "$${TOKIO_CONSOLE:-0}" = "1" ]; then \
		echo "--- tokio-console (instrumented proxy + headless snap) ---"; \
		_WD=/tmp/conproxy-console-full; mkdir -p "$$_WD"; \
		printf '[proxy]\nlisten = "127.0.0.1:8084"\n[contexts.console]\ndefault = true\n' > "$$_WD/conproxy.toml"; \
		RUSTFLAGS="--cfg tokio_unstable" cargo build --profile profiling --features tokio-console,tokio-taskdump --bin conproxy 2>&1 | tee "$$_RD/console-build.txt"; \
		cargo build --profile profiling --features tokio-console-snap --bin console_snap 2>&1 | tee -a "$$_RD/console-build.txt" || true; \
		$(CURDIR)/target/profiling/conproxy start --config "$$_WD/conproxy.toml" >"$$_WD/proxy.log" 2>&1 & \
		_PID=$$!; \
		trap 'kill -INT $$_PID 2>/dev/null' EXIT; \
		_READY=0; \
		for _i in $$(seq 1 40); do \
		  kill -0 $$_PID 2>/dev/null || break; \
		  if curl -sf http://127.0.0.1:8085/health >/dev/null 2>&1; then _READY=1; break; fi; \
		  sleep 0.25; \
		done; \
		if [ "$$_READY" != "1" ]; then \
		  echo "(tokio-console: proxy not ready on :8085; skipping)"; \
		  kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; trap - EXIT; \
		else \
		  echo "Sending load (12s) ..."; \
		  for _n in $$(seq 1 20); do \
		    for _q in "performance profiling sample" "rust async patterns" "vector search optimization"; do \
		      curl -sf -X POST http://127.0.0.1:8085/query -H 'Content-Type: application/json' \
		        -d "{\"query\":\"$$_q\",\"top_k\":5}" >/dev/null 2>&1 || true; \
		    done; \
		  done >/dev/null 2>&1 & \
		  _LOAD_PID=$$!; \
		  echo "Sampling console (5s) ..."; \
		  $(CURDIR)/target/profiling/console_snap http://127.0.0.1:6669 5 --out "$$_RD" --top 10 \
		    2>&1 | tee "$$_RD/console-snap.txt" || true; \
		  wait $$_LOAD_PID 2>/dev/null || true; \
		  if [ ! -f "$$_RD/console-snap.json" ]; then touch "$$_RD/console-snap.json"; fi; \
		  echo "Fetching /debug/tokio (RuntimeMetrics) ..."; \
		  curl -sf http://127.0.0.1:8085/debug/tokio -o "$$_RD/tokio-metrics.json" 2>/dev/null || \
		    echo "(no /debug/tokio endpoint)" > "$$_RD/tokio-metrics.json"; \
		  echo "Fetching /debug/tokio/dump (Handle::dump) ..."; \
		  curl -sf http://127.0.0.1:8085/debug/tokio/dump -o "$$_RD/tokio-dump.txt" 2>/dev/null || \
		    echo "(no /debug/tokio/dump endpoint — needs RUSTFLAGS=--cfg tokio_unstable + tokio-console feature)" > "$$_RD/tokio-dump.txt"; \
		  kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; trap - EXIT; \
		fi; \
		rm -rf "$$_WD"; \
	else \
		echo "--- tokio-console: SKIPPED (set TOKIO_CONSOLE=1 to enable; needs RUSTFLAGS=--cfg tokio_unstable + console-snap built) ---"; \
	fi; \
	echo "--- summarize ---"; \
	_PS_ARGS="--results-dir $$_RD --threshold 15 --mode full \
		--since $$_RD/.bench_start --fail-on-regression \
		--metrics-file $$_RD/metrics-prometheus.txt \
		--metrics-before-file $$_RD/metrics-before.txt \
		--flamegraph flamegraph.svg \
		--history-dir perf-history"; \
	if [ -f "$$_RD/hitrate/summary.json" ]; then _PS_ARGS="$$_PS_ARGS --hitrate-summary $$_RD/hitrate/summary.json"; fi; \
	if [ -f "$$_RD/console-snap.json" ] && [ -s "$$_RD/console-snap.json" ]; then _PS_ARGS="$$_PS_ARGS --tokio-snap $$_RD/console-snap.json"; fi; \
	if [ -f "$$_RD/tokio-metrics.json" ] && [ -s "$$_RD/tokio-metrics.json" ]; then _PS_ARGS="$$_PS_ARGS --tokio-metrics $$_RD/tokio-metrics.json"; fi; \
	if [ -f "$$_RD/tokio-dump.txt" ] && [ -s "$$_RD/tokio-dump.txt" ]; then _PS_ARGS="$$_PS_ARGS --tokio-dump $$_RD/tokio-dump.txt"; fi; \
	cargo run --bin perf_summarize -- $$_PS_ARGS; \
	mkdir -p "$$_RD/bench"; \
	cp "$$_RD/report_criterion.json" "$$_RD/bench/report_criterion.json"; \
	echo ""; \
	echo "=== Results: $$_RD ==="; \
	cat "$$_RD/SUMMARY.md"

# Remove all perf-tuning run dirs (gitignored).
perf-tuning-clean:
	rm -rf tests/results/perf-tuning/

# perf-publish: copy artifacts from latest run → perf-history/<ts>-<sha8>/
# Prune to last 100. NEVER auto-commits — prints the git command for you.
# Use `RUN=<dir>` to publish a specific run instead of the latest.
perf-publish:
	@_SRC="$${RUN:-$$(ls -1t tests/results/perf-tuning/ 2>/dev/null | head -1)}"; \
	if [ -z "$$_SRC" ] || [ ! -d "tests/results/perf-tuning/$$_SRC" ]; then \
		echo "No perf run to publish (RUN= not set and tests/results/perf-tuning/ empty)."; \
		echo "  Run: make perf-tuning-full"; exit 1; \
	fi; \
	_SHA=$$(cd tests/results/perf-tuning/$$_SRC && git rev-parse --short HEAD 2>/dev/null || echo "unknown"); \
	_DST="perf-history/$$_SRC-$$_SHA"; \
	echo "Publishing: tests/results/perf-tuning/$$_SRC → $$_DST/"; \
	mkdir -p "$$_DST"; \
	for f in summary.json SUMMARY.md ANALYSIS.md report_criterion.json env.json baseline_meta.json; do \
		if [ -f "tests/results/perf-tuning/$$_SRC/$$f" ]; then \
			cp "tests/results/perf-tuning/$$_SRC/$$f" "$$_DST/"; \
		fi; \
	done; \
	if [ -f "tests/results/perf-tuning/$$_SRC/hitrate/summary.json" ]; then \
		mkdir -p "$$_DST/hitrate"; \
		cp "tests/results/perf-tuning/$$_SRC/hitrate/summary.json" "$$_DST/hitrate/"; \
	fi; \
	for f in console-snap.json console-snap.txt tokio-metrics.json tokio-dump.txt; do \
		if [ -f "tests/results/perf-tuning/$$_SRC/$$f" ]; then \
			cp "tests/results/perf-tuning/$$_SRC/$$f" "$$_DST/"; \
		fi; \
	done; \
	echo ""; \
	echo "Pruning perf-history/ to most recent 100 runs…"; \
	_KEEP=100; _REMOVED=0; \
	for d in $$(ls -1t perf-history/ 2>/dev/null | tail -n +$$((_KEEP + 1))); do \
		if [ -d "perf-history/$$d" ]; then rm -rf "perf-history/$$d"; _REMOVED=$$((_REMOVED + 1)); fi; \
	done; \
	echo "  pruned $$_REMOVED runs (kept last $$_KEEP)"; \
	echo ""; \
	echo "To commit (never auto-commits per repo rule):"; \
	echo "  git add perf-history/"; \
	echo "  git commit -m 'perf(history): publish $$_SRC-$$_SHA'"

# ============================================================================
# Full Suite (all tests + benchmarks)
# ============================================================================

# Pre-build all binaries and test targets needed by the full suite.
test-all-prebuild:
	@echo ""
	@echo "=== Pre-building all conproxy targets ==="
	@echo "--- [1/2] Release binary (--release --features release) ---"
	cargo build --release --features release
	@echo "--- [2/2] Test binary + all dev targets (--features test) ---"
	cargo clippy --features test -- -D warnings
	cargo test --no-run --features test -q
	cargo build --bin test_runner --features test -q
	cargo test --no-run --test e2e_proxy --test e2e_eval --test e2e_uat --test e2e_load --features test,e2e -q
	cargo fmt
	@echo "All conproxy targets pre-built."

# Run everything: lint, unit tests, coverage, benchmarks, E2E proxy tests, HTTP load tests, eval
test-all: test-all-prebuild
	$(eval _RD := tests/results/$(shell date +%Y%m%d-%H%M%S)/$(_PROFILE_NAME))
	@mkdir -p $(_RD)
	@echo "Results directory: $(_RD)"
	@_FAIL=0; \
	$(MAKE) test-all-quality RESULTS_DIR=$(_RD) TEST_ALL_FEATURES=test || _FAIL=1; \
	$(MAKE) test-all-perf RESULTS_DIR=$(_RD) TEST_ALL_FEATURES=test || _FAIL=1; \
	cargo run --bin test_runner --features test -- index $(_RD); \
	echo ""; \
	echo "=========================================="; \
	echo "Full test suite complete!"; \
	echo "Results: $(_RD)"; \
	echo "  Open $(_RD)/index.html for a summary"; \
	echo "=========================================="; \
	exit $$_FAIL

# Lint + format check (fast quality gate)
test-all-lint:
	@mkdir -p $(RESULTS_DIR)/lint
	@date +%s > $(RESULTS_DIR)/lint/.start_time
	@echo ""
	@echo "=== Lint & Format Check ==="
	@{ \
		echo "--- cargo fmt --check ---"; \
		cargo fmt -- --check 2>&1; \
		FMT_RC=$$?; \
		echo ""; \
		echo "--- cargo clippy (test features) ---"; \
		cargo clippy --features $(or $(TEST_ALL_FEATURES),default) -- -D warnings 2>&1; \
		CLIPPY_RC=$$?; \
		echo ""; \
		if [ $$FMT_RC -eq 0 ]; then echo "fmt: PASS"; else echo "fmt: FAIL"; fi; \
		if [ $$CLIPPY_RC -eq 0 ]; then echo "clippy: PASS"; else echo "clippy: FAIL"; fi; \
	} | tee $(RESULTS_DIR)/lint/output.txt; \
	date +%s > $(RESULTS_DIR)/lint/.end_time; \
	! grep -q ": FAIL" $(RESULTS_DIR)/lint/output.txt

# Unit + integration tests
test-all-unit:
	@mkdir -p $(RESULTS_DIR)/unit
	@date +%s > $(RESULTS_DIR)/unit/.start_time
	@echo ""
	@echo "=== Unit + Integration Tests ==="
	set -o pipefail; cargo test --features $(or $(TEST_ALL_FEATURES),default) \
		--lib --bins --tests \
		2>&1 | tee $(RESULTS_DIR)/unit/output.txt
	@date +%s > $(RESULTS_DIR)/unit/.end_time

# Code coverage (tarpaulin + per-file threshold check)
test-all-coverage:
	@mkdir -p $(RESULTS_DIR)/coverage
	@date +%s > $(RESULTS_DIR)/coverage/.start_time
	@echo ""
	@echo "=== Code Coverage (tarpaulin) ==="
	cargo tarpaulin --features $(or $(TEST_ALL_FEATURES),default) --skip-clean \
		--lib --bins --tests \
		--out Html --out Json --output-dir $(RESULTS_DIR)/coverage 2>&1 \
		| tee $(RESULTS_DIR)/coverage/output.txt
	@echo ""
	@echo "Coverage report at $(RESULTS_DIR)/coverage/tarpaulin-report.json"
	@date +%s > $(RESULTS_DIR)/coverage/.end_time

# Security checks (cargo-audit + cargo-deny + lint-security)
test-all-security:
	@mkdir -p $(RESULTS_DIR)/security
	@date +%s > $(RESULTS_DIR)/security/.start_time
	@echo ""
	@echo "=== Security Checks ==="
	@{ \
		echo "--- cargo audit ---"; \
		cargo audit 2>&1; \
		AUDIT_RC=$$?; \
		echo ""; \
		echo "--- cargo deny check ---"; \
		cargo deny check 2>&1; \
		DENY_RC=$$?; \
		echo ""; \
		echo "--- clippy security lints ---"; \
		cargo clippy --features $(or $(TEST_ALL_FEATURES),default) -- -D warnings \
		  -W clippy::unwrap_used \
		  -W clippy::expect_used \
		  -W clippy::panic \
		  -W clippy::indexing_slicing \
		  -W clippy::arithmetic_side_effects 2>&1; \
		CLIPPY_RC=$$?; \
		echo ""; \
		if [ $$AUDIT_RC -eq 0 ]; then echo "audit: PASS"; else echo "audit: FAIL"; fi; \
		if [ $$DENY_RC -eq 0 ]; then echo "deny: PASS"; else echo "deny: FAIL"; fi; \
		if [ $$CLIPPY_RC -eq 0 ]; then echo "lint-security: PASS"; else echo "lint-security: FAIL"; fi; \
	} | tee $(RESULTS_DIR)/security/output.txt; \
	date +%s > $(RESULTS_DIR)/security/.end_time; \
	! grep -q ": FAIL" $(RESULTS_DIR)/security/output.txt

# Quality gate: lint + unit + coverage + security
test-all-quality:
	@_FAIL=0; \
	$(MAKE) test-all-lint RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	$(MAKE) test-all-unit RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	$(MAKE) test-all-coverage RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	$(MAKE) test-all-security RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	exit $$_FAIL

# Performance/E2E suite: bench + e2e + load + eval
test-all-perf:
	@_FAIL=0; _E2E_OK=1; \
	$(MAKE) test-all-bench RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	$(MAKE) test-all-e2e RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || { _FAIL=1; _E2E_OK=0; }; \
	if [ $$_E2E_OK -eq 1 ]; then \
		echo ""; \
		echo "=== E2E Eval Tests (e2e proxy tests passed) ==="; \
		echo "Waiting for port 8080 to be released..."; \
		for _i in $$(seq 1 10); do ss -tlnp 2>/dev/null | grep -q ':8080 ' || break; sleep 1; done; \
		$(MAKE) test-all-eval RESULTS_DIR=$(RESULTS_DIR) TEST_ALL_FEATURES=$(or $(TEST_ALL_FEATURES),test) || _FAIL=1; \
	else \
		echo ""; \
		echo "SKIP: eval tests — e2e proxy tests failed"; \
		mkdir -p $(RESULTS_DIR)/eval; \
		echo "SKIP: eval tests — e2e proxy tests failed" > $(RESULTS_DIR)/eval/output.txt; \
	fi; \
	$(MAKE) e2e-services-down 2>/dev/null; true; \
	exit $$_FAIL

# Criterion benchmarks — both targets. PIPESTATUS propagates cargo's real exit code
# through the pipe to make (without it, a build failure is masked by `tee`).
test-all-bench:
	@mkdir -p $(RESULTS_DIR)/bench
	@date +%s > $(RESULTS_DIR)/bench/.start_time
	@echo ""
	@echo "=== Criterion Benchmarks (core_ops) ==="
	cargo bench --bench core_ops 2>&1 | tee $(RESULTS_DIR)/bench/bench_core_ops.txt; 
	@echo "=== Criterion Benchmarks (cascade_scope) ==="
	cargo bench --bench cascade_scope 2>&1 | tee $(RESULTS_DIR)/bench/bench_cascade_scope.txt; 
	@date +%s > $(RESULTS_DIR)/bench/.end_time

# E2E tests (Docker services started here, kept alive for eval)
test-all-e2e:
	@mkdir -p $(RESULTS_DIR)/e2e $(RESULTS_DIR)/load
	@date +%s > $(RESULTS_DIR)/e2e/.start_time
	@echo ""
	@echo "=== E2E Tests (starting Docker services) ==="
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait TEST_ALL_FEATURES=$(TEST_ALL_FEATURES)
	@echo "Verifying backends are queryable..."
	@curl -sf http://localhost:9200/_cluster/health | grep -q '"status"' \
		|| { echo "ERROR: Elasticsearch at :9200 not responding"; exit 1; }
	@curl -sf http://localhost:6333/readyz >/dev/null \
		|| { echo "ERROR: Qdrant at :6333 not responding"; exit 1; }
	@echo "  Backends verified."
	$(MAKE) e2e-load-data TEST_ALL_FEATURES=$(TEST_ALL_FEATURES)
	@echo ""
	@echo "--- E2E Proxy Tests ---"
	@E2E_PROFILE=1 CONPROXY_DHAT=1 RUST_LOG=info E2E_SUITE=all E2E_OUTPUT_DIR=$(RESULTS_DIR)/e2e PROXY_BIN="$(TEST_BIN)" \
		cargo test --test e2e_proxy --features $(TEST_ALL_FEATURES),e2e -- --ignored --nocapture --test-threads=1 e2e_proxy_suite 2>&1 | tee $(RESULTS_DIR)/e2e/output.txt; \
	_E2E_RC=$$?; \
	exit $$_E2E_RC
	@date +%s > $(RESULTS_DIR)/e2e/.end_time
	@date +%s > $(RESULTS_DIR)/load/.start_time
	@echo ""
	@echo "--- Load Tests (rlt, gRPC + HTTP) ---"
	@mkdir -p .conproxy; \
	cp $(E2E_PROXY_DIR)/configs/single-elasticsearch.toml .conproxy/conproxy.toml; \
	_READY_FILE="$(CURDIR)/$(RESULTS_DIR)/load/.proc_monitor_ready"; \
	rm -f "$$_READY_FILE"; \
	CONPROXY_DHAT=1 RUST_LOG=info $(TEST_BIN) start --listen 127.0.0.1:8080 2>$(CURDIR)/$(RESULTS_DIR)/load/proxy_logs.txt & \
	_PID=$$!; \
	kill -STOP $$_PID 2>/dev/null; \
	$(TEST_RUNNER_BIN) proc-monitor \
		--pid $$_PID --output-dir $(CURDIR)/$(RESULTS_DIR)/load --perf --bpftrace --dhat --dhat-search-dir $(CURDIR) \
		$(if $(RESOURCE_PROFILE),--resource-profile $(RESOURCE_PROFILE)) \
		--ready-file "$$_READY_FILE" & \
	_MON_PID=$$!; \
	for _i in $$(seq 1 60); do \
		if [ -f "$$_READY_FILE" ]; then break; fi; \
		if ! kill -0 $$_MON_PID 2>/dev/null; then \
			echo "WARNING: proc-monitor exited before writing ready-file"; \
			break; \
		fi; \
		sleep 0.1; \
	done; \
	kill -CONT $$_PID 2>/dev/null; \
	_READY=0; \
	for _i in $$(seq 1 40); do \
		if ! kill -0 $$_PID 2>/dev/null; then \
			echo "ERROR: proxy exited before becoming healthy"; \
			break; \
		fi; \
		if curl -sf http://127.0.0.1:8081/health >/dev/null 2>&1; then \
			_READY=1; break; \
		fi; \
		sleep 0.25; \
	done; \
	if [ "$$_READY" = "1" ]; then \
		PROXY_URL=http://localhost:8081 GRPC_URL=http://localhost:8080 BENCH_OUTPUT_DIR=$(CURDIR)/$(RESULTS_DIR)/load DURATION=10 \
			cargo test --test e2e_load --features $(TEST_ALL_FEATURES),e2e 2>&1 \
			| tee $(CURDIR)/$(RESULTS_DIR)/load/output.txt; \
	else \
		echo "ERROR: proxy at 127.0.0.1:8081 did not become healthy within 10s" \
			| tee $(CURDIR)/$(RESULTS_DIR)/load/output.txt; \
	fi; \
	kill -INT $$_PID 2>/dev/null; wait $$_PID 2>/dev/null; \
	wait $$_MON_PID 2>/dev/null; \
	rm -f .conproxy/conproxy.toml "$$_READY_FILE"
	@date +%s > $(RESULTS_DIR)/load/.end_time

# E2E eval tests (Docker services must already be running)
test-all-eval:
	@mkdir -p $(RESULTS_DIR)/eval
	@date +%s > $(RESULTS_DIR)/eval/.start_time
	@echo ""
	@if ! curl -sf http://localhost:11434/api/tags >/dev/null 2>&1 && [ -z "$$EVAL_PROVIDER" ]; then \
		echo "SKIP: Ollama not running — skipping eval tests (set EVAL_PROVIDER=claude to use Claude)"; \
	else \
		E2E_PROFILE=1 CONPROXY_DHAT=1 EVAL_OUTPUT_DIR="$(RESULTS_DIR)/eval" PROXY_BIN="$(TEST_BIN)" \
			cargo test --test e2e_eval --features $(TEST_ALL_FEATURES),e2e -- --ignored --nocapture 2>&1 | tee $(RESULTS_DIR)/eval/output.txt; \
		_EVAL_RC=$$?; \
		exit $$_EVAL_RC; \
	fi
	@date +%s > $(RESULTS_DIR)/eval/.end_time

# ============================================================================
# E2E Proxy Testing
# ============================================================================

E2E_PROXY_DIR := tests/e2e

# Rust E2E test command (tests assume Docker services + proxy are up)
E2E_CARGO_CMD = PROXY_BIN="$(CURDIR)/target/release/conproxy" cargo test --test e2e_proxy --features e2e -- --ignored --nocapture

# Run all proxy E2E tests (requires Docker)
e2e-all: build-release
	@echo "Running all proxy E2E tests..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=all E2E_OUTPUT_DIR="$(E2E_PROXY_DIR)/results/$(shell date +%Y%m%d-%H%M%S)" $(E2E_CARGO_CMD)
	$(MAKE) e2e-services-down

# Run e2e tests against the live k8s cluster (kind + helm-installed conproxy).
# Requires:
#   - kind cluster up (./scripts/kind-up.sh)
#   - conproxy deployed via helm to kind (make k8s-deploy or `tilt up`)
#   - backends up on host (docker compose -f tests/e2e/docker-compose.yml up -d)
#   - corpus seeded (cargo run --bin corpus_seed --features embed,pgvector -- --corpus all --host http://localhost)
#   - port-forward svc/conproxy (e.g. `kubectl port-forward svc/conproxy 10000:10000 &`)
# Then runs cargo test --test e2e_proxy with all backend URLs + E2E_EXTERNAL_PROXY=1.
e2e-k8s:
	@PROXY_URL="$${PROXY_URL:-http://127.0.0.1:10000}" \
	QDRANT_URL="$${QDRANT_URL:-http://localhost:6333}" \
	ELASTIC_URL="$${ELASTIC_URL:-http://localhost:9200}" \
	OPENSEARCH_URL="$${OPENSEARCH_URL:-http://localhost:9201}" \
	MEILI1_URL="$${MEILI1_URL:-http://localhost:7700}" \
	MEILI2_URL="$${MEILI2_URL:-http://localhost:7701}" \
	PGVECTOR_URL="$${PGVECTOR_URL:-postgres://postgres:postgres@localhost:5432/conproxy_test}" \
	E2E_EXTERNAL_PROXY=1 \
	E2E_SUITE="$${E2E_SUITE:-all}" \
	$(CURDIR)/scripts/e2e-k8s.sh

# ---------------------------------------------------------------------------
# Dev lifecycle (kind + backends + tilt + seed)
# ---------------------------------------------------------------------------

# Tear down the full dev stack (tilt, kind, backends, free ports)
dev-down:
	@echo "Tearing down dev stack..."
	$(CURDIR)/scripts/dev-down.sh

# Start dev stack (kind → tilt up). Backends are auto-started by Tilt.
dev-up:
	@echo "Starting dev stack..."
	$(CURDIR)/scripts/dev-up.sh

# Full restart: teardown → fresh kind → backends → seed → tilt up
dev-restart:
	@echo "Restarting dev stack..."
	$(CURDIR)/scripts/dev-restart.sh

# ---------------------------------------------------------------------------
# DevEx: opencode-test container + auto-smoke (fresh session per container)
# ---------------------------------------------------------------------------

# Run the DevEx auto-smoke against the live cluster + opencode-test container.
# Mints a new session if no SID is set; otherwise continues the sticky one
# for the current container lifetime. The opencode session DB is in-container
# only — every container recreate starts with an empty session list and the
# host sticky SID is cleared. Re-runnable. Default model is
# opencode/big-pickle (free built-in, no key). Override with
# DEVEX_MODEL=opencode/<other> to swap.
devex:
	@echo "Running DevEx auto-smoke against live cluster..."
	@./scripts/devex-session.sh ensure
	@DEVEX_OPENCODE_PORT=$${DEVEX_OPENCODE_PORT:-14096} \
	 DEVEX_MODEL=$${DEVEX_MODEL:-opencode/big-pickle} \
	 ./scripts/devex-smoke.sh

# Attach the human TUI to the current DevEx session (in-container only;
# fails after a container recreate until a new smoke mints one).
devex-attach:
	@./scripts/devex-session.sh ensure
	@./scripts/devex-session.sh attach-cmd | bash

# Print the current DEVEX_SESSION + the last smoke result.
devex-status:
	@./scripts/devex-session.sh status

# Mint a fresh DevEx session on the next smoke.
devex-new:
	@./scripts/devex-session.sh clear
	@echo "Next 'make devex' (or Tilt devex-smoke) will mint a new session."

# Print the DevEx attach banner (used by docs / shell startup).
devex-banner:
	@./scripts/devex-session.sh banner

# Run all proxy E2E tests in dirty mode (no cleanup, for manual inspection)
e2e-dirty: build-release
	@echo "Running all proxy E2E tests (dirty mode)..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=all $(E2E_CARGO_CMD) || true
	@echo "Services still running. Use 'make e2e-services-down' to clean up."

# Run Qdrant-only E2E tests
e2e-qdrant: build-release
	@echo "Running Qdrant E2E tests..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=qdrant $(E2E_CARGO_CMD)
	$(MAKE) e2e-services-down

# Run Meilisearch E2E tests (Suite::Elastic = Meilisearch fixtures, name kept for env compat)
e2e-elastic: build-release
	@echo "Running Meilisearch E2E tests..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=elastic $(E2E_CARGO_CMD)
	$(MAKE) e2e-services-down

e2e-meili: e2e-elastic

# Run mixed upstream E2E tests
e2e-mixed: build-release
	@echo "Running mixed upstream E2E tests..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=mixed $(E2E_CARGO_CMD)
	$(MAKE) e2e-services-down

# Run E2E tests filtered by category
e2e-filter: build-release
	@echo "Running proxy E2E tests (filter: $(FILTER))..."
	E2E_SUITE=all E2E_FILTER=$(FILTER) $(E2E_CARGO_CMD)

# Quick smoke test (requires running proxy)
e2e-smoke: build-release
	@echo "Running smoke test..."
	$(E2E_CARGO_CMD) e2e_smoke_test

# Run proxy load benchmarks (rlt, gRPC + HTTP)
e2e-bench: build-release
	@echo "Running proxy load benchmarks..."
	PROXY_URL=http://localhost:8081 GRPC_URL=http://localhost:8080 cargo test --test e2e_load --features load-test,e2e --release

# Start E2E test services
e2e-services-up:
	@echo "Starting E2E test services..."
	@if [ ! -d "$(E2E_PROXY_DIR)" ]; then echo "ERROR: E2E infra dir not found at $(E2E_PROXY_DIR)"; exit 1; fi
	cd $(E2E_PROXY_DIR) && docker compose up -d

# Stop E2E test services
e2e-services-down:
	@echo "Stopping E2E test services..."
	@if [ -d "$(E2E_PROXY_DIR)" ]; then cd $(E2E_PROXY_DIR) && docker compose down -v; fi

# Wait for E2E services to be ready
e2e-wait:
	cargo run --bin test_runner --features $(or $(TEST_ALL_FEATURES),default) -- wait all

# Load test data into services
e2e-load-data:
	@echo "Loading test data..."
	cargo run --bin test_runner --features $(or $(TEST_ALL_FEATURES),default) -- load-data

# Generate embeddings for test data
e2e-generate-embeddings:
	@echo "Generating embeddings..."
	cargo run --bin generate_embeddings --features embed

# View latest E2E results
e2e-results:
	@latest=$$(ls -td $(E2E_PROXY_DIR)/results/*/ 2>/dev/null | head -1); \
	if [ -z "$$latest" ]; then echo "No results found. Run 'make e2e-all' first."; exit 1; fi; \
	echo "Latest results: $$latest"; \
	echo ""; \
	jq '.' "$$latest/results.json"

# Clean proxy E2E artifacts
e2e-proxy-clean:
	cd $(E2E_PROXY_DIR) && docker compose down -v 2>/dev/null || true
	rm -rf $(E2E_PROXY_DIR)/data/embeddings.json
	rm -rf $(E2E_PROXY_DIR)/data/query_embeddings.json
	rm -rf $(E2E_PROXY_DIR)/data/*_embeddings.json
	rm -rf $(E2E_PROXY_DIR)/results

# Generate markdown report from latest E2E results
e2e-report:
	@latest=$$(ls -td $(E2E_PROXY_DIR)/results/*/ 2>/dev/null | head -1); \
	if [ -z "$$latest" ]; then echo "No results found. Run 'make e2e-all' first."; exit 1; fi; \
	echo "Generating report from: $$latest"; \
	cargo run --bin test_runner -- report \
		--input "$$latest/results.json" \
		--output "$$latest/report.md" \
		--html "$$latest/report.html"; \
	echo "Report written to: $$latest/report.md"

# Run E2E benchmark comparison against previous results
e2e-bench-compare:
	@results=($$(ls -td $(E2E_PROXY_DIR)/results/*/ 2>/dev/null)); \
	if [ $${#results[@]} -lt 2 ]; then \
		echo "Need at least 2 result sets to compare. Run 'make e2e-all' twice."; exit 1; \
	fi; \
	current=$${results[0]}; \
	previous=$${results[1]}; \
	echo "Comparing:"; \
	echo "  Current:  $$current"; \
	echo "  Previous: $$previous"; \
	cargo run --bin test_runner -- report \
		--input "$$current/results.json" \
		--compare "$$previous/results.json" \
		--output "$$current/comparison.md" \
		--html "$$current/comparison.html"; \
	echo ""; \
	echo "Comparison report written to: $$current/comparison.md"

# ============================================================================
# Eval helpers (llama-server direct — not passthrough)
# ============================================================================

# Check that llama-server is running and /v1/models responds (for eval-llamacpp).
llm-server-check:
	@echo "Checking llama-server at $$LLAMA_BASE_URL ..."; \
	url=$${LLAMA_BASE_URL:-http://127.0.0.1:8081}/v1/models; \
	if curl -sf "$$url" > /dev/null 2>&1; then \
		echo "  llama-server: OK"; \
	else \
		echo "  FAIL: llama-server not reachable at $$url"; \
		echo "  Ensure llama-server is running."; \
		echo "  Default: llama-server -m models/llm.gguf --port 8081 --host 127.0.0.1 -c 2048 --jinja"; \
		exit 1; \
	fi


# E2E Eval with llama.cpp provider (hard fail, no soft skip).
eval-llamacpp: build-release
	$(MAKE) llm-server-check
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	EVAL_PROVIDER=llamacpp \
	EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" \
	PROXY_BIN="$(CURDIR)/target/release/conproxy" \
	    cargo test --test e2e_eval --features e2e -- --ignored --nocapture
	$(MAKE) e2e-services-down

# ============================================================================
# E2E Eval Testing (Ollama/Claude LLM vertical comparison)
# ============================================================================

E2E_EVAL_CMD = PROXY_BIN="$(CURDIR)/target/release/conproxy" \
    cargo test --test e2e_eval --features e2e -- --ignored --nocapture

eval-all: build-release
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)
	$(MAKE) e2e-services-down

eval-quick: build-release
	EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)

eval-vertical: build-release
	EVAL_VERTICALS=$(V) EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)

eval-queries: build-release
	EVAL_QUERIES=$(Q) EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)

eval-cheap: build-release
	EVAL_VERTICALS=no_context,mcp_tools EVAL_QUERIES=q-001,q-004,q-003 EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)

eval-results:
	@latest=$$(ls -td tests/e2e_eval/results/*/ 2>/dev/null | head -1); \
	[ -z "$$latest" ] && echo "No results. Run 'make eval-all' first." && exit 1; \
	jq '.' "$$latest/eval_results.json"

eval-clean:
	rm -rf tests/e2e_eval/results /tmp/conproxy-eval

# ============================================================================
# UAT Testing (CLI user acceptance tests)
# ============================================================================

uat: build-release
	PROXY_BIN="$(CURDIR)/target/release/conproxy" \
	    cargo test --test e2e_uat --features e2e -- --ignored --nocapture

uat-quick:
	PROXY_BIN="$(CURDIR)/target/release/conproxy" \
	    cargo test --test e2e_uat --features e2e -- --ignored --nocapture

# ============================================================================
# Profiled E2E/Eval Runs (resource monitoring via /proc)
# ============================================================================

e2e-profile: build-release
	@echo "Running E2E proxy tests with resource profiling..."
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_SUITE=all E2E_PROFILE=1 E2E_OUTPUT_DIR="$(E2E_PROXY_DIR)/results/$(shell date +%Y%m%d-%H%M%S)" $(E2E_CARGO_CMD)
	$(MAKE) e2e-services-down

eval-profile: build-release
	$(MAKE) e2e-services-up
	$(MAKE) e2e-wait
	$(MAKE) e2e-load-data
	E2E_PROFILE=1 EVAL_OUTPUT_DIR="tests/e2e_eval/results/$$(date +%Y%m%d-%H%M%S)" $(E2E_EVAL_CMD)
	$(MAKE) e2e-services-down

# ============================================================================
# Security
# ============================================================================

# cargo-deny: supply chain + license + advisory DB checks
security-deny:
	cargo deny check

# cargo-audit: standalone RustSec advisory scanner
audit:
	cargo audit

# Clippy with security-focused lints (separate from regular lint)
lint-security:
	cargo clippy -- -D warnings \
	  -W clippy::unwrap_used \
	  -W clippy::expect_used \
	  -W clippy::panic \
	  -W clippy::indexing_slicing \
	  -W clippy::arithmetic_side_effects

# Surface current pre-existing warning inventory (for SKILL.md Known Gaps refresh)
audit-known-gaps:
	@echo "=== 1. Pre-existing clippy unwrap/expect warnings ==="
	@cargo clippy -- -W clippy::unwrap_used,clippy::expect_used 2>&1 \
		| grep -E "warning: .*(unwrap|expect|used)" | wc -l \
		| xargs -I{} echo "count: {}" || echo "count: 0"
	@echo
	@echo "=== 2. Unallowed clippy warnings (should be 0) ==="
	@cargo clippy -- -D warnings 2>&1 \
		| grep -E "^(warning|error)" || echo "clean"
	@echo
	@echo "=== 3. Bench compile (passes/fails?) ==="
	@cargo bench --no-run 2>&1 | tail -3
	@echo
	@echo "Run this target when Known Gaps section needs refreshing."
	@echo "Line refs and counts drift as code moves; this target is authoritative."

# Security-focused E2E tests (auth bypass, rate limiting, payload abuse, header injection)
e2e-security: build-release
	E2E_SUITE=security E2E_FILTER=security PROXY_BIN="$(CURDIR)/target/release/conproxy" \
		cargo test --test e2e_proxy --features e2e -- --ignored --nocapture

# SBOM generation (CycloneDX format)
sbom:
	cargo cyclonedx --format json --output-file conproxy-sbom.json

# cargo-geiger: scan dependency tree for unsafe usage
unsafe-audit:
	cargo geiger --output-format ascii 2>&1 | tee unsafe-report.txt

# cargo-mutants: mutation testing on security-critical modules
mutant-security:
	cargo mutants -F 'src/proxy/middleware.rs' -F 'src/proxy/sandbox.rs' -F 'src/proxy/agent.rs'

# Fuzz targets
fuzz-query:
	cargo fuzz run fuzz_query_request -- -max_len=4096 -max_total_time=60

fuzz-config:
	cargo fuzz run fuzz_config_parse -- -max_len=8192 -max_total_time=60

fuzz-all:
	@for target in $$(cargo fuzz list); do \
		echo "Fuzzing $$target for 60s..."; \
		timeout 60 cargo fuzz run $$target -- -max_len=4096 || true; \
	done

# Fast security gate (< 2 min, run on every PR)
security-quick: audit security-deny lint-security

# Full security suite (run nightly/weekly)
security-full: security-quick sbom unsafe-audit fuzz-all e2e-security

# ============================================================================
# Clean
# ============================================================================

clean:
	cargo clean

# ============================================================================
# Help
# ============================================================================

help:
	@echo "conproxy Makefile"
	@echo ""
	@echo "Build targets:"
	@echo "  build             - Debug build"
	@echo "  build-release     - Release build"
	@echo "  build-embed       - Release with embedding feature"
	@echo "  build-persistence - Build with persistence feature"
	@echo "  build-pgvector    - Build with pgvector feature"
	@echo "  build-mcp         - Release with MCP server"
	@echo "  build-all         - Release with all standard features"
	@echo "  build-profiling   - Profiling build (release + debug symbols + dhat)"
	@echo "  docker-build      - Build conproxy:dev (Tilt default)"
	@echo "  docker-push V= R= - Versioned push (V=0.1.0 R=ghcr.io/me)"
	@echo "  docker-buildx     - Local multi-arch build (PLATFORMS=...)"
	@echo ""
	@echo "Test targets:"
	@echo "  test              - Run all tests (unit)"
	@echo "  test-unit         - Run unit tests only"
	@echo "  t                 - Fast local loop (nextest if installed, else cargo test --lib)"
	@echo "  test-fast         - Alias for t"
	@echo "  test-nextest      - Force cargo-nextest (dev profile)"
	@echo "  test-slow         - Show top-25 slowest tests (nextest)"
	@echo "  test-filter PAT=  - Run tests matching PAT"
	@echo "  nextest-install   - One-time install of cargo-nextest"
	@echo "  target-prune      - Drop conproxy-only build artifacts"
	@echo "  test-verbose      - Run tests with output"
	@echo "  test-coverage     - Generate coverage report (HTML)"
	@echo "  test-coverage-quick - Quick coverage to stdout"
	@echo "  test-coverage-check - Check per-file coverage threshold (80%)"
	@echo "  test-one TEST=name - Run specific test"
	@echo ""
	@echo "Proxy targets:"
	@echo "  proxy-start       - Start the proxy server"
	@echo "  proxy-stop        - Stop the proxy server"
	@echo "  proxy-status      - Show proxy status"
	@echo ""
	@echo "Quality targets:"
	@echo "  fmt               - Format code"
	@echo "  fmt-check         - Check formatting"
	@echo "  lint              - Run clippy"
	@echo "  lint-fix          - Auto-fix lint issues"
	@echo ""
	@echo "Benchmark targets:"
	@echo "  bench             - Run benchmarks"
	@echo "  bench-save        - Save baseline for comparison"
	@echo "  bench-compare     - Compare against saved baseline + report"
	@echo "  bench-hitrate     - Cache hit-rate benchmark (agentic + Zipf traces)"
	@echo "  bench-hitrate-mem - Count-cap vs byte-cap HR (mixed sizes)"
	@echo "  bench-hitrate-pressure - Starve/scan/flip/elephants/agentic-tight (S3-FIFO)"
	@echo "  bench-hitrate-sem - Hit-rate + semantic τ frontier (embed-api; ~6 min)"
	@echo "  bench-hitrate-onnx - Semantic τ frontier with live ONNX embedder (embed)"
	@echo "  bench-hitrate-live - Live wire mode vs real proxy + qdrant (docker + embed)"
	@echo "  bench-hitrate-replay - Bring-your-own-trace replay: QUERIES=path/to/trace.txt [POOL=1000 CACHE_SIZE=10000 MODE=exact TTL=600]"
	@echo ""
	@echo "E2E Proxy Testing targets:"
	@echo "  e2e-all           - Run all proxy E2E tests (Docker required)"
	@echo "  e2e-dirty         - Run all tests, keep services running"
	@echo "  e2e-qdrant        - Run Qdrant-only tests"
	@echo "  e2e-elastic       - Run Meilisearch tests (historical name; config single-elasticsearch.toml)"
	@echo "  e2e-meili         - Alias for e2e-elastic"
	@echo "  e2e-mixed         - Run mixed upstream tests"
	@echo "  e2e-filter FILTER=cat - Run by category"
	@echo "  e2e-smoke         - Quick smoke test (proxy must be running)"
	@echo "  e2e-smoke-core    - Docker up + load + smoke/health/query filter"
	@echo "  e2e-cascade       - Docker up + load + cascade filter (mixed suite)"
	@echo "  e2e-federated     - Federated category (mock upstream, no compose)"
	@echo "  test-integration  - Full core testcontainers matrix (Docker)"
	@echo "  test-integration-experimental - pinecone/milvus mock tests"
	@echo "  e2e-bench         - Run proxy load benchmarks (rlt, gRPC + HTTP)"
	@echo "  e2e-services-up   - Start Docker test services"
	@echo "  e2e-services-down - Stop Docker test services"
	@echo "  e2e-load-data     - Load test data into services"
	@echo "  e2e-results       - View latest E2E test results"
	@echo "  e2e-report        - Generate markdown report from latest results"
	@echo "  e2e-bench-compare - Compare latest two E2E result sets"
	@echo "  e2e-proxy-clean   - Clean proxy E2E artifacts + results"
	@echo ""
	@echo "Eval helpers (direct llama.cpp — not passthrough):"
	@echo "  llm-server-check  - Verify llama-server is running (hard fail)"
	@echo "  eval-llamacpp     - Run eval suite with llama.cpp provider (hard fail)"
	@echo ""
	@echo "E2E Eval Testing targets (Ollama/Claude LLM vertical comparison):"
	@echo "  eval-all          - Full eval: services up, load data, all verticals, down"
	@echo "  eval-quick        - All verticals, assumes Docker services already running"
	@echo "  eval-vertical V=name - Single vertical (no_context, knowledge_only, mcp_tools)"
	@echo "  eval-queries Q=ids - Query subset (q-001,q-003,...)"
	@echo "  eval-cheap        - 2 verticals x 3 queries (quick Ollama eval)"
	@echo "  eval-results      - View latest JSON results"
	@echo "  eval-clean        - Remove eval results and /tmp/conproxy-eval/"
	@echo ""
	@echo "UAT Testing targets (CLI user acceptance tests):"
	@echo "  uat               - Run CLI UAT tests (builds release binary first)"
	@echo "  uat-quick         - Run CLI UAT tests (assumes binary already built)"
	@echo ""
	@echo "Profiled runs (resource monitoring via /proc):"
	@echo "  e2e-profile       - E2E proxy tests + process resource profiling"
	@echo "  eval-profile      - E2E eval tests + process resource profiling"
	@echo "  profile-dhat      - DHAT heap profile (requires Qdrant running)"
	@echo "  profile-pgo       - PGO build (release + instrumentation + workload)"
	@echo "  profile-flamegraph - CPU flamegraph (requires flamegraph or samply)"
	@echo "  profile-tokio-console - Tokio async task console (CLI client; default bind 127.0.0.1:6669)"
	@echo "  profile-heaptrack     - Heap timeline profile (optional heaptrack tool; needs apt install heaptrack)"
	@echo "  profile-metrics-snap  - Prometheus metrics snapshot under load (writes /tmp/conproxy-metrics/metrics-prometheus.txt)"
	@echo ""
	@echo "Perf tuning (structured measure → summarize → plan):"
	@echo "  perf-tuning-quick   - Bench both targets (--baseline main) + summarize (fast feedback)"
	@echo "  perf-tuning-default - Quick + metrics-snap (CI-aware verdict + parsed metrics)"
	@echo "  perf-tuning-full    - Default + flamegraph + hitrate + heaptrack + tokio-console (env off-switches: HITRATE=0 HEAPTRACK=0 TOKIO_CONSOLE=0 FLAMEGRAPH=0 DHAT=1)"
	@echo "  perf-tuning-clean   - Remove tests/results/perf-tuning/ (gitignored)"
	@echo "  perf-publish        - Copy latest run artifacts to perf-history/<ts>-<sha8>/ (tracked, max 100 runs; prints git add cmd)"
	@echo ""
	@echo "Full suite (results in tests/results/<timestamp>/<profile>/):"
	@echo "  test-all          - Run ALL: quality + perf (sequential)"
	@echo "  test-all-quality  - Lint + unit + coverage"
	@echo "  test-all-perf     - Bench + e2e + load + eval"
	@echo "  test-all-prebuild - Pre-build all binaries and test targets"
	@echo ""
	@echo "Security targets:"
	@echo "  audit             - Run cargo-audit (RustSec advisory scan)"
	@echo "  security-deny     - Run cargo-deny (supply chain + license checks)"
	@echo "  lint-security     - Clippy with security-focused lints"
	@echo "  audit-known-gaps  - Surface current pre-existing warning inventory (for SKILL.md refresh)"
	@echo "  e2e-security      - Security-focused E2E tests (auth, rate limit, payload)"
	@echo "  sbom              - Generate SBOM (CycloneDX JSON)"
	@echo "  unsafe-audit      - Scan dependency tree for unsafe usage (cargo-geiger)"
	@echo "  mutant-security   - Mutation testing on security-critical modules"
	@echo "  fuzz-query        - Fuzz QueryRequest deserialization (60s)"
	@echo "  fuzz-config       - Fuzz config TOML parsing (60s)"
	@echo "  fuzz-all          - Fuzz all targets (60s each)"
	@echo "  security-quick    - Fast security gate: audit + deny + lint-security"
	@echo "  security-full     - Full suite: quick + sbom + geiger + fuzz + e2e-security"
	@echo ""
	@echo "Dev lifecycle (kind + Tilt + backends):"
	@echo "  dev-up            - Start: kind → tilt up (foreground)"
	@echo "  dev-down          - Stop: tilt down → free ports → kind-down → compose down -v"
	@echo "  dev-restart       - Full restart: teardown → fresh kind → backends → seed → tilt up"
	@echo "  devex             - Run DevEx auto-smoke (opencode MCP smoke) against live cluster"
	@echo "  devex-attach      - Exec TUI into opencode-test container with current session"
	@echo "  devex-status      - Print DEVEX_SESSION + last smoke result"
	@echo "  devex-new         - Mint a fresh DevEx session on next smoke"
	@echo "  devex-banner      - Print the DevEx handoff banner"
	@echo ""
	@echo "Feature flags: test (load-test+dhat-heap), embed, persistence, pgvector, mcp, linux-sandbox"
	@echo ""
	@echo "Quality gates (plan 05):"
	@echo "  cov-scope-tune    - Coverage gate: scope + tune ≥80% line (tarpaulin)"
	@echo "  proof-cascade     - Cascade + federated unit proofs (no Docker)"
	@echo "  sdk-smoke         - Python SDK: maturin build + import conproxy"
	@echo ""
	@echo "Other targets:"
	@echo "  clean             - Clean build artifacts"
	@echo "  help              - Show this help"