diff --git a/Claude/AGENTS.md b/Claude/AGENTS.md index b4f594a..7d940a0 100644 --- a/Claude/AGENTS.md +++ b/Claude/AGENTS.md @@ -1,12 +1,25 @@ # AI Agents - Project Rules -**Document Version:** v4 (independent, incremented on structural changes) +**Document Version:** v5 (independent, incremented on structural changes) -Rules and instructions for AI assistants (Claude Code, Cursor, Copilot, etc.) +Language-agnostic rules for AI assistants (Claude Code, Cursor, Copilot, etc.). + +Everything language-specific — package manager, formatter, linter, test framework, +logging library, line length, project layout — lives in the matching design document: + +| Project type | Design document | +|--------------------|------------------------------| +| Python application | `DESIGN_DOCUMENT.md` | +| Python library | `DESIGN_DOCUMENT_MODULE.md` | +| Rust application | `DESIGN_DOCUMENT.md` | +| Rust library | `DESIGN_DOCUMENT_LIB.md` | +| Godot | `DESIGN_DOCUMENT_GODOT.md` | + +Where this file and a design document disagree, **the design document wins**. ## First-time setup -- **On first read of this file, immediately read all other `.md` files in the project root** (e.g. `PROJECT.md`, `CHANGELOG.md`, `DESIGN_DOCUMENT.md`) to get full project context before starting any task. +- **On first read of this file, immediately read all other `.md` files in the project root** (e.g. `PROJECT.md`, `CHANGELOG.md`, `DESIGN_DOCUMENT*.md`) to get full project context before starting any task. ## Language @@ -14,70 +27,68 @@ Rules and instructions for AI assistants (Claude Code, Cursor, Copilot, etc.) ## Dependency Management -- **Always use `poetry add`** to add dependencies, **never edit `pyproject.toml` directly** - ```bash - poetry add requests - poetry add --group dev pytest - ``` -- Use `poetry remove` to remove dependencies — **never edit `pyproject.toml` manually** +- **Always add and remove dependencies through the package manager CLI** — **never edit the manifest** (`pyproject.toml`, `Cargo.toml`, …) by hand +- The exact commands are in the design document for the given language ## Project Structure -- Entry points are in the project root (named after project or by purpose: `project_name.py`, `cli.py`, `gui.py`, `server.py`) -- A project can have multiple entry points -- All modules belong in the `src/` folder -- Tests belong in the `tests/` folder -- Virtual environment is in `.venv/` (do not copy, do not generate) +- Source code, tests and detailed documentation each have their own directory — the concrete layout is in the design document +- Detailed documentation belongs in `docs/`, never in the project root +- The project root holds only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT*.md`, `PROJECT.md`, `CHANGELOG.md` +- Entry points follow the language convention; a project may have several +- The dependency/build directory (`.venv/`, `target/`, …) is tool-managed — do not copy it, do not generate it by hand ## Code -- Always use type annotations -- Follow PEP8 and format with Ruff (88 characters per line) -- Before commit run `poetry run ruff check` and `poetry run mypy` +- **Always use static typing** — annotate every parameter and return value +- Format and lint with the tools named in the design document +- **Nothing is committed without a clean formatter, linter and test run** ## Testing -- **Use pytest exclusively** - never use the `unittest` module -- No `unittest.TestCase` classes, no `self.assert*` methods -- Use plain `assert` statements and pytest fixtures - -## Running - -- Use `poetry run` to run scripts: - ```bash - poetry run python project_name.py - poetry run pytest - ``` +- Use the test framework named in the design document — never a second framework alongside it +- Arrange-Act-Assert pattern +- Test naming: `test__` ## Logging -- Use **loguru** for logging - never use `print()` for debugging +- Use the logging library named in the design document — **never `print()` (or its language equivalent) for debugging** - Never log secrets, passwords, tokens, or API keys ## Environment and Secrets -- Store secrets in `.env` file with `ENV_DEBUG=true/false` variable -- Load secrets using `python-dotenv` and `os.getenv()` -- **Never commit `.env` file** +- Applications store secrets in `.env` and load them at runtime — **never commit `.env`** +- Debug mode is driven by an `ENV_DEBUG=true/false` flag +- Libraries do not read `.env` — configuration is passed in by the caller ## Git -- `.gitignore` should contain: `.venv/`, `__pycache__/`, `*.pyc`, `.mypy_cache/`, `.env` -- Do not commit `poetry.lock` only if it's a library (for applications, commit it) -- **Never commit this documentation** (`DESIGN_DOCUMENT.md`, `AGENTS.md`, `.claudeignore`) -- `PROJECT.md` **should be committed** - it's project-specific +- `.gitignore` must cover at least: the dependency/build directory, tool caches, and `.env` +- **Commit the lock file for applications, do not commit it for libraries** +- **Never commit shared documentation** (`AGENTS.md`, `DESIGN_DOCUMENT*.md`) — it comes from the documentation repository, not from the project +- `README.md`, `PROJECT.md` and `CHANGELOG.md` **are committed** — they are project-specific ## Versioning -- **Always ask user before bumping version** - never increase version automatically -- **Keep `CHANGELOG.md` updated** - document all significant changes as they are made -- Update `CHANGELOG.md` with changes before version bump -- Version is defined in `pyproject.toml` under `[project]` section - Follow semantic versioning (MAJOR.MINOR.PATCH) +- **Always ask user before bumping version** — never increase version automatically +- **Keep `CHANGELOG.md` updated** — document all significant changes as they are made +- Update `CHANGELOG.md` **before** the version bump +- The version source of truth is the project manifest (`pyproject.toml`, `Cargo.toml`, `project.godot`) ## Task Management -- **When completing tasks, mark them as done** - if you finish any task with a checkbox anywhere in project documentation, check it off as completed `[ ]` → `[x]` -- **Track all work** - this applies to tasks in `PROJECT.md` (TODO section, Development Roadmap, any checklists) and other documentation -- **Update documentation** - when completing changes, update relevant sections in `PROJECT.md`, `CHANGELOG.md`, and architecture diagrams -- **Keep task lists current** - completed items with `[x]` stay visible to show progress history +Tasks are **single-line comments in the code**, written with Todo Tree tags: + +| Tag | Meaning | +|----------|--------------------------------------------------| +| `TODO:` | work still to be done | +| `FIXME:` | something broken that must be repaired | +| `BUG:` | a known defect, not fixed yet | +| `HACK:` | temporary workaround, needs rewriting | +| `NOTE:` | important context for whoever reads this next | + +- **No checkboxes and no numbered task lists in documentation** — the code is the task list +- `PROJECT.md` carries only cross-cutting tasks that have no single place in the code +- If a tag already exists at a location in code, do not repeat it in `PROJECT.md` +- **Update documentation** — when completing changes, update the relevant sections of `PROJECT.md` and `CHANGELOG.md` diff --git a/Claude/CLAUDE.md b/Claude/CLAUDE.md index c11f258..303b099 100644 --- a/Claude/CLAUDE.md +++ b/Claude/CLAUDE.md @@ -7,4 +7,4 @@ - `AGENTS.md` - `PROJECT.md` - `CHANGELOG.md` -- `DESIGN_DOCUMENT.md` \ No newline at end of file +- `DESIGN_DOCUMENT*.md` (`DESIGN_DOCUMENT.md`, `DESIGN_DOCUMENT_MODULE.md`, `DESIGN_DOCUMENT_LIB.md` or `DESIGN_DOCUMENT_GODOT.md`) \ No newline at end of file diff --git a/Claude/skills/changelog/SKILL.md b/Claude/skills/changelog/SKILL.md index de16535..2eef8aa 100644 --- a/Claude/skills/changelog/SKILL.md +++ b/Claude/skills/changelog/SKILL.md @@ -5,4 +5,4 @@ description: Updates changelog in project Take known info and make changes to CHANGELOG.md to make them up-to-date. -Ignore changes to: `docs/`, `CONTEXT.md`, `Documentation_AI.md`, `Documentation_human.md` — documentation and context files are not logged in the changelog. +Ignore changes to: `docs/`, `CONTEXT.md`, `AGENTS.md`, `DESIGN_DOCUMENT*.md`, `PROJECT.md` — documentation and context files are not logged in the changelog. diff --git a/Godot/.gitignore b/Godot/.gitignore new file mode 100644 index 0000000..addbf64 --- /dev/null +++ b/Godot/.gitignore @@ -0,0 +1,14 @@ +# Godot — append on top of the shared core (Project template/.gitignore). + +# --- Editor and import caches --- +.godot/ +.import/ + +# --- Builds --- +# Decide per project (DESIGN_DOCUMENT_GODOT.md, section 13): for small internal +# distribution the repository may be the channel, otherwise uncomment. +# build/ + +# NOTE: addons/ IS committed so the project opens cleanly on any machine +# (section 11). export_presets.cfg is committed too, but must never +# contain keystore passwords or signing credentials. diff --git a/Godot/DESIGN_DOCUMENT_GODOT.md b/Godot/DESIGN_DOCUMENT_GODOT.md index 238a8bd..c716df7 100644 --- a/Godot/DESIGN_DOCUMENT_GODOT.md +++ b/Godot/DESIGN_DOCUMENT_GODOT.md @@ -4,7 +4,7 @@ > **Note on Versioning:** > - This document version is independent — reused across Godot projects -> - **Project version** source of truth: `project.godot` (`config/version`) mirrored in an autoload `Constants` script +> - **Project version** source of truth: `project.godot` (`application/config/version`) mirrored in an autoload `Constants` script > - Version propagates: `project.godot` → `Constants.gd` → code > - `CHANGELOG.md` uses the project version @@ -19,7 +19,7 @@ All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. -The root directory contains only the core documents: `DESIGN_DOCUMENT_GODOT.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`. +The root directory contains only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT_GODOT.md`, `PROJECT.md`, `CHANGELOG.md`. --- @@ -213,16 +213,19 @@ When the game is distributed as a standalone build: ### Task notation -Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns: +Tasks are written as single-line comments directly in code, using the **Todo Tree** tags defined in `AGENTS.md` (`TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`). `PROJECT.md` carries only cross-cutting tasks that have no single place in the code. ```gdscript -# TODO: one-liner description of a task to be done -# FIXME: one-liner description of a known bug to be fixed +# TODO: move this into a reusable StateMachine scene +# FIXME: player clips through the floor at high speed +# BUG: hitbox stays active one frame too long +# HACK: temporary yield until the animation signal is wired up +# NOTE: must run in _physics_process, not _process ``` -No other task format is used — no checkboxes, no numbered lists in documentation. +No other task format is used — **no checkboxes, no numbered lists in documentation**. -If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`. +If a tag already exists at a specific location in code, do not repeat it in `PROJECT.md`. --- diff --git a/Project template/.gitignore b/Project template/.gitignore index e69de29..37862da 100644 --- a/Project template/.gitignore +++ b/Project template/.gitignore @@ -0,0 +1,22 @@ +# Shared core .gitignore — language-agnostic. +# Append the language-specific file from Python/, Rust/ or Godot/ on top of this. + +# --- Secrets --- +.env +.env.* +!.env.example + +# --- Logs --- +logs/ +*.log + +# --- IDE / editor --- +.vscode/ +.idea/ +*.code-workspace +*.swp + +# --- OS --- +Thumbs.db +Desktop.ini +.DS_Store diff --git a/Python/.gitignore b/Python/.gitignore new file mode 100644 index 0000000..0522a36 --- /dev/null +++ b/Python/.gitignore @@ -0,0 +1,28 @@ +# Python — append on top of the shared core (Project template/.gitignore). + +# --- Virtual environment --- +.venv/ + +# --- Bytecode and tool caches --- +__pycache__/ +*.py[cod] +.mypy_cache/ +.ruff_cache/ +.pytest_cache/ +.coverage + +# --- Build artefacts --- +build/ +*.egg-info/ + +# --- Distribution --- +# Applications built with PyInstaller COMMIT dist/ — the repository is the +# distribution channel (DESIGN_DOCUMENT.md, section 13), so leave the line below +# commented out. Libraries publish to PyPI and DO ignore dist/ +# (DESIGN_DOCUMENT_MODULE.md, section 14) — uncomment it there. +# dist/ + +# --- Lock file --- +# Applications commit poetry.lock — leave the line below commented out. +# Libraries do not commit it — uncomment it there. +# poetry.lock diff --git a/Python/DESIGN_DOCUMENT.md b/Python/DESIGN_DOCUMENT.md index 5d859f1..604af96 100644 --- a/Python/DESIGN_DOCUMENT.md +++ b/Python/DESIGN_DOCUMENT.md @@ -19,13 +19,13 @@ All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. -The root directory contains only the core documents: `DESIGN_DOCUMENT.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`. +The root directory contains only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT.md`, `PROJECT.md`, `CHANGELOG.md`. --- ## 1. Code Style -- **PEP8** with 150-character lines (Ruff) +- **PEP8** with 120-character lines (Ruff) - **4 spaces** indentation - **snake_case** functions/variables, **PascalCase** classes, **SCREAMING_SNAKE_CASE** constants - **Type hints** required on all functions @@ -76,7 +76,7 @@ Use **loguru** for all internal logging. Never log secrets, passwords, tokens, o File sink retains **max 10 log files** (`retention=10`). No rotation by size — each run creates a new file via `{time}` in the filename. -The `DEBUG` sink is only active when `constants.DEBUG` is `True` (controlled by `ENV_DEBUG=true` in `.env`). +The `DEBUG` sink is only active when `constants.DEFAULT_DEBUG` is `True` (controlled by `ENV_DEBUG=true` in `.env`). Additional sinks (e.g. GUI log panels) may be added per project as needed. @@ -131,6 +131,7 @@ Run before every commit: ```bash poetry run ruff check poetry run mypy +poetry run pytest ``` --- @@ -147,6 +148,10 @@ poetry run # Run command in virtualenv Never edit `pyproject.toml` directly to add or remove dependencies. +### poetry.lock + +`poetry.lock` **is committed** for applications — it pins the exact dependency graph and keeps builds reproducible. + --- ## 12. Project Structure @@ -171,12 +176,24 @@ When a project is distributed as a standalone executable (no Python required on - Use **PyInstaller** to compile each entry point into a single `.exe` - Each tool has its own `.spec` file in the project root -- All console tools must use `console=True` in the `.spec` — tools rely on `input()` and `print()` for user interaction +- **Console tools use `console=True`** in the `.spec` — they rely on `input()` and `print()` for user interaction. GUI applications turn the console off with `ENV_BUILD_CONSOLE=false`; that flag is independent of `ENV_DEBUG` - Compiled executables are stored in `dist/` and **committed to the repository** — the repository serves as the distribution channel for internal teams - `.gitignore` must **not** exclude `dist/` in projects that use this deployment model -Build command: +### prebuild.py + +`prebuild.py` runs before PyInstaller. It verifies that the active interpreter is the project `.venv`, prints the resolved version, and rewrites the `console=` line in the `.spec` to match `.env`. + +| Variable | Default | Effect | +|----------|---------|--------| +| `ENV_BUILD_CONSOLE` | `true` | Value written into `console=` in the `.spec` | +| `ENV_BUILD_SPEC` | `.spec` | Which `.spec` file to update | + +`prebuild.py` touches one `.spec` per run — with several entry points, set `ENV_BUILD_SPEC` for each build. + +Build commands: ```bash +poetry run python prebuild.py poetry run pyinstaller ToolName.spec ``` @@ -200,13 +217,16 @@ poetry run pyinstaller ToolName.spec ### Task notation -Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns: +Tasks are written as single-line comments directly in code, using the **Todo Tree** tags defined in `AGENTS.md` (`TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`). `PROJECT.md` carries only cross-cutting tasks that have no single place in the code. ```python -# TODO: one-liner description of a task to be done -# FIXME: one-liner description of a known bug to be fixed +# TODO: extract this into a separate loader +# FIXME: crashes on an empty file +# BUG: rounding is off by one cent on negative amounts +# HACK: temporary workaround until the API adds paging +# NOTE: order matters here, the parser is stateful ``` -No other task format is used — no checkboxes, no numbered lists in documentation. +No other task format is used — **no checkboxes, no numbered lists in documentation**. -If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`. +If a tag already exists at a specific location in code, do not repeat it in `PROJECT.md`. diff --git a/Python/DESIGN_DOCUMENT_MODULE.md b/Python/DESIGN_DOCUMENT_MODULE.md index 2ec4855..1906942 100644 --- a/Python/DESIGN_DOCUMENT_MODULE.md +++ b/Python/DESIGN_DOCUMENT_MODULE.md @@ -18,13 +18,13 @@ All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. -The root directory contains only the core documents: `DESIGN_DOCUMENT_MODULE.md`, `AGENTS.md`, `PROJECT.md`, `CHANGELOG.md`. +The root directory contains only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT_MODULE.md`, `PROJECT.md`, `CHANGELOG.md`. --- ## 1. Code Style -- **PEP8** with 150-character lines (Ruff) +- **PEP8** with 120-character lines (Ruff) - **4 spaces** indentation - **snake_case** functions/variables, **PascalCase** classes, **SCREAMING_SNAKE_CASE** constants - **Type hints** required on all functions @@ -248,13 +248,16 @@ poetry publish # Publishes to PyPI (requires credentials) ### Task notation -Tasks are written as single-line comments directly in code, or in `PROJECT.md` for cross-cutting concerns: +Tasks are written as single-line comments directly in code, using the **Todo Tree** tags defined in `AGENTS.md` (`TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`). `PROJECT.md` carries only cross-cutting tasks that have no single place in the code. ```python -# TODO: one-liner description of a task to be done -# FIXME: one-liner description of a known bug to be fixed +# TODO: extract this into a separate loader +# FIXME: crashes on an empty file +# BUG: rounding is off by one cent on negative amounts +# HACK: temporary workaround until the API adds paging +# NOTE: order matters here, the parser is stateful ``` -No other task format is used — no checkboxes, no numbered lists in documentation. +No other task format is used — **no checkboxes, no numbered lists in documentation**. -If a `# TODO:` comment already exists at a specific location in code, do not repeat it in `PROJECT.md`. +If a tag already exists at a specific location in code, do not repeat it in `PROJECT.md`. diff --git a/Python/TEMPLATE.md b/Python/TEMPLATE.md index 56955bc..57c91a7 100644 --- a/Python/TEMPLATE.md +++ b/Python/TEMPLATE.md @@ -16,10 +16,8 @@ template/ ├── pyproject.toml # Poetry config (ruff, mypy, pytest) ├── src/ │ ├── __init__.py -│ └── core/ -│ ├── __init__.py -│ ├── _version.py # Version fallback for PyInstaller -│ └── constants.py # Version extraction from toml + DEBUG mode +│ ├── _version.py # Version fallback for PyInstaller +│ └── constants.py # Version extraction from toml + DEBUG mode └── tests/ ├── __init__.py └── test_constants.py # Basic test @@ -28,14 +26,15 @@ template/ ## Key Features - **Version extraction** from `pyproject.toml` with `_version.py` fallback for PyInstaller builds -- **DEBUG mode** via `ENV_DEBUG=true` in `.env` (adds " DEV" suffix to version) +- **DEBUG mode** via `ENV_DEBUG=true` in `.env` (adds "DEV" suffix to version: v1.2.3DEV) - **loguru** for logging (never print) - **Poetry** for dependency management - **pytest** for testing (no unittest) -- **ruff + mypy** for linting and type checking +- **ruff + mypy** for linting and type checking (120-character lines) ## Rules - No `.example` suffixes - the folder itself is the separator - Generic/reusable format - Keep files simple and minimal +- `.gitignore` is the shared core from `Project template/.gitignore` with `Python/.gitignore` appended diff --git a/Python/prebuild.py b/Python/prebuild.py index 84de3ae..7d400b4 100644 --- a/Python/prebuild.py +++ b/Python/prebuild.py @@ -1,7 +1,9 @@ import os import sys from pathlib import Path + from dotenv import load_dotenv + from src.constants import VERSION load_dotenv() @@ -33,17 +35,20 @@ else: print(f"✓ Version: {VERSION}") -env_debug = os.getenv("ENV_DEBUG", "false").lower() == "true" -console_mode = env_debug +env_debug = os.getenv("ENV_DEBUG", "false").lower() in ("true", "1", "yes") + +# console=True is the default — console tools rely on input() and print(). +# GUI applications turn it off with ENV_BUILD_CONSOLE=false in .env. +console_mode = os.getenv("ENV_BUILD_CONSOLE", "true").lower() in ("true", "1", "yes") default_spec = Path(__file__).parent.name + ".spec" spec_filename = os.getenv("ENV_BUILD_SPEC", default_spec) print(f"\n{'-' * 50}") print("BUILD SETTINGS") print(f"{'-' * 50}") -print(f"ENV_DEBUG: {env_debug}") -print(f"Console mode: {console_mode}") -print(f"Spec file: {spec_filename}") +print(f"ENV_DEBUG: {env_debug}") +print(f"ENV_BUILD_CONSOLE: {console_mode}") +print(f"Spec file: {spec_filename}") spec_path = Path(__file__).parent / spec_filename if spec_path.exists(): diff --git a/Python/src/constants.py b/Python/src/constants.py index 68d877a..19340c3 100644 --- a/Python/src/constants.py +++ b/Python/src/constants.py @@ -1,6 +1,8 @@ """ Generic application constants template. +Requires Python 3.11+ (uses the stdlib `tomllib` module). + Usage in your project: 1. Copy this file to src/constants.py 2. Fill in APP_NAME and APP_FULL_NAME @@ -14,14 +16,14 @@ Version loading priority: Debug mode: Controlled exclusively via .env: ENV_DEBUG=true Accepted true-values: true, 1, yes (case-insensitive) + When enabled, VERSION carries a "DEV" suffix with no separator: v1.2.3DEV """ import os -import tomllib from pathlib import Path +import tomllib from dotenv import load_dotenv -from loguru import logger load_dotenv() @@ -38,22 +40,28 @@ def _load_version() -> str: # 1. pyproject.toml try: with open(_PYPROJECT, "rb") as f: - version = tomllib.load(f)["project"]["version"] - # Write fallback for frozen/PyInstaller builds - _VERSION_FILE.write_text( - f'"""Auto-generated — do not edit manually."""\n__version__ = "{version}"\n', - encoding="utf-8", - ) - return version - except (FileNotFoundError, KeyError): + version: str = tomllib.load(f)["project"]["version"] + except (OSError, KeyError, tomllib.TOMLDecodeError): pass + else: + # Write the fallback used by frozen/PyInstaller builds, which do not ship + # pyproject.toml. A read-only or missing target is not a fatal condition. + try: + _VERSION_FILE.write_text( + f'"""Auto-generated — do not edit manually."""\n__version__ = "{version}"\n', + encoding="utf-8", + ) + except OSError: + pass + return version # 2. _version.py try: - from src._version import __version__ # type: ignore[import] - return __version__ + from src._version import __version__ except ImportError: pass + else: + return __version__ # 3. last resort return "0.0.0" @@ -63,6 +71,7 @@ def _load_version() -> str: # Debug mode # --------------------------------------------------------------------------- + def _load_debug() -> bool: return os.getenv("ENV_DEBUG", "false").lower() in ("true", "1", "yes") diff --git a/Python/tests/test_constants.py b/Python/tests/test_constants.py index 7156474..2396e06 100644 --- a/Python/tests/test_constants.py +++ b/Python/tests/test_constants.py @@ -1,93 +1,125 @@ -"""Tests for constants module.""" +"""Tests for the constants module template.""" import re +import sys from pathlib import Path -from unittest.mock import mock_open, patch import pytest +from src import constants from src.constants import ( + APP_FULL_NAME, APP_NAME, APP_TITLE, - APP_VERSION, - ENV_DEBUG, - get_debug_mode, - get_version, + DEFAULT_DEBUG, + VERSION, ) - -# --------------------------------------------------------------------------- -# get_version() -# --------------------------------------------------------------------------- +SEMVER = re.compile(r"^\d+\.\d+\.\d+") -def test_get_version_returns_string() -> None: - """get_version() should return a string.""" - assert isinstance(get_version(), str) - - -def test_get_version_semver_format() -> None: - """get_version() should return a semver-like string X.Y.Z.""" - version = get_version() - assert re.match(r"^\d+\.\d+\.\d+", version), f"Not semver: {version!r}" - - -def test_get_version_fallback_when_toml_missing(tmp_path: Path) -> None: - """get_version() returns '0.0.0-unknown' when pyproject.toml and _version.py are both missing.""" - missing = tmp_path / "nonexistent.toml" - with patch("src.constants._PYPROJECT_PATH", missing): - result = get_version() - # Either fallback _version.py exists (from previous run) or returns unknown - assert isinstance(result, str) - assert len(result) > 0 - - -def test_get_version_unknown_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: - """get_version() returns '0.0.0-unknown' when all sources are unavailable.""" - missing = tmp_path / "nonexistent.toml" - monkeypatch.setattr("src.constants._PYPROJECT_PATH", missing) - - # Patch _version import to also fail - with patch("src.constants.Path.write_text", side_effect=OSError): - with patch.dict("sys.modules", {"src._version": None}): - result = get_version() - - assert isinstance(result, str) +def _write_pyproject(tmp_path: Path, version: str = "1.2.3") -> Path: + """Create a minimal pyproject.toml carrying the given version.""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text(f'[project]\nname = "demo"\nversion = "{version}"\n', encoding="utf-8") + return pyproject # --------------------------------------------------------------------------- -# get_debug_mode() +# _load_version() # --------------------------------------------------------------------------- -def test_get_debug_mode_returns_bool() -> None: - """get_debug_mode() should always return a bool.""" - assert isinstance(get_debug_mode(), bool) +def test_load_version_reads_pyproject(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """_load_version() takes the version from [project] in pyproject.toml.""" + monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) + monkeypatch.setattr(constants, "_VERSION_FILE", tmp_path / "_version.py") + + assert constants._load_version() == "1.2.3" -def test_get_debug_mode_true(monkeypatch: pytest.MonkeyPatch) -> None: - """get_debug_mode() returns True when ENV_DEBUG=true.""" - monkeypatch.setenv("ENV_DEBUG", "true") - assert get_debug_mode() is True +def test_load_version_writes_fallback_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """_load_version() regenerates _version.py so frozen builds keep the version.""" + version_file = tmp_path / "_version.py" + monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) + monkeypatch.setattr(constants, "_VERSION_FILE", version_file) + + constants._load_version() + + assert '__version__ = "1.2.3"' in version_file.read_text(encoding="utf-8") -def test_get_debug_mode_true_variants(monkeypatch: pytest.MonkeyPatch) -> None: - """get_debug_mode() accepts '1' and 'yes' as truthy values.""" - for value in ("1", "yes", "YES", "True", "TRUE"): - monkeypatch.setenv("ENV_DEBUG", value) - assert get_debug_mode() is True, f"Expected True for ENV_DEBUG={value!r}" +def test_load_version_survives_unwritable_fallback(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """An unwritable _version.py location must not break version loading.""" + monkeypatch.setattr(constants, "_PYPROJECT", _write_pyproject(tmp_path)) + monkeypatch.setattr(constants, "_VERSION_FILE", tmp_path / "missing_dir" / "_version.py") + + assert constants._load_version() == "1.2.3" -def test_get_debug_mode_false(monkeypatch: pytest.MonkeyPatch) -> None: - """get_debug_mode() returns False when ENV_DEBUG=false.""" - monkeypatch.setenv("ENV_DEBUG", "false") - assert get_debug_mode() is False +def test_load_version_falls_back_to_version_module(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Without pyproject.toml the version comes from src/_version.py.""" + from src import _version + + monkeypatch.setattr(constants, "_PYPROJECT", tmp_path / "nonexistent.toml") + + assert constants._load_version() == _version.__version__ -def test_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> None: - """get_debug_mode() returns False when ENV_DEBUG is not set.""" +def test_load_version_missing_version_key(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """A pyproject.toml without [project] version falls through to the next source.""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text('[project]\nname = "demo"\n', encoding="utf-8") + monkeypatch.setattr(constants, "_PYPROJECT", pyproject) + monkeypatch.setitem(sys.modules, "src._version", None) + + assert constants._load_version() == "0.0.0" + + +def test_load_version_malformed_toml(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """A malformed pyproject.toml falls through instead of raising.""" + pyproject = tmp_path / "pyproject.toml" + pyproject.write_text("[project\nversion = ", encoding="utf-8") + monkeypatch.setattr(constants, "_PYPROJECT", pyproject) + monkeypatch.setitem(sys.modules, "src._version", None) + + assert constants._load_version() == "0.0.0" + + +def test_load_version_last_resort(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """With no source available at all, _load_version() returns '0.0.0'.""" + monkeypatch.setattr(constants, "_PYPROJECT", tmp_path / "nonexistent.toml") + monkeypatch.setitem(sys.modules, "src._version", None) + + assert constants._load_version() == "0.0.0" + + +# --------------------------------------------------------------------------- +# _load_debug() +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("value", ["true", "TRUE", "True", "1", "yes", "YES"]) +def test_load_debug_truthy_values(value: str, monkeypatch: pytest.MonkeyPatch) -> None: + """_load_debug() accepts 'true', '1' and 'yes' in any casing.""" + monkeypatch.setenv("ENV_DEBUG", value) + + assert constants._load_debug() is True + + +@pytest.mark.parametrize("value", ["false", "FALSE", "0", "no", "", "maybe"]) +def test_load_debug_falsy_values(value: str, monkeypatch: pytest.MonkeyPatch) -> None: + """Anything outside the accepted true-values is False.""" + monkeypatch.setenv("ENV_DEBUG", value) + + assert constants._load_debug() is False + + +def test_load_debug_defaults_to_false(monkeypatch: pytest.MonkeyPatch) -> None: + """_load_debug() returns False when ENV_DEBUG is not set.""" monkeypatch.delenv("ENV_DEBUG", raising=False) - assert get_debug_mode() is False + + assert constants._load_debug() is False # --------------------------------------------------------------------------- @@ -95,47 +127,28 @@ def test_get_debug_mode_false_when_unset(monkeypatch: pytest.MonkeyPatch) -> Non # --------------------------------------------------------------------------- -def test_env_debug_is_bool() -> None: - """ENV_DEBUG should be a bool.""" - assert isinstance(ENV_DEBUG, bool) +def test_app_name_is_set() -> None: + """APP_NAME and APP_FULL_NAME must be filled in per project.""" + assert isinstance(APP_NAME, str) and APP_NAME + assert isinstance(APP_FULL_NAME, str) and APP_FULL_NAME -def test_app_version_is_string() -> None: - """APP_VERSION should be a string.""" - assert isinstance(APP_VERSION, str) +def test_default_debug_is_bool() -> None: + """DEFAULT_DEBUG should be a bool.""" + assert isinstance(DEFAULT_DEBUG, bool) -def test_app_version_semver_format() -> None: - """APP_VERSION should follow semver format X.Y.Z.""" - assert re.match(r"^\d+\.\d+\.\d+", APP_VERSION), f"Not semver: {APP_VERSION!r}" +def test_version_is_prefixed_semver() -> None: + """VERSION is the project version prefixed with 'v'.""" + assert VERSION.startswith("v") + assert SEMVER.match(VERSION.removeprefix("v")), f"Not semver: {VERSION!r}" -def test_app_name_value() -> None: - """APP_NAME should be 'X4 SavEd'.""" - assert APP_NAME == "X4 SavEd" +def test_version_dev_suffix_matches_debug_flag() -> None: + """VERSION ends with 'DEV' (no separator) exactly when DEFAULT_DEBUG is True.""" + assert VERSION.endswith("DEV") is DEFAULT_DEBUG -def test_app_title_contains_name_and_version() -> None: - """APP_TITLE should contain APP_NAME and APP_VERSION.""" - assert APP_NAME in APP_TITLE - assert APP_VERSION in APP_TITLE - - -def test_app_title_dev_suffix_when_debug(monkeypatch: pytest.MonkeyPatch) -> None: - """APP_TITLE ends with '-DEV' when ENV_DEBUG is True.""" - import importlib - import src.constants as consts - - monkeypatch.setenv("ENV_DEBUG", "true") - monkeypatch.setattr(consts, "ENV_DEBUG", True) - title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if True else "") - assert title.endswith("-DEV") - - -def test_app_title_no_dev_suffix_when_not_debug(monkeypatch: pytest.MonkeyPatch) -> None: - """APP_TITLE does not end with '-DEV' when ENV_DEBUG is False.""" - import src.constants as consts - - monkeypatch.setattr(consts, "ENV_DEBUG", False) - title = f"{consts.APP_NAME} v{consts.APP_VERSION}" + ("-DEV" if False else "") - assert not title.endswith("-DEV") +def test_app_title_is_full_name_and_version() -> None: + """APP_TITLE joins APP_FULL_NAME and VERSION with a single space.""" + assert APP_TITLE == f"{APP_FULL_NAME} {VERSION}" diff --git a/README.md b/README.md index ac4902b..3a3f41f 100644 --- a/README.md +++ b/README.md @@ -10,52 +10,72 @@ This repository is the single source of truth for how I develop software. AI ass ``` Dokumentace/ -├── Claude/ # AI assistant configuration -│ ├── CLAUDE.md # Claude Code session instructions -│ ├── AGENTS.md # Rules for all AI assistants -│ └── skills/ # Custom Claude Code skills +├── Claude/ # AI assistant configuration +│ ├── CLAUDE.md # Claude Code session instructions +│ ├── AGENTS.md # Language-agnostic rules for all AI assistants +│ └── skills/ # Custom Claude Code skills │ -├── Python/ # Python development guidelines -│ ├── DESIGN_DOCUMENT.md # Guidelines for Python applications -│ ├── DESIGN_DOCUMENT_MODULE.md # Guidelines for Python libraries -│ ├── TEMPLATE.md # New project template specification -│ └── prebuild.py # Pre-build script (PyInstaller) +├── Python/ # Python development guidelines +│ ├── DESIGN_DOCUMENT.md # Guidelines for Python applications +│ ├── DESIGN_DOCUMENT_MODULE.md # Guidelines for Python libraries +│ ├── TEMPLATE.md # New project template specification +│ ├── .gitignore # Python-specific ignore rules +│ ├── prebuild.py # Pre-build script (PyInstaller) +│ ├── src/ # Reference constants module + version fallback +│ └── tests/ # Tests for the reference module │ -├── Rust/ # Rust development guidelines -│ ├── DESIGN_DOCUMENT.md # Guidelines for Rust applications -│ └── DESIGN_DOCUMENT_LIB.md # Guidelines for Rust libraries +├── Rust/ # Rust development guidelines +│ ├── DESIGN_DOCUMENT.md # Guidelines for Rust applications +│ ├── DESIGN_DOCUMENT_LIB.md # Guidelines for Rust libraries +│ └── .gitignore # Rust-specific ignore rules │ -├── Project template/ # Reusable files for new projects -│ ├── CHANGELOG.md # Changelog template -│ └── PROJECT.md # Project documentation template +├── Godot/ # Godot development guidelines +│ ├── DESIGN_DOCUMENT_GODOT.md # Guidelines for Godot projects +│ └── .gitignore # Godot-specific ignore rules │ -└── Zscaler/ # Corporate network setup - ├── NODE_EXTRA_CA_CERTS.md # Instructions for Zscaler cert +├── Project template/ # Reusable files for new projects +│ ├── CHANGELOG.md # Changelog template +│ ├── PROJECT.md # Project documentation template +│ └── .gitignore # Shared, language-agnostic ignore rules +│ +└── Zscaler/ # Corporate network setup + ├── ZSCALER_CERTIFICATE.md # Cert setup for Node, Python, git, cargo └── ZscalerRootCertificate-2048-SHA256.crt ``` ## AI Coding Workflow -Each project references these documents so AI assistants operate within consistent rules: +Every project gets two rule documents, and the split between them is strict: -- **AGENTS.md** — language-agnostic rules: dependency management, testing, logging, git, versioning -- **DESIGN_DOCUMENT.md** — Python-specific: code style, tooling (Ruff, mypy, pytest, Poetry), project structure, logging with loguru, distribution via PyInstaller -- **DESIGN_DOCUMENT_MODULE.md** — same as above, adapted for Python libraries (no sinks, no `.env`, PyPI distribution) +- **AGENTS.md** — language-agnostic rules only: documentation language, dependency management, static typing, secrets, git, versioning, task notation. It names no concrete tool. +- **DESIGN_DOCUMENT\*.md** — everything language-specific: package manager, formatter, linter, test framework, logging library, line length, project layout, distribution. -## Languages Covered +Where the two disagree, **the design document wins**. -| Language | Guidelines | -|----------|-----------| +| Project type | Design document | +|--------------|-----------------| | Python (application) | `Python/DESIGN_DOCUMENT.md` | | Python (library) | `Python/DESIGN_DOCUMENT_MODULE.md` | | Rust (application) | `Rust/DESIGN_DOCUMENT.md` | | Rust (library) | `Rust/DESIGN_DOCUMENT_LIB.md` | +| Godot | `Godot/DESIGN_DOCUMENT_GODOT.md` | + +### Starting a new project + +1. Copy `Project template/PROJECT.md` and `Project template/CHANGELOG.md` into the project root — both are committed. +2. Build `.gitignore` from `Project template/.gitignore` plus the `.gitignore` of the matching language folder. +3. Copy `Claude/AGENTS.md` and the matching `DESIGN_DOCUMENT*.md` into the project root — these are **not** committed, they come from here. +4. For Python, follow `Python/TEMPLATE.md` to generate the project skeleton. ## Key Conventions - **Python tooling:** Poetry · Ruff · mypy · pytest · loguru -- **No print() for debugging** — loguru everywhere -- **Type hints required** on all functions -- **Tests:** pytest only, no unittest, no mocks of the database -- **Versioning:** semantic, always ask before bumping -- **Secrets:** `.env` + `python-dotenv`, never committed +- **Rust tooling:** Cargo · rustfmt · clippy · tracing (`thiserror` / `anyhow` for errors) +- **Godot tooling:** gdformat · gdlint · GUT +- **No `print()` for debugging** — the language's logging library, everywhere +- **Static typing required** on every parameter and return value +- **Line length:** 120 characters (Python), 100 soft limit (GDScript) +- **Tests:** the framework named in the design document — pytest (Python, never `unittest`), built-in `#[test]` (Rust), GUT (Godot) +- **Tasks:** single-line `TODO` / `FIXME` / `BUG` / `HACK` / `NOTE` comments in code — no checkboxes in documentation +- **Versioning:** semantic, always ask before bumping, `CHANGELOG.md` updated first +- **Secrets:** `.env`, never committed; libraries take configuration from the caller diff --git a/Rust/.gitignore b/Rust/.gitignore new file mode 100644 index 0000000..4bce8ce --- /dev/null +++ b/Rust/.gitignore @@ -0,0 +1,9 @@ +# Rust — append on top of the shared core (Project template/.gitignore). + +# --- Build output --- +target/ + +# --- Lock file --- +# Applications commit Cargo.lock — leave the line below commented out. +# Libraries do not commit it — uncomment it there. +# Cargo.lock diff --git a/Rust/DESIGN_DOCUMENT.md b/Rust/DESIGN_DOCUMENT.md index 6ede605..d2f1176 100644 --- a/Rust/DESIGN_DOCUMENT.md +++ b/Rust/DESIGN_DOCUMENT.md @@ -14,11 +14,17 @@ - **PROJECT.md** — Project goals and current state - **CHANGELOG.md** — Version history +### Documentation Organization + +All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. + +The root directory contains only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT.md`, `PROJECT.md`, `CHANGELOG.md`. + --- ## 1. Code Style -- **Rust edition:** 2021 +- **Rust edition:** 2024 (requires Rust 1.85 or newer) - Format with **rustfmt** — run `cargo fmt` before every commit - Lint with **clippy** — run `cargo clippy -- -D warnings` before every commit - **snake_case** functions/variables/modules, **PascalCase** types/traits, **SCREAMING_SNAKE_CASE** constants @@ -55,6 +61,7 @@ project/ │ └── / │ └── mod.rs ├── tests/ # Integration tests +├── docs/ # Detailed documentation ├── Cargo.toml └── Cargo.lock # Commit for applications, not for libraries ``` @@ -196,10 +203,20 @@ cross build --release --target x86_64-pc-windows-gnu ## 11. Documentation and Task Management - Keep `PROJECT.md` and `CHANGELOG.md` up to date when making changes +- Document architectural changes in this file or in `docs/` ### Task notation +Tasks are written as single-line comments directly in code, using the **Todo Tree** tags defined in `AGENTS.md` (`TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`). `PROJECT.md` carries only cross-cutting tasks that have no single place in the code. + ```rust -// TODO: one-liner description of a task to be done -// FIXME: one-liner description of a known bug to be fixed +// TODO: extract this into a separate module +// FIXME: panics on an empty slice +// BUG: off-by-one when the buffer is exactly full +// HACK: temporary workaround until the crate adds paging +// NOTE: order matters here, the parser is stateful ``` + +No other task format is used — **no checkboxes, no numbered lists in documentation**. + +If a tag already exists at a specific location in code, do not repeat it in `PROJECT.md`. diff --git a/Rust/DESIGN_DOCUMENT_LIB.md b/Rust/DESIGN_DOCUMENT_LIB.md index e1ca28f..868b513 100644 --- a/Rust/DESIGN_DOCUMENT_LIB.md +++ b/Rust/DESIGN_DOCUMENT_LIB.md @@ -14,11 +14,20 @@ - **PROJECT.md** — Project goals and current state - **CHANGELOG.md** — Version history +### Documentation Organization + +All detailed documentation of features and systems belongs in the `docs/` folder, not in the project root. + +The API reference is generated by `cargo doc` from doc comments — `docs/` holds the prose documentation that does not fit in doc comments. + +The root directory contains only the core documents: `README.md`, `AGENTS.md`, `DESIGN_DOCUMENT_LIB.md`, `PROJECT.md`, `CHANGELOG.md`. + --- ## 1. Code Style -- **Rust edition:** 2021 +- **Rust edition:** 2024 (requires Rust 1.85 or newer) +- Declare the minimum toolchain in `Cargo.toml` (`rust-version = "1.85"`) so consumers get a clear error instead of a compile failure - Format with **rustfmt** — run `cargo fmt` before every commit - Lint with **clippy** — run `cargo clippy -- -D warnings` before every commit - **snake_case** functions/variables/modules, **PascalCase** types/traits, **SCREAMING_SNAKE_CASE** constants @@ -43,7 +52,7 @@ Never edit `Cargo.toml` dependency versions by hand — use `cargo add`. --- -## 2. Project Structure +## 3. Project Structure ``` project/ @@ -54,6 +63,7 @@ project/ │ └── mod.rs ├── tests/ # Integration tests (test the public API only) ├── examples/ # Usage examples +├── docs/ # Detailed documentation ├── Cargo.toml └── Cargo.lock # Do NOT commit — add to .gitignore ``` @@ -62,7 +72,7 @@ No `main.rs` — libraries have no entry point. --- -## 3. Public API +## 4. Public API - Everything intended for external use must be `pub` and re-exported from `lib.rs` - Use `pub(crate)` for internal items that cross module boundaries @@ -79,7 +89,7 @@ pub use types::{Config, Response}; --- -## 4. Error Handling +## 5. Error Handling - Define all public error types in `src/error.rs` using **thiserror** - Export all errors from `lib.rs` @@ -101,7 +111,7 @@ pub enum MyError { --- -## 5. Logging +## 6. Logging This is a library. Libraries must **never configure logging sinks** — that is the responsibility of the consuming application. @@ -134,13 +144,13 @@ Never call `tracing_subscriber::fmt().init()` or any sink setup inside library c --- -## 6. Environment and Secrets +## 7. Environment and Secrets Libraries do not read environment variables or `.env` files. Configuration is passed by the caller via arguments or constructor parameters. --- -## 7. Testing +## 8. Testing - Use Rust's built-in test framework — `#[test]` and `#[cfg(test)]` - Unit tests live in the same file as the code, in a `mod tests` block @@ -163,7 +173,7 @@ mod tests { --- -## 8. Documentation +## 9. Documentation All public items must have doc comments. Use `cargo doc --open` to verify locally. @@ -187,7 +197,7 @@ pub fn parse(s: &str) -> Result { ... } --- -## 9. Tooling +## 10. Tooling | Tool | Purpose | |------|---------| @@ -205,7 +215,7 @@ cargo test --- -## 10. Distribution +## 11. Distribution Build and publish with Cargo: @@ -218,7 +228,7 @@ cargo publish # Publish to crates.io (requires login) --- -## 11. Versioning +## 12. Versioning - Follow **semantic versioning**: `MAJOR.MINOR.PATCH` - Version is defined in `Cargo.toml` under `[package]` @@ -228,14 +238,24 @@ cargo publish # Publish to crates.io (requires login) --- -## 12. Documentation and Task Management +## 13. Documentation and Task Management - Keep `PROJECT.md` and `CHANGELOG.md` up to date when making changes +- Document architectural changes in this file or in `docs/` - `README.md` must contain installation instructions and usage examples for the public API ### Task notation +Tasks are written as single-line comments directly in code, using the **Todo Tree** tags defined in `AGENTS.md` (`TODO`, `FIXME`, `BUG`, `HACK`, `NOTE`). `PROJECT.md` carries only cross-cutting tasks that have no single place in the code. + ```rust -// TODO: one-liner description of a task to be done -// FIXME: one-liner description of a known bug to be fixed +// TODO: extract this into a separate module +// FIXME: panics on an empty slice +// BUG: off-by-one when the buffer is exactly full +// HACK: temporary workaround until the crate adds paging +// NOTE: order matters here, the parser is stateful ``` + +No other task format is used — **no checkboxes, no numbered lists in documentation**. + +If a tag already exists at a specific location in code, do not repeat it in `PROJECT.md`. diff --git a/Zscaler/NODE_EXTRA_CA_CERTS.md b/Zscaler/NODE_EXTRA_CA_CERTS.md deleted file mode 100644 index 80f08d0..0000000 --- a/Zscaler/NODE_EXTRA_CA_CERTS.md +++ /dev/null @@ -1,7 +0,0 @@ -## NODE_EXTRA_CA_CERTS - -1. `Win + R` → `sysdm.cpl` → **Advanced → Environment Variables** -2. Pod **User variables** → **New** - - Name: `NODE_EXTRA_CA_CERTS` - - Value: `C:\cesta\k\zscaler.cer` -3. OK → restartovat VSCode diff --git a/Zscaler/ZSCALER_CERTIFICATE.md b/Zscaler/ZSCALER_CERTIFICATE.md new file mode 100644 index 0000000..3df7768 --- /dev/null +++ b/Zscaler/ZSCALER_CERTIFICATE.md @@ -0,0 +1,72 @@ +# Zscaler Root Certificate + +On the corporate network Zscaler terminates TLS and re-signs traffic with its own root +certificate. Every tool that ships its own certificate store — Node, Python, git, cargo — +rejects those connections until the Zscaler root is trusted explicitly. + +Typical symptoms: `unable to get local issuer certificate`, `SELF_SIGNED_CERT_IN_CHAIN`, +`SSL: CERTIFICATE_VERIFY_FAILED`, `server certificate verification failed`. + +## The certificate + +The root certificate ships in this folder as `ZscalerRootCertificate-2048-SHA256.crt`. +It is PEM-encoded — the `.crt` / `.cer` / `.pem` extension makes no difference, the tools +below only care about the content. + +| Field | Value | +|-------|-------| +| Subject | `CN=Zscaler Root CA, O=Zscaler Inc.` | +| Valid until | 2042-05-06 | +| SHA-256 | `04:F6:1F:1D:13:AA:E1:D1:65:73:DC:2C:37:F7:96:FD:F4:AC:97:71:3A:69:59:EB:B1:1D:24:73:95:8B:1A:53` | + +Copy it to a permanent location outside any repository — the rest of this document assumes +`C:\certs\ZscalerRootCertificate-2048-SHA256.crt`. + +Verify the copy before trusting it: + +```bash +openssl x509 -in C:/certs/ZscalerRootCertificate-2048-SHA256.crt -noout -fingerprint -sha256 +``` + +## Environment variables + +Set these as **user** variables on Windows: + +1. `Win + R` → `sysdm.cpl` → **Advanced → Environment Variables** +2. Under **User variables** → **New** +3. Enter the name and value from the table +4. OK → restart VS Code and every open terminal, otherwise the old value stays in effect + +| Variable | Used by | Value | +|----------|---------|-------| +| `NODE_EXTRA_CA_CERTS` | Node.js, npm, Electron, VS Code extensions | `C:\certs\ZscalerRootCertificate-2048-SHA256.crt` | +| `REQUESTS_CA_BUNDLE` | Python `requests`, Poetry, pip | same path | +| `SSL_CERT_FILE` | OpenSSL, Python `ssl` / `httpx` / `aiohttp` | same path | +| `CURL_CA_BUNDLE` | curl | same path | +| `CARGO_HTTP_CAINFO` | cargo, crates.io | same path | + +`SSL_CERT_FILE` covers most Python clients, but `requests` prefers `REQUESTS_CA_BUNDLE` — +set both. + +## Git + +Git reads none of those variables. Configure it directly, with **forward slashes** — git +treats a backslash in a config value as an escape character: + +```bash +git config --global http.sslCAInfo "C:/certs/ZscalerRootCertificate-2048-SHA256.crt" +``` + +Never use `http.sslVerify=false` as a workaround — it disables verification for every +remote, not just the ones behind Zscaler. + +## Verifying the setup + +```bash +curl -sSI https://pypi.org | head -1 +git ls-remote https://github.com/git/git HEAD +node -e "require('https').get('https://registry.npmjs.org', r => console.log(r.statusCode))" +poetry run python -c "import requests; print(requests.get('https://pypi.org', timeout=10).status_code)" +``` + +All four must succeed without a certificate error.