Skip to content

AI Dev Tools renders as two separate course families, orphaning the 2026 cohort #308

Description

@alexeygrigorev

Product outcome

The course catalogue has one AI Dev Tools family. The 2025 and 2026 cohorts are
owned by that family, the 2026 curriculum import cannot recreate a sibling family,
and an already-split database can be repaired without recreating learner or
curriculum rows.

This issue is a bounded identity/data-repair slice. It does not redesign the
Course/Cohort model or move the public course reader to a new source of truth.

Normative authority

Accepted identity contract

The owner decision in the issue comment is authoritative:

Field AI Dev Tools value Meaning
Course.slug ai-dev-tools Reusable family/public catalogue identity
Cohort.slug ai-dev-tools-2026 Globally unique retained legacy cohort slug; it is not the family slug
Cohort.identifier 2026 Stable cohort route segment scoped to Course; it is not inferred from a slug at request time
Cohort.year 2026 Schedule/display metadata, not the route identity
source_stable_id ai-dev-tools Canonical source/import identity for this course family
repository DataTalksClub/ai-dev-tools-zoomcamp Source provenance; the repository name is not the public family identity

The accepted canonical HTML route is /courses/ai-dev-tools/2026 (the accepted
no-cohorts/ syntax from #16). The legacy CMP route
https://courses.datatalks.club/ai-dev-tools-2026/ remains the observed
compatibility path. Existing /docs/.../ai-dev-tools-zoomcamp/..., FAQ, and blog
paths belong to other source contracts and are out of scope.

For existing published families whose canonical slug really includes
-zoomcamp (for example ml-zoomcamp), this issue does not rename them. The rule
is an explicit reviewed repository-to-family mapping: a repository suffix must not
mint a second family when its reviewed canonical family already exists. Unknown or
new divergent identities require an explicit mapping; runtime regex/year stripping
or a blanket suffix rewrite is not an identity policy.

Current compatibility evidence

  • _docs/compatibility/generated-path-baseline.jsonl contains no application path
    for /courses/ai-dev-tools or /courses/ai-dev-tools-zoomcamp; its AI Dev Tools
    rows are docs, FAQ, blog, and media contracts.
  • A read-only production probe on 2026-09-02 returned 200 for
    /ai-dev-tools-2026/ and 404 for /ai-dev-tools-zoomcamp-2026/ on
    courses.datatalks.club.
  • Therefore no redirect for the losing application family slug is owed or may be
    invented. If a future fresh crawl proves a previously served alias, stop and
    return that compatibility change to PM with its exact one-hop destination.

Scope

Source identity and recurrence prevention

  • Put the reviewed mapping in a checked-in generator/configuration boundary, not
    only in the gitignored .local/scratch manifest. A regenerated AI Dev Tools
    manifest must emit source_stable_id=ai-dev-tools while retaining the repository
    owner/name for provenance.
  • Resolve the repository-declared ai-dev-tools-zoomcamp course slug to the one
    reviewed family and the ai-dev-tools-2026 cohort slug. Preserve source content
    IDs and checksums; do not make a source identity claim from a title, year, or
    repository-name suffix alone.
  • The importer, local preparation path, and review/import adapter must use the
    same explicit mapping. A source/content identity collision, source-stable-ID
    conflict, or unmapped divergent source rejects before any database mutation.
  • Keep the existing source pin
    DataTalksClub/course-management-platform@98a235283904b4ef9ad29e196298540756cf1bcc.
    This issue is not permission to sync CMP main or port unrelated upstream code.

Existing-database repair

  • Add an idempotent repair migration or migration-owned helper against the current
    Course/Cohort schema. The migration identity rule is frozen independently of
    mutable runtime catalog code so historical replay remains deterministic.
  • The exact ai-dev-tools family is the survivor when both family rows exist. Move
    the orphaned cohorts to it by updating their family FK; preserve every Cohort
    primary key/UUID, identifier, year, and all downstream foreign keys (modules,
    units, homework, projects, campaigns, registrations, enrollments, submissions,
    reviews, scores, certificates, and other history).
  • Rewrite only the exact duplicate prefix
    ai-dev-tools-zoomcamp- to ai-dev-tools- for retained cohort slugs, including
    the 2026 row. Preserve unrelated legacy slugs and never derive an identifier from
    a year during repair.
  • If the canonical row is absent but the reviewed duplicate is the only row, reuse
    that row/primary key under the canonical slug rather than creating a second row.
    A healthy one-family database is a no-op.
  • Adopt source provenance onto the surviving family only under the reviewed identity
    rule. Conflicting non-null source IDs, source content IDs, cohort identifiers,
    years, destination slugs, or incompatible existing cohorts fail closed with a
    bounded diagnostic. The transaction must leave Course/Cohort and all dependent
    state unchanged on such a conflict (apart from a safe failure/audit record); it
    must never discard or silently choose between learner/history records.
  • Delete a duplicate family only after all cohort references have moved and the
    reference/integrity check passes. The repair must be safe to invoke/replay twice
    and must report redacted aggregate counts and conflict reasons only.

Test-only rows

  • Exact upstream fixture rows fake-course and fake-course-2 are development/test
    data, not public courses. If present in a review or production-like input, the
    adopted public dataset must exclude them (or mark them non-public before the
    catalogue query); the exclusion is explicit and exact, not a broad fake-* or
    substring filter.
  • Keep those rows available to isolated characterization fixtures where useful, and
    prove that they do not affect public family counts, routes, or migration totals.
    Unclassified visible rows fail closed rather than being silently treated as test
    data.

Freshness and provenance gate

  • Every accepted import/repair report records the exact source repository, source
    commit, manifest/checksum, migration leaf, and aggregate family/cohort counts.
    No report, log, screenshot, or issue comment contains learner data, protected
    exports, credentials, or raw source payloads.
  • Synthetic tests are not evidence that a deployed database is fresh. The eventual
    operator gate must run against the actual rollout target, using aggregate-only
    facts, and verify the course/cohort source was re-synced within the existing
    migration runbook window (≤72 hours before the cutover gate), the accepted source
    commit/provenance is current, and no failed/stale import is active.
  • The operator must verify after the exact release is deployed that /courses has
    one AI Dev Tools family, that both 2025 and 2026 cohorts are under it, that
    /courses/ai-dev-tools/2026 is the canonical 200, and that the unrecorded
    ai-dev-tools-zoomcamp family path is a real 404 with no redirect chain. A
    failed check holds rollout and triggers re-freeze/reconciliation; it is not
    waived as a pre-existing data condition.

Acceptance criteria

  • The checked-in reviewed mapping/generator emits the canonical AI Dev Tools
    source_stable_id, family slug, cohort slug, and route identifier above on every
    regeneration; the repository name remains provenance only.
  • Fresh and repeated imports produce one Course family (ai-dev-tools) with
    the 2025 and 2026 cohorts under it, and cannot create the ai-dev-tools-zoomcamp
    sibling or silently group an unmapped divergent source.
  • Course.slug, Cohort.slug, Cohort.identifier, and Cohort.year are kept
    distinct in importer, seed, URL, and compatibility tests; the canonical route
    uses the family slug plus identifier and does not infer identity from the year.
  • The repair migration/helper handles healthy, canonical-only, duplicate-only,
    and split-family databases idempotently; it preserves cohort UUID/PKs and every
    downstream FK, re-prefixes only the reviewed duplicate cohort slug, and removes
    no learner/history/curriculum row.
  • Source/provenance, cohort identifier/year/slug, and same-year/same-identifier
    collisions fail atomically with bounded redacted diagnostics; a second invocation
    after success is a no-op with the same safe aggregate result.
  • Exact fake-course and fake-course-2 fixture rows cannot enter the adopted
    public catalogue, while real/unclassified rows are not hidden by a broad pattern;
    this behavior is covered by import and public-count tests.
  • The compatibility evidence remains unchanged: no redirect is added for an
    application path absent from the preservation baseline, and the separate docs,
    FAQ, blog, and media -zoomcamp paths remain byte/route-compatible.
  • Migration-forward, apply-twice, rollback/abort, backend-portability, source
    regeneration, importer replay/conflict, route/404, FK-integrity, and PII/log
    canary tests pass on one frozen candidate with no migration drift.
  • Because the catalogue and course pages visibly change, the independent tester
    runs the graph-selected Playwright tier and captures/inspects desktop and mobile
    screenshots for /courses, the family page, and the 2025/2026 cohort paths.
  • [HUMAN] After the exact release is deployed, the named operator records the
    aggregate freshness/provenance and route checks above. A synthetic/local pass is
    not substituted for this gate; the issue remains open with the human label until
    it is recorded.
  • PM accepts the fresh independent tester report and the human operator evidence
    before the engineer commits; the orchestrator then performs the prescribed local
    merge/push and on-call observes CI/deployment.

Dependencies and delivery status

Explicit non-goals

No CMP source-pin update; no full Course → Cohort structural migration; no change to
canonical route grammar; no DNS/redirect-Lambda activation; no redirect for a path not
present in the preservation evidence; no docs/FAQ/blog/content slug rewrite; no
production/protected-data import or live database mutation during engineering/testing;
no learner/history export; no removal of local test fixtures; no unrelated frontend
redesign; and no commit, merge, push, or deployment before the required lifecycle gates.

Required scenarios

  1. Parse a repository whose declared slug is ai-dev-tools-zoomcamp, regenerate its
    manifest, and prove the canonical source/family/cohort identity and repository
    provenance are all retained separately.
  2. Import into an empty database, a database with canonical 2025 data, a database
    with only the duplicate 2026 family, and a database already split across both;
    replay each exact input and compare safe counts, IDs, and checksums.
  3. Seed a complex duplicate cohort with modules, units, homework, projects,
    campaign/registration, enrollment, submission/history, and source provenance;
    repair it and prove every dependent PK/FK and cohort identifier survives.
  4. Exercise duplicate same-year/identifier/slug, source-identity, missing-canonical,
    unmapped-source, and injected-transaction-failure cases; assert no partial writes.
  5. Include exact fake-course/fake-course-2, a real-looking unclassified course,
    and normal *-zoomcamp families; prove only the reviewed fixture rows are excluded
    and existing canonical families are unchanged.
  6. Verify /courses family grouping, family/cohort navigation, canonical metadata,
    query preservation, loser-path 404 behavior, and absence of a redirect chain.
  7. Run the redacted source/migration/freshness report and the post-release human
    aggregate route/provenance gate without accessing protected learner data.

Lifecycle

This is now groomed but remains an operationally held P0 lane until a clean current-main
reconstruction base exists. Engineering leaves an uncommitted frozen candidate, an
independent tester recomputes and verifies the complete plan, and PM accepts only a
tester PASS plus the required human evidence. Only then may the engineer commit with
Closes #308; the orchestrator locally merges/pushes and on-call observes the resulting
CI/deployment. No pull request is created.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Must-have or release-blockingbugSomething isn't workingcoursesArea: coursesdata-migrationArea: data-migrationfrontendArea: frontendhumanCode complete; manual verification requiredoperationsArea: operationsseoArea: seotestingArea: testing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions