Metadata-Version: 2.4
Name: kurmann-schnittprojekt-archivar
Version: 2.0.0
Summary: Archivar für Videoschnitt-Projekte: packt FCPXML-Bundles und iMovieMobile-Container ins Langzeit-Archiv, mit aggregierten Projekt-Metadaten und Video-Originalmedien-Inventar.
Author: Patrick Kurmann
License: MIT License
        
        Copyright (c) 2026 Patrick Kurmann
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/kurmann/schnittprojekt-archivar
Project-URL: Repository, https://github.com/kurmann/schnittprojekt-archivar
Project-URL: Changelog, https://github.com/kurmann/schnittprojekt-archivar/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/kurmann/schnittprojekt-archivar/issues
Keywords: video,fcpxml,imovie,schnittprojekt,archive,manifest
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: typer>=0.12
Requires-Dist: rich>=13
Requires-Dist: kurmann-schnittprojekt-leser<0.6,>=0.5.2
Requires-Dist: kurmann-medien-leser<1.0,>=0.3
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Dynamic: license-file

# 🎬 schnittprojekt-archivar

Archivar für Videoschnitt-Projekte. Verpackt FCPXML-Bundles und iMovieMobile-Container ins Langzeit-Archiv und schreibt zwei aggregierte Metadaten-Sidecars dazu: Projekt-Übersicht (`.project.toml`) und Clip-Inventar (`.project.clips.jsonl`) mit voller `MediaFileMetadata` pro referenziertem Quellclip.

> **Status: 2.0.0 (Beta)** — Greenfield-Neuanfang nach Bounded-Context-Wechsel; die Versionsnummer setzt den Git-Tag-Stream des Repos fort (v1.x war `fcplib-manager`, siehe [CHANGELOG](CHANGELOG.md)). API-Vertrag startet mit 2.0.0; künftige API-Erweiterungen kommen als Minor-Versionen, Breaking Changes erst bei 3.0.0.

> **Hinweis**: Bis Januar 2026 hiess dieses Repo `fcplib-manager` und archivierte Final-Cut-Pro-Libraries (FCPBundles) via tar+split+SSH. Mit der Umbenennung zu `schnittprojekt-archivar` ändert sich der Bounded Context komplett: Schnittprojekte im FCPXML-/iMovieMobile-Format statt FCP-Library-Bundles.

## ✨ Was es kann

- **Drei Schnittprojekt-Formate**:
  - `.fcpxmld` (FCP-Bundle, Verzeichnis) → ZIP-Container mit `ZIP_STORED`+`allowZip64` (kein erneutes Komprimieren, weil Bundle-Medien bereits HEVC/ProRes sind)
  - `.fcpxml` (Single-File) → 1:1-Kopie ohne Container
  - `.iMovieMobile` (ist schon ZIP) → 1:1-Kopie ohne erneutes Zippen
- **Atomares Schreiben** überall (write-to-tempfile + `os.replace`). Crashes hinterlassen keine Halb-Dateien.
- **Aggregiertes Projekt-Manifest** (`<stem>.project.toml`): Format, Sequenz, Aufnahmedatum mit Provenance, Clip-Anzahl, Bundle-Größe.
- **Clip-Inventar** (`<stem>.project.clips.jsonl`): pro referenziertem Quellclip eine JSONL-Zeile mit voller `MediaFileMetadata` aus `kurmann-medien-leser` (Codec, Auflösung, GPS, Kamera-Modell, Production-Felder, etc.) — kompatibel zum `kamera-einleser`-Manifest-Format für Cross-References zwischen Schnittprojekt-Sicht und Originalmedien-Volumes.
- **rclone-Ziele** unterstützt: lokal in `work_dir` aufbauen, dann via `rclone copyto` ins Archiv-Remote.
- **Flexibles Konsumenten-API**: Suffixe sind Konstanten (keine hardcoded Implementierungs-Details). Drei Schichten — Daten, Bundle-Packaging, Persistierung — können separat oder als Convenience zusammen genutzt werden.

## 📋 Voraussetzungen

