Profession vs project responsibility — split project_teams.role into firm-level profession + project-level responsibility #6

Open
opened 2026-05-06 15:02:54 +00:00 by mAi · 3 comments
Collaborator

Problem

m's bug report (2026-05-06 16:58):

"The Role should not be definable there. Whether a team member is PA or Associate etc is not defined when adding existing members. Roles for the project, maybe. But not the 'profession'."

The team-add form on /projects/{id} has a role dropdown with values that mix two distinct axes:

Current dropdown value What it actually represents
lead Project-level position
associate Profession (career tier)
pa Profession
of_counsel Profession
local_counsel Profession (external attorney)
expert Profession (technical witness)
observer Project-level position

A user IS an Associate or a PA at the firm — that's their career tier, not something you redefine when staffing them on a matter. What changes per project is their responsibility (lead vs. member vs. observer).

Existing data axes (post t-138 + t-139)

  • paliad.users.global_rolestandard | global_admin — tool admin gate only
  • paliad.users.job_title — free-text display, never gates anything
  • paliad.project_teams.role — current single column doing two jobs (the bug)
  • paliad.partner_unit_members.unit_role — added by t-139 Phase 2: lead | attorney | senior_pa | pa | paralegal — closer to "profession" axis but only meaningful when a unit is involved
  • t-138's ApprovalService.canApprove() strict ladder: lead(5) > of_counsel(4) > associate(3) > senior_pa(2) > pa(1) > local_counsel/expert/observer(0) — keys off project_teams.role

