learningfoundry 0.82.0__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.82.0 → learningfoundry-0.83.0}/CHANGELOG.md +29 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/PKG-INFO +39 -12
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/README.md +38 -11
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/pyproject.toml +1 -1
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/__init__.py +1 -1
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/cli.py +114 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/exceptions.py +25 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/generator.py +60 -15
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/nbfoundry_stub.py +1 -4
- learningfoundry-0.83.0/src/learningfoundry/launch.py +349 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/resolver.py +62 -15
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/schema_v1.py +5 -0
- 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.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.test.ts +5 -3
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.test.ts +42 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +20 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +11 -23
- learningfoundry-0.82.0/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -128
- learningfoundry-0.82.0/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.test.ts +0 -135
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/.gitignore +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/LICENSE +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/docs/specs/nbfoundry/README.md +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/docs/specs/pyve/README.md +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/docs/specs/quizazz/README.md +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/__main__.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/asset_resolver.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/config.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/directives.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/__init__.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/nbfoundry.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/protocols.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/quizazz.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/logging_config.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/parser.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/pipeline.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/py.typed +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/schema_extensions.py +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/README.md +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/finish.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/fixtures/curriculum.json +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/global-teardown.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/lifecycle.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/navigation.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/progress.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/reset.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/text-block-bottom.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/e2e/video.spec.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/package.json +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/playwright.config.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/pnpm-workspace.yaml +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/app.css +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/AssessmentBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/AssessmentBlock.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/LockedLessonPlaceholder.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.observer.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/lesson-view.helpers.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/mount.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.helpers.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/assessment-passed.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/assessment-passed.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/duration.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/duration.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown-directives.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/lib/utils/viewport-completion.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/page.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/assessment/[id]/+page.svelte +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/assessment/[id]/page.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.helpers.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/src/routes/layout.test.ts +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/test-results/.last-run.json +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
- {learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/sveltekit_template/vite.config.ts +0 -0
|
@@ -7,6 +7,35 @@ 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
|
+
|
|
10
39
|
## [0.82.0] - 2026-06-18
|
|
11
40
|
|
|
12
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`.
|
|
@@ -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
|
|
@@ -211,6 +211,27 @@ This serves the SvelteKit project from source via Vite's dev server; it does **n
|
|
|
211
211
|
|
|
212
212
|
---
|
|
213
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
|
+
|
|
214
235
|
## Curriculum YAML Format
|
|
215
236
|
|
|
216
237
|
```yaml
|
|
@@ -701,9 +722,12 @@ questions:
|
|
|
701
722
|
|
|
702
723
|
### Authoring nbfoundry exercises
|
|
703
724
|
|
|
704
|
-
[nbfoundry](https://github.com/pointmatic/nbfoundry) is the exercise provider — scaffolded, model-training exercises
|
|
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.**
|
|
705
728
|
|
|
706
|
-
|
|
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.
|
|
707
731
|
|
|
708
732
|
**Referencing an exercise.** Add an `exercise` content block to a lesson:
|
|
709
733
|
|
|
@@ -713,23 +737,26 @@ content_blocks:
|
|
|
713
737
|
source: nbfoundry
|
|
714
738
|
ref: exercises/mod-01/cnn-classifier.yml # located under --base-dir
|
|
715
739
|
status: ready # "ready" (default) | "stub"
|
|
740
|
+
mode: edit # "edit" (default) | "run"
|
|
716
741
|
# id: cnn-classifier # optional; see below
|
|
717
742
|
```
|
|
718
743
|
|
|
719
|
-
- **`status: ready`** (the default) compiles the exercise
|
|
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.
|
|
720
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.
|
|
721
747
|
|
|
722
|
-
**How `id` works.** Every exercise has an `id` that is two things at once: the **
|
|
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.
|
|
723
749
|
|
|
724
|
-
**Organizing source freely
|
|
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`:
|
|
725
751
|
|
|
726
752
|
```
|
|
727
|
-
# Source (organize however you like):
|
|
728
|
-
exercises/mod-01/cnn-classifier.yml
|
|
729
|
-
exercises/mod-
|
|
730
|
-
exercises/shared/sample.csv
|
|
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
|
|
731
756
|
```
|
|
732
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
|
+
|
|
733
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:
|
|
734
761
|
|
|
735
762
|
```yaml
|
|
@@ -737,7 +764,7 @@ exercises/shared/sample.csv
|
|
|
737
764
|
# mod-02 lesson: ref: exercises/mod-02/intro.yml → id "intro" ❌ build error
|
|
738
765
|
```
|
|
739
766
|
|
|
740
|
-
This is a **loud build-time error**, not a silent overwrite (two exercises would otherwise share one
|
|
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:
|
|
741
768
|
|
|
742
769
|
```yaml
|
|
743
770
|
- type: exercise
|
|
@@ -746,7 +773,7 @@ This is a **loud build-time error**, not a silent overwrite (two exercises would
|
|
|
746
773
|
id: mod-02-intro # explicit, curriculum-unique
|
|
747
774
|
```
|
|
748
775
|
|
|
749
|
-
**Why a stable `id` matters.** Set an explicit `id:` when you expect to **reorganize source content** later. Because the `id` is both the
|
|
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.
|
|
750
777
|
|
|
751
778
|
### Assessments
|
|
752
779
|
|
|
@@ -180,6 +180,27 @@ This serves the SvelteKit project from source via Vite's dev server; it does **n
|
|
|
180
180
|
|
|
181
181
|
---
|
|
182
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
|
+
|
|
183
204
|
## Curriculum YAML Format
|
|
184
205
|
|
|
185
206
|
```yaml
|
|
@@ -670,9 +691,12 @@ questions:
|
|
|
670
691
|
|
|
671
692
|
### Authoring nbfoundry exercises
|
|
672
693
|
|
|
673
|
-
[nbfoundry](https://github.com/pointmatic/nbfoundry) is the exercise provider — scaffolded, model-training exercises
|
|
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.**
|
|
674
697
|
|
|
675
|
-
|
|
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.
|
|
676
700
|
|
|
677
701
|
**Referencing an exercise.** Add an `exercise` content block to a lesson:
|
|
678
702
|
|
|
@@ -682,23 +706,26 @@ content_blocks:
|
|
|
682
706
|
source: nbfoundry
|
|
683
707
|
ref: exercises/mod-01/cnn-classifier.yml # located under --base-dir
|
|
684
708
|
status: ready # "ready" (default) | "stub"
|
|
709
|
+
mode: edit # "edit" (default) | "run"
|
|
685
710
|
# id: cnn-classifier # optional; see below
|
|
686
711
|
```
|
|
687
712
|
|
|
688
|
-
- **`status: ready`** (the default) compiles the exercise
|
|
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.
|
|
689
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.
|
|
690
716
|
|
|
691
|
-
**How `id` works.** Every exercise has an `id` that is two things at once: the **
|
|
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.
|
|
692
718
|
|
|
693
|
-
**Organizing source freely
|
|
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`:
|
|
694
720
|
|
|
695
721
|
```
|
|
696
|
-
# Source (organize however you like):
|
|
697
|
-
exercises/mod-01/cnn-classifier.yml
|
|
698
|
-
exercises/mod-
|
|
699
|
-
exercises/shared/sample.csv
|
|
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
|
|
700
725
|
```
|
|
701
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
|
+
|
|
702
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:
|
|
703
730
|
|
|
704
731
|
```yaml
|
|
@@ -706,7 +733,7 @@ exercises/shared/sample.csv
|
|
|
706
733
|
# mod-02 lesson: ref: exercises/mod-02/intro.yml → id "intro" ❌ build error
|
|
707
734
|
```
|
|
708
735
|
|
|
709
|
-
This is a **loud build-time error**, not a silent overwrite (two exercises would otherwise share one
|
|
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:
|
|
710
737
|
|
|
711
738
|
```yaml
|
|
712
739
|
- type: exercise
|
|
@@ -715,7 +742,7 @@ This is a **loud build-time error**, not a silent overwrite (two exercises would
|
|
|
715
742
|
id: mod-02-intro # explicit, curriculum-unique
|
|
716
743
|
```
|
|
717
744
|
|
|
718
|
-
**Why a stable `id` matters.** Set an explicit `id:` when you expect to **reorganize source content** later. Because the `id` is both the
|
|
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.
|
|
719
746
|
|
|
720
747
|
### Assessments
|
|
721
748
|
|
|
@@ -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"
|
|
@@ -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)."""
|
|
@@ -27,10 +27,9 @@ _TEMPLATE_DIR = Path(__file__).parent / "sveltekit_template"
|
|
|
27
27
|
# rebuild — keeps `learningfoundry preview` snappy when only markdown
|
|
28
28
|
# text changed and no new images were introduced.
|
|
29
29
|
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
# place.
|
|
30
|
+
# (Marimo exercise notebooks live at `exercises/<id>/<id>.py` — outside the
|
|
31
|
+
# web root — and are regenerated every build by `_write_exercises`, so they
|
|
32
|
+
# are intentionally NOT preserved here.)
|
|
34
33
|
#
|
|
35
34
|
# `static/sql-wasm.wasm` is preserved because it is gitignored and not
|
|
36
35
|
# shipped in the template — `pipeline._ensure_sql_wasm` is the single
|
|
@@ -43,7 +42,6 @@ _PRESERVED_PATHS: tuple[str, ...] = (
|
|
|
43
42
|
"build",
|
|
44
43
|
".svelte-kit",
|
|
45
44
|
"static/content",
|
|
46
|
-
"static/exercises",
|
|
47
45
|
"static/sql-wasm.wasm",
|
|
48
46
|
)
|
|
49
47
|
|
|
@@ -103,6 +101,7 @@ def generate_app(
|
|
|
103
101
|
|
|
104
102
|
_atomic_copy(src, output_dir)
|
|
105
103
|
_copy_assets(resolved, output_dir)
|
|
104
|
+
_write_exercises(resolved, output_dir)
|
|
106
105
|
_write_curriculum_json(resolved, output_dir)
|
|
107
106
|
|
|
108
107
|
logger.info("Generated SvelteKit project at: %s", output_dir)
|
|
@@ -216,9 +215,10 @@ def _compute_total_duration_minutes(resolved: ResolvedCurriculum) -> int | None:
|
|
|
216
215
|
def _write_curriculum_json(resolved: ResolvedCurriculum, output_dir: Path) -> None:
|
|
217
216
|
"""Serialize ResolvedCurriculum to output_dir/static/curriculum.json.
|
|
218
217
|
|
|
219
|
-
The ``assets``
|
|
220
|
-
``Path`` objects
|
|
221
|
-
by the generator's
|
|
218
|
+
The ``assets`` and ``exercises`` lists are intentionally stripped — they
|
|
219
|
+
carry build-output metadata (on-disk ``Path`` objects / notebook source)
|
|
220
|
+
consumed only by the generator's copy/write steps, never by the SvelteKit
|
|
221
|
+
frontend.
|
|
222
222
|
"""
|
|
223
223
|
static_dir = output_dir / "static"
|
|
224
224
|
static_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -227,6 +227,7 @@ def _write_curriculum_json(resolved: ResolvedCurriculum, output_dir: Path) -> No
|
|
|
227
227
|
try:
|
|
228
228
|
data = dataclasses.asdict(resolved)
|
|
229
229
|
data.pop("assets", None)
|
|
230
|
+
data.pop("exercises", None)
|
|
230
231
|
data["total_duration_minutes"] = _compute_total_duration_minutes(resolved)
|
|
231
232
|
curriculum_json.write_text(
|
|
232
233
|
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
@@ -240,17 +241,61 @@ def _write_curriculum_json(resolved: ResolvedCurriculum, output_dir: Path) -> No
|
|
|
240
241
|
logger.debug("Wrote curriculum.json (%d bytes)", curriculum_json.stat().st_size)
|
|
241
242
|
|
|
242
243
|
|
|
244
|
+
def _write_exercises(resolved: ResolvedCurriculum, output_dir: Path) -> None:
|
|
245
|
+
"""Write each ``ready`` exercise's marimo notebook to its runnable path and
|
|
246
|
+
emit the ``exercises-manifest.json`` sidecar at the project root.
|
|
247
|
+
|
|
248
|
+
Notebooks live **outside** ``static/`` — the learner runs them with
|
|
249
|
+
``marimo`` (via ``learningfoundry launch``), they are not web-served. The
|
|
250
|
+
sidecar maps ``id → {notebook_path, mode, port}``; ``learningfoundry
|
|
251
|
+
launch`` reads it to serve the right notebook. Both are regenerated each
|
|
252
|
+
build (not preserved) — only the curriculum author runs ``build``; the
|
|
253
|
+
learner runs the cloned output, so their marimo edits are never clobbered.
|
|
254
|
+
"""
|
|
255
|
+
if not resolved.exercises:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
manifest: dict[str, dict[str, object]] = {}
|
|
259
|
+
for ex in resolved.exercises:
|
|
260
|
+
notebook = output_dir / ex.notebook_path
|
|
261
|
+
try:
|
|
262
|
+
notebook.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
notebook.write_text(ex.notebook_source, encoding="utf-8")
|
|
264
|
+
except OSError as exc:
|
|
265
|
+
raise GenerationError(
|
|
266
|
+
f"Failed to write exercise notebook `{notebook}`: {exc}"
|
|
267
|
+
) from exc
|
|
268
|
+
manifest[ex.id] = {
|
|
269
|
+
"notebook_path": ex.notebook_path,
|
|
270
|
+
"mode": ex.mode,
|
|
271
|
+
"port": ex.port,
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
manifest_path = output_dir / "exercises-manifest.json"
|
|
275
|
+
try:
|
|
276
|
+
manifest_path.write_text(
|
|
277
|
+
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
|
|
278
|
+
encoding="utf-8",
|
|
279
|
+
)
|
|
280
|
+
except OSError as exc:
|
|
281
|
+
raise GenerationError(
|
|
282
|
+
f"Failed to write exercises manifest to `{manifest_path}`: {exc}"
|
|
283
|
+
) from exc
|
|
284
|
+
|
|
285
|
+
logger.info(
|
|
286
|
+
"Wrote %d exercise notebook(s) + manifest.", len(resolved.exercises)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
243
290
|
def _copy_assets(resolved: ResolvedCurriculum, output_dir: Path) -> None:
|
|
244
291
|
"""Copy each ``Asset`` to ``output_dir/static/<dest_relative>``.
|
|
245
292
|
|
|
246
293
|
Idempotent: a destination file whose size matches the source is left
|
|
247
|
-
untouched.
|
|
248
|
-
(``content/<sha256[:12]>/<basename>``) a matching size on a
|
|
249
|
-
path implies matching content
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
skipped; the `static/exercises` preservation across rebuilds plus the
|
|
253
|
-
stable `id` namespace make that an acceptable trade for fast rebuilds.
|
|
294
|
+
untouched. ``dest_relative`` is content-hashed
|
|
295
|
+
(``content/<sha256[:12]>/<basename>``), so a matching size on a
|
|
296
|
+
matching-hash path implies matching content — re-copying would be wasted
|
|
297
|
+
I/O. (Marimo exercise notebooks are written separately by
|
|
298
|
+
``_write_exercises``, not copied here.)
|
|
254
299
|
"""
|
|
255
300
|
if not resolved.assets:
|
|
256
301
|
return
|
{learningfoundry-0.82.0 → learningfoundry-0.83.0}/src/learningfoundry/integrations/nbfoundry_stub.py
RENAMED
|
@@ -27,13 +27,10 @@ def stub_exercise(ref_path: Path) -> dict: # type: ignore[type-arg]
|
|
|
27
27
|
"ref": str(ref_path),
|
|
28
28
|
"status": "stub",
|
|
29
29
|
"title": f"Exercise: {ref_path.stem}",
|
|
30
|
-
"
|
|
30
|
+
"description": (
|
|
31
31
|
f"<p>Exercise placeholder for <code>{ref_path}</code>. "
|
|
32
32
|
"nbfoundry integration pending.</p>"
|
|
33
33
|
),
|
|
34
|
-
"sections": [],
|
|
35
|
-
"expected_outputs": [],
|
|
36
|
-
"assets": [],
|
|
37
34
|
"hints": [],
|
|
38
35
|
"environment": None,
|
|
39
36
|
}
|