diff --git a/src/lib/workersRoster.js b/src/lib/workersRoster.js new file mode 100644 index 0000000..bcd7bdc --- /dev/null +++ b/src/lib/workersRoster.js @@ -0,0 +1,106 @@ +// Spiegel des Rollen-Registers (Übersicht §3) für die Hustler-Seite. +// Die Tabelle spiegelt den Dienst (src/roles.js); beide Repos testen gegen dieselbe Fixture workers-roster.json. +// Reine Daten und Funktionen: kein React, kein Appwrite, kein window/document. + +// Modell-Kurznamen der Rollen -> Modell-IDs (Übersicht §2.8) +export const MODEL_IDS = Object.freeze({ + opus: 'claude-opus-5-5', + sonnet: 'claude-sonnet-5-5', + haiku: 'claude-haiku-5-5', +}) + +// Etagen von oben nach unten; freigabe (Pult) und tresor haben keinen Worker +export const FLOORS = Object.freeze(['bruecke', 'anfragen', 'ohne_website', 'bewertung', 'gespraeche', 'werkstatt', 'pruefung', 'freigabe', 'tresor', 'keller']) + +// Die vier Branchen-Scouts (Ali, Hanna, Sven, Lea) haben dieselben Fähigkeiten +const SCOUT_CAPS = ['web_search', 'web_fetch', 'places', 'osm'] + +// Tabellenreihenfolge der Übersicht §3; role = Spalte „Einzige Rolle“ wörtlich +const TABELLE = [ + { workerId: 'otto', name: 'Otto', floor: 'bruecke', room: 'bruecke', model: 'opus', role: 'plant die Nacht, verteilt Aufträge, Budget, Morgenbericht', caps: ['plan', 'report'] }, + { workerId: 'rita', name: 'Rita', floor: 'anfragen', room: 'netz', model: 'haiku', role: 'sucht frische „Suche Webdesigner“-Posts über Websuche', caps: ['web_search', 'web_fetch'] }, + { workerId: 'kim', name: 'Kim', floor: 'anfragen', room: 'kleinanzeigen', model: 'haiku', role: 'liest Kleinanzeigen-Suchauftrags-Mails im Alarm-Postfach', caps: ['alert_inbox'] }, + { workerId: 'pia', name: 'Pia', floor: 'anfragen', room: 'portale', model: 'haiku', role: 'liest Projekt-Alerts/RSS der Portale', caps: ['alert_inbox', 'rss'] }, + { workerId: 'finn', name: 'Finn', floor: 'anfragen', room: 'foren', model: 'haiku', role: 'sucht in Foren/Frage-Seiten (nur erlaubte)', caps: ['web_search', 'web_fetch'] }, + { workerId: 'ali', name: 'Ali', floor: 'ohne_website', room: 'aerzte', model: 'haiku', role: 'Branchen-Scout Ärzte & Praxen', caps: SCOUT_CAPS }, + { workerId: 'hanna', name: 'Hanna', floor: 'ohne_website', room: 'handwerk', model: 'haiku', role: 'Branchen-Scout Handwerk', caps: SCOUT_CAPS }, + { workerId: 'sven', name: 'Sven', floor: 'ohne_website', room: 'selbst', model: 'haiku', role: 'Branchen-Scout Selbstständige', caps: SCOUT_CAPS }, + { workerId: 'lea', name: 'Lea', floor: 'ohne_website', room: 'laden', model: 'haiku', role: 'Branchen-Scout Läden & Gastro', caps: SCOUT_CAPS }, + { workerId: 'bea', name: 'Bea', floor: 'bewertung', room: 'waage', model: 'sonnet', role: 'bewertet, verteilt Top 3 / Rest', caps: ['score'] }, + { workerId: 'max', name: 'Max', floor: 'gespraeche', room: 'mail', model: 'sonnet', role: 'schreibt Mails und ist einziger Absender', caps: ['draft', 'send_mail'] }, + { workerId: 'clara', name: 'Clara', floor: 'gespraeche', room: 'forum', model: 'sonnet', role: 'schreibt Antworten auf Anfragen', caps: ['draft', 'pult'] }, + { workerId: 'nora', name: 'Nora', floor: 'gespraeche', room: 'posteingang', model: 'haiku', role: 'liest + sortiert Eingänge', caps: ['read_inbox', 'classify'] }, + { workerId: 'theo', name: 'Theo', floor: 'gespraeche', room: 'abschluss', model: 'sonnet', role: 'Nachrichten rund um den Kauf (Entwürfe)', caps: ['draft', 'pult', 'offer'] }, + { workerId: 'bob', name: 'Bob', floor: 'werkstatt', room: 'tisch1', model: 'opus', role: 'baut Vorschauen (nur nach Ja)', caps: ['build'] }, + { workerId: 'bianca', name: 'Bianca', floor: 'werkstatt', room: 'tisch2', model: 'opus', role: 'baut Vorschauen', caps: ['build'] }, + { workerId: 'ben', name: 'Ben', floor: 'werkstatt', room: 'tisch3', model: 'opus', role: 'baut Vorschauen', caps: ['build'] }, + { workerId: 'paul', name: 'Paul', floor: 'pruefung', room: 'pruefung', model: 'sonnet', role: 'prüft Vorschauen (Screenshots)', caps: ['review', 'screenshot'] }, + { workerId: 'arne', name: 'Arne', floor: 'keller', room: 'archiv', model: 'haiku', role: 'Nie-doppelt-Register + Bot-Lead-Liste', caps: ['register', 'archive'] }, +] + +// Eingefroren, jede Rolle mit eigener caps-Kopie +export const ROLES = Object.freeze(TABELLE.map((r) => Object.freeze({ ...r, caps: Object.freeze([...r.caps]) }))) + +// Räume der Etagen anfragen und ohne_website in ROLES-Reihenfolge (Spiegel von src/roles.js des Dienstes). +// Nur diese Such-Räume können „nicht angeschlossen“ (grau) sein, alle anderen Worker meldet der Dienst als +// angeschlossen (Übersicht §11.1 P1-E9). WOMS berechnet `connected` nicht selbst; die Konstante dient Anzeige und Prüfstand. +export const SEARCH_ROOMS = Object.freeze(ROLES.filter((r) => r.floor === 'anfragen' || r.floor === 'ohne_website').map((r) => r.room)) + +export const FLOOR_TITLES = Object.freeze({ + bruecke: 'Kommandobrücke', + anfragen: 'Anfragen', + ohne_website: 'Ohne Website', + bewertung: 'Bewertung', + gespraeche: 'Gespräche', + werkstatt: 'Werkstatt', + pruefung: 'Prüfung', + freigabe: 'Freigabe', + tresor: 'Tresor', + keller: 'Keller', +}) + +// 19 Worker-Räume plus Pult (Etage freigabe) und Tresor +export const ROOM_TITLES = Object.freeze({ + netz: 'Netz-Radar', + kleinanzeigen: 'Kleinanzeigen', + portale: 'Portale', + foren: 'Foren & Fragen', + aerzte: 'Ärzte & Praxen', + handwerk: 'Handwerk', + selbst: 'Selbstständige', + laden: 'Läden & Gastro', + waage: 'Waage', + mail: 'Mail-Stube', + forum: 'Forum-Stube', + posteingang: 'Posteingang', + abschluss: 'Abschluss', + tisch1: 'Tisch 1', + tisch2: 'Tisch 2', + tisch3: 'Tisch 3', + pruefung: 'Prüftisch', + archiv: 'Archiv & Register', + bruecke: 'Brücke', + pult: 'Dein Pult', + tresor: 'Tresor', +}) + +// Nachschlagen über Map statt Objekt: 'constructor' oder '__proto__' sind keine Treffer +const NACH_ID = new Map(ROLES.map((r) => [r.workerId, r])) + +// Rolle zur Kennung; unbekannte Kennung -> null +export function roleOf(workerId) { + return NACH_ID.get(workerId) ?? null +} + +// Hut der Figur nach Modell: Kurzname oder voller Modellname, sonst 'haiku' +export function hatForModel(model) { + const m = typeof model === 'string' ? model.match(/opus|sonnet|haiku/) : null + return m ? m[0] : 'haiku' +} + +// Otto trägt die Kommandomütze, alle anderen den Hut ihres Modells; unbekannte Kennung -> 'haiku' +export function hatForWorker(workerId) { + if (workerId === 'otto') return 'overseer' + const r = roleOf(workerId) + return r ? hatForModel(r.model) : 'haiku' +} diff --git a/tests/fixtures/workers-roster.json b/tests/fixtures/workers-roster.json new file mode 100644 index 0000000..835de10 --- /dev/null +++ b/tests/fixtures/workers-roster.json @@ -0,0 +1,24 @@ +{ + "floors": ["bruecke","anfragen","ohne_website","bewertung","gespraeche","werkstatt","pruefung","freigabe","tresor","keller"], + "roles": [ + {"workerId":"otto","name":"Otto","floor":"bruecke","room":"bruecke","model":"opus","role":"plant die Nacht, verteilt Aufträge, Budget, Morgenbericht","caps":["plan","report"]}, + {"workerId":"rita","name":"Rita","floor":"anfragen","room":"netz","model":"haiku","role":"sucht frische „Suche Webdesigner“-Posts über Websuche","caps":["web_search","web_fetch"]}, + {"workerId":"kim","name":"Kim","floor":"anfragen","room":"kleinanzeigen","model":"haiku","role":"liest Kleinanzeigen-Suchauftrags-Mails im Alarm-Postfach","caps":["alert_inbox"]}, + {"workerId":"pia","name":"Pia","floor":"anfragen","room":"portale","model":"haiku","role":"liest Projekt-Alerts/RSS der Portale","caps":["alert_inbox","rss"]}, + {"workerId":"finn","name":"Finn","floor":"anfragen","room":"foren","model":"haiku","role":"sucht in Foren/Frage-Seiten (nur erlaubte)","caps":["web_search","web_fetch"]}, + {"workerId":"ali","name":"Ali","floor":"ohne_website","room":"aerzte","model":"haiku","role":"Branchen-Scout Ärzte & Praxen","caps":["web_search","web_fetch","places","osm"]}, + {"workerId":"hanna","name":"Hanna","floor":"ohne_website","room":"handwerk","model":"haiku","role":"Branchen-Scout Handwerk","caps":["web_search","web_fetch","places","osm"]}, + {"workerId":"sven","name":"Sven","floor":"ohne_website","room":"selbst","model":"haiku","role":"Branchen-Scout Selbstständige","caps":["web_search","web_fetch","places","osm"]}, + {"workerId":"lea","name":"Lea","floor":"ohne_website","room":"laden","model":"haiku","role":"Branchen-Scout Läden & Gastro","caps":["web_search","web_fetch","places","osm"]}, + {"workerId":"bea","name":"Bea","floor":"bewertung","room":"waage","model":"sonnet","role":"bewertet, verteilt Top 3 / Rest","caps":["score"]}, + {"workerId":"max","name":"Max","floor":"gespraeche","room":"mail","model":"sonnet","role":"schreibt Mails und ist einziger Absender","caps":["draft","send_mail"]}, + {"workerId":"clara","name":"Clara","floor":"gespraeche","room":"forum","model":"sonnet","role":"schreibt Antworten auf Anfragen","caps":["draft","pult"]}, + {"workerId":"nora","name":"Nora","floor":"gespraeche","room":"posteingang","model":"haiku","role":"liest + sortiert Eingänge","caps":["read_inbox","classify"]}, + {"workerId":"theo","name":"Theo","floor":"gespraeche","room":"abschluss","model":"sonnet","role":"Nachrichten rund um den Kauf (Entwürfe)","caps":["draft","pult","offer"]}, + {"workerId":"bob","name":"Bob","floor":"werkstatt","room":"tisch1","model":"opus","role":"baut Vorschauen (nur nach Ja)","caps":["build"]}, + {"workerId":"bianca","name":"Bianca","floor":"werkstatt","room":"tisch2","model":"opus","role":"baut Vorschauen","caps":["build"]}, + {"workerId":"ben","name":"Ben","floor":"werkstatt","room":"tisch3","model":"opus","role":"baut Vorschauen","caps":["build"]}, + {"workerId":"paul","name":"Paul","floor":"pruefung","room":"pruefung","model":"sonnet","role":"prüft Vorschauen (Screenshots)","caps":["review","screenshot"]}, + {"workerId":"arne","name":"Arne","floor":"keller","room":"archiv","model":"haiku","role":"Nie-doppelt-Register + Bot-Lead-Liste","caps":["register","archive"]} + ] +} diff --git a/tests/unit/workersRoster.test.mjs b/tests/unit/workersRoster.test.mjs new file mode 100644 index 0000000..a9ef7e5 --- /dev/null +++ b/tests/unit/workersRoster.test.mjs @@ -0,0 +1,35 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { loadModule } from './load.mjs' +const { ROLES, FLOORS, SEARCH_ROOMS, FLOOR_TITLES, ROOM_TITLES, MODEL_IDS, hatForModel, hatForWorker, roleOf } = await loadModule('src/lib/workersRoster.js') +const fixture = JSON.parse(readFileSync(new URL('../fixtures/workers-roster.json', import.meta.url), 'utf8')) + +test('Spiegel ist gleich der gemeinsamen Fixture', () => { + assert.deepEqual([...FLOORS], fixture.floors) + assert.deepEqual(JSON.parse(JSON.stringify(ROLES)), fixture.roles) +}) +test('19 Worker, workerId und Raum einmalig, Etagen und Titel vollständig', () => { + assert.equal(ROLES.length, 19) + assert.equal(new Set(ROLES.map((r) => r.workerId)).size, 19) + assert.equal(new Set(ROLES.map((r) => r.room)).size, 19) + for (const r of ROLES) assert.ok(FLOORS.includes(r.floor), r.workerId) + assert.equal(FLOORS.length, 10) + assert.deepEqual(Object.keys(FLOOR_TITLES).sort(), [...FLOORS].sort()) + assert.deepEqual(Object.keys(ROOM_TITLES).sort(), [...ROLES.map((r) => r.room), 'pult', 'tresor'].sort()) +}) +test('Hut zeigt das Modell, Otto trägt die Kommandomütze', () => { + assert.deepEqual(['opus', 'claude-sonnet-5-5', 'claude-haiku-5-5', 'claude-xyz', undefined].map(hatForModel), ['opus', 'sonnet', 'haiku', 'haiku', 'haiku']) + assert.deepEqual(['otto', 'bob', 'bea', 'kim', 'zed'].map(hatForWorker), ['overseer', 'opus', 'sonnet', 'haiku', 'haiku']) + assert.equal(roleOf('paul').room, 'pruefung') + assert.equal(roleOf('zed'), null) + assert.equal(roleOf('constructor'), null) + assert.equal(hatForWorker('__proto__'), 'haiku') + assert.deepEqual(MODEL_IDS, { opus: 'claude-opus-5-5', sonnet: 'claude-sonnet-5-5', haiku: 'claude-haiku-5-5' }) +}) +test('SEARCH_ROOMS: festgenagelt wie im Dienst (P1-T05) und aus der Fixture ableitbar; nur diese Worker können grau sein (P1-E9)', () => { + assert.deepEqual([...SEARCH_ROOMS], ['netz', 'kleinanzeigen', 'portale', 'foren', 'aerzte', 'handwerk', 'selbst', 'laden']) + assert.deepEqual([...SEARCH_ROOMS], fixture.roles.filter((r) => ['anfragen', 'ohne_website'].includes(r.floor)).map((r) => r.room)) + assert.deepEqual(ROLES.filter((r) => SEARCH_ROOMS.includes(r.room)).map((r) => r.workerId), ['rita', 'kim', 'pia', 'finn', 'ali', 'hanna', 'sven', 'lea']) + assert.equal(ROLES.filter((r) => !SEARCH_ROOMS.includes(r.room)).length, 11) +})