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.
Files changed (117) hide show
  1. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/.gitignore +8 -1
  2. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/CHANGELOG.md +135 -0
  3. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/PKG-INFO +229 -9
  4. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/README.md +228 -8
  5. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/pyproject.toml +1 -1
  6. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/__init__.py +1 -1
  7. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/cli.py +41 -2
  8. learningfoundry-0.69.1/src/learningfoundry/directives.py +95 -0
  9. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/exceptions.py +5 -0
  10. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/generator.py +21 -0
  11. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/parser.py +22 -3
  12. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/pipeline.py +73 -2
  13. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/resolver.py +122 -21
  14. learningfoundry-0.69.1/src/learningfoundry/schema_extensions.py +322 -0
  15. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/schema_v1.py +119 -3
  16. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/fixtures/curriculum.json +2 -4
  17. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/app.css +32 -0
  18. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +112 -0
  19. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +351 -0
  20. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +8 -0
  21. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.test.ts +78 -0
  22. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +1 -0
  23. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.test.ts +1 -2
  24. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -13
  25. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.test.ts +3 -4
  26. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.svelte +37 -0
  27. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/RecordingPausedBanner.test.ts +72 -0
  28. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/module-list.helpers.ts +93 -0
  29. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +244 -0
  30. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/progress.test.ts +85 -0
  31. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +301 -0
  32. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +2 -4
  33. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.test.ts +57 -0
  34. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/stores/db-init.ts +57 -0
  35. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.test.ts +2 -4
  36. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +69 -2
  37. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/duration.test.ts +30 -0
  38. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/duration.ts +23 -0
  39. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.test.ts +1 -2
  40. learningfoundry-0.69.1/src/learningfoundry/sveltekit_template/src/lib/utils/markdown-directives.ts +74 -0
  41. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +77 -0
  42. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts +3 -0
  43. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +15 -3
  44. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +11 -1
  45. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/page.test.ts +2 -4
  46. learningfoundry-0.62.2/docs/project-guide/README.md +0 -51
  47. learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/app.css +0 -5
  48. learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -69
  49. learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.test.ts +0 -154
  50. learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/components/module-list.test.ts +0 -110
  51. learningfoundry-0.62.2/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +0 -218
  52. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/LICENSE +0 -0
  53. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/__main__.py +0 -0
  54. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/asset_resolver.py +0 -0
  55. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/config.py +0 -0
  56. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/__init__.py +0 -0
  57. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
  58. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/nbfoundry_stub.py +0 -0
  59. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/protocols.py +0 -0
  60. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/integrations/quizazz.py +0 -0
  61. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/logging_config.py +0 -0
  62. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/py.typed +0 -0
  63. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/README.md +0 -0
  64. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/finish.spec.ts +0 -0
  65. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/global-teardown.ts +0 -0
  66. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/lifecycle.spec.ts +0 -0
  67. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/navigation.spec.ts +0 -0
  68. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/progress.spec.ts +0 -0
  69. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/reset.spec.ts +0 -0
  70. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/text-block-bottom.spec.ts +0 -0
  71. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/e2e/video.spec.ts +0 -0
  72. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/package.json +0 -0
  73. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/playwright.config.ts +0 -0
  74. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
  75. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
  76. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
  77. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
  78. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/LockedLessonPlaceholder.svelte +0 -0
  79. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
  80. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
  81. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
  82. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
  83. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.svelte +0 -0
  84. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/ResetCourseButton.test.ts +0 -0
  85. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.observer.test.ts +0 -0
  86. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
  87. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.test.ts +0 -0
  88. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
  89. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.test.ts +0 -0
  90. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
  91. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/lesson-view.helpers.ts +0 -0
  92. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/mount.test.ts +0 -0
  93. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.helpers.ts +0 -0
  94. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/navigation.test.ts +0 -0
  95. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/components/progress-dashboard.helpers.ts +0 -0
  96. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/database.test.ts +0 -0
  97. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
  98. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
  99. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.test.ts +0 -0
  100. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/db/user-id.ts +0 -0
  101. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
  102. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/stores/progress.ts +0 -0
  103. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/locking.ts +0 -0
  104. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.test.ts +0 -0
  105. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/progress.ts +0 -0
  106. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/lib/utils/viewport-completion.ts +0 -0
  107. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
  108. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
  109. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.helpers.ts +0 -0
  110. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.test.ts +0 -0
  111. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.scroll.ts +0 -0
  112. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/src/routes/layout.test.ts +0 -0
  113. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
  114. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
  115. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/test-results/.last-run.json +0 -0
  116. {learningfoundry-0.62.2 → learningfoundry-0.69.1}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
  117. {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/**/*.bak.*
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.62.2
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 pre/post assessments (requires quizazz-builder)
223
- pre_assessment:
224
- source: quizazz
225
- ref: assessments/mod-01-pre.yml
226
-
227
- post_assessment:
228
- source: quizazz
229
- ref: assessments/mod-01-post.yml
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):