[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "bengal"
version = "0.5.0"
description = "Python static site generator built on free-threading — every layer pure Python, scales with your cores, zero npm"
readme = "README.md"
requires-python = ">=3.14"
license = "MIT"
authors = [{ name = "Bengal Contributors", email = "lbeezr@icloud.com" }]
keywords = [
    "python-static-site-generator",
    "static-site-generator",
    "ssg",
    "documentation-site-generator",
    "blog-generator",
    "documentation",
    "knowledge-base",
    "markdown",
    "website",
    "free-threading",
    "performance",
]
classifiers = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.14",
    "Topic :: Documentation",
    "Topic :: Software Development :: Documentation",
]
dependencies = [
    "click>=8.1.7",                            # Autodoc compatibility for documenting Click apps
    "milo-cli>=0.3.1",                         # B-stack CLI framework for Bengal commands

    "rosettes>=0.2.0",
    "kida-templates>=0.9.0",                   # Kida template engine (AST-native, free-threading ready)
    "patitas>=0.3.5",                          # Markdown + notebook + frontmatter parser
    "pyyaml>=6.0",
    "jsmin>=3.0.1",
    "pillow>=10.0.0",                          # Image processing for social cards
    "psutil>=5.9.0",                           # For better process management in cleanup
    "questionary>=2.0.0",                      # Interactive CLI prompts with arrow key navigation
    "watchfiles>=0.20",                        # Rust-based file watching for dev server (fast)
    "bengal-pounce>=0.8.0",                    # ASGI server for dev server (replaces ThreadingTCPServer)
    "httpx>=0.27.0",                           # Async HTTP client for link checking
    "uvloop>=0.21.0; sys_platform != 'win32'", # Rust-based event loop (Linux/macOS)
    "packaging>=23.0",                         # Version parsing for upgrade checks
    "tomli-w>=1.0.0",                          # TOML writing (pairs with stdlib tomllib)
]

[project.optional-dependencies]
# Pre-built Lunr search index (faster client-side search)
search = ["lunr>=0.7.0"]

# Chirp flagship theme library (external Kida UI components).
# Pins chirp-ui alongside the kida-templates contract version Bengal ships, so
# `pip install bengal[chirp]` resolves a theme-library wheel whose Kida engine
# matches Bengal's. The kida-templates lower bound is kept in lockstep with the
# core `kida-templates>=0.9.0` dependency above; the theme-provider capability
# guard (bengal/core/theme/providers.py) enforces this at build start.
chirp = [
    "chirp-ui>=0.9.0",
    "kida-templates>=0.9.0",
]


# Image processing (RFC: hugo-inspired-features)
# Note: Pillow is already a core dependency for social cards
# These extras add advanced image processing features
images = ["Pillow>=10.0"] # Already included, explicit for clarity
smartcrop = [
    "Pillow>=10.0",
    "smartcrop>=0.3",
] # Smart face/feature detection cropping
avif = ["Pillow>=10.0", "pillow-avif-plugin>=1.3"] # AVIF format output support
fast-images = ["pyvips>=2.2"] # libvips acceleration (advanced users)

# Gettext / PO file support (bengal i18n status, bengal i18n extract)
gettext = ["polib>=1.0"]

# Content Layer - Remote content sources
# These provide async HTTP for fetching from GitHub, REST APIs, Notion, etc.
github = ["aiohttp>=3.9.0"]
notion = ["aiohttp>=3.9.0", "cachetools>=5.0.0"]      # cachetools for block caching
rest = ["aiohttp>=3.9.0"]
all-sources = ["aiohttp>=3.9.0", "cachetools>=5.0.0"] # All remote sources

# Note: Dev dependencies use [dependency-groups] (PEP 735) - install with: uv sync --group dev

[project.scripts]
bengal = "bengal.cli.milo_app:run"

[project.entry-points."bengal.plugins"]

[project.urls]
Homepage = "https://lbliii.github.io/bengal/"
Documentation = "https://lbliii.github.io/bengal/"
Repository = "https://github.com/lbliii/bengal"
"Bug Tracker" = "https://github.com/lbliii/bengal/issues"

[tool.uv.workspace]
members = ["."]

[tool.setuptools.packages.find]
where = ["."]
include = ["bengal*"]

[tool.setuptools.package-data]
bengal = [
    "themes/**/*",
    "assets/**/*",
    "analysis/graph/templates/**/*",
    "errors/overlay/templates/**/*",
    "output/templates/**/*",
    "scaffolds/**/*",
    "py.typed",
]

[tool.ruff]
line-length = 100
target-version = "py314"

[tool.ruff.lint.isort]
# Pin first-party so CI and local (workspace vs repo root) agree on import grouping
known-first-party = ["bengal"]

[tool.ruff.lint]
# Enable Python upgrade rules and other quality checks
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes (unused imports, variables, etc.)
    "UP",   # pyupgrade - Python modernization
    "B",    # flake8-bugbear - common bugs
    "SIM",  # flake8-simplify - simplify code
    "I",    # isort - import sorting
    "PIE",  # misc lints (unnecessary pass, duplicate dict keys, etc.)
    "PERF", # perflint - performance anti-patterns
    "C4",   # unnecessary comprehensions/list()/dict() calls
    "RUF",  # Ruff-specific (unused noqa, mutable class defaults, etc.)
    "PT",   # flake8-pytest-style - pytest best practices
    "TCH",  # flake8-type-checking - move imports behind TYPE_CHECKING
    "FBT",  # flake8-boolean-trap - boolean positional args
    "T20",  # flake8-print - catch stray print() calls
    "ARG",  # flake8-unused-arguments - unused function parameters
    "RET",  # flake8-return - consistent return patterns
    "TRY",  # tryceratops - exception handling patterns
    "S",    # bandit - security checks
    "ERA",  # eradicate - commented-out code detection
]
ignore = [
    "E501",   # line too long - handled by formatter, manual review for long strings
    "RUF001", # ambiguous unicode in string (intentional × symbols, em-dashes)
    "RUF002", # ambiguous unicode in docstring (intentional typography)
    "RUF003", # ambiguous unicode in comment (intentional typography)
    # FBT: boolean positional args — would require API-breaking signature changes across 800+ sites.
    # Rules stay enabled for new code awareness; ignore existing violations.
    "FBT001", # boolean type hint positional argument
    "FBT002", # boolean default value positional argument
    "FBT003", # boolean positional value in function call
    # TRY: pragmatic exception patterns
    "TRY003", # raise with long message — intentional for clear user-facing errors
    "TRY203", # useless-try-except — intentional re-raise for CancelledError propagation
    "TRY300", # try-consider-else — style preference, not a correctness issue
    "TRY301", # raise-within-try — intentional re-raise patterns with logging
    "TRY400", # logger.error vs logger.exception — project uses .error intentionally
    # ARG: unused arguments common in interface/protocol/callback compliance
    "ARG001", # unused-function-argument — callbacks/hooks have fixed signatures
    "ARG002", # unused-method-argument — methods implement interfaces with fixed signatures
    "ARG003", # unused-class-method-argument — factory methods with fixed signatures
    "ARG005", # unused-lambda-argument — lambda callbacks with fixed signatures
    # RET: style preferences
    "RET504", # unnecessary-assign — intermediate vars aid debuggability
    # S: false positives for a documentation site generator
    "S105",   # hardcoded-password-string — false positives on config keys like "secret_key"
    # S110 enabled — silent try-except-pass now caught by ruff; use noqa with reason where intentional
    "S311",   # non-cryptographic random — fine for non-security uses (shuffling, sampling)
    "S324",   # insecure hash — MD5/SHA1 used for content hashing, not security
    "S701",   # jinja2-autoescape-false — template engine controls escaping contextually
    "S301",   # suspicious-pickle — used for build cache serialization
    "S314",   # suspicious-xml — used for sitemap/feed generation
    # S112 enabled — silent try-except-continue now caught by ruff; use noqa with reason where intentional
    "S101",   # assert — used as contract/invariant checks throughout
    "S310",   # suspicious-url-open — URL fetching is core functionality
    "S108",   # hardcoded-temp-file — /tmp paths in a build tool are fine
    "S323",   # unverified-context — intentional for dev server
    "S602",   # subprocess-popen-with-shell — controlled internal usage
    "S603",   # subprocess-without-shell-equals-true — controlled invocations
    "S607",   # start-process-with-partial-path — uses PATH lookup intentionally
    "S608",   # hardcoded-sql — not actual SQL, pattern matching false positive
    # ERA: commented-out code needs manual human review, not bulk auto-fix
    "ERA001", # commented-out code detection
]

