aYOUne

Changelog

What changed — grouped by release, filterable by component and repository.

No milestone 196
New page-editor

cta-Block bekommt seinen Entitaets-Bezug (Auswahl + Interessens-Automatik)

Der cta-Block ist mit 252 Vorkommen der viert-haeufigste Block im Seiten-Bestand und
war der einzige Conversion-Block ohne Entitaets-Bezug - der Redakteur tippte Texte
ein zweites Mal ab, die als CTA-Entitaet laengst gepflegt waren.

- Katalog-Eintrag 'cta' mit paletteHidden (Sonder-Maske BLEIBT, Inline-Weg ist der
Rueckfall): _cta (Auswahl echter Werte), _ctaAuto, _ctaInterest.
- DREI Marker-Entscheidungen weichen vom popup-Nachbarn ab, alle am Bestand gemessen
(2026-08-23, 55 Datensaetze): KEIN scopeByWebsite (_website fehlt 55/55), KEIN
markUnpublished (published fehlt 55/55 - die popup-Regel prueft published !== true
und haette alle 55 als 'nicht veroeffentlicht' gekennzeichnet), dafuer
markForeignLanguage MIT dem neuen langField.
- CatalogRefSource.langField (neu, Vorgabe 'lang'): CTAs fuehren ihre Sprache in
'locale'. Ohne den Zeiger laese foreignLanguageSuffix 'lang', saehe undefined und
kennzeichnete NICHTS - waehrend der Zeichner sehr wohl filtert. Der Schalter waere
lautlos wirkungslos gewesen.
- block-renderer: catalog-block-editor unter der cta-Sondermaske verdrahtet. Ohne
diese Zeile waere der ganze Katalog-Eintrag unsichtbar.
- Beide Bestands-Waechter der Probe-Datei erweitert (Renderer-Schluessel-Gleichheit,
Zeichenketten-Filter-Ausnahme) plus 6 neue cta-Proben. 64/64 gruen; Flip-Probe
belegt (langField + locale-Projektion entfernt -> genau 2 rot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pages

cta-Block liest die gepflegte CTA-Entitaet (Marker-Weg + Interessens-Automatik)

Der cta-Block war mit 252 Vorkommen der viert-haeufigste Block im Bestand und
zugleich der einzige Conversion-Block ohne Entitaets-Bezug: die Plattform pflegt
CTAs seit Jahren als eigene Entitaet mit eigenem Ausliefer-Dienst (ctas.ayoune.app)
und eigenem Klick-Zaehler (clt.ayoune.app), der Website-Bauer kannte sie nicht und
der Redakteur tippte denselben Text ein zweites Mal.

- mapCtasToItems: ICTA -> Block-Inhalt. Strippt HTML (21 von 55 Datensaetzen tragen
Markup in h1/h2), behandelt den Platzhalter '-' als leer, leitet den Sprach-Code
aus 'locale' ab (NICHT 'lang' wie bei References/Faqs/Quotes - ein lang-Filter
saehe undefined und liesse jede Fassung durch) und setzt die Knopf-Adresse auf
den Klick-Zaehler.
- resolveEntityBlocks: cta-Zweig. Ausdrueckliche Wahl (_cta) schlaegt den Auto-Zug
(_ctaAuto + _ctaInterest); der Auto-Zug ist fail-closed und fragt ohne Kriterium
die Datenbank gar nicht. WEDER _website- NOCH published-Filter - beide Felder
fehlen bei 55/55 Datensaetzen, ein vom popup-Nachbarn abgeschriebener Filter
haette jede CTA weggeschnitten.
- renderBlock case 'cta': Entitaet schlaegt Inline, Sprachwahl liegt hier (der
Resolver kennt die ausgelieferte Sprache nicht). Ohne passende Fassung bleibt der
Block still statt auf die alten Inline-Texte zurueckzufallen.
- app.ts: aMN.CTAs geladen (marketing-Modul, kein cms-Auto-Load) - ohne den Eintrag
waere der ganze Zweig still wirkungslos.
- app.ts: aMN.Quotes nachgetragen. NEBENBEFUND derselben Klasse: der quotes-Zweig
stand seit der Zitate-Welle im Zeichner, sein Modell wurde nie geladen. Latent -
quotes kommt im Seiten-Bestand 0-mal vor (Positiv-Kontrolle: 23 andere Typen).

24 Proben mit Negativ-Kontrollen; drei Flip-Proben belegt (Sprachwahl entfernt -> 2 rot,
fail-closed entfernt -> 2 rot, HTML-Strip entfernt -> 2 rot). Regression cta-nah 312/312.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pages

cta-Block liest die gepflegte CTA-Entitaet (Marker-Weg + Interessens-Automatik)

Der cta-Block war mit 252 Vorkommen der viert-haeufigste Block im Bestand und
zugleich der einzige Conversion-Block ohne Entitaets-Bezug: die Plattform pflegt
CTAs seit Jahren als eigene Entitaet mit eigenem Ausliefer-Dienst und Klick-Zaehler,
der Website-Bauer kannte sie nicht.

- mapCtasToItems: ICTA -> Block-Inhalt. Strippt HTML (21 von 55 Datensaetzen tragen
Markup in h1/h2), behandelt den Platzhalter '-' als leer, leitet den Sprach-Code
aus 'locale' ab (NICHT 'lang' wie bei References/Faqs/Quotes) und setzt die
Knopf-Adresse auf den Klick-Zaehler clt.ayoune.app/cta/:id.
- resolveEntityBlocks: cta-Zweig. Ausdrueckliche Wahl (_cta) schlaegt den Auto-Zug
(_ctaAuto + _ctaInterest); der Auto-Zug ist fail-closed und fragt ohne Kriterium
die Datenbank gar nicht. WEDER _website- NOCH published-Filter - beide Felder
fehlen bei 55/55 Datensaetzen, ein vom popup-Nachbarn abgeschriebener Filter
haette jede CTA weggeschnitten.
- renderBlock case 'cta': Entitaet schlaegt Inline, Sprachwahl liegt hier (der
Resolver kennt die ausgelieferte Sprache nicht). Ohne passende Fassung bleibt der
Block still statt auf die alten Inline-Texte zurueckzufallen.
- app.ts: aMN.CTAs geladen (marketing-Modul, kein cms-Auto-Load) - ohne den Eintrag
waere der ganze Zweig still wirkungslos.
- app.ts: aMN.Quotes nachgetragen. NEBENBEFUND derselben Klasse: der quotes-Zweig
stand seit der Zitate-Welle im Zeichner, sein Modell wurde nie geladen. Latent -
quotes kommt im Seiten-Bestand 0-mal vor (Positiv-Kontrolle: 23 andere Typen).

24 Proben mit Negativ-Kontrollen; drei Flip-Proben belegt (Sprachwahl entfernt -> 2 rot,
fail-closed entfernt -> 2 rot, HTML-Strip entfernt -> 2 rot). Regression cta-nah
312/312 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New ayauto

`job list` + `job trigger` — Faelligkeits-Jobs von Hand starten

Ein Eintrag der worker-automatic-checks-Registrierung war bis hierher NUR
startbar, indem man den Auftrag direkt in die BullMQ-Warteschlange schrieb —
genau der manuelle Eingriff, den die Tooling-first-Regel ausschliessen soll.
Die Route POST /jobs/:name/actions/trigger existierte, war aber von keinem
Werkzeug erreichbar: `ay` weist operator-only-Module ab, MCP sperrt jede
Nicht-GET-Methode in Produktion, `ayauto su` kann nur get/set auf EINEN
Datensatz. Form uebernommen von `su`/`queue`: benannter Eintrag, kein
Platzhalter, confirm-gated.

🔴 DER TRAGENDE BEFUND: die ausgelieferte Registrierung verschweigt
`defaultData`. Gemessen am laufenden Dienst enthaelt GET /jobs die Zeichenkette
null-mal, waehrend optionsSchema und queueName da sind (Positiv-Kontrolle) —
`serializeRegistry` gibt das Feld schlicht nicht aus. Zugleich reicht die
Ausloese-Route den Rumpf WOERTLICH als Auftragsdaten durch. Ein Hand-Start ohne
Nutzlast stellt also {}; ein Verbraucher, der auf `op` besteht, weist ihn ab —
und der Aufruf sieht trotzdem wie ein Erfolg aus (200 mit Auftrags-Kennung).
Genau das beschreibt der Eintrag HR_BENEFIT_RECEIPTS_MONTHLY woertlich.

Deshalb liest `job trigger` `defaultData` ZUSAETZLICH aus dem Arbeitsbaum und
zeigt es im Trockenlauf — ausdruecklich als Hinweis aus der QUELLE, nicht aus
der ausgelieferten Registrierung. Gesendet wird es nur mit --use-local-default.
Erfunden wird NIE etwas: ohne Angabe geht {} hinaus, und der Trockenlauf sagt,
was das bedeutet.

Der Extrakt hat zwei Riegel, beide mit eigener Probe: Zeilen-Kommentare werden
entfernt (der Quelltext WARNT an mehreren Stellen mit dem Wortlaut
`defaultData: { dryRun: false }` davor, es nachzutragen) und Zeichenketten
werden laengentreu maskiert (die Beschreibungen SPRECHEN ueber defaultData und
op). Echt-Abgleich gegen die Registrierung: 22 Vorkommen in der Quelle, 22 vom
Extrakt gefunden.

Optionen werden gegen das optionsSchema des Eintrags geprueft — ein vertippter
Schluessel wird abgewiesen samt Nennung der gueltigen, statt still zu
verpuffen. Ein erfundener Job-Name und ein `enabled: false`-Eintrag werden
abgewiesen, ohne etwas einzureihen.

23 Proben mit Umkehr- und Positiv-Kontrollen, gruen; Bau gruen.
Abnahme am laufenden Dienst: Trockenlauf zeigt den vollen Auftrag inkl. op ·
erfundene Option/erfundener Name je Exit 2 ohne Einreihung · echter Start von
check-all-health lief DURCH (recent-runs: manual:true, active→completed,
4621 ms), die Nachbarzeile desselben Takts traegt manual:false.

OFFEN: der saubere Rueckweg ist eine Zeile in serializeRegistry
(`defaultData: d.defaultData`) — sie liegt in domains/monitoring/
worker-automatic-checks, ausserhalb der beanspruchten Pfade, und ist als
eigener Vorgang erfasst.
Fix huelle

Fuss-Bereich mobil einspaltig + Menue-Schublade anklickbar

Zwei CEO-Screenshot-Befunde (tolinax.com mobil), beide in der GLOBALEN
Marken-Huelle — sie galten fuer jede Mandanten-Website.

1) Der Fuss-Bereich stand mobil zweispaltig gequetscht. Die Grund-Regel
repeat(auto-fit,minmax(150px,1fr)) hat keine Mobil-Fassung; am echten
396px-Sichtfeld gemessen: 2 Spalten a 158,5px, nachher 1 Spalte a 349px.
Dazu Abstand unten fuer den schwebenden Chat-Knopf.

2) Die Menue-Schublade war NICHT anklickbar — und z-index war nie die Ursache.
backdrop-filter auf dem Kopf macht ihn zum containing block fuer
position:fixed-Kinder UND zu einem eigenen Stapel-Kontext. Die Schublade
liegt IM Kopf, war also an ihm verankert statt am Sichtfeld (top:40px statt 0)
und ihr z-index:200 wirkte nur innerhalb des Kopfes. elementFromPoint in ihrer
Mitte lieferte ay-body. Flip-Probe: mit backdrop-filter:none top:0 und
ay-nav-link.

🔴 Die erste Fassung war HALB wirksam: die Kopf-Regel im 768er-Block wird vom
spaeteren Dunkel-Block ueberschrieben (gleiche Spezifitaet, Reihenfolge
entscheidet). Weichzeichner ging, Durchsicht blieb — schlechter als vorher. Die
geltende Fassung steht als eigener Media-Block am ENDE der Datei; eine Probe
nagelt die POSITION fest, nicht nur die Anwesenheit.

⚠ Der Weichzeichner wird nicht ersatzlos genommen, sondern ueberfluessig gemacht:
deckender Hintergrund aus --ay-bg, hell wie dunkel. Der Schreibtisch behaelt ihn.

⚠ Die Schublade aus dem Kopf herauszuziehen waere der andere Weg — er scheitert
an .ay-header-collapse{display:contents}, das auf dem Schreibtisch Navigation
und Aktionen zu direkten Flex-Kindern macht.

7 neue Proben (mit Positiv-Kontrolle und Umkehrprobe), volle Reihe 1765/1766.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix notifications

Broadcast-Zweig trifft auch die LEERE LISTE

Der Zweig vom Vormittag war zu eng: user liegt in drei Auspraegungen vor
(objectId 7.130 / array 826 / fehlend 418), und 102 der Listen sind leer.
{user:null} trifft eine leere Liste nicht (418 statt 520) — die 102 waren nach
dem ersten Rollout fuer NIEMANDEN mehr sichtbar. Kategorien wie bei den
unadressierten, also Broadcasts.

$in statt $size:0 — dieselben 520 Treffer, aber indizierbar.

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
Fix security

Broadcast-Zweig trifft auch die LEERE LISTE

An der Sammlung gemessen liegt user in drei Auspraegungen vor: objectId 7.130,
array 826, fehlend 418. Von den Listen sind 102 LEER, und {user:null} trifft eine
leere Liste nicht (418 statt 520). Diese 102 waren vor dem Schnitt fuer jeden
sichtbar und waeren mit dem engeren Zweig fuer NIEMANDEN mehr sichtbar gewesen.

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
Fix notifications

Broadcast-Zweig trifft auch die LEERE LISTE

An der Sammlung gemessen liegt `user` in drei Auspraegungen vor: objectId 7.130,
array 826, fehlend 418. Von den Listen sind 102 LEER — und {user:null} trifft eine
leere Liste NICHT ({user:null}=418, {$in:[null,[]]}=520). Diese 102 tragen dieselben
Kategorien wie die unadressierten und waren vor dem Schnitt fuer jeden sichtbar;
mit dem engeren Zweig waeren sie fuer NIEMANDEN mehr sichtbar gewesen.

$in statt $size:0 — dieselben 520 Treffer, aber indizierbar.

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
Fix security

Empfaenger-Schnitt fuer notifications an allen vier generischen Lesewegen

Der erzwungene Mandanten-Schnitt ist bei notifications zu grob: der Verteiler legt
EIN Dokument JE EMPFAENGER an, die SDUI-Tabelle zeigte also die Post der Kollegen.
Neu: {$or:[{user:<ich>},{user:null}]} — eigene ODER unadressierte, nie fremde.

Der Schnitt liegt am SERVER, nicht in der basePipeline des Zustands: die reist als
Teil der Aufrufer-Pipeline mit und kann weggelassen werden. Verdrahtet an allen
vier Lesewegen (aggregation, export, queries x2) — ein ungeschuetzter waere die
Umgehung aller anderen, und der Export gibt die Zeilen ausser Haus.

20 Proben, vier Flip-Proben (Feldname / Broadcast-Zweig / Export-Verdrahtung /
Einschub im Hauptweg).

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
Fix notifications

PWA-Glocke empfaenger-gescopet statt mandanten-gescopet

unreadNotifications + page.notifications lasen {_customerID, markread:false} —
der Verteiler legt aber EIN Dokument JE EMPFAENGER an, die Glocke zeigte also die
ungelesene Post der Kollegen. Neu: eigene ODER unadressierte, nie fremde.

Zweite Haelfte und ohne sie waere die erste wirkungslos: der Zwischenspeicher-
Schluessel traegt jetzt userId statt customerId — sonst fuellt der erste Nutzer
den Eintrag und jeder weitere bekommt 30 s lang dessen Post, ohne Fehler.

11 Proben, beide Haelften einzeln flip-belegt (6 rot / 2 rot).

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
Fix notifications

Empfaenger-Schnitt auch auf den zwei Mengen-Schreibwegen (PUT /many, updateMode many)

changeMany/updateMany bauen ihren Filter intern aus {_id} und nehmen keine
zusaetzliche Bedingung entgegen — der Schnitt liegt deshalb auf einer Vorab-Lesung
(filterIdsToRecipient). Fremde Kennungen fallen still heraus; trifft die Verengung
nichts, wird kein leerer bulkWrite gefahren (waere ein 500).

Rest-Schnitt zum P1 notifications-nutzer-scope (2026-08-23).

Session-Id: notifications-nutzer-scope-restschnitt-startup-aggregation-20260823-1739
New experiments

Baustein-A/B-Tests werden ausgespielt

Bis hierher wendete KEIN Pfad einen `level:'block'`-Override im DOM an: der
Builder legt die Tests seit dem 2026-08-23 an, die Lose werden gezogen — aber
jeder Besucher sah die Kontrolle, es entstanden 0 Impressionen, und nichts
meldete einen Fehler.

- `blockExperiments.ts` — findet die Baustein-Region ueber das Marken-Paar,
das der cms-Renderer beim Backen setzt, und ersetzt sie durch die
Varianten-Behaelter. Dieselbe Mechanik wie `applyPageExperiment`, eine Ebene
tiefer; der Zuweisungs-Algorithmus ist UNVERAENDERT (die drei Kopien bleiben
byte-gleich).
- `getBlockExperiments` — mehrere je Seite, je Baustein hoechstens eines,
derselbe Mandanten-/Website-Riegel und dieselbe Haltedauer wie beim
Seiten-Zwilling.
- Verdrahtet in `pages.ts` UND `home.ts`. Die Startseite wird NICHT von
`pages.ts` gezeichnet — wer nur eine der beiden verdrahtet, laesst die
wichtigste Seite aus (dieselbe Falle wie bei W7).

⚠ Die Marken BLEIBEN im HTML stehen, der Behaelter kommt dazwischen — sonst
waere die Region nach dem ersten Ersetzen weg.

⚠ Laufzeit-Definition genau EINMAL je Seite: traegt die Seite schon ein
Seiten-Experiment, kommen von hier nur noch die Aufrufe (`includeRuntime:false`).

11 neue Proben, darunter die woertliche Marken-Form (Vertrag ueber zwei
Repositorien) und eine Positiv-Kontrolle „ohne Experiment byte-identisch".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New webpages

Block-Marken im gebackenen HTML + render-block-variant

Die zwei cms-Haelften der Baustein-A/B-Ausspielung.

1) `renderBlocksToHtml` legt um jeden TOP-LEVEL-Baustein ein Marken-Paar
`<!--ay-block:<_id>-->…<!--/ay-block:<_id>-->`. Ohne einen stabilen
Schluessel im HTML kann die Auslieferung eine Baustein-Region nicht
wiederfinden — genau daran scheiterte `target.blockKey` bisher.

Bewusst HTML-KOMMENTARE statt des vorgeschlagenen `data-ay-block`-Attributs:
ein Wrapper-Element aenderte das DOM (Geschwister-Selektoren, Raster) und
braeche die Regressions-Zusicherung; ein Attribut am aeussersten Element
setzte voraus, dass jeder Baustein GENAU EIN Wurzel-Element liefert — ueber
die 47 Faelle des Zeichners ist das nicht zugesichert. Kommentare sind
DOM-neutral und begrenzen die Region exakt.

Der Hex-Riegel auf der Kennung ist kein Schoenheitsfilter: ein Schluessel mit
`-->` schloesse die Marke und liesse beliebiges HTML aus dem Kommentar.

2) `POST /webpages/:id/actions/render-block-variant` backt EINE Varianten-
Fassung eines Bausteins zu HTML. Gebacken wird beim ANLEGEN des Experiments,
nicht beim Ausliefern: die consumer-app rendert Bausteine nicht selbst, und
ein Backen beim Veroeffentlichen koppelte den Experiment-Lebenszyklus an das
Veroeffentlichen — wer ein Experiment startet, ohne die Seite erneut zu
veroeffentlichen, haette keine Variante im HTML. Dieselbe Form, die auf der
Seiten-Ebene bereits ausgebracht ist.

25 neue Proben (14 rein + Verdrahtung, 10 am Handler, alle mit Umkehrprobe).

⚠ Die Handler-Suite mockt jetzt getCustomerById/getDefaultEventConsumer/
remoteReplace: ohne sie lief EIN Fall 37 s in echte Netz-Zeitgrenzen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New podcast

Titelbild auf der Episoden-Karte, ueber einen geteilten Bauer

Die Episoden-Karte (Mediathek + Episodenliste) trug bisher kein Bild, waehrend
der Sendungs-Kopf eines hatte — die beiden Flaechen waren auseinandergelaufen.

- renderCoverBild(): die EINE Stelle, an der ein Sendungs-Titelbild entsteht.
renderSendungsKopf ist darauf umgestellt (war die einzige Kopie).
- PodcastWiedergabeKontext.coverBySendung: eine ZUORDNUNG sendungSlug -> Adresse,
kein Einzelwert. Die Mediathek zeigt Folgen MEHRERER Sendungen (sie filtert
nach sendungSlug, mediathek.ts:181/210); ein Einzelwert haette dort jeder
Karte das Bild derselben Sendung gegeben.
- Das Bild erscheint nur, wenn die Flaeche eine Zuordnung mitgibt. Sonst haette
die Mediathek ab sofort ein Raster aus Initialen-Kacheln gezeigt, obwohl es
kein einziges Bild gibt. Innerhalb einer bebilderten Flaeche traegt jede
Karte den Platzhalter, damit die Zeilen nicht springen.

7 neue Proben, beide Flip-Proben beissen (Bild-Ausgabe entfernt -> 3 rot;
Bauer umbenannt -> Wortgrenzen-Riegel rot). Volle Reihe 1735/1736 gruen.

Die Befuellung von coverBySendung liegt in routes/ und ist ein eigener Brief
(podcast-coverbysendung-befuellen) — bis dahin zeigt keine Flaeche ein Bild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New page-editor

Feld "Frühere Adressen" in der Seiten-Maske

Neben dem Adress-Kürzel ein mehrwertiges Feld (Chips, dem bestehenden
Schlagwort-Muster folgend): der Redakteur sieht die automatisch
fortgeschriebenen Vorgänger UND kann einen falsch angehängten Eintrag
entfernen — der abweichende Wert gewinnt serverseitig gegen die Automatik.

- eigene Kopie der Liste im Dialog, damit "Abbrechen" wirklich abbricht
(dieselbe Falle, die der Kommentar für die Schlagwörter beschreibt)
- eingetippte Adressen werden auf die gespeicherte Schreibweise gebracht
(kein führender/abschließender Schrägstrich) — sonst leitet die Adresse nie um
- alle vier Sprachen (de/en/it/es)

11 Proben: Schreibweise + Sprach-Vollständigkeit, je mit Positiv-Kontrolle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

Kanon-Wert verdict extend im Brief import-media-schreibt-in-den-heimat-mandanten

New webpages

Adress-Historie beim Slug-Wechsel automatisch fortschreiben

Haengt beim Speichern einer GEAENDERTEN Adresse die vorige an
IWebPage.formerSlugs an — die Schreiber-Haelfte der Umleitungs-Strecke,
deren Leser in der consumer-app seit 2026-08-23 lebt (alte Adresse -> 301).

Bauform der Nachbarn (routerWebPagesGuard / routerPodcastCover): custom-Router
VOR dem erzeugten Buendel, ergaenzt den Rumpf und faellt mit next() durch —
EIN Schreibvorgang, keine zweite Wahrheit. Entscheidung rein in adressHistorie.ts.

Zwei Befunde, die den Bau bestimmt haben:
- Kuration ist nur eine ABWEICHUNG vom gespeicherten Wert. Der Seiten-Editor
schickt beim Speichern den ganzen geladenen Datensatz zurueck; waere jedes
mitgeschickte formerSlugs schon Kuration, griffe die Automatik auf dem
einzigen Weg nie, auf dem sie gebraucht wird.
- PUT /webpages/many mit MEHR als einer Kennung kann die Automatik per Bauart
nicht bedienen (changeMany schreibt EIN gemeinsames Update an alle Kennungen;
ein Mengen-Operator wuerde von sanitizeUpdateBody entfernt). Bei genau einer
Kennung bedient, darueber gewarnt und nichts geschrieben.

31 Proben (15 rein, 13 ueber echtes HTTP, 3 Verdrahtung mit Positiv-Kontrolle),
4 Flip-Proben beissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix dashboard

Seiten-Filter-Leiste an den Dienst haengen (OnPush-Luecke)

Der Riegel in `syncDashboardConfig()` lief die ganze Zeit korrekt — gemessen: nach
dem Wechsel traegt `DashboardPageFilterService.layer` sauber `null`. Sichtbar blieb
der Fremd-Stand trotzdem, weil die Leiste `ChangeDetectionStrategy.OnPush` faehrt und
ihre Anzeige nur ueber GETTER auf den Dienst liest, ohne ihn zu abonnieren. Bei OnPush
prueft Angular ein Kind nur bei Referenz-Aenderung einer Eingabe, bei einem eigenen
Ereignis oder nach `markForCheck()`; beim Wechsel zwischen zwei Dashboards ohne
gespeicherten Filter trifft nichts davon zu (`@Input() layer` ist beide Male `null`),
und das `markForCheck()` am Ende von `syncDashboardConfig()` markiert die ELTERN-
Komponente, was ein OnPush-Kind nicht schmutzig macht.

Deshalb waren zwei Runden lang alle Proben gruen: sie messen `filter.layer`, also den
DIENST — und der war immer korrekt. Die Luecke sass zwischen Dienst und BILD.

- Leiste abonniert `pageFilter.layer$` und ruft `markForCheck()` (kein dritter Riegel:
der Zustand wird nicht erneut gesetzt, nur die Anzeige daran gehaengt).
- Optionen-Abruf nur bei geaenderten Filter-DEFINITIONEN, sonst laedt jede Bedienung neu.
- `ngOnChanges` ENTFERNT samt seiner fuenf Proben (Abschnitt B): er konnte den Fall per
Bauart nie fangen, und jeden Fall, den er wirklich abdeckte, deckt heute
`syncDashboardConfig()` ab.
- Neue Reihe misst das GERENDERTE Bild statt des Dienstes; beidseitige Flip-Probe
(vor dem Fix 1 FAILED, danach 6/6), Umkehrprobe (Dashboard MIT eigenem Filter behaelt
seinen Stand) und Gegenprobe (kein Optionen-Abruf pro Klick). 47/47 der vier
betroffenen Reihen gruen, `npm run build:stage` gruen.
Fix coord

Kanon-Werte im Brief mediathek-und-episodenkarte (verdict extend, surface fe-existing)

Fix coding

Rueckfall-Luecke des Ereignis-Stroms benannt + Waechter fuer die stream-json-Argumente

Der Glied-B-Fix (defaultAgentArgs) trug den ganzen Rueckkanal und hatte keine
einzige Probe: ein versehentliches Streichen der vier Zeichenketten faellt
nirgends auf, weil der Lauf gruen bleibt und nur der Strom verschwindet.

- agentArgs.test.mjs: 9 Proben (Bestandteile einzeln, Pfad-/Endungs-Aufloesung,
zwei Negativ-Kontrollen, Flip-Probe). In test:floors verdrahtet.
- structuredOutputMissing(): ein Dispatch MIT eigenen args behaelt sie
unveraendert und verliert dabei den Strom - das wird jetzt benannt statt
stillschweigend ueberstimmt.
- CLAUDE.md: der Strom-Vertrag samt empirischem Beleg gegen Claude-CLI 2.1.241.
New seeds

Xamine ACM-Onboarding-Prozess (Negativliste, App, Bundle-Rezept, Prozess-Vorlage)

New seeds

Onboarding-Mails in vier Sprachen + zwei Reseller-Anlage-Vorlagen

Kaskade reseller-onboarding-prozesse 2026-08-23: die vier Onboarding-Vorlagen (welcome-customer,
welcome-user-new-tenant, invite-user-to-tenant, invite-customer-from-reseller) tragen en_US/it_IT/es_ES
als Uebersetzungs-Nachtrag (Sync-Schleife schreibt je Sprache nur fehlende Schluessel — betreiberfest).
Neu: reseller-customer-created-notify (Betreiber + Prozess-Empfaenger) und reseller-customer-create-result
(Abschluss an den Anlegenden), beide viersprachig. Waechter: Platzhalter-Paritaet je Uebersetzung gegen die
Grundfassung (test/seed-onboarding-mail-translations.test.mjs, fing 2 echte Abweichungen).
Fix ayauto

Zeilenenden von src/index.ts zurueck auf LF

Der vorige Commit hat die Datei per Python-Schreibvorgang im Textmodus von LF auf
CRLF gedreht — aus 49 hinzugefuegten Zeilen wurde ein Diff ueber 10281. Der Blob
trug vorher 0 CR-Bytes, danach 5165 (genau eins je Zeile). Inhalt unveraendert;
gegen den Vor-Stand bleiben die 49 additiven Zeilen der su-Gruppe.

Die Falle steht in der Wurzel-CLAUDE.md: ein Python dreht unter
Windows die ganze Datei.
New ayauto

`ayauto su` — Betreiber-Kanal fuer operator-only-Module

Es gab zwei Betreiber-Kanaele und fuer `su` ihre Schnittmenge NICHT: `cli_call`
(ueber `ay`) kann mutieren, erreicht `su` aber nicht (CEO-Entscheid 2026-06-20,
Modul-Riegel VOR jeder Aufloesung); `api_call` (MCP) erreicht `su`, sperrt aber
jede Nicht-GET-Methode in Produktion. Serverseitig ist der Zugriff offen — die
Sperre lag ausschliesslich im Werkzeugkasten.

🔴 Die Regel von 2026-06-20 wird NICHT aufgeweicht: `tooling/cli` ist unangetastet
und erreicht `su` weiterhin nicht (gemessen, siehe Gegenprobe). Der Zugriff wandert
aus der ausgelieferten CLI in die interne, die per CEO-Regel 2026-08-10 nie
ausgeliefert wird.

Form uebernommen von `ayauto queue`: benannter Datensatz, kein Platzhalter,
confirm-gated. Ohne --confirm ein Trockenlauf, der NICHTS schreibt.

- `ayauto su get <entitaet> <id>` / `set <entitaet> <id> --field --value [--confirm]`
- Rueckroll-Abbild VOR jedem Schreibvorgang, das festhaelt, ob das Feld vorher
ueberhaupt existierte — der Rueckweg fuer ein ABWESENDES Feld ist ein $unset,
nicht ein `false`; sonst bliebe ein Wert stehen, den der Bestand nie kannte.
- Token aus --token / AYAUTO_API_TOKEN / angemeldeter ay-CLI ueber den GETEILTEN
Lader (keine zweite Umsetzung); wandert nie in den Baum.
- Schreibweg ist `PUT /<plural>` mit der Kennung im RUMPF (der erzeugte CRUD hat
kein `PUT /<plural>/<id>`); verifiziert wird am DATENSATZ, nicht an der Antwort.

Verify gegen Produktion (alle fuenf Tore des Briefes):
- ohne --confirm: `updatedAt` am Datensatz UNVERAENDERT (in der DB gemessen)
- mit --confirm: `updatedAt` 13:45:47 -> 14:25:22, Wert wertneutral true->true
- ohne Token: Exit 2 mit `NOT-READY:kein-betreiber-token — <grund>`
- Gegenprobe: `ay` sagt weiterhin "Module su is operator-only and is not reachable
from the customer-facing ay CLI"
- Rueckroll-Abbild geschrieben, 0 Token-Vorkommen, unterscheidet abwesend von false

Proben 14 gruen; Flip-Probe (abwesend wie vorhanden behandeln) faerbt 2 rot.
New cache

Override-Leerung pod-uebergreifend (Redis-Bus)

Gemessener Defekt (2026-08-23, marketing, zwei Pods): L4 (Override-Inhalt, 60 s)
und L5 (Override-Existenz, 5 min) sind lru-cache-Instanzen IM PROZESS. Ein
PUT/DELETE auf eine Override-Ebene leert nur den Pod, der die Anfrage bekommt —
nach einem DELETE zeigten 8 von 12 unmittelbar folgenden Abfragen den
geloeschten Override weiter, bis zu 5 Minuten lang.

Neu: cache/overrideInvalidationBus — dieselbe Bauform, die core fuer
ayoune:rights-registry-updated und ayoune:translations-updated schon faehrt
(Redis-pub/sub, JOB_REDIS_*, still ohne Redis). Kanal
ayoune:states-override-invalidated. Verworfen: ChangeStream auf der Sammlung
(deckt L5 nicht ab, braucht ein Replica-Set-Recht), Haltezeit 0, Warten im Client.

Alle VIER Schreibwege gehen jetzt ueber den einen Aufruf burstOverrideCaches:
DELETE user (services/userOverrideReset) und PUT user/reseller/agency
(routes/state.ts). Ein Waechter haelt fest, dass kein Schreibweg mehr nur
lokal leert. Haltezeiten unveraendert.

13 neue Proben (235/235 gruen). Flip-Proben beidseitig gefahren: ohne Sender
faellt Probe 8 (zwei Pods), ohne Boot-Verdrahtung Probe 13.

ioredis als DIREKTE Abhaengigkeit, exakt 5.11.1 wie core — die transitive
Aufloesung unter core ist keine Zusage; dedupliziert zu einer Kopie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01415vhKTqP7zhWwyf4xT7xn
New humanizer

Serien-Schnitt neben dem Je-Text-Pruefer

Der bestehende Pruefer misst 33 Muster JE TEXT und kann den geteilten Bauplan
einer SERIE per Bauart nicht sehen. Gemessen: er gibt jedem der 35 maschinell
erzeugten Mitarbeiter-Profile 100/100 - der neue Serien-Schnitt gibt derselben
Menge 61.

Vier Achsen ueber die Menge: gleiche Eroeffnungsform, wiederkehrende rhetorische
Figur (3 Figuren, je eigene Schwelle), Bekenntnis-Schluss, zu enges Wortband
(Spanne/Median). Unter 3 Texten schweigt er, statt eine Zahl zu erfinden.

Jede Schwelle ist an DREI echten Mengen aus der Produktion geeicht, keine
geraten: 35 maschinelle Profile (muss durchfallen, 61), 6 von Hand umgebaute
Team-Texte (muss bestehen, 100), 11 echte Kundenstimmen (muss bestehen, 100).
Menge C ist der Waechter gegen die eigentliche Gefahr dieses Schnitts - er darf
GENRE nicht fuer Maschinen-Duktus halten.

Zwei Detektoren trennten gemessen in die FALSCHE Richtung und sind daher
gestrichen bzw. umgebaut: der einraeumende Nachsatz (Maschine 0.11, Mensch 0.33)
und die Schluss-Signatur-Gruppierung (Maschine 0.43, Kundenstimmen 0.73 - sie
mass die Gleichfoermigkeit eines Genres).

Der Je-Text-Pruefer ist unveraendert (leerer Diff auf allen vier Bestandsdateien).
26 neue Proben inkl. Umkehr-Kontrolle je Detektor; 3 Flip-Proben gefahren, alle
beissen an der erwarteten Stelle. test:humanizer 177/177, test:floors ergaenzt.

Noch nicht verdrahtet: scoreSeries hat keinen Aufrufer, runHumanize arbeitet je
Dokument und kennt keine Menge.
New page-editor

dynamische UCD-Rolle je Baustein anbieten und anzeigen

Der Bauer bot `settings.id` bisher UEBERHAUPT NICHT an (15 Schluessel, `id` keiner
davon) — ein Redakteur konnte einen Baustein also nicht einmal von Hand als
dynamisch markieren, obwohl der Renderer die Kante seit 2026-08-23 traegt.

- `ucd-slots.ts`: dritte Kopie der 13 Vertrags-Namen mit Klartext-Beschriftungen;
angeboten werden nur die 7, die eine DOM-Kennung brauchen. `h1`-`h6` bewusst
nicht — der Klient findet sie ueber den Element-Namen.
- Panel "Kennung & dynamischer Inhalt": Auswahl der Rolle PLUS Freitext-Anker.
Beide schreiben auf dieselbe `settings.id`, deshalb sperrt eine gewaehlte Rolle
das Anker-Feld — die Grenze "ein Baustein traegt genau eine Kennung" wird von
der Oberflaeche gezeigt, nicht in einer Fussnote erklaert.
- Dauerhafte Marke am Baustein auf der Leinwand (nicht erst beim Ueberfahren),
nur ein Merkmal, keine eigene Aktions-Leiste.

Proben: 10 neue am gerenderten Bauteil + 7 auf der Liste; Gruppe 95/95 gruen.
Vier Flip-Proben beissen (h1 in der Auswahl 4 rot, Freitext als Rolle 2 rot,
Marke aus + Anker-Sperre aus 3 rot). Renderer-Probe cms-api 11/11 unveraendert.
Fix multi-select

aufgeloeste Verweis-Werte als ausgewaehlt zeigen

Der states-Dienst liefert Verweis-Felder POPULIERT (Liste von Objekten), die
Optionen tragen dagegen die Kennung als Wert. mat-select vergleicht ohne
compareWith per === — Objekt gegen Zeichenkette trifft nie, also war nichts
ausgewaehlt, obwohl der Wert da war. Im Browser am Feature "Anhang (HGB)"
gemessen: 21 Optionen geladen, 0 als ausgewaehlt gefuehrt.

Nicht nur Anzeige: mat-select multiple schreibt beim Speichern die Werte
zurueck, die es kennt — eine einzige Auswahl haette die unsichtbare
Zuordnung still ersetzt.

Der Wert wird deshalb vor dem ersten Rendern auf Kennungen zurueckgefuehrt
(emitEvent: false, kein dirty — die Maske speichert selbsttaetig auf
valueChanges). Bewusst nicht ueber compareWith: das haette allein die
Anzeige geheilt und weiter Objekte zum Server geschrieben.

Proben: 5 neue in multi-select-zeigt-gespeicherten-wert.spec.ts, davon zwei
als Flip-Probe belegt (vorher rot, nachher gruen); Reihe 30/30.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New ucds

Pflegeflaeche fuer dynamische Inhalte — Liste + Bearbeitungsmaske

Der Zustand marketing.ucds existierte, war aber seit 2024 deprecated und trug eine
reine Selbst-Erzeugung als viewConfig: die Spalten hiessen 'desktop' und 'tablet'
(die BLAETTER des h1-Unterobjekts) und zeigten garantiert nichts — weder key noch
url noch topic waren sichtbar. Eine Bearbeitungsmaske fehlte vollstaendig, die
Alt-Flaeche antwortete mit 404. Wer einen dynamischen Inhalt pflegen wollte, hatte
plattformweit keinen Weg dorthin.

Liste: Alt-Untergrenze aus old-ui-inventory (Schluessel · Adresse · Thema · Region ·
PLZ), Schluessel vorn, weil er in der Anzeigen-Adresse reist.
Maske: 49 Felder in sechs Abschnitten — Zuordnung, Platzhalter (var1-4 mit dem
Hinweis, dass sie Ersetzungen sind, keine Anzeige-Schlitze), dann die 13 Schlitze
je Geraeteklasse nebeneinander.

Kein headerKpis: die Sammlung traegt 10 Dokumente, davon 9 Pruefdaten des
naechtlichen Laeufers (gemessen 2026-08-23).

Drift-Sentinel nachgezogen: 43->44 states, 34->35 Tabellen, 19->20 Edit-Spezifikationen.
⚠ Hier wandert nur die Tabellen-Zahl mit — das Bearbeitungs-Gegenstueck lebt in der
anderen Datei; wer beide im Gleichschritt erwartet, sucht ein Formular, das hier
per Bauart nie stand.

Proben: 6316 gruen. Der Feld-Pruefer liest alle 49 neuen Felder — per Flip belegt
(erfundenes h1.gibtEsNicht -> rot mit Datei:Zeile). Umlaut-Waechter fing vier
ASCII-Ersatzschreibungen, geheilt.
Fundament-Frische mitgezogen (core 315.2, interfaces 453.0, models 458.0).
New pages

Adress-Historie leitet mit 301 statt 404 (formerSlugs, Kaskade 56)

Ein Adress-Wechsel einer CMS-Seite hinterliess bis hierher eine tote Adresse:
`/de/alte-adresse` antwortete 404 ohne `location`-Kopf, jeder externe Verweis
und jeder Suchmaschinen-Eintrag lief ins Leere, und die aufgebaute Bewertung
wanderte nicht mit. Der Fundament-Anteil (`IWebPage.formerSlugs` + Schema-
Zwilling + sparse-Index `{_customerID,_website,formerSlugs}`) liegt seit
Kaskade 56 vor und hatte bis jetzt keinen Leser.

- `getPageByFormerSlug` — gezielter Nachschlag, mandanten- UND website-gebunden,
nur veroeffentlichte Seiten, beidseitig zwischengespeichert (auch das negative
Ergebnis: eine frei waehlbare 404-Adresse darf keinen un-gecachten
Datenbank-Zugriff ausloesen). Fail-soft.
- `matchesFormerPageSlug` / `formerSlugRedirectTarget` — rein + exportiert, damit
die Proben die ECHTE Aufloesung pruefen statt einer Kopie.
- `resolveAndRender`: der Nachschlag steht VOR dem 404 und **301**, nicht 302 —
nur der dauerhafte Code laesst Suchmaschinen die Bewertung uebertragen. Der
bestehende Sprach-Rueckfall bleibt bewusst 302.

RANGFOLGE strukturell statt per Vergleich: der Nachschlag laeuft ausschliesslich
in einem `if (!page)`-Zweig, eine Historie kann eine lebende Adresse also nicht
ueberstimmen. Das ist der Unterschied zum Blog-Vorbild (`resolveBlogDocBySlug`),
das alle Beitraege als Liste haelt und deshalb zwei Durchgaenge braucht.

13 Proben, alle fuenf Flip-Proben beissen je genau eine Zusage: 301->302,
`if(!page)`-Klammer weg, `slug` aus der Projektion, Selbst-Schleifen-Riegel weg,
Trim weg. Volle Reihe 1698/1698. Negativ-Kontrollen belegen, dass der heutige
Bestand (0 von 372 Seiten mit Historie) unveraendert ausgeliefert wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New codegen

Riegel gegen den Sammel-Mount eines Rumpfes, der eine Kennung braucht

`actionAddressPlacement()` sagt, WO eine Aktion hingehoert — es sagt NICHT, ob der
vorhandene Rumpf dort bedienbar ist. Fuer `crm.Consumers` hat der Erzeuger sechs
`both`-Aktionen bereits EINMAL an die Sammel-Adresse gemountet (Commit 5bc61ad);
ihre Ruempfe lesen `req.params.id`, und Mongoose entfernt `_id: undefined` still
aus dem Filter — der Zugriff traefe einen BELIEBIGEN Datensatz des Mandanten.
Entdeckt wurde es von Hand, verhindert seither nur durch einen Hand-Haertungs-
Marker an der Router-Datei, also durch ABWESENHEIT der Route.

Neu: `collection-safety.ts` prueft den BESTEHENDEN Rumpf vor dem Sammel-Mount.
Kein `req.params.id` -> sicher. Mit kanonischem Waechter -> sicher. Datei existiert
noch nicht -> sicher (die Vorlage traegt den Waechter). Sonst wird der Mount
VERWEIGERT und AUSGEWIESEN (`[skip-unsafe-collection-mount]`), mit Grund UND
Ausweg — ein stilles Auslassen waere dieselbe unsichtbare Mechanik wie das stille
Setzen.

🔴 KOMMENTAR-BEWUSST, und das ist die teuerste Falle: `actionStreamRegenerate`
(cms.Streams) erwaehnt `req.params.id` VIER MAL in seinem Kopfkommentar, um zu
dokumentieren, dass es die Kennung eben NICHT von dort liest. Eine rohe Textsuche
verweigert dort einen Mount, der voellig in Ordnung ist — nach dem zweiten solchen
Fehlalarm schaltet jemand den Riegel ab.

FAIL-CLOSED mit ausdruecklichem Ausweg: erkannt wird nur die kanonische Waechter-
Form der Vorlage; wer einen anderen Stil faehrt, setzt den Marker `✅ SAMMEL-SICHER`.
Ein Irrtum laeuft damit immer in die sichere Richtung.

⚠ Beim Bau selbst getroffen: eine dynamisch gebaute Regex ueberlebt weder `node -e`
noch ein Heredoc (`'if\s*\('` wurde zu `'if\s*\('`, die Regex las `ifs*(`, und
der Waechter fand NIE einen Waechter). Die Waechter-Erkennung laeuft deshalb ueber
einen leerraum-normalisierten Zeichenketten-Vergleich, backslash-frei.

Proben: 20 Faelle mit Umkehrproben (mit Waechter schweigt der Riegel), zwei
Kommentar-Fallen samt Positiv-Kontrolle zum Strippen, Negativkontrollen (Waechter
auf ANDERER bzw. LAENGERER Variable zaehlt nicht; ASCII-Ersatzform des Markers
traegt nicht), sowie Proben gegen den ECHTEN Bestand und die ECHTE Vorlage.

Verify-Gate am echten Erzeuger beidseitig gefahren (`codegen routes --module crm
--dry-run`): alle sechs gehaertet -> kein verweigerter Mount; einen Waechter
temporaer entfernt -> Mount VERWEIGERT mit benanntem Grund; byte-genau
wiederhergestellt. Reihe 4830 (+20), die 6 roten sind beidseitig als vorbestehend
belegt (Fehlerlisten vor/nach identisch).

Brief: crm-consumers-both-aktionen-und-sammel-rechte-fundament-fix
Session-Id: crm-consumers-both-aktionen-und-sammel-rechte-fundament-fix-TBD

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LmDa2yAaQZtJrAss6EuZ8
Fix consumers

entityId-Waechter fuer die sechs both-Aktionen — 501 statt beliebigem Treffer

Die sechs `availability: 'both'`-Aktionen (archive, ignore-in-merge, blacklist,
add-to-list, assign-segment, export) lesen `req.params.id` und schreiben mit
`{_id: entityId, _customerID}`. An der Sammel-Adresse gibt es keine Kennung, und
Mongoose ENTFERNT `_id: undefined` still aus dem Filter statt zu werfen — der
Zugriff traefe damit einen BELIEBIGEN Datensatz des Mandanten, ohne Fehler und
ohne Protokollzeile.

Geschuetzt waren sie bis hierher nur durch die ABWESENHEIT der Sammel-Route: ein
Hand-Haertungs-Marker haelt den Erzeuger davon ab, sie dort erneut zu mounten.
Das war noetig, weil genau das schon EINMAL passiert ist (Commit 5bc61ad).
Abwesenheit ist keine Abwehr — die Ruempfe tragen jetzt selbst den Waechter aus
der Erzeuger-Vorlage (`actionStub.ts.tpl:64-82`) und antworten ehrlich 501.

Verhalten mit gueltiger Kennung ist unveraendert (reine Einfuegung, 6x16 Zeilen).

Proben: `sammelAdresseWaechter.test.ts` — je Aktion das PAAR aus Abweisung ohne
Kennung (KEIN Datenbank-Zugriff, KEIN Ereignis) und UMKEHRPROBE mit Kennung
(regulaerer Weg laeuft), plus zwei Negativkontrollen (leere Kennung wird ebenso
abgewiesen; der Waechter kommt beim isValid-Ausstieg nicht zum Zug). 14 Faelle.
Flip-Probe gefahren: Waechter aus actionConsumerArchive entfernt -> 2 rot, nach
byte-genauer Wiederherstellung 14/14. Volle Reihe 1878/1878.

Der Marker-Kommentar ist nachgezogen: er behauptete, die sechs seien draussen,
WEIL ihre Ruempfe die Kennung binden. Das gilt nicht mehr — sie bleiben draussen,
weil ein 501-Weg kein Nutzen ist; wer die Sammel-Form braucht, implementiert sie
mit Kennungen im Rumpf und aendert `availability` auf `many`.

Brief: crm-consumers-both-aktionen-und-sammel-rechte-fundament-fix
Session-Id: crm-consumers-both-aktionen-und-sammel-rechte-fundament-fix-TBD

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012LmDa2yAaQZtJrAss6EuZ8
New pages

UCD-Schlitze landen am Inhalts-Element statt am Abschnitt

Der Klient tauscht dynamische Inhalte per getElementById. Eine gesetzte Block-Kennung
ging bisher immer ans <section> — fuer einen UCD-Schlitz die falsche Ebene: der Tausch
ersetzt dann das innerHTML des GANZEN Abschnitts samt Innen-Huelle und Knopf-Klassen.

blockIdPlacement() trennt beide Bedeutungen: ein normaler Sprungziel-Anker (#kontakt)
bleibt unveraendert am Abschnitt, ein UCD-Schlitz wandert an das inhaltstragende
Element — beim Knopf-Schlitz an den Knopf, sonst an die Inhalts-Huelle. h1-h6 stehen
bewusst NICHT in der Liste: der Klient findet sie ueber den Element-Namen.

Bewusst KEIN neues settings-Feld: PageBlockSettingsSchema ist strict, ein Feld waere
Fundament-Anteil. Getragen wird die Markierung von settings.id (Vertrag + Schema
vorhanden). Grenze daraus: ein Block = hoechstens EIN dynamischer Schlitz.

Proben: 886 gruen in der pages-Reihe (11 neu). Flip gefahren — Kante entfernt: 5 rot,
die beiden Rueckwaerts-Proben blieben gruen.
Fix ucd

Mandanten-Riegel am dynamischen Inhalt + Schlitz-Setter nach Element-Art

Der UCD-Nachschlag fragte `UCDs.findOne({key})` ueber ALLE Mandanten hinweg. Der
Schluessel reist offen in der Adresse (?ayoune_ucd=), `key` traegt keinen
Eindeutigkeits-Riegel, und der zweite Zweig ({url,interest}) nimmt beide Werte vom
Klienten — eine fremde Adresse genuegte fuer fremde Inhalte. Filter-Bau jetzt in
tracking/ucdHelpers.ts, fail-closed (ohne _customerID kein Nachschlag, nie ein
leerer Filter).

Zweiter Aufrufer ({url,interest} im Interessens-Handler) war entgegen der
Brief-Annahme LIVE — vom Compiler gefangen, mit verriegelt.

idcadd.js: die Knopf-Schlitze setzten ausschliesslich .value. Der neue Seiten-Bauer
zeichnet Knoepfe als <a class=ay-btn>, wo .value wirkungslos ist — ohne Fehler und
ohne Warnung. ayoune_setSlot entscheidet ab jetzt nach Element-Art.

Proben: 523 gruen (15 neu). Flip beidseitig gefahren — Riegel entfernt: 5 rot;
Element-Art-Zweig entfernt: 2 rot.
New applications

der Loeschantrag aus dem Portal bekommt einen Leser (W6, Art. 17)

Das Bewerber-Portal setzt erasureRequestedAt seit W3 und sagt im eigenen
Kommentar, der Vollzug sei W6. Bis heute hatte das Feld KEINEN Leser: der
Bewerber klickte, und danach geschah nie etwas.

Ein Antrag sticht die restliche Frist, aber NICHT die Schutzregeln — eine
eingestellte Bewerbung und eine LAUFENDE bleiben unberuehrt (wer zuruecktreten
will, nutzt withdraw; danach laeuft die Frist). Beide Faelle werden benannt
gemeldet statt verschwiegen.

Antraege werden getrennt geholt und entdoppelt, damit der Bericht 'auf Antrag'
von 'Frist abgelaufen' unterscheidet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New voice

Anbieter-Wahl je Anruf statt eines festen Adapters fuer den Dienst

`stream.ts` erzeugte beim Modul-Laden EINEN `DeepgramSttProvider` fuer den
ganzen Dienst; der `ICallAgent.sttProvider`-Feldwert wurde NIRGENDS gelesen.
'deepgram' war damit blosser Zufall — es war der einzige Wert, der je vorkam,
nicht eine getroffene Wahl. Ein Agent mit `sttProvider: 'whisper'` bekam
trotzdem Deepgram.

- Der Handler waehlt den Adapter jetzt je Anruf ueber `sttProviderFor(name)`
aus dem Feldwert des Agenten. Das macht auch 'deepgram' erstmals zu einer
echten Wahl.
- `deps.stt` bleibt und SCHLAEGT die Wahl — das ist der Injektionspunkt der
bestehenden 617 Proben, die damit unveraendert gruen bleiben.
- Ohne Feldwert (und ohne Agenten am Anruf) bleibt es bei 'deepgram': die zwei
heutigen Agenten verhalten sich unveraendert, keine Migration noetig.
- Die Mandanten-Kennung reist bis zum Adapter durch — der Whisper-Weg loest
seinen Schluessel aus dem Tresor DIESES Mandanten auf.

6 neue Proben; zwei Flip-Proben belegen, dass sie beissen (Feldwert ignorieren
bzw. Mandanten-Kennung weglassen -> je genau die zugehoerige Probe faellt).

BEZUG: braucht @tolinax/ayoune-core mit `ISttOpenOptions._customerID`. Bis zu
dessen Veroeffentlichung schlaegt der Bau fehl — das ist die sichere Richtung,
kein stiller Schaden.
New applications

Recruiting-Kennzahlen + Fristen-Status an der Flaeche (W6 Reporting)

Time-to-Hire, Trichter je Stelle/Stufe, Absagequoten nach Kategorie — 13 Proben.

Gemessener Grund, warum das EIN Endpunkt ist und nicht drei Widget-Definitionen:
Time-to-Hire ist die Differenz zweier Datumsfelder, der generische Widget-Motor
kennt nur Akkumulatoren ueber EINEM Feld, und die Fluchttuer dataSource.pipeline
liest allein der Prerender-Worker (nicht platform/states). Drei Kennzahlen ueber
zwei Wege WAEREN das Parallel-System.

Flaeche: Loeschfrist + Anonymisierungs-Stempel als Spalten, Zeitraum-Filter,
Schnellfilter Anonymisiert. actionsForEntity(Applications) jetzt verdrahtet — die
Bedingung des frueheren Kommentars ist erfuellt, alle sechs Aktionen sind echt.

Gemessen statt gezaehlt: ein grep auf refuseUnimplementedAction liefert 11
Treffer, davon sind 8 Kommentare; die Messgroesse ist der IMPORT.

W2-Waechter mitgezogen und seine Positiv-Kontrolle auf applicants/merge
umgehaengt — sonst belegten die sechs not.toContain nichts mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New voice

Whisper-Spracherkennung als echter stroemender Adapter

Der `WhisperSttProvider` war ein Platzhalter, der beim ersten Verbinden warf
("TODO post-W2") — ein Agent mit `sttProvider: 'whisper'` brach jeden Anruf ab.

ANBIETER-ENTSCHEIDUNG (gemessen, nicht stillschweigend): das rohe Whisper-
Endpunkt ist Batch und kann `interim` per Bauart nicht liefern; die gehosteten
Stapel-Beschleuniger (Groq/Fireworks/Replicate) puffern die volle Aeusserung;
Selbstbetrieb verlangte einen eigenen Dienst samt GPU. Gewaehlt ist die
stroemende Transkriptions-Betriebsart der OpenAI-Realtime-Schnittstelle — der
einzige Weg, der `interim` bedient UND dessen Anbieter (`openai`) BEREITS im
Zugangsdaten-Katalog des Fundaments steht.

- Ton-Format g711_ulaw: Twilios mu-law 8 kHz reist ohne Umrechnung durch.
- Schluessel AUSSCHLIESSLICH aus dem Tresor des Mandanten, ausdruecklich OHNE
Rueckfall auf eine Umgebungsvariable — anders als die Praezedenz
`resolveElevenLabsKey`. Ein stiller Rueckfall rechnete den Anruf eines
Mandanten ueber den Schluessel eines anderen ab.
- `ISttOpenOptions` traegt dafuer `_customerID`/`requestingUserId` (optional,
der Deepgram-Weg bleibt unberuehrt).
- Fehlende Anbieter-Angaben (Guete, Zeitachse) bleiben `undefined` bzw. werden
aus der Sitzungs-Uhr abgeleitet und als Naeherung kommentiert.

15 neue Proben; zwei Flip-Proben belegen, dass sie beissen (ENV-Rueckfall
eingebaut -> genau die Tresor-Probe faellt; roher Rahmen statt base64 -> genau
die Uebertragungs-Probe faellt), je ohne Kollateral.
New monitoring

Aufbewahrungs-Lauf fuer Bewerbungen (W6 DSGVO)

Ruft den hr-api-Endpunkt je Mandant (Dienst-Zeichen traegt den Mandanten, nie
eine Abfrage — bei einem unumkehrbaren Loesch-Lauf die tragende Sicherung).
Regel und Vollzug bleiben in hr-api, hier steht nur der Zeitgeber.

Meldet die zwei Faelle, in denen NICHT vollzogen wurde: Mandant ohne
rechte-tragenden Nutzer (403 — sonst laufen seine Fristen fuer immer ab und der
Waechter sieht gruen aus) und fehlgeschlagener Lauf. NICHT je abgelaufener
Bewerbung — Fristen laufen ab, das ist der Normalfall.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New selfhost

eigene Abbild-Registry auf Hetzner statt Google Artifact Registry

CEO-Entscheid 2026-08-23 (Weg a) widerruft Weg a' vom 2026-08-06: eine
Selbst-Hosting-Loesung darf zum Beziehen ihrer Abbilder nicht von Google
Cloud abhaengen.

Neu: infrastructure/hetzner-registry-docker-images/ nach dem Muster des
npm-Registry-Hosts — registry:3 + Caddy, htpasswd-Zugang, cloud-init als
Quelle der Wahrheit, taegliche Sicherung, RESTORE.md.

Umgestellt: selfhost-release.mjs (Ziel jetzt zweiteilig, Quelle unveraendert
GCP), selfhost-customer.mjs (htpasswd ueber SSH statt IAM-Dienstkonto),
docker-compose.yml (65 Zeilen), SELFHOST-RELEASE-RUNBOOK.md neu geschrieben.

Proben 28 -> 32 gruen; Flip-Probe auf das alte Ziel-Muster faellt genau
einmal. assertSafeHtpasswd weist -c ab (loeschte sonst jeden Zugang) —
eine eigene Probe fing dabei, dass die erste Fassung jeden ENTZUG blockiert
haette.

NICHT ausgerollt: die Anlage der kostenpflichtigen Maschine und der
DNS-A-Eintrag liegen als operator-residual (high) vor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LiqTwHE7HSdHBaWhhZ9jeZ
New states

Rueckstell-Weg DELETE /:state/user fuer die Nutzer-Override-Ebene

Die Nutzer-Ebene ersetzt den viewConfig vollstaendig, hatte aber keine
Gegenrichtung: schon ein leerer Override ueberdeckte die Plattform-Vorgabe
dauerhaft (live eingetreten bei reporting.executive, 2026-08-23).

- resetUserOverride in services/userOverrideReset (eingespritzte Abhaengigkeiten,
ohne AY-Boot pruefbar), loescht erst und leert DANN beide Zwischenspeicher
- Filter traegt _userID UND _customerID = Mandanten-Riegel
- idempotent: zweiter Aufruf 200 mit deleted:false, leert trotzdem
- 7 Proben mit Positiv-Kontrolle, Verdrahtungs-Waechter und vier Flip-Proben
Fix logging

Betriebs-Protokoll des Arbeiters wieder sichtbar machen

Der Arbeiter war in `kubectl logs` vollstaendig stumm — null `ay.logger`-Zeilen,
obwohl er arbeitete (Gegenprobe: `domain-health` fuehrt abgeschlossene Auftraege).

Gemessen sind ZWEI unabhaengige Schwellen, und beide mussten fallen:

1. `logger.level: 'info'` (Stufe 3) ist die Schwelle des WURZEL-Protokollierers und
greift VOR jedem Ausgabeweg. `success` ist Stufe 5 — `Domain Workers started`
und die Abschluss-Zeile des Abgleichs erreichten damit WEDER die Standardausgabe
NOCH die Sammlung `ayounelogs` (dort 0 Zeilen der Stufe `success` fuer diesen
Dienst, waehrend die Flotte insgesamt 83.620 fuehrt).
2. Der Ausgabeweg auf die Standardausgabe steht in Kubernetes seit dem 2026-07-31
per Vorgabe auf `error` (`resolveConsoleLevel`, Buendel 20/A) — eine bewusste
Kosten-Abwaegung, kein Defekt.

Neu: `level: 'success'` + `consoleLevel: 'success'`. Die Schwelle ist eine
OBERGRENZE, also faellt `verbose` (8) heraus — genau richtig, denn das ist hier die
Zeile JE AUFTRAG und damit der Mengen-Treiber. Aufkommen rund 75 Zeilen je Stunde.

WIDERLEGT: die Boot-Reihenfolge ist NICHT die Ursache. Der `ay.on('ready')`-Rumpf
laeuft zwar vor `initLogger`, aber der Boot-Vor-Protokollierer schreibt in diesem
Fenster auf Stufe `silly` auf die Standardausgabe. Ein Verschieben nach hinten
wuerde die Stille VERSTAERKEN. Kommentar in server.ts haelt das fest.

Kern UNBERUEHRT — der Befund gilt fuer jeden Dienst in Kubernetes und gehoert in
einen Fundament-Platz, nicht in diesen Fix.
New selfhost

support-center-api aus dem support-Profil entfernen (CEO-Entscheid Weg a)

Der Dienst ist seit 2026-06-16 RETIRED, seit 2026-07-08 aus dem Cluster
entfernt und seine Auslieferungs-Kette seit 2026-07-25 abgeschaltet. Das
Selbst-Hosting-Buendel bot ihn unter --profiles support weiter an.

Auskommentiert statt nur die Profil-Zeile gestrichen: ein Dienst ohne
profiles: ist in Compose im Vorgabe-Satz und startet bei jedem up (an
Compose v5.3.1 gegengeprueft); ausserdem zaehlt parseComposeImages
profil-blind, das Gate 2->1 pausierte Ketten waere sonst unerfuellbar.

support-Profil 2 -> 1 Abbild, Audit 66 -> 65 Namen, pausierte Ketten 2 -> 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LiqTwHE7HSdHBaWhhZ9jeZ
New migrate

uptime-kpis-to-dailystats — einmalige Uebertragung der Uptime-Tageszeilen

globalkpis(typ:"check-uptime-daily") -> checkdailystats, CEO-Entscheid 2026-08-23
Weg B. Rein additiv: die Quelle wird NICHT angefasst.

🔴 Der Rechenweg wird aus platform/stats-generator/dist GELADEN, nicht nachgebaut.
Zwei Kopien derselben Feld-Zuordnung waeren eine Drift-Quelle, und die gefaehrliche
Richtung ist hier eindeutig: die Altzeile zaehlt ERREICHBARE (`count`), der Vertrag
FEHLGESCHLAGENE (`failed`) — vertauscht steht die Verfuegbarkeit auf dem Kopf.
Fehlt das dist, bricht der Lauf mit einer klaren Ansage ab, statt auf eine zweite
Umsetzung auszuweichen.

Ein Nachhol-Lauf des Verdichters kann das NICHT ersetzen: checkresults traegt
14 Tage Verfall, alles Aeltere existiert nur noch als Kennzahl-Zeile.

Gefahren gegen Produktion (2026-08-23), Ergebnis:
- 2.077 gelesen, 2.077 geschrieben, 0 uebersprungen
- zweiter Lauf: 2.077 -> 2.077, keine Verdopplung (gegen den materialisierten
unique-Index {_customerID,_check,day})
- verlustfrei gegengeprueft: sumTotal 9.839.313 = 9.839.313, failed 247.981 =
metrics.unhealthyRuns, Quer-Probe total-count ergibt dieselbe Zahl
- 28 Tage / 122 Pruefungen / 2 Mandanten deckungsgleich, 0 negative Werte

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sad6An2tuxWKtd9zHcVXqs
New karriere

Bewerbung schreibt die Dauerform statt einer FormSubmission

Die Karriere-Strecke legt seit dieser Aenderung `Applicants` + `Applications`
an, nicht mehr eine `FormSubmission` mit freiem `formdata._hrJob`. Der
Uebergangs-Traeger war als solcher gebaut (W4: die zwei Entitaeten existierten
damals gemessen nicht); seit Kaskade 61 gibt es sie.

- Prozess-Aufloesung nach der Kette Stelle -> Mandant -> Plattform, mit Pruefung
auf `entityCollection` (ein `Tasks`-Prozess darf eine Bewerbung nie fuehren).
- Anfangs-Status kommt AUS dem Prozess, nie aus einer festen Zeichenkette.
- Einwilligung ist ein Pflichtfeld; der Wortlaut wird als `consentText`
festgehalten und stammt aus derselben Funktion wie das Formular.
- Bewerber-Dedupe ueber die kleingeschriebene Adresse (Riegel
`{_customerID,email}`; das Portal sucht ebenso).
- Doppel-Bewerbung: Vorpruefung UND E11000-Fang, beide unabhaengig belegt.
- Der CRM-Lead entfaellt bewusst (W0 §3a: HR-Rechtekreis, nicht Kundenpflege).

Verify: tsc 0, volle Suite 1682/1682, Karriere-HTTP 24/24. Drei Flip-Proben
beissen punktgenau (entityCollection, Kleinschreibung, Einwilligung); eine
vierte belegt, dass der E11000-Fang allein traegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CLtxDVijYPDaZcJryaWjK
Fix route

ein eigenstaendiger Plattform-Host routet auf seine Flaeche, nicht auf die Website

Ein `pwa`-Eintrag OHNE `_websiteID` hat keine Geschwister. Die Zuordnung kannte
nur die Geschwister-Form und lieferte fuer ihn GENAU die eine Auffang-Regel auf
die consumer-app — er bekam Zertifikat, Tor und Route, war erreichbar und zeigte
die Marketing-Website unter der Anmelde-Adresse. Empirisch belegt am 2026-08-23:

buildHostRoutingRules({domain:'login.protos.ayoune.app', hosts:[]})
-> [{route:[{destination:{host:'consumer-svc',port:{number:3000}}}]}]

Diese Form ist der Regelfall, sobald ein Wiederverkaeufer gar keine Website hat —
also genau die Lage, in der Auftrag 4 (Beweis an EINEM Host) bewiesen werden soll.

- `hostRouting`: neuer Eingang `selfTarget`; ist er gesetzt, wandert **der Auffang
selbst** auf die Plattform-Flaeche (bewusst keine `match`-Regel davor: der
Platzhalter `*.<domaene>` steht in denselben `hosts` und fiele sonst weiter auf
die Website), und `rootTarget` wird ignoriert (die Umleitung waere eine Schleife).
- `platformHostGate`: `platformHostTargetFor()` gibt das ZIEL statt nur „ja";
`isEnabledPlatformHost` ruft sie — EINE Implementierung, nicht zwei.
- `provisionDomain` leitet den Schluessel aus dem Tor ab und reicht ihn durch.

Rueckwaerts-Identitaet festgenagelt: ohne `selfTarget` ist jede Bestands-Form
byte-gleich zu vorher (Probe ueber vier Formen), und eine `consumer`-Domaene
bekommt den Schluessel per Flip-Probe nachweislich NICHT.

Flip-Proben beidseitig gefahren: Regel-Ebene ausgehaengt -> 4 rot; Verdrahtung
ausgehaengt -> 1 rot; beide zurueck -> ganze Reihe exit 0 (25 + 47 + 4 + Rest).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018KTTjQDtPqzMCcA9pm8jND
Fix pwa

Hinweistexte im Panel ueberlappten das naechste Feld

Am laufenden Bildschirm gesehen: Material reserviert mit der Vorgabe EINE Zeile
fuer den Hinweis und rechnet mehrzeilige Hinweise nicht ein. Die Hinweise sind
hier bewusst laenger als eine Zeile, weil sie echte Fallen benennen (ein
Punkt-Pfad sperrt die Kante dauerhaft; der Joker ist nur als Ausgang zulaessig)
— sie standen deshalb im naechsten Feld.

subscriptSizing="dynamic" an allen zehn Feldern + etwas Abstand.

⚠ Vierter Treffer der Backtick-Falle an einem Tag, diesmal im neuen
CSS-Kommentar selbst. Der Warnhinweis stand schon zwei Zeilen darueber.

Proben 33/33. build:stage gruen.
Fix pwa

kennungsloser Einstieg — das Platzhalter-Segment traf keinen Zustand

Live gemessen (2026-08-23): der Listen-Einstieg zeigte auf
`/hr/applicationprocesses/edit/vorgabe`. Die Adresse wechselte, die LISTE blieb
stehen, und es erschien „State hr.applicationprocesses.edit.vorgabe nicht
registriert".

Ursache: `PageService.urlToView` nimmt bei VIER Segmenten das letzte nur weg,
wenn es eine ObjectId ist (`page.service.ts:1999`) — ein Platzhalter bleibt also
stehen und wird Teil des gesuchten Zustands-Namens. Die Drei-Segment-Form loest
unveraendert auf.

- Route traegt wieder BEIDE Kinder. Das leere heisst hier NICHT „anlegen" (ein
Prozess wird uebernommen, `hideCreate` bleibt), sondern „welchen Prozess?" —
die Liste kennt die Kennung der Plattform-Vorgabe nicht, der Editor loest sie
ueber `GET /applicationprocesses` auf und ersetzt die Adresse.
- Route-Probe auf beide Formen gedreht, Konstante umbenannt
(`EINSTIEG_OHNE_KENNUNG`), Begruendung an allen drei Fundstellen.

Proben 33/33. build:stage gruen.
Fix hr

Einstieg OHNE Kennung — das Platzhalter-Segment traf keinen Zustand

Live gemessen (2026-08-23, Mandant tolinax): der Einstieg zeigte auf
`/hr/applicationprocesses/edit/vorgabe`. Die Adresse wechselte, die LISTE blieb
stehen, und es erschien „State hr.applicationprocesses.edit.vorgabe nicht
registriert".

Ursache: `PageService.urlToView` nimmt bei VIER Segmenten das letzte nur weg,
wenn es eine ObjectId ist (`page.service.ts:1999`). Ein Platzhalter bleibt also
stehen und wird Teil des gesuchten Zustands-Namens. Die DREI-Segment-Form
(`/hr/applicationprocesses/edit`) loest unveraendert auf; die PWA-Route traegt
dafuer wieder ein leeres Kind — das heisst hier NICHT „anlegen" (der Prozess
wird uebernommen, `hideCreate` bleibt), sondern „welchen Prozess?".

Reihe gruen: 41 Proben der zwei betroffenen Reihen.
New crm

Codegen-Nachzug — Suchfelder, 4xx-Klassifikation, Mengen-Rechte; 15 Charakterisierungs-Proben umgekehrt

Voller Modul-Lauf `codegen routes --module crm`. Der Lauf zieht DREI angesammelte
Vorlagen-Fixe nach, die crm im Feldzug `codegen-suchfelder-vorlage-ohne-konsumenten-nachzug`
(11 Module / 472 Handler) ausgelassen hat:

1. Freitext-Suche liest `searchableFields` statt hartkodiert name/title/subject.
Fuer Consumers heisst das first_name|last_name|email|phone|company — KEIN
Consumer-Dokument traegt ein `name`-Feld, `GET /consumers?q=` fand also niemanden.
2. Lese-Vorlagen klassifizieren Aufrufer-Fehler als 4xx statt pauschal 500 und
protokollieren sie als `warn` statt `error`.
3. 19 Router: `PUT /many` prueft `edit` ODER `massEdit` statt `.new` (Korrektur —
eine Mengen-AENDERUNG verlangte bis hierher das ANLEGE-Recht), `DELETE /many`
prueft `delete` ODER `massDelete` (rein additiv).

15 Charakterisierungs-Proben umgekehrt, je mit Begruendung im Quelltext, WAS sich
geaendert hat und WARUM die alte Erwartung die defekte war. Sechs Negativ-Kontrollen
ergaenzt, damit der 4xx-Zweig nicht mit einem pauschalen 4xx trivial erfuellbar ist.

BEFUND, der ueber den Nachzug hinausgeht — routerConsumersCollectionActions.ts:
Die Datei war hand-geschrieben OHNE Haertungs-Marker und wurde vom Lauf ueberschrieben.
Uebernommen sind zwei echte Behebungen, eine dritte Aenderung ist zurueckgenommen:

- UEBERNOMMEN: die zehn `collection`-Aktionen verlangten `crm.consumers.<name>` ohne
das Segment `.actions.`. `registerModuleRights` seedet ausschliesslich
`crm.consumers.actions.<name>` — die alte Zeichenkette wurde NIE vergeben. Dieselbe
Fehlerklasse wie die 19 Routen aus §3 des Feldzug-Handoffs.
- UEBERNOMMEN: der Sammel-Router war im Eltern-Router GAR NICHT eingehaengt
(routerConsumers.ts kannte nur `/:id/actions`) — die zehn Wege waren toter Code
hinter einem toten Recht. Jetzt unter `/actions` eingehaengt, VOR den CRUD-Wegen.
- ZURUECKGENOMMEN + gehaertet: der Lauf haengte zusaetzlich die SECHS
`availability: 'both'`-Aktionen dort ein (archive, ignore-in-merge, blacklist,
add-to-list, assign-segment, export). Alle sechs Ruempfe lesen `req.params.id` und
schreiben mit `{_id: entityId, _customerID}`. An der Sammel-Adresse ist entityId
`undefined`, und Mongoose ENTFERNT einen undefined-Wert aus dem Filter (an mongoose
8.24.4 gemessen: `{_id: undefined, _customerID: X}` castet zu `{_customerID: X}`) —
der Schreibvorgang traefe einen BELIEBIGEN Datensatz des Mandanten, ohne Fehler.
Haertungs-Marker gesetzt, der Waechter meldet die Datei ab jetzt statt sie zu
ueberschreiben (3 -> 4 gehaertete Dateien im Modul).

Verify: 1864/1864 Proben in 106/106 Reihen (Vorher-Wert 1858/1858, +6 Negativ-Kontrollen),
tsc Exit 0, zweiter und dritter Erzeuger-Lauf idempotent (0 Abweichungen, mit
Positiv-Kontrolle), die zehn hand-verdrahteten Aktions-Routen in
routerConsumersActions.ts unveraendert vorhanden.

Brief: crm-api-codegen-nachzug-und-charakterisierungs-proben
Fix pwa

Prozess-Graph lesbar machen — Joker-Stummel + echte Theme-Tokens

Zwei Befunde vom AUSGEBRACHTEN Bildschirm, nicht vom Entwurf.

1) Joker-Kanten (from: '*') liefen quer ueber die ganze Leinwand — bei der
Plattform-Vorgabe drei Linien (reject/withdraw/hold), die alles kreuzen, was
dazwischen liegt. Sie haben per Bauart keinen Ausgangs-Knoten; ein kurzer
Stummel links am Ziel sagt dasselbe („kommt von ueberall") und kreuzt nichts.
Probe mit Gegenprobe: eine NORMALE Kante darf lang bleiben, sonst waere der
Graph nicht mehr verbunden.

2) --mat-app-background-color GIBT ES NICHT im ausgelieferten Stil (am gebauten
styles-*.css gemessen) — der Rueckfall #fff schlug also immer durch, und auf
dem dunklen Thema standen weisse Karten auf dunklem Grund. Ersetzt durch die
M3-System-Tokens, die real existieren (surface-container-high / on-surface /
outline-variant / primary).

⚠ Dritter Treffer der Backtick-Falle an einem Tag: ein Backtick in einem
CSS-Kommentar INNERHALB der styles-Vorlage beendet das Template-Literal, und
der Uebersetzer zeigt auf eine Zeile weit dahinter. Der Warnhinweis steht jetzt
in beiden Vorlagen dieser Datei.

Proben 33/33.
Fix auth

NUL-Byte aus login-brand.ts, Rueckfall-Vergleich ohne Sentinel

Der Rueckfall-Wert von `hostnameOf` fuer den eigenen Host war ein
Sentinel-String, der beim Schreiben zu einem NUL-Byte wurde. Folge: git
fuehrte die Quelldatei als BINAER (`Bin 0 -> 11342 bytes`) — kein Diff, keine
Durchsicht, obwohl Bau und Proben gruen blieben. Gefunden beim Pflicht-Blick
auf `git show --stat` nach dem Commit, nicht vom Bau.

Der Sentinel ist zugleich sachlich ersetzt: ein leerer eigener Host darf gar
nicht vergleichen, sonst waere er ein Treffer fuer jeden ebenso unlesbaren
Host. Statt eines Wertes, der nie trifft, jetzt eine ausdrueckliche Bedingung.

19/19 Proben unveraendert gruen, `tsc` (app + spec) und `build:stage` gruen.
Fix auth

Rueckweg zur Anmeldung traegt die Marke des Hosts statt aYOUne

`redirectToLogin` schickte einen Nutzer, dessen Sitzung ablaeuft, auf
`${loginUrl}/ayoune` — der Marken-Slug stand fest im Quelltext. Auf einem
weiss-etikettierten Host (`app.kunde.de`) verschwand damit mitten in der
Nutzung die Marke des Wiederverkaeufers, ohne sichtbaren Fehler.

Der Slug wird jetzt aus dem HOST abgeleitet, in derselben Reihenfolge wie in
`clients/auth`: Zwischenspeicher → Plattform-Host → Abruf (`GET /host/:host`
auf reseller-api, seit 2026-08-23 gebaut). Faellt der Abruf aus, gilt `ayoune`
— fail-safe, nicht fail-closed. Die Rueckfall-Regel `login.ayoune.app/<slug>`
bleibt unangetastet; es wird nichts umgeleitet und nichts ersetzt.

ZWEI gemessene Abweichungen gegenueber `clients/auth`, beide notwendig:
(a) der Apex kommt aus `loginUrl`, NICHT aus `frontendUrl` — der traegt hier
`pwa.ayoune.app`, also eine Flaeche UNTER dem Namen; dieselbe Ableitung haette
`app.ayoune.app` zum Fremd-Host gemacht und den haeufigsten Rueckweg einen
Abruf gekostet. (b) die Etiketten-Liste traegt zusaetzlich `pwa`.

Messung (Schritt 1 des Auftrags): genau EINE fest verdrahtete Fundstelle.
Zwei weitere sehen so aus, sind es aber nicht (`${startupUrl}/ayoune` ist
`/:product`, kein Marken-Slug), und die Lib `@tolinax/ayoune-auth-client`
leitet nackt auf `loginUrl` ohne Slug — den Fall deckt der Host-Riegel der
Anmelde-Oberflaeche ab.

19 Proben (`login-brand.spec.ts`) mit Flip-Probe `login.protos.ayoune.app`
(Reseller-Domaene unter dem Plattform-Namen ist KEIN Plattform-Host),
Byte-Gleichheit auf der Plattform-Marke und drei Negativ-Kontrollen
(404 / Netzfehler / Antwort ohne slug → `ayoune`, nie `undefined`).
Fix pwa

Leer-Zustand zeigte den Anlege-Knopf trotz hideCreate + Platzhalter-Weg

Zwei Befunde am Bewerbungsprozess, beide ueber ihn hinausreichend.

1) `resolveCreateCtaLabel` las `hideCreate` NICHT — der Leer-Zustand zeigte
seinen Anlege-Knopf auch dort, wo der Zustand das Anlegen ausdruecklich
unterdrueckt. Die drei anderen Leser respektieren ihn laengst
(`create-button-visibility.ts:59`, `primary-action.ts:41`,
`base-page.component.html:245`); allein die Beschriftung nicht, weil sie nur
`name_singular` brauchte. Am Bewerbungsprozess haette der Knopf einen leeren
`Workflows`-Datensatz ohne Stufen erzeugt — genau den kaputten Prozess,
gegen den die Server-Pruefung sieben Riegel faehrt. Gemessene Reichweite:
6 Zustaende tragen `hideCreate: true`. Flip-Probe: Riegel entfernt faerbt
genau 1 Probe rot, die Gegenprobe bleibt gruen.
⚠ `AyounePageViewConfig` DEKLARIERT `hideCreate` gar nicht (TS2345 „has no
properties in common") — das Feld existiert nur zur Laufzeit, `tableState()`
spreizt es hinein. Daher die Index-Signatur.

2) Platzhalter-Weg `edit/vorgabe`: die Liste kennt die Kennung der
Plattform-Vorgabe nicht (Aggregations-Weg blendet sie per Bauart aus), also
loest der Editor sie ueber die Sammel-Liste auf und ersetzt die Adresse.
Die EIGENE Kopie schlaegt dabei die Vorgabe.

Nebenbei: zwei Proben in `no-content.component.spec.ts` erwarteten seit dem
2026-08-12 ein `Create `-Praefix, das an jenem Tag ausgebaut wurde — sie waren
seither dauerhaft rot (baseline nachgemessen: 2 rot ohne jede eigene Aenderung)
und sind auf das gemessene Verhalten nachgezogen.

Proben 53/53. build:stage gruen (0 Fehler, Schrift-Waechter BESTANDEN).
Fix hr

die Prozess-Flaeche war eine Sackgasse — Einstieg in den Editor

Am laufenden Programm gemessen (Mandant tolinax): die Liste zeigt „0 von 0",
weil der Aggregations-Weg die Plattform-Vorgabe per Bauart ausblendet
(`workflows` steht in ACKNOWLEDGED_NON_CATALOGS). Es gibt also keine Zeile zum
Anklicken — und „Uebernehmen" liegt IM Editor, den man ohne Zeile nicht
erreicht. Ein Mandant ohne eigene Kopie kam ueber die Oberflaeche NIE zu einer;
der Leer-Zustand riet ihm woertlich zu einem POST-Aufruf.

- Listen-Aktion `link` auf `/hr/applicationprocesses/edit/vorgabe`. Der
Platzhalter statt einer Kennung ist Absicht: die Flaeche KENNT die Kennung der
Vorgabe nicht — derselbe Grund, aus dem `actions/adopt` auf der Sammel-Ebene
liegt. Der Editor loest ihn ueber `GET /applicationprocesses` auf und ersetzt
die Adresse durch die echte Kennung.
- Zwei Proben inkl. Gegenprobe („der Einstieg traegt keine Kennung").

Reihe gruen: 52 Suiten / 1095 Proben.
New hr

Bewerbungsprozess-Editor — Zustand pageType:component + kuratierter Zeilenklick

Die Konfigurations-Flaeche aus W2 war nicht bedienbar: der Formular-Renderer
kennt keinen Objektlisten-Typ. Statt eines neuen Feldtyps im Fundament (Weg a)
haengt jetzt ein zweck-gebauter Editor als Komponenten-Override an der
Standard-Route (Weg b) — CEO-Regel 2026-08-22 plus zwei gemessene Gruende:
transitions[].from/to verweisen auf die Geschwister-Liste statuses[], und kein
Feldtyp des Vertrags bindet eine Auswahl an eine Liste im selben Modell.

- registerStates: hr.applicationprocesses.edit (pageType component, endpoint
applicationprocesses, keine kuratierten Aktionen — sie reiten im zentralen
Aktions-Slot), Zeilenklick der Liste kuratiert statt abgeleitet
- Proben: 9 neue Flip-/Gegenproben, Katalog-Waechter 25 -> 26
- Reihe gruen: 52 Suiten / 1093 Proben
New reseller

Marke je HOST aufloesen — `GET /host/:host` neben `GET /slug/:reseller`

Auftrag 3 (Teil 1 von 2) des Briefes `white-label-anmelde-host-form-b-umsetzen`.

Befund der Messung vom 2026-08-19 (Station 8): die Anmelde-Oberflaeche loest ihre
Marke ausschliesslich aus dem PFAD auf (`/:reseller` -> `GET /slug/:reseller`),
und `aYOUneResellers.domain` wird von NIEMANDEM als Host-Zuordnung gelesen. Auf
einem eigenen Anmelde-Host (`login.kunde.de`) gibt es kein Pfad-Segment — der
Besucher landet auf der aYOUne-Marke, obwohl er die Domaene seines
Wiederverkaeufers aufgerufen hat.

Die neue Route ist das fehlende Gegenstueck, mit byte-gleicher Feld-Auswahl wie
`/slug/:reseller`, damit die Oberflaeche beide Wege gleich behandeln kann.
Oeffentlich gemountet wie `/slug` und aus demselben Grund: gefragt wird VOR jeder
Anmeldung, ausgeliefert wird nur, was ohnehin auf der Anmelde-Seite steht.

🔴 An den Freischalt-Schalter gebunden (CEO-Entscheid 3): der Filter verlangt
`platformHostEnabled: true`. Ohne die Bindung antwortete die Route auch fuer
Wiederverkaeufer, die lediglich eine Domaene EINGETRAGEN haben — und liefe damit
den beiden anderen Toren der Kette zuwider, die genau daran haengen.
Der Schalter steht IM Filter, nicht in einer Pruefung danach.

⚠ 404 statt 403 bei fehlender Freigabe: die Route ist oeffentlich, ein
unterscheidbarer Fehler waere eine Aufzaehlungs-Hilfe.

Die reine Regel liegt in `lib/platformHostLabels.ts`, nicht in der Route — sie ist
die probenfaehige Haelfte (die Test-Konfiguration deckt `src/lib/**`) und die
Haelfte, die spaeter in den Kern wandert.

🔴 DRITTE Kopie der Etiketten-Liste, bewusst benannt: dieselbe Liste steht in
`domain-worker/shared/hostRouting.ts` (welcher DIENST) und in
`auth/lib/platformHostOrigins.ts` (welche HERKUNFT). Drei Kopien einer Regel
laufen auseinander — dieselbe Erfahrung, die `MANUALLY_MANAGED_ZONES` in den Kern
gehoben hat. Der Hub ist als eigener Auftrag angemeldet.

🔴 LATENTER BEFUND, in einer Probe festgenagelt: mit `mongoose.set('strictQuery',
true)` faellt `platformHostEnabled` aus dem Filter, und die Route lieferte die
Marke JEDES Wiederverkaeufers mit passender Domaene aus — eine stille Weitung,
kein Fehler. Heute tritt das NICHT ein, und das ist gemessen statt angenommen:
Mongoose-8-Vorgabe ist `false`, und weder core noch models noch dieser Dienst
setzen die Option. Die Probe faellt, sobald das jemand aendert.

Proben: neue `lib/__tests__/platformHostLabels.test.ts`, Reihe 54/54 (vorher 45).
Sicherheits-Grenzen mit Umkehr- und Positiv-Kontrolle: unbekanntes Etikett,
zwei Ebenen tief, `login.de` (das sonst `de` als Kandidat ergaebe).
New domains

Plattform-Hosts bereitstellen — `pwa` aufgenommen, aber nur hinter dem Freischalt-Schalter

Zweite Haelfte von Auftrag 1 des Briefes `white-label-anmelde-host-form-b-umsetzen`.
Die Route kann einen Plattform-Host seit 2026-08-21 (`shared/hostRouting.ts`); was
fehlte, war der getrennte Freischalt-Schalter aus CEO-Entscheid 3 vom 2026-08-19.
Er steht seit `aYOUneResellers.platformHostEnabled` (interfaces 2026.452.0) im
Vertrag, also ist die Reihenfolge des Auftrags jetzt erfuellt.

`SERVABLE_SERVICE_TYPES` traegt jetzt `pwa` — und das ist NUR zusammen mit dem
neuen `shared/platformHostGate.ts` richtig. Die Liste ist fuer `pwa` ab jetzt eine
VORAUSWAHL, kein Praedikat; erlaubt ist ein Host nur, wenn BEIDES gilt:
1. sein Wiederverkaeufer traegt `platformHostEnabled: true` (die Entscheidung), UND
2. sein Name liegt als bekanntes Etikett (`login.`/`app.`, genau EINE Ebene)
direkt unter dessen `domain` (die Zuordnung).

Das Tor steht an VIER Stellen, und die vierte ist die massgebliche:
- `startup/reconcileDomains.ts` — kein Auftrag, der ohnehin abbricht
- `jobs/checkDomainHealth.ts` — sonst Dauer-Alarm alle fuenf Minuten fuer
einen Zustand, der die richtige Antwort ist
- `jobs/provisionDomain.ts` (Geschwister) — 🔴 ohne diese Stelle waere die
Negativ-Kontrolle des Auftrags NICHT erfuellt: `resolveFeatureHosts` laedt
art-blind, ein `app.kunde.de` reiste deshalb als Geschwister einer ganz
normalen `consumer`-Domaene als SAN ins Zertifikat UND als Regel in die
Route — ohne eigenen Auftrag und ohne Freischaltung. Genau der Befund
"Tor und Zertifikat tragen einen pwa-Host BEREITS" der Messung vom 08-19.
- `jobs/provisionDomain.ts` (Auftrag) — fail-closed, egal wie ein Auftrag
entstanden ist (Abgleich, `verifyDomain`, Wiederholung von Hand). Ein
Ausschluss nur am Abgleich waere eine Zusage mit Luecke.

Die Ladung des Datensatzes wandert dafuer VOR den docker-compose-Zweig — jener
setzt `tls.provisioned` und beendet den Auftrag, ohne den `try` je zu betreten.

Fail-closed in jeder Richtung: keine Datenbank, kein Treiber, ein Wurf, ein leeres
Feld ergeben die LEERE Menge und damit keinen Plattform-Host — Byte fuer Byte das
Verhalten von vorher. Der schlimmste Ausgang eines Ausfalls ist, dass ein
freigeschalteter Host wartet, nie dass ein nicht freigeschalteter einen bekommt.

⚠ ZWILLING, bewusst benannt: `platform/auth/src/lib/platformHostOrigins.ts` stellt
dieselbe Frage fuer die Herkunfts-Erlaubnis. Zwei Kopien einer Regel, die
entscheidet OB eine Domaene angefasst werden darf, laufen auseinander — dieselbe
Erfahrung, die `MANUALLY_MANAGED_ZONES` in den Kern gehoben hat. Der Hub ist als
eigener Auftrag angemeldet (er braucht einen Fundament-Platz, den dieser Brief
nicht haelt).

Proben: neue `shared/platformHostGate.test.mjs` 35/35 (beide Richtungen am
Auftrag selbst: OHNE Schalter kein Zertifikat/Tor/Route, MIT Schalter alle drei;
Fail-closed bei Wurf; Sicherheits-Grenze `app.boesekunde.example` gegen
`kunde.example`). `servableServiceTypes.test.mjs` mitgezogen und um eine
Flip-Probe erweitert (25/25) — die Behauptung zu `pwa` wurde UMGEDREHT statt
gestrichen, damit die Zwei-Stufigkeit sichtbar bleibt. Volle Reihe exit 0.
New coord

`foundation-slot` wird kanonischer awaiting-Wert — die Etiketten-Sackgasse schliesst

DER BEFUND: fuer "wartet auf einen freien Fundament-Platz" gab es KEINEN tragfaehigen Wert,
und beide Auswege endeten im Ausschluss.
- `foundation-slot` lag ausserhalb von AWAITING_ENUM -> jede Automatik uebersprang ihn
fail-closed, ohne Fehler und ohne Spur.
- `deferred` liegt im Enum, fehlt aber in BATCH_READY_AWAITING -> Kaskaden-Kollektor UND
Grant-Waehler sind blind dafuer.
Gemessen 2026-08-22: 54 blockierte Briefe warten auf den Platz, verteilt auf VIER Etiketten
(32 dependency, 9 none, 9 deferred, 1 trigger). Ein Brief dokumentiert den vergeblichen
Wechsel woertlich: "war `foundation-slot` - ein Wert AUSSERHALB von AWAITING_ENUM ... `deferred`
ist der etablierte Wert" (knowledgebase-eigene-ki-aktionen-je-sammlung, /resync 2026-08-18).
Jemand tauschte unsichtbar gegen unsichtbar.

DIE SEMANTIK KANNTE DEN WERT LAENGST: `awaitingKind()` gibt fuer ihn seit jeher die Klasse
'foundation-slot' zurueck (trigger-conditions.mjs:200) - nur das SYNTAX-Tor liess ihn nicht zu.
Diese Aenderung schliesst genau diese Luecke, sie erfindet nichts Neues.

GEAENDERT (drei Listen, bewusst nicht vier):
- AWAITING_ENUM (Quelle der Wahrheit) + AWAITING_ENUM_MIRROR (Spiegel) -> Wert erlaubt
- BATCH_READY_AWAITING -> Kollektor und Grant-Waehler sehen ihn
- NICHT AWAITING_CLAIMABLE: der Brief bleibt gesperrt, er wartet ja wirklich
- 🔴 NICHT PROMOTE_RESOLVES_AWAITING: erst so gebaut, dann ZURUECKGENOMMEN. Der Drift-Waechter
(coord-promote-unresolved-gate 5b) verbietet jede Erweiterung ausdruecklich, weil damit das
Loch wieder aufrisse, das die awaiting-Sperre geschlossen hat (purchase-approval, 3x geclaimt
/ 5 Rueckgaben). Folge, im Protokoll dokumentiert: der Promote WARNT laut, dass der Wert
stehen bleibt - wer den Platz gewaehrt, setzt `awaiting: none` mit.

WAECHTER-TESTS NACHGEZOGEN — und das war der heikle Teil: DREI Testreihen fuehrten
`foundation-slot` als BEISPIEL fuer einen erfundenen Wert. Sie zielten auf die AD-HOC-ERFINDUNG,
nicht auf das Konzept; sie sind auf `pl-grant` umgestellt (der 2026-08-19-Neufund, weiterhin
ungueltig) und um Positiv-Proben ergaenzt, die die Aufnahme festnageln.

Verify: awaiting-canon-guard 17/17 · promote-unresolved-gate + awaiting-guard 25/25 ·
volle Reihe 4804/4810. Die 6 verbleibenden Fehlschlaege sind VORBESTEHEND — gegen den
gestashten sauberen Baum gemessen, dort dieselben 6 (394/930/931/1234/1245/4200).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New applications

KI-Unterstuetzung W4 — Vorpruefung, Antwort-Entwurf, Gespraechsfragen

Drei Aktionen an der Bewerbung, alle drei mit dem AGG-/Art.-22-Riegel BAULICH
statt nur im Prompt:

POST /applications/:id/actions/ai-screen -> schreibt aiScreening
POST /applications/:id/actions/ai-reply-draft -> liefert nur zurueck
POST /applications/:id/actions/ai-interview-questions -> liefert nur zurueck

- redactProtectedAttributes() entfernt die AGG-Traeger (Geburtsdatum, Anrede,
Anschrift, Staatsangehoerigkeit, Religion, Behinderung, Lichtbild) VOR dem
Modell-Aufruf. Was nicht mitreist, kann nicht gewichtet werden.
- filterInadmissibleQuestions() wirft unzulaessige Gespraechsfragen aus dem
ERGEBNIS und WEIST SIE AUS statt sie still zu schlucken.
- Eine unlesbare Modell-Antwort wird zum Fehler, nie zu score 0 — ein Rueckfall
saehe aus wie die Bewertung "ungeeignet".
- Keine der drei Aktionen ruft transitionEntity oder versendet etwas; nur
ai-screen schreibt, und nur das Feld aiScreening.

Alle drei Lesezugriffe (Bewerbung, Stelle, Bewerber) mandanten-gescopt einzeln.
Rechte kaskadenfrei ueber additionalRights, drei getrennte.

66 neue Proben mit Negativ- UND Positiv-Kontrollen; die Art.-22-Strukturprobe
ist kommentar-bewusst (der erste Wurf fiel an der eigenen Kopfzeile).
Volle Reihe 1054/1054, Bau gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CLtxDVijYPDaZcJryaWjK
Fix coord

rang2 — CEO-Entscheid JobOffers=Beobachtung eingearbeitet, Brief entsperrt

CEO 2026-08-23: `JobOffers` IST Markt-BEOBACHTUNG (Job-Scraper-Zufluss), NICHT der Traeger
eigener Stellenangebote. Damit ist die einzige offene Produkt-Frage dieses Briefes
beantwortet und der `joboffers`-Teil faellt ENDGUELTIG aus dem Umfang — die fuenf Aktionen
(send/accept/reject/counter-offer/expire) brauchen einen EIGENEN Traeger, das ist ein
Folge-Brief (neue Entitaet ⇒ Fundament), nicht dieser hier.

Der Entscheid deckt sich mit dem am 2026-08-04 gemessenen Bestand, den der Brief selbst
fuehrt: 22 leere Stubs, KEIN `_customerID`, und die Form (`source`/`sourceId`/`applyLink`/
`descriptionHTML`) beschreibt ein eingelesenes FREMDES Angebot.

Drei Folgeaenderungen, damit der Brief nicht auf toten Praemissen stehen bleibt:
- awaiting `ceo-decision` → `none` (nichts Menschliches mehr offen)
- trigger `BLOCKED-ON-CEO-DECISION` → READY samt Begruendung
- claim_paths interfaces+models (Altlast, Fundament seit Kaskade 28 geliefert) → die drei
echten Ziel-Repos, AUS DER REGISTRY ABGELEITET statt geraten: Vacations→hr.js,
SupplierAgreements→purchase.js, Documents→crm.js; Umkehrprobe mit erfundenem Plural 0.

Danach gemessen: der Brief meldet nur noch NOT-READY:paths gegen eine LAUFENDE Sitzung
(bewerbermanagement-w4 haelt domains/hr/api) — ein echter, transienter Konflikt statt einer
toten Praemisse. Er loest sich, wenn jene Sitzung endet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix validator

Zeilenenden zurueck auf LF — und diesmal die URSACHE benannt

Der Vorgaenger-Commit drehte die Datei von LF auf CRLF und zeigte dadurch 8939
geaenderte Zeilen statt der echten 139. Zweiter Fall derselben Klasse in dieser
Sitzung (der erste in domains/cms/api, drei Commits lang).

🔴 DIE URSACHE, die beim ersten Mal noch nicht benannt war: Pythons TEXT-Schreibmodus.
`io.open(p, "w", encoding="utf-8")` uebersetzt auf Windows jedes `\n` beim SCHREIBEN
nach `\r\n` (newline=None => os.linesep). Wer eine Datei so einliest und zurueck-
schreibt, dreht ihre Zeilenenden — auch wenn er nur drei Zeichen geaendert hat.

Abhilfe fuer den naechsten Bearbeiter, in dieser Reihenfolge:
1. Zeilenenden der Datei VOR dem Edit messen: git show <ref>:<datei> | ...
2. Im BINAER-Modus lesen und schreiben (open(p,"rb") / open(p,"wb")) ODER beim
Text-Schreiben newline="" mitgeben.
3. VOR dem Push git show --stat lesen: eine vierstellige Zeilenzahl bei einer
kleinen Aenderung ist immer dieses Problem.

⚠ Es gibt hier KEINE .gitattributes, also keine Repo-weite Politik — jede Datei
traegt ihren eigenen Stand. Diese hier ist LF (8939 Zeilen), pauschales
"normalisieren" ist deshalb genauso falsch wie das Drehen.

--selftest-intake unveraendert 32 ok / 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New validator

das Eintritts-Tor prueft jetzt auch den surface-Kanon

Das Tor prueft `awaiting:` gegen seinen Kanon, `fe_coverage.surface` aber nicht —
obwohl beide dieselbe Wirkung haben: validation_status incomplete, und jeder Walker
ueberspringt den Brief STILL. Am 2026-08-22 lagen fuenf pending-Briefe gleichzeitig
mit fe-coverage-malformed-surface in toPost, darunter ein P1 — alle fuenf hatte das
Tor durchgelassen.

⚠ HERKUNFT: die Haupt-Arbeit (101 Zeilen: checkIntakeSurfaceKanon, SURFACE_ENUM als
EINE Quelle fuer Fehlertext und Heilungs-Hinweis, --selftest-intake) stammt aus der
Sitzung intake-tor-prueft-surface-kanon-nicht-TBD. Sie ist am 2026-08-23 gestorben
und hatte den Stand UNVERSIONIERT im Arbeitsbaum liegen lassen — ihr einziger Commit
war der Claim. Diese Sitzung hat sie gereapt, den Stand gelesen statt ueberschrieben
und zu Ende gebracht.

🔴 DIE EINE ROTE PROBE, mit der sie starb, war der Kern des Auftrags: die Einhaengung
war ZU BREIT. Die geteilte Guete-Pruefung meldet einen fe_coverage-Block MIT
user_facing und OHNE surface-Zeile als `surface-value-invalid` (ein leerer Wert steht
nun einmal ausserhalb der Werteliste). Am Tor haette das Briefe abgewiesen, deren
Autor den Block angefangen und nicht zu Ende gedacht hat — also einen INHALTLICHEN
Mangel, der zum todo-author-Weg gehoert. Genau davor warnt der Auftrag in Punkt 1.
Behoben durch eine Vorbedingung: es muss eine surface-Zeile MIT Wert geben, bevor ihr
Wert beurteilt wird.

Dazu zwei fehlende Gegenproben ergaenzt: der GANZ fehlende fe_coverage-Block (die
woertliche Negativ-Kontrolle des Auftrags) und die auskommentierte Muster-Zeile des
Geruests, die `coord new-brief` in jeden frischen Brief schreibt.

VERIFY: --selftest-intake 32 ok / 0 fail. Flip-Probe Einhaengung: genau die VIER
Beanstandungs-Proben fallen, keine Gegenprobe. Flip-Probe Einengung: genau die ZWEI
Fehlt-Gegenproben fallen. Gegen den echten Bestand: beide offenen Faelle
(briefing-absender-datenpflege, podcast-cover-w3) melden INTAKE-DEFECT MIT
Heilungs-Hinweis, der die fuenf Werte, den Pflicht-Slug und das fe--Praefix nennt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix blocks

CRLF fuer routerPageActions.ts zurueck — die Normalisierung war zu breit

Dritter Anlauf an der Zeilenenden-Frage, und diesmal GEMESSEN statt pauschal:

routerPageActions.ts CRLF <- war so, ist wieder so
die fuenf *.test.ts LF <- waren so, sind wieder so

Der Vorgaenger 80a05c4 hat ALLE SECHS auf LF gedreht. Fuer die fuenf Testdateien war
das richtig (mein erster Commit hatte ihnen CRLF eingeschleppt), fuer
routerPageActions.ts falsch — die Datei war im Repositorium schon immer CRLF, und die
"Normalisierung" war dort eine 7081-Zeilen-Aenderung, die niemand wollte.

⚠ Lehre: "Zeilenenden normalisieren" ist KEINE pauschale Handlung. Ohne .gitattributes
gibt es keine Repo-weite Politik; jede Datei traegt ihren eigenen Stand, und der ist
am URSPRUNGS-Commit zu messen (git show <ref>:<datei>), nicht am Nachbarn zu raten.

Der Netto-Diff dieser drei Commits gegen d43a167 ist jetzt das, was er von Anfang an
haette sein sollen: 241 Zeilen dazu, 19 weg, in sechs Dateien.

CI-Tor npm run test:unit: 2398/2398, 100 Reihen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix blocks

Zeilenenden zurueck auf LF + die richtige Beschreibung zum Vorgaenger

ZWEI Fehler am Vorgaenger-Commit b8656bf, beide meine:

1. DIE COMMIT-NACHRICHT IST FREMD. Sie lautet "fix(import): Menue kuriert ueber
ALLE Seiten der Website" und beschreibt eine ganz andere Arbeit. Ursache: der
erste Commit-Versuch wurde vom Fundament-Precheck-Hook geblockt — DER GANZE
Bash-Aufruf, also auch das `cat > /tmp/cmsmsg.txt` davor. Der zweite Versuch
las dieselbe Datei, die inzwischen einen fremden Stand trug.
⚠ Lehre: nach einem Hook-Block ist NICHTS aus dem Aufruf gelaufen — auch nicht
die Vorbereitung. Wer danach nur den letzten Befehl wiederholt, baut auf einem
Zustand auf, den er nicht hergestellt hat. Dieselbe Klasse wie das verschluckte
`git add` vom 2026-08-22 (neun Dateien fielen aus dem Commit).

2. ZEILENENDEN: meine Edits schrieben CRLF in sechs Dateien, das Repositorium
speichert LF (Nachbardateien 0 CRLF, core.autocrlf=false, keine .gitattributes).
Der Vorgaenger-Diff zeigte deshalb 3360 Zeilen statt der echten 241/19. Hier
zurueckgedreht; kuenftige Diffs und blame auf diese Dateien sind wieder lesbar.

INHALT DES VORGAENGERS (das, was in seiner Nachricht haette stehen sollen):
- logos: Eintrag ohne verwertbare Bildadresse wird als Text-Chip gezeichnet,
nicht als <img src="">. Gemischte Reihen tragen.
- mapCustomerLogosToItems: der Filter warf bildlose Eintraege WEG — deshalb waren
sie unsichtbar; ein Fix nur im Zeichner waere wirkungslos geblieben.
- references: Leer-Aussage zweisprachig, nur bei Entitaets-Speisung; ein leerer
INLINE-Block bleibt still. Resolver setzt __referenceItems auch bei leerer Menge.
- BEWUSSTE EINENGUNG: eine ABGELEHNTE Adresse bleibt src="#" wie bei den fuenf
Geschwister-Sinks (klassenweiter Vertrag, renderBlockMediaBlocks.test.ts:324).

CI-Tor npm run test:unit nach der Normalisierung: 2398/2398, 100 Reihen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix import

Menue kuriert ueber ALLE Seiten der Website, nicht nur den Lauf

Am Beleg-Lauf gemessen: das kurierte Hauptmenue liess ausgerechnet die
Schwerpunkt-Seite /active-campaign-management weg. Sie ist von Hand gebaut und
steht in keiner Quell-Sitemap, war also nie im Lauf-Bestand. Ein Menue, das nur
zeigt, was der Import gerade angefasst hat, ist bei jedem zweiten Lauf ein anderes.

Neu findPages(websiteId) am Datenbank-Weg (optional — ein Adapter ohne ihn faellt
auf das bisherige Verhalten zurueck statt zu scheitern) und collectCurationPages:
Lauf-Bestand plus alle UEBRIGEN veroeffentlichten Seiten. Die Angaben des Laufs
gewinnen, weil ihr Titel bereits gereinigt ist.

Nur veroeffentlichte Fremd-Seiten kommen dazu — ein fremder Entwurf im Hauptmenue
waere ein Verweis auf eine Seite, die es oeffentlich nicht gibt. Die Seiten des
Laufs sind davon ausgenommen, sie sind per Bauart Entwuerfe.

4 neue Proben inkl. Negativ-Kontrolle (Entwurf faellt weg, Positiv-Kontrolle
daneben), Rechtsseiten-Zuordnung und Fehlertoleranz. 88/2027 -> 88/2031.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix invite

Betreiber-Anlage ist annehmbar — offene Status sind eine MENGE

Die Annahme-Seite liess ausschliesslich 'invite-sent' als offen gelten und wies
'user-created-by-admin' als not-open ab. Der Betreiber-Weg ueber
superuser.ayoune.app legt aber genau mit diesem Status an
(routerOnboardingActions.ts:96/105) - eine so angelegte Person konnte ihr
Passwort ueber /:reseller/accept-invite?token= nie setzen. Der CEO-Entscheid
2026-08-22 (Weg a) verlangt genau das.

- INVITE_OPEN_STATUSES als Menge; INVITE_OPEN_STATUS bleibt der Einzelwert fuer
den Rueckbau-Rueckfall.
- Der atomare Riegel fragt {$in: INVITE_OPEN_STATUSES}. Mit dem Einzelwert
haette er bei einer Betreiber-Einladung nie gegriffen: die Vorpruefung liesse
sie als valid durch, hier waere sie als race-lost gescheitert - ein 410
"schon angenommen" fuer eine Einladung, die niemand angenommen hat.
- Der Rueckbau stellt den URSPRUENGLICHEN Status wieder her ({new:false} liefert
ihn). Vorher haette ein Fehlschlag eine Betreiber-Einladung still in eine
Team-Einladung verwandelt und dem Betreiber-Weg seinen Wiedervorlage-Datensatz
entzogen.

NICHT der Erzeuger auf 'invite-sent' umgestellt: der Status traegt die HERKUNFT,
und der Betreiber-Weg sucht mit ihm. Eine Verschmelzung braeche jene Suche.

Die W2-Probe, die 'user-created-by-admin' auf not-open festnagelte, ist
umgedreht statt geloescht - samt Negativ-Kontrolle, dass die Weitung GENAU einen
Status umfasst und die Frist fuer beide gilt. Flip-Probe: 2 rot / 9 gruen.
Volle Reihe 660/665 (5 uebersprungen), tsc --noEmit sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix applications

einen Fehler mit eigenem Status nicht auf 500 ueberschreiben

Gefunden an der LIVE-Probe, nicht an den 987 gruenen Proben: ein Uebergang auf eine
nicht sichtbare Kennung meldete "Error 500: Applications not found".

Ursache: `customerMongoose.findById` gibt bei Nicht-Fund kein leeres Ergebnis zurueck,
sondern WIRFT (`RessourceNotFoundError`, core methods/find/findOne.js:35) — und der
Fehler traegt seinen Status (404) bereits im zweiten super-Parameter. Der `if (!doc)`-
Zweig ist damit unerreichbar, und der pauschale 500er im Fang-Zweig machte aus JEDEM
Aufruf mit fremder oder falscher Kennung eine Dienst-Stoerung.

Der Fang-Zweig respektiert jetzt `e.status` (4xx/5xx) und protokolliert ein 4xx als
`warn` statt `error` — dieselbe Klassifikation wie im Fang-Zweig von createApplication.
Ein Bedienfehler weckt damit niemanden in der Ueberwachung.

⚠ Die Klasse ist breiter als dieser Weg — eine Parallel-Sitzung arbeitet an derselben
Sache im Kern (core-updateone-wickelt-404-in-500). Hier ist der Verbraucher behoben.

Proben 988/988. Flip-Probe: Status ignorieren faerbt genau 1 rot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coding

Rueckkanal zur Plattform — Kennung im Rumpf, Ausfaelle sichtbar, schlankes Terminal-Fenster

Nach dem ersten erfolgreichen PWA->DC-Dispatch (CEO-Test 2026-08-22) lief der
Auftrag am Geraet bis Exit 0, waehrend das Sitzungs-Dokument unveraendert auf
'dispatching' stand. Drei Glieder, alle am Quelltext vermessen.

A) Der Rueckschreiber baute PUT /codeagentsessions/<id>. Diesen Weg gibt es an
der Entitaet nicht: der erzeugte Router fuehrt nur put("/") und put("/many"),
sein Abschluss get("*") ist nur fuer Lese-Wege. Der Aufruf traf keine Route
und endete in "Not Found" - geschluckt, also von "laeuft" ununterscheidbar.
Jetzt der Vertragsweg mit _id im Rumpf.

Warum die Messung in 546803a den Aufrufer nicht sah: Adresse und Verb stehen
in verschiedenen Funktionen (Adresse in writeBackToServer, method:'PUT' erst
in flushWriteBack, das sie als Parameter bekommt). Eine Suche nach "PUT nahe
einem Pfad-Muster" kann diese Form per Bauart nicht finden.

B) Der Ereignis-Strom hat KEINEN statisch auffindbaren Defekt - die Kette ist
Ende-zu-Ende verdrahtet und die Aufnahme-Route ist eingehaengt. Der Grund
fuer null Ereignisse war nicht ermittelbar, weil jeder Aussteig wortlos war.
Die drei stillen Aussteige melden sich jetzt einmal je Sitzung, damit der
naechste Lauf die Antwort liefert statt wieder zu schweigen.

C) Das Terminal-Fenster bekam kein preload, also gab es dort kein
window.electronAPI; der Renderer-Dienst stieg in listen() sofort aus, der
Fokus-Kanal wurde nie gehoert und das Fenster blieb auf der Standard-Route -
eine zweite volle Anwendung. Die Whitelist trug den Kanal die ganze Zeit
(preload.ts:52) und der Hoerer existierte auch; nur die Bruecke fehlte.
Zusaetzlich laedt das Fenster die Sitzung jetzt direkt ueber die Adresse
(?terminal=1), damit die schlanke Huelle vor dem ersten Bild feststeht.

Ausserdem: ein non-2xx-Rueckschreiben protokolliert benannt und meldet nach dem
zweiten Fehlversuch einmalig ans Geraet - "schreibt nicht" und "schreibt
unsichtbar" waren drei Monate lang ununterscheidbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix hr

die drei Bewerbungs-Flaechen aus W1 fehlten im Modul-Raster

Gemessen am AUSGEBRACHTEN Modul-Zustand: 17 Verweise, keiner auf `hr.applications`,
`hr.applications-board` oder `hr.applicants`. Die Flaechen waren gebaut, geseedet und
in der PWA gerendert — aber aus der Oberflaeche NICHT auffindbar: diese Liste ist die
einzige Auswahl des Rasters "ALLE FUNKTIONEN" (resolveNavigationLinks serverseitig,
base-page.component.ts im Klienten). Wer eine Flaeche registriert und sie hier vergisst,
baut einen Tiefverweis, den niemand findet — dieselbe Klasse wie pm.onboarding
(CEO-Entscheid 2026-08-22).

Ergaenzt in fachlicher Reihenfolge: Bewerbungen, Bewerbungs-Pipeline, Bewerber
(vor dem W2-Verweis "Bewerbungsprozesse").

Neue Probe haelt alle VIER fest, mit Positiv-Kontrolle gegen eine leere Liste.
Flip-Probe: einen Verweis entfernt faerbt genau 1 rot. Proben 987/987.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix applications

Ablehnungen sagen im Rumpf denselben Status wie auf der Leitung

Am laufenden Dienst gemessen (2026-08-22): GET /applicationprocesses/<erfunden>
antwortete mit HTTP 404 und `meta.code: 500`. Die ay-CLI liest den Rumpf und meldete
deshalb "Error 500" fuer einen Bedienfehler — in der Ueberwachung eine Stoerung.

Ursache: der HTTP-Status kommt aus `res.locals.status` (returnResponse), der
`meta.code` aber aus `err.status || 500` (defaultAPIAnswerError) bzw. einem
HARTKODIERTEN 400 (defaultAPIAnswerRequestFailed). Beide lesen `res.locals` nicht.
Damit kam auch die ganze GUARD_STATUS-Abbildung der Motor-Ablehnungen (403/409/422)
als 400 beim Aufrufer an — der Kern von W2 war im Rumpf unsichtbar.

Neue Naht `lib/antwortMitStatus.ts` haengt den Status an den Fehler, damit
`err.status` greift; alle Ablehnungen der drei W2-Wege laufen darueber.

⚠ Die zwei Antwort-Bauer in `platform/core` sind NICHT angefasst — sie liegen
ausserhalb der claim_paths, und eine Aenderung dort traefe ~330 Verbraucher. Die
Codegen-Vorlage traegt denselben Widerspruch (actionJobOfferSend).

⚠ Der strukturelle Waechter allein reichte NICHT: eine Flip-Probe, die der Naht das
Anhaengen wegnimmt, liess ihn gruen — er prueft, DASS sie benutzt wird, nicht DASS sie
wirkt. Deshalb zusaetzlich `antwortMitStatus.test.ts` (7 Faelle inkl. Negativ-Kontrolle
am rohen Bauer). Mit ihr beisst die Flip-Probe: 6 rot.

Proben 986/986 (49 Reihen, vorher 979/48).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix applications

NUL-Byte aus dem Pruefer entfernen — git fuehrte die Datei als binaer

Der Dubletten-Schluessel trug ein literales NUL als Trenner. Folge: git stuft die
Datei als binaer ein und zeigt keinen Diff mehr — eine Codeueberpruefung saehe
Aenderungen daran nie. Gefunden am "Bin 0 -> 8496 bytes" im Commit-Statistik-Block,
nicht an einem Fehlschlag: Bau und Proben waren die ganze Zeit gruen.

Ersetzt durch eine verschachtelte Karte (Map<from, Set<event>>) statt eines
zusammengesetzten Schluessels — damit entfaellt die Trenner-Frage ganz. Ein
Escape half nicht: es wurde auf dem Schreibweg erneut zu einem echten NUL.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix applications

die neun neuen W2-Dateien nachreichen — sie fielen aus dem Vorgaenger-Commit

130e28a committete mit , und das nimmt UNVERSIONIERTE Dateien nicht auf:
die zwoelf geaenderten Dateien gingen mit, die neun neuen nicht. Der lokale Bau blieb
gruen (er sieht den Arbeitsbaum), die Kette #174 fiel mit neun — genau den neun fehlenden Dateien.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New applications

der Bewerbungsprozess laeuft — Anlege-Naht, fuenf echte Statuswechsel, Konfigurations-Flaeche

Bewerbermanagement W2. W1 hat die Flaechen und den Prozess-Seed gebaut, aber die
Strecke war nicht bedienbar: alle sechs Aktions-Ruempfe lehnten mit 422 ab, und
`POST /applications` konnte gar nicht gelingen — das Schema fuehrt drei Pflichtfelder
(`_workflow`, `currentStatus`, `statusSince`), die kein Schreibweg setzte.

- Anlege-Naht `custom/applications/routerApplicationCreate`: loest den geltenden Prozess
ueber die dreistufige Kette auf (Stelle -> Mandanten-Kopie -> Plattform-Vorgabe),
setzt die drei Felder und loest die Eingangsbestaetigung ueber den Orchestrator aus.
Verwirft `_workflow`/`currentStatus`/`statusSince` AUS DEM RUMPF.
- Fuenf Aktionen echt ueber `core/lib/workflow/transitionEntity`
(transition/reject/send-offer/hire/withdraw); `anonymize` bleibt ehrlich abgelehnt (W6).
- Mandanten-Riegel: der Motor laedt roh (`model.findById`), die Naht laedt VORHER
ueber `customerMongoose` — sonst waere die Bewerbung eines fremden Mandanten schaltbar.
- `statusSince` + `stageHistory` werden nachgeschrieben (der Motor schreibt die Namen
des Piloten `ITask`, `strict` verwirft sie hier still).
- Konfigurations-Route `custom/applicationprocesses` (Liste/Lesen/Aendern/Uebernehmen)
mit einem Pruefer, der sieben Graph-Fehler abfaengt, die der Motor NICHT abfaengt.
- SDUI: Flaeche `hr.applicationprocesses` + Modul-Verweis; Status-Aktionen am
Bearbeitungs-Zustand verdrahtet (Hausregel "erst befuellen, dann verdrahten" erfuellt).
- Rechte `hr.applicationprocesses.view|edit` kaskadenfrei ueber `additionalRights`.

Proben 978/978 (48 Reihen, vorher 954/47). Drei Flip-Proben, je genau 1 rot, mit
nachgewiesener Ersetzung: Mandanten-Riegel, Punkt-Pfad-Riegel, Aktions-Verdrahtung.

Es gibt KEINE `ApplicationProcesses`-Entitaet und soll keine geben — das Fundament
sagt es woertlich (`IApplication`). Der Prozess IST ein `Workflows`-Dokument.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

zwei Briefe hingen an einem Aufloeser, der ihre Abhaengigkeit per Bauart nie finden kann

BEFUND (systemisch, aber klein): der Pruefer loest `DEPENDS-ON <slug>` gegen SITZUNGS-
Kennungen in sessions/done/ auf. Wird ein Brief innerhalb einer FUNDAMENT-KASKADE geliefert,
wandert er nach queue/done/, ohne je eine eigene Sitzung getragen zu haben — gemessen 0
Sitzungen fuer beide Ziele, bei status:done. Ein stamm-basierter Aufloeser findet das nie,
und der abhaengige Brief meldet fuer immer `trigger:waiting`.
GROESSE der Klasse gemessen (nicht geschaetzt): 36 blockierte Briefe tragen ein DEPENDS-ON,
6 davon haben ein Ziel in queue/done/, und genau 2 davon haben keine Sitzung. Diese zwei.

Beide Abhaengigkeiten stattdessen EMPIRISCH am installierten Fundament belegt
(@tolinax/ayoune-interfaces 2026.451.0), je mit Positiv- und Umkehrprobe:
- presse-portale: `PressReleases` in data/modelsAndRights/marketing.js (1 Treffer;
Positiv-Kontrolle `Partners` 2; Umkehrprobe erfundener Name 0). Geliefert von Kaskade 64.
- white-label: IDomain.d.ts fuehrt `serviceType` (Z.66) UND `rootTarget` (Z.94)
(Positiv-Kontrolle 51 Feldzeilen; Umkehrprobe 0). Der Auslöser fragte ohnehin nach
PUBLIKATION, nicht nach Brief-Status — also so gemessen.

⚠ Beim Schreiben in die dokumentierte Zitat-Falle gelaufen und wieder heraus: das woertliche
Zitat des alten Auslösers wurde vom Pruefer als AKTIVER Auslöser gelesen, der Brief blieb
`waiting`. Genau das haelt `backup-pending-haenger-und-erster-lauf-beleg` in seinem eigenen
trigger fest. Marker-Schreibweise deshalb vermieden; danach beide READY.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

zwei weitere Fundament-Praemissen richtiggestellt

paymenttransactions-consumerid — Auslöser UEBERHOLT, und der Brief widerlegt ihn selbst.
Er verlangte "einen FUNDAMENT-PLATZ (Einzelschreiber auf interfaces + models)", waehrend
sein eigener STOPP-Block auf Zeile 53 vier positive Quelltext-Belege fuehrt, dass die
Fundament-Haelfte seit Kaskade 28 geliefert ist (IPaymentTransaction._consumerID:48 ·
IConsumer.recipients:848 · PaymentTransactions.ts:29 + Index :37 · Consumers.ts:996).
Offen sind nur die zwei Verbraucher-Teile: die Schreib-Seite von `_consumerID` und der
Reiter "Empfaenger", der weiterhin `Notifications` zeigt. Beides ohne Fundament-Platz.
claim_paths BEWUSST nicht ersetzt — der Rumpf benennt fuer die zwei offenen Haelften keine
eindeutigen Ziel-Repos; ein Ersatz waere geraten. Als erster Schritt im Lauf zu setzen.

oauth-app-kontingente-bau — Schwelle NACHGEMESSEN statt geglaubt (rein lesend, Prod):
`oauthclients` = 1, `oauthtokens` = 0. Der Auslöser verlangt BEIDE > 0, er ist NICHT
erfuellt — der Brief liegt zu Recht. Gegenueber der 08-03-Messung (0/0) hat sich die eine
Haelfte bewegt. Nebenbefund festgehalten, NICHT behoben: claim_paths fuehrt platform/core
bei `foundation: false`; der Widerspruch ist latent, solange die Schwelle sperrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

zwei Briefe mit ueberholtem Fundament-Blocker richtiggestellt

callagent-drei-tote-felder — EINGEHAENGT. Sein Auslöser nannte
"foundation-batch-cascade-31" als Halter von platform/interfaces; der Bestand steht bei
68. Vor allem aber braucht der Brief gar keinen Platz: sein eigener Defer-Grund sagt
"Nach dem ABBILDEN-Entscheid gibt es KEINEN interfaces/models-Anteil — die Arbeit liegt in
drei anderen Repos", und Rumpf-Zeile 84 benennt sie woertlich (communication-server /
mobile-app / domains/ai/api). claim_paths entsprechend gesetzt — vorher standen dort
interfaces+models, was ihn bei JEDER laufenden Fundament-Sitzung mit NOT-READY:paths
kollidieren liess (4x zurueckgelegt). Danach gemessen: nur noch ein ECHTER, transienter
Konflikt mit einer aktiven Sitzung, den der Pruefer selbst als verengbar ausweist.
Der Teilbefund `sttProvider ist nicht abbildbar` bleibt ausdruecklich offen.

rang2-restposten-zustandsfelder — ZWEITE Korrektur am selben Tag, sie nimmt meine erste
zurueck. Der Commit 158ae945cd hob ihn auf `dependency` mit der Begruendung "reiner
Platz-Warter"; das war SACHLICH FALSCH. Gemessen: die Fundament-Haelfte ist geliefert
(3 von 4 Entitaeten), der Rest braucht keinen Platz. Offen ist eine PRODUKT-Entscheidung,
die der Brief selbst benennt: `joboffers` wurde am Bestand gestoppt (22 leere Stubs, kein
`_customerID`) — gehoeren die fuenf Aktionen ueberhaupt an diese Entitaet? Daher
`ceo-decision`, damit /interview ihn ueberhaupt sieht.
Die Fundament-Pfade blieben dort BEWUSST stehen: der Brief benennt kein einziges
Nicht-Fundament-Repo, ein Ersatz waere geraten gewesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

sieben Briefe beanspruchten Fundament-Pfade, deren Arbeit laengst geliefert ist

Jeder der sieben traegt `foundation: false` UND sagt im eigenen Rumpf, dass sein
Fundament-Anteil erledigt ist ("Der Fundament-Teil dieses Briefes ist GELIEFERT",
"KEIN Fundament-Diff", "FUNDAMENT-ANTEIL GELIEFERT"). Stehen geblieben war nur der
`claim_paths`-Eintrag auf platform/interfaces|models|core.

DER SCHADEN ist der in CLAUDE.md (Kaskade 68) beschriebene: ein `foundation:false`-Brief,
der einen Fundament-Pfad weiter beansprucht, faellt beim Promote mit
`NOT-READY:paths:<sitzung>:<pfad>`, sobald irgendeine Fundament-Sitzung laeuft — und die
laufen rund fuenfmal taeglich. Er wird eingesammelt, kollidiert, wird zurueckgelegt,
wiederholt. Gemessen: von sieben geprueften Briefen dieser Klasse sind ALLE SIEBEN
mehrfach zurueckgelegt (2-4x).

Eine Stichprobe zusaetzlich am Quelltext belegt statt am Brieftext geglaubt:
sdui-aktions-schiene verlangt ein Verb-Feld an `actionDef` — es steht seit laengerem in
platform/core/src/lib/api/registerModuleStates.ts:899 und wird :914 verwendet.

WIRKUNG GEMESSEN: drei der sieben melden nach der Verengung `READY`
(dashboard-geteilter-seitenfilter, stackdump-restmenge, statusseite-tages-rollup);
vorher keiner. Alle sieben behalten nicht-leere claim_paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix routes

Codegen-Nachzug hub — Mengen-Wege pruefen ab jetzt das Aenderungs-Recht

Der Ausstoss eines Erzeuger-Laufs vom 2026-08-22T05:13Z lag 121 Dateien gross unversioniert
im Arbeitsbaum. Gepruefte Entscheidung: uebernommen, mit EINER benannten Korrektur.

BELEG, dass der Ausstoss reproduzierbar ist: Arbeitsbaum-Stand gesichert, auf HEAD
zurueckgesetzt, voller Modul-Lauf `codegen routes --module hub` neu gefahren, danach
byte-verglichen — deckungsgleich (sha1 ueber alle 171 .ts identisch), mit Positiv-Kontrolle
gegen HEAD (163 Unterschiede, der Vergleich greift also wirklich).

WAS SICH FACHLICH AENDERT — 11 Router (chats, examresults, exams, faqs, glossaries,
hubquestions, knowledgebases, polls, quotes, savedreplies, wikipages):

PUT /many vorher extendForNewRights(BASE) -> verlangte `hub.<e>.new`
nachher extendForMassEditOrEdit() -> `hub.<e>.edit` ODER `hub.<e>.massEdit`
DELETE /many vorher extendForDeleteRights(BASE) -> verlangte `hub.<e>.delete`
nachher extendForMassDeleteOrDelete() -> `hub.<e>.delete` ODER `hub.<e>.massDelete`

Der PUT-Fall ist eine Korrektur, kein Zusatz: eine Mengen-AENDERUNG verlangte bis hierher das
ANLEGE-Recht. Wer `new` haelt, aber weder `edit` noch `massEdit`, verliert den Mengen-Weg — er
haette ihn nie haben duerfen. Wer `edit` haelt, aber nicht `new`, gewinnt ihn. Der DELETE-Fall
ist rein additiv (any-of): niemand verliert, `massDelete`-Halter gewinnen.

Dazu die 4xx-Klassifikation aus der Vorlage (ValidationError/CastError enden als 400 + warn
statt 500 + error) in allen create/update/delete/get-Handlern.

DIE EINE KORREKTUR AM AUSSTOSS: der Modul-Lauf entfernt in routerHubQuestions.ts still die von
Hand gesetzte Montage `use('/:id/comments', routerHubQuestionsComments)` samt Einfuhr — acht
Kommentar-/Reaktions-/Stimmen-Routen waeren danach ueber HTTP unerreichbar gewesen.
Wiederhergestellt und mit dem vorgeschriebenen Zeile-1-Haertungsmarker versehen, damit der
naechste Lauf sie meldet statt sie zu entfernen. Gegenprobe am Waechter: `--probe` meldete
vorher 0 gehaertete Dateien, jetzt 1, mit dem Vermerk "NUR ueber den Marker" — die zwei
Bestands-Achsen (verlorene Deklaration / verlorener Hand-Import) sind fuer eine verlorene
`use()`-Montage strukturell blind.

Verify: `npm run build` gruen; Testreihe 217/217 in 16 Suiten gruen. Kontrolle vorher gefahren:
mit dem rohen Ausstoss fielen 2 Proben in `hubquestionsRouterMounting.test.ts`, bei HEAD waren
dieselben 5/5 gruen — die Regression war echt und ist geschlossen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix onboarding

Betreiber-Verdrahtung zurueckgenommen — die Annahme-Seite weist den Status ab

Der Katalog-Eintrag aus 7281a71 haette eine Mail verschickt, deren Verweis auf
eine Seite fuehrt, die ihn ABWEIST — also genau die Fehlerklasse, mit der der
CEO-Entscheid Weg (b) verworfen hat ("toter Verweis"), nur eine Ebene weiter.

Gemessen: `platform/auth/src/lib/inviteState.ts:32` fuehrt
`INVITE_OPEN_STATUS = 'invite-sent'` als "der EINZIGE Status, aus dem heraus
eine Einladung angenommen werden darf"; Zeile 70 weist jeden anderen als
`not-open` ab. Der Betreiber-Weg schreibt `user-created-by-admin`. W2 hat den
Fall gesehen und den Wert in einer eigenen Probe FESTGENAGELT
(`platform/auth/test/inviteState.test.ts:44`) — eine getroffene Entscheidung
der Nachbarstrecke, kein Versehen. Der Entscheid nahm an, W2 trage die Annahme;
er sagt selbst "die Annahme-Seite muss stehen, BEVOR diese Mail sinnvoll
verschickt wird".

Die VORARBEIT bleibt und ist geprueft: Empfaenger-Rueckfall auf `evt_data.email`
(der Betreiber-Weg kennt kein `inviteEmail`) und Handelnden-Rueckfall auf
`aYOUneInvites.sourceUser` (das Ereignis meldet keinen `sourceUserId`; ohne ihn
begaenne der Betreff mit einer Luecke — am Bestand belegt: 2 von 2 Betreiber-
Einladungen tragen `sourceUser`).

Zwei Proben lesen jetzt die BLOCKER-Zeilen im Quelltext: wird eine geaendert,
faellt die Probe und zwingt zur erneuten Messung, statt dass jemand den Eintrag
blind setzt. Umkehrproben: Vorarbeit ausgehebelt -> 2 rot, auth-Zeile geweitet
-> 1 rot. Volle Reihe 864/864.

Rest-Schnitt: queue/toPost/annahme-seite-nimmt-betreiber-einladung-nicht-an

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New coord

Selbst-Hosting-Restprofile als vier Wellen geschnitten

Vier Wellen-Briefe (crm+marketing / content+ecommerce / Betrieb / Fachmodule)
plus ein CEO-Entscheid-Brief zum stillgelegten support-center-api im Compose.

Faktenlage neu gemessen statt aus der Vorlage vom 08-05 uebernommen: 19 statt
17 Profile, core 11 statt 10, 66 statt 65 Abbilder, 19,37 GB gemessen statt
~21 GB hochgerechnet, Namens-Luecken bereits geschlossen (66/66).

Der Ausloeser des Ueberbau-Briefes war NICHT eingetreten (drei Betreiber-
Residuals seit 16 Tagen offen). Die neuen Briefe verankern ihr DEPENDS-ON
deshalb auf dem Residual statt auf dem Brief, der es hinterlassen hat —
Befoerderungs-Trockenlauf weist alle vier mit Namen des Blockers ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New onboarding

Betreiber-Anlage verschickt die Einladungs-Mail (CEO-Entscheid Weg (a))

`user.created-by-admin` zeigt auf dieselbe Vorlage und Huelle wie die
Team-Einladung (`invite-user-to-tenant` / `forgotpass`) — beide fuehren eine
Person OHNE Passwort ueber einen Token zum Passwort-Setzen.

Der Katalog-Eintrag allein haette NICHT getragen: das Ereignis meldet dieselben
Daten unter teilweise anderen Namen (`email` statt `inviteEmail`) und laesst den
Handelnden ganz aus (kein `sourceUserId`). Ohne Rueckfall haette der Betreff
mit einer Luecke begonnen. Der Handelnde wird deshalb vom Einladungs-Datensatz
gelesen (`aYOUneInvites.sourceUser`) — nur dann, wenn das Ereignis ihn nicht
mitschickt.

Die bestehende Negativ-Kontrolle benutzte ausgerechnet `created-by-admin` als
"fremdes Ereignis" und waere durch die Verdrahtung gebrochen — sie nennt jetzt
`password-changed`.

Umkehrproben (je einzeln gefahren): Katalog-Eintrag entfernt -> 13 rot,
Handelnden-Rueckfall entfernt -> 1 rot, `data.email` entfernt -> 1 rot.
Volle Reihe 871/871.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix deps

Gantt-Bibliothek auf 2026.3.4 und die drei Angular-Pakete aus dem Lock nehmen

Die Bibliothek deklariert ihre Angular-Peers ab 2026.3.4 als optional, der
Express-SSR-Dienst zog sie bis dahin mit, ohne sie je zu laden. Gemessen:
985 -> 982 Lock-Eintraege, 25 MB weniger im Abbild, npm ci gruen (958 Pakete),
tsc-Bau gruen gegen einen Baum ohne jedes Angular-Paket.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

sechs reine Fundament-Platz-Warter aus der Etiketten-Sackgasse befreit

`deferred` steht im AWAITING_ENUM, fehlt aber in BATCH_READY_AWAITING
(coord-admin.mjs:6716) — die sechs Briefe waren damit fuer den Kaskaden-
Kollektor UND den Grant-Waehler dauerhaft unsichtbar, obwohl ihr trigger
READY sagt und einzig der Fundament-Platz fehlt.

Fuer Fundament-Platz-Warter gab es nur zwei Etiketten, und BEIDE fuehren in
den Ausschluss: `foundation-slot` liegt ausserhalb des Enums (fail-closed),
`deferred` faellt aus BATCH_READY_AWAITING. Ein Brief dokumentiert den
vergeblichen Wechsel woertlich (knowledgebase-eigene-ki-aktionen-je-sammlung,
/resync 2026-08-18). Gehoben auf `dependency` — der Wert, der bei 32
Geschwistern nachweislich traegt.

Wirkung gemessen (foundation-batch --dry-run): 5 der 6 sind jetzt als
[core]/[field] IM Kollektor. Positiv-Kontrolle haelt: der echt geparkte
devopsalerts-entity-retire-candidate bleibt [awaiting-human] EXCLUDED, das
Tor ist also nicht pauschal geoeffnet.

Der sechste (rang2-restposten-zustandsfelder-fundament) bleibt draussen und
legt einen eigenen Befund frei: er beansprucht platform/interfaces+models,
traegt aber `foundation: false` — 11 Briefe liegen so, davon 7 mehrfach
zurueckgelegt. Nicht in diesem Commit behoben (Koordinations-Entscheid).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix

Angular-Peers als optional deklarieren, damit sie nicht in framework-freie Verbraucher wandern

Der Zweit-Einstiegspunkt /core ist framework-frei, das Paket deklarierte seine
Angular-Peers aber ohne peerDependenciesMeta — npm installierte sie deshalb in
jeden Verbraucher. Gemessen an einem leeren Testpaket: 3 Angular-Eintraege plus
rxjs vorher, 0 danach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

Kaskade 65 stand als tote Next-in-line in der Fundament-Reihe

`foundation-batch-cascade-65-fields` trug seit dem CEO-Einfalten in Kaskade 66
(2026-08-21) ein `superseded_by`, aber weiter `status: blocked`. `foundation-advance`
liest den STATUS, nicht das Feld — der Brief meldete sich damit als
"Next-in-line (P1, 8-14 h)", obwohl sein Inhalt in 66 geliefert ist.

- status blocked -> superseded (Vorbild: Kaskade 67, die es korrekt traegt)
- echter Next-in-line jetzt: core-updateone-wickelt-404-in-500 (P1)

Gefunden durch CEO-Rueckfrage im /resync-Tick, nicht durch einen Waechter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

drei Briefe trugen `awaiting: pl-grant` — ein Wert ausserhalb des Kanons

`pl-grant` steht NICHT im kanonischen Vorrat (`ceo-decision | ceo-verification |
user-input | operator-action | hardware | dependency | deferred | dedicated-session |
none`). Der Pruefer sagt dazu woertlich: „ein Wert ausserhalb macht ihn fuer JEDE
Automatik unsichtbar" — die drei Briefe waren also weder claimbar noch befoerderbar.

🔴 ZWEI DAVON SIND MEINE, aus derselben Sitzung (Iteration 5, 2026-08-22): ich hatte
den Wert aus dem BESTAND abgeschrieben (`grep` zeigte ihn an einem vorhandenen Brief),
statt den Kanon zu pruefen. Genau die Fehlerklasse „vom Bestand abschreiben statt die
Regel lesen" — der vorhandene Brief trug denselben Fehler.

Korrigiert auf `awaiting: none`, und das ist gemessen, nicht geraten: der
Fundament-Grant laeuft ueber `foundation: true` plus den Grant-Mechanismus, NICHT ueber
`awaiting`. Die Verteilung der `foundation: true`-Briefe in `blocked/` bestaetigt es —
26x `dependency`, 11x `none`, 11x `deferred`, 1x `trigger`; kein einziger nutzt
`awaiting` als Grant-Tor.

Wirkung belegt: `ensure-worker-prompt` meldete fuer beide vorher
`awaiting-not-in-enum … nichts angefasst`, danach `already complete (no-op)`.

⚠ NICHT MITKORRIGIERT, bewusst: drei weitere Briefe tragen `awaiting: trigger`
(ebenfalls nicht im Kanon). Dort ist die richtige Ersetzung NICHT eindeutig —
`deferred` und `dependency` bedeuten Verschiedenes, und die Wahl aendert die
Einreihung. Das gehoert gelesen, nicht geraten:
`codingjobs-lebenszyklus-routen-und-cli-stilllegen`,
`codingjobs-strukturell-entfernen`, `deployment-health-severity-wirkung-nachmessen`.
Fix pipeline-red-head-audit

„Kette aus" ist nicht „Kette rot" — Ursache statt Vermutung

Der Melder schrieb fuer JEDEN fehlenden Publish „die Kette meldet gruen, geliefert
wird nichts". Bei `ayoune-google-ads-client` meldete gar keine Kette etwas
(`pipelines_config` 404, 0 Laeufe). Der Satz beschrieb also einen anderen Zustand als
den vorliegenden — und schickte den Folge-Auftrag in die falsche Richtung: er suchte
den Fehler im `|| echo` des Publish-Schritts, waehrend der Schritt nie ausgefuehrt
worden war. ⚠ Diese Vermutung stammte aus MEINEM eigenen Brief; sie war nie gemessen.

Ueber zehn Bibliotheken gemessen: 5 mit eingeschalteter Kette, 4 OHNE Kette und
trotzdem veroeffentlicht (von Hand), 1 ohne Kette und ohne Paket. Die Haelfte hat gar
keine laufende Kette — und das ist offenbar in Ordnung.

Getrennt wird jetzt die URSACHE, nicht die Schwere: die Befund-Klasse bleibt
`paket-fehlt` (Ausstiegs-Vertrag unveraendert, beide Faelle faerben rot), dazu kommt je
Fund ein Feld `chain` und ein eigener Satz. Die Klassen-UEBERSCHRIFT ist jetzt
ursachen-NEUTRAL — der alte Satz steht nur noch dort, wo er zutrifft.

🔴 DREI BEFUNDE UEBER DAS EIGENE WERKZEUG, alle beim Verify aufgefallen:

1. `--registry` war fuer SCOPED Pakete WIRKUNGSLOS. Gemessen:
`npm view @tolinax/ayoune-http --registry https://registry.npmjs.org` liefert
`2026.1.0`, weil `npm config get @tolinax:registry` auf `registry.ayoune.app` zeigt
und eine scope-spezifische Registry ueber `--registry` GEWINNT. Gegenprobe im selben
Lauf: `npm view express --registry …` greift. Der Schalter war also fuer genau die
Pakete blind, fuer die dieser Waechter gebaut ist — und haette jeden Gegen-Lauf
gegen ein anderes Register still verfaelscht. Behoben ueber `npmViewArgs`, das den
Scope aus dem Paketnamen ableitet und `--@scope:registry=` mitgibt (3 Proben).

2. Die 404-ZWEIDEUTIGKEIT: Bitbucket antwortet auf `pipelines_config` mit 404, wenn die
Kette nie eingeschaltet wurde UND wenn es das Repositorium gar nicht gibt. Aus dem
404 allein folgt „nie eingeschaltet" NICHT. Geloest OHNE dritten Abruf: der
Publish-Durchgang laeuft per Bauart nur ueber lokal existierende Verzeichnisse, die
Existenz ist dort belegt und wird durchgereicht. ⚠ Ein erster Anlauf fragte sie per
API nach — und machte aus einem korrekten `kette-nie-gelaufen` ein `unbestimmt`,
sobald jene Abfrage aus einem anderen Grund als 404 fehlschlug. Eine Probe, die
scheitern KANN, darf eine belegte Tatsache nicht ueberschreiben.

3. Der Status-Regex stand als `/API (d{3}) /` statt `\d{3}` in der Datei (der Backslash
ging beim Einsetzen ueber ein Heredoc verloren). Er traf damit NIE, `configStatus`
blieb null, und JEDER Befund fiel auf `unbestimmt` — die ganze Nachschaerfung war
still ausgehebelt, ohne dass eine Probe rot wurde. Die vorhandenen Proben pruefen die
reine Klassifikation; der Regex lag im netzabhaengigen Abruf. Jetzt als reine
`statusFromError` herausgezogen und mit 3 Proben festgenagelt. LEHRE: eine
Zeichenketten-Auswertung gehoert in eine reine Funktion, sonst ist sie per Bauart
ungeprueft.

VERIFY, live gegen Bitbucket (nicht per Fixture):
- `ayoune-google-ads-client` -> `kette-nie-gelaufen` (404 + size=0, unabhaengig gemessen)
- `ayoune-email-blocks` -> `kette-laeuft` (enabled=true, size=9)
- chainCounts zaehlt beide getrennt: {nie-gelaufen 1, laeuft 1, unbestimmt 0}
- Negativ-Kontrolle mit ungueltigen Zugangsdaten: der Melder laeuft durch, meldet den
Registry-Befund und behauptet KEINE Ursache (`unbestimmt`), Ausstieg unveraendert.

Proben +21 (I Ursachen-Klassen · J Registry-Schalter · K 404-Zweideutigkeit ·
L Status-Regex), 84/84 in dieser Datei gruen. Volle Paket-Reihe 4797/8 — dieselben acht
Bestandsfehler wie vor der Aenderung (4776/8 in Iteration 2 gemessen), also +21 Tests,
+21 pass, fail unveraendert: keine Regression.

🟢 NEBENBEFUND: `ayoune-google-ads-client` ist inzwischen VEROEFFENTLICHT — der Brief,
den dieser Waechter am 2026-08-22 erzeugt hat, ist abgearbeitet worden. Der Live-Beleg
oben laeuft deshalb bewusst
Fix alarme

Alarm-Stufe aus dem Verhaeltnis ableiten, nicht aus der Absolutzahl

`checkHealth.ts:205` lautete `severity: unavailable > 1 ? 'critical' : 'warning'`.
Die Stufe hing damit an der ABSOLUTZAHL nicht verfuegbarer Instanzen statt am
Verhaeltnis zur Sollzahl - in beide Richtungen falsch, beides an echten Alarmen
gemessen (Fenster 2026-08-15..22, Sammlung `alerts`):

- Falsches critical: 636 von 688 kritischen `deployment_failed` trugen
`availableReplicas > 0`, liefen also weiter. Spitzenreiter sind drei
Ein-Instanz-Laeufer, bei denen "0 verfuegbar" der Normalzustand jedes
Neustarts ist (368 + 149 + 54 Alarme).
- Verpasster Totalausfall: ein Ein-Instanz-Dienst, der ganz ausfaellt, zeigt
`unavailable = 1` und war damit nur `warning` - niedriger eingestuft als das
harmlose Neuausrollen des Nachbarn.

Neu: `helpers/deploymentOutage.ts` haelt das Urteil EINMAL (rein, AY-frei) -
`outageScope` (full/partial/none) plus der aus `syncCluster` hierher gezogene
`deploymentHealthStatus`. Beide Regeln standen an getrennten Stellen und sind
genau deshalb auseinandergelaufen.

Karenz gegen Ausroll-Rauschen ueber den BESTEHENDEN Aufschlag des geteilten
Schreibers (`upsertAlert.escalateAfterMs`), kein neuer Mechanismus: ein
Totalausfall startet als `warning` und wird `critical`, sobald er die Karenz
(Vorgabe 10 min, `DEPLOYMENT_OUTAGE_CRITICAL_MINUTES`) ununterbrochen steht.
Gemessen trennt das sauber: von 1.960 Alarmen mit `availableReplicas = 0`
standen 1.934 (98,7 %) genau EINEN Takt, 25 laenger als drei Stunden, genau
einer dazwischen.

Zwei Praemissen des Auftrags korrigiert:
- `syncCluster.ts:81` (readyReplicas === 0) und `availableReplicas === 0` sind
NICHT dieselbe Regel - 430 Alarme ueber 33 Dienste tragen ready=1 bei
available=0. Fuer die Stufe gilt `availableReplicas`, weil
`escalationGate.decideEscalation` genau dieses Feld fuer sein FULL OUTAGE liest.
- Die vorgeschlagene Regel ALLEIN haette die Lage verschlimmert: sie macht aus
1.908 heutigen Warnungen kritische Alarme (Ein-Instanz-Neustart). Erst die
Karenz dreht das um. `status.updatedReplicas` geprueft und nicht gebaut -
ohne Zusatznutzen neben der Karenz.

Proben: `test/deploymentOutage.test.ts`, die vier Faelle des Verify-Gates plus
Karenz, Zahl-Lesung und Verhaltensgleichheit des Gesundheitszustands. Die
abgeloeste Regel steht als `legacySeverity` daneben, damit der rote Gegenpart im
selben Lauf entsteht. Flip-Probe gefahren: mit der alten Regel an derselben
Stelle fallen 11 Proben, die uebrigen 355 bleiben gruen. `npm test` 366/366.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix node

Spreizung zaehlt nur Pods derselben Fassung (matchLabelKeys, 0.1.10)

GEMESSEN, nicht abgeleitet: nach der Ausbringung von 0.1.9 landeten BEIDE Repliken
von ayoune-updates-develop-node auf demselben Knoten (fp7m), waehrend heartbeats im
selben Lauf sauber auf zwei Knoten verteilte. Gegenprobe: das Loeschen einer der
beiden updates-Repliken liess den Ersatz-Pod auf einem ANDEREN Knoten (fny8) starten
- der Mechanismus greift also, nur nicht waehrend eines Rollouts.

Ursache: der labelSelector der Spreizung (name/instance/app) trifft auch die Pods der
ALTEN Fassung. Waehrend eines Rollouts zaehlen die mit, die Schiefe wird gegen den
falschen Bestand gerechnet, und mit dem weichen ScheduleAnyway kann die Wertung dann
zugunsten eines einzelnen Knotens ausfallen.

matchLabelKeys: [pod-template-hash] beschraenkt die Zaehlung auf dieselbe Fassung
(stabil seit K8s 1.30, Cluster faehrt 1.35). Umkehrprobe gefahren: der API-Server
liefert das Feld im Objekt zurueck, verwirft es also nicht still.

Ein-Replikat-Render bleibt gegen 0.1.7 identisch (ausser dem Fassungs-Etikett);
leere matchLabelKeys-Liste laesst das Feld ersatzlos entfallen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New tasks

Code-Agent-Aktion aus der Registry ziehen (Pin auf interfaces 2026.452.0)

Der Pin-Bump zieht den neuen entityActions-Eintrag `dispatch-code-agent` nach —
ohne ihn rendert kein Knopf, egal was publiziert ist (actionsForEntity liest die
INSTALLIERTE Registry). Wirksam wird er an den zwei Aufgaben-Tabellen
(registerStates.ts:483/589) und in der Rechte-Ableitung.

An der Detail-Ansicht wird der generische Eintrag BEWUSST herausgefiltert: dort
stehen seit CEO 2026-07-08 zwei handgebaute Knoepfe auf derselben Route, die die
Laufzeit-Wahl (bodyData dc/k8s) und einen Bestaetigungs-Dialog tragen —
entityActions kann beides nicht ausdruecken (actionsForEntity setzt bodyData
fest). Ohne den Filter stuenden dort drei Knoepfe auf einer Route, davon einer
namenlos.

Lock-Beleg vor dem Push: interfaces/models/core je genau EINE Aufloesung, alle
gleich dem publizierten Stand. 726/726 Proben gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New node

PodDisruptionBudget + Knoten-Spreizung fuer Mehr-Replikat-Dienste (0.1.8)

Anlass: 2026-08-22T18:19Z raeumte ein ScaleDown des Cluster-Autoscalers BEIDE
Repliken von ayoune-states-develop-node zugleich (0/2, rund 14 Minuten). Das Chart
bot dem Autoscaler nichts an, was ihn bremst - im ganzen Baum existierte genau EIN
PodDisruptionBudget, und der gehoert dem Istio-Ingressgateway.

- templates/pdb.yaml: PDB, gerendert NUR bei replicaCount > 1, minAvailable: 1,
Selektor = die drei POD-Etiketten (name/instance/app). Ein echter bool in
podDisruptionBudget.enabled schlaegt die Automatik; maxUnavailable schlaegt
minAvailable.
- deployment.yaml: topologySpreadConstraints - Roh-Durchreichung (Bauform wie
affinity) plus Automatik ab replicaCount > 1 mit chart-gerendertem labelSelector.
whenUnsatisfiable: ScheduleAnyway, bewusst weich.
- values.yaml: podDisruptionBudget / topologySpreadConstraints / topologySpread.

Fuer die 227 Ein-Replikat-Dienste aendert sich NICHTS: helm template 0.1.7 gegen
0.1.8 ist identisch ausser dem helm.sh/chart-Etikett (das jeder Chart-Bump traegt);
bei replicaCount 0 und 1 wird weder PDB noch Spread gerendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix tasks

waitingOnOthers auf den Kanon-Wert blocked abbilden

Outlooks `waitingOnOthers` wurde auf `waiting` abgebildet — kein Wert des Kanons
`PmStatus`. CEO-Entscheid 2026-08-22 (via /interview), Weg (a): `blocked`.

Gemessen mit Positiv-Kontrolle: der Normalisierungs-Haken `TASK_LIKE_STATUS_MAP`
fuehrt sieben Eintraege, KEINER heisst `waiting` — die Annahme des Auftrags, der
Haken fange den Wert ohnehin ab, ist falsch. Diese Zeile ist der einzige Riegel.

Waechter-Test nagelt JEDEN ausgegebenen Status gegen den Kanon fest (nicht nur den
einen Fix); Flip-Probe: mit dem alten Wert 93/94 rot, mit dem Fix 94/94.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pm

Aufgaben-Aktion dispatch-code-agent in modelsAndRights deklarieren

Die Route POST /tasks/:id/actions/dispatch-code-agent existiert handgebaut seit
2026.28.0, war aber ohne Registry-Eintrag fuer Nutzer unerreichbar — actionsForEntity
baut ohne Deklaration keinen Knopf. Rein additiv: modelsAndRights bleibt bei 843
Eintraegen, null neue plural-Zeilen.

Bewusst OHNE emits (die custom-Route gewinnt per Mount-Reihenfolge, ein deklariertes
Ereignis haette keinen Erzeuger), ohne defaultOnScan (ein Barcode-Scan darf nie
ungefragt eine Code-Agent-Sitzung starten) und ohne right (die Ableitung
pm.tasks.actions.dispatch-code-agent traegt).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New pipeline-red-head-audit

zweiter Durchgang für VERÖFFENTLICHUNGS-Ketten — „Kette grün, geliefert wird nichts"

Der Wächter beantwortete bisher eine Frage: „Ist der letzte Lauf rot?" Ein
Repositorium, das nicht ausbringt sondern VERÖFFENTLICHT, fällt durch dieses
Raster per Bauart — es hat nie ein Deployment. `ayoune-vscode-copilot` war
deshalb vier Tage lang nicht abrufbar, während jedes neue cloud-init es zog und
leer ausging, ohne dass irgendwo ein Fehler entstand.

🔴 WARUM EIN ZWEITER DURCHGANG UND KEINE WEITERE KLASSE IN classifyRedHead:
der bestehende Weg beginnt mit selectRedHead, und bei einem GRÜNEN Kopf ist dort
Schluss (reason: 'kopf-nicht-rot'). Die Klasse `nie-ausgebracht` entsteht also
erst NACH einem roten Kopf. Der teure Fall ist aber genau der andere.

Vier reine, prüfbare Funktionen (netzfrei testbar):
classifyPipelineKind ausbringung | veroeffentlichung | keine — KOMMENTAR-BEWUSST,
sonst macht eine Zeile, die "kein gke-deploy" DOKUMENTIERT,
aus einer Bibliothek einen Dienst (dieselbe Falle hat die
Helm-Zahl zuletzt am 2026-08-20 um acht überhöht).
derivePackageName Kette > Publish-Verzeichnis > Wurzel-Name MIT Scope.
derivePublishDir cd <dir> && npm publish
classifyPublishState paket-fehlt (BEFUND) · paket-unbestimmt · registry-unbestimmt
· paket-vorhanden · null (kein Veröffentlichungs-Repo).

🔴 DIE REIHENFOLGE IN derivePackageName IST DER KERN, und der Anlassfall belegt
sie: ayoune-vscode-copilot trägt in seiner Wurzel-package.json den Namen
`ayoune-copilot` OHNE Scope — veröffentlicht wird @tolinax/ayoune-vscode-copilot
aus npm-dist/. Wer blind den Wurzel-Namen nimmt, fragt nach einem nie
veröffentlichten Namen und meldet einen Defekt, den es nicht gibt. Ein Name ohne
Scope wird deshalb NICHT geraten, sondern als `paket-unbestimmt` benannt.

Ehrlichkeits-Regel wie bei classifyRedHead: registryHas unterscheidet 404 vom
Netzfehler AM AUSGABETEXT, nicht am Ausstiegs-Code (npm view endet bei JEDEM
Fehler != 0). Ein unerreichbares Register ist `registry-unbestimmt`, nie ein Befund.

Damit automatisiert der Wächter die Regel aus feedback_pipeline-silent-no-publish
("Tags pushed != Lib published — ALWAYS empirisch mit npm view verifizieren"), die
bisher Handarbeit war und genau deshalb ausblieb.

🟢 ERSTER SCHARFER FLOTTEN-LAUF FAND SOFORT EINEN UNBEKANNTEN FALL:
ayoune-google-ads-client (domains/ads/lib-google-ads) — Kette grün, Paket 404,
letzter Commit 2026-06-19. Ursache dort ist eine Fehler-Unterdrückung IN der Kette
(npm publish || echo "Version already published, skipping"), die jeden Fehlschlag
als Erfolg meldet. 0 Verbraucher, also heute latent; eigener Brief, nicht hier behoben.

🟢 Und die Zahl trägt den Ausstiegs-Code: 46 von 47 Veröffentlichungs-Repositorien
sind grün. Der Wächter steht NICHT ab Tag eins auf rot — die Lärm-Regel des
Kopf-Kommentars bleibt gewahrt. Ab jetzt färben ZWEI Klassen rot.

🔴 NEBENBEFUND, beim Bauen gefunden und mitbehoben: slugForDir erkannte eine
.git/config mit ANMELDE-TEIL nicht (https://x-...-token-auth:<token>@bitbucket.org/...).
Ein so geklontes Repositorium fiel aus JEDEM Lauf heraus — weder auf roten Kopf noch
auf sein Paket geprüft, still unter `skipped` verbucht. Gemessen: 379 Ketten, 363
erkannt; von den 16 übrigen haben 15 gar keine .git/config (dort ist `skipped`
richtig), genau EINES scheiterte an der Adress-Form — ayoune-search-lib, und das ist
eine Veröffentlichungs-Kette. Ausgelagert als reine slugFromGitConfig, sechs Proben.

Proben: +32 (F reine Klassifikation · G Ende-zu-Ende im echten Skript-Lauf · H Kennung),
63/63 in dieser Datei grün. Flip-Probe, Negativ- und Positiv-Kontrolle je vorhanden;
die bestehenden Proben A-E bleiben unberührt und netzfrei (Fixture ohne __publish
überspringt den Durchgang). Volle Paket-Reihe 4776 Tests, 8 Fehlschläge — dieselben
acht, namentlich identisch, auch OHNE diese Änderung gemessen (git-stash-Gegenprobe):
Bestandsfehler des Pakets, keine Regression.
New coord

Walker prueft gegen queue/done + Aufraeumer meldet Fertig-Doppel

VORFALL 2026-08-22: `podcast-takt-zeit-je-objekt` fuhr 17:33:35Z ein sauberes
`coord done` (8596553cf2). 17:40:32Z claimte ein Walker denselben Brief erneut
aus toPost — auf dem AKTUELLEN Stand (merge-base --is-ancestor: ja). Ergebnis:
derselbe Slug in ZWEI Ordnern getrackt.

- Guard (a1) `already-done` vor dem O_EXCL-Lock, direkt nach `pending` — die
zurueckgekehrte Kopie traegt `status: pending`, der Vorgaenger-Guard faengt
sie also nicht. Benannter Grund `copy-in-queue/done`, nie stilles false.
- `coord reap` meldet FERTIG-DOPPEL (Slug in done UND toPost/blocked), nur
gemeldet, nie bewegt — die Ursache ist ungemessen, und eine getrackte Datei
wegen Namensgleichheit zu entfernen ist die riskantere Richtung.

GEMESSEN statt angenommen (der Brief verlangte es ausdruecklich): der bestehende
`done-zombie`-Zaehler stand zu Recht auf 0 — `classifyZombieBrief` laeuft
ausschliesslich ueber queue/active, der Doppel-Eintrag lag in toPost. Anderes
Quellverzeichnis, kein schwaecheres Signal.

`active` ist bei FERTIG-DOPPEL BEWUSST ausgenommen: waehrend eines laufenden
`coord done` liegt derselbe Slug kurz in active UND done.

Proben: 10 neu, ins test:coord-Fach VERDRAHTET (1066 -> 1076, Fehlschlaege
unveraendert 5 = Bestand, beidseitig gegengeprueft). Flip-Probe beidseitig.
Flottenweite Zaehlung 0 auf beiden Achsen, zweifach gemessen.
Fix pm-states

zwei Waechter-Befunde aus Kette #219 — Umlaute in der Beschriftung + Drift-Sentinel

Der Vor-Commit `8c7d7ed` brachte die Kette zu Fall, und beide Waechter hatten recht:

1. `germanUmlautHygiene` — die Kurzbeschreibung des neuen Zustands trug „pruefen"
und „uebernehmen" statt „prüfen"/„übernehmen". Der Waechter liest die
einfach-gequoteten Zeichenketten der Seeder (Kommentare vorher entfernt) gegen
die geteilte Stamm-Liste `@tolinax/ayoune-umlaut-stems`.
2. `registerStates.test.ts` — der Drift-Sentinel steht per Bauart auf einer festen
Zahl; ein neuer Zustand IST die beabsichtigte Abweichung. 89 -> 90, mit der
Herleitung in der Zeile wie bei allen Vorgaengern.

🔴 Die Lehre, nicht der Fix: `npm run build` war gruen und hat NICHTS davon
gesehen. Beide Waechter haengen an `npm test`, und genau der wurde vor dem Push
nicht gefahren. Die repo-eigene Testreihe ist vor dem Push Pflicht, nicht der Bau.

Der Umlaut-Waechter ist damit an der Sache selbst als beissend belegt — er hat den
Fehler in Kette #219 real gefangen, nicht in einer gestellten Probe.

Testreihe jetzt: 44/44 Suites, 726/726 Proben gruen.
New sub-view

`ayoune-pm-onboarding-assistant` registrieren — mit LEEREM Eingangs-Vertrag

Zweiter Andockweg des Assistenten „Bilde deine Software ab" (Muster
`ayoune-hub-question-thread`): Zustand + Route sind der eine Weg, diese
Registrierung der andere — sie laesst die Komponente kuenftig als Reiter eines
Detail-Verbunds haengen. Fuer den EINSTIEG ist sie wirkungslos; den liefert der
Zustand `pm.onboarding` plus die bestehende Route `/pm/onboarding`.

Der Eingangs-Vertrag ist leer, und das ist gemessen statt gewaehlt: die
Komponente traegt KEIN einziges `@Input()` (anders als die beiden Nachbarn) —
sie holt Rechte ueber `PageService` und Daten ueber `ApiService`. Der
Vorgabe-Mapper in `inputsFor()` wuerde ihr `{consumer, consumerId, pageData,
id}` zuweisen, und `ComponentRef.setInput` auf einen nicht deklarierten Eingang
meldet in Angular 21.1.4 `NG0303` (`_debug_node-chunk.mjs:8437`) — in
Produktion sogar STILL, also unbemerkt. Der leere Mapper ist deshalb Pflicht,
nicht Kosmetik; der im Auftrag vorgeschlagene Drei-Feld-Mapper waere ein Defekt
gewesen.

Zwei Proben dazu, mit Negativ-Kontrolle (ohne eigenen Mapper kaeme der
generische Vier-Schluessel-Abzug). Flip-Probe gefahren: mit dem
Drei-Feld-Mapper faellt genau die eine Probe, die Kontrolle bleibt gruen.
13/13 gruen.
New pm-states

Zustand `pm.onboarding` macht den Assistenten „Software abbilden" auffindbar

Der Assistent ist seit dem 2026-08-22 gebaut, geprueft und ausgebracht
(clients/mobile-app 9002f976, Route `/pm/onboarding`) — aber nur ueber den
Tiefverweis erreichbar. CEO-Entscheid vom selben Tag, Weg (a): ein
Platzhalter-Zustand plus kuratierter Verweis im Modul-Raster.

Die Bauform ist `accounting.ledgerimport` / `accounting.anhang`, NICHT der im
Auftrag genannte HR-Assistent — gemessen: `base-page.component.html` rendert
bei `pageType:'component'` ausschliesslich einen `<router-outlet>`, der Rumpf
kommt also von der Angular-Route. `platform/states` leitet die Adresse
deterministisch aus dem Titel ab (`moduleOverviewLinks.ts:154`), `pm.onboarding`
→ `/pm/onboarding`, und genau dort haengt die bestehende Route. Deshalb
BEWUSST kein `componentSelector`: der wird nur von `sub-view.component.ts:230`
gelesen, also fuer einen Reiter innerhalb eines Detail-Verbunds — auf oberster
Ebene liest ihn niemand, und ihn zu setzen behauptete eine Wirkung, die es
nicht gibt.

Wozu dann der Zustand? `resolveNavigationLinks` verwirft einen kuratierten
Verweis ohne Ziel-Zustand STILL (`moduleOverviewLinks.ts:348`). Ohne diesen
Eintrag bleibt der Assistent unauffindbar.

`_right: 'pm.modules'` ist ein BESTEHENDES Recht — dasselbe, das
`scan-repository` verlangt und das der Baustein selbst prueft; ein aus dem
Titel abgeleitetes `pm.onboarding` gibt es als Recht nicht und der Zustand
waere fuer alle unsichtbar.

Belegt am Bau (nicht behauptet): `getStates()` liefert den Zustand mit
`pageType:'component'` und `pwaLink:/pm/onboarding`, der kuratierte Verweis
steht an Position 13 von 30 direkt hinter „Komponenten"; Umkehrprobe mit einem
erfundenen Ziel liefert nichts.
New scheduler

CHECK_PODCAST_EPISODES wird Faelligkeits-Tick alle 10 Minuten

CEO 2026-08-22, woertlich: 'Was ist wenn ein anderer Kunde das Briefing ODER DEN
PODCAST erst um 9 haben will?' Dieselbe Korrektur wie bei CHECK_TENANT_BRIEFINGS
(4b076c9) und aus demselben Satz. Muster CHECK_EXPORTS: haeufiger Tick ohne
Uhrzeit, Faelligkeits-Pruefung je Objekt.

15 7 * * * Europe/Berlin -> */10 * * * * (ohne tz — ein Zehn-Minuten-Tick ist
zeitzonen-frei; eine tz hier waere eine Behauptung ohne Wirkung und suggerierte,
die Uhrzeit laege wieder im Cron).

Die VORGABE bleibt 07:15 Europe/Berlin und liegt jetzt in
worker-automatic-checks-v2/src/lib/podcastEpisodeCore.ts. Sie ist bewusst NICHT
die 07:00 des Briefings: der abgeloeste Cron stand nach den Briefings, weil beide
dasselbe Artikel-Fenster lesen und die Folge den Stoff des Morgens tragen soll.
Bestands-Sendungen verhalten sich damit exakt wie vorher.

5 neue Registry-Proben (Tick-Vertrag, keine tz, Beschreibung nennt sendTime/
sendDays, Vorgabe 07:15, attempts 1, kein Mandant im Namen). Reihe 70/70 (vorher 65).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVmnsay4L5Rw8MvnFMVqmj
New podcast

Sendezeit je Sendung statt festem 07:15-Cron

CEO 2026-08-22, woertlich: 'Was ist wenn ein anderer Kunde das Briefing ODER DEN
PODCAST erst um 9 haben will? Der Scheduler sollte nur checken welche in der
Zukunft liegen wie bei anderen Themen auf der Platform auch, siehe Exporte und
Reporte.' Dieselbe Korrektur wie bei den Briefings am selben Tag.

GETEILT, NICHT KOPIERT: die Faelligkeits-Logik liegt jetzt in lib/sendSchedule.ts
(rein, uhrlos, ohne DB-Zugriff). tenantBriefing.ts importiert sie und re-exportiert
unter seinen bisherigen Namen weiter, also ohne Aufrufer-Nachzug; der Vorgabewert
ist Parameter geworden, weil Briefing 07:00 und Podcast 07:15 traegt.

PARITAET IST DER TRAGENDE TEIL: eine Sendung ohne Zeit-Felder faellt auf 07:15
Europe/Berlin und ihre schedule-Tage zurueck — exakt das Verhalten des abgeloesten
Cron. Die 07:15 sind kein Schoenheitsfehler: der alte Cron stand bewusst NACH den
Briefings (07:00), weil beide dasselbe Artikel-Fenster lesen.

12 neue Proben, davon 5 Paritaets-Proben. Flip-Probe: Vorgabe testweise auf 07:00
gezogen -> genau diese 5 fallen, nichts anderes. Volle Reihe 489/489 (vorher 477).

Zwei info-Zeilen auf debug: der Takt laeuft jetzt 144-mal am Tag statt einmal.

OFFEN und gemessen: sendTime/sendDays/timezone stehen WEDER im Vertrag IPodcast
NOCH im Podcasts-Schema (models 2026.456.0). Gelesen werden sie ueber .lean(),
wie die Datei schon autoPublish und curation liest; GESCHRIEBEN werden koennen sie
noch nicht (strict verwirft sie lautlos). Der Lese-Weg steht bereit, die Eingabe
fehlt — Fundament-Glied als eigener Brief podcast-sendezeit-felder-am-vertrag.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVmnsay4L5Rw8MvnFMVqmj
Fix playground

offenen Weiterleiter haerten — Sperrliste, Rate-, Zeit- und Groessen-Deckel

Gemessener Ausgangszustand (2026-08-22): POST /api/playground/proxy war OHNE
Anmeldung nutzbar, OHNE Rate-Begrenzung, OHNE Zeitlimit und OHNE Groessen-Deckel,
und liess JEDE .ayoune.app-Unterdomaene zu — su-api (Superuser/godfather, laut
Wurzel-CLAUDE.md ausdruecklich nie aus Kunden-Clients), credentials-api
(Zugangs-Tresor) und registry (private Paket-Registry) eingeschlossen. Der
Weiterleiter verbarg dabei die Herkunfts-Adresse des Aufrufers hinter der
Pod-Adresse.

Der Riegel liegt in der REINEN Funktion checkPlaygroundTarget (kein Express,
kein Netz, kein Zustand) — nur so ist er ohne laufenden Dienst pruefbar:
- Sperrliste privilegierter Wirte, ausgewertet VOR der Nachsilben-Pruefung
(sonst faellt su-api durch das -api-Muster wieder herein)
- Anmeldedaten in der Adresse abgewiesen (verschiebt, was ein Mensch LIEST,
ohne zu aendern, wohin die Anfrage GEHT)
- ausdruecklicher Port abgewiesen (sonst Port-Abtaster fuer alles unter der
Nachsilbe); der Regel-Port bleibt erlaubt
- Rate-Begrenzung 20/min je Herkunft ueber das vorhandene checkRateLimit
- Zeitlimit 10 s und Antwort-Deckel 512 KiB; redirect manual, weil eine
Umleitung aus einem erlaubten Wirt einen gesperrten machen koennte, NACHDEM
die Pruefung gelaufen ist

Verify: npm run build Ende 0 / 0 TS-Fehler; neue Reihe 19/19; volle Reihe des
Repositoriums 1632/1632, 0 Fehler. Die Reihe traegt drei FLIP-PROBEN, die die
alte Regel woertlich nachbauen und belegen, dass sie genau diese Faelle
durchliess — samt Gegenkontrolle, dass das regulaere Ziel crm-api.ayoune.app
bei BEIDEN Regeln durchgeht, der Playground also nicht gebrochen ist.

Brief: api-lieferwege-playground-und-livemap (Punkt 5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T2uHkCzw3A1XWKSCf6c7JF
New tasks

type-Normalisierung (Stufe 2) + zwei nachgetragene status/priority-Luecken

CEO-Entscheid 2026-08-21 (E10). Neue Karte TASK_TYPE_MAP faengt die zwei gemessenen
Schreibweisen-Dubletten am SCHREIBWEG ab: 'Code-Agent' -> 'CodeAgent', 'Task' -> 'Aufgabe'.
Registriert an `Tasks` neben status/priority.

WARUM DER HAKEN UND NICHT DIE UNION: gemessen 2026-08-22 schreiben die realen Erzeuger
ueber `any`/`string`-typisierte Objektliterale (`tooling/cli` dispatcher.ts:365 schreibt bis
heute 'Code-Agent'; `platform/middleware-worker` handleBitBucket.ts:66) — eine geschlossene
TypeScript-Union faengt davon NICHTS, dieser Haken alles. Er ist damit der tragende Riegel
des Auftrags, nicht der Vertrag.

NICHT in der Karte: 'CodeAgent-Todo'. Am Bestand gemessen ein EINZELPUNKT INNERHALB einer
Sitzung (origin `code-agent-hook:<id>`), nicht die Sitzungs-Aufgabe (origin
`code-agent-hook`) — der lebende Erzeuger sucht ihn idempotent ueber genau diesen Wert.

ZWEI GEMESSENE LUECKEN NACHGETRAGEN (dieselbe Schreibstelle, middleware-worker, ausgerollt):
`Erledigt` -> done und `Hoch` -> high waren der Karte UNBEKANNT und liefen damit unbehelligt
in den Bestand. Wirkt zugleich fuer Projects/FeatureRequests, die dieselbe Karte teilen.
Der Erzeuger liegt ausserhalb der Anspruchspfade — die Karte ist der Hebel ohne Fremd-Edit.

`waiting` (worker-outlook) bleibt BEWUSST unzugeordnet: es gibt kein offensichtliches
Kanon-Ziel, und raten waere schlimmer als offenlassen. Liegt als Rest-Schnitt-Brief.

19 neue Proben, davon vier gegen das ECHTE Tasks-Schema (die Attrappen-Tests belegen die
Mechanik, nicht die Verdrahtung). Flip-Probe: Haken-Zeile entfernt -> genau der eine
Verdrahtungs-Test faellt, alles andere bleibt gruen. Volle Reihe 707/707.

interfaces-Caret auf ^2026.451.0 (Fundament-interne Kette bleibt Caret).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVmnsay4L5Rw8MvnFMVqmj
New pm

PmTaskType-Kanon fuer tasks.type (Stufe 2) + ITask.type verdrahtet

CEO-Entscheid 2026-08-21 (decisions.log 20:22:43Z, LOCKED, E10). Der Kanon ist aus dem
GEMESSENEN Bestand abgeleitet (42.521 Aufgaben, nach dem Dublettenlauf
`ayauto migrate task-status-type-kanon`): elf Werte, `ITask.type` traegt jetzt
`PmTaskTypeValue` statt `string`.

BEWUSST OFFEN gelassen (`| (string & {})`), wie `status`: das Schliess-Gate des Auftrags
ist NICHT erfuellt. Gemessen schreiben die realen Erzeuger ueber `any`/`string`-typisierte
Objektliterale (`tooling/cli` dispatcher.ts:365, `platform/middleware-worker`
handleBitBucket.ts:66) — eine geschlossene Union faengt davon NICHTS. Der tragende Riegel
ist der Normalisierungs-Haken in `platform/models`, nicht dieser Typ.

BEWUSST NICHT sprach-normalisiert: `type:'Bug'` traegt die plattformweite
Errors-as-Tasks-Konvention (24.468 Dokumente). Eine Umbenennung auf `bug` waere ein
Flotten-Eingriff mit Leser-Implikationen, kein Vertrags-Detail — Produkt-Entscheid, CEO.

`CodeAgent` und `CodeAgent-Todo` sind ZWEI Sachverhalte, keine Dublette (Sitzungs-Aufgabe
gegen Einzelpunkt innerhalb einer Sitzung) — beide im Kanon, kein Merge.

Flip-Probe beidseitig gefahren: offen -> erfundener Wert geht durch; testweise geschlossen
-> TS2322 auf `type` UND `status`, waehrend die Kanon-Positiv-Kontrolle still bleibt.
Der Vertrag haengt also wirklich am Feld.

Nicht-brechend: PmTaskTypeValue ist von jedem string zuweisbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVmnsay4L5Rw8MvnFMVqmj
New onboarding

user.invite-sent wird zu einer echten Einladungs-Mail

Der Einladungs-Weg endete nach dem aYOUneInvites-Datensatz: die Vorlage
onboarding/invite-user-to-tenant liegt seit Monaten global in mails, aber eine
Suche nach ihrem Namen ueber die ganze Flotte fand NUR den Seed — kein
Verbraucher. Die eingeladene Person erfuhr nie von ihrer Einladung.

dispatchOnboardingMail haengt als fuenfter Zweig in defaultTriggers, ist also
eine Plattform-Regel, die ein Mandant nicht loeschen kann (gleiche Begruendung
wie bei den drei Geschwistern). Versand ueber den consumer-losen
sendSecurityEmail-Weg, weil eine eingeladene Person noch keinen Consumers-
Datensatz hat. Adresse auf PASSWORD_RESET_FRONTEND_URL mit Marken-Segment,
nicht auf authHost — das ist die API, nicht die Oberflaeche.

18 Proben inkl. fuenf Negativ-Kontrollen; beide Flip-Proben beissen
(Vorlagen-Name 1 rot, Marken-Segment 3 rot), Geschwister 50/50 gruen.
New i18n

zwoelf Nav-Beschriftungen und den Menue-Titel in den globalen Seed

Die Seitenleiste und die Strg+K-Palette zeigten rohe Uebersetzungs-Schluessel.
Gemessen am ausgelieferten Sprachpaket (GET /locale/{de,en,it,es}.json), nicht
am Quelltext: die PWA verweist auf 37 nav-Beschriftungen, zwoelf davon fehlten
in ALLEN vier Sprachen. Der Befund nannte sechs; die restlichen sechs sind
dieselbe Familie und standen roh im selben Bild.

Die deutschen und englischen Texte sind aus dem bereits sichtbaren
Client-Rueckfall uebernommen (admin-nav-i18n.ts, me-sections-i18n.ts), nicht neu
gewaehlt: der Rueckfall setzt nur Schluessel, die das geladene Paket nicht kennt,
also gewinnt ab diesem Seed der Server. Eigene Worte haetten die Beschriftung
beim Ausrollen still geaendert.

Dazu ein Waechter, der die verwendeten Schluessel aus den beiden
Definitions-Dateien der PWA LIEST statt sie abzuschreiben, und sie gegen die
Vereinigung aller Seed-Quellen haelt. Flip-Probe gefahren: ein entfernter
Schluessel macht ihn rot.

Schluesselzahl im Kopf neu gemessen: 290 -> 303.
New migrate

task-status-type-kanon — Bestand von tasks.status/.type auf den Kanon

Bestands-Nachtrag zu CEO-Entscheid 2026-08-21 (E7+E10). Zwei getrennte Achsen:

status 'Nicht begonnen'->open, 'Abgeschlossen'/'Erledigt'->done, ... — die Paare
stehen bereits in TASK_LIKE_STATUS_MAP, der Schreib-Haken deckt aber nur
NEUE Schreibvorgaenge; dieser Lauf holt den Bestand nach.
type NUR Schreibweisen-Dubletten: 'Code-Agent'->'CodeAgent', 'Task'->'Aufgabe'.

NICHT zusammengefuehrt wird 'CodeAgent-Todo': am Bestand gemessen ist das ein
EINZELPUNKT INNERHALB einer Sitzung (origin code-agent-hook:<id>), nicht die
Sitzungs-Aufgabe (origin code-agent-hook), und der lebende Erzeuger sucht ihn
idempotent ueber genau diesen Wert.

Die Sprachmischung (Bug/Aufgabe/...) wird NICHT aufgeloest — Produkt-Entscheid, CEO.

Trocken als Vorgabe. --apply zieht VORHER ein vollstaendiges Rueckroll-Abbild und
schreibt ohne dieses nicht (harter Abbruch, kein catch).

Gefahren 2026-08-22 gegen Produktion: 15 (status) + 302 (type) geschrieben,
Zweitlauf 0 Kandidaten, Kreuzprobe 42521 == 42521 auf beiden Achsen.
Abbilder mitversioniert unter .claude/reports/status-typ-kanon/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UVmnsay4L5Rw8MvnFMVqmj
Fix onboarding

create-with-welcome legt username an — unique-Index ohne sparse liess jede Anlage in E11000 laufen (500)

Ursache am Bestand gemessen: ayouneusers traegt username_1 als unique OHNE
sparse/partialFilterExpression, und der EINE erlaubte Platz ohne Wert ist seit
dem 2026-07-12 belegt (1 von 200 Zeilen). Die Route legte ohne username an, also
scheiterte JEDE Anlage — zweimal 500 ohne Datensatz, gemessen 2026-08-22.

Neu: deriveUsername leitet deterministisch ab (Adresse, sonst Adresse+Mandant,
sonst zaehlend) und scheitert laut statt still. Zusaetzlich antwortet die Route
auf einen Schluessel-Konflikt jetzt mit 409 statt 500. 8 Proben inkl. zwei
Negativ-Kontrollen.
New rag

hide-internal-doc-chunks in den rag-backfill-Verteiler haengen

Der Lauf `hideInternalDocChunks` war seit 0bafeff gebaut und getestet, aber
nicht verdrahtet: der Auftragsname war dem Verteiler unbekannt, und der
Rueckfall am Ende haette ihn still durch `backfillChunkLanguage` ersetzt --
also einen falschen Lauf ohne Fehlermeldung geliefert.

- src/index.ts: Einfuhr + Verteiler-Zweig VOR dem Rueckfall, Kopf-Kommentar
der Auftrags-Liste um die Signatur ergaenzt.
- package.json: `hideInternalDocChunks.core.test.mjs` in die Testkette.

Verify: Bau gruen; Testkette gruen und die neue Reihe belegt in der Ausgabe
(`hideInternalDocChunks.core: 56 passed, 0 failed`). Flip-Probe gegen das
kompilierte dist/index.js gefahren: mit dem Zweig HINTER dem Rueckfall landet
`hide-internal-doc-chunks` in `backfillChunkLanguage` -- die Reihenfolge ist
damit gemessen, nicht behauptet. Der Uebersetzer faengt es NICHT (der Zweig
ist zwar unerreichbar, bricht den Bau aber nicht).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DW1XRFQDrDUNPyT6mdAoB
New coord

Einzelschreiber-Riegel unterscheidet Reservierung von Schreiber (CEO 2026-08-22)

Der Fundament-Riegel sperrte bis dahin gleich hart, egal ob ein Brief in toPost LIEGT
(Reservierung, niemand arbeitet) oder eine Sitzung AKTIV schreibt — und der ERSTE
gefundene Halter gewann, unabhaengig von Prioritaet und sogar davon, ob er ueberhaupt
dispatchbar war.

GEMESSENER SCHADEN (2026-08-22, DREI Faelle an EINEM Tag):
- Ein P3 hielt den models-Platz vor einer P1-Kaskade mit drei Gliedern.
- Ein P2 hielt den interfaces-Platz SECHS STUNDEN vor einem P1 — und war dabei selbst
nicht dispatchbar (NOT-READY:fe-coverage-missing). Ein Brief, den niemand claimen
KANN, blockierte einen, den jemand claimen soll.
- Ein weiterer P3 rueckte sofort nach, als der erste geraeumt war.
Alle drei mussten von Hand per demote --force weggeraeumt werden.

DIE REGEL:
- active / active-session sperren IMMER — dort schreibt jemand; Prioritaet darf einen
laufenden Schreiber nie verdraengen (Einzelschreiber-Garantie).
- toPost sperrt nur gegen gleich- oder hoeherrangige Anwaerter — eine Reservierung
weicht einem STRIKT dringlicheren Vorgang (gleicher Rang sperrt weiter, sonst
entstuende ein Wettlauf zweier claimbarer Briefe).
- FAIL-CLOSED: fehlt eine der beiden Prioritaeten oder ist sie unlesbar, haelt die
Sperre — ein Tippfehler darf nie zum Freifahrtschein werden.
- Rueckwaerts-vertraeglich: ohne uebergebene Prioritaet gilt das alte Verhalten.

VERIFIKATION:
- 17 neue Proben, im test:coord-Fach namentlich verdrahtet.
- Flip-Probe: Ausnahme neutralisiert → 4 von 17 rot, wiederhergestellt 17/17.
- Ende-zu-Ende am ECHTEN Fall: status-und-typ-kanon (P1) war vom P2-Halter REFUSED,
nach dem Fix "would promote".
- Bestands-Reihe coord-admin 257/259 — die 2 roten sind VORBESTEHEND, beidseitig
belegt per git stash (identisch rot am unveraenderten HEAD; byte-genaue
Wiederherstellung per Pruefsummen-Vergleich). Beide ohne Bezug zum Riegel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TzYJQKJ6eGLzeZXmLMMxX
Fix pm

lesbare Beschriftungen — primary war als Text auf dunklem Grund blind

Sicht-Kritik am LIVE ausgebrachten Stand (pwa.ayoune.app, dunkles Thema), nicht
am Entwurf: zwei Stellen faerbten Text in `--mat-sys-primary` und waren damit die
am schlechtesten lesbaren der ganzen Seite.

- Der AKTIVE Schritt der Schiene traf es am haertesten — ausgerechnet seine
Beschriftung war unlesbar, waehrend die Ziffer daneben (primary als FLAECHE
mit `on-primary` darauf) einwandfrei stand. Die Betonung traegt jetzt Rahmen
und Ziffer, der Text hat volle Textfarbe.
- „Alle"/„Keines" sind Neben-Griffe: Sekundaerfarbe plus Unterstreichung, beim
Ueberfahren volle Textfarbe.

Lehre: in einem dunklen M3-Thema ist `primary` als Flaeche gedacht, nicht als
Textfarbe auf dunklem Grund.

Verify: `npm run build:stage` Exit 0 inkl. Schrift-Check.
New i18n

Fehlertexte fuer die Firmen-Seite in Endkunden-Sprache

Sechs Schluessel fuer den neuen Fehler-Zweig der PWA-Firmen-Seite
(`clients/mobile-app/.../company.component.ts`): eine Ueberschrift, die
Zusicherung dass nichts verloren ist, und vier nach Ursache unterschiedene
Meldungen (offline / keine Berechtigung / Dienst startet / Dienst antwortet
nicht).

Anlass: die Seite zeigte bei einem fehlgeschlagenen Ladeaufruf ein leeres
Formular ohne jeden Hinweis. Ein Rohtext der Gegenstelle taugt dafuer nicht —
er lautete woertlich „Cannot read properties of undefined (reading 'findOne')".

Alle vier Sprachen (de/en/it/es) wie im Bestand, alphabetisch eingeordnet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix pm

Assistent nennt die richtige Grenze und sagt, WAS er liest

Zwei Befunde aus dem Live-Durchlauf gegen echte Repositorien (pwa.ayoune.app,
Mandant tolinax) — beide gefunden, weil die Fläche wirklich gefahren wurde:

1. Der Abschneide-Hinweis schob JEDE Grenze auf die Tiefe und meldete am
Wurzel-Repositorium woertlich "0 Eintraege unterhalb von Ebene 3" — der
Leser war dort an der SEITEN-Grenze. Ein Hinweis, der die falsche Ursache
nennt, ist schlimmer als keiner: er schickt den Leser die Tiefe erhoehen,
wo die Seitenzahl das Problem ist. Jetzt getrennt benannt.
2. Filtert die Suche die gewaehlte Karte aus der Sicht, blieb "Struktur lesen"
scharf, ohne dass irgendwo stand, WAS gelesen wird. Die Fussleiste nennt
das gewaehlte Repositorium jetzt beim Namen.

Verify: 16 Proben gruen, `npm run build:stage` Exit 0 inkl. Schrift-Check.
Fix portrait

das Geschlecht wird gelesen statt geraten

`IEmployee.gender` stand im Vertrag und erreichte den Prompt-Bau nie — er las
ausschliesslich `appearance`. Drei von sechs erzeugten Portraets trugen daraufhin
das falsche Geschlecht ("Tobias Kern" als Frau, CEO-Sichtbefund 2026-08-22). Der
Fehler war STILL: kein Fehlschlag, kein Protokolleintrag, nur ein falsches Bild.

- `PortraitSubject.gender` (`m|w|d|x`, 1:1 der Vertrag) + Satz im Prompt
- `d`/`x` behaupten bewusst NICHTS — aus ihnen folgt kein Bild-Merkmal
- fehlt das Feld, wird aus `appearance` abgeleitet (de+en, WORTGRENZEN:
"woman" enthaelt "man"); nennt der Text beide, wird nichts abgeleitet
- schweigen beide Quellen: `PortraitGenderUnknownError` statt Ratewurf,
im Router als 422 mit echter Meldung statt 502 "bitte erneut versuchen"
- der Aufrufer reicht `employee.gender` aus der Akte durch (die fehlende Zeile)
- `buildStylizePrompt` bleibt BEWUSST ohne Geschlechts-Satz: beim Angleichen
fuehrt das Foto, ein widersprechendes Feld erzwaenge den Gesichts-Tausch,
den PORTRAIT_IDENTITY_CLAUSE verbietet
- `portraitSeed` unberuehrt — die Szenen-Wahl darf sich nicht verschieben,
wenn jemand ein Geschlecht nachtraegt

Proben: 915/915 gruen (13 neue, mit Umkehr- und Negativ-Kontrollen).
Gegenprobe am gebauten dist fuer m / w / leer / leer+Freitext / d / Angleichung.

Brief: portraet-erzeuger-liest-das-geschlecht-nicht
Fix kiosk-settings

lokalen Schreibweg (applied$) an die Kopfleiste koppeln

Der Socket-Ruf des Servers war der einzige Abonnent der Kiosk-Einstellungen —
`KioskSettingsService.applied$` (die HTTP-Antwort des eigenen Schreibvorgangs)
hatte flottenweit null Abonnenten. Der Socket-Ruf ist aber best-effort: der Server
schluckt jeden Fehler (config-api lib/kioskDeviceSettings.ts:169-173) und antwortet
trotzdem mit 200. Ist der Melder nicht erreichbar oder das Geraet gerade nicht im
Raum controlpanel:<id>, blieben Kopfleiste, Ton-Politik und Stumm-Riegel bis zum
naechsten Neuladen auf dem alten Stand.

- applied$ abonniert, mit feedPanel=false (das Panel kennt den Wert aus seiner
eigenen save()-Antwort; ein Rueckfluss ueber external$ waere eine zweite
Zuweisung desselben Wertes)
- 9 Proben: Kopplungswirkung, Panel-Rueckfluss-Schalter, Fern-Weg unveraendert,
Geraet ohne Alarm-Anzeige, leerer Satz, plus die Bibliotheks-Kettenhaelfte gegen
die echt installierte Lib (voller Server-Satz, Weg-Trennung, Fehlerfall)
- angular.json: die neue Spec in die explizite include-Liste (ohne den Eintrag wird
eine Spec-Datei hier NIE eingesammelt und laeuft still gar nicht mit)

Verify: Bau gruen (Application bundle generation complete), 188/188 Proben gruen,
Flip-Probe beisst (Schalter ausgehebelt -> genau die Schalter-Probe rot).
New pm

Assistent "Bilde deine Software ab" — Oberflaeche (W5, Weg b)

Statischer PWA-Baustein unter `pm/onboarding`, CEO-Entscheid 2026-08-22 Weg (b).
Der Motor (scan-repository / apply-proposal, pm-api `a6b805b`) bleibt unangetastet.

Drei Schritte, und die Reihenfolge traegt eine Aussage: verbinden -> pruefen ->
uebernehmen. Erst der dritte schreibt.

- Der BELEG steht sichtbar an jeder Zeile (die Manifest-Datei, die den Vorschlag
traegt), nicht in einem Tooltip. Was nicht eindeutig war, steht als OFFEN
gleichberechtigt daneben statt in einer Fussnote — ein Vorschlag, der seine
Luecken verschweigt, sieht vollstaendig aus und ist es nicht.
- Zuordnungen sind korrigierbar; ein abgewaehltes Modul zieht seine Zuordnungen
mit, sonst zeigte die Oberflaeche eine Zuordnung, die der Server als OFFEN
behandelt. Eine Komponente ohne geltendes Modul reist OHNE `moduleKey`.
- Rechte prueft der Baustein SELBST (`pm.modules` / `pm.modules.edit`), weil kein
SDUI-Zustand die Sichtbarkeit uebernimmt — ueber den treuen Spiegel
`holdsRight`, also dieselbe Glob-Semantik wie `checkRights` im Kern.
- Benannte Fehler statt leerem Vorschlag: ein fehlender Vault-Zugang wird als
solcher gezeigt, nicht als leere Struktur.

Verify: `npm run build:stage` (das Gate, das master faehrt) Exit 0 inkl.
Schrift-Check BESTANDEN; 16 Proben gruen; Flip-Probe am Rechte-Riegel faellt
mit genau 2 roten Reihen und ist zurueckgebaut.
Fix codegen

unbefuellter Aktions-Rumpf verweigert laut (422) statt still 200

Bis hierher verwarf ein Rumpf mit leerer ALLOWED_FIELDS jedes Rumpf-Feld,
schrieb nur noch `_id` und antwortete 200 mit dem unveraenderten Dokument —
fuer den Aufrufer von Erfolg nicht zu unterscheiden. Genau dieses Bild hat
den Etikettendruck der Lagerplaetze sieben Wochen lang stillgelegt.

Der leere Vorgabewert bleibt RICHTIG (fail-closed; der Erzeuger kann die
Felder einer Aktion gar nicht kennen). Geaendert ist, was der Rumpf damit tut.

- 422, nicht 501: 501 hiesse „nicht auf diesem Wirt" und trifft nicht zu; die
Aktion IST zustaendig, sie hat nur keinen Rumpf. Vorbild: die dreifach im
Bestand umgesetzte `refuseUnimplementedAction` (hr/config).
- Der Sammel-Zweig behaelt sein 501 — dort ist die ADRESSE unbedienbar.
Reihenfolge test-gepinnt: 501 greift VOR 422.
- Kein Schalter, kein Opt-out: sobald ein Feld opt-in ist, faellt der Zweig
weg und der Pfad laeuft unveraendert.
- Die Kopf-Prosa beider Varianten nachgezogen; die emits-Variante sagt
ausdruecklich, dass auch KEIN Ereignis mehr feuert (die Verweigerung greift
vor dem Ereignis-Block).

Belege: echter Erzeuger-Lauf gegen cms/WebPages in einem Wegwerf-Ziel, der
erzeugte Rumpf gelesen UND uebersetzt (npm run build Exit 0), danach voll
zurueckgenommen (Arbeitsbaum sauber, Aktions-Verdrahtung 1 -> 2 -> 1).
11 neue Proben inkl. Kontrollfluss-Nachbildung und Flip (ohne die Wache 6 rot).
Volle Reihe 4709/4717 — die 8 roten sind vorbestehend und beidseitig belegt.

⚠ first-write-wins: wirkt NUR auf kuenftige Stuempfe, die 146 Bestands-
Fundstellen bleiben unveraendert. Der Melder zaehlt einen verweigernden Rumpf
weiter — gemessen und richtig: er misst Funktionslosigkeit, nicht Stille.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New briefings

Quelle "Entwicklung" am bestehenden Briefing-Motor

CEO-Wunsch 2026-08-22: taegliches Entwicklungs-Briefing, konfigurierbar wie der
Podcast — "andocken und erweitern, nicht neu bauen".

Es ist eine QUELLE, kein zweiter Motor: derselbe Laeufer, dieselbe Verdichtung,
dasselbe Gedaechtnis, dasselbe Konsistenz-Tor, dieselbe Audio-Kette, dieselbe
Mail, derselbe Zeitplan (CHECK_TENANT_BRIEFINGS, werktags 07:00). Ein reines
Entwicklungs-Briefing ist ein Automations-Objekt mit sources: ["development"] —
kein neuer Zeitplan, keine zweite Pflegestelle, kein Codegen.

- developmentSource.ts REIN (kein ay, kein Mongoose, kein Netz): Umfang-Stufen,
Filter, Rangfolge, Entdopplung, Rohstoff-Saeuberung, Endkunden-Regeln.
- tenantBriefing.ts Quellen-Weiche in Aufforderung, Hoerstueck, Betreff,
Kopf, Fuss und Gedaechtnis-Strang.
- tenantBriefings.ts loadDevelopment() + Verdrahtung; nicht aktive Quellen
werden gar nicht erst abgefragt.

Nicht-Brechen an SECHS Literalen festgenagelt (Betreff, Prompt-Kopf, Fuss
"Internes Briefing", Audio-Anfang, Strang "tenant-briefing:intern", Vorgabe-
Quellen). Beide LIVE-Objekte (Xamine, tolinax) tragen kein sources-Feld und
verhalten sich unveraendert. 439/439 Proben gruen.

DREI Praemissen des Auftrags gemessen widerlegt:
- Die Modul-Achse ist LEER: _module 30/5.665 (0,5 %), _component 2 (0,04 %),
_repository zu 93 % EIN Wert. Gefiltert wird ueber commit.repo (100 %) und
scope (98 %). Ein Filter auf _module haette lautlos alles weggeschnitten.
- Releases haengt (juengster publishedAt 17.08., fuenf Tage alt) — Zusatz-Achse,
nie Traeger. Zeit-Achse ist commit.date, nicht createdAt (der Nachfuell-Laeufer
schrieb Jahrgaenge 2023/2024 an einem Tag).
- Tasks.completedAt existiert NICHT (0 von 1.897) — es heisst doneAt (878).
Erste Messung war eine falsche Null, gefangen von der Positiv-Kontrolle.

Zwei Fehler, die erst die Verdrahtung sichtbar machte:
- Das Konsistenz-Tor bekam den Entwicklungs-Stoff nicht und haette JEDEN
Entwicklungs-Punkt als "erfunden" gemeldet: Urteil rot, Korrektur wirkungslos,
Versand nie.
- Der Probelauf gegen echten Stoff sagte "keine Neuigkeiten" UND "Grundlage
waren 25 Aenderungen" in derselben Folge — Stoff ohne Verdichtung wird jetzt
als Ausfall benannt statt als Nachrichtenlage.

Probelauf gegen 31 echte Eintraege vom 21.08.: 25 ausgewaehlt, 13 Repositorien,
Aufforderung 19.889 Zeichen, Mail 9.917 Zeichen, sechs Riegel gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XVq1MjzHpUt7bUtRzJTny
New page-editor

Pflege-Flaeche fuer die Entitaets-Bloecke — Auswahl, Automatik, Filter

Der Renderer bedient elf Entitaets-Blocktypen; der Editor bot die Felder fuer
drei an. Die uebrigen waren ueber die Schnittstelle voll bedienbar und ueber die
Oberflaeche gar nicht.

🔴 Die elf zerfallen in DREI Klassen, nicht in eine — der Brief nahm eine an:
• Im Katalog (references/partners/portfolio/jobs): Felder ergaenzt. `portfolio`
hatte KEINEN Auto-Schalter, `jobs` trug null Entitaets-Schluessel, obwohl der
Eintrag seit dem 2026-08-21 im Katalog steht und damit versorgt aussah.
• Mit Sonder-Maske (gallery/faq/logos/testimonial/pricing): ein Katalog-Eintrag
allein waere hier TOT — `block-renderer` hat fuer sie eigene @case-Zweige, der
Katalog-Editor laeuft nur im @default. Sie bekommen den Katalog-Editor jetzt
ZUSAETZLICH unter die Sonder-Maske; der Inline-Weg bleibt unangetastet.
• Im Editor gar nicht vorhanden (awards/quotes): weder Kachel noch Maske — sie
waren nicht bloss unpflegbar, sie waren nicht EINFUEGBAR.

Neu: `entity-picker-field` — kuratierte Auswahl MIT Reihenfolge. Ein
`mat-select multiple` traegt das strukturell nicht (es liefert Options- statt
Wahl-Reihenfolge), und bei der Galerie IST die Reihenfolge die Kuration; der
Renderer haelt sie ausdruecklich ein. Verschieben per Ziehen, wie es dieser
Ordner schon tut (team/stats/navigation).

Gemessen statt angenommen: die Entitaeten liegen NICHT alle im cms-Modul
(References/Partners/CustomerLogos/Ratings/Awards/Portfolios → marketing,
Faqs/Quotes → hub, HRJobs → hr, Images → cms, PricingTables → config). `hub`
und `hr` kannte der Host-Aufloeser nicht — ein falscher Rechner liefert eine
LEERE Auswahl, ohne dass etwas fehlschlaegt.

Der Kriterien-Riegel ist jetzt in der Oberflaeche sichtbar: `_imagesAuto` und
`_quotesAuto` ziehen OHNE Kriterium NICHTS (Absicht — 1.303 Bilder bzw. 48.428
importierte Fremdzitate bei je einem Mandanten). Am Renderer gemessen sind das
die einzigen zwei mit Riegel; `partners` etwa zieht auch ohne, dort waere die
Warnung falsch.

Verify: Bau gruen, Typpruefung gruen, 50/50 Proben. Flip-Proben BEIDSEITIG —
verdrehter Feldname, entfernter Riegel-Hinweis und falscher Rechner werden je
rot gemeldet (4 Proben), ebenso eine zerstoerte Wahl-Reihenfolge; danach
zurueckgerollt und wieder gruen. Die Galerie-Sonder-Maske ist unveraendert.

⚠ Der Ende-zu-Ende-Beleg am laufenden Editor (Feld setzen → speichern →
ausliefern) steht aus — siehe Handoff.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yTiSkb8SH9siwzkjmMDHT
New rag

deterministische Bestands-Korrektur der Sprach-Beschriftung von collection-Abschnitten

Der Schreibweg liest die Sprache seit 1a32d05 ab statt sie zu raten — es kommt
also nichts Falsches mehr dazu. Dieser Lauf raeumt den Altbestand: er setzt
`metadata.language` aus dem Ursprungs-Datensatz neu und UEBERSCHREIBT dabei.
`backfillChunkLanguage` fragt `$exists:false` und traegt nur Fehlendes nach —
ein solcher Lauf liesse genau den Schaden stehen (84,5 % Widerspruch zum
`Locale:` des Datensatzes).

Ausloesen: rag-backfill / "relabel-collection-language"
{ apply?, batchSize?, maxBatches?, startAfterId?, sourceIds?, maxUnsetRatio? }
Ohne `apply` ein reiner Trockenlauf.

Gemessen (Produktion, rein lesend, 2026-08-22):
- Text-Weg gegen Dokument-Weg entschieden, nicht geraten: 3 Fenster, 2 Quellen,
6.000 Abschnitte — 5.977/5.977 Uebereinstimmung, 0 Widersprueche; 23 ohne
`Locale:` im Text (geteilte Datensaetze), die das Dokument trotzdem aufloest.
Also: Dokument ist die Quelle, Text nur Rueckfall.
- Auf den zwei Riesenquellen (96,6 % des Bestandes) ueber 60.000 Abschnitte:
0 unset, 0 skip, 93,1 % korrigiert oder gefuellt.

Zwei Befunde, die im Auftrag nicht standen:
- `metadata.documentId` ist eine ZEICHENKETTE, `_sourceDocument` eine ObjectId.
Der im Auftrag genannte Mengen-Lookup ueber `documentId` liefert typ-bedingt
null Treffer OHNE Fehler (0 von 2.000 gegen 2.000 von 2.000) — der Lauf haette
daraus "keine Sprache" gelesen und auf 14,6 Mio Dokumenten geloescht.
- Die Entfern-Regel gilt nicht ueberall: die Loesch-Menge sind die prosa-nahen
Mandanten-Modelle (WikiPages Median 2.204 Zeichen, Contents 2.989), wo franc
die richtige Mechanik ist und kein `Locale:` gerendert wird. Deshalb im Betrieb
ueber `sourceIds` beschraenken; die Loesch-Bremse (maxUnsetRatio, Vorgabe 5 %)
bricht einen unbeschraenkten Lauf ab, BEVOR er schreibt.

Entscheidung und Schleife liegen boot-frei im Kern, damit Trockenlauf und
Ernstfall dieselbe Schleife fahren. 69 Proben, in `npm test` aufgenommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix rag

internal-Riegel im laufenden Abgleich — interne Seiten wurden endkunden-sichtbar

`incrementalSync` schrieb `consumerVisible: source.consumerVisible || false`,
waehrend sein Zwilling `structuredDataChunker.ts:125` seit dem 2026-05-26
(`758d69e`, "wiki-internal leak fix") den Riegel `&& doc.internal !== true`
traegt. Der Abgleich ist der LAUFENDE Schreiber (alle 30/60/120 min je Quelle):
eine interne Wiki-Seite, die nach der Erst-Indizierung bearbeitet wurde, lief
durch den Weg ohne Riegel und wurde dabei endkunden-sichtbar.

Gemessen (Produktion, rein lesend, 2026-08-22): 18 von 18 Wiki-Seiten mit
`internal: true` haben endkunden-sichtbare Abschnitte — 46 aktive Abschnitte
ueber zwei Mandanten, kein einziger korrekt verborgen. Alle 18 Seiten sind
NACH der Haertung des Zwillings entstanden (aelteste 2026-07-22), die Ursache
ist also dieser Weg und nicht Alt-Bestand. Direkter Beleg: Seite
6a82e86536b34963d60a2b78 traegt `internal: true` seit 11:53:54Z, ihr Abschnitt
wurde am 2026-08-18T12:30:04Z neu geschrieben — sichtbar.

Zweiter Zwilling geprueft: die Objekt-Literale sind sonst feldgleich. Die
Abweichung bei `_customerID` ist begruendet (globale Quellen erben den Stempel
des Rebuilds, incrementalSync.ts:51-70), also kein zweiter Befund.

⚠ Der Bestand heilt hierdurch NICHT: beide Zwillinge ueberspringen Abschnitte
mit unveraendertem `textHash`, und der Schluessel enthaelt `consumerVisible`
nicht — eine reine Flag-Aenderung loest also weder hier noch im Rebuild ein
Neuschreiben aus. Die 46 brauchen einen eigenen Korrektur-Lauf; Abbild liegt
unter .claude/reports/rueckroll-abbilder/.

Verify: Flip-Probe gegen den gebauten Ausdruck beidseitig (internal:true unter
sichtbarer Quelle true→false), zwei Negativ-Kontrollen unveraendert (ohne
`internal` bleibt sichtbar; Quelle consumerVisible:false bleibt unsichtbar),
6/6 gruen; Ausdruck jetzt identisch mit dem Zwilling; Regression 29/29.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015yTiSkb8SH9siwzkjmMDHT
New pages

Galerie-Block speist aus der Mediathek Images (W9 Glied 2)

CEO-Entscheid 2026-08-22 Weg (a): der `gallery`-Block bekommt eine Entitaets-Quelle
aus dem LEBENDEN Bild-Bestand statt aus der Leer-Entitaet `Collections` (0 Dokumente
ueber alle 90 Mandanten, 0 Verbraucher, kein W0-Kanon). Kein Fundament-Anteil, kein
neuer Blocktyp, die Blocktyp-Zahl bleibt unveraendert.

Zwei Wege wie bei references/portfolio/quotes:
- ausdrueckliche Auswahl `content._images[]`, Reihenfolge der Redaktion
- Auto-Zug `content._imagesAuto` mit Kriterium `_imageTags` / `_imageCategory`

IImage traegt KEINEN W0-Kanon (kein published/_websites/sortOrder, am Vertrag UND am
Schema gemessen, Positiv-Kontrolle IReference.published). Daraus folgen die zwei
quotes-Vertraege: die ausdrueckliche Auswahl filtert nicht, der Auto-Zug verlangt ein
Kriterium und fragt ohne eines die Datenbank gar nicht erst. Der Grund ist hier
schaerfer als bei den Zitaten: images ist die MEDIATHEK - bei EINEM Mandanten 1.303
Bilder; ein kriterienloser Zug haette die halbe Mediathek auf eine oeffentliche
Website gestellt, ohne dass etwas fehlschlaegt.

Gemessene Achsen (Prod, 3.384 Bilder): tags 459 · category 0 · _contentCategoryID 1.
tags ist die einzige lebende Gruppierungs-Achse; category bleibt als Filter angeboten
(das Feld existiert), _contentCategoryID bewusst nicht.

Alternativtexte nach der bindenden S8-Semantik (altTranslations[locale] vor alt),
inkl. der Mongoose-MAP-Form: auf einem hydrierten Dokument liefert
doc.altTranslations['en'] sonst still undefined und der Besucher laese die
Vorgabe-Sprache, obwohl die Uebersetzung vorliegt (357 Bilder tragen sie).

Nicht-Brechen: ein Block ohne Entitaets-Bezug laeuft durch den unveraenderten
Inline-Zweig - byte-identisch, als Literal festgenagelt, mit Umkehrprobe.

28 neue Proben, alle vier Flip-Proben beissen (Kriterium-Riegel, Map-Form,
Inline-Ausgabe, Reihenfolge). Voller Lauf des Repositoriums: 101 Suiten / 2391 Proben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014XVq1MjzHpUt7bUtRzJTny
Fix coord

foundation-freshness hint no longer suggests --pin-exact for the 6 transitional shared libs

The freshness hint at coord claim treats any non-exact pin as "kein exakter Pin"
and recommends --pin-exact for it — correct for normal consumers, but wrong for
the six shared libraries (ayoune-theming, ayoune-device-shell, comm-sidebar,
notifications, permission-editor, communicator) that CLAUDE.md's transitional
override explicitly keeps on ranges until the deployment-plan bump_dependency
step carries them along. Following the hint there creates a second Foundation
resolution at their consumers (measured 2026-08-02, 11 affected pairs).

TRANSITIONAL_RANGE_LIBS excludes exactly those six from the hint; everything
else is unaffected (flip-probe-verified, gegenprobe for a 7th unlisted lib).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New modules

Assistent "Bilde deine Software ab" — Struktur lesen, vorschlagen, bestaetigt uebernehmen

W5 des PM-Selbstabbildungs-EPICs, Server-Haelfte.

Der fehlende Baustein war gemessen: die Plattform liest Repositorien, Zweige und
seit dem 2026-08-13 Commits — aber es gab plattformweit KEINEN Leser der
Verzeichnis-STRUKTUR. Ohne ihn hat ein Onboarding-Assistent nichts, woraus er
vorschlagen koennte.

- repoStructure.ts Baum-Leser fuer bitbucket/github/gitlab auf EINE flache
Form; Klient injizierbar (Proben ohne Netz); Zugang
ausschliesslich aus dem Credential-Vault des Mandanten.
- proposeStructure.compute.ts REIN: Baum -> Vorschlag. Schliesst nur auf BELEGE
(Manifest-Datei), nie auf Ordnernamen — sonst haette das
Verify-Gate "echtes fremdes Repositorium" strukturell nie
bestanden. Kein Sprachmodell: die Eingabe ist Struktur.
- applyStructure.compute.ts REIN: Bestaetigung + Bestand -> Schreib-Plan.
Wiederholbar (zweiter Lauf legt nichts doppelt an),
ergaenzt nur und ueberschreibt nie.
- actions/ POST /modules/actions/scan-repository (lesend, schreibt
NICHTS) und /apply-proposal (schreibt nur Bestaetigtes).
Keine neuen Rechte: pm.modules bzw. pm.modules.edit.

Dogfooding am eigenen Bestand: Bauform korrekt erkannt, 37 Module / 183
Komponenten vorgeschlagen, 250 Punkte ausdruecklich OFFEN statt geraten.

Dabei einen echten Defekt gefunden und behoben: der Tiefen-Deckel verschluckte
`domains/finance` lautlos (Manifest eine Ebene tiefer) — weder vorgeschlagen noch
als offen gemeldet. `droppedByDepth` meldet die Grenze jetzt; Regression gepinnt.

38 neue Proben, volle Reihe 726/726 gruen.
Fix courses

Wiederholungs-Versuche deckeln und benennen

Am laufenden Dienst gefunden, nicht am Reissbrett: nach dem Abschluss
eines Durchgangs fiel der Stand bei der naechsten Antwort kommentarlos
von 2/2 auf 1/2 zurueck. Ursache ist ein NEUER Ergebnis-Datensatz —
richtig im Ergebnis (Exams.retries sieht Wiederholungen vor), aber
unangekuendigt und ohne Deckel.

Drei Maengel behoben:

1. `retries` wird geprueft. Die Lesart ist ausdruecklich festgeschrieben:
retries sind die WIEDERHOLUNGEN, der erste Durchgang zaehlt nicht dazu
(retries:2 = drei Durchgaenge). Der Vertrag sagt es nicht, und beide
Lesarten sind vertretbar - wer sie anders liest, deckelt um eins
daneben. Aufgebraucht -> 409 versuche-aufgebraucht.

2. Der NEUESTE Versuch bestimmt den Stand. Liste und Detail sortierten
nicht; bei mehreren Ergebnis-Datensaetzen war unbestimmt, welcher den
Stand liefert - die Uebersicht zeigte je nach Fund-Reihenfolge der
Datenbank mal den alten, mal den neuen.

3. Die Antwort meldet `neuerVersuch`, damit die Oberflaeche den
Ruecksprung erklaeren kann statt ihn geschehen zu lassen.

Dabei von der Testreihe eine Null-Falle gefangen: Number(null) ist 0,
nicht NaN - ein nicht gesetztes `retries` haette den Deckel still auf
EINEN Durchgang gesenkt statt auf den Vorgabewert zu fallen.

69 Tests, Flip-Probe auf den Deckel gefahren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New i18n

Kiosk-Sprachschluessel der Einarbeitung (kiosk-w5, Punkt 17)

31 Schluessel fuer die Einarbeitungs-Flaeche plus drei fuer die
vertieften Produktdaten (Merkmale, Varianten), je de+en — wie der
Terminal-Katalog es im Dateikopf verlangt.

Festgehalten und ausdruecklich NICHT nachgezogen: `terminal.nav.personal`
fehlt in diesem Seed (die Selbstbedienung wurde nur im gebuendelten
Terminal-Katalog gepflegt). Das ist ein fremder Auftrag; der Kommentar
haelt die Luecke fest, damit sie der naechste Leser nicht fuer Absicht
haelt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New learning

Einarbeitung am Arbeitsplatz + vertiefte Produktdaten (kiosk-w5)

Punkt 17 — Einarbeitung:
Neue Flaeche /learning + /learning/:id. Zugewiesene Kurse mit Stand,
Pflichtaufgaben mit Frist und Quittierung, Lektions-Ablauf mit Frage,
Rueckmeldung und Erklaerung.

Rollenneutral und deshalb OHNE roleGuard — die Einarbeitung ist kein
eigener Arbeitsplatz-Typ, sondern ein Bereich, den jede Rolle tragen
kann. Der Sidebar-Eintrag erscheint trotzdem nur, wenn wirklich etwas
zugewiesen ist (Tote-Flagge-Regel wie Service-Desk und Selbstbedienung).

Der Dienst schickt KEINEN Besitz-Filter mit — die hr-api leitet den
Besitzer aus dem Anmeldezeichen ab (/courses/own). Die Bewertung macht
der Server; diese Seite kennt die Loesung gar nicht.

`learning` in TERMINAL_TOP_ROUTES — ohne den Eintrag haelt der
Serial-Parser das erste Pfad-Segment fuer eine Geraete-Seriennummer und
der roleGuard wartet auf eine Rollen-Aufloesung, die nie kommt.

Punkt 16 — Produktdaten:
Die Detailseite hatte Stammdaten, Masse, Bild und Lagerstand bereits.
Ergaenzt sind die MERKMALE — 570 Produkte tragen gepflegte Attribute,
die Seite zeigte davon nichts — sowie die Varianten-Karte. Die
Lagerort-Zeile verlinkt jetzt auf /warehouse/storage/:id, statt den Ort
ein zweites Mal auszubreiten (verlinken statt nachbauen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New courses

eigene Einarbeitung am Arbeitsplatz (kiosk-w5, Punkt 17)

Zugewiesene Kurse, Lektionen, Fragen und Pflichtaufgaben mit Frist —
ausschliesslich die EIGENEN. Der Besitzer kommt aus dem Anmeldezeichen,
nie aus dem Aufruf: der Arbeitsplatz ist ein geteiltes Werkstatt-Geraet.

GET /courses/own
GET /courses/own/:id
POST /courses/:id/actions/answer-own
GET /mandatorytasks/own
POST /mandatorytasks/:id/actions/acknowledge-own

Kein eigener Fortschritts-Traeger: geschrieben wird `ExamResults` (Modul
hub, sichtbar unter hub.examresults). Wer am Kiosk eine Lektion
abschliesst, hat sie abgeschlossen — nicht „am Kiosk abgeschlossen".

Die Bewertung macht der Server. `answers[].correct` wird entfernt, bevor
eine Frage das Geraet erreicht; die Oberflaeche kennt die Loesung nicht.

Zwei Zuweisungs-Wege (Team + Pflichtaufgabe), weil der Bestand zwei
fuehrt — ohne beide waere eine leere Liste mehrdeutig. Ohne offenen Weg
entsteht KEIN Filter (sonst lieferte er alle Kurse des Mandanten).

Vier eigene Rechte ueber additionalRights, kaskadenfrei — bewusst nicht
`hr.courses`/`hr.mandatorytasks`: das sind die Rechte der
Personalabteilung auf ALLE Datensaetze.

Teams/Exams/ExamResults zu modelsToLoad (fallen durch den hr-Filter);
alle drei am installierten Paket geprueft, Umkehrprobe negativ.

60 Tests, drei Flip-Proben gefahren (Null-Weg-Riegel, Loesungs-Leck,
Mengen-Gleichheit) — alle beissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Perf gantt

Ladepfad folgt der Dimension, Zeitfenster statt pauschalem Deckel

Gemessen am groessten Projekt der Plattform (aYOUne, 4206 Aufgaben): der
Endpunkt braucht 6,2 s, davon 4,4 s allein fuers Laden — auch fuer
nur-ausfuehrung, das nur Sitzungen und Auftraege zeigt und dafuer 4206
Aufgaben und 5022 Stories VOLLSTAENDIG holte, um von ihnen nichts als die
Kennungen zu benutzen. Der Deckel LEVEL_LIMIT war nicht die Ursache.

- loadCascade nimmt Ebenen und Zeitfenster entgegen; ohne Angabe unveraendert
- neededSources und dateWindowFilter als reine, gepruefte Helfer in ganttTypes
- Ebenen, die nur als Anker gebraucht werden, kommen als _id statt 15 Feldern
- das Fenster greift NUR an den Blatt-Ebenen; Zeilen ohne Termin fallen NIE
heraus, und Sitzungen folgen ihren Ankern von selbst
- LEVEL_LIMIT bleibt letzte Sicherung

Die Invariante ist als Probe festgenagelt: fuer jede Dimension baut die
beschnittene Kaskade knotengleich zur vollen, mit Negativ-Kontrolle.

Befund nebenbei: 5022 von 5022 Stories haben KEINEN Termin und reissen den
4000er-Deckel schon heute — die Kappung war also laengst still wirksam. Eine
reine Zeitraum-Filterung haette sie alle entfernt und 4190 der 4206 Aufgaben
ihre Eltern genommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New audit

Arbeitsliste — wo gearbeitet wird und das Modul fehlt

Der Melder repo-platform-drift kannte die Sitzungs-Achse nicht. Er meldete
221 Repositorien ohne Modul-Zuordnung, ohne zu sagen, in welchem davon
ueberhaupt gearbeitet wird. Ergaenzt wird genau diese Achse — kein zweiter
Ableiter: der Vorschlag stammt aus dem BESTEHENDEN deriveModule.

- Er schlaegt vor, er setzt nicht. Ein Mensch entscheidet je Zeile.
- 'Kein Vorschlag' ist ein Ergebnis mit Grund, kein leeres Feld: der
Ableiter greift nur unter domains/, ausserhalb ist es eine Entscheidung.
- Die zweite Haelfte traegt den wichtigsten Fall: eine Plattform-Zeile ohne
Gegenstueck im Inventar. Die Grundgesamtheit 'verbunden' schliesst das
Wurzel-Repositorium aus, an dem 5.318 von 5.331 Sitzungen haengen.
- Eine nicht gemessene Sitzungs-Achse meldet das ausdruecklich, statt eine
leere Liste zu zeigen (falsche Null).

23 neue Proben, fuenf Flip-Proben beissen (8/1/2/4/1).
New kiosk-dashboard

Herkunft verlinkt, Knopf-Kontrast, Diktat, Verlaufslinie

Vier CEO-Befunde der Runde vom 2026-08-22 (Bildbelege 5a, 8b-8d).

5a -- Herkunft anklickbar. GEMESSEN, statt die Kette zu erfinden: nur
`model`/`modelName` benennt eine Entitaet; domain/group/nameSpace/appName sind
Ordnungs-Begriffe des Erzeugers und zeigen auf nichts Oeffenbares. Und es bleibt
beim Verweis auf die ENTITAET: der Zustand traegt keine Dokument-Kennung --
`entityId` sieht wie eine aus, ist aber ein Slug (an 307 k8deployments-Zustaenden
in der Produktion gemessen). Als AUSGABE, nicht als Router-Aufruf: die Bibliothek
bleibt router- und netzfrei.

🔴 Die verlangte Kette Cluster -> Projekt -> Modul -> Komponente -> Feature ist
NICHT gebaut, weil die Kanten fehlen: IK8Deployment traegt _project, _cluster und
_repository, aber weder _module noch _component noch _feature (gegen interfaces
2026.447.1 gemessen, Positiv-Kontrolle greift). Befund in der Uebergabe.

8b -- Knopf-Kontrast. Der Befund war keine schlechte Farbwahl, sondern eine
Bauform: der Grund war ein 22-Prozent-Schleier UEBER dem Untergrund, die Schrift
ein fester heller Ton. Ein durchscheinender Grund kann seinen Kontrast per Bauart
nicht kennen. Jetzt deckend in Marken-Farben mit weisser Schrift; Kontrast
gerechnet und mit Flip- UND Umkehrprobe belegt (alt auf hell faellt durch, alt
auf dunkel bestand -- daher fiel es nie auf).

8c -- Spracheingabe. Die bestehende Diktier-Direktive aus @tolinax/ayoune-speech
angeschlossen, nichts gebaut: Umschalten statt Halten, Text erscheint waehrend
des Sprechens. Optionale peer-Abhaengigkeit -- von vier Verbrauchern nutzen nur
terminal und control-panel das Aufgaben-Detail, und beide fuehren das Paket schon.

8d -- Verlaufslinie. Die Auftrags-Annahme „die Kurve gibt es schon, im
Zustands-Detail" traegt nicht: jene laedt sich selbst ueber eine stateId aus
statelogs, und eine Aufgabe hat keine. Wiederverwendet ist stattdessen der
Sparkline-Helfer des Fehler-Centers DIESER Bibliothek, samt seinem
Erzeuger-Muster (der Wirt liefert dort seit jeher stats.sparkline).

5 neue Proben zu 5a + 6 zum Kontrast, je mit Positiv-Kontrolle und Umkehrprobe,
flip-geprueft. Volle Reihe 299/299.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New workspace-create

der Tagespreis steht in der Groessen-Zeile

Die Auswahl-Zeile nennt jetzt den Kundenpreis je Tag, wo der Dienst einen
liefert — zwischen Rechner-Groesse und Abschalt-Frist. Betrag und Waehrung
kommen aus `GET /workspaces/templates` (`dailyPrice`), formatiert wird mit
`Intl.NumberFormat` nach der Sprache der Oberflaeche (de `1,30 € pro Tag`,
en `€1.30 per day`) — NIE als roher Fliesskomma-Wert.

- Ohne Betrag bleibt die Zeile unveraendert: kein Platzhalter, kein Strich,
keine 0,00 € (ein Nullbetrag saehe aus wie kostenlos).
- Die Notiz unter der Auswahl folgt dem GEMELDETEN Preis-Zustand des Dienstes
(`pricing: live|unavailable`) statt eines eingefrorenen Satzes; sie behauptete
bisher, es gebe keine Preisangabe.
- Das Einheiten-Wort ist als Zwei-Sprachen-Karte im Baustein — benannte
Abweichung: diese Flaeche haelt keine i18n-Datei, ein Schluessel ohne
Server-Seed rendert roh. Begruendung steht an der Karte.

Proben: 20 gruen (Karma, ChromeHeadless), darunter die KETTE Dienst-Antwort →
`enumValues`-Label, die Negativ-Kontrolle ohne Betrag und die Umkehrprobe
`preiseLive` folgt dem Feld, nicht den Zeilen.

⚠ Karma meldet Exit 0 AUCH bei roten Faellen — der Beleg ist die TOTAL-Zeile.
🔴 `Intl` setzt ein GESCHUETZTES Leerzeichen (U+00A0) vor das Waehrungszeichen;
eine Erwartung mit gewoehnlichem Leerzeichen faellt mit einer Meldung, in der
beide Zeichenketten identisch AUSSEHEN (hier 3-mal passiert).
New workspaces

Tagespreis der Groessen-Leiter — live aus der Anbieter-Preisliste

Die Route meldete das Literal `pricing: 'unavailable'`. Sie leitet den Kunden-
Tagespreis jetzt LIVE aus `GET /v1/pricing` ab — derselben Ressource, derselben
Formel (min(Std x 24, Monat / 30)) und demselben Aufschlag (x2, CEO 2026-08-10),
aus denen der Abrechnungs-Lauf seine Betraege bildet. Wizard und Rechnung koennen
damit per Bauart nicht auseinanderlaufen (CEO-Entscheid 2026-08-22, Wege A1+B1).

- lib/templatePricing.ts — reine Ableitung, Zwilling von hetznerBilling.ts
(gespiegelt statt importiert, mit Begruendung); Waehrung wird GELESEN, nicht
angenommen; location aus der Vorlage statt hartkodiert.
- lib/hetznerPricingSource.ts — Zugang aus dem Vault (CEO 2026-08-15),
ausdruecklich OHNE Umgebungs-Rueckfall: ein fremdes Konto zeigte einen falschen
Listenpreis. Vorhalt 15 min. Wirft per Bauart nie.
- Ein Fehlschlag reisst die Groessen-Leiter NICHT mit — dann bleibt es bei
pricing: 'unavailable', der ehrlichen Richtung.
- costAggregator bleibt bewusst tot: befuellen hiesse eine ZWEITE, eingefrorene
Preis-Quelle neben der live entstehenden Rechnung.

Proben: 23 neu (Formel, Waehrung, Standort, Vault-Feld, die drei Betraege,
Negativ-Kontrolle ohne Preisliste, Groesse ohne Zeile). Flip-Proben beidseitig:
Aufschlag 2->1 und min->max machen je 3 Faelle rot, rueckstandsfrei zurueck.
New coding

Modul-Zuordnung und Wissens-Kontext im Job-Kontext

Ein Coding-Job kannte sein Repositorium, aber nicht das Modul dahinter -
und konnte einen Doku-Vorschlag deshalb nicht verorten. performDispatch
ist der EINE geteilte Ausliefer-Kern hinter allen vier Eingaengen; der
Block haengt dort, nicht in einem zweiten Weg.

- Die Kette wird GELESEN (Repository._module/._component), nicht aus dem
Pfad abgeleitet - der Ableiter bleibt im Melder repo-platform-drift.
- Der Wissens-Kontext kommt aus GET /{modules,components}/:id/knowledge
(pm-api), kein zweiter Aufloeser, kein zweites Retrieval.
- Die Luecke wird BENANNT statt weggefiltert; gemessen tragen 5.318 von
5.331 Sitzungen ein Repositorium ohne Zuordnung.
- Mandanten-Grenze: _customerID-gebundener Lesezugriff, Token des
ausloesenden Benutzers, bewusst ohne Zwischenspeicher.
- IRepository fuehrt KEIN name-Feld (gemessen 0 von 200) - Anzeige ueber
friendlyName > repoFullName > repoSlug, per Umkehrprobe festgenagelt.
Fix notifications

Akustik-Fenster -- until-Vertrag, Popover-Ruecksetzer, Marken-Farben

Drei CEO-Befunde der Runde vom 2026-08-22, zwei davon aus EINER Ursache.

1) Stumm-Knoepfe antworteten „until fehlt" und haben nie eine Stummschaltung
angelegt. GEMESSEN sass der Fehler auf der CLIENT-Seite: der bibliothekseigene
Anbieter schickte eine DAUER (`minutes`), der Vermittler verlangt einen
ZEITPUNKT (`until`, buildSnoozeDoc in @tolinax/ayoune-core). Die einzige andere
Bindung (clients/desktop-client) rechnet seit jeher korrekt um -- ein „Fix" am
Vermittler haette sie mitgerissen und eine zweite Drift erzeugt.

2) Weisser Grund im Dunkeln UND Fremd-Rahmen im Hellen sind EIN Fehler: dem
Akustik-Fenster (seit 2026-08-21) fehlte sein [popover]-Ruecksetz-Block, also
schlug das Browser-Blatt durch (`background-color: Canvas`, `border: solid`).
Der Block existierte fuer die fuenf aelteren Top-Layer-Flaechen.

3) Quittieren war rot und teilte sich die Zeile mit den drei Stumm-Knoepfen.
Jetzt eigene Zeile, volle Breite, 60px hoch, Farbe aus --brand-primary.
Der rote Rahmen der Karte BLEIBT -- er zeichnet den Alarm, nicht die Handlung.

Karte, Innentexte und Stumm-Knoepfe ziehen gegen --brand-background/--brand-text
(bisherige Werte als Rueckfall), damit ein heller Mandanten-Anstrich traegt.

Proben: 8 zum Rumpf-Vertrag + 10 zum Top-Layer (als REGEL ueber alle Flaechen aus
dem Bauplan, nicht als Einzelfall), je mit Positiv-Kontrolle und Umkehrprobe,
beide flip-geprueft. Volle Reihe gruen.

Nicht enthalten: der vorgelesene Satz (CEO „Alerts created by System"). Er
entsteht in domains/config/worker-notifications/src/lib/alertTts.ts:101 aus
subject+body -- ausserhalb der beanspruchten Pfade dieses Auftrags.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New kiosk-keyboard

fokussiertes Feld wird freigescrollt (CEO-Bildbeleg 8a)

Die Auftrags-Praemisse war widerlegt: die Bibliothek meldet ihre Hoehe SEIT JEHER
(CSS-Variable --gkb-h + .gkb-open, `hoehenEffekt`), und terminal wie control-panel
verbrauchen sie. Die Ausgangs-Messung suchte nach `keyboardHeight|scrollIntoView|
offset` -- englische Namen fuer eine deutsch benannte Mechanik -- und lieferte eine
falsche Null.

Was wirklich fehlte, ist die zweite Haelfte: freischeuchen, wenn niemand Platz
reserviert. Die Variable hilft nur einer Flaeche, die sie liest; ein Feld, das eine
BIBLIOTHEK rendert (Bildbeleg 8a: Kommentarfeld im Aufgaben-Detail von
ayoune-kiosk-dashboard), liegt ausserhalb jedes Wirt-Layouts.

- keyboard-inset.util.ts: reine, browserlose Regeln -- scrollDeltaForField (nie
weiter als Platz ueber dem Feld), isScrollContainer, reportedKeyboardHeight
- Komponente scrollt den naechsten scrollbaren Vorfahr (Dialog!), sonst das Fenster
- Neues oeffentliches Signal `hoehe` neben der CSS-Variablen
- 13 neue Proben (62 gesamt) inkl. Positiv-Kontrolle, Umkehr- und Flip-Probe

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix portal-invite

Token nur noch gehasht in Consumers.verificationCode

Der von send-portal-invite geminteten Einladungs-Token wurde im KLARTEXT in
die Consumer-Zeile geschrieben. Wer die Datenbank lesen kann, konnte damit
jeden offenen Einladungs-Link uebernehmen (Passwort setzen = Konto-
Uebernahme). Dieselbe Fehlerklasse wie storeevent-persistiert-rohen-reset-
token (behoben 2026-08-20).

Neu: hashPortalInviteToken = sha256(token) hex; nur der Digest geht in die
Zeile. Der Roh-Token verlaesst diesen Prozess ausschliesslich in
setPasswordUrl, also in der Mail.

🔴 AUSBRINGUNGS-REIHENFOLGE, nicht optional: clients/consumer-app MUSS
ZUERST live sein. Sein Leser ist beidseitig (Hash zuerst, Roh-Wert als
Uebergangs-Rueckfall). Geht dieses Repositorium zuerst raus, schreibt es
Hashes, waehrend der alte Leser nur den Klartext kennt — jede neue
Einladung waere unaufloesbar, ohne Fehlermeldung.

⚠ ZWILLING: dieselbe Funktion liegt in
clients/consumer-app/src/routes/data-fetcher.ts, genau wie
mintPortalInviteToken schon vorher. Laufen sie auseinander, findet der
Nachschlag einfach nichts — kein Fehler, kein Protokolleintrag. Beide
Proben-Reihen nageln denselben fest verdrahteten Digest fest; das ist die
einzige Stelle, an der eine Abweichung auffaellt.

Die zwei bestehenden Proben, die verificationCode auf /^pi-/ pruefen, sind
auf den neuen Vertrag umgestellt — sie hielten das alte, unsichere Verhalten
fest und wurden von dieser Aenderung erwartungsgemaess rot (der Diff zeigte
den Digest an der Stelle des pi-Tokens). Fuenf neue Proben dazu.
22/22 gruen, tsc --noEmit Exit 0.

⚠ Scoped Commit: der Arbeitsbaum trug zugleich 227 fremde Dateien aus einem
laufenden Codegen-Lauf. Sie sind NICHT Teil dieses Commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix portal

Einladungs-Token nur noch gehasht persistieren (Leser beidseitig)

Der einmal verwendbare Portal-Einladungs-/Reset-Token lag im KLARTEXT in
Consumers.verificationCode. Wer die Datenbank lesen kann, konnte damit jeden
offenen Einladungs-Link uebernehmen — Passwort setzen ist Konto-Uebernahme.
Dieselbe Fehlerklasse wie storeevent-persistiert-rohen-reset-token (behoben
2026-08-20); die Nutzer-Ebene (platform/auth password.ts) hasht laengst.

Neu: hashPortalInviteToken = sha256(token) hex. In der Zeile liegt nur noch
der Digest; der Roh-Token verlaesst den Prozess ausschliesslich in der
Mail-Adresse.

Reihenfolge ist tragend — der LESER kommt zuerst und ist beidseitig:
findConsumerByPortalInviteToken fragt ZUERST den Hash und faellt danach auf
den Roh-Wert zurueck, damit vor dem Umbau geminteten Einladungen einloesbar
bleiben. Der Rueckfall traegt ein Ablaufdatum im Kommentar (ENTFERNEN AB
2026-08-29 = Umbau-Tag + PORTAL_INVITE_TTL_MS); danach kann kein Roh-Token
mehr gueltig sein, weil inspectPortalInviteToken vorher abweist.

Bewusst OHNE pi-Praefix am Hash: ein reiner Hex-Digest kann per Bauart nicht
mit pi- beginnen, der Praefix bleibt damit die Trennung zum Telegram-
Anmelde-Wert in DERSELBEN Spalte. Gemessen 2026-08-22: 158 Zeilen mit
verificationCode, davon 2 mit pi-; die uebrigen 156 gehoeren Telegram und
werden nicht angefasst. Von den 2 war genau EINE noch innerhalb der
Lebensdauer (gesetzt 07:30Z), die andere 24 Tage alt und damit laengst
abgewiesen.

Kein Salz: der Token traegt 24 zufaellige Bytes, ein Woerterbuch-Angriff ist
gegenstandslos — und ein Salz machte den Gleichheits-Nachschlag unmoeglich,
den dieser Weg braucht.

portal-auth.ts ist NICHT angefasst: es ruft nur mint + set + find und
bekommt den Roh-Token weiterhin fuer die Mail-Adresse. Die Schichtung
stimmt damit von selbst.

8 neue Proben mit AY-Attrappe, die den WEG pruefen (welcher Wert im Filter
stand, in welcher Reihenfolge gefragt wurde) — nicht nur das Ergebnis.
Zwei Flip-Proben gefahren: Schreiber zurueck auf Klartext => 1 rot,
Lookup-Reihenfolge umgedreht => 2 rot, zurueck => 15/15. Volle Reihe 1574
gruen. Der fest verdrahtete Digest in der Probe ist derselbe wie in
crm-api — laufen die Zwillinge auseinander, wird eine Einladung
unaufloesbar, OHNE dass ein Fehler entsteht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix user-menu

Suchfeld im Mandanten-Wechsel bekommt den Fokus (CEO-Bildbeleg 4)

Der Dialog wurde mit autoFocus:false geoeffnet (brand-switcher.component.ts) --
deshalb blieb das Suchfeld am Kiosk leer, bis man hineintippte. Gemessen lag die
Ursache an der OEFFNUNGSSTELLE, nicht wie vermutet in der Dialog-Vorlage.

- brand-switcher: autoFocus 'first-tabbable' (beachtet [cdkFocusInitial])
- customer-switcher-dialog: Suchfeld traegt [cdkFocusInitial]
- Der Fortschritts-Dialog behaelt autoFocus:false (Spinner unter disableClose)
- Neue Testreihe (4 Proben) inkl. Umkehrprobe + Positiv-Kontrolle; flip-geprueft

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix credentials

Alt-Bestands-Messung las die falsche Antwort-Form — und schwieg dabei

Der Vorab-Riegel von vault-change-password griff NICHT. Gefunden hat es erst
die Live-Probe: der Wechsel ging trotz eines vorhandenen Alt-Zugangs raus und
wurde vom SERVER mit 409 gestoppt — die Zusage "stoppt, bevor gesendet wird"
war damit gebrochen.

Ursache, am laufenden Dienst gemessen: die Antwort ist
{ payload: { data: [...] }, meta } — `payload` ist ein OBJEKT, kein Feld. Der
Code las `res.payload ?? res.data` als Liste, bekam ein Objekt, und
`Array.isArray(...) ? ... : []` machte daraus STILL eine leere Liste. Eine
leere Liste heisst hier "keine Alt-Bestaende", also: senden.

Zwei Fehler, nicht einer — und der zweite ist der teurere:
(a) falscher Pfad in die Antwort;
(b) eine unerwartete Form wurde als "nichts vorhanden" gelesen statt als
"nicht feststellbar". Genau die Fehlerklasse, gegen die dieser Riegel
steht.

Behoben: der gemessene Pfad zuerst, danach zwei ausdrueckliche Rueckfaelle —
und FAIL-CLOSED. Ist das Ergebnis keine Liste, bricht der Befehl ab, statt
weiterzumachen. Ebenso, wenn die Liste die Obergrenze (500) erreicht und
damit abgeschnitten sein koennte; `meta` traegt keine Gesamtzahl, mit der
sich das anders pruefen liesse.

Belegt: dieselbe Live-Probe stoppt jetzt VOR dem Senden, nennt den Zugang
namentlich (69fe8996e1772fc374ef1c86 "Rustdesk Public Key") und endet mit
Exit 2 statt mit dem Server-409. Drei neue Proben halten die gemessene Form,
die unerwartete Form und den Obergrenzen-Fall fest; die bestehenden Proben
sind auf die ECHTE Form umgestellt — sie hatten meine falsche Annahme
gespiegelt und deshalb gruen gemeldet.

Dazu: die Phrasen-Abfrage nimmt die Phrase nicht-interaktiv ueber die
STANDARDEINGABE (nie ueber ein Flag — das stuende in der Prozessliste), je
eine Zeile pro Abfrage. Der Leser wird je Befehlsaufruf erzeugt statt im
Modul gehalten; als Modul-Zustand teilten sich zwei Aufrufe im selben Prozess
einen bereits geleerten Vorrat.

23 Proben in dieser Datei; volle Reihe 1545 gruen / 3 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New mcp/rag

rag_knowledge_search — Coding-Agenten erreichen kuratiertes Wissen

Gemessen 2026-08-22: unter domains/coding gibt es NULL Aufrufer eines
Wissens-Retrievals. Coding-/Workspace-Agenten hatten Code-RAG, aber keinen Weg
zu einer kuratierten Bibliothek — ein Auftrag "baue nach den Prinzipien der
Ueberzeugungspsychologie" traf ein Modell ohne Quellen.

Das Werkzeug ruft ausschliesslich den fertigen, LLM-freien Endpunkt
POST /rag/retrieve auf. KEIN zweiter Retriever: Bibliotheks-Aufloeser und
Mandanten-Sichtbarkeit bleiben in ai-api.

Die Bibliothek ist Pflicht und ist die Mandanten-Grenze, kein Filter. Ohne sie
verweigert das Werkzeug den Aufruf, statt eine sinnlose Anfrage zu schicken —
sonst lernt der Agent, dass "ohne Bibliothek" eine gueltige Form waere. Eine
fremde Bibliothek antwortet 404 und wird als 404 durchgereicht; ein 403 wuerde
verraten, dass sie existiert.

Die Urheberrechts-Auflage steht in der Werkzeug-Beschreibung, woertlich aus dem
Systemprompt der 21 bereits ausgestatteten Agenten: anwenden statt zitieren,
harte Grenze 15 Woerter Originalwortlaut, Werk/Autor/Seite als Beleg erwuenscht.

E2E gegen Produktion belegt: librarySlug=verkaufspsychologie liefert 200 mit
Cialdini-Treffern (Influence, S. 224/157/187/223, Abschnitt SOCIAL PROOF), eine
erfundene Bibliothek liefert 404 und null Treffer.

Der E2E-Lauf hat dabei einen Fehler in der eigenen Beschreibung gefunden: das
Feld heisst `page`, nicht `pageNumber`, und `bibliographic` liegt unter `source`.
Ein Agent haette nach `pageNumber` gesucht und ohne Seite zitiert. Beschreibung
korrigiert und per Probe festgenagelt.

Proben 181/181; 7 neue mit zwei Umkehrkontrollen. Vier Flip-Proben belegen, dass
sie beissen: Endpunkt umgebogen, Bibliotheks-Zwang entfernt, Urheberrechts-Grenze
gestrichen, falscher Feldname behauptet — je genau eine Probe faellt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New alerts

Schweregrad je Quelle — Rueckfall-Kette und Entity-Dropout sind Warnungen

CEO-Befund 2026-08-22: "Rueckfall-Kette disarmed [oxy/...]" und "Entity dropout
[oxy/...]" loesten den akustischen Alarm samt Voll-Overlay aus, obwohl sie
Warnungen sind. Gemessen: 309 Dokumente, alle severity:'critical'.

Die Ausnahme sitzt im Klassifizierer, nicht am Erzeuger: das Erzeuger-Paket ist
eine Bibliothek mit 14 Crawler-Verbrauchern (15 Releases gegen 1), und Zeile 81
ist ohnehin der Ort, an dem der Schweregrad entschieden wird.

Die Eskalations-Leiter wird fuer eine herabgestufte Quelle NICHT durchgereicht,
und das ist tragend statt Kosmetik: der geteilte Schreiber hebt eine stehende
Meldung nach escalateAfterMs auf critical und stuft sie danach nie wieder herab
(core upsertAlert.ts:278-287). Gemessen — crawler-fallback-readiness schickt eine
30-Minuten-Leiter, waehrend seine Meldungen bis zu 215 Minuten offen stehen und
32 von ihnen mehr als einmal ticken. Ohne diese Haelfte waere der Fix nach einer
halben Stunde wieder aufgehoben.

Ausdruecklich KEINE globale Entschaerfung von levelToSeverity: andere Quellen
nutzen level:'alert' zu Recht (Anbieter-Ausfall). Ein ausdrueckliches
marker.severity behaelt Vorrang. Exakter Namensvergleich, kein Praefix.

Proben 157/157, davon 10 neue mit vier Umkehrkontrollen; drei Flip-Proben
belegen, dass die Kontrollen beissen (global entschaerft -> 16+17 rot, Leiter
durchgereicht -> 20 rot, Praefix statt exakt -> 23 rot).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix coord

reuse_check.verdict auf gueltigen Wert (fix ist keiner)

Erlaubt sind laut @tolinax/ayoune-workitem-quality genau: reuse | extend |
genuinely-new | verify. Der Validator hatte die drei Briefe deshalb als
reuse-check-missing gefuehrt.
New vault

OS-Schluesselbund-Adapter und Sitzungs-Zwischenspeicher

Zwei neue Helfer als Grundlage der Master-Phrasen-Befehle.

osKeychain.ts — store/lookup/clear ueber die Werkzeuge, die das System
ohnehin mitbringt: DPAPI (Windows), security (macOS), secret-tool/libsecret
(Linux). KEINE neue Abhaengigkeit; die Vorlage dazu liegt unter
.claude/reports/credentials-store/schluesselbund-bibliothek-vorlage-2026-08-22.md
(keytar: unbetreut, im Haus 0 lebende Nutzer, und nativ in einem heute rein
javascriptigen Paket).

Kein Geheimnis reist ueber die Befehlszeile — argv steht in der Prozessliste.
Alle drei Rueckseiten bekommen den Wert ueber stdin, das PowerShell-Skript
reist als -EncodedCommand. Eine Probe nagelt das fest; Flip-Probe gefahren
(Geheimnis nach argv verschoben => 1 rot, zurueck => 16/16).

vaultSession.ts — legt den abgeleiteten Schluessel ab, nicht die Phrase.
Ausdruecklich NICHT in secureStorage: dessen Schluessel ist
sha256(hostname+username), fuer jeden lokalen Leser nachrechenbar. Fuer ein
Anmelde-Token mag das reichen, fuer einen Schluessel, der die e2e-Zugaenge
oeffnet, waere es Theater.

Degradation ist erlaubt, Stille nicht: ohne Schluesselbund wird NICHT
heimlich Klartext geschrieben, sondern geworfen; die Zustimmung ist
ausdruecklich, AYOUNE_REQUIRE_ENCRYPTED_STORAGE=1 verbietet sie ganz
(Semantik aus desktop-client machineAuth/persistence.ts gespiegelt).

Ein Zwischenspeicher unter veralteten Ableitungs-Parametern wird verworfen
statt benutzt — nach einem Phrasen-Wechsel oeffnete er gar nichts mehr und
saehe dabei aus wie ein beschaedigter Datensatz.

35 Proben (16 + 19), tsc --noEmit Exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New css

Darstellung des awards-Blocks (W11a)

.ay-award-* — der Zeichner der cms-api gibt die Klassen beim Publizieren aus; ohne
diese Regeln stuenden die Auszeichnungen als nackte Aufzaehlung da (dieselbe Kopplung
wie bei .ay-partner-*).

Anders als beim Partner-Verzeichnis gibt es hier nur EINEN Erzeuger — eine eigene
Strecke /auszeichnungen existiert nicht. Wer sie spaeter baut, schliesst an DIESE
Klassen an und erfindet keinen zweiten Satz.

Das Siegel bekommt object-fit:contain wie das Partner-Logo (eine Marke darf nicht
angeschnitten werden), steht aber zentriert statt linksbuendig — ein Siegel ist rund
und wirkt links angeschlagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New blocks

awards-Block fuer Auszeichnungen und Zertifikate (W11a)

Neuer Blocktyp 'awards' aus der marketing-Entitaet Awards (Fundament-Kaskade 62):
Block-Config _awards[] / _awardsAuto, Filter _awardCategory, _awardsLimit, layout.
Aufgeloest in resolveEntityBlocks nach block.__awardItems (Muster partners).

AUFLOESER-Weg, und die Einordnung ist hier NICHT so glatt wie bei partners: IAward
traegt mit validUntil sehr wohl ein Zeitfenster, das ohne Zutun eines Menschen
ablaeuft. Massgeblich bleibt der Aufloeser-Weg, weil die MENGE sich nur durch
published aendert und ein Ablauf ein einmaliges, im Voraus datiertes Ereignis ist —
kein taeglich wanderndes Fenster wie bei events/locations. Die Kehrseite wird an zwei
Stellen entschaerft: der Resolver laesst zur Publizier-Zeit Abgelaufenes heraus, und
der Zeichner schreibt die Gueltigkeit SICHTBAR an die Kachel. Damit wird aus einer
stillen Falschaussage ein sichtbar veraltetes Datum.

Zwei Mengen-Bedingungen in EINEM Filter stehen unter $and, nicht zweimal $or — ein
zweites $or auf derselben Ebene ueberschriebe das erste STILL und der Website-Schnitt
waere ersatzlos weg.

KEIN interner Verweis aus dem slug: die Strecke /auszeichnung/<slug> existiert nicht
(anders als /partner). Der Verweis der Kachel ist certificateUrl, gehaertet.

formatAwardDate bewusst ohne toLocaleDateString — dessen Ausgabe haengt am
ICU-Bestand des Laufzeit-Abbilds; deutsch Punktform, sonst eindeutige ISO-Form.

aMN.Awards in die Ladeliste (18. Fremd-Modell) — ohne den Eintrag ist ay.models.Awards
undefined und der Block faellt STILL auf seinen Inline-Inhalt zurueck.

BEIDE Blocktyp-Waechter nachgezogen (rg ueber src/ und __tests__/ VOR dem Push
gefahren): der Mengen-Vergleich in pageblocks.test.ts bekommt seinen
Charakterisierungs-Fall, die harte Zahl in renderBlockListBlocks.test.ts geht 40 -> 41.
Dritter Waechter EXPLICIT_PUSHES in app.bootstrap.test.ts ebenfalls (17 -> 18) — er
hat den fehlenden Eintrag im vollen Lauf gefunden.

Verify: volle Einheiten-Menge 99 Suiten / 2358 Proben gruen, davon 34 neue in
awardsBlock.test.ts. Doku-Zahl 38 -> 41 (war schon vor diesem Block veraltet:
locations und quotes fehlten in der Liste).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New awards

Pflegeflaeche fuer Auszeichnungen und Zertifikate (W11a)

Erste Oberflaeche der Entitaet Awards (Fundament-Kaskade 62) — bis hierher konnte
niemand eine Auszeichnung anlegen, der Website-Block waere dauerhaft leer geblieben
(Praezedenzfall Portfolios). Vorlage ist der Partner-Zustand 23d, gleiche
Kurations-Familie.

- marketing.awards Liste mit Kennzahlen, Filtern, Schnellfiltern, Schnellanlage
- marketing.awards.edit Formular in sechs Abschnitten

Zwei Schalter, die nicht dasselbe meinen: published ist die Freigabe (fail-closed),
validUntil die Gueltigkeit — sie laeuft OHNE Zutun eines Menschen ab. Deshalb ein
eigener Schnellfilter Abgelaufen und eine eigene Kennzahl.

Der Zeitpunkt darin ist '{{now}}', NICHT new Date() — letzteres fröre den Zeitpunkt
des Pod-Starts in den gespeicherten Zustand ein (dieselbe Regel wie an 23g).

Verify: seederFieldsExist/germanUmlautHygiene/selectionFieldGuard gruen (18 Proben);
Flip-Probe mit erfundenem Feld meldet 'Awards.diesesFeldGibtEsNicht' namentlich rot,
der Waechter deckt den neuen Zustand also wirklich ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New codingjobs

den Koordinations-Spiegel stilllegen (410)

CEO-Entscheid 2026-08-21 (coding-pm-abschluss-zuteilung-und-spiegel, E6):
CodingJobs wird eingestellt, die Zuteilung wandert an ITask. Schritt 4.3
des Auftrags — der Erzeuger faellt ZUERST, sonst legt der naechste
Nacht-Lauf an, was der Aufraeum-Lauf gerade archiviert.

POST /codingjobs/actions/sync-coord-briefs sagt jetzt vor jeder
Auswertung mit 410 ab (nicht 404/500: der zweite Erzeuger ayauto coord
publish-brief ist fail-open und laeuft damit sauber weiter). Der
Betreiber-Melder npm run coord:report steigt mit einer Klartext-Meldung
und Ende 0 aus.

Gemessen 2026-08-22 gegen Prod: 332 Dokumente, davon 322 reine
Spiegel-Zeilen, 10 Kern (7 eigene Sonden, 2 Testlaeufer-Daten, 1 leerer
Entwurf). lease.generation>0 unveraendert bei 5, alle vom 08-10. Es ging
nie ein echter Auftrag durch diese Sammlung.

Laeufer und Abtast-Mechanik bleiben vollstaendig erhalten — die
Stilllegung ist eine Zeile zum Zuruecknehmen; die strukturelle
Entfernung ist ein eigener Fundament-Auftrag nach einem
Beobachtungsfenster. 4 Proben inkl. Umkehrprobe.
Fix tasks

die vier Zuteilungs-Rechte ausdruecklich registrieren

Ohne additionalRights sieht registerModuleRights sie nie -> sie stehen in
keiner Rolle und keinem Paket -> checkRights weist JEDEN Aufruf der vier
Lease-Wege mit 403 ab. Die Wege waeren tot geboren gewesen.

Die Praemisse des Auftrags ist widerlegt und nachgemessen:
CodingJobs.entityActions fuehrt claim/heartbeat/complete/cancel/approve/
reject — das dort zitierte 'hat keine Registry-Zeile' trifft nicht zu.
Die zweite genannte Praezedenz (dispatchCodeAgent) traegt nicht, weil sie
gar kein neues Recht erfindet.

additionalRights ist genau dafuer da und braucht keinen Fundament-Griff.
Drei Proben pinnen es fest, inkl. Textprobe auf app.ts.
New mailfunnel

vier Achsen auf eine konsolidiert, Aufschluesselung statt drei leerer Dashboards

CEO-Entscheid 2026-08-22 (Kombination B+A). Die drei Achsen brand-mails /
campaign-mails / newsletter-types entfallen; ihre Unterscheidung wandert als
vier Aufschluesselungs-Tabellen IN den einen Trichter mailfunnel-mails.

Warum das ohne Verlust geht: KPIMails fuehrt brand/campaign/newsletter/
newsletterType bereits im Schluessel UND im Rumpf (am Schema geprueft). Die drei
abgeloesten Sammlungen fuehrten zusammen 22.487 Zeilen, von denen genau EINE je
einen Wert trug — ihre Nullen waren wahr, es gab seit 2024-09-12 nichts zu
zaehlen.

Drei Befunde aus dem Bau:
1. Die alte mails-Tabelle gruppierte nach `name`, und mailStats schreibt das Feld
NIE (7/7 missing gemessen). Ausgerechnet die ueberlebende Achse waere eine
einzige Sammelzeile gewesen. Alle vier Tabellen loesen ihre Bezugs-Kennung
jetzt per $lookup auf _id auf — die Bauform, die der Aggregations-Dienst
ausdruecklich zulaesst (Primaerschluessel-Join, pipelineValidator.ts:100-106).
2. Ein additiver Seeder entfernt nichts: eine gestrichene Achse verschwindet aus
dem Code, nicht aus dem Katalog. Beide Seeder haben deshalb eine Aufraeum-Stufe
(deleteMany, doppelt eingegrenzt auf seedSource UND Slug-Praefix).
Der Schnitt ist ein PRAEFIX-Vergleich, kein Teilstring — sonst loeschte er
mailfunnel-mails-table-newsletter-type gleich mit.
3. Die Geometrie trug nur EINE Tabelle: alle vier haetten dieselbe y bekommen und
sich ueberlappt. Jede Tabelle hat jetzt ihre eigene Zeile.

Riegel ergaenzt: Aufraeum-Schnitt mit Negativ-Kontrolle, _id-Join je Tabelle,
vier verschiedene Dimensionen, vier verschiedene y-Werte. deleteMany im Mock
(ohne ihn haette der never-throw-Seeder die Stufe still verschluckt).
37 Proben gruen; volle Reihe 492/493 (der eine ist ein pushdeliveries-Timeout
ohne Bezug zu dieser Aenderung).
New mails

E-Mail-Trichter auf kpimails konsolidiert, Dimension gerettet und vorgezogen

CEO-Entscheid 2026-08-22 (Kombination der Wege B und A des Briefes
mail-kennzahlen-dimension-marke-kampagne-entscheiden): die drei KPI-Schreiber
brandEmailsStats / campaignEmailsStats / newsletterStats entfallen — ihre
Dimensions-Rueckschreibung an den MailLog NICHT.

Gemessen gegen Prod am 2026-08-22:
- kpibrandmails 3.772 Zeilen / kpicampaignmails 15.636 / kpinewslettertypes 3.079
= 22.487, davon traegt GENAU EINE einen Wert (Newsletter-Typ "Messe",
2024-09-12, sent 355 / delivered 280 / opened 127).
- maillogs: brand und campaign bei allen 21.550 Dokumenten missing,
newsletterType bei 355 gesetzt.

Zwei Befunde, die den Zuschnitt bestimmt haben:
1. Die drei Schreiber sind im ganzen Baum der EINZIGE Schreiber von
brand/campaign/newsletterType am MailLog. Ersatzlos entfernt haetten sie die
Achse genau in dem Moment verloren, in dem die Konsolidierung sie retten soll.
Der Kern lebt als mailLogDimensions weiter.
2. Die REIHENFOLGE war falsch: stats.mails rief mailStats ZUERST und die drei
Dimensions-Schreiber danach. mailStats liest die Dimension per $first — es las
sie also, bevor sie gesetzt war, und weil der naechste Lauf einen anderen
Zeitausschnitt traegt, erreichte sie kpimails nie. Unsichtbar, solange keine
Rundmails laufen; wirksam ab der naechsten. Jetzt laeuft mailLogDimensions
vorneweg.

Riegel: test/mailLogDimensions.test.mjs — Schema-Pfade (strict verwirft sonst
lautlos, mailCounts-Klasse), KPIMails traegt alle vier Dimensionen, Reihenfolge
gegen die kommentar-befreite Quelle, Umkehr- und Positiv-Kontrollen.
Flip-Probe gefahren: umgedrehte Reihenfolge macht genau Probe 6 rot. 104/104.

⚠ Der Bestand heilt nicht rueckwirkend — nur Zeitraeume nach der Umstellung
tragen die Dimension.
Fix memory-curator-daily

Phase-B-Gate auf die Harness-Kappung ziehen (30000 -> 24985 B)

Das Gate lag ueber der Kappung: autopilot-safe.md §3.1A Pfad 1 rief das Skript
genau in dem Band auf, in dem es nichts tat. Einheit bleibt bewusst BYTES, der
Wert wird die Zeichen-Kappung — fuer UTF-8 gilt Buffer.byteLength(s) >= s.length,
also folgt aus der Aufruf-Bedingung (Zeichen > 24985) zwingend Bytes > 24985; das
Gate kann damit nie mehr ein No-Op sein, wenn sein Aufrufer feuert.

Herkunft auf decisions.log 2026-08-18 umgestellt (loest den 2026-07-01-Entscheid
ab, der die 30.000 setzte). Byte-Kommentar berichtigt: der System-Reminder misst
ZEICHEN, nicht Bytes (gemessen 25.436 Z = 24,84 KB gegen die gemeldeten 24.8KB) —
die alte Gleichsetzung war der Mechanismus der Drift.

Flip-Probe auf identischer Fixture (26756 B): alt no-op, neu -2285 B unter Limit.
Umkehr-Kontrolle (20000 B) weiterhin no-op. SIZE_FLOOR byte-identisch. Margin 512
bleibt: Ziel 24473 B erreicht mit 31 von 56 Kandidaten in Reserve, kein Thrash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New tasks

Zuteilung im Pull-Modell — claim/lease-heartbeat/lease-complete/lease-cancel

Portiert aus domains/coding/api/.../codingjobs (CEO-Entscheid 2026-08-21,
coding-pm-abschluss-zuteilung-und-spiegel: die Lease wandert an ITask,
CodingJobs wird eingestellt). Vier Wege plus drei Bibliotheken.

Die Abhol-Bereitschaft ist entschieden: ein unabhaengiges Merkmal aus
BESTEHENDEN Feldern (runtimePolicy als Designation + offener PmStatus +
keine lebende Lease), kein neuer Status-Wert und kein Workflow-Uebergang.
Ohne die Designation waeren 3.547 offene Aufgaben holbar (Prod gemessen).

Drei Stellen, an denen eine 1:1-Portierung STILL falsch gewesen waere:
- priority ist ein WORT, keine Zahl: sort{priority:-1} liefert normal vor
critical. Ersetzt durch Stufen-Durchgaenge; Flip-Probe belegt die Umkehr.
- ITask fuehrt weder availableAt noch attemptCount noch result: strict
haette diese Schreibvorgaenge verworfen. Ersetzt bzw. entfallen.
- complete existiert bereits im PM-Sinn -> lease-complete.

18 Proben gegen echten mongod, davon 20-fache Nebenlaeufigkeit.
Fix autopilot-safe

Memory-Schwelle auf die Harness-Kappung ziehen (30000 -> 24985)

Die Curation loeste bei 30.000 Zeichen aus, die Harness kappt MEMORY.md aber
schon bei 24.985 und verwirft alles darueber STILL. Dazwischen lag ein Fenster
von rund 5.000 Zeichen, in dem der Index abgeschnitten geladen wurde, waehrend
jede Iteration gruen meldete. Gemessen war es besetzt: MEMORY.md = 25.297.

Vollzug des CEO-Entscheids decisions.log:5019 (2026-08-18), der in
memory-curator.md schon umgesetzt war und hier nicht mitgezogen wurde.

Die Brief-Tabelle nannte VIER Stellen, real waren es SIEBEN Vorkommen auf sechs
Zeilen plus die 30KB-Anzeige (zusaetzlich Zeile 338 und ein zweites Vorkommen in
Zeile 585). Aufgefallen ist es nur, weil das Verify-Gate grep -c '30000' => 0
verlangt statt 'die vier Zeilen'. Jede Fundstelle wurde vor dem Ersetzen im
Kontext geprueft; der historische Messwert 30.962 blieb unberuehrt.

Dazu je ein Absatz an 1b und 3.1A, der die Zahl ERKLAERT (Schmerzgrenze, nicht
Ziel) — sonst wandert sie beim naechsten Umbau zurueck.

Nebenbefund als Folge-Brief: scripts/memory-curator-daily.mjs traegt dieselbe
30000 als ausfuehrbares Gate (Phase B no-op bei 26.271 Bytes) und rechnet dabei
in BYTES statt Zeichen — ausserhalb der claim_paths und an einen aelteren
CEO-Entscheid gebunden, daher eigener Brief statt stiller Mitnahme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix infra

redeploy-entity-consumers fuehrt stillgelegte Repositorien nicht mehr als Rueckstand

Der Melder stufte allein nach locked gegen published ein und fragte nie, ob ein
Repositorium ueberhaupt ausbringbar ist — dieselbe Fehlerklasse, die CLAUDE.md
fuer die Pipelines-Zeile festhaelt (zaehlt Dateien, nie laufende Dienste).

Gemessen fuehrte er platform/internal als BEHIND: nie ausgebracht, nie eine Kette
gehabt, seit 2026-08-22 stillgelegt. Zwei belegte Schaeden — es entstand echte
Pflege-Arbeit daran (zwei Fundament-Pin-Commits am 2026-08-21 an einem
Repositorium ohne fachlichen Commit seit 2024-02-19), und --apply haette eine
Kette ausgeloest, die es gar nicht gibt.

Ein Repositorium mit RETIRED*.md im Wurzelverzeichnis faellt jetzt VOR der
Einstufung heraus — damit zugleich aus BEHIND, aus BEDINGT und aus der
Modul-Zaehlung. Es wird als eigene Zeile AUSGEWIESEN, nie still verschluckt
(ein unsichtbarer Ausschluss waere dieselbe Fehlerklasse in der anderen
Richtung); die JSON-Form traegt es ebenfalls.

--apply ist damit MITgehaertet und braucht keinen zweiten Waechter: die
Ausloese-Menge faellt ueber vier filter-Stufen aus derselben Liste, wer aus ihr
faellt, kann sie nicht erreichen. Eine Probe nagelt genau das fest.

Verify: Trockenlauf 24 -> 23 Kandidaten, platform/internal nur noch in der
Stillgelegt-Zeile, Negativ-Kontrolle domains/audit/api weiterhin drin. Neue
Probenreihe 7 Proben je Richtung; alle redeploy-Reihen 49/49.
Die 8 Fehlschlaege der vollen Reihe sind VORBESTEHEND — mit engem Rueckbau nur
dieser Datei nachgemessen: identische acht Titel auch ohne die Aenderung.
New cascade-68

Schema-Zwillinge - Zuteilungs-Achse an Tasks, Reiter+Seitenfilter an der lebenden Dashboard-Ebene, 1:n-Kante geschaerft

- Tasks: fuenf Sub-Schemas (lease/providerPolicy/runtimePolicy/
capabilityRequirements/budget), tokenHash mit select:false, Abhol-Index
{_customerID, status, lease.expiresAt} - Spanne bewusst am ENDE, abweichend
von der Feld-Reihenfolge der CodingJobs-Vorlage.
lease.generation OHNE Vorgabewert (die Vorlage hat default:0): ein
nicht-leeres Unter-Dokument entkaeme minimize und wanderte in jede
Bestands-Aufgabe.
- aYOUneStatesViewConfigSchema: pageFilter/tabs an der Dashboard-Ebene,
filterValues/tab/ignorePageFilter an der Karte - dieselben geteilten
Unter-Schemata wie aYOUneDashboards und CustomerDashboards, kein Nachbau.
- Projects/Repositories: Kommentar-Zwillinge mit am 2026-08-22 selbst
gemessenen Zahlen (0/46 bzw. 499/499 auf zwei Projekten, groesstes 466).

Flip-Probe beidseitig: gegen interfaces 2026.449.0 sechs Fehler, gegen
2026.450.0 null. Cascade68.roundtrip.test.ts 17 Proben, volle Reihe 680/680.
New overlays

Ebenen-Modell des Bild-Erzeugers wiederbeleben — type/Geometrie/Wasserzeichen + titelbild-taugliche Ausgabe

Der Vertrag `IOverlay` deklariert seit jeher `type: text|caption|image` samt
x/y/width/height/contentPosition/fontWeight — der Renderer las davon NICHTS und
rendphrase jede Ebene als zentrierte Bildunterschrift ueber die volle Leinwandbreite.

Gemessen gegen die Produktion (2026-08-22): die zwei lebenden Vorlagen tragen
x:20,y:700, contentPosition:"west", fontWeight:"bold" — alles verworfen. Eine Ebene
mit type:"image" waere als woertliches `caption:undefined` gerendert worden; deshalb
hat sie nie jemand angelegt (0 von 16 Vorlagen).

- jobs/overlayPlan.ts reiner Planer: Text- UND Bild-Ebenen (Marke, Wasserzeichen)
mit Schwerkraft, Versatz, Groesse, Deckkraft
- jobs/outputProfile.ts reines Ausgabe-Profil: Inhaltstyp folgt der ECHTEN Endung
(bisher fest "image/png", auch bei .jpg) und legt fuer ein
Titelbild flach — Apple weist Alpha-Kanaele ab, Spotify
verlangt sRGB
- generateImage.ts/.js verdrahtet beides; Bild-Ebene ohne Quelle wird uebersprungen
statt als caption:undefined gerendert

Rueckwaerts-vertraeglich: eine Ebene mit den Schema-Vorgabewerten erzeugt die
BYTE-GLEICHE Kommandozeile wie bisher — gegen die echte gm-Bibliothek geprueft, mit
Negativ-Kontrolle. PNG-Ausgaben bleiben unveraendert, solange kein Titelbild
angefordert wird (coverSafe reist im Auftrag, kein Vertrags-Feld noetig).

BEWUSST NICHT wiederbelebt: backgroundColor (Vorgabewert #FFFFFF ist deckend,
"nicht gesetzt" und "ausdruecklich weiss" sind nicht unterscheidbar).
NOCH NICHT im Vertrag: opacity — Mechanismus gebaut, heute No-Op.

23 neue Proben (62 gesamt), drei Umkehrproben beissen nachweislich.
New cascade-68

Zuteilungs-Achse an ITask, Reiter+Seitenfilter an der Dashboard-Ebene, 1:n-Kante Projekt/Repositorium benannt

Kaskade 68 buendelt drei rein additive Fundament-Glieder:
- ITask += lease/providerPolicy/runtimePolicy/capabilityRequirements/budget
samt fuenf neu deklarierten Sub-Vertraegen (Feld-Form 1:1 aus ICodingJob*,
bewusst NICHT re-importiert - der Quell-Vertrag wird stillgelegt).
- AyounePageViewConfigDashboards += pageFilter/tabs, ...DashboardCards +=
filterValues/tab/ignorePageFilter - die lebende Ebene kannte die Felder der
Vorlagen-Ebene nicht, strict verwarf sie beim Speichern still.
- Vertrags-Kommentare an IProject._defaultRepository (Rueckfall, nicht 'das
Repositorium des Projekts') und IRepository._project (kanonische 1:n-Kante),
mit am 2026-08-22 selbst gemessenen Zahlen.

modelsAndRights unveraendert bei 843 (null Entitaeten, kein Codegen-Fan-out).
Fix coord

Praemisse von alarm-umschlag-traegt-keinen-ursprungs-zeitpunkt korrigiert

An der Flaeche widerlegt: das ausgebrachte Bedienpanel (App-Laufzeit
360 s) zeigt 'seit 13.08. 18:30 · vor 8d'. Ein acht Tage alter Ursprung
kann nicht aus dem Arbeitsspeicher stammen — der START-Weg
(app.component.ts:1388-1392) liefert die Vorkommens-Daten sehr wohl.

Die Suche, die zur falschen Praemisse fuehrte, war ein FALSCHES NEGATIV:
eine Namens-Suche kann einen generisch gebauten Umschlag nicht sehen.
Die Positiv-Kontrolle fing es nicht — sie belegte nur, dass die Suche
ueberhaupt trifft, nicht dass der Erzeuger seine Felder woertlich
hinschreibt.

Offen ist damit nur noch, ob der LIVE-Weg (Socket) sie ebenfalls traegt.
Fix crawl-monitor

stehende Alarme ueber den ZUSTAND aufloesen, nicht nur ueber den Uebergang

Am 2026-08-22 standen fuenf von sechs Alarmen dieses Melders 4,4 bis 4,6 Stunden
und wurden ausschliesslich von Hand geschlossen — jeder 2 bis 67 Sekunden nach
seiner eigenen Quittierung. Der Melder lief durchgehend und meldete fuer de_de
schon um 00:07:54Z wieder `queueStatus: ok` bei 236.603 wartenden Auftraegen.

Ursache der Klasse: `decideSymptom` loest ausschliesslich ueber den UEBERGANG
bad→gut auf. Geht dieser eine Uebergang verloren, steht der Alarm fuer immer,
und zwar lautlos — der Zustandsautomat nimmt danach den stillen Zweig
`gut + prevStatus !== "bad"` → `none`.

Neu: `decideStandingAlertSweep` loest ueber den ZUSTAND auf. Solange fuer eine
Kachel ein Alarm als offen vermerkt ist und die Lage HEUTE gut und beurteilbar
ist, wird aufgeloest — unabhaengig davon, ob der Uebergang je erkannt wurde.
Der Marker faellt dabei, der Weg feuert also genau einmal je Episode.

- eigenes Feld `SymptomState.openAlert`, BEWUSST keine Umdeutung von
`lastAlertAtMs` (das traegt die Melde-Daempfung und wird von
checkCrawlSuccessRate mitbenutzt) — `decideSymptom` bleibt unveraendert
- kein Ruhe-Fenster-Aufloesen: `judgeable` ist Bedingung
- nachgeholte Aufloesungen loggen als `warn`, nicht `info` — sie heissen, dass
der regulaere Weg versagt hat
- Waechter test/crawlStandingAlertSweep.test.ts (G1 Wiedergabe des echten Falls,
G2 Flip-Probe, G3 Positiv-Kontrolle de_ch inkl. keine Doppel-Aufloesung)
New ayauto

migrate features-namen-tranche2-nachziehen

Die Namens-Tranche 2 korrigiert 27 Zustands-Namen in der PLATTFORM. Der
erste Abgleich-Durchgang (2026-08-21) hatte die WEBSITE-Namen genau
dieser Schluessel mit den damals SCHLECHTEN Plattform-Namen ueberschrieben
(Tools, Prompts, SLAs, Middlewares). Beide Seiten laufen dadurch wieder
auseinander -- diesmal mit der Plattform als der besseren Seite.

🔴 OHNE diesen Nachzug SENKT die Tranche die Deckung, statt sie zu heben:
gemessen unmittelbar nach der Ausbringung 453/554 deckungsgleich gegen
471/554 davor. Nach dem Lauf: 477/554.

🔴 Das AUSBRINGUNGS-TOR ist der Kern: geschrieben wird nur, wo der
Plattform-Zustand den neuen Namen BEREITS fuehrt. Ein Dienst mit roter
oder laufender Kette faellt heraus statt eine Behauptung zu erzeugen, die
die Plattform nicht deckt -- gemessen fielen die drei cms-Schluessel
genau so heraus (Kette vorbestehend rot). Ein erneuter Lauf holt sie nach.

🔴 BEWUSST kein blindes "Website uebernimmt die Plattform" -- genau das
war der Fehler des ersten Durchgangs, der 96 kuratierte Namen zerstoert
hat. Hier steht eine ausdrueckliche Liste von 27 Schluesseln, deren
Zielnamen aus dem deutschen `summary` des jeweiligen Zustands abgeleitet
sind.

Waechter: Ausbringungs-Tor (Plattform-Seite), Alt-Wert-Waechter
(Website-Seite, kein Ueberschreiben + Idempotenz), Positiv-Kontrolle auf
alle 27 Schluessel, Umkehr-Kontrolle mit erfundenem Schluessel,
Rueckroll-Abbild VOR dem Schreiben.

Gefahren 2026-08-22: DRY 24/27 -> APPLY geschrieben=24, Rest auf
Alt-Wert=3 (die drei wartenden), an der Datenbank nachgemessen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0181TrRUCtUiQJtm5yWRLYKR
New reporting

Abschlussquote-Familie N2 — zwei Dashboard-Vorlagen, zehn Kacheln

Die zweite Bauart des Reporting-Umzugs nach dem E-Mail-Trichter (M1):
`assignment-appointments` und `assignment-events`, die Stufe 4 als falsch
einsortiert nachgewiesen hat (Matrix-Befund B4). Beide standen im Ledger auf
`covered-by-template` OHNE Beleg.

Je Achse fuenf Vorlagen: zwei Zaehler, die Abschlussquote, ein Ring nach dem
Verknuepfungs-Zustand und eine Uebersicht mit zwei Spalten.

Sechs gemessene Befunde bestimmen die Bauform:

- Der Ring geht NICHT nach `type`. In der Alt-Antwort ist `chart[].type` ein
Beschriftungs-Text, kein Feld; `calendarentries.typ` ist bei 13.477 von
13.492 `standard` — das waere ein Ring mit einem Segment.
- Das Zeitfenster ist ZWEISEITIG (sieben Tage bis morgen, wie die
Alt-Steuerung). Ohne Obergrenze faellt es auf 1.639 statt 22 Termine,
Faktor 74. M1 durfte die Obergrenze weglassen, weil Kennzahl-Zeilen nicht
in der Zukunft liegen — Termine schon.
- Die Verknuepfung braucht die Unter-Pipeline-Form MIT `_customerID`, also das
Gegenteil der M1-Regel: der Pruefer weist ein `$lookup` ueber ein
Nicht-`_id`-Feld sonst als moeglichen mandanten-uebergreifenden Lesezugriff
ab. Auf der obersten Ebene bleibt der Vergleich weiterhin verboten.
- Der Mandanten-Vergleich ist typ-tolerant (`$toString` beidseitig) —
`assignments._customerID` ist gemischt typisiert (96 Objekte, 1 Zeichenkette).
- Der Alt-Feldname `attendees_tookPart.attendee` ist gebrochen (das Schema
kennt nur `_attendee`) und hier korrigiert, nicht portiert.
- Die Alt-Beschriftungen sind irrefuehrend: `stats.total` zaehlt die
Basis-Menge, nicht das, was das Etikett sagt.

Beide Verknuepfungs-Achsen sind im Bestand leer (`assignments.calendarentry`
bei 0 von 97, `meetings.attendees_tookPart` bei 0 von 32) — beide Quoten stehen
auf 0 %. Dass die Rechnung traegt, ist mit drei Positiv-Kontrollen belegt:
dieselbe Quoten-Pipeline mit gelockerter Verknuepfung rechnet 57,5 %, die
Veranstaltungs-Verknuepfung ohne den Teilnehmer-Vergleich findet 21 von 97, und
`$in` ueber einen gepunkteten Listen-Pfad trifft 89 Dokumente.

35 neue Proben, reine Reihe 225/225 ueber 12 Suiten. Sieben Flip-Proben, sechs
beissen; die siebte (`RING_SPALTEN` 5 -> 8) beisst zu Recht NICHT, weil sie
keine Verletzung erzeugt. Eine Flip-Probe hat eine eigene Probe als zu schwach
entlarvt (`let: {cid: "$_customerID"}` enthaelt die Zeichenkette schon) — die
nachgeschaerfte Probe haelt jetzt den Vergleich selbst fest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix build

test/ aus dem Haupt-Bau ausschliessen (2x TS6059)

Der Bau war rot, seit `tsc || exit 0` entfernt wurde. Ohne `include` greift das
Vorgabe-Muster `**/*` und zieht test/jobEventBuffer.test.ts + test/jobRegistry.test.ts
in den Haupt-Bau — beide liegen ausserhalb von rootDir './src' (TS6059).

Die Testreihe hat ihr EIGENES tsconfig.test.json (rootDir '.', outDir './.test-out')
und ueberschreibt `exclude` vollstaendig, laeuft also unveraendert weiter. dist und
.test-out sind Bau-Ergebnisse und faenden sich sonst beim naechsten Lauf selbst als
Quelle wieder.

Kein @ts-ignore, kein || exit 0, kein noEmitOnError-Aequivalent.
Verify: npx tsc --noEmit 0 Fehler - npm run build Exit 0 - npm test 60/60 gruen -
dist-Struktur unveraendert (dist/server.js liegt weiter obenauf, Einstiegspunkt intakt).
Fix types

Images-Felder aus dem Documents-Datensatz entfernen

Der Bau war rot, seit `tsc || exit 0` entfernt wurde — 10 Uebersetzungsfehler,
alle TS2339 in jobs/generateVideo.ts an zwei gleichlautenden Stellen.

Der Code baut einen `Documents`-Datensatz, setzt darauf aber fuenf Felder, die zu
`IImage` gehoeren: _productID, _productGroupID, _productDivisionID, _publicationID,
_filename. Gemessen: 0 Treffer in Vertrag UND Schema von Documents, je 1 Treffer in
IImage; Positiv-Kontrolle (originalname/mimetype/linktype/isCloudFile) 1/1, Umkehr-
probe mit erfundenem Feld 0. Der Block stammt aus einem Bild-Upload-Pfad.

Mongoose `strict` hat alle fuenf beim Speichern die ganze Zeit STILL verworfen — der
Riegel verdeckte also nicht nur einen roten Bau, sondern eine tote Schreiboperation.
Es geht nichts verloren: die vier Verweise wurden ohnehin nur auf null gesetzt, und
_filename trug denselben Wert wie originalname, das echte Feld des Vertrags, das eine
Zeile weiter unten gesetzt wird.

Kein @ts-ignore, keine Fundament-Kaskade (nichts fehlt im Vertrag - das Feld gehoert
an eine andere Entitaet).
Verify: npx tsc --noEmit 0 Fehler - npm run build Exit 0 (20 .js emittiert).
Fix types

echte puppeteer-Typen statt Cleanup-Vertrag + ClosableProcess.kill akzeptiert number

Der Bau war rot, seit `tsc || exit 0` entfernt wurde — 22 Uebersetzungsfehler, alle
in runBrowserAutomation.ts. Zwei getrennte Defekte:

1. Die Arbeits-Handles waren als ClosableBrowser/ClosablePage deklariert. Das sind
die NARROW Cleanup-Vertraege (nur close()/process()), damit closeBrowserSafely
ohne echtes Chrome testbar bleibt — kein Ersatz fuer die volle puppeteer-API.
Jeder Schritt-Aufruf (newPage, goto, $$eval, screenshot, ...) war dadurch ein
TS2339. Jetzt Browser/Page aus puppeteer; die erfuellen den Cleanup-Vertrag
strukturell an den closeBrowserSafely-Aufrufstellen.

2. ClosableProcess.kill(signal: string) war in KEINER Richtung zu einem echten
ChildProcess.kill (signal?: NodeJS.Signals|number) zuweisbar — number ist kein
string. Ein echter Browser erfuellte ClosableBrowser also gar nicht (TS2322),
entgegen dem eigenen Kommentar der Datei. Jetzt string|number: echter
ChildProcess UND die Testattrappen (kill: (sig: string)) passen, ohne Node-
oder puppeteer-Typen in den bewusst abhaengigkeitsfreien Helfer zu ziehen.

Kein @ts-ignore, kein || exit 0, kein noEmitOnError-Aequivalent.
Verify: npx tsc --noEmit 0 Fehler - npm run build Exit 0 (9 .js emittiert) -
npm test 49/49 gruen.
New ayauto

migrate ayounestates-waisen-namen — Namen der sechs Waisen

Fuer sechs Bildschirm-Zustaende greift der Code-Weg STRUKTURELL nicht:
`ayounestates` wird bei jedem Pod-Start von `registerModuleStates` neu
gesaet, diese sechs haben aber keinen lebenden Saeer unter ihrem Titel.
Sie brauchen eine direkte Daten-Korrektur; Zielnamen aus der W7-Kuration
(features-namen-snapshot-2026-08-22.tsv), nicht neu erfunden.

🔴 ZWEI PRAEMISSEN DES AUFTRAGS SIND GEGEN PRODUKTION WIDERLEGT:

(1) "alle sechs tragen _appName: null" -- falsch fuer zwei.
monitoring.computingentities und -logs tragen
ayoune-computing-entities-develop. Der Lauf meldet die Saeer-Kante
je Zeile, statt sie zu verschweigen: ein Pod-Start KANN sie
ueberschreiben.

(2) "bei den Zwillingen pruefen, ob Korrektur oder Stilllegung richtig
ist" -- die Messung entscheidet fuer KORREKTUR, gegen die naive
Erwartung. Der Feature-Katalog der Website kennt ausschliesslich die
monitoring.*-Schluessel; fuer computing-entities.* gibt es dort KEINE
Zeile. Eine Stilllegung liesse also zwei Website-Features OHNE
Plattform-Gegenstueck zurueck und machte die Lage schlechter.
Dasselbe bei sale.orders (die Website fuehrt sale.orders UND
ecommerce.orders). Die zugrundeliegende Dublette bleibt und ist eine
PRODUKT-Entscheidung (welcher Schluessel ist kanonisch?) -- sie wird
benannt, nicht still geloest.

Waechter: Alt-Wert im Filter (kein Ueberschreiben einer fremden
Korrektur + Idempotenz), Positiv-Kontrolle auf alle sechs Titel,
Umkehr-Kontrolle mit einem erfundenen Titel, Rueckroll-Abbild VOR dem
Schreiben. `name_en` bleibt unangetastet.

Gefahren 2026-08-22: DRY 6/6 -> APPLY geschrieben=6, Rest im Filter=0,
an der Datenbank unabhaengig nachgemessen; Re-DRY 0 von 6 bei weiterhin
6/6 Positiv-Kontrolle (keine falsche Null).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0181TrRUCtUiQJtm5yWRLYKR
New hr

Fundament-Pin auf 448.1/452.1 + voller Codegen-Modul-Lauf

Pin-Bump auf den publizierten Stand (interfaces 2026.447.1->2026.448.1, models
2026.451.0->2026.452.1, core 2026.315.1 unveraendert) per fix-foundation-pinning
--pin-exact --de-latest, danach npm ci — ohne das liest der Erzeuger die ALTE
installierte Fassung und ueberspringt neue Entitaeten still als [skew]
(Kaskade-50-Falle). Lock-Beleg vor dem Commit: genau DREI Aufloesungen der
Fundament-Pakete, keine verschachtelte Zweitkopie.

Kaskade-48-Pruefpunkt gefahren: use('/:id/actions') 5 -> 7. NICHTS verloren; neu
verdrahtet sind employees und payslips, deren Aktionen deklariert, aber nie an
den Router gehaengt waren.

Der uebrige Diff ist die Suchfelder-Vorlage (74b04f2150): die Freitextsuche liest
jetzt searchableFields statt hartkodiert name/title/subject. Fuer die zwei neuen
Entitaeten ist das keine Kosmetik, sondern Bedingung — ?q= haette sonst NICHT in
firstName/lastName/email/headline gesucht. Testreihe 785/785 gruen (42 Suiten);
der in marketing gemessene Rot-Effekt derselben Vorlage tritt in hr nicht auf.

MESSUNG statt Annahme zu den neu erzeugten Aktions-Ruempfen: von den fuenf
Employees-Aktionen sind VIER durch handgeschriebene custom-Router gedeckt
(generate-/stylize-portrait, grant-/revoke-portal-access) und damit tot, weil
custom vor default mountet; Payslips print ebenso. UNGEDECKT ist allein
import-lexware-dls — sein Rumpf ist fail-closed (ALLOWED_FIELDS leer, schreibt
nichts), aber er antwortet 200 und feuert das Ereignis, ist also ein STILLER
Nicht-Vollzug, bis der echte Importer gebaut ist (eigener Brief
lexware-dls-importer-hr-upload-diff-upsert).

Nur die 19 inhaltlich geaenderten Dateien sind aufgenommen; der Erzeuger schreibt
LF und liess 153 weitere Dateien als reine Zeilenende-Umschreibung im Status
stehen — die sind verworfen, nicht committet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XC3R3yf9u7xnZneEKnU8w
Fix routes

getMany-Suchfelder nachgezogen — beide Lese-Routen suchen wieder gleich

Der Vorlagen-Fix 74b04f2150 (2026-08-21) hat `getMany.ts.tpl` an ihre Schwester
`list.ts.tpl` angeglichen: statt der fest verdrahteten Felder name/title/subject
liest sie jetzt die am Modell deklarierten `searchableFields`. Der Commit fasste
GENAU EINE Datei an — kein Modul wurde danach neu erzeugt. Dieses Modul ist der
erste nachgezogene Konsument.

GEMESSEN statt geschaetzt (eigene Messung 2026-08-22, gegen interfaces 2026.448.1):
ueber die 18 Module mit erzeugten Lese-Routen wuerden sich 109 getMany- und 70
list-Handler bewegen; 17 Entitaeten sind hand-gehaertet und bleiben unberuehrt.
marketing allein stellt 109 dieser Bewegungen — der Modul-Lauf hat 110 Dateien
geaendert.

⚠ DIE ERSTE MESSUNG WAR EINE FALSCHE NULL, und sie ist hier festgehalten, weil sie
die naechste Messung genauso treffen wuerde: hand-gehaertete Handler bauen ihr `$or`
aus `SEARCH_FIELDS.map(...)` statt als Literal-Block. Ein Muster, das nur den
Literal-Block liest, meldet fuer sie eine LEERE Feldliste — also weder einen Treffer
noch einen Fehler. marketing sah dadurch nach 2 betroffenen Entitaeten aus statt 69.

ZWEI BEKANNTE NEBENWIRKUNGEN DES MODUL-LAUFS: beide gemessen, keine eingetreten.
Aktions-Verdrahtung `use('/:id/actions'` 1 -> 1 Datei. Testdateien unter
routes/default/__tests__ 1 -> 1, keine Loeschung im Diff. EXPECTED_MOUNT_COUNT
bleibt 72 (der Waechter ist gruen, es kam keine Route dazu).

PROBEN AUF DAS NEUE VERHALTEN GEHOBEN, nicht nachgezogen: 78 Proben in 41 Reihen
nagelten den ALTEN Zustand fest. Sie behaupten jetzt die aus `modelsAndRights`
ABGELEITETE Feldliste (`src/lib/testing/searchableFields.ts`), nicht eine von Hand
abgeschriebene — eine Probe, die die Implementierung spiegelt, belegt nichts. Die
Ableitung wirft bei einem unbekannten Plural, damit ein Tippfehler nicht still auf
den Rueckfall laeuft.

NEUER MODUL-WAECHTER `searchableFieldsParity.guard.test.ts` (7 Behauptungen,
statisch, ohne AY-Start): jeder erzeugte getMany-Handler gegen die Deklaration,
jeder list-Handler gegen seinen getMany-Zwilling, UMKEHRPROBE an LoyaltyPoints
(einzige marketing-Entitaet ohne Deklaration -> exakt der Rueckfall), GEGENPROBE
dass keine deklarierte Entitaet auf dem Rueckfall steht, plus die namentliche Liste
der 15 hand-gehaerteten Entitaeten (eine stille Haertung faellt damit auf).

FLIP-PROBE GEFAHREN: ein Handler kuenstlich auf den Rueckfall zurueckgesetzt ->
GENAU DIE DREI tragenden Behauptungen fallen, die vier Kontrollen bleiben gruen;
nach dem Zuruecksetzen wieder 7/7. Der Waechter beisst also wirklich.

⚠ Der Haertungs-Marker taugt NICHT als Erkennungsmerkmal: die Vorlage zitiert die
Zeichenkette `HAND-HÄRTUNG` selbst in einem Anleitungs-Kommentar, jede erzeugte
Datei enthaelt sie. Erkannt wird an der zeilen-verankerten `const SEARCH_FIELDS`,
die in keiner Vorlage vorkommt (29 Dateien / 15 Entitaeten).

WIRKUNGS-BELEG, nicht Bau-Beleg: Awards deklariert name/slug/issuer/description —
getAwards UND listAwards rendern genau diese vier. LoyaltyPoints ohne Deklaration
rendert weiterhin name/title/subject.

Bau gruen, volle Reihe 120 Suiten / 6316 Proben gruen (vorher 119 / 6309).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EK5PonxfgqrAuKZCvkjyHs
Fix pages

testimonial-Auto-Zug sortiert `createdAt` statt des schema-losen `created`

`IRating` fuehrt `created`, das `RatingSchema` deklariert es NICHT — `strict` verwirft es beim
Schreiben still, es lebt nur in Alt-Dokumenten. Am Bestand gemessen 2026-08-22: `created` in 6
von 46 Dokumenten, `createdAt` in 46/46 (Positiv-Kontrolle im selben Lauf). Fuer 40 Dokumente
sortierte MongoDB gegen `null` — der Zug lieferte also NICHT die neuesten `_ratingsLimit`,
sondern eine beliebige Auswahl, ohne je fehlzuschlagen. Die vier Alt-Anzeigewege sortieren
`createdAt` (`legacy-core-api/services/micro/content/dynamic.ts:315`).

Heute ohne sichtbare Wirkung (plattformweit passieren nur 4 Bewertungen den Filter, die
Vorgabe-Grenze ist 12) — der Zweig wird erst mit wachsendem Bestand falsch, und dann lautlos.
Genau deshalb war er nicht aufgefallen: der bestehende Fall (c) prueft Filter UND Grenze, die
Sortierung nie. Die neue Probe schliesst die Luecke, mit Negativ-Kontrolle auf das Phantom-Feld
(rot vor dem Fix belegt, gruen danach).

Dazu die Abgrenzung der ACHT bewertungs-artigen Achsen im Blocktyp-Kanon — welche Entitaet auf
eine Website gehoert und welche nicht (`StoreViews` ist gar keine Bewertung; `Settings.rating`
ist die achte Achse und steht per Bauart NICHT in `modelsAndRights`). Voller Beleg:
`.claude/reports/bewertungs-achsen/abgrenzung-2026-08-22.md`.

`KNOWN_BLOCK_TYPES` unveraendert — kein neuer Blocktyp, die zwei Waechter bleiben gueltig.

Verify: `tsc --noEmit` Exit 0; volle Reihe 2318/2319 (der eine Fehlschlag
`__tests__/contents.test.ts:282` ist VORBESTEHEND — mit gestashten Aenderungen identisch rot).
Fix ctas

Baustein `testimonials` filtert `web: true` — Freigabe-Tor wiederhergestellt

Der Zweig zog `Ratings` mit `_customerID` + `rating != ""` und sonst nichts — als einziger
oeffentliche Massen-Zug der Plattform ohne Freigabe-Filter. Der `references`-Zweig 40 Zeilen
tiefer in DERSELBEN Datei filtert `published: true`; vier Alt-Anzeigewege filtern `web: true`
(`legacy-core-api/services/micro/content/dynamic.ts:314/:346/:390`, `replacer.ai.ts:584`).

Gemessen 2026-08-22 an den drei lebenden `testimonials`-Bausteinen: 35 Texte im Zug, davon 4
web+freigegeben und 1 mit ausdruecklich abgewaehltem `web`. `rejected` ist 0 — es wurde also
keine ABGELEHNTE Bewertung gezeigt; gebrochen war das Versprechen des Schalters, nicht die
Moderation.

Bewusst NICHT die strengere Dreier-Bedingung des CMS-Blocktyps `testimonial`
(`approved+web+!rejected`): sie kuerzte diese drei Bausteine von ~10 auf <=4 Eintraege — eine
sichtbare Inhaltsaenderung und damit eine Entscheidung, keine Fehlerbehebung. Vorgelegt als E1 in
`.claude/reports/bewertungs-achsen/abgrenzung-2026-08-22.md`.

Verify: `tsc --noEmit` Exit 0 (das Repositorium fuehrt kein Testwerk).
New doc-engine

auch den Takt-Schluessel an den Fundament-Katalog binden

`DOC_TYPE` im Sachbezugs-Takt war eine weitere frei getippte Kopie. Ein
Tippfehler dort haette gar keinen Fehler erzeugt, sondern einen stillen
Leerlauf: der Takt schickt den Render-Auftrag, der Renderer findet den Fall
nicht und haelt sich fuer unzustaendig.

Flip-Probe: `benefit-in-kind-reciept` faellt mit TS2820.

Verify: tsc gruen, npm test 285 Tests / 0 fail.
New doc-engine

Bescheinigungs-docType gegen den Fundament-Katalog typisieren

`ICertificateCase.docType` stand auf `string` — ein Tippfehler in einem
Katalog-Eintrag ging still durch den Bau und schlug erst zur Laufzeit auf:
`NACH_DOCTYPE` fand den Fall nicht und der Renderer hielt sich fuer
unzustaendig, ohne dass irgendwo ein Fehler entstand.

Gemessen vor der Aenderung: mit `employer-sick-certificat` (ein Buchstabe
fehlt) blieb der Bau gruen (Exit 0), waehrend die Positiv-Kontrolle (ein
zusaetzliches Pflichtfeld am Interface) korrekt mit TS2741 an BEIDEN
Katalog-Eintraegen fiel — die Kopplung fehlte also wirklich.

Jetzt traegt das Feld `EmployeeCertificateDocType` aus
@tolinax/ayoune-interfaces, dieselbe Quelle, gegen die `hr-api` seine
Auswahl-Selektoren prueft. Beide Tippfehler fallen mit TS2820, samt Vorschlag
des richtigen Wertes.

Die zwei Nachschlage-Funktionen behalten `string` als Eingang: der Wert kommt
aus einem Render-Auftrag, also vom Netz, und kann per Sache nicht zur Bauzeit
geprueft werden. `NACH_DOCTYPE` bleibt aus demselben Grund auf `string`
verschluesselt — eine engere Karte zwaenge jeden Aufrufer zu einer
`as`-Zusicherung, und eine Zusicherung ist genau die Aufweichung, gegen die
dieser Umbau steht. Der Bau-Schutz sitzt am Katalog-Eintrag, nicht an der
Karte; die Datei traegt danach null neue Zusicherungen.

KEIN Pin-Bump noetig: der Worker steht bereits auf interfaces 2026.448.1 und
damit exakt auf dem publizierten Stand (dist-tags.latest), der Lock traegt
denselben Wert (kein halber Pin). Der Katalog ist installiert und fuehrt beide
Werte; die Umkehrprobe mit einem erfundenen Wert faellt.

Verify: tsc gruen, zwei Flip-Proben (je TS2820), Gegenprobe Exit 0,
npm test 281 Tests / 0 fail.
New hr

Bescheinigungs-Selektoren gegen den Fundament-Katalog pruefen

`ABSENCE_CERTIFICATE_SELECTORS` stand auf `Record<string, ...>` — ein
Tippfehler im `docType` ging still durch und schlug erst zur Laufzeit auf.
Gemessen: mit `employer-sick-certificat` (ein Buchstabe fehlt) blieb der Bau
gruen (Exit 0), waehrend die Positiv-Kontrolle (`maxDurationDays: "drei"`)
korrekt mit TS2322 fiel — die Kopplung fehlte also wirklich, die gruene Null
war echt.

Jetzt prueft ein `satisfies Partial<Record<EmployeeCertificateDocType, ...>>`
das Katalog-Literal gegen `EMPLOYEE_CERTIFICATE_DOC_TYPES` aus
@tolinax/ayoune-interfaces. Derselbe Tippfehler faellt mit TS2353.

`Partial` und nicht das volle `Record` ist Absicht: der Fundament-Katalog
fuehrt seit Kaskade 64 auch `benefit-in-kind-receipt`, und die haengt am
Mitarbeiter und einem Monat (`entityType: "Employees"`, `periodic: true`),
nicht an einer Abwesenheit — ein Abwesenheits-Selektor dafuer existiert per
Sache nicht. Ein volles Record verlangte einen Schluessel, der hier nie
hingehoert. Gegenprobe gefahren: der zweite Katalog-Wert waere zulaessig
(Exit 0), er ist nur nicht erzwungen.

`AbsenceCertificateDocType` ist aus dem Selektor-Bestand ABGELEITET
(`keyof typeof`), damit keine dritte Wahrheit entsteht.
`istBekannterBescheinigungsFall` ist jetzt ein Type-Guard und bleibt
ausdruecklich am lokalen Bestand statt auf `isEmployeeCertificateDocType`
zurueckzufallen: der Fundament-Waechter beantwortet "kennt der Katalog den
Typ?", diese Funktion "kann DIESER Massenlauf ihn auswaehlen?" — fuer
`benefit-in-kind-receipt` lautet die erste Antwort ja und die zweite nein.

`IAuswahlArgs.docType` bleibt `string`: der Wert kommt aus einem
Anfrage-Rumpf und kann per Sache nicht zur Bauzeit geprueft werden; der Wurf
bleibt der Laufzeit-Schutz, den die Testreihe festnagelt.

Verify: tsc gruen, drei Flip-Proben (Tippfehler TS2353, erfundener Schluessel
TS2353, Katalog-Wert zulaessig Exit 0), npm test 42 Suites / 781 Tests gruen.
Fix coord

awaiting auf dependency ziehen (sed traf den Kommentar-Suffix nicht)

Die Zeile lautete 'awaiting: none # CEO 2026-08-21 …' — mein Muster verlangte das Zeilenende
direkt nach 'none' und griff deshalb nicht. Ein blockierter Brief mit 'awaiting: none' waere
weiterhin claimbar gewesen (none steht in AWAITING_CLAIMABLE).
New personal

Lohnzettel-Sicht meldet sich als heikel — kuerzere Sperrfrist

Am geteilten Werkstatt-Geraet darf ein Schichtplan 20 Minuten offen stehen, ein
Lohnzettel nicht. Bisher gab es genau EINE geraeteweite Frist; die Flaeche
behalf sich mit „Fertig" (sofort sperren) und Vergessen beim Verlassen — beide
erfassen den Fall NICHT, um den es geht: weglaufen, ohne etwas anzufassen.

Die Sicht MELDET sich jetzt als heikel (@tolinax/ayoune-lock 2026.19.0,
enterSensitiveView/leaveSensitiveView); die Bibliothek waehlt daraus die
KUERZERE der Mandanten-Fristen. Es bleibt bei der EINEN Sperre — nur der Wert
ihres Zeitgebers aendert sich. Kein zweiter Zeitgeber.

Drei Einhaengepunkte, weil die Sicht ein ZUSTAND der Komponente ist und keine
eigene Route:
- wechsle() meldet an bzw. ab, je nachdem wohin gewechselt wird
- ngOnDestroy meldet IMMER ab — ein Vergessen liesse die kurze Frist
geraeteweit stehen (leaveSensitiveView ist bei fremder Kennung wirkungslos)
- der Kopfkommentar fuehrt nicht mehr den alten Befund, sondern die Loesung

Das „Fertig" BLEIBT. Es ist die ausdrueckliche Handlung, nicht der Ersatz.

⚠ Pin bewusst gehoben (2026.18.0 -> 2026.19.0) mit Lock-Beleg VOR dem Bau.
⚠ Pfad ausserhalb des claim_paths dieses Auftrags (platform/ayoune-lock/src) —
clients/terminal war zum Zeitpunkt des Edits von KEINER Sitzung gehalten
(geprueft); der Auftrag verlangt die Flaechen-Umstellung ausdruecklich als
Punkt 3, sein claim_path war zu eng geschnitten.

Verify: tsc --noEmit 0 · Produktions-Bau gruen (inkl. Schrift-Eigenauslieferung)
· Bibliotheks-Proben 62/62.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New lock

kuerzere Sperrfrist fuer als heikel gemeldete Sichten

Am geteilten Werkstatt-Geraet sind nicht alle Sichten gleich heikel: ein
Schichtplan darf 20 Minuten offen stehen, ein Lohnzettel nicht. Bisher gab es
genau EINE geraeteweite Untaetigkeits-Frist.

🔴 DIE ENTSCHEIDUNG, DIE DER AUFTRAG ZU MESSEN VERLANGTE — die Sicht MELDET sich
als heikel, statt eine Dauer zu uebersteuern. Der Grund ist kein Geschmack:
eine Flaeche, die eine ZAHL setzen darf, kann die Frist auch VERLAENGERN. Das
waere ein Sicherheits-Rueckschritt, den niemand bemerkt — kein Fehler, keine
Meldung, nur eine Sicht, die laenger offen steht als der Mandant erlaubt hat.
Man muesste ihn mit `Math.min` abfangen — und waere damit faktisch bei dieser
Loesung, nur mit einer API, die etwas anderes verspricht als sie tut.
Die Flaeche sagt WAS sie ist, die Bibliothek entscheidet WIE LANGE; die
Frist-Hoheit bleibt beim Mandanten.

KEINE zweite Sperre: es bleibt bei dem EINEN Zeitgeber, nur der Wert aendert
sich, mit dem er gestellt wird.

🔴 Der laufende Zeitgeber wird beim An- und Abmelden SOFORT neu gestellt. Ohne
das griffe die kuerzere Frist erst nach der naechsten Nutzer-Aktivitaet — also
genau dann NICHT, wenn jemand die heikle Sicht oeffnet und weglaeuft, ohne
etwas anzufassen. Das ist der Fall, fuer den es die Faehigkeit gibt.

🔴 Ein SET statt eines Schalters: bei einem Sichten-WECHSEL meldet sich die
zweite Sicht an, BEVOR die erste sich abmeldet. Ein boolean wuerde beim
`leave` der ersten zuruecksetzen, und die zweite stuende still auf der langen
Frist. Als Probe festgenagelt.

Rein additiv: ohne `enterSensitiveView()` verhaelt sich alles byte-identisch
wie bisher (`sensitiveViews.size === 0` -> regulaere Frist). Die neue
Kontext-Methode ist optional.

Proben 62/62 (17 neu), Bau gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
Fix deps

device-shell 2026.9.0 -> 2026.10.1 (nicht 10.0)

Letzte drei von sieben Verbrauchern, die exakt auf 2026.9.0 festhingen und deshalb nicht
von selbst heilen.

🔴 ABWEICHUNG VOM BRIEF, und sie ist der Kern: er nennt 2026.10.0 — publiziert ist
2026.10.1, und der Unterschied ist tragend. Changelog:
10.0 feat(capabilities): Faehigkeits-Erkennung in die Kiosk-Huelle, plus Mikrofon
10.1 fix(capabilities): enumerateDevices bekommt eine Frist — die Erkennung wurde
sonst NIE fertig
Auf 10.0 zu pinnen hiesse, das neue Feature mitsamt seinem bekannten Haenger auf drei
Kiosk-Geraeteklassen auszurollen.

PIN-FORM: exakt beibehalten. Der Brief laesst beides zu; exakt folgt der Grundregel
(Richtung A, 2026-07-23) fuer VERBRAUCHER-Repositorien — reproduzierbare Baeue mit
committetem Lock. Die Uebergangs-Regel auf Spannen gilt Bibliotheken, die FUNDAMENT
pinnen, nicht Clients, die eine Bibliothek pinnen. `clients/pos` mit Caret ist der
Ausreisser, nicht das Vorbild.

Lock-Beleg: je genau EINE Aufloesung auf 2026.10.1 in allen drei.
Wirkung gemessen: `shared-lib-drift` client-facing stale (exact pins) 4 -> 1 = Grundlinie
(der verbleibende mobile-app|communicator-Eintrag). Ratsche NICHT bewegt.
New pm

Abschnitt "Website" an den Modul- und Feature-Bildschirmen

Die Kaskade `modul-feature-vertrag-website-felder-kanon` hat den Freigabe-Schalter
geliefert — es gab aber keinen Ort, an dem ihn jemand umlegt. Diese Datei ist
feld-fuer-feld hand-kuriert; ein neues Vertragsfeld erscheint hier NICHT von
selbst. Der Schmerzpunkt der Quell-Kaskade ("ein Redakteur kann einen Eintrag
nicht zurueckhalten, ohne seinen Text zu loeschen") war damit nicht behoben,
sondern eine Schicht nach oben gewandert.

Der eigentliche Auftrag sind die HINWEISE, nicht die Felder: ein leeres Kaestchen
heisst hier NICHT "aus", sondern "keine Aussage — es gilt das Ersatz-Tor". Jeder
Hinweis nennt deshalb die Bedeutung bei LEER, und jeder ist am echten Tor belegt
(isPubliclyVisible, consumer-app), nicht am Vertragstext:
- showOnWebsite false=versteckt / true=sichtbar (gewinnt in BEIDE Richtungen)
- published:false haelt zurueck AUCH bei showOnWebsite:true
- leer MIT Beschreibung sichtbar, leer OHNE versteckt
- _websites leer = auf ALLEN Auftritten, nicht "auf keinem"
- slug leer = Rueckfall auf key (kein 404)

Der echte Bestandsfall traegt `slug:""` und `showOnWebsite:null` — also GESETZTE
leere Werte, nicht "absent". Am Tor geprueft: verhaelt sich identisch zu absent,
die Flaeche darf sie also schreiben.

Bewusst `multiSelect` statt des im Auftrag genannten `remoteSelect`+multiple: in
dieser Datei ist `multiSelect` die belegte Bauform (1x, `_components`),
`remoteSelect` kommt 0x mit `multiple` vor.

`sortOrder` nur beim Modul — das Feature fuehrt es bereits unter "Stammdaten";
ein zweites Feld auf denselben Pfad waeren zwei Bedienorte fuer einen Wert.

Vorab gemessen: das installierte models kennt alle fuenf Felder (2026.452.1) —
der lokale node_modules hing auf 451.0, wo KEINES existiert; der Lock war korrekt,
`npm ci` hat es geradegezogen. Ohne diese Pruefung haette `strict` jede Eingabe
still verworfen.

Proben 650/650 (38 Reihen), inkl. der feldweisen Spec-Struktur-Probe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New senderbrands

Sektion "Gegenzeichnung" — Bedienflaeche fuer die zwei Signatur-Felder

`ISenderBrandLetterhead.signatureImage`/`.signatureName` stehen seit der Kaskade
`arbeitgeber-gegenzeichnung-...` im Vertrag (interfaces 2026.446.0), hatten aber
KEINE Bedienflaeche: `letterhead` war in `senderbrands.edit` vollstaendig
unkuriert (gemessen: 0 Treffer fuer den Feldpfad). Ein Vertrag ohne Flaeche ist
kein lebender Zweig.

Die Hinweistexte sind am Vertrag abgelesen, nicht erfunden (`ISenderBrand.ts:193-203`):
die Marke ist der kanonische Traeger, NICHT der Mandant (CEO-Entscheid 2026-08-05
— ein Mandant fuehrt mehrere Marken, die Grafik gehoert zu der, unter der das
Schreiben hinausgeht); `ayounecustomers.signatureImage` bleibt Rueckfall fuer
Bestandsdaten, `signatureName` faellt auf `managingDirectors[0]` zurueck.

Vorab gemessen statt angenommen: das INSTALLIERTE models (2026.450.0) kennt beide
Feldpfade — ohne das haette `strict` jede Eingabe still verworfen.

Scope-Grenze eingehalten: die uebrigen elf `letterhead`-Felder (Anschrift,
Rechtsform, USt-IdNr., ...) sind bewusst NICHT mitkuriert. Am gebauten Zustand
belegt — Negativ-Kontrolle: keines von ihnen im Zustand; Positiv-Kontrolle:
beide neuen Feldpfade und "Countersignature" drin.

Proben 1130/1130 (59 Reihen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
Fix infra

redeploy-entity-consumers meldete eine stille Null aus dem falschen Verzeichnis

URSACHE, reproduziert statt vermutet: die Scan-Wurzel kam aus `process.cwd()`, und der
Verzeichnis-Durchlauf verschluckt einen fehlenden Pfad still (`catch { return }`). Aus einem
beliebigen anderen Verzeichnis gefahren lieferte der Befehl deshalb `affected: []` — bei
FUNKTIONIERENDER Registry-Abfrage und ohne jeden Hinweis.

Das ist byte-genau die Form des Befundes vom 2026-08-21 (`affected: []` bei
`published: 2026.444.0`) und beantwortet die Frage des Briefes, welche der beiden Stufen
versagt: die KANDIDATEN-Erkennung, nicht der Versions-Vergleich. Waere es der Vergleich
gewesen, haetten 24 Zeilen mit `UNKNOWN` dagestanden.

Drei Aenderungen:
- `defaultScanRoot()` nutzt `getMonorepoRoot()` (leitet aus dem EIGENEN Ort ab) statt cwd —
der kanonische Helfer, den drei andere Befehle bereits nutzen.
- `scanEntityConsumers` WIRFT, wenn unter der Wurzel keiner der vier Ordner liegt. Eine leere
Menge ist hier nie eine Entwarnung; ohne den Waechter ist eine falsche Wurzel von einem
leeren Monorepo nicht zu unterscheiden.
- Ehrliche Zaehlung in Zusammenfassung UND json: `Kandidaten: N · davon BEHIND: M · CURRENT ·
UNBEKANNT`. „0 von 0" und „0 von 24" sahen bisher gleich aus — genau dieser Unterschied war
der ganze Befund. Plus ein ausdruecklicher Hinweis, wenn die veroeffentlichte Fassung nicht
lesbar war (dann ist jeder Stand UNBEKANNT, nicht CURRENT).

Flip-Probe (test/redeploy-entity-consumers-scan-root.test.mjs, 3 Proben inkl.
Negativ-Kontrolle): vorher liefert `scanEntityConsumers(<leeres Verzeichnis>)` still `[]`,
nachher wirft es. Suite-Ebene: Grundlinie 9 Fehlschlaege, danach 8 — die 8 sind vorbestehend
und fremd (Renderer, coord-admin, Formatter), meine Datei kippt von rot auf gruen.

Ende-zu-Ende aus /tmp gefahren: 24 Kandidaten, 24 BEHIND, `custom-functions-worker` gefuehrt.
New jobs

HR_BENEFIT_RECEIPTS_MONTHLY scharf schalten (Glied C, Takt 0 6 1 * *)

Beide dokumentierten Sperrgruende sind aufgeloest, und beide waren echte
Hindernisse: (1) die Personalakte traegt die Zuordnung (IEmployee.benefits[],
Kaskade 64), bedienbar seit dem Reiter "Sachbezuege"; (2) der doc-engine-Laeufer
kennt den Verteiler-Zweig dispatch-benefit-receipts.

🔴 `defaultData: { op: "dispatch-benefit-receipts" }` ist die tragende Zeile, nicht
Beiwerk. Der Einplaner reicht woertlich `data: d.defaultData ?? {}` durch — ohne
sie kaeme beim Laeufer ein LEERER Job an, der weder `op` noch `sourceEntityType`
traegt und mit "Unsupported sourceEntityType" abgewiesen wuerde. Der Takt liefe
dann jeden Monat und erzeugte NICHTS, ohne dass etwas fehlschlaegt.

Der vorgesehene Takt `0 6 1 * *` ist unveraendert uebernommen (06:00 UTC am 1. —
frueh genug fuer die Lohnabrechnung des Vormonats); der Verteiler bescheinigt
entsprechend den VORmonat.

⚠ Heute tragen 0 von 53 Akten eine Zuordnung — der Takt laeuft zunaechst ins
Leere, und das ist die richtige Richtung: der Auftrag verbietet Erst-Zuordnungen
ausdruecklich, die pflegt der CEO selbst ueber die neue Flaeche.

Proben 60/60.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New dispatch

Verteiler-Zweig `dispatch-benefit-receipts` (Glied C)

Faechert den Monats-Auftrag HR_BENEFIT_RECEIPTS_MONTHLY in die Kohorte auf und
stellt je Treffer einen regulaeren render-employee-certificate-Job ein. Damit
entstehen die Bestaetigungen mit den ECHTEN Werten der Personalakte statt mit
Aufruf-Parametern — der Grund, warum der Takt bis zur Sachbezugs-Definition
abgeschaltet angemeldet war.

🔴 DERSELBE idempotencyKey wie der Hand-Massenlauf
(`Employees:<id>:benefit-in-kind-receipt:pdf:<period>`) — keine Kosmetik, sondern
die Zusage, dass Takt und Handgriff sich nicht doppeln. Der Hand-Weg bleibt als
Ausnahme-Werkzeug bestehen.

🔴 Ein VERTEILER, kein Renderer: der Auftrag traegt weder renderID noch
sourceEntityType, steht deshalb oben bei `stamp-signature`. Unten waere er mit
"Unsupported sourceEntityType" abgewiesen worden.

Die drei Regeln, an denen ein Fehler teuer waere, liegen in der puren Haelfte und
sind einzeln geprobt:
- Zweckbindung PFLICHT (§ 2 Abs. 1 Nr. 10 ZAG) — ohne sie ist der Gutschein eine
Geldleistung und ein Beleg mit dem Freigrenzen-Text eine FALSCHE Steuer-Aussage.
- Freigrenze (§ 8 Abs. 2 S. 11 EStG) UEBERSPRINGT, wirft nicht — ein Wurf liesse
den Monats-Lauf fuer ALLE Mandanten sterben. Genau auf 50,00 € ist noch tragbar.
- Ein FEHLENDES Datum ist offen, nicht ungueltig — sonst fiele jede Zuordnung ohne
Enddatum aus dem Takt, und das waeren nach Bauart fast alle.

Mandanten-uebergreifend (Plattform-Takt), anders als der hr-api-Hand-Lauf. Mehrere
tragbare Zuordnungen: die hoechste gewinnt, es wird NICHT summiert — die
Bestaetigung beschreibt EINE Zuwendung.

⚠ `npm run build` ist hier `tsc || exit 0` und schluckt Fehler; separat mit
`npx tsc --noEmit` geprueft (0 Fehler). Proben 281/281.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New employees

Reiter "Sachbezuege" an hr.employees.edit (Glied B)

Die Bedienflaeche zur Zuordnungs-Achse `IEmployee.benefits[]` (Fundament aus
Kaskade 64). Ohne sie traegt das Fundament, aber niemand kann eine Zuordnung
pflegen — der Vertrag war bis hierher kein lebender Zweig.

Wiederhol-Editor (`typ: 'ObjectArray'`) nach dem Muster des `wage`-Reiters, mit
`_wageType` als Referenz-Select ueber die atomare Komponente (CEO-Regel: jedes
ObjectId-Feld ist ein Auswahlfeld, nie Freitext). `showIf: NUR_MENSCH` — ein
KI-Mitarbeiter bekommt keinen Tankgutschein, und die Freigrenze des § 8 Abs. 2
S. 11 EStG setzt ein Arbeitsverhaeltnis voraus.

🔴 BEWUSSTE ABWEICHUNG VOM AUFTRAG: der Monatswert wird in CENT erfasst, nicht
"als Euro-Eingabe". Gemessen 2026-08-22 gibt es in der SDUI-Schicht keine
Cent/Euro-Umrechnung — `amountCents` kommt plattformweit nur im Lohn-Export vor,
nie als Eingabefeld, und FIELD_TYPE_MAP kennt keinen Geld-Typ. Ein als "Euro"
beschriftetes number-Feld schriebe den Wert UNVERAENDERT nach monthlyValueCents:
wer 50 tippt, haette 0,50 € hinterlegt und der Beleg wiese 0,50 € aus, ohne dass
irgendetwas fehlschlaegt. Ein Feld, das sagt was es speichert, ist richtiger als
eine vorgetaeuschte Umrechnung; die Freigrenze steht als Rechenhilfe im Hilfetext.

Der Katalog-Waechter traegt eine harte Zahl (13 -> 14, Beschreibung mitgezogen);
der Feld-Waechter (employeeEditFieldsExist) bestaetigt, dass alle benefits.*-Pfade
im Schema existieren. Proben 78/78.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New shop

verwandte Produkte am Produkt + CSS fuer den `quotes`-Block (W9)

VERWANDTE PRODUKTE — die Auftrags-Auflage war eine Messung, nicht eine Vermutung
("woraus folgt die Verwandtschaft?"). Gemessen an `IProduct` UND am Schema: sie
ist GEPFLEGT, nicht abgeleitet — fuenf Achsen als `[{product: ObjectId(ref
'Products'), name}]`. Vier davon sind Empfehlungen und werden angezeigt;
`bundleProducts` bleibt DRAUSSEN, weil eine Stueckliste ("dieses Set enthaelt X")
eine andere Frage beantwortet als "was koennte Sie noch interessieren". Der
Bestand stuetzt das: die 91 Buendel verweisen ueberwiegend auf Varianten
DESSELBEN Titels — als "verwandtes Produkt" gezeigt waere das schlicht falsch.

`isPublic: true` wirkt fail-closed (derselbe Schalter wie `getShopProduct`): ein
nicht-oeffentliches Produkt erscheint nicht, auch wenn es verknuepft ist. Ohne
den Filter waere die Empfehlungs-Achse ein Weg, Unveroeffentlichtes zu zeigen.

Diese Strecke liest LIVE — die Frage "Marker oder Publizier-Zeit" stellt sich
hier gar nicht, anders als bei den gebackenen Seiten der cms-api. Die Karte kommt
von `renderProductCard` (Wiederverwendung), nicht von einem zweiten Zeichner.

CSS FUER `quotes` — der cms-Renderer gibt `.ay-quote*` aus; ohne Regeln faellt der
Block als linksbuendiger Stapel auf die Seite. Das ist dieselbe STILLE Klasse,
die 2026-08-17 die Kundenlogos und Referenzen traf: fehlendes CSS wirft nichts
und faellt in keinem Bau auf. Die sechs Selektoren sind im Layout-Wachposten
festgenagelt, der genau dieses Auseinanderlaufen melden soll.

Proben: 10 neu (related-products.test.mjs), volle Menge 1538/1538 gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
New pages

Blocktyp `quotes` — zitierte Aussagen Dritter aus `Quotes` (hub), W9

Dritter zitat-naher Block neben `testimonial` (Ratings, ohne Sprache) und
`references` (Kundenstimmen des Mandanten). Drei Traeger, drei Quellen — keiner
ersetzt einen anderen; die Abgrenzung steht im Kanon-Kommentar.

Gemessen (nicht angenommen): `Quotes` traegt KEINEN W0-Kanon — kein `published`,
kein `slug`, kein `_websites`, kein `sortOrder`, am publizierten Vertrag UND am
Mongoose-Schema geprueft. Daraus die zwei Vertraege dieses Zweiges:

(1) Die ausdrueckliche Auswahl (`_quotes[]`) filtert nicht — zeichengleich zum
`_ratings[]`/`_faqs[]`-Weg. Wer ein Zitat in den Block legt, gibt es frei.
(2) Der Auto-Zug verlangt ein Kriterium (`_quoteCategory`/`_quoteLang`) und
fragt OHNE Kriterium die Datenbank gar nicht erst. Grund ist gemessen: bei
EINEM Mandanten liegen 48.428 importierte Fremdzitate — ein kriterienloser
Zug haette daraus zwoelf willkuerliche auf die Website gestellt, ohne dass
irgendetwas fehlschlaegt.

Publizier-Zeit statt Marker (EPIC-Regel): ein Zitat aendert sich nur, wenn ein
Mensch es aendert — kein Zeitfenster, keine wandernde Adresse.

Alle drei Wachposten bedient: KNOWN_BLOCK_TYPES 39 -> 40, die harte Zahl in
renderBlockListBlocks.test.ts, UND der Charakterisierungs-Fall in BLOCK_CASES
(den der Auftrag nicht nannte — er verlangt Markup-Belege, keine Zaehlung).

Proben: 13 neu (quotesBlock.test.ts), volle Einheiten-Menge 2312/2313. Die eine
rote ist ein Bestandsbefund (contents.test.ts, searchableFields-Vorlage) und von
dieser Arbeit unberuehrt — belegt und im bestehenden Brief nachgetragen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmMj9oQCU9nxusjcbD8Dp8
Fix wallboard

die uebrigen Aufgaben-Kacheln filtern und gruppieren auf _project

Derselbe tote Feldname wie in taskBySPE, an acht weiteren Stellen derselben Datei - alle
auf ay.models.Tasks. Nach dem pm-Praefix-Feldzug W1 (2026-08-20) tragen gemessen 30.579 von
42.392 Aufgaben _project und nur 106 den Altnamen; jede Projekt-Einengung der Wand wurde
damit still verworfen.

Am schwersten traf es taskByProject: die Kachel GRUPPIERTE auf $project, wodurch 42.286 von
42.392 Aufgaben (99,75 %) in einem einzigen Eimer _id:null landeten und als 'Projekt
geloescht' beschriftet wurden. Die Kachel war damit vollstaendig unbrauchbar.

Betroffen: buildTaskQuery (bedient 5 Test-/Review-Kacheln), gantt, taskByEmployee (2x),
lastTasks, simpleTaskBreakdown (taskByType/-Priority/-Status/-Progress), taskTestCoverage,
taskDocuCoverage, taskByProject.

NICHT angefasst - und das ist ein Messergebnis, keine Auslassung: worklogs hat die Wanderung
NICHT mitgemacht (gemessen 3.709 von 3.987 Arbeitszeiten tragen project, 0 tragen _project).
In dieser Datei wird jedoch keine Arbeitszeit nach Projekt gefiltert; die einzige
WorkLogs-Abfrage laeuft ueber buildWorkLogsQuery. Ein WorkLogs-Feldzug ist ein eigener Vorgang.

Restkontrolle: 0 Vorkommen des Altnamens in der Datei, 10 Stellen auf _project, 331 Proben gruen.
Fix wallboard

taskBySPE wendet das Zeitfenster an und filtert auf die lebenden Feldnamen

Die Kachel nahm :from/:to entgegen und benutzte KEINE der beiden Grenzen - sie zeigte
immer den gesamten Bestand und behauptete dabei, sie zeige den gewaehlten Zeitraum
(gemessen ~124.000 Dokument-Abrufe je Aufruf, 3 x 41.441). Zwei weitere Filter waren aus
demselben Grund wirkungslos und sind mitbehoben:

- project -> _project (pm-Praefix-Feldzug W1). Gemessen 2026-08-21: 30.579 von 42.392
Aufgaben tragen _project, nur 106 den Altnamen. Jede Projekt-Einengung wurde still
verworfen.
- service -> servicedesk. Das Feld 'service' existiert NICHT (0 von 42.392); der Balken
'Service' war strukturell immer 0, ohne Fehler, ohne Warnung.

Zeitachse createdAt, gemessen statt angenommen: alle fuenf Geschwister-Kacheln mit
demselben :from/:to-Vertrag filtern darauf (neue BFF-Route wie Alt-Route), createdAt ist
zu 100 % belegt (doneAt 26,5 %, due 13,2 %), und start/end sind zu 99,9 % sekundengenaue
Kopien von createdAt. Herleitung im Kopf von lib/taskStatsQuery.ts.

Fehlende, unlesbare oder vertauschte Grenzen liefern jetzt 400 statt eines stillen
Rueckfalls auf 'alles' - ebenso eine unlesbare Projekt-Kennung.

11 neue Proben, Flip-Probe beidseitig: alle drei wiederhergestellten Defekte werden von
genau der zustaendigen Probe rot gemeldet, die uebrigen 328 bleiben gruen.
Fix briefing

Bau-Skript verschluckt keine Uebersetzungsfehler mehr; drei ueberholte Begruendungen richtiggestellt

- `build`: `rimraf dist && tsc || exit 0` -> `rimraf dist && tsc`. Das `|| exit 0`
machte JEDEN Bau gruen. Beidseitig belegt: altes Skript + absichtlicher TS-Fehler
-> exit 0 (verschluckt, Fehler stand im Protokoll); neues Skript + derselbe Fehler
-> exit 2. Fehler danach zurueckgenommen, Bau exit 0, Testreihe 326/326.

- `tenantBriefings.ts` + `lib/tenantBriefing.ts`: beide Kommentare behaupteten,
`Settings.defaultmails.briefing` existiere im Schema nicht, und nannten
"94 Briefings, 0 zugestellt". Beides ueberholt. Das Feld liegt seit Kaskade 59
im Schema (installiert 2026.450.0, mit Negativ-Kontrolle belegt). Der
Umgehungs-Weg bleibt richtig, aber weil das Feld je Mandant LEER ist.

- `dailyBriefing/channels.ts`: `sendBriefingEmail` ist NICHT tot. Es hat genau
einen lebenden Aufrufer monorepo-weit - diesen - und er erzeugt alle 72
gescheiterten Zustellungen (550 5.6.0 No Sending Domain found in From header).
Ausserdem festgehalten: dieser Weg schreibt keine MailLogs-Zeile.

Kein Verhaltens-Change am Zustellweg - nur Bau-Riegel und Begruendungen.
New sitemap

Kanal-Schalter und slug-Adresse der Funktions-Achse nachziehen

Spiegel der consumer-app-Aenderung — eine Sitemap darf NIE mehr zeigen als
der Renderer. Beide Tore sind jetzt wieder Zeile fuer Zeile identisch.

published = LEBENSZYKLUS (false haelt zurueck)
showOnWebsite = KANAL (false haelt zurueck, true gibt frei)
D3 des Kanons, CEO-Entscheid 2026-08-21 "beide Felder".

ADRESSE: slug gewinnt, key traegt weiter. Eine Sitemap, die den key ausgibt,
waehrend der Renderer das slug bedient, zeigt auf eine Weiterleitung statt
auf die Seite.

Der Adressierbarkeits-Filter fragt ab jetzt nach dem SEGMENT (slug ODER key)
statt nur nach key: ein Modul mit kuratiertem slug, aber ohne Code-Bezeichner
— der Normalfall bei einem KUNDEN-Mandanten — waere sonst still aus der
Sitemap gefallen, obwohl der Renderer es ausliefert.

Kein Fundament-Bump noetig, gemessen statt angenommen: das Repositorium
deklariert nur core, sein Lock haelt models 2026.451.0 (ohne die neuen
Felder). Der Leseweg ist davon unberuehrt — mongoose reicht unbekannte
Schema-Pfade in die MongoDB-Projektion durch, und lean() hydriert nicht.
Positiv-Kontrolle: ein bekanntes Feld ist ebenfalls in der Projektion.
New funktionen

Freigabe-Schalter und slug-Adresse der Modul-Achse verdrahten

Die oeffentliche Modul- und Funktions-Achse lieh sich ihr Tor bisher aus:
gezeigt wurde, was eine kuratierte Beschreibung traegt. Das trug fail-closed,
war aber kein Vertrag — ein Redakteur konnte einen Eintrag nicht
zurueckhalten, ohne seinen Text zu loeschen.

Ab interfaces 2026.448.1 / models 2026.452.1 gibt es ZWEI echte Schalter
(D3 des Kanons, CEO-Entscheid 2026-08-21 "beide Felder"):
published = LEBENSZYKLUS der Beschreibung
showOnWebsite = KANAL (auf DIESEM Auftritt zeigen)
Beide sind hier verdrahtet. Ein Schalter, den kein Leser liest, ist ein Feld
ohne Wirkung — genau die Fehlerklasse, gegen die der Auftrag steht.

ADRESSE (Kanon R3): slug gewinnt, key traegt weiter. getPublicModule loest
ab jetzt ueber BEIDE auf — sonst waere jede bereits verlinkte key-Adresse ab
dem ersten kuratierten slug eine 404. Kanonische Adresse, hreflang und
Brotkrumen ziehen mit, sonst zeigte canonical auf eine andere Seite als die
besuchte.

DAS ERSATZ-TOR BLEIBT und das ist Absicht: 38 Module und 554 Funktionen
tragen die neuen Felder nicht, Backfill ist ausgeschlossen — ein strenges Tor
schaltete die Achse in derselben Sekunde dunkel. Es zu entfernen, sobald der
Katalog befuellt ist, ist ein eigener Vorgang.

Probe test/module-feature-website-gate.test.mjs (11 Faelle, je mit Gegenprobe).
Flip-Probe gefahren: Kanal-Veto ausgehebelt => 1 rot, wiederhergestellt => 11
gruen; sie bestaetigt sich also nicht selbst. Volle Reihe 1522/1522
(ohne die Aenderung 1511/1511 — die Differenz sind genau die neuen Proben).
New ledger

siebter Status covered-by-template + Nenner-Trennung

CEO-Entscheid 2026-08-21 (Weg b): der Seiten-Ausgang "geht in einer
Dashboard-Vorlage auf" bekommt einen eigenen Status statt der
Notloesung blocked+Feld.

- STATUSES 6 -> 7, COVERED_BY_TEMPLATE_CUTOVER + SIDE_EXIT_STATUSES
- isOpenWork als EINE Definition von "offen" (pickNext + summarizeCounts
lesen sie beide, koennen also nicht auseinanderlaufen)
- summarizeCounts: neue open/pctOpen/sideExit/coveredByTemplate;
total und pctVerifiedImproved BEWUSST unveraendert -> keine stille
Nenner-Verschiebung, die Quote bleibt ueber den Tag hinweg vergleichbar
- STATUS_PROGRESS_RANK umnummeriert, neuer Wert ueber blocked/unter
compatible - ohne Rang faellt er ueber ?? 0 auf pending und verloere
still jeden Zwillings-Vergleich
- renderMd: Bruch-Warnung wie DEDUP_CUTOVER, Modul-Spalten aus STATUSES
statt hart verdrahtet, eigene offen-Zeile
- cmdAudit gegen SIDE_EXIT_STATUSES, Kennzahl-Namen tragen die
gemessene Groesse (_ohneSeitenausgang statt _notWontfix)
- Testdatei umgedreht: die Probe "Status-Satz wird NICHT erweitert" ist
ueberholt und belegt jetzt das Gegenteil, mit Positiv-Kontrollen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MnH17sGDL6zTAvv1cbcKFS
Fix module-feature

_websites braucht default:undefined, sonst materialisiert die Liste als []

Die Umkehrprobe am Persistat hat es gefangen, der Bau nicht: eine Mongoose-
Liste materialisiert sich IMMER als []. Ohne default: undefined waere
_websites: [] beim naechsten Speichern in alle 38 Module und 554 Features
gewandert — genau der Vorgabewert, den der Auftrag ausdruecklich verbietet,
und er machte "nie entschieden" von "ausdruecklich mandanten-global"
ununterscheidbar.

Dieselbe Fehlerklasse wie quality.flags und IEmployee.benefits (Kaskade 64).

Hydrierung jetzt 20/20 gruen, inkl. Negativ-Kontrolle (erfundenes Feld faellt,
strict greift also wirklich) und Umkehrprobe am Persistat.
New module-feature

Schema-Zwillinge fuer die W0-Kanon-Website-Felder

Modules += slug, _websites[], published, showOnWebsite, sortOrder
Features += slug, _websites[], published, showOnWebsite

Ohne die Zwillinge verwirft `strict` jede Eingabe STILL — der Vertrag traege
die Felder, gespeichert wuerde nichts, und kein Fehler erschiene.

Flip-Probe beidseitig gefahren: gegen interfaces 2026.447.1 bricht der Bau mit
2x TS2353 (Features.ts:53, Modules.ts:58), gegen 2026.448.1 mit 0 Fehlern.
Beide .add()-Bloecke sind also wirklich typgeprueft (der Konstruktor traegt
den Typ-Parameter).

ZWEI Schalter, nicht einer — D3 des Kanons, CEO-Entscheid 2026-08-21
"beide Felder": published = LEBENSZYKLUS, showOnWebsite = KANAL.

KEIN Default, KEIN Backfill bei keinem der Felder: ein Vorgabewert wanderte
beim naechsten Speichern in alle 38 Module bzw. 554 Features.

Zwei Lesepfad-Indexe je Schema deklariert (Bauform wie Awards). BEWUSST nicht
unique: der Kanon nennt {_customerID, _websites, slug} als Schluessel, und
_websites ist eine LISTE — ein unique-Index darueber traefe je Eintrag statt je
Kombination. Gleiche Lage wie Awards/Partners.
Fix alerts

Laerm-Drossel las den Kill-Schalter falsch — value statt isEnabled

`Flags.isEnabled` beantwortet NICHT „steht der Schalter auf an?", sondern „gibt
es das Merkmal und ist es nicht stale/archived?" — `flag.value` kommt in seiner
Auswertung gar nicht vor. Ein Kill-Schalter mit dem Wert "false" galt damit als
EINGESCHALTET, allein weil jemand ihn angelegt hat.

Gemessener Schaden: beim Mandanten tolinax UG steht
`config.wallboardAlertThrottle.disabled` seit dem 2026-07-14T19:52 auf
value:"false", und die Beschreibung des Schalters sagt woertlich „REVERT des
Notnagels … auf false … wird nicht mehr gebraucht". Der Rueckbau ist NIE
wirksam geworden — die Drossel war fuenfeinhalb Wochen aus. Unbemerkt, weil ein
erneut klingelnder Dauerfehler an einer Wandanzeige wie normales Alarmieren
aussieht. Belegt am 2026-08-21: dieselbe Signatur klingelte in zehn Minuten
16-mal, waehrend zehn andere Alarme im selben Fenster je genau einmal klingelten.

Der Beleg stand in derselben Datei: die Fenster-Groesse liest `getValue`, und der
ENV-Zweig parst 'true'|'1'|'yes'. Der WERT war immer gemeint.

Rueckfall bewusst beibehalten: ohne hinterlegten Wert entscheidet weiterhin
`isEnabled`, ein wertloses Merkmal aendert sein Verhalten also nicht.

5 Proben (249 statt 244), Flip auf `Boolean(raw)` — den Originalfehler — beisst.
Fix keyword-planner

raise BullMQ lockDuration to survive long domain jobs

Domain jobs upsert up to 10,000 keywords sequentially (2 DB round-trips
each) plus Google Ads quota-retry backoff, routinely exceeding the 30s
BullMQ default lockDuration. Once the lock expires mid-job, stalled-job
recovery reassigns the job and the original process's later
moveToFinished races the reassignment, surfacing as "could not renew
lock", "Missing key for job ... moveToFinished" and "Lock mismatch"
errors (dozens/week in the Errors-as-Tasks firehose). Raise lockDuration
to 10 minutes on both keyword-planner workers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XC3R3yf9u7xnZneEKnU8w
Fix module-feature

Vertrag sagt die UEBERGANGS-Semantik der Schalter, nicht die Ziel-Semantik

Der 448.0-Kommentar behauptete "absent = auf der Website nicht zeigen". Der
Leser kann das heute nicht tun: 38 Module und 554 Funktionen tragen das Feld
nicht, und Backfill ist ausdruecklich ausgeschlossen — ein strenges Tor
schaltete die Achse in derselben Sekunde dunkel.

Richtig ist: absent = KEINE Aussage, Rueckfall auf das bestehende Ersatz-Tor
("traegt eine kuratierte Beschreibung", selbst fail-closed); GESETZT gewinnt
das Feld in beide Richtungen. Die Ziel-Semantik (absent = nicht zeigen) ist
als solche benannt und braucht einen eigenen Vorgang.

Ein Vertrag, der etwas anderes behauptet als sein Leser tut, ist die teuerste
Sorte Kommentar — er bescheinigt sich selbst.