The reward chip added in eecf64c1c (community/feedback +5 chip) carries
the brand-term "Mana Credits" as a hardcoded German label. Brand terms
that are deliberately untranslated still count against the baseline, so
the +1 violation in onboarding/wish/+page.svelte was blocking
validate:all.
Bumping the baseline (1111 → 1112) to unblock deploys. Real fix is to
either route the chip through $_() with a brand-noun key or carve a
formal exception list — neither is in scope for this change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches header, action button, error banner, active/revoked sections
with pluralized counts, empty state, key list rows (rate badge,
created/last-used metadata, revoke button), how-to section,
create/success modal incl. all form labels and rate-limit hint.
Locale-aware Date via get(locale).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches form labels, event type options (now reactive via $derived),
source/comparison/period selectors, action buttons. Locale JSONs
landed in the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches all toast/error messages, page header, action buttons, tabs,
received/created sections, create form, info card. Locale-aware
Date/number formatting via get(locale). Locale JSONs landed in the
previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patches ListView, AssessmentWizard, ReminderManager, RoutineCreator,
SessionHistory, SessionPlayer, plus the /stretch route page title.
Locale JSONs landed in 421663ba3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds wardrobe namespace (de/en/es/fr/it) covering ListView,
GridView, OutfitsView, DetailGarmentView, DetailOutfitView,
GarmentForm, OutfitComposer, GarmentTryOnButton, TryOnButton,
TryOnModelPicker, CategoryTabs, GarmentCard, OutfitCard, plus
the /wardrobe/compose route. Categories/occasions/seasons routed
through dynamic `wardrobe.categories.{key}` lookups so constants.ts
keeps the order-tuples without leaking DE labels into UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ContextOverview ("Freundebuch" profile cards) was the single biggest
hardcoded-string hot-spot at 35 strings — every user sees this on their
profile. Extended `profile.context.*` namespace with section titles,
field labels (routine/social/leisure), placeholders, weekday short
names, and empty-state hints across DE/EN/ES/FR/IT.
Bonus: ratchet i18n-hardcoded baseline from 1879 → 1817 (settings
namespace + ContextOverview together cleared 62 violations).
- validate:i18n-parity: 39 namespaces × 5 locales — 3381 keys aligned
- svelte-check: 7647 files, 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final milestone of docs/plans/llm-fallback-aliases.md. Every backend
caller now requests models via the `mana/<class>` alias system instead
of hardcoded `ollama/...` strings. mana-llm resolves aliases through
`services/mana-llm/aliases.yaml` with health-aware fallback (M3) and
emits resolved-model + fallback metrics (M4).
SSOT moved to `packages/shared-ai/src/llm-aliases.ts` so apps/api,
apps/mana/apps/web, and services/mana-ai all import the same
`MANA_LLM` constant via the existing `@mana/shared-ai` workspace
dependency. Three additional sites (memoro-server, mana-events,
mana-research) inline the alias string with a SSOT comment because
they don't pull @mana/shared-ai today.
Migrated 14 sites across 10 files:
- apps/api: writing(LONG_FORM), comic(STRUCTURED), context(FAST_TEXT),
food(VISION), plants(VISION), research orchestrator (3 tiers
collapsed to STRUCTURED+FAST_TEXT/LONG_FORM)
- apps/mana/apps/web: voice/parse-task + parse-habit (STRUCTURED)
- services/mana-ai: planner llm-client + tick.ts (REASONING)
- services/mana-events: website-extractor (STRUCTURED, inlined)
- services/mana-research: mana-llm client (FAST_TEXT, inlined)
- apps/memoro/apps/server: ai.ts (FAST_TEXT, inlined)
Legacy env-vars removed: WRITING_MODEL, COMIC_STORYBOARD_MODEL,
VISION_MODEL, MANA_LLM_DEFAULT_MODEL. The chain in aliases.yaml is
now the single tuning surface; SIGHUP reloads it without redeploys.
New `scripts/validate-llm-strings.mjs` regex-scans 2538 files for
hardcoded `<provider>/<model>` strings and fails the build if any
land outside the SSOT or the explicitly-allowed paths (image-gen
modules, model-inspector code, this validator itself, the registry).
Wired into `validate:all` next to the i18n + theme validators.
Verified: `pnpm validate:llm-strings` clean, `pnpm --filter @mana/api
type-check` clean, `pnpm --filter @mana/ai-service type-check`
clean. Web type-check has 2 pre-existing errors in
SettingsSidebar.svelte (i18n MessageFormatter type drift, last
touched in 988c17a67 — unrelated to this work).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two DetailView polish items.
Print / PDF (fixes M10's "Drucken / PDF" action):
- New <article class="print-target"> at the top of the route renders
just the title + current version content. Hidden on screen, only
visible under @media print so window.print() produces a clean
manuscript instead of dumping the whole workbench chrome.
- :global(body > *) toggle suppresses the surrounding SvelteKit /
workbench frame; the .shell + the per-card chrome are explicitly
display:none in print. @page margin: 2cm gives a readable page
with no further user setup.
- Body uses ui-serif so the printed prose looks like manuscript.
Keyboard shortcuts (DetailView document-level listener):
- ⌘G / Ctrl+G → generate / re-generate (was: only the button)
- ⌘⇧S / Ctrl+Shift+S → save checkpoint
- ⌘Z / Ctrl+Z → undo last refinement (only fires when refineUndo
is set; otherwise falls through to the textarea's
native undo as the user expects)
Buttons + the undo row carry the shortcut in their title attribute so
mouse-users discover them via tooltip.
i18n baseline +1 for DetailView (the new "(⌘Z)" tooltip suffix counts
as one additional German fragment per the validator's heuristic).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three more reference kinds the resolver previously stubbed out are now
fully wired through the briefing form into the LLM prompt.
- reference-resolver: three new resolveX functions.
* Kontext is a singleton per space (the picker uses a sentinel
targetId; the resolver ignores it and reads via scopedForModule
+ first non-deleted row). Decrypts content and trims to budget.
* Goal reads from companionGoals (plaintext today) and surfaces
title + description + status + current/target so the model can
tie the draft into the user's actual progress.
* MeImage reads from the space-scoped meImages table; encrypts
label + tags. Hands the model a textual descriptor (kind / label /
tags) since the binary blob can't help prose generation.
- ReferencePicker: three new kind-tabs (🗂 Kontext, 🎯 Ziel, 🖼 Bild).
Kontext renders as a single-click "Kontext-Dokument verknüpfen" entry
if the space has one (with /kontext deep-link otherwise). Goals
active-first, then archived/done. Me-images render with thumbnail +
label + tags. Live-resolved chips via labelFor() for all three.
- i18n baseline bumped by one for ReferencePicker (the new
"Kontext-Dokument verknüpfen" string is intentional, in line with
the rest of the picker's existing German labels).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires Calendar through the M8.1+M8.2 backbone: flipping an event to
'unlisted' now publishes a server-side snapshot, the visible link in
the DetailView/EventDetailModal opens a real /share/[token] page, and
recipients can download an .ics file for their own calendar.
Changes:
- lib/data/unlisted/resolvers.ts (new):
buildUnlistedBlob(collection, recordId) dispatcher.
buildEventBlob: load LocalEvent + linked TimeBlock, decrypt
client-side, return { title, location, startTime, endTime,
isAllDay, timezone }. Description, reminders, tagIds, calendarId,
color stay out of the blob — sensitive context the user didn't
consent to share by flipping a single flag.
- modules/calendar/types: CalendarEvent gains `unlistedToken: string`
(empty string when no active token). timeBlockToCalendarEvent
forwards from LocalEvent. Draft-event scaffold initializes empty.
- modules/calendar/stores/events:
setVisibility now coordinates with mana-api. Flip-to-unlisted:
build blob -> publishUnlistedSnapshot -> store server-issued
token in patch.unlistedToken -> commit local update. If the
server call fails, no local change happens (no drift).
Flip-from-unlisted: revoke server snapshot first, then clear
local token + commit visibility change.
deleteEvent: revoke active unlisted snapshot before tombstoning,
so the share-link dies in lock-step with the local delete.
updateEvent + updateSingleInstance fire-and-forget
refreshUnlistedSnapshot(id) so the published blob tracks any
whitelist-field edits. Failures log; the next successful
refresh heals.
New regenerateUnlistedToken(id): revoke + republish in one call,
returns the fresh token. Powers the "Neu erzeugen" UI.
- routes/share/[token]/+layout.svelte: minimal anonymous chrome —
no app nav, no auth, no Dexie. Light/dark via prefers-color-scheme.
Footer carries "Geteilt via Mana" + signup CTA.
- routes/share/[token]/+page.server.ts: SSR loader. Fetches
/api/v1/unlisted/public/:token, dispatches 404/410 cleanly,
sets Cache-Control: private, max-age=60 + X-Robots-Tag: noindex.
- routes/share/[token]/+page.svelte: dispatcher; renders
SharedEventView for collection='events', stub message otherwise.
- modules/calendar/SharedEventView.svelte: standalone public render —
big date, location, "Zum eigenen Kalender hinzufügen" .ics link,
optional expiry note. OG/Twitter meta tags for WhatsApp/Slack
preview embedding. Uses $derived everywhere so prop updates
propagate through reactive recompute.
- routes/share/[token]/ical/+server.ts: RFC 5545 builder. No npm
library — small enough to inline. Escapes per spec, CRLF endings,
DTSTART/DTEND swap between VALUE=DATE and UTC depending on isAllDay.
Wrong-collection requests get 400.
- modules/calendar/views/DetailView (Workbench) + components/
EventDetailModal (/calendar route): SharedLinkControls dropped in
below the visibility row when event.visibility === 'unlisted'
AND event.unlistedToken AND shareUrl computed. The URL is built
client-side via buildShareUrl(window.location.origin, token) so it
stays in sync with whichever host the editor is open on.
Verified:
- pnpm check (web): 7541 files, 0 errors, 0 warnings
- pnpm test calendar + website: 26/26
- typecheck of new resolver, store hooks, SSR loader, iCal builder
Manual test path:
1. Open /calendar event in Detail view, flip Sichtbarkeit -> "Per Link"
2. Server publishes snapshot, Dexie record gets the server token
3. SharedLinkControls appear with copy + regenerate + revoke buttons
4. Open the URL in incognito → SSR fetches snapshot, renders
SharedEventView with date / location / .ics download
5. Edit the event title back in the main app → snapshot auto-refreshes
(refreshUnlistedSnapshot fires after updateEvent succeeds)
6. Flip back to "Bereich" → snapshot revoked server-side; subsequent
incognito reloads return 410 Gone
Next: M8.4 — same wiring for Library + Places. Uses the same
infra (resolvers dispatcher, share dispatcher) — just adds two new
buildXBlob functions, two SharedXView components, and the store
hooks.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stoppt das Wachsen des 1877-String-Backlogs hardgecodeter deutscher
User-facing Strings in .svelte Files. Per-file Count vs. committed
Baseline; Datei darf NIE über ihrer Baseline liegen, neue Files müssen
mit 0 Verstößen starten.
- Erkennt: placeholder/title/aria-label/label/alt mit Umlauten,
Text-Content `>Großbuchstabe…<` (ohne Interpolation).
- Aktuelle Baseline: 1877 Verstöße in 428 Files; jeder Fix ratchet't
den erlaubten Wert nach unten.
- Lokales Update nach gewolltem Wachstum: `pnpm run validate:i18n-hardcoded -- --update`.
- In validate:all + CI verdrahtet.
- Drift-Test bestätigt: ein zusätzlicher umlaut-Placeholder lässt die
Datei "2 (was 1, +1)" failen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Validator im Stil von validate:theme-parity. Scannt
apps/mana/apps/web/src/lib/i18n/locales/<namespace>/<locale>.json
und failt hart, sobald ein Locale-File vom kanonischen DE-Key-Set
abweicht (fehlende oder überzählige Keys).
- DE ist canonical weil fallbackLocale='de' in i18n/index.ts. Missing
keys führten zu mixed-language UI, extra keys sind tote Altlasten.
- In validate:all verdrahtet — CI failt ab sofort bei neuem Drift.
- Smoke-Test: 35 namespaces × 5 locales, 2724 canonical keys clean.
- Failure-Test bestätigt: künstlicher extra-key in apps/it.json führt
zu exit 1 mit klarer Fehlermeldung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Google's Gemini image edit family (Nano Banana) as a user-
selectable model for Wardrobe Try-On next to the existing OpenAI
path. Three concrete choices now expose themselves in the Solo and
Outfit Try-On buttons:
- openai/gpt-image-2 (default, falls back to gpt-image-1
server-side when the org isn't
verified)
- google/gemini-3-pro-image-preview (Nano Banana Pro — premium
identity / character consistency)
- google/gemini-3.1-flash-image-preview (Nano Banana 2 — newest,
fast, cheapest)
All three accept multi-image refs (face + body + garment) through
the same /api/v1/picture/generate-with-reference endpoint; the only
differences are the provider-specific request/response shape and
the model-id routing.
Server (apps/api/src/modules/picture/routes.ts):
- Guard now accepts `openai/*` and `google/*` prefixes and rejects
everything else as "not supported for edits". Each provider's key
is validated separately so missing GEMINI_API_KEY doesn't break
OpenAI calls and vice versa.
- New `callGeminiEdits(modelName)` helper mirrors the shape of
callOpenAiEdits: encodes the normalized PNG refs as base64
inline_data parts, POSTs to
generativelanguage.googleapis.com/v1beta/models/{model}:generateContent
with responseModalities=["TEXT","IMAGE"] and imageConfig
(aspectRatio + imageSize), pulls the generated image out of
candidates[].content.parts[].inlineData.
- Our internal size strings map cleanly: 1024x1024 → 1:1 / 1K,
1024x1536 → 2:3 / 1K, 1536x1024 → 3:2 / 1K. Gemini 1K is enough
for the thumbnail sizes Wardrobe renders; going higher bloats
payload without visible gain.
- creditsFor() gains a google/ branch proportional to upstream
pricing (pro ≈ 18, 3.1-flash ≈ 6, 2.5-flash ≈ 5).
- Response `model` reports `${provider}/${modelUsed}` so the picture
row's model metadata is accurate across providers.
Client (apps/mana/apps/web/src/lib/modules/wardrobe):
- api/try-on.ts: export `TryOnModel` union + `DEFAULT_TRY_ON_MODEL`.
RunGarmentTryOnParams / RunOutfitTryOnParams gain an optional
`model` field, threaded through `callGenerateWithReference`.
- components/TryOnModelPicker.svelte: new segmented control, three
options with label + one-line hint. Grid-auto-fits so it reflows
on the narrow workbench card.
- components/GarmentTryOnButton.svelte + TryOnButton.svelte: both
mount the picker above the Sparkle CTA. `estimatedCredits` on the
button label updates live when the user switches model so the
cost signal matches what the server will actually charge.
Env (scripts/generate-env.mjs): GEMINI_API_KEY and GOOGLE_API_KEY
now propagate from the root `.env.development` into `apps/api/.env`
so mana-api can pick them up at boot. The route reads GEMINI_API_KEY
with GOOGLE_API_KEY as fallback, matching how mana-llm ships today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
push_schema used to print "Failed (may not have db:push script)" for
every non-zero exit, lumping real failures (stuck rename prompts,
pre-existing public enums) in with missing scripts. Now it prints the
real exit code and tails the last 5 lines of drizzle-kit output so the
root cause is visible without re-running by hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two loose ends from M3/M4:
1. Tool_use_id-based error attribution in the persona-runner
-----------------------------------------------------------
The previous collectActionsFromMessage() flipped the *most recent*
ActionRow to 'error' when a tool_result carried is_error:true. That was
fine as long as Claude invoked tools strictly in sequence, but when
the planner pipelines multiple tools in one turn, a later tool_result
carries an earlier tool_use_id — the last-action fallback mis-
attributes the error.
runMainTurn() now keeps a tool_use_id → action-index Map for the
duration of the tick. On tool_use we stash block.id, on tool_result we
look up the exact ActionRow via tool_use_id and flip that one. The
"flip last" path survives as a pure fallback if a future SDK ever
ships a block without an id.
2. New audit:encrypted-tools script
-----------------------------------
scripts/audit-encrypted-tools.ts — loads registerAllModules() and
apps/mana/…/crypto/registry.ts, diffs every ToolSpec.encryptedFields
against the authoritative web-app ENCRYPTION_REGISTRY.
Catches three classes of drift:
- missing-table : tool declares a table the web-app doesn't encrypt
- field-drift : both agree a table is encrypted but the field lists
differ (half-encryption in the wire is silent death)
- disabled : web-app has enabled:false while the tool still
encrypts — advisory warning, not a fail
Negative-tested by injecting a deliberate drift on todo.create +
todo.list (shortened ENCRYPTED_FIELDS to ['title']); the auditor
flagged both tools with full field diffs, restore returned to green.
Wired into `pnpm run validate:all` so the contract survives future
edits on either side. Fills the M4 audit gap noted in
project_mana_mcp_personas.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Continuation of docs/plans/mana-mcp-and-personas.md. Personas are the
auto-test users the M3 runner will drive — they're real Mana users
(kind='persona', tier='founder'), registered through the same Better
Auth pipeline as humans, just stamped differently and metadata-tracked
so the persona-runner knows how to role-play them.
Schemas (auth namespace — personas are 1:1 with users, no reason for a
separate platform.* schema that the plan originally sketched)
- userKindEnum ('human' | 'persona' | 'system') + users.kind column,
wired into better-auth additionalFields so the JWT/user object carry
the flag. Default 'human' keeps every existing user untouched.
- auth.personas — 1:1 descriptor (archetype, systemPrompt, moduleMix
jsonb, tickCadence, lastActiveAt). CASCADE from users.id.
- auth.persona_actions — tick-grouped audit of every tool call the
runner makes (toolName, inputHash for dedup, result, latency).
- auth.persona_feedback — structured 1-5 ratings per module per tick,
plus free-text notes. This is where the runner writes the
self-reflection step at end of each tick.
Admin endpoints (/api/v1/admin/personas, admin-tier-gated)
- POST / create-or-update by email. Uses auth.api.signUpEmail
if the user's new, then stamps kind+tier+verified
and upserts the personas row. Idempotent — safe to
re-run after catalog edits.
- GET / list with 7-day action count per persona.
- GET /:id detail + recent 20 actions + per-module feedback
aggregate.
- DELETE /:id hard delete. Refuses non-persona users as
defense-in-depth: an admin typo here would cascade
through the full user-delete chain.
Catalog + seed pipeline (scripts/personas/)
- catalog.json 10 handwritten personas spanning 7 archetypes
(adhd-student, ceo-busy, creative-parent, solo-dev,
researcher, freelancer, overwhelmed-newbie).
Five pairs of personas that will later share
family/team spaces (cross-space setup is deferred
to M2.d per the plan).
- catalog.ts zod-validated loader. Refines email to require
@mana.test TLD — non-existent, no bounce risk.
- password.ts deterministic HMAC-SHA256(PERSONA_SEED_SECRET,
email). No stored per-persona credentials; the
runner re-derives on every login. Refuses the
dev-fallback secret in production.
- seed.ts POST /admin/personas per catalog entry. Flags:
--auth=, --jwt=, --dry-run.
- cleanup.ts Hard-delete every live persona. Warns when the
live set drifts from the catalog.
Root package.json:
pnpm seed:personas
pnpm seed:personas:cleanup
Extends the ESLint root-ignore list with `scripts/**` so Bun-typed
utility scripts don't fail the typed-parser check they weren't opted
into. Consistent with the rest of scripts/ being .mjs+.sh.
To go live (user action):
pnpm docker:up
cd services/mana-auth && bun run db:push
export MANA_ADMIN_JWT=...
pnpm seed:personas
M2.d deferred: cross-space (family/team/practice) memberships between
persona pairs. Better Auth's org-invite flow is multi-step and would
roughly double the M2 scope; the persona-runner (M3) can operate in
personal spaces first, shared-space tests land as their own milestone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pieces of the same cleanup:
1. build-app.sh now passes `--env-file .env.macmini` explicitly via a
shared COMPOSE_ARGS array. Without it, docker compose silently fell
back to `.env` in the project root — a separate file that happened
to hold MANA_AUTH_KEK and other secrets that `.env.macmini` lacked.
deploy.sh, restart.sh, and the CD workflow already used the flag;
this aligns build-app.sh with the rest. Server-side .env.macmini
was reconciled 2026-04-23 with the union of both files, so the
duplicate `.env` is no longer needed.
2. .env.macmini.example now documents 7 keys the prod stack actually
depends on but that had never been listed: GOOGLE_GEMINI_API_KEY /
GOOGLE_GENAI_API_KEY (SDK aliases for Deep-Research + mana-ai),
MANA_AI_PRIVATE_KEY_PEM / MANA_AI_PUBLIC_KEY_PEM (Mission-Grant
keypair), MANA_AI_DEEP_RESEARCH_ENABLED + PUBLIC_AI_MISSION_GRANTS
(feature flags), MANA_CORE_SERVICE_KEY (legacy alias), and the STT/
TTS internal shared secrets.
Matrix-bot tokens deliberately left undocumented — no Matrix homeserver
in the current running stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third provider path to /api/v1/picture/generate that calls OpenAI
gpt-image-2 when model starts with "openai/". Supports n=1..4 batch
generation with character continuity, base64 response decoded server-side
and uploaded to mana-media for dedup + thumbnails. Credit cost scales
by quality (low=3, medium=10, high=25) × n.
Env plumbing:
- scripts/generate-env.mjs: new apps/api/.env stanza propagates
OPENAI_API_KEY + REPLICATE_API_TOKEN from .env.secrets
- .env.macmini.example: documents OPENAI_API_KEY for prod
Frontend /picture/generate: model + quality + aspect-ratio + batch-count
selectors, real fetch with auth, persists each image via imagesStore.insert
(encrypted + synced). Wrapped in ModuleShell variant=fill with back-arrow
to /picture and a live credit badge in the header actions slot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/(app)/invoices/[id] route bundle drops from **534 KB → 18.6 KB** by
moving PDF rendering behind dynamic imports.
Changes:
- views/DetailView.svelte: `await import('../pdf/renderer')` inside
renderPdf() + downloadPdf(), cached in a module-local ref.
- components/SendModal.svelte: same for openAndDownload().
- pdf/scor.ts (new): generateSCORReference extracted so the
invoices store can derive a reference string without pulling
swissqrbill/svg + pdf-lib into the list-view bundle.
- pdf/qr-bill.ts: re-exports generateSCORReference from scor.ts
for backward compatibility.
- stores/invoices.svelte.ts: imports from ../pdf/scor (light) instead
of ../pdf/qr-bill (heavy).
- index.ts: drop re-export of the PDF renderer from the module
barrel so `import ... from '$lib/modules/invoices'` never drags
pdf-lib in.
The heavy chunk (pdf-lib + swissqrbill, ~576 KB) now only loads when
a user actually opens an invoice detail — list views, create flow, and
all other routes stay lean.
20/20 qr-bill tests pass; svelte-check clean.
Bonus: scripts/audit-icon-usage.mjs (+ pnpm run audit:icon-usage)
audits @mana/shared-icons imports. Reveals 204 distinct icons across
the codebase, 199 of them at default weight but paying for all 6
Phosphor weights. Biggest offender: app-registry/apps.ts with 69
static icon imports accounting for ~290 KB of the shared 466 KB icon
chunk. Migration path for that is documented in
docs/optimizable/bundle-analysis.md §2 — next session's work.
docs/optimizable/bundle-analysis.md also updated with the root (app)
layout (260 KB) investigation notes (start/stop lifecycle hooks to
defer via idleCallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#6 test coverage (pivot to reporting): 34/653 tests currently fail
(in-flight spaces-foundation migrations). Hard coverage thresholds
aren't enforceable until the suite is green, so this session ships a
file-presence audit instead of line-coverage gates.
- scripts/audit-test-coverage.mjs — counts .svelte + .ts source files
vs .test.ts + .spec.ts per module. Reports total ratio, lists
modules with 0 tests + ≥3 source files (prioritised by size).
- pnpm run audit:test-coverage wires it into audit:*.
- docs/optimizable/test-health.md — state + prevention path + top
untested modules ranked by impact.
Current baseline: 2.6% file-level coverage. 66/78 modules have zero
tests. Biggest untested: times (32 src), articles (29), events (27),
inventory + skilltree (20 each).
#8 audit:all: single entry point for the reporting audits. Runs
port-drift + i18n-coverage + test-coverage in --summary mode. Distinct
from validate:all (which is gates, not reports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>