Skip to content

feat(ogar-vocab): PortSpec trait + OpenProjectPort/RedminePort#87

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/port-spec-trait
Jun 21, 2026
Merged

feat(ogar-vocab): PortSpec trait + OpenProjectPort/RedminePort#87
AdaWorldAPI merged 2 commits into
mainfrom
claude/port-spec-trait

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

PortSpec trait + the first two impls

Per the user's direction: enforce the migration of WoaBridge /
MedcareBridge style per-port bridges to a generic
UnifiedBridge<P: PortSpec> harness where the differences between
bridges come from the OGAR class schema, not from per-bridge hardcoded
tables.

This PR delivers the OGAR side of that migration. The
lance-graph-ontology side (define UnifiedBridge<P>, replace
OpenProjectBridge/RedmineBridge with type aliases) lands as a
follow-up PR depending on this one.

What ships

pub trait PortSpec: 'static + Send + Sync {
    const NAMESPACE: &'static str;      // "OpenProject" / "Redmine"
    const BRIDGE_ID: &'static str;      // "openproject" / "redmine"
    fn aliases() -> &'static [(&'static str, u16)];
    fn class_id(public_name: &str) -> Option<u16> { /* linear scan */ }
}

pub struct OpenProjectPort;   // impl PortSpec — 25 aliases
pub struct RedminePort;       // impl PortSpec — 25 aliases

Both ports reference the same class_ids::* constants for converging
concepts, so:

OpenProjectPort::class_id("WorkPackage") == RedminePort::class_id("Issue")
                                         == Some(class_ids::PROJECT_WORK_ITEM)
                                         == Some(0x0102)

Convergence test pins (24 concept pairs)

let pairs: &[(&str, &str, u16)] = &[
    ("Project",       "Project",       class_ids::PROJECT),
    ("WorkPackage",   "Issue",         class_ids::PROJECT_WORK_ITEM),
    ("TimeEntry",     "TimeEntry",     class_ids::BILLABLE_WORK_ENTRY),
    ("Status",        "IssueStatus",   class_ids::PROJECT_STATUS),
    ("Type",          "Tracker",       class_ids::PROJECT_TYPE),
    ("Membership",    "Member",        class_ids::PROJECT_MEMBERSHIP),
    ("Relation",      "IssueRelation", class_ids::PROJECT_RELATION),
    ("Forum",         "Board",         class_ids::PROJECT_FORUM),
    // ... 16 more identity pairs
];

What this unlocks (the lance-graph follow-up)

// In lance-graph-ontology, after this lands:
pub struct UnifiedBridge<P: PortSpec> {
    registry: Arc<OntologyRegistry>,
    g_lock: NamespaceId,
    _spec: PhantomData<P>,
}

impl<P: PortSpec> NamespaceBridge for UnifiedBridge<P> {
    fn bridge_id(&self) -> &'static str { P::BRIDGE_ID }
    fn entity(&self, name: &str) -> Result<EntityRef, BridgeError> {
        if let Some(id) = P::class_id(name) { /* synthesize */ }
        else { /* fall back to registry */ }
    }
}

pub type OpenProjectBridge = UnifiedBridge<ogar_vocab::ports::OpenProjectPort>;
pub type RedmineBridge     = UnifiedBridge<ogar_vocab::ports::RedminePort>;

The duplicated OPENPROJECT_CODEBOOK / REDMINE_CODEBOOK tables in
crates/lance-graph-ontology/src/bridges/{openproject,redmine}_bridge.rs
(currently 25 entries each, duplicated across the two files) get
deleted — the canonical class schema is the only source of truth.

Files

  • crates/ogar-vocab/src/ports.rs (new) — trait + two impls + 8 tests
  • crates/ogar-vocab/src/lib.rspub mod ports;

🤖 Generated with Claude Code

Adds `pub mod ports` to ogar-vocab carrying the per-port
specifications consumed by `lance_graph_ontology::UnifiedBridge`.
Each port spec is three pieces of data attached to the canonical
class schema:

    const NAMESPACE: &str       // "OpenProject" / "Redmine"
    const BRIDGE_ID: &str       // "openproject" / "redmine"
    fn aliases() -> &[(&str, u16)]
        // [("WorkPackage", PROJECT_WORK_ITEM), ...]

