fix: resolve final Starlight migration review
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

This commit is contained in:
msa46 2026-08-02 18:59:28 +02:00
parent 583cf83e65
commit bcb8dcfe2a
43 changed files with 637 additions and 140 deletions

View file

@ -7,13 +7,10 @@ on:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs: jobs:
build: build:
permissions:
contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the mirror - name: Check out the mirror
@ -27,6 +24,9 @@ jobs:
BASE: ${{ vars.BASE_PATH }} BASE: ${{ vars.BASE_PATH }}
deploy: deploy:
permissions:
pages: write
id-token: write
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: environment:

View file

@ -1,42 +1,42 @@
# DokuWiki migration report # 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 ## 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/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 | Replaced namespace navigation with landing-page cards and official links | None | Corrected punctuation, capitalization, and bachelor/master wording | | `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; 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/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 year namespace links to Starlight routes; removed DokuWiki editing controls | None | Corrected bachelor capitalization and sentence structure | | `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 | 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.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 | 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-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 | Converted related-project links and removed editing controls | None | Corrected grammar, spacing, and capitalization | | `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 | 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.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 | Removed empty DokuWiki navigation/edit controls | None | Preserved the empty page as an explicit awaiting-content placeholder | | `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 | 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-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 | Repaired the nested route to the Year 1 prerequisite; removed editing controls | None | Corrected punctuation 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 | Converted thesis and project links; removed DokuWiki navigation controls | None | Corrected block references and the schedule explanation | | `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 | Converted related-study links and removed editing controls | None | Corrected possessive bachelors wording, punctuation, and sentence flow | | `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 bachelors 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/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 prerequisite links and removed editing controls | None | Corrected grammar while retaining the stated prerequisites and reading | | `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 | Retained useful external destinations as historical references and removed editing controls | None | Corrected grammar; did not silently modernize the dated eligibility claims | | `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 year links to final routes and removed namespace controls | None | Standardized masters-programme capitalization and punctuation | | `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 masters-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 | Converted research-project links and removed editing controls | None | Corrected sentence structure and project naming | | `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 | Repaired internal navigation to the programme hierarchy | None | Corrected punctuation and rendered the project name consistently | | `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 | 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 | **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 | Removed obsolete DokuWiki page-creation and navigation controls | None | Reworded incomplete prose as historical programme guidance | | `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 year links to final routes and removed namespace controls | None | Standardized DSDM and masters-programme capitalization | | `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 masters-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 | Removed obsolete DokuWiki page-creation and navigation controls | None | Corrected punctuation and retained the source-era structure | | `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 | 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 | **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 child-page links to final Starlight routes; removed DokuWiki controls | None | Corrected heading capitalization and introductory grammar | | `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 | 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/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 | Removed empty DokuWiki edit links | None | Corrected capitalization; marked the two empty sections as awaiting content | | `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 | 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/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 | 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/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 ## 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 ## 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 ## Removed DokuWiki controls
@ -56,11 +60,11 @@ The Bachelor Year 2 source combined its missing schedule embed with a malformed
## Historical Study Abroad guidance ## 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 ## 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 ## Historical laptop advice

1
package-lock.json generated
View file

@ -14,6 +14,7 @@
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "0.9.10", "@astrojs/check": "0.9.10",
"js-yaml": "4.3.1",
"linkedom": "0.18.13", "linkedom": "0.18.13",
"turndown": "7.2.4", "turndown": "7.2.4",
"typescript": "6.0.2" "typescript": "6.0.2"

View file

