.PHONY: all check-mdx dev build export htmltest export-htmltest format lint test install install_vale clean lint_md lint_md_fix lint_prose broken-links broken-links-with-anchors format-check code-snippets test-code-samples update-code-sample-traces check-cross-refs check-legacy-links skills
# Default target
all: help
dev:
@echo "Starting development mode..."
npm install
PYTHONPATH=$(CURDIR) uv run pipeline dev
build:
@echo "Building documentation..."
npm install
PYTHONPATH=$(CURDIR) uv run pipeline build
# Offline zip via Mintlify (https://www.mintlify.com/docs/deploy/export).
# Must run from build/: docs.json paths are oss/python/... and oss/javascript/... but sources live under src/oss/... until the pipeline emits build/oss/{python,javascript}/...
# Default mint output when run from build/ is build/export.zip. Override with MINT_EXPORT_ARGS='--output other.zip' (path relative to build/) and matching EXPORT_ZIP=build/other.zip for htmltest.
# Requires: recent mint CLI (mint export), Node LTS 20/22 (Node 25+ unsupported), Enterprise Mintlify plan.
export: build
@command -v mint >/dev/null 2>&1 || { echo "Error: mint not installed. Run: npm install -g mint@latest"; exit 1; }
@mint help 2>&1 | grep -q 'mint export' || { \
echo "Error: 'mint export' is missing from mint $$(mint --version 2>/dev/null || echo unknown)."; \
echo "Upgrade: npm install -g mint@latest"; \
echo "Also needs Node LTS (20 or 22; Node 25 is unsupported) and an Enterprise Mintlify plan."; \
exit 1; \
}
@NODE_MAJOR=$$(node -p "process.versions.node.split('.')[0]"); \
if [ "$$NODE_MAJOR" -ge 25 ]; then \
echo "Error: mint does not support Node $$NODE_MAJOR. Switch to Node 20 or 22 (e.g. nvm use 22), install mint for that Node (npm install -g mint@latest), then retry."; \
exit 1; \
fi
@cd build && mint export $(MINT_EXPORT_ARGS)
# Zip produced by make export (default Mintlify name: export.zip in build/). Override if you used --output.
EXPORT_ZIP ?= build/export.zip
# Unpacked copy for htmltest (gitignored under build/).
HTMLTEST_UNPACK_DIR ?= build/mint-export-htmltest-unpacked
# Extra htmltest CLI flags (config already checks external URLs only). Example: HTMLTEST_ARGS='-l 1'
HTMLTEST_ARGS ?=
# Unzip EXPORT_ZIP and run htmltest (https://github.com/wjdp/htmltest). Run after make export.
# Uses htmltest-mint-export.yml: external URLs only (mint export omits many pages, so internals are noisy).
htmltest:
@command -v htmltest >/dev/null 2>&1 || { echo "Error: htmltest not found. Install: brew install htmltest OR curl https://htmltest.wjdp.uk | sudo bash -s -- -b /usr/local/bin"; exit 1; }
@command -v unzip >/dev/null 2>&1 || { echo "Error: unzip not found."; exit 1; }
@test -f $(EXPORT_ZIP) || { echo "Error: $(EXPORT_ZIP) not found. Run make export first, or set EXPORT_ZIP to your mint export zip path."; exit 1; }
mkdir -p $(HTMLTEST_UNPACK_DIR)
unzip -q -o $(EXPORT_ZIP) -d $(HTMLTEST_UNPACK_DIR)
@bash -ec 'ROOT="$(HTMLTEST_UNPACK_DIR)"; \
COUNT=$$(find "$$ROOT" -mindepth 1 -maxdepth 1 2>/dev/null | wc -l | tr -d " "); \
if [ "$$COUNT" -eq 1 ]; then \
ONLY=$$(find "$$ROOT" -mindepth 1 -maxdepth 1 2>/dev/null | head -n 1); \
if [ -d "$$ONLY" ]; then ROOT="$$ONLY"; fi; \
fi; \
echo "htmltest root: $$ROOT"; \
htmltest $(HTMLTEST_ARGS) -c "$(CURDIR)/htmltest-mint-export.yml" "$$ROOT"'
# make export then make htmltest (sequential; use this for a one-shot check).
export-htmltest:
@$(MAKE) export
@$(MAKE) htmltest
# Define a variable for the test file path.
TEST_FILE ?= tests/unit_tests
# Define a variable for Python and notebook files.
PYTHON_FILES=.
lint:
uv run ruff format $(PYTHON_FILES) --diff
uv run ruff check $(PYTHON_FILES)
uv run ty check
uv run codespell src
format:
uv run ruff format $(PYTHON_FILES)
uv run ruff check --fix $(PYTHON_FILES)
# Check formatting without applying changes (for CI)
format-check:
uv run ruff format $(PYTHON_FILES) --check --diff
uv run ruff check $(PYTHON_FILES)
lint_md:
@echo "Linting markdown files..."
@if command -v markdownlint >/dev/null 2>&1; then \
find src -name "*.md" -o -name "*.mdx" | xargs markdownlint; \
else \
echo "markdownlint not found. Install with: npm install -g markdownlint-cli or VSCode extension"; \
exit 1; \
fi
lint_md_fix:
@echo "Linting and fixing markdown files..."
@if command -v markdownlint >/dev/null 2>&1; then \
find src -name "*.md" -o -name "*.mdx" | xargs markdownlint --fix; \
else \
echo "markdownlint not found. Install with: npm install -g markdownlint-cli or VSCode extension"; \
exit 1; \
fi
VALE_BIN ?= .bin/vale
# Single source of truth for the Vale pin is .mise.toml. Override on the
# command line only for a one-off test: make lint_prose VALE_VERSION=3.16.0
VALE_VERSION ?= $(shell sed -n 's/^[[:space:]]*vale[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .mise.toml | head -n 1)
install_vale:
@bash scripts/install-vale.sh "$(VALE_BIN)" "$(VALE_VERSION)"
lint_prose:
@echo "Linting prose with Vale..."
@bash scripts/install-vale.sh "$(VALE_BIN)" "$(VALE_VERSION)"
@if [ -n "$(FILES)" ]; then \
"$(VALE_BIN)" --glob='!{**/node_modules/**,src/code-samples/**}' $(FILES); \
else \
"$(VALE_BIN)" --glob='!{**/node_modules/**,src/code-samples/**}' src/; \
fi
test:
uv run pytest --disable-socket --allow-unix-socket $(TEST_FILE) -vv
install:
@echo "Installing all dependencies"
uv sync --all-groups
npm install
@for attempt in 1 2 3; do \
if npm install -g mint@latest; then \
break; \
elif [ "$$attempt" -eq 3 ]; then \
echo "Mintlify CLI installation failed after $$attempt attempts." >&2; \
exit 1; \
fi; \
delay=$$((attempt * 10)); \
echo "Mintlify CLI installation failed; retrying in $${delay}s ($$attempt/3)..." >&2; \
sleep "$$delay"; \
done
@$(MAKE) --no-print-directory skills
@echo "If the docs command is not available, relaunch your shell so it picks up the docs binary."
clean:
@echo "Cleaning build artifacts..."
@rm -rf build/
@rm -rf __pycache__/
@find . -name "*.pyc" -delete
@find . -name "*.pyo" -delete
@find . -name "*.pyd" -delete
@find . -name "__pycache__" -type d -exec rm -rf {} +
# Mintlify commands (run from build directory where final docs are generated)
# broken-links: Checks for broken links, excluding OpenAPI-generated pages and snippet files
# Excluded: /langsmith/agent-server-api/, /api-reference/ (Mintlify-generated at deploy, not in local build)
# Excluded: entire snippets/ report sections (scripts/filter_mint_broken_links.py)
# Snippet /oss/ links are absolute language-prefixed paths under
# build/snippets/{python|javascript}/...; mint checks snippets as standalone files
# so those look broken until inlined into a page.
# --check-redirects also validates that every docs.json redirect destination resolves.
# Without it, 261 redirects pointing at unversioned /oss/ paths sat broken undetected.
# Reported as indented "source -> destination" lines, so the same failure test catches them.
# Failure: only when filtered output still has indented link lines (real broken links we care about)
# Run mint, capture output, filter exclusions. Only show output when failing.
broken-links: build
@command -v mint >/dev/null 2>&1 || { echo "Error: mint not installed. Run 'npm install -g mint@latest'"; exit 1; }
@KATEX_MJS="$$(npm root -g 2>/dev/null)/mint/node_modules/katex/dist/katex.mjs"; \
if [ -f "$$KATEX_MJS" ] && grep -q '__VERSION__' "$$KATEX_MJS" 2>/dev/null; then \
KATEX_DIR="$$(cd "$$(dirname "$$KATEX_MJS")/.." && pwd)"; \
VERSION=$$(node -e "console.log(require('$$KATEX_DIR/package.json').version)" 2>/dev/null); \
if [ -n "$$VERSION" ]; then sed -i.bak "s/__VERSION__/\"$$VERSION\"/g" "$$KATEX_MJS" 2>/dev/null || true; fi; \
fi
@cd build && mint broken-links --check-redirects 2>&1 | tee /tmp/broken-links.txt > /dev/null; \
filtered=$$(python3 ../scripts/filter_mint_broken_links.py --input /tmp/broken-links.txt); \
if echo "$$filtered" | grep -qE '^[[:space:]]+[^[:space:]]'; then \
echo "$$filtered"; echo ""; echo "❌ Broken links found"; exit 1; \
else \
echo "✅ No broken links"; \
fi
broken-links-with-anchors: build
@command -v mint >/dev/null 2>&1 || { echo "Error: mint not installed. Run 'npm install -g mint@latest'"; exit 1; }
@KATEX_MJS="$$(npm root -g 2>/dev/null)/mint/node_modules/katex/dist/katex.mjs"; \
if [ -f "$$KATEX_MJS" ] && grep -q '__VERSION__' "$$KATEX_MJS" 2>/dev/null; then \
KATEX_DIR="$$(cd "$$(dirname "$$KATEX_MJS")/.." && pwd)"; \
VERSION=$$(node -e "console.log(require('$$KATEX_DIR/package.json').version)" 2>/dev/null); \
if [ -n "$$VERSION" ]; then sed -i.bak "s/__VERSION__/\"$$VERSION\"/g" "$$KATEX_MJS" 2>/dev/null || true; fi; \
fi
@cd build && mint broken-links --check-anchors --check-redirects 2>&1 | tee /tmp/broken-links.txt > /dev/null; \
filtered=$$(python3 ../scripts/filter_mint_broken_links.py --check-anchors --input /tmp/broken-links.txt); \
if echo "$$filtered" | grep -qE '^[[:space:]]+[^[:space:]]'; then \
echo "$$filtered"; echo ""; echo "❌ Broken links found"; exit 1; \
else \
echo "✅ No broken links"; \
fi
check-legacy-links:
@bash scripts/check-legacy-doc-links.sh
# check-mdx: Fails on JSX components that are neither built in nor imported, and on
# foreign syntax such as :::note. mint exits 0 on warnings with --no-build, so count them.
check-mdx: build
@cd build && mint validate --no-build --disable all --enable unknown-component,foreign-syntax --format json 2>/dev/null | \
python3 -c 'import json, sys; r = json.load(sys.stdin); [print("%s:%s: %s" % (d["file"], d["line"], d["message"])) for d in r["diagnostics"]]; sys.exit(bool(r["diagnostics"]))'
check-openapi: build
@echo "Checking openapi spec validity"
@command -v mint >/dev/null 2>&1 || { echo "Error: mint is not installed. Run 'npm install -g mint@latest'"; exit 1; }
@cd build && output=$$(mint openapi-check langsmith/agent-server-openapi.json) && echo "$$output"
# Extract code snippets from src/code-samples (line-based, Bluehawk-compatible tags)
code-snippets:
@echo "Extracting code snippets..."
@mkdir -p src/code-samples-generated
@PYTHONPATH=$(CURDIR) python scripts/extract_code_snippets.py
@PYTHONPATH=$(CURDIR) python scripts/generate_code_snippet_mdx.py
# Run code samples. By default runs all; pass FILES to test specific paths.
# make test-code-samples
# make test-code-samples FILES="src/code-samples/langchain/return-a-string.py"
# Set CODE_SAMPLE_PATHS to space-separated directories under src/code-samples/
# to run one product's samples (CI runs one matrix leg per product this way):
# CODE_SAMPLE_PATHS="src/code-samples/langsmith" make test-code-samples
# Set CODE_SAMPLE_JOBS=N (default 4) to run samples concurrently.
# Samples that share a LangSmith dataset/fixture (evaluate-rag-*,
# experiment-runs-query-*) stay serialized within their group.
test-code-samples:
@if [ -f src/code-samples/package.json ]; then (cd src/code-samples && npm install --silent); fi
@FILES="$(FILES)" PYTHONPATH=$(CURDIR) uv run python scripts/test_code_samples.py
# Run code samples with LangSmith tracing, update public share links in
# src/code-samples/trace-links.json, then regenerate snippet MDX so docs show
# "View example trace" under single-snippet samples that produced an agent run.
# Each traced sample stamps LANGSMITH_DOCS_SNIPPET=; collection
# matches that metadata (no time-window LangGraph fallback). Multi-snippet
# source files are skipped until split. Requires LANGSMITH_API_KEY.
# make update-code-sample-traces
# make update-code-sample-traces FILES="src/code-samples/deepagents/overview-quickstart.py"
update-code-sample-traces:
@if [ -f src/code-samples/package.json ]; then (cd src/code-samples && npm install --silent); fi
@CODE_SAMPLE_TRACING=1 \
LANGSMITH_PROJECT="$${LANGSMITH_PROJECT:-docs-code-samples}" \
FILES="$(FILES)" \
PYTHONPATH=$(CURDIR) uv run python scripts/test_code_samples.py
@$(MAKE) code-snippets
# Check that all @[ref] cross-references in source files resolve against link_map.py
check-cross-refs:
@PYTHONPATH=$(CURDIR) uv run python scripts/check_cross_refs.py
skills:
@mkdir -p .claude/skills
@for d in .agents/skills/*/; do \
n=$$(basename "$$d"); \
if [ -e ".claude/skills/$$n" ] && [ ! -L ".claude/skills/$$n" ]; then \
echo "Skipped $$n: .claude/skills/$$n exists and is not a symlink"; \
else \
ln -sfn "../../.agents/skills/$$n" ".claude/skills/$$n"; \
echo "Linked .claude/skills/$$n"; \
fi; \
done
@for l in .claude/skills/*; do \
if [ -L "$$l" ] && [ ! -e "$$l" ]; then rm "$$l"; echo "Removed stale link $$l"; fi; \
done
help:
@echo "Available commands:"
@echo " make dev - Start development mode with file watching and mint dev"
@echo " make build - Build documentation to ./build directory"
@echo " make export - Run mint export from ./build (optional: MINT_EXPORT_ARGS)"
@echo " make htmltest - Unzip EXPORT_ZIP, run htmltest on external URLs only (HTMLTEST_ARGS)"
@echo " make export-htmltest - make export then make htmltest"
@echo " make broken-links - Check for broken links in built documentation"
@echo " make check-cross-refs - Check for unresolved @[ref] cross-references"
@echo " make broken-links-with-anchors - Same as above, also validates anchor links"
@echo " make check-mdx - Check build/ for unknown components and foreign syntax"
@echo " make format - Format code"
@echo " make lint - Lint code"
@echo " make lint_md - Lint markdown files"
@echo " make lint_md_fix - Lint and fix markdown files"
@echo " make lint_prose - Lint prose with Vale (terminology, style)"
@echo " make test - Run tests"
@echo " make install - Install dependencies and link skills"
@echo " make code-snippets - Extract code snippets (line-based, Bluehawk-compatible)"
@echo " make test-code-samples - Run code samples (FILES=\"path ...\" for specific)"
@echo " make skills - Link .agents/skills into .claude/skills for Claude Code"
@echo " make update-code-sample-traces - Trace samples, update share links, regenerate snippets"
@echo " make clean - Clean build artifacts"
@echo " make help - Show this help message"