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).
project_offer (Bundle), solcom_project_offer (Modul), industry (Vocabulary). Domain-Begriff von solcom.de/de/projektportal übernommen; „Publication" demoted zu BH4SF-RecordType-Implementierungs-Detail.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.ProjectOfferHardDeleteSubscriber auf MigrateEvents::POST_IMPORT. Per-Run-Sweep gegen migrate_map_project_offer. Empty-Feed-Schutz: 0 Records → kein Delete + Watchdog.hook_node_insert + hook_node_update mit Status-Transition-Guard → Drupal-Queue project_offer_outbound → ProjectOfferOutboundWorker → REST-PATCH von hohr__Job_Application_URL__c via SalesforceClient-Service.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_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./projektportal/projektangebote/[node:title]. Listing-Page-Canvas-Page unter /projektportal. Merkliste unter /projektportal/merkliste.schema.org/JobPosting via ProjectOfferJsonLdBuilder-Service + hook_node_view-Adapter. 1:1-Pattern aus Vacancy-Vorbild übernommen.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).drupal/salesforce Suite explizit rejected (ADR-0013-Bruch, ext-soap-Pflicht, Switching-Cost). Vollständige Argumentation in ADR-0029 (amended).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 |
|---|---|---|---|
| 0 | Pre-Build-Gate Live-Describe FCSB-Sandbox | AFK | — |
| 1 | Domain-Rename-Refactor (sf_publication → project_offer) | AFK | 0 |
| 2 | Lifecycle-Window Render-Time-Auswertung | AFK | 1 |
| 3 | JSON-LD nach schema.org/JobPosting | AFK | 1 |
| 4 | Canvas-Page /projektportal + Cross-Sell-Block | AFK | 1 |
| 5 | Pre-Build-Gate Layer-B-Felder (Skills + Remote-Anteil) | AFK | 0 |
| 6 | Skill-Badges auf Card + Skills-Filter | AFK | 1, 5 |
| 7 | Remote-Anteil-Slider + Range-Filter | AFK | 1, 5 |
| 8 | Geocoder-Setup + Suchradius-Filter | AFK | 1 |
| 9 | Merkliste (anon LocalStorage) | AFK | 1 |
| 10 | Matching-Rate (V1 faked) | AFK | 1 |
| 11 | Projekt-Alert (E-Mail-Subscription) | AFK | 1 |
Jede Slice ist eine vertikale Trennung durch alle Layer (Schema, Config, API, UI, Tests) und unabhängig demoabar.
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.
docs/context/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_publication → migrate_map_project_offer via SQL-RENAME. 38 Config-Sync-YAMLs renamed + inhaltlich aktualisiert. Canvas-Templates node.sf_publication.{card,full}.yml → node.project_offer.{card,full}.yml. View publications_listing → project_offers_listing. Block-Plugin-IDs aktualisiert. Twig-Bundle-Refs in byte_theme/ nachgezogen. Vocabulary sf_industry → industry (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-Driftdrush updb -y migriert alle 21 Field-Storages + Map-Tabelle ohne Datenverlustmigrate_map_project_offer erhalten (Idempotenz beim nächsten Cron-Run gewährleistet)CONTEXT.md Module-Boundary-Eintrag auf neues Modul aktualisiertacli-Sweepfield_sf_publish_start.yml (Drift gegen 0008-Snapshot: Field heißt hohr__Publishing_start_date__c, datetime trotz Name)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.
AND TR1__Job_Title__c != null, process.title auf skip_on_empty(method: row) (ID-Fallback entfernt) — kein SF-ID-als-Titel mehr.sticky-apply-bar, badge, heading, meta-row — Canvas-Bindings an Outbound-/Optional-Felder kollabieren nicht mehr./projektpartnerportal (war /projektportal). PRD/Plan-Drafts mit /projektportal/projektangebote/ superseded — kanonische URL ist die ADR-0031-Variante./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.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.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).docs/context/0002-sitemap.html ist die Soll-Vorlage; aktueller DB-State hat sie auf falschem oder fehlendem Parent. Eigener Restructuring-PR.
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).
node.status nutzt nur noch static_map auf SC_PublicationStatus__c, nicht mehr kombinierte BedingungProjectOfferWindowAccessTest deckt 4 Time-Boundary-Cases abNeuer 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.
/projektportal/projektangebote/[alias] liefert ein <script type="application/ld+json">-TagProjectOfferJsonLdBuilderTest deckt Mapping + UTF-8-Escaping + ISO-8601-Format abCanvas-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).
/projektportal existiert in config/sync/ und ist via Pathauto erreichbarproject_offers_listing mit BEF-FilternPppCrossSellBlock ist platziert und zeigt 3 konfigurierbare Cardsdocs/design/MAPPING.html für die zwei Figma-Frames auf matched aktualisiertLive-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/.
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.
skill existiert in config/sync/ + leeres Pre-Seedfield_skills Entity-Reference-Field auf project_offer + Migrate-ProcessNeues 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-FieldComposer-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 erfolgreichfield_geo existiert + Migrate-Process geocodet beim ImportAnonyme 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".
/projektportal/merkliste existiert und lädt aktive Project-Offers aus Browser-LocalStorageV1-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.
(session, project_offer_id)-TupelSubmodul 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.
solcom_project_offer_alert aktiviertproject_offer_alert in config/sync/ProjectOfferAlertSubscriptionTest deckt Alert-Entity + Matching + Mail-Dispatch abRisiken mit Severity + Mitigation. High-Severity-Items brauchen während der Slice-Execution dedizierte Aufmerksamkeit.
hook_update_N idempotent + transactional; vor Run: DB-Backup via drush sql:dump; nach Run: Smoke-Test (Migrate-Run + Outbound-Run + 1 Listing-View)drush solcom-project-offer:push-urls --force direkt nach Slice 1 Mergesolcom-docs-ingestion-Agent angepasst, BEVOR Slice 1 startetskill manuell ergänzen; Feedback-Mechanik in Phase 2Punkte 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).
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.