@ -12,9 +12,10 @@
"check:migration": "node scripts/check-migration.mjs", "check:migration": "node scripts/check-migration.mjs",
"audit:content": "node scripts/audit-content.mjs", "audit:content": "node scripts/audit-content.mjs",
"check:links": "node scripts/check-internal-links.mjs dist", "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", "capture:live-wiki": "node scripts/capture-live-wiki.mjs",
"test": "node --test", "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": { "dependencies": {
"@astrojs/starlight": "0.41.6", "@astrojs/starlight": "0.41.6",
@ -23,6 +24,7 @@
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "0.9.10", "@astrojs/check": "0.9.10",
"js-yaml": "4.3.1",
"linkedom": "0.18.13", "linkedom": "0.18.13",
"turndown": "7.2.4", "turndown": "7.2.4",
"typescript": "6.0.2" "typescript": "6.0.2"

View file

@ -1,6 +1,7 @@
import { access, readFile, readdir } from 'node:fs/promises'; import { access, readFile, readdir } from 'node:fs/promises';
import { join, relative, resolve, sep } from 'node:path'; import { join, relative, resolve, sep } from 'node:path';
import { pathToFileURL } from 'node:url'; import { pathToFileURL } from 'node:url';
import { load as loadYaml } from 'js-yaml';
import { checkDestinations, loadManifest, validateManifest } from './lib/migration.mjs'; import { checkDestinations, loadManifest, validateManifest } from './lib/migration.mjs';
const DOCS_PREFIX = 'src/content/docs/'; const DOCS_PREFIX = 'src/content/docs/';
@ -59,9 +60,17 @@ async function containsNamedDirectory(root, name) {
function frontmatterIssue(content) { function frontmatterIssue(content) {
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
if (!match) return 'is missing YAML frontmatter'; 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']) { for (const field of ['title', 'description']) {
const fieldMatch = match[1].match(new RegExp(`^${field}:\\s*(.+?)\\s*$`, 'm')); const value = frontmatter && typeof frontmatter === 'object' ? frontmatter[field] : undefined;
if (!fieldMatch || /^(['"]{2})$/.test(fieldMatch[1])) return `is missing non-empty ${field} frontmatter`; if (typeof value !== 'string' || !value.trim()) return `is missing non-empty ${field} frontmatter`;
} }
return null; return null;
} }
@ -73,11 +82,11 @@ function reportRows(report) {
function projectRootFor(docsRoot) { function projectRootFor(docsRoot) {
const normalized = resolve(docsRoot); const normalized = resolve(docsRoot);
const suffix = ['src', 'content', 'docs'].join(sep); const projectRoot = resolve(normalized, '..', '..', '..');
if (!normalized.endsWith(suffix)) { if (resolve(projectRoot, 'src', 'content', 'docs') !== normalized) {
throw new TypeError(`docsRoot must identify src/content/docs: ${docsRoot}`); throw new TypeError(`docsRoot must identify src/content/docs: ${docsRoot}`);
} }
return resolve(normalized, '..', '..', '..'); return projectRoot;
} }
export async function auditContent({ docsRoot, manifest }) { export async function auditContent({ docsRoot, manifest }) {
@ -100,6 +109,7 @@ export async function auditContent({ docsRoot, manifest }) {
const destinationResult = await checkDestinations({ entries }, projectRoot); const destinationResult = await checkDestinations({ entries }, projectRoot);
for (const destination of destinationResult.missing) issues.push(`missing manifest destination: ${destination}`); 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 contentFiles = await listContentFiles(absoluteDocsRoot);
const contentByDestination = new Map(); 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) { for (const [localPath, notice] of EMPTY_PAGE_NOTICES) {
const content = contentByDestination.get(`${DOCS_PREFIX}${localPath}`) ?? ''; const content = contentByDestination.get(`${DOCS_PREFIX}${localPath}`) ?? '';
if (!content.includes(notice)) issues.push(`${DOCS_PREFIX}${localPath} is missing the exact awaiting-content notice`); if (!content.includes(notice)) issues.push(`${DOCS_PREFIX}${localPath} is missing the exact awaiting-content notice`);

View file

@ -10,8 +10,11 @@ console.log(`Migration manifest accounts for ${result.total} source pages.`);
for (const destination of result.missing) { for (const destination of result.missing) {
console.log(`Missing destination: ${destination}`); 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) { if ((result.missing.length > 0 && !allowMissing) || result.notFiles.length > 0) {
console.error(`Migration check failed: ${result.missing.length} destination file(s) are missing.`); 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; process.exitCode = 1;
} }

View file

@ -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;
});
}

View file

@ -1,5 +1,5 @@
import { access, readFile, readdir, stat } from 'node:fs/promises'; import { readFile, readdir, stat } from 'node:fs/promises';
import { join, relative, sep } from 'node:path'; import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { parseHTML } from 'linkedom'; import { parseHTML } from 'linkedom';
function normalizeBase(base) { function normalizeBase(base) {
@ -52,15 +52,24 @@ async function isBuiltTarget(distRoot, route) {
return false; return false;
} }
const relativeTarget = decoded.replace(/^\/+/, ''); const normalized = decoded.replaceAll('\\', '/');
const exact = join(distRoot, relativeTarget); if (normalized.split('/').includes('..')) return false;
const candidates = decoded.endsWith('/')
const absoluteDistRoot = resolve(distRoot);
const relativeTarget = normalized.replace(/^\/+/, '');
const exact = resolve(absoluteDistRoot, relativeTarget);
const candidates = normalized.endsWith('/')
? [join(exact, 'index.html')] ? [join(exact, 'index.html')]
: [exact, join(exact, 'index.html')]; : [exact, join(exact, 'index.html')];
for (const candidate of candidates) { for (const candidate of candidates) {
const candidateRelativePath = relative(absoluteDistRoot, candidate);
if (isAbsolute(candidateRelativePath)
|| candidateRelativePath === '..'
|| candidateRelativePath.startsWith(`..${sep}`)) {
continue;
}
try { try {
await access(candidate);
if ((await stat(candidate)).isFile()) return true; if ((await stat(candidate)).isFile()) return true;
} catch { } catch {
// Try the directory-index form before reporting the route. // Try the directory-index form before reporting the route.

View file

@ -1,12 +1,51 @@
import { readFile } from 'node:fs/promises'; import { readFile, stat } from 'node:fs/promises';
import { access } from 'node:fs/promises'; import { isAbsolute, relative, resolve, sep } from 'node:path';
import { join } from 'node:path';
const SOURCE_COUNT = 29; const SOURCE_COUNT = 29;
const DOCS_ROOT = 'src/content/docs/'; 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) { 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) { export async function loadManifest(path) {
@ -37,13 +76,19 @@ export function validateManifest(manifest) {
if (!['page', 'merge'].includes(mode)) { if (!['page', 'merge'].includes(mode)) {
throw new TypeError('Each migration manifest entry mode must be page or merge.'); 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}`); 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}`); 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}`); throw new Error(`Destination is outside ${DOCS_ROOT}: ${destination}`);
} }
if (sources.has(source)) { if (sources.has(source)) {
@ -57,16 +102,31 @@ export function validateManifest(manifest) {
destinations.add(destination); 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 }; return { entries: manifest.entries };
} }
export async function checkDestinations(manifest, root) { export async function checkDestinations(manifest, root) {
const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))]; const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))];
const missing = []; const missing = [];
const notFiles = [];
const projectRoot = resolve(root);
const docsRoot = resolve(projectRoot, 'src/content/docs');
for (const destination of destinations) { 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 { try {
await access(join(root, destination)); const details = await stat(candidate);
if (!details.isFile()) notFiles.push(destination);
} catch { } catch {
missing.push(destination); missing.push(destination);
} }
@ -75,5 +135,6 @@ export async function checkDestinations(manifest, root) {
return { return {
total: manifest.entries.length, total: manifest.entries.length,
missing, missing,
notFiles,
}; };
} }

View file

@ -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')),
};
}

View file

@ -3,8 +3,6 @@ title: About Incognito
description: Learn more about MSV Incognito, the study association behind this wiki. description: Learn more about MSV Incognito, the study association behind this wiki.
--- ---
# MSV Incognito MSV Incognito is the study association for the Department of Advanced Computing Sciences at Maastricht University.
MSV Incognito is the study association for the Department of Data Science & Artificial Intelligence 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. 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.

View file

@ -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. description: Information about the bachelor's programme in Data Science and Artificial Intelligence at Maastricht University.
--- ---
# Data Science and Artificial Intelligence
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. 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). For the most up-to-date overview of the courses taught, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education).

View file

@ -3,8 +3,6 @@ title: Year 1
description: Historical schedule context and project information for the first year of the bachelor's programme. description: Historical schedule context and project information for the first year of the bachelor's programme.
--- ---
# Year 1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Project 1-1
description: Historical course description and prerequisites for Project 1-1. description: Historical course description and prerequisites for Project 1-1.
--- ---
# Project 1-1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Project 1-2
description: Historical course description, prerequisites, and recommended reading for Project 1-2. description: Historical course description, prerequisites, and recommended reading for Project 1-2.
--- ---
# Project 1-2
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Honours Programme
description: Historical placeholder for the Year 2 honours programme page. description: Historical placeholder for the Year 2 honours programme page.
--- ---
# Honours Programme
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Year 2
description: Historical schedule context and project information for the second year of the bachelor's programme. description: Historical schedule context and project information for the second year of the bachelor's programme.
--- ---
# Year 2
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Project 2-1
description: Historical course description, prerequisites, and recommended reading for Project 2-1. description: Historical course description, prerequisites, and recommended reading for Project 2-1.
--- ---
# Project 2-1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Project 2-2
description: Historical course description, prerequisites, and recommended reading for Project 2-2. description: Historical course description, prerequisites, and recommended reading for Project 2-2.
--- ---
# Project 2-2
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Bachelor's Thesis
description: Historical course description, prerequisites, and recommended reading for the bachelor's thesis. description: Historical course description, prerequisites, and recommended reading for the bachelor's thesis.
--- ---
# Bachelor's Thesis
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: DKE Honours Programme — KE@Work (3-1)
description: Historical placeholder for the Year 3 honours programme page. description: Historical placeholder for the Year 3 honours programme page.
--- ---
# DKE Honours Programme — KE@Work (3-1)
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Year 3
description: Historical schedule context and course information for the third year of the bachelor's programme. description: Historical schedule context and course information for the third year of the bachelor's programme.
--- ---
# Year 3
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Project 3-1
description: Historical course description, prerequisites, and recommended reading for Project 3-1. description: Historical course description, prerequisites, and recommended reading for Project 3-1.
--- ---
# Project 3-1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Study Abroad
description: Historical study-abroad guidance for the third year of the bachelor's programme. description: Historical study-abroad guidance for the third year of the bachelor's programme.
--- ---
# Study Abroad
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -7,9 +7,7 @@ editUrl: false
import { CardGrid, LinkButton, LinkCard } from '@astrojs/starlight/components'; 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 Advanced Computing Sciences 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 Data Science & Artificial Intelligence at Maastricht University. This wiki brings together study information and course resources for the programmes we support.
<p> <p>
<LinkButton href="https://msvincognito.nl/" icon="external">Visit MSV Incognito</LinkButton> <LinkButton href="https://msvincognito.nl/" icon="external">Visit MSV Incognito</LinkButton>
@ -27,7 +25,9 @@ MSV Incognito is the study association for the Department of Data Science & Arti
## Historical programme notes ## 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 ### Maastricht University
@ -54,5 +54,5 @@ The bachelors programme includes Java and Matlab as primary languages, with P
## Useful official links ## Useful official links
- [MSV Incognito website](https://msvincognito.nl/) - [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) - [Maastricht University ICT manuals](https://www.maastrichtuniversity.nl/support/ict-services/manuals-and-information)

View file

@ -3,13 +3,10 @@ title: Artificial Intelligence
description: Historical information about the master's programme in Artificial Intelligence at Maastricht University. description: Historical information about the master's programme in Artificial Intelligence at Maastricht University.
--- ---
# Artificial Intelligence
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. 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). For the most up-to-date overview of the courses taught, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education).

View file

@ -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. description: Historical course and research-project information for the first year of the master's programme in Artificial Intelligence.
--- ---
# Year 1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Research Project MAI 1
description: Historical description, prerequisites, and recommended reading for Research Project MAI 1. description: Historical description, prerequisites, and recommended reading for Research Project MAI 1.
--- ---
# Research Project MAI 1
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Research Project MAI 2
description: Historical description, prerequisites, and recommended reading for Research Project MAI 2. description: Historical description, prerequisites, and recommended reading for Research Project MAI 2.
--- ---
# Research Project MAI 2
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -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. description: Historical elective and thesis information for the second year of the master's programme in Artificial Intelligence.
--- ---
# Year 2
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -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. 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] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. 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). For the most up-to-date overview of the courses taught, visit the [Maastricht University education website](https://www.maastrichtuniversity.nl/education).

View file

@ -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. 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] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. 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 ## Core courses
:::note[Historical course list]
The following course list originated in the previous wiki and may have changed.
:::
- Block 1: Data Mining. - Block 1: Data Mining.
- Block 2: Model Identification and Data Fitting. - Block 2: Model Identification and Data Fitting.
- Block 4: Algorithms for Big Data. - Block 4: Algorithms for Big Data.
@ -26,10 +20,6 @@ The following course list originated in the previous wiki and may have changed.
## Electives ## 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 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 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. - Block 4: Building and Mining Knowledge Graphs, Information Security, and Dynamic Game Theory.

View file

@ -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. 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] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::
## Schedule ## 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 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. The second semester of Year 2 consists of a 30-credit master's thesis.

View file

@ -3,8 +3,6 @@ title: Locations of DKE
description: Historical location information for the Department of Data Science and Knowledge Engineering at Maastricht University. description: Historical location information for the Department of Data Science and Knowledge Engineering at Maastricht University.
--- ---
# Locations of DKE
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. Verify your current schedule and location with Maastricht University. This information originated in the previous wiki and may be outdated. Verify your current schedule and location with Maastricht University.
::: :::

View file

@ -3,8 +3,6 @@ title: Handy Locations
description: Historical suggestions for useful student locations in Maastricht. description: Historical suggestions for useful student locations in Maastricht.
--- ---
# Handy Locations
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. This information originated in the previous wiki and may be outdated.
::: :::

View file

@ -3,8 +3,6 @@ title: Useful Information
description: Historical student information for the Department of Data Science and Knowledge Engineering at Maastricht University. description: Historical student information for the Department of Data Science and Knowledge Engineering at Maastricht University.
--- ---
# Useful Information
:::caution[Historical information] :::caution[Historical information]
This information originated in the previous wiki and may be outdated. 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). 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 ## Useful pages

View file

@ -3,8 +3,6 @@ title: IT Services
description: Historical IT service guidance for Maastricht University students. description: Historical IT service guidance for Maastricht University students.
--- ---
# IT Services
:::caution[Historical information] :::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, 20162017 timetable information, VPN access, printing, service-desk hours, and every contact detail with Maastricht University before relying on them. 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, 20162017 timetable information, VPN access, printing, service-desk hours, and every contact detail with Maastricht University before relying on them.
::: :::

View file

@ -3,8 +3,6 @@ title: Laptop Buying Advice
description: Historical laptop-buying guidance for DKE students. description: Historical laptop-buying guidance for DKE students.
--- ---
# Laptop Buying Advice
:::caution[Historical information] :::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. 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.
::: :::

View file

@ -1,9 +1,37 @@
import assert from 'node:assert/strict'; 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 test from 'node:test';
import { auditContent } from '../scripts/audit-content.mjs'; import { auditContent } from '../scripts/audit-content.mjs';
import { loadManifest } from '../scripts/lib/migration.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 () => { test('the full migration audit accounts for every source and destination', async () => {
const manifest = await loadManifest('docs/migration-manifest.json'); const manifest = await loadManifest('docs/migration-manifest.json');
const result = await auditContent({ 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); 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>/]; const forbidden = [/\[\[/, /\{\{/, /~~NOCACHE~~/, /NEWPAGE>/, /indexmenu>/];
for (const file of [ for (const file of [

View file

@ -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'), '<a href="/manual.pdf">Manual</a>');
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 () => { test('maps the root route to the built root index', async () => {
await withBuiltSite(async (root) => { await withBuiltSite(async (root) => {
await writeFile(join(root, 'index.html'), '<a href="/">Home</a>'); await writeFile(join(root, 'index.html'), '<a href="/">Home</a>');
@ -86,3 +97,24 @@ test('ignores fragments, remote protocols, and Astro-generated assets', async ()
assert.equal(result.broken.length, 0); 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'), `<a href="${href}">Escaped</a>`);
const result = await checkInternalLinks({ distRoot, base: '/' });
assert.deepEqual(result.broken.map((broken) => broken.href), [href]);
} finally {
await rm(siteRoot, { recursive: true, force: true });
}
});
}

View file

@ -24,6 +24,27 @@ test('validation rejects source traversal outside the selected scope', async ()
assert.throws(() => validateManifest(manifest), /outside the selected migration scope/); 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 () => { test('validation rejects destination traversal outside the docs root', async () => {
const manifest = await loadManifest('docs/migration-manifest.json'); const manifest = await loadManifest('docs/migration-manifest.json');
manifest.entries[2].destination = 'src/content/docs/../../../outside.md'; 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/); 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 () => { test('validation rejects duplicate destinations unless the later entry is a merge', async () => {
const manifest = await loadManifest('docs/migration-manifest.json'); const manifest = await loadManifest('docs/migration-manifest.json');
manifest.entries[1].mode = 'page'; 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 }); 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 });
}
});

View file

@ -83,6 +83,18 @@ test('the optional GitHub Pages workflow deploys only main or manual runs', asyn
assert.match(workflow, /actions\/deploy-pages@v5/); 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 () => { test('maintainer documentation keeps Forgejo CI-independent', async () => {
const readme = await readFile('README.md', 'utf8'); const readme = await readFile('README.md', 'utf8');
assert.match(readme, /Forgejo[^\n]*authoritative private repository/i); assert.match(readme, /Forgejo[^\n]*authoritative private repository/i);

View file

@ -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'), '<main><h1>Home</h1></main>');
await writeFile(join(root, 'about/index.html'), '<main><h1>About</h1><h1>Duplicate</h1></main>');
await writeFile(join(root, 'empty/index.html'), '<main><p>No heading</p></main>');
await writeFile(join(root, '404.html'), '<main><p>Generated error page</p></main>');
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'), '<main><h1>Home</h1></main>');
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']);
});
});