diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7397a8c..65584e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -7,13 +7,10 @@ on: branches: [main] workflow_dispatch: -permissions: - contents: read - pages: write - id-token: write - jobs: build: + permissions: + contents: read runs-on: ubuntu-latest steps: - name: Check out the mirror @@ -27,6 +24,9 @@ jobs: BASE: ${{ vars.BASE_PATH }} deploy: + permissions: + pages: write + id-token: write needs: build runs-on: ubuntu-latest environment: diff --git a/docs/migration-report.md b/docs/migration-report.md index 36ddb07..69cd262 100644 --- a/docs/migration-report.md +++ b/docs/migration-report.md @@ -1,42 +1,42 @@ # DokuWiki migration report -This report accounts for the 29 selected DokuWiki source pages and their 28 unique Starlight destinations. `pages/start.txt` and `pages/study.txt` were intentionally combined on the landing page. Generic DokuWiki documentation was outside the selected source scope. Newer live-wiki captures remain research material under `to-be-studied/` and are not published. +This report accounts for the 29 selected DokuWiki source pages and their 28 unique Starlight destinations. `pages/start.txt` and `pages/study.txt` were intentionally combined on the landing page. Generic DokuWiki documentation was outside the selected source scope. Newer live-wiki captures remain research material under `to-be-studied/` and are not published. Present-day association references use the current Department of Advanced Computing Sciences name; legacy department and programme names remain only in clearly historical material. ## Source inventory -“Outdated” records whether the destination displays the standard warning that its migrated information may be outdated. “None” means that the source had no item in that category, not that it was skipped during review. +“Outdated” records whether the destination displays the standard warning that its migrated information may be outdated. Link handling distinguishes converted source links, converted plain-text references, newly verified links, genuine removals, and sources with no content links. “None” means that the source had no item in that category, not that it was skipped during review. -| Source | Destination | Mode | Outdated | Repaired or removed links | Missing assets | Significant spelling or grammar fixes | +| Source | Destination | Mode | Outdated | Link handling | Missing assets | Significant spelling or grammar fixes | | --- | --- | --- | --- | --- | --- | --- | -| `pages/start.txt` | `src/content/docs/index.mdx` | page | Yes, on migrated programme notes | Removed nonexistent Minutes and Manuals targets; replaced association and university destinations with working public links | None | Recast the welcome copy and normalized programme terminology | -| `pages/study.txt` | `src/content/docs/index.mdx` | merge | Yes | Replaced namespace navigation with landing-page cards and official links | None | Corrected punctuation, capitalization, and bachelor/master wording | -| `pages/study/msv_incognito.txt` | `src/content/docs/about-incognito.md` | page | No; concise current association overview | Replaced the old namespace link with the public Incognito site | None | Simplified an incomplete association description without adding historical claims | -| `pages/study/bachelor.txt` | `src/content/docs/bachelor/index.md` | page | Yes | Converted year namespace links to Starlight routes; removed DokuWiki editing controls | None | Corrected bachelor capitalization and sentence structure | -| `pages/study/bachelor/year_1.txt` | `src/content/docs/bachelor/year-1/index.md` | page | Yes | Converted project links to local routes; removed page-creation and index-menu controls | `study:dke-schedule.png` documented as absent | Corrected schedule wording and standardized Year 1/block capitalization | -| `pages/study/bachelor/year_1/project_1-1.txt` | `src/content/docs/bachelor/year-1/project-1-1.md` | page | Yes | Converted related-project links and removed editing controls | None | Corrected punctuation and clarified prerequisite prose without changing requirements | -| `pages/study/bachelor/year_1/project_1-2.txt` | `src/content/docs/bachelor/year-1/project-1-2.md` | page | Yes | Converted related-project links and removed editing controls | None | Corrected grammar, spacing, and capitalization | -| `pages/study/bachelor/year_2.txt` | `src/content/docs/bachelor/year-2/index.md` | page | Yes | Converted project links; removed DokuWiki controls and the malformed media-upload link | `study:dke-schedule.png` documented as absent | Corrected schedule wording and Year 2/block capitalization | -| `pages/study/bachelor/year_2/honours_programme.txt` | `src/content/docs/bachelor/year-2/honours-programme.md` | page | Yes | Removed empty DokuWiki navigation/edit controls | None | Preserved the empty page as an explicit awaiting-content placeholder | -| `pages/study/bachelor/year_2/project_2-1.txt` | `src/content/docs/bachelor/year-2/project-2-1.md` | page | Yes | Repaired the nested route to the Year 1 prerequisite; removed editing controls | None | Corrected grammar and standardized project naming | -| `pages/study/bachelor/year_2/project_2-2.txt` | `src/content/docs/bachelor/year-2/project-2-2.md` | page | Yes | Repaired the nested route to the Year 1 prerequisite; removed editing controls | None | Corrected punctuation and standardized project naming | -| `pages/study/bachelor/year_3.txt` | `src/content/docs/bachelor/year-3/index.md` | page | Yes | Converted thesis and project links; removed DokuWiki navigation controls | None | Corrected block references and the schedule explanation | -| `pages/study/bachelor/year_3/bachelors_thesis.txt` | `src/content/docs/bachelor/year-3/bachelors-thesis.md` | page | Yes | Converted related-study links and removed editing controls | None | Corrected possessive bachelor’s wording, punctuation, and sentence flow | -| `pages/study/bachelor/year_3/honours_programme.txt` | `src/content/docs/bachelor/year-3/honours-programme.md` | page | Yes | Removed empty DokuWiki navigation/edit controls | None | Preserved the source title and made the empty state explicit | -| `pages/study/bachelor/year_3/project_3-1.txt` | `src/content/docs/bachelor/year-3/project-3-1.md` | page | Yes | Converted prerequisite links and removed editing controls | None | Corrected grammar while retaining the stated prerequisites and reading | -| `pages/study/bachelor/year_3/study_abroad.txt` | `src/content/docs/bachelor/year-3/study-abroad.md` | page | Yes, with a 2022-specific warning | Retained useful external destinations as historical references and removed editing controls | None | Corrected grammar; did not silently modernize the dated eligibility claims | -| `pages/study/master_ai.txt` | `src/content/docs/master-ai/index.md` | page | Yes | Converted year links to final routes and removed namespace controls | None | Standardized master’s-programme capitalization and punctuation | -| `pages/study/master_ai/year_1.txt` | `src/content/docs/master-ai/year-1/index.md` | page | Yes | Converted research-project links and removed editing controls | None | Corrected sentence structure and project naming | -| `pages/study/master_ai/year_1/project_mai_1.txt` | `src/content/docs/master-ai/year-1/research-project-1.md` | page | Yes | Repaired internal navigation to the programme hierarchy | None | Corrected punctuation and rendered the project name consistently | -| `pages/study/master_ai/year_1/project_mai_2.txt` | `src/content/docs/master-ai/year-1/research-project-2.md` | page | Yes | Repaired internal navigation to the programme hierarchy | None | Corrected punctuation and rendered the project name consistently | -| `pages/study/master_ai/year_2.txt` | `src/content/docs/master-ai/year-2/index.md` | page | Yes | Removed obsolete DokuWiki page-creation and navigation controls | None | Reworded incomplete prose as historical programme guidance | -| `pages/study/master_dsdm.txt` | `src/content/docs/master-dsdm/index.md` | page | Yes | Converted year links to final routes and removed namespace controls | None | Standardized DSDM and master’s-programme capitalization | -| `pages/study/master_dsdm/year_1.txt` | `src/content/docs/master-dsdm/year-1/index.md` | page | Yes | Removed obsolete DokuWiki page-creation and navigation controls | None | Corrected punctuation and retained the source-era structure | -| `pages/study/master_dsdm/year_2.txt` | `src/content/docs/master-dsdm/year-2/index.md` | page | Yes | Removed obsolete DokuWiki page-creation and navigation controls | None | Corrected punctuation and retained the source-era structure | -| `pages/study/useful_information.txt` | `src/content/docs/useful-information/index.md` | page | Yes | Converted child-page links to final Starlight routes; removed DokuWiki controls | None | Corrected heading capitalization and introductory grammar | -| `pages/study/useful_information/pages/dke_locations.txt` | `src/content/docs/useful-information/dke-locations.md` | page | Yes | Kept usable map links as historical references and removed editing controls | None | Corrected sentence fragments, comma use, and room-list phrasing | -| `pages/study/useful_information/pages/handy_locations.txt` | `src/content/docs/useful-information/handy-locations.md` | page | Yes | Removed empty DokuWiki edit links | None | Corrected capitalization; marked the two empty sections as awaiting content | -| `pages/study/useful_information/pages/it_services.txt` | `src/content/docs/useful-information/it-services.md` | page | Yes, prominently historical | Retained old service URLs only as labeled historical references; linked to current official manuals where unambiguous | None | Corrected errors such as “Eletctronic” and “contact he,” plus punctuation and service-name casing | -| `pages/study/useful_information/pages/laptop_buy_advice.txt` | `src/content/docs/useful-information/laptop-buying-advice.md` | page | Yes, prominently historical | Removed DokuWiki controls; preserved relevant external references as historical | None | Corrected spacing in capacities, punctuation, and awkward phrasing while retaining old price/specification facts | +| `pages/start.txt` | `src/content/docs/index.mdx` | page | Yes, on migrated programme notes | **Converted source links:** Bachelor, Master AI, Master DSDM, Useful Information, the Incognito site, and ICT manuals. **Newly verified link:** the current DACS page replaced the source-era DKE department URL. **Genuinely removed links:** unresolved Minutes, Event Manuals, and Function Manuals targets; redundant study/year navigation was omitted. | None | Recast the welcome copy and normalized programme terminology | +| `pages/study.txt` | `src/content/docs/index.mdx` | merge | Yes | **Converted source links:** Bachelor, Master AI, Master DSDM, and the YouTube embed. **Genuinely removed links:** redundant year navigation was omitted. | None | Condensed the source-era university and programme overview, including omission of its dated growth/ranking sentence; corrected punctuation, capitalization, and bachelor/master wording | +| `pages/study/msv_incognito.txt` | `src/content/docs/about-incognito.md` | page | No | **Converted source link:** the MSV Incognito website. No links were repaired or removed. | None | Combined the source website link with the association-description sentence from `pages/start.txt`; the present-day introduction uses the current DACS name | +| `pages/study/bachelor.txt` | `src/content/docs/bachelor/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Removed source links:** redundant DokuWiki year-navigation links, now covered by site navigation. | None | Corrected bachelor capitalization and sentence structure; disclosed that promised summaries and old exams were absent from the export | +| `pages/study/bachelor/year_1.txt` | `src/content/docs/bachelor/year-1/index.md` | page | Yes | **Newly verified links:** the two migrated project pages, derived from the exported child-page inventory. No source content links required repair or removal. | `study:dke-schedule.png` documented as absent | Corrected schedule wording and standardized Year 1/block capitalization | +| `pages/study/bachelor/year_1/project_1-1.txt` | `src/content/docs/bachelor/year-1/project-1-1.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and clarified prerequisite prose without changing requirements | +| `pages/study/bachelor/year_1/project_1-2.txt` | `src/content/docs/bachelor/year-1/project-1-2.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected grammar, spacing, and capitalization | +| `pages/study/bachelor/year_2.txt` | `src/content/docs/bachelor/year-2/index.md` | page | Yes | **Newly verified links:** the two migrated project pages and honours placeholder. **Genuinely removed link:** the malformed DokuWiki media-manager/upload URL. | `study:dke-schedule.png` documented as absent | Corrected schedule wording and Year 2/block capitalization | +| `pages/study/bachelor/year_2/honours_programme.txt` | `src/content/docs/bachelor/year-2/honours-programme.md` | page | Yes | **None.** The source and destination contain no content links. | None | Replaced the source's “Under Construction” text with the standard awaiting-content placeholder | +| `pages/study/bachelor/year_2/project_2-1.txt` | `src/content/docs/bachelor/year-2/project-2-1.md` | page | Yes | **Converted plain-text reference:** Project 1-1 became a verified local link. The Project 3-1 reference remains text. No source link was repaired. | None | Corrected grammar and standardized project naming | +| `pages/study/bachelor/year_2/project_2-2.txt` | `src/content/docs/bachelor/year-2/project-2-2.md` | page | Yes | **Converted plain-text reference:** Project 1-2 became a verified local link. No source link was repaired. | None | Corrected punctuation and standardized project naming | +| `pages/study/bachelor/year_3.txt` | `src/content/docs/bachelor/year-3/index.md` | page | Yes | **Newly verified links:** the four migrated child pages, derived from the exported child-page inventory. No source content links required repair or removal. | None | Corrected block references and the schedule explanation | +| `pages/study/bachelor/year_3/bachelors_thesis.txt` | `src/content/docs/bachelor/year-3/bachelors-thesis.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected possessive bachelor’s wording, punctuation, and sentence flow | +| `pages/study/bachelor/year_3/honours_programme.txt` | `src/content/docs/bachelor/year-3/honours-programme.md` | page | Yes | **None.** The source and destination contain no content links. | None | Preserved the source title and made its under-construction state an explicit awaiting-content placeholder | +| `pages/study/bachelor/year_3/project_3-1.txt` | `src/content/docs/bachelor/year-3/project-3-1.md` | page | Yes | **Converted plain-text reference:** Project 2-1 became a verified local link. No source link was repaired. | None | Corrected grammar while retaining the stated prerequisites and reading | +| `pages/study/bachelor/year_3/study_abroad.txt` | `src/content/docs/bachelor/year-3/study-abroad.md` | page | Yes, with a 2022-specific warning | **Newly verified link:** the official UM study-abroad page. No source links existed. | None | Corrected grammar; did not silently modernize the dated eligibility claims | +| `pages/study/master_ai.txt` | `src/content/docs/master-ai/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Removed source links:** redundant DokuWiki year-navigation links, now covered by site navigation. | None | Standardized master’s-programme capitalization and punctuation; disclosed that promised summaries and old exams were absent from the export | +| `pages/study/master_ai/year_1.txt` | `src/content/docs/master-ai/year-1/index.md` | page | Yes | **Newly verified links:** the two migrated research-project pages, derived from the exported child-page inventory. No source content links required repair or removal. | None | Corrected sentence structure and project naming | +| `pages/study/master_ai/year_1/project_mai_1.txt` | `src/content/docs/master-ai/year-1/research-project-1.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and rendered the project name consistently | +| `pages/study/master_ai/year_1/project_mai_2.txt` | `src/content/docs/master-ai/year-1/research-project-2.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and rendered the project name consistently | +| `pages/study/master_ai/year_2.txt` | `src/content/docs/master-ai/year-2/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected capitalization and hyphenation and framed the two schedule statements historically | +| `pages/study/master_dsdm.txt` | `src/content/docs/master-dsdm/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Removed source links:** redundant DokuWiki year-navigation links, now covered by site navigation. | None | Standardized DSDM and master’s-programme capitalization; disclosed that promised summaries and old exams were absent from the export | +| `pages/study/master_dsdm/year_1.txt` | `src/content/docs/master-dsdm/year-1/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure | +| `pages/study/master_dsdm/year_2.txt` | `src/content/docs/master-dsdm/year-2/index.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected punctuation and retained the source-era structure | +| `pages/study/useful_information.txt` | `src/content/docs/useful-information/index.md` | page | Yes | **Converted source link:** Maastricht University education website. **Newly verified links:** the four migrated child pages, derived from the exported child-page inventory. | None | Corrected heading capitalization and introductory grammar; scoped contribution guidance to repository maintainers | +| `pages/study/useful_information/pages/dke_locations.txt` | `src/content/docs/useful-information/dke-locations.md` | page | Yes | **Converted source links:** the three Google Maps URLs. No links were repaired or removed. | None | Corrected sentence fragments, comma use, and room-list phrasing | +| `pages/study/useful_information/pages/handy_locations.txt` | `src/content/docs/useful-information/handy-locations.md` | page | Yes | **None.** The source and destination contain no content links. | None | Corrected capitalization; marked the two empty sections as awaiting content | +| `pages/study/useful_information/pages/it_services.txt` | `src/content/docs/useful-information/it-services.md` | page | Yes, prominently historical | **Converted source links:** ICT manuals, Inner City map, Student Portal, Student Desktop Anywhere, and MyPrint. **Removed hyperlinking:** legacy EleUM and VPN URLs were retained as non-clickable historical code. No successor URL was added. | None | Corrected errors such as “Eletctronic” and “contact he,” plus punctuation and service-name casing | +| `pages/study/useful_information/pages/laptop_buy_advice.txt` | `src/content/docs/useful-information/laptop-buying-advice.md` | page | Yes, prominently historical | **None.** The source and destination contain no content links. | None | Corrected spacing in capacities, punctuation, and awkward phrasing while retaining old price/specification facts | ## Missing schedule asset @@ -44,7 +44,11 @@ The Bachelor Year 1 and Bachelor Year 2 sources both embedded `study:dke-schedul ## Removed unresolved home-page targets -The old home page exposed Minutes and Manuals targets, but the selected export contained no corresponding source pages and therefore no valid destinations. These knowingly broken targets were removed. The useful association, programme, and official university destinations were retained or replaced with explicit working links. +The old home page exposed Minutes and Manuals targets, but the selected export contained no corresponding source pages and therefore no valid destinations. These knowingly broken targets were removed. Source programme links became local navigation, the Incognito and ICT links were converted, and the source-era DKE department URL was replaced with the newly verified current DACS page. + +## Source-era summaries and exams claim + +The Bachelor, Master AI, and Master DSDM overview sources said course summaries and old exams could be found in corresponding folders. Neither the selected export nor the published inventory contains those materials. The destinations preserve the statement only as a source-era claim and explicitly disclose the absence instead of promising unavailable content. ## Removed DokuWiki controls @@ -56,11 +60,11 @@ The Bachelor Year 2 source combined its missing schedule embed with a malformed ## Historical Study Abroad guidance -The Study Abroad source makes time-sensitive claims tied to 2022, including eligibility and application guidance. Those claims remain for historical context, but the destination explicitly says they must be verified against current Maastricht University and programme rules. +The Study Abroad source makes time-sensitive claims tied to 2022, including eligibility and application guidance. Those claims remain for historical context, but the destination explicitly says they must be verified against current Maastricht University and programme rules. The official UM study-abroad page is a newly verified destination link; it did not originate in the source. ## Historical IT services -The IT page names source-era services, URLs, and instructions, including MyUM, EleUM, Surfgroepen, the DKE file server, VPN steps, printing, and historical timetable navigation. These are not presented as current instructions. The destination uses a prominent historical warning, retains context where useful, and directs readers to current official ICT manuals when the successor is unambiguous. +The IT page names source-era services, URLs, and instructions, including MyUM, EleUM, Student Desktop Anywhere, VPN steps, printing, and historical timetable navigation. These are not presented as current instructions. The destination uses a prominent historical warning, converts useful source links, and retains the legacy EleUM and VPN URLs only as non-clickable historical code. No successor URL was added. ## Historical laptop advice diff --git a/package-lock.json b/package-lock.json index 74467f9..6bfceb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ }, "devDependencies": { "@astrojs/check": "0.9.10", + "js-yaml": "4.3.1", "linkedom": "0.18.13", "turndown": "7.2.4", "typescript": "6.0.2" diff --git a/package.json b/package.json index 3d91740..16a5757 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "check:migration": "node scripts/check-migration.mjs", "audit:content": "node scripts/audit-content.mjs", "check:links": "node scripts/check-internal-links.mjs dist", + "check:rendered": "node scripts/check-rendered-output.mjs dist", "capture:live-wiki": "node scripts/capture-live-wiki.mjs", "test": "node --test", - "verify": "npm run check && npm run audit:content && npm run build && npm run check:links" + "verify": "npm run check && npm run audit:content && npm run build && npm run check:rendered && npm run check:links" }, "dependencies": { "@astrojs/starlight": "0.41.6", @@ -23,6 +24,7 @@ }, "devDependencies": { "@astrojs/check": "0.9.10", + "js-yaml": "4.3.1", "linkedom": "0.18.13", "turndown": "7.2.4", "typescript": "6.0.2" diff --git a/scripts/audit-content.mjs b/scripts/audit-content.mjs index 0c3d231..2a764fc 100644 --- a/scripts/audit-content.mjs +++ b/scripts/audit-content.mjs @@ -1,6 +1,7 @@ import { access, readFile, readdir } from 'node:fs/promises'; import { join, relative, resolve, sep } from 'node:path'; import { pathToFileURL } from 'node:url'; +import { load as loadYaml } from 'js-yaml'; import { checkDestinations, loadManifest, validateManifest } from './lib/migration.mjs'; const DOCS_PREFIX = 'src/content/docs/'; @@ -59,9 +60,17 @@ async function containsNamedDirectory(root, name) { function frontmatterIssue(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); if (!match) return 'is missing YAML frontmatter'; + + let frontmatter; + try { + frontmatter = loadYaml(match[1]); + } catch (error) { + return `has malformed YAML frontmatter: ${error.reason ?? error.message}`; + } + for (const field of ['title', 'description']) { - const fieldMatch = match[1].match(new RegExp(`^${field}:\\s*(.+?)\\s*$`, 'm')); - if (!fieldMatch || /^(['"]{2})$/.test(fieldMatch[1])) return `is missing non-empty ${field} frontmatter`; + const value = frontmatter && typeof frontmatter === 'object' ? frontmatter[field] : undefined; + if (typeof value !== 'string' || !value.trim()) return `is missing non-empty ${field} frontmatter`; } return null; } @@ -73,11 +82,11 @@ function reportRows(report) { function projectRootFor(docsRoot) { const normalized = resolve(docsRoot); - const suffix = ['src', 'content', 'docs'].join(sep); - if (!normalized.endsWith(suffix)) { + const projectRoot = resolve(normalized, '..', '..', '..'); + if (resolve(projectRoot, 'src', 'content', 'docs') !== normalized) { throw new TypeError(`docsRoot must identify src/content/docs: ${docsRoot}`); } - return resolve(normalized, '..', '..', '..'); + return projectRoot; } export async function auditContent({ docsRoot, manifest }) { @@ -100,6 +109,7 @@ export async function auditContent({ docsRoot, manifest }) { const destinationResult = await checkDestinations({ entries }, projectRoot); for (const destination of destinationResult.missing) issues.push(`missing manifest destination: ${destination}`); + for (const destination of destinationResult.notFiles) issues.push(`manifest destination is not a regular file: ${destination}`); const contentFiles = await listContentFiles(absoluteDocsRoot); const contentByDestination = new Map(); @@ -116,6 +126,19 @@ export async function auditContent({ docsRoot, manifest }) { } } + const manifestDestinationSet = new Set(destinations); + const publishedDestinationSet = new Set(contentByDestination.keys()); + for (const destination of manifestDestinationSet) { + if (!publishedDestinationSet.has(destination)) { + issues.push(`manifest destination is absent from published content: ${destination}`); + } + } + for (const destination of publishedDestinationSet) { + if (!manifestDestinationSet.has(destination)) { + issues.push(`published content file is absent from manifest: ${destination}`); + } + } + for (const [localPath, notice] of EMPTY_PAGE_NOTICES) { const content = contentByDestination.get(`${DOCS_PREFIX}${localPath}`) ?? ''; if (!content.includes(notice)) issues.push(`${DOCS_PREFIX}${localPath} is missing the exact awaiting-content notice`); diff --git a/scripts/check-migration.mjs b/scripts/check-migration.mjs index 8aef7f7..0f9a476 100644 --- a/scripts/check-migration.mjs +++ b/scripts/check-migration.mjs @@ -10,8 +10,11 @@ console.log(`Migration manifest accounts for ${result.total} source pages.`); for (const destination of result.missing) { console.log(`Missing destination: ${destination}`); } +for (const destination of result.notFiles) { + console.log(`Destination is not a regular file: ${destination}`); +} -if (result.missing.length > 0 && !allowMissing) { - console.error(`Migration check failed: ${result.missing.length} destination file(s) are missing.`); +if ((result.missing.length > 0 && !allowMissing) || result.notFiles.length > 0) { + console.error(`Migration check failed: ${result.missing.length} destination file(s) are missing and ${result.notFiles.length} destination path(s) are not regular files.`); process.exitCode = 1; } diff --git a/scripts/check-rendered-output.mjs b/scripts/check-rendered-output.mjs new file mode 100644 index 0000000..934c58a --- /dev/null +++ b/scripts/check-rendered-output.mjs @@ -0,0 +1,26 @@ +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { checkRenderedOutput } from './lib/rendered-output.mjs'; + +async function main() { + const distRoot = process.argv[2] ?? 'dist'; + const result = await checkRenderedOutput({ distRoot }); + const issues = [ + ...result.headingIssues.map(({ file, h1Count }) => `${file} contains ${h1Count} H1 headings; expected exactly 1`), + ...result.unpublishedPaths.map((path) => `unpublished to-be-studied path reached built output: ${path}`), + ]; + + if (issues.length > 0) { + throw new Error(`Rendered-output check failed with ${issues.length} issue(s):\n${issues.map((issue) => `- ${issue}`).join('\n')}`); + } + + console.log(`Rendered-output check passed: ${result.htmlFilesChecked} published pages, exactly one H1 each, no to-be-studied paths.`); +} + +const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : ''; +if (invokedPath === import.meta.url) { + main().catch((error) => { + console.error(error.message); + process.exitCode = 1; + }); +} diff --git a/scripts/lib/internal-links.mjs b/scripts/lib/internal-links.mjs index 2c3943f..bf11a06 100644 --- a/scripts/lib/internal-links.mjs +++ b/scripts/lib/internal-links.mjs @@ -1,5 +1,5 @@ -import { access, readFile, readdir, stat } from 'node:fs/promises'; -import { join, relative, sep } from 'node:path'; +import { readFile, readdir, stat } from 'node:fs/promises'; +import { isAbsolute, join, relative, resolve, sep } from 'node:path'; import { parseHTML } from 'linkedom'; function normalizeBase(base) { @@ -52,15 +52,24 @@ async function isBuiltTarget(distRoot, route) { return false; } - const relativeTarget = decoded.replace(/^\/+/, ''); - const exact = join(distRoot, relativeTarget); - const candidates = decoded.endsWith('/') + const normalized = decoded.replaceAll('\\', '/'); + if (normalized.split('/').includes('..')) return false; + + const absoluteDistRoot = resolve(distRoot); + const relativeTarget = normalized.replace(/^\/+/, ''); + const exact = resolve(absoluteDistRoot, relativeTarget); + const candidates = normalized.endsWith('/') ? [join(exact, 'index.html')] : [exact, join(exact, 'index.html')]; for (const candidate of candidates) { + const candidateRelativePath = relative(absoluteDistRoot, candidate); + if (isAbsolute(candidateRelativePath) + || candidateRelativePath === '..' + || candidateRelativePath.startsWith(`..${sep}`)) { + continue; + } try { - await access(candidate); if ((await stat(candidate)).isFile()) return true; } catch { // Try the directory-index form before reporting the route. diff --git a/scripts/lib/migration.mjs b/scripts/lib/migration.mjs index 3fa5603..e37dea1 100644 --- a/scripts/lib/migration.mjs +++ b/scripts/lib/migration.mjs @@ -1,12 +1,51 @@ -import { readFile } from 'node:fs/promises'; -import { access } from 'node:fs/promises'; -import { join } from 'node:path'; +import { readFile, stat } from 'node:fs/promises'; +import { isAbsolute, relative, resolve, sep } from 'node:path'; const SOURCE_COUNT = 29; const DOCS_ROOT = 'src/content/docs/'; +const CANONICAL_ENTRIES = new Map([ + ['pages/start.txt', ['src/content/docs/index.mdx', 'page']], + ['pages/study.txt', ['src/content/docs/index.mdx', 'merge']], + ['pages/study/msv_incognito.txt', ['src/content/docs/about-incognito.md', 'page']], + ['pages/study/bachelor.txt', ['src/content/docs/bachelor/index.md', 'page']], + ['pages/study/bachelor/year_1.txt', ['src/content/docs/bachelor/year-1/index.md', 'page']], + ['pages/study/bachelor/year_1/project_1-1.txt', ['src/content/docs/bachelor/year-1/project-1-1.md', 'page']], + ['pages/study/bachelor/year_1/project_1-2.txt', ['src/content/docs/bachelor/year-1/project-1-2.md', 'page']], + ['pages/study/bachelor/year_2.txt', ['src/content/docs/bachelor/year-2/index.md', 'page']], + ['pages/study/bachelor/year_2/honours_programme.txt', ['src/content/docs/bachelor/year-2/honours-programme.md', 'page']], + ['pages/study/bachelor/year_2/project_2-1.txt', ['src/content/docs/bachelor/year-2/project-2-1.md', 'page']], + ['pages/study/bachelor/year_2/project_2-2.txt', ['src/content/docs/bachelor/year-2/project-2-2.md', 'page']], + ['pages/study/bachelor/year_3.txt', ['src/content/docs/bachelor/year-3/index.md', 'page']], + ['pages/study/bachelor/year_3/bachelors_thesis.txt', ['src/content/docs/bachelor/year-3/bachelors-thesis.md', 'page']], + ['pages/study/bachelor/year_3/honours_programme.txt', ['src/content/docs/bachelor/year-3/honours-programme.md', 'page']], + ['pages/study/bachelor/year_3/project_3-1.txt', ['src/content/docs/bachelor/year-3/project-3-1.md', 'page']], + ['pages/study/bachelor/year_3/study_abroad.txt', ['src/content/docs/bachelor/year-3/study-abroad.md', 'page']], + ['pages/study/master_ai.txt', ['src/content/docs/master-ai/index.md', 'page']], + ['pages/study/master_ai/year_1.txt', ['src/content/docs/master-ai/year-1/index.md', 'page']], + ['pages/study/master_ai/year_1/project_mai_1.txt', ['src/content/docs/master-ai/year-1/research-project-1.md', 'page']], + ['pages/study/master_ai/year_1/project_mai_2.txt', ['src/content/docs/master-ai/year-1/research-project-2.md', 'page']], + ['pages/study/master_ai/year_2.txt', ['src/content/docs/master-ai/year-2/index.md', 'page']], + ['pages/study/master_dsdm.txt', ['src/content/docs/master-dsdm/index.md', 'page']], + ['pages/study/master_dsdm/year_1.txt', ['src/content/docs/master-dsdm/year-1/index.md', 'page']], + ['pages/study/master_dsdm/year_2.txt', ['src/content/docs/master-dsdm/year-2/index.md', 'page']], + ['pages/study/useful_information.txt', ['src/content/docs/useful-information/index.md', 'page']], + ['pages/study/useful_information/pages/dke_locations.txt', ['src/content/docs/useful-information/dke-locations.md', 'page']], + ['pages/study/useful_information/pages/handy_locations.txt', ['src/content/docs/useful-information/handy-locations.md', 'page']], + ['pages/study/useful_information/pages/it_services.txt', ['src/content/docs/useful-information/it-services.md', 'page']], + ['pages/study/useful_information/pages/laptop_buy_advice.txt', ['src/content/docs/useful-information/laptop-buying-advice.md', 'page']], +]); + +function normalizeSeparators(path) { + return path.replaceAll('\\', '/'); +} function hasParentTraversal(path) { - return path.split('/').includes('..'); + return normalizeSeparators(path).split('/').includes('..'); +} + +function isContained(root, candidate) { + const path = relative(root, candidate); + return path === '' || (!isAbsolute(path) && path !== '..' && !path.startsWith(`..${sep}`)); } export async function loadManifest(path) { @@ -37,13 +76,19 @@ export function validateManifest(manifest) { if (!['page', 'merge'].includes(mode)) { throw new TypeError('Each migration manifest entry mode must be page or merge.'); } - if (source.startsWith('pages/wiki/')) { + const normalizedSource = normalizeSeparators(source); + const normalizedDestination = normalizeSeparators(destination); + const virtualProjectRoot = resolve(sep, 'migration-project'); + const virtualDocsRoot = resolve(virtualProjectRoot, 'src/content/docs'); + const resolvedDestination = resolve(virtualProjectRoot, normalizedDestination); + + if (normalizedSource.startsWith('pages/wiki/')) { throw new Error(`Generic DokuWiki source is excluded: ${source}`); } - if (hasParentTraversal(source) || !(source === 'pages/start.txt' || source === 'pages/study.txt' || source.startsWith('pages/study/'))) { + if (hasParentTraversal(source) || !(normalizedSource === 'pages/start.txt' || normalizedSource === 'pages/study.txt' || normalizedSource.startsWith('pages/study/'))) { throw new Error(`Source is outside the selected migration scope: ${source}`); } - if (hasParentTraversal(destination) || !destination.startsWith(DOCS_ROOT)) { + if (hasParentTraversal(destination) || !normalizedDestination.startsWith(DOCS_ROOT) || !isContained(virtualDocsRoot, resolvedDestination)) { throw new Error(`Destination is outside ${DOCS_ROOT}: ${destination}`); } if (sources.has(source)) { @@ -57,16 +102,31 @@ export function validateManifest(manifest) { destinations.add(destination); } + for (const { source, destination, mode } of manifest.entries) { + const [canonicalDestination, canonicalMode] = CANONICAL_ENTRIES.get(source) ?? []; + if (canonicalDestination !== destination || canonicalMode !== mode) { + throw new Error(`Entry differs from the canonical migration contract: ${source} -> ${destination} (${mode}).`); + } + } + return { entries: manifest.entries }; } export async function checkDestinations(manifest, root) { const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))]; const missing = []; + const notFiles = []; + const projectRoot = resolve(root); + const docsRoot = resolve(projectRoot, 'src/content/docs'); for (const destination of destinations) { + const candidate = resolve(projectRoot, normalizeSeparators(destination)); + if (!isContained(docsRoot, candidate)) { + throw new Error(`Destination resolves outside ${DOCS_ROOT}: ${destination}`); + } try { - await access(join(root, destination)); + const details = await stat(candidate); + if (!details.isFile()) notFiles.push(destination); } catch { missing.push(destination); } @@ -75,5 +135,6 @@ export async function checkDestinations(manifest, root) { return { total: manifest.entries.length, missing, + notFiles, }; } diff --git a/scripts/lib/rendered-output.mjs b/scripts/lib/rendered-output.mjs new file mode 100644 index 0000000..d75e908 --- /dev/null +++ b/scripts/lib/rendered-output.mjs @@ -0,0 +1,38 @@ +import { readFile, readdir } from 'node:fs/promises'; +import { join, relative, resolve, sep } from 'node:path'; +import { parseHTML } from 'linkedom'; + +async function listFiles(root, directory = root) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isDirectory()) files.push(...await listFiles(root, path)); + else if (entry.isFile()) files.push(path); + } + return files.sort(); +} + +export async function checkRenderedOutput({ distRoot }) { + const absoluteDistRoot = resolve(distRoot); + const files = await listFiles(absoluteDistRoot); + const relativeFiles = files.map((file) => relative(absoluteDistRoot, file).split(sep).join('/')); + const htmlFiles = files.filter((file) => { + const path = relative(absoluteDistRoot, file).split(sep).join('/'); + return path.endsWith('.html') && path !== '404.html'; + }); + const headingIssues = []; + + for (const file of htmlFiles) { + const path = relative(absoluteDistRoot, file).split(sep).join('/'); + const { document } = parseHTML(await readFile(file, 'utf8')); + const h1Count = document.querySelectorAll('h1').length; + if (h1Count !== 1) headingIssues.push({ file: path, h1Count }); + } + + return { + htmlFilesChecked: htmlFiles.length, + headingIssues, + unpublishedPaths: relativeFiles.filter((path) => path.split('/').includes('to-be-studied')), + }; +} diff --git a/src/content/docs/about-incognito.md b/src/content/docs/about-incognito.md index 7af555f..b3cb683 100644 --- a/src/content/docs/about-incognito.md +++ b/src/content/docs/about-incognito.md @@ -3,8 +3,6 @@ title: About Incognito description: Learn more about MSV Incognito, the study association behind this wiki. --- -# MSV Incognito - -MSV Incognito is the study association for the Department of Data Science & Artificial Intelligence at Maastricht University. +MSV Incognito is the study association for the Department of Advanced Computing Sciences at Maastricht University. Visit the [MSV Incognito website](https://msvincognito.nl/) to learn more about the association, its activities, and opportunities to become active in one of its committees. diff --git a/src/content/docs/bachelor/index.md b/src/content/docs/bachelor/index.md index bda6bb9..7e0c1f6 100644 --- a/src/content/docs/bachelor/index.md +++ b/src/content/docs/bachelor/index.md @@ -3,12 +3,10 @@ title: Data Science and Artificial Intelligence description: Information about the bachelor's programme in Data Science and Artificial Intelligence at Maastricht University. --- -# Data Science and Artificial Intelligence - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: -The following pages contain information about the bachelor's programme of the Department of Data Science and Artificial Intelligence at Maastricht University. Course summaries and old exams can be found in their respective folders. +The previous wiki described these pages as information about the bachelor's programme of the Department of Data Science and Artificial Intelligence at Maastricht University. It also said that course summaries and old exams could be found in corresponding folders. Those summaries and exams were not present in the export and are not published here. For the most up-to-date overview of the courses taught, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education). diff --git a/src/content/docs/bachelor/year-1/index.md b/src/content/docs/bachelor/year-1/index.md index 7bfca56..409766f 100644 --- a/src/content/docs/bachelor/year-1/index.md +++ b/src/content/docs/bachelor/year-1/index.md @@ -3,8 +3,6 @@ title: Year 1 description: Historical schedule context and project information for the first year of the bachelor's programme. --- -# Year 1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-1/project-1-1.md b/src/content/docs/bachelor/year-1/project-1-1.md index 8bde6b5..9878dde 100644 --- a/src/content/docs/bachelor/year-1/project-1-1.md +++ b/src/content/docs/bachelor/year-1/project-1-1.md @@ -3,8 +3,6 @@ title: Project 1-1 description: Historical course description and prerequisites for Project 1-1. --- -# Project 1-1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-1/project-1-2.md b/src/content/docs/bachelor/year-1/project-1-2.md index 38a3353..67ee031 100644 --- a/src/content/docs/bachelor/year-1/project-1-2.md +++ b/src/content/docs/bachelor/year-1/project-1-2.md @@ -3,8 +3,6 @@ title: Project 1-2 description: Historical course description, prerequisites, and recommended reading for Project 1-2. --- -# Project 1-2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-2/honours-programme.md b/src/content/docs/bachelor/year-2/honours-programme.md index 8516a25..34d9a0f 100644 --- a/src/content/docs/bachelor/year-2/honours-programme.md +++ b/src/content/docs/bachelor/year-2/honours-programme.md @@ -3,8 +3,6 @@ title: Honours Programme description: Historical placeholder for the Year 2 honours programme page. --- -# Honours Programme - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-2/index.md b/src/content/docs/bachelor/year-2/index.md index e75f6ca..877071c 100644 --- a/src/content/docs/bachelor/year-2/index.md +++ b/src/content/docs/bachelor/year-2/index.md @@ -3,8 +3,6 @@ title: Year 2 description: Historical schedule context and project information for the second year of the bachelor's programme. --- -# Year 2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-2/project-2-1.md b/src/content/docs/bachelor/year-2/project-2-1.md index 639cb0c..a9c7bb3 100644 --- a/src/content/docs/bachelor/year-2/project-2-1.md +++ b/src/content/docs/bachelor/year-2/project-2-1.md @@ -3,8 +3,6 @@ title: Project 2-1 description: Historical course description, prerequisites, and recommended reading for Project 2-1. --- -# Project 2-1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-2/project-2-2.md b/src/content/docs/bachelor/year-2/project-2-2.md index 07d496d..15a9447 100644 --- a/src/content/docs/bachelor/year-2/project-2-2.md +++ b/src/content/docs/bachelor/year-2/project-2-2.md @@ -3,8 +3,6 @@ title: Project 2-2 description: Historical course description, prerequisites, and recommended reading for Project 2-2. --- -# Project 2-2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-3/bachelors-thesis.md b/src/content/docs/bachelor/year-3/bachelors-thesis.md index 14a6553..05f5d01 100644 --- a/src/content/docs/bachelor/year-3/bachelors-thesis.md +++ b/src/content/docs/bachelor/year-3/bachelors-thesis.md @@ -3,8 +3,6 @@ title: Bachelor's Thesis description: Historical course description, prerequisites, and recommended reading for the bachelor's thesis. --- -# Bachelor's Thesis - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-3/honours-programme.md b/src/content/docs/bachelor/year-3/honours-programme.md index 316d23c..cac8276 100644 --- a/src/content/docs/bachelor/year-3/honours-programme.md +++ b/src/content/docs/bachelor/year-3/honours-programme.md @@ -3,8 +3,6 @@ title: DKE Honours Programme — KE@Work (3-1) description: Historical placeholder for the Year 3 honours programme page. --- -# DKE Honours Programme — KE@Work (3-1) - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-3/index.md b/src/content/docs/bachelor/year-3/index.md index c067cae..0a4b5c3 100644 --- a/src/content/docs/bachelor/year-3/index.md +++ b/src/content/docs/bachelor/year-3/index.md @@ -3,8 +3,6 @@ title: Year 3 description: Historical schedule context and course information for the third year of the bachelor's programme. --- -# Year 3 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-3/project-3-1.md b/src/content/docs/bachelor/year-3/project-3-1.md index 068b020..9b936e0 100644 --- a/src/content/docs/bachelor/year-3/project-3-1.md +++ b/src/content/docs/bachelor/year-3/project-3-1.md @@ -3,8 +3,6 @@ title: Project 3-1 description: Historical course description, prerequisites, and recommended reading for Project 3-1. --- -# Project 3-1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/bachelor/year-3/study-abroad.md b/src/content/docs/bachelor/year-3/study-abroad.md index 378577c..2de3675 100644 --- a/src/content/docs/bachelor/year-3/study-abroad.md +++ b/src/content/docs/bachelor/year-3/study-abroad.md @@ -3,8 +3,6 @@ title: Study Abroad description: Historical study-abroad guidance for the third year of the bachelor's programme. --- -# Study Abroad - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 479727f..5fef167 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -7,9 +7,7 @@ editUrl: false import { CardGrid, LinkButton, LinkCard } from '@astrojs/starlight/components'; -# Welcome to the knowledge base of MSV Incognito - -MSV Incognito is the study association for the Department of Data Science & Artificial Intelligence at Maastricht University. This wiki brings together study information and course resources for the programmes we support. +MSV Incognito is the study association for the Department of Advanced Computing Sciences at Maastricht University. This wiki brings together study information and course resources for the programmes we support.
No heading Generated error pageHome
About
Duplicate
Home