112 lines
10 KiB
Markdown
112 lines
10 KiB
Markdown
<!-- retoor <retoor@molodetz.nl> -->
|
|
# 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 (`/terms/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).
|