diff --git a/.agents/skills/protowiki-components/references/chrome-primitives.md b/.agents/skills/protowiki-components/references/chrome-primitives.md
index 4b5f10b..1ee7d4a 100644
--- a/.agents/skills/protowiki-components/references/chrome-primitives.md
+++ b/.agents/skills/protowiki-components/references/chrome-primitives.md
@@ -12,7 +12,7 @@ chrome-without-the-wrapper (e.g., a custom layout that doesn't use
| Skin | Chrome feel | Notes |
| --- | --- | --- |
-| `desktop` | **Vector 2022–style** | Wordmark/tagline (**`wordmarkSrc`**, **`taglineSrc`**, **`#logo` override**), **`SearchBar`** + **Search** button, username link (**`username`** + **`#username`**), user-tool cluster ( **`navTools`** preset vs **`#nav`** override ). **Main-menu glyph is icon-only** (mock). Global skin stays **desktop** until the viewport is **≤640px**; **below 1120px**, inline search collapses to a search icon; **below 768px**, watchlist hides (`nav-button-desktop` parity). |
+| `desktop` | **Vector 2022–style** | Wordmark/tagline (**`wordmarkSrc`**, **`taglineSrc`**, **`#logo` override**), **`SearchBar`** + **Search** button, username link (**`username`** + **`#username`**), user-tool cluster ( **`navTools`** preset vs **`#nav`** override ). **Main-menu glyph is icon-only mock by default** — override via **`#menu`**. Global skin stays **desktop** until the viewport is **≤640px**; **below 1120px**, inline search collapses to a search icon; **below 768px**, watchlist hides (`nav-button-desktop` parity). |
| `mobile` | **Minerva-style** | Grey elevated bar: menu, Wikipedia wordmark (**`mobileWordmarkSrc`** / **`wordmarkSrc`**, **`#logo`**), search icon + notifications — **`navTools` is ignored**. |
**`ChromeFooter`** matches the skin:
@@ -52,6 +52,7 @@ Desktop **inline search** is always **``** inside the header (not a
| Slot | Default | Use for |
| --- | --- | --- |
| `#logo` | EN Wikipedia wordmark (+ tagline on desktop) via `
` | Replace with another project's wordmark / lockup |
+| `#menu` | Mock burger icon (**desktop**: non-interactive glyph; **mobile**: quiet button) | Replace with an interactive main-menu trigger + popover (**`ChromeWrapper`** forwards **`#menu`**) |
| `#username` | Anchor from **`username`** (hidden when empty after trim) | Replace with custom markup before tool icons (**desktop**) |
| `#nav` | Vector-style tool icons on **desktop** only | Replace the default user-tool cluster (**`navTools` ignored**) |
diff --git a/.agents/skills/protowiki-components/references/dashboard.md b/.agents/skills/protowiki-components/references/dashboard.md
index 469a3f9..d49c300 100644
--- a/.agents/skills/protowiki-components/references/dashboard.md
+++ b/.agents/skills/protowiki-components/references/dashboard.md
@@ -14,7 +14,7 @@ Reference prototypes:
Do not confuse:
- **`SpecialPageWrapper` `help`** — desktop title-row **Help** link (Codex docs URL)
-- **`dashpage/HelpModule.vue`** — prototype-local **"Get help with editing"** sidebar/mobile card (wraps **`DashboardModule`**)
+- **`template-homepage/HelpModule.vue`** — prototype-local **"Get help with editing"** sidebar/mobile card (wraps **`DashboardModule`**)
## `Dashboard`
@@ -35,7 +35,7 @@ With **`data-skin="desktop"`**, the two-column grid (`primary` ~66% + `sidebar`
Utility classes from the component stylesheet:
- **`dashboard-slot`** — min-height baseline on module roots
-- **`dashboard-slot--desktop-primary`**, **`dashboard-slot--mobile-primary`**, etc. — prototype hooks for per-slot sizing (see **`template-dashboard`** / **`dashpage`**)
+- **`dashboard-slot--desktop-primary`**, **`dashboard-slot--mobile-primary`**, etc. — prototype hooks for per-slot sizing (see **`template-dashboard`** / **`template-homepage`**)
### Props
diff --git a/.agents/skills/protowiki-getting-started/SKILL.md b/.agents/skills/protowiki-getting-started/SKILL.md
index bdac5a7..ff9256b 100644
--- a/.agents/skills/protowiki-getting-started/SKILL.md
+++ b/.agents/skills/protowiki-getting-started/SKILL.md
@@ -51,7 +51,7 @@ protowiki/
│ │ ├── index.vue ← home / gallery (auto-lists prototypes)
│ │ ├── template-chrome/index.vue
│ │ ├── template-dashboard/index.vue
-│ │ └── dashpage/index.vue
+│ │ └── template-homepage/index.vue
│ ├── components/ ← shipped components (wrappers, primitives, article, dashboard)
│ ├── composables/ ← useSkin / useTheme (read-only hooks)
│ ├── lib/ ← theming logic, helpers
diff --git a/package-lock.json b/package-lock.json
index 2ed7fda..982ded4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@wikimedia/codex": "^2.5.1",
"@wikimedia/codex-design-tokens": "^2.5.1",
"@wikimedia/codex-icons": "^2.5.1",
+ "fakewiki": "^0.0.13",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
@@ -1906,6 +1907,7 @@
"resolved": "https://registry.npmjs.org/@wikimedia/codex-icons/-/codex-icons-2.5.1.tgz",
"integrity": "sha512-ZbAXQD0dLuqj8uoMrpswusPymAZ07Pt0Y5wp1mNS3YyHuc1aAvsOGWdsV88YYoJN1l2jvYUE98/KoAKn7Cw5YA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.20.2",
"npm": ">=10.8.2"
@@ -2580,6 +2582,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fakewiki": {
+ "version": "0.0.13",
+ "resolved": "https://registry.npmjs.org/fakewiki/-/fakewiki-0.0.13.tgz",
+ "integrity": "sha512-bPIK55+eHRfA5tn7fGHBg/bpTSTP6r4S/HWEskcD5h2XZT3NFB+lxVVgNByvZCOSWI34cv3X8DFCFMzE6R8r+A==",
+ "license": "GPL-2.0-only",
+ "peerDependencies": {
+ "@wikimedia/codex-icons": "^2.0.0",
+ "vue": "^3.5.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
diff --git a/package.json b/package.json
index 5fa316b..d16dd22 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@wikimedia/codex": "^2.5.1",
"@wikimedia/codex-design-tokens": "^2.5.1",
"@wikimedia/codex-icons": "^2.5.1",
+ "fakewiki": "^0.0.13",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
diff --git a/scripts/probe-dashpage-recent-changes-signals.ts b/scripts/probe-dashpage-recent-changes-signals.ts
new file mode 100644
index 0000000..66a7488
--- /dev/null
+++ b/scripts/probe-dashpage-recent-changes-signals.ts
@@ -0,0 +1,62 @@
+#!/usr/bin/env npx tsx
+/**
+ * Probe language-agnostic articlequality score deltas for recent edits.
+ * Run: npx tsx scripts/probe-dashpage-recent-changes-signals.ts
+ */
+
+import { createDashpageRecentChangesWiki } from '../src/lib/fetchDashpageRecentChanges'
+import { fetchArticleQualityScore } from '../src/lib/fetchArticleQualityScore'
+import { DASHPAGE_RC_API_USER_AGENT } from '../src/lib/dashpageRecentChangesConstants'
+
+async function sleep(ms: number): Promise {
+ await new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+async function main(): Promise {
+ const wiki = createDashpageRecentChangesWiki()
+ const deltas: number[] = []
+
+ console.log('Fetching recent changes (needs review)…')
+ const rc = await wiki.getRecentChanges({ limit: 12, onlyNeedsReview: true })
+
+ for (const rev of rc.revisions) {
+ if (!rev.id || !rev.pageName || rev.pageName.includes(':')) continue
+
+ const parentId = await wiki.getParentRevisionId(rev.pageName, rev.id)
+ if (parentId == null || parentId <= 0) continue
+
+ const before = await fetchArticleQualityScore(parentId, 'en', DASHPAGE_RC_API_USER_AGENT)
+ await sleep(300)
+ const after = await fetchArticleQualityScore(rev.id, 'en', DASHPAGE_RC_API_USER_AGENT)
+ await sleep(300)
+
+ if (before && after) {
+ const delta = after.score - before.score
+ deltas.push(delta)
+ console.log(
+ `rev ${rev.id} (${rev.pageName}): before=${before.score.toFixed(4)} after=${after.score.toFixed(4)} delta=${delta.toFixed(4)}`,
+ )
+ }
+ }
+
+ if (!deltas.length) {
+ console.log('No deltas collected — keeping default threshold 0.03')
+ return
+ }
+
+ const absDeltas = deltas.map(Math.abs).sort((a, b) => a - b)
+ const median = absDeltas[Math.floor(absDeltas.length / 2)] ?? 0
+ const p75 = absDeltas[Math.floor(absDeltas.length * 0.75)] ?? median
+ const suggested = Math.max(0.02, Math.min(0.05, Math.round(p75 * 100) / 100))
+
+ console.log('\nSummary:')
+ console.log(` samples: ${deltas.length}`)
+ console.log(` |delta| median: ${median.toFixed(4)}`)
+ console.log(` |delta| p75: ${p75.toFixed(4)}`)
+ console.log(` suggested display threshold (sprinthackular uses ${1e-6}): any delta > 0`)
+}
+
+main().catch((error) => {
+ console.error(error)
+ process.exit(1)
+})
diff --git a/src/components/ArticleLive.vue b/src/components/ArticleLive.vue
index 02ffd2d..44ccb5b 100644
--- a/src/components/ArticleLive.vue
+++ b/src/components/ArticleLive.vue
@@ -5,6 +5,7 @@ import { CdxMessage, CdxProgressBar } from '@wikimedia/codex'
import ArticleRenderer from '@/components/ArticleRenderer.vue'
import ArticleWrapper from '@/components/ArticleWrapper.vue'
import type { Skin, Theme } from '@/lib/theming'
+import { wipeLocalStorage } from '@/lib/wipeLocalStorage'
/** Cache of successfully fetched live article HTML (key: host + title). */
type CachedArticleBody = { html: string; liveTitle: string }
@@ -29,6 +30,7 @@ function loadFromStorage(key: string): CachedArticleBody | null {
if (!raw) return null
return JSON.parse(raw) as CachedArticleBody
} catch {
+ wipeLocalStorage()
return null
}
}
diff --git a/src/components/ChromeHeader.vue b/src/components/ChromeHeader.vue
index c87617f..48fe46f 100644
--- a/src/components/ChromeHeader.vue
+++ b/src/components/ChromeHeader.vue
@@ -96,10 +96,12 @@ function navHas(tool: ChromeNavTool): boolean {