-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
817 lines (687 loc) · 39.6 KB
/
Copy pathMakefile
File metadata and controls
817 lines (687 loc) · 39.6 KB
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
# ============================================================================
# Finima — Root Makefile
# ============================================================================
# Full-stack financial intelligence platform.
# Rust backend + React/Vite frontend.
#
# Quick Start:
# make help - Show all available targets
# make install - Install all dependencies
# make start - Start everything (infra + backend + frontend)
# make dev - Start backend + frontend (assumes infra running)
# make docker-infra - Start dev infrastructure
# make ci - Run full CI pipeline
# ============================================================================
# ============================================================================
# Variables and Configuration
# ============================================================================
SHELL := /bin/bash
.DEFAULT_GOAL := help
# Load .env if present so Docker Compose targets and Make-level variables
# (e.g. POSTGRES_PASSWORD for docker-compose.yml substitution) are available.
# The backend also loads .env itself via dotenvy, so APP__* vars work
# regardless of whether the user starts via Make or cargo run directly.
# Values must be bare (unquoted) — both Make and Docker Compose read literally.
ifneq (,$(wildcard ./.env))
include .env
export
endif
BACKEND_DIR := .
FRONTEND_DIR := frontend
COMPOSE := docker compose
# Auto-detect hardware for GPU acceleration
HAS_NVIDIA := $(shell command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1 && echo 1 || echo 0)
HAS_METAL := $(shell xcrun -find metal >/dev/null 2>&1 && echo 1 || echo 0)
GPU_OVERLAY := $(if $(filter 1,$(HAS_NVIDIA)), -f docker-compose.gpu.yml,)
COMPOSE_DEV := $(COMPOSE) -f docker-compose.yml$(GPU_OVERLAY)
COMPOSE_PROD := $(COMPOSE) -f docker-compose.prod.yml$(GPU_OVERLAY)
COMPOSE_TEST := $(COMPOSE) -f docker-compose.test.yml
COMPOSE_OBS := $(COMPOSE) -f docker-compose.yml -f docker-compose.observability.yml$(GPU_OVERLAY)
# ── LLM backend selection ──────────────────────────────────────
# Set LLM= to control which AI backend is compiled and which Docker
# services are started. Valid values:
# candle – in-process inference (CPU)
# candle-metal – in-process inference (Apple Metal GPU)
# candle-cuda – in-process inference (NVIDIA CUDA GPU)
# ollama – HTTP inference via Ollama container
# none – no LLM; categorization uses Tiers 0-2 only
#
# Default: none (no LLM — categorization uses Tiers 0-2 only).
# Override with: make start LLM=ollama or make start LLM=candle
LLM ?= none
# Auto-promote bare "candle" to the best accelerator for this machine.
ifeq ($(LLM),candle)
ifeq ($(HAS_NVIDIA),1)
override LLM := candle-cuda
else ifeq ($(HAS_METAL),1)
override LLM := candle-metal
endif
endif
# Derive Cargo feature flags from LLM choice.
ifeq ($(LLM),ollama)
CARGO_LLM_FEATURES := --features ollama
else ifeq ($(LLM),candle-metal)
CARGO_LLM_FEATURES := --features candle,metal
else ifeq ($(LLM),candle-cuda)
CARGO_LLM_FEATURES := --features candle,cuda
else ifeq ($(LLM),candle)
CARGO_LLM_FEATURES := --features candle
else
CARGO_LLM_FEATURES :=
endif
# ── Embedder backend selection ────────────────────────────────
# Set EMBEDDER= to control which local embedding backend is compiled
# and used for Tier 2 / flow-pattern vectors. Valid values mirror
# LLM= — no external / paid providers.
# candle – in-process inference (CPU)
# candle-metal – in-process inference (Apple Metal GPU)
# candle-cuda – in-process inference (NVIDIA CUDA GPU)
# ollama – HTTP inference via Ollama container
# none – no embedder; Tier 2 uses Jaccard / BYO vectors
#
# Default: inherits from LLM so `make start LLM=ollama` also gives
# you the Ollama embedder without extra flags. Override with
# `make start LLM=ollama EMBEDDER=none` to opt out.
EMBEDDER ?= $(LLM)
# Auto-promote bare "candle" the same way LLM does.
ifeq ($(EMBEDDER),candle)
ifeq ($(HAS_NVIDIA),1)
override EMBEDDER := candle-cuda
else ifeq ($(HAS_METAL),1)
override EMBEDDER := candle-metal
endif
endif
# Derive Cargo feature flags from EMBEDDER choice. The feature lives
# on `finima-embed`, but is forwarded through `finima-api` via the
# `embedder-*` feature group (see crates/finima-api/Cargo.toml).
ifeq ($(EMBEDDER),ollama)
CARGO_EMBED_FEATURES := --features embedder-ollama
else ifeq ($(EMBEDDER),candle-metal)
CARGO_EMBED_FEATURES := --features embedder-candle-metal
else ifeq ($(EMBEDDER),candle-cuda)
CARGO_EMBED_FEATURES := --features embedder-candle-cuda
else ifeq ($(EMBEDDER),candle)
CARGO_EMBED_FEATURES := --features embedder-candle
else
CARGO_EMBED_FEATURES :=
endif
# ── Ollama detection ──────────────────────────────────────────
# When LLM=ollama, detect whether a local Ollama is already serving
# on port 11434 so we can skip the Docker container.
OLLAMA_PORT ?= 11434
OLLAMA_MODEL ?= gemma4:26b-a4b-it-q4_K_M
OLLAMA_LOCAL := $(shell curl -sf http://localhost:$(OLLAMA_PORT)/api/version >/dev/null 2>&1 && echo 1 || echo 0)
OLLAMA_DOCKER := $(shell docker inspect -f '{{.State.Running}}' finima-ollama 2>/dev/null)
# Determine infrastructure services to start.
# - LLM != ollama → postgres + minio only (no ollama needed)
# - LLM = ollama → postgres + minio + ollama UNLESS a local Ollama is
# already responding (avoids port conflicts)
ifeq ($(LLM),ollama)
ifeq ($(OLLAMA_LOCAL),1)
INFRA_SERVICES := postgres minio
OLLAMA_SOURCE := local
else
INFRA_SERVICES := postgres minio ollama
OLLAMA_SOURCE := docker
endif
else
INFRA_SERVICES := postgres minio
OLLAMA_SOURCE := none
endif
LYCHEE := $(shell command -v lychee 2>/dev/null)
PRUNE_DIRS := \( -name node_modules -o -name target -o -name .claude \
-o -name .claude-flow -o -name .git -o -name .swarm \
-o -name dist -o -name coverage -o -name .agents \) -prune
# Colors
BOLD := $(shell tput bold 2>/dev/null || echo '')
GREEN := $(shell tput setaf 2 2>/dev/null || echo '')
YELLOW := $(shell tput setaf 3 2>/dev/null || echo '')
BLUE := $(shell tput setaf 4 2>/dev/null || echo '')
CYAN := $(shell tput setaf 6 2>/dev/null || echo '')
RESET := $(shell tput sgr0 2>/dev/null || echo '')
# ============================================================================
# Default Target
# ============================================================================
.PHONY: help
help:
@echo "$(BOLD)$(BLUE)╔════════════════════════════════════════════════════════════════════╗$(RESET)"
@echo "$(BOLD)$(BLUE)║ Finima Makefile ║$(RESET)"
@echo "$(BOLD)$(BLUE)╚════════════════════════════════════════════════════════════════════╝$(RESET)"
@echo ""
@echo "$(BOLD)Quick Start:$(RESET)"
@echo " make install - Install all dependencies"
@echo " make start - Start everything (infra + backend + frontend)"
@echo " make dev - Start backend + frontend (assumes infra running)"
@echo " make docker-infra - Start dev infrastructure"
@echo " make ci - Run full CI pipeline"
@echo " make test - Run all tests"
@echo ""
@echo "$(BOLD)LLM Backend (current: $(LLM)$(if $(filter ollama,$(LLM)), — $(OLLAMA_SOURCE),)):$(RESET)"
@echo " LLM=candle make dev - In-process inference (auto-detects Metal/CUDA/CPU)"
@echo " LLM=ollama make dev - HTTP inference (auto-detects local vs Docker)"
@echo " LLM=none make dev - No LLM (Tiers 0-2 only)"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Install & Build ═════════════════════════════════════════════════$(RESET)"
@echo " install - Install all dependencies (backend + frontend)"
@echo " build - Build all (backend debug + frontend)"
@echo " build-release - Build backend in release mode"
@echo " start - Start everything (infra + backend + frontend)"
@echo " dev - Start backend + frontend (assumes infra running)"
@echo " dev-backend - Start backend API server only"
@echo " dev-watch - Start backend with auto-reload (cargo-watch)"
@echo " clean - Clean build artifacts"
@echo " clean-all - Clean build + Docker volumes (DESTROYS DATA)"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Test ════════════════════════════════════════════════════════════$(RESET)"
@echo " test - Run unit tests only (backend + frontend)"
@echo " test-all - Run ALL tests (auto-starts/stops test DB)"
@echo " test-unit - Run backend unit tests (no DB needed)"
@echo " test-integration - Run backend integration tests (auto-starts DB)"
@echo " test-llm - Run LLM tests (auto-starts Ollama, pulls model)"
@echo " test-frontend - Run frontend unit tests"
@echo " test-e2e - Run end-to-end tests (requires running backend)"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Lint & Format ═══════════════════════════════════════════════════$(RESET)"
@echo " lint - Lint everything (code + docs)"
@echo " lint-backend - Run clippy on backend"
@echo " lint-frontend - Run ESLint on frontend"
@echo " format - Format all code + docs"
@echo " format-check - Check formatting (no changes)"
@echo " typecheck - TypeScript type checking"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Documentation ══════════════════════════════════════════════════$(RESET)"
@echo " lint-md - Lint Markdown files"
@echo " lint-yaml - Lint YAML files"
@echo " lint-docs - Lint all docs (Markdown + YAML)"
@echo " format-md - Format Markdown files"
@echo " format-yaml - Format YAML files"
@echo " format-docs - Format all docs (Markdown + YAML)"
@echo " links-check - Check internal links in Markdown"
@echo " links-check-external - Check external links (slow)"
@echo " links-check-all - Check all links"
@echo ""
@echo "$(BOLD)$(BLUE)═══ CI Pipeline ════════════════════════════════════════════════════$(RESET)"
@echo " ci - Full CI pipeline (format + lint + typecheck + test)"
@echo " ci-full - CI + link checking + E2E tests"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Database ═══════════════════════════════════════════════════════$(RESET)"
@echo " migrate - Run database migrations"
@echo " migrate-create name=x - Create a new migration"
@echo " migrate-revert - Revert the last migration"
@echo " db-seed - Load test seed data (dev/test only)"
@echo " sample-load - Load data/sample/sample.sql demo fixture"
@echo " sample-attach EMAIL=x - Re-own the sample portfolio to an existing user"
@echo " sample-purge - Remove the sample portfolio + user"
@echo " sample-regen - Regenerate data/sample/sample.sql"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Docker — Infrastructure ════════════════════════════════════════$(RESET)"
@echo " docker-infra - Start dev infrastructure (services depend on LLM)"
@echo " docker-infra-down - Stop dev infrastructure"
@echo " docker-infra-restart - Restart dev infrastructure"
@echo " docker-infra-logs - Tail infrastructure container logs"
@echo " docker-infra-ps - Show infrastructure container status"
@echo " docker-infra-health - Health check infrastructure containers"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Docker — Production ════════════════════════════════════════════$(RESET)"
@echo " docker-up - Start full production stack"
@echo " docker-down - Stop production stack"
@echo " docker-logs - Tail production logs"
@echo " docker-build - Build Docker images"
@echo " docker-build-no-cache - Build images without cache"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Docker — Testing ═══════════════════════════════════════════════$(RESET)"
@echo " docker-test-up - Start test database (port 5433)"
@echo " docker-test-down - Stop test database"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Dependencies ═══════════════════════════════════════════════════$(RESET)"
@echo " outdated - Show outdated dependencies"
@echo " upgrade - Upgrade dependencies within semver"
@echo " audit - Security audit all dependencies"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Coverage & Quality ═════════════════════════════════════════════$(RESET)"
@echo " coverage - Generate test coverage report (cargo-llvm-cov)"
@echo " deadcode - Check for dead code"
@echo ""
@echo "$(BOLD)$(BLUE)═══ Infrastructure ═════════════════════════════════════════════════$(RESET)"
@echo " minio - Start MinIO object storage"
@echo " backup - Run database backup manually"
@echo " observability - Start SigNoz observability stack"
@echo ""
@echo "$(BOLD)$(BLUE)═══ AI & Models ═════════════════════════════════════════════════════$(RESET)"
@echo " dev-candle - Start with Candle LLM (auto-detects GPU)"
@echo " dev-ollama - Start with Ollama LLM"
@echo " dev-no-llm - Start without any LLM (Tiers 0-2 only)"
@echo " models - List downloaded models (set LLM=candle or ollama)"
@echo " download-model - Download the default model (set LLM=candle or ollama)"
@echo " check-ollama - Diagnose Ollama setup (local vs Docker)"
@echo ""
@echo " Run '$(BOLD)make -C frontend$(RESET)' for frontend-specific targets."
# ═══════════════════════════════════════════════════════════════
# Install & Build
# ═══════════════════════════════════════════════════════════════
.PHONY: install build build-release start dev dev-backend dev-watch
install: ## Install all dependencies (backend + frontend)
cargo fetch
$(MAKE) -C $(FRONTEND_DIR) install
build: ## Build all (backend debug + frontend)
cargo build --workspace
$(MAKE) -C $(FRONTEND_DIR) build
build-release: ## Build backend in release mode
cargo build --release -p finima-api $(CARGO_LLM_FEATURES) $(CARGO_EMBED_FEATURES)
start: docker-infra ## Start everything (infra + backend + frontend)
@echo "$(GREEN)Waiting for infrastructure to be healthy...$(RESET)"
@for i in $$(seq 1 30); do \
pg_isready -h localhost -p 5432 -U finima >/dev/null 2>&1 && break; \
sleep 1; \
done
@$(MAKE) dev
dev: ## Start backend + frontend (assumes infra is running)
@echo "$(GREEN)Backend: http://localhost:3000 Frontend: http://localhost:5173$(RESET)"
@echo "$(CYAN)LLM backend: $(LLM)$(if $(filter ollama,$(LLM)), ($(OLLAMA_SOURCE)),) Embedder: $(EMBEDDER)$(RESET)"
@trap 'kill 0' INT TERM EXIT; \
APP_ENV=development cargo run --bin finima-api $(CARGO_LLM_FEATURES) $(CARGO_EMBED_FEATURES) & \
$(MAKE) -C $(FRONTEND_DIR) dev & \
wait
dev-backend: ## Start backend API server only
@echo "$(CYAN)LLM backend: $(LLM)$(RESET)"
APP_ENV=development cargo run --bin finima-api $(CARGO_LLM_FEATURES) $(CARGO_EMBED_FEATURES)
dev-watch: ## Start backend with auto-reload (requires cargo-watch)
@echo "$(CYAN)LLM backend: $(LLM)$(RESET)"
APP_ENV=development cargo watch -x 'run --bin finima-api $(CARGO_LLM_FEATURES) $(CARGO_EMBED_FEATURES)'
# ═══════════════════════════════════════════════════════════════
# Testing
# ═══════════════════════════════════════════════════════════════
.PHONY: test test-all test-unit test-integration test-llm test-frontend test-e2e
test: test-unit test-frontend ## Run all unit tests (backend + frontend)
test-all: docker-test-up test-all-backend test-frontend docker-test-down ## Run ALL tests (starts/stops test DB automatically)
test-all-backend: ## Run all backend tests (unit + integration, requires test DB)
@echo "Waiting for test database..."
@for i in 1 2 3 4 5 6 7 8 9 10; do \
pg_isready -h localhost -p 5433 -U finima -d finima_test >/dev/null 2>&1 && break; \
sleep 1; \
done
TEST_DATABASE_URL="postgres://finima:test@localhost:5433/finima_test" \
cargo test --workspace
test-unit: ## Run backend unit tests (no database required)
cargo test --workspace --lib
test-integration: ## Run backend integration tests (starts test DB if needed)
@if ! pg_isready -h localhost -p 5433 -U finima -d finima_test >/dev/null 2>&1; then \
echo "Starting test database..."; \
$(COMPOSE_TEST) up -d postgres; \
for i in 1 2 3 4 5 6 7 8 9 10; do \
pg_isready -h localhost -p 5433 -U finima -d finima_test >/dev/null 2>&1 && break; \
sleep 1; \
done; \
fi
TEST_DATABASE_URL="postgres://finima:test@localhost:5433/finima_test" \
APP_ENV=test cargo test --workspace --test '*'
OLLAMA_TEST_PORT ?= 11435
OLLAMA_TEST_MODEL ?= gemma4:e4b-it-q4_K_M
test-llm: ## Run LLM integration tests (auto-starts Ollama, pulls model)
@echo "Starting Ollama test container..."
@$(COMPOSE_TEST) up -d ollama
@echo "Waiting for Ollama to be ready..."
@for i in $$(seq 1 30); do \
curl -sf http://localhost:$(OLLAMA_TEST_PORT)/api/version >/dev/null 2>&1 && break; \
sleep 2; \
done
@echo "Pulling test model $(OLLAMA_TEST_MODEL) (this may take a while on first run)..."
@curl -sf http://localhost:$(OLLAMA_TEST_PORT)/api/pull -d '{"name":"$(OLLAMA_TEST_MODEL)"}' \
| while read -r line; do \
status=$$(echo "$$line" | grep -o '"status":"[^"]*"' | head -1); \
printf "\r %s" "$$status"; \
done; echo ""
@echo "Running LLM integration tests..."
OLLAMA_URL="http://localhost:$(OLLAMA_TEST_PORT)" \
OLLAMA_TEST_MODEL="$(OLLAMA_TEST_MODEL)" \
cargo test -p finima-llm --features ollama -- --ignored
@echo "LLM tests complete."
test-frontend: ## Run frontend unit tests
$(MAKE) -C $(FRONTEND_DIR) test
test-e2e: ## Run end-to-end tests (requires running backend)
$(MAKE) -C $(FRONTEND_DIR) test-e2e
# ═══════════════════════════════════════════════════════════════
# Lint & Format — Code
# ═══════════════════════════════════════════════════════════════
.PHONY: lint lint-backend lint-frontend format format-check typecheck
lint: lint-docs lint-backend lint-frontend ## Lint everything (code + docs)
lint-backend: ## Run clippy on backend
cargo clippy --workspace --all-targets -- -D warnings
lint-frontend: ## Run ESLint on frontend
$(MAKE) -C $(FRONTEND_DIR) lint
format: format-docs ## Format all code + docs
cargo fmt --all
$(MAKE) -C $(FRONTEND_DIR) format
format-check: format-check-docs ## Check formatting (code + docs)
cargo fmt --all -- --check
$(MAKE) -C $(FRONTEND_DIR) format-check
typecheck: ## TypeScript type checking
$(MAKE) -C $(FRONTEND_DIR) typecheck
# ═══════════════════════════════════════════════════════════════
# Lint & Format — Documentation
# ═══════════════════════════════════════════════════════════════
.PHONY: lint-md lint-yaml lint-docs format-md format-yaml format-docs format-check-md format-check-yaml format-check-docs
lint-md: ## Lint Markdown files
@echo "$(GREEN)Linting Markdown...$(RESET)"
@if command -v markdownlint-cli2 >/dev/null 2>&1; then \
markdownlint-cli2 '**/*.md' '#**/node_modules' '#**/target' '#.claude/worktrees/**' || true; \
else \
echo "$(YELLOW)markdownlint-cli2 not installed. Run: npm i -g markdownlint-cli2$(RESET)"; \
fi
lint-yaml: ## Lint YAML files
@echo "$(GREEN)Linting YAML...$(RESET)"
@find . $(PRUNE_DIRS) -o \( -name '*.yaml' -o -name '*.yml' \) ! -name 'pnpm-lock.yaml' -print | \
xargs yamllint -c .yamllint.yaml 2>/dev/null || \
echo "$(YELLOW)yamllint not installed. Run: pip install yamllint$(RESET)"
lint-docs: lint-md lint-yaml ## Lint all docs (Markdown + YAML)
format-md: ## Format Markdown files
@find . $(PRUNE_DIRS) -o -name '*.md' -print | xargs npx prettier --write --no-error-on-unmatched-pattern
format-yaml: ## Format YAML files
@find . $(PRUNE_DIRS) -o \( -name '*.yaml' -o -name '*.yml' \) ! -name 'pnpm-lock.yaml' -print | \
xargs npx prettier --write --no-error-on-unmatched-pattern
format-docs: format-md format-yaml ## Format all docs (Markdown + YAML)
format-check-md: ## Check Markdown formatting
@find . $(PRUNE_DIRS) -o -name '*.md' -print | xargs npx prettier --check --no-error-on-unmatched-pattern
format-check-yaml: ## Check YAML formatting
@find . $(PRUNE_DIRS) -o \( -name '*.yaml' -o -name '*.yml' \) ! -name 'pnpm-lock.yaml' -print | \
xargs npx prettier --check --no-error-on-unmatched-pattern
format-check-docs: format-check-md format-check-yaml ## Check doc formatting
# ═══════════════════════════════════════════════════════════════
# Link Checking
# ═══════════════════════════════════════════════════════════════
.PHONY: links-check links-check-external links-check-all
links-check: ## Check internal links in Markdown
@echo "$(GREEN)Checking local file links...$(RESET)"
@if [ -n "$(LYCHEE)" ]; then \
$(LYCHEE) --scheme file --include-fragments --config .lychee.toml '**/*.md'; \
else \
echo "$(YELLOW)lychee not installed. Run: cargo install lychee$(RESET)"; \
fi
links-check-external: ## Check external links (may take minutes)
@echo "$(GREEN)Checking external links...$(RESET)"
@if [ -n "$(LYCHEE)" ]; then \
$(LYCHEE) --scheme https --scheme http --config .lychee.toml '**/*.md'; \
else \
echo "$(YELLOW)lychee not installed. Run: cargo install lychee$(RESET)"; \
fi
links-check-all: links-check links-check-external ## Check all links (internal + external)
# ═══════════════════════════════════════════════════════════════
# CI Pipeline
# ═══════════════════════════════════════════════════════════════
.PHONY: ci ci-full
ci: format-check lint typecheck test ## Full CI pipeline (format + lint + typecheck + test)
ci-full: ci links-check test-e2e ## CI + link checking + E2E tests
# ═══════════════════════════════════════════════════════════════
# Database
# ═══════════════════════════════════════════════════════════════
.PHONY: migrate migrate-create migrate-revert db-seed sample-load sample-purge sample-regen sample-attach
migrate: ## Run database migrations
sqlx migrate run --source crates/finima-db/src/migrations
migrate-create: ## Create a new migration (usage: make migrate-create name=add_foo)
sqlx migrate add -r $(name) --source crates/finima-db/src/migrations
migrate-revert: ## Revert the last migration
sqlx migrate revert --source crates/finima-db/src/migrations
db-seed: ## Load test seed data (dev/test only)
@if [ "$${APP_ENV}" = "production" ]; then \
echo "ERROR: Cannot seed production database"; exit 1; \
fi
psql "$${DATABASE_URL:-postgres://finima:finima_dev@localhost:5432/finima}" -f tests/seed.sql
sample-load: ## Load data/sample/sample.sql demo fixture (dev only)
@if [ "$${APP_ENV}" = "production" ]; then \
echo "ERROR: Cannot load sample data into production"; exit 1; \
fi
psql "$${DATABASE_URL:-postgres://finima:finima_dev@localhost:5432/finima}" -f data/sample/sample.sql
# Identify sample rows by stable semantic markers (email, portfolio name) —
# never by hard-coded UUIDs. Each SQL script is piped as a single stdin
# stream so the whole thing runs inside one shell / one psql invocation
# (Make runs each recipe line in a fresh shell, so heredocs don't survive
# across lines).
define SAMPLE_PURGE_SQL
BEGIN;
DELETE FROM portfolios WHERE name = 'Sample Household';
DELETE FROM users WHERE email = 'sample@finima.local';
COMMIT;
endef
export SAMPLE_PURGE_SQL
define SAMPLE_ATTACH_SQL
BEGIN;
-- Abort if target user doesn't exist (division-by-zero).
SELECT 1/COUNT(*)::int FROM users WHERE email = :'email';
-- Abort if sample fixture isn't loaded.
SELECT 1/COUNT(*)::int FROM users WHERE email = 'sample@finima.local';
-- Re-own the sample portfolio. Scoped by both name AND current ownership
-- so a real user's own 'Sample Household' is never touched.
UPDATE portfolios
SET user_id = (SELECT id FROM users WHERE email = :'email')
WHERE name = 'Sample Household'
AND user_id = (SELECT id FROM users WHERE email = 'sample@finima.local');
DELETE FROM users WHERE email = 'sample@finima.local';
COMMIT;
endef
export SAMPLE_ATTACH_SQL
sample-purge: ## Remove the sample portfolio + user (safe whether or not sample-attach was run)
@if [ "$${APP_ENV}" = "production" ]; then \
echo "ERROR: Cannot purge sample data in production"; exit 1; \
fi
@echo "$$SAMPLE_PURGE_SQL" | psql "$${DATABASE_URL:-postgres://finima:finima_dev@localhost:5432/finima}" -v ON_ERROR_STOP=1
sample-attach: ## Re-own the sample portfolio to an existing user (usage: make sample-attach EMAIL=you@real.com)
@if [ -z "$(EMAIL)" ]; then \
echo "ERROR: EMAIL is required. Usage: make sample-attach EMAIL=you@real.com"; exit 1; \
fi
@if [ "$${APP_ENV}" = "production" ]; then \
echo "ERROR: Cannot attach sample data in production"; exit 1; \
fi
@echo "$$SAMPLE_ATTACH_SQL" | psql "$${DATABASE_URL:-postgres://finima:finima_dev@localhost:5432/finima}" -v ON_ERROR_STOP=1 -v email='$(EMAIL)'
sample-regen: ## Regenerate data/sample/sample.sql from the deterministic generator
cargo run -p finima-api --bin finima-generate-sample
# ═══════════════════════════════════════════════════════════════
# Docker — Infrastructure
# ═══════════════════════════════════════════════════════════════
.PHONY: docker-infra docker-infra-down docker-infra-restart docker-infra-logs docker-infra-ps docker-infra-health
docker-infra: ## Start dev infrastructure (services depend on LLM setting)
ifeq ($(LLM),ollama)
ifeq ($(OLLAMA_SOURCE),local)
@echo "$(GREEN)Ollama: using local instance on port $(OLLAMA_PORT)$(RESET)"
@if docker inspect -f '{{.State.Running}}' finima-ollama 2>/dev/null | grep -q true; then \
echo "$(YELLOW)Stopping Docker Ollama (finima-ollama) to avoid port conflict...$(RESET)"; \
docker stop finima-ollama >/dev/null 2>&1; \
fi
else
@echo "$(CYAN)Ollama: starting Docker container (no local instance detected)$(RESET)"
endif
endif
$(COMPOSE_DEV) up -d $(INFRA_SERVICES)
docker-infra-down: ## Stop dev infrastructure
$(COMPOSE_DEV) down
docker-infra-restart: ## Restart dev infrastructure
$(COMPOSE_DEV) restart
docker-infra-logs: ## Tail infrastructure container logs
$(COMPOSE_DEV) logs -f
docker-infra-ps: ## Show infrastructure container status
$(COMPOSE_DEV) ps
docker-infra-health: ## Health check infrastructure containers
@$(COMPOSE_DEV) ps --format '{{.Name}}\t{{.Status}}' | column -t
# ═══════════════════════════════════════════════════════════════
# Docker — Production
# ═══════════════════════════════════════════════════════════════
.PHONY: docker-up docker-down docker-logs docker-build docker-build-no-cache
docker-up: ## Start full production stack
$(COMPOSE_PROD) up -d
docker-down: ## Stop production stack
$(COMPOSE_PROD) down
docker-logs: ## Tail production logs
$(COMPOSE_PROD) logs -f
docker-build: ## Build Docker images
docker build -t finima-backend -f Dockerfile.backend .
docker build -t finima-frontend -f frontend/Dockerfile.frontend frontend/
docker-build-no-cache: ## Build Docker images without cache
docker build --no-cache -t finima-backend -f Dockerfile.backend .
docker build --no-cache -t finima-frontend -f frontend/Dockerfile.frontend frontend/
# ═══════════════════════════════════════════════════════════════
# Release
# ═══════════════════════════════════════════════════════════════
.PHONY: release changelog
release: ## Cut a release (bumps versions, updates CHANGELOG, commits, tags, pushes). Usage: make release VERSION=0.2.0
@[ -n "$(VERSION)" ] || (echo "usage: make release VERSION=X.Y.Z" >&2; exit 1)
@./scripts/release.sh $(VERSION)
changelog: ## Regenerate CHANGELOG.md from git history using git-cliff
git-cliff --output CHANGELOG.md
# ═══════════════════════════════════════════════════════════════
# Docker — Testing
# ═══════════════════════════════════════════════════════════════
.PHONY: docker-test-up docker-test-down
docker-test-up: ## Start test database (port 5433)
$(COMPOSE_TEST) up -d
docker-test-down: ## Stop test database
$(COMPOSE_TEST) down
# ═══════════════════════════════════════════════════════════════
# Dependencies
# ═══════════════════════════════════════════════════════════════
.PHONY: outdated upgrade audit
outdated: ## Show outdated dependencies
cargo outdated -R --workspace 2>/dev/null || echo "Install: cargo install cargo-outdated"
$(MAKE) -C $(FRONTEND_DIR) outdated
upgrade: ## Upgrade dependencies within semver
cargo update
$(MAKE) -C $(FRONTEND_DIR) upgrade
audit: ## Security audit all dependencies (respects /audit-ignore exceptions)
@command -v cargo-audit >/dev/null 2>&1 || { echo "Install: cargo install cargo-audit --locked"; exit 1; }
bash .github/scripts/cargo-audit.sh
$(MAKE) -C $(FRONTEND_DIR) audit
# ═══════════════════════════════════════════════════════════════
# Coverage & Quality
# ═══════════════════════════════════════════════════════════════
.PHONY: coverage deadcode
coverage: ## Generate test coverage report (requires cargo-llvm-cov)
cargo llvm-cov --workspace --lib --html
@echo "Coverage report: target/llvm-cov/html/index.html"
deadcode: ## Check for dead code
cargo clippy --workspace -- -W dead-code
$(MAKE) -C $(FRONTEND_DIR) deadcode
# ═══════════════════════════════════════════════════════════════
# LLM / AI Models
# ═══════════════════════════════════════════════════════════════
.PHONY: dev-candle dev-ollama dev-no-llm models download-model check-ollama
dev-candle: ## Start with Candle in-process LLM (auto-detects Metal/CUDA/CPU)
@$(MAKE) dev LLM=candle
dev-ollama: ## Start with Ollama HTTP LLM
@$(MAKE) dev LLM=ollama
dev-no-llm: ## Start without any LLM (Tiers 0-2 only)
@$(MAKE) dev LLM=none
models: ## List downloaded models (set LLM=candle or LLM=ollama)
ifeq ($(filter candle candle-metal candle-cuda,$(LLM)),)
ifeq ($(LLM),ollama)
@if docker inspect -f '{{.State.Running}}' finima-ollama 2>/dev/null | grep -q true; then \
echo "$(CYAN)Models in Docker Ollama (finima-ollama):$(RESET)"; \
docker exec finima-ollama ollama list; \
fi
@if command -v ollama >/dev/null 2>&1 && ollama list >/dev/null 2>&1; then \
echo "$(CYAN)Models in local Ollama:$(RESET)"; \
ollama list; \
fi
@if ! docker inspect -f '{{.State.Running}}' finima-ollama 2>/dev/null | grep -q true \
&& ! (command -v ollama >/dev/null 2>&1 && ollama list >/dev/null 2>&1); then \
echo "No Ollama instance found. Start with: make docker-infra LLM=ollama"; \
fi
else
$(error Set LLM to candle or ollama (current: $(LLM)))
endif
else
@HF_CACHE="$${HF_HOME:-$${HOME}/.cache/huggingface}/hub"; \
if [ -d "$$HF_CACHE" ]; then \
found=0; \
for d in "$$HF_CACHE"/models--*; do \
[ -d "$$d" ] || continue; \
name=$$(basename "$$d" | sed 's/^models--//; s/--/\//g'); \
gguf_count=$$(find "$$d" -name '*.gguf' 2>/dev/null | wc -l | tr -d ' '); \
printf " %-45s (%s GGUF files)\n" "$$name" "$$gguf_count"; \
found=1; \
done; \
[ "$$found" = "1" ] || echo "No models found in $$HF_CACHE"; \
else \
echo "HuggingFace cache not found at $$HF_CACHE"; \
echo "Run 'make download-model' to download a model."; \
fi
endif
download-model: ## Download the default model (set LLM=candle or LLM=ollama)
ifeq ($(filter candle candle-metal candle-cuda,$(LLM)),)
ifeq ($(LLM),ollama)
@if [ "$(OLLAMA_SOURCE)" = "docker" ]; then \
echo "$(CYAN)Pulling $(OLLAMA_MODEL) into Docker Ollama...$(RESET)"; \
docker exec finima-ollama ollama pull $(OLLAMA_MODEL); \
elif [ "$(OLLAMA_SOURCE)" = "local" ]; then \
echo "$(CYAN)Pulling $(OLLAMA_MODEL) into local Ollama...$(RESET)"; \
ollama pull $(OLLAMA_MODEL); \
else \
echo "No Ollama instance found. Start with: make docker-infra LLM=ollama"; \
exit 1; \
fi
else
$(error Set LLM to candle or ollama (current: $(LLM)))
endif
else
cargo run -p finima-llm --features candle --bin download_model
endif
check-ollama: ## Diagnose Ollama setup (local vs Docker, model availability)
@echo "$(BOLD)Ollama Diagnostics$(RESET)"
@echo ""
@echo "$(BOLD)Local Ollama:$(RESET)"
@if command -v ollama >/dev/null 2>&1; then \
ver=$$(ollama --version 2>/dev/null || echo "unknown"); \
echo " Installed: yes ($$ver)"; \
if ollama list >/dev/null 2>&1; then \
echo " Status: running"; \
echo " Models:"; \
ollama list 2>/dev/null | sed 's/^/ /'; \
else \
echo " Status: not running"; \
fi; \
else \
echo " Installed: no"; \
fi
@echo ""
@echo "$(BOLD)Docker Ollama (finima-ollama):$(RESET)"
@if docker inspect -f '{{.State.Running}}' finima-ollama 2>/dev/null | grep -q true; then \
echo " Status: running"; \
port=$$(docker port finima-ollama 11434 2>/dev/null | head -1); \
echo " Port: $$port"; \
echo " Models:"; \
docker exec finima-ollama ollama list 2>/dev/null | sed 's/^/ /'; \
else \
echo " Status: not running"; \
fi
@echo ""
@echo "$(BOLD)Backend connects to:$(RESET) http://localhost:$(OLLAMA_PORT)"
@if curl -sf http://localhost:$(OLLAMA_PORT)/api/version >/dev/null 2>&1; then \
echo " Port $(OLLAMA_PORT): $(GREEN)responding$(RESET)"; \
if curl -sf http://localhost:$(OLLAMA_PORT)/api/tags 2>/dev/null \
| grep -q '"$(OLLAMA_MODEL)"'; then \
echo " Model $(OLLAMA_MODEL): $(GREEN)available$(RESET)"; \
else \
echo " Model $(OLLAMA_MODEL): $(YELLOW)NOT FOUND$(RESET)"; \
echo " Run: make download-model LLM=ollama"; \
fi; \
else \
echo " Port $(OLLAMA_PORT): $(YELLOW)not responding$(RESET)"; \
fi
# ═══════════════════════════════════════════════════════════════
# Infrastructure (MinIO, Backups, Observability)
# ═══════════════════════════════════════════════════════════════
.PHONY: minio backup observability
minio: ## Start MinIO object storage
$(COMPOSE_DEV) up -d minio
backup: ## Run database backup manually
$(COMPOSE_PROD) run --rm backup /scripts/backup.sh
observability: ## Start SigNoz observability stack
$(COMPOSE_OBS) up -d
# ═══════════════════════════════════════════════════════════════
# Clean
# ═══════════════════════════════════════════════════════════════
.PHONY: clean clean-all
clean: ## Clean build artifacts
cargo clean
$(MAKE) -C $(FRONTEND_DIR) clean
clean-all: clean ## Clean build + Docker volumes (DESTROYS DATA)
$(COMPOSE_DEV) down -v 2>/dev/null || true
$(COMPOSE_PROD) down -v 2>/dev/null || true
$(COMPOSE_TEST) down -v 2>/dev/null || true