Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions crates/ogar-from-rails/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,46 @@ mod tests {
}
}

/// **Board/Forum convergence** on real source. Cross-curator name
/// divergence (Redmine `Board` ↔ OP `Forum`) unified onto
/// `project_forum`. Closes the parent-container reference from
/// `project_message`.
#[test]
#[ignore = "requires Redmine + OpenProject checkouts"]
fn redmine_board_and_openproject_forum_converge_as_project_forum() {
let Ok(redmine_src) = std::env::var("REDMINE_SRC") else {
eprintln!("skipping: REDMINE_SRC not set");
return;
};
let op_src = std::env::var("OPENPROJECT_SRC")
.unwrap_or_else(|_| "/home/user/openproject".to_string());
let op_path = PathBuf::from(&op_src);
if !op_path.exists() {
eprintln!("skipping: OpenProject not present at {op_src}");
return;
}

let redmine = extract(&PathBuf::from(redmine_src));
let openproject = extract(&op_path);
let id = ogar_vocab::canonical_concept_id("project_forum");
assert!(id.is_some());

let redmine_board = redmine
.iter()
.find(|c| c.name == "Board")
.expect("Redmine ships a Board model");
let op_forum = openproject
.iter()
.find(|c| c.name == "Forum")
.expect("OpenProject ships a Forum model");

for c in [redmine_board, op_forum] {
assert_eq!(c.canonical_concept.as_deref(), Some("project_forum"));
assert_eq!(c.source_domain.as_deref(), Some("project"));
assert_eq!(c.canonical_id(), id);
}
}

/// Exactly-one-Model invariant post AdaWorldAPI/ruff#26: OP's
/// `app/models/work_package/` sub-files reopen `class WorkPackage`
/// without adding ontology declarations; before the fix this produced
Expand Down
40 changes: 39 additions & 1 deletion crates/ogar-vocab/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1088,6 +1088,7 @@ const CODEBOOK: &[(&str, u16)] = &[
("project_watcher", 0x0113), // Redmine Watcher ↔ OP Watcher
("project_news", 0x0114), // Redmine News ↔ OP News
("project_message", 0x0115), // Redmine Message ↔ OP Message (board/forum divergence)
("project_forum", 0x0116), // Redmine Board ↔ OP Forum (name divergence; parent of project_message)

// ── 0x02XX — commerce / billing / ERP domain (OSB ↔ Odoo) ──
// Promoted from the parallel session's `lance-graph-ontology::ar_shape`
Expand Down Expand Up @@ -1600,6 +1601,16 @@ pub fn canonical_concept(name: &str) -> String {
) {
return "project_message".to_string();
}
// ProjectForum — the parent container for [`project_message`].
// Cross-curator name divergence: Redmine `Board`, OpenProject `Forum`
// (same shape — project + last_message + name/description, with
// has_many :messages).
if matches!(lower.as_str(),
"board" | "boards" | "forum" | "forums"
| "project_forum" | "projectforum"
) {
return "project_forum".to_string();
}
// ── Commerce / billing / ERP domain (OSB ↔ Odoo) ──
// CommercialLineItem — line on a commercial document. OSB
// `InvoiceLineItem`, Odoo `account_move_line` (ruff normalises
Expand Down Expand Up @@ -2224,6 +2235,30 @@ pub fn project_message() -> Class {
c
}

/// `Board`/`Forum` — the parent container for [`project_message`].
/// Cross-curator name divergence: Redmine `Board`, OpenProject `Forum`.
/// Universal shape: `belongs_to :project` + `has_many :messages` +
/// `belongs_to :last_message` + `name` / `description` / `position`.
#[must_use]
pub fn project_forum() -> Class {
let mut c = Class::new("ProjectForum");
c.language = Language::Unknown;
c.canonical_concept = Some("project_forum".to_string());
c.associations = vec![
family_edge("project", "Project"),
family_has_many("messages", "ProjectMessage"),

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 Add the missing message-to-forum edge

Adding only the ProjectForumProjectMessage collection edge leaves the new parent-container reference one-way: project_message() still emits only author and last_reply, so consumers that start from a ProjectMessage cannot navigate to its canonical Board/Forum parent even though both Rails surfaces carry that parent association. Please add the corresponding ProjectMessageProjectForum family edge, otherwise the canonical graph still loses the message container relationship this promotion is meant to close.

Useful? React with 👍 / 👎.

family_edge("last_message", "ProjectMessage"),
];
let mut name = Attribute::new("name");
name.type_name = Some("string".to_string());
let mut description = Attribute::new("description");
description.type_name = Some("text".to_string());
let mut position = Attribute::new("position");
position.type_name = Some("integer".to_string());
c.attributes = vec![name, description, position];
c
}

// ─────────────────────────────────────────────────────────────────────
// Commerce / billing / ERP domain canonical classes (OSB ↔ Odoo).
//
Expand Down Expand Up @@ -2849,7 +2884,7 @@ mod tests {
"project_version", "project_wiki_page", "project_query",
"project_attachment", "project_comment", "project_custom_field",
"project_relation", "project_changeset", "project_watcher",
"project_news", "project_message",
"project_news", "project_message", "project_forum",
] {
let id = canonical_concept_id(project_concept)
.unwrap_or_else(|| panic!("{project_concept} missing from codebook"));
Expand Down Expand Up @@ -2900,6 +2935,7 @@ mod tests {
(project_watcher(), "ProjectWatcher", 0x0113),
(project_news(), "ProjectNews", 0x0114),
(project_message(), "ProjectMessage", 0x0115),
(project_forum(), "ProjectForum", 0x0116),
] {
assert_eq!(canonical.name, name);
assert_eq!(canonical.language, Language::Unknown);
Expand Down Expand Up @@ -2980,6 +3016,8 @@ mod tests {
("project_watcher", &["Watcher", "watchers", "ProjectWatcher"]),
("project_news", &["News", "news", "ProjectNews"]),
("project_message", &["Message", "messages", "ProjectMessage"]),
// Cross-curator name divergence: Redmine `Board` ↔ OP `Forum`.
("project_forum", &["Board", "boards", "Forum", "forums", "ProjectForum"]),
];
for (concept, aliases) in cases {
let id = canonical_concept_id(concept).unwrap();
Expand Down
Loading