# Per-file ignore rules
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]   # Allow unused imports in __init__ files
"tests/**/*.py" = [
    "S101",  # Allow assert in tests
    "ARG",   # Unused args common in fixtures/mocks
    "T201",  # Print in test helpers is fine
    "S108",  # Hardcoded /tmp paths in tests are fine
    "S311",  # Non-crypto random in tests is fine
    "S105",  # Hardcoded "password" strings in test fixtures
    "S603",  # Subprocess in tests is fine
    "S607",  # Partial path in tests is fine
    "TRY",   # Exception patterns in tests are intentionally varied
    "S110",  # try-except-pass is fine in test setup/teardown
    "S112",  # try-except-continue is fine in test iteration
]
# CLI layer — callbacks have fixed signatures; terminal output routes through CLIOutput/Milo.
"bengal/cli/**/*.py" = ["ARG"]
# Dev server / build output — print for output, callbacks have fixed signatures
"bengal/server/**/*.py" = ["T201", "S108", "S603", "S607", "ARG"]
"bengal/build/**/*.py" = ["T201"]
"bengal/core/site/**/*.py" = ["T201"]
# Rendering/template layer — print for output, callbacks have fixed signatures
"bengal/rendering/**/*.py" = ["T201", "ARG"]
# Scaffolds/init — print for user-facing scaffolding output
"bengal/scaffolds/**/*.py" = ["T201", "S108"]
# Analysis/debug/health tools — print for output, callbacks have fixed signatures
"bengal/analysis/**/*.py" = ["T201", "ARG"]
"bengal/debug/**/*.py" = ["T201"]
"bengal/health/**/*.py" = ["T201", "ARG"]
# Orchestration/output — print for build status, callbacks have fixed signatures
"bengal/orchestration/**/*.py" = ["T201", "ARG"]
"bengal/output/**/*.py" = ["T201", "ARG"]
# Autodoc config — print for deprecation warnings
"bengal/autodoc/**/*.py" = ["T201"]
# Config validators — print for user-facing validation errors
"bengal/config/**/*.py" = ["T201", "S108", "S603", "S607"]
# Content layer — print for warnings, subprocess for git, URL handling
"bengal/content/**/*.py" = ["T201", "S310", "S603", "S607"]
# Theme generation — print for CLI output
"bengal/themes/**/*.py" = ["T201"]
# Utils — print for output, callbacks have fixed signatures, subprocess for tools
"bengal/utils/**/*.py" = ["T201", "ARG", "S603", "S607"]
# Notebooks — print is the output mechanism
"site/**/*.ipynb" = ["T201"]
# Cursor/audit scripts — standalone scripts
".cursor/**/*.py" = ["T201", "S101", "S110", "S112", "S603", "S607"]
# Assets/CLI — subprocess for external tools (npm, pip, etc.)
"bengal/assets/**/*.py" = ["S603", "S607"]
"bengal/cli/commands/upgrade/**/*.py" = ["S603", "S607"]
"bengal/cli/commands/theme.py" = ["S603", "S607"]
# Protocols — unused args are inherent to interface definitions
"bengal/protocols/**/*.py" = ["ARG"]
# Directive renderers/parsers — callback signatures dictated by framework
"bengal/parsing/**/*.py" = ["ARG"]
# Template functions — many accept standard args they don't all use
"bengal/rendering/template_functions/**/*.py" = ["ARG"]
# Services — interface compliance
"bengal/services/**/*.py" = ["ARG"]
# Postprocess — subprocess for external tools, callbacks have fixed signatures
"bengal/postprocess/**/*.py" = ["S603", "S607", "ARG"]
# Scripts and benchmarks
"scripts/**/*.py" = ["T201", "S"]
"benchmarks/**/*.py" = ["T201", "S101", "S110", "S112", "S603", "S607", "ARG"]
# Jinja test functions use test_ prefix by convention (not pytest tests)
"bengal/rendering/template_tests.py" = ["PT028"]
# LRUCache.keys() returns list[K], not a dict - SIM118 false positive
"bengal/rendering/template_functions/navigation/scaffold.py" = ["SIM118"]
# PT017: These tests verify "either success or graceful failure" pattern
# PT012/SIM117: Context manager exception tests use nested structure intentionally
"tests/integration/test_error_recovery.py" = ["PT017"]
"tests/integration/test_config_system_integration.py" = ["PT017"]
"tests/integration/warm_build/test_data_files.py" = ["PT017"]
"tests/unit/rendering/test_asset_manifest_contextvar.py" = ["PT012", "SIM117"]
"tests/unit/rendering/parsers/test_patitas/test_contextvar_config.py" = [
    "PT012",
    "SIM117",
]
# Hypothesis @given injects parameter - pytest PT019 false positive
"tests/unit/utils/test_dates_properties.py" = ["PT019"]

