# Prismedia Repository Contract ## Commit & Changelog Policy - Commit after every logical set of changes. - Keep commits small, reviewable, and intentionally scoped. - With every release-note-worthy change, update `CHANGELOG.md` under `## [Unreleased]`. - Keep changelog entries user-facing and high level. Internal cleanup belongs in git history, not the changelog. - Use Keep a Changelog sections: `What's New`, `Added`, `Changed`, `Fixed`, `Removed`, and `Docs`. - The root `package.json` version is the build version and the single source of truth. All workspace package versions must match it exactly. - Do not change the build version while publishing a channel image. Move the version forward when a new body of work starts, then let dev, alpha, beta, and release images publish that decided version. - The first release-note-worthy commit in a new body of work MUST include the appropriate SemVer bump in the root and every workspace package manifest. Do not commit user-facing work against the previous body's version. - Before choosing that bump, inspect every commit since the last manifest-version change and use the highest SemVer impact in the accumulated unreleased body: breaking change = MAJOR, feature = MINOR, fix-only = PATCH. - Later commits in the same unreleased body keep the already-decided version; versions advance once per body of work, not once per commit. The version-bump commit message or body must state the SemVer reason. - Before handing off any release-note-worthy commit, run `pnpm release:check` and report the resulting version. - Suggested commit style: - `chore: bootstrap workspace` - `docs: define repo contract` - `feat(web): add media library shell` - `feat(api): add health and jobs routes` - `fix(worker): stabilize queue startup` ## Versioning - Prismedia starts at `1.0.0`. - Versions are plain `X.Y.Z`; do not use development suffixes. - MAJOR: breaking API changes, schema changes that require user action, or config format changes. - MINOR: new features, new API endpoints, or new UI views. - PATCH: bug fixes, UI tweaks, dependency updates, and documentation. - Docker builds run `pnpm release:check`, which verifies changelog structure and workspace package version alignment. ## Release Channels - Every push to `main` builds only `ghcr.io/pauljoda/prismedia:dev`. - Validation, documentation-site publishing, and channel image publishing are manual workflows. - Manual channel publishing is handled by `.github/workflows/publish-channel.yml`. - The manual workflow accepts `alpha`, `beta`, or `release`. - `alpha` publishes `alpha`, `alpha-`, and `alpha--`. - `beta` publishes `beta`, `beta-`, and `beta--`. - `release` publishes `release`, `release-`, `release--`, and `latest`. - Publishing a channel image never edits package versions, rewrites changelog headings, creates git tags, or commits release artifacts. ## Product Prismedia is a private media library for self-hosted collections. It is video-first, but images, galleries, books, comics, audio, performers, studios, tags, and collections are first-class library entities. It is optimized for a single trusted user or household on a private LAN and ships as a Docker image. Stash compatibility exists for plugin and metadata workflows. Prismedia should not be framed as a replacement for another app, and third-party schemas should not define Prismedia's persistence model. ## Project Structure ```text apps/web-svelte/ Svelte frontend only. Built as static assets and served by the .NET API. apps/backend/ .NET API, application/domain/infrastructure layers, EF Core persistence, and .NET worker. packages/ui-svelte/ Shared Svelte design tokens and UI primitives. infra/docker/ Dockerfiles and dev compose stack. scripts/release/ Version and changelog validation tooling. docs/ Architecture and design language docs. ``` ## Architecture - Monorepo with `pnpm` workspaces and `turbo`. - Runtime processes: .NET API/HTTP ingress, static Svelte frontend assets, and the .NET worker. - PostgreSQL 16 is the sole stateful dependency for application data and queue/job state. - Public HTTP contracts live in the .NET backend and are consumed by the generated Svelte client under `apps/web-svelte/src/lib/api/generated`. - Backend work must follow `docs/backend-architecture-contract.md`: Clean Architecture, DDD-lite domain behavior, CQRS-lite use cases, EF Core as infrastructure persistence, DTO API boundaries, and generated frontend clients. ## Key Decisions 1. .NET API + static Svelte UI: the .NET API owns endpoints, persistence, and server orchestration. Svelte is a frontend client only. 2. PostgreSQL + EF Core: typed schema and EF migrations are managed from `apps/backend/src/Prismedia.Infrastructure/Persistence`. 3. .NET background worker: scan, probe, thumbnail, sprite, HLS, and metadata work runs in `apps/backend/src/Prismedia.Worker`. 4. HLS streaming: videos are transcoded to HLS on demand via ffmpeg and served by the .NET API. 5. Typed contracts: .NET contracts are the server source of truth. The frontend should prefer generated OpenAPI types. 6. No global EntityGraph: relationship links are EF persistence structures for bounded domain slices and read projections. ## Database - Schema is defined by EF Core entity mappings in `apps/backend/src/Prismedia.Infrastructure/Persistence`. - Core entities: media entities, performers, studios, tags, fingerprints, library roots, settings, sources, files, and job runs. - EF migration files live under `apps/backend/src/Prismedia.Infrastructure/Persistence/Migrations` and are applied by the .NET runtime on startup. - Adding a schema change: 1. Edit the EF Core entity/model mapping. 2. Generate an EF migration from `apps/backend`. 3. Open the migration and review it before committing. 4. Commit the migration, model snapshot, entity/mapping changes, tests, and changelog entry together. 5. Apply locally by restarting the .NET API or worker and verify. - Never introduce Drizzle, SvelteKit `/api` routes, or a TypeScript worker. - Do not add broad compatibility bridges for abandoned schemas. If a breaking schema change needs user consent, add one explicit product flow for that exact change and remove it when it is no longer needed. ## Design System Rules - Follow the prism visual system in `docs/design-language.md`: neutral Prismedia chrome, spectrum identity for entity families, and artwork-reactive detail atmosphere. - Controlled radii from a unified scale (`radius-xs: 4px` through `radius-2xl: 24px`). Tight, subtle softening — never bubbly or pill-shaped containers. - Keep true black and opaque neutral content surfaces as the ground layer; reserve frosted glass for navigation, toolbars, menus, sheets, dialogs, and floating controls. - Use neutral silver for app-level state and the canonical entity spectrum for sparse media markers and selected-state rails. Keep surrounding text, icons, cards, and controls neutral; do not introduce route-local accent palettes. - Give each page one muted accent moment that communicates its identity, such as an EntityGrid toolbar edge, a Settings section icon, or artwork atmosphere on detail pages. Do not repeat that accent across every element. - On detail pages, extract background/primary/secondary colors from already-loaded real artwork, preserve readable neutral text, and only render a hero when the API explicitly provides one. - Mobile first. Desktop expands the mobile layout. - Font voices: Cinzel for rare display/brand moments, Geist for product headings, Inter for body, JetBrains Mono for utility and metadata. - Shape, border, and animation express state. Entity color should read like flat material paint and occupy a small visual budget; reserve emitted-light glow for transient prism/loading energy. - Avoid generic SaaS styling and unmodified shadcn defaults. - Core actions must not depend on hover-only affordances. ## Data & Integration Rules - Keep provider integrations behind stable adapter interfaces. - Do not embed a third-party application schema as Prismedia's application schema. - Normalize external hashes and metadata into Prismedia-owned tables and contracts. - Plugin development may use a separate Prismedia-Plugins checkout, but no developer-specific checkout location belongs in tracked repository files. ## Repository Privacy - Do not commit private hostnames, IP addresses, usernames, home-directory paths, credentials, private media details, or deployment topology. - Keep dated audits, active roadmaps, campaign plans, submission packets, and other working notes outside tracked documentation. - Tracked documentation must be durable project documentation or content intended for publication. Use ignored local workspaces for private planning and audits. ## Identifier Discipline (Magic-String Contract) Closed-set string identifiers are NEVER written as bare literals. Every kind code, relationship/credit/file-role code, job type, capability key, setting key, problem code, sort/filter/status/position/progress/date code, playback/stream mode, provider or runtime id, image-asset/meta-icon kind, and external wire scalar (Stash, ffprobe) has exactly one source of truth and is referenced from it. ### The source of truth - Entity-kind codes and domain facts live in discovered `EntityKindDefinition` implementations beside their concrete Entity types; the `EntityKind` enum is only the typed identity. Other backend closed sets are `[Code("...")]` enums under `Prismedia.Domain/Entities/Enums/**`. Both resolve through `CodecRegistry` (e.g. `EntityKind.Video.ToCode()`, `RelationshipKind.Tags.ToCode()`, `CreditRole.Director.ToCode()`, `JobType`, `PlaybackMode`, `EntityFileRole`). - Cross-cutting wire/path constants live in dedicated static classes (e.g. `AppSettingKeys` and — as they are introduced — `ApiProblemCodes`, `AssetPaths`, `MediaCodecs`, `MediaContainers`). - The frontend consumes code values ONLY from `src/lib/api/generated/codes.ts` (re-exported via `$lib/entities/entity-codes.ts`): `ENTITY_KIND`, `RELATIONSHIP_CODE`, `CREDIT_ROLE`, `ENTITY_FILE_ROLE`, `JOB_TYPE`, `PLAYBACK_MODE`, `CAPABILITY_KIND`, `SETTING_KEYS`, `EXTERNAL_ID_PROVIDER`, and any future codegen'd families. `codes.ts` is generated — never edit it by hand. ### Rules 1. Need a closed-set string? Find its Entity-kind definition, `[Code]` enum, or constant class and reference it. Do NOT retype the literal. The definition code, `[Code]` attribute, or const declaration is the only place the literal text appears. 2. A new Entity kind requires its enum identity plus one discovered definition; do not edit a registry. For any other new closed set with no home, CREATE a `[Code]` enum (or a constant class for non-domain wire vocab), then surface it to the frontend: add the enum to `ENUM_EXPORTS` in `apps/web-svelte/scripts/gen-codes.mjs` (or a constant group in `Prismedia.Api/Codegen/CodesManifest.cs`), and run `pnpm api:generate` with the dev API up. Never hand-maintain a parallel TS union of codes. 3. Frontend types for codes come from orval-generated models or `codes.ts` — never a hand-written `"a" | "b"` union that duplicates a backend set. 4. External wire vocab (Stash, ffprobe) belongs in its protocol/constant class, referenced — not retyped. External decode-boundary field names may stay inline ONLY at the single parse site, annotated `// prism-vocab: external`. 5. Problem/error codes: throwers and HTTP mappers both reference the shared `ApiProblemCodes`; the frontend matches generated problem-code consts, never English message text. 6. NOT identifiers (leave inline): user-facing display text, log/exception messages, CSS/Tailwind classes, ARIA labels, file paths, URLs, format strings, test descriptions, and standard DOM key/MIME tokens. When in doubt, grep for the literal: if it already lives in an Entity-kind definition, a `[Code]` enum, or `codes.ts`, reference that. If it does not but is a closed set, you are adding the canonical home, not another copy. Enforcement belongs in Roslyn analyzers, architecture tests, `codes.ts` CI parity, and a frontend `no-magic-codes` lint; do not bypass it. ## UI Composition Discipline (Shared Building Blocks) New pages are COMPOSED from shared building blocks, not hand-rolled. Before writing a control or layout, find the block that owns the pattern and use it. - Presentational, domain-free primitives live in `packages/ui-svelte` (`Button`, `TextInput`, `Select`, `Checkbox`, `Toggle`, `Badge`, `Meter`, plus shared overlays/inputs as they land). Membership test: no `$lib`, `$app`, or `$lib/api` imports. - Domain-aware field wrappers live in `apps/web-svelte/src/lib/components/forms` (`TextField`, `SearchSelect`, `TagSelect`, `EntityPicker`, `DateField`, `ToggleChip`, `FormActions`, `EditFormShell`, …). - Entity scaffolds live in `apps/web-svelte/src/lib/components/entities` (`EntityIndexPage`, `EntityGrid`, `EntityThumbnail`, and the config-driven detail scaffolds as they are extracted). - Do NOT hand-roll raw `/