Skip to content

feat(ogar-vocab): ProjectRole (RBAC) + Group folds into ProjectActor#72

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-vocab-role-group
Jun 19, 2026
Merged

feat(ogar-vocab): ProjectRole (RBAC) + Group folds into ProjectActor#72
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-vocab-role-group

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

ProjectRole (RBAC) + Group folds into ProjectActor

Two related actor/authorization promotions — one new canonical concept, one STI-collapse:

Curator class → Canonical id Note
Role project_role 0x0117 NEW RBAC permission-set class
Group project_actor 0x0104 folds in (Principal STI subtype)

project_role — 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 shape: name (string) + position (integer) + permissions (text, coarse) + family edge memberships → ProjectMembership.

Naming note: distinct from ogar_from_ruff::project_role, which is a helper mapping a curator association name to a ProjectWorkItem family-edge role. This is the authorization Role model. Documented on both the resolver arm and the class fn (different crates, no Rust collision).

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 (rather than minting a redundant project_group).

Tests

  • project_actor_resolver_collapses_user_principal_sti_chain extended with Group / Groups.
  • Domain / batch-shape / resolver-arm coverage 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.

Green

ogar-vocab 41 · ogar-from-ruff 24 · ogar-from-rails 14 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

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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's ConceptDomain matches the concept's home domain (high byte); otherwise it falls back to lexical_concept. lift_model_graph now re-resolves through it with the curator's domain (source_domain_concept), so a generic Role in a non-project app stays role, not project_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 via is_cross_domain_concept; domain-specific concepts are not.
  • lift_model stays domain-blind (its contract/tests unchanged); the gate lives at lift_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.
@AdaWorldAPI AdaWorldAPI merged commit a1f62ae into main Jun 19, 2026
1 check passed
AdaWorldAPI pushed a commit that referenced this pull request Jun 19, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants