You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Groomed by the product manager. Raw intake preserved below under "Original report".
Summary
The public homepage advertises 2025 cohorts for Machine Learning, LLM and AI Dev Tools while the
database holds live 2026 cohorts for all three. The cause is a source-of-truth deviation, not stale
data: core/home_content.py and content/review_views.py read checked-in JSON projections for
course facts, while courses.Cohort is the specification-mandated owner of that data.
Owner direction: "We don't read projections. It must come from the database."
This is spec-conforming, not a spec amendment. No specification change is required and none is
in scope.
Normative basis
_docs/specs/README.md Fixed requirements: "Django serves the public site from validated,
versioned database read models."
_docs/specs/01-platform-architecture.md "Data ownership": courses, cohorts, teaching teams,
course registrations, enrollments, assignments… are Database-owned and editable through
Studio/API. Courses appear nowhere in the GitHub-owned list.
_docs/specs/04-courses-and-cohorts.md is entirely a database model specification. It never
mentions a course projection.
Reading content/public_projection/courses.json for public course display is therefore a deviation
from specifications 01 and 04.
Ownership classification (settles the #276 hand-off)
#276 explicitly declines to own this and asks for a classification: "The manifest must state which
course-owned surfaces, if any, are direct-sync content rather than course-domain reads."
Classification: every surface listed in Scope is course_domain. None of them is editorial
content and none of them is direct-sync content. The work is owned here. If the owner disagrees
with that classification, this issue closes as won't-fix and the surfaces return to #276.
Verified root cause
core/home_content.py:258 is the only runtime reader of public_projection()["courses"]:
The artifact holds 12 cohorts pinned to DataTalksClub/course-management-platform@98a235283904b4ef9ad29e196298540756cf1bcc (2026-08-04).
The current course dataset holds 15 cohorts across 7 course families.
Verified against the current 15-cohort dataset:
Family (DB Course.slug)
Homepage link today (projection)
Homepage link required (database)
de-zoomcamp
/courses/de-zoomcamp/2026
/courses/de-zoomcamp/2026
ml-zoomcamp
/courses/ml-zoomcamp/2025
/courses/ml-zoomcamp/2026
llm-zoomcamp
/courses/llm-zoomcamp/2025
/courses/llm-zoomcamp/2026
mlops-zoomcamp
/courses/mlops-zoomcamp/2025
/courses/mlops-zoomcamp/2025
sma-zoomcamp
/courses/sma-zoomcamp/2025
/courses/sma-zoomcamp/2025
AI Dev Tools
/courses/ai-dev-tools/2025
/courses/ai-dev-tools-zoomcamp/2026
The URL grammar does not change.Cohort.identifier is "2026", Course.slug is the family,
and reverse("course", args=[family.slug, cohort.identifier]) reproduces exactly the /courses/<family>/<identifier> shape the projection already emits. No route, no redirect, and no
preserved path is touched.
Reuse, do not rewrite
courses/views/course_list.py:76visible_course_list_queryset() already serves /courses from
the ORM with homework_count, project_count and learner_count annotations — exactly the fields core/home_content.py:265-275 builds from JSON. Reuse it. templates/core/home.html:1058-1065
consumes only .label, .title, .cohort_label and .public_path, and templates/core/home.html:1028-1029 consumes .homework_count and .project_count; all are
available from that queryset, so the catalogue template needs no structural change.
Scope
1. core/home_content.pycourse_catalog() — the live defect
Rebuild CatalogCourse construction on visible_course_list_queryset():
one card per visibleCourse whose newest visibleCohort is selected by (year, start_date, id) descending;
homework_count / project_count from the existing annotations.
COURSE_FAMILIES (core/home_content.py:18-49) stops being the list of courses that exist. It may
remain only as a presentation table supplying the short chip label and the preferred display
order. The database decides which families exist; a family the database holds but the table does
not must still render, using its Course.title as the chip label. An allowlist that silently drops
a new upstream family would reproduce this same staleness bug in a new place and is not acceptable.
core/home_content.py:263 currently raises ImproperlyConfigured when a family is missing. A
partially populated database would therefore return 500 on the homepage. It must instead render
an empty catalogue, matching the existing /courses empty state.
2. core/views.py:73 — the featured-cohort hero
review_projection()["course"] supplies the homepage hero's course identity, cohort title, start
date and description. Move the facts to the database; keep the editorial copy page-owned.
From the database: course title, cohort title, cohort year, cohort start date, cohort public path,
homework/project counts.
Page-owned constants in core/home_content.py (same pattern as the existing FEATURED_BUILD_ITEMS, FEATURED_GROUP_NOTE, MEMBER_STORIES): the hero summary, format, price and notice strings. Cohort has no format, price or notice field, and Cohort.description for the 2026 rows is generated boilerplate
("The 2026 live delivery of AI Dev Tools Zoomcamp.") while Course.description for ai-dev-tools-zoomcamp is raw README markup containing external image tags and courses.datatalks.club links. Reading either into the hero would be a copy regression and a
markup-injection risk. Move today's exact strings into page constants; do not change a word.
Adding format / price / notice model fields is a schema change and is a non-goal here.
core/views.py:87-89 uses next(entry for entry in catalog if entry.family == FEATURED_FAMILY),
which raises StopIteration (500) when that family is absent. It must fail soft.
3. content/review_views.py:379course_cohort — the cohort page
Same split: cohort title, start date and the linked platform cohort come from courses.Cohort; format, price and notice stay page-owned constants with today's exact strings.
content/review_views.py:382 currently does Cohort.objects.filter(slug=cohort["legacy_platform_slug"]).first() with legacy_platform_slug = "ai-dev-tools-2026". No cohort with that slug exists — the dataset holds ai-dev-tools-2025 and ai-dev-tools-zoomcamp-2026 — so legacy_course is None and the
"Open the existing course workspace" call to action (templates/review/course_cohort.html:163-169)
is silently missing today. Resolving the cohort from the database fixes that link.
The cohort title in the heading and breadcrumb comes from the database.
5. content/public_views.py:1200 sitemap — verification only
The courses sitemap section already reads Cohort.objects.filter(visible=True, course__visible=True).
It is the proof that the pattern exists and needs no change. It must be covered by a regression
assertion that no course entry in the sitemap derives from courses.json.
6. Documentation corrections (in scope)
courses/services/local_course_seed.py:8-15 claims "Specification 03 keeps public requests on
that projection." Specification 03 is GitHub content and people; it says nothing about the CMP
course catalogue. This over-claim is the most likely reason the deviation went unchallenged.
Correct it.
_docs/runbooks/local-course-content-review.md:193-195 states "the public homepage does not read
this database at all… Refreshing the review database therefore cannot by itself change what the
homepage advertises." That becomes false with this change. Update it.
7. Tests that assert the current premise
These fail or become meaningless and must be rewritten, not deleted:
core/tests/test_homepage.py:46test_root_uses_the_shared_course_platform_shell renders /
against an empty database and asserts "AI Dev Tools Zoomcamp", "Starts August 31" and the
featured-cohort link. It must build cohorts in the test database.
core/tests/test_homepage.py:104test_single_destination_cards_stretch_their_existing_semantic_links
asserts a course-card exists in the rendered homepage.
playwright_tests/test_course_design_parity.py:677 test_no_database_course_catalog_uses_the_design_system_empty_state asserts not Cohort.objects.exists(). It currently covers only /courses; with the homepage now
database-backed, the same premise must also assert that / renders 200 with an empty catalogue
rather than 500.
Design rules
Course facts and identity always come from courses.Course / courses.Cohort.
Course editorial copy that has no database field stays a page-owned constant with today's
exact wording. It is never invented and never read from a projection.
Only visible=True cohorts of visible=True courses reach a public surface.
No public URL string changes. /courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026 and /courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/ keep their exact
paths and URL names; only their data source changes.
Every converted surface degrades to an empty state, never to a 500.
Blast radius (verified, and deliberately small)
No preserved path is touched./courses/<family>/<year> appears nowhere in the 2,937-row _docs/compatibility/generated-path-baseline.jsonl (the only /courses/* legacy row is /courses/2021-winter-ml-zoomcamp.html).
Content digests stay green because content/public_projection/courses.json stays in the tree,
byte-identical.EXPECTED_COUNTS["courses"] = 12 (content/public_data.py:65), PROJECTION_TREE_SHA256 and PROJECTION_MANIFEST_SHA256
(content_sync/dtc_content/contract.py:32-34) all keep validating the unchanged artifact. Leaving the artifact in place is an explicit requirement of this issue, not an oversight.
content/review_projection.json likewise stays in the tree; the content/apps.py startup check
and REQUIRED_PUBLIC_PATHS keep passing unchanged.
The verification trap — read this before testing
courses/services/local_course_seed.py:68-75 seeds the local database from the same pinned scripts/production_like_course_specs.json@98a2352 that built courses.json. On a freshly seeded
local database, switching the reader changes nothing visible: the seed writes the same 12 cohorts,
so Machine Learning and LLM still resolve to 2025 and AI Dev Tools still resolves to ai-dev-tools-2025. A verifier who checks only a seeded database will wrongly conclude the fix does
nothing.
Verification must therefore use a database that actually contains ml-zoomcamp-2026, llm-zoomcamp-2026 and ai-dev-tools-zoomcamp-2026. Two documented paths produce one:
_docs/runbooks/local-course-content-review.md (CMP snapshot import), plus
_docs/runbooks/local-course-modules-preparation.md, which is explicitly the flow that supplies
the LLM / ML / AI Dev Tools 2026 cohorts the snapshot does not contain.
Focused Django tests must construct cohorts directly rather than going through the pinned seed, so
they are independent of the pin.
Non-goals
Regenerating content/public_projection/courses.json. scripts/build_public_projection.py:2435
emits "public_path": f"/courses/{slug}" → /courses/de-zoomcamp-2026, while the checked artifact
holds /courses/de-zoomcamp/2026. The builder cannot reproduce its own committed output, and /courses/de-zoomcamp-2026 resolves to course_family_view with no matching family — a 404. Regeneration would rewrite every homepage course link into a 404. This is recorded here as
evidence for Reconcile non-reproducible public projection after face8e4 #253 and is an explicit non-goal.
Deleting courses.json, review_projection.json, or any projection artifact.
The ai-dev-tools vs ai-dev-tools-zoomcampURL/redirect question. /courses/ai-dev-tools/2025
keeps resolving; whether it should redirect is a separate URL-identity issue.
Adding format / price / notice fields to Cohort, or any other schema change or migration.
Adding /courses/<family>/<year> cohort pages to the sitemap. The sitemap lists family paths only;
that is pre-existing and unchanged here.
Deduplicating the two AI Dev Tools Course rows in the database (see Owner decisions).
Dependencies
None blocking.#253, #297, #301 and #302 are related but supply no model, interface, decision or
infrastructure prerequisite for this reader change, and this change deliberately does not touch the
artifacts they are red on. #276 is not a dependency; it explicitly defers this classification here.
Owner decisions requested (do not block implementation)
These are escalated rather than decided. The stated default ships if no direction arrives, because
the site is currently advertising superseded cohorts and this is P0.
Two visible AI Dev Tools Course rows. The database holds ai-dev-tools (title
"AI Dev Tools Zoomcamp", cohort ai-dev-tools-2025) andai-dev-tools-zoomcamp (same title,
cohort ai-dev-tools-zoomcamp-2026), both visible = 1. /courses already renders two
identically-titled family cards because of this. A repair (merging the families) is a data
migration with URL consequences and is out of scope here. Default for this issue: the homepage renders exactly one AI Dev Tools card and one
featured hero, resolved to the newest cohort across the AI Dev Tools family slugs, via a small
reviewed alias entry in the core/home_content.py presentation table. This is presentation-only,
changes no URL and no row, and is reversible. /courses keeps showing both cards until the data
is repaired; that inconsistency is knowingly accepted for the duration and must be filed as its
own issue.
Upstream test rows on a public surface._docs/runbooks/local-course-content-review.md:186-188
records that the CMP snapshot contains fake-course and fake-course-2 with visible = 1, each
becoming its own visible family. courses/views/course_list.py:130 already carries a title.lower().startswith("fake") guard, which is a smell. Once the homepage is
database-backed, a database carrying those rows would advertise them on the front page. Default: the reader gains no name-based denylist; it renders only visible=True rows, and
hiding upstream test rows is an operator data fix that must be added to the apex-swap
data-freshness gate in _docs/runbooks/production-hosting-and-dns-migration.md. Confirm that the
cutover database has no visible fixture course.
Acceptance criteria
Source of truth
[AUTO] core/home_content.py no longer imports or calls public_projection for course data,
and public_projection()["courses"] has no runtime reader anywhere in the application.
[AUTO] core/views.py and content/review_views.py no longer read review_projection()["course"].
[AUTO] content/public_projection/courses.json and content/review_projection.json are
unchanged byte-for-byte; EXPECTED_COUNTS["courses"] == 12, PROJECTION_TREE_SHA256 and PROJECTION_MANIFEST_SHA256 are unchanged and still validate.
[AUTO] A regression test asserts that no course entry in production_sitemap() derives from courses.json.
Correct cohort selection
[AUTO] Against a database containing ml-zoomcamp-2025andml-zoomcamp-2026, the
homepage catalogue links Machine Learning to /courses/ml-zoomcamp/2026 and shows
"2026 cohort"; the 2025 link does not appear in the catalogue.
[AUTO] The same holds for LLM Zoomcamp (/courses/llm-zoomcamp/2026).
[AUTO] MLOps and Stock Markets, whose newest visible cohorts are 2025, still link to /courses/mlops-zoomcamp/2025 and /courses/sma-zoomcamp/2025.
[AUTO] Exactly one AI Dev Tools card and exactly one data-featured-course element render,
resolved to ai-dev-tools-zoomcamp-2026 at /courses/ai-dev-tools-zoomcamp/2026.
[AUTO] A visible=False cohort, and any cohort of a visible=False course, never reaches the
homepage catalogue, the hero, the cohort page or the registration preview.
[AUTO] A course family present in the database but absent from the core/home_content.py
presentation table still renders, labelled from its Course.title.
Copy and counts
[AUTO] Hero and cohort-page copy that has no database field (format, price, notice, the
hero summary, FEATURED_BUILD_ITEMS, FEATURED_GROUP_NOTE) renders the same strings as
before this change, now from page-owned constants.
[AUTO] No raw markdown, HTML tag, external image reference or courses.datatalks.club link
from Course.description or Cohort.description reaches any rendered page.
[AUTO] The featured panel handles a zero count: ai-dev-tools-zoomcamp-2026 has 4 homework
assignments and 0 projects, and the page must not render "0 projects". The projects clause
is omitted when the count is zero, and singular/plural is correct for a count of one.
[AUTO] course_family_count and course_family_word are derived from the rendered catalogue,
so the "One free account. Six courses." line cannot contradict the cards above it.
Empty and failure states
[AUTO] With an empty database, / returns 200 with an empty course catalogue and no
featured hero, and raises no ImproperlyConfigured, StopIteration or NoReverseMatch.
[AUTO] With a database holding some but not all families, / returns 200 and renders only the
families that exist.
[AUTO] With no AI Dev Tools cohort at all, /, /courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026 and its /registration-preview/ return a designed empty or 404 state, never a 500.
URLs and compatibility
[AUTO] /courses, /courses/<family>, /courses/<family>/<year>, /courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026 and /courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/ keep their
exact paths, URL names and status codes.
[AUTO] The preserved-path baseline check and the redirect/compatibility suites pass unchanged.
[AUTO] Every course link rendered on / resolves to a 200 on the same database.
Documentation
[AUTO] The courses/services/local_course_seed.py docstring no longer claims specification 03
keeps public requests on the course projection.
[AUTO] _docs/runbooks/local-course-content-review.md no longer states that the homepage does
not read the database.
Verification integrity
[AUTO] Focused Django tests build cohorts directly, not through courses.services.local_course_seed, so they do not silently depend on the 98a2352 pin.
[HUMAN] The tester confirms the browser evidence was captured against a database containing ml-zoomcamp-2026, llm-zoomcamp-2026 and ai-dev-tools-zoomcamp-2026, and records how that
database was produced. Evidence from a freshly seeded pinned database does not satisfy any
criterion in this issue.
Django / integration scenarios
Empty database → GET / returns 200, no course cards, no featured hero, no exception.
One family with 2024/2025/2026 visible cohorts → the catalogue shows one card for 2026 only.
2026 cohort marked visible=False → the card falls back to 2025 and links to /courses/<family>/2025.
Course marked visible=False → the family disappears from the catalogue entirely.
Two cohorts in the same year → selection is deterministic by (year, start_date, id).
A family in the database that is absent from the presentation table renders with its Course.title.
Cohort with zero projects → the featured panel omits the projects clause.
production_sitemap() course entries are unchanged by this issue and independent of courses.json.
core/views.py and content/review_views.py import graphs contain no review_projection
course read.
Browser scenarios (desktop 1280×800 and mobile 390×844, light and dark)
/ with the real 2026 dataset. The Machine Learning card reads "2026 cohort" and links to /courses/ml-zoomcamp/2026; LLM reads "2026 cohort"; the AI Dev Tools hero shows the 2026 cohort
title and start date; exactly one AI Dev Tools card is present; the "Six courses" line matches the
number of cards. Follow the Machine Learning link and confirm a 200 course page for the 2026
cohort.
/ with an empty database. The page renders its design-system empty state, not an error or a
debug page.
/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026. Title, start date, format, price
and notice render as before, and the "Open the existing course workspace" call to action now
resolves to a real cohort instead of being silently absent.
/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/. Heading and
breadcrumb carry the database cohort title.
/courses is visually and factually consistent with / for every family except the known,
escalated AI Dev Tools duplicate.
No horizontal overflow and no broken layout at either width.
Repository / operations scenarios
git status shows no change to content/public_projection/ or content/review_projection.json.
Content digest, manifest, inventory and tree-reproducibility checks pass unchanged.
Migration-drift check reports no migration (this issue adds none).
The versioned verification plan in _docs/ci/change-selective-ci.md is generated with recorded
base/head, graph and plan digests, and every rerun / reused / skipped / not_applicable
disposition is justified. Screenshot components may not be marked not_applicable: this change
has render impact on / and on both review routes.
Why P0
_docs/runbooks/production-hosting-and-dns-migration.md has this site replacing datatalks.club.
Cutting over while the front page advertises superseded cohorts would send real prospective learners
to the wrong cohort. This must be resolved before the apex swap, and it is now known that refreshing
the database alone cannot fix it.
Related
#253 (projection not reproducible — this issue supplies the concrete public_path divergence as
evidence), #276 (editorial direct-sync cutover; defers course surfaces here), #297 (CMP adoption
ledger), #301 (projection media), #302 (inventory validator).
Original report (raw intake, preserved)
Raw user intake — needs PM grooming. Reported by the owner while reviewing the running site: "for ml zoomcamp there is actually a 2026 cohort — did we not sync all the data to the local db? Let us make sure that ALL THE DATA is up to date."
Symptom
The homepage advertises 2025 cohorts for courses that have open 2026 cohorts. Prospective learners would see and click through to a stale cohort.
Root cause
The homepage reads the checked-in projection, not the database — core/home_content.py:258 calls _latest_cohort_records(tuple(public_projection()["courses"])), and core/home_content.py:12 imports public_projection from content.public_data.
content/public_projection/courses.json holds 12 entries; the local database holds 15. Comparison:
Course
Projection (rendered)
Database
Data Engineering
de-zoomcamp-2026
de-zoomcamp-2026
Machine Learning
ml-zoomcamp-2025
ml-zoomcamp-2026
LLM
llm-zoomcamp-2025
llm-zoomcamp-2026
AI Dev Tools
ai-dev-tools-2025
ai-dev-tools-zoomcamp-2026
MLOps
mlops-zoomcamp-2025
mlops-zoomcamp-2025
Stock Markets
sma-zoomcamp-2025
sma-zoomcamp-2025
The projection is built from DataTalksClub/course-management-platform@98a235283904b4ef9ad29e196298540756cf1bcc, pinned 2026-08-04. Cohorts created upstream after that date are absent.
Note the AI Dev Tools slug also differs (ai-dev-tools-2025 vs ai-dev-tools-zoomcamp-2026), so this is a URL/identity question, not only a display label.
What grooming must decide
Which source is authoritative for public course display — the projection or the database? This is the core question. The projection exists for reproducibility and is bound to content-authority digests; the database is current. Today the homepage uses the projection and the seed command uses scripts/production_like_course_specs.json pinned to the same revision, so a DB-only refresh would not fix the site.
How does course data stay fresh going forward? A pinned projection guarantees reproducibility but guarantees staleness. Cohorts are created continuously upstream. Whatever is chosen must answer: when a new cohort opens upstream, what makes it appear on the site, and how quickly?
Moving the pin has known costs. A CMP upstream audit found a forward sync to origin/main is status=blocked with 3 fatal errors and 45 overlay conflicts, plus four new migrations. That audit recommended NOT moving the pin for code. The course catalogue may be separable from the code sync — determine whether the catalogue can be refreshed independently of the adoption ledger.
Regeneration blast radius. Rebuilding courses.json changes manifest.json digests, tree_sha256, and EXPECTED_COUNTS (content/public_data.py:66 pins "courses": 12). It touches scripts/production_like_course_specs.json, courses/services/local_course_seed.py, and the parity contracts in content_sync/dtc_content/. This interacts with Reconcile non-reproducible public projection after face8e4 #253 (projection reproducibility) and Restore the GitHub editorial source/projection inventory validator on main #302 (inventory validator drift) — both currently red on main. Sequencing matters.
AI Dev Tools slug change — if the canonical slug becomes ai-dev-tools-zoomcamp-2026, does /courses/ai-dev-tools-2025 need a redirect? Check against the 2,937-row preservation baseline.
Why this is P0
The migration runbook (_docs/runbooks/production-hosting-and-dns-migration.md) has this site replacing datatalks.club. Cutting over while advertising superseded cohorts would send real prospective learners to the wrong place. This should be resolved before the apex swap, not after.
Related
#253 (projection not reproducible after face8e4), #302 (inventory validator red), #297 (CMP adoption ledger, broken sync script), #301 (media out of the projection), #276 (cut public reads over to direct-sync authority) — #276 may be the strategic answer to question 2.
Groomed by the product manager. Raw intake preserved below under "Original report".
Summary
The public homepage advertises 2025 cohorts for Machine Learning, LLM and AI Dev Tools while the
database holds live 2026 cohorts for all three. The cause is a source-of-truth deviation, not stale
data:
core/home_content.pyandcontent/review_views.pyread checked-in JSON projections forcourse facts, while
courses.Cohortis the specification-mandated owner of that data.Owner direction: "We don't read projections. It must come from the database."
This is spec-conforming, not a spec amendment. No specification change is required and none is
in scope.
Normative basis
_docs/specs/README.mdFixed requirements: "Django serves the public site from validated,versioned database read models."
_docs/specs/01-platform-architecture.md"Data ownership": courses, cohorts, teaching teams,course registrations, enrollments, assignments… are Database-owned and editable through
Studio/API. Courses appear nowhere in the GitHub-owned list.
_docs/specs/04-courses-and-cohorts.mdis entirely a database model specification. It nevermentions a course projection.
_docs/architecture/app-boundaries.md:coursesowns "database-owned courses, cohorts, andlearner workflows";
contentowns "versioned GitHub-owned read models".Reading
content/public_projection/courses.jsonfor public course display is therefore a deviationfrom specifications 01 and 04.
Ownership classification (settles the #276 hand-off)
#276 explicitly declines to own this and asks for a classification: "The manifest must state which
course-owned surfaces, if any, are direct-sync content rather than course-domain reads."
Classification: every surface listed in Scope is
course_domain. None of them is editorialcontent and none of them is direct-sync content. The work is owned here. If the owner disagrees
with that classification, this issue closes as won't-fix and the surfaces return to #276.
Verified root cause
core/home_content.py:258is the only runtime reader ofpublic_projection()["courses"]:The artifact holds 12 cohorts pinned to
DataTalksClub/course-management-platform@98a235283904b4ef9ad29e196298540756cf1bcc(2026-08-04).The current course dataset holds 15 cohorts across 7 course families.
Verified against the current 15-cohort dataset:
Course.slug)de-zoomcamp/courses/de-zoomcamp/2026/courses/de-zoomcamp/2026ml-zoomcamp/courses/ml-zoomcamp/2025/courses/ml-zoomcamp/2026llm-zoomcamp/courses/llm-zoomcamp/2025/courses/llm-zoomcamp/2026mlops-zoomcamp/courses/mlops-zoomcamp/2025/courses/mlops-zoomcamp/2025sma-zoomcamp/courses/sma-zoomcamp/2025/courses/sma-zoomcamp/2025/courses/ai-dev-tools/2025/courses/ai-dev-tools-zoomcamp/2026The URL grammar does not change.
Cohort.identifieris"2026",Course.slugis the family,and
reverse("course", args=[family.slug, cohort.identifier])reproduces exactly the/courses/<family>/<identifier>shape the projection already emits. No route, no redirect, and nopreserved path is touched.
Reuse, do not rewrite
courses/views/course_list.py:76visible_course_list_queryset()already serves/coursesfromthe ORM with
homework_count,project_countandlearner_countannotations — exactly the fieldscore/home_content.py:265-275builds from JSON. Reuse it.templates/core/home.html:1058-1065consumes only
.label,.title,.cohort_labeland.public_path, andtemplates/core/home.html:1028-1029consumes.homework_countand.project_count; all areavailable from that queryset, so the catalogue template needs no structural change.
Scope
1.
core/home_content.pycourse_catalog()— the live defectRebuild
CatalogCourseconstruction onvisible_course_list_queryset():Coursewhose newest visibleCohortis selected by(year, start_date, id)descending;public_path=reverse("course", args=[course.slug, cohort.identifier]);cohort_label=f"{cohort.year} cohort"(unchanged copy);homework_count/project_countfrom the existing annotations.COURSE_FAMILIES(core/home_content.py:18-49) stops being the list of courses that exist. It mayremain only as a presentation table supplying the short chip label and the preferred display
order. The database decides which families exist; a family the database holds but the table does
not must still render, using its
Course.titleas the chip label. An allowlist that silently dropsa new upstream family would reproduce this same staleness bug in a new place and is not acceptable.
core/home_content.py:263currently raisesImproperlyConfiguredwhen a family is missing. Apartially populated database would therefore return 500 on the homepage. It must instead render
an empty catalogue, matching the existing
/coursesempty state.2.
core/views.py:73— the featured-cohort heroreview_projection()["course"]supplies the homepage hero's course identity, cohort title, startdate and description. Move the facts to the database; keep the editorial copy page-owned.
homework/project counts.
core/home_content.py(same pattern as the existingFEATURED_BUILD_ITEMS,FEATURED_GROUP_NOTE,MEMBER_STORIES): the hero summary,format,priceandnoticestrings.Cohorthas noformat,priceornoticefield, andCohort.descriptionfor the 2026 rows is generated boilerplate(
"The 2026 live delivery of AI Dev Tools Zoomcamp.") whileCourse.descriptionforai-dev-tools-zoomcampis raw README markup containing external image tags andcourses.datatalks.clublinks. Reading either into the hero would be a copy regression and amarkup-injection risk. Move today's exact strings into page constants; do not change a word.
format/price/noticemodel fields is a schema change and is a non-goal here.core/views.py:87-89usesnext(entry for entry in catalog if entry.family == FEATURED_FAMILY),which raises
StopIteration(500) when that family is absent. It must fail soft.3.
content/review_views.py:379course_cohort— the cohort pageSame split: cohort title, start date and the linked platform cohort come from
courses.Cohort;format,priceandnoticestay page-owned constants with today's exact strings.content/review_views.py:382currently doesCohort.objects.filter(slug=cohort["legacy_platform_slug"]).first()withlegacy_platform_slug = "ai-dev-tools-2026". No cohort with that slug exists — the dataset holdsai-dev-tools-2025andai-dev-tools-zoomcamp-2026— solegacy_courseisNoneand the"Open the existing course workspace" call to action (
templates/review/course_cohort.html:163-169)is silently missing today. Resolving the cohort from the database fixes that link.
4.
content/review_views.py:399registration_previewThe cohort title in the heading and breadcrumb comes from the database.
5.
content/public_views.py:1200sitemap — verification onlyThe
coursessitemap section already readsCohort.objects.filter(visible=True, course__visible=True).It is the proof that the pattern exists and needs no change. It must be covered by a regression
assertion that no course entry in the sitemap derives from
courses.json.6. Documentation corrections (in scope)
courses/services/local_course_seed.py:8-15claims "Specification 03 keeps public requests onthat projection." Specification 03 is GitHub content and people; it says nothing about the CMP
course catalogue. This over-claim is the most likely reason the deviation went unchallenged.
Correct it.
_docs/runbooks/local-course-content-review.md:193-195states "the public homepage does not readthis database at all… Refreshing the review database therefore cannot by itself change what the
homepage advertises." That becomes false with this change. Update it.
7. Tests that assert the current premise
These fail or become meaningless and must be rewritten, not deleted:
core/tests/test_homepage.py:46test_root_uses_the_shared_course_platform_shellrenders/against an empty database and asserts
"AI Dev Tools Zoomcamp","Starts August 31"and thefeatured-cohort link. It must build cohorts in the test database.
core/tests/test_homepage.py:104test_single_destination_cards_stretch_their_existing_semantic_linksasserts a
course-cardexists in the rendered homepage.playwright_tests/test_course_design_parity.py:677test_no_database_course_catalog_uses_the_design_system_empty_stateassertsnot Cohort.objects.exists(). It currently covers only/courses; with the homepage nowdatabase-backed, the same premise must also assert that
/renders 200 with an empty cataloguerather than 500.
Design rules
courses.Course/courses.Cohort.exact wording. It is never invented and never read from a projection.
visible=Truecohorts ofvisible=Truecourses reach a public surface./courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026and/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/keep their exactpaths and URL names; only their data source changes.
Blast radius (verified, and deliberately small)
/courses/<family>/<year>appears nowhere in the 2,937-row_docs/compatibility/generated-path-baseline.jsonl(the only/courses/*legacy row is/courses/2021-winter-ml-zoomcamp.html).content/public_projection/courses.jsonstays in the tree,byte-identical.
EXPECTED_COUNTS["courses"] = 12(content/public_data.py:65),PROJECTION_TREE_SHA256andPROJECTION_MANIFEST_SHA256(
content_sync/dtc_content/contract.py:32-34) all keep validating the unchanged artifact.Leaving the artifact in place is an explicit requirement of this issue, not an oversight.
content/review_projection.jsonlikewise stays in the tree; thecontent/apps.pystartup checkand
REQUIRED_PUBLIC_PATHSkeep passing unchanged.The verification trap — read this before testing
courses/services/local_course_seed.py:68-75seeds the local database from the same pinnedscripts/production_like_course_specs.json@98a2352that builtcourses.json. On a freshly seededlocal database, switching the reader changes nothing visible: the seed writes the same 12 cohorts,
so Machine Learning and LLM still resolve to 2025 and AI Dev Tools still resolves to
ai-dev-tools-2025. A verifier who checks only a seeded database will wrongly conclude the fix doesnothing.
Verification must therefore use a database that actually contains
ml-zoomcamp-2026,llm-zoomcamp-2026andai-dev-tools-zoomcamp-2026. Two documented paths produce one:_docs/runbooks/local-course-content-review.md(CMP snapshot import), plus_docs/runbooks/local-course-modules-preparation.md, which is explicitly the flow that suppliesthe LLM / ML / AI Dev Tools 2026 cohorts the snapshot does not contain.
Focused Django tests must construct cohorts directly rather than going through the pinned seed, so
they are independent of the pin.
Non-goals
content/public_projection/courses.json.scripts/build_public_projection.py:2435emits
"public_path": f"/courses/{slug}"→/courses/de-zoomcamp-2026, while the checked artifactholds
/courses/de-zoomcamp/2026. The builder cannot reproduce its own committed output, and/courses/de-zoomcamp-2026resolves tocourse_family_viewwith no matching family — a 404.Regeneration would rewrite every homepage course link into a 404. This is recorded here as
evidence for Reconcile non-reproducible public projection after face8e4 #253 and is an explicit non-goal.
courses.json,review_projection.json, or any projection artifact.ai-dev-toolsvsai-dev-tools-zoomcampURL/redirect question./courses/ai-dev-tools/2025keeps resolving; whether it should redirect is a separate URL-identity issue.
format/price/noticefields toCohort, or any other schema change or migration./courses/<family>/<year>cohort pages to the sitemap. The sitemap lists family paths only;that is pre-existing and unchanged here.
Courserows in the database (see Owner decisions).Dependencies
None blocking. #253, #297, #301 and #302 are related but supply no model, interface, decision or
infrastructure prerequisite for this reader change, and this change deliberately does not touch the
artifacts they are red on. #276 is not a dependency; it explicitly defers this classification here.
Owner decisions requested (do not block implementation)
These are escalated rather than decided. The stated default ships if no direction arrives, because
the site is currently advertising superseded cohorts and this is P0.
Courserows. The database holdsai-dev-tools(title"AI Dev Tools Zoomcamp", cohort
ai-dev-tools-2025) andai-dev-tools-zoomcamp(same title,cohort
ai-dev-tools-zoomcamp-2026), bothvisible = 1./coursesalready renders twoidentically-titled family cards because of this. A repair (merging the families) is a data
migration with URL consequences and is out of scope here.
Default for this issue: the homepage renders exactly one AI Dev Tools card and one
featured hero, resolved to the newest cohort across the AI Dev Tools family slugs, via a small
reviewed alias entry in the
core/home_content.pypresentation table. This is presentation-only,changes no URL and no row, and is reversible.
/courseskeeps showing both cards until the datais repaired; that inconsistency is knowingly accepted for the duration and must be filed as its
own issue.
_docs/runbooks/local-course-content-review.md:186-188records that the CMP snapshot contains
fake-courseandfake-course-2withvisible = 1, eachbecoming its own visible family.
courses/views/course_list.py:130already carries atitle.lower().startswith("fake")guard, which is a smell. Once the homepage isdatabase-backed, a database carrying those rows would advertise them on the front page.
Default: the reader gains no name-based denylist; it renders only
visible=Truerows, andhiding upstream test rows is an operator data fix that must be added to the apex-swap
data-freshness gate in
_docs/runbooks/production-hosting-and-dns-migration.md. Confirm that thecutover database has no visible fixture course.
Acceptance criteria
Source of truth
core/home_content.pyno longer imports or callspublic_projectionfor course data,and
public_projection()["courses"]has no runtime reader anywhere in the application.core/views.pyandcontent/review_views.pyno longer readreview_projection()["course"].content/public_projection/courses.jsonandcontent/review_projection.jsonareunchanged byte-for-byte;
EXPECTED_COUNTS["courses"] == 12,PROJECTION_TREE_SHA256andPROJECTION_MANIFEST_SHA256are unchanged and still validate.production_sitemap()derives fromcourses.json.Correct cohort selection
ml-zoomcamp-2025andml-zoomcamp-2026, thehomepage catalogue links Machine Learning to
/courses/ml-zoomcamp/2026and shows"2026 cohort"; the 2025 link does not appear in the catalogue.
/courses/llm-zoomcamp/2026)./courses/mlops-zoomcamp/2025and/courses/sma-zoomcamp/2025.data-featured-courseelement render,resolved to
ai-dev-tools-zoomcamp-2026at/courses/ai-dev-tools-zoomcamp/2026.visible=Falsecohort, and any cohort of avisible=Falsecourse, never reaches thehomepage catalogue, the hero, the cohort page or the registration preview.
core/home_content.pypresentation table still renders, labelled from its
Course.title.Copy and counts
format,price,notice, thehero summary,
FEATURED_BUILD_ITEMS,FEATURED_GROUP_NOTE) renders the same strings asbefore this change, now from page-owned constants.
courses.datatalks.clublinkfrom
Course.descriptionorCohort.descriptionreaches any rendered page.ai-dev-tools-zoomcamp-2026has 4 homeworkassignments and 0 projects, and the page must not render "0 projects". The projects clause
is omitted when the count is zero, and singular/plural is correct for a count of one.
course_family_countandcourse_family_wordare derived from the rendered catalogue,so the "One free account. Six courses." line cannot contradict the cards above it.
Empty and failure states
/returns 200 with an empty course catalogue and nofeatured hero, and raises no
ImproperlyConfigured,StopIterationorNoReverseMatch./returns 200 and renders only thefamilies that exist.
/,/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026and its/registration-preview/return a designed empty or 404 state, never a 500.URLs and compatibility
/courses,/courses/<family>,/courses/<family>/<year>,/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026and/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/keep theirexact paths, URL names and status codes.
/resolves to a 200 on the same database.Documentation
courses/services/local_course_seed.pydocstring no longer claims specification 03keeps public requests on the course projection.
_docs/runbooks/local-course-content-review.mdno longer states that the homepage doesnot read the database.
Verification integrity
courses.services.local_course_seed, so they do not silently depend on the 98a2352 pin.ml-zoomcamp-2026,llm-zoomcamp-2026andai-dev-tools-zoomcamp-2026, and records how thatdatabase was produced. Evidence from a freshly seeded pinned database does not satisfy any
criterion in this issue.
Django / integration scenarios
GET /returns 200, no course cards, no featured hero, no exception.visible=False→ the card falls back to 2025 and links to/courses/<family>/2025.visible=False→ the family disappears from the catalogue entirely.(year, start_date, id).Course.title.production_sitemap()course entries are unchanged by this issue and independent ofcourses.json.core/views.pyandcontent/review_views.pyimport graphs contain noreview_projectioncourse read.
Browser scenarios (desktop 1280×800 and mobile 390×844, light and dark)
/with the real 2026 dataset. The Machine Learning card reads "2026 cohort" and links to/courses/ml-zoomcamp/2026; LLM reads "2026 cohort"; the AI Dev Tools hero shows the 2026 cohorttitle and start date; exactly one AI Dev Tools card is present; the "Six courses" line matches the
number of cards. Follow the Machine Learning link and confirm a 200 course page for the 2026
cohort.
/with an empty database. The page renders its design-system empty state, not an error or adebug page.
/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026. Title, start date, format, priceand notice render as before, and the "Open the existing course workspace" call to action now
resolves to a real cohort instead of being silently absent.
/courses/ai-dev-tools-zoomcamp/cohorts/ai-dev-tools-2026/registration-preview/. Heading andbreadcrumb carry the database cohort title.
/coursesis visually and factually consistent with/for every family except the known,escalated AI Dev Tools duplicate.
Repository / operations scenarios
git statusshows no change tocontent/public_projection/orcontent/review_projection.json._docs/ci/change-selective-ci.mdis generated with recordedbase/head, graph and plan digests, and every
rerun/reused/skipped/not_applicabledisposition is justified. Screenshot components may not be marked
not_applicable: this changehas render impact on
/and on both review routes.Why P0
_docs/runbooks/production-hosting-and-dns-migration.mdhas this site replacingdatatalks.club.Cutting over while the front page advertises superseded cohorts would send real prospective learners
to the wrong cohort. This must be resolved before the apex swap, and it is now known that refreshing
the database alone cannot fix it.
Related
#253 (projection not reproducible — this issue supplies the concrete
public_pathdivergence asevidence), #276 (editorial direct-sync cutover; defers course surfaces here), #297 (CMP adoption
ledger), #301 (projection media), #302 (inventory validator).
Original report (raw intake, preserved)
Raw user intake — needs PM grooming. Reported by the owner while reviewing the running site: "for ml zoomcamp there is actually a 2026 cohort — did we not sync all the data to the local db? Let us make sure that ALL THE DATA is up to date."
Symptom
The homepage advertises 2025 cohorts for courses that have open 2026 cohorts. Prospective learners would see and click through to a stale cohort.
Root cause
The homepage reads the checked-in projection, not the database —
core/home_content.py:258calls_latest_cohort_records(tuple(public_projection()["courses"])), andcore/home_content.py:12importspublic_projectionfromcontent.public_data.content/public_projection/courses.jsonholds 12 entries; the local database holds 15. Comparison:de-zoomcamp-2026de-zoomcamp-2026ml-zoomcamp-2025ml-zoomcamp-2026llm-zoomcamp-2025llm-zoomcamp-2026ai-dev-tools-2025ai-dev-tools-zoomcamp-2026mlops-zoomcamp-2025mlops-zoomcamp-2025sma-zoomcamp-2025sma-zoomcamp-2025The projection is built from
DataTalksClub/course-management-platform@98a235283904b4ef9ad29e196298540756cf1bcc, pinned 2026-08-04. Cohorts created upstream after that date are absent.Note the AI Dev Tools slug also differs (
ai-dev-tools-2025vsai-dev-tools-zoomcamp-2026), so this is a URL/identity question, not only a display label.What grooming must decide
Which source is authoritative for public course display — the projection or the database? This is the core question. The projection exists for reproducibility and is bound to content-authority digests; the database is current. Today the homepage uses the projection and the seed command uses
scripts/production_like_course_specs.jsonpinned to the same revision, so a DB-only refresh would not fix the site.How does course data stay fresh going forward? A pinned projection guarantees reproducibility but guarantees staleness. Cohorts are created continuously upstream. Whatever is chosen must answer: when a new cohort opens upstream, what makes it appear on the site, and how quickly?
Moving the pin has known costs. A CMP upstream audit found a forward sync to
origin/mainisstatus=blockedwith 3 fatal errors and 45 overlay conflicts, plus four new migrations. That audit recommended NOT moving the pin for code. The course catalogue may be separable from the code sync — determine whether the catalogue can be refreshed independently of the adoption ledger.Regeneration blast radius. Rebuilding
courses.jsonchangesmanifest.jsondigests,tree_sha256, andEXPECTED_COUNTS(content/public_data.py:66pins"courses": 12). It touchesscripts/production_like_course_specs.json,courses/services/local_course_seed.py, and the parity contracts incontent_sync/dtc_content/. This interacts with Reconcile non-reproducible public projection after face8e4 #253 (projection reproducibility) and Restore the GitHub editorial source/projection inventory validator on main #302 (inventory validator drift) — both currently red onmain. Sequencing matters.AI Dev Tools slug change — if the canonical slug becomes
ai-dev-tools-zoomcamp-2026, does/courses/ai-dev-tools-2025need a redirect? Check against the 2,937-row preservation baseline.Why this is P0
The migration runbook (
_docs/runbooks/production-hosting-and-dns-migration.md) has this site replacingdatatalks.club. Cutting over while advertising superseded cohorts would send real prospective learners to the wrong place. This should be resolved before the apex swap, not after.Related
#253 (projection not reproducible after face8e4), #302 (inventory validator red), #297 (CMP adoption ledger, broken sync script), #301 (media out of the projection), #276 (cut public reads over to direct-sync authority) — #276 may be the strategic answer to question 2.