qrusty 0.21.1

A trusty priority queue server built with Rust
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
.PHONY: all build doc machete test examples validate-deps smoke-test smoke-test-constrained smoke-test-memory
.PHONY: show-docs
.PHONY: build-qrusty publish publish-docker
.PHONY: traceability traceability-report
.PHONY: fmt fmt-check lint
.PHONY: fmt-rust fmt-check-rust lint-rust
.PHONY: fmt-py fmt-check-py lint-py
.PHONY: fmt-node fmt-check-node lint-node
.PHONY: fmt-yaml fmt-check-yaml lint-yaml
.PHONY: fmt-md fmt-check-md lint-md
.PHONY: fmt-toml fmt-check-toml lint-toml
.PHONY: coverage coverage-report coverage-missing coverage-html coverage-summary-json coverage-clean
.PHONY: pre-merge upgrade-deps upgrade-deps-rust upgrade-deps-node
.PHONY: audit-deps audit-deps-unused audit-deps-outdated audit-deps-security
.PHONY: bench bench-storage bench-http bench-smoke bench-smoke-storage bench-smoke-http
.PHONY: bench-memory bench-http-memory bench-ws-memory
.PHONY: bench-smoke-memory bench-smoke-http-memory bench-smoke-ws-memory
.PHONY: test-live-pyclient test-selenium test-live-nodeclient test-integration-all

PY_RUFF ?= uv tool run ruff
PY_MYPY ?= uv tool run --with types-requests mypy
NODE_PRETTIER ?= npx -y prettier@3.3.3
TAPLO ?= taplo
YAMLLINT ?= uv tool run yamllint
MARKDOWNLINT ?= npx -y markdownlint-cli2

NPM_PUBLISH_ARGS :=
ifdef NPM_OTP
NPM_PUBLISH_ARGS += --otp=$(NPM_OTP)
endif

# Docker platform configuration
DOCKER_PLATFORMS := linux/amd64,linux/arm64

all: build doc machete test examples

build:
	cargo build

doc:
	cargo doc

machete:
	cargo machete

test:
	cargo test --features test-helpers

coverage:
	cargo llvm-cov --workspace --all-features

# Wraps scripts/coverage.py — runs `cargo llvm-cov` + `cargo test --list`
# and writes a markdown report styled to match the traceability report.
coverage-report:
	@echo "Generating coverage report to docs/coverage_report.md..."
	@if command -v uv >/dev/null 2>&1; then \
		uv run --python .venv/bin/python scripts/coverage.py; \
	else \
		. .venv/bin/activate && python scripts/coverage.py; \
	fi
	@$(NODE_PRETTIER) --write --log-level=silent docs/coverage_report.md
	@echo "Report saved to docs/coverage_report.md"

coverage-missing:
	@mkdir -p target/llvm-cov
	cargo llvm-cov --workspace --all-features --text --show-missing-lines --output-path target/llvm-cov/missing.txt
	@echo "Saved: target/llvm-cov/missing.txt"

coverage-html:
	cargo llvm-cov --workspace --all-features --html
	@echo "Open: target/llvm-cov/html/index.html"

coverage-summary-json:
	@mkdir -p target/llvm-cov
	cargo llvm-cov --workspace --all-features --json --summary-only --output-path target/llvm-cov/summary.json
	@echo "Saved: target/llvm-cov/summary.json"

coverage-clean:
	cargo llvm-cov clean

# Benchmark targets
# Full statistical runs — criterion warms up, samples, and writes HTML reports
# to target/criterion/<group>/<case>/report/index.html
bench: bench-storage bench-http bench-memory bench-http-memory bench-ws-memory

bench-storage:
	cargo bench --bench storage_benchmarks

bench-http:
	cargo bench --bench http_benchmarks

bench-memory:
	cargo bench --bench memory_benchmarks

bench-http-memory:
	cargo bench --bench http_memory_benchmarks

bench-ws-memory:
	cargo bench --bench ws_memory_benchmarks

