Skip to main content

Data Platform Hub Learning Platform Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Turn the existing Docusaurus wiki into a domain-first learning platform with a complete generated library, four guided paths, reusable learner state, and review-state warnings while preserving every canonical lesson route.

Architecture: Canonical MD/MDX frontmatter, learning/taxonomy.mjs, and learning/paths.mjs are the sources of truth. Pure Node modules validate and generate src/generated/learningCatalog.json; a Docusaurus route plugin renders library and path routes from that manifest, and a swizzled doc layout adds lesson context through an adapter over the existing local-storage keys. A separate scheduled checker updates committed source-status data, so ordinary builds never require source-network access.

Tech Stack: Docusaurus 3.9.2, React 19, TypeScript 5.6, Node 20 ESM, Vitest, Testing Library, gray-matter, yaml, and cheerio.


File map

  • learning/taxonomy.mjs: stable domain/topic registry and allowed metadata values.
  • learning/paths.mjs: four ordered path manifests referencing canonical document IDs only.
  • learning/source-fingerprints.json: maintainer-accepted official-source fingerprints.
  • learning/source-status.json: last observed review state consumed by offline builds.
  • scripts/catalog/frontmatter.mjs: structured MD/MDX frontmatter parsing and source normalization.
  • scripts/catalog/classify.mjs: one-time complete classification rules for the 90 publishable lessons.
  • scripts/catalog/model.mjs: route, review-state, lesson, domain, and path validation.
  • scripts/generate-learning-catalog.mjs: deterministic manifest generator.
  • scripts/migrate-learning-metadata.mjs: idempotent metadata migration; it never changes lesson bodies or routes.
  • scripts/check-source-freshness.mjs: scheduled network checker and report writer.
  • scripts/source-providers.mjs: provider detection and provider-specific article extraction.
  • src/generated/learningCatalog.json: deterministic derived manifest committed for Docusaurus builds.
  • src/features/learning/types.ts: browser-facing catalog contracts.
  • src/features/learning/catalog.ts: typed catalog selectors.
  • src/features/learning/learnerState.ts: compatibility adapter for bookmarks, reading state, and XP completion.
  • src/features/learning/LibraryPage.tsx: /learn and /learn/<domain-id>.
  • src/features/learning/PathsPage.tsx: /paths and /paths/<path-id>.
  • src/features/learning/LessonContext.tsx: domain, review, bookmark, completion, and path-continuation controls.
  • src/features/learning/learning.css: responsive learning surfaces in the existing visual system.
  • src/plugins/learning-routes/index.mjs: static Docusaurus route registration from the generated catalog.
  • src/theme/DocItem/Layout/index.tsx: canonical lesson wrapper.
  • src/pages/index.tsx: working learning-platform entrance.
  • scripts/__tests__/*.test.mjs and src/features/learning/__tests__/*.test.tsx: focused unit and component tests.
  • .github/workflows/source-freshness.yml: scheduled rolling review PR.

Task 1: Preserve the current Oracle and Azure-foundation content baseline

Files:

  • Create: docs/oracle-at-azure/**

  • Create: docs/oracle-at-azure-migration/**

  • Create: docs/exadata-database-service/**

  • Create: docs/azure-identity-networking/**

  • Create: static/img/dp-300/oracle/**

  • Create: static/img/dp-300/identity/**

  • Step 1: Copy only the approved current content and its referenced assets

Run from the feature worktree:

$sourceRepo = Resolve-Path '..\..\Projects\azure-wiki'
$docFolders = @('oracle-at-azure', 'oracle-at-azure-migration', 'exadata-database-service', 'azure-identity-networking')
foreach ($folder in $docFolders) {
Copy-Item -LiteralPath (Join-Path $sourceRepo "docs\$folder") -Destination 'docs' -Recurse
}
Copy-Item -LiteralPath (Join-Path $sourceRepo 'static\img\dp-300\oracle') -Destination 'static\img\dp-300' -Recurse
Copy-Item -LiteralPath (Join-Path $sourceRepo 'static\img\dp-300\identity') -Destination 'static\img\dp-300' -Recurse

Expected: 26 lesson files and 122 SVG files are added. Do not copy the primary worktree's modified configuration, workflows, deployment scripts, logs, patches, or .superpowers/ directory.

  • Step 2: Prove byte preservation

Run:

$sourceRepo = Resolve-Path '..\..\Projects\azure-wiki'
$relativeRoots = @(
'docs\oracle-at-azure',
'docs\oracle-at-azure-migration',
'docs\exadata-database-service',
'docs\azure-identity-networking',
'static\img\dp-300\oracle',
'static\img\dp-300\identity'
)
$mismatches = foreach ($root in $relativeRoots) {
Get-ChildItem -LiteralPath (Join-Path $sourceRepo $root) -Recurse -File | ForEach-Object {
$relative = $_.FullName.Substring($sourceRepo.Path.Length + 1)
$target = Join-Path (Get-Location) $relative
if (-not (Test-Path $target) -or (Get-FileHash $_.FullName).Hash -ne (Get-FileHash $target).Hash) { $relative }
}
}
if ($mismatches) { throw "Content baseline mismatch: $($mismatches -join ', ')" }

Expected: command exits successfully with no output.

  • Step 3: Run the unchanged production build

Run: npm run build

Expected: encoding, live-link, and Docusaurus production build checks pass with the imported routes and SVGs.

  • Step 4: Commit the isolated baseline
git add docs/oracle-at-azure docs/oracle-at-azure-migration docs/exadata-database-service docs/azure-identity-networking static/img/dp-300/oracle static/img/dp-300/identity
git commit -m "docs: import current Oracle learning modules"

Task 2: Add test tooling and the framework-neutral learning contracts

Files:

  • Modify: package.json

  • Modify: package-lock.json

  • Create: vitest.config.ts

  • Create: src/test/setup.ts

  • Create: learning/taxonomy.mjs

  • Create: learning/paths.mjs

  • Create: scripts/__tests__/learning-contracts.test.mjs

  • Step 1: Install the focused test and parsing dependencies

Run:

npm install --save-dev vitest@3.2.4 jsdom@26.1.0 @testing-library/react@16.3.0 @testing-library/jest-dom@6.6.3 gray-matter@4.0.3 yaml@2.8.1 cheerio@1.1.2

Add these scripts to package.json:

"test": "vitest run",
"test:watch": "vitest",
"catalog:generate": "node scripts/generate-learning-catalog.mjs",
"catalog:check": "node scripts/generate-learning-catalog.mjs --check",
"catalog:migrate": "node scripts/migrate-learning-metadata.mjs",
"check-sources": "node scripts/check-source-freshness.mjs"

Change prebuild to:

"prebuild": "npm run catalog:check && npm run check-encoding && npm run check-links"
  • Step 2: Configure Vitest

Create vitest.config.ts:

import {defineConfig} from 'vitest/config';

export default defineConfig({
test: {
environment: 'node',
setupFiles: ['./src/test/setup.ts'],
include: ['scripts/__tests__/**/*.test.mjs', 'src/**/*.test.{ts,tsx}'],
},
});