with a default `class_id(public_name)` linear-scan that ports
inherit unchanged. Both `OpenProjectPort::class_id("WorkPackage")`
and `RedminePort::class_id("Issue")` resolve to
`class_ids::PROJECT_WORK_ITEM` (0x0102) — the cross-fork
convergence pin lives here, sourced from the canonical class_ids
constants rather than re-declared per bridge.

After lance-graph migrates to `UnifiedBridge<P: PortSpec>` (sister
PR), the bridges become trivial type aliases:
    pub type OpenProjectBridge = UnifiedBridge<OpenProjectPort>;
    pub type RedmineBridge = UnifiedBridge<RedminePort>;
and the duplicated `OPENPROJECT_CODEBOOK` / `REDMINE_CODEBOOK`
tables in lance-graph-ontology can be deleted in favour of these
OGAR-sourced specs.

Tests (8) pin the convergence: namespace/bridge_id strings, the
headline WorkPackage/Issue → 0x0102 mapping, full alias-table
convergence across 24 concept pairs (Project ↔ Project,
Status ↔ IssueStatus, Type ↔ Tracker, Forum ↔ Board, etc.),
unknown-name None fall-through, alias drift guard against
class_ids::ALL.

@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: bf1010da8e

ℹ️ 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".

("Project", class_ids::PROJECT),
("WorkPackage", class_ids::PROJECT_WORK_ITEM),
("TimeEntry", class_ids::BILLABLE_WORK_ENTRY),
("User", class_ids::PROJECT_ACTOR),

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 Include Principal and Group actor aliases

When the follow-up UnifiedBridge asks this table for actual Rails actor classes, class_id("Principal") and class_id("Group") return None because User is the only alias for PROJECT_ACTOR. The existing real-source coverage in crates/ogar-from-rails/src/lib.rs (redmine_and_openproject_actors_converge_through_canonical) documents that both Redmine and OpenProject ship User and Principal, and later coverage also folds Group into project_actor; falling back for those names will give them non-codebook entity ids instead of the shared 0x0104.

Useful? React with 👍 / 👎.

Comment thread crates/ogar-vocab/src/ports.rs Outdated
("User", class_ids::PROJECT_ACTOR),
("Status", class_ids::PROJECT_STATUS),
("Type", class_ids::PROJECT_TYPE),
("Priority", class_ids::PRIORITY),

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 Map IssuePriority instead of Priority

The port table is matched with exact string equality, but both Rails curators expose the priority class as IssuePriority, not Priority (see redmine_and_openproject_issuepriority_converge_through_canonical in crates/ogar-from-rails/src/lib.rs). As written, OpenProject returns None for class_id("IssuePriority"), and the Redmine table has no priority alias at all, so the unified bridge will miss the promoted priority codebook id for the actual class name.

Useful? React with 👍 / 👎.

Two codex P2 items on PR #87:

1. Include Principal and Group actor aliases — both Rails curators
   ship `User`, `Principal`, `Group` as three classes backed by
   one table; the codebook collapses them via `PROJECT_ACTOR`
   ("Principal + User + Group STI chain collapsed"). All three
   now route to the same id in both ports.

2. Map IssuePriority instead of Priority — both curators expose
   the priority class as `IssuePriority`, not `Priority`. The
   original OpenProject entry was the canonical-concept name,
   not the Rails class name; Redmine had no priority entry.
   Fixed both.

Also: expose `OPENPROJECT_ALIASES` and `REDMINE_ALIASES` as
`pub const` slices so downstream `pub const` re-exports
(e.g. `lance_graph_ontology::bridges::OPENPROJECT_CODEBOOK` compat
shim) can alias them.

Test deltas:
- `each_port_has_expected_alias_count` — OP=27, RM=28 (Redmine
  carries an extra standalone Comment model).
- New `sti_actor_fold_routes_user_principal_group_to_project_actor`
  walks all three STI-fold names against PROJECT_ACTOR.
- New `issue_priority_resolves_via_actual_rails_class_name`.
- `openproject_and_redmine_converge_on_shared_concepts` extended
  with Principal/Principal, Group/Group, IssuePriority/IssuePriority
  pairs.

All 10 inline ports tests pass under `cargo test -p ogar-vocab --lib ports::`.
@AdaWorldAPI AdaWorldAPI merged commit f4323c0 into main Jun 21, 2026
1 check passed
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.

1 participant