So project_teams.role is currently:

  • The label shown on the team-add dropdown (m's UX complaint)
  • The driver of approval authority (t-138 hard dependency)
  • The thing inherited down the project tree

Goals

  1. Separate profession from project responsibility. Profession lives in one column (firm-wide), project responsibility lives in another (per-project).
  2. Team-add dropdown shows only project responsibility. No PA / Associate / Of Counsel options — those aren't your call when staffing.
  3. Profession surfaces automatically. When you pick "Anna Schmidt" from the autocomplete, her profession (read from her firm record) shows next to her name in the team table.
  4. Approval ladder stays intact. Whatever value drives t-138's approval authority continues to work — but that value should come from profession, not from the project-team-add form.

Locked decisions (m, 2026-05-06)

  • Profession is not redefined per project. It comes from the user's firm-level record.
  • Project-level role is meaningful. Stays editable per project, but with a smaller value set focused on responsibility.

Open design questions (for inventor — m will answer in design pass)

Profession axis

  1. Where does profession live? Three candidates:
    • (a) New paliad.users.profession column (firm-wide, simple)
    • (b) Reuse paliad.partner_unit_members.unit_role (already added in t-139 Phase 2; only set when the user is in a unit)
    • (c) New separate paliad.user_professions(user_id, profession, valid_from) table for history
    • Inventor recommendation, m signs off.
  2. What are the profession values? From the t-139 unit_role enum + t-138 ladder — likely partner | of_counsel | associate | senior_pa | pa | paralegal. External roles (local_counsel, expert) — are those professions or project-only labels? Likely the latter.
  3. Onboarding flow. When a new user is invited, who fills in their profession? Auto-default to "associate" with admin-edit, or required-on-invite?

Project responsibility axis

  1. What's the value set? Likely lead | member | observer | external or similar — flat, not a ladder. Inventor proposes, m signs off.
  2. Default value. What does the team-add dropdown default to when staffing? member is the natural choice.
  3. Display. In the team table, what's shown? "Anna Schmidt — PA · Lead on this project"? Or just "Anna Schmidt — Lead"? UX call.

Approval ladder coordination (t-138 cross-cut)

  1. Does the ladder migrate from project_teams.role to the new profession column? If yes, all references in ApprovalService.canApprove(), approval_role_level(), the policy authoring page, and the inbox SQL move to the new source. Inventor maps the rename.
  2. Or does the ladder become a tuple of (profession, project_responsibility)? E.g. "this approval needs an associate-level lawyer who is at least a member on this project". Allows finer policy authoring; more complex.

Migration

  1. Backfill plan for the rename. Every existing project_teams row carries a value from the legacy enum. The script:
    • For each row, copy project_teams.role → user's new profession (if not already set), AND
    • Map project_teams.role → new project_responsibility:
      • leadlead
      • observerobserver
      • everything else → member
    • Rule: when a user appears on multiple projects with different legacy roles, profession defaults to the highest-tier observed.
    • Inventor: confirm the rule, propose tie-breaks.
  2. Down-migration safety. Schema migration must be reversible (per the project's rules). Inventor confirms.

UX

  1. Team table layout post-fix. Three columns?: User · Profession (read-only badge) · Project responsibility (editable inline). Or just User · Responsibility with profession as a hover-tooltip badge?
  2. Bulk add / invite-new flow. Today shannon's invite affordance pre-fills email but doesn't ask for profession. New flow needs profession capture during invite OR a default + admin-edit later.

Out of scope (v1)

  • Replacing the existing partner-unit-derivation mechanism (t-139 Phase 2) — derivation stays as designed.
  • A full firm-roles / hierarchy / org-chart feature — just the profession column.
  • Multi-profession (a paralegal-turned-associate scenario). One profession per user; admin can edit when promoted.
  • Time-sliced profession history (who was a PA in 2024 — separate table only if Q1c is chosen).

References

  • paliad.project_teams.role (current single source) — internal/services/team_service.go, migration 018
  • paliad.partner_unit_members.unit_role (t-139 Phase 2) — migration 055
  • ApprovalService.canApprove() (t-138) — internal/services/approval_service.go
  • internal/services/approval_levels.go — strict ladder definition
  • frontend/src/projects-detail.tsx:114-123 — current team-add dropdown options (the UX surface m complained about)
  • t-138 design doc docs/design-approvals-2026-05-06.md §2 — the role taxonomy locked at the time

Inventor brief

  • Role: inventor
  • Worker assignment is HELD until m signs off the direction. Then assign a fresh inventor (NOT cronus per memory: cronus retired from paliad).
  • Branch convention: mai/<inventor>/inventor-profession-vs-project-role
  • Deliverable: docs/design-profession-vs-project-role-2026-05-06.md. Three sub-designs:
    1. Profession schema + onboarding flow (Q1–Q3, Q12)
    2. Project responsibility schema + UX (Q4–Q6, Q11)
    3. Approval ladder rename + migration plan (Q7–Q10) — explicit coordination with t-138
  • Inventor STOPs after design. Awaits m's go on the design before any coder shift.
## Problem m's bug report (2026-05-06 16:58): > "The Role should not be definable there. Whether a team member is PA or Associate etc is not defined when adding existing members. Roles for the project, maybe. But not the 'profession'." The team-add form on `/projects/{id}` has a role dropdown with values that mix two distinct axes: | Current dropdown value | What it actually represents | |---|---| | `lead` | Project-level position | | `associate` | Profession (career tier) | | `pa` | Profession | | `of_counsel` | Profession | | `local_counsel` | Profession (external attorney) | | `expert` | Profession (technical witness) | | `observer` | Project-level position | A user IS an Associate or a PA at the firm — that's their career tier, not something you redefine when staffing them on a matter. What changes per project is their *responsibility* (lead vs. member vs. observer). ## Existing data axes (post t-138 + t-139) - `paliad.users.global_role` — `standard | global_admin` — tool admin gate only - `paliad.users.job_title` — free-text display, never gates anything - **`paliad.project_teams.role`** — current single column doing two jobs (the bug) - `paliad.partner_unit_members.unit_role` — added by t-139 Phase 2: `lead | attorney | senior_pa | pa | paralegal` — closer to "profession" axis but only meaningful when a unit is involved - t-138's `ApprovalService.canApprove()` strict ladder: `lead(5) > of_counsel(4) > associate(3) > senior_pa(2) > pa(1) > local_counsel/expert/observer(0)` — keys off `project_teams.role` So `project_teams.role` is currently: - The label shown on the team-add dropdown (m's UX complaint) - The driver of approval authority (t-138 hard dependency) - The thing inherited down the project tree ## Goals 1. **Separate profession from project responsibility.** Profession lives in one column (firm-wide), project responsibility lives in another (per-project). 2. **Team-add dropdown shows only project responsibility.** No PA / Associate / Of Counsel options — those aren't your call when staffing. 3. **Profession surfaces automatically.** When you pick "Anna Schmidt" from the autocomplete, her profession (read from her firm record) shows next to her name in the team table. 4. **Approval ladder stays intact.** Whatever value drives t-138's approval authority continues to work — but that value should come from profession, not from the project-team-add form. ## Locked decisions (m, 2026-05-06) - **Profession is not redefined per project.** It comes from the user's firm-level record. - **Project-level role is meaningful.** Stays editable per project, but with a smaller value set focused on responsibility. ## Open design questions (for inventor — m will answer in design pass) ### Profession axis 1. **Where does profession live?** Three candidates: - (a) New `paliad.users.profession` column (firm-wide, simple) - (b) Reuse `paliad.partner_unit_members.unit_role` (already added in t-139 Phase 2; only set when the user is in a unit) - (c) New separate `paliad.user_professions(user_id, profession, valid_from)` table for history - Inventor recommendation, m signs off. 2. **What are the profession values?** From the t-139 unit_role enum + t-138 ladder — likely `partner | of_counsel | associate | senior_pa | pa | paralegal`. External roles (`local_counsel`, `expert`) — are those professions or project-only labels? Likely the latter. 3. **Onboarding flow.** When a new user is invited, who fills in their profession? Auto-default to "associate" with admin-edit, or required-on-invite? ### Project responsibility axis 4. **What's the value set?** Likely `lead | member | observer | external` or similar — flat, not a ladder. Inventor proposes, m signs off. 5. **Default value.** What does the team-add dropdown default to when staffing? `member` is the natural choice. 6. **Display.** In the team table, what's shown? "Anna Schmidt — PA · Lead on this project"? Or just "Anna Schmidt — Lead"? UX call. ### Approval ladder coordination (t-138 cross-cut) 7. **Does the ladder migrate from `project_teams.role` to the new profession column?** If yes, all references in `ApprovalService.canApprove()`, `approval_role_level()`, the policy authoring page, and the inbox SQL move to the new source. Inventor maps the rename. 8. **Or does the ladder become a tuple of (profession, project_responsibility)?** E.g. "this approval needs an associate-level lawyer who is at least a member on this project". Allows finer policy authoring; more complex. ### Migration 9. **Backfill plan for the rename.** Every existing `project_teams` row carries a value from the legacy enum. The script: - For each row, copy `project_teams.role` → user's new profession (if not already set), AND - Map `project_teams.role` → new project_responsibility: - `lead` → `lead` - `observer` → `observer` - everything else → `member` - Rule: when a user appears on multiple projects with different legacy roles, profession defaults to the highest-tier observed. - Inventor: confirm the rule, propose tie-breaks. 10. **Down-migration safety.** Schema migration must be reversible (per the project's rules). Inventor confirms. ### UX 11. **Team table layout post-fix.** Three columns?: User · Profession (read-only badge) · Project responsibility (editable inline). Or just User · Responsibility with profession as a hover-tooltip badge? 12. **Bulk add / invite-new flow.** Today shannon's invite affordance pre-fills email but doesn't ask for profession. New flow needs profession capture during invite OR a default + admin-edit later. ## Out of scope (v1) - Replacing the existing partner-unit-derivation mechanism (t-139 Phase 2) — derivation stays as designed. - A full firm-roles / hierarchy / org-chart feature — just the profession column. - Multi-profession (a paralegal-turned-associate scenario). One profession per user; admin can edit when promoted. - Time-sliced profession history (who was a PA in 2024 — separate table only if Q1c is chosen). ## References - `paliad.project_teams.role` (current single source) — `internal/services/team_service.go`, migration 018 - `paliad.partner_unit_members.unit_role` (t-139 Phase 2) — migration 055 - `ApprovalService.canApprove()` (t-138) — `internal/services/approval_service.go` - `internal/services/approval_levels.go` — strict ladder definition - `frontend/src/projects-detail.tsx:114-123` — current team-add dropdown options (the UX surface m complained about) - t-138 design doc `docs/design-approvals-2026-05-06.md` §2 — the role taxonomy locked at the time ## Inventor brief - Role: inventor - **Worker assignment is HELD until m signs off the direction.** Then assign a fresh inventor (NOT cronus per memory: cronus retired from paliad). - Branch convention: `mai/<inventor>/inventor-profession-vs-project-role` - Deliverable: `docs/design-profession-vs-project-role-2026-05-06.md`. Three sub-designs: 1. Profession schema + onboarding flow (Q1–Q3, Q12) 2. Project responsibility schema + UX (Q4–Q6, Q11) 3. Approval ladder rename + migration plan (Q7–Q10) — explicit coordination with t-138 - Inventor STOPs after design. Awaits m's go on the design before any coder shift.
mAi self-assigned this 2026-05-06 15:02:54 +00:00
Author
Collaborator

Inventor design ready for review — kepler shift-1 (2026-05-07)

Doc: docs/design-profession-vs-project-role-2026-05-07.md (841 lines, commit 1eb43ce on mai/kepler/inventor-profession-vs).

TL;DR

Split paliad.project_teams.role into:

  1. paliad.users.profession — firm-wide career tier (partner | of_counsel | associate | senior_pa | pa | paralegal | NULL). Drives the t-138 approval ladder. NULL = external (level 0).
  2. paliad.project_teams.responsibility — per-project (lead | member | observer | external). Default member. Replaces the team-add dropdown values m complained about.

Approval ladder evaluated as a tuple-with-gate: effective_level = profession_level IF responsibility ∈ {lead, member} ELSE 0. Policy grammar from t-138 (required_role single value) stays unchanged.

Single migration 057. project_teams.role kept as deprecated shadow for one release; dropped in follow-up 058.

Verified live state

  • project_teams: 3 rows, all role='lead'. Backfill is trivial.
  • partner_unit_members: 20 rows, all default unit_role='attorney'. Bridge unchanged.
  • t-138 (e2e1381) and t-139 phases all merged on main. No blockers.
  • Migration tracker at 56, next is 057.

12 open questions — recommendation summary

# Question Recommendation
Q1 Where does profession live? (a) New users.profession column
Q2 Profession values partner / of_counsel / associate / senior_pa / pa / paralegal (NULL = external)
Q3 Onboarding flow Required-on-invite, default associate
Q4 Project responsibility values lead / member / observer / external
Q5 Default value member
Q6 Display 3-column tabular: Name · Profession (read-only badge) · Responsibility (inline-edit)
Q7 vs Q8 Ladder migration Q7 (rename to profession) WITH responsibility as a binary gate
Q9 Backfill Profession = highest legacy tier; responsibility = single-row map
Q10 Down-migration Reversible with documented best-effort data loss
Q11 Team table layout 3-column tabular (rejecting tooltip-only profession)
Q12 Bulk add / invite Profession capture on invite (default associate, "Extern" hides field)

Any pattern-fluent coder. NOT cronus (retired from paliad per memory directive). Sonnet work — 70% mechanical rename, 30% new SQL function + 4 ladder-site rewrites + new team-table layout. Single PR, 6 commits.

Inventor parked

Awaiting m's pass through the 12 questions in §10. After sign-off, design locks and head can dispatch a fresh coder shift on this branch.

**Inventor design ready for review** — kepler shift-1 (2026-05-07) Doc: `docs/design-profession-vs-project-role-2026-05-07.md` (841 lines, commit 1eb43ce on `mai/kepler/inventor-profession-vs`). ### TL;DR Split `paliad.project_teams.role` into: 1. **`paliad.users.profession`** — firm-wide career tier (`partner | of_counsel | associate | senior_pa | pa | paralegal | NULL`). Drives the t-138 approval ladder. NULL = external (level 0). 2. **`paliad.project_teams.responsibility`** — per-project (`lead | member | observer | external`). Default `member`. Replaces the team-add dropdown values m complained about. Approval ladder evaluated as a **tuple-with-gate**: `effective_level = profession_level IF responsibility ∈ {lead, member} ELSE 0`. Policy grammar from t-138 (`required_role` single value) stays unchanged. Single migration **057**. `project_teams.role` kept as deprecated shadow for one release; dropped in follow-up 058. ### Verified live state - `project_teams`: 3 rows, all `role='lead'`. Backfill is trivial. - `partner_unit_members`: 20 rows, all default `unit_role='attorney'`. Bridge unchanged. - t-138 (`e2e1381`) and t-139 phases all merged on main. No blockers. - Migration tracker at 56, next is 057. ### 12 open questions — recommendation summary | # | Question | Recommendation | |---|---|---| | Q1 | Where does profession live? | (a) New `users.profession` column | | Q2 | Profession values | `partner / of_counsel / associate / senior_pa / pa / paralegal` (NULL = external) | | Q3 | Onboarding flow | Required-on-invite, default `associate` | | Q4 | Project responsibility values | `lead / member / observer / external` | | Q5 | Default value | `member` | | Q6 | Display | 3-column tabular: Name · Profession (read-only badge) · Responsibility (inline-edit) | | Q7 vs Q8 | Ladder migration | Q7 (rename to profession) WITH responsibility as a binary gate | | Q9 | Backfill | Profession = highest legacy tier; responsibility = single-row map | | Q10 | Down-migration | Reversible with documented best-effort data loss | | Q11 | Team table layout | 3-column tabular (rejecting tooltip-only profession) | | Q12 | Bulk add / invite | Profession capture on invite (default `associate`, "Extern" hides field) | ### Recommended implementer Any pattern-fluent coder. **NOT cronus** (retired from paliad per memory directive). Sonnet work — 70% mechanical rename, 30% new SQL function + 4 ladder-site rewrites + new team-table layout. Single PR, 6 commits. ### Inventor parked Awaiting m's pass through the 12 questions in §10. After sign-off, design locks and head can dispatch a fresh coder shift on this branch.
Author
Collaborator

DESIGN LOCKED — m approved all 12 recommendations verbatim (2026-05-07 21:35): "lets go with those - but if you are fine, go for shift". m authorized kepler to take the coder shift. Status flipped from READY-FOR-REVIEW to LOCKED.

Proceeding with implementation on mai/kepler/inventor-profession-vs. Single PR, 6 commits per §7. Will merge to main after final verification.

**DESIGN LOCKED** — m approved all 12 recommendations verbatim (2026-05-07 21:35): *"lets go with those - but if you are fine, go for shift"*. m authorized kepler to take the coder shift. Status flipped from READY-FOR-REVIEW to LOCKED. Proceeding with implementation on `mai/kepler/inventor-profession-vs`. Single PR, 6 commits per §7. Will merge to main after final verification.
Author
Collaborator

SHIPPED — PR ready for merge 🟢

Kepler completed implementation in single shift. PR: #11

6 commits on mai/kepler/inventor-profession-vs:

  1. ab2530f — Migration 057 (schema + backfill + SQL fn)
  2. 6506864 — ApprovalService + DerivationService
  3. e6937d2 — TeamService + UserService + Models + Handlers
  4. 9184e9b — Reminder + Deadline + Derivation cleanup
  5. 2af4bf1 — Frontend (the bug surface — team-add dropdown)
  6. 0b4de1c — Deprecation notes + grep sweep

+1969 / -247 lines · 23 files · mergeable: true

Verification clean:

  • go build ./...
  • go vet ./...
  • go test ./... ✓ (all 7 packages, including new tests for profession ladder + responsibility gate + NULL trap)
  • bun build.ts ✓ (1723 i18n keys, all referenced)
  • Migration 057 BEGIN/ROLLBACK live-DB dry-run verified (commit 1)

Ready for m's merge → Dokploy deploy → migration 057 applies on boot. Follow-up: file t-paliad-149 for migration 058 to drop the deprecated shadow column after one release of soak time.

**SHIPPED — PR ready for merge** 🟢 Kepler completed implementation in single shift. PR: https://mgit.msbls.de/m/paliad/pulls/11 6 commits on `mai/kepler/inventor-profession-vs`: 1. `ab2530f` — Migration 057 (schema + backfill + SQL fn) 2. `6506864` — ApprovalService + DerivationService 3. `e6937d2` — TeamService + UserService + Models + Handlers 4. `9184e9b` — Reminder + Deadline + Derivation cleanup 5. `2af4bf1` — Frontend (the bug surface — team-add dropdown) 6. `0b4de1c` — Deprecation notes + grep sweep **+1969 / -247** lines · **23 files** · `mergeable: true` **Verification clean:** - `go build ./...` ✓ - `go vet ./...` ✓ - `go test ./...` ✓ (all 7 packages, including new tests for profession ladder + responsibility gate + NULL trap) - `bun build.ts` ✓ (1723 i18n keys, all referenced) - Migration 057 BEGIN/ROLLBACK live-DB dry-run verified (commit 1) Ready for m's merge → Dokploy deploy → migration 057 applies on boot. Follow-up: file t-paliad-149 for migration 058 to drop the deprecated shadow column after one release of soak time.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: m/paliad#6
No description provided.