Create src/test/setup.ts:

import '@testing-library/jest-dom/vitest';
  • Step 3: Write the failing contract test

Create scripts/__tests__/learning-contracts.test.mjs:

import {describe, expect, it} from 'vitest';
import {domains, levels, contentTypes, sourceProviders} from '../../learning/taxonomy.mjs';
import {paths} from '../../learning/paths.mjs';

describe('learning contracts', () => {
it('defines exactly eight populated release-one domains', () => {
expect(domains).toHaveLength(8);
expect(new Set(domains.map(({id}) => id)).size).toBe(8);
expect(domains.every(({topics}) => topics.length > 0)).toBe(true);
});

it('defines the allowed metadata enums', () => {
expect(levels).toEqual(['foundation', 'intermediate', 'advanced']);
expect(contentTypes).toEqual(['lesson', 'lab', 'reference']);
expect(sourceProviders).toEqual(['microsoft-learn', 'oracle-docs', 'oracle-livelabs']);
});

it('defines four unique path IDs using document references only', () => {
expect(paths.map(({id}) => id)).toEqual([
'operate-azure-sql',
'oracle-dba-to-azure-data-platform',
'deploy-oracle-database-azure',
'prepare-dp-300',
]);
for (const path of paths) {
expect(path.stages.flatMap(({documents}) => documents).every((id) => !id.startsWith('/'))).toBe(true);
}
});
});
  • Step 4: Run the test to verify it fails

Run: npm test -- scripts/__tests__/learning-contracts.test.mjs

Expected: FAIL because learning/taxonomy.mjs and learning/paths.mjs do not exist.

  • Step 5: Implement the registries

Create learning/taxonomy.mjs with these eight IDs and topic sets:

export const levels = ['foundation', 'intermediate', 'advanced'];
export const contentTypes = ['lesson', 'lab', 'reference'];
export const sourceProviders = ['microsoft-learn', 'oracle-docs', 'oracle-livelabs'];

export const domains = [
domain('azure-database-platforms', 'Azure Database Platforms', 'Choose, deploy, and operate Azure database services.', ['service-selection', 'azure-sql-database', 'azure-sql-managed-instance', 'sql-server-azure-vm', 'open-source-databases', 'hybrid-multicloud', 'operations-frameworks']),
domain('sql-server-architecture', 'SQL Server Architecture & Internals', 'Understand engine, memory, storage, and query execution behavior.', ['engine-architecture', 'storage-transactions', 'query-processing', 'deployment-configuration']),
domain('security-governance', 'Security & Governance', 'Design identity, access, network, protection, and governance controls.', ['identity-access', 'data-protection', 'network-security', 'threat-protection', 'governance']),
domain('monitoring-performance-automation', 'Monitoring, Performance & Automation', 'Observe, tune, troubleshoot, and automate database operations.', ['monitoring-observability', 'query-performance', 'indexing-storage', 'automation-deployment', 'jobs-alerts', 'ai-assisted-operations']),
domain('resilience-backup-recovery', 'Resilience, Backup & Recovery', 'Plan availability, backup, continuity, and recovery.', ['availability-architecture', 'backup-restore', 'geo-replication', 'failover-operations']),
domain('migration-modernization', 'Migration & Modernization', 'Assess, move, validate, and modernize data workloads.', ['assessment-strategy', 'data-movement', 'compatibility-modernization', 'migration-labs']),
domain('oracle-database-azure', 'Oracle Database@Azure & Exadata', 'Deploy and operate Oracle and Exadata services colocated in Azure.', ['adoption-architecture', 'exadata-platform', 'identity-networking', 'provisioning', 'operations-resilience', 'migration']),
domain('azure-foundations-data', 'Azure Foundations for Data Professionals', 'Build the Azure identity and network foundation required for data work.', ['identity-rbac', 'subscriptions-governance', 'virtual-networking', 'routing-security']),
];