# Smoke runs — one iteration per case, no sampling; fast compile-and-correctness check
bench-smoke: bench-smoke-storage bench-smoke-http bench-smoke-memory bench-smoke-http-memory bench-smoke-ws-memory

bench-smoke-storage:
	cargo bench --bench storage_benchmarks -- --test

bench-smoke-http:
	cargo bench --bench http_benchmarks -- --test

bench-smoke-memory:
	cargo bench --bench memory_benchmarks -- --test

bench-smoke-http-memory:
	cargo bench --bench http_memory_benchmarks -- --test

bench-smoke-ws-memory:
	cargo bench --bench ws_memory_benchmarks -- --test

fmt: fmt-rust fmt-py fmt-node fmt-yaml fmt-md fmt-toml

fmt-check: fmt-check-rust fmt-check-py fmt-check-node fmt-check-yaml fmt-check-md fmt-check-toml

lint: lint-rust lint-py lint-node lint-yaml lint-md lint-toml

fmt-rust:
	cargo fmt --all

fmt-check-rust:
	cargo fmt --all -- --check

lint-rust:
	cargo clippy --all-targets --all-features -- -D warnings

fmt-py:
	@$(PY_RUFF) format scripts integrations/python integrations/examples integrations/pyclient

fmt-check-py:
	@$(PY_RUFF) format --check scripts integrations/python integrations/examples integrations/pyclient

lint-py:
	@$(PY_RUFF) check scripts integrations/python integrations/examples integrations/pyclient
	@$(PY_MYPY) --config-file mypy.ini scripts/traceability.py scripts/smoke_test.py scripts/live_client_tests.py scripts/selenium_ui_tests.py integrations/python integrations/examples integrations/pyclient

fmt-node:
	@$(NODE_PRETTIER) --write --ignore-unknown --ignore-path .gitignore \
		"web_ui/**/*.{ts,tsx,js,jsx,json,css,md,html}" \
		"integrations/nodeclient/**/*.{js,json,md}" \
		"integrations/node-red/**/*.{js,json,md}"

fmt-check-node:
	@$(NODE_PRETTIER) --check --ignore-unknown --ignore-path .gitignore \
		"web_ui/**/*.{ts,tsx,js,jsx,json,css,md,html}" \
		"integrations/nodeclient/**/*.{js,json,md}" \
		"integrations/node-red/**/*.{js,json,md}"

lint-node:
	@# JS syntax check
	@find integrations/nodeclient integrations/node-red -type f -name '*.js' -not -path '*/node_modules/*' -print0 | xargs -0 -r -n 1 node --check
	@# TS typecheck (does not emit) - only if deps are installed
	@if [ -d webui/node_modules ]; then \
		npx -y --package=typescript@5.4.5 tsc -p webui/tsconfig.json --noEmit; \
	else \
		echo "Skipping webui typecheck (run 'make build-webui' or 'cd webui && npm install')"; \
	fi

# YAML targets (prettier for format, yamllint for lint)
#
# Lazy `=` so unrelated targets (and recursive sub-makes from pre-merge etc.)
# don't pay the find cost — it expands only when lint-yaml actually references
# YAML_GLOB. The `-prune` form keeps find from descending into target/, .git/,
# .venv/, .claude-home/, and any node_modules/ — without -prune, scanning
# target/ alone took ~14 minutes on this workspace.
YAML_GLOB = $(shell find . \
	\( -name target -o -name .git -o -name .venv -o -name .claude-home \
	   -o -name node_modules \) -prune \
	-o \( -name "*.yml" -o -name "*.yaml" \) -type f -print 2>/dev/null)

fmt-yaml:
	@$(NODE_PRETTIER) --write --ignore-unknown --ignore-path .gitignore \
		"**/*.yml" "**/*.yaml"

fmt-check-yaml:
	@$(NODE_PRETTIER) --check --ignore-unknown --ignore-path .gitignore \
		"**/*.yml" "**/*.yaml"

lint-yaml:
	@$(YAMLLINT) --config-file .yamllint.yml $(YAML_GLOB)

