From 3fb47bf401d1e4a4f9c144b608e0661f5addf920 Mon Sep 17 00:00:00 2001 From: retoor Date: Mon, 5 Oct 2026 09:43:49 +0200 Subject: [PATCH] Compliance checklist, coverage parallel, locust teardown, docs coverage test --- .coveragerc | 2 +- COMPLIANCE.md | 111 ++++++++++++++++++++++++++++++++ Makefile | 1 + pyproject.toml | 2 +- tests/unit/docs_api/registry.py | 9 +++ 5 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 COMPLIANCE.md diff --git a/.coveragerc b/.coveragerc index 8355858..8ac6981 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ # retoor [run] source = molodetz -parallel = False +parallel = True sigterm = True omit = tests/* diff --git a/COMPLIANCE.md b/COMPLIANCE.md new file mode 100644 index 0000000..c651280 --- /dev/null +++ b/COMPLIANCE.md @@ -0,0 +1,111 @@ + +# Compliance: Molodetz against `/workspace/dpp/dptemplate.md` + +Status date: 2026-10-05. Template read in full and never edited. Selection: `SELECTION.md`. +Proof runs: `out/pytest-full.log` (full suite), `out/check-ui.log` (six gates), `out/dpp-gates.txt` (DPP tooling `dpp.gates.run_gates`, all PASS), `out/locust-smoke.log` (51 requests, 0 failures), `make coverage` (76% including the server subprocess). + +Legend: MET, PARTIAL (works, but narrower than the template text), NOT MET (missing, with the reason). + +## Section 1.1 Backend choices + +| Choice | Status | Where / how | +|---|---|---| +| Python >= 3.12 | MET | `pyproject.toml` `requires-python = ">=3.12"`; venv runs 3.13 | +| FastAPI + Jinja2, server-rendered first | MET | `molodetz/main.py`; one `Jinja2Templates` in `molodetz/templating.py` (gate 1) | +| uvicorn (standard extras), reload in dev, workers in prod | MET | `uvicorn[standard]`; `make dev` (`--reload --reload-dir molodetz --backlog 4096`), `make prod` (`--workers`, `--backlog 8192`, proxy headers, pinned static version) | +| SQLite via `dataset`, sync by design, no ORM/migrations | MET | `molodetz/database/core.py`: `dataset.connect` with NullPool, exact `connect_args` and PRAGMA list; schema sync in `database/init_db.py`; no threadpool around DB calls (backup DB writes moved out of `to_thread`) | +| passlib PBKDF2-SHA256, no JWT | MET | `molodetz/utils/auth.py` `hash_password`/`verify_password`; sessions in `sessions` table | +| httpx (HTTP/2) only through the stealth factory; curl_cffi impersonation | MET | `molodetz/stealth.py` (only file that builds httpx clients) + `molodetz/curl_transport.py`; `httpx[http2]`; SSRF guard `molodetz/net_guard.py`; used by `link_check.py` (admin repo-link check) | +| mistune (GFM) server-side, emoji as shortcode source | MET | `molodetz/rendering.py`; `molodetz/emoji_builder.py` writes `static/js/generated/EmojiMap.js` from the `emoji` library | +| Pillow, imagehash | MET | `molodetz/gallery.py`: webp thumbnails as blobs in `data/uploads`, perceptual-hash dedupe | +| pypdf | MET | `molodetz/cli/commands.py` `posts import` reads `.pdf`; unit test `tests/unit/cli/commands.py` | +| sqlglot | MET | `molodetz/database/sql_lint.py` parses every raw SQL literal; `molodetz system sql-lint`; `tests/unit/database/sql_lint.py` | +| defusedxml | MET | `routers/seo.py` sitemap self-check, `link_check.py` XML titles; tests parse sitemap with it | +| beautifulsoup4 + lxml | MET | `rendering.py` (media pass, plain text), `docs_search.py`, `link_check.py` | +| uuid_utils (uuid7) | MET | `database/core.py` `generate_uid()`; blob sharding on the uuid7 tail | +| brotli, zstandard | MET | `services/backup.py` archive codecs (`zstd` default, `brotli` option) | +| aiofiles | MET | `routers/admin/backups.py` streams the backup download | +| python-dotenv, one `load_dotenv()` | MET | only in `molodetz/config.py` | +| locust | MET | `locustfile.py`, `make locust` / `make locust-headless` | +| playwright (direct API) + Chromium | MET | `tests/conftest.py` fixtures, no pytest-playwright plugin | +| pytest, no xdist, no pytest-playwright | MET | `pyproject.toml` addopts `-p no:xdist` | + +## Section 1.2 Frontend choices + +| Choice | Status | Where / how | +|---|---|---| +| Pure ES6 modules, one class per file, OO, relative imports, no NPM/bundler | MET | `molodetz/static/js/**`; `Application.js` imports `components/index.js` first; `window.app` | +| marked (live content only) | MET | `static/vendor/marked.esm.js`, used only by `dp-content` in the editor preview | +| DOMPurify | MET | `static/vendor/purify.es.mjs`; fail-closed in `utils/ContentRenderer.js`; e2e asserts script/javascript:/onclick are stripped | +| highlight.js | MET | `static/vendor/highlight.es.min.js` via `dp-code` | +| Scripts loaded with `type="module"` | MET | `templates/base.html` and page blocks | +| Hand-written token-driven CSS | MET | `static/css/variables.css` tokens + per-area sheets; breakpoints exactly 360/480/768/1024 (gate 2) | +| Optional libs chromadb, weasyprint, cairosvg, faker | MET (not used) | their modules are not selected and the libs are not dependencies | + +## Section 1.3 Bans + +| Ban | Status | Evidence | +|---|---|---| +| No JWT, OAuth/social login, payments, ads, tracking | MET | session cookie + API key only; `swagger_ui_oauth2_redirect_url=None`; no external scripts (CSP in `main.py`); test `tests/unit/main.py` | +| No JS framework, bundler, transpiler | MET | no `package.json`; vendored ESM files only | +| No async DB driver, no threadpool around DB | MET | sync `dataset`; `asyncio.to_thread` only for PBKDF2, archive build, DNS | +| No bare HTTP client | MET | gate 3: client constructions only in `stealth.py` and `curl_transport.py` | +| No hardcoded `/static/` | MET | gate 5 on templates; `static_url()` in `molodetz/assets.py` (also used for the OG image); API test checks rendered HTML | + +## Section 33.1 Structural gates + +| # | Gate | Status | Where | +|---|---|---|---| +| 1 | Template env in one file | MET | `molodetz/gates.py`, `tests/unit/gates.py`, `make check-ui` | +| 2 | Breakpoints equal closed set | MET | same | +| 3 | HTTP client allowlist | MET | same (allowlist: `stealth.py`, `curl_transport.py`) | +| 4 | Client-render marker allowlist | MET | same (only `admin/post_form.html`) | +| 5 | No bare `/static/` | MET | same | +| 6 | No manual `is-loading` outside `LoadingButton.js` | MET | same | +| 7 | No em-dash characters or entities | MET | `gates.gate_em_dashes` + DPP `gate_emdash` | +| 8 | `json_error` status-first (AST) | MET | `gates.gate_json_error_order` + DPP gate | +| 9 | Reportable/unreportable partition | MET | `tests/unit/constants.py` (reportable set is empty because section 18 is not selected) | +| 10 | Documented auth equals real auth for anon/member/admin | MET | `tests/api/docs/api.py` over every endpoint in `molodetz/docs_api`; plus `tests/unit/docs_api/registry.py` (every product route is documented) | +| 11 | Index plans use the index, no temp b-tree | MET | `tests/unit/database/init_db.py` (EXPLAIN QUERY PLAN for the main read paths) | +| 12 | Full suite green, three tiers, one pass | MET | `out/pytest-full.log` | + +## Section 33.2 Architectural gates + +| # | Gate | Status | Note | +|---|---|---|---| +| 1 | Four faces | PARTIAL | HTML, JSON (`respond(..., model=)`), docs registry: yes. Assistant face skipped because section 16 is not selected | +| 2 | Soft-delete reads/inserts | MET | `deleted_at IS NULL` filters; inserts write `deleted_at`/`deleted_by` | +| 3 | Cache versions | MET | `cache_state` version sync for auth/settings; display caches have TTL env pins | +| 4 | Role writes invalidate admin cache | MET | `database/users.set_role` | +| 5 | File mutations guard writability | MET | blob writer and backup paths under `DATA_PATHS` | +| 6 | Outbound through the guarded factory | MET | `stealth.guarded_async_client` | +| 7 | Side effects through funnels | MET | `utils/audit.py`, `utils/notifications.py`, `services/queue.py` | +| 8 | Destructive assistant tools gated | N/A | no assistant | +| 9 | Personal data disclosed | MET | `templates/privacy.html` covers join requests, sessions, visit counts | +| 10 | Events catalogued | MET | `events.md` | +| 11 | Section 31 features | N/A | no value, state-machine or multi-path features (31 not selected) | +| 12 | Production data untouched | PARTIAL | the app only writes its own `data/`; `hooks/prod_guard.py` exists but cannot be wired into this agent runtime | +| 13 | Tree matches `SELECTION.md` | MET | DPP `gate_selection_vs_tree` PASS | + +## Other conventions checked + +- Layout (section 2): routers and `tests/api`, `tests/e2e` mirror URL paths; `tests/unit` mirrors module paths; `cli/main.py`; `routers/__init__.py` does not aggregate; the admin package aggregates on its base router. +- Config (section 3): `DATA_PATHS` registry, `ensure_data_dirs()`, `APP_VERSION` via `tomllib`, `BOOT_ID`/`STATIC_VERSION`, `make prod` exports `MOLODETZ_STATIC_VERSION`. The pre-commit hook bumps the patch version (seen working: 1.0.0 to 1.0.4). +- Secrets: the admin password comes only from `.env` (mode 600, generated randomly, not in git). `.env.example` has no secrets. +- Browser tests use `wait_until="domcontentloaded"` on every `goto` and `wait_for_url`, and check for horizontal overflow on mobile. +- Makefile (27.1): venv stamp, `PYTHONDONTWRITEBYTECODE=1`, dev/prod/test matrix, `check-ui`, `preflight`, `coverage*`, `locust*`, `prune`/`prune-dry-run`, `tree`/`tree-loc`/`zip`/`delete-pyc`/`clean`. + +## Not met or narrowed (honest list) + +1. Docker, nginx sidecar, `instance`, `docker-*` and `deploy` make targets: not shipped. Section 28.2 is not selected (bare metal only), and deploy would push to a remote, which was not requested. +2. Assistant face (8.2 item 3) and action catalogue: skipped (section 16 not selected). +3. Presence is the CORE subset and works by polling `/presence/roster` (20 s) plus server-side touch throttling. There is no WebSocket relay; the rest of section 17 is deferred in `SELECTION.md`. +4. There is no public signup. The only account is the env-bootstrapped admin (`retoor`), so the birth-date/terms signup flow and its shared e2e helper do not exist. Terms acceptance exists (`/voorwaarden/accept`, `TermsGate.js`). +5. Votes, reports, comments, maturity partials and the report dialog are absent (sections 18 and 20.2 not selected). The JobPoller, OptimisticAction, StickyScrollPane and emoji picker front-end modules are not built, because no selected feature needs them. +6. Mutation routes parse bodies with a shared `json_or_form(request, Model)` helper (form or JSON into one Pydantic model) instead of `Annotated[Model, Form()]`. Validation is equivalent; the signature style differs. +7. The repo-link check on join requests runs inline in the admin request, not as a durable job (section 14.4 not selected). +8. The notification type registry has 6 types (join, backup, system and similar), not the about 20 the template lists for bigger products. +9. `.coveragerc` uses `parallel = True`. The template text says "no parallelism", but subprocess coverage of the server (also required there) only works with per-process data files that are then combined. With `parallel = False` the server data was overwritten (47% instead of 76%). +10. `hooks/prod_guard.py` (28.3) is present but not wired into any agent hook runner on this box. +11. Web lock (`web.lock`) path is defined but unused: there is a single web role and services run under `service.lock`. A suspension check exists in auth, but there is no UI to suspend accounts. +12. `fuser` is not installed on the box, so `locust-run` port cleanup is skipped there (the `-` prefix ignores it). diff --git a/Makefile b/Makefile index 42ca3eb..6410d71 100644 --- a/Makefile +++ b/Makefile @@ -100,6 +100,7 @@ locust-run: for i in $$(seq 1 60); do curl -fs http://127.0.0.1:$(LOCUST_PORT)/health >/dev/null && break; sleep 0.5; done -$(PY) -m locust -f locustfile.py --host http://127.0.0.1:$(LOCUST_PORT) $(LOCUST_ARGS) -kill $$(cat $(LOCUST_DATA)/server.pid) + -for i in $$(seq 1 40); do kill -0 $$(cat $(LOCUST_DATA)/server.pid) 2>/dev/null || break; sleep 0.25; done rm -rf $(LOCUST_DATA) prune: $(STAMP) diff --git a/pyproject.toml b/pyproject.toml index cec4504..5dda73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "molodetz" -version = "1.0.4" +version = "1.0.5" description = "Molodetz, a calm community blog roll." readme = "README.md" requires-python = ">=3.12" diff --git a/tests/unit/docs_api/registry.py b/tests/unit/docs_api/registry.py index 9d73dbe..d1e44ea 100644 --- a/tests/unit/docs_api/registry.py +++ b/tests/unit/docs_api/registry.py @@ -18,3 +18,12 @@ def test_documented_endpoints_exist_in_app(): def test_target_types_known(): assert set(TARGET_TYPES) == {"post", "join_request", "user"} + + +def test_every_product_route_is_documented(): + from molodetz.main import app + + documented = {(item["method"], item["path"]) for item in all_endpoints()} + routes = {(method.upper(), path) for path, methods in app.openapi()["paths"].items() for method in methods} + undocumented = sorted(route for route in routes - documented if not route[1].startswith("/docs")) + assert undocumented == []