Skip to content

Commit 0fa00b5

Browse files
thomasahleclaude
andcommitted
Fix cross-coverage intersect typo, enable minification, Lighthouse a11y fixes
UVM lessons: - cross-coverage: s/intersecting/intersect/ in solution, starter hint, and description — intersecting is LRM prose; intersect is the grammar keyword; all 11 UVM e2e tests now pass with esbuild minification - Various UVM lesson content updates (constrained-random, covergroup, coverage-driven, driver, env, factory-override, monitor, reporting, seq-item, sequence) Build / performance: - vite.config.js: enable minify: 'esbuild' (~40% JS size reduction) - vite.config.js: add sourcemap: true Accessibility (Lighthouse): - +layout.svelte: wrap content in <main> landmark - CodeEditor.svelte: add aria-label to CodeMirror textbox via EditorView.contentAttributes - +page.svelte: remove opacity-60 from "press ▶ to run" placeholder (low-contrast fix) E2E: - Add diag-focus.spec.js and perf-profile.spec.js Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5e9b59a commit 0fa00b5

35 files changed

Lines changed: 749 additions & 127 deletions

CURRICULUM.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -190,33 +190,33 @@ Flag numbers identify the weak dimension(s): 1=Concept Focus, 2=Starter Calibrat
190190
### Chapter: UVM Foundations
191191
| Slug | Title | Status | Score | Prereqs | Teaches |
192192
|---|---|---|---|---|---|
193-
| `uvm/reporting` | The First UVM Test || 21/27 ⚠️2,4 | `sv/modules-and-ports`, `sv/interfaces` | `uvm_component`, `uvm_test`, `` `uvm_info/warning/error ``, severity levels, `build_phase`/`run_phase`, objections |
194-
| `uvm/seq-item` | Sequence Items || 22/27 ⚠️2 | `uvm/reporting`, `sv/packed-structs` | `uvm_sequence_item`, `` `uvm_object_utils ``, `rand` fields, constraints, `convert2string` |
193+
| `uvm/reporting` | The First UVM Test || 25/27 ⚠️4,5 | `sv/modules-and-ports`, `sv/interfaces` | `uvm_component`, `uvm_test`, `` `uvm_info/warning/error ``, severity levels, `build_phase`/`run_phase`, objections |
194+
| `uvm/seq-item` | Sequence Items || 26/27 ⚠️9 | `uvm/reporting`, `sv/packed-structs` | `uvm_sequence_item`, `` `uvm_object_utils ``, `rand` fields, constraints, `convert2string` |
195195

196196
### Chapter: Stimulus
197197
| Slug | Title | Status | Score | Prereqs | Teaches |
198198
|---|---|---|---|---|---|
199-
| `uvm/sequence` | Sequences || 23/27 ⚠️2 | `uvm/seq-item` | `uvm_sequence`, `body()`, `start_item`/`randomize`/`finish_item` loop |
200-
| `uvm/driver` | The Driver || 23/27 ⚠️2 | `uvm/sequence`, `sv/interfaces`, `sv/always-ff` | `uvm_driver`, `get_next_item`/`item_done`, virtual interface driving, 1-cycle latency capture |
201-
| `uvm/constrained-random` | Constrained-Random Scenarios || 21/27 ⚠️2,5 | `uvm/seq-item` | `dist`, inline `randomize() with {}`, `constraint_mode()` |
199+
| `uvm/sequence` | Sequences || 27/27 | `uvm/seq-item` | `uvm_sequence`, `body()`, `start_item`/`randomize`/`finish_item` loop |
200+
| `uvm/driver` | The Driver || 24/27 ⚠️1,4,5 | `uvm/sequence`, `sv/interfaces`, `sv/always-ff` | `uvm_driver`, `get_next_item`/`item_done`, virtual interface driving, 1-cycle latency capture |
201+
| `uvm/constrained-random` | Constrained-Random Scenarios || 24/27 ⚠️1,4,9 | `uvm/seq-item` | `dist`, inline `randomize() with {}`, `constraint_mode()` |
202202

203203
### Chapter: Checking
204204
| Slug | Title | Status | Score | Prereqs | Teaches |
205205
|---|---|---|---|---|---|
206-
| `uvm/monitor` | Monitor and Scoreboard || 23/27 ⚠️2 | `uvm/driver`, `uvm/seq-item` | `uvm_monitor`, `uvm_analysis_port`, `write()`, `uvm_scoreboard`, shadow memory |
207-
| `uvm/env` | Environment and Test || 24/27 ⚠️2 | `uvm/monitor` | `uvm_env`, `uvm_agent`, analysis port → scoreboard wiring |
206+
| `uvm/monitor` | Monitor and Scoreboard || 25/27 ⚠️1,4 | `uvm/driver`, `uvm/seq-item` | `uvm_monitor`, `uvm_analysis_port`, `write()`, `uvm_scoreboard`, shadow memory |
207+
| `uvm/env` | Environment and Test || 26/27 ⚠️4 | `uvm/monitor` | `uvm_env`, `uvm_agent`, analysis port → scoreboard wiring |
208208

209209
### Chapter: Functional Coverage
210210
| Slug | Title | Status | Score | Prereqs | Teaches |
211211
|---|---|---|---|---|---|
212-
| `uvm/covergroup` | Functional Coverage || 24/27 ⚠️2 | `uvm/monitor`, `sv/covergroup-basics` | functional coverage in UVM, `uvm_subscriber`, sampling transactions |
213-
| `uvm/cross-coverage` | Cross Coverage || 23/27 ⚠️2,8 | `uvm/covergroup`, `sv/cross-coverage` | cross in UVM context, `addr × we` 2D coverage |
214-
| `uvm/coverage-driven` | Coverage-Driven Verification || 24/27 ⚠️2 | `uvm/cross-coverage` | coverage-driven loop, `get_coverage()` exit condition |
212+
| `uvm/covergroup` | Functional Coverage || 25/27 ⚠️4,9 | `uvm/monitor`, `sv/covergroup-basics` | functional coverage in UVM, `uvm_subscriber`, sampling transactions |
213+
| `uvm/cross-coverage` | Cross Coverage || 27/27 | `uvm/covergroup`, `sv/cross-coverage` | cross in UVM context, `addr × we` 2D coverage |
214+
| `uvm/coverage-driven` | Coverage-Driven Verification || 26/27 ⚠️4 | `uvm/cross-coverage` | coverage-driven loop, `get_coverage()` exit condition |
215215

216216
### Chapter: Advanced UVM
217217
| Slug | Title | Status | Score | Prereqs | Teaches |
218218
|---|---|---|---|---|---|
219-
| `uvm/factory-override` | Factory Overrides || 24/27 ⚠️2 | `uvm/seq-item` | `uvm_factory`, `type_id::set_type_override`, corner-case testing via type substitution |
219+
| `uvm/factory-override` | Factory Overrides || 24/27 ⚠️2,7,9 | `uvm/seq-item` | `uvm_factory`, `type_id::set_type_override`, corner-case testing via type substitution |
220220
| `uvm/virtual-seq` | Virtual Sequences | 💡 || `uvm/env` | `uvm_virtual_sequencer`, coordinating stimulus across multiple agents |
221221
| `uvm/ral` | Register Abstraction Layer (RAL) | 💡 || `uvm/driver` | `uvm_reg_block`, `uvm_reg`, frontdoor/backdoor register access |
222222

e2e/diag-focus.spec.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Diagnostic: probe Surfer's id_of_name / FocusItem / VisibleItemIndex in the current build.
3+
* Run with: npx playwright test e2e/diag-focus.spec.js --headed
4+
*/
5+
import { test, expect } from '@playwright/test';
6+
7+
test('diag: probe FocusItem vs id_of_name in current Surfer build', async ({ page }) => {
8+
// Skip if Surfer crashes (no WebGL)
9+
await page.goto('/surfer/index.html#dev');
10+
try {
11+
await expect(page.getByText('Sorry, Surfer crashed')).toBeVisible({ timeout: 3000 });
12+
test.skip(true, 'Surfer crashes in this environment');
13+
} catch { /* good — Surfer works */ }
14+
15+
// Use priority-enc which has a simple flat scope (grant/req/valid signals)
16+
// and is known to generate a VCD waveform from the solution.
17+
await page.goto('/lesson/sv/priority-enc');
18+
await page.evaluate(() => {
19+
for (const key of Object.keys(localStorage)) {
20+
if (key.startsWith('svt:')) localStorage.removeItem(key);
21+
}
22+
});
23+
await page.getByTitle('Editor options').click();
24+
await page.getByTestId('solve-button').click();
25+
await page.getByTestId('run-button').click();
26+
await expect(page.getByTestId('runtime-tab-waves')).toBeVisible({ timeout: 90_000 });
27+
await page.getByTestId('runtime-tab-waves').click();
28+
29+
const waveFrame = page.getByTestId('waveform-frame-wrapper');
30+
await expect(waveFrame).toHaveAttribute('data-wave-state', 'ready', { timeout: 30_000 });
31+
32+
// Give Surfer extra time after signals are ready
33+
await page.waitForTimeout(1000);
34+
35+
const iframeHandle = await page.getByTestId('waveform-iframe').elementHandle();
36+
const iframeCtx = await iframeHandle.contentFrame();
37+
38+
const result = await iframeCtx.evaluate(async () => {
39+
const cw = window;
40+
const out = {};
41+
42+
// 1. Which APIs exist?
43+
out.apis = {
44+
id_of_name: typeof cw.id_of_name,
45+
index_of_name: typeof cw.index_of_name,
46+
focused_item: typeof cw.focused_item,
47+
get_focused_item: typeof cw.get_focused_item,
48+
visible_item_count: typeof cw.visible_item_count,
49+
item_at_index: typeof cw.item_at_index,
50+
name_of_id: typeof cw.name_of_id,
51+
};
52+
53+
// 1b. index_of_name for the same paths (should return VisibleItemIndex, different from id_of_name)
54+
if (typeof cw.index_of_name === 'function') {
55+
const paths2 = ['tb.grant', 'tb.req', 'tb.valid', 'grant', 'req', 'valid', 'tb'];
56+
out.index_of_name = {};
57+
for (const p of paths2) {
58+
try { out.index_of_name[p] = await cw.index_of_name(p); } catch (e) { out.index_of_name[p] = 'ERR:' + e.message; }
59+
}
60+
}
61+
62+
// 2. id_of_name for various paths (priority-enc signals: grant, req, valid)
63+
if (typeof cw.id_of_name === 'function') {
64+
const paths = ['tb.grant', 'tb.req', 'tb.valid', 'grant', 'req', 'valid', 'tb'];
65+
out.id_of_name = {};
66+
for (const p of paths) {
67+
try { out.id_of_name[p] = await cw.id_of_name(p); } catch (e) { out.id_of_name[p] = 'ERR:' + e.message; }
68+
}
69+
}
70+
71+
// 3. Try focused_item / get_focused_item
72+
if (typeof cw.focused_item === 'function') {
73+
try { out.focused_item_result = await cw.focused_item(); } catch (e) { out.focused_item_result = 'ERR:' + e.message; }
74+
}
75+
if (typeof cw.get_focused_item === 'function') {
76+
try { out.get_focused_item_result = await cw.get_focused_item(); } catch (e) { out.get_focused_item_result = 'ERR:' + e.message; }
77+
}
78+
79+
// 4. Scan item_at_index(0..9) to map VisibleItemIndex → DisplayedItemRef
80+
if (typeof cw.visible_item_count === 'function') {
81+
try { out.visible_item_count = await cw.visible_item_count(); } catch (e) { out.visible_item_count = 'ERR:' + e.message; }
82+
}
83+
if (typeof cw.item_at_index === 'function') {
84+
out.item_at_index = {};
85+
for (let i = 0; i < 10; i++) {
86+
try { out.item_at_index[i] = await cw.item_at_index(i); } catch (e) { out.item_at_index[i] = 'ERR:' + e.message; }
87+
}
88+
}
89+
if (typeof cw.name_of_id === 'function') {
90+
out.name_of_id = {};
91+
for (let i = 0; i < 10; i++) {
92+
try { out.name_of_id[i] = await cw.name_of_id(i); } catch (e) { out.name_of_id[i] = 'ERR:' + e.message; }
93+
}
94+
}
95+
96+
return out;
97+
});
98+
99+
console.log('\n=== SURFER PROBE ===\n', JSON.stringify(result, null, 2));
100+
101+
// 4. Now send FocusItem with the id_of_name value and check if focus moved
102+
const mainCtx = page;
103+
const sendMsg = async (msg) => {
104+
await iframeCtx.evaluate((m) => {
105+
window.postMessage({ command: 'InjectMessage', message: JSON.stringify(m) }, '*');
106+
}, msg);
107+
await page.waitForTimeout(300);
108+
};
109+
110+
// Try focusing each visible item index 0..5 and after each, read focused_item
111+
if (result.apis.focused_item === 'function' || result.apis.get_focused_item === 'function') {
112+
const focusResults = {};
113+
for (let i = 0; i <= 5; i++) {
114+
await sendMsg({ FocusItem: i });
115+
const fi = await iframeCtx.evaluate(async () => {
116+
const cw = window;
117+
if (typeof cw.focused_item === 'function') return await cw.focused_item();
118+
if (typeof cw.get_focused_item === 'function') return await cw.get_focused_item();
119+
return null;
120+
});
121+
focusResults[`FocusItem(${i})`] = fi;
122+
}
123+
console.log('\n=== FocusItem scan ===\n', JSON.stringify(focusResults, null, 2));
124+
}
125+
126+
// 5. After focusing what we think is cmd, send transition_next and see if cursor moves
127+
// Try with the id_of_name value
128+
// Use index_of_name (VisibleItemIndex) for FocusItem, not id_of_name (DisplayedItemRef).
129+
const cmdId = result.index_of_name?.['tb.req'] ?? result.id_of_name?.['tb.req'];
130+
if (cmdId !== undefined) {
131+
await sendMsg({ FocusItem: cmdId });
132+
await page.waitForTimeout(300);
133+
134+
// Send transition_next via command file
135+
const cmdUrl = await mainCtx.evaluate(() => {
136+
const blob = new Blob(['transition_next\n'], { type: 'text/plain' });
137+
return URL.createObjectURL(blob);
138+
});
139+
await iframeCtx.evaluate((url) => {
140+
window.postMessage({ command: 'InjectMessage', message: JSON.stringify({ LoadCommandFileFromUrl: url }) }, '*');
141+
}, cmdUrl);
142+
await page.waitForTimeout(500);
143+
console.log(`\nSent FocusItem(${cmdId}) then transition_next`);
144+
}
145+
146+
// Always pass — this is a diagnostic
147+
expect(result.apis.id_of_name).toBe('function');
148+
});