# Markdown targets (prettier for format, markdownlint-cli2 for lint)
fmt-md:
	@$(NODE_PRETTIER) --write --ignore-unknown --ignore-path .gitignore \
		"**/*.md"

fmt-check-md:
	@$(NODE_PRETTIER) --check --ignore-unknown --ignore-path .gitignore \
		"**/*.md"

lint-md:
	@$(MARKDOWNLINT) "**/*.md" "!.venv/**" "!target/**" "!**/node_modules/**" "!.git/**" "!.claude-home/**"

# TOML targets (taplo for format, format-check, and lint)
fmt-toml:
	@$(TAPLO) fmt Cargo.toml crates/*/Cargo.toml integrations/pyclient/pyproject.toml

fmt-check-toml:
	@$(TAPLO) fmt --check Cargo.toml crates/*/Cargo.toml integrations/pyclient/pyproject.toml

lint-toml:
	@$(TAPLO) lint Cargo.toml crates/*/Cargo.toml integrations/pyclient/pyproject.toml

examples:
	@echo "Testing Python integration example..."
	@cd integrations/python && python3 -m py_compile qrusty_publish.py
	@cd integrations/python && python3 -m py_compile qrusty_consume.py
	@cd integrations/python && python3 -m py_compile qrusty_demo.py
	@cd integrations/python && test -f requirements.txt && echo "Python requirements file exists" || echo "Python requirements missing"
	@echo "Testing Python queue management examples..."
	@cd integrations/examples && python3 -m py_compile queue_management_examples.py
	@echo "Validating Node-RED integration..."
	@cd integrations/node-red && (command -v node >/dev/null 2>&1 && node -e "JSON.parse(require('fs').readFileSync('package.json', 'utf8'))" || echo "Node.js not available, skipping Node-RED validation")
	@cd integrations/node-red && test -f qrusty-publish.js && echo "Node-RED publish example exists" || echo "Node-RED publish example missing"
	@cd integrations/node-red && test -f qrusty-consume.js && echo "Node-RED consume example exists" || echo "Node-RED consume example missing"
	@echo "Validating environment example..."
	@test -f example.env && echo "Environment example exists" || echo "Environment example missing"
	@echo "Validating documentation examples..."
	@test -f integrations/examples/priority_examples.md && echo "Priority examples documentation exists" || echo "Priority examples documentation missing"
	@test -f integrations/examples/api_usage_examples.md && echo "API usage examples documentation exists" || echo "API usage examples documentation missing"
	@test -f integrations/examples/queue_management_guide.md && echo "Queue management guide exists" || echo "Queue management guide missing"
	@echo "All available integration examples validated successfully!"

show-docs:
	cargo doc --no-deps --open

# Traceability targets - run in venv with doorstop installed
# Uses uv if available, otherwise falls back to standard venv activation
traceability:
	@echo "Generating traceability report..."
	@if command -v uv >/dev/null 2>&1; then \
		uv run --python .venv/bin/python scripts/traceability.py; \
	else \
		. .venv/bin/activate && python scripts/traceability.py; \
	fi

traceability-report:
	@echo "Generating traceability report to docs/traceability_report.md..."
	@if command -v uv >/dev/null 2>&1; then \
		uv run --python .venv/bin/python scripts/traceability.py > docs/traceability_report.md; \
	else \
		. .venv/bin/activate && python scripts/traceability.py > docs/traceability_report.md; \
	fi
	@$(NODE_PRETTIER) --write --log-level=silent docs/traceability_report.md
	@echo "Report saved to docs/traceability_report.md"

publish:
	@bash scripts/publish.sh

build-qrusty:
	docker buildx build --no-cache --pull --platform $(DOCKER_PLATFORMS) -t greeng340or/qrusty -f Dockerfile --push .

publish-docker:
	$(eval VERSION := $(shell grep '^version' Cargo.toml | head -n1 | sed 's/.*"\(.*\)".*/\1/'))
	docker buildx build --no-cache --pull --platform $(DOCKER_PLATFORMS) \
		-t greeng340or/qrusty:$(VERSION) \
		-t greeng340or/qrusty:latest \
		-f Dockerfile --push .