[tool.ty.environment]
python-version = "3.14"

[tool.ty.src]
exclude = ["tests/roots/", "MagicMock/", "dist/", "build/"]

[tool.ty.rules]
# === ERRORS: Correctness rules that catch real bugs ===
missing-argument = "error"              # Enforce correct call signatures
unsupported-operator = "error"          # Enforce valid type operations
not-iterable = "error"                  # Enforce iterable before iteration
too-many-positional-arguments = "error" # Enforce correct argument counts
no-matching-overload = "error"          # Enforce matching overloads
unused-ignore-comment = "error"         # Enforce cleanup of stale type: ignore comments
# === TIGHTEN NEXT: promote to "error" as false positives decrease ===
invalid-argument-type = "warn"          # 852 violations — PageLike structural matching + list invariance (2026-04-10)
unresolved-attribute = "warn"           # 507 violations — hasattr narrowing + mixin Self + Optional access (2026-04-10)
invalid-assignment = "warn"             # 298 violations — list invariance + read-only protocol attrs (2026-04-10)

# === WARNINGS: Gradual typing — enforce incrementally ===
unresolved-import = "warn"               # Third-party stubs often incomplete
possibly-unresolved-reference = "ignore" # Too noisy with conditional imports
call-non-callable = "warn"              # False positives with Protocol/dynamic types
invalid-return-type = "warn"            # Gradual typing flexibility
invalid-type-arguments = "warn"         # Generics constraints too strict
not-subscriptable = "warn"              # Protocol objects missing __getitem__
invalid-method-override = "warn"        # Liskov violations - review later
unresolved-reference = "warn"           # Missing imports - fix incrementally
unknown-argument = "warn"               # Wrong kwargs - needs investigation

# Bengal is its own uv workspace root. Local development uses the pinned lockfile here;
# CI and standalone installs use PyPI per dependencies above.

[dependency-groups]
dev = [
    # Testing
    "pytest>=8.0.0",
    "pytest-cov>=4.1.0",
    "pytest-mock>=3.11.0",
    "pytest-xdist>=3.3.0",
    "pytest-split>=0.10.0",
    "pytest-timeout>=2.2.0",
    "pytest-asyncio>=0.23.0",
    "pytest-forked>=1.6.0",           # Isolated test execution for stateful tests
    "hypothesis>=6.92.0",
    # Linting & Type Checking
    "ty>=0.0.11",   # Astral type checker (Rust-based)
    "ruff>=0.15.1", # 0.15.1+ fixes except-parenthesis bug (PEP 758 + as clause)
    # Test Utilities
    "beautifulsoup4>=4.12.0", # HTML validation in integration tests
    "markdown>=3.5.0",        # Optional python-markdown parser for tests
    # Type Stubs
    "types-pyyaml>=6.0.12",
    "types-psutil>=5.9.0",
    "types-pygments>=2.18.0",
    "types-python-dateutil>=2.9.0",
    "types-markdown>=3.5.0",
    "types-jinja2>=2.11.0",
    "pytest-benchmark>=5.2.3",
    "radon>=6.0.1",
    "polib>=1.0",              # Gettext .po file support (i18n tests)
    "prek>=0.3.2",
    "poethepoet>=0.35.0",
    "pytest-randomly>=3.15.0",
    "towncrier>=24.0",
    "import-linter>=2.0",             # Layered-architecture enforcement (Epic: Errors & Site S-Tier, Sprint B0.2)
    "build>=1.2",                     # PEP 517 wheel builder; powers tests/integration/test_wheel_install.py
]

# =============================================================================
# Changelog (Towncrier) — see docs/b-stack-changelog-strategy.md
# =============================================================================

[tool.towncrier]
directory = "changelog.d"
filename = "CHANGELOG.md"
package = "bengal"
package_dir = "."
title_format = "## [{version}] - {project_date}"
issue_format = "`#{issue}`"
underlines = ["", "", ""]
# Steward docs / placeholders that live in changelog.d/ but are not fragments.
ignore = ["AGENTS.md", ".gitkeep"]

[[tool.towncrier.type]]
directory = "added"
name = "Added"
showcontent = true

[[tool.towncrier.type]]
directory = "changed"
name = "Changed"
showcontent = true

[[tool.towncrier.type]]
directory = "deprecated"
name = "Deprecated"
showcontent = true

[[tool.towncrier.type]]
directory = "removed"
name = "Removed"
showcontent = true

[[tool.towncrier.type]]
directory = "fixed"
name = "Fixed"
showcontent = true

[[tool.towncrier.type]]
directory = "security"
name = "Security"
showcontent = true

# =============================================================================
# Task Runner (poethepoet) - use: poe <task>
# =============================================================================
# Replaces Makefile with Python-native task definitions
# Run `poe --help` to see all available tasks

[tool.poe.tasks]
# Development Server
serve = { cmd = "bengal serve --source site", help = "Start development server for site/" }
dev = { cmd = "bengal serve", help = "Start dev server (auto-detects site)" }

# Building
build = { cmd = "bengal build --source site", help = "Build the documentation site" }
build-prod = { cmd = "bengal build --source site --environment production", help = "Build with production config" }

# Testing
test = { cmd = "pytest", help = "Run full test suite" }
test-unit = { cmd = "pytest tests/unit/ -x -q", help = "Run unit tests only (fast)" }
test-integration = { cmd = "pytest tests/integration/ -x -q --timeout=30", help = "Run integration tests" }
test-integration-durations = { cmd = "pytest tests/integration/ -o addopts= -n 0 --store-durations --durations-path=.test_durations -q -m \"not slow and not performance and not stateful and not autodoc and not known_gap\"", help = "Regenerate .test_durations for CI pytest-split (matches integration job filter; slow)" }
test-fast = { cmd = "pytest tests/ -x -q --ignore=tests/e2e --ignore=tests/integration", help = "Fast feedback loop (unit only)" }
test-cov = { cmd = "pytest tests/ --cov=bengal --cov-report=term-missing", help = "Run tests with coverage" }
test-parallel = { cmd = "pytest tests/ -n auto -x -q", help = "Run tests in parallel (pytest-xdist)" }
test-random = { cmd = "pytest tests/ -p randomly --randomly-seed=random -x -q", help = "Run tests in random order" }
test-verbose = { cmd = "pytest tests/ -v --tb=long", help = "Run tests with verbose output" }
proof-pr = { sequence = [
    "lint",
    "format-check",
    "ty",
    "test-unit",
    "benchmark-smoke",
], help = "Bounded PR proof path: lint, format, types, unit tests, and benchmark smoke guards" }
proof-pr-plan = { cmd = "python scripts/proof_pr_plan.py --format json", help = "Print the resolved bounded PR proof path as JSON" }
output-write-audit = { cmd = "python scripts/check_output_writes.py", help = "Audit direct writes in generated-output and cache-adjacent code" }
core-surface-audit = { cmd = "python scripts/check_core_surface_usage.py", help = "Audit internal use of core compatibility surfaces marked for retirement" }
performance-evidence-check = { cmd = "python scripts/check_performance_evidence.py --body-file .github/pull_request_template.md --allow-template", help = "Validate Performance Evidence fields in a PR body markdown file" }

