Developer documentation
If you haven't, please read the Contributing guidelines first.
If you want to make contributions to this package that involves code, then this guide is for you.
First time clone
If you have writing rights, you don't have to fork. Instead, simply clone and skip ahead. Whenever upstream is mentioned, use origin instead.
If this is the first time you work with this repository, follow the instructions below to clone the repository.
Fork this repo
Clone your repo (this will create a
git remotecalledorigin)Add this repo as a remote:
git remote add upstream https://github.com/dcelisgarza/PortfolioOptimisers.jlThis will ensure that you have two remotes in your git: origin and upstream. You will create branches and push to origin, and you will fetch and update your local main branch from upstream.
Linting and formatting
Install a plugin on your editor to use EditorConfig. This will ensure that your editor is configured with important formatting settings.
We use https://pre-commit.com to run the linters and formatters. In particular, the Julia code is formatted using JuliaFormatter.jl, so please install it globally first:
julia> # Press ]pkg> activatepkg> add JuliaFormatterTo install pre-commit, we recommend using pipx as follows:
# Install pipx following the link
pipx install pre-commitWith pre-commit installed, activate it as a pre-commit hook:
pre-commit installTo run the linting and formatting manually, enter the command below:
pre-commit run -aNow, you can only commit if all the pre-commit tests pass.
Testing
As with most Julia packages, you can just open Julia in the repository folder, activate the environment, and run test:
julia> # press ]pkg> activate .pkg> testWorking on a new issue
We try to keep a linear history in this repo, so it is important to keep your branches up-to-date.
Fetch from the remote and fast-forward your local main
git fetch upstream git switch main git merge --ff-only upstream/mainBranch from
mainto address the issue (see below for naming)git switch -c 42-add-answer-universePush the new local branch to your personal remote repository
git push -u origin 42-add-answer-universeCreate a pull request to merge your remote branch into the org main.
Branch naming
- If there is an associated issue, add the issue number.
- If there is no associated issue, and the changes are small, add a prefix such as "typo", "hotfix", "small-refactor", according to the type of update.
- If the changes are not small and there is no associated issue, then create the issue first, so we can properly discuss the changes.
- Use dash separated imperative wording related to the issue (e.g.,
14-add-tests,15-fix-model,16-remove-obsolete-files).
Commit message
- Use imperative or present tense, for instance: Add feature or Fix bug.
- Have informative titles.
- When necessary, add a body with details.
- If there are breaking changes, add the information to the commit message.
Before creating a pull request
Try to create "atomic git commits" (recommended reading: The Utopic Git History).
Make sure the tests pass.
Make sure the pre-commit tests pass.
Fetch any
mainupdates from upstream and rebase your branch, if necessary:bash git fetch upstream git rebase upstream/main BRANCH_NAMEThen you can open a pull request and work with the reviewer to address any issues.
When a code-health gate turns red
Two checks hold the numbers that issue #250 asks to fall: Complexity and JET. Both are a ratchet. A file passes when its number has not risen above the number code_health/ records for it, so neither check ever asks you to improve a number you did not cause. A file far above every threshold is green while its number holds steady.
The Complexity check carries a third ratchet beside the two complexity ones, and it reads differently. The size ratchet counts the code lines in a file, where a docstring line, a comment line and a blank line are not code. A file's ceiling there is the greater of 500 and the number the baseline records for it, so a file under 500 code lines is free to grow and a file over 500 may fall and may not rise. See docs/adr/0101-the-size-gate-counts-code-lines-and-binds-over-a-threshold.md.
When one turns red, the run names every offending file, the metric, the baseline number and the new one. Take one of three routes, in order.
Lower the number. This is the route to take whenever you can. For a complexity number the file's number is the maximum over its definitions, so only the worst definition moves it. For the size ratchet, move code out of the file or delete it. A docstring costs nothing there.
Dismiss the report, for
JETalone. A Dismissal says that a class of report is not a real defect. It cites a Rationale by name, and the two halves have different owners: citing an approved Rationale is bookkeeping that any contributor may do, and adding a new Rationale is a claim that some code is correct. See Dismissing a JET report.Record the rise. A number may rise, but only through a diff that a person approved. Run the refresh with the flag that names the act:
julia --project=code_health code_health/complexity.jl refresh --accept-riseYou do not need to measure it yourself. The run that trips uploads the file that clears it. Download the run's Refresh Artifact from its page, put the file at its committed path under
code_health/, and commit it. That needs no Julia at all.
What a contributor without write access cannot finish alone
Route 2, and only when the Rationale is new. A new [rationale.…] block needs the maintainer, and CI flags a pull request that adds one. Cite an existing Rationale and nothing blocks you; write a new one and say so in the pull request, so a maintainer can rule on it.
Routes 1 and 3 are open to everybody. A fork pull request runs both checks and can download both artifacts, because the artifact attaches to the run in this repository and this repository is public. One thing a fork contributor cannot do is re-run a check, so a transient failure needs a new push from you or a re-run by a maintainer.
Two red cases the artifact does not clear on its own
- A renamed file under
JET. The Report Fingerprint holds the file, so a rename breaks every Dismissal on it. The refresh carries the row to the new path and prints the Dismissal lines that now name a dead path; you editcode_health/rulings.tomlyourself. - A tracked
.jlfile that is neither measured nor a named Unmeasured Path. Add an[[unmeasured_path]]entry with a reason, or move the file undersrc/orext/.
In both, the artifact is correct and worth committing. It is not the whole fix.
The maintenance loop that drives these numbers down, rather than merely holding them, is Code health: the maintenance loop.
Writing documentation
Please document new features. The documentation must include:
- Links to related functions and types.
- Exhaustive descriptions of the arguments, keyword arguments, type information, and data validation.
- Exhaustive usage examples as REPL-style
jldoctestblocks, they should maximize code coverage.
The Capability Catalogue
The capability catalogue is the user-facing inventory of everything the package can do, grouped by the job each thing does. It is generated: docs/capability_catalogue.jl curates only the grouping, and every description is the first sentence of the corresponding docstring, so a description cannot drift from the type it describes.
Adding a new type or exported function means adding it here too. This is enforced, not merely requested — test/test_26_docs.jl fails if any name on the Choice Surface is missing, and the docs build refuses to render an incomplete page. A concrete type the package declares is on the surface when it is a leaf subtype of AbstractEstimator, of AbstractAlgorithm or of AbstractCovarianceEstimator, or when it is an export under its own name; a Result and an error are subtracted, because a caller receives them and never chooses them. choice_surface_names in docs/generate_capability_catalogue.jl is the one statement of that rule, and both the test and the docs build call it.
- Add a
Cap(:YourType)to the group it belongs to, chosen by what it does rather than which file it lives in. - Do not write a description. It comes from the docstring. Pass
labelonly where the docstring genuinely reads worse as a bullet — for instance when every sibling in a group would repeat the same prefix. - A function that is not a user-facing capability goes in
NOT_A_FEATUREwith a reason (:alias,:base_overload,:trait,:internal) instead. Removing an export means removing its entry there too; the check runs in both directions. - A type the library constructs for itself, and no caller ever writes, goes in
NOT_A_CHOICEwith a reason (:internal) instead. It keeps its docstring and its API page; it is simply not a choice. LikeNOT_A_FEATURE, this list is checked in both directions, so an exemption cannot outlive its type.
Because descriptions come from docstrings, a type's first sentence has to stand alone in a bullet list. See the summary-sentence rules in .github/instructions/julia-docstrings.instructions.md.
Building and viewing the documentation locally
Following the latest suggestions, we recommend using LiveServer to build the documentation. Here is how you do it:
Run
julia --project=docsto open Julia in the environment of the docs.If this is the first time building the docs
- Press
]to enterpkgmode - Run
pkg> dev .to use the development version of your package - Press backspace to leave
pkgmode
- Press
Run
julia> using LiveServerRun
julia> servedocs()
Making a new release
To create a new release, you can follow these simple steps:
Create a branch
release-x.y.zUpdate
versioninProject.tomlCreate a commit "Release vx.y.z", push, create a PR, wait for it to pass, merge the PR.
Go back to main screen and click on the latest commit (link: https://github.com/dcelisgarza/PortfolioOptimisers.jl/commit/main)
At the bottom, write
@JuliaRegistrator register
After that, you only need to wait and verify:
- Wait for the bot to comment (should take < 1m) with a link to a PR to the registry
- Follow the link and wait for a comment on the auto-merge
- The comment should said all is well and auto-merge should occur shortly
- After the merge happens, TagBot will trigger and create a new GitHub tag. Check on https://github.com/dcelisgarza/PortfolioOptimisers.jl/releases
- After the release is create, a "docs" GitHub action will start for the tag.
- After it passes, a deploy action will run.
- After that runs, the stable docs should be updated. Check them and look for the version number.