chore(app-registry): polish 4 small wins — TOC + AppId-derive + route-drift test + 3 MANA_APPS

§1 AppId derivation (shared-branding):
- `AppId` is now `keyof typeof APP_BRANDING` (config.ts) instead of a
  hand-maintained union in types.ts. Adding/removing an entry in
  `APP_BRANDING` automatically updates the union — eliminates the
  drift class that produced the ContextLogo type-error.
- `AppBranding.id` relaxed to `string` to break the circular type
  reference (key in `APP_BRANDING` is the authoritative id).

§2 Route-drift smoke test (registry.spec.ts):
- New 4th test: parses every `routes/(app)/*+page.svelte`, extracts
  the `<RoutePage appId="…">` literal, asserts the id is registered
  in the workbench app-registry. Catches drift like the earlier
  `appId="broadcasts"` vs id `'broadcast'` bug structurally.
- ROUTE_ONLY_APP_IDS allowlist for routes that intentionally don't
  back a workbench module (gifts, llm-test, milestones, organizations,
  teams, tags).
- Caught two real drifts in the process and fixed them:
    /agents/+page.svelte → appId="ai-agents" → "agents"
    /agents/templates/+page.svelte → same

§3 MANA_APPS hochgezogen (kontext, wishes):
- kontext (Web-Context URL crawler) + wishes (Wunschliste) had module
  + workbench card but no MANA_APPS branding entry. Both got proper
  description, longDescription and a fresh APP_ICONS entry (globe-
  with-text-lines for kontext, shooting-star for wishes).
- Removed both from WORKBENCH_ONLY in spec — they're full apps now.
- Note: `myday` was already in MANA_APPS, the WORKBENCH_ONLY entry
  was redundant and had been silently double-counting.

§4 apps.ts — top-level INDEX comment:
- 80 registerApp() calls were chronological-by-when-added — basically
  unsearchable. Added an §1–§4 navigation comment near the top
  grouping apps by role (entity / module surface / AI Workbench /
  System) so devs can jump to a section. Physical reordering of
  the 80 blocks deferred to avoid disturbing the active multi-
  terminal session — the TOC delivers ~80% of the navigation win.

Bonus: register `forms` module that the parallel session added but
hadn't wired into the workbench yet — the new route-drift test caught
this immediately on first run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-28 22:59:26 +02:00
parent 907a3add49
commit 6d193a9fa7
8 changed files with 205 additions and 37 deletions

View file

