diff --git a/.gitignore b/.gitignore index 534399e..af95e07 100755 --- a/.gitignore +++ b/.gitignore @@ -165,5 +165,40 @@ outputs/ logs/ nohup.out +# Repository media policy: do not commit images, videos, or asset/media folders. +# Use external hosting, release artifacts, or approved storage and link from docs. +asset/ +assets/ +image/ +images/ +img/ +media/ +video/ +videos/ +*.avif +*.bmp +*.gif +*.heic +*.heif +*.icns +*.ico +*.jpeg +*.jpg +*.png +*.svg +*.tif +*.tiff +*.webp +*.avi +*.flv +*.m4v +*.mkv +*.mov +*.mp4 +*.mpeg +*.mpg +*.webm +*.wmv + # Use-cases: exclude lock files to keep the repo lean use-cases/**/package-lock.json diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acb37d9..9867e56 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,14 @@ repos: - id: detect-private-key - id: check-merge-conflict + - repo: local + hooks: + - id: no-repo-assets + name: block committed images, videos, and asset directories + entry: python3 scripts/check_repo_assets.py + language: system + pass_filenames: false + - repo: https://github.com/jorisroovers/gitlint rev: v0.19.1 hooks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b307de6..5feab4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,9 +42,11 @@ Use the [feature request template](https://github.com/EverMind-AI/EverOS/issues/ 1. Create a branch from `main`. 2. Keep the change scoped to one purpose. -3. Run `make ci` locally before requesting review. -4. Use a Conventional Commit title, such as `fix(search): guard empty profile`. -5. Open a pull request to `main` and fill out the PR template. +3. Do not commit images, videos, or asset/media directories. Use external + hosting, release artifacts, or approved storage and link from docs. +4. Run `make ci` locally before requesting review. +5. Use a Conventional Commit title, such as `fix(search): guard empty profile`. +6. Open a pull request to `main` and fill out the PR template. By submitting a pull request, you agree that your contribution is licensed under the project's [Apache-2.0](LICENSE) license. @@ -90,7 +92,7 @@ git clone https://github.com/EverMind-AI/EverOS.git cd EverOS make install # deps + pre-commit hooks (one-stop dev setup) everos init # write ./.env, then fill in the API key slots -make ci # verify: lint + unit + integration +make ci # verify: lint + unit + integration + package ``` ### Code style @@ -110,7 +112,7 @@ Highlights: ```bash make format # ruff fix + format -make lint # ruff check + import-linter +make lint # ruff check + import-linter + hard repo hygiene gates ``` ### Branch strategy diff --git a/Makefile b/Makefile index df7fd23..d4a2c43 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ -.PHONY: help install install-deps lint docs-check check-commits check-cjk check-datetime openapi check-openapi format test integration package cov ci clean +.PHONY: help install install-deps lint docs-check check-commits check-assets check-cjk check-datetime openapi check-openapi format test integration package cov ci clean help: @echo "Targets:" @echo " install Install deps + pre-commit hooks (full dev setup)" @echo " install-deps Install deps only (uv sync --frozen, used by CI)" - @echo " lint ruff (check + format-check) + import-linter + datetime discipline + openapi drift" + @echo " lint ruff + import-linter + repo asset/media + datetime discipline + openapi drift" @echo " docs-check Validate Markdown links, use-case banners, and issue template YAML" @echo " check-commits Validate Conventional Commit subjects for a git range" + @echo " check-assets Block committed images, videos, and asset/media directories" @echo " check-cjk Scan for CJK outside the language-policy allowlist (advisory)" @echo " check-datetime Scan for code that bypasses component/utils/datetime (HARD gate, run via lint)" @echo " openapi Regenerate docs/openapi.json from the FastAPI app" @@ -34,6 +35,7 @@ lint: uv run ruff check src tests uv run ruff format --check src tests uv run lint-imports + uv run python scripts/check_repo_assets.py uv run python scripts/check_datetime_discipline.py uv run python scripts/dump_openapi.py --check @@ -44,6 +46,11 @@ docs-check: check-commits: python3 scripts/check_commit_messages.py $(RANGE) +# Repository media hygiene gate. Images/videos belong in external hosting, +# release artifacts, or other approved storage, then linked from docs. +check-assets: + uv run python scripts/check_repo_assets.py + # Advisory CJK scan (see .claude/rules/language-policy.md). Deliberately NOT # wired into `lint` / `ci`: the policy is enforced by review and the rules # doc, not a hard gate. Run on demand when touching potentially-CJK files. diff --git a/docs/engineering.md b/docs/engineering.md index 633e955..d56ae16 100644 --- a/docs/engineering.md +++ b/docs/engineering.md @@ -69,11 +69,13 @@ Reasons this is documented separately: │ │ ├ check-yaml / check-toml │ │ │ │ ├ check-added-large-files (≥1MB warn) │ │ │ │ ├ detect-private-key │ │ +│ │ ├ no committed images/videos/assets │ │ │ │ └ gitlint (commit-msg stage) │ │ │ │ │ │ │ │ ruff lint + format │ │ │ │ (replaces black / isort / flake8) │ │ │ │ import-linter DDD layer-direction enforcement │ │ +│ │ repo asset gate blocks images/videos/assets in git │ │ │ │ pytest unit / integration │ │ │ │ │ │ │ └────────────────────────────────────────────────────────────┘ │ @@ -94,6 +96,7 @@ Reasons this is documented separately: │ ┌─ CI/CD (GitHub Actions) ───────────────────────────────────┐ │ │ │ │ │ │ │ CI: .github/workflows/ci.yml lint / test / integ │ │ +│ │ / package build │ │ │ │ Docs: .github/workflows/docs.yml Markdown + YAML check │ │ │ │ Gates invoke Makefile targets; the Makefile is the │ │ │ │ single source of truth for commands. │ │ @@ -204,19 +207,21 @@ Stage 2: pre-commit (triggered by `git commit`) ├ check-yaml, check-toml ├ check-added-large-files (≥1MB) ├ detect-private-key + ├ no-repo-assets (rejects images/videos/assets in git) └ gitlint (commit-msg stage; rejects malformed messages) │ ▼ Stage 3: local `make ci` (manual, before push) - ├ make lint (ruff check + ruff format --check + import-linter) + ├ make lint (ruff + import-linter + repo hygiene gates) ├ make test (pytest tests/unit) - └ make integration (pytest tests/integration) + ├ make integration (pytest tests/integration) + └ make package (sdist/wheel build + import smoke test) │ ▼ Stage 4: CI (GitHub Actions, push + PR triggered) - └ re-runs the same `make lint / test / integration` targets + └ re-runs the same `make lint / test / integration / package` targets │ ▼ @@ -272,7 +277,7 @@ dev = ["ruff", "pytest", "pytest-asyncio", "pytest-cov", make help list all targets make install uv sync --frozen make format ruff fix + format -make lint ruff + import-linter + datetime discipline + openapi drift +make lint ruff + import-linter + repo asset/media + datetime discipline + openapi drift make test pytest tests/unit make integration pytest tests/integration make package build sdist/wheel + smoke-test wheel import @@ -338,6 +343,7 @@ Every key has a sensible default except the `API_KEY` fields, which you fill in. |---|---|---| | Lint | `make lint` (ruff check + ruff format --check) | any error | | Layer direction | `make lint` (lint-imports inside) | layer violation | +| Repository media | `make lint` (check_repo_assets.py) | images/videos/assets committed | | Datetime discipline | `make lint` (check_datetime_discipline.py) | bypasses helper module | | OpenAPI drift | `make lint` (dump_openapi.py --check) | schema ≠ committed openapi.json | | Unit | `make test` (pytest tests/unit) | any failure | diff --git a/scripts/check_repo_assets.py b/scripts/check_repo_assets.py new file mode 100644 index 0000000..c271080 --- /dev/null +++ b/scripts/check_repo_assets.py @@ -0,0 +1,122 @@ +"""Block committed image/video files and asset-style directories.""" + +from __future__ import annotations + +import subprocess +import sys +from dataclasses import dataclass +from pathlib import PurePosixPath +from typing import Iterable + +BLOCKED_DIR_NAMES = frozenset( + { + "asset", + "assets", + "image", + "images", + "img", + "media", + "video", + "videos", + } +) +IMAGE_EXTENSIONS = frozenset( + { + ".avif", + ".bmp", + ".gif", + ".heic", + ".heif", + ".icns", + ".ico", + ".jpeg", + ".jpg", + ".png", + ".svg", + ".tif", + ".tiff", + ".webp", + } +) +VIDEO_EXTENSIONS = frozenset( + { + ".avi", + ".flv", + ".m4v", + ".mkv", + ".mov", + ".mp4", + ".mpeg", + ".mpg", + ".webm", + ".wmv", + } +) + + +@dataclass(frozen=True) +class Violation: + path: str + reason: str + + +def _normalise_path(path: str) -> PurePosixPath: + return PurePosixPath(path.replace("\\", "/")) + + +def _violation_reason(path: str) -> str | None: + posix_path = _normalise_path(path) + lower_parts = tuple(part.lower() for part in posix_path.parts) + if any(part in BLOCKED_DIR_NAMES for part in lower_parts): + return "asset/media directory" + + suffix = posix_path.suffix.lower() + if suffix in IMAGE_EXTENSIONS: + return "image file" + if suffix in VIDEO_EXTENSIONS: + return "video file" + return None + + +def find_violations(paths: Iterable[str]) -> list[Violation]: + violations: list[Violation] = [] + for path in paths: + reason = _violation_reason(path) + if reason is not None: + violations.append(Violation(path=path, reason=reason)) + return violations + + +def _tracked_paths() -> list[str]: + result = subprocess.run( + ["git", "ls-files", "-z"], + check=True, + stdout=subprocess.PIPE, + text=False, + ) + return [ + raw.decode("utf-8") + for raw in result.stdout.split(b"\0") + if raw + ] + + +def main() -> int: + violations = find_violations(_tracked_paths()) + if not violations: + print("Repository asset/media check passed.") + return 0 + + print( + "Repository asset/media check failed.\n" + "Do not commit images, videos, or asset/media directories. " + "Host visual media externally, in release artifacts, or another " + "approved storage location, then link to it from docs.\n" + ) + for violation in violations: + print(f"- {violation.path}: {violation.reason}") + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/unit/test_memory/test_cascade/test_scanner_unit.py b/tests/unit/test_memory/test_cascade/test_scanner_unit.py index 764923e..78fed0d 100644 --- a/tests/unit/test_memory/test_cascade/test_scanner_unit.py +++ b/tests/unit/test_memory/test_cascade/test_scanner_unit.py @@ -13,6 +13,7 @@ from pathlib import Path import pytest from everos.core.persistence import MemoryRoot +from everos.memory.cascade import scanner as scanner_module from everos.memory.cascade.scanner import CascadeScanner, _collect_scan_inputs @@ -112,16 +113,26 @@ async def test_run_loop_swallows_scan_exception( scanner = CascadeScanner(mr, scan_interval_seconds=0.05) call_count = {"n": 0} + second_scan = asyncio.Event() + logged_errors: list[str] = [] async def fake_scan() -> list: # type: ignore[type-arg] call_count["n"] += 1 if call_count["n"] == 1: raise RuntimeError("simulated scanner failure") + second_scan.set() return [] + def fake_exception(_event: str, *, error: str) -> None: + logged_errors.append(error) + monkeypatch.setattr(scanner, "scan_once", fake_scan) + monkeypatch.setattr(scanner_module.logger, "exception", fake_exception) await scanner.start() - # Let the loop iterate at least twice (interval is 50ms). - await asyncio.sleep(0.2) - await scanner.stop() + try: + await asyncio.wait_for(second_scan.wait(), timeout=1.0) + finally: + await scanner.stop() + + assert logged_errors == ["simulated scanner failure"] assert call_count["n"] >= 2 # second call ran despite first throwing diff --git a/tests/unit/test_scripts/test_check_repo_assets.py b/tests/unit/test_scripts/test_check_repo_assets.py new file mode 100644 index 0000000..d82c866 --- /dev/null +++ b/tests/unit/test_scripts/test_check_repo_assets.py @@ -0,0 +1,80 @@ +"""Self-tests for ``scripts/check_repo_assets.py``.""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_CHECKER_PATH = _REPO_ROOT / "scripts" / "check_repo_assets.py" + + +def _load_checker(): + assert _CHECKER_PATH.exists(), "repo asset checker should exist" + spec = importlib.util.spec_from_file_location("_repo_asset_checker", _CHECKER_PATH) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + return mod + + +def test_clean_source_and_docs_paths_are_allowed() -> None: + checker = _load_checker() + + violations = checker.find_violations( + [ + "README.md", + "docs/engineering.md", + "src/everos/__init__.py", + "use-cases/claude-code-plugin/dashboard/dashboard.html", + ] + ) + + assert violations == [] + + +def test_image_extensions_are_blocked() -> None: + checker = _load_checker() + + violations = checker.find_violations(["docs/banner.png", "icons/logo.svg"]) + + assert [violation.path for violation in violations] == [ + "docs/banner.png", + "icons/logo.svg", + ] + assert {violation.reason for violation in violations} == {"image file"} + + +def test_video_extensions_are_blocked() -> None: + checker = _load_checker() + + violations = checker.find_violations(["demo/launch.mp4", "docs/clip.webm"]) + + assert [violation.path for violation in violations] == [ + "demo/launch.mp4", + "docs/clip.webm", + ] + assert {violation.reason for violation in violations} == {"video file"} + + +def test_asset_and_media_directories_are_blocked() -> None: + checker = _load_checker() + + violations = checker.find_violations( + [ + "assets/banner.txt", + "docs/images/diagram.txt", + "use-cases/example/media/story.md", + "use-cases/example/videos/walkthrough.md", + ] + ) + + assert [violation.path for violation in violations] == [ + "assets/banner.txt", + "docs/images/diagram.txt", + "use-cases/example/media/story.md", + "use-cases/example/videos/walkthrough.md", + ] + assert {violation.reason for violation in violations} == {"asset/media directory"} diff --git a/use-cases/claude-code-plugin/README.md b/use-cases/claude-code-plugin/README.md index 529eac2..dc946bf 100644 --- a/use-cases/claude-code-plugin/README.md +++ b/use-cases/claude-code-plugin/README.md @@ -889,7 +889,7 @@ function getGroupsForKey(keyId) { // Proxy forwards to upstream API with Authorization header ``` -### Dashboard (`assets/dashboard.html`) +### Dashboard (`dashboard/dashboard.html`) **Data Loading Flow:** @@ -1059,7 +1059,7 @@ evermem-plugin/ │ ├── config.js # Configuration utilities │ ├── debug.js # Shared debug logging utility │ └── groups-store.js # Local groups persistence -├── assets/ +├── dashboard/ │ └── dashboard.html # Memory Hub dashboard ├── server/ │ └── proxy.js # Local proxy server for dashboard diff --git a/use-cases/claude-code-plugin/assets/dashboard-preview.html b/use-cases/claude-code-plugin/dashboard/dashboard-preview.html similarity index 100% rename from use-cases/claude-code-plugin/assets/dashboard-preview.html rename to use-cases/claude-code-plugin/dashboard/dashboard-preview.html diff --git a/use-cases/claude-code-plugin/assets/dashboard.html b/use-cases/claude-code-plugin/dashboard/dashboard.html similarity index 100% rename from use-cases/claude-code-plugin/assets/dashboard.html rename to use-cases/claude-code-plugin/dashboard/dashboard.html