learningfoundry 0.62.2__tar.gz → 0.69.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/.gitignore +8 -1
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/CHANGELOG.md +135 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/PKG-INFO +229 -9
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/README.md +228 -8
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/pyproject.toml +1 -1
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/__init__.py +1 -1
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/cli.py +41 -2
- learningfoundry-0.69.1/src/learningfoundry/directives.py +95 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/exceptions.py +5 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/generator.py +21 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/parser.py +22 -3
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/pipeline.py +73 -2
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/resolver.py +122 -21
- learningfoundry-0.69.1/src/learningfoundry/schema_extensions.py +322 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/schema_v1.py +119 -3
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/fixtures/curriculum.json +2 -4
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/app.css +32 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +112 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +351 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +8 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.test.ts +78 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +1 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.test.ts +1 -2
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -13
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.test.ts +3 -4
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.svelte +37 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.test.ts +72 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts +93 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +244 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/progress.test.ts +85 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +301 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +2 -4
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.test.ts +57 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.ts +57 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.test.ts +2 -4
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +69 -2
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/duration.test.ts +30 -0
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/duration.ts +23 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.test.ts +1 -2
- learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/markdown-directives.ts +74 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +77 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +3 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +15 -3
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +11 -1
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/page.test.ts +2 -4
- learningfoundry-0.62.2/docs/project-guide/README.md +0 -51
- learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/app.css +0 -5
- learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -69
- learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +0 -154
- learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +0 -110
- learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +0 -218
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/LICENSE +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/__main__.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/asset_resolver.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/config.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/__init__.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/nbfoundry_stub.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/protocols.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/quizazz.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/logging_config.py +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/py.typed +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/README.md +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/finish.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/global-teardown.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/lifecycle.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/navigation.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/progress.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/reset.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/text-block-bottom.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/video.spec.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/package.json +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/playwright.config.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LockedLessonPlaceholder.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.observer.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/lesson-view.helpers.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/mount.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.helpers.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/database.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/viewport-completion.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.helpers.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.test.ts +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/test-results/.last-run.json +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
- {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/vite.config.ts +0 -0
|
@@ -39,4 +39,11 @@ node_modules/
|
|
|
39
39
|
*.tsbuildinfo
|
|
40
40
|
|
|
41
41
|
# project-guide
|
|
42
|
-
docs/project-guide
|
|
42
|
+
/docs/project-guide/.metadata.yml
|
|
43
|
+
/docs/project-guide/README.md
|
|
44
|
+
/docs/project-guide/developer/
|
|
45
|
+
/docs/project-guide/templates/
|
|
46
|
+
/docs/project-guide/**/*.bak.*
|
|
47
|
+
|
|
48
|
+
# Project
|
|
49
|
+
/scripts/data/
|
|
@@ -7,6 +7,141 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.69.1] - 2026-05-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`CurriculumMeta`** — new optional `meta:` block on the top-level `curriculum:` mapping (Story J.h). Mirrors the existing `LessonMeta` / `ModuleMeta` shape with three declared fields (`target_audience`, `objectives`, `prerequisites`) plus `extra="allow"` so authors can attach curriculum-wide pedagogical context without forcing a schema change. Threaded through the resolver into `curriculum.json`; mirrored in the SvelteKit `Curriculum` TypeScript interface. Rendering is deferred to a later phase, matching the J.a precedent for `ModuleMeta`.
|
|
15
|
+
- **Project-specific schema extensions** — opt-in mechanism for tightening the three `meta` blocks' `extra="allow"` posture into strict whitelist-reject validation (Story J.h). When `learningfoundry-schema-extensions.yml` is present next to `curriculum.yml`, learningfoundry synthesizes strict subclasses of `CurriculumMeta` / `ModuleMeta` / `LessonMeta` with project-declared fields appended and `extra` flipped to `forbid`. Motivated by LLM-driven authoring workflows where phantom fields (typos like `prequisites` instead of `prerequisites`) silently pass `extra="allow"` and get lost in the resolved JSON.
|
|
16
|
+
- **`--schema-extensions PATH`** CLI flag on `build`, `validate`, and `preview`. Resolution order: CLI flag > `[tool.learningfoundry] schema_extensions = "..."` in `pyproject.toml` next to the curriculum > auto-discovery of `learningfoundry-schema-extensions.yml` next to the curriculum > none (base behaviour preserved).
|
|
17
|
+
- **Supported extension field types:** `str`, `int`, `bool`, `list[str]`, `enum` (with `values:` list). Per-field `required: bool` (default `true`) and `default:` (optional — presence makes the field optional regardless of `required`).
|
|
18
|
+
- **`SchemaExtensionError`** — new typed exception for missing / malformed / invalid extension files. Mapped to exit code 4 (`EXIT_CONFIG`) in the CLI.
|
|
19
|
+
- **New module:** [`src/learningfoundry/schema_extensions.py`](src/learningfoundry/schema_extensions.py) with `SchemaExtensions`, `MetaExtensions`, five discriminated `FieldDef` variants, `build_extended_meta_models`, `build_extended_curriculum_v1`, `load_schema_extensions`.
|
|
20
|
+
- **Tests:** [`tests/test_schema_extensions.py`](tests/test_schema_extensions.py) — 45 cases covering base behaviour preservation, strict whitelist rejection, every supported field type, required/default rules, per-model `extra` override, `CurriculumMeta` extension propagation, end-to-end through `parse_curriculum` and `run_validate`, file-path resolution precedence, CLI invocation via Click `CliRunner`, and strict validation of the extension file itself. Plus 6 new `CurriculumMeta` cases in `test_schema_v1.py` and 2 round-trip cases in `test_resolver.py`.
|
|
21
|
+
- **Docs:** new "Strict project-specific extensions" subsection in [README.md](README.md) and `CurriculumMeta` entry in the meta reference; new "Project-specific `meta` schema extensions" subsection in [docs/specs/features.md](docs/specs/features.md); new `schema_extensions.py` subsection plus `CurriculumMeta` in the schema overview in [docs/specs/tech-spec.md](docs/specs/tech-spec.md).
|
|
22
|
+
|
|
23
|
+
### Notes
|
|
24
|
+
|
|
25
|
+
- Backward compatible: when no extensions file is found, today's `extra="allow"` behaviour is preserved bit-for-bit. No existing curriculum needs to change.
|
|
26
|
+
- The mechanism is scoped to `meta` blocks (`CurriculumMeta`, `ModuleMeta`, `LessonMeta`) — `Lesson`, `Module`, `CurriculumDef` themselves stay unconditionally strict. `Hook` extensions are deferred (small surface, no asks).
|
|
27
|
+
- A Python-module schema hook (`schema_module = "..."`) is recorded as a possible follow-up if a curriculum ever needs cross-field validators.
|
|
28
|
+
|
|
29
|
+
## [0.69.0] - 2026-05-08
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **Sidebar module flow renders assessments at their resolved positions** (Story J.f). [LessonList.svelte](src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte) now interleaves assessment rows with lesson rows according to each assessment's `position`: `before_lessons` at the top, `{ before_lesson: <id> }` / `{ after_lesson: <id> }` around the named lesson, `after_lessons` at the bottom. Order in `module.assessments` is the canonical iteration order — the resolver emitted it that way and the component does not re-resolve placement.
|
|
34
|
+
- **Role label + pass-threshold annotation on each assessment row.** Role rendered capitalized (`pre` → `Pre Assessment`); `pass_threshold` rendered as a secondary `"70% to pass"` annotation when set. Rows are non-interactive — gating semantics is a future concern.
|
|
35
|
+
- **`interleaveModuleFlow`, `capitalizeRole`, `formatPassThreshold`** helpers in [module-list.helpers.ts](src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts) plus 10 new unit tests in `module-list.test.ts` and 5 new DOM tests in `LessonList.test.ts`.
|
|
36
|
+
- [ModuleList.svelte](src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte) now passes `mod.assessments` to `LessonList`.
|
|
37
|
+
|
|
38
|
+
### Phase J close (Story J.g)
|
|
39
|
+
|
|
40
|
+
- **Cross-cutting Phase J smoke fixture** at [tests/fixtures/phase-j-curriculum.yml](tests/fixtures/phase-j-curriculum.yml) and [tests/fixtures/phase-j-content/](tests/fixtures/phase-j-content/) exercises every Phase J affordance — lesson + module `meta`, all three tutorial directives, all three assessment roles with mixed positions, and `duration_minutes` aggregation — in a single curriculum. The new [tests/test_phase_j_smoke.py](tests/test_phase_j_smoke.py) (9 cases) pins the composition end-to-end through parse → resolve → generate. Per-feature tests stay narrow; this one is the integration anchor.
|
|
41
|
+
- **README "Pedagogical authoring" section** stitches the per-feature docs into a single author-facing story with a worked example covering meta blocks, tutorial directives, and the `assessments[]` array. Includes a migration paragraph for `pre_assessment` / `post_assessment` → `assessments[]` for any external curriculum that pre-dates v0.68.0.
|
|
42
|
+
- **features.md FR-2** now has a brief "Phase J: Pedagogical authoring" header tying the J.a–J.f subsections together and pointing at the integration test.
|
|
43
|
+
|
|
44
|
+
J.g is doc + integration-test only — shares the v0.69.0 release with J.f, no extra version bump.
|
|
45
|
+
|
|
46
|
+
### Notes
|
|
47
|
+
|
|
48
|
+
- Defensive belt: a lesson-anchored assessment whose target lesson does not exist is silently dropped at render time. The parser already rejects unknown refs at build time (`Module.validate_assessment_lesson_refs`); this guard ensures the component does not crash if one slips through.
|
|
49
|
+
- Mid-lesson assessment placement, gating ("must pass post to advance"), per-role styling beyond label text, and assessment-specific routes remain out of scope.
|
|
50
|
+
|
|
51
|
+
## [0.68.0] - 2026-05-08
|
|
52
|
+
|
|
53
|
+
### Removed (BREAKING)
|
|
54
|
+
|
|
55
|
+
- **`Module.pre_assessment` / `Module.post_assessment`** are gone from the curriculum schema, the resolved-curriculum dataclass, the generated `curriculum.json`, and the SvelteKit `Module` TypeScript interface (Story J.e). No alias, no deprecation warning, no shim — pre-1.0 makes the clean break acceptable. Curriculum YAML authors **must** migrate to the new `assessments[]` array; an unmigrated `pre_assessment` / `post_assessment` field at module level now produces a `ValidationError` from strict-mode Pydantic.
|
|
56
|
+
- ProgressDashboard's "Pre-assessment: X/Y" / "Post-assessment: X/Y" inline score rows are gone. The per-learner score data still lives in IndexedDB; a future story rebuilds the score-display UI atop the new shape.
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
|
|
60
|
+
- **`Module.assessments: list[AssessmentDefinition]`** — generalized array replacing the two-slot pre/post (Story J.e). Each `AssessmentDefinition` carries:
|
|
61
|
+
- `role`: open string (conventional values `pre`, `practice`, `post`, `checkpoint`).
|
|
62
|
+
- `position`: discriminated union — `"before_lessons"` / `"after_lessons"` / `{ before_lesson: <id> }` / `{ after_lesson: <id> }`.
|
|
63
|
+
- `source`, `ref`, optional `pass_threshold`.
|
|
64
|
+
- **Parse-time lesson-id validation.** A `model_validator` on `Module` rejects assessments whose `before_lesson` / `after_lesson` ref names a lesson that does not exist in the module, with an error message naming the module id, the assessment role, and the unknown lesson id.
|
|
65
|
+
- **Resolver materializes canonical assessment order.** `_resolve_assessments` walks the `assessments` array and emits a `ResolvedAssessment` list in placement order: `before_lessons` first, then for each lesson the `before_lesson` and `after_lesson` anchors, then `after_lessons`. Order is the canonical iteration order; each entry retains its original `position` (serialized as JSON-friendly string or single-key mapping) so the frontend can interleave at render time.
|
|
66
|
+
- **TypeScript `AssessmentDefinition` and `AssessmentPosition` types** in [lib/types/index.ts](src/learningfoundry/sveltekit_template/src/lib/types/index.ts).
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
|
|
70
|
+
- Frontend rendering of assessments at their resolved positions in the module flow lands in **Story J.f** (v0.69.0). Until then, `module.assessments[]` is plumbed through end-to-end but no UI surfaces it.
|
|
71
|
+
- `pass_threshold` is recorded but not enforced in v1 — gating semantics is a future concern that may also touch the progress-DB schema.
|
|
72
|
+
|
|
73
|
+
## [0.67.1] - 2026-05-08
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- **Build-time lint for unbalanced tutorial-scaffold directives** (Story J.d.2). New [directives.py](src/learningfoundry/directives.py) (`lint_directives`) scans every `text` content block's markdown for `::: worked-example` / `::: faded-example` / `::: independent-practice` opens with no matching `:::` close on its own line, and raises `ContentResolutionError` with the lesson location and the 1-based opening line number. Hooked into [resolver.py](src/learningfoundry/resolver.py) `_resolve_text` after the markdown is read but before image-asset resolution, so authors get a build-time error rather than discovering the problem as a render-time anomaly.
|
|
78
|
+
- Lint is conservative by design: only the three known directive names are tracked; unknown names (`::: tip`) pass through untouched, and lines inside fenced code blocks (``` or `~~~`) are skipped so prose that demonstrates the directive syntax isn't mistaken for an actual directive.
|
|
79
|
+
- tech-spec.md gained a `directives.py` subsection documenting the lint contract and the TS↔Python coupling on the `KNOWN_DIRECTIVES` list.
|
|
80
|
+
|
|
81
|
+
## [0.67.0] - 2026-05-08
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- **Tutorial scaffold container directives in lesson markdown** (Story J.d.1). Three named container directives — `::: worked-example`, `::: faded-example`, `::: independent-practice` — recognised by a custom `marked` extension at [markdown-directives.ts](src/learningfoundry/sveltekit_template/src/lib/utils/markdown-directives.ts). Each directive opens with `::: <name>` on its own line and closes with `:::` on its own line; inner content is itself markdown so headings, lists, math, and emphasis nest naturally.
|
|
86
|
+
- **CSS for the three directive treatments** in [app.css](src/learningfoundry/sveltekit_template/src/app.css): `lf-directive-worked-example` = filled gray card; `lf-directive-faded-example` = outlined dim card; `lf-directive-independent-practice` = amber-highlighted challenge prompt. Pure CSS so the styles ship in the bundled output regardless of Tailwind content-detection.
|
|
87
|
+
- README "Tutorial scaffold directives" section with author-facing examples; features.md FR-3 subsection documenting the rendering contract.
|
|
88
|
+
|
|
89
|
+
### Notes
|
|
90
|
+
|
|
91
|
+
- Unknown directive names (e.g. `::: tip`) pass through untouched at render time. The Python-side lint that flags malformed or unknown directive blocks at build time lands in **Story J.d.2** (v0.67.1).
|
|
92
|
+
- Static styling only — interactivity (progressive reveal, hint toggles, checkmark affordances) is out of scope.
|
|
93
|
+
|
|
94
|
+
## [0.66.0] - 2026-05-08
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **Curriculum-wide aggregate time estimate** (Story J.c). [generator.py](src/learningfoundry/generator.py) sums every lesson's `meta.duration_minutes` and emits the result as `curriculum.total_duration_minutes` at the top level of `curriculum.json`. Lessons without `meta` or without `duration_minutes` are skipped; when no lesson contributes, the field is `null`.
|
|
99
|
+
- **Index page renders the estimate above the dashboard** when non-null, formatted as `≈ Xh Ym` (or `≈ Xm` under an hour, `≈ Xh` for whole-hour totals). Hidden entirely when `total_duration_minutes` is `null`. New helper [duration.ts](src/learningfoundry/sveltekit_template/src/lib/utils/duration.ts) (`formatDurationEstimate`) owns the formatting; [+page.svelte](src/learningfoundry/sveltekit_template/src/routes/+page.svelte) renders it.
|
|
100
|
+
- TypeScript `Curriculum` interface gained `total_duration_minutes?: number | null` ([lib/types/index.ts](src/learningfoundry/sveltekit_template/src/lib/types/index.ts)).
|
|
101
|
+
|
|
102
|
+
### Notes
|
|
103
|
+
|
|
104
|
+
- Per-module aggregation, learner-elapsed-time display, and adaptive estimates from past learner pace remain out of scope (Phase J / J.c).
|
|
105
|
+
|
|
106
|
+
## [0.65.0] - 2026-05-08
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **Lesson role chip in the sidebar** (Story J.b). When `lesson.meta.role` is set, [LessonList.svelte](src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte) renders a small uppercase chip (e.g. `OPENER`, `PRACTICE`) at the right edge of the lesson row, distinct in styling from the progress glyph and the locked-row indicator. Hidden when `meta.role` is absent.
|
|
111
|
+
- **Lesson hook tagline above the title** (Story J.b). When `lesson.meta.hook.tagline` is set, [LessonView.svelte](src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte) renders the tagline as a quiet italic line directly above the `<h1>` lesson title. Hidden when absent.
|
|
112
|
+
- Test fixture [valid-curriculum.yml](tests/fixtures/valid-curriculum.yml) now exercises lesson and module `meta` end-to-end; smoke test pins the meta-passthrough data contract on the production `build/curriculum.json`.
|
|
113
|
+
|
|
114
|
+
### Notes
|
|
115
|
+
|
|
116
|
+
- `hook.image_prompt` is intentionally not rendered — no consumer exists pre image-generation pipeline.
|
|
117
|
+
- Module-level `meta.theme` rendering on the module index and `meta.duration_minutes` aggregation land in subsequent Phase J stories.
|
|
118
|
+
|
|
119
|
+
## [0.64.0] - 2026-05-08
|
|
120
|
+
|
|
121
|
+
### Added
|
|
122
|
+
|
|
123
|
+
- **Pedagogical metadata on lessons and modules** (Story J.a — Phase J kickoff). New `Hook`, `LessonMeta`, and `ModuleMeta` Pydantic models in [schema_v1.py](src/learningfoundry/schema_v1.py); optional `meta` fields on `Lesson` and `Module`. `LessonMeta` carries `role`, `hook` (`tagline` + optional `image_prompt`), `introduces`, `reinforces`, and `duration_minutes`; `ModuleMeta` carries `theme`, `big_problem`, `objectives`, `experiential_summary`, and `target_audience`. All meta models use `extra="allow"` so authors can attach genre-specific fields without schema churn.
|
|
124
|
+
- **`meta` propagated verbatim into `curriculum.json`.** [resolver.py](src/learningfoundry/resolver.py) carries the resolved meta dicts on `ResolvedLesson` and `ResolvedModule`; [generator.py](src/learningfoundry/generator.py) emits them through `dataclasses.asdict`. Absent meta serializes as `"meta": null`.
|
|
125
|
+
- **TypeScript mirrors** for `Hook`, `LessonMeta`, and `ModuleMeta` in [lib/types/index.ts](src/learningfoundry/sveltekit_template/src/lib/types/index.ts).
|
|
126
|
+
|
|
127
|
+
### Notes
|
|
128
|
+
|
|
129
|
+
- No frontend rendering of `meta` yet — surfacing of `meta.role` and `meta.hook.tagline` lands in Story J.b; `duration_minutes` aggregation lands in Story J.c.
|
|
130
|
+
|
|
131
|
+
## [0.63.0] - 2026-05-03
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **`WasmAssetMissingError` is now visible to the learner** (Story I.bb). When `Database.getDb()` rejects because `/sql-wasm.wasm` is unavailable, a persistent layout-level banner appears above the main content area: "Progress recording is paused. Your activity in this session will not be saved. Try refreshing to retry." with a Refresh CTA that calls `location.reload()`. Pre-fix the failure was CLI-log-only — the learner saw checkmarks fail to land, no in-progress icons, no module advancement, and no UI signal explaining why. Story I.aa hardened the asset pipeline so this should be rare in deployed apps; Story I.bb closes the loop on what the learner actually sees if it ever recurs (asset-pipeline regression, deploy misconfiguration, browser cache poisoning, network partition).
|
|
136
|
+
- New [src/lib/stores/db-init.ts](src/learningfoundry/sveltekit_template/src/lib/stores/db-init.ts) — `dbInit` writable store (`pending` | `ready` | `wasm-missing` | `failed`) plus an idempotent `initializeDatabase()` that drives the store from a one-shot `database.getDb()` call wired into `+layout.svelte`. Single-signal layout-level surfacing (option (c) from the story design notes), so per-write rejections don't have to handle the case independently.
|
|
137
|
+
- New [RecordingPausedBanner.svelte](src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.svelte) renders only when `dbInit` is in `wasm-missing` state. AlertTriangle icon, amber palette, accessible `role="status"` + `aria-live="polite"`.
|
|
138
|
+
|
|
139
|
+
### Changed
|
|
140
|
+
|
|
141
|
+
- **`progress.ts` swallows `WasmAssetMissingError` at the repository boundary.** Once the layout banner is up, per-call rejections are an information duplicate that every UI call site would have to defend against. Writes (`markLessonComplete` / `markLessonOpened` / `markLessonInProgress` / `saveQuizScore` / `updateExerciseStatus` / `resetProgress`) resolve as no-ops; reads (`getLessonProgress`, `getQuizScore`) return `null`; `getModuleProgress` returns an empty `not_started` shape so the dashboard renders the empty state instead of an error page. Non-WASM errors still propagate. Module-level doc comment in [progress.ts](src/learningfoundry/sveltekit_template/src/lib/db/progress.ts) documents the rule so a future maintainer doesn't refactor the catches away.
|
|
142
|
+
- **`+layout.svelte`** wraps `<main>` in a flex column that hosts the banner above the scrollable content region; main keeps `overflow-y-auto`, banner stays pinned at the top.
|
|
143
|
+
- **[features.md](docs/specs/features.md) FR-4** now documents the recording-paused state as a hard requirement (closes the requirements gap Story I.aa identified).
|
|
144
|
+
|
|
10
145
|
## [0.62.2] - 2026-05-02
|
|
11
146
|
|
|
12
147
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learningfoundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.69.1
|
|
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
|
|
@@ -48,6 +48,7 @@ A curriculum engine that turns a YAML curriculum definition into a deployable Sv
|
|
|
48
48
|
- [Video blocks](#video-blocks)
|
|
49
49
|
- [Lesson titles and markdown headings](#lesson-titles-and-markdown-headings)
|
|
50
50
|
- [Images and assets](#images-and-assets)
|
|
51
|
+
- [Pedagogical authoring](#pedagogical-authoring)
|
|
51
52
|
- [Content locking](#content-locking)
|
|
52
53
|
- [Configuration File](#configuration-file)
|
|
53
54
|
- [Development Setup](#development-setup)
|
|
@@ -219,14 +220,28 @@ curriculum:
|
|
|
219
220
|
title: "Module One" # required
|
|
220
221
|
description: "..." # optional
|
|
221
222
|
|
|
222
|
-
# Optional
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
223
|
+
# Optional assessments (requires quizazz-builder).
|
|
224
|
+
# Each assessment carries an open-string `role` and a `position`
|
|
225
|
+
# (`before_lessons`, `after_lessons`, or `{ before_lesson: <id> }`
|
|
226
|
+
# / `{ after_lesson: <id> }`). The order in `assessments` after
|
|
227
|
+
# build is the canonical placement order.
|
|
228
|
+
assessments:
|
|
229
|
+
- role: pre
|
|
230
|
+
position: before_lessons
|
|
231
|
+
source: quizazz
|
|
232
|
+
ref: assessments/mod-01-pre.yml
|
|
233
|
+
|
|
234
|
+
- role: practice
|
|
235
|
+
position: { before_lesson: lesson-04 }
|
|
236
|
+
source: quizazz
|
|
237
|
+
ref: assessments/mod-01-practice.yml
|
|
238
|
+
pass_threshold: 0.7
|
|
239
|
+
|
|
240
|
+
- role: post
|
|
241
|
+
position: after_lessons
|
|
242
|
+
source: quizazz
|
|
243
|
+
ref: assessments/mod-01-post.yml
|
|
244
|
+
pass_threshold: 0.8
|
|
230
245
|
|
|
231
246
|
lessons:
|
|
232
247
|
- id: lesson-01 # required, kebab-case; unique within module
|
|
@@ -390,6 +405,211 @@ For production deployment to a CDN, just run `cd dist && pnpm build` — the `st
|
|
|
390
405
|
|
|
391
406
|
---
|
|
392
407
|
|
|
408
|
+
## Pedagogical authoring
|
|
409
|
+
|
|
410
|
+
Phase J adds first-class authoring affordances for the worked-example → faded-example → independent-practice progression and for declaring pedagogical context the build pipeline can act on. Three building blocks compose into one story:
|
|
411
|
+
|
|
412
|
+
1. **`meta` blocks** declare the *intent* of a module or lesson — its theme, role, opening hook, learning items, and time estimate.
|
|
413
|
+
2. **Container directives** in lesson markdown style worked / faded / independent-practice cards inline.
|
|
414
|
+
3. **`assessments[]`** on each module places quizzes at named positions (before all lessons, before/after a specific lesson, after all lessons) — replacing the legacy `pre_assessment` / `post_assessment` pair (see migration note below).
|
|
415
|
+
|
|
416
|
+
A small worked example bringing the three together:
|
|
417
|
+
|
|
418
|
+
```yaml
|
|
419
|
+
modules:
|
|
420
|
+
- id: mod-01
|
|
421
|
+
title: "Why convolutions exist"
|
|
422
|
+
|
|
423
|
+
meta:
|
|
424
|
+
theme: "Why convolutions exist"
|
|
425
|
+
objectives:
|
|
426
|
+
- "Explain why FC nets fail on images"
|
|
427
|
+
- "Describe weight sharing"
|
|
428
|
+
target_audience: "Intermediate Python; high-school math"
|
|
429
|
+
|
|
430
|
+
assessments:
|
|
431
|
+
- role: pre
|
|
432
|
+
position: before_lessons
|
|
433
|
+
source: quizazz
|
|
434
|
+
ref: assessments/mod-01-pre.yml
|
|
435
|
+
- role: practice
|
|
436
|
+
position: { before_lesson: lesson-02 }
|
|
437
|
+
source: quizazz
|
|
438
|
+
ref: assessments/mod-01-practice.yml
|
|
439
|
+
pass_threshold: 0.7
|
|
440
|
+
- role: post
|
|
441
|
+
position: after_lessons
|
|
442
|
+
source: quizazz
|
|
443
|
+
ref: assessments/mod-01-post.yml
|
|
444
|
+
pass_threshold: 0.8
|
|
445
|
+
|
|
446
|
+
lessons:
|
|
447
|
+
- id: lesson-01
|
|
448
|
+
title: "What is a convolution?"
|
|
449
|
+
meta:
|
|
450
|
+
role: opener
|
|
451
|
+
hook:
|
|
452
|
+
tagline: "What if your first layer of vision was just a flashlight on the world?"
|
|
453
|
+
introduces: [receptive_field, simple_cells]
|
|
454
|
+
duration_minutes: 15
|
|
455
|
+
content_blocks:
|
|
456
|
+
- type: text
|
|
457
|
+
ref: content/mod-01/lesson-01.md
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
And the lesson markdown can sprinkle in the three directives:
|
|
461
|
+
|
|
462
|
+
```markdown
|
|
463
|
+
# What is a convolution?
|
|
464
|
+
|
|
465
|
+
::: worked-example
|
|
466
|
+
Compute the output shape for a 32×32 input, 3×3 kernel, stride 1, padding 0.
|
|
467
|
+
|
|
468
|
+
We apply $(W - K + 2P) / S + 1 = 30$. Output: **30×30**.
|
|
469
|
+
:::
|
|
470
|
+
|
|
471
|
+
::: faded-example
|
|
472
|
+
For a 64×64 input, 5×5 kernel, stride 1, padding 2 — what's the output shape?
|
|
473
|
+
:::
|
|
474
|
+
|
|
475
|
+
::: independent-practice
|
|
476
|
+
Given a 28×28 input, design a `Conv2d` that outputs 14×14. State your kernel, stride, and padding.
|
|
477
|
+
:::
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### `meta` reference
|
|
481
|
+
|
|
482
|
+
**Lesson `meta`** carries:
|
|
483
|
+
- `role` — open string, conventional values `opener`, `concept`, `story`, `math`, `tutorial`, `practice`, `hands_on`, `bonus`. Renders as a small chip in the sidebar.
|
|
484
|
+
- `hook` — `{ tagline, image_prompt? }`. The tagline renders as a quiet italic line above the lesson title.
|
|
485
|
+
- `introduces` / `reinforces` — lists of learning-item ids (open vocabulary; useful for downstream tooling).
|
|
486
|
+
- `duration_minutes` — integer; aggregated across the curriculum into `total_duration_minutes` and surfaced on the index page as `≈ Xh Ym`.
|
|
487
|
+
|
|
488
|
+
**Module `meta`** carries:
|
|
489
|
+
- `theme`, `big_problem`, `objectives`, `experiential_summary`, `target_audience`. Rendering of these is deferred; today they pass through to `curriculum.json` for downstream tooling.
|
|
490
|
+
|
|
491
|
+
**Curriculum `meta`** (Story J.h) carries:
|
|
492
|
+
- `target_audience`, `objectives`, `prerequisites`. Curriculum-wide pedagogical context — passed through to `curriculum.json` for downstream tooling, no rendering in v1.
|
|
493
|
+
|
|
494
|
+
### Custom `meta` fields
|
|
495
|
+
|
|
496
|
+
Both `LessonMeta` and `ModuleMeta` (and the `hook` sub-block) accept undeclared fields — authors can attach genre-specific data alongside the declared ones without a schema change:
|
|
497
|
+
|
|
498
|
+
```yaml
|
|
499
|
+
lessons:
|
|
500
|
+
- id: lesson-01
|
|
501
|
+
title: "What is a convolution?"
|
|
502
|
+
meta:
|
|
503
|
+
# Declared fields — type-checked:
|
|
504
|
+
role: opener
|
|
505
|
+
introduces: [receptive_field]
|
|
506
|
+
duration_minutes: 15
|
|
507
|
+
|
|
508
|
+
# Author-defined extras — accepted as-is:
|
|
509
|
+
covers: ["pe:hubel-wiesel", "hi:receptive-field-discovery"]
|
|
510
|
+
difficulty: intermediate
|
|
511
|
+
prerequisites: [lesson-00]
|
|
512
|
+
author_notes: "Revisit after the kernel-size deep-dive lands."
|
|
513
|
+
content_blocks:
|
|
514
|
+
- type: text
|
|
515
|
+
ref: content/mod-01/lesson-01.md
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
The escape hatch is scoped to `meta` (and `hook`) only. `Lesson`, `Module`, and the top-level `curriculum:` reject unknown fields, so a misplaced `difficulty:` at the lesson level (sibling of `meta`, not nested inside it) still fails the build — the strictness that catches typos like a mis-nested `sequential: true` is preserved everywhere outside the `meta` blocks.
|
|
519
|
+
|
|
520
|
+
Declared fields keep their normal type checks; only undeclared keys ride through unvalidated. Extras pass through unchanged into the generated `curriculum.json`, so downstream tooling (custom Svelte components, analytics dashboards, external reports) can read them without any further pipeline wiring.
|
|
521
|
+
|
|
522
|
+
### Strict project-specific extensions
|
|
523
|
+
|
|
524
|
+
The permissive `extra="allow"` posture above is too loose for LLM-driven authoring — an LLM that writes `prequisites` instead of `prerequisites` will pass validation, lose the data in `curriculum.json`, and break downstream consumers silently. The schema-extensions mechanism is an opt-in tightening: a project drops `learningfoundry-schema-extensions.yml` next to its `curriculum.yml`, declares the additional fields it cares about, and learningfoundry flips the `meta` blocks from "allow anything" to "reject anything not on the list."
|
|
525
|
+
|
|
526
|
+
Minimal example — `learningfoundry-schema-extensions.yml`:
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
version: "1"
|
|
530
|
+
lesson_meta:
|
|
531
|
+
fields:
|
|
532
|
+
covers: { type: list[str], default: [] }
|
|
533
|
+
difficulty: { type: enum, values: [intro, intermediate, advanced] }
|
|
534
|
+
prerequisites: { type: list[str], default: [] }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
With this file in place:
|
|
538
|
+
|
|
539
|
+
```yaml
|
|
540
|
+
# curriculum.yml lesson — typo `prequisites` instead of `prerequisites`
|
|
541
|
+
meta:
|
|
542
|
+
difficulty: intermediate
|
|
543
|
+
prequisites: [lesson-00] # ❌ now a build error
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
`learningfoundry validate` exits non-zero with a message naming the offending field (the Pydantic `ValidationError` puts `prequisites` directly in the output). Without the extensions file, the same typo silently passes — the original `extra="allow"` posture is preserved.
|
|
547
|
+
|
|
548
|
+
**Supported field types:** `str`, `int`, `bool`, `list[str]`, `enum` (with `values:` list). Each field accepts `required: bool` (default `true`) and `default:` (presence makes the field optional). Per-model `extra: allow` overrides the default `extra: forbid` if you want one meta layer tight and another loose during a staged rollout.
|
|
549
|
+
|
|
550
|
+
**File-path resolution order** (highest precedence first):
|
|
551
|
+
|
|
552
|
+
1. `--schema-extensions PATH` CLI flag on `build`, `validate`, `preview`.
|
|
553
|
+
2. `[tool.learningfoundry] schema_extensions = "..."` in `pyproject.toml` next to the curriculum.
|
|
554
|
+
3. Auto-discovery: `learningfoundry-schema-extensions.yml` next to the curriculum.
|
|
555
|
+
4. None — base `extra="allow"` behaviour, no enforcement.
|
|
556
|
+
|
|
557
|
+
The extensions file is itself strict-validated, so a typo there (e.g. `defalt:` instead of `default:`) fails at load time naming the field, not silently degrading the contract the file is supposed to tighten. The mechanism applies to all three meta layers (`curriculum_meta`, `module_meta`, `lesson_meta`) independently — declaring one does not require declaring the others.
|
|
558
|
+
|
|
559
|
+
### Tutorial scaffold directives
|
|
560
|
+
|
|
561
|
+
Three named container directives:
|
|
562
|
+
|
|
563
|
+
- `::: worked-example` — filled gray card. Use it for fully worked solutions.
|
|
564
|
+
- `::: faded-example` — outlined dim card. Use it for similar problems with reduced scaffolding.
|
|
565
|
+
- `::: independent-practice` — amber-highlighted challenge prompt. Use it for problems the learner solves on their own.
|
|
566
|
+
|
|
567
|
+
Inner markdown (headings, lists, math, emphasis) renders normally inside each directive. Unknown directive names pass through untouched at render time. Static styling only — no progressive-reveal interactivity in v1. An unbalanced known-name block (open with no `:::` close on its own line) fails the build with the lesson location, so the failure mode is loud rather than rendered as silent prose.
|
|
568
|
+
|
|
569
|
+
### Assessments
|
|
570
|
+
|
|
571
|
+
Each module declares an `assessments[]` array; each entry carries:
|
|
572
|
+
|
|
573
|
+
- `role` — open string. Conventional values: `pre`, `practice`, `post`, `checkpoint`. Surfaces as a capitalized label in the sidebar (`Pre Assessment`, `Practice Assessment`, …).
|
|
574
|
+
- `position` — discriminated union:
|
|
575
|
+
- `before_lessons` — anchors at the start of the module flow.
|
|
576
|
+
- `after_lessons` — anchors at the end.
|
|
577
|
+
- `{ before_lesson: <lesson-id> }` — anchors immediately before the named lesson.
|
|
578
|
+
- `{ after_lesson: <lesson-id> }` — anchors immediately after.
|
|
579
|
+
- `source`, `ref` — provider + path, same shape as `quiz` content blocks.
|
|
580
|
+
- `pass_threshold` — optional `0.0`–`1.0`. Recorded but not gating in v1; surfaces as a `"X% to pass"` annotation on the assessment row when set.
|
|
581
|
+
|
|
582
|
+
Lesson-anchored refs (`before_lesson` / `after_lesson`) are validated against the module's `lessons` at build time — typing a wrong lesson id fails the build with the module id, role, and unknown lesson id.
|
|
583
|
+
|
|
584
|
+
### Migrating from `pre_assessment` / `post_assessment` (pre-v0.68.0)
|
|
585
|
+
|
|
586
|
+
`Module.pre_assessment` and `Module.post_assessment` were removed in v0.68.0 (Story J.e). To migrate an external curriculum that pre-dates the cutover, replace each block with a single `assessments[]` entry using the `before_lessons` or `after_lessons` position:
|
|
587
|
+
|
|
588
|
+
```yaml
|
|
589
|
+
# BEFORE (v0.67.x and earlier)
|
|
590
|
+
pre_assessment:
|
|
591
|
+
source: quizazz
|
|
592
|
+
ref: assessments/mod-01-pre.yml
|
|
593
|
+
post_assessment:
|
|
594
|
+
source: quizazz
|
|
595
|
+
ref: assessments/mod-01-post.yml
|
|
596
|
+
|
|
597
|
+
# AFTER (v0.68.0+)
|
|
598
|
+
assessments:
|
|
599
|
+
- role: pre
|
|
600
|
+
position: before_lessons
|
|
601
|
+
source: quizazz
|
|
602
|
+
ref: assessments/mod-01-pre.yml
|
|
603
|
+
- role: post
|
|
604
|
+
position: after_lessons
|
|
605
|
+
source: quizazz
|
|
606
|
+
ref: assessments/mod-01-post.yml
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
Strict-mode Pydantic rejects an unmigrated `pre_assessment` / `post_assessment` field with a `ValidationError` naming the offending field, so the build fails loudly until the migration is complete. There is no compatibility shim or deprecation warning — pre-1.0 makes the clean break acceptable.
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
393
613
|
## Content locking
|
|
394
614
|
|
|
395
615
|
Control access to modules and lessons with a three-level configuration hierarchy (most local wins):
|