Skip to content
Back to skills

Shadowling Db

ASecurity

Use whenever you read, query, or mutate shadowling's sqlite database (shadowling.db) — any SELECT/INSERT/UPDATE/DELETE, ad-hoc SQL, dropping/clearing/resetting rows, inspecting data, or checking a migration's result. All DB access goes through a skill entrypoint, a repository method (`models/*`), or `sql.py` — never raw sqlite3. Schema/data-layer changes are NOT made here — they go through an appended migration in appdb.py; this skill only documents that rule and verifies the outcome.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 6, 2026
developmentpythonrustgobashsqltestingdatabase

Works with

  • mcp

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add OleksandrHavuka/shadowling --skill shadowling-db --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Shadowling Db?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Shadowling Db
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/oleksandrhavuka-shadowling-db/badge)](https://www.skillsdirectory.com/skills/oleksandrhavuka-shadowling-db)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: shadowling-db
description: Use whenever you read, query, or mutate shadowling's sqlite database (shadowling.db) — any SELECT/INSERT/UPDATE/DELETE, ad-hoc SQL, dropping/clearing/resetting rows, inspecting data, or checking a migration's result. All DB access goes through a skill entrypoint, a repository method (`models/*`), or `sql.py` — never raw sqlite3. Schema/data-layer changes are NOT made here — they go through an appended migration in appdb.py; this skill only documents that rule and verifies the outcome.
---

# shadowling DB conventions

One sqlite database (`~/.shadowling/shadowling.db`), stdlib `sqlite3` only.
`plugins/shadowling/appdb.py` owns the connection (`connect()`: WAL,
`busy_timeout=5000`, `row_factory=Row`, migration runner, view recreation).
Never open the DB another way; ad-hoc reads and dev surgery go through
`sql.py` (the old `capture.py query` verb is folded into `sql.py`).

## Schema changes: MIGRATIONS only

Versioning = `PRAGMA user_version` + the ordered `MIGRATIONS` list in
`appdb.py`. To change schema, APPEND one new migration entry. The runner
backs up the file (`.bak`), applies pending steps each in its own
transaction, and bumps `user_version` atomically.

**Never:**
- edit an already-shipped migration or a `CREATE TABLE` in place — fresh
  installs replay the SAME migration chain old installs took; there is one
  schema history, not two;
- run ad-hoc `ALTER`/"add column if missing" checks at connect time;
- add a migrations table, alembic, aiosqlite, or any ORM (stdlib-only rule).

## Views are derived code, not schema

`*_ranked` views are ensured current on EVERY `connect()` (recreated whenever
the definition in code differs from `sqlite_master`). Changing a
ranking/aggregation = edit the view definition in `appdb.py`; no migration,
no version bump. Products (counters, created/updated, latest example) are
COMPUTED from incident rows — never stored.

## Data doctrine

- Category tables (grammar, friction, …) are **append-only event logs**: one
  INSERT per incident; never UPDATE/DELETE recorded text. Uniqueness lives in
  `GROUP BY` (keys pre-normalized: slugify / casefold).
- Mutable state is the explicit exception: `vocab.remaining/status`,
  `messages.langs/processed_at/kind`, and the tutor's `mastery` table
  (box/due_date — scheduling state). `attempts` is append-only like the
  incident tables; its `answer` is stored VERBATIM (it doubles as the
  mark-drills registry).

## Column naming convention (since migration 3)

- **`created_at`** is the row-creation column on EVERY table — full ISO
  datetime on the event logs (`messages`, `attempts`) and on `vocab`/`mastery`;
  **date-only** (`YYYY-MM-DD`, via `core.today()`) on the six daily incident
  tables, whose frequency product wants days, not seconds.
- **`updated_at`** is STORED only where a row mutates: `vocab` and `mastery`.
  The incident tables expose it as the view's `MAX(created_at)` (a product, not
  a stored column — see the doctrine above); `messages` uses `processed_at` for
  its one lifecycle event; append-only `attempts` has no `updated_at`.
- **`learner_wrote`** is the canonical "what the learner produced" column on
  `rephrasing`/`idioms`/`decode`/`friction` (the views still alias it to a
  category-readable header: "you wrote" / "you reached for"). `grammar` keeps
  its matched `original`/`fixed` pair.
- Suffix rule: `_at` = full ISO datetime, a bare date (`due_date`) = `YYYY-MM-DD`.
  The retired names `ts` and `date` must not come back.
- IDs: semantic slugs where meaning matters, bare `INTEGER PRIMARY KEY`
  where mechanics matter. No readable-ID generators.
- Always parameterized queries (`?`), transactions via `with con:`.

## Ad-hoc queries & dev surgery (sql.py)

`plugins/shadowling/sql.py` is the dev console — the replacement for any DB
MCP / raw `sqlite3` use. Raw `sqlite3` against the live db is forbidden
(WAL-consistency + perms).

- Prefer the data layer first: each skill's entrypoint (`<cat>.py record/select`,
  `loot.py add`, `drop.py remove`, `triage.py tag`, `debrief.py mark-processed`) and
  the repository methods in `models/*`. `db.py` export is replaced by
  `sql.py --md "SELECT …"`.
- Any read: `python3 sql.py "<SELECT …>" [param …]` (JSON per row) or
  `--md` (markdown table). Params bind to `?` — never inline values.
- Mutations: `python3 sql.py --write "<SQL>" [param …]` — LAST resort for
  surgery (deduping a corrupted row, fixing a bad tag). Auto-snapshots to
  `<data_dir>/backups/` (keep last 10) before executing.
- Manual snapshot before risky experiments: `python3 sql.py backup`.
- Temp-home testing: `export SHADOWLING_HOME=$(mktemp -d)` and EVERY command
  that depends on it MUST run in the SAME Bash invocation — env vars do not
  survive across calls; a lost SHADOWLING_HOME silently retargets every
  command at the real `~/.shadowling`. `sql.py --write` announces its target
  (`db: <path>` on stderr) — verify it before trusting a destructive demo.
- Restore: `cp <data_dir>/backups/<snap>.db <data_dir>/shadowling.db`
  (remove the `-wal`/`-shm` siblings first).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.