mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
chore(audit): module complexity reports + workbench map
Adds four audit scripts (module health, inter-module coupling, per-function cognitive complexity, D3 treemap) with generated reports under docs/ and an iframe-embedded workbench app at /admin/complexity. Reports regenerate weekly via the module-health GitHub Action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b857063120
commit
7c1c6cd54c
12 changed files with 1453 additions and 0 deletions
44
.github/workflows/module-health.yml
vendored
Normal file
44
.github/workflows/module-health.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: Module Health
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Mondays 06:00 UTC
|
||||
- cron: '0 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Run audits
|
||||
run: |
|
||||
node scripts/audit-modules.mjs
|
||||
node scripts/audit-module-coupling.mjs
|
||||
node scripts/audit-complexity.mjs || true
|
||||
node scripts/build-complexity-map.mjs
|
||||
|
||||
- name: Commit reports if changed
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
PATHS="docs/module-health.md docs/module-coupling.md docs/complexity-hotspots.md docs/complexity-map.html apps/mana/apps/web/static/admin/complexity-map.html"
|
||||
if git diff --quiet $PATHS 2>/dev/null; then
|
||||
echo "No changes to reports."
|
||||
exit 0
|
||||
fi
|
||||
git add $PATHS 2>/dev/null || true
|
||||
git commit -m "chore(audit): weekly module health report
|
||||
|
||||
Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>"
|
||||
git push
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<!--
|
||||
Complexity — workbench-embedded treemap of the codebase.
|
||||
Area = LOC, color = git churn. Generated by `pnpm audit:map`.
|
||||
Admin-only: hides itself for non-admin users.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { authStore } from '$lib/stores/auth.svelte';
|
||||
import { ShieldWarning } from '@mana/shared-icons';
|
||||
|
||||
let isAdmin = $derived(authStore.user?.role === 'admin');
|
||||
let loadError = $state(false);
|
||||
|
||||
function onError() {
|
||||
loadError = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !isAdmin}
|
||||
<div class="flex h-full flex-col items-center justify-center gap-3 p-8 text-center">
|
||||
<ShieldWarning size={40} class="text-muted-foreground" />
|
||||
<h3 class="text-base font-medium">Admin-only</h3>
|
||||
<p class="max-w-sm text-sm text-muted-foreground">
|
||||
Die Complexity Map ist ein internes Diagnose-Tool und nur für Admin-Nutzer sichtbar.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full flex-col gap-3 p-3">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold">Codebase Complexity Map</h2>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Area = LOC · color = git churn (6 months) · hover for details
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="/admin/complexity"
|
||||
class="text-xs px-3 py-1.5 rounded-md border bg-muted/40 hover:bg-muted transition-colors"
|
||||
title="Open standalone view"
|
||||
>
|
||||
Full page →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if loadError}
|
||||
<div
|
||||
class="flex flex-1 items-center justify-center rounded-md border border-amber-500/30 bg-amber-500/5 p-6 text-sm"
|
||||
>
|
||||
<div>
|
||||
<p class="font-medium text-amber-700 dark:text-amber-300">Map nicht generiert.</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Im Repo-Root <code class="rounded bg-muted px-1 py-0.5">pnpm audit:map</code> ausführen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 overflow-hidden rounded-md border bg-background">
|
||||
<iframe
|
||||
src="/admin/complexity-map.html"
|
||||
title="Complexity Map"
|
||||
class="h-full w-full border-0"
|
||||
onerror={onError}
|
||||
></iframe>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<script lang="ts">
|
||||
let iframeEl: HTMLIFrameElement | undefined = $state();
|
||||
let loaded = $state(false);
|
||||
let error = $state(false);
|
||||
|
||||
function onLoad() {
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
function onError() {
|
||||
error = true;
|
||||
}
|
||||
|
||||
function regenerateHint() {
|
||||
navigator.clipboard?.writeText('pnpm audit:map');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Complexity Map</h2>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Interactive treemap of the entire codebase. Area = lines of code, color = git change
|
||||
frequency (last 6 months).
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={regenerateHint}
|
||||
class="text-xs px-3 py-1.5 rounded-md border bg-muted/40 hover:bg-muted transition-colors"
|
||||
title="Copy regeneration command to clipboard"
|
||||
>
|
||||
Regenerate: <code>pnpm audit:map</code>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="rounded-md border border-amber-500/30 bg-amber-500/5 p-4 text-sm">
|
||||
<p class="font-medium text-amber-700 dark:text-amber-300">Map not yet generated.</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Run <code class="px-1 py-0.5 rounded bg-muted">pnpm audit:map</code> from the repo root. It
|
||||
writes to
|
||||
<code class="px-1 py-0.5 rounded bg-muted">static/admin/complexity-map.html</code>.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="rounded-lg border overflow-hidden bg-background"
|
||||
style="height: calc(100vh - 280px); min-height: 500px;"
|
||||
>
|
||||
<iframe
|
||||
bind:this={iframeEl}
|
||||
src="/admin/complexity-map.html"
|
||||
title="Complexity Map"
|
||||
class="w-full h-full border-0"
|
||||
onload={onLoad}
|
||||
onerror={onError}
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<details class="text-sm rounded-md border bg-muted/20 p-3">
|
||||
<summary class="cursor-pointer font-medium">Related reports</summary>
|
||||
<ul class="mt-2 space-y-1 text-muted-foreground">
|
||||
<li><code>docs/module-health.md</code> — per-module LOC × churn score</li>
|
||||
<li><code>docs/module-coupling.md</code> — inter-module imports (fan-in / fan-out)</li>
|
||||
<li><code>docs/complexity-hotspots.md</code> — top functions by cognitive complexity</li>
|
||||
</ul>
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
All reports regenerate automatically every Monday 06:00 UTC via the
|
||||
<code>module-health</code> GitHub Action.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
115
apps/mana/apps/web/static/admin/complexity-map.html
Normal file
115
apps/mana/apps/web/static/admin/complexity-map.html
Normal file
File diff suppressed because one or more lines are too long
94
docs/complexity-hotspots.md
Normal file
94
docs/complexity-hotspots.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Cognitive Complexity Hotspots
|
||||
|
||||
_Generated 2026-04-14 — heuristic scan (no ESLint deps)_
|
||||
|
||||
Complexity = sum of decision points per function (`if`, `for`, `while`, `case`, `catch`, ternary, `&&`, `||`, `??`). Threshold ≥ 10.
|
||||
|
||||
**84 functions** exceed threshold across the scanned tree. Showing top 84.
|
||||
|
||||
| # | Complexity | Lines | Function | File |
|
||||
|---:|---:|---:|---|---|
|
||||
| 1 | 77 | 217 | `createEventsRoutes` | `services/mana-events/src/routes/events.ts` |
|
||||
| 2 | 65 | 426 | `createTokenManager` | `packages/shared-auth/src/core/tokenManager.ts` |
|
||||
| 3 | 43 | 83 | `filteredTasks` | `apps/mana/apps/web/src/lib/modules/todo/components/pages/TodoPage.svelte` |
|
||||
| 4 | 42 | 38 | `createContact` | `apps/mana/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts` |
|
||||
| 5 | 42 | 226 | `createRsvpRoutes` | `services/mana-events/src/routes/rsvp.ts` |
|
||||
| 6 | 41 | 389 | `createAuthRoutes` | `services/mana-auth/src/routes/auth.ts` |
|
||||
| 7 | 38 | 258 | `createCreditService` | `packages/credits/src/createCreditService.ts` |
|
||||
| 8 | 35 | 36 | `updateContact` | `apps/mana/apps/web/src/lib/modules/contacts/stores/contacts.svelte.ts` |
|
||||
| 9 | 33 | 237 | `createPlayerStore` | `apps/mana/apps/web/src/lib/modules/music/stores/player.svelte.ts` |
|
||||
| 10 | 32 | 80 | `logDay` | `apps/mana/apps/web/src/lib/modules/period/stores/dayLogs.svelte.ts` |
|
||||
| 11 | 32 | 203 | `createDecksStore` | `apps/mana/apps/web/src/lib/modules/presi/stores/decks.svelte.ts` |
|
||||
| 12 | 27 | 200 | `useAiTierItems` | `apps/mana/apps/web/src/lib/components/layout/use-ai-tier-items.svelte.ts` |
|
||||
| 13 | 27 | 219 | `createEncryptionVaultRoutes` | `services/mana-auth/src/routes/encryption-vault.ts` |
|
||||
| 14 | 25 | 221 | `useTaskForm` | `apps/mana/apps/web/src/lib/modules/todo/composables/useTaskForm.svelte.ts` |
|
||||
| 15 | 23 | 39 | `onPhotoSelected` | `apps/mana/apps/web/src/lib/modules/food/ListView.svelte` |
|
||||
| 16 | 23 | 80 | `updateTask` | `apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts` |
|
||||
| 17 | 21 | 143 | `createSettingsRoutes` | `services/mana-auth/src/routes/settings.ts` |
|
||||
| 18 | 21 | 59 | `handleKeydown` | `packages/shared-ui/src/command-bar/CommandBar.svelte` |
|
||||
| 19 | 21 | 87 | `dropTarget` | `packages/shared-ui/src/dnd/drop-target.ts` |
|
||||
| 20 | 21 | 62 | `handleKeydown` | `packages/shared-ui/src/quick-input/InputBar.svelte` |
|
||||
| 21 | 20 | 43 | `publishEvent` | `apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts` |
|
||||
| 22 | 20 | 34 | `handleKeydown` | `apps/mana/apps/web/src/routes/(app)/+layout.svelte` |
|
||||
| 23 | 20 | 90 | `passiveDropZone` | `packages/shared-ui/src/dnd/passive-drop.ts` |
|
||||
| 24 | 19 | 29 | `syncSnapshotIfPublished` | `apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts` |
|
||||
| 25 | 18 | 49 | `updateDream` | `apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts` |
|
||||
| 26 | 18 | 103 | `uploadRoutes` | `services/mana-media/apps/api/src/routes/upload.ts` |
|
||||
| 27 | 17 | 44 | `flushCompleteEvents` | `apps/mana/apps/web/src/lib/data/sync.ts` |
|
||||
| 28 | 17 | 40 | `loadPhotos` | `apps/mana/apps/web/src/lib/modules/photos/stores/photos.svelte.ts` |
|
||||
| 29 | 17 | 80 | `logPosition` | `apps/mana/apps/web/src/lib/modules/places/stores/tracking.svelte.ts` |
|
||||
| 30 | 17 | 56 | `handleQuickSubmit` | `apps/mana/apps/web/src/lib/modules/times/components/EntryForm.svelte` |
|
||||
| 31 | 17 | 27 | `handleReanalyze` | `apps/mana/apps/web/src/routes/(app)/food/[id]/+page.svelte` |
|
||||
| 32 | 17 | 141 | `dragSource` | `packages/shared-ui/src/dnd/drag-source.ts` |
|
||||
| 33 | 16 | 26 | `updateAlarm` | `apps/mana/apps/web/src/lib/modules/times/stores/alarms.svelte.ts` |
|
||||
| 34 | 16 | 33 | `saveEdit` | `apps/mana/apps/web/src/routes/(app)/food/[id]/+page.svelte` |
|
||||
| 35 | 16 | 118 | `createGeocodeRoutes` | `services/mana-geocoding/src/routes/geocode.ts` |
|
||||
| 36 | 15 | 62 | `handleSend` | `apps/mana/apps/web/src/routes/(app)/playground/+page.svelte` |
|
||||
| 37 | 15 | 225 | `createBetterAuth` | `services/mana-auth/src/auth/better-auth.config.ts` |
|
||||
| 38 | 15 | 42 | `handleKeydown` | `packages/shared-ui/src/organisms/network/NetworkGraph.svelte` |
|
||||
| 39 | 14 | 21 | `saveField` | `apps/mana/apps/web/src/lib/modules/contacts/views/DetailView.svelte` |
|
||||
| 40 | 14 | 24 | `createAlarm` | `apps/mana/apps/web/src/lib/modules/times/stores/alarms.svelte.ts` |
|
||||
| 41 | 14 | 55 | `handleBenchmark` | `apps/mana/apps/web/src/routes/(app)/llm-test/+page.svelte` |
|
||||
| 42 | 14 | 198 | `createAdminRoutes` | `services/mana-auth/src/routes/admin.ts` |
|
||||
| 43 | 14 | 59 | `handleLogin` | `packages/shared-auth-ui/src/pages/LoginPage.svelte` |
|
||||
| 44 | 14 | 42 | `add` | `packages/shared-hono/src/auth.ts` |
|
||||
| 45 | 14 | 17 | `handleKeyDown` | `packages/shared-stores/src/keyboard-shortcuts.ts` |
|
||||
| 46 | 13 | 28 | `useFoodMealsSince` | `apps/mana/apps/web/src/lib/modules/body/queries.ts` |
|
||||
| 47 | 13 | 15 | `updateEvent` | `apps/mana/apps/web/src/lib/modules/events/stores/events.svelte.ts` |
|
||||
| 48 | 13 | 22 | `useAllFirsts` | `apps/mana/apps/web/src/lib/modules/firsts/queries.ts` |
|
||||
| 49 | 13 | 132 | `createMoodsStore` | `apps/mana/apps/web/src/lib/modules/moodlit/stores/moods.svelte.ts` |
|
||||
| 50 | 13 | 62 | `uploadAll` | `apps/mana/apps/web/src/lib/modules/picture/ListView.svelte` |
|
||||
| 51 | 13 | 26 | `handleKeydown` | `apps/mana/apps/web/src/routes/(app)/calc/standard/+page.svelte` |
|
||||
| 52 | 13 | 71 | `handleRegister` | `packages/shared-auth-ui/src/pages/RegisterPage.svelte` |
|
||||
| 53 | 13 | 35 | `handleKeydown` | `packages/shared-ui/src/molecules/FilterDropdown.svelte` |
|
||||
| 54 | 12 | 34 | `onSearch` | `apps/mana/apps/web/src/lib/modules/times/quick-input-adapter.ts` |
|
||||
| 55 | 12 | 84 | `createSplitStore` | `apps/mana/apps/web/src/lib/splitscreen/store.svelte.ts` |
|
||||
| 56 | 12 | 30 | `handleAnalyzePhoto` | `apps/mana/apps/web/src/routes/(app)/food/add/+page.svelte` |
|
||||
| 57 | 12 | 96 | `createFeedbackService` | `packages/feedback/src/createFeedbackService.ts` |
|
||||
| 58 | 11 | 22 | `onCreate` | `apps/mana/apps/web/src/lib/modules/calendar/quick-input-adapter.ts` |
|
||||
| 59 | 11 | 21 | `updatePlace` | `apps/mana/apps/web/src/lib/modules/places/stores/places.svelte.ts` |
|
||||
| 60 | 11 | 23 | `saveField` | `apps/mana/apps/web/src/lib/modules/plants/views/DetailView.svelte` |
|
||||
| 61 | 11 | 35 | `initFromTask` | `apps/mana/apps/web/src/lib/modules/todo/composables/useTaskForm.svelte.ts` |
|
||||
| 62 | 11 | 41 | `execute` | `apps/mana/apps/web/src/lib/modules/todo/tools.ts` |
|
||||
| 63 | 11 | 38 | `scored` | `apps/mana/apps/web/src/lib/modules/todo/utils/time-estimator.ts` |
|
||||
| 64 | 11 | 124 | `handleAuthReady` | `apps/mana/apps/web/src/routes/(app)/+layout.svelte` |
|
||||
| 65 | 11 | 35 | `claimItem` | `apps/mana/apps/web/src/routes/rsvp/[token]/+page.svelte` |
|
||||
| 66 | 11 | 28 | `handlePointerMove` | `packages/shared-ui/src/dnd/passive-drop.ts` |
|
||||
| 67 | 11 | 44 | `calculatePosition` | `packages/shared-ui/src/molecules/ConfirmationPopover.svelte` |
|
||||
| 68 | 11 | 80 | `results` | `packages/shared-ui/src/navigation/GlobalSpotlight.svelte` |
|
||||
| 69 | 11 | 74 | `accountLinks` | `packages/shared-ui/src/navigation/PillNavigation.svelte` |
|
||||
| 70 | 11 | 29 | `handleCreate` | `packages/shared-uload/src/ShareModal.svelte` |
|
||||
| 71 | 10 | 41 | `uploadFile` | `apps/mana/apps/web/src/lib/components/wallpaper/WallpaperPicker.svelte` |
|
||||
| 72 | 10 | 41 | `executeCurrentStep` | `apps/mana/apps/web/src/lib/modules/companion/components/RitualRunner.svelte` |
|
||||
| 73 | 10 | 20 | `onSearch` | `apps/mana/apps/web/src/lib/modules/contacts/quick-input-adapter.ts` |
|
||||
| 74 | 10 | 12 | `createDream` | `apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts` |
|
||||
| 75 | 10 | 27 | `mergeSymbols` | `apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts` |
|
||||
| 76 | 10 | 12 | `markAsLived` | `apps/mana/apps/web/src/lib/modules/firsts/stores/firsts.svelte.ts` |
|
||||
| 77 | 10 | 40 | `logHabit` | `apps/mana/apps/web/src/lib/modules/habits/stores/habits.svelte.ts` |
|
||||
| 78 | 10 | 64 | `setSchedule` | `apps/mana/apps/web/src/lib/modules/habits/stores/habits.svelte.ts` |
|
||||
| 79 | 10 | 27 | `handleTimerEnd` | `apps/mana/apps/web/src/lib/modules/stretch/components/SessionPlayer.svelte` |
|
||||
| 80 | 10 | 14 | `createTask` | `apps/mana/apps/web/src/lib/modules/todo/stores/tasks.svelte.ts` |
|
||||
| 81 | 10 | 27 | `handleSuggestFromText` | `apps/mana/apps/web/src/routes/(app)/food/add/+page.svelte` |
|
||||
| 82 | 10 | 85 | `redeemGift` | `services/mana-credits/src/services/gift-code.ts` |
|
||||
| 83 | 10 | 65 | `deliveryRoutes` | `services/mana-media/apps/api/src/routes/delivery.ts` |
|
||||
| 84 | 10 | 25 | `handlePointerMove` | `packages/shared-ui/src/dnd/drop-target.ts` |
|
||||
115
docs/complexity-map.html
Normal file
115
docs/complexity-map.html
Normal file
File diff suppressed because one or more lines are too long
136
docs/module-coupling.md
Normal file
136
docs/module-coupling.md
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
# Module Coupling Report
|
||||
|
||||
_Generated 2026-04-14_
|
||||
|
||||
- **fan-in** = how many other modules import from this module (high = shared / core)
|
||||
- **fan-out** = how many other modules this module imports from (high = tightly coupled / leaky)
|
||||
|
||||
Ideal: most modules have fan-in ≤ 2 and fan-out ≤ 2. Outliers are refactor candidates.
|
||||
|
||||
## Ranked by fan-in (shared modules)
|
||||
|
||||
| Module | fan-in | fan-out | Imported by |
|
||||
|---|---:|---:|---|
|
||||
| `calendar` | 1 | 1 | `todo` |
|
||||
| `period` | 1 | 0 | `core` |
|
||||
| `food` | 1 | 0 | `body` |
|
||||
| `todo` | 1 | 1 | `calendar` |
|
||||
| `admin` | 0 | 0 | — |
|
||||
| `api-keys` | 0 | 0 | — |
|
||||
| `automations` | 0 | 0 | — |
|
||||
| `body` | 0 | 1 | — |
|
||||
| `calc` | 0 | 0 | — |
|
||||
| `cards` | 0 | 0 | — |
|
||||
| `chat` | 0 | 0 | — |
|
||||
| `citycorners` | 0 | 0 | — |
|
||||
| `companion` | 0 | 0 | — |
|
||||
| `contacts` | 0 | 0 | — |
|
||||
| `context` | 0 | 0 | — |
|
||||
| `core` | 0 | 1 | — |
|
||||
| `dreams` | 0 | 0 | — |
|
||||
| `drink` | 0 | 0 | — |
|
||||
| `events` | 0 | 0 | — |
|
||||
| `activity` | 0 | 0 | — |
|
||||
| `feedback` | 0 | 0 | — |
|
||||
| `finance` | 0 | 0 | — |
|
||||
| `firsts` | 0 | 0 | — |
|
||||
| `goals` | 0 | 0 | — |
|
||||
| `guides` | 0 | 0 | — |
|
||||
| `habits` | 0 | 0 | — |
|
||||
| `help` | 0 | 0 | — |
|
||||
| `inventory` | 0 | 0 | — |
|
||||
| `journal` | 0 | 0 | — |
|
||||
| `mail` | 0 | 0 | — |
|
||||
| `meditate` | 0 | 0 | — |
|
||||
| `memoro` | 0 | 0 | — |
|
||||
| `mood` | 0 | 0 | — |
|
||||
| `moodlit` | 0 | 0 | — |
|
||||
| `music` | 0 | 0 | — |
|
||||
| `myday` | 0 | 0 | — |
|
||||
| `news` | 0 | 0 | — |
|
||||
| `notes` | 0 | 0 | — |
|
||||
| `photos` | 0 | 0 | — |
|
||||
| `picture` | 0 | 0 | — |
|
||||
| `places` | 0 | 0 | — |
|
||||
| `plants` | 0 | 0 | — |
|
||||
| `playground` | 0 | 0 | — |
|
||||
| `presi` | 0 | 0 | — |
|
||||
| `profile` | 0 | 0 | — |
|
||||
| `questions` | 0 | 0 | — |
|
||||
| `recipes` | 0 | 0 | — |
|
||||
| `settings` | 0 | 0 | — |
|
||||
| `skilltree` | 0 | 0 | — |
|
||||
| `sleep` | 0 | 0 | — |
|
||||
| `spiral` | 0 | 0 | — |
|
||||
| `storage` | 0 | 0 | — |
|
||||
| `stretch` | 0 | 0 | — |
|
||||
| `subscription` | 0 | 0 | — |
|
||||
| `themes` | 0 | 0 | — |
|
||||
| `times` | 0 | 0 | — |
|
||||
| `uload` | 0 | 0 | — |
|
||||
| `who` | 0 | 0 | — |
|
||||
| `zitare` | 0 | 0 | — |
|
||||
|
||||
## Ranked by fan-out (leaky modules)
|
||||
|
||||
| Module | fan-out | fan-in | Imports from |
|
||||
|---|---:|---:|---|
|
||||
| `body` | 1 | 0 | `food` |
|
||||
| `calendar` | 1 | 1 | `todo` |
|
||||
| `core` | 1 | 0 | `period` |
|
||||
| `todo` | 1 | 1 | `calendar` |
|
||||
| `admin` | 0 | 0 | — |
|
||||
| `api-keys` | 0 | 0 | — |
|
||||
| `automations` | 0 | 0 | — |
|
||||
| `calc` | 0 | 0 | — |
|
||||
| `cards` | 0 | 0 | — |
|
||||
| `chat` | 0 | 0 | — |
|
||||
| `citycorners` | 0 | 0 | — |
|
||||
| `companion` | 0 | 0 | — |
|
||||
| `contacts` | 0 | 0 | — |
|
||||
| `context` | 0 | 0 | — |
|
||||
| `period` | 0 | 1 | — |
|
||||
| `dreams` | 0 | 0 | — |
|
||||
| `drink` | 0 | 0 | — |
|
||||
| `events` | 0 | 0 | — |
|
||||
| `activity` | 0 | 0 | — |
|
||||
| `feedback` | 0 | 0 | — |
|
||||
| `finance` | 0 | 0 | — |
|
||||
| `firsts` | 0 | 0 | — |
|
||||
| `food` | 0 | 1 | — |
|
||||
| `goals` | 0 | 0 | — |
|
||||
| `guides` | 0 | 0 | — |
|
||||
| `habits` | 0 | 0 | — |
|
||||
| `help` | 0 | 0 | — |
|
||||
| `inventory` | 0 | 0 | — |
|
||||
| `journal` | 0 | 0 | — |
|
||||
| `mail` | 0 | 0 | — |
|
||||
| `meditate` | 0 | 0 | — |
|
||||
| `memoro` | 0 | 0 | — |
|
||||
| `mood` | 0 | 0 | — |
|
||||
| `moodlit` | 0 | 0 | — |
|
||||
| `music` | 0 | 0 | — |
|
||||
| `myday` | 0 | 0 | — |
|
||||
| `news` | 0 | 0 | — |
|
||||
| `notes` | 0 | 0 | — |
|
||||
| `photos` | 0 | 0 | — |
|
||||
| `picture` | 0 | 0 | — |
|
||||
| `places` | 0 | 0 | — |
|
||||
| `plants` | 0 | 0 | — |
|
||||
| `playground` | 0 | 0 | — |
|
||||
| `presi` | 0 | 0 | — |
|
||||
| `profile` | 0 | 0 | — |
|
||||
| `questions` | 0 | 0 | — |
|
||||
| `recipes` | 0 | 0 | — |
|
||||
| `settings` | 0 | 0 | — |
|
||||
| `skilltree` | 0 | 0 | — |
|
||||
| `sleep` | 0 | 0 | — |
|
||||
| `spiral` | 0 | 0 | — |
|
||||
| `storage` | 0 | 0 | — |
|
||||
| `stretch` | 0 | 0 | — |
|
||||
| `subscription` | 0 | 0 | — |
|
||||
| `themes` | 0 | 0 | — |
|
||||
| `times` | 0 | 0 | — |
|
||||
| `uload` | 0 | 0 | — |
|
||||
| `who` | 0 | 0 | — |
|
||||
| `zitare` | 0 | 0 | — |
|
||||
120
docs/module-health.md
Normal file
120
docs/module-health.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Module Health Report
|
||||
|
||||
_Generated 2026-04-14 — git window: 6.months_
|
||||
|
||||
**Score** = `LOC × log₂(changes + 2)`. High score = big *and* churny = refactor candidate.
|
||||
|
||||
**Totals:** web `103,053` · api `4,400` · services `35,660` LOC
|
||||
|
||||
## Frontend modules (`apps/mana/apps/web/src/lib/modules`)
|
||||
|
||||
| Module | LOC | Files | Largest file (LOC) | Changes (6mo) | Last changed | Score |
|
||||
|---|---:|---:|---|---:|---|---:|
|
||||
| `calendar` | 8,379 | 38 | `calendar/components/EventDetailModal.svelte` (657) | 26 | 19 hours ago | 40,281 |
|
||||
| `todo` | 6,817 | 56 | `todo/stores/tasks.svelte.ts` (472) | 27 | 3 hours ago | 33,117 |
|
||||
| `times` | 5,334 | 32 | `times/types.ts` (454) | 10 | 17 hours ago | 19,122 |
|
||||
| `body` | 4,337 | 22 | `body/stores/body.svelte.ts` (467) | 11 | 46 minutes ago | 16,049 |
|
||||
| `period` | 3,182 | 19 | `period/ListView.svelte` (780) | 17 | 17 hours ago | 13,517 |
|
||||
| `dreams` | 2,835 | 10 | `dreams/ListView.svelte` (998) | 19 | 17 hours ago | 12,452 |
|
||||
| `skilltree` | 3,178 | 20 | `skilltree/types.ts` (589) | 12 | 17 hours ago | 12,100 |
|
||||
| `contacts` | 2,590 | 17 | `contacts/components/pages/ContactPage.svelte` (564) | 19 | 46 minutes ago | 11,376 |
|
||||
| `habits` | 2,792 | 14 | `habits/ListView.svelte` (593) | 12 | 17 hours ago | 10,630 |
|
||||
| `events` | 2,899 | 19 | `events/views/DetailView.svelte` (555) | 10 | 17 hours ago | 10,393 |
|
||||
| `places` | 2,386 | 10 | `places/ListView.svelte` (1017) | 17 | 19 hours ago | 10,136 |
|
||||
| `moodlit` | 2,143 | 13 | `moodlit/components/mood/MoodFullscreen.svelte` (613) | 10 | 2 days ago | 7,683 |
|
||||
| `photos` | 2,147 | 17 | `photos/ListView.svelte` (430) | 9 | 46 minutes ago | 7,427 |
|
||||
| `stretch` | 4,685 | 13 | `stretch/ListView.svelte` (710) | 1 | 2 hours ago | 7,426 |
|
||||
| `memoro` | 1,352 | 12 | `memoro/views/DetailView.svelte` (320) | 23 | 70 minutes ago | 6,278 |
|
||||
| `zitare` | 1,667 | 16 | `zitare/views/DetailView.svelte` (249) | 11 | 2 days ago | 6,169 |
|
||||
| `guides` | 1,765 | 10 | `guides/views/DetailView.svelte` (583) | 9 | 46 minutes ago | 6,106 |
|
||||
| `music` | 1,600 | 13 | `music/ListView.svelte` (402) | 11 | 17 hours ago | 5,921 |
|
||||
| `news` | 1,909 | 16 | `news/ListView.svelte` (364) | 6 | 17 hours ago | 5,727 |
|
||||
| `plants` | 2,421 | 16 | `plants/views/DetailView.svelte` (744) | 3 | 46 minutes ago | 5,621 |
|
||||
| `chat` | 1,440 | 14 | `chat/views/DetailView.svelte` (273) | 12 | 17 hours ago | 5,483 |
|
||||
| `calc` | 1,872 | 16 | `calc/components/CasioSkin.svelte` (285) | 5 | 2 days ago | 5,255 |
|
||||
| `companion` | 1,460 | 9 | `companion/components/CompanionChat.svelte` (538) | 10 | 2 hours ago | 5,234 |
|
||||
| `core` | 1,414 | 13 | `core/widgets/NutritionProgressWidget.svelte` (177) | 9 | 46 minutes ago | 4,892 |
|
||||
| `inventory` | 2,076 | 20 | `inventory/queries.ts` (327) | 3 | 17 hours ago | 4,820 |
|
||||
| `sleep` | 2,303 | 11 | `sleep/ListView.svelte` (559) | 2 | 2 hours ago | 4,606 |
|
||||
| `questions` | 1,227 | 12 | `questions/stores/answers.svelte.ts` (271) | 10 | 17 hours ago | 4,399 |
|
||||
| `firsts` | 1,835 | 8 | `firsts/ListView.svelte` (1266) | 3 | 17 hours ago | 4,261 |
|
||||
| `picture` | 1,096 | 9 | `picture/ListView.svelte` (379) | 9 | 4 days ago | 3,792 |
|
||||
| `who` | 1,035 | 8 | `who/views/PlayView.svelte` (306) | 8 | 2 days ago | 3,438 |
|
||||
| `cards` | 993 | 13 | `cards/components/CreateDeckModal.svelte` (156) | 9 | 17 hours ago | 3,435 |
|
||||
| `notes` | 981 | 9 | `notes/ListView.svelte` (405) | 9 | 17 hours ago | 3,394 |
|
||||
| `journal` | 1,418 | 8 | `journal/ListView.svelte` (854) | 3 | 17 hours ago | 3,292 |
|
||||
| `mood` | 1,534 | 9 | `mood/ListView.svelte` (548) | 2 | 2 hours ago | 3,068 |
|
||||
| `drink` | 1,461 | 8 | `drink/ListView.svelte` (820) | 2 | 19 hours ago | 2,922 |
|
||||
| `presi` | 767 | 9 | `presi/stores/decks.svelte.ts` (233) | 10 | 4 days ago | 2,750 |
|
||||
| `storage` | 825 | 10 | `storage/stores/files.svelte.ts` (269) | 8 | 17 hours ago | 2,741 |
|
||||
| `uload` | 885 | 7 | `uload/queries.ts` (270) | 5 | 4 days ago | 2,485 |
|
||||
| `finance` | 905 | 8 | `finance/ListView.svelte` (417) | 4 | 17 hours ago | 2,339 |
|
||||
| `citycorners` | 787 | 10 | `citycorners/queries.ts` (175) | 5 | 4 days ago | 2,209 |
|
||||
| `recipes` | 1,352 | 8 | `recipes/ListView.svelte` (884) | 1 | 17 hours ago | 2,143 |
|
||||
| `meditate` | 2,068 | 15 | `meditate/components/SessionPlayer.svelte` (551) | 0 | 17 hours ago | 2,068 |
|
||||
| `automations` | 998 | 6 | `automations/ListView.svelte` (723) | 2 | 2 days ago | 1,996 |
|
||||
| `food` | 1,742 | 15 | `food/mutations.test.ts` (294) | 0 | 46 minutes ago | 1,742 |
|
||||
| `playground` | 715 | 9 | `playground/ListView.svelte` (155) | 2 | 4 days ago | 1,430 |
|
||||
| `context` | 447 | 7 | `context/queries.ts` (155) | 5 | 4 days ago | 1,255 |
|
||||
| `goals` | 556 | 2 | `goals/GoalEditor.svelte` (303) | 2 | 46 minutes ago | 1,112 |
|
||||
| `mail` | 1,038 | 9 | `mail/ListView.svelte` (575) | 0 | 20 hours ago | 1,038 |
|
||||
| `subscription` | 793 | 1 | `subscription/ListView.svelte` (793) | 0 | 2 hours ago | 793 |
|
||||
| `api-keys` | 686 | 1 | `api-keys/ListView.svelte` (686) | 0 | 2 hours ago | 686 |
|
||||
| `spiral` | 624 | 4 | `spiral/stores/mana-spiral.svelte.ts` (232) | 0 | 9 days ago | 624 |
|
||||
| `themes` | 280 | 1 | `themes/ListView.svelte` (280) | 1 | 78 minutes ago | 444 |
|
||||
| `activity` | 183 | 1 | `activity/ListView.svelte` (183) | 2 | 46 minutes ago | 366 |
|
||||
| `admin` | 265 | 1 | `admin/ListView.svelte` (265) | 0 | 2 hours ago | 265 |
|
||||
| `myday` | 231 | 1 | `myday/ListView.svelte` (231) | 0 | 18 hours ago | 231 |
|
||||
| `profile` | 181 | 1 | `profile/ListView.svelte` (181) | 0 | 2 hours ago | 181 |
|
||||
| `settings` | 101 | 1 | `settings/ListView.svelte` (101) | 0 | 2 hours ago | 101 |
|
||||
| `help` | 40 | 1 | `help/ListView.svelte` (40) | 0 | 2 hours ago | 40 |
|
||||
| `feedback` | 21 | 1 | `feedback/ListView.svelte` (21) | 0 | 2 hours ago | 21 |
|
||||
|
||||
## API modules (`apps/api/src/modules`)
|
||||
|
||||
| Module | LOC | Files | Largest file (LOC) | Changes (6mo) | Last changed | Score |
|
||||
|---|---:|---:|---|---:|---|---:|
|
||||
| `who` | 1,065 | 4 | `who/data/characters.ts` (490) | 3 | 4 days ago | 2,473 |
|
||||
| `research` | 827 | 4 | `research/orchestrator.ts` (389) | 2 | 2 days ago | 1,654 |
|
||||
| `traces` | 307 | 1 | `traces/routes.ts` (307) | 3 | 2 days ago | 713 |
|
||||
| `todo` | 301 | 1 | `todo/routes.ts` (301) | 3 | 2 days ago | 699 |
|
||||
| `presi` | 265 | 2 | `presi/routes.ts` (188) | 4 | 2 days ago | 685 |
|
||||
| `news` | 190 | 1 | `news/routes.ts` (190) | 3 | 2 days ago | 441 |
|
||||
| `picture` | 158 | 1 | `picture/routes.ts` (158) | 3 | 6 days ago | 367 |
|
||||
| `guides` | 219 | 1 | `guides/routes.ts` (219) | 1 | 6 days ago | 347 |
|
||||
| `storage` | 134 | 1 | `storage/routes.ts` (134) | 3 | 6 days ago | 311 |
|
||||
| `music` | 122 | 1 | `music/routes.ts` (122) | 3 | 6 days ago | 283 |
|
||||
| `chat` | 130 | 1 | `chat/routes.ts` (130) | 2 | 6 days ago | 260 |
|
||||
| `contacts` | 102 | 1 | `contacts/routes.ts` (102) | 3 | 6 days ago | 237 |
|
||||
| `food` | 222 | 1 | `food/routes.ts` (222) | 0 | 47 minutes ago | 222 |
|
||||
| `plants` | 118 | 1 | `plants/routes.ts` (118) | 1 | 47 minutes ago | 187 |
|
||||
| `context` | 87 | 1 | `context/routes.ts` (87) | 2 | 6 days ago | 174 |
|
||||
| `calendar` | 111 | 1 | `calendar/routes.ts` (111) | 0 | 12 days ago | 111 |
|
||||
| `moodlit` | 42 | 1 | `moodlit/routes.ts` (42) | 0 | 12 days ago | 42 |
|
||||
|
||||
## Services (`services/`)
|
||||
|
||||
| Module | LOC | Files | Largest file (LOC) | Changes (6mo) | Last changed | Score |
|
||||
|---|---:|---:|---|---:|---|---:|
|
||||
| `mana-auth` | 5,206 | 32 | `encryption-vault/index.ts` (607) | 45 | 47 minutes ago | 28,917 |
|
||||
| `mana-notify` | 3,139 | 22 | `services/mana-notify/internal/handler/notifications.go` (493) | 20 | 6 days ago | 13,998 |
|
||||
| `mana-sync` | 2,484 | 13 | `services/mana-sync/internal/sync/handler.go` (436) | 22 | 47 minutes ago | 11,389 |
|
||||
| `mana-tts` | 2,444 | 10 | `services/mana-tts/app/main.py` (678) | 12 | 6 days ago | 9,305 |
|
||||
| `mana-credits` | 2,140 | 23 | `sync-billing.ts` (357) | 15 | 2 hours ago | 8,747 |
|
||||
| `mana-stt` | 1,948 | 9 | `services/mana-stt/app/main.py` (393) | 20 | 6 days ago | 8,687 |
|
||||
| `mana-search` | 2,029 | 14 | `services/mana-search/internal/search/searxng.go` (305) | 16 | 4 days ago | 8,461 |
|
||||
| `mana-llm` | 2,314 | 22 | `services/mana-llm/src/providers/ollama.py` (349) | 9 | 5 days ago | 8,005 |
|
||||
| `mana-media` | 1,571 | 15 | `upload.ts` (393) | 24 | 47 minutes ago | 7,384 |
|
||||
| `mana-events` | 2,063 | 20 | `services/mana-events/src/__tests__/items.test.ts` (344) | 5 | 6 days ago | 5,792 |
|
||||
| `mana-api-gateway` | 1,381 | 12 | `services/mana-api-gateway/internal/service/apikeys.go` (257) | 11 | 7 days ago | 5,110 |
|
||||
| `mana-crawler` | 1,411 | 8 | `services/mana-crawler/internal/crawler/crawler.go` (365) | 8 | 7 days ago | 4,687 |
|
||||
| `mana-geocoding` | 900 | 10 | `services/mana-geocoding/src/routes/geocode.ts` (219) | 11 | 3 days ago | 3,330 |
|
||||
| `mana-subscriptions` | 830 | 15 | `subscriptions.ts` (223) | 5 | 2 days ago | 2,330 |
|
||||
| `mana-user` | 792 | 20 | `tags.ts` (211) | 4 | 6 days ago | 2,047 |
|
||||
| `news-ingester` | 876 | 10 | `services/news-ingester/src/sources.ts` (261) | 3 | 5 days ago | 2,034 |
|
||||
| `mana-image-gen` | 851 | 5 | `services/mana-image-gen/app/main.py` (365) | 3 | 6 days ago | 1,976 |
|
||||
| `mana-video-gen` | 685 | 3 | `services/mana-video-gen/app/main.py` (405) | 3 | 6 days ago | 1,591 |
|
||||
| `mana-analytics` | 470 | 12 | `feedback.ts` (127) | 5 | 6 days ago | 1,319 |
|
||||
| `mana-mail` | 1,267 | 20 | `jmap-client.ts` (323) | 0 | 20 hours ago | 1,267 |
|
||||
| `mana-voice-bot` | 507 | 2 | `services/mana-voice-bot/app/main.py` (505) | 2 | 6 days ago | 1,014 |
|
||||
| `mana-landing-builder` | 352 | 8 | `services/mana-landing-builder/src/builder/builder.service.ts` (225) | 5 | 7 days ago | 988 |
|
||||
175
scripts/audit-complexity.mjs
Normal file
175
scripts/audit-complexity.mjs
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
#!/usr/bin/env node
|
||||
// Lightweight per-function complexity audit. No deps.
|
||||
// Heuristic: counts decision points (if / else if / for / while / switch case / catch / ternary / && / ||) per function body.
|
||||
// Not as rigorous as SonarJS cognitive complexity, but finds the same outliers.
|
||||
// Output: docs/complexity-hotspots.md — top 50 functions.
|
||||
|
||||
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, relative, extname } from 'node:path';
|
||||
|
||||
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
||||
const SCAN_ROOTS = ['apps/mana/apps/web/src', 'apps/api/src', 'services', 'packages'];
|
||||
const CODE_EXT = new Set(['.ts', '.tsx', '.js', '.mjs', '.svelte']);
|
||||
const IGNORE = new Set(['node_modules', '.svelte-kit', 'dist', 'build', 'coverage', '.turbo']);
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (IGNORE.has(e.name)) continue;
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...walk(p));
|
||||
else if (e.isFile() && CODE_EXT.has(extname(e.name))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Strip /* */ and // comments and string contents to avoid false matches.
|
||||
function sanitize(src) {
|
||||
return src
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.replace(/\/\/[^\n]*/g, '')
|
||||
.replace(/`[^`\\]*(?:\\.[^`\\]*)*`/g, '``')
|
||||
.replace(/'[^'\\\n]*(?:\\.[^'\\\n]*)*'/g, "''")
|
||||
.replace(/"[^"\\\n]*(?:\\.[^"\\\n]*)*"/g, '""');
|
||||
}
|
||||
|
||||
// For .svelte: extract <script> block(s) for function scanning, but also scan whole file for inline event handlers.
|
||||
function extractJS(path, src) {
|
||||
if (extname(path) !== '.svelte') return src;
|
||||
const blocks = [];
|
||||
const re = /<script[^>]*>([\s\S]*?)<\/script>/g;
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) blocks.push(m[1]);
|
||||
return blocks.join('\n');
|
||||
}
|
||||
|
||||
// Find function starts and their body (best-effort brace matching).
|
||||
function findFunctions(src) {
|
||||
const out = [];
|
||||
// Simpler approach: regex-based function heads, then count decision tokens in next ~200 lines or until matching brace.
|
||||
const headRe =
|
||||
/(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][\w$]*)\s*\([^)]*\)\s*\{|(?:const|let)\s+([a-zA-Z_$][\w$]*)\s*[:=][^={\n]*?=>\s*\{|([a-zA-Z_$][\w$]*)\s*\([^)]*\)\s*\{(?=\s*(?:\/\/|\n|\/\*|[a-z]))/g;
|
||||
let m;
|
||||
while ((m = headRe.exec(src)) !== null) {
|
||||
const name = m[1] || m[2] || m[3];
|
||||
if (
|
||||
!name ||
|
||||
name === 'if' ||
|
||||
name === 'for' ||
|
||||
name === 'while' ||
|
||||
name === 'switch' ||
|
||||
name === 'catch' ||
|
||||
name === 'return'
|
||||
)
|
||||
continue;
|
||||
// Find matching closing brace
|
||||
let depth = 0;
|
||||
const start = src.indexOf('{', m.index);
|
||||
if (start < 0) continue;
|
||||
let end = start;
|
||||
for (let i = start; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (c === '{') depth++;
|
||||
else if (c === '}') {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (end <= start) continue;
|
||||
const body = src.slice(start, end + 1);
|
||||
const lines = body.split('\n').length;
|
||||
out.push({ name, body, lines, offset: start });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function complexity(body) {
|
||||
// Count decision points. Each adds 1.
|
||||
const counts = {
|
||||
if: (body.match(/\bif\s*\(/g) || []).length,
|
||||
elseIf: (body.match(/\belse\s+if\s*\(/g) || []).length, // already counted by `if`; don't double
|
||||
for: (body.match(/\bfor\s*\(/g) || []).length,
|
||||
while: (body.match(/\bwhile\s*\(/g) || []).length,
|
||||
case: (body.match(/\bcase\s+[^:]+:/g) || []).length,
|
||||
catch: (body.match(/\bcatch\s*\(/g) || []).length,
|
||||
ternary: (body.match(/\?[^?:]*:/g) || []).length,
|
||||
and: (body.match(/&&/g) || []).length,
|
||||
or: (body.match(/\|\|/g) || []).length,
|
||||
coalesce: (body.match(/\?\?/g) || []).length,
|
||||
};
|
||||
const total =
|
||||
counts.if +
|
||||
counts.for +
|
||||
counts.while +
|
||||
counts.case +
|
||||
counts.catch +
|
||||
counts.ternary +
|
||||
counts.and +
|
||||
counts.or +
|
||||
counts.coalesce;
|
||||
return total;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const r of SCAN_ROOTS) {
|
||||
const abs = join(ROOT, r);
|
||||
const files = walk(abs);
|
||||
for (const f of files) {
|
||||
let src;
|
||||
try {
|
||||
src = readFileSync(f, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
const js = sanitize(extractJS(f, src));
|
||||
if (!js.trim()) continue;
|
||||
const funcs = findFunctions(js);
|
||||
for (const fn of funcs) {
|
||||
const c = complexity(fn.body);
|
||||
if (c >= 10) {
|
||||
results.push({
|
||||
file: relative(ROOT, f),
|
||||
name: fn.name,
|
||||
complexity: c,
|
||||
lines: fn.lines,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results.sort((a, b) => b.complexity - a.complexity);
|
||||
const top = results.slice(0, 100);
|
||||
|
||||
const md = [
|
||||
'# Cognitive Complexity Hotspots',
|
||||
'',
|
||||
`_Generated ${new Date().toISOString().slice(0, 10)} — heuristic scan (no ESLint deps)_`,
|
||||
'',
|
||||
'Complexity = sum of decision points per function (`if`, `for`, `while`, `case`, `catch`, ternary, `&&`, `||`, `??`). Threshold ≥ 10.',
|
||||
'',
|
||||
`**${results.length} functions** exceed threshold across the scanned tree. Showing top ${top.length}.`,
|
||||
'',
|
||||
'| # | Complexity | Lines | Function | File |',
|
||||
'|---:|---:|---:|---|---|',
|
||||
...top.map(
|
||||
(r, i) => `| ${i + 1} | ${r.complexity} | ${r.lines} | \`${r.name}\` | \`${r.file}\` |`
|
||||
),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const outDir = join(ROOT, 'docs');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = join(outDir, 'complexity-hotspots.md');
|
||||
writeFileSync(outPath, md);
|
||||
console.log(`Wrote ${relative(ROOT, outPath)} — ${results.length} hotspots (≥10)`);
|
||||
109
scripts/audit-module-coupling.mjs
Normal file
109
scripts/audit-module-coupling.mjs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env node
|
||||
// Cross-module coupling audit. For each frontend module, count how many OTHER
|
||||
// modules import from it (fan-in) and how many other modules it imports (fan-out).
|
||||
// Writes docs/module-coupling.md.
|
||||
|
||||
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, relative, extname } from 'node:path';
|
||||
|
||||
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
||||
const MODULES_ROOT = join(ROOT, 'apps/mana/apps/web/src/lib/modules');
|
||||
const CODE_EXT = new Set(['.ts', '.svelte']);
|
||||
const IGNORE_DIRS = new Set(['node_modules', '.svelte-kit', 'dist']);
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (IGNORE_DIRS.has(e.name)) continue;
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...walk(p));
|
||||
else if (e.isFile() && CODE_EXT.has(extname(e.name))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const modules = readdirSync(MODULES_ROOT, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name);
|
||||
|
||||
// Build: module -> set of files
|
||||
const filesByModule = new Map();
|
||||
for (const m of modules) {
|
||||
filesByModule.set(m, walk(join(MODULES_ROOT, m)));
|
||||
}
|
||||
|
||||
// For each file, scan imports; detect cross-module imports (paths containing `/modules/<other>/`)
|
||||
const importRe = /(?:from\s+['"]|import\(['"])([^'"]+)['"]/g;
|
||||
|
||||
const fanIn = Object.fromEntries(modules.map((m) => [m, new Set()])); // who imports me
|
||||
const fanOut = Object.fromEntries(modules.map((m) => [m, new Set()])); // who do I import
|
||||
|
||||
for (const [mod, files] of filesByModule.entries()) {
|
||||
for (const f of files) {
|
||||
let src;
|
||||
try {
|
||||
src = readFileSync(f, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
let m;
|
||||
importRe.lastIndex = 0;
|
||||
while ((m = importRe.exec(src)) !== null) {
|
||||
const spec = m[1];
|
||||
const match = spec.match(/modules\/([a-z0-9_-]+)\//i);
|
||||
if (!match) continue;
|
||||
const other = match[1];
|
||||
if (other === mod || !modules.includes(other)) continue;
|
||||
fanOut[mod].add(other);
|
||||
fanIn[other].add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows = modules.map((m) => ({
|
||||
module: m,
|
||||
fanIn: fanIn[m].size,
|
||||
fanOut: fanOut[m].size,
|
||||
inList: [...fanIn[m]].sort(),
|
||||
outList: [...fanOut[m]].sort(),
|
||||
}));
|
||||
|
||||
const md = [
|
||||
'# Module Coupling Report',
|
||||
'',
|
||||
`_Generated ${new Date().toISOString().slice(0, 10)}_`,
|
||||
'',
|
||||
'- **fan-in** = how many other modules import from this module (high = shared / core)',
|
||||
'- **fan-out** = how many other modules this module imports from (high = tightly coupled / leaky)',
|
||||
'',
|
||||
'Ideal: most modules have fan-in ≤ 2 and fan-out ≤ 2. Outliers are refactor candidates.',
|
||||
'',
|
||||
'## Ranked by fan-in (shared modules)',
|
||||
'',
|
||||
'| Module | fan-in | fan-out | Imported by |',
|
||||
'|---|---:|---:|---|',
|
||||
...[...rows]
|
||||
.sort((a, b) => b.fanIn - a.fanIn)
|
||||
.map(
|
||||
(r) =>
|
||||
`| \`${r.module}\` | ${r.fanIn} | ${r.fanOut} | ${r.inList.map((x) => `\`${x}\``).join(', ') || '—'} |`
|
||||
),
|
||||
'',
|
||||
'## Ranked by fan-out (leaky modules)',
|
||||
'',
|
||||
'| Module | fan-out | fan-in | Imports from |',
|
||||
'|---|---:|---:|---|',
|
||||
...[...rows]
|
||||
.sort((a, b) => b.fanOut - a.fanOut)
|
||||
.map(
|
||||
(r) =>
|
||||
`| \`${r.module}\` | ${r.fanOut} | ${r.fanIn} | ${r.outList.map((x) => `\`${x}\``).join(', ') || '—'} |`
|
||||
),
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const outDir = join(ROOT, 'docs');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = join(outDir, 'module-coupling.md');
|
||||
writeFileSync(outPath, md);
|
||||
console.log(`Wrote ${relative(ROOT, outPath)} — ${rows.length} modules`);
|
||||
178
scripts/audit-modules.mjs
Normal file
178
scripts/audit-modules.mjs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env node
|
||||
// Module complexity audit. Writes docs/module-health.md.
|
||||
// Usage: node scripts/audit-modules.mjs [--since=6.months]
|
||||
|
||||
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, relative, extname } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
||||
const SINCE = (process.argv.find((a) => a.startsWith('--since=')) || '--since=6.months').split(
|
||||
'='
|
||||
)[1];
|
||||
|
||||
const CODE_EXT = new Set(['.ts', '.tsx', '.js', '.mjs', '.svelte', '.go', '.py']);
|
||||
const IGNORE_DIRS = new Set([
|
||||
'node_modules',
|
||||
'.turbo',
|
||||
'.svelte-kit',
|
||||
'dist',
|
||||
'build',
|
||||
'.next',
|
||||
'coverage',
|
||||
'__snapshots__',
|
||||
]);
|
||||
|
||||
const TARGETS = [
|
||||
{ label: 'web', root: 'apps/mana/apps/web/src/lib/modules' },
|
||||
{ label: 'api', root: 'apps/api/src/modules' },
|
||||
{ label: 'service', root: 'services' },
|
||||
];
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (IGNORE_DIRS.has(e.name)) continue;
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...walk(p));
|
||||
else if (e.isFile() && CODE_EXT.has(extname(e.name))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function countLines(path) {
|
||||
try {
|
||||
return readFileSync(path, 'utf8').split('\n').length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function gitChangeCount(path) {
|
||||
try {
|
||||
const out = execSync(
|
||||
`git log --since=${SINCE} --pretty=format:%H -- "${path}" 2>/dev/null | wc -l`,
|
||||
{ cwd: ROOT }
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
return Number(out) || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function gitLastChanged(path) {
|
||||
try {
|
||||
const out = execSync(`git log -1 --format=%ar -- "${path}" 2>/dev/null`, { cwd: ROOT })
|
||||
.toString()
|
||||
.trim();
|
||||
return out || '—';
|
||||
} catch {
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
|
||||
function auditModule(absPath, label) {
|
||||
const files = walk(absPath);
|
||||
if (files.length === 0) return null;
|
||||
let loc = 0;
|
||||
let maxFile = { path: '', loc: 0 };
|
||||
for (const f of files) {
|
||||
const l = countLines(f);
|
||||
loc += l;
|
||||
if (l > maxFile.loc) maxFile = { path: relative(ROOT, f), loc: l };
|
||||
}
|
||||
const changes = gitChangeCount(relative(ROOT, absPath));
|
||||
const lastChanged = gitLastChanged(relative(ROOT, absPath));
|
||||
// score: LOC * log(changes+1) — hotspot heuristic
|
||||
const score = Math.round(loc * Math.log2(changes + 2));
|
||||
return {
|
||||
label,
|
||||
name: absPath.split('/').pop(),
|
||||
loc,
|
||||
files: files.length,
|
||||
maxFile: maxFile.path.replace(/^.*\/modules\//, '').replace(/^.*\/services\//, ''),
|
||||
maxFileLoc: maxFile.loc,
|
||||
changes,
|
||||
lastChanged,
|
||||
score,
|
||||
};
|
||||
}
|
||||
|
||||
function collect() {
|
||||
const rows = [];
|
||||
for (const t of TARGETS) {
|
||||
const rootAbs = join(ROOT, t.root);
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(rootAbs, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
if (IGNORE_DIRS.has(e.name)) continue;
|
||||
const r = auditModule(join(rootAbs, e.name), t.label);
|
||||
if (r) rows.push(r);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function fmt(n) {
|
||||
return n.toLocaleString('en-US');
|
||||
}
|
||||
|
||||
function renderMarkdown(rows) {
|
||||
const byLabel = (l) => rows.filter((r) => r.label === l);
|
||||
const section = (title, list) => {
|
||||
const sorted = [...list].sort((a, b) => b.score - a.score);
|
||||
const lines = [
|
||||
`## ${title}`,
|
||||
'',
|
||||
'| Module | LOC | Files | Largest file (LOC) | Changes (6mo) | Last changed | Score |',
|
||||
'|---|---:|---:|---|---:|---|---:|',
|
||||
...sorted.map(
|
||||
(r) =>
|
||||
`| \`${r.name}\` | ${fmt(r.loc)} | ${r.files} | \`${r.maxFile}\` (${r.maxFileLoc}) | ${r.changes} | ${r.lastChanged} | ${fmt(r.score)} |`
|
||||
),
|
||||
'',
|
||||
];
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const totals = {
|
||||
web: byLabel('web').reduce((s, r) => s + r.loc, 0),
|
||||
api: byLabel('api').reduce((s, r) => s + r.loc, 0),
|
||||
service: byLabel('service').reduce((s, r) => s + r.loc, 0),
|
||||
};
|
||||
|
||||
return [
|
||||
'# Module Health Report',
|
||||
'',
|
||||
`_Generated ${new Date().toISOString().slice(0, 10)} — git window: ${SINCE}_`,
|
||||
'',
|
||||
'**Score** = `LOC × log₂(changes + 2)`. High score = big *and* churny = refactor candidate.',
|
||||
'',
|
||||
`**Totals:** web \`${fmt(totals.web)}\` · api \`${fmt(totals.api)}\` · services \`${fmt(totals.service)}\` LOC`,
|
||||
'',
|
||||
section('Frontend modules (`apps/mana/apps/web/src/lib/modules`)', byLabel('web')),
|
||||
section('API modules (`apps/api/src/modules`)', byLabel('api')),
|
||||
section('Services (`services/`)', byLabel('service')),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const rows = collect();
|
||||
const md = renderMarkdown(rows);
|
||||
const outDir = join(ROOT, 'docs');
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const outPath = join(outDir, 'module-health.md');
|
||||
writeFileSync(outPath, md);
|
||||
console.log(`Wrote ${relative(ROOT, outPath)} — ${rows.length} modules`);
|
||||
226
scripts/build-complexity-map.mjs
Normal file
226
scripts/build-complexity-map.mjs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env node
|
||||
// Generates docs/complexity-map.html — interactive D3 treemap.
|
||||
// Area = LOC per file. Color = git change frequency (last 6 months).
|
||||
// Groups: frontend modules, API modules, services.
|
||||
|
||||
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import { join, relative, extname } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const ROOT = new URL('..', import.meta.url).pathname.replace(/\/$/, '');
|
||||
const SINCE = '6.months';
|
||||
const CODE_EXT = new Set(['.ts', '.tsx', '.js', '.mjs', '.svelte', '.go', '.py']);
|
||||
const IGNORE = new Set([
|
||||
'node_modules',
|
||||
'.turbo',
|
||||
'.svelte-kit',
|
||||
'dist',
|
||||
'build',
|
||||
'.next',
|
||||
'coverage',
|
||||
'__snapshots__',
|
||||
]);
|
||||
|
||||
const TARGETS = [
|
||||
{ label: 'web', root: 'apps/mana/apps/web/src/lib/modules' },
|
||||
{ label: 'api', root: 'apps/api/src/modules' },
|
||||
{ label: 'services', root: 'services' },
|
||||
];
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return out;
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (IGNORE.has(e.name)) continue;
|
||||
const p = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...walk(p));
|
||||
else if (e.isFile() && CODE_EXT.has(extname(e.name))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function loc(path) {
|
||||
try {
|
||||
return readFileSync(path, 'utf8').split('\n').length;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch git log across many files: one call per module (fast enough).
|
||||
function changeCountForFile(relPath) {
|
||||
try {
|
||||
const out = execSync(`git log --since=${SINCE} --pretty=format:%H -- "${relPath}" | wc -l`, {
|
||||
cwd: ROOT,
|
||||
})
|
||||
.toString()
|
||||
.trim();
|
||||
return Number(out) || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const tree = { name: 'mana', children: [] };
|
||||
|
||||
for (const t of TARGETS) {
|
||||
const group = { name: t.label, children: [] };
|
||||
const rootAbs = join(ROOT, t.root);
|
||||
let modules;
|
||||
try {
|
||||
modules = readdirSync(rootAbs, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const m of modules) {
|
||||
if (!m.isDirectory() || IGNORE.has(m.name)) continue;
|
||||
const modAbs = join(rootAbs, m.name);
|
||||
const files = walk(modAbs);
|
||||
if (files.length === 0) continue;
|
||||
const modNode = { name: m.name, children: [] };
|
||||
for (const f of files) {
|
||||
const l = loc(f);
|
||||
if (l === 0) continue;
|
||||
const rel = relative(ROOT, f);
|
||||
modNode.children.push({
|
||||
name: f.split('/').slice(-2).join('/'),
|
||||
path: rel,
|
||||
value: l,
|
||||
changes: changeCountForFile(rel),
|
||||
});
|
||||
}
|
||||
if (modNode.children.length > 0) group.children.push(modNode);
|
||||
}
|
||||
tree.children.push(group);
|
||||
}
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Mana — Complexity Map</title>
|
||||
<style>
|
||||
:root { color-scheme: dark; }
|
||||
html, body { margin: 0; padding: 0; background: #0b0d10; color: #e8e8e8; font: 13px/1.4 system-ui, sans-serif; }
|
||||
header { padding: 12px 16px; border-bottom: 1px solid #1f2329; display: flex; gap: 16px; align-items: center; flex-wrap: wrap; }
|
||||
header h1 { margin: 0; font-size: 14px; font-weight: 600; }
|
||||
header .meta { color: #888; font-size: 12px; }
|
||||
header label { color: #aaa; font-size: 12px; }
|
||||
header select { background: #1a1d22; color: #e8e8e8; border: 1px solid #2a2f36; padding: 4px 8px; border-radius: 4px; }
|
||||
#chart { position: fixed; inset: 48px 0 0 0; }
|
||||
.cell { stroke: #0b0d10; stroke-width: 1; cursor: pointer; }
|
||||
.cell:hover { stroke: #fff; stroke-width: 2; }
|
||||
.label { fill: #fff; font-size: 11px; pointer-events: none; font-weight: 500; text-shadow: 0 1px 2px rgba(0,0,0,0.8); }
|
||||
.tip { position: fixed; background: #1a1d22; border: 1px solid #2a2f36; padding: 8px 10px; border-radius: 6px; font-size: 12px; pointer-events: none; display: none; max-width: 360px; z-index: 10; }
|
||||
.tip b { color: #fff; } .tip .k { color: #888; }
|
||||
.legend { display: flex; gap: 8px; align-items: center; }
|
||||
.legend .bar { width: 160px; height: 10px; border-radius: 3px; background: linear-gradient(to right, #1e3a5f, #2b6cb0, #d97706, #dc2626); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Mana Complexity Map</h1>
|
||||
<span class="meta">Area = LOC · Color = git changes (last ${SINCE})</span>
|
||||
<label>Group:
|
||||
<select id="group">
|
||||
<option value="all">all</option>
|
||||
<option value="web">web</option>
|
||||
<option value="api">api</option>
|
||||
<option value="services">services</option>
|
||||
</select>
|
||||
</label>
|
||||
<span class="legend"><span class="k" style="color:#888">cold</span><span class="bar"></span><span class="k" style="color:#888">hot</span></span>
|
||||
<span class="meta" id="stats"></span>
|
||||
</header>
|
||||
<div id="chart"></div>
|
||||
<div class="tip" id="tip"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
||||
<script>
|
||||
const DATA = ${JSON.stringify(tree)};
|
||||
|
||||
const tip = document.getElementById('tip');
|
||||
const sel = document.getElementById('group');
|
||||
const stats = document.getElementById('stats');
|
||||
|
||||
function filtered(group) {
|
||||
if (group === 'all') return DATA;
|
||||
return { name: 'mana', children: DATA.children.filter(c => c.name === group) };
|
||||
}
|
||||
|
||||
function maxChanges(root) {
|
||||
let max = 0;
|
||||
root.each(d => { if (d.data.changes && d.data.changes > max) max = d.data.changes; });
|
||||
return max || 1;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const group = sel.value;
|
||||
const container = document.getElementById('chart');
|
||||
container.innerHTML = '';
|
||||
const w = container.clientWidth, h = container.clientHeight;
|
||||
const root = d3.hierarchy(filtered(group)).sum(d => d.value || 0).sort((a,b) => b.value - a.value);
|
||||
d3.treemap().size([w, h]).paddingInner(1).paddingTop(d => d.depth === 1 ? 18 : d.depth === 2 ? 14 : 1).round(true)(root);
|
||||
|
||||
const max = maxChanges(root);
|
||||
const color = d3.scaleSequential([0, Math.log2(max + 1)], d3.interpolateInferno);
|
||||
|
||||
const totalLOC = root.value;
|
||||
const fileCount = root.leaves().length;
|
||||
stats.textContent = \`\${fileCount} files · \${totalLOC.toLocaleString()} LOC\`;
|
||||
|
||||
const svg = d3.select(container).append('svg').attr('width', w).attr('height', h);
|
||||
|
||||
// group labels (depth 1 and 2)
|
||||
svg.selectAll('g.group').data(root.descendants().filter(d => d.depth > 0 && d.depth < 3))
|
||||
.join('g').attr('class', 'group')
|
||||
.each(function(d) {
|
||||
const g = d3.select(this);
|
||||
g.append('rect').attr('x', d.x0).attr('y', d.y0).attr('width', d.x1-d.x0).attr('height', d.depth === 1 ? 18 : 14)
|
||||
.attr('fill', d.depth === 1 ? '#141820' : '#1a1f27');
|
||||
g.append('text').attr('x', d.x0 + 6).attr('y', d.y0 + (d.depth === 1 ? 13 : 10))
|
||||
.attr('class', 'label').attr('font-weight', d.depth === 1 ? 700 : 500)
|
||||
.text(\`\${d.data.name} (\${d.value.toLocaleString()})\`);
|
||||
});
|
||||
|
||||
svg.selectAll('rect.cell').data(root.leaves()).join('rect')
|
||||
.attr('class', 'cell')
|
||||
.attr('x', d => d.x0).attr('y', d => d.y0)
|
||||
.attr('width', d => Math.max(0, d.x1-d.x0)).attr('height', d => Math.max(0, d.y1-d.y0))
|
||||
.attr('fill', d => color(Math.log2((d.data.changes || 0) + 1)))
|
||||
.on('mousemove', (e, d) => {
|
||||
tip.style.display = 'block';
|
||||
tip.style.left = Math.min(e.clientX + 12, window.innerWidth - 370) + 'px';
|
||||
tip.style.top = (e.clientY + 12) + 'px';
|
||||
tip.innerHTML = \`<b>\${d.data.path}</b><br>
|
||||
<span class="k">LOC:</span> \${d.data.value.toLocaleString()}<br>
|
||||
<span class="k">Changes (\${'${SINCE}'}):</span> \${d.data.changes || 0}\`;
|
||||
})
|
||||
.on('mouseleave', () => { tip.style.display = 'none'; });
|
||||
|
||||
svg.selectAll('text.leaf').data(root.leaves().filter(d => (d.x1-d.x0) > 60 && (d.y1-d.y0) > 18))
|
||||
.join('text').attr('class', 'label leaf')
|
||||
.attr('x', d => d.x0 + 4).attr('y', d => d.y0 + 14)
|
||||
.text(d => d.data.name.split('/').pop());
|
||||
}
|
||||
|
||||
sel.addEventListener('change', render);
|
||||
window.addEventListener('resize', render);
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const outputs = [
|
||||
join(ROOT, 'docs', 'complexity-map.html'),
|
||||
join(ROOT, 'apps/mana/apps/web/static/admin/complexity-map.html'),
|
||||
];
|
||||
for (const p of outputs) {
|
||||
mkdirSync(join(p, '..'), { recursive: true });
|
||||
writeFileSync(p, html);
|
||||
console.log(`Wrote ${relative(ROOT, p)}`);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue