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.

Visit MSV Incognito @@ -27,7 +25,9 @@ MSV Incognito is the study association for the Department of Data Science & Arti ## Historical programme notes -This information originated in the previous wiki and may be outdated. For current curricula, consult the [official Maastricht University programme pages](https://www.maastrichtuniversity.nl/about-um/faculties/humanities-and-sciences/department-data-science-knowledge-engineering). +:::caution[Historical information] +This information originated in the previous wiki and may be outdated. For current curricula, consult the [Maastricht University education website](https://www.maastrichtuniversity.nl/education). +::: ### Maastricht University @@ -54,5 +54,5 @@ The bachelor’s programme includes Java and Matlab as primary languages, with P ## Useful official links - [MSV Incognito website](https://msvincognito.nl/) -- [Maastricht University department website](https://www.maastrichtuniversity.nl/about-um/faculties/humanities-and-sciences/department-data-science-knowledge-engineering) +- [Department of Advanced Computing Sciences](https://www.maastrichtuniversity.nl/research/department-advanced-computing-sciences) - [Maastricht University ICT manuals](https://www.maastrichtuniversity.nl/support/ict-services/manuals-and-information) diff --git a/src/content/docs/master-ai/index.md b/src/content/docs/master-ai/index.md index 388adbc..75f35fe 100644 --- a/src/content/docs/master-ai/index.md +++ b/src/content/docs/master-ai/index.md @@ -3,13 +3,10 @@ title: Artificial Intelligence description: Historical information about the master's programme in Artificial Intelligence at Maastricht University. --- -# Artificial Intelligence - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: -The following pages contain information about the master's programme in Artificial Intelligence at the Department of Data Science and Knowledge Engineering 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 master's programme in Artificial Intelligence at the Department of Data Science and Knowledge Engineering 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/master-ai/year-1/index.md b/src/content/docs/master-ai/year-1/index.md index d1eb1cb..7419816 100644 --- a/src/content/docs/master-ai/year-1/index.md +++ b/src/content/docs/master-ai/year-1/index.md @@ -3,8 +3,6 @@ title: Year 1 description: Historical course and research-project information for the first year of the master's programme in Artificial Intelligence. --- -# Year 1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/master-ai/year-1/research-project-1.md b/src/content/docs/master-ai/year-1/research-project-1.md index 6cc716f..da916d3 100644 --- a/src/content/docs/master-ai/year-1/research-project-1.md +++ b/src/content/docs/master-ai/year-1/research-project-1.md @@ -3,8 +3,6 @@ title: Research Project MAI 1 description: Historical description, prerequisites, and recommended reading for Research Project MAI 1. --- -# Research Project MAI 1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/master-ai/year-1/research-project-2.md b/src/content/docs/master-ai/year-1/research-project-2.md index c32f4a1..cf82bbd 100644 --- a/src/content/docs/master-ai/year-1/research-project-2.md +++ b/src/content/docs/master-ai/year-1/research-project-2.md @@ -3,8 +3,6 @@ title: Research Project MAI 2 description: Historical description, prerequisites, and recommended reading for Research Project MAI 2. --- -# Research Project MAI 2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/master-ai/year-2/index.md b/src/content/docs/master-ai/year-2/index.md index 11afc26..5c5f7ae 100644 --- a/src/content/docs/master-ai/year-2/index.md +++ b/src/content/docs/master-ai/year-2/index.md @@ -3,8 +3,6 @@ title: Year 2 description: Historical elective and thesis information for the second year of the master's programme in Artificial Intelligence. --- -# Year 2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/master-dsdm/index.md b/src/content/docs/master-dsdm/index.md index df2050a..3d5ee63 100644 --- a/src/content/docs/master-dsdm/index.md +++ b/src/content/docs/master-dsdm/index.md @@ -3,12 +3,10 @@ title: Data Science for Decision Making description: Historical information about the master's programme in Data Science for Decision Making at Maastricht University. --- -# Data Science for Decision Making - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: -The following pages contain information about the master's programme in Data Science for Decision Making at the Department of Data Science and Knowledge Engineering 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 master's programme in Data Science for Decision Making at the Department of Data Science and Knowledge Engineering 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/master-dsdm/year-1/index.md b/src/content/docs/master-dsdm/year-1/index.md index 3dce4d4..2bb9baf 100644 --- a/src/content/docs/master-dsdm/year-1/index.md +++ b/src/content/docs/master-dsdm/year-1/index.md @@ -3,8 +3,6 @@ title: Year 1 description: Historical course, elective, and project information for the first year of the master's programme in Data Science for Decision Making. --- -# Year 1 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: @@ -15,10 +13,6 @@ Year 1 has two courses per block in Blocks 1, 2, 4, and 5. There are four core c ## Core courses -:::note[Historical course list] -The following course list originated in the previous wiki and may have changed. -::: - - Block 1: Data Mining. - Block 2: Model Identification and Data Fitting. - Block 4: Algorithms for Big Data. @@ -26,10 +20,6 @@ The following course list originated in the previous wiki and may have changed. ## Electives -:::note[Historical course list] -The following elective list originated in the previous wiki and may have changed. -::: - - Block 1: Signal and Image Processing, Mathematical Optimization, and Stochastic Decision-Making. - Block 2: Advanced Concepts in Machine Learning and Applications of Image and Video Processing. - Block 4: Building and Mining Knowledge Graphs, Information Security, and Dynamic Game Theory. diff --git a/src/content/docs/master-dsdm/year-2/index.md b/src/content/docs/master-dsdm/year-2/index.md index 5a969d5..5c58163 100644 --- a/src/content/docs/master-dsdm/year-2/index.md +++ b/src/content/docs/master-dsdm/year-2/index.md @@ -3,18 +3,12 @@ title: Year 2 description: Historical elective and thesis information for the second year of the master's programme in Data Science for Decision Making. --- -# Year 2 - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: ## Schedule -:::note[Historical study structure] -The following study structure originated in the previous wiki and may have changed. -::: - The first semester of Year 2 consists of 30 credits of electives. The second semester of Year 2 consists of a 30-credit master's thesis. diff --git a/src/content/docs/useful-information/dke-locations.md b/src/content/docs/useful-information/dke-locations.md index ebfa310..bb8eda0 100644 --- a/src/content/docs/useful-information/dke-locations.md +++ b/src/content/docs/useful-information/dke-locations.md @@ -3,8 +3,6 @@ title: Locations of DKE description: Historical location information for the Department of Data Science and Knowledge Engineering at Maastricht University. --- -# Locations of DKE - :::caution[Historical information] This information originated in the previous wiki and may be outdated. Verify your current schedule and location with Maastricht University. ::: diff --git a/src/content/docs/useful-information/handy-locations.md b/src/content/docs/useful-information/handy-locations.md index 2f3267d..a1fe2ef 100644 --- a/src/content/docs/useful-information/handy-locations.md +++ b/src/content/docs/useful-information/handy-locations.md @@ -3,8 +3,6 @@ title: Handy Locations description: Historical suggestions for useful student locations in Maastricht. --- -# Handy Locations - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: diff --git a/src/content/docs/useful-information/index.md b/src/content/docs/useful-information/index.md index 24e9644..e32853e 100644 --- a/src/content/docs/useful-information/index.md +++ b/src/content/docs/useful-information/index.md @@ -3,8 +3,6 @@ title: Useful Information description: Historical student information for the Department of Data Science and Knowledge Engineering at Maastricht University. --- -# Useful Information - :::caution[Historical information] This information originated in the previous wiki and may be outdated. ::: @@ -13,7 +11,7 @@ The following pages contain useful information for students of the Department of For the most up-to-date overview of our studies, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education). -If you would like to add useful information that is not yet listed below, please add it to the most relevant page. If no page seems relevant, please create a new page. +Repository maintainers can add useful information to the most relevant Markdown page and update the manual sidebar. They should add a new page only when no existing page fits. ## Useful pages diff --git a/src/content/docs/useful-information/it-services.md b/src/content/docs/useful-information/it-services.md index 8d32ec2..d5ef1d0 100644 --- a/src/content/docs/useful-information/it-services.md +++ b/src/content/docs/useful-information/it-services.md @@ -3,8 +3,6 @@ title: IT Services description: Historical IT service guidance for Maastricht University students. --- -# IT Services - :::caution[Historical information] This information originated in the previous wiki and may be outdated. The instructions below are retained for reference only. Verify EleUM, `unimaas.nl` accounts, MyUM navigation, 2016–2017 timetable information, VPN access, printing, service-desk hours, and every contact detail with Maastricht University before relying on them. ::: diff --git a/src/content/docs/useful-information/laptop-buying-advice.md b/src/content/docs/useful-information/laptop-buying-advice.md index 55d5171..c297b82 100644 --- a/src/content/docs/useful-information/laptop-buying-advice.md +++ b/src/content/docs/useful-information/laptop-buying-advice.md @@ -3,8 +3,6 @@ title: Laptop Buying Advice description: Historical laptop-buying guidance for DKE students. --- -# Laptop Buying Advice - :::caution[Historical information] This information originated in the previous wiki and may be outdated. Prices, Windows versions, hardware capacities, and processor availability below are historical and must be verified before making a purchase. ::: diff --git a/tests/content-audit.test.mjs b/tests/content-audit.test.mjs index 04d28da..caa675d 100644 --- a/tests/content-audit.test.mjs +++ b/tests/content-audit.test.mjs @@ -1,9 +1,37 @@ import assert from 'node:assert/strict'; -import { readFile } from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import test from 'node:test'; import { auditContent } from '../scripts/audit-content.mjs'; import { loadManifest } from '../scripts/lib/migration.mjs'; +async function withAuditFixture(run) { + const root = await mkdtemp(join(tmpdir(), 'incognito-audit-')); + const docsRoot = join(root, 'src/content/docs'); + try { + await mkdir(join(root, 'src/content'), { recursive: true }); + await cp('src/content/docs', docsRoot, { recursive: true }); + await mkdir(join(root, 'docs'), { recursive: true }); + await cp('docs/migration-report.md', join(root, 'docs/migration-report.md')); + await mkdir(join(root, 'public'), { recursive: true }); + const manifest = structuredClone(await loadManifest('docs/migration-manifest.json')); + await run({ root, docsRoot, manifest }); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +async function expectAuditFailure(mutate, pattern) { + await withAuditFixture(async ({ root, docsRoot, manifest }) => { + await mutate({ root, docsRoot, manifest }); + await assert.rejects( + auditContent({ docsRoot, manifest }), + pattern, + ); + }); +} + test('the full migration audit accounts for every source and destination', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); const result = await auditContent({ @@ -16,6 +44,241 @@ test('the full migration audit accounts for every source and destination', async assert.equal(result.pageCount, 28); }); +test('published source pages leave the sole H1 to Starlight frontmatter rendering', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))]; + + for (const destination of destinations) { + const content = await readFile(destination, 'utf8'); + assert.doesNotMatch(content, /^#\s+/m, `${destination} contains a body H1`); + } +}); + +test('present-day association introductions use the current DACS name', async () => { + for (const destination of [ + 'src/content/docs/index.mdx', + 'src/content/docs/about-incognito.md', + ]) { + const content = await readFile(destination, 'utf8'); + assert.match(content, /Department of Advanced Computing Sciences/); + assert.doesNotMatch(content, /study association for the Department of Data Science & Artificial Intelligence/); + } +}); + +test('programme overviews disclose that promised summaries and exams were absent from the export', async () => { + for (const destination of [ + 'src/content/docs/bachelor/index.md', + 'src/content/docs/master-ai/index.md', + 'src/content/docs/master-dsdm/index.md', + ]) { + const content = await readFile(destination, 'utf8'); + assert.match(content, /not present in the export/i); + assert.doesNotMatch(content, /Course summaries and old exams can be found in their respective folders\./); + } +}); + +test('the home historical warning uses the standard Starlight caution', async () => { + const content = await readFile('src/content/docs/index.mdx', 'utf8'); + assert.match(content, /:::caution\[Historical information\]\nThis information originated in the previous wiki and may be outdated\.[\s\S]*?\n:::/); +}); + +test('Useful Information scopes contribution guidance to repository maintainers', async () => { + const content = await readFile('src/content/docs/useful-information/index.md', 'utf8'); + assert.match(content, /Repository maintainers/); + assert.doesNotMatch(content, /please create a new page/i); +}); + +test('DSDM year pages rely on their page-level caution without redundant inner notes', async () => { + for (const destination of [ + 'src/content/docs/master-dsdm/year-1/index.md', + 'src/content/docs/master-dsdm/year-2/index.md', + ]) { + const content = await readFile(destination, 'utf8'); + assert.doesNotMatch(content, /:::note\[Historical (?:course list|study structure)\]/); + } +}); + +test('all 29 migration-report rows classify link handling truthfully', async () => { + const report = await readFile('docs/migration-report.md', 'utf8'); + const expectedPrefixes = new Map([ + ['pages/start.txt', '**Converted source links:**'], + ['pages/study.txt', '**Converted source links:**'], + ['pages/study/msv_incognito.txt', '**Converted source link:**'], + ['pages/study/bachelor.txt', '**Converted source link:**'], + ['pages/study/bachelor/year_1.txt', '**Newly verified links:**'], + ['pages/study/bachelor/year_1/project_1-1.txt', '**None.**'], + ['pages/study/bachelor/year_1/project_1-2.txt', '**None.**'], + ['pages/study/bachelor/year_2.txt', '**Newly verified links:**'], + ['pages/study/bachelor/year_2/honours_programme.txt', '**None.**'], + ['pages/study/bachelor/year_2/project_2-1.txt', '**Converted plain-text reference:**'], + ['pages/study/bachelor/year_2/project_2-2.txt', '**Converted plain-text reference:**'], + ['pages/study/bachelor/year_3.txt', '**Newly verified links:**'], + ['pages/study/bachelor/year_3/bachelors_thesis.txt', '**None.**'], + ['pages/study/bachelor/year_3/honours_programme.txt', '**None.**'], + ['pages/study/bachelor/year_3/project_3-1.txt', '**Converted plain-text reference:**'], + ['pages/study/bachelor/year_3/study_abroad.txt', '**Newly verified link:**'], + ['pages/study/master_ai.txt', '**Converted source link:**'], + ['pages/study/master_ai/year_1.txt', '**Newly verified links:**'], + ['pages/study/master_ai/year_1/project_mai_1.txt', '**None.**'], + ['pages/study/master_ai/year_1/project_mai_2.txt', '**None.**'], + ['pages/study/master_ai/year_2.txt', '**None.**'], + ['pages/study/master_dsdm.txt', '**Converted source link:**'], + ['pages/study/master_dsdm/year_1.txt', '**None.**'], + ['pages/study/master_dsdm/year_2.txt', '**None.**'], + ['pages/study/useful_information.txt', '**Converted source link:**'], + ['pages/study/useful_information/pages/dke_locations.txt', '**Converted source links:**'], + ['pages/study/useful_information/pages/handy_locations.txt', '**None.**'], + ['pages/study/useful_information/pages/it_services.txt', '**Converted source links:**'], + ['pages/study/useful_information/pages/laptop_buy_advice.txt', '**None.**'], + ]); + const rows = report.split('\n').filter((line) => line.startsWith('| `pages/')); + + assert.equal(rows.length, expectedPrefixes.size); + for (const row of rows) { + const cells = row.split(' | '); + const source = cells[0].match(/^\| `([^`]+)`$/)?.[1]; + assert.ok(source, `could not parse report source from ${row}`); + assert.ok(expectedPrefixes.has(source), `unexpected report source ${source}`); + assert.ok( + cells[4].startsWith(expectedPrefixes.get(source)), + `${source} link handling must start with ${expectedPrefixes.get(source)}`, + ); + } + + assert.match(report, /\| Link handling \|/); + assert.doesNotMatch(report, /Surfgroepen|DKE file server|old namespace link/i); + assert.match( + report, + /pages\/study\/bachelor\/year_3\/study_abroad\.txt[^\n]*\*\*Newly verified link:\*\* the official UM study-abroad page/, + ); +}); + +test('audit rejects a published page with missing frontmatter', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + await writeFile(join(docsRoot, 'about-incognito.md'), 'Missing frontmatter.\n'); + }, /missing YAML frontmatter/); +}); + +test('audit rejects frontmatter with an empty required field', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + await writeFile(join(docsRoot, 'about-incognito.md'), [ + '---', + 'title:', + 'description: Still described.', + '---', + '', + 'Body.', + ].join('\n')); + }, /missing non-empty title frontmatter/); +}); + +test('audit rejects syntactically malformed YAML frontmatter', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + await writeFile(join(docsRoot, 'about-incognito.md'), [ + '---', + 'title: "unterminated', + 'description: Still described.', + '---', + '', + 'Body.', + ].join('\n')); + }, /malformed YAML frontmatter/); +}); + +test('audit rejects forbidden DokuWiki syntax in published content', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + const page = join(docsRoot, 'about-incognito.md'); + await writeFile(page, `${await readFile(page, 'utf8')}\n[[forbidden:link]]\n`); + }, /contains forbidden DokuWiki token/); +}); + +test('audit rejects a missing published document', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + await rm(join(docsRoot, 'about-incognito.md')); + }, /missing manifest destination: src\/content\/docs\/about-incognito\.md/); +}); + +test('audit rejects an extra Markdown document outside the manifest destination set', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + await writeFile(join(docsRoot, 'extra.md'), [ + '---', + 'title: Extra', + 'description: Not in the migration contract.', + '---', + ].join('\n')); + }, /published content file is absent from manifest: src\/content\/docs\/extra\.md/); +}); + +test('audit rejects a directory where the manifest requires a regular file', async () => { + await expectAuditFailure(async ({ docsRoot }) => { + const destination = join(docsRoot, 'about-incognito.md'); + await rm(destination); + await mkdir(destination); + }, /manifest destination is not a regular file: src\/content\/docs\/about-incognito\.md/); +}); + +test('audit rejects coordinated manifest and report drift from the canonical sources', async () => { + await expectAuditFailure(async ({ root, manifest }) => { + const original = 'pages/study/msv_incognito.txt'; + const fabricated = 'pages/study/fabricated.txt'; + manifest.entries[2].source = fabricated; + const reportPath = join(root, 'docs/migration-report.md'); + const report = await readFile(reportPath, 'utf8'); + await writeFile(reportPath, report.replace(original, fabricated)); + }, /canonical migration contract/); +}); + +test('audit rejects coordinated destination, report, and published-file drift', async () => { + await expectAuditFailure(async ({ root, docsRoot, manifest }) => { + const original = 'src/content/docs/about-incognito.md'; + const drifted = 'src/content/docs/association.md'; + manifest.entries[2].destination = drifted; + await rename( + join(docsRoot, 'about-incognito.md'), + join(docsRoot, 'association.md'), + ); + const reportPath = join(root, 'docs/migration-report.md'); + const report = await readFile(reportPath, 'utf8'); + await writeFile(reportPath, report.replace(original, drifted)); + }, /canonical migration contract/); +}); + +test('audit rejects a report row whose mapping differs from the manifest', async () => { + await expectAuditFailure(async ({ root }) => { + const reportPath = join(root, 'docs/migration-report.md'); + const report = await readFile(reportPath, 'utf8'); + await writeFile( + reportPath, + report.replace( + '`src/content/docs/about-incognito.md` | page', + '`src/content/docs/not-about-incognito.md` | page', + ), + ); + }, /migration report mapping differs from manifest: pages\/study\/msv_incognito\.txt/); +}); + +test('audit rejects docs roots that merely end with a false src/content/docs suffix', async () => { + const root = await mkdtemp(join(tmpdir(), 'incognito-false-root-')); + try { + const docsRoot = join(root, 'not-src/content/docs'); + await mkdir(docsRoot, { recursive: true }); + const manifest = await loadManifest('docs/migration-manifest.json'); + await assert.rejects( + auditContent({ docsRoot, manifest }), + /docsRoot must identify src\/content\/docs/, + ); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test('audit rejects to-be-studied directories on either publication path', async () => { + await expectAuditFailure(async ({ root, docsRoot }) => { + await mkdir(join(docsRoot, 'to-be-studied'), { recursive: true }); + await mkdir(join(root, 'public/to-be-studied'), { recursive: true }); + }, /to-be-studied must not appear under src\/content\/docs/); +}); + const forbidden = [/\[\[/, /\{\{/, /~~NOCACHE~~/, /NEWPAGE>/, /indexmenu>/]; for (const file of [ diff --git a/tests/internal-links.test.mjs b/tests/internal-links.test.mjs index c62c7eb..c2b1062 100644 --- a/tests/internal-links.test.mjs +++ b/tests/internal-links.test.mjs @@ -32,6 +32,17 @@ test('accepts an existing directory index route', async () => { }); }); +test('accepts an existing exact-file link', async () => { + await withBuiltSite(async (root) => { + await writeFile(join(root, 'index.html'), 'Manual'); + await writeFile(join(root, 'manual.pdf'), 'manual'); + + const result = await checkInternalLinks({ distRoot: root, base: '/' }); + + assert.equal(result.broken.length, 0); + }); +}); + test('maps the root route to the built root index', async () => { await withBuiltSite(async (root) => { await writeFile(join(root, 'index.html'), 'Home'); @@ -86,3 +97,24 @@ test('ignores fragments, remote protocols, and Astro-generated assets', async () assert.equal(result.broken.length, 0); }); }); + +for (const { name, href } of [ + { name: 'encoded slash', href: '/..%2Fmanual.pdf' }, + { name: 'encoded backslash', href: '/..%5Cmanual.pdf' }, +]) { + test(`rejects ${name} traversal even when the escaped target exists`, async () => { + const siteRoot = await mkdtemp(join(tmpdir(), 'incognito-links-escape-')); + const distRoot = join(siteRoot, 'dist'); + try { + await mkdir(distRoot); + await writeFile(join(siteRoot, 'manual.pdf'), 'outside dist'); + await writeFile(join(distRoot, 'index.html'), `Escaped`); + + const result = await checkInternalLinks({ distRoot, base: '/' }); + + assert.deepEqual(result.broken.map((broken) => broken.href), [href]); + } finally { + await rm(siteRoot, { recursive: true, force: true }); + } + }); +} diff --git a/tests/migration-manifest.test.mjs b/tests/migration-manifest.test.mjs index 5e7095a..06ffe5f 100644 --- a/tests/migration-manifest.test.mjs +++ b/tests/migration-manifest.test.mjs @@ -24,6 +24,27 @@ test('validation rejects source traversal outside the selected scope', async () assert.throws(() => validateManifest(manifest), /outside the selected migration scope/); }); +test('validation rejects backslash source traversal outside the selected scope', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].source = 'pages/study/..\\..\\wiki\\x.txt'; + + assert.throws(() => validateManifest(manifest), /outside the selected migration scope/); +}); + +test('validation rejects a fabricated source inside the broad study path', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].source = 'pages/study/fabricated.txt'; + + assert.throws(() => validateManifest(manifest), /canonical migration contract/); +}); + +test('validation rejects a mode that differs from the canonical migration contract', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].mode = 'merge'; + + assert.throws(() => validateManifest(manifest), /canonical migration contract/); +}); + test('validation rejects destination traversal outside the docs root', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); manifest.entries[2].destination = 'src/content/docs/../../../outside.md'; @@ -31,6 +52,13 @@ test('validation rejects destination traversal outside the docs root', async () assert.throws(() => validateManifest(manifest), /outside src\/content\/docs/); }); +test('validation rejects backslash destination traversal outside the docs root', async () => { + const manifest = await loadManifest('docs/migration-manifest.json'); + manifest.entries[2].destination = 'src/content/docs/..\\..\\..\\outside.md'; + + assert.throws(() => validateManifest(manifest), /outside src\/content\/docs/); +}); + test('validation rejects duplicate destinations unless the later entry is a merge', async () => { const manifest = await loadManifest('docs/migration-manifest.json'); manifest.entries[1].mode = 'page'; @@ -57,3 +85,19 @@ test('destination check reports only files that have not been migrated', async ( await rm(root, { recursive: true, force: true }); } }); + +test('destination check rejects a directory at a manifest file path', async () => { + const root = await mkdir(join(tmpdir(), `migration-${process.pid}-${Date.now()}`), { recursive: true }); + const directory = join(root, 'src/content/docs/not-a-file.md'); + await mkdir(directory, { recursive: true }); + + try { + const result = await checkDestinations({ + entries: [{ destination: 'src/content/docs/not-a-file.md' }], + }, root); + + assert.deepEqual(result.notFiles, ['src/content/docs/not-a-file.md']); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); diff --git a/tests/project-structure.test.mjs b/tests/project-structure.test.mjs index 6f6caba..99ba54f 100644 --- a/tests/project-structure.test.mjs +++ b/tests/project-structure.test.mjs @@ -83,6 +83,18 @@ test('the optional GitHub Pages workflow deploys only main or manual runs', asyn assert.match(workflow, /actions\/deploy-pages@v5/); }); +test('GitHub Pages write and OIDC permissions are scoped only to deployment', async () => { + const workflow = await readFile('.github/workflows/deploy.yml', 'utf8'); + assert.doesNotMatch(workflow, /^permissions:/m); + + const build = workflow.match(/\n build:\n([\s\S]*?)(?=\n deploy:\n)/)?.[1] ?? ''; + assert.match(build, /^ permissions:\n contents: read$/m); + assert.doesNotMatch(build, /pages: write|id-token: write/); + + const deploy = workflow.match(/\n deploy:\n([\s\S]*)$/)?.[1] ?? ''; + assert.match(deploy, /^ permissions:\n pages: write\n id-token: write$/m); +}); + test('maintainer documentation keeps Forgejo CI-independent', async () => { const readme = await readFile('README.md', 'utf8'); assert.match(readme, /Forgejo[^\n]*authoritative private repository/i); diff --git a/tests/rendered-output.test.mjs b/tests/rendered-output.test.mjs new file mode 100644 index 0000000..13de53b --- /dev/null +++ b/tests/rendered-output.test.mjs @@ -0,0 +1,46 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import test from 'node:test'; +import { checkRenderedOutput } from '../scripts/lib/rendered-output.mjs'; + +async function withRenderedSite(run) { + const root = await mkdtemp(join(tmpdir(), 'incognito-rendered-')); + try { + await run(root); + } finally { + await rm(root, { recursive: true, force: true }); + } +} + +test('reports every published HTML page that does not contain exactly one H1', async () => { + await withRenderedSite(async (root) => { + await mkdir(join(root, 'about'), { recursive: true }); + await mkdir(join(root, 'empty'), { recursive: true }); + await writeFile(join(root, 'index.html'), '

Home

'); + await writeFile(join(root, 'about/index.html'), '

About

Duplicate

'); + await writeFile(join(root, 'empty/index.html'), '

No heading

'); + await writeFile(join(root, '404.html'), '

Generated error page

'); + + const result = await checkRenderedOutput({ distRoot: root }); + + assert.equal(result.htmlFilesChecked, 3); + assert.deepEqual(result.headingIssues, [ + { file: 'about/index.html', h1Count: 2 }, + { file: 'empty/index.html', h1Count: 0 }, + ]); + }); +}); + +test('reports any to-be-studied path that reaches built output', async () => { + await withRenderedSite(async (root) => { + await writeFile(join(root, 'index.html'), '

Home

'); + await mkdir(join(root, 'to-be-studied'), { recursive: true }); + await writeFile(join(root, 'to-be-studied/leak.txt'), 'unpublished'); + + const result = await checkRenderedOutput({ distRoot: root }); + + assert.deepEqual(result.unpublishedPaths, ['to-be-studied/leak.txt']); + }); +});