e2e/perf-profile.spec.js

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// Temporary performance profiling spec — not meant for CI assertions.
2+
// Run with: npx playwright test e2e/perf-profile.spec.js --reporter=list
3+
4+
import { test } from '@playwright/test';
5+
6+
const BASE_URL = 'http://127.0.0.1:4173';
7+
8+
test('Core Web Vitals on /lesson/sv/welcome', async ({ page }) => {
9+
// Inject PerformanceObserver listeners before navigation so we catch all entries.
10+
await page.addInitScript(() => {
11+
window.__cwv = { lcp: null, fcp: null, cls: 0, inp: null };
12+
13+
// LCP
14+
try {
15+
new PerformanceObserver((list) => {
16+
const entries = list.getEntries();
17+
if (entries.length) {
18+
window.__cwv.lcp = entries[entries.length - 1].startTime;
19+
}
20+
}).observe({ type: 'largest-contentful-paint', buffered: true });
21+
} catch (e) {}
22+
23+
// FCP
24+
try {
25+
new PerformanceObserver((list) => {
26+
for (const entry of list.getEntries()) {
27+
if (entry.name === 'first-contentful-paint') {
28+
window.__cwv.fcp = entry.startTime;
29+
}
30+
}
31+
}).observe({ type: 'paint', buffered: true });
32+
} catch (e) {}
33+
34+
// CLS
35+
try {
36+
new PerformanceObserver((list) => {
37+
for (const entry of list.getEntries()) {
38+
if (!entry.hadRecentInput) {
39+
window.__cwv.cls += entry.value;
40+
}
41+
}
42+
}).observe({ type: 'layout-shift', buffered: true });
43+
} catch (e) {}
44+
45+
// INP (Interaction to Next Paint) — only available in some browsers
46+
try {
47+
new PerformanceObserver((list) => {
48+
for (const entry of list.getEntries()) {
49+
if (window.__cwv.inp === null || entry.duration > window.__cwv.inp) {
50+
window.__cwv.inp = entry.duration;
51+
}
52+
}
53+
}).observe({ type: 'event', buffered: true, durationThreshold: 16 });
54+
} catch (e) {}
55+
});
56+
57+
const navStart = Date.now();
58+
await page.goto(`${BASE_URL}/lesson/sv/welcome`, { waitUntil: 'networkidle' });
59+
const navEnd = Date.now();
60+
61+
// Give observers a moment to flush
62+
await page.waitForTimeout(1000);
63+
64+
const cwv = await page.evaluate(() => window.__cwv);
65+
66+
// TTFB from Navigation Timing API
67+
const ttfb = await page.evaluate(() => {
68+
const nav = performance.getEntriesByType('navigation')[0];
69+
return nav ? nav.responseStart - nav.requestStart : null;
70+
});
71+
72+
console.log('\n========== CORE WEB VITALS ==========');
73+
console.log(`TTFB: ${ttfb !== null ? ttfb.toFixed(1) + ' ms' : 'n/a'}`);
74+
console.log(`FCP: ${cwv.fcp !== null ? cwv.fcp.toFixed(1) + ' ms' : 'n/a'}`);
75+
console.log(`LCP: ${cwv.lcp !== null ? cwv.lcp.toFixed(1) + ' ms' : 'n/a'}`);
76+
console.log(`CLS: ${cwv.cls.toFixed(4)}`);
77+
console.log(`INP: ${cwv.inp !== null ? cwv.inp.toFixed(1) + ' ms' : 'n/a (no interactions)'}`);
78+
console.log(`Wall-clock page load: ${navEnd - navStart} ms`);
79+
console.log('=====================================\n');
80+
});
81+
82+
test('JS resource load times on /lesson/sv/welcome', async ({ page }) => {
83+
await page.goto(`${BASE_URL}/lesson/sv/welcome`, { waitUntil: 'networkidle' });
84+
await page.waitForTimeout(500);
85+
86+
const resources = await page.evaluate(() => {
87+
return performance.getEntriesByType('resource')
88+
.filter(r => r.initiatorType === 'script' || r.name.endsWith('.js') || r.name.endsWith('.wasm'))
89+
.map(r => ({
90+
name: r.name.replace(/.*\//, '').slice(0, 60),
91+
duration: r.duration,
92+
transferSize: r.transferSize,
93+
encodedBodySize: r.encodedBodySize,
94+
decodedBodySize: r.decodedBodySize,
95+
initiatorType: r.initiatorType,
96+
}))
97+
.sort((a, b) => b.duration - a.duration);
98+
});
99+
100+
const totalTransfer = await page.evaluate(() => {
101+
return performance.getEntriesByType('resource')
102+
.reduce((sum, r) => sum + (r.transferSize || 0), 0);
103+
});
104+
105+
const totalDecoded = await page.evaluate(() => {
106+
return performance.getEntriesByType('resource')
107+
.reduce((sum, r) => sum + (r.decodedBodySize || 0), 0);
108+
});
109+
110+
console.log('\n========== JS/WASM RESOURCE LOAD TIMES (slowest first) ==========');
111+
for (const r of resources.slice(0, 20)) {
112+
const kb = r.transferSize ? (r.transferSize / 1024).toFixed(1) + ' KB' : '(cached/0)';
113+
console.log(` ${r.duration.toFixed(0).padStart(6)} ms ${kb.padStart(12)} ${r.name}`);
114+
}
115+
console.log(`\nTotal JS/WASM resources shown: ${resources.length}`);
116+
console.log(`Total transferred (all resources): ${(totalTransfer / 1024).toFixed(1)} KB`);
117+
console.log(`Total decoded (all resources): ${(totalDecoded / 1024).toFixed(1)} KB`);
118+
console.log('==================================================================\n');
119+
});
120+
121+
test('SPA navigation timing: welcome → modules-and-ports → always-comb', async ({ page }) => {
122+
// The sidebar uses button[data-active] for lesson buttons, not <a> links.
123+
// Chapter buttons have no data-active attribute.
124+
await page.goto(`${BASE_URL}/lesson/sv/welcome`, { waitUntil: 'networkidle' });
125+
126+
// Wait for sidebar to initialise
127+
await page.locator('button[data-active]').first().waitFor({ timeout: 10000 });
128+
129+
// --- Navigate to modules-and-ports ---
130+
// "Modules and Ports" is in the "Basics" chapter.
131+
// First ensure the lesson button is visible; if not, open the chapter.
132+
const modulesBtn = page.locator('button[data-active]').filter({ hasText: 'Modules and Ports' });
133+
if ((await modulesBtn.count()) === 0) {
134+
await page.locator('button:not([data-active])').filter({ hasText: 'Basics' }).click();
135+
}
136+
137+
const t0 = Date.now();
138+
await modulesBtn.first().click({ timeout: 10000 });
139+
// Wait for lesson content heading to appear
140+
await page.locator('button[data-active]').filter({ hasText: 'Modules and Ports' }).waitFor({ timeout: 10000 });
141+
const t1 = Date.now();
142+
const nav1 = t1 - t0;
143+
144+
await page.waitForTimeout(300);
145+
146+
// --- Navigate to always-comb ---
147+
// "always_comb and case" lesson name
148+
const alwaysBtn = page.locator('button[data-active]').filter({ hasText: 'always_comb' });
149+
if ((await alwaysBtn.count()) === 0) {
150+
// It may be in a chapter that's not yet open — try "Combinational Logic" chapter
151+
await page.locator('button:not([data-active])').filter({ hasText: 'Combinational Logic' }).first().click();
152+
}
153+
154+
const t2 = Date.now();
155+
await alwaysBtn.first().click({ timeout: 10000 });
156+
await page.locator('button[data-active]').filter({ hasText: 'always_comb' }).waitFor({ timeout: 10000 });
157+
const t3 = Date.now();
158+
const nav2 = t3 - t2;
159+
160+
console.log('\n========== SPA NAVIGATION TIMING ==========');
161+
console.log(`welcome → modules-and-ports: ${nav1} ms`);
162+
console.log(`modules-and-ports → always-comb: ${nav2} ms`);
163+
console.log(`Average SPA nav time: ${((nav1 + nav2) / 2).toFixed(0)} ms`);
164+
console.log('===========================================\n');
165+
});
166+
167+
test('Total JS transferred summary', async ({ page }) => {
168+
await page.goto(`${BASE_URL}/lesson/sv/welcome`, { waitUntil: 'networkidle' });
169+
await page.waitForTimeout(500);
170+
171+
const summary = await page.evaluate(() => {
172+
const entries = performance.getEntriesByType('resource');
173+
const byType = {};
174+
let totalTransfer = 0;
175+
let totalDecoded = 0;
176+
for (const r of entries) {
177+
const type = r.initiatorType || 'other';
178+
if (!byType[type]) byType[type] = { count: 0, transfer: 0, decoded: 0 };
179+
byType[type].count++;
180+
byType[type].transfer += r.transferSize || 0;
181+
byType[type].decoded += r.decodedBodySize || 0;
182+
totalTransfer += r.transferSize || 0;
183+
totalDecoded += r.decodedBodySize || 0;
184+
}
185+
return { byType, totalTransfer, totalDecoded, count: entries.length };
186+
});
187+
188+
console.log('\n========== TOTAL JS TRANSFERRED ==========');
189+
console.log(`Total resources: ${summary.count}`);
190+
console.log(`Total transferred: ${(summary.totalTransfer / 1024).toFixed(1)} KB`);
191+
console.log(`Total decoded: ${(summary.totalDecoded / 1024).toFixed(1)} KB`);
192+
console.log('\nBy initiator type:');
193+
for (const [type, data] of Object.entries(summary.byType).sort((a, b) => b[1].transfer - a[1].transfer)) {
194+
console.log(` ${type.padEnd(12)} count=${data.count} transfer=${(data.transfer/1024).toFixed(1)} KB decoded=${(data.decoded/1024).toFixed(1)} KB`);
195+
}
196+
console.log('==========================================\n');
197+
});

0 commit comments

Comments
 (0)