From 16a33159fec3bee4f560a086fcb0748bd6d19d10 Mon Sep 17 00:00:00 2001 From: msa46 Date: Sat, 15 Aug 2026 20:51:46 +0200 Subject: [PATCH] docs: plan programme course navigation implementation --- ...rogramme-sidebar-and-course-integration.md | 608 ++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-15-programme-sidebar-and-course-integration.md diff --git a/docs/superpowers/plans/2026-08-15-programme-sidebar-and-course-integration.md b/docs/superpowers/plans/2026-08-15-programme-sidebar-and-course-integration.md new file mode 100644 index 0000000..dee853a --- /dev/null +++ b/docs/superpowers/plans/2026-08-15-programme-sidebar-and-course-integration.md @@ -0,0 +1,608 @@ +# Programme Sidebar and Course Integration Implementation Plan + +> **Execution note:** Follow this plan task by task with test-driven development. Keep `main` green at every commit and do not merge `origin/isaacs-changes`; copy only the five reviewed substantive pages from commit `d5d6730`. + +**Goal:** Publish the useful Computer Science material, give Computer Science and Data Science & AI stable programme-specific routes, remove duplicated shared course bodies, and show a route-aware programme switch without hiding global wiki navigation. + +**Architecture:** The docs collection owns public pages. Seven shared course bodies live as unpublished MDX partials and are imported by thin programme-specific wrappers. Pure navigation helpers select the programme, paired switch target, and sidebar tree from the pathname; Astro middleware applies that tree to Starlight route data, while a `Sidebar` component override renders the accessible switch and delegates the actual navigation to Starlight's default component. Astro redirects preserve every legacy `/bachelor/...` route. + +**Stack:** Astro 7, Starlight 0.41, MD/MDX content collections, Astro middleware and component overrides, Node's built-in test runner, `linkedom`, existing migration/audit scripts. + +--- + +## Non-negotiable content decisions + +- Treat all current `src/content/docs/bachelor/**` pages as Data Science & AI and move them to `src/content/docs/data-science-and-ai/**`. +- Preserve every old `/bachelor/...` URL with an explicit permanent redirect to the exact new route. +- Import only these substantive Computer Science pages from `d5d6730`: + - `computer-science/year-1/period-1/introduction-to-computer-science.md` + - `computer-science/year-1/period-2/computer-architecture.md` + - `computer-science/year-1/period-4/algorithmic-design.md` + - `computer-science/year-1/period-4/databases.md` + - `computer-science/year-1/period-5/statistics.md` +- Do not import the malformed `computer-science/course-description.md`, any `Empty Page` placeholder, or the branch deletion of `previous-exams-and-documents.md`. +- Store the body of each byte-identical shared course once, outside `src/content/docs`, and expose it through two programme wrappers: + +| Shared body | Data Science & AI route | Computer Science route | +| --- | --- | --- | +| Discrete Mathematics | `year-1/block-1/discrete-mathematics` | `year-1/period-1/discrete-mathematics` | +| Procedural Programming | `year-1/block-1/procedural-programming` | `year-1/period-1/procedural-programming` | +| Calculus | `year-1/block-2/calculus` | `year-1/period-2/calculus` | +| Logic | `year-1/block-2/logic` | `year-1/period-2/logic` | +| Objects in Programming | `year-1/block-2/objects-in-programming` | `year-1/period-2/objects-in-programming` | +| Data Structures and Algorithms | `year-1/block-4/data-structures-and-algorithms` | `year-1/period-4/data-structures-and-algorithms` | +| Linear Algebra | `year-1/block-4/linear-algebra` | `year-1/period-4/linear-algebra` | + +## Task 1: Add migration-contract tests for the canonical DSAI route move + +**Files:** + +- Create: `tests/programme-content.test.mjs` +- Modify: `tests/content-audit.test.mjs` +- Modify: `tests/project-structure.test.mjs` +- Modify: `tests/pdf-archive.test.mjs` +- Modify: `scripts/audit-content.mjs` +- Modify: `scripts/lib/migration.mjs` +- Modify: `docs/migration-manifest.json` +- Modify: `docs/migration-report.md` +- Modify: `docs/supplemental-content.json` +- Modify: `src/content/docs/index.mdx` +- Rename: `src/content/docs/bachelor/` to `src/content/docs/data-science-and-ai/` + +### Step 1: Write the failing route/provenance assertions + +In `tests/programme-content.test.mjs`, add assertions that: + +```js +const legacyRoot = 'src/content/docs/bachelor'; +const dsaiRoot = 'src/content/docs/data-science-and-ai'; + +await assert.rejects(stat(legacyRoot), { code: 'ENOENT' }); +assert.ok((await stat(dsaiRoot)).isDirectory()); +``` + +Also recursively inspect the manifest, supplemental registry, migration report, landing page, audit exceptions, and migration canonical map so no destination or internal link contains `src/content/docs/bachelor/` or `href="./bachelor/"`, while the historical DokuWiki source identifiers under `pages/study/bachelor` remain unchanged. + +Update existing tests to expect `data-science-and-ai` destinations and landing-page links. Keep the expected source count `29`, unique migration destinations `28`, and migration-report row count `29`; only destination paths change in this task. + +### Step 2: Run the focused tests and confirm red + +Run: + +```sh +node --test tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs +``` + +Expected: failure because the DSAI tree and provenance records still use `bachelor`. + +### Step 3: Move the tree and update provenance atomically + +Rename the directory once so Git can retain rename history: + +```sh +mv src/content/docs/bachelor src/content/docs/data-science-and-ai +``` + +Update all destination paths in the manifest, migration report, supplemental registry, audit exception map, and canonical source map from `src/content/docs/bachelor/` to `src/content/docs/data-science-and-ai/`. Update internal public links from `/bachelor/` to `/data-science-and-ai/`, including the landing-page card. Do not change the original source paths or DokuWiki IDs because they document provenance. + +Update the programme overview/year wording from generic “Bachelor” to “Data Science & AI” only where the page is describing the current published destination; preserve quoted or historical source terminology when necessary for accuracy. + +### Step 4: Run the focused tests and content audit + +Run: + +```sh +node --test tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs +npm run audit:content +``` + +Expected: all pass, with the same `29` sources, `28` unique manifest destinations, `86` published pages, and `58` supplemental pages. + +### Step 5: Commit + +```sh +git add src/content/docs/data-science-and-ai src/content/docs/index.mdx docs/migration-manifest.json docs/migration-report.md docs/supplemental-content.json scripts/audit-content.mjs scripts/lib/migration.mjs tests/programme-content.test.mjs tests/content-audit.test.mjs tests/project-structure.test.mjs tests/pdf-archive.test.mjs +git commit -m "refactor: give data science canonical programme routes" +``` + +## Task 2: Preserve every legacy bachelor URL with exact redirects + +**Files:** + +- Create: `src/config/legacy-bachelor-redirects.mjs` +- Create: `tests/programme-redirects.test.mjs` +- Modify: `astro.config.mjs` + +### Step 1: Write a failing redirect coverage test + +Build expected redirects by recursively listing all `.md` and `.mdx` pages under `src/content/docs/data-science-and-ai`. Convert `index` files to directory routes and all other filenames to extensionless trailing-slash routes. Assert that `legacyBachelorRedirects` has exactly one key for every DSAI page after replacing the `/data-science-and-ai/` prefix with `/bachelor/`, and that every value is the exact canonical DSAI route. + +Add explicit examples: + +```js +assert.equal(legacyBachelorRedirects['/bachelor/'], '/data-science-and-ai/'); +assert.equal( + legacyBachelorRedirects['/bachelor/year-1/block-2/calculus/'], + '/data-science-and-ai/year-1/block-2/calculus/', +); +assert.equal( + legacyBachelorRedirects['/bachelor/year-3/study-abroad/'], + '/data-science-and-ai/year-3/study-abroad/', +); +``` + +Also assert `astro.config.mjs` passes the map through the top-level `redirects` property. + +### Step 2: Run the test and confirm red + +```sh +node --test tests/programme-redirects.test.mjs +``` + +Expected: module-not-found or missing redirects. + +### Step 3: Implement the explicit redirect map + +Create `legacy-bachelor-redirects.mjs` as a checked-in plain object. Do not derive redirects at runtime from the filesystem. Include overview, all three year indexes, projects, honours, thesis, study abroad, and every currently published DSAI course page. Export a frozen object: + +```js +export const legacyBachelorRedirects = Object.freeze({ + '/bachelor/': '/data-science-and-ai/', + // every exact published descendant follows +}); +``` + +Import it into `astro.config.mjs` and set: + +```js +redirects: legacyBachelorRedirects, +``` + +### Step 4: Prove generated redirects work at root and under BASE + +Run: + +```sh +node --test tests/programme-redirects.test.mjs +npm run build +BASE=/wiki npm run build +``` + +Inspect representative generated redirect HTML and assert its destination includes the configured base exactly once. + +### Step 5: Commit + +```sh +git add astro.config.mjs src/config/legacy-bachelor-redirects.mjs tests/programme-redirects.test.mjs +git commit -m "feat: redirect legacy bachelor routes to data science" +``` + +## Task 3: Replace seven duplicated course bodies with shared MDX partials + +**Files:** + +- Create: `src/content/shared-courses/year-1/*.mdx` (7 files) +- Rename: the 7 DSAI shared pages from `.md` to `.mdx` +- Create: the 7 corresponding `src/content/docs/computer-science/year-1/period-*/...mdx` wrappers +- Create: `tests/shared-programme-courses.test.mjs` +- Modify: `astro.config.mjs` +- Modify: `docs/supplemental-content.json` + +### Step 1: Write failing shared-source tests + +Define the seven route pairs in the test. For each pair, assert: + +- both published wrappers exist and contain valid `title` and `description` frontmatter; +- both import the same file under `src/content/shared-courses/year-1/`; +- neither wrapper contains the course sections duplicated from the partial; +- the partial is outside the docs collection and contains the substantive body; +- each wrapper contains the standard historical caution; +- no shared course appears as a second body file anywhere else. + +Also assert Starlight config includes: + +```js +markdown: { + processedDirs: ['./src/content/shared-courses/'], +}, +``` + +Add the seven Computer Science wrappers to the supplemental registry with a provenance kind such as `shared-course-wrapper` and retain the DSAI registry entries with their migrated destinations. + +### Step 2: Run the test and confirm red + +```sh +node --test tests/shared-programme-courses.test.mjs +``` + +Expected: missing partials and Computer Science wrappers. + +### Step 3: Extract each DSAI body exactly once + +For each shared course, retain the existing frontmatter values and historical caution in its DSAI wrapper. Move everything after the caution into the matching partial. The wrapper structure must be: + +```mdx +--- +title: Course title +description: Existing verified description +--- + +import SharedCourse from '../../../../shared-courses/year-1/course-slug.mdx'; + +:::caution[Historical information] +This information originated in the previous wiki and may be outdated. +::: + + +``` + +Create the Computer Science wrapper with the same shared import and warning, but programme-specific frontmatter. Add an empty, documented programme-notes seam only when content actually diverges later; do not add speculative notes now. + +### Step 4: Verify preservation and rendering + +Run: + +```sh +node --test tests/shared-programme-courses.test.mjs tests/content-audit.test.mjs +npm run audit:content +npm run build +``` + +Expected: both routes render the same substantive headings and links, exactly one `

