t-paliad-148: split project_teams.role — profession vs project responsibility #11
Open
mAi
wants to merge 0 commits from
mai/kepler/inventor-profession-vs into main
pull from: mai/kepler/inventor-profession-vs
merge into: m:main
m:main
m:mai/planck/coder-b5-b6-train-share
m:mai/archimedes/fixer-port-engine
m:mai/maxwell/coder-b4-akte-mode
m:mai/lorenz/coder-b3-event-triggered
m:mai/euler/fixer-builder-add
m:mai/brunel/fixer-prod-500s-after-b1
m:mai/galileo/coder-b1-b2-mvp-train
m:mai/pasteur/fixer-pkg-litigationplann
m:mai/newton/coder-b0-scenario-db
m:mai/edison/inventor-prd-columnar
m:mai/knuth/coder-workflow-tracker
m:mai/atlas/inventor-extend-tools
m:mai/cronus/inventor-unified
m:mai/atlas/inventor-deadline-system
m:mai/atlas/inventor-followup-rules
m:mai/athena/consultant-deadline
m:mai/brunel/fixer-dark-mode-support
m:mai/knuth/coder-cronus-fristenrechn
m:mai/ritchie/coder-mig-153-proceeding
m:mai/atlas/inventor-proceeding
m:mai/cronus/inventor-fristenrechner
m:mai/curie/coder-mig152-clone-dedupe
m:mai/darwin/researcher-lexy-draft
m:mai/knuth/coder-dedupe-null
m:mai/cronus/coder-composer-slice-f
m:mai/cronus/coder-composer-slice-e
m:mai/cronus/coder-composer-slice-d
m:mai/curie/coder-slice-b6-url-rename
m:mai/curie/coder-slice-b5-go-rename
m:mai/cronus/coder-composer-slice-c
m:mai/curie/coder-slice-b4-destructive-drop
m:mai/cronus/coder-composer-slice-b
m:mai/cronus/coder-composer-slice-a
m:mai/cronus/inventor-prd-for
m:mai/knuth/coder-verfahrensablauf
m:mai/ritchie/coder-make-backup
m:mai/diesel/fixer-dark-mode-css
m:mai/curie/coder-slice-b3-read-cutover
m:mai/diesel/fixer-verfahrensablauf
m:mai/curie/coder-slice-b2-dual-write
m:mai/cronus/coder-slice-d-scenarios
m:mai/knuth/coder-backfill-applies
m:mai/hermes/gitster-verfahrensablauf
m:mai/cronus/coder-berufung-labels-refactor
m:mai/diesel/hotfix-2-mig-134-missing
m:mai/curie/coder-slice-b1-procedural-events
m:mai/cronus/coder-slice-c-upc-snapshot
m:mai/brunel/hotfix-rename-upc-apl
m:mai/cronus/coder-slice-b3-primary-party
m:mai/cronus/coder-slice-b2-catalog-query
m:mai/cronus/inventor-litigation-slice-b
m:mai/curie/researcher-slice-b-zero
m:mai/cronus/inventor-litigation
m:mai/artemis/gitster-remove-admin
m:mai/ritchie/coder-sort-post-trigger
m:mai/knuth/coder-conditional-label
m:mai/hermes/coder-verfahrensablauf
m:mai/brunel/rebase-121-conditional
m:mai/knuth/coder-conditional-rule
m:mai/hermes/gitster-dark-mode-fix
m:mai/ritchie/coder-submission-form
m:mai/artemis/gitster-re-surface
m:mai/brunel/fixer-views-any-filters
m:mai/cronus/coder-cicd-slice-a
m:mai/knuth/coder-wave-1-tier-1-rule
m:mai/ritchie/coder-upc-damages-add
m:mai/cronus/inventor-ci-cd-pre
m:mai/brunel/rebase-108-language
m:mai/hermes/gitster-admin-rules-list
m:mai/artemis/gitster-submission
m:mai/icarus/gitster-verfahrensablauf
m:mai/orpheus/gitster-search-input
m:mai/atlas/coder-event-card-choices-slice-ab
m:mai/hermes/gitster-date-range
m:mai/demeter/gitster-submission
m:mai/knuth/coder-hl-patents-style
m:mai/hermes/gitster-draft-editor
m:mai/atlas/inventor-per-event-card
m:mai/knuth/coder-deadline-rule-tier
m:mai/cronus/coder-procedural-events-slice-a
m:mai/hermes/gitster-deadline-form
m:mai/artemis/gitster-add-missing-i18n
m:mai/demeter/gitster-paliadin-chat
m:mai/brunel/wave0-tier0-deadline-fixes
m:mai/artemis/coder-docker-compose-yml
m:mai/icarus/coder-inbox-overhaul-slice-a
m:mai/atlas/coder-date-range-picker-slice-a
m:mai/brunel/fixer-de-inf-lg-cfi
m:mai/cronus/inventor-procedural
m:mai/hermes/gitster-event-type-modal
m:mai/cronus/coder-backup-mode
m:mai/curie/researcher-bulletproof
m:mai/hermes/gitster-draft-editor-focus-jump
m:mai/cronus/inventor-backup-mode
m:mai/hermes/gitster-submissions
m:mai/artemis/gitster-deadline-form
m:mai/brunel/fixer-submission-preview
m:mai/brunel/fixer-test-data-reset
m:mai/artemis/gitster-approval-withdraw
m:mai/demeter/gitster-events
m:mai/hermes/gitster-sidebar-loses
m:mai/hermes/gitster-browse-a
m:mai/brunel/fixer-submissions-demo
m:mai/icarus/inventor-inbox-overhaul
m:mai/atlas/inventor-symmetric-date
m:mai/artemis/gitster-demote-daten
m:mai/hermes/gitster-team-view-mailto
m:mai/knuth/coder-global-schriftsatze
m:mai/knuth/coder-schriftsatze
m:mai/ritchie/coder-author-demo-docx
m:mai/knuth/coder-add-schriftsatze
m:mai/knuth/coder-add-checklist
m:mai/knuth/coder-anchor-lookup-must
m:mai/tesla/dashboard-resize-clamp
m:mai/knuth/coder-demote-projekt
m:mai/knuth/coder-paliadin-chat
m:mai/knuth/coder-print-views
m:mai/knuth/coder-add-proceeding
m:mai/knuth/coder-submission
m:mai/ritchie/coder-extend-team-email
m:mai/knuth/coder-changelog-catch-up
m:mai/tesla/dashboard-overlap
m:mai/pasteur/fixercoder-dashboard
m:mai/newton/inventor-configurable
m:mai/dirac/inventorcoder-user
m:mai/gauss/inventorcoder-team-admin
m:mai/kepler/inventorcoder-project
m:mai/darwin/roadmap-ccr-en
m:mai/euler/coder-small-ux-polish
m:mai/darwin/fristenrechner-cleanup
m:mai/darwin/fixercoder-priority-bug
m:mai/leibniz/inventor-caldav-multi
m:mai/hertz/inventor-unified-modal
m:mai/archimedes/inventor-excel-data
m:mai/boltzmann/inventor-gap-tolerant
m:mai/copernicus/submission-slice-1
m:mai/fermi/interactive-session
m:mai/hertz/inventor-suggest-changes
m:mai/copernicus/inventor-submission
m:mai/mendel/test-strategy-slice-1
m:mai/mendel/inventor-test-strategy
m:mai/ampere/custom-views-improvements
m:mai/joule/mig-097-apply-huygens-s
m:mai/ohm/workstream-b-rename
m:mai/huygens/workstream-a-backfill
m:mai/kelvin/t-204-phase-2-proceeding
m:mai/bohr/ingest-t-paliad-203-rule
m:mai/curie/fristenrechner-gap
m:mai/maxwell/inbox-grey-out
m:mai/rutherford/slice-9-follow-up-b-re
m:mai/dirac/slice-9-follow-up-a
m:mai/bose/determinator-cascade-slice-3
m:mai/bose/determinator-cascade-slice-2
m:mai/bose/determinator-row-cascade
m:mai/lorenz/fristen-phase-3-slice-9
m:mai/curie/fristen-phase-3-slice-12
m:mai/planck/aichat-phase-b-paliad
m:mai/young/fristen-phase-3-slice-11b
m:mai/lorenz/fristen-phase-3-slice-11a
m:mai/lorenz/fristen-phase-3-slice-10
m:mai/lorenz/fristen-phase-3-slice-8
m:mai/lorenz/fristen-phase-3-slice-7
m:mai/lorenz/fristen-phase-3-slice-6
m:mai/lorenz/fristen-phase-3-slice-5
m:mai/lorenz/fristen-phase-3-slice-4
m:mai/lorenz/fristen-phase-3-slice-3
m:mai/lorenz/fristen-phase-3-slice-2
m:mai/lorenz/fristen-phase-3-slice-1
m:mai/pauli/fristen-phase2-design
m:mai/tesla/project-timeline-chart
m:mai/pauli/fristen-logic-audit
m:mai/pauli/determinator-b1-row-by
m:mai/noether/tools-cleanup-slice-1
m:mai/kelvin/inventor-tools-surface
m:mai/planck/paliadin-per-user-rls
m:mai/maxwell/bug-bundle-filterbar
m:mai/faraday/project-timeline-chart
m:mai/schroedinger/smarttimeline-slice-4
m:mai/bohr/smarttimeline-slice-3
m:mai/gauss/smarttimeline-slice-2
m:mai/riemann/filterbar-phase-2-slice
m:mai/lagrange/smarttimeline-design-the
m:mai/curie/researcher-determinator
m:mai/noether/collapse-regel-typ-on
m:mai/riemann/inventor-universal
m:mai/minkowski/project-level-our-side
m:mai/dirac/inventor-inline-paliadin
m:mai/feynman/fristenrechner
m:mai/minkowski/navbar-dashboard-reorg
m:mai/shannon/approval-rework
m:mai/einstein/consultant-deadline-data
m:mai/curie/researcher-upc-rop-audit
m:mai/noether/paliadin-real-claude
m:mai/noether/inventor-paliadin
m:mai/hilbert/inventor-approval-policy
m:mai/shannon/bug-frist-due-date
m:mai/fritz/bug-fristen-termine
m:mai/godel/inventor-projects-page
m:mai/fritz/bug-paliadin-chat
m:mai/noether/inventor-paliadin-in-app
m:mai/fritz/bulk-team-email-send-to
m:mai/noether/inventor-local-chat-for
m:mai/noether/inventor-data-display
m:mai/fritz/bug-derived-team-members
m:mai/fritz/bug-sidebar-visibly
m:mai/noether/inventor-project
m:mai/shannon/bug-project-team-add
m:mai/cronus/inventor-dual-control
m:mai/fritz/bug-edit-mode-on
m:mai/cronus/inventor-holidays-per
m:mai/ritchie/phase-h-ai-deadline
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
No description provided.
Delete Branch "mai/kepler/inventor-profession-vs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes m/paliad#6 (when merged).
Summary
Splits
paliad.project_teams.roleinto two clean axes:paliad.users.profession— firm-wide career tier (partner | of_counsel | associate | senior_pa | pa | paralegal | NULL). Drives the t-138 approval ladder.paliad.project_teams.responsibility— per-project (lead | member | observer | external). Defaultmember. Replaces the team-add dropdown values m complained about.Approval ladder is now a tuple-with-gate:
effective_level = profession_level IF responsibility ∈ {lead, member} ELSE 0. Policy grammar from t-138 (required_rolesingle value) stays unchanged.Design locked by m on 2026-05-07 21:35: "lets go with those - but if you are fine, go for shift". All 12 §10 recommendations approved verbatim.
Commits (6)
ab2530f) — schema + backfill +user_project_authority_levelSQL function. project_teams.role kept as deprecated shadow.6506864) — 4 SQL ladder sites switch to tuple-with-gate;levelOf → professionLevel; newresponsibilityOpensGate. NULL trap pinned byTestProfessionLevel_NilIsZero.e6937d2) — write profession + responsibility. JSON shape extends withresponsibility(preferred) +user_profession; legacyroleaccepted as fallback for one release.9184e9b) — pt.role → pt.responsibility. Drops dead'admin'reference in deadline reopen check.2af4bf1) — team-add dropdown 7 mixed → 4 responsibility-only. 3-column team table (Name · Profession · Responsibility · Herkunft). admin-team gains Profession column. Onboarding gains Profession select. ~30 new i18n keys DE+EN. CSS pill variants.0b4de1c).Live data impact
Backfill is essentially trivial:
project_teamsrows (alllead) → 2 users getprofession='partner', 3 rows getresponsibility='lead'.partner_unit_membersrows (all defaultattorney) — bridge unchanged.profession=NULLand will need admin pass via/admin/team(honest visibility of pre-existing data debt).Live-DB BEGIN/ROLLBACK dry-run verified during commit 1.
Verification
go build ./...✓go vet ./...✓go test ./...✓ — all 7 test packages pass; new tests for profession ladder + responsibility gate + NULL trap + new validators.bun build.ts✓ — 1723 i18n keys, all referenced.Test plan
Follow-up
File t-paliad-149 to drop
paliad.project_teams.roleafter one release of soak time on main. Trivial migration 058.Adds paliad.users.profession (firm-wide career tier) and paliad.project_teams.responsibility (per-project responsibility, default 'member'). Backfills both from the legacy project_teams.role column — highest-tier-per-user for profession, single-row map for responsibility (lead→lead, observer→observer, local_counsel/expert→external, others→member). Updates paliad.approval_role_level to recognise 'partner' as the new ceiling (replaces 'lead' as the firm-tier ceiling), keeping 'lead' at level 5 as a deprecated-shadow row until follow-up migration 058 retires project_teams.role. Updates paliad.approval_role_from_unit_role: lead → partner. Creates paliad.user_project_authority_level(user_id, project_id) — the tuple-with-gate ladder. Returns profession_level if responsibility ∈ {lead,member} else 0; max with derived authority via partner-unit attachments where derive_grants_authority=true. Updates approval_policies.required_role + approval_requests.required_role CHECK constraints (drop 'lead', add 'partner'); backfills any existing rows. Rewrites project_partner_units write RLS policy to read pt.responsibility='lead' instead of pt.role='lead'. Live-DB BEGIN/ROLLBACK dry-run verified: 2 users get profession='partner' (matthias.siebels, tester@hlc.de — the only users currently on project_teams), 45 users get profession=NULL (admin fills via /admin/team). project_teams.role kept as deprecated shadow column. Drop in follow-up migration 058.Rewires the 4 SQL ladder sites in approval_service.go (canApprove, hasQualifiedApprover, ListPendingForApprover, PendingCountForUser) to read the new tuple: project_teams.responsibility ∈ {lead, member} AND users.profession at or above the threshold. observer/external rows close the gate even if the user's profession would otherwise qualify — that's the project-level call. approval_levels.go renamed levelOf → professionLevel and added responsibilityOpensGate helper. New constants: ProfessionPartner / ProfessionOfCounsel / … and ResponsibilityLead / ResponsibilityMember / ResponsibilityObserver / ResponsibilityExternal. New validators IsValidProfession + IsValidResponsibility. RoleSeniorPA kept as legacy alias for the one remaining call site that hasn't migrated yet. CRITICAL trap pinned by TestProfessionLevel_NilIsZero: NULL profession returns 0, never silently defaults to associate. External collaborators must stay ineligible. derivation_service.go: requireWritePermission switches from pt.role='lead' to pt.responsibility='lead' — project-management writes gate on the project responsibility, not the firm tier. EffectiveProjectRole replaced by UserProjectAuthorityLevel (thin wrapper over the SQL function in migration 057). The legacy method was unused dead code despite t-139 design intent. Tests extended: profession ladder, responsibility gate, NULL trap, new validators. Build + vet clean.Models: - ProjectTeamMember.Responsibility (new) + .Role (kept as deprecated shadow). JSON exposes both during the deprecation window. - ProjectTeamMemberWithUser.UserProfession — populated by reads so the team-tab UI can render the firm-tier badge. - User.Profession (*string) — structured firm-tier driving the approval ladder. Distinct from JobTitle (display) and GlobalRole (tool admin). TeamService: - AddMember signature kept as (callerID, projectID, userID, responsibility) — third arg renamed conceptually. Accepts the new responsibility enum and writes both legacy `role` (via legacyRoleFromResponsibility helper) and `responsibility` to keep the deprecated shadow consistent. - ListDirectMembers + ListEffectiveMembers SELECT both `pt.role`, `pt.responsibility`, and `u.profession`. ORDER BY switches from pt.role to pt.responsibility. - legacy isValidRole removed (unused after switch to IsValidResponsibility). UserService: - CreateUserInput + AdminCreateInput + AdminUpdateInput accept Profession. Self-service onboarding defaults to 'associate' when empty. AdminCreate likewise. AdminUpdate empty-string clears to NULL (external collaborator). Invalid values rejected with ErrInvalidInput. - INSERT statements write the new column on both Create paths. ProjectService.Create: - Auto-add-creator INSERT writes responsibility='lead' alongside legacy role='lead'. Handlers: - POST /api/projects/{id}/team accepts `responsibility` (preferred) and falls back to legacy `role` for one release while frontend migrates. Build + vet clean. Pure-Go tests pass.reminder_service.go: BuildDigest audience predicate switches the "project lead anywhere on the path" branch from `pt.role = 'lead'` to `pt.responsibility = 'lead'`. Two SQL sites + comment updated. deadline_service.go: assertCanAdminProject (Reopen permission) switches from `pt.role IN ('admin','lead')` to `pt.responsibility = 'lead'`. The legacy 'admin' was already dead since t-paliad-051 — never present in project_teams.role to begin with — so this also drops a slow leak. Doc comments + error message updated. derivation_service.go: ListDescendantStaffed SELECTs both `pt.role` and `pt.responsibility`, returns the new column to the team-tab "from descendants" subsection (so the firm-tier badge + responsibility pill both render). ORDER BY switches to responsibility. Build + vet clean. Pure-Go tests pass.projects-detail.tsx (the bug surface): - Team-add dropdown switches from 7 mixed values (lead/associate/pa/of_counsel/local_counsel/expert/observer) to 4 responsibility-only values (lead/member/observer/external). Default 'member'. Closes m's bug — staffing a person no longer pretends to define their firm tier. - Team table gains a Profession column (between Name and Responsibility), so the firm-tier badge is glanceable at staffing time. - form.team-profession-hint surfaces the picked person's profession or warns when none is set ("kann keine 4-Augen-Genehmigungen erteilen"). projects-detail.ts: - ProjectTeamMember type gains responsibility + user_profession. Legacy .role field kept readable for the deprecation window but UI no longer uses it. - renderTeam renders 3-column tabular layout. Profession pill is read-only (.projekt-team-profession[--none]); responsibility is visible inline (inline-edit deferred to follow-up). - canManagePartnerUnits switches from m.role==="lead" to m.responsibility==="lead". - Team-add submit posts {responsibility} instead of {role}. admin-team.tsx + client/admin-team.ts: - New Profession column with inline-edit dropdown (6 values + "(extern)" NULL option). User type extends with profession?: string|null. - Read-only cell uses .projekt-team-profession pill with "(extern)" placeholder for NULL. onboarding.tsx + client/onboarding.ts: - New required profession <select> with default 'associate'. Six values match the new enum. Hint copy explains the difference from job_title. - POST /api/onboarding payload gains profession field. i18n.ts: ~30 new keys DE+EN — projects.team.profession.* / .responsibility.* / projects.detail.team.col.profession / .responsibility / .form.responsibility / .form.profession.* / admin.team.col.profession.* / onboarding.profession.* / projects.team.profession.none + .hint variants. CSS: - .projekt-team-profession pill (firm-tier, read-only). - .projekt-team-profession--none italic-dashed for NULL professions. - .projekt-team-responsibility pill (per-project). - .form-hint--warning for the team-add no-profession warning. Build: bun build.ts clean (1723 i18n keys, all referenced). go build + go vet + go test (pure-Go) clean.Mark the legacy Role* constants in project_service.go as DEPRECATED. They stay defined for one release because team_service.go still writes the deprecated shadow column via legacyRoleFromResponsibility; follow-up migration 058 (t-paliad-149) retires both the column and the constants. Final grep sweep clean: no live-code call sites remaining for project_teams.role outside of: - the deprecated legacyRoleFromResponsibility mapper (intentional) - team_service.go RETURNING + SELECT (reads the shadow column for the JSON .role field still surfaced for the deprecation window) - migrations 018/023/054/055 (historical, not modified) Test suite green across all packages: auth, branding, calc, changelog, handlers, offices, services. Frontend bun build clean (1723 i18n keys).View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.