ci: block repository media assets (#256)
* ci: block repository media assets * test: stabilize cascade scanner loop test
This commit is contained in:
35
.gitignore
vendored
35
.gitignore
vendored
@ -165,5 +165,40 @@ outputs/
|
|||||||
logs/
|
logs/
|
||||||
nohup.out
|
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: exclude lock files to keep the repo lean
|
||||||
use-cases/**/package-lock.json
|
use-cases/**/package-lock.json
|
||||||
|
|||||||
@ -28,6 +28,14 @@ repos:
|
|||||||
- id: detect-private-key
|
- id: detect-private-key
|
||||||
- id: check-merge-conflict
|
- 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
|
- repo: https://github.com/jorisroovers/gitlint
|
||||||
rev: v0.19.1
|
rev: v0.19.1
|
||||||
hooks:
|
hooks:
|
||||||
|
|||||||
@ -42,9 +42,11 @@ Use the [feature request template](https://github.com/EverMind-AI/EverOS/issues/
|
|||||||
|
|
||||||
1. Create a branch from `main`.
|
1. Create a branch from `main`.
|
||||||
2. Keep the change scoped to one purpose.
|
2. Keep the change scoped to one purpose.
|
||||||
3. Run `make ci` locally before requesting review.
|
3. Do not commit images, videos, or asset/media directories. Use external
|
||||||
4. Use a Conventional Commit title, such as `fix(search): guard empty profile`.
|
hosting, release artifacts, or approved storage and link from docs.
|
||||||
5. Open a pull request to `main` and fill out the PR template.
|
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
|
By submitting a pull request, you agree that your contribution is licensed under
|
||||||
the project's [Apache-2.0](LICENSE) license.
|
the project's [Apache-2.0](LICENSE) license.
|
||||||
@ -90,7 +92,7 @@ git clone https://github.com/EverMind-AI/EverOS.git
|
|||||||
cd EverOS
|
cd EverOS
|
||||||
make install # deps + pre-commit hooks (one-stop dev setup)
|
make install # deps + pre-commit hooks (one-stop dev setup)
|
||||||
everos init # write ./.env, then fill in the API key slots
|
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
|
### Code style
|
||||||
@ -110,7 +112,7 @@ Highlights:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make format # ruff fix + format
|
make format # ruff fix + format
|
||||||
make lint # ruff check + import-linter
|
make lint # ruff check + import-linter + hard repo hygiene gates
|
||||||
```
|
```
|
||||||
|
|
||||||
### Branch strategy
|
### Branch strategy
|
||||||
|
|||||||
11
Makefile
11
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:
|
help:
|
||||||
@echo "Targets:"
|
@echo "Targets:"
|
||||||
@echo " install Install deps + pre-commit hooks (full dev setup)"
|
@echo " install Install deps + pre-commit hooks (full dev setup)"
|
||||||
@echo " install-deps Install deps only (uv sync --frozen, used by CI)"
|
@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 " 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-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-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 " 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"
|
@echo " openapi Regenerate docs/openapi.json from the FastAPI app"
|
||||||
@ -34,6 +35,7 @@ lint:
|
|||||||
uv run ruff check src tests
|
uv run ruff check src tests
|
||||||
uv run ruff format --check src tests
|
uv run ruff format --check src tests
|
||||||
uv run lint-imports
|
uv run lint-imports
|
||||||
|
uv run python scripts/check_repo_assets.py
|
||||||
uv run python scripts/check_datetime_discipline.py
|
uv run python scripts/check_datetime_discipline.py
|
||||||
uv run python scripts/dump_openapi.py --check
|
uv run python scripts/dump_openapi.py --check
|
||||||
|
|
||||||
@ -44,6 +46,11 @@ docs-check:
|
|||||||
check-commits:
|
check-commits:
|
||||||
python3 scripts/check_commit_messages.py $(RANGE)
|
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
|
# Advisory CJK scan (see .claude/rules/language-policy.md). Deliberately NOT
|
||||||
# wired into `lint` / `ci`: the policy is enforced by review and the rules
|
# 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.
|
# doc, not a hard gate. Run on demand when touching potentially-CJK files.
|
||||||
|
|||||||
@ -69,11 +69,13 @@ Reasons this is documented separately:
|
|||||||
│ │ ├ check-yaml / check-toml │ │
|
│ │ ├ check-yaml / check-toml │ │
|
||||||
│ │ ├ check-added-large-files (≥1MB warn) │ │
|
│ │ ├ check-added-large-files (≥1MB warn) │ │
|
||||||
│ │ ├ detect-private-key │ │
|
│ │ ├ detect-private-key │ │
|
||||||
|
│ │ ├ no committed images/videos/assets │ │
|
||||||
│ │ └ gitlint (commit-msg stage) │ │
|
│ │ └ gitlint (commit-msg stage) │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ ruff lint + format │ │
|
│ │ ruff lint + format │ │
|
||||||
│ │ (replaces black / isort / flake8) │ │
|
│ │ (replaces black / isort / flake8) │ │
|
||||||
│ │ import-linter DDD layer-direction enforcement │ │
|
│ │ import-linter DDD layer-direction enforcement │ │
|
||||||
|
│ │ repo asset gate blocks images/videos/assets in git │ │
|
||||||
│ │ pytest unit / integration │ │
|
│ │ pytest unit / integration │ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ └────────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────────┘ │
|
||||||
@ -94,6 +96,7 @@ Reasons this is documented separately:
|
|||||||
│ ┌─ CI/CD (GitHub Actions) ───────────────────────────────────┐ │
|
│ ┌─ CI/CD (GitHub Actions) ───────────────────────────────────┐ │
|
||||||
│ │ │ │
|
│ │ │ │
|
||||||
│ │ CI: .github/workflows/ci.yml lint / test / integ │ │
|
│ │ CI: .github/workflows/ci.yml lint / test / integ │ │
|
||||||
|
│ │ / package build │ │
|
||||||
│ │ Docs: .github/workflows/docs.yml Markdown + YAML check │ │
|
│ │ Docs: .github/workflows/docs.yml Markdown + YAML check │ │
|
||||||
│ │ Gates invoke Makefile targets; the Makefile is the │ │
|
│ │ Gates invoke Makefile targets; the Makefile is the │ │
|
||||||
│ │ single source of truth for commands. │ │
|
│ │ single source of truth for commands. │ │
|
||||||
@ -204,19 +207,21 @@ Stage 2: pre-commit (triggered by `git commit`)
|
|||||||
├ check-yaml, check-toml
|
├ check-yaml, check-toml
|
||||||
├ check-added-large-files (≥1MB)
|
├ check-added-large-files (≥1MB)
|
||||||
├ detect-private-key
|
├ detect-private-key
|
||||||
|
├ no-repo-assets (rejects images/videos/assets in git)
|
||||||
└ gitlint (commit-msg stage; rejects malformed messages)
|
└ gitlint (commit-msg stage; rejects malformed messages)
|
||||||
|
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
Stage 3: local `make ci` (manual, before push)
|
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 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)
|
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 help list all targets
|
||||||
make install uv sync --frozen
|
make install uv sync --frozen
|
||||||
make format ruff fix + format
|
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 test pytest tests/unit
|
||||||
make integration pytest tests/integration
|
make integration pytest tests/integration
|
||||||
make package build sdist/wheel + smoke-test wheel import
|
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 |
|
| Lint | `make lint` (ruff check + ruff format --check) | any error |
|
||||||
| Layer direction | `make lint` (lint-imports inside) | layer violation |
|
| 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 |
|
| Datetime discipline | `make lint` (check_datetime_discipline.py) | bypasses helper module |
|
||||||
| OpenAPI drift | `make lint` (dump_openapi.py --check) | schema ≠ committed openapi.json |
|
| OpenAPI drift | `make lint` (dump_openapi.py --check) | schema ≠ committed openapi.json |
|
||||||
| Unit | `make test` (pytest tests/unit) | any failure |
|
| Unit | `make test` (pytest tests/unit) | any failure |
|
||||||
|
|||||||
122
scripts/check_repo_assets.py
Normal file
122
scripts/check_repo_assets.py
Normal file
@ -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())
|
||||||
@ -13,6 +13,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from everos.core.persistence import MemoryRoot
|
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
|
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)
|
scanner = CascadeScanner(mr, scan_interval_seconds=0.05)
|
||||||
|
|
||||||
call_count = {"n": 0}
|
call_count = {"n": 0}
|
||||||
|
second_scan = asyncio.Event()
|
||||||
|
logged_errors: list[str] = []
|
||||||
|
|
||||||
async def fake_scan() -> list: # type: ignore[type-arg]
|
async def fake_scan() -> list: # type: ignore[type-arg]
|
||||||
call_count["n"] += 1
|
call_count["n"] += 1
|
||||||
if call_count["n"] == 1:
|
if call_count["n"] == 1:
|
||||||
raise RuntimeError("simulated scanner failure")
|
raise RuntimeError("simulated scanner failure")
|
||||||
|
second_scan.set()
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def fake_exception(_event: str, *, error: str) -> None:
|
||||||
|
logged_errors.append(error)
|
||||||
|
|
||||||
monkeypatch.setattr(scanner, "scan_once", fake_scan)
|
monkeypatch.setattr(scanner, "scan_once", fake_scan)
|
||||||
|
monkeypatch.setattr(scanner_module.logger, "exception", fake_exception)
|
||||||
await scanner.start()
|
await scanner.start()
|
||||||
# Let the loop iterate at least twice (interval is 50ms).
|
try:
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.wait_for(second_scan.wait(), timeout=1.0)
|
||||||
await scanner.stop()
|
finally:
|
||||||
|
await scanner.stop()
|
||||||
|
|
||||||
|
assert logged_errors == ["simulated scanner failure"]
|
||||||
assert call_count["n"] >= 2 # second call ran despite first throwing
|
assert call_count["n"] >= 2 # second call ran despite first throwing
|
||||||
|
|||||||
80
tests/unit/test_scripts/test_check_repo_assets.py
Normal file
80
tests/unit/test_scripts/test_check_repo_assets.py
Normal file
@ -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"}
|
||||||
@ -889,7 +889,7 @@ function getGroupsForKey(keyId) {
|
|||||||
// Proxy forwards to upstream API with Authorization header
|
// Proxy forwards to upstream API with Authorization header
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dashboard (`assets/dashboard.html`)
|
### Dashboard (`dashboard/dashboard.html`)
|
||||||
|
|
||||||
**Data Loading Flow:**
|
**Data Loading Flow:**
|
||||||
|
|
||||||
@ -1059,7 +1059,7 @@ evermem-plugin/
|
|||||||
│ ├── config.js # Configuration utilities
|
│ ├── config.js # Configuration utilities
|
||||||
│ ├── debug.js # Shared debug logging utility
|
│ ├── debug.js # Shared debug logging utility
|
||||||
│ └── groups-store.js # Local groups persistence
|
│ └── groups-store.js # Local groups persistence
|
||||||
├── assets/
|
├── dashboard/
|
||||||
│ └── dashboard.html # Memory Hub dashboard
|
│ └── dashboard.html # Memory Hub dashboard
|
||||||
├── server/
|
├── server/
|
||||||
│ └── proxy.js # Local proxy server for dashboard
|
│ └── proxy.js # Local proxy server for dashboard
|
||||||
|
|||||||
Reference in New Issue
Block a user