-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
1062 lines (895 loc) · 50.1 KB
/
Copy pathMakefile
File metadata and controls
1062 lines (895 loc) · 50.1 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
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
# Backend / frontend dev ports. ``ADAPTIVE_LEARNER_PORT`` is the
# project-wide name; the Makefile aliases it as BACKEND_PORT for
# readability. Defaults are intentionally non-standard (18001 /
# 15174) so Adaptive Learner can coexist with other projects already
# bound to 8000 / 5173 on the same workstation. Overriding either
# via env or ``make BACKEND_PORT=… dev`` flows through to uvicorn,
# vite, and the health-check curl below.
BACKEND_PORT ?= $(or $(ADAPTIVE_LEARNER_PORT),18001)
FRONTEND_PORT ?= $(or $(ADAPTIVE_LEARNER_FRONTEND_PORT),15174)
# Dev-only Fernet key. Generated on first ``make dev-secret`` (or
# implicitly by ``make dev``). Lives at the repo root in a
# gitignored dir. Production deployments must supply
# ADAPTIVE_LEARNER_SECRET_KEY by export OR by storing
# ``secret_key: <key>`` in ~/.config/adaptive_learner/secrets.yaml;
# this file is dev-only.
ADAPTIVE_LEARNER_DEV_SECRET_FILE ?= .adaptive-learner/dev-secret.env
.PHONY: dev dev-bg dev-bg-logs dev-down dev-backend dev-frontend dev-secret dev-lan build-frontend stop restart fix-watchers \
install install-backend install-frontend install-plugins install-e2e \
test test-fast test-changed test-backend test-frontend test-plugins test-plugin-assessment \
test-plugin-ai-anthropic test-plugin-ai-openai test-plugin-ai-gemini \
test-plugin-session test-plugin-tracking \
test-plugin-tools test-plugin-gamification test-plugin-anki test-plugin-notebooklm test-plugin-learning-repo test-plugin-content-loader test-plugin-missions test-e2e test-e2e-ui test-e2e-smoke test-e2e-smoke-retries test-dexie-smoke test-webkit test-manual-automation \
test-coverage test-coverage-backend test-coverage-frontend \
test-one test-watch tdd-help \
stryker stryker-quick \
verify-theme verify-theme-baseline-update \
check-types check-types-backend check-types-frontend check-file-sizes check-css-size css-identity-ref css-identity-check check-dead-classnames audit-legacy-conflicts check-complexity check-complexity-gate check-complexity-gate-update \
check-directory-size check-directory-size-gate \
check-folder-size check-folder-size-update \
check-blockers archive-task archive-task-dry install-hooks \
sync-versions sync-versions-dry sync-versions-check \
docs-install docs-build docs-serve sync-mkdocs-nav verify-mkdocs-nav \
verify-docs verify-docs-fix check-mkdocs-orphans verify-docs-discipline docs-checklist \
sync-i18n sync-plugin-config sync-praise sync-missions \
i18n-quality-check i18n-quality-check-dry i18n-csv-export \
verify-i18n-scripts \
sync-schema sync-schema-check sync-schema-mirror engine-parity-check \
lock-all-plugins verify-plugin-locks \
audit-backend audit-frontend bandit-backend security-backend check-security circular-deps \
release-state release-outdated release-test release-build \
release-discover release-tag release-publish \
clean prod prod-down prod-logs \
launcher-test launcher-install launcher-status launcher-check launcher-stop \
launcher-uninstall launcher-cleanup launcher-port launcher-version launcher-logs \
launcher-pytest launcher-update-package launcher-venv-fix launcher-tray-check help
# --- Development ---
dev-secret: ## Ensure $(ADAPTIVE_LEARNER_DEV_SECRET_FILE) carries a dev-only Fernet key (idempotent)
@mkdir -p "$$(dirname $(ADAPTIVE_LEARNER_DEV_SECRET_FILE))"
@if [ ! -s "$(ADAPTIVE_LEARNER_DEV_SECRET_FILE)" ]; then \
key=$$(cd backend && poetry run python -c \
"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"); \
{ \
echo "# Adaptive Learner dev-only Fernet key. NOT for production."; \
echo "# Generated by 'make dev-secret' on $$(date +%Y-%m-%d)."; \
echo "# Safe to delete + regenerate; re-running make dev-secret will rewrite this file."; \
echo "# Sourced automatically by 'make dev' and 'make dev-bg'."; \
echo "# The ?= form preserves any value already exported in the parent shell."; \
echo "export ADAPTIVE_LEARNER_SECRET_KEY=\"\$${ADAPTIVE_LEARNER_SECRET_KEY:-$$key}\""; \
} > "$(ADAPTIVE_LEARNER_DEV_SECRET_FILE)"; \
echo "Wrote dev-secret file: $(ADAPTIVE_LEARNER_DEV_SECRET_FILE)"; \
else \
echo "Dev-secret file already populated: $(ADAPTIVE_LEARNER_DEV_SECRET_FILE) (delete to regenerate)"; \
fi
dev: dev-secret ## Start backend + frontend (backend first, then frontend)
@if [ -r /proc/sys/fs/inotify/max_user_watches ]; then \
watches=$$(cat /proc/sys/fs/inotify/max_user_watches); \
if [ "$$watches" -lt 100000 ]; then \
echo "WARNING: fs.inotify.max_user_watches=$$watches is low (< 100000)."; \
echo " vite dev will likely fail with ENOSPC."; \
echo " Run 'make fix-watchers' for the persistent fix."; \
fi; \
fi
@echo "Starting Adaptive Learner (backend $(BACKEND_PORT) / frontend $(FRONTEND_PORT))..."
@cd backend && poetry env use python3.12 -q 2>/dev/null; \
. ../$(ADAPTIVE_LEARNER_DEV_SECRET_FILE) && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
poetry run uvicorn app.main:app --reload --port $(BACKEND_PORT) &
@echo "Waiting for backend on :$(BACKEND_PORT)..."
@for i in 1 2 3 4 5 6 7 8 9 10; do \
curl -s http://localhost:$(BACKEND_PORT)/api/health > /dev/null 2>&1 && break; \
sleep 1; \
done
@echo "Backend ready. Starting frontend on :$(FRONTEND_PORT)..."
@cd frontend && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
ADAPTIVE_LEARNER_FRONTEND_PORT=$(FRONTEND_PORT) \
bun run dev
build-frontend: ## Build frontend/dist. Default API mode; STORAGE_MODE=dexie for the GH-Pages shape.
@echo "Building frontend/dist (storage mode: $(or $(STORAGE_MODE),api))..."
@cd frontend && $(if $(filter dexie,$(STORAGE_MODE)),VITE_STORAGE_MODE=dexie ,)bun run build
dev-lan: dev-secret build-frontend ## LAN device test: serve built frontend + API on ONE origin at 0.0.0.0:$(BACKEND_PORT), no --reload
@LAN_IP=$$(hostname -I 2>/dev/null | awk '{print $$1}'); \
echo ""; \
echo "Adaptive Learner — LAN device-test mode (single origin, API mode, no reload)"; \
echo " On this machine: http://localhost:$(BACKEND_PORT)"; \
if [ -n "$$LAN_IP" ]; then \
echo " On your phone: http://$$LAN_IP:$(BACKEND_PORT) (same WLAN, open in Safari/Chrome)"; \
else \
echo " (Could not detect a LAN IP via 'hostname -I'; find it manually with 'ip addr'.)"; \
fi; \
echo " This is a dev build, NOT the installed PWA. Press Ctrl+C to stop."; \
echo ""
@cd backend && poetry env use python3.12 -q 2>/dev/null; \
. ../$(ADAPTIVE_LEARNER_DEV_SECRET_FILE) && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
ADAPTIVE_LEARNER_SERVE_FRONTEND=1 \
poetry run uvicorn app.main:app --host 0.0.0.0 --port $(BACKEND_PORT)
DEV_LOG_DIR ?= /tmp/adaptive-learner-logs
dev-bg: dev-secret ## Start in background, logs to $(DEV_LOG_DIR) (stop with: make dev-down)
@mkdir -p $(DEV_LOG_DIR)
@echo "Starting Adaptive Learner (background)..."
@echo " Backend log: $(DEV_LOG_DIR)/backend.log"
@echo " Frontend log: $(DEV_LOG_DIR)/frontend.log"
@cd backend && \
. ../$(ADAPTIVE_LEARNER_DEV_SECRET_FILE) && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
setsid poetry run uvicorn app.main:app --reload --port $(BACKEND_PORT) \
< /dev/null > $(DEV_LOG_DIR)/backend.log 2>&1 & \
echo $$! > .pid-backend
@cd frontend && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
ADAPTIVE_LEARNER_FRONTEND_PORT=$(FRONTEND_PORT) \
setsid bun run dev \
< /dev/null > $(DEV_LOG_DIR)/frontend.log 2>&1 & \
echo $$! > .pid-frontend
@sleep 2
@if kill -0 $$(cat .pid-backend) 2>/dev/null; then \
echo " Backend PID: $$(cat .pid-backend) (alive)"; \
else \
echo " ERROR: backend died on startup. tail $(DEV_LOG_DIR)/backend.log"; \
rm -f .pid-backend; \
exit 1; \
fi
@if kill -0 $$(cat .pid-frontend) 2>/dev/null; then \
echo " Frontend PID: $$(cat .pid-frontend) (alive)"; \
else \
echo " ERROR: frontend died on startup. tail $(DEV_LOG_DIR)/frontend.log"; \
rm -f .pid-frontend; \
exit 1; \
fi
@echo "Stop with: make dev-down | Tail logs with: make dev-bg-logs"
dev-bg-logs: ## Tail backend + frontend logs from a `make dev-bg` run
@if [ ! -f $(DEV_LOG_DIR)/backend.log ] && [ ! -f $(DEV_LOG_DIR)/frontend.log ]; then \
echo "No logs in $(DEV_LOG_DIR). Run 'make dev-bg' first."; \
exit 1; \
fi
@echo "Tailing $(DEV_LOG_DIR)/backend.log + $(DEV_LOG_DIR)/frontend.log (Ctrl+C to stop)..."
@tail -F $(DEV_LOG_DIR)/backend.log $(DEV_LOG_DIR)/frontend.log
dev-down: ## Stop background dev servers
@if [ -f .pid-backend ]; then kill $$(cat .pid-backend) 2>/dev/null; rm -f .pid-backend; echo "Backend stopped"; fi
@if [ -f .pid-frontend ]; then kill $$(cat .pid-frontend) 2>/dev/null; rm -f .pid-frontend; echo "Frontend stopped"; fi
@pkill -f "uvicorn app.main:app" 2>/dev/null || true
@pkill -f "vite" 2>/dev/null || true
@echo "Done"
stop: dev-down ## Alias for dev-down (stop dev servers)
restart: dev-down dev ## Stop and restart dev servers (use after a hung session)
fix-watchers: ## Persist Linux inotify limits for vite dev (sudo required, runs once)
@echo "Adaptive Learner: persist inotify limits for vite dev mode."
@echo "Sudo prompt is for the sysctl write to /etc/sysctl.d/."
@echo ""
@echo "fs.inotify.max_user_watches=524288" | sudo tee /etc/sysctl.d/99-adaptive-learner-watchers.conf > /dev/null
@echo "fs.inotify.max_user_instances=512" | sudo tee -a /etc/sysctl.d/99-adaptive-learner-watchers.conf > /dev/null
@sudo sysctl --system > /dev/null
@echo "Wrote /etc/sysctl.d/99-adaptive-learner-watchers.conf and applied:"
@echo " fs.inotify.max_user_watches = $$(cat /proc/sys/fs/inotify/max_user_watches)"
@echo " fs.inotify.max_user_instances = $$(cat /proc/sys/fs/inotify/max_user_instances)"
@echo "Persistent across reboots."
dev-backend: dev-secret
cd backend && poetry env use python3.12 -q 2>/dev/null; \
. ../$(ADAPTIVE_LEARNER_DEV_SECRET_FILE) && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
poetry run uvicorn app.main:app --reload --port $(BACKEND_PORT)
dev-frontend:
cd frontend && \
ADAPTIVE_LEARNER_PORT=$(BACKEND_PORT) \
ADAPTIVE_LEARNER_FRONTEND_PORT=$(FRONTEND_PORT) \
bun run dev
# --- Install ---
install: install-plugins install-backend install-frontend install-e2e ## Install all dependencies
install-backend:
cd backend && poetry install
install-frontend:
cd frontend && bun install
install-e2e:
cd e2e && bun install && npx playwright install chromium
install-plugins:
@for dir in plugins/adaptive-learner-plugin-*; do \
if [ -f "$$dir/pyproject.toml" ]; then \
echo "Installing $$dir..."; \
cd "$$dir" && poetry install && cd ../..; \
fi; \
done
# --- Test ---
test: test-backend test-plugins test-frontend ## Run ALL tests, no coverage (everyday use; coverage runs in CI)
@echo ""
@echo "=== All tests complete ==="
# Fast local PR-mirror gate (#1174). Mirrors the merge-blocking checks the
# PR CI runs (ci.yml: backend-tests pytest + lint-and-type-check ruff/mypy +
# frontend-tests tsc/vitest), MINUS the slow/heavy bits (no coverage, no
# plugins, no build/eslint/stylelint/audit). Runtime budget roughly < 15 min.
# Needs no running backend -- pytest uses the in-memory TestClient.
test-fast: ## Fast PR-mirror gate: backend ruff+mypy+pytest, frontend tsc+vitest (no coverage, no plugins) (#1174)
@echo ""
@echo "=== test-fast: backend ruff check app/ ==="
cd backend && poetry run ruff check app/
@echo ""
@echo "=== test-fast: backend mypy app/ ==="
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run mypy app/
@echo ""
@echo "=== test-fast: backend pytest tests/ ==="
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run pytest tests/ -q
@echo ""
@echo "=== test-fast: frontend tsc --noEmit ==="
cd frontend && bunx tsc --noEmit
@echo ""
@echo "=== test-fast: frontend vitest run ==="
cd frontend && bunx vitest run
@echo ""
@echo "test-fast mirrors the PR gate (ci.yml). Full suite incl. plugins: 'make test'."
# Test Impact Analysis (#615/#1174): run ONLY the tests whose covered code
# changed vs origin/develop -- the local counterpart of the PR-CI selective
# runs. Frontend uses vitest --changed; backend uses pytest-testmon (installed
# inline if absent, mirroring CI). With no relevant changes the frontend run
# passes with no tests and testmon selects nothing (pytest exit 5), both
# treated as success -- never an error.
test-changed: ## Test Impact Analysis: only tests affected vs origin/develop (vitest --changed + pytest --testmon) (#1174)
@echo ""
@echo "=== test-changed: frontend Vitest --changed origin/develop ==="
cd frontend && bunx vitest run --changed origin/develop --passWithNoTests
@echo ""
@echo "=== test-changed: backend pytest --testmon (vs .testmondata) ==="
cd backend && poetry run pip install -q pytest-testmon
cd backend && { poetry env use python3.12 -q 2>/dev/null; poetry run pytest tests/ --testmon -q; code=$$?; [ $$code -eq 5 ] && exit 0; exit $$code; }
@echo ""
@echo "test-changed runs only impacted tests. Full run: 'make test' (nightly/CI run the full suite)."
test-frontend: ## Run frontend unit tests (Vitest)
@echo ""
@echo "=== Frontend Tests ==="
cd frontend && bunx vitest run
test-backend: ## Run backend tests
@echo ""
@echo "=== Backend Tests ==="
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run pytest tests/ -v
# Plugin test targets. Each plugin lives at
# plugins/adaptive-learner-plugin-<name>/ and is installed as an
# editable path-dep of the backend (so the backend's poetry env
# already has every plugin's source on its import path). The
# per-plugin pytest run uses that same env via its absolute Python
# binary; the plugin doesn't need its own poetry env / lock.
test-plugins: test-plugin-assessment test-plugin-ai-anthropic test-plugin-ai-openai test-plugin-ai-gemini test-plugin-session test-plugin-tracking test-plugin-tools test-plugin-gamification test-plugin-anki test-plugin-notebooklm test-plugin-learning-repo test-plugin-content-loader test-plugin-missions ## Run every plugin's own test suite (incl. content-loader v1.27.0)
@echo ""
@echo "=== All plugin tests complete ==="
# Resolve the backend's poetry venv python at Makefile parse time
# (NOT inside a recipe — the recipe's CWD may have already cd'd
# into a plugin subdir). Override with PLUGIN_PYTHON=… on the
# command line if your env layout differs.
PLUGIN_PYTHON ?= $(shell cd backend && poetry env info -p)/bin/python
test-plugin-assessment: ## Assessment plugin: 12 questions DE+EN, profile calc
@echo ""
@echo "=== Plugin: assessment ==="
cd plugins/adaptive-learner-plugin-assessment && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-ai-anthropic: ## ai-anthropic plugin: Claude provider for ai_complete (mocked SDK)
@echo ""
@echo "=== Plugin: ai-anthropic ==="
cd plugins/adaptive-learner-plugin-ai-anthropic && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-ai-openai: ## ai-openai plugin: GPT provider for ai_complete (mocked SDK)
@echo ""
@echo "=== Plugin: ai-openai ==="
cd plugins/adaptive-learner-plugin-ai-openai && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-ai-gemini: ## ai-gemini plugin: Gemini provider for ai_complete (mocked SDK)
@echo ""
@echo "=== Plugin: ai-gemini ==="
cd plugins/adaptive-learner-plugin-ai-gemini && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-session: ## session plugin: 7-step prompts, method-switch rec, 4 routes
@echo ""
@echo "=== Plugin: session ==="
cd plugins/adaptive-learner-plugin-session && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-tracking: ## tracking plugin: ProgressCommit writer + summary aggregator, 2 routes
@echo ""
@echo "=== Plugin: tracking ==="
cd plugins/adaptive-learner-plugin-tracking && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-tools: ## tools plugin: static external-tool recommendations per profile, 1 route
@echo ""
@echo "=== Plugin: tools ==="
cd plugins/adaptive-learner-plugin-tools && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-gamification: ## gamification plugin: XP calculator + level curve (v1.16.0 / Phase 29A)
@echo ""
@echo "=== Plugin: gamification ==="
cd plugins/adaptive-learner-plugin-gamification && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-anki: ## anki plugin: card-extraction parser + vocabulary transform (v1.17.0 / Phase 30B)
@echo ""
@echo "=== Plugin: anki ==="
cd plugins/adaptive-learner-plugin-anki && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-notebooklm: ## notebooklm plugin: question + study guide generators (v1.19.0 / Phase 32)
@echo ""
@echo "=== Plugin: notebooklm ==="
cd plugins/adaptive-learner-plugin-notebooklm && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-learning-repo: ## learning-repo plugin: Article-3 Git-backed Learning Repository (Phase 42 / BL-30)
@echo ""
@echo "=== Plugin: learning-repo ==="
cd plugins/adaptive-learner-plugin-learning-repo && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-content-loader: ## content-loader plugin: downloads + caches lesson sets (Phase 43 / EXP-002)
@echo ""
@echo "=== Plugin: content-loader ==="
cd plugins/adaptive-learner-plugin-content-loader && $(PLUGIN_PYTHON) -m pytest tests/ -q
test-plugin-missions: ## missions plugin: daily mission catalog + generator (EXP-010 / Phase 56)
@echo ""
@echo "=== Plugin: missions ==="
cd plugins/adaptive-learner-plugin-missions && $(PLUGIN_PYTHON) -m pytest tests/ -q
# --- Coverage (heavy, opt-in; CI runs this on every push) ---
test-coverage: test-coverage-backend test-coverage-frontend ## Run ALL tests with coverage (slow; prefer CI)
@echo ""
@echo "=== All coverage runs complete ==="
test-coverage-backend: ## Backend coverage report (htmlcov/)
@echo ""
@echo "=== Backend Coverage ==="
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run pytest tests/ --cov=app --cov-report=html --cov-report=term
test-coverage-frontend: ## Frontend coverage report (coverage/)
@echo ""
@echo "=== Frontend Coverage ==="
cd frontend && bun run test:coverage
# --- TDD inner-loop helpers (.claude/rules/tdd.md) ---
# Support the Red-Green-Refactor loop. The bundled lint+type+test GREEN
# gate already exists as `test-fast` (backend ruff+mypy+pytest, frontend
# tsc+vitest) — these only add what it does not cover: a targeted
# single-test run and a Vitest watch loop. Coverage lives in
# test-coverage-{backend,frontend}; a backend watch is intentionally
# omitted (no pytest-watch/ptw in the venv, and adding one is a separate
# Library-First decision).
test-one: ## Run ONE test by path with the real exit code (TDD). Usage: make test-one TEST=backend/tests/test_x.py | frontend/src/x.test.tsx | plugins/<pkg>/tests/test_y.py | e2e/<spec>
ifndef TEST
$(error TEST= is required, e.g. make test-one TEST=backend/tests/test_users.py (also: frontend/src/foo.test.tsx, plugins/<pkg>/tests/test_y.py, e2e/<spec>))
endif
@t='$(TEST)'; \
case "$$t" in \
e2e/*) \
rel="$${t#e2e/}"; \
case "$$t" in \
e2e/manual-automation/*) cfg=playwright.manual.config.ts ;; \
e2e/dexie/*) cfg=playwright.dexie.config.ts ;; \
e2e/visual/*) cfg=playwright.visual.config.ts ;; \
*) cfg=playwright.config.ts ;; \
esac; \
echo "=== playwright ($$cfg): $$rel ==="; \
cd e2e && npx playwright test --config="$$cfg" "$$rel" ;; \
backend/*) \
echo "=== pytest: $$t ==="; \
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run pytest "$${t#backend/}" ;; \
plugins/*) \
d="$$(printf '%s' "$$t" | cut -d/ -f1-2)"; f="$${t#*/*/}"; \
echo "=== pytest (plugin $$d): $$f ==="; \
cd "$$d" && $(PLUGIN_PYTHON) -m pytest "$$f" ;; \
frontend/*) \
echo "=== vitest: $$t ==="; \
cd frontend && bunx vitest run "$${t#frontend/}" ;; \
src/*|*.test.ts|*.test.tsx) \
echo "=== vitest: $$t ==="; \
cd frontend && bunx vitest run "$$t" ;; \
*) \
echo "Cannot route TEST=$$t"; \
echo "Expected: backend/<path> | frontend/<path> | src/<path> | *.test.ts(x) | plugins/<pkg>/<path> | e2e/<spec>"; \
exit 2 ;; \
esac
test-watch: ## Frontend Vitest in watch mode (TDD inner loop). Optional TEST= to scope, e.g. make test-watch TEST=src/lib/x.test.ts
cd frontend && bunx vitest $(TEST)
tdd-help: ## Print the Red-Green-Refactor workflow (.claude/rules/tdd.md) + which targets to use
@echo "TDD — Red-Green-Refactor (.claude/rules/tdd.md)"
@echo ""
@echo " 1. RED write the failing test FIRST, watch it fail:"
@echo " make test-one TEST=<path> # one test, real exit code"
@echo " make test-watch # or keep Vitest watching"
@echo " 2. GREEN minimal code until that test passes:"
@echo " make test-one TEST=<path>"
@echo " 3. REFACTOR clean up, keep it green:"
@echo " make test-fast # backend ruff+mypy+pytest + frontend tsc+vitest"
@echo ""
@echo " Full suite before pushing: make test (CI-mirror gate: make test-fast)"
# --- Mutation testing (Stryker) ---
stryker: ## Frontend mutation testing — full src/lib + src/hooks + src/api (slow; nightly/manual)
@echo ""
@echo "=== Frontend Mutation Testing (Stryker) ==="
cd frontend && bunx stryker run
stryker-quick: ## Scoped Stryker run, e.g. make stryker-quick MUTATE="src/lib/token-diff.ts"
@echo ""
@echo "=== Frontend Mutation Testing (Stryker, scoped: $(MUTATE)) ==="
cd frontend && bunx stryker run --mutate "$(MUTATE)"
# --- Blocker / archival ---
check-blockers: ## Ping upstream sources for every BLOCKED item in docs/backlog.md
@bash scripts/check-blockers.sh
archive-task: ## Move completed [x] tasks out of ROADMAP/backlog into docs/roadmap-archive/YYYY-MM.md (interactive)
@python3 scripts/archive_completed_task.py
archive-task-dry: ## Same as archive-task but writes nothing (preview)
@python3 scripts/archive_completed_task.py --dry-run
# --- Git Hooks ---
install-hooks: ## Install scripts/git-hooks/* into .git/hooks
@mkdir -p .git/hooks
@for hook in scripts/git-hooks/*; do \
name=$$(basename $$hook); \
ln -sf ../../$$hook .git/hooks/$$name; \
echo "linked .git/hooks/$$name -> $$hook"; \
done
# --- Type Checking ---
check-file-sizes: ## Cohesion watcher: warn >500, error >1000 lines (ratchet via .filesize-baseline)
bash scripts/check-file-sizes.sh
check-css-size: ## CSS inflow-stop: global.css may only shrink (ratchet via .css-size-baseline, #1467)
bash scripts/check-css-size.sh
css-identity-ref: ## EXP-044 concern-split (#1655): build + store the byte-identity reference (run on the PRE-split state)
bash scripts/check-css-identity.sh ref
css-identity-check: ## EXP-044 concern-split (#1655): build + byte-compare the emitted CSS against the stored reference
bash scripts/check-css-identity.sh check
check-dead-classnames: ## Usage-side gates: dead classNames (#1491) + render-unstyled archetype (all-dead className, #1892)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie (Tailwind oracle) ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
python3 scripts/check-dead-classnames.py
@echo ""
python3 scripts/check-dead-classnames.py --unstyled
audit-legacy-conflicts: ## EXP-044 pre-wrap conflict audit (analysis only, NO gate; Refs #1485). BLOCKS="--block A-B:Label ..." or default --wrapped
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie (Tailwind oracle) ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
python3 scripts/check-legacy-wrap-conflicts.py $(if $(BLOCKS),$(BLOCKS),--wrapped)
check-complexity: ## Complexity watcher (warn-only): radon (Python) + eslint complexity (TS)
bash scripts/check-complexity.sh
check-directory-size: ## God-folder watcher (warn-only): >15 flat source files per dir (#809)
bash scripts/check-directory-size.sh
check-directory-size-gate: ## God-folder ratchet gate: fail on a NEW oversized dir vs .dirsize-baseline
bash scripts/check-directory-size.sh --gate
check-folder-size: ## Folder-size guard (gate): alias of check-directory-size-gate (#809)
bash scripts/check-folder-size.sh
check-folder-size-update: ## Folder-size: show current offenders to whitelist in .dirsize-baseline
@echo "Current flat-source-file counts over threshold (edit .dirsize-baseline to whitelist):"
@bash scripts/check-folder-size.sh --warn
check-complexity-gate: ## Complexity ratchet gate (#407): fail on new/regressed offenders vs .complexity-baseline
bash scripts/check-complexity.sh --gate
check-complexity-gate-update: ## Regenerate .complexity-baseline from current offenders (ratchet may only shrink)
bash scripts/check-complexity.sh --update-baseline
verify-theme: ## Theme/token gate: Python token-matrix + WCAG contrast gate, then the Vitest no-hardcoded-colors / parity / contrast guards (#1169)
@echo ""
@echo "=== verify-theme: token completeness + undefined refs + WCAG contrast (Python, stdlib) ==="
python3 scripts/verify_theme.py --enforce
@echo ""
@echo "=== verify-theme: no-hardcoded-colors + token-parity + contrast (Vitest guards) ==="
cd frontend && bunx vitest run src/styles/no-hardcoded-colors.test.ts src/styles/contrast.test.ts src/styles/themes/themes.test.ts
verify-theme-baseline-update: ## Re-record .theme-baseline.json from current violations (ratchet only shrinks unless --allow-baseline-growth)
python3 scripts/verify_theme.py --update-baseline
check-types: check-types-backend check-types-frontend ## Run all type checks
check-types-backend: ## Run mypy on backend
@echo ""
@echo "=== mypy Backend ==="
cd backend && poetry env use python3.12 -q 2>/dev/null; poetry run mypy app/
check-types-frontend: ## Run tsc --noEmit on frontend
@echo ""
@echo "=== TypeScript Frontend ==="
cd frontend && bunx tsc --noEmit
# --- E2E Tests ---
test-e2e: ## Run Playwright e2e tests (starts servers automatically)
cd e2e && npx playwright test
test-e2e-ui: ## Run e2e tests with Playwright UI
cd e2e && npx playwright test --ui
# Critical-flow smoke (#1177): the ``smoke`` Playwright project
# (e2e/smoke/, defined in e2e/playwright.config.ts) covering the core
# user journeys. It uses the default config's webServer, which
# auto-starts the backend (uvicorn) + frontend (bun run dev) in API mode
# — no build step (distinct from the Dexie-mode ``test-dexie-smoke``
# gate, which builds VITE_STORAGE_MODE=dexie). This is the command the
# release-test gate references.
test-e2e-smoke: ## Playwright smoke project — critical user flows (auto-starts backend+frontend dev servers)
cd e2e && npx playwright test --project=smoke
test-e2e-smoke-retries: ## Smoke project in CI mode (--retries=1, against flake)
cd e2e && npx playwright test --project=smoke --retries=1
# DEXIE-MODE-RELEASE-GATE-01 — builds the frontend in
# ``VITE_STORAGE_MODE=dexie`` (matching the GitHub Pages
# deployment) and runs the dexie-mode Playwright spec against
# the static preview, with NO backend running. Catches the
# class of bug where a feature works in API mode but crashes
# in Dexie mode (Phase 42 / Learning Repository, May 2026).
# Exits non-zero if any error toast appears or any route
# crashes on its primary content render.
test-dexie-smoke: ## Dexie-mode release gate (build + Playwright preview-mode smoke)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Running Dexie-mode Playwright smoke ==="
cd e2e && npx playwright test --config=playwright.dexie.config.ts
# WebKit engine gate (#1834). Catches iOS/Safari CSS-ENGINE layout bugs
# that the Chromium gates structurally cannot — e.g. the lesson-footer
# Pause/Next overlap that WebKit produces under justify-content:
# space-between on overflow while Blink clamps. Serves the same
# Dexie/GH-Pages-shape build under Playwright's `webkit` browser with an
# emulated iPhone 12 profile. NOT in the default gate chain: the WebKit
# browser must be installed first (`cd e2e && npx playwright install-deps
# webkit && npx playwright install webkit`), which needs egress to the
# Playwright browser CDN.
test-webkit: ## WebKit lesson-layout gate (#1834); needs `playwright install webkit`
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Running WebKit layout gate (iPhone 12 profile) ==="
cd e2e && npx playwright test --config=playwright.webkit.config.ts
test-manual-automation: ## Automated manual-test-plan suite (#616; build dexie + Playwright)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Running manual-test-plan automation (#616) ==="
cd e2e && npx playwright test --config=playwright.manual.config.ts
test-visual: ## Visual regression (build dexie + Playwright screenshot matrix)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Running visual-regression screenshots ==="
cd e2e && npx playwright test --config=playwright.visual.config.ts
test-visual-update: ## Regenerate the visual baseline (REVIEW the diff before committing)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Updating visual-regression baseline (review before committing!) ==="
cd e2e && npx playwright test --config=playwright.visual.config.ts --update-snapshots
capture-screenshots: ## Capture/update per-feature screenshot baselines (REVIEW the diff before committing)
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Capturing per-feature screenshots (review before committing!) ==="
cd e2e && npx playwright test --config=playwright.features.config.ts --update-snapshots
verify-screenshots: ## Verify per-feature screenshots against the committed baselines
@echo "=== Building frontend with VITE_STORAGE_MODE=dexie ==="
cd frontend && VITE_STORAGE_MODE=dexie bun run build
@echo ""
@echo "=== Verifying per-feature screenshots ==="
cd e2e && npx playwright test --config=playwright.features.config.ts
capture-blog-screenshots: ## Capture lesson-creator screenshots for the engine blog series (writes only, no baseline)
@echo "=== Building frontend (needs the backend too: the UI language comes from /api/i18n) ==="
cd frontend && bun run build
@echo ""
@echo "=== Capturing blog screenshots (DOCS_LANG=$(or $(DOCS_LANG),en)) ==="
cd e2e && DOCS_LANG=$(or $(DOCS_LANG),en) npx playwright test --config=playwright.docs.config.ts
@echo ""
@echo "Output: e2e/docs/output/$(or $(DOCS_LANG),en)/ (git-ignored; copy what you need into the article)"
# --- Version sync ---
sync-versions: ## Propagate backend/pyproject.toml version to all subsystems
@python3 scripts/sync_versions.py
sync-versions-dry: ## Show what sync-versions would change without writing
@python3 scripts/sync_versions.py --dry-run
sync-versions-check: ## Exit non-zero if any subsystem version drifts from canonical
@python3 scripts/sync_versions.py --check
sync-i18n: ## Regenerate frontend/src/data/i18n/*.json from backend YAML catalogs
@python3 scripts/sync_i18n_to_frontend.py
verify-i18n-scripts: ## Script-sanity lint: de substitute spelling + el/hi latin transliteration (#1755, hard gate)
@python3 scripts/verify_i18n_scripts.py
i18n-quality-check: ## LLM translation quality-check over the 8 machine-translated catalogs (#1296; needs an Anthropic key). Pass args via ARGS="--langs ja --limit 50"
@cd backend && poetry run python ../scripts/i18n_quality_check.py $(ARGS)
i18n-quality-check-dry: ## i18n quality-check coverage/cache stats only, no API calls (#1296). ARGS="--langs ja"
@cd backend && poetry run python ../scripts/i18n_quality_check.py --dry-run $(ARGS)
i18n-csv-export: ## Per-language CSV review export: de | target | LLM verdict | correction (#1296). ARGS="--langs ja --flagged-only"
@cd backend && poetry run python ../scripts/export_i18n_csv.py $(ARGS)
i18n-import-corrections: ## Surgically write i18n corrections back into the YAML (#1296). Diacritics-only from cache: ARGS="--langs fr es --source cache --verdict missing_diacritics". Run make sync-i18n after.
@cd backend && poetry run python ../scripts/import_i18n_corrections.py $(ARGS)
sync-plugin-config: ## Regenerate frontend/src/data/plugin-config/*.json from backend/config/plugins/*.yaml (Phase 49 / v1.32.0)
@python3 scripts/sync_plugin_config_to_frontend.py
sync-schema-mirror: ## Refresh the bundle-local ajv schema mirror (TS types now come from learn-content-engine, D1b #1521)
@cd frontend && node scripts/sync-schema-mirror.mjs
sync-schema: ## Refresh the engine schema mirror, then regenerate the derived artefacts + structural Pydantic layer + ajv mirror (EXP-039 / D3b, engine-canonical)
@python3 scripts/sync_schema_mirror_from_engine.py
@cd backend && poetry run python ../scripts/generate_lesson_schema.py
@cd backend && poetry run python ../scripts/generate_pydantic_models.py
@cd frontend && node scripts/sync-schema-mirror.mjs
sync-schema-check: ## Exit non-zero if the schema mirror, generated artefacts or structural Pydantic layer drift from the pinned engine (EXP-039 / D3b)
@python3 scripts/sync_schema_mirror_from_engine.py --check
@cd backend && poetry run python ../scripts/generate_lesson_schema.py --check
@cd backend && poetry run python ../scripts/generate_pydantic_models.py --check
@cd frontend && node scripts/sync-schema-mirror.mjs --check
engine-parity-check: ## Exit non-zero if schema/*.json differs from the pinned learn-content-engine release (mirror decoupling; network)
@python3 scripts/check_engine_schema_parity.py
sync-help: ## Regenerate frontend/src/data/help/*.json from backend/config/help YAML files (Phase 38)
@python3 scripts/sync_help_to_frontend.py
sync-praise: ## Regenerate frontend/src/data/praise/*.json from backend/config/praise/*.yaml (EXP-008 / Phase 55A)
@python3 scripts/sync_praise_to_frontend.py
sync-missions: ## Regenerate frontend/src/data/missions/templates.json from the missions plugin catalog (EXP-010 / Phase 56)
@python3 scripts/sync_missions_to_frontend.py
# --- Production (Docker) ---
prod: ## Start production via Docker Compose
docker compose -f docker-compose.prod.yml up --build -d
prod-down: ## Stop production
docker compose -f docker-compose.prod.yml down
prod-logs: ## Show production logs
docker compose -f docker-compose.prod.yml logs -f
# --- Launcher (desktop, Docker-based) ---
# Per-run capture of the desktop launcher's --debug output for later
# analysis. Each run writes two timestamped files under launcher/logs/
# (gitignored via the launcher-*.log pattern):
# launcher-<ts>.log combined stdout/stderr (the live --debug stream)
# launcher-<ts>-debug.log archived copy of the CWD launcher-debug.log
# Pass extra flags via ARGS, e.g. `make launcher-test ARGS="--status"`.
launcher-test: ## Run the desktop launcher in --debug; capture timestamped logs to launcher/logs/ for analysis
@mkdir -p launcher/logs
@ts=$$(date +%Y%m%d-%H%M%S); \
log="logs/launcher-$$ts.log"; \
echo "Launcher debug run -> launcher/$$log (close the window or Ctrl-C to stop)"; \
cd launcher && poetry run python -m adaptive_learner_launcher --debug $(ARGS) 2>&1 | tee "$$log"; \
cp -f launcher-debug.log "logs/launcher-$$ts-debug.log" 2>/dev/null || true; \
echo "Saved: launcher/logs/launcher-$$ts.log + launcher/logs/launcher-$$ts-debug.log"
launcher-install: ## Set up the launcher venv (with the tray extra)
cd launcher && poetry install --extras tray
launcher-status: ## Print the app state (headless)
cd launcher && poetry run python -m adaptive_learner_launcher --status
launcher-check: ## Check Docker (headless)
cd launcher && poetry run python -m adaptive_learner_launcher --check
launcher-stop: ## Stop the app (headless)
cd launcher && poetry run python -m adaptive_learner_launcher --stop
launcher-uninstall: ## Remove the app containers/images (headless)
cd launcher && poetry run python -m adaptive_learner_launcher --uninstall
launcher-cleanup: ## Remove stale leftover artifacts (headless)
cd launcher && poetry run python -m adaptive_learner_launcher --cleanup
launcher-port: ## Change the host port (usage: make launcher-port PORT=9000)
@test -n "$(PORT)" || { echo "Usage: make launcher-port PORT=<1024-65535>"; exit 1; }
cd launcher && poetry run python -m adaptive_learner_launcher --port $(PORT)
launcher-version: ## Print the launcher version
cd launcher && poetry run python -m adaptive_learner_launcher --version
launcher-logs: ## Show the most recent launcher debug logs (launcher/logs/)
@ls -lt launcher/logs/*.log 2>/dev/null | head -5 || echo "No logs yet"
@echo "---"
@latest=$$(ls -t launcher/logs/*.log 2>/dev/null | head -1); \
if [ -n "$$latest" ]; then tail -50 "$$latest"; else echo "No log file present"; fi
launcher-pytest: ## Run the launcher unit tests
cd launcher && poetry run pytest tests/ -v
launcher-update-package: ## Update the docker-app-launcher PyPI package
cd launcher && poetry update docker-app-launcher
launcher-venv-fix: ## Enable system-site-packages (system gi / tray) in the launcher venv
@venv=$$(cd launcher && poetry env info -p) && \
sed -i 's/include-system-site-packages = false/include-system-site-packages = true/' "$$venv/pyvenv.cfg" && \
echo "system-site-packages enabled for $$venv"
launcher-tray-check: ## Check the tray dependencies (pystray / Pillow / gi)
@cd launcher && poetry run python -c 'import importlib.util as u; \
checks=[("pystray","pystray","poetry install --extras tray"),("Pillow","PIL","poetry install --extras tray"),("gi (PyGObject)","gi","make launcher-venv-fix, or apt install python3-gi gir1.2-ayatanaappindicator3-0.1")]; \
[print(f"{n}: OK" if u.find_spec(m) else f"{n}: MISSING ({h})") for n,m,h in checks]'
# --- Documentation (MkDocs) ---
docs-install: ## Install MkDocs dependencies (separate venv in docs/)
cd docs && poetry install
docs-build: ## Build static documentation site
cd docs && poetry run python ../scripts/generate_mkdocs_nav.py
cd docs && poetry run mkdocs build -f ../mkdocs.yml
docs-serve: ## Serve documentation locally (hot-reload)
cd docs && poetry run python ../scripts/generate_mkdocs_nav.py
cd docs && poetry run mkdocs serve -f ../mkdocs.yml
sync-mkdocs-nav: ## Regenerate mkdocs.yml nav blocks from docs/help/_meta.yaml
cd docs && poetry run python ../scripts/generate_mkdocs_nav.py
verify-mkdocs-nav: ## Check mkdocs.yml is in sync with docs/help/_meta.yaml (CI-friendly)
cd docs && poetry run python ../scripts/generate_mkdocs_nav.py --check
# --- Documentation verification (drift detection) ---
verify-docs: ## Verify documentation for drift (version/counts/features/help/i18n/themes; stdlib only)
@python3 scripts/verify_docs.py
verify-docs-fix: ## Best-effort auto-fix of mechanical docs drift (version badges, counts, i18n sync)
@python3 scripts/verify_docs.py --fix
check-mkdocs-orphans: ## List help .md files orphaned from / dangling in mkdocs.yml nav
@python3 scripts/verify_docs.py --check mkdocs
verify-docs-discipline: ## Full docs gate: drift verifier + mkdocs nav sync (release-blocking)
@$(MAKE) verify-docs
@$(MAKE) verify-mkdocs-nav
docs-checklist: ## Print the post-release docs to-do list. Usage: make docs-checklist VERSION=X.Y.Z
ifndef VERSION
$(error VERSION is required, e.g. make docs-checklist VERSION=1.41.0)
endif
@python3 scripts/generate_docs_checklist.py $(VERSION)
# --- Plugin lockfile discipline ---
lock-all-plugins: ## Re-lock every plugin's poetry.lock (after a shared-dep pin bump)
@for d in plugins/adaptive-learner-plugin-*/; do \
echo ""; echo "=== $$(basename $$d) ==="; \
cd "$$d" && poetry lock && cd - >/dev/null; \
done
verify-plugin-locks: ## Detect drift between each plugin's pyproject.toml and its poetry.lock
@drift=0; \
for d in plugins/adaptive-learner-plugin-*/; do \
name=$$(basename $$d); \
out=$$(cd "$$d" && poetry install --dry-run --no-interaction --no-ansi 2>&1 | head -3); \
if echo "$$out" | grep -q "changed significantly"; then \
echo "DRIFT: $$name (run \`make lock-all-plugins\`)"; \
drift=1; \
fi; \
done; \
if [ $$drift -eq 1 ]; then \
echo ""; echo "ERROR: at least one plugin pyproject.toml drifts from its poetry.lock."; \
exit 1; \
fi; \
echo "OK: all plugin pyproject.toml/poetry.lock pairs in sync."
# --- Security (local pre-PR checks; mirror the nightly Security Scan) ---
# These targets mirror .github/workflows/security-scan.yml so a green local
# run predicts a green nightly scan. The CI workflow stays the source of
# truth (warn-only there); these are the convenient local pre-flight. No
# accepted-advisory / ignore list exists in CI, so none is applied here.
# pip-audit is already a backend dev-dep; bandit is installed inline into
# the backend venv (mirrors CI's `pip install bandit`), no lockfile change.
audit-backend: ## pip-audit the backend venv (incl. plugin path-deps), mirrors the nightly scan (warn-only)
@echo "=== pip-audit (backend venv, incl. plugin path-deps) ==="
@cd backend && poetry run pip-audit --skip-editable --progress-spinner=off || true
audit-frontend: ## bun audit the frontend dependencies, mirrors the nightly scan (warn-only)
@echo "=== bun audit (frontend) ==="
@cd frontend && bun audit || true
bandit-backend: ## bandit SAST over app + plugins + scripts (MEDIUM+ severity & confidence), mirrors the nightly scan (warn-only)
@echo "=== bandit (app + plugins + scripts; MEDIUM+ severity & confidence) ==="
@cd backend && poetry run python -m pip install -q bandit >/dev/null 2>&1 || \
echo "WARN: could not install bandit (offline?); skipping the bandit run."
@cd backend && poetry run bandit -r app ../plugins ../scripts -ll -ii \
-x '*/tests/*,*/test_*.py' || true
security-backend: bandit-backend audit-backend ## Backend security sweep: bandit SAST + pip-audit deps (warn-only, mirrors the nightly scan)
@echo "Backend security sweep complete (warn-only). The nightly Security Scan is the source of truth."
check-security: ## Blocking dependency gate: pip-audit + bun audit fail on HIGH/CRITICAL (local pre-PR check)
@echo "=== check-security: pip-audit (backend, fails on any known vuln) ==="
@cd backend && poetry run pip-audit --skip-editable --progress-spinner=off
@echo "=== check-security: bun audit --audit-level=high (frontend) ==="
@cd frontend && bun audit --audit-level=high
@echo "check-security passed: no high/critical dependency vulnerabilities."
circular-deps: ## Circular-dependency check over frontend/src (madge via the existing check:circular script)
@cd frontend && bun run check:circular
# --- Release ---
# Aggregate Makefile targets for the release-workflow.md mechanical
# steps (Step 1 state capture, Step 4b dep currency, Step 5 test
# gate, Step 6 builds, Step 7 tag+push, Step 8 GitHub release).
# Composes existing tools; does NOT replace the canonical
# sync-versions / verify_version_pins chain. The LLM/human-content
# steps (CHANGELOG draft, per-release notes composition, CLAUDE.md
# prose, journal entry) stay manual - release narrative is the
# human/LLM value-add.
release-state: ## Print state-capture report (release-workflow.md Step 1)
@echo "=== Latest tags ==="
@git tag --sort=-creatordate | head -5
@LAST_TAG=$$(git describe --tags --abbrev=0); \
echo ""; \
echo "=== Commits since $$LAST_TAG ==="; \
git log $$LAST_TAG..HEAD --oneline --no-merges | head -100; \
echo ""; \
echo "Commit count: $$(git log $$LAST_TAG..HEAD --oneline --no-merges | wc -l)"; \
echo ""; \
echo "=== Diff stat $$LAST_TAG..HEAD ==="; \
git diff $$LAST_TAG..HEAD --stat | tail -1
@echo ""
@echo "=== Current canonical version ==="
@grep "^version" backend/pyproject.toml
release-outdated: ## Dependency currency check across all surfaces (release-workflow.md Step 4b)
@echo "=== Backend (poetry show --outdated) ==="
@cd backend && COLUMNS=200 poetry show --outdated --no-ansi 2>&1 | grep -v "^adaptive-learner-plugin-" || true
@echo ""
@echo "=== Launcher (poetry show --outdated) ==="
@cd launcher && COLUMNS=200 poetry show --outdated --no-ansi 2>&1 || true
@echo ""
@echo "=== Frontend (bun outdated) ==="
@cd frontend && bun outdated 2>&1 || true
@echo ""
@echo "Reminder: apply routine bumps (patch+minor within same major) via 'poetry update <allowlist>' (never bare)."
release-test: ## Aggregate pre-tag test gate (release-workflow.md Step 5)
@$(MAKE) test
@echo ""
@echo "=== Frontend bun run build ==="
@cd frontend && bun run build
@echo ""
@echo "=== Frontend bun run test (vitest) ==="
@cd frontend && bun run test
@echo ""
@echo "=== Theme/token gate (verify-theme, Python-only; #1185) ==="
@echo "Vitest theme guards already ran via 'make test' + 'bun run test' above;"
@echo "here we only run the unique stdlib token-matrix + WCAG gate."
@python3 scripts/verify_theme.py --enforce
@echo ""
@echo "=== Documentation drift gate (verify-docs-discipline) ==="
@$(MAKE) verify-docs-discipline
@echo ""
@echo "=== Subsystem lock-step (sync-versions --check) ==="
@$(MAKE) sync-versions-check
@echo ""
@echo "=== Lesson-schema drift gate (sync-schema --check, EXP-039) ==="
@$(MAKE) sync-schema-check
@echo ""
@echo "=== Plugin lockfile drift (verify-plugin-locks) ==="
@$(MAKE) verify-plugin-locks
@echo ""
@echo "=== Dexie-mode release gate (DEXIE-MODE-RELEASE-GATE-01) ==="
@$(MAKE) test-dexie-smoke
@echo ""
@echo "=== Manual-test-plan automation (#616) ==="
@$(MAKE) test-manual-automation
@echo ""
@echo "Release test gate green. Run the full Playwright smoke separately: make test-e2e-smoke"
release-build: ## Build release artifacts (release-workflow.md Step 6)
@PACKAGE_MODE=$$(grep "^package-mode" backend/pyproject.toml | head -1 | awk '{print $$3}' | tr -d ' '); \
if [ "$$PACKAGE_MODE" = "false" ]; then \
echo "=== Backend poetry build: SKIPPED (package-mode=false) ==="; \
else \
echo "=== Backend poetry build ==="; \
cd backend && poetry build; \
fi
@echo ""
@echo "=== Frontend bun run build ==="
@cd frontend && bun run build
release-discover: ## Discover version-shape literals outside the sync-versions target list (advisory)
@echo "=== Version-literal discovery ==="
@echo "Scanning for X.Y.Z literals in version-assignment contexts"
@echo "outside the known sync-versions targets + Tier-4 manual files."
@echo ""
@output=$$(bash scripts/discover_version_literals.sh); \
if [ -z "$$output" ]; then \
echo "Clean. No unknown version literals."; \
else \
echo "$$output"; \
fi
release-tag: ## Tag + push main + push tag. Usage: make release-tag VERSION=X.Y.Z
ifndef VERSION
$(error VERSION is required, e.g. make release-tag VERSION=1.25.0)
endif
@echo "=== Pre-tag verification (verify_version_pins.sh) ==="
@bash scripts/verify_version_pins.sh $(VERSION)
@echo ""
@echo "=== Creating tag v$(VERSION) ==="
@git tag -a v$(VERSION) -m "Release v$(VERSION)"
@echo "=== Pushing main ==="
@git push origin main
@echo "=== Pushing tag v$(VERSION) ==="
@git push origin v$(VERSION)
@echo ""
@echo "=== Post-release documentation checklist ==="
@python3 scripts/generate_docs_checklist.py $(VERSION) || true
@echo ""
@echo "Tag pushed. Next: make release-publish VERSION=$(VERSION)"
release-publish: ## Create GitHub Release from changelog/releases/vX.Y.Z.md. Usage: make release-publish VERSION=X.Y.Z
ifndef VERSION
$(error VERSION is required, e.g. make release-publish VERSION=1.25.0)
endif
@if [ ! -f "changelog/releases/v$(VERSION).md" ]; then \
echo "ERROR: changelog/releases/v$(VERSION).md missing."; \
echo "Draft the per-release notes file first (release-workflow.md Step 3)."; \
exit 1; \
fi
@echo "=== Creating GitHub Release v$(VERSION) ==="
gh release create v$(VERSION) \
--title "Adaptive Learner v$(VERSION)" \
--notes-file changelog/releases/v$(VERSION).md
# --- Gitflow release branch flow (#334) ---
release-prepare: ## Gitflow: cut release/$(VERSION) from develop. Usage: make release-prepare VERSION=X.Y.Z
ifndef VERSION
$(error VERSION is required, e.g. make release-prepare VERSION=1.77.0)