learningfoundry 0.79.3__tar.gz → 0.83.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.
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/CHANGELOG.md +92 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/PKG-INFO +88 -5
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/README.md +85 -4
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/pyproject.toml +8 -1
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/__init__.py +1 -1
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/asset_resolver.py +11 -2
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/cli.py +114 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/exceptions.py +25 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/generator.py +61 -6
- learningfoundry-0.83.0/src/learningfoundry/integrations/nbfoundry.py +54 -0
- learningfoundry-0.83.0/src/learningfoundry/integrations/nbfoundry_stub.py +49 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/integrations/protocols.py +4 -1
- learningfoundry-0.83.0/src/learningfoundry/launch.py +349 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/resolver.py +80 -5
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/schema_v1.py +58 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +4 -1
- learningfoundry-0.83.0/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +124 -0
- learningfoundry-0.83.0/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.test.ts +133 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.test.ts +5 -3
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.test.ts +42 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +20 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +20 -5
- learningfoundry-0.79.3/src/learningfoundry/integrations/nbfoundry_stub.py +0 -31
- learningfoundry-0.79.3/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -35
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/.gitignore +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/LICENSE +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/docs/specs/nbfoundry/README.md +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/docs/specs/pyve/README.md +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/docs/specs/quizazz/README.md +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/__main__.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/config.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/directives.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/integrations/__init__.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/integrations/quizazz.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/logging_config.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/parser.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/pipeline.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/py.typed +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/schema_extensions.py +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/README.md +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/finish.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/fixtures/curriculum.json +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/global-teardown.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/lifecycle.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/navigation.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/progress.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/reset.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/text-block-bottom.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/video.spec.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/package.json +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/playwright.config.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/pnpm-workspace.yaml +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/app.css +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/AssessmentBlock.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/AssessmentBlock.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LockedLessonPlaceholder.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.observer.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/lesson-view.helpers.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/mount.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.helpers.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/assessment-passed.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/assessment-passed.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/duration.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/duration.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown-directives.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/viewport-completion.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/page.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/assessment/[id]/+page.svelte +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/assessment/[id]/page.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.helpers.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.test.ts +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/test-results/.last-run.json +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
- {learningfoundry-0.79.3 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/vite.config.ts +0 -0
|
@@ -7,6 +7,98 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.83.0] - 2026-06-18
|
|
11
|
+
|
|
12
|
+
**Option C — live marimo exercises** (Subphase K-2, Stories K.g–K.j). The single release for the subphase: nbfoundry now compiles a `ready` exercise to a runnable **marimo notebook** instead of a static `sections`/`expected_outputs` dict (which rendered as inert source in a `<pre>` — no executed cells, plots, or metrics). learningfoundry stages the notebook + an `exercises-manifest.json` sidecar, ships a learner-side `launch`/`stop` CLI that owns the notebook's lifecycle, and the frontend `ready` renderer becomes a launch banner.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`learningfoundry launch <id>` / `learningfoundry stop [<id>]` CLI** (Stories K.i.1–K.i.4) — the learner-side runtime that owns a `ready` exercise's marimo lifecycle (a static page cannot spawn an OS process). `launch` resolves the notebook path / `mode` / port from `exercises-manifest.json`, socket-probes the port, manages a port-keyed pidfile under `.learningfoundry/`, and spawns `marimo edit|run <notebook> --headless -p <port> --no-token` detached; it offers to replace a **launch-owned** marimo on the port but never blind-kills a **foreign** process. `stop <id>` tears down one exercise; bare `stop` stops all launch-owned notebooks. Cross-platform Python (`marimo` is a learner-runtime dependency found via `PATH`, never imported by the build).
|
|
17
|
+
- **Notebook staging + `exercises-manifest.json`** (Story K.h) — the resolver pulls `notebook_source` out of the banner content into an `ExerciseArtifact`; the generator writes each notebook to `exercises/<id>/<id>.py` (outside the web root) and the `id → {notebook_path, mode, port}` manifest sidecar at the project root.
|
|
18
|
+
- **`ExerciseBlock.mode`** (`edit` | `run`, default `edit`; Story K.g) — the author's per-exercise choice of how `launch` serves the notebook (editable notebook vs. read-only app).
|
|
19
|
+
- **`progressRepo.getExerciseStatus(exerciseRef)`** (Story K.j.1) — reads the persisted `exercise_status` so the banner derives its completed slate on load.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **nbfoundry contract → Option C** (Story K.g) — `compile_exercise` returns `{title, description, hints, environment, notebook_source}` (consumer-dependency-spec BR-1); the codegen MUST be torch-free (torch is a learner-runtime dependency only). The pass-through `NbfoundryProvider` carries the new dict unchanged.
|
|
24
|
+
- **`ExerciseBlock.svelte` `ready` renderer → launch banner** (Story K.j.1) — Copy the `learningfoundry launch <id>` command (transient "Copied ✓") → Open Exercise ↗ (new tab to `http://localhost:<port>`) → Mark as Complete (persists `exercise_status`, fires the completion callbacks) → completed slate (derived on load). Stub status still renders the placeholder.
|
|
25
|
+
- **`ExerciseContent` TS type + `stub_exercise()` → banner shape** (Story K.j.1) — `description` replaces `instructions`; `mode`/`port` are ready-only optionals; the resolver injects `id`/`status`/`mode`/`port` onto `ready` content.
|
|
26
|
+
|
|
27
|
+
### Removed
|
|
28
|
+
|
|
29
|
+
- **The Option-B static-display path** (Stories K.h, K.j.1) — `ExerciseContent.sections`/`expected_outputs`/`assets`, the `ExerciseSection`/`ExpectedOutput` TS types, and the K.e `static/exercises/<id>/` image-asset staging. The live notebook renders its own cells and outputs at run time.
|
|
30
|
+
|
|
31
|
+
### Notes
|
|
32
|
+
|
|
33
|
+
- **Phase-bundled release.** Stories K.g–K.j ran unversioned; this single **minor** bump (v0.83.0) ships the whole subphase. Future: Marimo-WASM in-browser execution (Option A) for non-GPU exercises and graded `submission` (cell-output scoring) remain deferred — the banner/launch contract is forward-compatible.
|
|
34
|
+
|
|
35
|
+
### Verified
|
|
36
|
+
|
|
37
|
+
- `pyve test` → 498 passed. `npx vitest run` → 289 passed. `svelte-check` → 0 errors / 0 warnings. `ruff` + `mypy src/` clean.
|
|
38
|
+
|
|
39
|
+
## [0.82.0] - 2026-06-18
|
|
40
|
+
|
|
41
|
+
`ExerciseBlock` ready renderer + real `ExerciseContent` types (Story K.f), the third and final story of the Subphase K-1 nbfoundry bundle. A `ready` exercise now renders inline in the SvelteKit frontend (manual-completion flavor) instead of drawing only instructions + hints; graded `submission` remains deferred to `## Future`.
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **`ExerciseBlock.svelte` ready renderer (manual completion).** Renders code-scaffold `sections` (read-only in v1; the `editable` flag marks learner insertion points but is reserved for the Marimo-WASM future), `expected_outputs` (`image` via the runtime-composed `/exercises/<id>/<path>` URL with `alt` + `loading="lazy"`; `text`/`table` inline), collapsible hints, and local-run setup instructions from `environment`. A "Mark as Complete" control persists `exercise_status` via `progressRepo.updateExerciseStatus(id, 'complete')`, fires a typed `oncomplete` event `{ exerciseRef: id, status: 'completed' }`, and contributes to lesson-level block completion through `ContentBlock`. `status: stub` still renders the placeholder card.
|
|
46
|
+
- **Real `ExerciseContent` TypeScript types** (`lib/types/index.ts`): `ExerciseSection` (title/description/code/editable), the `ExpectedOutput` discriminated union (`image` with `path`+`alt`; `text`/`table` with `content`), `ExerciseEnvironment`, plus `assets: string[]` and the resolver-injected `id: string`. Kept in lockstep with the Python compiled-exercise dict (Hidden Coupling).
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Resolver injects the exercise `id` into the resolved content** (both stub and ready) so the frontend has the `/exercises/<id>/<path>` asset-URL namespace and the `exerciseRef` progress key. Authoritative over the compiled dict (nbfoundry doesn't know learningfoundry's id or an explicit author override).
|
|
51
|
+
- **`stub_exercise()` factory now includes `assets: []`** so the `ExerciseContent` type invariant holds for stub content too (matches the documented stub shape).
|
|
52
|
+
|
|
53
|
+
### Notes
|
|
54
|
+
|
|
55
|
+
- **Pinned open item resolved (no notebook-location field).** The released-nbfoundry compiled dict carries no runnable-notebook location in the v1 (Option B / static) path — `marimo_wasm_bundle` is the Option-A future field. The renderer therefore surfaces the code-scaffold `sections` plus `environment.setup_instructions` and relies on the learner's cloned curriculum repo to run the exercise locally. Authoritative source: `docs/specs/nbfoundry/consumer-dependency-spec.md` BR-1 + § "v1 Rendering Behavior".
|
|
56
|
+
|
|
57
|
+
### Verified
|
|
58
|
+
|
|
59
|
+
- `pyve test` → 453 passed (was 450; +3 resolver id-injection tests). `npx vitest run` → 285 passed (+7 `ExerciseBlock.test.ts`). `svelte-check` → 0 errors. `ruff` + `mypy src/` clean.
|
|
60
|
+
|
|
61
|
+
## [0.81.0] - 2026-06-18
|
|
62
|
+
|
|
63
|
+
Exercise `id` + asset staging (Story K.e), the second of the Subphase K-1 bundle. A compiled `ready` exercise's referenced asset files are now staged into `static/exercises/<id>/<path>`, where `id` is the build-output namespace and the progress key — auto-derived from the `ref` stem and unique curriculum-wide.
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
|
|
67
|
+
- **`ExerciseBlock.id: str | None = None`** in `schema_v1.py`. Auto-derived from the `ref` filename stem when omitted; uniqueness enforced **curriculum-wide** on `CurriculumDef` (the id is a `static/exercises/<id>/` URL + progress key, not just per-module). A stem collision between two exercises — even in different modules — fails loud at parse time so the author sets an explicit `id`. Mirrors the `AssessmentDefinition.id` auto-gen precedent (Story J.r).
|
|
68
|
+
- **Exercise asset staging in the resolver.** After compiling a `ready` exercise, the resolver reads the dict's `assets: list[str]` and emits `Asset(source=base_dir/path, dest_relative="exercises/<id>/<path>")` into the shared `assets_by_dest` aggregator (deduped on `dest_relative`). `stub` exercises carry no assets and stage nothing.
|
|
69
|
+
- **`static/exercises` added to the generator's `_PRESERVED_PATHS`**, so previously-staged exercise assets survive a `learningfoundry build` re-run. The existing asset-copy loop stages exercise assets unchanged (it writes any `dest_relative`).
|
|
70
|
+
|
|
71
|
+
### Changed
|
|
72
|
+
|
|
73
|
+
- **Generalized the `Asset` dedup note and `_copy_assets` docstring** to cover both content-hashed image paths and non-hashed exercise paths (the dedup key is `dest_relative` for both; the size-based copy skip is exact for hashed paths, a cheap heuristic for exercise paths).
|
|
74
|
+
|
|
75
|
+
### Verified
|
|
76
|
+
|
|
77
|
+
- `pyve test` → 450 passed (was 437; +13 new tests across `test_schema_v1.py`, `test_resolver.py`, `test_generator.py`). No regressions.
|
|
78
|
+
- `ruff check` clean; `mypy src/` clean.
|
|
79
|
+
|
|
80
|
+
## [0.80.0] - 2026-06-18
|
|
81
|
+
|
|
82
|
+
Real nbfoundry exercise integration (Story K.d), the first of the Subphase K-1 bundle. Now that nbfoundry is published, `ready` exercise blocks compile through a real `NbfoundryProvider`; stub-vs-real selection is a per-block `status` field handled in the resolver, defaulting to `ready` so a typo'd `ref` fails loud instead of silently degrading to a placeholder.
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
|
|
86
|
+
- **`NbfoundryProvider` (`integrations/nbfoundry.py`).** A real `ExerciseProvider` that lazy-imports and delegates to `nbfoundry.compile_exercise(ref_path, base_dir)`, returning its dict unchanged. Missing package → `ImportError` with a `pip install learningfoundry[nbfoundry]` hint; any nbfoundry error → `IntegrationError` citing `ref_path`. Signature-identical to the protocol — the `status` switch lives in the resolver, not the provider.
|
|
87
|
+
- **`[nbfoundry]` optional-dependency extra (`nbfoundry>=0.1`)** in `pyproject.toml`, plus a mypy `ignore_missing_imports` override mirroring `quizazz`.
|
|
88
|
+
- **`ExerciseBlock.status: Literal["stub", "ready"] = "ready"`** in `schema_v1.py`. `ready` (default) compiles via `NbfoundryProvider`; `stub` is the explicit "not built yet" opt-in.
|
|
89
|
+
- **`stub_exercise(ref_path)` factory (`integrations/nbfoundry_stub.py`).** Single source of truth for the placeholder dict, shared by the resolver's `stub` path and the retained `NbfoundryStub`.
|
|
90
|
+
|
|
91
|
+
### Changed
|
|
92
|
+
|
|
93
|
+
- **Resolver owns the `status` switch.** `status: stub` emits `stub_exercise()` directly — no provider call, no nbfoundry import, so an all-stub curriculum never imports nbfoundry. `status: ready`/default compiles via the provider and fails loud on a bad ref. The default exercise provider is now `NbfoundryProvider` (was `NbfoundryStub`).
|
|
94
|
+
- **`NbfoundryStub` demoted to a test double / "no-notebooks" injectable** — it is no longer the default provider and is never selected by the `status` switch.
|
|
95
|
+
- **Provider protocols are now `@runtime_checkable`**, enabling runtime `isinstance` conformance assertions alongside the mypy-checked contract.
|
|
96
|
+
|
|
97
|
+
### Verified
|
|
98
|
+
|
|
99
|
+
- `pyve test` → 437 passed (was 413; +24 new tests across `test_nbfoundry.py`, `test_nbfoundry_stub.py`, `test_schema_v1.py`, `test_resolver.py`). No regressions.
|
|
100
|
+
- `ruff check` clean; `mypy src/` clean. The protocol-match contract is enforced in CI via the resolver's `exercise_provider = NbfoundryProvider()` default assignment into an `ExerciseProvider`-typed slot.
|
|
101
|
+
|
|
10
102
|
## [0.79.3] - 2026-06-15
|
|
11
103
|
|
|
12
104
|
Fix `learningfoundry preview` appearing to hang silently during the `pnpm install` step and then reporting an empty failure on Ctrl-C. The install step captured pnpm's output (with no timeout) while leaving stdin attached, so progress and any interactive prompt were hidden — a prompting or slow install looked like a silent hang — and a non-zero exit produced `` `pnpm install` failed in `dist`: `` with nothing after the colon. Fixed via Story K.a.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learningfoundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.83.0
|
|
4
4
|
Summary: A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pointmatic/learningfoundry
|
|
6
6
|
Project-URL: Repository, https://github.com/pointmatic/learningfoundry
|
|
@@ -23,6 +23,8 @@ Requires-Python: >=3.12
|
|
|
23
23
|
Requires-Dist: click>=8.1
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
25
|
Requires-Dist: pyyaml>=6.0.3
|
|
26
|
+
Provides-Extra: nbfoundry
|
|
27
|
+
Requires-Dist: nbfoundry>=0.1; extra == 'nbfoundry'
|
|
26
28
|
Provides-Extra: quizazz
|
|
27
29
|
Requires-Dist: quizazz>=0.1; extra == 'quizazz'
|
|
28
30
|
Description-Content-Type: text/markdown
|
|
@@ -63,7 +65,7 @@ A curriculum engine that turns a YAML curriculum definition into a deployable Sv
|
|
|
63
65
|
- **Text** — Markdown content rendered in the browser
|
|
64
66
|
- **Video** — YouTube embeds
|
|
65
67
|
- **Assessment** — Interactive assessments via [quizazz](https://github.com/pointmatic/quizazz) (optional)
|
|
66
|
-
- **Exercise** —
|
|
68
|
+
- **Exercise** — Scaffolded model-training exercises via [nbfoundry](https://github.com/pointmatic/nbfoundry) (optional; `status: stub` for not-yet-built)
|
|
67
69
|
- **Visualization** — D3-based charts via d3foundry (stub provided)
|
|
68
70
|
|
|
69
71
|
Learner progress is persisted locally in SQLite (via sql.js) — no backend required.
|
|
@@ -76,10 +78,12 @@ Learner progress is persisted locally in SQLite (via sql.js) — no backend requ
|
|
|
76
78
|
pip install learningfoundry
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
**With optional
|
|
81
|
+
**With optional integration extras:**
|
|
80
82
|
|
|
81
83
|
```bash
|
|
82
|
-
pip install "learningfoundry[quizazz]"
|
|
84
|
+
pip install "learningfoundry[quizazz]" # assessments
|
|
85
|
+
pip install "learningfoundry[nbfoundry]" # exercises
|
|
86
|
+
pip install "learningfoundry[quizazz,nbfoundry]"
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
**Requirements:**
|
|
@@ -207,6 +211,27 @@ This serves the SvelteKit project from source via Vite's dev server; it does **n
|
|
|
207
211
|
|
|
208
212
|
---
|
|
209
213
|
|
|
214
|
+
### `learningfoundry launch` / `learningfoundry stop`
|
|
215
|
+
|
|
216
|
+
Run a `ready` exercise's marimo notebook locally. These are **learner-side** commands — run from inside the generated app (where `build` writes `exercises-manifest.json`), not from the author's repo. The generated app is static and a browser page can't spawn a process, so the exercise's banner asks the learner to copy and run `learningfoundry launch <id>`.
|
|
217
|
+
|
|
218
|
+
```
|
|
219
|
+
Usage: learningfoundry launch [OPTIONS] EXERCISE_ID
|
|
220
|
+
learningfoundry stop [OPTIONS] [EXERCISE_ID]
|
|
221
|
+
|
|
222
|
+
Options:
|
|
223
|
+
--dir PATH Directory holding exercises-manifest.json (the
|
|
224
|
+
generated app's root). [default: .]
|
|
225
|
+
--log-level LEVEL Logging verbosity. [default: INFO]
|
|
226
|
+
--help Show this message and exit.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`launch <id>` resolves the notebook path / `mode` / port from the manifest, checks the port, then spawns `marimo edit|run <notebook> --headless -p <port> --no-token` detached (so it outlives the CLI) and prints the local URL. If the port is already held by a **launch-owned** marimo it offers to replace it; a **foreign** process on the port is never killed. `stop <id>` tears down that exercise's notebook; bare `stop` stops every launch-owned notebook.
|
|
230
|
+
|
|
231
|
+
> **`marimo` is a learner-runtime dependency**, not a learningfoundry dependency — `launch` finds it on `PATH` (it is never imported by the build). Install it alongside the exercise's other run requirements (e.g. `pip install marimo torch`); the banner lists them. A missing `marimo` yields an install hint, not a traceback.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
210
235
|
## Curriculum YAML Format
|
|
211
236
|
|
|
212
237
|
```yaml
|
|
@@ -266,10 +291,13 @@ curriculum:
|
|
|
266
291
|
source: quizazz
|
|
267
292
|
ref: assessments/mod-01-assessment.yml
|
|
268
293
|
|
|
269
|
-
# Exercise block — requires nbfoundry
|
|
294
|
+
# Exercise block — `ready` requires learningfoundry[nbfoundry];
|
|
295
|
+
# `status: stub` renders a placeholder with no install needed.
|
|
296
|
+
# See "Authoring nbfoundry exercises" below for the full author flow.
|
|
270
297
|
- type: exercise
|
|
271
298
|
source: nbfoundry
|
|
272
299
|
ref: exercises/mod-01-exercise.yml
|
|
300
|
+
status: stub
|
|
273
301
|
|
|
274
302
|
# Visualization block — requires d3foundry (stub included)
|
|
275
303
|
- type: visualization
|
|
@@ -692,6 +720,61 @@ questions:
|
|
|
692
720
|
- **`learningfoundry[quizazz]` is an optional extra.** Plain `pip install learningfoundry` does not pull in quizazz; running `learningfoundry build` on a curriculum that references `source: quizazz` will fail with an `ImportError`. Install the extra explicitly.
|
|
693
721
|
- **`<QuizBlock>` is a vendor component name** — preserved at the vendor boundary. A future "consistency rename" pass that tried to rename it to `<AssessmentBlock>` (learningfoundry's wrapper component) would break the integration silently. See the "Vendor terminology stops at the vendor boundary" note in [project-essentials.md](docs/specs/project-essentials.md).
|
|
694
722
|
|
|
723
|
+
### Authoring nbfoundry exercises
|
|
724
|
+
|
|
725
|
+
[nbfoundry](https://github.com/pointmatic/nbfoundry) is the exercise provider — scaffolded, model-training exercises. Under the hood (Option C) nbfoundry compiles each exercise to a **runnable marimo notebook**; learningfoundry stages it at build time and the lesson renders a **launch banner**. The learner copies the `learningfoundry launch <id>` command, runs it to start the notebook locally, opens it in a new tab, works through the cells, and clicks **Mark as Complete**. The exercise runs in the learner's own Python environment, so PyTorch and other heavy dependencies stay out of the build and the browser. In-browser (Marimo-WASM) execution and graded submission are deferred to future versions; the authored YAML does not change when they land.
|
|
726
|
+
|
|
727
|
+
**What you need.**
|
|
728
|
+
|
|
729
|
+
- *Build side:* `pip install learningfoundry[nbfoundry]` installs the [`nbfoundry` PyPI package](https://pypi.org/project/nbfoundry/) so `learningfoundry build` can compile exercise YAML into the notebook. Like `[quizazz]`, it is an optional extra — plain `pip install learningfoundry` does not pull it in, and a `ready` exercise referencing `source: nbfoundry` without it fails the build with an `ImportError` and an install hint.
|
|
730
|
+
- *Learner side:* running the exercise needs **`marimo`** (plus the exercise's own dependencies, e.g. `torch`) on the learner's `PATH`. `learningfoundry launch` finds `marimo` there — it is a **learner-runtime** dependency, never imported by the build. The banner lists the exercise's prerequisites.
|
|
731
|
+
|
|
732
|
+
**Referencing an exercise.** Add an `exercise` content block to a lesson:
|
|
733
|
+
|
|
734
|
+
```yaml
|
|
735
|
+
content_blocks:
|
|
736
|
+
- type: exercise
|
|
737
|
+
source: nbfoundry
|
|
738
|
+
ref: exercises/mod-01/cnn-classifier.yml # located under --base-dir
|
|
739
|
+
status: ready # "ready" (default) | "stub"
|
|
740
|
+
mode: edit # "edit" (default) | "run"
|
|
741
|
+
# id: cnn-classifier # optional; see below
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
- **`status: ready`** (the default) compiles the exercise to a notebook at build time. A typo'd `ref` fails the build **loud** rather than silently degrading to a placeholder.
|
|
745
|
+
- **`status: stub`** renders the "nbfoundry integration pending" placeholder card — the explicit "not built yet" opt-in while you scaffold the curriculum. No nbfoundry call, no install needed for stub-only curricula.
|
|
746
|
+
- **`mode`** chooses how `learningfoundry launch` serves the notebook: **`edit`** (the default) opens marimo's editable notebook (the learner writes code into the scaffold); **`run`** opens it as a read-only app. It is the same notebook either way — the choice is pedagogical.
|
|
747
|
+
|
|
748
|
+
**How `id` works.** Every exercise has an `id` that is two things at once: the **notebook namespace** (the build writes the marimo notebook to `exercises/<id>/<id>.py` and indexes it in `exercises-manifest.json`, which `learningfoundry launch <id>` reads) and the **progress key** (`exerciseRef`) recorded in the in-browser database. When you omit `id:`, it is auto-derived from the `ref` filename **stem** (`exercises/mod-01/cnn-classifier.yml` → `cnn-classifier`). The `id` must be **unique across the whole curriculum** — not just within a module.
|
|
749
|
+
|
|
750
|
+
**Organizing source freely.** The `id` namespaces the *output notebook*; it does **not** constrain where you organize *source* content. Lay out exercise YAML however you like under `--base-dir` — nbfoundry locates it via the relative `ref`:
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
# Source (organize however you like): # Build output (id-namespaced notebook):
|
|
754
|
+
exercises/mod-01/cnn-classifier.yml dist/exercises/cnn-classifier/cnn-classifier.py
|
|
755
|
+
exercises/mod-02/intro.yml dist/exercises/intro/intro.py
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
The notebook renders its own plots, tables, and metrics when it runs — there is no separate image-staging step (that was the retired Option-B static-display path).
|
|
759
|
+
|
|
760
|
+
**The stem-collision case.** Because the auto-derived `id` is just the filename stem, two exercises whose `ref` files share a stem collide — even in different modules:
|
|
761
|
+
|
|
762
|
+
```yaml
|
|
763
|
+
# mod-01 lesson: ref: exercises/mod-01/intro.yml → id "intro"
|
|
764
|
+
# mod-02 lesson: ref: exercises/mod-02/intro.yml → id "intro" ❌ build error
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
This is a **loud build-time error**, not a silent overwrite (two exercises would otherwise share one notebook path + progress key). Fix it by setting an explicit `id:` on at least one:
|
|
768
|
+
|
|
769
|
+
```yaml
|
|
770
|
+
- type: exercise
|
|
771
|
+
source: nbfoundry
|
|
772
|
+
ref: exercises/mod-02/intro.yml
|
|
773
|
+
id: mod-02-intro # explicit, curriculum-unique
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
**Why a stable `id` matters.** Set an explicit `id:` when you expect to **reorganize source content** later. Because the `id` is both the notebook namespace and the progress key, keeping it stable while you move or rename the underlying `ref` file preserves both the `learningfoundry launch <id>` command *and* learners' recorded completion — whereas relying on the auto-derived stem means a rename silently changes the `id` and orphans prior progress.
|
|
777
|
+
|
|
695
778
|
### Assessments
|
|
696
779
|
|
|
697
780
|
Each module declares an `assessments[]` array; each entry carries:
|
|
@@ -34,7 +34,7 @@ A curriculum engine that turns a YAML curriculum definition into a deployable Sv
|
|
|
34
34
|
- **Text** — Markdown content rendered in the browser
|
|
35
35
|
- **Video** — YouTube embeds
|
|
36
36
|
- **Assessment** — Interactive assessments via [quizazz](https://github.com/pointmatic/quizazz) (optional)
|
|
37
|
-
- **Exercise** —
|
|
37
|
+
- **Exercise** — Scaffolded model-training exercises via [nbfoundry](https://github.com/pointmatic/nbfoundry) (optional; `status: stub` for not-yet-built)
|
|
38
38
|
- **Visualization** — D3-based charts via d3foundry (stub provided)
|
|
39
39
|
|
|
40
40
|
Learner progress is persisted locally in SQLite (via sql.js) — no backend required.
|
|
@@ -47,10 +47,12 @@ Learner progress is persisted locally in SQLite (via sql.js) — no backend requ
|
|
|
47
47
|
pip install learningfoundry
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**With optional
|
|
50
|
+
**With optional integration extras:**
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
pip install "learningfoundry[quizazz]"
|
|
53
|
+
pip install "learningfoundry[quizazz]" # assessments
|
|
54
|
+
pip install "learningfoundry[nbfoundry]" # exercises
|
|
55
|
+
pip install "learningfoundry[quizazz,nbfoundry]"
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
**Requirements:**
|
|
@@ -178,6 +180,27 @@ This serves the SvelteKit project from source via Vite's dev server; it does **n
|
|
|
178
180
|
|
|
179
181
|
---
|
|
180
182
|
|
|
183
|
+
### `learningfoundry launch` / `learningfoundry stop`
|
|
184
|
+
|
|
185
|
+
Run a `ready` exercise's marimo notebook locally. These are **learner-side** commands — run from inside the generated app (where `build` writes `exercises-manifest.json`), not from the author's repo. The generated app is static and a browser page can't spawn a process, so the exercise's banner asks the learner to copy and run `learningfoundry launch <id>`.
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
Usage: learningfoundry launch [OPTIONS] EXERCISE_ID
|
|
189
|
+
learningfoundry stop [OPTIONS] [EXERCISE_ID]
|
|
190
|
+
|
|
191
|
+
Options:
|
|
192
|
+
--dir PATH Directory holding exercises-manifest.json (the
|
|
193
|
+
generated app's root). [default: .]
|
|
194
|
+
--log-level LEVEL Logging verbosity. [default: INFO]
|
|
195
|
+
--help Show this message and exit.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`launch <id>` resolves the notebook path / `mode` / port from the manifest, checks the port, then spawns `marimo edit|run <notebook> --headless -p <port> --no-token` detached (so it outlives the CLI) and prints the local URL. If the port is already held by a **launch-owned** marimo it offers to replace it; a **foreign** process on the port is never killed. `stop <id>` tears down that exercise's notebook; bare `stop` stops every launch-owned notebook.
|
|
199
|
+
|
|
200
|
+
> **`marimo` is a learner-runtime dependency**, not a learningfoundry dependency — `launch` finds it on `PATH` (it is never imported by the build). Install it alongside the exercise's other run requirements (e.g. `pip install marimo torch`); the banner lists them. A missing `marimo` yields an install hint, not a traceback.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
181
204
|
## Curriculum YAML Format
|
|
182
205
|
|
|
183
206
|
```yaml
|
|
@@ -237,10 +260,13 @@ curriculum:
|
|
|
237
260
|
source: quizazz
|
|
238
261
|
ref: assessments/mod-01-assessment.yml
|
|
239
262
|
|
|
240
|
-
# Exercise block — requires nbfoundry
|
|
263
|
+
# Exercise block — `ready` requires learningfoundry[nbfoundry];
|
|
264
|
+
# `status: stub` renders a placeholder with no install needed.
|
|
265
|
+
# See "Authoring nbfoundry exercises" below for the full author flow.
|
|
241
266
|
- type: exercise
|
|
242
267
|
source: nbfoundry
|
|
243
268
|
ref: exercises/mod-01-exercise.yml
|
|
269
|
+
status: stub
|
|
244
270
|
|
|
245
271
|
# Visualization block — requires d3foundry (stub included)
|
|
246
272
|
- type: visualization
|
|
@@ -663,6 +689,61 @@ questions:
|
|
|
663
689
|
- **`learningfoundry[quizazz]` is an optional extra.** Plain `pip install learningfoundry` does not pull in quizazz; running `learningfoundry build` on a curriculum that references `source: quizazz` will fail with an `ImportError`. Install the extra explicitly.
|
|
664
690
|
- **`<QuizBlock>` is a vendor component name** — preserved at the vendor boundary. A future "consistency rename" pass that tried to rename it to `<AssessmentBlock>` (learningfoundry's wrapper component) would break the integration silently. See the "Vendor terminology stops at the vendor boundary" note in [project-essentials.md](docs/specs/project-essentials.md).
|
|
665
691
|
|
|
692
|
+
### Authoring nbfoundry exercises
|
|
693
|
+
|
|
694
|
+
[nbfoundry](https://github.com/pointmatic/nbfoundry) is the exercise provider — scaffolded, model-training exercises. Under the hood (Option C) nbfoundry compiles each exercise to a **runnable marimo notebook**; learningfoundry stages it at build time and the lesson renders a **launch banner**. The learner copies the `learningfoundry launch <id>` command, runs it to start the notebook locally, opens it in a new tab, works through the cells, and clicks **Mark as Complete**. The exercise runs in the learner's own Python environment, so PyTorch and other heavy dependencies stay out of the build and the browser. In-browser (Marimo-WASM) execution and graded submission are deferred to future versions; the authored YAML does not change when they land.
|
|
695
|
+
|
|
696
|
+
**What you need.**
|
|
697
|
+
|
|
698
|
+
- *Build side:* `pip install learningfoundry[nbfoundry]` installs the [`nbfoundry` PyPI package](https://pypi.org/project/nbfoundry/) so `learningfoundry build` can compile exercise YAML into the notebook. Like `[quizazz]`, it is an optional extra — plain `pip install learningfoundry` does not pull it in, and a `ready` exercise referencing `source: nbfoundry` without it fails the build with an `ImportError` and an install hint.
|
|
699
|
+
- *Learner side:* running the exercise needs **`marimo`** (plus the exercise's own dependencies, e.g. `torch`) on the learner's `PATH`. `learningfoundry launch` finds `marimo` there — it is a **learner-runtime** dependency, never imported by the build. The banner lists the exercise's prerequisites.
|
|
700
|
+
|
|
701
|
+
**Referencing an exercise.** Add an `exercise` content block to a lesson:
|
|
702
|
+
|
|
703
|
+
```yaml
|
|
704
|
+
content_blocks:
|
|
705
|
+
- type: exercise
|
|
706
|
+
source: nbfoundry
|
|
707
|
+
ref: exercises/mod-01/cnn-classifier.yml # located under --base-dir
|
|
708
|
+
status: ready # "ready" (default) | "stub"
|
|
709
|
+
mode: edit # "edit" (default) | "run"
|
|
710
|
+
# id: cnn-classifier # optional; see below
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
- **`status: ready`** (the default) compiles the exercise to a notebook at build time. A typo'd `ref` fails the build **loud** rather than silently degrading to a placeholder.
|
|
714
|
+
- **`status: stub`** renders the "nbfoundry integration pending" placeholder card — the explicit "not built yet" opt-in while you scaffold the curriculum. No nbfoundry call, no install needed for stub-only curricula.
|
|
715
|
+
- **`mode`** chooses how `learningfoundry launch` serves the notebook: **`edit`** (the default) opens marimo's editable notebook (the learner writes code into the scaffold); **`run`** opens it as a read-only app. It is the same notebook either way — the choice is pedagogical.
|
|
716
|
+
|
|
717
|
+
**How `id` works.** Every exercise has an `id` that is two things at once: the **notebook namespace** (the build writes the marimo notebook to `exercises/<id>/<id>.py` and indexes it in `exercises-manifest.json`, which `learningfoundry launch <id>` reads) and the **progress key** (`exerciseRef`) recorded in the in-browser database. When you omit `id:`, it is auto-derived from the `ref` filename **stem** (`exercises/mod-01/cnn-classifier.yml` → `cnn-classifier`). The `id` must be **unique across the whole curriculum** — not just within a module.
|
|
718
|
+
|
|
719
|
+
**Organizing source freely.** The `id` namespaces the *output notebook*; it does **not** constrain where you organize *source* content. Lay out exercise YAML however you like under `--base-dir` — nbfoundry locates it via the relative `ref`:
|
|
720
|
+
|
|
721
|
+
```
|
|
722
|
+
# Source (organize however you like): # Build output (id-namespaced notebook):
|
|
723
|
+
exercises/mod-01/cnn-classifier.yml dist/exercises/cnn-classifier/cnn-classifier.py
|
|
724
|
+
exercises/mod-02/intro.yml dist/exercises/intro/intro.py
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
The notebook renders its own plots, tables, and metrics when it runs — there is no separate image-staging step (that was the retired Option-B static-display path).
|
|
728
|
+
|
|
729
|
+
**The stem-collision case.** Because the auto-derived `id` is just the filename stem, two exercises whose `ref` files share a stem collide — even in different modules:
|
|
730
|
+
|
|
731
|
+
```yaml
|
|
732
|
+
# mod-01 lesson: ref: exercises/mod-01/intro.yml → id "intro"
|
|
733
|
+
# mod-02 lesson: ref: exercises/mod-02/intro.yml → id "intro" ❌ build error
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
This is a **loud build-time error**, not a silent overwrite (two exercises would otherwise share one notebook path + progress key). Fix it by setting an explicit `id:` on at least one:
|
|
737
|
+
|
|
738
|
+
```yaml
|
|
739
|
+
- type: exercise
|
|
740
|
+
source: nbfoundry
|
|
741
|
+
ref: exercises/mod-02/intro.yml
|
|
742
|
+
id: mod-02-intro # explicit, curriculum-unique
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
**Why a stable `id` matters.** Set an explicit `id:` when you expect to **reorganize source content** later. Because the `id` is both the notebook namespace and the progress key, keeping it stable while you move or rename the underlying `ref` file preserves both the `learningfoundry launch <id>` command *and* learners' recorded completion — whereas relying on the auto-derived stem means a rename silently changes the `id` and orphans prior progress.
|
|
746
|
+
|
|
666
747
|
### Assessments
|
|
667
748
|
|
|
668
749
|
Each module declares an `assessments[]` array; each entry carries:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "learningfoundry"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.83.0"
|
|
8
8
|
description = "A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -34,6 +34,9 @@ dependencies = [
|
|
|
34
34
|
quizazz = [
|
|
35
35
|
"quizazz>=0.1",
|
|
36
36
|
]
|
|
37
|
+
nbfoundry = [
|
|
38
|
+
"nbfoundry>=0.1",
|
|
39
|
+
]
|
|
37
40
|
|
|
38
41
|
[project.urls]
|
|
39
42
|
Homepage = "https://github.com/pointmatic/learningfoundry"
|
|
@@ -71,6 +74,10 @@ python_version = "3.12"
|
|
|
71
74
|
module = "quizazz"
|
|
72
75
|
ignore_missing_imports = true
|
|
73
76
|
|
|
77
|
+
[[tool.mypy.overrides]]
|
|
78
|
+
module = "nbfoundry"
|
|
79
|
+
ignore_missing_imports = true
|
|
80
|
+
|
|
74
81
|
[tool.coverage.run]
|
|
75
82
|
source = ["src/learningfoundry"]
|
|
76
83
|
omit = ["*/sveltekit_template/*"]
|
|
@@ -85,11 +85,20 @@ _PASSTHROUGH_PREFIXES: tuple[str, ...] = (
|
|
|
85
85
|
class Asset:
|
|
86
86
|
"""A single asset that must be copied into the generated project.
|
|
87
87
|
|
|
88
|
+
``dest_relative`` is the dedup key wherever Assets aggregate (the
|
|
89
|
+
resolver's ``assets_by_dest`` map): two records with the same
|
|
90
|
+
destination collapse to one copy. This holds both for the content-hashed
|
|
91
|
+
markdown-image paths produced here (``content/<sha256[:12]>/<basename>``,
|
|
92
|
+
where a matching hash implies matching bytes) and for the **non-hashed**
|
|
93
|
+
exercise-asset paths the resolver emits (``exercises/<id>/<path>``,
|
|
94
|
+
Story K.e) — same key, same dedup semantics.
|
|
95
|
+
|
|
88
96
|
Attributes:
|
|
89
97
|
source: Absolute path to the source file on disk.
|
|
90
98
|
dest_relative: Destination path relative to the generated project's
|
|
91
|
-
``static/`` directory (e.g. ``"content/abc123def456/diagram.png"``
|
|
92
|
-
|
|
99
|
+
``static/`` directory (e.g. ``"content/abc123def456/diagram.png"``
|
|
100
|
+
or ``"exercises/mod-01-exercise-01/data/sample.csv"``). Always
|
|
101
|
+
uses forward slashes — this becomes a URL fragment too.
|
|
93
102
|
"""
|
|
94
103
|
|
|
95
104
|
source: Path
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Copyright 2026 Pointmatic
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
+
import shutil
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ from learningfoundry.exceptions import (
|
|
|
13
14
|
CurriculumValidationError,
|
|
14
15
|
CurriculumVersionError,
|
|
15
16
|
GenerationError,
|
|
17
|
+
LaunchError,
|
|
16
18
|
SchemaExtensionError,
|
|
17
19
|
)
|
|
18
20
|
from learningfoundry.logging_config import setup_logging as _setup_logging
|
|
@@ -24,6 +26,7 @@ EXIT_VALIDATION = 1
|
|
|
24
26
|
EXIT_RESOLUTION = 2
|
|
25
27
|
EXIT_GENERATION = 3
|
|
26
28
|
EXIT_CONFIG = 4
|
|
29
|
+
EXIT_RUNTIME = 5
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
# ---------------------------------------------------------------------------
|
|
@@ -270,3 +273,114 @@ def preview(
|
|
|
270
273
|
sys.exit(EXIT_CONFIG)
|
|
271
274
|
|
|
272
275
|
click.echo(f"Preview server started at http://localhost:{port}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# launch
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
_launch_dir_option = click.option(
|
|
283
|
+
"--dir",
|
|
284
|
+
"launch_dir",
|
|
285
|
+
type=click.Path(file_okay=False, path_type=Path),
|
|
286
|
+
default=Path("."),
|
|
287
|
+
show_default=True,
|
|
288
|
+
help=(
|
|
289
|
+
"Directory holding `exercises-manifest.json` (the generated app's "
|
|
290
|
+
"root). Defaults to the current directory."
|
|
291
|
+
),
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@main.command()
|
|
296
|
+
@click.argument("exercise_id")
|
|
297
|
+
@_launch_dir_option
|
|
298
|
+
@_log_level_option
|
|
299
|
+
def launch(exercise_id: str, launch_dir: Path, log_level: str) -> None:
|
|
300
|
+
"""Launch an exercise's marimo notebook locally."""
|
|
301
|
+
_setup_logging(level=log_level)
|
|
302
|
+
|
|
303
|
+
from learningfoundry import launch as _launch
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
spec = _launch.resolve_launch_spec(launch_dir, exercise_id)
|
|
307
|
+
except LaunchError as exc:
|
|
308
|
+
click.echo(f"Launch error: {exc}", err=True)
|
|
309
|
+
sys.exit(EXIT_VALIDATION)
|
|
310
|
+
|
|
311
|
+
if shutil.which("marimo") is None:
|
|
312
|
+
click.echo(
|
|
313
|
+
"marimo not found on PATH. It is a learner-runtime dependency — "
|
|
314
|
+
"install it (e.g. `pip install marimo`) to run exercises.",
|
|
315
|
+
err=True,
|
|
316
|
+
)
|
|
317
|
+
sys.exit(EXIT_RUNTIME)
|
|
318
|
+
|
|
319
|
+
status = _launch.classify_port(launch_dir, spec.port)
|
|
320
|
+
if status == "foreign":
|
|
321
|
+
click.echo(
|
|
322
|
+
f"Port {spec.port} is in use by another process. Refusing to "
|
|
323
|
+
"kill it — free the port (or stop that process) and retry.",
|
|
324
|
+
err=True,
|
|
325
|
+
)
|
|
326
|
+
sys.exit(EXIT_RUNTIME)
|
|
327
|
+
if status == "ours":
|
|
328
|
+
if not click.confirm(
|
|
329
|
+
f"An exercise is already running on port {spec.port}. Replace it?"
|
|
330
|
+
):
|
|
331
|
+
click.echo("Left the running exercise in place.")
|
|
332
|
+
return
|
|
333
|
+
_launch.stop_launch_on_port(launch_dir, spec.port)
|
|
334
|
+
|
|
335
|
+
pid = _launch.spawn_detached(_launch.marimo_argv(spec), launch_dir)
|
|
336
|
+
_launch.write_pidfile(
|
|
337
|
+
launch_dir,
|
|
338
|
+
_launch.PidfileEntry(
|
|
339
|
+
pid=pid,
|
|
340
|
+
exercise_id=spec.id,
|
|
341
|
+
port=spec.port,
|
|
342
|
+
mode=spec.mode,
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
click.echo(
|
|
346
|
+
f"Launched `{spec.id}` ({spec.mode}) → http://localhost:{spec.port}"
|
|
347
|
+
)
|
|
348
|
+
click.echo(f"Stop it with: learningfoundry stop {spec.id}")
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# stop
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
@main.command()
|
|
356
|
+
@click.argument("exercise_id", required=False)
|
|
357
|
+
@_launch_dir_option
|
|
358
|
+
@_log_level_option
|
|
359
|
+
def stop(exercise_id: str | None, launch_dir: Path, log_level: str) -> None:
|
|
360
|
+
"""Stop a launch-owned marimo notebook (all of them if no id is given)."""
|
|
361
|
+
_setup_logging(level=log_level)
|
|
362
|
+
|
|
363
|
+
from learningfoundry import launch as _launch
|
|
364
|
+
|
|
365
|
+
if exercise_id is not None:
|
|
366
|
+
try:
|
|
367
|
+
spec = _launch.resolve_launch_spec(launch_dir, exercise_id)
|
|
368
|
+
except LaunchError as exc:
|
|
369
|
+
click.echo(f"Stop error: {exc}", err=True)
|
|
370
|
+
sys.exit(EXIT_VALIDATION)
|
|
371
|
+
stopped = _launch.stop_launch_on_port(launch_dir, spec.port)
|
|
372
|
+
if stopped is not None:
|
|
373
|
+
click.echo(f"Stopped `{stopped.exercise_id}` (port {stopped.port}).")
|
|
374
|
+
else:
|
|
375
|
+
click.echo(f"No running exercise found for `{exercise_id}`.")
|
|
376
|
+
return
|
|
377
|
+
|
|
378
|
+
# No id → stop every launch-owned marimo.
|
|
379
|
+
ports = _launch.launched_ports(launch_dir)
|
|
380
|
+
if not ports:
|
|
381
|
+
click.echo("No running exercises.")
|
|
382
|
+
return
|
|
383
|
+
for port in ports:
|
|
384
|
+
stopped = _launch.stop_launch_on_port(launch_dir, port)
|
|
385
|
+
if stopped is not None:
|
|
386
|
+
click.echo(f"Stopped `{stopped.exercise_id}` (port {stopped.port}).")
|
|
@@ -43,3 +43,28 @@ class GenerationError(LearningFoundryError):
|
|
|
43
43
|
class SchemaExtensionError(LearningFoundryError):
|
|
44
44
|
"""Project schema-extensions file is missing, malformed, or declares an
|
|
45
45
|
unsupported field type / shape (Story J.h)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LaunchError(LearningFoundryError):
|
|
49
|
+
"""Base for `learningfoundry launch` / `stop` runtime errors (Story K.i)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ManifestNotFoundError(LaunchError):
|
|
53
|
+
"""The `exercises-manifest.json` sidecar was not found in the launch dir.
|
|
54
|
+
|
|
55
|
+
The learner runs `launch`/`stop` from inside the generated app's root,
|
|
56
|
+
where `build` writes the manifest; a missing file usually means the wrong
|
|
57
|
+
directory or an un-built app.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class UnknownExerciseError(LaunchError):
|
|
62
|
+
"""The requested exercise id is absent from the manifest.
|
|
63
|
+
|
|
64
|
+
The message lists the ids the learner can actually launch.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ManifestError(LaunchError):
|
|
69
|
+
"""The `exercises-manifest.json` sidecar is malformed (bad JSON, not an
|
|
70
|
+
object, or an entry missing required fields)."""
|