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
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crates/op-codegen-pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ use ruff_spo_triplet::Triple as RuffTriple;
#[cfg(feature = "ogar-emit")]
pub mod ogar_consumer;

/// Klickweg harvest bridge: run the Rails `navigates_to` harvest (ruff #62)
/// over an OpenProject source tree to produce the board's navigation edges —
/// the harvested source of what `op-server::nav::NAV_EDGES` bakes by hand.
pub mod nav_harvest;

// `lance_graph_contract::codegen_spine::TripletProjection` is implemented
// on the projection type — we don't reference the trait directly here, only
// its associated method, which keeps the import surface narrow.
Expand Down
91 changes: 91 additions & 0 deletions crates/op-codegen-pipeline/src/nav_harvest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Klickweg harvest bridge — Rails navigation topology into op-nexgen.
//!
//! Runs [`ruff_ruby_spo::extract_nav_edges`] (the two-shape Rails
//! `navigates_to` harvest — ERB `link_to`/`button_to` clicks + controller
//! `redirect_to` redirects, ruff #62) over an OpenProject Rails source tree
//! and returns the navigation edges between served screens.
//!
//! This is the **harvested** source of the board's klickweg — the same
//! screen-to-screen connectivity that `op-server::nav::NAV_EDGES` currently
//! encodes **by hand** (derived from the OGAR `ClassView` association graph)
//! and proves connected via the Core brick
//! `lance_graph_contract::class_view::nav_is_fully_connected` (lance-graph
//! #670/#673). When the OpenProject Rails source is present in the pipeline
//! input, these harvested edges are the ground truth and `NAV_EDGES` is their
//! baked mirror; `nav_harvest_probe` proves the harvest reproduces exactly
//! that edge set on a synthetic fixture.
//!
//! ## Two vocabularies, one graph
//!
//! The harvest speaks Rails **resource** stems (`work_packages`, `projects`);
//! op-server's `ClassView` graph speaks **concept** names (`ProjectWorkItem`,
//! `Project`). The mapping is 1:1 ([`RESOURCE_TO_CONCEPT`], the inverse of
//! `op-server::nav::SCREEN_UNIVERSE`), so the connectivity graph is identical
//! under it — a `(work_packages → projects)` harvested edge IS the
//! `(ProjectWorkItem → Project)` ClassView edge.

use std::collections::BTreeSet;
use std::path::Path;

use ruff_ruby_spo::{
extract_nav_edges, extract_nav_edges_with_report, NavScanReport, NavVocab, RubyNavEdge,
};

/// The OpenProject board's served screens, as Rails resource stems — the
/// closed nav vocabulary. 1:1 with `op-server::nav::SCREEN_UNIVERSE` (which
/// names the same screens as `ClassView` concepts; see [`RESOURCE_TO_CONCEPT`]).
pub const OPENPROJECT_SCREENS: &[&str] = &["work_packages", "projects"];

/// Rails resource stem → op-server `ClassView` concept name. The 1:1 bridge
/// between the harvest vocabulary and `op-server::nav::SCREEN_UNIVERSE`
/// (`["ProjectWorkItem", "Project"]`).
pub const RESOURCE_TO_CONCEPT: &[(&str, &str)] = &[
("work_packages", "ProjectWorkItem"),
("projects", "Project"),
];

/// Harvest the klickweg edges from a Rails `app_root`, restricted to the
/// OpenProject served screens ([`OPENPROJECT_SCREENS`]).
#[must_use]
pub fn harvest_klickweg(app_root: &Path) -> Vec<RubyNavEdge> {
extract_nav_edges(app_root, &openproject_vocab())
}

/// Like [`harvest_klickweg`] but also returns the [`NavScanReport`] ledger
/// (files scanned, files with edges, raw target references — the honest
/// denominator).
#[must_use]
pub fn harvest_klickweg_with_report(app_root: &Path) -> (Vec<RubyNavEdge>, NavScanReport) {
extract_nav_edges_with_report(app_root, &openproject_vocab())
}

/// The distinct `(source, target)` screen pairs of a harvested edge set — the
/// connectivity graph, shape-agnostic (ERB-click and controller-redirect
/// edges between the same two screens collapse to one pair). This is what
/// must match `op-server::nav::NAV_EDGES`' connectivity.
#[must_use]
pub fn klickweg_pairs(edges: &[RubyNavEdge]) -> BTreeSet<(String, String)> {
edges
.iter()
.map(|e| (e.source.clone(), e.target.clone()))
.collect()
}

/// The op-server `ClassView` concept name for a Rails resource stem, or
/// `None` if it is not a served screen.
#[must_use]
pub fn concept_for(resource: &str) -> Option<&'static str> {
RESOURCE_TO_CONCEPT
.iter()
.find(|(r, _)| *r == resource)
.map(|(_, c)| *c)
}

