|
| 1 | +# Juniper Monorepo Analysis |
| 2 | + |
| 3 | +**Last Updated:** 2026-02-18 |
| 4 | +**Version:** 1.0.0 |
| 5 | +**Status:** Current |
| 6 | +**Author:** Paul Calnon / Claude Code |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## Table of Contents |
| 11 | + |
| 12 | +- [Executive Summary](#executive-summary) |
| 13 | +- [Current Architecture](#current-architecture) |
| 14 | +- [Assessment by Dimension](#assessment-by-dimension) |
| 15 | + - [Best Practices Alignment](#1-best-practices-alignment) |
| 16 | + - [Maintainability](#2-maintainability) |
| 17 | + - [Potential Problems](#3-potential-problems) |
| 18 | + - [Scalability](#4-scalability) |
| 19 | +- [Recommendations](#recommendations) |
| 20 | +- [Concerns](#concerns) |
| 21 | +- [Strategy Comparison](#strategy-comparison) |
| 22 | +- [Decision](#decision) |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## Executive Summary |
| 27 | + |
| 28 | +The Juniper project uses a **single GitHub repository** (`pcalnon/Juniper.git`) containing three subprojects, each with its own application. This analysis evaluates the current approach against best practices and identifies structural risks that will worsen as the project grows. |
| 29 | + |
| 30 | +**Key Finding:** The current approach is an informal monorepo without monorepo tooling. It carries the complexity costs of a monorepo (merge conflicts, entangled history, shared CI triggers) without the benefits that proper monorepo tooling provides (dependency graphs, selective builds, workspace management). The three subprojects are developed on long-lived branches and maintained as three independent full git clones of the same remote, each tracking a different branch. |
| 31 | + |
| 32 | +**Recommendation:** Migrate to a **multi-repository (polyrepo) architecture** with proper package distribution via PyPI. This is the correct long-term strategy given the project's direction toward service-oriented architecture, where CasCor will run as an independent service communicating via REST API and WebSockets rather than via `sys.path` injection and direct Python imports. |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Current Architecture |
| 37 | + |
| 38 | +### Repository Layout |
| 39 | + |
| 40 | +| Subproject | Application | Version | Role | |
| 41 | +|---|---|---|---| |
| 42 | +| JuniperData | `juniper_data` | 0.4.2 | Dataset generation service (FastAPI, port 8100) | |
| 43 | +| JuniperCascor | `juniper_cascor` | 0.3.17 | CasCor neural network backend (library, no server) | |
| 44 | +| JuniperCanopy | `juniper_canopy` | 0.2.3 | Monitoring dashboard (FastAPI + Dash, port 8050) | |
| 45 | + |
| 46 | +### How It Works Today |
| 47 | + |
| 48 | +- **Single GitHub remote:** All three subprojects share `git@github.com:pcalnon/Juniper.git` |
| 49 | +- **Branch-per-subproject development:** Long-lived branches like `subproject.juniper_cascor.feature.frontend_integration.pre_deploy.spiral_gen_extract.release` |
| 50 | +- **Three full clones locally:** Each subproject is cloned separately, each tracking a different branch |
| 51 | +- **No monorepo tooling:** No Nx, Turborepo, Pants, Bazel, uv workspaces, or hatch workspaces |
| 52 | +- **Per-subproject configuration:** Each has its own `pyproject.toml`, `.github/workflows/`, `.pre-commit-config.yaml`, and `conf/` directory |
| 53 | +- **Cross-project integration via vendoring and sys.path:** |
| 54 | + - `juniper_data_client` is vendored (copied) into Canopy and Cascor |
| 55 | + - Canopy imports CasCor classes via runtime `sys.path` manipulation |
| 56 | + |
| 57 | +### Dependency Graph (Current) |
| 58 | + |
| 59 | +``` |
| 60 | +juniper-data-client (vendored copies in each consumer - no single source of truth) |
| 61 | + ↑ ↑ |
| 62 | +juniper-data juniper-cascor (library, no API) |
| 63 | + ↑ ↑ |
| 64 | + └──── juniper-canopy ┘ (imports CasCor via sys.path injection) |
| 65 | +``` |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## Assessment by Dimension |
| 70 | + |
| 71 | +### 1. Best Practices Alignment |
| 72 | + |
| 73 | +**What the current approach gets right:** |
| 74 | + |
| 75 | +- **Unified visibility** — All code is discoverable in one place |
| 76 | +- **Atomic cross-project changes** — A single commit can theoretically touch all three subprojects |
| 77 | +- **Consistent tooling conventions** — All three use the same linter settings (line-length 512), test framework (pytest), conda environment (`JuniperPython`), and code style |
| 78 | +- **Hierarchical branch naming** — `subproject.<name>.<feature>.<phase>` provides clear organization |
| 79 | + |
| 80 | +**Where it diverges from monorepo best practices:** |
| 81 | + |
| 82 | +- **No workspace/build orchestration** — True monorepos use tools like Nx, Turborepo, Pants, or Bazel to manage inter-project dependencies, selective builds, and caching. Juniper has none of this. |
| 83 | +- **No dependency graph** — There is no declarative way to express that Canopy depends on CasCor and Data, or that changes to Data's client should trigger Canopy and Cascor CI. |
| 84 | +- **Three clones instead of one** — Running three independent `git clone` operations of the same repo, each on different branches, negates the primary benefit of a monorepo (one working tree, one source of truth). |
| 85 | +- **Duplicated configuration** — `pre-commit-config.yaml`, CI workflows, bash utility scripts (`conf/common.conf`, `conf/logging_functions.conf`), and conda configs are maintained as independent copies rather than shared. |
| 86 | + |
| 87 | +### 2. Maintainability |
| 88 | + |
| 89 | +**Current pain points observed in the repository:** |
| 90 | + |
| 91 | +| Issue | Evidence | Severity | |
| 92 | +|---|---|---| |
| 93 | +| Merge conflicts | `pyproject.toml`, `ci.yml`, `.pre-commit-config.yaml`, `CHANGELOG.md`, `README.md` all show `UU` (unmerged) status | **High** | |
| 94 | +| Vendored code drift | `juniper_data_client` exists in 3+ locations with different implementations (Canopy has HTTPAdapter/retry; Cascor has manual retry; Canopy's vendored copy lacks `api_key` parameter present in canonical version) | **High** | |
| 95 | +| Config duplication | `conf/common.conf`, `conf/logging_functions.conf`, etc. are copied across all three projects | **Medium** | |
| 96 | +| CI/CD divergence | Python matrix differs: Canopy/Data use 3.12–3.14, Cascor uses 3.11–3.13 | **Medium** | |
| 97 | +| Branch proliferation | Deep branch hierarchies like `subproject.juniper_cascor.feature.frontend_integration.pre_deploy.spiral_gen_extract.release` | **Low** | |
| 98 | + |
| 99 | +**The merge conflict problem is structural**, not incidental. When all three subprojects share `main` and each has files at the same relative paths (`pyproject.toml`, `README.md`, `CHANGELOG.md`), merging any subproject branch into `main` will conflict with the others. This will get worse, not better, as the project grows. |
| 100 | + |
| 101 | +### 3. Potential Problems |
| 102 | + |
| 103 | +#### Problem 1: Identity Crisis — Three Projects Sharing One `pyproject.toml` Path |
| 104 | + |
| 105 | +Each subproject has its own `pyproject.toml` at its root, but they all live on `main`. When `main` is checked out, which project's `pyproject.toml` is "the" project? The current merge conflict in `pyproject.toml` shows JuniperData and JuniperCanopy headers colliding — this is a fundamental structural issue, not a one-time conflict. |
| 106 | + |
| 107 | +#### Problem 2: CI/CD Cannot Target Changed Subprojects |
| 108 | + |
| 109 | +All three subprojects have `.github/workflows/ci.yml`. When a push to `main` triggers CI, GitHub Actions will attempt to run all workflows. There is no path-based filtering (`paths:` in workflow triggers) to run only the affected subproject's tests. This means: |
| 110 | +- Every push runs all three CI pipelines (wasteful) |
| 111 | +- Or CI runs the wrong pipeline for the branch (incorrect) |
| 112 | +- Or CI only works correctly on subproject branches, not `main` (fragile) |
| 113 | + |
| 114 | +#### Problem 3: Vendored Dependencies Create Silent Divergence |
| 115 | + |
| 116 | +The `juniper_data_client` is vendored (copied) into Canopy and Cascor rather than installed as a package. The copies have already diverged: |
| 117 | +- Canopy's copy uses `requests.adapters.HTTPAdapter` with `urllib3.util.retry.Retry` |
| 118 | +- Cascor's copy uses manual retry loops with `time.sleep` |
| 119 | +- The canonical version in JuniperData has an `api_key` constructor parameter; Canopy's vendored copy does not |
| 120 | + |
| 121 | +When a bug is fixed or feature added in Data's client, it must be manually synchronized to two other locations. This is the classic "copy-paste inheritance" anti-pattern. |
| 122 | + |
| 123 | +#### Problem 4: `sys.path` Manipulation Is Fragile |
| 124 | + |
| 125 | +Canopy imports CasCor classes by injecting `CASCOR_BACKEND_PATH` into `sys.path` at runtime. This: |
| 126 | +- Breaks IDE type checking and autocompletion |
| 127 | +- Makes dependency relationships invisible to tooling |
| 128 | +- Can silently import wrong versions if paths are misconfigured |
| 129 | +- Cannot be validated at build time |
| 130 | +- Creates tight coupling between two projects that should be independently deployable |
| 131 | + |
| 132 | +#### Problem 5: Git History Entanglement |
| 133 | + |
| 134 | +All three subprojects share a single git history on `main`. Running `git log` on `main` shows an interleaved history of unrelated changes across all three projects. This makes: |
| 135 | +- `git bisect` unreliable (changes to Data should not affect Cascor tests) |
| 136 | +- Release tagging ambiguous (does `v0.4.2` refer to Data, Cascor, or Canopy?) |
| 137 | +- `git blame` noisy with cross-project merge commits |
| 138 | + |
| 139 | +### 4. Scalability |
| 140 | + |
| 141 | +**Current state (3 subprojects):** Manageable with discipline, but already showing strain (merge conflicts, config drift). |
| 142 | + |
| 143 | +**At 5–7 subprojects:** The approach will become untenable: |
| 144 | +- Merge conflicts multiply combinatorially (each new project conflicts with all others on shared-path files) |
| 145 | +- CI run time grows linearly (all pipelines trigger on every push) |
| 146 | +- Vendored code copies become impossible to synchronize |
| 147 | +- Branch namespace becomes unwieldy |
| 148 | +- New contributors face a steep learning curve understanding which clone/branch combination to use |
| 149 | + |
| 150 | +**At 10+ subprojects:** The system will effectively be unmaintainable without monorepo tooling. |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## Recommendations |
| 155 | + |
| 156 | +### Primary Recommendation: Multi-Repository (Polyrepo) Architecture |
| 157 | + |
| 158 | +Given Juniper's architectural direction — CasCor becoming an independent service, communication via REST/WebSocket, PyPI-published client packages — separate repositories are the correct strategy. |
| 159 | + |
| 160 | +**Target architecture:** |
| 161 | + |
| 162 | +``` |
| 163 | +github.com/pcalnon/juniper-data # Dataset generation service |
| 164 | +github.com/pcalnon/juniper-cascor # CasCor neural network service |
| 165 | +github.com/pcalnon/juniper-canopy # Monitoring dashboard |
| 166 | +github.com/pcalnon/juniper-data-client # PyPI: juniper-data-client |
| 167 | +github.com/pcalnon/juniper-cascor-client # PyPI: juniper-cascor-client (new) |
| 168 | +``` |
| 169 | + |
| 170 | +**Dependency graph (target):** |
| 171 | + |
| 172 | +``` |
| 173 | +juniper-data-client (PyPI) juniper-cascor-client (PyPI, new) |
| 174 | + ↑ ↑ ↑ ↑ |
| 175 | +juniper-data juniper-cascor juniper-canopy remote workers |
| 176 | + (service, FastAPI) (REST/WS client) |
| 177 | +``` |
| 178 | + |
| 179 | +**See:** [POLYREPO_MIGRATION_PLAN.md](POLYREPO_MIGRATION_PLAN.md) for the detailed migration plan. |
| 180 | + |
| 181 | +### Immediate Guard-Rails (While Migration Is In Progress) |
| 182 | + |
| 183 | +1. **Resolve the active merge conflicts** — The `UU` files in `juniper_canopy` will block all further merges |
| 184 | +2. **Stop vendoring `juniper_data_client`** — Install from source as an editable package immediately |
| 185 | +3. **Flatten branch names** — Use short-lived feature branches (`cascor/add-api`, not `subproject.juniper_cascor.feature.frontend_integration.pre_deploy.spiral_gen_extract.release`) |
| 186 | + |
| 187 | +--- |
| 188 | + |
| 189 | +## Concerns |
| 190 | + |
| 191 | +1. **The merge conflict backlog is a ticking clock.** The `UU` status on `pyproject.toml`, `ci.yml`, `.pre-commit-config.yaml`, `README.md`, `CHANGELOG.md`, and 10+ other files means the current `main` branch is in a broken state. Every day this persists makes resolution harder. |
| 192 | + |
| 193 | +2. **Vendored client divergence is a bug factory.** If Canopy's `juniper_data_client` has retry logic that Cascor's does not, and one has `api_key` support while the other does not, bugs will manifest differently in each consumer. This is invisible until production. |
| 194 | + |
| 195 | +3. **The three-clone workflow is non-obvious.** A new contributor would need to understand that they must clone the same repo three times, check out different branches in each, and mentally track which clone they are working in. This is high cognitive overhead with no tooling support. |
| 196 | + |
| 197 | +4. **Release versioning is ambiguous.** With three packages at versions 0.2.3, 0.3.17, and 0.4.2 all in one repo, git tags like `v0.4.2` are ambiguous. Prefixed tags (`data-v0.4.2`, `cascor-v0.3.17`) add complexity that separate repos eliminate. |
| 198 | + |
| 199 | +5. **CasCor's `sys.path` coupling blocks independent deployment.** Canopy cannot be deployed without a local copy of the CasCor source tree, even though in a service architecture they should be independently deployable to different hosts. |
| 200 | + |
| 201 | +--- |
| 202 | + |
| 203 | +## Strategy Comparison |
| 204 | + |
| 205 | +| Dimension | Current (Informal Monorepo) | True Monorepo (uv workspaces) | Polyrepo (Recommended) | |
| 206 | +|---|---|---|---| |
| 207 | +| Merge conflicts | Frequent, structural | Rare (separate paths) | None (separate repos) | |
| 208 | +| Cross-project changes | Possible but painful | Easy (one commit) | Coordinated releases | |
| 209 | +| CI efficiency | All pipelines always run | Path-filtered | Independent | |
| 210 | +| Dependency management | Vendored copies | Workspace installs | Published packages (PyPI) | |
| 211 | +| Contributor onboarding | 3 clones, branch juggling | 1 clone, standard PRs | Multiple repos, standard PRs | |
| 212 | +| Tooling complexity | None (manual) | Moderate (workspace setup) | Low (standard Python) | |
| 213 | +| Scalability | Poor (>5 projects breaks) | Good (proven pattern) | Good (proven pattern) | |
| 214 | +| Independent deployment | Not possible | Possible with effort | Natural | |
| 215 | +| Migration effort | N/A | Medium | Medium-High | |
| 216 | +| Service-oriented fit | Poor | Moderate | Excellent | |
| 217 | + |
| 218 | +--- |
| 219 | + |
| 220 | +## Decision |
| 221 | + |
| 222 | +**Adopt the polyrepo architecture** with PyPI-published client packages. |
| 223 | + |
| 224 | +This decision is driven by: |
| 225 | +1. The project's architectural direction toward independent services (CasCor as a service, not a library import) |
| 226 | +2. The need for independently deployable and versionable components |
| 227 | +3. The current structural problems (merge conflicts, vendored code drift) that will only worsen |
| 228 | +4. The desire to publish client packages to PyPI for external consumption |
| 229 | +5. The addition of remote worker clients that must be independently installable on worker hardware |
| 230 | + |
| 231 | +See [POLYREPO_MIGRATION_PLAN.md](POLYREPO_MIGRATION_PLAN.md) for the concrete implementation plan. |
0 commit comments