# Benchmarks
benchmark = { cmd = "pytest benchmarks/ tests/performance/ -v --benchmark-only", help = "Run all benchmarks" }
benchmark-compare = { cmd = "pytest benchmarks/ tests/performance/ --benchmark-compare=baseline -v", help = "Compare to baseline" }
benchmark-save = { cmd = "pytest benchmarks/ tests/performance/ --benchmark-save=baseline -v", help = "Save baseline" }
benchmark-scenarios = { cmd = "python scripts/generate_benchmark_scenarios.py", help = "Generate cached benchmark scenarios" }
benchmark-smoke = { cmd = "pytest -o addopts= -n 0 -q tests/unit/benchmarks/test_benchmark_matrix.py tests/performance/test_autodoc_render_regression.py tests/performance/test_asset_fallback_cost.py tests/performance/test_post_render_pipeline_budget.py tests/performance/test_warm_build_baseline.py", help = "Run canonical smoke performance guards without full benchmark timings" }
benchmark-matrix = { cmd = "python benchmarks/run_matrix.py", help = "Run benchmark rows from benchmarks/benchmark_matrix.toml" }

# Type Checking
ty = { cmd = "ty check bengal/", help = "Run ty type checker (fast, Rust-based)" }
ty-strict = { cmd = "ty check bengal/ --error-on-warning", help = "Strict type checking (warnings as errors)" }

# Linting & Formatting
lint = { cmd = "ruff check .", help = "Run ruff linter" }
lint-fix = { cmd = "ruff check . --fix", help = "Run ruff linter with auto-fix" }
format = { cmd = "ruff format .", help = "Format code with ruff" }
format-check = { cmd = "ruff format . --check", help = "Check formatting without changing files" }
lint-imports = { cmd = "lint-imports --config .importlinter", help = "Check layered-architecture contracts (import-linter)" }

# Pre-commit
hooks = { cmd = "prek run --all-files", help = "Run all pre-commit hooks" }
hooks-install = { cmd = "prek install", help = "Install pre-commit hooks" }

# CI Pipeline (what CI runs)
ci = { sequence = [
    "lint",
    "ty",
    "test",
], help = "Run full CI check (lint → type → test)" }
check = { sequence = [
    "lint",
    "format-check",
    "ty",
], help = "Quick check (lint + format + types)" }

# Build & Release
dist = { shell = "rm -rf dist/ && uv build && ls -la dist/", help = "Build distribution packages" }
publish = { cmd = "uv publish", help = "Publish to PyPI" }
changelog = { cmd = "towncrier build --yes", help = "Compile changelog.d fragments into CHANGELOG.md" }
changelog-draft = { cmd = "towncrier build --draft", help = "Preview changelog from fragments (stdout)" }
changelog-check = { cmd = "towncrier check --compare-with origin/main", help = "Verify branch adds a fragment (vs main)" }
changelog-lint = { cmd = "python scripts/lint_changelog_voice.py", help = "Lint fragments for internal/meta leaks (coinage, branch-slugs); add --releases for site pages" }
release-notes = { cmd = "python scripts/draft_release_notes.py", help = "Draft a user-facing site release page from CHANGELOG.md (--version/--theme/--bundle)" }
gh-release = { cmd = "python scripts/publish_github_release.py", help = "Create/update the GitHub release from the site release page (--version/--theme/--create)" }
release = { sequence = [
    "changelog",
    "dist",
    "publish",
], help = "Compile changelog, build, and publish" }

# Code Quality Analysis
deadcode = { cmd = "ruff check . --select F401,F841 --statistics", help = "Find unused imports/variables" }
complexity = { cmd = "radon cc bengal/ -a -s", help = "Analyze code complexity" }

# Deploy Test (simulates GitHub Pages)
deploy-test.shell = """
bengal build --source site --environment production
echo ""
echo "Verifying CSS exists..."
ls -la site/public/assets/css/style.css || (echo "ERROR: CSS not found!" && exit 1)
echo ""
echo "Starting server at http://localhost:8000/bengal/"
echo "Press Ctrl+C to stop"
cd site/public && python3 -m http.server 8000
"""
deploy-test.help = "Build production & serve (simulates GitHub Pages)"

# Cleanup
clean.shell = """
rm -rf dist build .pytest_cache .coverage htmlcov .ruff_cache .bengal public .ty_cache
find . -type d -name '__pycache__' -exec rm -rf {} + 2>/dev/null || true
"""
clean.help = "Remove build artifacts and caches"