- Python 3.11 oder höher
- [exiftool](https://exiftool.org/) (Pflicht, für Clip-Inventar) — `brew install exiftool` auf macOS
- [rclone](https://rclone.org/) (optional, für rclone-Ziele) — `brew install rclone` auf macOS

> **Quelle = lokaler Pfad.** Der Archivar liest das Schnittprojekt heute ausschliesslich vom lokalen Filesystem (echte lokale Pfade oder gemountete Netzpfade wie `/Volumes/<share>/...`). rclone-URIs (`remote:path/...`) sind nur als **Ziel** (`--target-dir`) erlaubt, nicht als Quelle. Wer ein Projekt vom NAS archivieren will, ohne den Share gemountet zu haben, holt es vorher via `rclone copy` ins lokale Work-Dir.

> **Archivieren = kopieren, nicht verschieben.** Die Originalquelle wird nie gelöscht oder verändert. Der Archivar arbeitet ausschliesslich Copy-and-Verify; Aufräumen der Quelle ist Aufgabe des Konsumenten (z. B. `familienfilm-manager`).

> **`.fcpxml` vs. `.fcpxmld` — was archiviert wird:**
> FCP exportiert oft beides nebeneinander: eine Flat-`.fcpxml` (paar KB XML) und ein `.fcpxmld/`-Bundle (Verzeichnis mit `Info.fcpxml` + Resources). Wenn beide mit gleichem Stem nebeneinander liegen, **bricht der Archivar mit Fehler ab**, statt zu raten — du gibst gezielt das `.fcpxmld/`-Verzeichnis an, wenn du das Bundle willst. Override: `--allow-flat-fcpxml`.
>
> **Originalmedien werden NIE mitarchiviert.** Schnittprojekte enthalten Edit-Decisions + Asset-Referenzen, nicht die Videodateien selbst. Diese leben auf separaten Volumes (ISO-Manifests, [`kamera-einleser`](https://github.com/kurmann/kamera-einleser)) und werden vom `.project.clips.jsonl`-Inventar nur **referenziert**, nicht kopiert. Wer „das ganze Schnittprojekt inklusive Medien" archivieren will, braucht ein Media-Pool-Archiv — andere Domäne.

## 🚀 Installation

```bash
uv add kurmann-schnittprojekt-archivar
# oder
pipx install kurmann-schnittprojekt-archivar
# oder
pip install kurmann-schnittprojekt-archivar
```

## 🎯 Quick Start

### CLI

```bash
schnittprojekt-archivar archive \
    "/Volumes/Videoschnitt/Familienfilme/Projekte/Mädchen spazieren.fcpxmld" \
    --target-dir "lyssach-nas:/Archiv/2026/Projekte" \
    --target-stem "2026-05-10-Maedchen-spazieren" \
    --timezone Europe/Zurich
```

Erzeugt im Ziel:
```
2026-05-10-Maedchen-spazieren.fcpxmld.zip   ← Bundle als ZIP
2026-05-10-Maedchen-spazieren.project.toml  ← Projekt-Metadaten
2026-05-10-Maedchen-spazieren.project.clips.jsonl   ← Clip-Inventar
```

### Als Bibliothek

```python
from pathlib import Path
from schnittprojekt_archivar import archive_project
from schnittprojekt_archivar.models import ArchiveProjectRequest, RuntimeOptions

result = archive_project(
    ArchiveProjectRequest(
        project_path=Path("Mädchen spazieren.fcpxmld"),
        target_dir="lyssach-nas:/Archiv/2026/Projekte",
        target_stem="2026-05-10-Maedchen-spazieren",
        timezone_assumption="Europe/Zurich",
    ),
    RuntimeOptions(),
)
if result.success:
    print(result.bundle.target_uri, result.bundle.bundle_size_bytes)
else:
    print("Fehler:", result.error_message)
```

## 🧾 Was kommt zurück? Beispiel-Output

### `<stem>.project.toml` (aggregiert)

```toml
[meta]
schema_version = 1
generator = "Kurmann Schnittprojekt-Archivar 2.0.0"
created_at = "2026-05-21T10:00:00+02:00"

[project]
name = "Fünffacher Büsi-Nachwuchs bei Tagesmutter Karin"
format = "fcpxml_bundle"
schema_version = "1.8"
original_filename = "Fünffacher Büsi-Nachwuchs bei Tagesmutter Karin poster-13s080.fcpxmld"
mtime = "2026-05-10T20:02:57+02:00"

[recording]
first_clip_date = "2026-05-05"
first_clip_datetime = "2026-05-05T17:43:51+02:00"
first_clip_source_label = "exif:Keys:CreationDate"
first_clip_confidence = "high"

[sequence]
width = 3840
height = 2160
color_space = "9-16-9 (Rec. 2020 PQ)"
audio_layout = "stereo"

[counts]
clips_primary = 6
clips_total = 7

[bundle]
archive_filename = "2026-05-05-Buesi-Nachwuchs.fcpxmld.zip"
archive_size_bytes = 421857393
```

### `<stem>.project.clips.jsonl` (Inventar)

```jsonl
{"_type":"header","manifest_version":1,"project_name":"Fünffacher Büsi-Nachwuchs bei Tagesmutter Karin","project_format":"fcpxml_bundle","clip_count":6,"generator":"Kurmann Schnittprojekt-Archivar 2.0.0","created_at":"2026-05-21T10:00:00+02:00"}
{"spine_index":0,"relative_path":"./A001_05051743_C491.mov","asset_id":"p1997","clip_name":"A001_05051743_C491","is_video":true,"resolution_status":"resolved","resolved_uri":"/.../A001_05051743_C491.mov","media_metadata":{"recorded_at":"2026-05-05T17:43:51+02:00","recorded_at_source":"exif:Keys:CreationDate","source_app":"Blackmagic Camera","video":{"codec":"HEVC","resolution":"3840x2160","fps":60.007},"camera":{"model":"Apple iPhone 15 Pro Max 120mm","software":"Blackmagic Cam 3.3.100001"},"gps":{"lat":47.06675,"lon":7.58025}}}
{"spine_index":1,"relative_path":"./A001_05051744_C492.mov",...}
```

## 🏛 Architektur — drei Schichten + ein Wrapper

```
┌─────────────────────────────────────────────────────────────────┐
│ Convenience / CLI                                               │
│ ─────────────────                                               │
│ archive_project(request, runtime)                               │
│   opinion-getriebener Happy-Path: liest, packt, persistiert.    │
└─────────────────────────────────────────────────────────────────┘
              │
        ┌─────┼─────┐
        ▼     ▼     ▼
 ┌──────────┐ ┌──────────────┐ ┌────────────────────┐
 │ Daten    │ │ Bundle       │ │ Persistierung      │
 │ ─────    │ │ ──────       │ │ ──────────────     │
 │ read_    │ │ package_     │ │ write_manifest_    │
 │ project_ │ │ project_     │ │ toml(m, path)      │
 │ manifest │ │ bundle       │ │ write_clips_jsonl( │
 │  → in-   │ │  → ZIP /     │ │   m, path)         │
 │   memory │ │   1:1-Kopie  │ │ Suffix-Konstanten  │
 │ Mani-    │ │   nach Ziel  │ │ als Empfehlung,    │
 │ fest     │ │              │ │ nicht Pflicht      │
 └──────────┘ └──────────────┘ └────────────────────┘
```

Konsumenten können nach Bedarf entweder den **Convenience-Wrapper** nutzen oder die einzelnen Schichten kombinieren — siehe „Konsumenten-Szenarien" weiter unten.

## 📚 Public API

| Symbol | Beschreibung |
|---|---|
| `archive_project(request, runtime) → ArchiveProjectResult` | Convenience-Orchestrator: Manifest lesen + Bundle packen + Sidecars schreiben |
| `read_project_manifest(path, runtime) → ProjectManifest` | Liest Schnittprojekt + Clips, liefert in-memory Objekt (keine Side-Effects) |
| `package_project_bundle(path, target_dir, filename, runtime) → PackageResult` | Verpackt Bundle ins Ziel (Caller wählt Filename) |
| `write_manifest_toml(manifest, path)` | Aggregiertes Manifest als TOML (atomar) |
| `write_clips_jsonl(manifest, path)` | Clip-Inventar als JSONL (atomar) |
| `PROJECT_TOML_SUFFIX`, `CLIPS_JSONL_SUFFIX`, `FCPXML_BUNDLE_ZIP_SUFFIX`, `IMOVIE_MOBILE_SUFFIX`, `FCPXML_SINGLE_SUFFIX` | Empfohlene Suffixe als Konstanten |
| `ArchiveProjectRequest`, `ArchiveProjectResult`, `PackageResult`, `RuntimeOptions`, `ProjectManifest`, `ClipEntry`, `ProjectHeader`, `SequenceInfo`, `ArchivarEvent` | Dataclasses (Type-Hints + Serialisierungs-Helpers) |
| `SchnittprojektArchivarError`, `BundleError`, `ManifestReadError`, `ArchiveTargetError`, `ArchiveTargetExistsError` | Exception-Hierarchie |

## 🏷 Begriffe und Suffix-Konventionen

Die Suffixe sind so gewählt, dass sie auch ohne diese README verständlich sind und nicht mit anderen kurmann-Sidecars kollidieren.

### Quellformate: `.fcpxml`, `.fcpxmld`, `.iMovieMobile`

| Suffix | Was es ist | Wie's der Archivar verpackt |
|---|---|---|
| `.fcpxml` | **Flat-XML**, Single-File. Reine Edit-Decision-XML, paar KB bis paar MB. | 1:1-Kopie. |
| `.fcpxmld` | **FCPXML-Document** = macOS-Package (Verzeichnis). Enthält `Info.fcpxml` + optional `Resources/` mit eingebetteten Vorschau-Daten. Das `d`-Suffix ist Apples Konvention für „Bundle-Variante eines flachen Formats" (analog `.rtf` ↔ `.rtfd`). | ZIP-Container `.fcpxmld.zip` mit `ZIP_STORED+allowZip64`, alphabetisch sortierte Member (deterministisch). |
| `.iMovieMobile` | iMovie-iOS-Export — bereits ein ZIP-Container. | 1:1-Kopie (kein Re-Zip). |

**Wenn FCP beides exportiert** (`.fcpxml` *und* `.fcpxmld` mit gleichem Stem): immer das **`.fcpxmld`** archivieren — das ist die vollständige Variante. Der Archivar erkennt diese Mehrdeutigkeit und bricht ab, wenn man die Flat-Variante als Quelle angibt, während ein Bundle daneben liegt. Override falls bewusst nur die Flat-XML gewünscht: `--allow-flat-fcpxml`.

### Namespace `.project.*` (Archivar) vs. `.video.*` (familienfilm-manager)

Wenn ein Familienfilm gleichzeitig als Schnitt-Export **und** als Schnittprojekt archiviert wird, leben beide Sätze problemlos im selben Verzeichnis nebeneinander — gleicher `<stem>`, unterschiedliche Domäne-Präfixe:

```
2026-05-10-Maedchen-spazieren.mov                       ← FFM-Video-Export
2026-05-10-Maedchen-spazieren.video.toml                ← FFM-Sidecar (Recording, Publishing)
2026-05-10-Maedchen-spazieren.fcpxmld.zip               ← Archivar-Bundle
2026-05-10-Maedchen-spazieren.project.toml              ← Archivar-Projekt-Manifest
2026-05-10-Maedchen-spazieren.project.clips.jsonl       ← Archivar-Clip-Inventar
```

| Namespace | Domäne | Verantwortlich |
|---|---|---|
| `.video.*` | exportierter Schnitt-Stream (was publiziert wird) | [`familienfilm-manager`](https://github.com/kurmann/familienfilm-manager) |
| `.project.*` | Schnittprojekt-Quelldatei (woraus exportiert wurde) | dieser Archivar |

`<domäne>.toml` ohne weiteren Zusatz = **Haupt-Manifest dieser Domäne** (= „Metadaten"). Zusatz-Suffixe wie `.clips` qualifizieren spezialisierte Inventare.

### Warum „Clips" und nicht „Originalmedien"?

Zwei legitime Vokabulare treffen aufeinander:

- **Schnittprojekt-Sicht** (im FCPXML als `<asset-clip>`, im iMovieMobile als `editList[i]` mit `clipType`): hier sind die referenzierten Asset-Einheiten **Clips**. Das ist die Sprache, in der das Projekt selbst über sein Material spricht. Auch Audio-Subspuren wären „Audio-Clips" im Schnitt-Sinne.
- **NAS-Sicht** (`/Volumes/Originalmedien/`, `kamera-einleser`-ISO-Manifests): hier heisst dasselbe Material **Originalmedien** — die unbeschnittenen Aufnahmen von der Kamera, bevor sie ein Projekt sehen.

Beide Begriffe widersprechen sich nicht, sie zeigen dasselbe Material aus zwei Schichten. In diesem Archivar gilt die **Schnittprojekt-Sicht** — daher `.project.clips.jsonl`. Cross-References zwischen `kamera-einleser`-ISO-Manifest (Originalmedien-Sicht) und `<stem>.project.clips.jsonl` (Schnittprojekt-Sicht) bleiben über die gemeinsame `MediaFileMetadata`-Struktur möglich (gleiche Felder, gleiche Schemas, beide aus `kurmann-medien-leser`).

## 🎯 Scope von `include_clips` (v2.0.0)

Standardmässig (`include_clips=True`) wird das vollständige Inventar **aller referenzierten Video-Clips** in `<stem>.project.clips.jsonl` geschrieben.

**Enthalten**: Top-Level-Spine-Video-Clips mit Asset-Backing — bei FCPXML alles, was `asset-clip` mit echter Quelldatei referenziert; bei iMovieMobile `editList`-Einträge mit `clipType=1`.

**Bewusst NICHT enthalten** (Stand v2.0.0):
- Audio-Subspuren / separate Audio-Synchronspuren
- Foto-Clips (`clipType=5` iMovieMobile)
- Transitions (`clipType=3`)
- Sekundäre Tracks / Picture-in-Picture
- Generators, Titles, Effekte ohne Asset-Backing

**Hintergrund**: Die Detail-Metadaten pro Clip werden vom [`kurmann-medien-leser`](https://github.com/kurmann/medien-leser) gelesen, der heute auf Video-Metadaten ausgerichtet ist. Sobald Audio-Schemas dort verfügbar sind, wird `include_audio_tracks=True` als additives Flag ergänzt — kein Schema-Bruch nötig, weil „Clip" konzeptuell auch Audio-Clips abdecken kann.

**Use-Case**: Cross-References zwischen Schnittprojekten und Originalmedien-Volumes (z.B. ISO-Manifests aus [`kamera-einleser`](https://github.com/kurmann/kamera-einleser)). Fragen wie „welche Clips aus ISO XYZ wurden in welchem Schnittprojekt verwendet?" sind mit beiden JSONL-Manifesten gemeinsam beantwortbar — gleiches `MediaFileMetadata`-Schema.

## 🤝 Konsumenten-Szenarien

### Szenario α: Standalone-Aufruf via CLI

```bash
schnittprojekt-archivar archive <projekt> \
    --target-dir <archiv-uri> --target-stem <slug>
```

Alles ins Ziel, drei Sidecars nebeneinander.

### Szenario β: Bundle packen, Manifest in eigene Struktur einbetten

Für Konsumenten wie den `familienfilm-manager`, die ihr eigenes Sidecar-Schema haben:

```python
from schnittprojekt_archivar import (
    read_project_manifest,
    package_project_bundle,
)

# 1. Manifest als in-memory-Objekt lesen
manifest = read_project_manifest(project_path, runtime)

# 2. Bundle separat verpacken (Konsument wählt Filename)
package_project_bundle(
    project_path,
    target_dir="nas:/Archiv/2026/Videoschnitt",
    target_filename="2026-05-10-Maedchen.fcpxmld.zip",
    runtime=runtime,
)

# 3. Konsument bettet Manifest in eigene .video.toml-Sektion ein —
#    schreibt KEIN .project.toml oder .project.clips.jsonl daneben.
ffm_sidecar.schnittprojekt = {
    "bundle_filename": "2026-05-10-Maedchen.fcpxmld.zip",
    "first_clip_date": manifest.header.first_clip_date,
    "clip_count": len(manifest.clips),
    # ... etc
}
```

### Szenario γ: Convenience, aber kein project.clips.jsonl

```python
from schnittprojekt_archivar import archive_project
from schnittprojekt_archivar.models import ArchiveProjectRequest

archive_project(
    ArchiveProjectRequest(
        project_path=project_path,
        target_dir=target_dir,
        target_stem="2026-05-10-Maedchen",
        write_clips_jsonl_file=False,  # nur .project.toml + Bundle
    ),
    runtime,
)
```

## 🐚 CLI-Disziplin (Unix-Style)

Der `schnittprojekt-archivar`-CLI folgt klassischer Unix-Philosophie: **Stille ist Erfolg.**

| Kanal | Default | `--verbose` | `--json` |
|---|---|---|---|
| stdout | (leer) | (leer) | Result-JSON |
| stderr | (leer); Fehler bei Misserfolg | Event-Trace + Slug-Hinweis + Sub-Prozess-Output | wie ohne `--json` |
| Exit-Code | 0 (Erfolg) / 1 (Fehler) | wie Default | wie Default |

**Beispiele:**

```bash
# Stiller Aufruf — Erfolg erkennt man am Exit-Code
schnittprojekt-archivar archive <projekt> --target-dir <ziel>
echo $?    # 0

# Pipeline-Konsument
schnittprojekt-archivar archive <projekt> --target-dir <ziel> --json \
  | jq -r .target_uri

# Diagnose
schnittprojekt-archivar archive <projekt> --target-dir <ziel> -v
# stderr zeigt: [stem_slugified] target_stem »...« → »...«
#               [bundle_build_started] ...
#               [bundle_upload_completed] ...
```

Fehler landen immer auf stderr, unabhängig von `--verbose`. `--verbose` und `--json` sind orthogonal kombinierbar.

## 🏷 ASCII-Slug-Disziplin

Der Archivar wendet **per Default** eine Archivierungs-Disziplin auf den Ziel-Dateinamen an: `target_stem` wird durch `to_archive_stem()` geschickt und damit ASCII-safe gemacht. So bleiben die Archiv-Dateinamen filesystem-, cloud- und Tooling-neutral lesbar.

**Algorithmus** (in `schnittprojekt_archivar.models.slug`):

1. Unicode-Normalisierung NFC.
2. Deutsche Transliteration: `ä→ae`, `ö→oe`, `ü→ue`, `ß→ss` (Gross-Schreibung analog).
3. NFKD + ASCII-Encode `errors="ignore"` (entfernt restliche Diakritika und Nicht-ASCII).
4. Regex `[^a-zA-Z0-9]+ → '-'`, dann führende/abschliessende `-` strippen.

**Beispiele**:

| Eingabe | Ergebnis |
|---|---|
| `Mädchen spazieren` | `Maedchen-spazieren` |
| `2026-05-10 – Höhenflug` | `2026-05-10-Hoehenflug` |
| `Café crème` | `Cafe-creme` |
| `2026-05-10-Maedchen` | `2026-05-10-Maedchen` (unverändert) |

Die Funktion ist **idempotent** — doppelte Anwendung ändert nichts.

**Public-API**:

```python
from schnittprojekt_archivar import to_archive_stem, is_archive_safe_stem

to_archive_stem("Mädchen spazieren")        # "Maedchen-spazieren"
is_archive_safe_stem("Mädchen spazieren")   # False
is_archive_safe_stem("Maedchen-spazieren")  # True
```

**Default-Verhalten** in `archive_project`: Umlaute werden still umgeformt, das Ergebnis steht in `ArchiveProjectResult.effective_stem`, das Original in `original_stem`, das Flag `stem_was_slugified=True`. Zusätzlich emittiert der Archivar eine Warnung und ein `stem_slugified`-Event.

**Strict-Modus** mit `strict_stem=True` (CLI: `--strict-stem`): der Archivar slugifiziert **nicht**, sondern verlangt, dass der gelieferte Stem bereits archivtauglich ist. Andernfalls bricht er mit klarer Fehlermeldung ab, bevor irgendetwas geschrieben wird — sinnvoll für Konsumenten, die selbst über die finale Namensgebung bestimmen wollen (z. B. `familienfilm-manager`, der seine eigene Slug-Logik hat).

> Diese Slug-Logik existiert 1:1 auch im `familienfilm-manager` (Code-Spiegelung). Sobald ein dritter Konsument auftaucht oder Archiv-Helpers gebündelt werden sollen (PAR2, ZIP64, Indexierung), lohnt sich ein `kurmann-archiv-utils`-Helper-Repo. Heute YAGNI.

## 🛣️ Roadmap

- **0.x**: API-Stabilisierung mit dem ersten Konsumenten (familienfilm-manager v1.5+).
- **0.2.x**: `extract`-Subcommand für Retrieval aus dem Archiv (Architektur ist eingeplant, Schemas tragen die nötigen Felder bereits).
- **0.3.x**: Indexierung über mehrere Projekte (optional — Discovery via externe `jq`-Queries reicht heute).
- **1.0.0**: API-Lock nach SemVer.
- **Später**: `include_audio_tracks=True`, sobald `kurmann-medien-leser` Audio-Schemas unterstützt.

## 📝 Lizenz

MIT