` per page, and the shared partials do not become public routes. + +Update the audit count assertions to the actual new published-page total: `86 + 7 = 93`; supplemental total becomes `58 + 7 = 65`. The seven partials are excluded from both published counts. + +### Step 5: Commit + +```sh +git add astro.config.mjs src/content/shared-courses src/content/docs/data-science-and-ai/year-1 src/content/docs/computer-science docs/supplemental-content.json tests/shared-programme-courses.test.mjs tests/content-audit.test.mjs +git commit -m "refactor: share common bachelor course content" +``` + +## Task 4: Import the five substantive Computer Science pages and clean overviews + +**Files:** + +- Create: `src/content/docs/computer-science/index.md` +- Create: `src/content/docs/computer-science/year-1/index.md` +- Create: five reviewed Computer Science course pages under `year-1/period-*` +- Modify: `docs/supplemental-content.json` +- Modify: `tests/programme-content.test.mjs` +- Modify: `tests/content-audit.test.mjs` + +### Step 1: Write failing content-selection tests + +Assert the exact five unique course destinations exist, have valid frontmatter, contain the standard historical caution, and contain substantive headings or text from `d5d6730`. + +Assert forbidden imports are absent: + +```js +assert.doesNotMatch(allComputerScienceContent, /^#Hello$/m); +assert.doesNotMatch(allComputerScienceContent, /Empty Page/); +assert.doesNotMatch(allComputerScienceContent, /course-description/i); +``` + +Assert there are exactly `14` Computer Science published pages at this point: overview, Year 1 overview, five unique courses, and seven shared wrappers. Assert `previous-exams-and-documents.md` still exists. + +### Step 2: Run the test and confirm red + +```sh +node --test tests/programme-content.test.mjs +``` + +Expected: five unique pages and two overview pages are missing. + +### Step 3: Copy only reviewed branch blobs through explicit patches + +Use read-only `git show d5d6730:` to inspect each source, normalize frontmatter and the standard warning, remove any body H1, and add only the five approved pages. Do not perform a merge or checkout from the branch. + +Write a concise Computer Science overview and Year 1 overview based only on the published course set. State that the material is recovered historical wiki content and may not represent the current curriculum. Link to official Maastricht University curriculum information and to Previous Exams without inventing year 2 or year 3 content. + +Register all seven new pages (five courses plus two overviews) in `docs/supplemental-content.json` with source commit `d5d6730`, original branch path where applicable, and a note that malformed/placeholder pages were excluded. + +### Step 4: Audit and build + +```sh +node --test tests/programme-content.test.mjs tests/content-audit.test.mjs +npm run audit:content +npm run build +``` + +Expected final published count for this content phase: `100` pages, with `72` supplemental destinations (`65 + 7`). Verify actual counts from the audit and encode them in the tests only after confirming the registry matches the filesystem. + +### Step 5: Commit + +```sh +git add src/content/docs/computer-science docs/supplemental-content.json tests/programme-content.test.mjs tests/content-audit.test.mjs +git commit -m "feat: publish reviewed computer science courses" +``` + +## Task 5: Build pure programme navigation data and route selection + +**Files:** + +- Create: `src/config/programme-navigation.mjs` +- Create: `tests/programme-navigation.test.mjs` +- Modify: `src/config/recovered-course-sidebar.mjs` +- Modify: `src/config/sidebar.mjs` + +### Step 1: Write failing pure-function tests + +Test `programmeForPathname()`, `programmeSwitchTargets()`, and `sidebarForPathname()` without Astro. Cover: + +```js +programmeForPathname('/computer-science/year-1/period-2/calculus/') === 'computer-science' +programmeForPathname('/data-science-and-ai/year-3/') === 'data-science-and-ai' +programmeForPathname('/bachelor/year-1/') === 'data-science-and-ai' +programmeForPathname('/useful-information/') === null +``` + +Switch mapping cases: + +- shared CS course -> paired DSAI course; +- shared DSAI course -> paired CS course; +- unique CS course -> DSAI Year 1; +- unique DSAI Year 2/3 course -> Computer Science overview; +- programme overview/year page -> corresponding overview/year fallback where it exists; +- global page -> both programme overviews with neither active. + +Sidebar assertions must prove: + +- exactly one detailed undergraduate programme tree is visible on programme pages; +- global pages show compact links to both programme overviews; +- Home, About, Previous exams, Master AI, Master DSDM, and Useful Information remain present for every route; +- Useful Information retains all guide children. + +### Step 2: Run and confirm red + +```sh +node --test tests/programme-navigation.test.mjs +``` + +Expected: navigation module is missing. + +### Step 3: Implement immutable navigation structures + +Export: + +```js +export const sharedCoursePairs = Object.freeze({ /* 7 bidirectional route pairs */ }); +export const computerScienceSidebar = [/* overview, Year 1, periods and 12 courses */]; +export const dataScienceSidebar = [/* migrated overview, Years 1–3 and all existing pages */]; +export const globalSidebar = [/* Home, About, exams, masters, Useful Information */]; +export function programmeForPathname(pathname) { /* prefix match incl. /bachelor */ } +export function programmeSwitchTargets(pathname) { /* pair/fallback policy */ } +export function sidebarForPathname(pathname) { /* compose programme/global tree */ } +``` + +Normalize the recovered-course keys/slugs from `bachelor/...` to `data-science-and-ai/...`. Keep course ordering by period/block, not alphabetically across the whole year. The Computer Science tree contains only Year 1 and only the twelve published courses. + +Make `src/config/sidebar.mjs` export a default global/compact sidebar for Starlight's initial configuration and re-export the selector data needed by middleware. Avoid duplicating masters or Useful Information in two source files. + +### Step 4: Run tests + +```sh +node --test tests/programme-navigation.test.mjs tests/useful-guides.test.mjs tests/pdf-archive.test.mjs +``` + +Expected: all pass. + +### Step 5: Commit + +```sh +git add src/config/programme-navigation.mjs src/config/recovered-course-sidebar.mjs src/config/sidebar.mjs tests/programme-navigation.test.mjs tests/useful-guides.test.mjs tests/pdf-archive.test.mjs +git commit -m "feat: add route-aware programme navigation data" +``` + +## Task 6: Apply route-aware sidebar data through Astro middleware + +**Files:** + +- Create: `src/middleware.ts` +- Create: `tests/programme-middleware.test.mjs` + +### Step 1: Write a failing middleware behavior test + +Export a small handler factory or pure mutation helper so the Node test can provide a fake context with `url.pathname` and `locals.starlightRoute.sidebar`. Assert that it replaces only the sidebar and preserves the rest of route data. + +Test Computer Science, DSAI, legacy `/bachelor`, master, Useful Information, and home paths. Include a no-Starlight-locals guard so non-Starlight routes/assets are unaffected. + +### Step 2: Run and confirm red + +```sh +node --test tests/programme-middleware.test.mjs +``` + +Expected: middleware module is missing. + +### Step 3: Implement middleware using Starlight's supported route data + +Use Astro's `defineMiddleware` and the pure selector: + +```ts +import { defineMiddleware } from 'astro:middleware'; +import { sidebarForPathname } from './config/programme-navigation.mjs'; + +export const onRequest = defineMiddleware(async (context, next) => { + const response = await next(); + if (context.locals.starlightRoute) { + context.locals.starlightRoute.sidebar = sidebarForPathname(context.url.pathname); + } + return response; +}); +``` + +Adjust ordering to the Starlight 0.41 route-data lifecycle if the focused integration build shows route data must be mutated before `next()`. Keep all selection logic in the pure module, not in middleware. + +### Step 4: Run test and focused builds + +```sh +node --test tests/programme-middleware.test.mjs tests/programme-navigation.test.mjs +npm run build +BASE=/wiki npm run build +``` + +Inspect representative built HTML to confirm the active programme tree is present and the inactive detailed tree is absent. + +### Step 5: Commit + +```sh +git add src/middleware.ts tests/programme-middleware.test.mjs +git commit -m "feat: select programme sidebar per route" +``` + +## Task 7: Add the accessible programme switch through a Sidebar override + +**Files:** + +- Create: `src/components/ProgrammeSwitch.astro` +- Create: `src/components/Sidebar.astro` +- Create: `tests/programme-switch.test.mjs` +- Modify: `astro.config.mjs` +- Modify: `src/styles/incognito.css` + +### Step 1: Write failing structure/accessibility tests + +Assert the override is registered as: + +```js +components: { + Sidebar: './src/components/Sidebar.astro', +}, +``` + +Assert `Sidebar.astro` imports and renders the custom switch followed by Starlight's default Sidebar component. Assert the switch: + +- uses a `