publish-client:
	cd crates/qrusty_client && cargo publish

# Python Client
build-pyclient:
	rm -rf integrations/pyclient/dist
	cd integrations/pyclient && python3 -m build

test-pyclient:
	cd integrations/pyclient && python3 -m unittest discover -s tests

doc-pyclient:
	cd integrations/pyclient && pdoc qrusty_pyclient.py -o docs

publish-pyclient: build-pyclient
	cd integrations/pyclient && python3 -m twine upload dist/*

# Node Client
build-nodeclient:
	cd integrations/nodeclient && npm install

test-nodeclient:
	cd integrations/nodeclient && npm test

doc-nodeclient:
	cd integrations/nodeclient && npm run docs

publish-nodeclient: build-nodeclient
	@cd integrations/nodeclient && (set -e; \
		trap 'rm -f .npmrc' EXIT; \
		printf "%s\n" "//registry.npmjs.org/:_authToken=$$NPM_TOKEN" > .npmrc; \
		npm publish $(NPM_PUBLISH_ARGS))

# Depends on build-node-red (which runs `npm install`) so the
# qrusty-client npm dep is present before `npm publish` verifies the
# package.  Note: if qrusty-client's version has been bumped but not
# yet published to npm, the `npm install` step here will fail — run
# `make publish-nodeclient` first.
publish-node-red: build-node-red
	@cd integrations/node-red && (set -e; \
		trap 'rm -f .npmrc' EXIT; \
		printf "%s\n" "//registry.npmjs.org/:_authToken=$$NPM_TOKEN" > .npmrc; \
		npm publish $(NPM_PUBLISH_ARGS))

# Node-RED integration
build-node-red:
	cd integrations/node-red && npm install

test-node-red:
	@# Validate package.json and JS syntax
	@cd integrations/node-red && node -e "JSON.parse(require('fs').readFileSync('package.json', 'utf8'))"
	@find integrations/node-red -type f -name '*.js' -not -path '*/node_modules/*' -print0 | xargs -0 -r -n 1 node --check
	@# Run the mocha test suite (loads every node through node-red-node-test-helper)
	@cd integrations/node-red && npm test

pack-node-red:
	@cd integrations/node-red && npm pack --dry-run

# Web UI
build-webui:
	cd web_ui && npm install && npm run build

.PHONY: clean clean-webui immaculate

clean-webui:
	rm -rf static_webui

# Wipe build / cache / generated artifacts. Leaves node_modules, .venv,
# and the cargo registry alone — those are heavy to repopulate. Use
# `make immaculate` if you want those gone too.
clean: clean-webui coverage-clean
	@echo "── cargo clean (workspace target/) ──"
	cargo clean
	@echo "── purging integrations build outputs ──"
	@rm -rf integrations/pyclient/dist \
	        integrations/pyclient/docs \
	        integrations/pyclient/qrusty_pyclient.egg-info \
	        integrations/nodeclient/docs
	@echo "── clearing python tool caches + test-results ──"
	@rm -rf .mypy_cache .ruff_cache test-results
	@echo "── removing stray __pycache__ dirs ──"
	@find . \( -name target -o -name .git -o -name .venv \
	           -o -name node_modules -o -name .claude-home \) -prune \
	       -o -name __pycache__ -type d -print -exec rm -rf {} +
	@echo "Clean."

# Same as `clean`, plus the heavy roots that `clean` deliberately leaves
# alone: web_ui/node_modules, integrations/*/node_modules, and .venv.
# Reach for this only when one of those trees is genuinely broken — a
# fresh build afterwards needs `npm install` in three dirs plus a fresh
# Python venv with all dev tools (uv tool install ... etc.).
immaculate: clean
	@echo "── removing node_modules trees ──"
	@rm -rf web_ui/node_modules \
	        integrations/nodeclient/node_modules \
	        integrations/node-red/node_modules
	@echo "── removing .venv ──"
	@rm -rf .venv
	@echo "Immaculate."

run-dev:
	make build
	make build-webui
	echo "Starting qrusty server..."
	echo "Access the UI at: http://localhost:6784/ui"
	DATA_PATH=/tmp/qrusty/data WEBUI_DIR=static_webui cargo run

# Validate dependencies (requires updated Docker containers)
validate-deps:
	@if test -f scripts/validate_dependencies.sh; then \
		chmod +x scripts/validate_dependencies.sh && ./scripts/validate_dependencies.sh; \
	else \
		echo "Dependency validation script not found"; \
	fi

# Live smoke test — starts a real qrusty binary and exercises it end-to-end.
# Implements: SYS-0011
# Pass extra args via SMOKE_ARGS, e.g.:  make smoke-test SMOKE_ARGS="--duration 60"
#
# Resolves the binary path by asking rustc for the host target triple so the
# correct platform-specific subdirectory is used even when ~/.cargo/config.toml
# sets a non-default build target.
CARGO_HOST_TARGET := $(shell rustc -vV 2>/dev/null | awk '/^host:/{print $$2}')
QRUSTY_DEBUG_BIN  := target/$(CARGO_HOST_TARGET)/debug/qrusty

smoke-test: build
	@echo "Running live smoke test (Implements: SYS-0011) ..."
	@if command -v uv >/dev/null 2>&1; then \
		QRUSTY_MEMORY_PRESSURE_THRESHOLD=0.99 uv run --python .venv/bin/python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS); \
	else \
		QRUSTY_MEMORY_PRESSURE_THRESHOLD=0.99 . .venv/bin/activate && python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS); \
	fi

smoke-test-constrained: build
	@echo "Running smoke test with constrained memory (256 MB) ..."
	@echo "This validates memory pressure detection and load shedding."
	@QRUSTY_MEMORY_LIMIT_MB=256 \
	 QRUSTY_HOT_TIER_SIZE=100 \
	 QRUSTY_REFILL_THRESHOLD=25 \
	 ROCKSDB_CACHE_MB=16 \
	 ROCKSDB_WRITE_BUFFER_MB=8 \
	 $(if $(shell command -v uv 2>/dev/null), \
		uv run --python .venv/bin/python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS), \
		. .venv/bin/activate && python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS))

smoke-test-memory: build
	@echo "Running live smoke test with in-memory storage (Implements: SYS-0013) ..."
	@if command -v uv >/dev/null 2>&1; then \
		STORAGE_MODE=memory QRUSTY_MEMORY_PRESSURE_THRESHOLD=0.99 uv run --python .venv/bin/python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS); \
	else \
		STORAGE_MODE=memory QRUSTY_MEMORY_PRESSURE_THRESHOLD=0.99 . .venv/bin/activate && python scripts/smoke_test.py --binary $(QRUSTY_DEBUG_BIN) $(SMOKE_ARGS); \
	fi

# Live integration tests — start a real server and exercise client libraries
LIVE_TEST_PORT    ?= 17785
SELENIUM_PORT     ?= 17786
NODE_LIVE_PORT    ?= 17787
SELENIUM_HUB      ?= http://172.17.0.1:4444
QRUSTY_HOST       ?= $(shell hostname -I | awk '{print $$1}')

test-live-pyclient: build
	@echo "Running Python live client tests ..."
	@if command -v uv >/dev/null 2>&1; then \
		uv run --python .venv/bin/python scripts/live_client_tests.py \
			--binary $(QRUSTY_DEBUG_BIN) --port $(LIVE_TEST_PORT); \
	else \
		. .venv/bin/activate && python scripts/live_client_tests.py \
			--binary $(QRUSTY_DEBUG_BIN) --port $(LIVE_TEST_PORT); \
	fi

test-selenium: build
	@echo "Running Selenium UI tests (hub: $(SELENIUM_HUB)) ..."
	@if command -v uv >/dev/null 2>&1; then \
		uv run --python .venv/bin/python scripts/selenium_ui_tests.py \
			--binary $(QRUSTY_DEBUG_BIN) --port $(SELENIUM_PORT) \
			--selenium-hub $(SELENIUM_HUB) --qrusty-host $(QRUSTY_HOST); \
	else \
		. .venv/bin/activate && python scripts/selenium_ui_tests.py \
			--binary $(QRUSTY_DEBUG_BIN) --port $(SELENIUM_PORT) \
			--selenium-hub $(SELENIUM_HUB) --qrusty-host $(QRUSTY_HOST); \
	fi

test-live-nodeclient: build
	@echo "Running Node.js live client tests ..."
	@node integrations/nodeclient/live_test.js \
		--binary $(QRUSTY_DEBUG_BIN) --port $(NODE_LIVE_PORT)

test-integration-all: test test-live-pyclient test-live-nodeclient test-node-red test-selenium smoke-test
	@echo "All integration tests complete."

# ---- Dependency upgrades (rewrite manifests + lockfiles) -------------------
#
# These targets REWRITE manifests (Cargo.toml, web_ui/package.json,
# integrations/{nodeclient,node-red}/package.json) and refresh lockfiles.
# They are NOT idempotent — running them produces a working-tree diff.
# Each upgrade target is split so an operator can take one half at a time
# when the other half might fall over.

upgrade-deps: upgrade-deps-rust upgrade-deps-node
	@echo ""
	@echo "Upgrades complete. Before committing:"
	@echo "  make build"
	@echo "  make test"
	@echo "  make audit-deps-security"

upgrade-deps-rust:
	@echo "── cargo upgrade (rewriting Cargo.toml across workspace) ──"
	@if ! cargo upgrade --help >/dev/null 2>&1; then \
		echo "Installing cargo-edit (one-time)..."; \
		cargo install --locked cargo-edit; \
	fi
	cargo upgrade --incompatible
	@echo ""
	@echo "── cargo update (refreshing Cargo.lock) ──"
	cargo update

# Iterates the three Node package roots in qrusty. Skips any that lack
# node_modules with a hint so an operator running this in a fresh clone
# isn't bitten by a hard failure.
NODE_PACKAGE_DIRS := web_ui integrations/nodeclient integrations/node-red

upgrade-deps-node:
	@for dir in $(NODE_PACKAGE_DIRS); do \
		echo ""; \
		echo "── ncu --upgrade ($$dir) ──"; \
		if [ ! -d $$dir/node_modules ]; then \
			echo "$$dir has no node_modules; run 'cd $$dir && npm install' first. Skipping."; \
			continue; \
		fi; \
		(cd $$dir && npx --yes npm-check-updates --upgrade) || exit 1; \
		echo ""; \
		echo "── npm install ($$dir, refreshing package-lock.json) ──"; \
		(cd $$dir && npm install) || exit 1; \
	done

# ---- Read-only dependency-health audits ------------------------------------
#
# All targets here are read-only and safe to run any time. None rewrite
# manifests. For "actually upgrade things," see upgrade-deps above.
#
# Three orthogonal concerns:
#   audit-deps-unused    — unused workspace deps. cargo machete.
#   audit-deps-outdated  — what's behind latest. cargo outdated + npm outdated.
#   audit-deps-security  — known CVEs. cargo audit + npm audit.

audit-deps: audit-deps-unused audit-deps-outdated audit-deps-security

audit-deps-unused:
	@echo "── cargo machete (Rust unused workspace deps) ──"
	cargo machete

audit-deps-outdated:
	@echo "── cargo outdated (workspace, root deps only) ──"
	@if ! command -v cargo-outdated >/dev/null 2>&1; then \
		echo "Installing cargo-outdated (one-time)..."; \
		cargo install --locked cargo-outdated; \
	fi
	cargo outdated --workspace --root-deps-only
	@for dir in $(NODE_PACKAGE_DIRS); do \
		echo ""; \
		echo "── npm outdated ($$dir) ──"; \
		if [ -d $$dir/node_modules ]; then \
			(cd $$dir && npm outdated || true); \
		else \
			echo "Skipping npm outdated — $$dir has no node_modules."; \
		fi; \
	done