function domain(id, title, outcome, topicIds) {
return {id, title, outcome, topics: topicIds.map((topicId) => ({id: topicId, title: titleCase(topicId)}))};
}

function titleCase(value) {
return value.split('-').map((part) => part[0].toUpperCase() + part.slice(1)).join(' ');
}

Create learning/paths.mjs. Each object must contain only id, title, outcome, audience, prerequisites, and ordered {title, documents} stages. Use these canonical IDs:

export const paths = [
path('operate-azure-sql', 'Operate Azure SQL', 'Select, secure, monitor, tune, and recover an Azure SQL workload.', 'Database administrators moving into Azure operations', [], [
stage('Choose the platform', ['azure-services/azure-sql-database', 'dp-300/01-plan-implement/azure-sql-options', 'dp-300/01-plan-implement/vcore-vs-dtu']),
stage('Secure and observe', ['dp-300/02-security/authentication-authorization', 'azure-services/defender-for-sql', 'azure-services/azure-monitor']),
stage('Tune and recover', ['dp-300/03-monitor-optimize/performance-tuning', 'dp-300/05-hadr/backup-restore-pitr-ltr', 'dp-300/05-hadr/failover-groups-vs-geo-rep']),
]),
path('oracle-dba-to-azure-data-platform', 'Oracle DBA to Azure Data Platform', 'Translate Oracle administration experience into Azure identity, SQL, operations, and resilience decisions.', 'Oracle database administrators expanding into Azure', [], [
stage('Map existing knowledge', ['dp-300/00-oracle-to-azure/ace-learning-plan', 'dp-300/00-oracle-to-azure/oracle-azure-mapping', 'dp-300/00-oracle-to-azure/oracle-sqlserver-engine-internals']),
stage('Learn the Azure control plane', ['azure-identity-networking/01-microsoft-entra-id', 'azure-identity-networking/02-azure-subscription', 'azure-identity-networking/03-entra-vs-azure-roles', 'azure-identity-networking/04-azure-virtual-network']),
stage('Operate the data platform', ['dp-300/01-plan-implement/azure-sql-options', 'dp-300/03-monitor-optimize/monitoring-tools-matrix', 'dp-300/05-hadr/high-availability-dr']),
]),
path('deploy-oracle-database-azure', 'Deploy Oracle Database@Azure', 'Plan, provision, connect, and operate Oracle Database@Azure on Exadata.', 'Oracle and Azure engineers delivering an Oracle Database@Azure deployment', ['azure-identity-networking/04-azure-virtual-network'], [
stage('Understand the service', ['oracle-at-azure/intro', 'oracle-at-azure/01-introduction', 'oracle-at-azure/02-use-case-scenarios', 'oracle-at-azure/03-architecture', 'exadata-database-service/01-x9m-product-overview', 'exadata-database-service/02-exadata-database-service-overview']),
stage('Prepare identity and networking', ['oracle-at-azure/04-subscribe-onboard', 'oracle-at-azure/05-identity', 'oracle-at-azure/06-identity-federation', 'oracle-at-azure/07-networking']),
stage('Provision and operate', ['oracle-at-azure/08-provisioning', 'oracle-at-azure/09-demo-provision-infrastructure', 'oracle-at-azure/10-demo-provision-vm-cluster', 'oracle-at-azure/11-demo-provision-database', 'oracle-at-azure/12-operations-management', 'oracle-at-azure/13-high-availability-disaster-recovery']),
]),
path('prepare-dp-300', 'Prepare for DP-300', 'Cover the implementation, security, operations, automation, and resilience skills measured by DP-300.', 'Azure database professionals preparing for DP-300', [], [
stage('Plan and implement', ['dp-300/intro', 'dp-300/01-plan-implement/azure-sql-options', 'dp-300/01-plan-implement/azure-sql-db-configuration', 'dp-300/01-plan-implement/sql-vm-configuration']),
stage('Secure', ['dp-300/02-security/authentication-authorization', 'dp-300/02-security/encryption-network', 'dp-300/02-security/compliance-advanced-security']),
stage('Monitor and automate', ['dp-300/03-monitor-optimize/monitoring-tools-matrix', 'dp-300/03-monitor-optimize/index-design-query-tuning', 'dp-300/04-automation/automation-decision-matrix', 'dp-300/04-automation/deployment-automation']),
stage('Build resilience and practise', ['dp-300/05-hadr/high-availability-dr', 'dp-300/05-hadr/backup-restore-pitr-ltr', 'dp-300/05-hadr/failover-groups-vs-geo-rep', 'dp-300/06-labs/index', 'dp-300/exam-cram-kit']),
]),
];

function path(id, title, outcome, audience, prerequisites, stages) {
return {id, title, outcome, audience, prerequisites, stages};
}

function stage(title, documents) {
return {title, documents};
}
  • Step 6: Run the contract test

Run: npm test -- scripts/__tests__/learning-contracts.test.mjs

Expected: 3 tests pass.

  • Step 7: Commit
git add package.json package-lock.json vitest.config.ts src/test/setup.ts learning/taxonomy.mjs learning/paths.mjs scripts/__tests__/learning-contracts.test.mjs
git commit -m "feat: define learning taxonomy and paths"

