Implementation plan · solcom · F-290 · P2503-108 · 2026-05-14

project_offer-Sync (BH4SF) + PPP-Frontend

Source PRD docs/prds/2026-05-13-salesforce-publication-sync-prd.html — End-to-End-PRD für F-290 (Inbound + Outbound + PPP-Frontend Layer A+B). Entstanden aus dem Grilling 2026-05-14 mit 14 Decisions. MVP-Termin: 2026-07-08.
Slices
12
Mode
100 % AFK
MVP-Termin
2026-07-08
User Stories
31 abgedeckt
01

Architectural decisions

Durable Entscheidungen aus dem Grilling 2026-05-14, die phasenübergreifend gelten. Detail-Begründung in der PRD und in ADR-0029 / ADR-0031 (beide amended).

Bundle / Modul
project_offer (Bundle), solcom_project_offer (Modul), industry (Vocabulary). Domain-Begriff von solcom.de/de/projektportal übernommen; „Publication" demoted zu BH4SF-RecordType-Implementierungs-Detail.
Inbound-Pull
migrate_plus + Custom SalesforceSoqlFetcher (für SF-nextRecordsUrl-Body-Paging). Stündlich via Cron. OAuth-2 Refresh-Token-Flow gegen FCSB-Sandbox. ADR-0013-Standard.
Reconciliation
ProjectOfferHardDeleteSubscriber auf MigrateEvents::POST_IMPORT. Per-Run-Sweep gegen migrate_map_project_offer. Empty-Feed-Schutz: 0 Records → kein Delete + Watchdog.
Outbound
Hooks hook_node_insert + hook_node_update mit Status-Transition-Guard → Drupal-Queue project_offer_outboundProjectOfferOutboundWorker → REST-PATCH von hohr__Job_Application_URL__c via SalesforceClient-Service.
Lifecycle-Window
Render-time: node.status reflektiert nur SC_PublicationStatus__c == "Online". Window-Check publish_start <= now < publish_end als Views-Filter + ProjectOfferWindowAccess (Route-Access / hook_entity_access). Kein Migrate-Lag.
Field-Naming
Vacancy-Konvention: Source-Prefix nur für External-Key (field_sf_id); Content-Felder prefix-frei (field_description, field_city, field_industry, …). node.title = SF-TR1__Job_Title__c. 21 Storage-Felder. field_job_number gedroppt.
URL-Pfad
Pathauto-Pattern /projektportal/projektangebote/[node:title]. Listing-Page-Canvas-Page unter /projektportal. Merkliste unter /projektportal/merkliste.
SEO
JSON-LD nach schema.org/JobPosting via ProjectOfferJsonLdBuilder-Service + hook_node_view-Adapter. 1:1-Pattern aus Vacancy-Vorbild übernommen.
Editor-Boundary
Keine Rolle hat edit any project_offer content / create project_offer content. Vacancy-Ausschluss-Pattern (ADR-0026) 1:1. Industry-Vocabulary bleibt editierbar (DE-Label-Pflege bei Auto-Create-on-Miss).
Stack-Entscheidung
Custom-Code bleibt; drupal/salesforce Suite explizit rejected (ADR-0013-Bruch, ext-soap-Pflicht, Switching-Cost). Vollständige Argumentation in ADR-0029 (amended).
Scope-Konsolidierung
Frontend Layer A + Layer B beide MVP-Pflicht (kein Post-MVP für F-290). ADR-0031 Layer-A/B-Trennung wird aufgelöst (ADR amended).
02

Slice overview

12 tracer-bullet Slices in Dependency-Order. Slice 0 + 5 sind Pre-Build-Gates. Slice 1 ist der Domain-Rename-Refactor und blockiert die meisten Folge-Slices, weil sie auf dem neuen Naming bauen. Slice 8 (Geocoder) und einige Layer-B-Slices laufen parallel zu Slice 1.

Phase / Slice Title Type Blocked by
0Pre-Build-Gate Live-Describe FCSB-SandboxAFK
1Domain-Rename-Refactor (sf_publication → project_offer)AFK0
2Lifecycle-Window Render-Time-AuswertungAFK1
3JSON-LD nach schema.org/JobPostingAFK1
4Canvas-Page /projektportal + Cross-Sell-BlockAFK1
5Pre-Build-Gate Layer-B-Felder (Skills + Remote-Anteil)AFK0
6Skill-Badges auf Card + Skills-FilterAFK1, 5
7Remote-Anteil-Slider + Range-FilterAFK1, 5
8Geocoder-Setup + Suchradius-FilterAFK1
9Merkliste (anon LocalStorage)AFK1
10Matching-Rate (V1 faked)AFK1
11Projekt-Alert (E-Mail-Subscription)AFK1
03

