codepraxis 0.1.0__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.
- codepraxis-0.1.0/.gitignore +10 -0
- codepraxis-0.1.0/CONTRIBUTING.md +61 -0
- codepraxis-0.1.0/PKG-INFO +193 -0
- codepraxis-0.1.0/README.md +168 -0
- codepraxis-0.1.0/RELEASING.md +133 -0
- codepraxis-0.1.0/pyproject.toml +75 -0
- codepraxis-0.1.0/src/codepraxis/__init__.py +3 -0
- codepraxis-0.1.0/src/codepraxis/__main__.py +8 -0
- codepraxis-0.1.0/src/codepraxis/cli.py +219 -0
- codepraxis-0.1.0/src/codepraxis/commands/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/commands/login.py +75 -0
- codepraxis-0.1.0/src/codepraxis/commands/publish.py +136 -0
- codepraxis-0.1.0/src/codepraxis/commands/validate.py +67 -0
- codepraxis-0.1.0/src/codepraxis/domain/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/domain/contract.py +96 -0
- codepraxis-0.1.0/src/codepraxis/domain/pack.py +83 -0
- codepraxis-0.1.0/src/codepraxis/domain/results.py +160 -0
- codepraxis-0.1.0/src/codepraxis/errors.py +15 -0
- codepraxis-0.1.0/src/codepraxis/execution/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/execution/executor.py +39 -0
- codepraxis-0.1.0/src/codepraxis/execution/local/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/execution/local/backends.py +124 -0
- codepraxis-0.1.0/src/codepraxis/execution/local/executor.py +320 -0
- codepraxis-0.1.0/src/codepraxis/execution/local/worker.py +232 -0
- codepraxis-0.1.0/src/codepraxis/execution/local/workspace.py +73 -0
- codepraxis-0.1.0/src/codepraxis/execution/remote/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/execution/remote/client.py +131 -0
- codepraxis-0.1.0/src/codepraxis/execution/remote/config.py +85 -0
- codepraxis-0.1.0/src/codepraxis/execution/remote/executor.py +152 -0
- codepraxis-0.1.0/src/codepraxis/packio/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/packio/archive.py +69 -0
- codepraxis-0.1.0/src/codepraxis/packio/discovery.py +95 -0
- codepraxis-0.1.0/src/codepraxis/packio/loader.py +91 -0
- codepraxis-0.1.0/src/codepraxis/packio/toc.py +72 -0
- codepraxis-0.1.0/src/codepraxis/plugin/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/plugin/installer.py +121 -0
- codepraxis-0.1.0/src/codepraxis/plugin/templates/commands/new.md +32 -0
- codepraxis-0.1.0/src/codepraxis/plugin/templates/commands/validate.md +30 -0
- codepraxis-0.1.0/src/codepraxis/plugin/templates/marketplace.json +16 -0
- codepraxis-0.1.0/src/codepraxis/plugin/templates/plugin.json +8 -0
- codepraxis-0.1.0/src/codepraxis/plugin/templates/skills/pack-authoring/SKILL.md +103 -0
- codepraxis-0.1.0/src/codepraxis/reporting/__init__.py +0 -0
- codepraxis-0.1.0/src/codepraxis/reporting/human.py +164 -0
- codepraxis-0.1.0/src/codepraxis/reporting/json_reporter.py +85 -0
- codepraxis-0.1.0/src/codepraxis/reporting/reporter.py +32 -0
- codepraxis-0.1.0/tests/conformance/test_corpus.py +80 -0
- codepraxis-0.1.0/tests/conftest.py +26 -0
- codepraxis-0.1.0/tests/test_classification.py +124 -0
- codepraxis-0.1.0/tests/test_publish.py +205 -0
- codepraxis-0.1.0/tests/test_toc.py +53 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## The content boundary
|
|
4
|
+
|
|
5
|
+
This package is published to PyPI. Anything committed here becomes public.
|
|
6
|
+
|
|
7
|
+
**Never commit:**
|
|
8
|
+
|
|
9
|
+
- Challenge packs, questions, or reference solutions
|
|
10
|
+
- Test fixtures derived from real question content
|
|
11
|
+
- API keys, storage connection strings, registry credentials
|
|
12
|
+
- Runner image internals (`koro`, `perry`, proctoring, evaluation logic)
|
|
13
|
+
|
|
14
|
+
The CLI deliberately contains nothing proprietary: it runs the *author's* tests
|
|
15
|
+
against the *author's* code. Keeping that true is what makes public
|
|
16
|
+
distribution safe. If a change would put platform internals in this repo, the
|
|
17
|
+
change belongs on the server instead.
|
|
18
|
+
|
|
19
|
+
Test fixtures must be synthetic and written from scratch. The conformance suite
|
|
20
|
+
reads a private corpus through `PRAXIS_CONFORMANCE_PACKS` and skips when unset.
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
Layers, innermost first. Dependencies point inward only.
|
|
25
|
+
|
|
26
|
+
| Layer | Responsibility | Must not |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `domain/` | Data and the mirrored runner contract | Touch the filesystem, subprocesses, or the network |
|
|
29
|
+
| `packio/` | Filesystem → `Pack` | Import or execute pack code |
|
|
30
|
+
| `validation/` | Static rules over a `Pack` | Execute pack code |
|
|
31
|
+
| `execution/` | Run packs, produce `RunResult` | Print anything |
|
|
32
|
+
| `reporting/` | Render a `RunResult` | Read the filesystem |
|
|
33
|
+
| `commands/` | Orchestrate the above | Instantiate concrete executors or reporters |
|
|
34
|
+
| `cli.py` | Parse args, inject dependencies | Contain logic |
|
|
35
|
+
|
|
36
|
+
The load-bearing rule: **every executor returns the same `RunResult`.** Local and
|
|
37
|
+
remote tiers are interchangeable, so reporting and exit codes are
|
|
38
|
+
written once.
|
|
39
|
+
|
|
40
|
+
### Adding things
|
|
41
|
+
|
|
42
|
+
- **A lint rule** — a new file in `validation/rules/`, registered. No edits elsewhere.
|
|
43
|
+
- **A backend** (`backend.conf`'s `BACKEND`) — a new `BackendAdapter` in
|
|
44
|
+
`execution/local/backends.py`, registered. Declare honestly whether the local
|
|
45
|
+
tier can run it; a backend that needs container-only infrastructure should set
|
|
46
|
+
`locally_supported = False` rather than emit false failures.
|
|
47
|
+
- **An output format** — a new class satisfying `Reporter`.
|
|
48
|
+
- **An execution tier** — a new class satisfying `Executor`.
|
|
49
|
+
|
|
50
|
+
### Mirroring the runner
|
|
51
|
+
|
|
52
|
+
`domain/contract.py` mirrors constants and semantics from the runner image and
|
|
53
|
+
records provenance per constant. When you change it:
|
|
54
|
+
|
|
55
|
+
1. Cite the runner source for the new behaviour.
|
|
56
|
+
2. Add a conformance case that would fail under the old behaviour.
|
|
57
|
+
|
|
58
|
+
Where the local tier cannot reproduce the runner, emit a
|
|
59
|
+
`Severity.UNVERIFIABLE` diagnostic. Never guess — a false pass locally is worse
|
|
60
|
+
than no local check at all, because it costs an author a full remote cycle to
|
|
61
|
+
discover.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codepraxis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Author and validate CodePraxis challenge packs.
|
|
5
|
+
Project-URL: Documentation, https://docs.codepraxis.com/authoring
|
|
6
|
+
Author: CodePraxis
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Keywords: assessment,authoring,challenge,codepraxis
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build>=1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff==0.16.2; extra == 'dev'
|
|
23
|
+
Requires-Dist: twine>=5; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# codepraxis
|
|
27
|
+
|
|
28
|
+
Author and validate CodePraxis challenge packs from your own repository.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install codepraxis
|
|
32
|
+
codepraxis --login
|
|
33
|
+
codepraxis validate --local my-challenge # fast, advisory
|
|
34
|
+
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## What this is
|
|
38
|
+
|
|
39
|
+
A challenge pack is a directory — starter code, a test module, instructions.
|
|
40
|
+
This CLI lets you keep packs in your own git repository, iterate on them with
|
|
41
|
+
your own editor, and check them before they reach candidates.
|
|
42
|
+
|
|
43
|
+
Two tiers, one result shape:
|
|
44
|
+
|
|
45
|
+
| Command | Runs | Speed | Authoritative? |
|
|
46
|
+
|---|---|---|---|
|
|
47
|
+
| `codepraxis validate --local` | Pure-Python harness on your machine | seconds | No — advisory |
|
|
48
|
+
| `codepraxis validate --remote` | The real runner image, on CodePraxis | ~1 min | **Yes — gates publish** |
|
|
49
|
+
|
|
50
|
+
`--local` reproduces how the runner loads, orders and scores a pack, so it
|
|
51
|
+
catches most authoring mistakes in the inner loop. It is **not** the container:
|
|
52
|
+
it does not run `setup.sh` and does not have the image's package set. Anything
|
|
53
|
+
it cannot check is reported as a `note` rather than silently passing.
|
|
54
|
+
Publishing always requires a remote run.
|
|
55
|
+
|
|
56
|
+
### Packs that call a model
|
|
57
|
+
|
|
58
|
+
By default there is no model endpoint locally, so cases that need one are
|
|
59
|
+
reported **unverifiable** rather than failed — a pack is not broken just because
|
|
60
|
+
your laptop has no LLM proxy. Point it at a real endpoint and they run for real:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
export OPENAI_API_KEY=... # or --llm-api-key
|
|
64
|
+
export OPENAI_BASE_URL=... # or --llm-base-url
|
|
65
|
+
codepraxis validate --local my-challenge
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Once a key is configured the leniency stops: a model failure is then a real
|
|
69
|
+
failure, because it can be judged.
|
|
70
|
+
|
|
71
|
+
## The two fixtures
|
|
72
|
+
|
|
73
|
+
Every pack is validated twice:
|
|
74
|
+
|
|
75
|
+
- **solution** — `source/` overlaid with `solution/`. Must pass everything.
|
|
76
|
+
- **starter** — `source/` alone. Must *fail*.
|
|
77
|
+
|
|
78
|
+
The starter run is the one authors forget. A pack whose starter already passes
|
|
79
|
+
has tests that do not discriminate, and every candidate will score full marks.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
askgit (local)
|
|
83
|
+
solution 18/18 passed
|
|
84
|
+
starter 0/18 passed
|
|
85
|
+
PASSED 8.9s
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Three verdicts: **PASSED**, **FAILED**, and **INCONCLUSIVE** (this tier lacked
|
|
89
|
+
the infrastructure to judge it — not a failure, and it does not fail the
|
|
90
|
+
command). `--json` always emits a single document with a `packs` array, however
|
|
91
|
+
many packs ran.
|
|
92
|
+
|
|
93
|
+
## Pack layout
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
my-challenge/
|
|
97
|
+
├── metadata.json # {"name": "..."} — becomes the workspace directory
|
|
98
|
+
├── backend.conf # {"BACKEND": "AI", "LANGUAGE": "PYTHON"}
|
|
99
|
+
├── setup.sh # optional; installs dependencies (remote only)
|
|
100
|
+
├── source/ # what the candidate starts from
|
|
101
|
+
├── ._tests/test_1.py # a `testCases` class
|
|
102
|
+
└── ._course_data/
|
|
103
|
+
├── course_toc.json # selects the active test module
|
|
104
|
+
└── feature.md # the Instructions tab
|
|
105
|
+
solution/ # sibling, never uploaded — the reference solution
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Authoring with Claude Code
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
codepraxis --install claude-plugin
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
115
|
+
commands to enable it. You get:
|
|
116
|
+
|
|
117
|
+
- **`/codepraxis:new`** — scaffold a pack from a description
|
|
118
|
+
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
119
|
+
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
120
|
+
so it already knows the `testCases` contract and the two-fixture rule
|
|
121
|
+
|
|
122
|
+
Re-run with `--force` to overwrite an existing install.
|
|
123
|
+
|
|
124
|
+
## Authentication
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
codepraxis --login
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Prompts for an API key (hidden input), verifies it against the platform, and
|
|
131
|
+
stores it at `~/.config/codepraxis/config.json` with `0600` permissions. It
|
|
132
|
+
prints which company the key publishes as — worth reading, because that is what
|
|
133
|
+
every publish is scoped to.
|
|
134
|
+
|
|
135
|
+
In CI, skip the prompt:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
export CODEPRAXIS_TOKEN=...
|
|
139
|
+
export CODEPRAXIS_API_URL=... # optional; defaults to the production API
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Publishing
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
codepraxis --publish my-challenge # draft, with confirmation
|
|
146
|
+
codepraxis --publish my-challenge --live # straight to candidates
|
|
147
|
+
codepraxis --publish my-challenge --yes # non-interactive, for CI
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Publishing is deliberately strict, because a published challenge can be
|
|
151
|
+
assigned to candidates immediately:
|
|
152
|
+
|
|
153
|
+
- **Remote validation runs first.** Local results never qualify. Reuse an
|
|
154
|
+
earlier passing run with `--validation-run-id` if you have one.
|
|
155
|
+
- **A reference solution is required.** It is what proves the challenge is
|
|
156
|
+
solvable.
|
|
157
|
+
- **It publishes as a draft** unless you pass `--live`.
|
|
158
|
+
- **The company comes from your API key.** The CLI never sends a company id —
|
|
159
|
+
ownership is derived server-side, so a compromised or mistyped client can't
|
|
160
|
+
publish into someone else's catalog.
|
|
161
|
+
|
|
162
|
+
You'll be shown the company and asked to confirm before anything is created.
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
python3.11 -m pip install -e '.[dev]'
|
|
168
|
+
pytest
|
|
169
|
+
ruff check src tests scripts
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The package has **no runtime dependencies**. The harness must run on an author's
|
|
173
|
+
machine with nothing but a Python interpreter, so keep it that way — the remote
|
|
174
|
+
tier uses `urllib` from the standard library for the same reason.
|
|
175
|
+
|
|
176
|
+
### Conformance tests
|
|
177
|
+
|
|
178
|
+
The harness mirrors the production runner's behaviour (`setupCodeBase.py`,
|
|
179
|
+
`koro/test_loader.py`, `koro/test_runner.py`). Mirrors drift, so
|
|
180
|
+
`tests/conformance/` replays the harness across a corpus of real packs and
|
|
181
|
+
asserts the expected verdicts.
|
|
182
|
+
|
|
183
|
+
That corpus is **private and lives outside this repository**:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
PRAXIS_CONFORMANCE_PACKS=/path/to/question-bank pytest tests/conformance
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Without the variable the conformance tests skip.
|
|
190
|
+
|
|
191
|
+
> **Do not vendor packs into this repository.** This package is published
|
|
192
|
+
> publicly. No challenge content, no reference solutions, no fixtures derived
|
|
193
|
+
> from real questions. Scaffold templates must be written from scratch.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# codepraxis
|
|
2
|
+
|
|
3
|
+
Author and validate CodePraxis challenge packs from your own repository.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install codepraxis
|
|
7
|
+
codepraxis --login
|
|
8
|
+
codepraxis validate --local my-challenge # fast, advisory
|
|
9
|
+
codepraxis --publish my-challenge # validates in the runner, then publishes
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## What this is
|
|
13
|
+
|
|
14
|
+
A challenge pack is a directory — starter code, a test module, instructions.
|
|
15
|
+
This CLI lets you keep packs in your own git repository, iterate on them with
|
|
16
|
+
your own editor, and check them before they reach candidates.
|
|
17
|
+
|
|
18
|
+
Two tiers, one result shape:
|
|
19
|
+
|
|
20
|
+
| Command | Runs | Speed | Authoritative? |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `codepraxis validate --local` | Pure-Python harness on your machine | seconds | No — advisory |
|
|
23
|
+
| `codepraxis validate --remote` | The real runner image, on CodePraxis | ~1 min | **Yes — gates publish** |
|
|
24
|
+
|
|
25
|
+
`--local` reproduces how the runner loads, orders and scores a pack, so it
|
|
26
|
+
catches most authoring mistakes in the inner loop. It is **not** the container:
|
|
27
|
+
it does not run `setup.sh` and does not have the image's package set. Anything
|
|
28
|
+
it cannot check is reported as a `note` rather than silently passing.
|
|
29
|
+
Publishing always requires a remote run.
|
|
30
|
+
|
|
31
|
+
### Packs that call a model
|
|
32
|
+
|
|
33
|
+
By default there is no model endpoint locally, so cases that need one are
|
|
34
|
+
reported **unverifiable** rather than failed — a pack is not broken just because
|
|
35
|
+
your laptop has no LLM proxy. Point it at a real endpoint and they run for real:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
export OPENAI_API_KEY=... # or --llm-api-key
|
|
39
|
+
export OPENAI_BASE_URL=... # or --llm-base-url
|
|
40
|
+
codepraxis validate --local my-challenge
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Once a key is configured the leniency stops: a model failure is then a real
|
|
44
|
+
failure, because it can be judged.
|
|
45
|
+
|
|
46
|
+
## The two fixtures
|
|
47
|
+
|
|
48
|
+
Every pack is validated twice:
|
|
49
|
+
|
|
50
|
+
- **solution** — `source/` overlaid with `solution/`. Must pass everything.
|
|
51
|
+
- **starter** — `source/` alone. Must *fail*.
|
|
52
|
+
|
|
53
|
+
The starter run is the one authors forget. A pack whose starter already passes
|
|
54
|
+
has tests that do not discriminate, and every candidate will score full marks.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
askgit (local)
|
|
58
|
+
solution 18/18 passed
|
|
59
|
+
starter 0/18 passed
|
|
60
|
+
PASSED 8.9s
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Three verdicts: **PASSED**, **FAILED**, and **INCONCLUSIVE** (this tier lacked
|
|
64
|
+
the infrastructure to judge it — not a failure, and it does not fail the
|
|
65
|
+
command). `--json` always emits a single document with a `packs` array, however
|
|
66
|
+
many packs ran.
|
|
67
|
+
|
|
68
|
+
## Pack layout
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
my-challenge/
|
|
72
|
+
├── metadata.json # {"name": "..."} — becomes the workspace directory
|
|
73
|
+
├── backend.conf # {"BACKEND": "AI", "LANGUAGE": "PYTHON"}
|
|
74
|
+
├── setup.sh # optional; installs dependencies (remote only)
|
|
75
|
+
├── source/ # what the candidate starts from
|
|
76
|
+
├── ._tests/test_1.py # a `testCases` class
|
|
77
|
+
└── ._course_data/
|
|
78
|
+
├── course_toc.json # selects the active test module
|
|
79
|
+
└── feature.md # the Instructions tab
|
|
80
|
+
solution/ # sibling, never uploaded — the reference solution
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Authoring with Claude Code
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
codepraxis --install claude-plugin
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Writes a local plugin into `.codepraxis/claude-plugin/`, then tells you the two
|
|
90
|
+
commands to enable it. You get:
|
|
91
|
+
|
|
92
|
+
- **`/codepraxis:new`** — scaffold a pack from a description
|
|
93
|
+
- **`/codepraxis:validate`** — validate and fix what fails, in a loop
|
|
94
|
+
- **`pack-authoring` skill** — loads automatically when Claude touches a pack,
|
|
95
|
+
so it already knows the `testCases` contract and the two-fixture rule
|
|
96
|
+
|
|
97
|
+
Re-run with `--force` to overwrite an existing install.
|
|
98
|
+
|
|
99
|
+
## Authentication
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
codepraxis --login
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Prompts for an API key (hidden input), verifies it against the platform, and
|
|
106
|
+
stores it at `~/.config/codepraxis/config.json` with `0600` permissions. It
|
|
107
|
+
prints which company the key publishes as — worth reading, because that is what
|
|
108
|
+
every publish is scoped to.
|
|
109
|
+
|
|
110
|
+
In CI, skip the prompt:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
export CODEPRAXIS_TOKEN=...
|
|
114
|
+
export CODEPRAXIS_API_URL=... # optional; defaults to the production API
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Publishing
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
codepraxis --publish my-challenge # draft, with confirmation
|
|
121
|
+
codepraxis --publish my-challenge --live # straight to candidates
|
|
122
|
+
codepraxis --publish my-challenge --yes # non-interactive, for CI
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Publishing is deliberately strict, because a published challenge can be
|
|
126
|
+
assigned to candidates immediately:
|
|
127
|
+
|
|
128
|
+
- **Remote validation runs first.** Local results never qualify. Reuse an
|
|
129
|
+
earlier passing run with `--validation-run-id` if you have one.
|
|
130
|
+
- **A reference solution is required.** It is what proves the challenge is
|
|
131
|
+
solvable.
|
|
132
|
+
- **It publishes as a draft** unless you pass `--live`.
|
|
133
|
+
- **The company comes from your API key.** The CLI never sends a company id —
|
|
134
|
+
ownership is derived server-side, so a compromised or mistyped client can't
|
|
135
|
+
publish into someone else's catalog.
|
|
136
|
+
|
|
137
|
+
You'll be shown the company and asked to confirm before anything is created.
|
|
138
|
+
|
|
139
|
+
## Development
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
python3.11 -m pip install -e '.[dev]'
|
|
143
|
+
pytest
|
|
144
|
+
ruff check src tests scripts
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The package has **no runtime dependencies**. The harness must run on an author's
|
|
148
|
+
machine with nothing but a Python interpreter, so keep it that way — the remote
|
|
149
|
+
tier uses `urllib` from the standard library for the same reason.
|
|
150
|
+
|
|
151
|
+
### Conformance tests
|
|
152
|
+
|
|
153
|
+
The harness mirrors the production runner's behaviour (`setupCodeBase.py`,
|
|
154
|
+
`koro/test_loader.py`, `koro/test_runner.py`). Mirrors drift, so
|
|
155
|
+
`tests/conformance/` replays the harness across a corpus of real packs and
|
|
156
|
+
asserts the expected verdicts.
|
|
157
|
+
|
|
158
|
+
That corpus is **private and lives outside this repository**:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
PRAXIS_CONFORMANCE_PACKS=/path/to/question-bank pytest tests/conformance
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Without the variable the conformance tests skip.
|
|
165
|
+
|
|
166
|
+
> **Do not vendor packs into this repository.** This package is published
|
|
167
|
+
> publicly. No challenge content, no reference solutions, no fixtures derived
|
|
168
|
+
> from real questions. Scaffold templates must be written from scratch.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
`codepraxis` is published to PyPI. **A version number can never be reused** —
|
|
4
|
+
even a deleted release permanently burns its version. Treat every publish as
|
|
5
|
+
irreversible.
|
|
6
|
+
|
|
7
|
+
## One-time setup
|
|
8
|
+
|
|
9
|
+
The GitHub side is done: `codepraxis-org/codepraxis-cli` is private, and the
|
|
10
|
+
`pypi` and `testpypi` environments exist. What remains can only be done from a
|
|
11
|
+
browser signed in to PyPI — there is no API for creating a Trusted Publisher.
|
|
12
|
+
|
|
13
|
+
### 1. TestPyPI — https://test.pypi.org
|
|
14
|
+
|
|
15
|
+
Do this one first; it is the rehearsal target and mistakes there are cheap.
|
|
16
|
+
|
|
17
|
+
Account settings → **Publishing** → *Add a new pending publisher*:
|
|
18
|
+
|
|
19
|
+
| Field | Value |
|
|
20
|
+
|---|---|
|
|
21
|
+
| PyPI Project Name | `codepraxis` |
|
|
22
|
+
| Owner | `codepraxis-org` |
|
|
23
|
+
| Repository name | `codepraxis-cli` |
|
|
24
|
+
| Workflow name | `release.yml` |
|
|
25
|
+
| Environment name | `testpypi` |
|
|
26
|
+
|
|
27
|
+
### 2. PyPI — https://pypi.org
|
|
28
|
+
|
|
29
|
+
Same form, one field different:
|
|
30
|
+
|
|
31
|
+
| Field | Value |
|
|
32
|
+
|---|---|
|
|
33
|
+
| PyPI Project Name | `codepraxis` |
|
|
34
|
+
| Owner | `codepraxis-org` |
|
|
35
|
+
| Repository name | `codepraxis-cli` |
|
|
36
|
+
| Workflow name | `release.yml` |
|
|
37
|
+
| Environment name | **`pypi`** |
|
|
38
|
+
|
|
39
|
+
A *pending* publisher claims the name `codepraxis` and creates the project on
|
|
40
|
+
first upload, so registering a placeholder release is unnecessary — but do add
|
|
41
|
+
it before someone else takes the name.
|
|
42
|
+
|
|
43
|
+
### 3. Enable 2FA
|
|
44
|
+
|
|
45
|
+
On both accounts, for every maintainer.
|
|
46
|
+
|
|
47
|
+
No API tokens are stored anywhere. Trusted Publishing exchanges a short-lived
|
|
48
|
+
GitHub OIDC token for an upload credential, so there is no long-lived secret to
|
|
49
|
+
leak, rotate, or print into a log.
|
|
50
|
+
|
|
51
|
+
## The human gate
|
|
52
|
+
|
|
53
|
+
`codepraxis-org` is on **GitHub Free**, where private repositories cannot have
|
|
54
|
+
environment protection rules — required reviewers are rejected with HTTP 422.
|
|
55
|
+
So the gate is structural instead:
|
|
56
|
+
|
|
57
|
+
- **A tag only ever publishes to TestPyPI.** There is no path from
|
|
58
|
+
`git push --tags` to a permanent PyPI release.
|
|
59
|
+
- **PyPI requires a deliberate manual dispatch**: Actions → release → *Run
|
|
60
|
+
workflow* → `target: pypi`.
|
|
61
|
+
|
|
62
|
+
If the org moves to GitHub Team, add required reviewers to the `pypi`
|
|
63
|
+
environment, and the `pypi` job's `if:` condition can be relaxed to allow tags
|
|
64
|
+
through directly.
|
|
65
|
+
|
|
66
|
+
## Cutting a release
|
|
67
|
+
|
|
68
|
+
Version lives in exactly one place: `src/codepraxis/__init__.py`. Hatchling reads it
|
|
69
|
+
from there, so there is nothing to keep in sync.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# 1. Bump the version
|
|
73
|
+
vim src/codepraxis/__init__.py # __version__ = "0.2.0"
|
|
74
|
+
|
|
75
|
+
# 2. Verify locally, exactly as CI will
|
|
76
|
+
python -m build
|
|
77
|
+
python scripts/check_artifact.py dist
|
|
78
|
+
python -m twine check --strict dist/*
|
|
79
|
+
|
|
80
|
+
# 3. Smoke-test the built wheel in a clean environment
|
|
81
|
+
python -m venv /tmp/smoke && /tmp/smoke/bin/pip install dist/*.whl
|
|
82
|
+
/tmp/smoke/bin/codepraxis --version
|
|
83
|
+
|
|
84
|
+
# 4. Tag — runs every gate and publishes to TestPyPI
|
|
85
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
86
|
+
pip install --index-url https://test.pypi.org/simple/ codepraxis==0.2.0
|
|
87
|
+
|
|
88
|
+
# 5. Promote to PyPI, deliberately
|
|
89
|
+
gh workflow run release.yml -f target=pypi
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Step 5 is the only thing that writes to PyPI, and it cannot happen by accident.
|
|
93
|
+
The build re-runs every gate before uploading, including a check that the tag
|
|
94
|
+
matches `__version__`.
|
|
95
|
+
|
|
96
|
+
## What the gates check
|
|
97
|
+
|
|
98
|
+
| Gate | Catches |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `check_artifact.py` | Challenge packs, solutions, or credentials swept into a public artifact |
|
|
101
|
+
| `twine check --strict` | Metadata that renders wrong on the project page |
|
|
102
|
+
| Clean-venv smoke test | A module that imports from the source tree but is missing from the wheel |
|
|
103
|
+
| CI matrix | A syntax or stdlib feature newer than the `requires-python` floor |
|
|
104
|
+
|
|
105
|
+
The artifact check is the important one. This package is public and the
|
|
106
|
+
question bank is not; see CONTRIBUTING.md.
|
|
107
|
+
|
|
108
|
+
## Versioning
|
|
109
|
+
|
|
110
|
+
Semantic versioning, where the public contract is **the CLI surface and the pack
|
|
111
|
+
format** — not the Python API. `codepraxis.*` modules are internal and may change in
|
|
112
|
+
any release.
|
|
113
|
+
|
|
114
|
+
- **patch** — bug fixes, better diagnostics
|
|
115
|
+
- **minor** — new commands, new lint rules, new pack fields
|
|
116
|
+
- **major** — a pack that validated before now fails, or a command changes shape
|
|
117
|
+
|
|
118
|
+
Because the CLI talks to the platform, every request carries
|
|
119
|
+
`X-Praxis-CLI-Version`. When the server drops support for a pack contract, it
|
|
120
|
+
returns a clear upgrade error rather than failing obscurely — so shipping a
|
|
121
|
+
breaking pack-format change means updating the server's minimum supported
|
|
122
|
+
version in the same release.
|
|
123
|
+
|
|
124
|
+
## Yanking
|
|
125
|
+
|
|
126
|
+
If a release is broken, `yank` rather than delete:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# PyPI → Manage → Releases → Yank
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Yanking hides it from new resolutions while leaving pinned installs working.
|
|
133
|
+
Deleting breaks anyone who pinned it and still does not free the version.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "codepraxis"
|
|
7
|
+
description = "Author and validate CodePraxis challenge packs."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "Proprietary" }
|
|
11
|
+
authors = [{ name = "CodePraxis" }]
|
|
12
|
+
keywords = ["codepraxis", "assessment", "challenge", "authoring"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# The local harness is deliberately dependency-free: it must run on an author's
|
|
26
|
+
# machine with nothing but a Python interpreter. Network commands (push/publish)
|
|
27
|
+
# may add an HTTP client behind an extra; the harness may not.
|
|
28
|
+
dependencies = []
|
|
29
|
+
|
|
30
|
+
# Single source of truth: praxis.__version__. Keeping a literal here as well
|
|
31
|
+
# guarantees the two drift.
|
|
32
|
+
dynamic = ["version"]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
# ruff is pinned exactly: its default rule set changes between releases, so a
|
|
36
|
+
# floating version means lint passes locally and fails in CI (or vice versa).
|
|
37
|
+
dev = ["pytest>=7", "ruff==0.16.2", "build>=1", "twine>=5"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Documentation = "https://docs.codepraxis.com/authoring"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
codepraxis = "codepraxis.cli:main"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/codepraxis/__init__.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/codepraxis"]
|
|
50
|
+
|
|
51
|
+
# Explicit allow-list rather than exclusions. This package is published
|
|
52
|
+
# publicly, so a new top-level directory must be opted in deliberately —
|
|
53
|
+
# never swept into a release because nobody updated an ignore list.
|
|
54
|
+
[tool.hatch.build.targets.sdist]
|
|
55
|
+
include = [
|
|
56
|
+
"src/codepraxis",
|
|
57
|
+
"tests",
|
|
58
|
+
"README.md",
|
|
59
|
+
"CONTRIBUTING.md",
|
|
60
|
+
"RELEASING.md",
|
|
61
|
+
"pyproject.toml",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 120
|
|
66
|
+
target-version = "py39"
|
|
67
|
+
|
|
68
|
+
# Declared explicitly rather than inherited from ruff's defaults, which shift
|
|
69
|
+
# between releases. Adding a rule should be a deliberate commit, not a side
|
|
70
|
+
# effect of an upgrade.
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = ["E", "F", "W", "I", "UP", "B", "C4", "SIM"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|