Task 3: Build and validate the deterministic catalog

Files:

  • Create: scripts/catalog/frontmatter.mjs

  • Create: scripts/catalog/classify.mjs

  • Create: scripts/catalog/model.mjs

  • Create: scripts/generate-learning-catalog.mjs

  • Create: scripts/migrate-learning-metadata.mjs

  • Create: scripts/__tests__/catalog-model.test.mjs

  • Create: scripts/__tests__/catalog-generator.test.mjs

  • Create: scripts/__fixtures__/catalog/**

  • Create: learning/source-status.json

  • Create: src/generated/learningCatalog.json

  • Modify: all 90 publishable files under docs/ (frontmatter only)

  • Step 1: Write parser and validation tests first

The model test must assert all of the following with fixed generic fixtures:

it.each([
['unknown domain', {domain: 'unknown'}],
['unknown topic', {topic: 'unknown'}],
['unknown level', {level: 'expert'}],
['unknown content type', {content_type: 'video'}],
['malformed date', {last_reviewed: '10/07/2026'}],
['non-positive interval', {review_interval_days: 0}],
])('rejects %s', (_label, override) => {
expect(() => validateLesson({...validLesson, ...override}, registry)).toThrow();
});

it('normalizes legacy Learn sources without losing URLs', () => {
expect(normalizeSources({learn_sources: ['https://learn.microsoft.com/azure/azure-sql/']})).toEqual([
{provider: 'microsoft-learn', url: 'https://learn.microsoft.com/azure/azure-sql/'},
]);
});

it('uses source-changed, source-unavailable, review-due, current priority', () => {
expect(resolveReviewState({changed: true, unavailable: true, overdue: true})).toBe('source-changed');
expect(resolveReviewState({changed: false, unavailable: true, overdue: true})).toBe('source-unavailable');
expect(resolveReviewState({changed: false, unavailable: false, overdue: true})).toBe('review-due');
expect(resolveReviewState({changed: false, unavailable: false, overdue: false})).toBe('current');
});

it('rejects duplicate IDs, duplicate routes, empty domains, and missing path references', () => {
expect(() => validateCatalog(invalidCatalog)).toThrowError(/duplicate document ID|duplicate route|no publishable lessons|missing document/);
});

The generator test must run the generator twice against scripts/__fixtures__/catalog/docs and assert byte-identical output sorted by domain order, topic order, title, then document ID.

  • Step 2: Run tests to verify failure

Run: npm test -- scripts/__tests__/catalog-model.test.mjs scripts/__tests__/catalog-generator.test.mjs

Expected: FAIL because the catalog modules do not exist.

  • Step 3: Implement structured frontmatter and source normalization

Use gray-matter with yaml.parse/yaml.stringify. normalizeSources(frontmatter) must merge and deduplicate sources plus learn_sources, infer providers only from these host rules, and reject every other provider:

const providerHosts = new Map([
['learn.microsoft.com', 'microsoft-learn'],
['docs.oracle.com', 'oracle-docs'],
['livelabs.oracle.com', 'oracle-livelabs'],
]);

resolveRoute(id, frontmatter) returns an absolute slug unchanged when present and otherwise returns /docs/${id}. Exclude draft: true, docs/superpowers/**, docs/about-this-wiki.md, and docs/wiki-architecture.mdx from publication.

  • Step 4: Implement the complete classification rules

classify.mjs must return {domain, topic, level, content_type} for every publishable path and throw for an unmatched path. Use folder ownership first, then filename-specific switches:

export function classifyDocument(id) {
if (id.startsWith('oracle-at-azure-migration/')) return meta('oracle-database-azure', 'migration', levelFor(id), typeFor(id));
if (id.startsWith('oracle-at-azure/')) return classifyOracle(id);
if (id.startsWith('exadata-database-service/')) return meta('oracle-database-azure', 'exadata-platform', levelFor(id), typeFor(id));
if (id.startsWith('azure-identity-networking/')) return classifyFoundation(id);
if (id.startsWith('sql-server/')) return classifySqlServer(id);
if (id.startsWith('azure-services/')) return classifyAzureService(id);
if (id.startsWith('dp-300/00-oracle-to-azure/')) return classifyOracleBridge(id);
if (id.startsWith('dp-300/01-plan-implement/')) return classifyPlanImplement(id);
if (id.startsWith('dp-300/02-security/')) return meta('security-governance', securityTopic(id), levelFor(id), typeFor(id));
if (id.startsWith('dp-300/03-monitor-optimize/')) return meta('monitoring-performance-automation', performanceTopic(id), levelFor(id), typeFor(id));
if (id.startsWith('dp-300/04-automation/')) return meta('monitoring-performance-automation', automationTopic(id), levelFor(id), typeFor(id));
if (id.startsWith('dp-300/05-hadr/')) return meta('resilience-backup-recovery', resilienceTopic(id), levelFor(id), typeFor(id));
if (id.startsWith('dp-300/06-labs/')) return classifyLab(id);
if (id.startsWith('dp-300/')) return meta('azure-database-platforms', 'service-selection', 'foundation', 'reference');
throw new Error(`No learning classification for ${id}`);
}

Implement every helper with explicit filename sets. The migration test must enumerate the repository and assert classifyDocument succeeds for exactly 90 files; this is the discriminating completeness check.

  • Step 5: Implement validation and generation

generate-learning-catalog.mjs must:

  1. Walk .md and .mdx files.
  2. Parse frontmatter structurally.
  3. Normalize metadata and sources.
  4. Merge learning/source-status.json by document ID.
  5. Validate lessons, domains, routes, path references, and duplicates.
  6. Derive a description from frontmatter or the first plain prose paragraph, capped at 160 characters.
  7. Project path stages to lesson objects without copying lesson metadata into learning/paths.mjs.
  8. Sort deterministically.
  9. Write src/generated/learningCatalog.json, or compare bytes and exit nonzero under --check.

Use this manifest root contract:

{
schemaVersion: 1,
generatedFrom: 'canonical-content',
domains: [{id, title, outcome, topics: [{id, title, lessons: []}], lessonCount, pathIds}],
paths: [{id, title, outcome, audience, prerequisites: [], stages: [{title, lessons: []}], lessonCount}],
lessons: [{id, route, title, description, domain, topic, level, contentType, lastReviewed, reviewIntervalDays, sources, reviewState}],
}

Do not include a wall-clock generation timestamp; identical inputs must generate identical bytes.

  • Step 6: Implement and run the idempotent metadata migration

migrate-learning-metadata.mjs must preserve body bytes after the closing frontmatter delimiter, preserve every existing learn_sources URL, and write these fields for every publishable lesson: domain, topic, level, content_type, last_reviewed, review_interval_days, and sources. For a tracked file missing last_reviewed, use its latest Git commit date; imported untracked files already carry explicit dates. Source-less pages receive sources: []; do not invent official links.

Run:

npm run catalog:migrate
npm run catalog:migrate
git diff --exit-code -- docs

The third command is expected to fail after the first migration because the metadata changes are uncommitted. Capture git diff -- docs, then prove idempotence instead with:

$before = Get-ChildItem docs -Recurse -File -Include *.md,*.mdx | Get-FileHash
npm run catalog:migrate
$after = Get-ChildItem docs -Recurse -File -Include *.md,*.mdx | Get-FileHash
Compare-Object $before $after -Property Path,Hash

Expected: no hash differences on the second migration.

  • Step 7: Generate and test the complete catalog

Initialize learning/source-status.json as {"schemaVersion":1,"lessons":{}}, then run:

npm run catalog:generate
npm test -- scripts/__tests__/learning-contracts.test.mjs scripts/__tests__/catalog-model.test.mjs scripts/__tests__/catalog-generator.test.mjs
npm run catalog:check

Expected: all tests pass; manifest contains exactly 90 lessons, 8 domains, and 4 paths; catalog:check reports that generated output is current.

  • Step 8: Commit
git add learning/source-status.json scripts/catalog scripts/generate-learning-catalog.mjs scripts/migrate-learning-metadata.mjs scripts/__tests__ scripts/__fixtures__ src/generated/learningCatalog.json docs package.json package-lock.json
git commit -m "feat: generate validated learning catalog"

Task 4: Adapt existing learner state without replacing storage

Files:

  • Create: src/features/learning/types.ts

  • Create: src/features/learning/catalog.ts

  • Create: src/features/learning/learnerState.ts

  • Create: src/features/learning/__tests__/learnerState.test.ts

  • Modify: src/components/ReadingProgress.tsx

  • Step 1: Write compatibility tests in jsdom

Add // @vitest-environment jsdom and assert:

it('recognizes canonical IDs, routes, and historical route aliases without rewriting stored values', () => {
localStorage.setItem('azure-wiki-xp', JSON.stringify({topicsCompleted: ['/docs/dp-300/intro'], totalXP: 10}));
const before = localStorage.getItem('azure-wiki-xp');
expect(isLessonComplete(lesson('dp-300/intro', '/docs/dp-300/intro'))).toBe(true);
expect(localStorage.getItem('azure-wiki-xp')).toBe(before);
});

it('adds only the canonical document ID when completing a lesson', () => {
completeLesson(lesson('dp-300/intro', '/docs/dp-300/intro'));
expect(JSON.parse(localStorage.getItem('azure-wiki-xp')!).topicsCompleted).toContain('dp-300/intro');
});

it('keeps route-based bookmarks and tolerates malformed storage', () => {
localStorage.setItem('azure-wiki-bookmarks', '{bad json');
expect(getLessonState(lesson('dp-300/intro', '/docs/dp-300/intro'))).toEqual({bookmarked: false, complete: false});
});
  • Step 2: Run tests to verify failure

Run: npm test -- src/features/learning/__tests__/learnerState.test.ts

Expected: FAIL because learnerState.ts does not exist.

  • Step 3: Implement the adapter

Keep the existing keys exactly: azure-wiki-bookmarks, azure-wiki-xp, and azure-wiki-reading. Export getLessonState, isLessonComplete, completeLesson, toggleLessonBookmark, getContinueLearning, and subscribeToLearnerState. Completion aliases are [lesson.id, lesson.route, lesson.route.replace(/^\/docs\//, '')]. Writes add only lesson.id, preserve every prior value, and dispatch azure-wiki-learner-state after a successful change. All reads catch malformed JSON and all functions support server rendering.

  • Step 4: Route scroll completion through the adapter

Change ReadingProgress to accept {lesson: CatalogLesson} and call completeLesson(lesson, {awardXP: true}) at 65%. Retain the existing progress bar behavior and reading key; do not create another store.

  • Step 5: Run focused validation and commit

Run:

npm test -- src/features/learning/__tests__/learnerState.test.ts
npm run typecheck
git add src/features/learning src/components/ReadingProgress.tsx
git commit -m "feat: preserve learner state across canonical lessons"

Expected: tests and typecheck pass.

Task 5: Render the complete library and guided paths

Files:

  • Create: src/features/learning/LibraryPage.tsx

  • Create: src/features/learning/PathsPage.tsx

  • Create: src/features/learning/ReviewStatus.tsx

  • Create: src/features/learning/LessonActions.tsx

  • Create: src/features/learning/learning.css

  • Create: src/features/learning/__tests__/LibraryPage.test.tsx

  • Create: src/features/learning/__tests__/PathsPage.test.tsx

  • Create: src/plugins/learning-routes/index.mjs

  • Modify: docusaurus.config.ts

  • Step 1: Write component tests

Mock @docusaurus/Link, @theme/Layout, and router location. Assert:

it('shows all eight domains and complete counts on /learn', () => {
render(<LibraryPage routeKind="library" />);
expect(screen.getAllByRole('link', {name: /lessons$/i})).toHaveLength(8);
});

it('shows every domain lesson by default and filters without changing links', async () => {
const user = userEvent.setup();
render(<LibraryPage routeKind="domain" domainId="security-governance" />);
const initialLinks = screen.getAllByRole('link', {name: /open lesson/i});
await user.selectOptions(screen.getByLabelText('Level'), 'advanced');
expect(screen.getAllByRole('link', {name: /open lesson/i}).length).toBeLessThan(initialLinks.length);
expect(screen.getAllByRole('link', {name: /open lesson/i}).every((link) => link.getAttribute('href')?.startsWith('/docs/'))).toBe(true);
});

it('explains active filters and clears a no-match state', async () => {
render(<LibraryPage routeKind="domain" domainId="azure-foundations-data" />);
// Select a combination absent from the fixture.
expect(await screen.findByText(/no lessons match/i)).toBeVisible();
expect(screen.getByRole('button', {name: /clear filters/i})).toBeEnabled();
});

it('projects path progress and identifies the next unfinished lesson', () => {
render(<PathsPage routeKind="path" pathId="operate-azure-sql" />);
expect(screen.getByText(/next lesson/i)).toBeVisible();
expect(screen.getAllByRole('link', {name: /open lesson/i})[0]).toHaveAttribute('href', expect.stringMatching(/^\/docs\//));
});

Add @testing-library/user-event@14.6.1 if the tests require it.

  • Step 2: Run tests to verify failure

Run: npm test -- src/features/learning/__tests__/LibraryPage.test.tsx src/features/learning/__tests__/PathsPage.test.tsx

Expected: FAIL because the pages do not exist.

  • Step 3: Implement the shared presentation

Use semantic <main>, <section>, headings, labels, <select> controls, and real links. The library default renders all lessons; filters are all | foundation | intermediate | advanced, all | lesson | lab | reference, and all | current | review-due | source-changed | source-unavailable. ReviewStatus always renders text plus an icon. LessonActions uses the learner-state adapter and stable-size icon buttons with tooltips.

Use the existing CSS custom properties from src/css/custom.css. Add a restrained editorial/technical layout with unframed sections, 1px rules, maximum 8px card radii, stable count columns, visible focus rings, responsive grids, light/dark selectors, one staggered entrance sequence, and a reduced-motion override. Do not add gradient orbs, nested cards, or viewport-scaled type.

  • Step 4: Register all static routes from the generated manifest

src/plugins/learning-routes/index.mjs reads src/generated/learningCatalog.json and calls addRoute for /learn, /paths, every /learn/${domain.id}, and every /paths/${path.id}. The list and detail components receive routeKind plus domainId or pathId through route modules; unknown IDs are never registered.

Add the plugin to docusaurus.config.ts:

plugins: [require.resolve('./src/plugins/learning-routes/index.mjs')],
  • Step 5: Run focused validation and commit

Run:

npm test -- src/features/learning/__tests__/LibraryPage.test.tsx src/features/learning/__tests__/PathsPage.test.tsx
npm run typecheck
npm run build
git add src/features/learning src/plugins/learning-routes docusaurus.config.ts package.json package-lock.json
git commit -m "feat: add domain library and guided paths"

Expected: component tests, typecheck, live links, route generation, and production build pass.

Task 6: Add canonical lesson context and controls

Files:

  • Create: src/features/learning/LessonContext.tsx

  • Create: src/features/learning/__tests__/LessonContext.test.tsx

  • Create: src/theme/DocItem/Layout/index.tsx

  • Step 1: Write the lesson-context tests

Assert that a canonical lesson shows its domain link, current review date/status, bookmark and completion controls, related paths, and a next link when ?path=<path-id> names a path containing the lesson. Assert a direct visit still shows domain and related paths. Assert a non-catalog project artifact renders only the original doc layout.

  • Step 2: Run the test to verify failure

Run: npm test -- src/features/learning/__tests__/LessonContext.test.tsx

Expected: FAIL because the component and wrapper do not exist.

  • Step 3: Implement the context component and theme wrapper

Use useDoc() in src/theme/DocItem/Layout/index.tsx to obtain metadata.id, metadata.permalink, and metadata.title. Look up the canonical lesson by ID. Wrap @theme-original/DocItem/Layout; for cataloged lessons render ReadingProgress, a compact LessonContext before the article children, and path continuation after the article. Preserve the original layout unchanged for excluded docs.

Path links append ?path=<path-id> only when entering from a path. The component validates that query value against the generated path registry before projecting stage position. It never changes the canonical pathname used by bookmarks.

  • Step 4: Validate and commit

Run:

npm test -- src/features/learning/__tests__/LessonContext.test.tsx src/features/learning/__tests__/learnerState.test.ts
npm run typecheck
npm run build
git add src/features/learning src/theme/DocItem/Layout
git commit -m "feat: connect lessons to domains and learning paths"

Expected: tests, typecheck, and production build pass; sampled existing /docs/... routes remain generated.

Task 7: Replace DP-300 framing with the working learning-platform entrance

Files:

  • Modify: src/pages/index.tsx

  • Modify: docusaurus.config.ts

  • Modify: sidebars.ts

  • Modify: src/css/custom.css

  • Create: src/features/learning/__tests__/Home.test.tsx

  • Step 1: Write the homepage test

Assert the first viewport exposes the literal Data Platform Hub heading, search control, /learn action, and the start of the domains section. Assert eight populated domains, four paths, recently reviewed lessons, and conditional continue/bookmark sections. Assert DP-300 appears only as a path, not as the page's primary action.

  • Step 2: Run the test to verify failure

Run: npm test -- src/features/learning/__tests__/Home.test.tsx

Expected: FAIL against the current DP-300-first homepage.

  • Step 3: Implement the homepage

Render @theme/SearchBar as the dominant action, an immediate /learn command, all domains and paths from the generated catalog, recently reviewed lessons sorted by ISO date then ID, and learner-state sections only after hydration. Keep the product name as the H1. Ensure the next section remains visible at 360x800, 768x1024, and 1440x900.

  • Step 4: Update global navigation and docs grouping

Set navbar destinations in this order: Learn, Paths, Oracle to Azure, My Learning, then the existing search and theme controls. Implement My Learning as the Docusaurus dropdown containing /bookmarks and /progress. Point Oracle to Azure to /learn/oracle-database-azure and include the deployment path in its dropdown. Change the footer from certification framing to Learn, Paths, and Oracle to Azure internal links; retain only existing, already-live external references.

Replace the DP-300-spine sidebar with clean autogenerated groups for Oracle Database@Azure, Exadata, Azure identity/networking, Oracle migration, DP-300, SQL Server, and Azure services. Do not copy the malformed primary-worktree sidebar diff.

  • Step 5: Add only the existing SVG sizing fix from the primary worktree

Append the verified diagram rule to src/css/custom.css so inlined and file-served SVGs fill the content column. Preserve all existing palette and theme tokens.

  • Step 6: Validate and commit

Run:

npm test -- src/features/learning/__tests__/Home.test.tsx
npm run typecheck
npm run build
git add src/pages/index.tsx docusaurus.config.ts sidebars.ts src/css/custom.css src/features/learning/__tests__/Home.test.tsx
git commit -m "feat: make the library the primary learning entrance"

Expected: tests and build pass; homepage copy no longer describes the product as primarily a DP-300 study guide.

Task 8: Upgrade source freshness to provider-aware review state

Files:

  • Create: scripts/source-providers.mjs

  • Create: scripts/check-source-freshness.mjs

  • Create: scripts/__tests__/source-providers.test.mjs

  • Create: scripts/__fixtures__/sources/learn.html

  • Create: scripts/__fixtures__/sources/oracle-docs.html

  • Create: scripts/__fixtures__/sources/oracle-livelabs.html

  • Create: learning/source-fingerprints.json

  • Modify: learning/source-status.json

  • Delete: scripts/check-learn-freshness.mjs

  • Create: .github/workflows/source-freshness.yml

  • Modify: .gitignore

  • Step 1: Write provider and state tests

Assert exact host-to-provider routing, rejection of unsupported hosts, extraction of article text without nav/script/style chrome for all three fixtures, stable SHA-256 after whitespace-only changes, bounded retry behavior, and state priority. Assert an unavailable source produces status data and a report but does not throw a build-blocking error.

  • Step 2: Run tests to verify failure

Run: npm test -- scripts/__tests__/source-providers.test.mjs

Expected: FAIL because source-providers.mjs does not exist.

  • Step 3: Implement provider adapters

Use cheerio.load. Provider selector priority:

export const adapters = {
'microsoft-learn': {hosts: ['learn.microsoft.com'], selectors: ['main#main', 'main', '[data-bi-name="content"]']},
'oracle-docs': {hosts: ['docs.oracle.com'], selectors: ['main', '#maincontent', '.body-content']},
'oracle-livelabs': {hosts: ['livelabs.oracle.com'], selectors: ['main', 'article', '.lab-content']},
};

For the first matching selector, remove script, style, nav, header, footer, and aside; normalize Unicode whitespace; hash only normalized article text. Throw a validation error for an unsupported provider or for a successful response with no recognized article container.

  • Step 4: Implement the scheduled checker

Read sources from the generated catalog and accepted hashes from learning/source-fingerprints.json. Fetch with concurrency 6, a 20-second timeout, and at most 2 retries for timeout/429/5xx. Write deterministic learning/source-status.json and .freshness-report.md. Return exit 0 for changed/unavailable/review-due states; return nonzero only for invalid metadata, unsupported providers, or malformed files.

Initialize accepted fingerprints as:

{"schemaVersion":1,"accepted":{}}

Do not auto-accept observed hashes. A new source without an accepted hash is source-changed and requires the same human review as a changed source.

  • Step 5: Add the rolling-PR workflow

Create a weekly and manually dispatchable Windows self-hosted workflow with contents: write and pull-requests: write. Pin checkout, setup-node, and peter-evans/create-pull-request to full commit SHAs. Run npm ci, npm run catalog:generate, npm run check-sources, and npm test -- scripts/__tests__/source-providers.test.mjs; then create or update branch automation/source-freshness with only learning/source-status.json and .freshness-report.md. The PR body must state that merging publishes review warnings but does not accept fingerprints or rewrite lessons.

  • Step 6: Prove ordinary builds are offline

Run:

$env:HTTPS_PROXY = 'http://127.0.0.1:1'
npm run build
Remove-Item Env:HTTPS_PROXY

Expected: build passes because prebuild reads committed catalog/status files and the existing cached live-link behavior; if the link checker requires network on an empty cache, prove the catalog portion independently with npm run catalog:check and document that the existing mandatory live-link gate remains the only networked build step.

  • Step 7: Test and commit

Run:

npm test -- scripts/__tests__/source-providers.test.mjs scripts/__tests__/catalog-model.test.mjs
npm run catalog:generate
npm run catalog:check
git add scripts/source-providers.mjs scripts/check-source-freshness.mjs scripts/__tests__/source-providers.test.mjs scripts/__fixtures__/sources learning/source-fingerprints.json learning/source-status.json .github/workflows/source-freshness.yml .gitignore package.json
git rm scripts/check-learn-freshness.mjs
git commit -m "feat: detect official source drift for human review"

Task 9: Verify the release across routes, themes, viewports, and public-safety gates

Files:

  • Modify only files needed to repair defects found by the checks below.

  • Step 1: Run the complete executable suite

Run:

npm test
npm run catalog:check
npm run typecheck
npm run check-encoding
npm run check-links:fresh
npm run build

Expected: all tests pass; catalog is current; typecheck, encoding, 2xx/3xx external links, and production build pass.

  • Step 2: Prove route compatibility from build output

Run a PowerShell assertion over these pre-existing samples and new routes:

$routes = @(
'build\docs\dp-300\intro\index.html',
'build\docs\dp-300\01-plan-implement\azure-sql-options\index.html',
'build\docs\sql-server\engine-memory-architecture\index.html',
'build\docs\azure-services\azure-sql-database\index.html',
'build\docs\oracle-at-azure\intro\index.html',
'build\learn\index.html',
'build\learn\oracle-database-azure\index.html',
'build\paths\index.html',
'build\paths\operate-azure-sql\index.html'
)
$missing = $routes | Where-Object { -not (Test-Path $_) }
if ($missing) { throw "Missing routes: $($missing -join ', ')" }

Expected: no missing route.

  • Step 3: Start the production server

Run: npm run serve -- --host 127.0.0.1 --port 3000

Expected: server remains available at http://127.0.0.1:3000 for browser verification.

  • Step 4: Run Playwright browser checks

At 1440x900 and 390x844, test dark and light modes for /, /learn, /learn/oracle-database-azure, /paths/deploy-oracle-database-azure, and /docs/oracle-at-azure/intro. Verify nonblank content, no horizontal overflow, no text/control overlap, visible focus, keyboard-operable filters, route transitions, complete lesson counts, a working bookmark toggle, completion continuity after reload, and a valid next lesson from path context. Capture screenshots under the test runner's temporary output, not the repository.

  • Step 5: Run public-safety and changed-link review

Invoke the mandatory share-safety skill against every changed public artifact. Confirm there are no tenant IDs, subscription IDs, app/client IDs, resource IDs, TPIDs, internal links, customer names, or internal email addresses. Confirm every new or changed https:// URL was covered by check-links:fresh.

  • Step 6: Request a code review and repair high-confidence findings

Invoke the requesting-code-review skill and the code-reviewer agent against the approved spec and branch diff. Repair correctness, accessibility, route, storage-compatibility, and missing-test findings; rerun the focused check after each repair and the full suite once at the end.

  • Step 7: Commit verification repairs, if any
git add -A
git commit -m "fix: close learning platform verification gaps"

Skip this commit when verification required no file changes.

Spec coverage review

  • Runtime and canonical-route preservation: Tasks 1, 3, 5, 6, and 9.
  • Eight-domain taxonomy and complete catalogs: Tasks 2, 3, and 5.
  • Four manifest-driven paths: Tasks 2, 3, 5, and 6.
  • Library-first homepage and navigation: Task 7.
  • Existing bookmark/progress continuity: Tasks 4, 5, 6, and 9.
  • Provider-aware detection and human review: Task 8.
  • Structural CI failures versus nonblocking review states: Tasks 3 and 8.
  • Accessibility, responsive behavior, themes, and route compatibility: Tasks 5, 7, and 9.
  • Link liveness and public safety: Tasks 1, 8, and 9.

The plan intentionally leaves 43 older source-less lessons with an explicit empty sources array rather than fabricating references. They remain cataloged and age through review_interval_days; grounding those pages is editorial review work, not a hidden prerequisite for the platform release.