skillplay 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of skillplay might be problematic. Click here for more details.
- skillplay-1.1.1/.github/workflows/ci.yml +100 -0
- skillplay-1.1.1/ARCHITECTURE.md +357 -0
- skillplay-1.1.1/PKG-INFO +13 -0
- skillplay-1.1.1/README.md +96 -0
- skillplay-1.1.1/config.sample.yaml +30 -0
- skillplay-1.1.1/docs/authoring-packs.md +143 -0
- skillplay-1.1.1/docs/editor-integration.md +76 -0
- skillplay-1.1.1/pyproject.toml +41 -0
- skillplay-1.1.1/scripts/build_binary.py +76 -0
- skillplay-1.1.1/skillplay/__init__.py +3 -0
- skillplay-1.1.1/skillplay/__main__.py +24 -0
- skillplay-1.1.1/skillplay/core/__init__.py +39 -0
- skillplay-1.1.1/skillplay/core/achievements.py +73 -0
- skillplay-1.1.1/skillplay/core/adaptive.py +291 -0
- skillplay-1.1.1/skillplay/core/capstone.py +140 -0
- skillplay-1.1.1/skillplay/core/cli.py +823 -0
- skillplay-1.1.1/skillplay/core/config.py +61 -0
- skillplay-1.1.1/skillplay/core/demo.py +138 -0
- skillplay-1.1.1/skillplay/core/engine.py +620 -0
- skillplay-1.1.1/skillplay/core/exam.py +122 -0
- skillplay-1.1.1/skillplay/core/generate.py +642 -0
- skillplay-1.1.1/skillplay/core/goals.py +138 -0
- skillplay-1.1.1/skillplay/core/hlr.py +73 -0
- skillplay-1.1.1/skillplay/core/i18n.py +112 -0
- skillplay-1.1.1/skillplay/core/leaderboard_server.py +288 -0
- skillplay-1.1.1/skillplay/core/loader.py +214 -0
- skillplay-1.1.1/skillplay/core/mentor.py +214 -0
- skillplay-1.1.1/skillplay/core/progress.py +184 -0
- skillplay-1.1.1/skillplay/core/registry.py +314 -0
- skillplay-1.1.1/skillplay/core/registry_index.json +8 -0
- skillplay-1.1.1/skillplay/core/schema.py +177 -0
- skillplay-1.1.1/skillplay/core/share.py +61 -0
- skillplay-1.1.1/skillplay/core/skillgraph.py +161 -0
- skillplay-1.1.1/skillplay/core/sound.py +34 -0
- skillplay-1.1.1/skillplay/core/stats.py +88 -0
- skillplay-1.1.1/skillplay/core/streak.py +33 -0
- skillplay-1.1.1/skillplay/core/sync.py +182 -0
- skillplay-1.1.1/skillplay/core/telemetry.py +66 -0
- skillplay-1.1.1/skillplay/core/validators.py +568 -0
- skillplay-1.1.1/skillplay/packs/algorithms/challenges/01-fib.yaml +36 -0
- skillplay-1.1.1/skillplay/packs/algorithms/challenges/02-factorial.yaml +34 -0
- skillplay-1.1.1/skillplay/packs/algorithms/challenges/03-prime.yaml +38 -0
- skillplay-1.1.1/skillplay/packs/algorithms/challenges/04-max.yaml +31 -0
- skillplay-1.1.1/skillplay/packs/algorithms/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/01-p.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/02-id.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/03-class.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/04-desc.yaml +17 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/05-color.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/css-basics/challenges/06-em.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/css-basics/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/data-structures/challenges/01-first.yaml +41 -0
- skillplay-1.1.1/skillplay/packs/data-structures/challenges/02-last.yaml +41 -0
- skillplay-1.1.1/skillplay/packs/data-structures/challenges/03-reverse.yaml +41 -0
- skillplay-1.1.1/skillplay/packs/data-structures/challenges/04-count.yaml +41 -0
- skillplay-1.1.1/skillplay/packs/data-structures/pack.yaml +14 -0
- skillplay-1.1.1/skillplay/packs/fix-bug/challenges/01-sum.yaml +31 -0
- skillplay-1.1.1/skillplay/packs/fix-bug/challenges/02-greet.yaml +31 -0
- skillplay-1.1.1/skillplay/packs/fix-bug/challenges/03-max.yaml +31 -0
- skillplay-1.1.1/skillplay/packs/fix-bug/challenges/04-even.yaml +31 -0
- skillplay-1.1.1/skillplay/packs/fix-bug/pack.yaml +12 -0
- skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/01-double.yaml +33 -0
- skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/02-sum.yaml +36 -0
- skillplay-1.1.1/skillplay/packs/fix-bug-js/challenges/03-palindrome.yaml +34 -0
- skillplay-1.1.1/skillplay/packs/fix-bug-js/pack.yaml +12 -0
- skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/01-greet.yaml +32 -0
- skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/02-sum_even.yaml +34 -0
- skillplay-1.1.1/skillplay/packs/freeform-intro/challenges/03-palindrome-js.yaml +33 -0
- skillplay-1.1.1/skillplay/packs/freeform-intro/pack.yaml +12 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/01-init.yaml +19 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/02-status.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/03-add.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/04-commit.yaml +21 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/05-branch.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/06-mc-stage.yaml +28 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/07-mc-clone.yaml +28 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/08-mc-merge-rebase.yaml +28 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/09-mc-undo.yaml +29 -0
- skillplay-1.1.1/skillplay/packs/git-basics/challenges/10-mc-remote.yaml +28 -0
- skillplay-1.1.1/skillplay/packs/git-basics/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/01-get.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/02-post.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/03-put.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/04-404.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/05-ctype.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/http-rest/challenges/06-auth.yaml +21 -0
- skillplay-1.1.1/skillplay/packs/http-rest/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/mini-cli/challenges/01-greet.yaml +33 -0
- skillplay-1.1.1/skillplay/packs/mini-cli/challenges/02-add.yaml +33 -0
- skillplay-1.1.1/skillplay/packs/mini-cli/challenges/03-main.yaml +32 -0
- skillplay-1.1.1/skillplay/packs/mini-cli/pack.yaml +17 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/01-literal.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/02-digit.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/03-nondigit.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/04-start.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/05-end.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/06-plus.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/07-star.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/08-optional.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/09-class.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/10-negclass.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/11-boundary.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/12-alt.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/13-range.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/14-group.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/challenges/15-anchored.yaml +20 -0
- skillplay-1.1.1/skillplay/packs/regex-101/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/schemas/challenge.schema.json +80 -0
- skillplay-1.1.1/skillplay/packs/schemas/pack.schema.json +31 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/01-ls.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/02-pwd.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/03-cd.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/04-rm.yaml +16 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/05-grep.yaml +17 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/challenges/06-find.yaml +17 -0
- skillplay-1.1.1/skillplay/packs/shell-basics/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/01-select.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/02-where.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/03-order.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/04-limit.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/05-count.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/06-distinct.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/07-and.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/08-or.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/09-in.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/10-between.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/11-like.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/12-join.yaml +27 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/13-group.yaml +27 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/14-having.yaml +26 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/challenges/15-subquery.yaml +24 -0
- skillplay-1.1.1/skillplay/packs/sql-basics/pack.yaml +10 -0
- skillplay-1.1.1/skillplay/tui/__init__.py +3 -0
- skillplay-1.1.1/skillplay/tui/app.py +1108 -0
- skillplay-1.1.1/tests/conftest.py +49 -0
- skillplay-1.1.1/tests/leaderboard_server_test.py +115 -0
- skillplay-1.1.1/tests/registry_test.py +48 -0
- skillplay-1.1.1/tests/smoke_core.py +90 -0
- skillplay-1.1.1/tests/smoke_tui.py +70 -0
- skillplay-1.1.1/tests/test_capstone.py +82 -0
- skillplay-1.1.1/tests/test_community.py +82 -0
- skillplay-1.1.1/tests/test_exam.py +85 -0
- skillplay-1.1.1/tests/test_features.py +166 -0
- skillplay-1.1.1/tests/test_hardening.py +165 -0
- skillplay-1.1.1/tests/test_hlr.py +98 -0
- skillplay-1.1.1/tests/test_intelligence.py +428 -0
- skillplay-1.1.1/tests/test_mentor.py +87 -0
- skillplay-1.1.1/tests/test_skillgraph.py +150 -0
- skillplay-1.1.1/tests/test_sync.py +152 -0
- skillplay-1.1.1/tests/test_v9.py +80 -0
- skillplay-1.1.1/tests/test_validators.py +210 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- name: Install
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
pip install -e ".[dev]"
|
|
24
|
+
- name: Validate packs
|
|
25
|
+
run: python -m skillplay validate-packs
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: python -m pytest tests/ -q
|
|
28
|
+
|
|
29
|
+
lint:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: actions/setup-python@v5
|
|
34
|
+
with:
|
|
35
|
+
python-version: "3.12"
|
|
36
|
+
- name: Install ruff
|
|
37
|
+
run: python -m pip install --upgrade pip ruff
|
|
38
|
+
- name: Ruff lint
|
|
39
|
+
run: python -m ruff check .
|
|
40
|
+
- name: Ruff format check
|
|
41
|
+
run: python -m ruff format --check .
|
|
42
|
+
|
|
43
|
+
publish:
|
|
44
|
+
needs: test
|
|
45
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
# id-token: write is required for PyPI trusted publishing (OIDC) and for
|
|
48
|
+
# generating build attestations. No PYPI_API_TOKEN secret is needed when
|
|
49
|
+
# the project is configured as a trusted publisher on pypi.org.
|
|
50
|
+
permissions:
|
|
51
|
+
contents: read
|
|
52
|
+
id-token: write
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.12"
|
|
58
|
+
- name: Build distribution
|
|
59
|
+
run: |
|
|
60
|
+
python -m pip install --upgrade pip build
|
|
61
|
+
python -m build
|
|
62
|
+
- name: Publish to PyPI
|
|
63
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
64
|
+
with:
|
|
65
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
66
|
+
# Homebrew: publishing to a tap is repo-specific; the wheel on PyPI is the
|
|
67
|
+
# source of truth. Add a homebrew tap formula in a separate repo pointing
|
|
68
|
+
# at the PyPI release when you want `brew install skillplay`.
|
|
69
|
+
|
|
70
|
+
# D: ship a single self-contained binary per tag, for each major OS.
|
|
71
|
+
build-binary:
|
|
72
|
+
needs: test
|
|
73
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
74
|
+
strategy:
|
|
75
|
+
matrix:
|
|
76
|
+
include:
|
|
77
|
+
- os: ubuntu-latest
|
|
78
|
+
artifact: skillplay-linux
|
|
79
|
+
- os: macos-latest
|
|
80
|
+
artifact: skillplay-macos
|
|
81
|
+
- os: windows-latest
|
|
82
|
+
artifact: skillplay-windows.exe
|
|
83
|
+
runs-on: ${{ matrix.os }}
|
|
84
|
+
permissions:
|
|
85
|
+
contents: read
|
|
86
|
+
steps:
|
|
87
|
+
- uses: actions/checkout@v4
|
|
88
|
+
- uses: actions/setup-python@v5
|
|
89
|
+
with:
|
|
90
|
+
python-version: "3.12"
|
|
91
|
+
- name: Install PyInstaller
|
|
92
|
+
run: python -m pip install --upgrade pip pyinstaller
|
|
93
|
+
- name: Build single binary
|
|
94
|
+
run: python scripts/build_binary.py --method pyinstaller
|
|
95
|
+
- name: Upload binary
|
|
96
|
+
uses: actions/upload-artifact@v4
|
|
97
|
+
with:
|
|
98
|
+
name: ${{ matrix.artifact }}
|
|
99
|
+
path: dist/skillplay*
|
|
100
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# skillplay — Architecture
|
|
2
|
+
|
|
3
|
+
> System design: project structure, data schemas, and runtime flows.
|
|
4
|
+
> Companion docs: `roadmap.md` (phases/status). This file describes **how the system works as built**.
|
|
5
|
+
|
|
6
|
+
## 1. Tech choices
|
|
7
|
+
|
|
8
|
+
- Python 3.12, Textual (TUI) + Rich (rendering), PyYAML (packs), platformdirs (data dir), `sqlite3` stdlib (SQL validation).
|
|
9
|
+
- Offline-first, keyboard-only. Sound off by default (`settings.sound: false`, no audio backend wired).
|
|
10
|
+
- Entry point: `skillplay = "skillplay.__main__:main"` (`pyproject.toml`), `SkillPlayApp().run()`.
|
|
11
|
+
|
|
12
|
+
## 2. Project structure
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
skillplay/ # project root
|
|
16
|
+
pyproject.toml # hatchling build, deps, console script
|
|
17
|
+
roadmap.md # phases + verified state
|
|
18
|
+
ARCHITECTURE.md # this file
|
|
19
|
+
README.md # overview + pipx install + usage
|
|
20
|
+
config.sample.yaml # documented user config template
|
|
21
|
+
docs/
|
|
22
|
+
authoring-packs.md # community pack authoring guide
|
|
23
|
+
skillplay/ # package
|
|
24
|
+
__init__.py
|
|
25
|
+
__main__.py # main() -> TUI, or dispatches CLI subcommands
|
|
26
|
+
core/
|
|
27
|
+
__init__.py
|
|
28
|
+
loader.py # pack discovery + YAML parsing (loader.py:49-127)
|
|
29
|
+
validators.py # answer checking, behavior not strings (validators.py:16-150)
|
|
30
|
+
engine.py # session loop, scoring, SRS, weighting (engine.py:23-150)
|
|
31
|
+
progress.py # progress.json load/save, XP math (progress.py:18-58)
|
|
32
|
+
streak.py # daily streak rules (streak.py:9-33)
|
|
33
|
+
schema.py # lightweight pack/challenge schema checks
|
|
34
|
+
config.py # optional config.yaml merge over settings
|
|
35
|
+
cli.py # validate-packs / new-pack / export-stats / leaderboard
|
|
36
|
+
tui/
|
|
37
|
+
__init__.py
|
|
38
|
+
app.py # HomeScreen / PlayScreen / SummaryScreen / StatsScreen / SkillPlayApp
|
|
39
|
+
packs/
|
|
40
|
+
sql-basics/ (15 challenges)
|
|
41
|
+
regex-101/ (15 challenges, regex_tester)
|
|
42
|
+
git-basics/ (10 challenges, exact + multiple_choice)
|
|
43
|
+
fix-bug/ (4 challenges, test_cases / python)
|
|
44
|
+
fix-bug-js/ (3 challenges, test_cases / js via node)
|
|
45
|
+
css-basics/ (6 challenges, exact + multiple_choice)
|
|
46
|
+
shell-basics/ (6 challenges, exact)
|
|
47
|
+
http-rest/ (6 challenges, exact + multiple_choice)
|
|
48
|
+
data-structures/ (4 challenges, test_cases / python)
|
|
49
|
+
algorithms/ (4 challenges, test_cases / python)
|
|
50
|
+
schemas/
|
|
51
|
+
pack.schema.json
|
|
52
|
+
challenge.schema.json
|
|
53
|
+
tests/
|
|
54
|
+
smoke_core.py # loader/validators/engine/streak checks (isolated tmp data dir)
|
|
55
|
+
smoke_tui.py # headless Textual E2E: Home→Play→retry→Summary→Home
|
|
56
|
+
conftest.py # tmp-data-dir fixture + pack fixtures
|
|
57
|
+
test_validators.py # pytest: per-validator + engine unit tests
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Module responsibilities:
|
|
61
|
+
|
|
62
|
+
| Module | Owns | Never touches |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `core/loader` | finding + parsing packs into `Pack`/`Challenge` dataclasses | UI, progress file |
|
|
65
|
+
| `core/validators` | `validate(challenge, input) -> Result(correct, detail)` | session state, disk |
|
|
66
|
+
| `core/engine` | challenge selection, scoring, SRS box updates, finalize, daily pick | widgets, YAML |
|
|
67
|
+
| `core/progress` | `progress.json` load/save, `level_for_xp` / `xp_for_level` | challenges |
|
|
68
|
+
| `core/streak` | pure date math for streaks | disk, UI |
|
|
69
|
+
| `core/schema` | `validate_pack(pack, strict) -> (errors, warnings)` — required keys, known modes, mode-specific fields, duplicate ids/prompts, over-broad regex, reference self-validation | disk, UI |
|
|
70
|
+
| `core/config` | optional `config.yaml` merge over `settings` | disk |
|
|
71
|
+
| `core/cli` | `validate-packs` / `export-stats` / `leaderboard` subcommands | UI |
|
|
72
|
+
| `tui/app` | screens, input, rendering, wiring engine calls | YAML parsing, SQL |
|
|
73
|
+
|
|
74
|
+
Dependency direction: `tui → core.{engine,loader,progress,streak}`; `engine → validators, progress, streak`. Validators and streak are leaf modules (no internal imports).
|
|
75
|
+
|
|
76
|
+
## 3. Schemas
|
|
77
|
+
|
|
78
|
+
### 3.1 Pack manifest (`pack.yaml`)
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
id: sql-basics
|
|
82
|
+
name: SQL Basics
|
|
83
|
+
version: 0.1.0
|
|
84
|
+
author: core
|
|
85
|
+
skill: sql
|
|
86
|
+
description: SELECT, WHERE, ORDER BY — learn SQL by doing.
|
|
87
|
+
difficulty: beginner
|
|
88
|
+
schema_version: 1 # bump on manifest-format changes
|
|
89
|
+
tags: [sql, beginner] # shown on Home; aids discovery
|
|
90
|
+
license: MIT # optional
|
|
91
|
+
min_version: "0.1.0" # optional: requires skillplay >= this
|
|
92
|
+
contributor: core # defaults to author
|
|
93
|
+
entry: challenges/*.yaml # glob, default challenges/*.yaml
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Discovery order (`loader.py:49-56`): builtin `skillplay/packs/` → `%LOCALAPPDATA%/skillplay/packs` (user) → `./packs` (local). First pack id wins (dedupe in `load_all_packs`); dirs without `pack.yaml` or with zero challenges are skipped, never fatal.
|
|
97
|
+
|
|
98
|
+
### 3.2 Challenge file
|
|
99
|
+
|
|
100
|
+
Required: `id`, `type`. Defaults: `difficulty: 1`, `xp: 10`, `topic/title/hints/explanation` empty. `skill` is inherited from the pack manifest (`loader.py:86`). Real example (`02-where.yaml`):
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
id: sql-where-01
|
|
104
|
+
title: Filter with WHERE
|
|
105
|
+
topic: where
|
|
106
|
+
difficulty: 1
|
|
107
|
+
xp: 15
|
|
108
|
+
type: sql_query
|
|
109
|
+
prompt: |
|
|
110
|
+
Return the names of users who are older than 30.
|
|
111
|
+
context:
|
|
112
|
+
db_seed_sql: |
|
|
113
|
+
CREATE TABLE users(name TEXT, age INT);
|
|
114
|
+
INSERT INTO users VALUES ('ana', 25), ('bob', 35), ('cid', 40);
|
|
115
|
+
answer:
|
|
116
|
+
reference_sql: "SELECT name FROM users WHERE age > 30"
|
|
117
|
+
validation:
|
|
118
|
+
mode: sql_result
|
|
119
|
+
order_matters: false
|
|
120
|
+
normalize: true
|
|
121
|
+
hints: [...]
|
|
122
|
+
explanation: |
|
|
123
|
+
`WHERE` filters rows before they are projected.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`type` values in use / planned: `sql_query`, `regex_build`, `git_command`, `multiple_choice` (used) → `fill_blank`, `fix_bug`, `ordering` (planned).
|
|
127
|
+
|
|
128
|
+
#### Challenge field reference (plugin API contract)
|
|
129
|
+
|
|
130
|
+
| Key | Required | Default | Notes |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| `id` | yes | — | unique across all packs; used for SRS + `completed_ids` |
|
|
133
|
+
| `type` | yes | — | selects validator family (`sql_query` today) |
|
|
134
|
+
| `prompt` | no | `""` | Markdown-ish text shown to the player |
|
|
135
|
+
| `title` / `topic` | no | `id` / `""` | `topic` groups stats later (e.g. `where`) |
|
|
136
|
+
| `difficulty` | no | `1` | 1–5 scale; currently informational only |
|
|
137
|
+
| `xp` | no | `10` | base XP before combo multiplier / retry halving |
|
|
138
|
+
| `answer` | no | `{}` | `reference_sql` for `sql_result`; `value`/`answers` for `exact` |
|
|
139
|
+
| `validation` | no | `{}` | must contain a known `mode` or validation always fails (§3.3) |
|
|
140
|
+
| `starter_code` | no (freeform) | `""` | V1: template pre-filled in the editor for `freeform` challenges (writing from scratch) |
|
|
141
|
+
| `validation.mode` | effectively | — | `exact` \| `regex_tester` \| `sql_result` \| `multiple_choice` \| `test_cases` \| `freeform` |
|
|
142
|
+
| `validation.normalize` | no | `true` | sort both row sets (order-insensitive compare) |
|
|
143
|
+
| `validation.order_matters` | no | `false` | skip sorting; use with `ORDER BY` challenges |
|
|
144
|
+
| `validation.case_insensitive` / `strip` | no | `false` / `true` | `exact` mode only |
|
|
145
|
+
| `validation.answers` | no | `[answer.value]` | accepted strings for `exact` mode |
|
|
146
|
+
| `validation.answer_id` | yes (MC) | — | correct option id for `multiple_choice`; must appear in `options[]` |
|
|
147
|
+
| `validation.must_match` / `must_not_match` | no | `[]` | test strings for `regex_tester` (empty = always fail) |
|
|
148
|
+
| `context.db_seed_sql` | no | `""` | `CREATE`/`INSERT` script run before both user + reference queries |
|
|
149
|
+
| `options[]` | yes (MC) | `[]` | `[{id, text}, …]` — rendered as buttons; max 8 (fixed button pool) |
|
|
150
|
+
| `hints` | no | `[]` | `hints[0]` shown on `Ctrl+H` |
|
|
151
|
+
| `explanation` | no | `""` | shown after a correct answer and on demand via `Ctrl+E` |
|
|
152
|
+
| `skill` | no (forbidden) | inherited | always overwritten from pack manifest (`loader.py:86`) — do not set per-challenge |
|
|
153
|
+
| `srs_reason` | no (runtime) | `""` | set by the engine on selection (`reason_for_challenge`); shown as `why: …` in PlayScreen — never authored in YAML |
|
|
154
|
+
|
|
155
|
+
### 3.3 Validators (`validators.py:90-150`)
|
|
156
|
+
|
|
157
|
+
Dispatch on `validation.mode`:
|
|
158
|
+
|
|
159
|
+
| Mode | Rule | Used by |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `exact` | string match after optional `strip` / `case_insensitive`; `answers[]` or `answer.value` | git-basics commands |
|
|
162
|
+
| `regex_tester` | compile user input; must match all `must_match`, none of `must_not_match` | regex-101 |
|
|
163
|
+
| `sql_result` | run user SQL + `reference_sql` on seeded in-memory sqlite, compare result sets | sql-basics |
|
|
164
|
+
| `multiple_choice` | compare chosen option id (case-insensitive) to `validation.answer_id` | git-basics concepts |
|
|
165
|
+
| `test_cases` | run user code in sandboxed subprocess; check named `function` against `test_cases` | fix-bug, data-structures, algorithms |
|
|
166
|
+
| `freeform` (V1) | identical runtime to `test_cases` but the TUI hides the test list and pre-fills `starter_code` so the player writes the function from scratch | freeform-intro |
|
|
167
|
+
| unknown | always `Result(False, "Unknown validation mode: …")` | — |
|
|
168
|
+
|
|
169
|
+
SQL safety (`validators.py:_run_sql` / `_reject_risky_sql`): seed via `executescript` first, **then** `PRAGMA query_only = ON`, then run the user query. Reversed order would block the seed itself. `normalize: true` sorts both row sets (order-insensitive); ordered challenges keep `normalize: false` (see `03-order.yaml`). Invalid SQL returns `Result(False, "SQL error: …")` — never raises.
|
|
170
|
+
|
|
171
|
+
Hardening (`validators.py`):
|
|
172
|
+
- `_reject_risky_sql` strips comments/string literals, then rejects multi-statement input (`;` followed by content) and write keywords (`INSERT/UPDATE/DELETE/DROP/…`) before the query runs — defense in depth on top of `query_only`.
|
|
173
|
+
- `_run_sql` installs `set_progress_handler` and aborts any query exceeding `_MAX_SQL_STEPS` (runaway `WITH RECURSIVE` / cartesian joins); results beyond `_MAX_SQL_ROWS` are rejected.
|
|
174
|
+
|
|
175
|
+
Never compare SQL or regex answers by string equality — only by behavior (rows returned / strings matched).
|
|
176
|
+
|
|
177
|
+
### 3.4 Progress (`progress.json`)
|
|
178
|
+
|
|
179
|
+
Location via `platformdirs.user_data_dir("skillplay")`: `%LOCALAPPDATA%\skillplay\progress.json` (Windows), `~/.local/share/skillplay/` (Linux), `~/Library/Application Support/skillplay/` (macOS).
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"version": 1,
|
|
184
|
+
"total_xp": 40,
|
|
185
|
+
"skills": {
|
|
186
|
+
"sql": { "xp": 40, "level": 1, "completed_ids": ["sql-select-01"], "attempts": 9, "correct": 5 }
|
|
187
|
+
},
|
|
188
|
+
"challenges": {
|
|
189
|
+
"sql-where-01": { "seen": 1, "correct": 1, "box": 2, "next_due": "2026-09-06" }
|
|
190
|
+
},
|
|
191
|
+
"streak": { "current": 1, "longest": 1, "last_played_date": "2026-09-05" },
|
|
192
|
+
"settings": { "session_size": 8, "sound": false }
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
- XP/level (`progress.py:18-23`): `level = floor(sqrt(xp/100)) + 1`; `xp_for_level(L) = (L-1)² × 100`.
|
|
197
|
+
- SRS box (`engine.py:106-121`): correct → `box = min(5, box+1)`; wrong → `box = 1`. `next_due` offsets by box: `{1:0, 2:1, 3:3, 4:7, 5:16}` days.
|
|
198
|
+
- V3 Half-life regression (HLR): `next_due` is now driven by a per-card learned
|
|
199
|
+
half-life (`core/hlr.py`), not the fixed box map. Each review takes one gradient
|
|
200
|
+
step using the delay since `last_review`, correctness, and response `latency`
|
|
201
|
+
(slow correct answers shrink stability — the W4 hesitant-recall rule). The card
|
|
202
|
+
is rescheduled when predicted recall is expected to fall to `TARGET_RECALL`
|
|
203
|
+
(0.90). `box` is still updated for backward-compatible weighting/display; new
|
|
204
|
+
cards stay due today on first encounter. `Stats` shows per-skill avg `memory`
|
|
205
|
+
half-life.
|
|
206
|
+
- Save is atomic (`progress.py:49-54`): write `progress.tmp` + `os.replace`. Corrupt JSON falls back to defaults (`progress.py:37-46`).
|
|
207
|
+
|
|
208
|
+
### 3.5 Streak rules (`streak.py:9-33`)
|
|
209
|
+
|
|
210
|
+
- Same day → no-op. Consecutive day (`diff == 1`) → `current += 1`. Gap (`diff > 1`) or first run → `current = 1`. `longest = max(longest, current)`. Dates stored as local ISO `YYYY-MM-DD`.
|
|
211
|
+
|
|
212
|
+
## 4. Flows
|
|
213
|
+
|
|
214
|
+
### 4.1 Boot (`SkillPlayApp`)
|
|
215
|
+
|
|
216
|
+
1. `load_all_packs()` → `load()` progress (or defaults) → `apply_config` merges optional `config.yaml`.
|
|
217
|
+
2. `compose` renders placeholder → `on_mount` updates streak, saves, pushes `HomeScreen`.
|
|
218
|
+
3. Home shows streak / total XP (refreshed on `on_show`), plus buttons: **Stats**, **Daily Challenge**, **Mixed**, **Due today (N)**, **Goals**, **Achievements**, **Community packs**, **Settings**, and one `Button` per pack (`id="pack-{id}"`, colon-free — Textual ids forbid `:`). `Due today (N)` shows `engine.count_due_today` and launches the due-today review session (B). **Community packs** opens `CommunityScreen` (D) listing discoverable packs from the registry index with in-app Install buttons.
|
|
219
|
+
|
|
220
|
+
### 4.2 Session loop (`PlayScreen`, `engine.py`)
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
pick pack / daily → on_mount: Session(skill, select_challenges(pack, progress, session_size))
|
|
224
|
+
select: skip challenges with next_due > today; if none due, use all;
|
|
225
|
+
weighted sample without replacement, session_size items
|
|
226
|
+
weight = (6 - box) + 2×mistakes + 1 (low box & mistake-heavy first)
|
|
227
|
+
# A3 mistake-type targeting: add 2×dominant-type mistakes, +3 when the
|
|
228
|
+
# challenge's dominant type equals the player's globally weakest type
|
|
229
|
+
# (_weakest_mistake_type). Each chosen card gets `srs_reason`
|
|
230
|
+
# (reason_for_challenge) rendered as "why: …" in the TUI.
|
|
231
|
+
per challenge:
|
|
232
|
+
render HUD (Q i/n, skill XP, combo multiplier) + prompt
|
|
233
|
+
multiple_choice → render option buttons (fixed pool of 8, reused — ids opt0..opt7,
|
|
234
|
+
option ids tracked in opt_map); Input hidden.
|
|
235
|
+
test_cases → multi-line `TextArea` (#code, language per `validation.lang`) +
|
|
236
|
+
`â–¶ Run tests (Ctrl+Enter)` button; Input hidden. (A1 real code UX.)
|
|
237
|
+
otherwise → Input shown (up/down recalls input history)
|
|
238
|
+
grade via Input.Submitted or option button → engine.submit(input):
|
|
239
|
+
correct → combo+=1, best_combo=max, xp = int(base × mult)
|
|
240
|
+
mult: combo≥5 → 1.5x, ≥3 → 1.2x, else 1.0x
|
|
241
|
+
retried-before → xp //= 2; advance SRS box; index+=1
|
|
242
|
+
wrong, first time on this challenge → retried_ids.add(id), stay (retry, half XP available)
|
|
243
|
+
wrong, already retried → combo=0, SRS box→1, index+=1
|
|
244
|
+
correct: green "+N XP" + explanation; retry offered: yellow detail; final wrong: red detail
|
|
245
|
+
Ctrl+H → first hint; Ctrl+E → explanation of current challenge;
|
|
246
|
+
Ctrl+S → skip_current (advance, no grade, no SRS/XP);
|
|
247
|
+
Esc → _finish(): finalize (once, guarded by _finalized) → Summary. No progress lost.
|
|
248
|
+
all answered → _finish(): finalize() if any results → pop Play → push Summary
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`Session` state (`engine.py`): `index`, `combo`/`best_combo`, `correct_count`, `xp_gained`, `results[]`, `retried_ids{}`. `done` ⟺ `index >= len(challenges)`.
|
|
252
|
+
|
|
253
|
+
Daily challenge: `engine.daily_challenge(packs)` seeds `random.Random(today)` over all challenges across packs; Home wraps the single pick in a synthetic one-challenge `Pack` and pushes `PlayScreen`.
|
|
254
|
+
|
|
255
|
+
Due-today review (B): `engine.select_due_today(packs, progress, size)` returns only SRS-due (or never-seen) cards across *all* skills, sorted weakest-first; Home's `Due today` button launches it as the daily habit loop. Goals screen (B): each goal exposes a `▶ Practice` button that builds a `Session` from `engine.adaptive_order(pack, progress)` — challenges reordered weakest-first so a curated "path" self-adjusts to the player's performance rather than being a fixed list.
|
|
256
|
+
|
|
257
|
+
### 4.3 Finalize + summary (`engine.py`, `app.py`)
|
|
258
|
+
|
|
259
|
+
`finalize`: `total_xp += session.xp_gained`; per-skill `xp`, recompute `level`, `attempts += total`, `correct += correct_count`, append correct ids to `completed_ids`; atomic save. Summary screen then updates streak + saves again, shows XP gained / accuracy / best combo; "Back to home" pops once, revealing Home.
|
|
260
|
+
|
|
261
|
+
### 4.4 Screen stack
|
|
262
|
+
|
|
263
|
+
`[base "Loading…" | Home] → push Play → [base | Home | Play] → finish: pop Play, push Summary → [base | Home | Summary] → back: pop once → [base | Home]`. Stats likewise: push StatsScreen → pop back. The base screen is never visible after mount. StatsScreen reads progress live (total XP, streak current/longest, per-skill level/XP/accuracy/completed).
|
|
264
|
+
|
|
265
|
+
### 4.7 Mastery exams (V6 / W6)
|
|
266
|
+
|
|
267
|
+
A real learning metric (not engagement). `core/exam.py` builds a randomized, mixed
|
|
268
|
+
session of ~`EXAM_SIZE` (20) challenges across all of a skill's packs, weighted
|
|
269
|
+
toward the *target difficulty tier* (current certification level + 1, capped at the
|
|
270
|
+
skill's hardest challenge). The session is graded normally; on finish,
|
|
271
|
+
`exam.certify` records `progress["certifications"][skill]` if accuracy ≥ 90%
|
|
272
|
+
(`PASS_RATIO`), setting `level` to the target tier and incrementing `exams_passed`.
|
|
273
|
+
Re-taking and passing raises the certified level. The TUI `ExamScreen` lists each
|
|
274
|
+
skill with its certified level + "Take exam" button; the result is shown on the
|
|
275
|
+
summary screen and persisted (with `progress.save`).
|
|
276
|
+
|
|
277
|
+
### 4.6 Encrypted sync (V5 / W5)
|
|
278
|
+
|
|
279
|
+
Account-free, client-side-encrypted `progress.json` sync. No passwords, no
|
|
280
|
+
accounts — pairing is a shared **device key** (a Fernet key, `cryptography`):
|
|
281
|
+
|
|
282
|
+
1. First device: `skillplay sync --pair` → prints the key (stored locally in
|
|
283
|
+
`DATA_DIR/sync.key`, *never* inside the synced blob).
|
|
284
|
+
2. Other devices: `skillplay sync --set-key <KEY>` (same key).
|
|
285
|
+
3. `sync --push` encrypts the on-disk `progress.json` (Fernet/AES-128-CBC+HMAC)
|
|
286
|
+
and `POST /sync/put` to the leaderboard server. `sync --pull` `GET /sync/get`s
|
|
287
|
+
the blob for an unguessable **slot** = `HMAC(key, handle)`, decrypts, and
|
|
288
|
+
overwrites local progress. `progress.save` snapshots the old file first, so a
|
|
289
|
+
pull is always undoable via `restore-progress`. The server stores only opaque
|
|
290
|
+
ciphertext (`leaderboard_sync.json`); it never sees the key or the plaintext.
|
|
291
|
+
|
|
292
|
+
### 4.5 CLI (`core/cli.py`, dispatched from `__main__`)
|
|
293
|
+
|
|
294
|
+
Bare `skillplay` → TUI; with subcommands → CLI:
|
|
295
|
+
|
|
296
|
+
| Command | Behavior |
|
|
297
|
+
|---|---|
|
|
298
|
+
| `validate-packs [--json] [--strict] [--fix]` | `schema.validate_pack` on every discovered pack: required keys, known modes, mode-specific fields, duplicate ids **and prompts**, over-broad regex, and reference-answer self-validation. `--json` emits a CI-friendly report; `--strict` promotes warnings (missing explanation/hints) to errors; `--fix` prints concrete fix suggestions per problem. Exit 1 on any error. |
|
|
299
|
+
| `new-pack <id> [--skill S] [--dir D]` | scaffolds a new pack at `D/<id>/` with `pack.yaml` + a sample `challenges/01-example.yaml` (A2 authoring). |
|
|
300
|
+
| `play --pack PATH` | loads a single local pack (no install) and launches the TUI with just that pack — for authoring previews. |
|
|
301
|
+
| `export-stats [--format json|md] [--output PATH]` | dumps `progress.json` or a Markdown table (per-skill XP/level/accuracy/completed). |
|
|
302
|
+
| `leaderboard [--name] [--url] [--view] [--skill S] [--publish]` | opt-in, account-free. Default POSTs `{"name", "total_xp", "skills":{...}}` (per-skill XP; URL from flag → `SKILLPLAY_LEADERBOARD_URL` env → `leaderboard.url` config, treated as a **base** URL). `--view [--skill S]` fetches and prints the all-time + weekly boards (per skill when given). `--publish` uploads the player's progress + SVG card to the server's public `/u/<handle>` page. No URL configured → friendly exit 1. |
|
|
303
|
+
| `share-stats [--format md\|svg\|json] [--output PATH] [--url URL] [--publish]` | generates a Markdown snippet / SVG card / JSON dump; `--publish` posts the SVG card as a public handle page (see `leaderboard --publish`). |
|
|
304
|
+
| `serve-leaderboard [--host H] [--port P] [--db PATH]` | runs the opt-in, account-free leaderboard server. Stores scores in `leaderboard.json` + profiles in `leaderboard_profiles.json`. Routes: `POST /submit` (per-skill scores), `POST /profile` (public page), `GET /api/board[?skill=]`, `GET /api/profile?name=`, `GET /u/<handle>` (HTML card). Weekly board = current ISO week (rolls over); all-time is cumulative. |
|
|
305
|
+
| `registry [--index URL]` | D: lists discoverable community packs from the index (`core/registry.available_packs`). |
|
|
306
|
+
| `install <name> [--index URL] [--force]` | installs a pack by URL, local path, or index name (`install_from_index`). Updates `.registry.json` so `update-packs` can re-pull. |
|
|
307
|
+
| `context --file PATH [--open]` | D: editor integration — maps a file's extension to the best matching pack and (with `--open`) launches its TUI. |
|
|
308
|
+
| `play --pack PATH` / `play --pack-id ID` | launches the TUI with one pack (local dir, or a built-in pack by id). |
|
|
309
|
+
|
|
310
|
+
Config (`core/config.py`): optional `config.yaml` next to `progress.json`; honors `session_size`, `sound`, `leaderboard.{name,url}` — see `config.sample.yaml`.
|
|
311
|
+
|
|
312
|
+
## 5. Invariants for contributors
|
|
313
|
+
|
|
314
|
+
1. Validators are pure: `(challenge, str) -> Result`. No I/O, no exceptions outward.
|
|
315
|
+
2. Engine mutates `progress` dict in memory; only `finalize`/`progress.save` touch disk.
|
|
316
|
+
3. Never trust string equality for SQL/regex — add a `validation.mode`, not a special case.
|
|
317
|
+
4. New packs = new folder under `skillplay/packs/` (or user/local dir) with `pack.yaml` + `challenges/*.yaml`. No code changes needed.
|
|
318
|
+
5. Textual constraints learned the hard way: no screen ops inside `compose()`; no `self.app` access in `Screen.__init__` (use `on_mount`); widget ids `[A-Za-z0-9_-]` only; `align`/`content-align` need both axes in current Textual.
|
|
319
|
+
|
|
320
|
+
## 6. Decision log
|
|
321
|
+
|
|
322
|
+
| Decision | Chosen | Rejected | Why |
|
|
323
|
+
|---|---|---|---|
|
|
324
|
+
| TUI framework | Textual | BubbleTea (Go), Ratatui (Rust) | content iteration speed matters more than binary size at MVP; Python lets pack authors read validator code; `run_test()` pilot gives free headless E2E |
|
|
325
|
+
| Pack format | YAML | TOML, JSON | multi-line `prompt`/`db_seed_sql` blocks are readable in YAML; authors are humans, not machines |
|
|
326
|
+
| Progress store | single JSON file, atomic replace | SQLite | progress is one small document, always read/written whole; JSON is inspectable/debuggable by users; atomic replace is enough crash safety |
|
|
327
|
+
| SQL validation | in-memory sqlite, result-set compare | string match, hosted DB | zero deps (stdlib), true semantic checking, per-query DB so challenges can't interfere; `query_only` after seed blocks writes (verified) |
|
|
328
|
+
| Challenge identity | global string `id` | per-pack numbering | SRS records, `completed_ids`, and `retried_ids` all key on it across sessions |
|
|
329
|
+
| Level curve | `floor(sqrt(xp/100))+1` | linear | diminishing returns keep early game rewarding without runaway numbers |
|
|
330
|
+
|
|
331
|
+
## 7. Failure modes & limits (verified)
|
|
332
|
+
|
|
333
|
+
| # | Exposure | Behavior today | Status |
|
|
334
|
+
|---|---|---|---|
|
|
335
|
+
| 1 | Runaway user SQL (`WITH RECURSIVE` bomb, cartesian join) | `set_progress_handler` aborts after `_MAX_SQL_STEPS` (2M) instructions → `Result(False, "SQL error: interrupted")` | HANDLED (P4 guard) |
|
|
336
|
+
| 2 | Regex catastrophic backtracking (`(a+)+$` on long non-match) | hangs inside `_regex_tester` `.search()`; test strings are short so impact is a brief freeze | OPEN — acceptable until user-authored packs grow; then add `regex` timeout or length cap |
|
|
337
|
+
| 3 | `Esc` mid-session | `_finish()` → finalize (guarded once) → Summary. Answered XP + SRS updates are saved, nothing lost | HANDLED (was: silent loss — fixed in P4) |
|
|
338
|
+
| 4 | Multi-statement input (`SELECT …; DELETE …`) | `_reject_risky_sql` strips comments/literals, then rejects `;` followed by content before sqlite sees it → `Result(False, "SQL error: Only a single SQL statement is allowed")` | HANDLED (explicit pre-flight reject) |
|
|
339
|
+
| 5 | Write attempt (`DROP TABLE`, `INSERT`) | double-guarded: `_reject_risky_sql` rejects write keywords; `query_only` pragma → `OperationalError` if anything slips through | HANDLED |
|
|
340
|
+
| 6 | Corrupt `progress.json` | falls back to defaults (`progress.py`); corrupt file is overwritten on next save (data loss of old progress, app survives) | HANDLED (survival over recovery — by design) |
|
|
341
|
+
| 7 | Unknown `validation.mode` / bad pack YAML | challenge always grades wrong (`_unknown`); `validate-packs` CLI now surfaces schema errors + failed self-validation explicitly | HANDLED |
|
|
342
|
+
| 8 | Unbounded `fetchall` | queries returning more than `_MAX_SQL_ROWS` (5000) rows are rejected with a clean message | HANDLED (P4 row cap) |
|
|
343
|
+
| 9 | Option-button remount id collisions | option buttons are a fixed pool (`opt0..opt7`) reused via `opt_map`, never re-mounted per challenge | HANDLED (Textual `remove_children` is async — dynamic remount races) |
|
|
344
|
+
|
|
345
|
+
## 8. Testing
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
cd skillplay
|
|
349
|
+
python tests/smoke_core.py # 7 checks: loader, validators, retry/half-XP, finalize, streak, SRS
|
|
350
|
+
python tests/smoke_tui.py # headless pilot: Home→Play→wrong→retry→correct→Summary→Home
|
|
351
|
+
python -m pytest tests/ # unit suite: validators, engine, pack validation (needs `pytest`)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
- All scripts resolve the repo root from `__file__`, so they run from any cwd (packs load via builtin path).
|
|
355
|
+
- **Isolation:** all monkeypatch `progress.DATA_DIR`/`PROGRESS_PATH` to a fresh `tempfile.mkdtemp()` before running — real user progress is never touched. (Earlier versions wrote to the live data dir; fixed when moving into the repo.)
|
|
356
|
+
- `smoke_tui.py` and the pytest suite need `textual`; `smoke_core.py` needs only `pyyaml` + `platformdirs`. Dev deps: `pip install -e ".[dev]"`.
|
|
357
|
+
- pytest suite (`tests/test_validators.py` + `tests/conftest.py` fixtures) covers per-validator behavior (multi-statement/write rejection, runaway-query abort, multiple-choice, regex), engine retry/half-XP, session weighting, and `validate_pack` linting. Smoke scripts are retained for a zero-dep sanity check.
|
skillplay-1.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: skillplay
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: Learn-by-Play TUI — learn dev skills by playing bite-sized challenges
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: cryptography>=42.0
|
|
7
|
+
Requires-Dist: platformdirs>=4.2
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: rich>=13.7
|
|
10
|
+
Requires-Dist: textual>=0.83
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# skillplay
|
|
2
|
+
|
|
3
|
+
> Learn-by-Play TUI: learn dev skills (SQL, regex, git, CSS, JS) through
|
|
4
|
+
> bite-sized terminal challenges. Offline-first, keyboard-only, zero accounts.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
With [pipx](https://pipx.pypa.io/) (recommended — isolates the app in its own env):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pipx install skillplay # from PyPI
|
|
12
|
+
# or, from a clone of this repo:
|
|
13
|
+
pipx install . # editable install from source
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or with plain pip:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install . # from the repo root
|
|
20
|
+
python -m skillplay # launch
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Requires Python 3.11+.
|
|
24
|
+
|
|
25
|
+
## Run
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
skillplay # launches the TUI
|
|
29
|
+
skillplay validate-packs # check all installed packs against the schema
|
|
30
|
+
skillplay export-stats # print progress as Markdown
|
|
31
|
+
skillplay export-stats --format json --output stats.json
|
|
32
|
+
skillplay share-stats --format md # a copy-pasteable stats snippet
|
|
33
|
+
skillplay share-stats --format svg --output card.svg
|
|
34
|
+
skillplay leaderboard --name you # opt-in POST of (per-skill) XP (needs a URL)
|
|
35
|
+
skillplay leaderboard --view --skill sql # fetch + print the board, by skill
|
|
36
|
+
skillplay leaderboard --publish # publish your public profile/handle page
|
|
37
|
+
skillplay serve-leaderboard # run the opt-in leaderboard server (C)
|
|
38
|
+
skillplay registry # list discoverable community packs
|
|
39
|
+
skillplay install css-basics # install a community pack by index name
|
|
40
|
+
skillplay context --file app.py # D: suggest a pack for the file you're editing
|
|
41
|
+
skillplay play --pack-id sql-basics # jump straight into one built-in pack
|
|
42
|
+
skillplay install <url|path|name> # install a community pack (P10)
|
|
43
|
+
skillplay update-packs # re-install community packs
|
|
44
|
+
skillplay restore-progress # W5: list/restore automatic daily progress backups
|
|
45
|
+
skillplay sync --pair # V5: generate + print the device pairing key
|
|
46
|
+
skillplay sync --set-key <KEY> # V5: install a key copied from another device
|
|
47
|
+
skillplay sync --push # V5: encrypt + upload progress to the server
|
|
48
|
+
skillplay sync --pull # V5: download + decrypt progress onto this device
|
|
49
|
+
skillplay exam --skill sql # V6: take a randomized mastery exam (TUI)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What's included
|
|
53
|
+
|
|
54
|
+
| Pack | Skill | Challenges | Validator |
|
|
55
|
+
|---|---|---|---|
|
|
56
|
+
| `sql-basics` | sql | 15 | `sql_result` (in-memory sqlite) |
|
|
57
|
+
| `regex-101` | regex | 15 | `regex_tester` |
|
|
58
|
+
| `git-basics` | git | 10 | `exact` + `multiple_choice` |
|
|
59
|
+
| `fix-bug` | python | 4 | `test_cases` (sandboxed subprocess) |
|
|
60
|
+
| `fix-bug-js` | javascript | 3 | `test_cases` (node, sandboxed) |
|
|
61
|
+
| `css-basics` | css | 6 | `exact` + `multiple_choice` |
|
|
62
|
+
| `shell-basics` | shell | 6 | `exact` |
|
|
63
|
+
| `http-rest` | http | 6 | `exact` + `multiple_choice` |
|
|
64
|
+
| `data-structures` | python | 4 | `test_cases` |
|
|
65
|
+
| `freeform-intro` | python | 3 | `freeform` (hidden-test construction) |
|
|
66
|
+
|
|
67
|
+
## How to play
|
|
68
|
+
|
|
69
|
+
- Pick a pack (or **Mixed** for an interleaved session across all skills).
|
|
70
|
+
- `Ctrl+H` cycles through hints, `Ctrl+S` skips, `Ctrl+E` shows the explanation,
|
|
71
|
+
`Ctrl+U`/`Ctrl+D` rate the explanation, `Esc` finishes and saves the session.
|
|
72
|
+
- Correct answers earn XP with a combo multiplier; the first wrong answer
|
|
73
|
+
offers a retry for half XP.
|
|
74
|
+
- A Leitner spaced-repetition schedule brings weak challenges back sooner.
|
|
75
|
+
Mistakes are tagged (syntax / logic / off-by-one) and bias future weighting.
|
|
76
|
+
- Adaptive difficulty tilts selection toward harder challenges as you improve.
|
|
77
|
+
- Daily challenge, achievements, curated goals, and a crash-safe session
|
|
78
|
+
resume are all built in. Settings (sound, theme, language, telemetry) live in
|
|
79
|
+
the in-app **Settings** screen.
|
|
80
|
+
- **Mastery exams** (V6): take a randomized, mixed exam per skill — score ≥ 90%
|
|
81
|
+
to certify the next difficulty level. A real learning metric, not just XP.
|
|
82
|
+
|
|
83
|
+
## Authoring your own packs
|
|
84
|
+
|
|
85
|
+
See [docs/authoring-packs.md](docs/authoring-packs.md). Packs are just a
|
|
86
|
+
folder of YAML — no code changes needed. The built-in `git-basics` pack is a
|
|
87
|
+
minimal example using only `exact` and `multiple_choice` validators.
|
|
88
|
+
|
|
89
|
+
## Distribution
|
|
90
|
+
|
|
91
|
+
A single-binary build is supported via `scripts/build_binary.py` (PyInstaller,
|
|
92
|
+
falling back to `shiv`). On each tagged release (`v*`), CI builds a one-file
|
|
93
|
+
binary for Linux, macOS, and Windows and uploads them as artifacts
|
|
94
|
+
(`.github/workflows/ci.yml` → `build-binary` job), and publishes the wheel to
|
|
95
|
+
PyPI (`.github/workflows/ci.yml` → `publish` job).
|
|
96
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Sample user config
|
|
2
|
+
#
|
|
3
|
+
# Copy to the skillplay data dir (next to progress.json) and rename to
|
|
4
|
+
# `config.yaml`. All keys are optional; defaults are shown below.
|
|
5
|
+
#
|
|
6
|
+
# Linux/macOS: ~/.local/share/skillplay/config.yaml
|
|
7
|
+
# Windows: %LOCALAPPDATA%\skillplay\config.yaml
|
|
8
|
+
|
|
9
|
+
# How many challenges per session (default 8).
|
|
10
|
+
session_size: 8
|
|
11
|
+
|
|
12
|
+
# Sound effects (default off; dependency-free beep via terminal bell / winsound).
|
|
13
|
+
sound: false
|
|
14
|
+
|
|
15
|
+
# Theme: dark | light | high_contrast (respects NO_COLOR if set in the env).
|
|
16
|
+
theme: dark
|
|
17
|
+
|
|
18
|
+
# Language: en | es (community packs may add more).
|
|
19
|
+
language: en
|
|
20
|
+
|
|
21
|
+
# Anonymous telemetry of low first-try rates. Local-only by default; set true to
|
|
22
|
+
# also POST an anonymized aggregate (no names) to the leaderboard URL.
|
|
23
|
+
telemetry: false
|
|
24
|
+
|
|
25
|
+
# Opt-in online leaderboard (disabled until `url` is set — see `skillplay leaderboard --help`).
|
|
26
|
+
# `url` is the BASE url of a `serve-leaderboard` instance (e.g. http://localhost:8000);
|
|
27
|
+
# endpoints (/submit, /profile, /api/board, /u/<handle>) are appended automatically.
|
|
28
|
+
leaderboard:
|
|
29
|
+
name: anon
|
|
30
|
+
# url: http://localhost:8000
|