Skip to content

Commit a99a840

Browse files
pcalnonclaude
andcommitted
Add monorepo analysis and polyrepo migration plan
Evaluates the current single-repo approach and provides a concrete 6-phase migration plan to separate repositories with PyPI-published client packages and CasCor as an independent service. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b2c55a9 commit a99a840

2 files changed

Lines changed: 1848 additions & 0 deletions

File tree

notes/MONOREPO_ANALYSIS.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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

Comments
 (0)