feat(ecosystem-audit): add Tier 3 metrics (git activity, a11y, auth guard, docker)

Adds 4 new Tier 3 metrics to the ecosystem health audit script:
- Git Activity: % of apps with commits in the last 30 days (97%)
- A11y Indicators: alt-text coverage, role=dialog, focusTrap (36%)
- Auth Guard Coverage: AuthGate/authGuard presence per app (83%)
- Docker Readiness: Dockerfile present per app (80%)

Overall score updated from 74 → 72 (23 metrics, 135 total weight).
Dashboard at /manascore/ecosystem updated with new category rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-31 16:32:56 +02:00
parent df1cd4bfa0
commit d460c9ec07
3 changed files with 303 additions and 68 deletions

View file

@ -1,26 +1,30 @@
{
"generatedAt": "2026-03-31T12:18:59.699Z",
"overallScore": 74,
"generatedAt": "2026-03-31T14:32:40.747Z",
"overallScore": 72,
"scores": {
"sharedPackages": 90,
"iconConsistency": 89,
"iconConsistency": 78,
"modalConsistency": 19,
"errorHandling": 17,
"i18nCoverage": 86,
"errorHandling": 16,
"i18nCoverage": 87,
"localFirst": 93,
"styleConsistency": 87,
"errorBoundaries": 54,
"errorBoundaries": 52,
"typescriptStrict": 100,
"testCoverage": 72,
"pwaSupport": 2,
"testCoverage": 73,
"pwaSupport": 4,
"maintainability": 0,
"securityHeaders": 76,
"skeletonLoading": 76,
"securityHeaders": 73,
"skeletonLoading": 77,
"toastConsistency": 100,
"storePattern": 95,
"sharedTypes": 62,
"storePattern": 94,
"sharedTypes": 53,
"depFreshness": 80,
"bundleConfig": 100
"bundleConfig": 100,
"gitActivity": 97,
"a11yIndicators": 36,
"authGuardCoverage": 83,
"dockerReadiness": 80
},
"weights": {
"sharedPackages": 20,
@ -41,13 +45,17 @@
"storePattern": 4,
"sharedTypes": 3,
"depFreshness": 2,
"bundleConfig": 2
"bundleConfig": 2,
"gitActivity": 3,
"a11yIndicators": 4,
"authGuardCoverage": 5,
"dockerReadiness": 3
},
"details": {
"icons": {
"adoption": 89,
"adoption": 78,
"phosphorFiles": 347,
"inlineSvgFiles": 45,
"inlineSvgFiles": 98,
"perApp": {
"calc": {
"phosphor": 1,
@ -97,6 +105,10 @@
"phosphor": 26,
"inlineSvg": 0
},
"memoro": {
"phosphor": 0,
"inlineSvg": 53
},
"moodlit": {
"phosphor": 5,
"inlineSvg": 0
@ -175,72 +187,72 @@
},
"packages": {
"coreAdoption": 90,
"totalApps": 29,
"totalApps": 30,
"perPackage": {
"Auth": {
"count": 29,
"total": 29,
"count": 30,
"total": 30,
"adoption": 100
},
"UI": {
"count": 28,
"total": 29,
"count": 29,
"total": 30,
"adoption": 97
},
"Theme": {
"count": 24,
"total": 29,
"count": 25,
"total": 30,
"adoption": 83
},
"Branding": {
"count": 28,
"total": 29,
"count": 29,
"total": 30,
"adoption": 97
},
"i18n": {
"count": 23,
"total": 29,
"adoption": 79
"count": 24,
"total": 30,
"adoption": 80
},
"Error Tracking": {
"count": 24,
"total": 29,
"count": 25,
"total": 30,
"adoption": 83
},
"Icons": {
"count": 25,
"total": 29,
"adoption": 86
"count": 26,
"total": 30,
"adoption": 87
},
"Local Store": {
"count": 27,
"total": 29,
"count": 28,
"total": 30,
"adoption": 93
}
}
},
"errors": {
"adoption": 17,
"inline": 193,
"adoption": 16,
"inline": 217,
"shared": 40
},
"i18n": {
"adoption": 86,
"withI18n": 25,
"adoption": 87,
"withI18n": 26,
"without": 4
},
"localFirst": {
"adoption": 93,
"count": 27
"count": 28
},
"styles": {
"themeAdoption": 83,
"tailwindAdoption": 90
},
"errorBoundaries": {
"adoption": 54,
"errorAdoption": 28,
"offlineAdoption": 79,
"adoption": 52,
"errorAdoption": 27,
"offlineAdoption": 77,
"appsWithErrorPage": 8,
"appsWithOfflinePage": 23,
"missing": {
@ -252,6 +264,7 @@
"context",
"manadeck",
"manavoxel",
"memoro",
"moodlit",
"mukke",
"news",
@ -267,20 +280,20 @@
"wisekeep",
"zitare"
],
"offline": ["manavoxel", "moodlit", "news", "playground", "uload", "wisekeep"]
"offline": ["manavoxel", "memoro", "moodlit", "news", "playground", "uload", "wisekeep"]
}
},
"typescript": {
"adoption": 100,
"strictApps": 29,
"strictApps": 30,
"nonStrict": []
},
"tests": {
"adoption": 72,
"e2eAdoption": 14,
"appsWithTests": 21,
"adoption": 73,
"e2eAdoption": 13,
"appsWithTests": 22,
"appsWithE2e": 4,
"totalTestFiles": 110,
"totalTestFiles": 122,
"noTests": [
"calc",
"inventar",
@ -293,10 +306,10 @@
]
},
"pwa": {
"adoption": 2,
"manifestAdoption": 3,
"adoption": 4,
"manifestAdoption": 7,
"swAdoption": 0,
"appsWithManifest": 1,
"appsWithManifest": 2,
"appsWithServiceWorker": 0,
"noPwa": [
"calc",
@ -331,7 +344,7 @@
},
"fileSizes": {
"adoption": 0,
"totalLargeFiles": 38,
"totalLargeFiles": 42,
"largestFile": {
"path": "matrix: lib/matrix/store.svelte.ts",
"lines": 2019
@ -350,7 +363,7 @@
{
"app": "contacts",
"file": "lib/components/ContactDetailModal.svelte",
"lines": 1511
"lines": 1502
},
{
"app": "todo",
@ -375,7 +388,7 @@
{
"app": "calendar",
"file": "lib/components/calendar/WeekView.svelte",
"lines": 953
"lines": 946
},
{
"app": "calendar",
@ -390,9 +403,10 @@
]
},
"todos": {
"totalCount": 22,
"totalCount": 33,
"perApp": {
"manacore": 13,
"memoro": 11,
"contacts": 5,
"todo": 2,
"chat": 1,
@ -400,13 +414,22 @@
}
},
"securityHeaders": {
"adoption": 76,
"adoption": 73,
"appsWithHeaders": 22,
"missing": ["manavoxel", "moodlit", "news", "playground", "times", "uload", "wisekeep"]
"missing": [
"manavoxel",
"memoro",
"moodlit",
"news",
"playground",
"times",
"uload",
"wisekeep"
]
},
"skeletons": {
"adoption": 76,
"appsWithSkeletons": 22,
"adoption": 77,
"appsWithSkeletons": 23,
"missing": ["citycorners", "manadeck", "manavoxel", "planta", "presi", "times", "zitare"]
},
"toasts": {
@ -415,25 +438,79 @@
"customToast": 0
},
"storePattern": {
"adoption": 95,
"totalRunesStores": 176,
"totalOldStores": 9,
"adoption": 94,
"totalRunesStores": 177,
"totalOldStores": 12,
"appsWithRunesStores": 24,
"appsWithOldStores": 4
"appsWithOldStores": 5
},
"sharedTypes": {
"adoption": 62,
"adoption": 53,
"sharedTypeImports": 8,
"localTypeFiles": 5
"localTypeFiles": 7
},
"depFreshness": {
"adoption": 80,
"totalDeps": 1068,
"totalDeps": 1106,
"avgDepsPerApp": 37
},
"bundleSize": {
"adoption": 100,
"appsWithBundleConfig": 29
"appsWithBundleConfig": 30
},
"gitActivity": {
"adoption": 97,
"activeApps": 29,
"perApp": {
"manacore": 166,
"todo": 135,
"calendar": 125,
"contacts": 95,
"mukke": 90,
"storage": 87,
"zitare": 80,
"chat": 73,
"picture": 70,
"presi": 68,
"clock": 64,
"manadeck": 63,
"citycorners": 61,
"nutriphi": 56,
"photos": 56,
"planta": 55,
"context": 48,
"matrix": 48,
"skilltree": 46,
"questions": 39,
"inventar": 19,
"playground": 18,
"manavoxel": 11,
"uload": 10,
"calc": 6,
"moodlit": 5,
"times": 5,
"news": 4,
"wisekeep": 4,
"memoro": 0
}
},
"a11y": {
"adoption": 36,
"altAdoption": 100,
"dialogAdoption": 0,
"trapAdoption": 7,
"totalImgFiles": 21,
"totalImgWithAlt": 21
},
"authGuard": {
"adoption": 83,
"appsWithAuthGuard": 25,
"missing": ["manacore", "manavoxel", "matrix", "memoro", "playground"]
},
"docker": {
"adoption": 80,
"appsWithDockerfile": 24,
"missing": ["context", "moodlit", "news", "planta", "questions", "wisekeep"]
}
},
"apps": [
@ -449,6 +526,7 @@
"manadeck",
"manavoxel",
"matrix",
"memoro",
"moodlit",
"mukke",
"news",

View file

@ -148,6 +148,30 @@ const categories = [
icon: '⚙️',
description: 'SvelteKit Adapter konfiguriert',
},
{
key: 'gitActivity',
label: 'Git Activity',
icon: '🔄',
description: 'Apps mit Commits in den letzten 30 Tagen',
},
{
key: 'a11yIndicators',
label: 'A11y Indicators',
icon: '♿',
description: 'Alt-Texte, role=dialog, focusTrap',
},
{
key: 'authGuardCoverage',
label: 'Auth Guard',
icon: '🔑',
description: 'AuthGate / authGuard in allen Apps',
},
{
key: 'dockerReadiness',
label: 'Docker Readiness',
icon: '🐳',
description: 'Dockerfile pro App vorhanden',
},
];
const packageDetails = details.packages.perPackage;

View file

@ -754,6 +754,123 @@ function measureBundleSize() {
return { adoption, appsWithBundleConfig };
}
function measureGitActivity() {
console.log('📊 Measuring Git Activity (last 30 days)...');
let activeApps = 0;
const perApp = {};
for (const app of WEB_APPS) {
const appDir = join(APPS_DIR, app);
try {
const result = execSync(
`git log --since="30 days ago" --oneline -- "${appDir}" 2>/dev/null | wc -l`,
{ encoding: 'utf-8' }
);
const commits = parseInt(result.trim()) || 0;
perApp[app] = commits;
if (commits > 0) activeApps++;
} catch {
perApp[app] = 0;
}
}
const adoption = Math.round((activeApps / WEB_APPS.length) * 100);
const sorted = Object.entries(perApp).sort(([, a], [, b]) => b - a);
console.log(` Active apps (≥1 commit): ${activeApps}/${WEB_APPS.length} (${adoption}%)`);
sorted.slice(0, 5).forEach(([a, c]) => console.log(` ${a}: ${c} commits`));
console.log('');
return { adoption, activeApps, perApp: Object.fromEntries(sorted) };
}
function measureA11yIndicators() {
console.log('📊 Measuring Accessibility Indicators...');
let totalImgFiles = 0;
let totalImgWithAlt = 0;
let appsWithDialogRole = 0;
let appsWithFocusTrap = 0;
for (const app of WEB_APPS) {
const webSrc = join(APPS_DIR, app, 'apps/web/src');
if (!existsSync(webSrc)) continue;
// img tags with and without alt
const imgWithAlt = grepOccurrences('<img[^>]*alt=', webSrc, '*.svelte');
const imgWithoutAlt = grepOccurrences('<img(?![^>]*alt=)', webSrc, '*.svelte');
totalImgWithAlt += imgWithAlt;
totalImgFiles += imgWithAlt + imgWithoutAlt;
if (grepCount('role="dialog"', webSrc, '*.svelte') > 0) appsWithDialogRole++;
if (grepCount('use:focusTrap', webSrc, '*.svelte') > 0) appsWithFocusTrap++;
}
// Score: alt text coverage + dialog/focusTrap presence
const altAdoption = totalImgFiles > 0 ? Math.round((totalImgWithAlt / totalImgFiles) * 100) : 100;
const dialogAdoption = Math.round((appsWithDialogRole / WEB_APPS.length) * 100);
const trapAdoption = Math.round((appsWithFocusTrap / WEB_APPS.length) * 100);
const adoption = Math.round((altAdoption + dialogAdoption + trapAdoption) / 3);
console.log(` img with alt: ${totalImgWithAlt}/${totalImgFiles} (${altAdoption}%)`);
console.log(
` Apps with role=dialog: ${appsWithDialogRole}/${WEB_APPS.length} (${dialogAdoption}%)`
);
console.log(` Apps with focusTrap: ${appsWithFocusTrap}/${WEB_APPS.length} (${trapAdoption}%)`);
console.log(` Combined: ${adoption}%\n`);
return { adoption, altAdoption, dialogAdoption, trapAdoption, totalImgFiles, totalImgWithAlt };
}
function measureAuthGuardCoverage() {
console.log('📊 Measuring Auth Guard Coverage...');
let appsWithAuthGuard = 0;
const missing = [];
for (const app of WEB_APPS) {
const webSrc = join(APPS_DIR, app, 'apps/web/src');
if (!existsSync(webSrc)) continue;
const hasAuthGuard =
grepCount('AuthGate', webSrc, '*.svelte') > 0 ||
grepCount('authGuard', webSrc, '*.ts') > 0 ||
grepCount('authGuard', webSrc, '*.server.ts') > 0 ||
grepCount('requireAuth', webSrc, '*.ts') > 0;
if (hasAuthGuard) appsWithAuthGuard++;
else missing.push(app);
}
const adoption = Math.round((appsWithAuthGuard / WEB_APPS.length) * 100);
console.log(` Apps with auth guard: ${appsWithAuthGuard}/${WEB_APPS.length} (${adoption}%)`);
if (missing.length > 0 && missing.length <= 10) console.log(` Missing: ${missing.join(', ')}`);
console.log('');
return { adoption, appsWithAuthGuard, missing };
}
function measureDockerReadiness() {
console.log('📊 Measuring Docker Readiness...');
let appsWithDockerfile = 0;
const missing = [];
for (const app of WEB_APPS) {
const appDir = join(APPS_DIR, app);
const hasDockerfile =
existsSync(join(appDir, 'apps/web/Dockerfile')) ||
existsSync(join(appDir, 'Dockerfile')) ||
fileCount('Dockerfile', appDir) > 0;
if (hasDockerfile) appsWithDockerfile++;
else missing.push(app);
}
const adoption = Math.round((appsWithDockerfile / WEB_APPS.length) * 100);
console.log(` Apps with Dockerfile: ${appsWithDockerfile}/${WEB_APPS.length} (${adoption}%)`);
if (missing.length > 0 && missing.length <= 10) console.log(` Missing: ${missing.join(', ')}`);
console.log('');
return { adoption, appsWithDockerfile, missing };
}
// ============================================================
// Main
// ============================================================
@ -781,6 +898,10 @@ const storePattern = measureStorePattern();
const sharedTypes = measureSharedTypeUsage();
const depFreshness = measureDependencyFreshness();
const bundleSize = measureBundleSize();
const gitActivity = measureGitActivity();
const a11y = measureA11yIndicators();
const authGuard = measureAuthGuardCoverage();
const docker = measureDockerReadiness();
// Calculate overall scores
const scores = {
@ -803,6 +924,10 @@ const scores = {
sharedTypes: sharedTypes.adoption,
depFreshness: depFreshness.adoption,
bundleConfig: bundleSize.adoption,
gitActivity: gitActivity.adoption,
a11yIndicators: a11y.adoption,
authGuardCoverage: authGuard.adoption,
dockerReadiness: docker.adoption,
};
// Weighted overall score
@ -826,6 +951,10 @@ const weights = {
sharedTypes: 3,
depFreshness: 2,
bundleConfig: 2,
gitActivity: 3,
a11yIndicators: 4,
authGuardCoverage: 5,
dockerReadiness: 3,
};
let totalWeight = 0;
@ -887,6 +1016,10 @@ const output = {
sharedTypes,
depFreshness,
bundleSize,
gitActivity,
a11y,
authGuard,
docker,
},
apps: WEB_APPS,
};