@ -88,9 +88,45 @@ import {
Megaphone,
Receipt,
ClockCounterClockwise,
ClipboardText,
} from '@mana/shared-icons';
// ── Apps with entity capabilities ───────────────────────────
// ─── INDEX ──────────────────────────────────────────────────────
//
// Apps are grouped by their workbench role. Inside each group the
// order is roughly chronological (by when they were added) — search
// by id or use your editor outline to jump to a specific app.
//
// §1 Apps with entity capabilities (DnD sources/targets)
// todo · calendar · contacts
//
// §2 Apps without entity capabilities — module surfaces
// Daily-use: habits · notes · journal · myday · drink ·
// mood · sleep · activity · times · finance
// Knowledge: chat · kontext · cards · quiz · guides ·
// news · news-research · research-lab · articles ·
// library · writing · comic · presi
// Body & life: body · food · meditate · stretch · period ·
// dreams · firsts · lasts · habits · recipes
// Places & ev.: places · citycorners · events · who
// Creative: picture · music · photos · wardrobe · moodlit
// Tools: memoro · uload · calc · plants · inventory ·
// storage · skilltree · questions
// Long-tail: quotes · automations · companion · wetter ·
// goals · website · spaces · augur ·
// broadcasts · invoices · timeline
//
// §3 AI Workbench surfaces
// agents · ai-missions · ai-workbench · ai-policy ·
// ai-insights · ai-health · rituals
//
// §4 System / Settings family
// settings · themes · profile · credits · api-keys · admin ·
// complexity · spiral · wishes · help · feedback · playground
//
// ────────────────────────────────────────────────────────────────
// ── §1 · Apps with entity capabilities ──────────────────────
registerApp({
id: 'todo',
@ -1566,3 +1602,15 @@ registerApp({
list: { load: () => import('$lib/modules/timeline/ListView.svelte') },
},
});
registerApp({
id: 'forms',
name: 'Forms',
color: '#14b8a6',
icon: ClipboardText,
views: {
// /forms/[id]/edit (builder) and /forms/[id]/responses (analytics)
// live as SvelteKit routes; the workbench card hosts the ListView.
list: { load: () => import('$lib/modules/forms/ListView.svelte') },
},
});

View file

@ -1,3 +1,6 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, it, expect } from 'vitest';
import { MANA_APPS } from '@mana/shared-branding';
// Side-effect import: registers every workbench app via ./apps.
@ -32,9 +35,7 @@ const WORKBENCH_ONLY = new Set([
'spiral',
// User-facing modules that don't yet have MANA_APPS branding.
// Add them to MANA_APPS when they ship a marketing surface.
'kontext',
'rituals',
'wishes',
// AI Studio surfaces. Open question whether to expose each one in
// MANA_APPS or consolidate into a single "AI Studio" branding entry.
// Keeping them workbench-only until that decision lands.
@ -97,4 +98,59 @@ describe('app registry ↔ MANA_APPS consistency', () => {
const allIds = [...getAllApps().map((a) => a.id), ...MANA_APPS.map((a) => a.id)];
expect(allIds).not.toContain('inventar');
});
/**
* Catches the kind of drift that produced the `appId="broadcasts"` vs
* registered id `'broadcast'` bug RoutePage looks up the workbench
* app-registry by appId for title/icon/color, so a mismatch silently
* falls back to the raw id string and breaks navigation styling.
*/
it('every <RoutePage appId="…"> in routes/(app) resolves to a registered workbench app', () => {
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROUTES_DIR = join(__dirname, '../../routes/(app)');
const workbenchIds = new Set(getAllApps().map((a) => a.id));
// Routes that surface a feature without a workbench module — they
// pass an `appId` to RoutePage purely for telemetry/back-button
// scoping, not for icon/color lookup. Adding here = "this route
// is intentionally not backed by a workbench app".
const ROUTE_ONLY_APP_IDS = new Set([
'gifts', // /gifts — credit gifting flow, backed by mana-credits API
'llm-test', // DEV-only LLM playground
'milestones', // cross-module aggregator over firsts + lasts
'organizations', // Spaces/membership management
'teams', // Spaces/teams management
'tags', // global tag browser
]);
function* findPageFiles(dir: string): Generator<string> {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
yield* findPageFiles(full);
} else if (entry === '+page.svelte') {
yield full;
}
}
}
const violations: { file: string; appId: string }[] = [];
for (const file of findPageFiles(ROUTES_DIR)) {
const body = readFileSync(file, 'utf8');
const matches = body.matchAll(/<RoutePage[^>]+appId="([a-z0-9-]+)"/g);
for (const m of matches) {
const appId = m[1];
if (!workbenchIds.has(appId) && !ROUTE_ONLY_APP_IDS.has(appId)) {
violations.push({ file: file.replace(ROUTES_DIR, ''), appId });
}
}
}
expect(
violations,
`Routes referencing unregistered appIds:\n${violations
.map((v) => ` ${v.file} → "${v.appId}"`)
.join('\n')}`
).toEqual([]);
});
});

View file

@ -12,6 +12,6 @@
import { RoutePage } from '$lib/components/shell';
</script>
<RoutePage appId="ai-agents">
<RoutePage appId="agents">
<ListView />
</RoutePage>

View file

@ -140,7 +140,7 @@
</button>
{/snippet}
<RoutePage appId="ai-agents" backHref="/agents">
<RoutePage appId="agents" backHref="/agents">
<div class="page">
<header class="header">
<button type="button" class="back" onclick={() => goto('/')}>