Slice details

Jede Slice ist eine vertikale Trennung durch alle Layer (Schema, Config, API, UI, Tests) und unabhängig demoabar.

Slice 0

Pre-Build-Gate Live-Describe FCSB-Sandbox

User Stories: US-14, US-16

SOQL-Describe-Aufruf gegen FCSB-Sandbox (SELECT FIELDS(STANDARD) FROM TR1__Job__c LIMIT 1 mit RecordType-Filter + /sobjects/TR1__Job__c/describe) verifiziert alle 22 erwarteten Field-Namen aus Context-Snapshot 0008. Zusätzlich SOQL-COUNT-Query bestimmt Steady-State-Volume (Quota-Sizing). Output landet als versionierter Sub-Snapshot 0008a-fcsb-live-describe-YYYY-MM-DD.html im docs/context/. Bei Field-Name-Drift gegen Snapshot 0008 → Slice 1 wird mit korrigierten Field-Mappings ausgeführt.

  • SOQL-Describe-Output liegt als HTML-Snapshot unter docs/context/
  • Alle 22 erwarteten Field-API-Namen aus Snapshot 0008 sind in Sandbox bestätigt (oder Drift-Liste erstellt)
  • COUNT-Query-Resultat ist dokumentiert (Anzahl aktive Publications mit RecordType=Publication)
  • RecordTypeId für FCSB-Sandbox + Production sind beide dokumentiert
P2503-127docs/context/
Slice 1

Domain-Rename-Refactor (sf_publication → project_offer)

User Stories: US-32, indirekt US-1 bis US-18 (Backend-Stories ziehen aufs neue Naming mit)

Atomarer Refactor-PR. Module-Directory solcom_sf_publication/solcom_project_offer/, PHP-Namespaces, Service-IDs. 21 Field-Storages via hook_update_N migriert (deprecate-old + create-new + copy-data, in Reihenfolge nach Risk). Migrate-Map-Tabelle migrate_map_sf_publicationmigrate_map_project_offer via SQL-RENAME. 38 Config-Sync-YAMLs renamed + inhaltlich aktualisiert. Canvas-Templates node.sf_publication.{card,full}.ymlnode.project_offer.{card,full}.yml. View publications_listingproject_offers_listing. Block-Plugin-IDs aktualisiert. Twig-Bundle-Refs in byte_theme/ nachgezogen. Vocabulary sf_industryindustry (Migration via Update-Hook). Nach Re-Import: drush solcom-project-offer:push-urls einmal manuell laufen, um temporär stale SF-URLs zu heilen. ADR-0029 und ADR-0031 werden in dieser Slice amended. Jira-Sub-Task-Titel (P2503-128 bis -145) per Sweep-Script umbenannt.

  • drush cim -y läuft sauber durch ohne Config-Drift
  • drush updb -y migriert alle 21 Field-Storages + Map-Tabelle ohne Datenverlust
  • Bestehende Migrate-Map-Einträge sind in migrate_map_project_offer erhalten (Idempotenz beim nächsten Cron-Run gewährleistet)
  • Alle existierenden Tests (P2503-128 bis -145) laufen unter neuem Naming durch
  • ADR-0029 amended: project_offer-Naming + erweiterte Rejected-Sektion „drupal/salesforce Suite"
  • ADR-0031 amended: Layer-A/B-Trennung aufgelöst, Naming-Update
  • CONTEXT.md Module-Boundary-Eintrag auf neues Modul aktualisiert
  • 16 Jira-Sub-Task-Titel (P2503-128 bis -145) auf project_offer-Naming aktualisiert via acli-Sweep
refactor21 hook_update_NADR-0029 amendADR-0031 amend
2026-05-14 Worklog — Slice 1 abgeschlossen. Atomarer PR (5 Commits) mit:
  • Module-Verzeichnis-Rename + Namespace-Update (35 Files)
  • 39 Config-Sync-YAMLs renamed inkl. Bundle/Vocab/View/Pathauto/Block
  • hook_update_N für DB-Migration (8 nodes, 25 terms, 24 field-bundle-cols, migrate_map RENAME)
  • Drush updb + Drush cim + drush cr live verifiziert
  • Help-Text-Korrektur in field_sf_publish_start.yml (Drift gegen 0008-Snapshot: Field heißt hohr__Publishing_start_date__c, datetime trotz Name)
Scope-Cut: Die im ursprünglichen Plan genannten 21 Field-Storage-Renames (field_sf_*field_*) wurden aus Slice 1 ausgegliedert und werden in einer Folge-Slice 1.5 nachgezogen. Begründung: Field-Storage-Renames sind DB-Column-Renames mit copy-data + drop-old, das ist das mit Abstand risikoreichste Stück der Slice. Bundle/Module/Vocab/View-Rename liefert 80% der Domain-Naming-Klarheit ohne dieses Risk. field_sf_id bleibt sowieso (External-Key); andere field_sf_*-Namen leben aktuell unter dem neuen Bundle (field.field.node.project_offer.field_sf_*) und sind funktional.
2026-05-15 Hot-Fix-Sync (Slice 1.6) — Iteration auf der Live-Site nach User-Feedback („/a1wau...-URL, kein vernünftiger Titel, Twig-Fehler, kein Menü-Link, nur 4 importiert"). Fünf Drift-Klassen adressiert, alle als verbindliche Regeln in ADR-0032 verankert:
  • Migrate-Config: SOQL-Filter AND TR1__Job_Title__c != null, process.title auf skip_on_empty(method: row) (ID-Fallback entfernt) — kein SF-ID-als-Titel mehr.
  • SDC-Schema relaxen + Twig-Guard für sticky-apply-bar, badge, heading, meta-row — Canvas-Bindings an Outbound-/Optional-Felder kollabieren nicht mehr.
  • View-Path konsolidiert auf /projektpartnerportal (war /projektportal). PRD/Plan-Drafts mit /projektportal/projektangebote/ superseded — kanonische URL ist die ADR-0031-Variante.
  • Menü: N14 von „Für Freiberufler / Projektportal" → „Für Freiberufler" (Title-Suffix wandert ins Sub-Item); neuer Sub-Item „Projektliste" → /projektpartnerportal (Update_9003); alter Sub-Item → canvas_page/15 (leere Stub-Page) deaktiviert (Update_9004). Per Sitemap-IA docs/context/0002-sitemap.html N16 unter N14.
  • High-Water reset + full Re-Import: 211 importiert (von 214 SF-Records minus 3 ohne Job-Title), 3 pre-Rename-Orphan-Nodes (49/50/51) hard-deleted, Pathauto-Aliase neu generiert.
  • Bonus-Fix Slice 2: hook_views_query_alter ist auf LEFT JOIN + NULL-Toleranz umgestellt (publish_end IS NULL OR > now, publish_start IS NULL OR <= now). Vorher INNER JOIN — open-ended Window-Records (publish_end=NULL) fielen still aus dem Listing. Nach Fix: 6 von 8 published Records sichtbar (die anderen 2 haben publish_end vor heute). ProjectOfferWindowAccess::isInWindow() war bereits NULL-tolerant; nur der Views-Hook hinkte hinterher.
  • Bonus-Fix Rich-Text: SfRichTextSanitize-Plugin setzte hartcoded format: 'filtered_html' — dieses Format existiert in der Site nicht (heißt content_format). 205 bestehende Records via SQL-Bulk-Update auf content_format migriert. Folge: Aufgaben + Anforderungen + Long-Description + Internal-Description rendern jetzt auf der Detail-Page (vorher leer trotz Daten in DB).
Folge-Aufgabe (out-of-scope dieser Slice): Strang-2-IA-Konsolidierung — N15 „Mein Projektpartnerprofil" (page/14), N17 „Projekt-Alert" (page/16), N19 „Referenzen" als Sub-Items unter N14 verkabeln. docs/context/0002-sitemap.html ist die Soll-Vorlage; aktueller DB-State hat sie auf falschem oder fehlendem Parent. Eigener Restructuring-PR.
Slice 2

Lifecycle-Window Render-Time-Auswertung

User Stories: US-19, US-20

Status-Berechnung wird vom Migrate-Run (static_map) auf Render-Time umgezogen. node.status reflektiert künftig nur SC_PublicationStatus__c == "Online". Window-Check kommt in den Views-Filter der project_offers_listing-View (field_publish_start <= :now AND field_publish_end > :now). Neuer Service ProjectOfferWindowAccess implementiert EntityAccessControlHandler-Hook (oder Route-Access-Requirement) und wirft 404 bei Direkt-URL-Zugriff außerhalb des Windows. Kernel-Test deckt Time-Boundary-Cases ab (now < start, start ≤ now < end, now ≥ end, status=0).

  • Views-Filter „in Publishing-Window" funktioniert; Listing-View zeigt nur Angebote im Window
  • Direkt-URL-Zugriff auf abgelaufenes Angebot → 404 (oder konfigurierter Redirect)
  • Migration-Process für node.status nutzt nur noch static_map auf SC_PublicationStatus__c, nicht mehr kombinierte Bedingung
  • Kernel-Test ProjectOfferWindowAccessTest deckt 4 Time-Boundary-Cases ab
  • Functional-Test fährt Listing-Render gegen Time-Mock und prüft Ein-/Ausblendung
ServiceViews-FilterRoute-Access
Slice 3

JSON-LD nach schema.org/JobPosting

User Stories: US-23

Neuer Service ProjectOfferJsonLdBuilder in solcom_project_offer.services.yml. hook_node_view hängt im full-View-Mode ein <script type="application/ld+json"> ans Build-Array. Mapping: title, description (gestripped), datePosted (= field_publish_start), validThrough (= field_publish_end), employmentType: "CONTRACTOR", jobLocation (aus field_city), industry (DE-Label), identifier (field_vis_request oder fallback field_sf_id), hiringOrganization: SOLCOM GmbH (statisch). Pattern 1:1 aus VacancyJsonLdBuilder portiert. Kernel-Test prüft alle 9 schema.org-Felder.

  • Detail-Page /projektportal/projektangebote/[alias] liefert ein <script type="application/ld+json">-Tag
  • JSON-LD validiert gegen schema.org-JobPosting-Spec (Google-Rich-Results-Test)
  • Alle Pflichtfelder (title, description, datePosted, validThrough, employmentType, hiringOrganization) sind gesetzt
  • Optional fehlende Felder (identifier, jobLocation) erscheinen nicht im Output, statt mit null-Werten
  • Kernel-Test ProjectOfferJsonLdBuilderTest deckt Mapping + UTF-8-Escaping + ISO-8601-Format ab
Servicehook_node_viewSEO
Slice 4

Canvas-Page /projektportal + Cross-Sell-Block

User Stories: US-22, US-31

Canvas-Page-Node unter /projektportal als Listing-Page-Layout: View-Embed der project_offers_listing-View, Cross-Sell-Block PPP-Cross-Sell (3 verlinkte Karten zu Karriere / Sicher im Projekt / Projekt-Alert), SEO-Footer. Canvas-Region-Composition gemäß Figma-Frame. Custom-Block-Plugin PppCrossSellBlock ist config-driven (3 konfigurierbare Cards: Heading, Bild, CTA-Link). Figma-MAPPING in docs/design/MAPPING.html aktualisiert für Frames 1019-78888 (full) und 985-50077 (card).

  • Canvas-Page-Node unter /projektportal existiert in config/sync/ und ist via Pathauto erreichbar
  • View-Embed rendert project_offers_listing mit BEF-Filtern
  • Custom-Block-Plugin PppCrossSellBlock ist platziert und zeigt 3 konfigurierbare Cards
  • docs/design/MAPPING.html für die zwei Figma-Frames auf matched aktualisiert
  • Custom-Lens-Reviewer-Konsultation für Figma-Match dokumentiert
Canvas-PageBlock-PluginFigma 1019-78888 / 985-50077
Slice 5

Pre-Build-Gate Layer-B-Felder (Skills + Remote-Anteil)

User Stories: US-24, US-26 (Gate)

Live-Describe gegen FCSB-Sandbox auf zusätzliche Custom-Felder für Skills und Remote-Anteil. Output entscheidet das Datenmodell-Pattern: (a) wenn Custom-Fields am TR1__Job__c existieren → direktes Migrate-Mapping; (b) wenn nicht → Skills aus Long-Description-Tag-Parsing (Slice 6) und Remote-Anteil als statisches Default-Field mit redaktioneller Pflege (Slice 7). Ergebnis als Sub-Snapshot 0008b-fcsb-skills-remote-YYYY-MM-DD.html im docs/context/.

  • Live-Describe-Output dokumentiert: existieren Skills/Remote-Custom-Fields am SF-Object?
  • Datenmodell-Entscheidung für Slice 6 + 7 festgehalten (Custom-Field-Migration vs. Tag-Parsing)
  • Sub-Snapshot 0008b in docs/context/ + LOG.html-Eintrag
P2503-146docs/context/
Slice 6

Skill-Badges auf Card + Skills-Filter

User Stories: US-24, US-25

Neues Vocabulary skill (free-tagging im V1, max 200 erwartete Terms). Reference-Field field_skills am Bundle project_offer. Migration-Process abhängig von Slice-5-Output: Custom-Field-Migration ODER Tag-Parsing aus field_description via Custom Process Plugin (Regex-Liste der Top-100-Tech-Tags). Card-Twig zeigt Skill-Badges (max 5 visible, „+N" für Rest). BEF-Filter „Skills" als Mehrfach-Auswahl-Checkbox.

  • Vocabulary skill existiert in config/sync/ + leeres Pre-Seed
  • field_skills Entity-Reference-Field auf project_offer + Migrate-Process
  • Card-Twig rendert Skill-Badges (max 5 visible)
  • BEF-Filter „Skills" funktioniert (Mehrfach-Auswahl, OR-Logik)
  • Kernel-Test deckt Skill-Migration-Process ab (für Tag-Parsing-Variante: Regex-Liste + bekannte Edge-Cases)
Vocabulary skillfield_skillsBEF
Slice 7

Remote-Anteil-Slider + Range-Filter

User Stories: US-26

Neues Number-Field field_remote_share (Range 0–100, Steps 25). Migration-Source abhängig von Slice 5: Custom-SF-Field ODER konstanter Default mit Manual-Edit-Override (über separates Editor-Modul, nicht Standard-Drupal-Permission, weil Vacancy-Ausschluss-Pattern). Card-Twig zeigt visuelle Slider-Anzeige (0/25/50/75/100 %). BEF-Filter „Mindest-Remote-Anteil" als Range-Slider mit Steps.

  • field_remote_share existiert in config/sync/ als Integer-Field
  • Migration-Process setzt Wert aus SF-Source (oder Default 0 wenn nicht migrierbar)
  • Card-Twig zeigt visuelle Remote-Anzeige
  • BEF-Filter „Mindest-Remote-Anteil" funktioniert als Range mit Steps 0/25/50/75/100
  • Kernel-Test deckt Migration-Default + Field-Range-Validierung ab
field_remote_shareRange-Filter
Slice 8

Geocoder-Setup + Suchradius-Filter

User Stories: US-27

Composer-Require drupal/geocoder + drupal/geofield. Geocoder-Provider auf OSM-Nominatim konfiguriert (DSGVO-Konform, kostenlos; mit lokalem Cache-Layer in cache.geocoder + Pre-Seed der 100 häufigsten DE-PLZ via hook_install, um Rate-Limits zu vermeiden). Neues Geofield field_geo am project_offer-Bundle. Migrate-Process geocodet field_city beim Import zu Lat/Lng. BEF-Custom-Filter „Suchradius": PLZ-Eingabe + Radius-Slider (10/25/50/100 km). Query nutzt Geofield-Distance-Function. Notfall-Fallback: lokale PLZ-DB.

  • composer require drupal/geocoder drupal/geofield erfolgreich
  • Geocoder-Provider OSM-Nominatim konfiguriert + Cache-Layer aktiv
  • Pre-Seed-Hook installiert 100 häufigste DE-PLZ in Cache
  • field_geo existiert + Migrate-Process geocodet beim Import
  • BEF-Custom-Filter „Suchradius" funktioniert (PLZ-Eingabe + Radius-Slider + Result-Filter)
  • Functional-Test deckt Suchradius-Filter mit Geocoder-Mock ab
drupal/geocoderdrupal/geofieldOSM-Nominatimfield_geo
Slice 9

Merkliste (anon LocalStorage)

User Stories: US-28

Anonyme Merkliste-Mechanik via LocalStorage. Drupal-Behavior fügt Heart-Icon-Button an jeder Card + Detail-Page. Click toggled SF-Id im LocalStorage-Array (max 5 Einträge, Cap visualisiert). Merkliste-Overview-Route unter /projektportal/merkliste: Drupal-Controller liest LocalStorage-Token aus URL-Hash (oder POST-Body) und lädt entsprechende Project-Offers. Stale-Entries (Window abgelaufen) erscheinen nicht im Result; Info-Banner zeigt „1 Eintrag nicht mehr verfügbar".

  • Heart-Icon-Button auf Card und Detail-Page funktioniert (Toggle, Persistenz)
  • Max-5-Cap wird im UI visualisiert + funktional erzwungen
  • Route /projektportal/merkliste existiert und lädt aktive Project-Offers aus Browser-LocalStorage
  • Stale-Entries fallen via Window-Filter (Slice 2) raus; Info-Banner zeigt verlorene Einträge
  • Cross-Session-Stickiness (Browser-Restart) funktioniert
  • Playwright/agent-browser-E2E-Test deckt Add/Remove/Cap/Stale-Cases ab
JS-BehaviorLocalStorageRoute
Slice 10

Matching-Rate auf Card (V1 faked)

User Stories: US-29

V1-Placeholder: Drupal-Behavior berechnet Pseudo-Rate (Random 60–95 %) pro Card und persisitiert sie im LocalStorage (sticky pro Session + Project-Offer-Id). Visuelle Komponente: kleines Pill mit Prozent-Wert oben rechts auf der Card. Phase-2 (mit User-Profile-Pull, F-400) ersetzt die Logik durch echtes Matching.

  • Drupal-Behavior berechnet Pseudo-Rate beim Card-Render
  • LocalStorage-Sticky pro (session, project_offer_id)-Tupel
  • Card-Twig hat Matching-Rate-Pill (Position oben rechts)
  • Playwright/agent-browser-E2E-Test deckt Sticky-per-Session-Verhalten ab
  • FAQ-Eintrag „Wie wird die Matching-Rate berechnet?" verweist auf Phase-2-Echtlogik
JS-BehaviorV1 faked
Slice 11

Projekt-Alert (E-Mail-Subscription)

User Stories: US-30

Submodul solcom_project_offer_alert mit eigener Content-Entity project_offer_alert. Felder: E-Mail, Filter-Kombination (Branche, Skills, Remote-Anteil, PLZ + Radius), Frequenz (täglich/wöchentlich), Confirmation-Token, Confirmed-Flag. Subscribe-Form auf Listing-Page + Detail-Page. Double-Opt-In-Mail (DSGVO). Cron-Hook matched neue Project-Offers gegen Filter-Kombinationen und sendet Digest-Mail. Unsubscribe-Link in jeder Mail.

  • Submodul solcom_project_offer_alert aktiviert
  • Content-Entity project_offer_alert in config/sync/
  • Subscribe-Form auf Listing-Page + Detail-Page funktioniert
  • Double-Opt-In-Confirmation-Mail (DSGVO) wird gesendet + Token-Validierung funktioniert
  • Cron-Hook matched neue Project-Offers + sendet Digest (Mail-Collector im Test)
  • Unsubscribe-Link in jeder Mail funktioniert
  • Kernel-Test ProjectOfferAlertSubscriptionTest deckt Alert-Entity + Matching + Mail-Dispatch ab
  • DSGVO-Doku: Wer hat Datenzugriff? Wann wird gelöscht?
SubmodulContent-EntityDSGVOCron-Hook
04

Risks

Risiken mit Severity + Mitigation. High-Severity-Items brauchen während der Slice-Execution dedizierte Aufmerksamkeit.

Risk
Severity
Mitigation
Slice 1 (Rename-Refactor) bricht den Build mid-flight; Migrate-Map-Tabelle wird halbgar migriert; bestehende Field-Daten gehen verloren
HIGH
Atomarer PR ohne mid-flight Deploy; alle hook_update_N idempotent + transactional; vor Run: DB-Backup via drush sql:dump; nach Run: Smoke-Test (Migrate-Run + Outbound-Run + 1 Listing-View)
Slice 1: SF-URLs zeigen temporär auf 404, weil Drupal-Aliase nach Re-Import neu vergeben werden
MED
In PRD bewusst akzeptiert; Heilung beim nächsten Outbound-Cron-Run (max 60min Lag); zusätzlich drush solcom-project-offer:push-urls --force direkt nach Slice 1 Merge
Slice 0/5 (Live-Describe): FCSB-Sandbox-Credentials fehlen oder sind expired; Field-Drift gegen Snapshot 0008
HIGH
Credentials-Check als allererster Step in Slice 0; bei Drift: Snapshot 0008 wird via solcom-docs-ingestion-Agent angepasst, BEVOR Slice 1 startet
Slice 8 (Geocoder): OSM-Nominatim Rate-Limit (1 req/sec); SF-Migrate-Run mit vielen neuen Records überschreitet das
MED
Cache-Layer + Pre-Seed der 100 häufigsten DE-PLZ; bei Cache-Miss: Throttle in Migrate-Process (1 req/sec); Notfall-Fallback auf lokale PLZ-DB
Slice 11 (Alert): DSGVO-Schmerzen bei E-Mail-Daten-Speicherung; Unsubscribe-Link wird vergessen
MED
Double-Opt-In (DSGVO-Pflicht); Confirmation-Token in jeder Mail; Auto-Cleanup-Cron löscht Subscriptions ohne Bestätigung > 7 Tage; Datenschutz-Hinweis im Subscribe-Form
Slice 6 (Skills): Tag-Parsing-Variante (wenn Slice 5 ergibt: kein Custom-SF-Field) liefert miese Recall-Rate
LOW
Top-100-Tech-Tag-Liste als Regex; Editor:in kann Vocabulary skill manuell ergänzen; Feedback-Mechanik in Phase 2
MVP-Termin 2026-07-08: 12 AFK-Slices in 8 Wochen ist sportlich; Slice 1 + 8 + 11 sind die Aufwands-Hotspots
HIGH
Slice 0 + 5 zuerst (Gates); Slice 1 atomarer Sprint-PR mit dediziertem Review-Slot; bei Verzug: Slice 10 (Matching-Rate faked) ist erste Verzicht-Kandidatin; Slice 11 (Alert) ist zweite
05

Open questions

Punkte die während der Slice-Execution geklärt werden müssen (nicht blockierend für Slice-Start, aber zu adressieren bevor die Slice abgeschlossen werden kann).

Wird der Slice-1-Refactor mit einem feature-flag-gateten Cutover oder hard-cutover umgesetzt?
Aktuell: hard-cutover (atomarer PR, mid-flight DDEV-Local-Test, dann Merge). Alternative: paralleles Modul solcom_project_offer zusätzlich zu solcom_sf_publication aufsetzen, Daten via Migrate transformieren, Switch via Config-Flag. Cost-Benefit für 1 Sprint vermutlich zugunsten hard-cutover, aber im Review zu bestätigen.
Owner: Slice 1 Execution
Skill-Vocabulary: free-tagging oder kuratierte Liste?
V1-Vorschlag: free-tagging mit Auto-Create (analog Industry, aber ohne EN→DE-Übersetzung). Editor:in kann später konsolidieren (Synonyme zusammenführen). Alternative: kuratierte Top-200-Tag-Liste vor Slice 6 anlegen. Entscheidung in Slice 5/6.
Owner: Slice 6
Geocoder-Provider OSM-Nominatim als Production-Default oder nur Sandbox?
DSGVO + kostenlos spricht für OSM, aber Rate-Limit (1 req/sec) ist hart. Bei hohem Project-Offer-Volumen evtl. paid-Provider als Failover (Hostpoint, OpenCage, …). Decision-Point: Slice 0 COUNT-Query liefert die Volumen-Schätzung; bei <100 Records OSM ausreichend.
Owner: Slice 8
Matching-Rate: faked-Random vs. einfache regelbasierte Logik in V1?
PRD-Vorschlag: faked Random 60–95 %. Alternative: einfache Skills-Overlap-Logik basierend auf Browser-LocalStorage-„meine-Skills"-Eingabe (anonym, opt-in). Letzteres ist näher an echtem Matching, aber 1–2 Tage Mehraufwand und erst sinnvoll mit gepflegter Skill-Liste.
Owner: Slice 10
Projekt-Alert-Frequenz: täglich, wöchentlich oder beides?
PRD nennt beide. Empfehlung V1: nur wöchentlich (Sonntag-Abend-Digest), weil tägliche Mails zu schnellen Unsubscribes führen. Decision in Slice 11.
Owner: Slice 11
Customer-Lens-Reviewer-Konsultation für Slice 4 (Canvas-Page)
Frontend-Slice mit Figma-Match → Customer-Lens-Reviewer-Pflicht laut ADR-Workflow. Tooling: Spawn als Subagent während Slice 4 Execution. Output landet als Konsultations-Eintrag im Worklog der Story F-290.
Owner: Slice 4