Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
449f02b
chore: scaffold mailing_list_integration service (CM-1318)
themarolt Jul 14, 2026
1c9c16c
feat: add mailinglist schema tables (CM-1318)
themarolt Jul 14, 2026
1194e9c
feat: add mailing list database layer (CM-1318)
themarolt Jul 14, 2026
2cbb25f
feat: port noteren email parser into service (CM-1318)
themarolt Jul 14, 2026
93e5793
feat: add public-inbox mirror service (CM-1318)
themarolt Jul 14, 2026
3da5d5c
feat: add kafka queue producer (CM-1318)
themarolt Jul 14, 2026
f250f68
feat: add mailing list poll worker and server (CM-1318)
themarolt Jul 15, 2026
9fd947f
build: add mailing_list_integration dockerfile and wiring (CM-1318)
themarolt Jul 15, 2026
7e086b0
chore: add mailing list dev seed and docs (CM-1318)
themarolt Jul 15, 2026
bfc56b6
fix: ruff CI-breaking lint errors and wrong activity type (CM-1318)
themarolt Jul 15, 2026
212875a
chore: remove dead settings from mailing list integration (CM-1318)
themarolt Jul 15, 2026
f6b291c
fix: regenerate manifest after initial lore list clone (CM-1318)
themarolt Jul 15, 2026
04136f0
fix: correct base image and container paths for mailing-list-integrat…
themarolt Jul 15, 2026
1b9e96a
feat: add mailing list onboarding endpoint (CM-1318)
themarolt Jul 15, 2026
037a969
fix: harden mailing list bug fixes and add activity type wiring (CM-1…
themarolt Jul 19, 2026
cf4735c
Merge branch 'main' into feat/mailing-list-integration-CM-1318
themarolt Jul 20, 2026
f521e7a
Merge remote-tracking branch 'origin/main' into feat/mailing-list-int…
themarolt Jul 21, 2026
9985ca8
fix: address simple mailing list PR review comments (CM-1318)
themarolt Jul 22, 2026
d324ea1
fix: validate mailing list name to prevent path traversal (CM-1318)
themarolt Jul 22, 2026
3cacf06
fix: honor stored sourceUrl for mirror clone and permalinks (CM-1318)
themarolt Jul 22, 2026
28263ab
fix: skip unparseable commits instead of blocking list forever (CM-1318)
themarolt Jul 22, 2026
bfdcf1e
fix: reclaim stale locked mailing lists after crash (CM-1318)
themarolt Jul 22, 2026
0e43d27
fix: flush activities and checkpoint heads in batches during onboardi…
themarolt Jul 22, 2026
48c6de2
fix: make activity result inserts idempotent on retry (CM-1318)
themarolt Jul 22, 2026
1d2fcb8
fix: await kafka delivery futures for actual broker ack (CM-1318)
themarolt Jul 22, 2026
9a86494
fix: run read_email in a thread to avoid blocking event loop (CM-1318)
themarolt Jul 22, 2026
6287b8c
fix: use longer timeout for initial public-inbox clone (CM-1318)
themarolt Jul 22, 2026
435772b
fix: set score on mailing list message activities (CM-1318)
themarolt Jul 22, 2026
e993187
fix: avoid sourceId collision for messages without Message-ID (CM-1318)
themarolt Jul 22, 2026
694d244
refactor: convert ListWorker class to plain functions (CM-1318)
themarolt Jul 22, 2026
6461853
fix: soft-delete mailing lists removed from integration settings (CM-…
themarolt Jul 22, 2026
997b0d6
fix: reject mailing list connect if already owned by another project …
themarolt Jul 22, 2026
fac52d3
fix: normalize case in member/objectMember username comparisons (CM-1…
themarolt Jul 22, 2026
834aa40
fix: only strip first-line From: override, not later From: content (C…
themarolt Jul 22, 2026
b055aa7
fix: key mirror dir by list id, not user-supplied name (CM-1318)
themarolt Jul 22, 2026
d698cc9
fix: harden list-worker lock/checkpoint handling against races (CM-1318)
themarolt Jul 22, 2026
8064547
fix: harden mailing list connect request validation (CM-1318)
themarolt Jul 22, 2026
977d45e
fix: clear deletedAt when re-onboarding removed mailing list (CM-1318)
themarolt Jul 22, 2026
2df8842
fix: avoid collapsing distinct authors with missing From header (CM-1…
themarolt Jul 22, 2026
3a19d99
fix: emit compact JSONL from parse_id debug CLI (CM-1318)
themarolt Jul 22, 2026
9eafca1
fix: gate mailing list onboarding on integration.results backlog (CM-…
themarolt Jul 22, 2026
d640e6e
docs: fix example list name in mailing list integration README (CM-1318)
themarolt Jul 22, 2026
11332de
ci: run python lint/test on pyproject.toml/uv.lock changes too (CM-1318)
themarolt Jul 22, 2026
e46243a
fix: validate mailing list script input with shared zod schema (CM-1318)
themarolt Jul 22, 2026
42406aa
fix: skip onboarding/reprocessing lists of disconnected integrations …
themarolt Jul 22, 2026
12d147c
fix: canonicalize mailing list sourceUrl trailing slash (CM-1318)
themarolt Jul 22, 2026
1ee0dc9
fix: prevent concurrent mailing list connects from racing ownership (…
themarolt Jul 23, 2026
2c49057
fix: allow reconnect after integration disconnect (CM-1318)
themarolt Jul 23, 2026
05ac034
fix: reset processing state when reactivating soft-deleted mailing li…
themarolt Jul 23, 2026
caa9fc6
docs: clarify upsertMailingLists reactivation behavior (CM-1318)
themarolt Jul 23, 2026
d2a6465
fix: reject loopback/localhost mailing list sourceUrl (CM-1318)
themarolt Jul 23, 2026
71b7f96
perf: bound integration.results scan in can_onboard_more (CM-1318)
themarolt Jul 23, 2026
6262729
fix: clone lore mirror to temp dir, atomic rename on success (CM-1318)
themarolt Jul 23, 2026
929bdfc
fix: throw on empty lists in mailingListConnectOrUpdate (CM-1318)
themarolt Jul 23, 2026
c0c2b06
fix: percent-encode message-id in built activity url (CM-1318)
themarolt Jul 23, 2026
629c688
fix: annotate get_body's optional return type (CM-1318)
themarolt Jul 23, 2026
0268e9d
fix: don't checkpoint shard head past a transient git-read failure (C…
themarolt Jul 23, 2026
10f71da
fix: retry update_processed_heads on transient failure (CM-1318)
themarolt Jul 23, 2026
cfce316
fix: sync identities and segment on conflict-redirect owner attach (C…
themarolt Jul 23, 2026
2eeeec4
fix: retry missing identities on per-member constraint conflict (CM-1…
themarolt Jul 23, 2026
8698810
fix: soft-delete mailing lists when their integration is destroyed (C…
themarolt Jul 23, 2026
ec0ae0c
fix: set result-level segmentId so activities route to the correct li…
themarolt Jul 23, 2026
a3b8bc2
fix: canonicalize sourceUrl scheme/host case, default port, credentia…
themarolt Jul 23, 2026
c93b9fc
fix: persist LORE_MIRROR_DIR across container recreation (CM-1318)
themarolt Jul 23, 2026
e881aa9
fix: relax mailing-list name validation, mirror keys by list id not n…
themarolt Jul 23, 2026
db4b062
fix: don't fast-path create-conflict when identities resolve to multi…
themarolt Jul 23, 2026
736dbe2
fix: use result() not selectNone() for advisory lock query (CM-1318)
themarolt Jul 23, 2026
b23b108
fix: initialize Exception.args from error_message on CrowdMailError (…
themarolt Jul 23, 2026
f970b07
fix: mark list FAILED not COMPLETED after unretried shard read error …
themarolt Jul 23, 2026
de6a08c
fix: promote matching identity to verified before early-return (CM-1318)
themarolt Jul 23, 2026
19018a8
fix: use payload integrationId and promote verified identities on att…
themarolt Jul 23, 2026
6797104
fix: await cancelled worker task so shutdown finally runs (CM-1318)
themarolt Jul 23, 2026
53d8e80
fix: don't merge on ambiguous multi-owner identity conflict (CM-1318)
themarolt Jul 23, 2026
d952d18
chore: drop data_sink_worker fixes, split to separate branch (CM-1318)
themarolt Jul 23, 2026
a4bcebf
Merge remote-tracking branch 'origin/main' into feat/mailing-list-int…
themarolt Jul 23, 2026
d8e0d28
style: prettier format ADR files after merge (CM-1318)
themarolt Jul 23, 2026
a2ac5b9
style: fix prettier formatting in en.ts (CM-1318)
themarolt Jul 23, 2026
bd7e116
Merge remote-tracking branch 'origin/main' into feat/mailing-list-int…
themarolt Jul 24, 2026
2fbabc4
Merge remote-tracking branch 'origin/main' into feat/mailing-list-int…
themarolt Jul 24, 2026
5bcf617
Merge remote-tracking branch 'origin/main' into feat/mailing-list-int…
themarolt Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/backend-lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,48 @@ jobs:
if: steps.changes.outputs.python_changed == 'true'
run: uv sync --group dev --frozen

- name: Check Python linting and formatting
if: steps.changes.outputs.python_changed == 'true'
run: |
uv run ruff check src/ --output-format=github
Comment thread
themarolt marked this conversation as resolved.

lint-python-mailing-list-integration:
runs-on: ubuntu-latest
defaults:
run:
shell: bash
working-directory: ./services/apps/mailing_list_integration

steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Check for Python file changes
id: changes
run: |
if git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.sha }} | grep -q "^services/apps/mailing_list_integration/.*\.py$"; then
Comment thread
themarolt marked this conversation as resolved.
Outdated
echo "python_changed=true" >> $GITHUB_OUTPUT
else
echo "python_changed=false" >> $GITHUB_OUTPUT
fi

- name: Install uv
if: steps.changes.outputs.python_changed == 'true'
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "services/apps/mailing_list_integration/uv.lock"

- name: Set up Python
if: steps.changes.outputs.python_changed == 'true'
run: uv python install 3.13

- name: Install dependencies
if: steps.changes.outputs.python_changed == 'true'
run: uv sync --group dev --frozen
Comment thread
themarolt marked this conversation as resolved.

- name: Check Python linting and formatting
if: steps.changes.outputs.python_changed == 'true'
run: |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-- Create the mailinglist schema
CREATE SCHEMA IF NOT EXISTS mailinglist;

-- Mailing lists (lore/public-inbox lists onboarded into CDP)
CREATE TABLE mailinglist.lists (
id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(),
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"deletedAt" TIMESTAMP WITH TIME ZONE,

name TEXT NOT NULL,
"sourceUrl" TEXT NOT NULL,

"segmentId" UUID NOT NULL REFERENCES segments (id),
"integrationId" UUID NOT NULL REFERENCES public."integrations" (id),
Comment thread
themarolt marked this conversation as resolved.

UNIQUE ("sourceUrl")
Comment thread
themarolt marked this conversation as resolved.
);

-- Per-list processing state (public-inbox shard head tracking)
CREATE TABLE mailinglist."listProcessing" (
"listId" UUID PRIMARY KEY NOT NULL REFERENCES mailinglist.lists (id) ON DELETE CASCADE,

"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

state VARCHAR(50) NOT NULL,
priority INTEGER NOT NULL DEFAULT 2, -- 0=urgent, 1=high, 2=normal

"lockedAt" TIMESTAMP WITH TIME ZONE,
"lastProcessedAt" TIMESTAMP WITH TIME ZONE,
"lastProcessedHeads" JSONB NOT NULL DEFAULT '{}'
);

-- Indexes for optimal query performance

CREATE INDEX "ix_mailinglist_lists_segmentId" ON mailinglist.lists ("segmentId");
CREATE INDEX "ix_mailinglist_lists_integrationId" ON mailinglist.lists ("integrationId");

CREATE INDEX "ix_mailinglist_listProcessing_state" ON mailinglist."listProcessing" (state);
CREATE INDEX "ix_mailinglist_listProcessing_state_priority" ON mailinglist."listProcessing" (state, priority);

-- Comments for documentation
COMMENT ON SCHEMA mailinglist IS 'Schema for mailing list integration system that manages mailing list processing state';
COMMENT ON TABLE mailinglist.lists IS 'Stores onboarded mailing lists (public-inbox/lore) and their segment/integration associations';
COMMENT ON TABLE mailinglist."listProcessing" IS 'Per-list processing state including public-inbox shard head tracking';

COMMENT ON COLUMN mailinglist."listProcessing".priority IS 'Processing priority: 0=urgent, 1=high, 2=normal';
COMMENT ON COLUMN mailinglist."listProcessing".state IS 'Current processing state of the list';
COMMENT ON COLUMN mailinglist."listProcessing"."lastProcessedHeads" IS 'JSONB map of shard index to last processed commit SHA, e.g. {"0": sha, "1": sha}';
4 changes: 4 additions & 0 deletions scripts/builders/mailing-list-integration.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DOCKERFILE="./services/docker/Dockerfile.mailing_list_integration"
CONTEXT="../"
REPO="sjc.ocir.io/axbydjxa5zuh/mailing-list-integration"
SERVICES="mailing-list-integration"
2 changes: 1 addition & 1 deletion scripts/cli
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ while test $# -gt 0; do
exit
;;
clean-start-fe-dev)
IGNORED_SERVICES=("frontend" "python-worker" "job-generator" "webhook-api" "profiles-worker" "organizations-enrichment-worker" "merge-suggestions-worker" "members-enrichment-worker" "exports-worker" "entity-merging-worker" "cache-worker" "categorization-worker" "cron-service" "data-sink-worker" "git-integration" "integration-run-worker" "integration-stream-worker" "nango-webhook-api" "nango-worker" "script-executor-worker" "search-sync-api" "search-sync-worker" "security-best-practices-worker" "snowflake-connectors-worker")
IGNORED_SERVICES=("frontend" "python-worker" "job-generator" "webhook-api" "profiles-worker" "organizations-enrichment-worker" "merge-suggestions-worker" "members-enrichment-worker" "exports-worker" "entity-merging-worker" "cache-worker" "categorization-worker" "cron-service" "data-sink-worker" "git-integration" "mailing-list-integration" "integration-run-worker" "integration-stream-worker" "nango-webhook-api" "nango-worker" "script-executor-worker" "search-sync-api" "search-sync-worker" "security-best-practices-worker" "snowflake-connectors-worker")
CLEAN_START=1
DEV=1
start
Expand Down
69 changes: 69 additions & 0 deletions scripts/services/docker/Dockerfile.mailing_list_integration
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Base image for both stages
FROM python:3.13.5-slim-bullseye AS base

# Builder stage: install build dependencies, uv, and dependencies
FROM base AS builder

# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Copy uv binary from official image
COPY --from=ghcr.io/astral-sh/uv:0.7.17 /uv /usr/local/bin/uv

WORKDIR /usr/crowd/app

ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \
UV_PYTHON=python3.13 \
UV_PROJECT_ENVIRONMENT=/usr/crowd/app/.venv \
UV_VENV_PATH=/usr/crowd/app/.venv

# Copy only lock, pyproject.toml and License for dependency install caching
COPY ./services/apps/mailing_list_integration/pyproject.toml ./services/apps/mailing_list_integration/uv.lock ./LICENSE ./

# Install dependencies excluding the project itself for better caching
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev

# Copy full source code
COPY ./services/apps/mailing_list_integration ./LICENSE ./

# Sync full project including the project itself
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev


# Runner: minimal image with runtime deps and virtualenv only
FROM base AS runner

# Install runtime dependencies, including public-inbox for mirroring lists
RUN apt-get update && apt-get install -y \
ca-certificates \
git \
public-inbox \
Comment thread
themarolt marked this conversation as resolved.
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off

WORKDIR /usr/crowd/app

# Copy virtual environment and app source from builder
COPY --from=builder /usr/crowd/app/.venv /usr/crowd/app/.venv
COPY --from=builder /usr/crowd/app /usr/crowd/app

# Add virtual environment bin to PATH
ENV PATH="/usr/crowd/app/.venv/bin:$PATH"

# Make runner script executable
RUN chmod +x ./src/runner.sh

EXPOSE 8085

CMD ["./src/runner.sh"]
50 changes: 50 additions & 0 deletions scripts/services/mailing-list-integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@

x-env-args: &env-args
DOCKER_BUILDKIT: 1
NODE_ENV: docker
SERVICE: mailing-list-integration
SHELL: /bin/sh

services:
mailing-list-integration:
build:
context: ../../
dockerfile: ./scripts/services/docker/Dockerfile.mailing_list_integration
working_dir: /usr/crowd/app/services/apps/mailing_list_integration
Comment thread
Copilot marked this conversation as resolved.
Outdated
env_file:
- ../../backend/.env.dist.local
- ../../backend/.env.dist.composed
- ../../backend/.env.override.local
- ../../backend/.env.override.composed
environment:
<<: *env-args
restart: always
networks:
- crowd-bridge
ports:
- '8086:8085'
Comment thread
themarolt marked this conversation as resolved.

mailing-list-integration-dev:
build:
context: ../../
dockerfile: ./scripts/services/docker/Dockerfile.mailing_list_integration
command: ["./src/runner.sh", "local"]
working_dir: /usr/crowd/app/services/apps/mailing_list_integration
env_file:
- ../../backend/.env.dist.local
- ../../backend/.env.dist.composed
- ../../backend/.env.override.local
- ../../backend/.env.override.composed
environment:
<<: *env-args
hostname: mailing-list-integration
ports:
- '8086:8085'
networks:
- crowd-bridge
volumes:
- ../../services/apps/mailing_list_integration/src:/usr/crowd/app/services/apps/mailing_list_integration/src

networks:
crowd-bridge:
external: true
1 change: 1 addition & 0 deletions services/apps/mailing_list_integration/LICENSE
116 changes: 116 additions & 0 deletions services/apps/mailing_list_integration/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
.DEFAULT_GOAL := help

help: ## Shows this help message
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m %s\033[0m\n\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

ifeq (cmd,$(firstword $(MAKECMDGOALS)))
CMD_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
$(eval $(CMD_ARGS):;@:)
endif

COMPOSE_FILE = docker-compose -f ../../../scripts/services/mailing-list-integration.yaml

# Check if local setup is complete
check_setup:
@if ! command -v uv >/dev/null 2>&1; then \
echo "❌ uv not found. Run 'make setup' first."; \
exit 1; \
fi
@if ! uv run ruff --version >/dev/null 2>&1; then \
echo "📦 Installing dev dependencies..."; \
uv sync --group dev; \
fi

##@ 🚀 Setup and installation

setup: ## Install uv and dev dependencies (only if not already installed)
@echo "🔧 Checking if uv is installed..."
@if command -v uv >/dev/null 2>&1; then \
echo "✅ uv is already installed at $$(which uv)"; \
echo "📦 Installing dev dependencies..."; \
uv sync --group dev; \
echo "✅ Setup complete"; \
else \
echo "📦 uv is required to:"; \
echo " - Add/remove packages to pyproject.toml"; \
echo " - Manage dependency versions and conflicts"; \
echo " - Generate uv.lock for reproducible builds"; \
echo " - Install dependencies locally for development"; \
echo ""; \
echo "Installing uv..."; \
curl -LsSf https://astral.sh/uv/install.sh | sh; \
echo "Adding uv to PATH..."; \
echo 'export PATH="$$HOME/.cargo/bin:$$PATH"' >> ~/.zshrc; \
echo 'export PATH="$$HOME/.cargo/bin:$$PATH"' >> ~/.bash_profile; \
echo 'export PATH="$$HOME/.cargo/bin:$$PATH"' >> ~/.bashrc; \
export PATH="$$HOME/.cargo/bin:$$PATH"; \
echo "📦 Installing dev dependencies..."; \
uv sync --group dev; \
echo "✅ uv installed and dev dependencies ready"; \
echo "📝 Next steps:"; \
echo " - Restart your terminal or run: source ~/.zshrc"; \
echo " - Use: make add_package name=<package> to add dependencies"; \
fi

##@ 🛠️ Local development tools

run: ## Start the mailing-list-integration service in interactive mode
$(COMPOSE_FILE) run --service-ports --rm mailing-list-integration-dev

cmd: ## Run any command inside the container, example: make cmd -- ls -la
$(COMPOSE_FILE) run --rm --no-deps mailing-list-integration-dev $(CMD_ARGS)

lint: check_setup ## Check code with ruff
@uv run ruff check src/

format: check_setup ## Format code with ruff
@uv run ruff check --fix --select I src/
@uv run ruff format src/
$(MAKE) lint

fix-all: check_setup ## Fix all lint issues, format everything, and run final lint check
@echo "🔧 Fixing all linting issues..."
@uv run ruff check --fix src/
@echo "✨ Formatting all code..."
@uv run ruff format src/
@echo "🔍 Running final lint check..."
@uv run ruff check src/
@echo "✅ All done! Code is linted and formatted."

rebuild: ## Rebuild the container
$(COMPOSE_FILE) build mailing-list-integration-dev
@echo "✅ Container rebuilt"

##@ 📦 Dependency management

add_package: check_setup ## Add new package to pyproject.toml, example: make add_package name=requests
@echo "Adding $(name) to dependencies..."
@uv add $(name)
@echo "Rebuilding container..."
$(COMPOSE_FILE) build mailing-list-integration-dev
@echo "✅ Package $(name) added and container rebuilt"

remove_package: check_setup ## Remove package from pyproject.toml, example: make remove_package name=requests
@echo "Removing $(name) from dependencies..."
@uv remove $(name)
@echo "Rebuilding container..."
$(COMPOSE_FILE) build mailing-list-integration-dev
@echo "✅ Package $(name) removed and container rebuilt"

update_deps: check_setup ## Update all dependencies
@echo "Updating dependencies..."
@uv lock
@echo "Rebuilding container..."
$(COMPOSE_FILE) build mailing-list-integration-dev
@echo "✅ Dependencies updated and container rebuilt"

lock_deps: check_setup ## Generate/update uv.lock file
@echo "Generating uv.lock file..."
@uv lock
@echo "✅ uv.lock updated"

##@ 🧪 Testing

test: check_setup ## Run tests
@echo "🧪 Running tests..."
@uv run pytest src/test/ -v
Loading