audit-deps-security:
	@echo "── cargo audit (RustSec advisory DB) ──"
	@if ! command -v cargo-audit >/dev/null 2>&1; then \
		echo "Installing cargo-audit (one-time)..."; \
		cargo install --locked cargo-audit; \
	fi
	cargo audit
	@for dir in $(NODE_PACKAGE_DIRS); do \
		echo ""; \
		echo "── npm audit ($$dir, production deps only) ──"; \
		if [ -d $$dir/node_modules ]; then \
			(cd $$dir && npm audit --omit=dev || true); \
		else \
			echo "Skipping npm audit — $$dir has no node_modules."; \
		fi; \
	done

# ---- pre-merge: full preflight before a big merge --------------------------
#
# `pre-merge` chains the automated half of the standard pre-merge workflow
# in the order required for it to actually pass:
#
#   1. upgrade-deps          rewrites Cargo.toml + web_ui/package.json + lockfiles
#                            (and the two integrations/* Node packages)
#   2. audit-deps            verifies upgraded state — unused / outdated / CVEs
#   3. test                  workspace tests against upgraded deps
#   4. coverage-report       regenerates docs/coverage_report.md
#   5. traceability-report   regenerates docs/traceability_report.md
#   6. fmt                   formats freshly generated reports + everything else
#                            (must precede fmt-check, which would fail on
#                             un-formatted markdown the previous two steps emit)
#   7. fmt-check             confirms the tree is fully formatted
#   8. lint                  clippy / ruff / mypy / tsc / markdownlint / taplo / yamllint
#
# Manual prerequisite (NOT automated by this target):
#   • Update docs and requirements as needed for the changes being merged.
#
# After `make pre-merge` completes, review and commit the diffs in:
#   • Cargo.toml + Cargo.lock
#   • web_ui/package.json + web_ui/package-lock.json
#     (and the same in integrations/nodeclient + integrations/node-red,
#      if their deps actually moved)
#   • docs/coverage_report.md
#   • docs/traceability_report.md
pre-merge:
	@echo "═══════════════════════════════════════════════════════════════════"
	@echo "  pre-merge — full preflight before a big merge"
	@echo "═══════════════════════════════════════════════════════════════════"
	@echo ""
	@echo "Manual prerequisite (NOT automated):"
	@echo "  • Update docs and requirements as needed for changes being merged."
	@echo ""
	@echo "Continuing in 5s — Ctrl-C now to abort."
	@sleep 5
	@echo ""
	@echo "▶ [1/8] upgrade-deps"
	@$(MAKE) upgrade-deps
	@echo ""
	@echo "▶ [2/8] audit-deps"
	@$(MAKE) audit-deps
	@echo ""
	@echo "▶ [3/8] test"
	@$(MAKE) test
	@echo ""
	@echo "▶ [4/8] coverage-report"
	@$(MAKE) coverage-report
	@echo ""
	@echo "▶ [5/8] traceability-report"
	@$(MAKE) traceability-report
	@echo ""
	@echo "▶ [6/8] fmt (covers freshly generated coverage + traceability reports)"
	@$(MAKE) fmt
	@echo ""
	@echo "▶ [7/8] fmt-check"
	@$(MAKE) fmt-check
	@echo ""
	@echo "▶ [8/8] lint"
	@$(MAKE) lint
	@echo ""
	@echo "═══════════════════════════════════════════════════════════════════"
	@echo "  ✓ pre-merge passed"
	@echo "═══════════════════════════════════════════════════════════════════"
	@echo "Review and commit the diffs:"
	@echo "  • Cargo.toml + Cargo.lock"
	@echo "  • web_ui/package.json + web_ui/package-lock.json"
	@echo "    (and integrations/{nodeclient,node-red} if their deps moved)"
	@echo "  • docs/coverage_report.md"
	@echo "  • docs/traceability_report.md"