feat(ogar-vocab): ProjectRole (RBAC) + Group folds into ProjectActor#72
Conversation
Two related actor/authorization promotions from the probe: | Curator class | -> Canonical | id | Note | |---------------|-----------------|---------|-------------------------------| | Role | project_role | 0x0117 | NEW RBAC permission-set class | | Group | project_actor | 0x0104 | folds in (Principal STI subtype) | ProjectRole (new canonical class): - Both curators ship Role with has_many :member_roles + :members through, a name, and a permission set (Redmine serializes inline; OP normalizes into role_permissions). Canonical: name (string), position (integer), permissions (text, coarse), family edge memberships -> ProjectMembership. - Distinct from ogar_from_ruff::project_role (a helper mapping a curator association NAME to a ProjectWorkItem family-edge role) — documented on both the resolver arm and the class fn. Group folds into project_actor (NOT a new concept): - Group < Principal in both curators — an STI subtype of the same actor root that already collapses User + Principal. A Group is assignable / member-able exactly where a User is; its has_many :users aggregation is curator-local structure the canonical layer abstracts over. Consistent with the existing STI-collapse doctrine. - Added group/groups to the project_actor resolver arm. Tests: - project_actor_resolver_collapses_user_principal_sti_chain extended with Group / Groups. - codebook_ids_are_domain_prefixed_and_consistent + batch-shape + resolver-arms tests extended with project_role. - ogar-from-rails real-corpus: redmine_and_openproject_role_and_group_- converge asserts Role -> project_role and Group -> project_actor on both curators' actual extracts. ogar-vocab 41, ogar-from-ruff 24, real-corpus 14 (+1); clippy clean. Project-mgmt block (0x01XX) now 23 concepts.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 553c8591c4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // NAME to a ProjectWorkItem family-edge role — this is the | ||
| // authorization concept (the `Role` model), not a graph-edge role. | ||
| if matches!(lower.as_str(), | ||
| "role" | "roles" | "project_role" | "projectrole" |
There was a problem hiding this comment.
Avoid globally promoting bare Role
This bare alias has no project-domain guard: ogar_from_ruff::lift_model calls canonical_concept(&model.name) for every harvested model and stores source_domain separately, so any non-Redmine/OpenProject app with a generic Role class now receives project_role/0x0117 and routes as ConceptDomain::ProjectMgmt. Please restrict the promoted arm to the canonical spelling (ProjectRole) or make the Role alias domain-aware before assigning the project-mgmt codebook id.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in d29b741 with option 2 (domain-aware) — option 1 (restrict to ProjectRole) would break the real-corpus convergence, since Redmine ships Role, not ProjectRole.
- New
ogar_vocab::canonical_concept_in_domain(name, domain)gates promotion: a codebook concept is kept only when the curator'sConceptDomainmatches the concept's home domain (high byte); otherwise it falls back tolexical_concept.lift_model_graphnow re-resolves through it with the curator's domain (source_domain_concept), so a genericRolein a non-project app staysrole, notproject_role. - One nuance your suggestion didn't cover:
billable_work_entry(0x0103) is an intentional cross-domain bridge (OP TimeEntry ↔ Odoo account.analytic.line ↔ WoA), so strict gating would have severed its erp witnesses. Bridges are exempt viais_cross_domain_concept; domain-specific concepts are not. lift_modelstays domain-blind (its contract/tests unchanged); the gate lives atlift_model_graph, the entry point that knows the namespace.
Tests: +5 ogar-vocab, +1 ogar-from-ruff (a non-project Role resolves to role), all 14 real-corpus convergence tests still green.
A bare `Role` (or any name colliding with a promoted alias) was being
promoted domain-blind: `ogar_from_ruff::lift_model` calls
`canonical_concept(&model.name)` and stores `source_domain` separately,
so any non-project app with a generic `Role` class received
`project_role`/0x0117 and routed as ConceptDomain::ProjectMgmt.
Make the producer domain-aware (codex's option 2) instead of restricting
the arm to the canonical spelling (option 1 would break the real-corpus
Redmine `Role` -> project_role convergence, since Redmine ships `Role`,
not `ProjectRole`).
ogar-vocab:
- lexical_concept(name): extract Layer-2 fallback as a public fn so a
withheld promotion can fall back to it.
- canonical_concept_in_domain(name, domain): domain-gated resolver. Keeps
a promotion only when the curator's domain matches the concept's
codebook domain; otherwise (foreign domain, or unknown/None) falls back
to lexical. Already-lexical names are unchanged.
- source_domain_concept(tag): map the coarse source_domain string
("project"/"erp"/"german-erp") to its ConceptDomain, kept here (not in
each producer) so it tracks the CODEBOOK layout.
- is_cross_domain_concept + CROSS_DOMAIN_CONCEPTS: bridge exemption.
`billable_work_entry` (0x0103, project-mgmt home) is an intentional
cross-domain bridge — OpenProject TimeEntry, Odoo
account.analytic.line, WoA Arbeitszeit all converge onto it. Gating it
by home domain would sever the erp/german-erp witnesses, so bridges are
exempt; domain-specific concepts (project_role, commercial_document, …)
are not.
ogar-from-ruff:
- lift_model_graph re-resolves each class through
canonical_concept_in_domain with the curator's domain. lift_model
itself stays domain-blind (its contract + tests unchanged).
Tests: ogar-vocab 46 (+5: source_domain_concept, the gate, each domain's
own promotions, bridge survives, lexical_concept parity), ogar-from-ruff
25 (+1: lift_model_graph_domain_gates_the_canonical_concept). All 14
real-corpus convergence tests still pass — Role/Group converge on the
project curators and billable_work_entry bridge intact. Workspace test
green.
…l completers) Final structural-completion batch for the OP <-> Redmine project-mgmt domain. Each completes a relationship around an existing canonical: | Canonical | id | Completes | |--------------------------|---------|------------------------------------| | project_member_role | 0x0118 | RBAC triangle: membership <-> role | | project_custom_value | 0x0119 | value side of project_custom_field | | project_enabled_module | 0x011A | per-project module enablement | All three are identical across both curators (belongs_to joins). - project_member_role: membership -> ProjectMembership, role -> ProjectRole (closes the RBAC triangle now that ProjectRole exists via #72), plus the real inherited_from attribute both curators carry for group-derived role assignments. - project_custom_value: custom_field -> ProjectCustomField, polymorphic customized kept opaque (customized_type discriminator + value text). - project_enabled_module: project -> Project + name (module key). 0x0117 left reserved for project_role (OGAR#72, in flight) — this batch uses 0x0118-0x011A so neither PR rebases regardless of merge order. The member_role -> ProjectRole family edge references the target by string, so it's valid before #72 merges. - Resolver arms covering each curator name + plural-table forms + PascalCase (per the established doctrine). - Extended the 3 coverage tests + new real-corpus test redmine_and_openproject_memberrole_customvalue_module_converge. ogar-vocab 41, ogar-from-ruff 24, real-corpus 14 (+1); clippy clean. The OP <-> Redmine project-mgmt cross-curator overlap is now saturated: every substantive common-name model from the 38-name probe is promoted.
ProjectRole (RBAC) + Group folds into ProjectActor
Two related actor/authorization promotions — one new canonical concept, one STI-collapse:
Roleproject_role0x0117Groupproject_actor0x0104project_role— new canonical classBoth curators ship
Rolewithhas_many :member_roles+:members(through), aname, and a permission set (Redmine serializes inline; OP normalizes intorole_permissions). Canonical shape:name(string) +position(integer) +permissions(text, coarse) + family edgememberships → ProjectMembership.Naming note: distinct from
ogar_from_ruff::project_role, which is a helper mapping a curator association name to aProjectWorkItemfamily-edge role. This is the authorizationRolemodel. Documented on both the resolver arm and the class fn (different crates, no Rust collision).Groupfolds intoproject_actor(not a new concept)Group < Principalin both curators — an STI subtype of the same actor root that already collapsesUser+Principal. A Group is assignable / member-able exactly where a User is; itshas_many :usersaggregation is curator-local structure the canonical layer abstracts over. Consistent with the existing STI-collapse doctrine (rather than minting a redundantproject_group).Tests
project_actor_resolver_collapses_user_principal_sti_chainextended withGroup/Groups.project_role.redmine_and_openproject_role_and_group_convergeassertsRole → project_roleandGroup → project_actoron both curators' actual extracts.Green
ogar-vocab41 ·ogar-from-ruff24 ·ogar-from-rails14 real-corpus (+1) · clippy clean.Codebook is now 25 promoted concepts
Project-mgmt (
0x01XX): 23 · Commerce (0x02XX): 6 (minus the actor-collapse, since Group reuses 0x0104).🤖 Generated with Claude Code