fn openproject_vocab() -> NavVocab {
NavVocab {
screens: OPENPROJECT_SCREENS
.iter()
.map(|s| (*s).to_string())
.collect(),
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class WorkPackagesController < ApplicationController
def create
@work_package = WorkPackage.new(wp_params)
if @work_package.save
redirect_to projects_path
else
render :new
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<h1><%= @project.name %></h1>
<%= link_to "Work packages", work_packages_path(project_id: @project.id) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<h1>Work packages</h1>
<ul>
<% @work_packages.each do |wp| %>
<li><%= wp.subject %></li>
<% end %>
</ul>
<%= link_to "Back to project", project_path(@project) %>
93 changes: 93 additions & 0 deletions crates/op-codegen-pipeline/tests/nav_harvest_probe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Probe: the Rails `navigates_to` harvest (ruff #62) reproduces the board's
//! klickweg — the same screen graph `op-server::nav::NAV_EDGES` bakes by hand.
//!
//! Drives a synthetic OpenProject Rails fixture (`tests/fixtures/rails_nav/`,
//! no proprietary source) through `op_codegen_pipeline::nav_harvest` and
//! asserts:
//! 1. the harvested `(source, target)` screen pairs are EXACTLY the
//! OpenProject board klickweg (`work_packages ↔ projects`), both
//! directions — proving the harvest yields the connectivity that
//! `op-server::nav::NAV_EDGES` encodes by hand;
//! 2. both harvest shapes fire (ERB `link_to` click + controller
//! `redirect_to` redirect);
//! 3. the resource→concept bridge maps every harvested screen onto an
//! `op-server::nav::SCREEN_UNIVERSE` concept.

use std::path::PathBuf;

use op_codegen_pipeline::nav_harvest::{
concept_for, harvest_klickweg_with_report, klickweg_pairs, OPENPROJECT_SCREENS,
};
use ruff_ruby_spo::NavShape;

fn fixture_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/rails_nav")
}

/// The harvested screen pairs are exactly the OpenProject board klickweg —
/// `work_packages ↔ projects`, both directions.
#[test]
fn harvest_reproduces_the_board_klickweg() {
let (edges, report) = harvest_klickweg_with_report(&fixture_root());
let pairs = klickweg_pairs(&edges);

let expected: std::collections::BTreeSet<(String, String)> =
[("work_packages", "projects"), ("projects", "work_packages")]
.iter()
.map(|(s, t)| ((*s).to_string(), (*t).to_string()))
.collect();

assert_eq!(
pairs, expected,
"harvested klickweg must be exactly work_packages <-> projects; got {edges:?}"
);

// The ledger accounts for the fixture: 2 ERB views + 1 controller, every
// file producing at least one edge.
assert_eq!(report.erb_files, 2, "{report:?}");
assert_eq!(report.controller_files, 1, "{report:?}");
assert_eq!(report.files_with_edges, 3, "{report:?}");
}

/// Both harvest shapes fire: the ERB `link_to` click edges AND the controller
/// `redirect_to` redirect edge are all present (Ruby's two-shape harvest, not
/// a single body-walk).
#[test]
fn both_harvest_shapes_are_present() {
let (edges, _) = harvest_klickweg_with_report(&fixture_root());

assert!(
edges.iter().any(|e| e.shape == NavShape::ErbClick),
"expected at least one ERB click edge: {edges:?}"
);
assert!(
edges
.iter()
.any(|e| e.shape == NavShape::ControllerRedirect),
"expected the controller redirect edge (work_packages -> projects): {edges:?}"
);

// The redirect edge specifically is work_packages -> projects.
assert!(
edges.iter().any(|e| e.shape == NavShape::ControllerRedirect
&& e.source == "work_packages"
&& e.target == "projects"),
"controller redirect must be work_packages -> projects: {edges:?}"
);
}

/// Every served screen maps 1:1 onto an `op-server::nav::SCREEN_UNIVERSE`
/// concept — the harvest (resource-stem) and the ClassView graph
/// (concept-name) speak the same connectivity graph under the bridge.
#[test]
fn every_screen_bridges_to_a_classview_concept() {
for resource in OPENPROJECT_SCREENS {
assert!(
concept_for(resource).is_some(),
"served screen {resource} has no ClassView concept in RESOURCE_TO_CONCEPT"
);
}
assert_eq!(concept_for("work_packages"), Some("ProjectWorkItem"));
assert_eq!(concept_for("projects"), Some("Project"));
assert_eq!(concept_for("not_a_screen"), None);
}
11 changes: 11 additions & 0 deletions crates/op-server/src/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ pub fn screen_id(concept: &str) -> Option<u8> {
/// class; `nav_edges_match_static_table` cross-checks this table against the
/// live derivation so it can never silently drift. `inputs` is `&'static`
/// (the Core edge shape), so this is a plain `const` manifest.
///
/// **Baked mirror of the harvest.** This table is the hand-authored (from the
/// `ClassView` association graph) equivalent of what
/// `op_codegen_pipeline::nav_harvest::harvest_klickweg` produces from the
/// OpenProject Rails source via the `ruff_ruby_spo` `navigates_to` harvest
/// (ruff #62). `op-codegen-pipeline`'s `nav_harvest_probe` proves the harvest
/// yields exactly the `work_packages ↔ projects` klickweg these edges encode
/// (`RESOURCE_TO_CONCEPT` bridges the Rails resource stems to the
/// `ProjectWorkItem`/`Project` concepts here). Once the OpenProject source is
/// in the pipeline input, this manifest regenerates from that harvest rather
/// than being hand-authored.
pub const NAV_EDGES: &[ComputeEdge] = &[
// ProjectWorkItem.project (belongs_to) → Project
ComputeEdge {
Expand Down