From f7922d492e2780557c21782345f3069aaa592665 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 23 Jul 2026 15:10:04 +0000
Subject: [PATCH 1/3] docs: fix math rendering and refresh project guide
Co-authored-by: Jiangki
---
README.md | 173 +++++++++++++++++++---------------
docs/OPEN_CORE.md | 10 +-
docs/PLAN.md | 16 ++--
docs/ROADMAP.md | 13 ++-
docs/algorithm.md | 12 +--
docs/book/src/introduction.md | 8 +-
docs/factor_structure_note.md | 6 +-
python/README.md | 6 +-
8 files changed, 135 insertions(+), 109 deletions(-)
diff --git a/README.md b/README.md
index 48871c1..15df614 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
# Ledge
[](https://github.com/Jiangki/ledge/actions/workflows/ci.yml)
+[](https://pypi.org/project/ledge-portfolio/)
+[](https://crates.io/crates/ledge-portfolio)
+[](https://jiangki.github.io/ledge/)
[](LICENSE)
[](CHANGELOG.md)
@@ -10,7 +13,7 @@ Ledge solves continuous convex portfolio QPs **without expanding** the factor
covariance
$$
-\Sigma = F\Omega F^{\mathsf{T}} + \operatorname{diag}(d)
+\Sigma = F\Omega F^{\mathsf{T}} + \mathrm{diag}(d)
$$
into a dense asset-by-asset matrix. The practical target is repeated,
@@ -21,16 +24,23 @@ modest number of linear constraints.
-**Status:** alpha (`0.2.0`). Residuals are auditable; APIs and defaults may
-change before 1.0. This is usable early software, not a production risk system.
+**Status:** alpha (`0.2.0`). The release is available from
+[PyPI](https://pypi.org/project/ledge-portfolio/), [crates.io
+(`ledge-portfolio`)](https://crates.io/crates/ledge-portfolio), and
+[crates.io (`ledge-core`)](https://crates.io/crates/ledge-core).
+The Python import and Rust library name are both `ledge`.
-Ledge is licensed under [Apache-2.0](LICENSE). The complete open-core boundary,
-clean-history publication procedure, and package-release controls live in the
-[release runbook](docs/OPEN_CORE.md) and [maintainer sign-off
-checklist](docs/PUBLIC_RELEASE_CHECKLIST.md).
+> [!WARNING]
+> APIs and defaults may change before 1.0. Ledge exposes auditable residuals,
+> but it is not a production risk system or a substitute for independent
+> validation.
-Rust package / library name: `ledge-portfolio` / `ledge`. Python distribution /
-import name: `ledge-portfolio` / `ledge`.
+**[Documentation](https://jiangki.github.io/ledge/)** ·
+**[Python quick start](#five-minute-python-example)** ·
+**[Rust API](#rust-api)** ·
+**[Benchmarks](#performance-smoke)** ·
+**[Limitations](#limitations)** ·
+**[Contributing](CONTRIBUTING.md)**
@@ -50,26 +60,41 @@ reproduction script live in [`docs/assets/`](docs/assets/README.md).
- **Researchers and practitioners** who want reproducible factor-QP examples and
independently inspectable KKT residuals.
-## Install
+## Why Ledge?
+
+| Need | What Ledge provides |
+|---|---|
+| Preserve factor structure | Accepts `F`, `omega`, and `d` directly; never materializes dense covariance |
+| Rebalance repeatedly | Cached equilibration/factorizations plus automatic primal-dual warm starts |
+| Model trading costs | Smooth L2 turnover and exact L1 proportional costs with a no-trade region |
+| Audit solver output | Original-space KKT residuals, diagnostics, polishing, and infeasibility certificates |
+| Embed without a service | Pure-Rust core, a public Rust API, and NumPy bindings that release the GIL |
+
+## Quick start
Requirements: Rust 1.83+; Python 3.9+ for the bindings.
+Python:
+
```bash
-git clone https://github.com/Jiangki/ledge.git
-cd ledge
-cargo test --workspace
-cargo run -p ledge-portfolio --release --example rebalance
+python -m pip install ledge-portfolio==0.2.0
```
-Python registry install (after the `0.2.0` artifacts are published):
+Rust:
-```bash
-python -m pip install ledge-portfolio==0.2.0
+```toml
+[dependencies]
+ledge = { package = "ledge-portfolio", version = "0.2" }
```
-Build from source:
+Run the repository examples or build the Python package from source:
```bash
+git clone https://github.com/Jiangki/ledge.git
+cd ledge
+cargo test --workspace
+cargo run -p ledge-portfolio --release --example rebalance
+
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip maturin
@@ -77,12 +102,8 @@ python -m pip install -e python/
python python/examples/rebalance.py
```
-Rust registry dependency:
-
-```toml
-[dependencies]
-ledge = { package = "ledge-portfolio", version = "0.2" }
-```
+Platform wheels are published for Linux x86-64/aarch64, macOS universal2,
+and Windows x86-64. Other platforms build from the source distribution.
## Five-minute Python example
@@ -159,24 +180,28 @@ is executed against cvxpy + Clarabel in CI.
```rust
use ledge::{FactorCovariance, Matrix, PortfolioProblem, SolveStatus};
-let factors = Matrix::new(3, 1, vec![1.0, -0.5, 0.25])?;
-let problem = PortfolioProblem::new(
- factors,
- FactorCovariance::Diagonal(vec![0.1]),
- vec![0.2, 0.3, 0.25],
- vec![0.08, 0.04, 0.06],
-)?
-.with_risk_aversion(5.0)?
-.with_bounds(vec![0.0; 3], vec![0.6; 3])?;
-
-let solution = problem.solve(None)?;
-assert_eq!(solution.status, SolveStatus::Solved);
-println!("{:?}", solution.x);
-# Ok::<(), Box>(())
+fn main() -> Result<(), Box> {
+ let factors = Matrix::new(3, 1, vec![1.0, -0.5, 0.25])?;
+ let problem = PortfolioProblem::new(
+ factors,
+ FactorCovariance::Diagonal(vec![0.1]),
+ vec![0.2, 0.3, 0.25],
+ vec![0.08, 0.04, 0.06],
+ )?
+ .with_risk_aversion(5.0)?
+ .with_bounds(vec![0.0; 3], vec![0.6; 3])?;
+
+ let solution = problem.solve(None)?;
+ assert_eq!(solution.status, SolveStatus::Solved);
+ println!("{:?}", solution.x);
+ Ok(())
+}
```
Use `solution.warm_start()` for a full primal/dual warm start. The lower-level
-`QpProblem` API remains available for custom continuous convex QPs.
+`QpProblem` API remains available for custom continuous convex QPs. API docs:
+[`ledge`](https://docs.rs/ledge-portfolio) and
+[`ledge-core`](https://docs.rs/ledge-core).
## Architecture
@@ -239,6 +264,19 @@ Use `solution.warm_start()` for a full primal/dual warm start. The lower-level
- Installable PyO3 package (releases the GIL while solving).
- Deterministic Rust / Python examples.
+## Documentation
+
+| Topic | Resource |
+|---|---|
+| Guided tutorials and API overview | [mdBook documentation](https://jiangki.github.io/ledge/) |
+| Python installation and first solve | [Python quick start](https://jiangki.github.io/ledge/tutorial/quickstart-python.html) |
+| Rust installation and first solve | [Rust quick start](https://jiangki.github.io/ledge/tutorial/quickstart-rust.html) |
+| Rolling rebalances and batch solves | [Rolling tutorial](https://jiangki.github.io/ledge/tutorial/rolling.html) · [Batch tutorial](https://jiangki.github.io/ledge/tutorial/batch.html) |
+| Constraints, turnover, diagnostics, tuning | [Guides](https://jiangki.github.io/ledge/guide/constraints.html) · [Tuning reference](https://jiangki.github.io/ledge/reference/tuning.html) |
+| Migration from cvxpy | [`docs/cvxpy_migration.md`](docs/cvxpy_migration.md) |
+| Mathematics and design | [`docs/algorithm.md`](docs/algorithm.md) · [`docs/factor_structure_note.md`](docs/factor_structure_note.md) |
+| Reproducible performance evidence | [`benchmarks/README.md`](benchmarks/README.md) · [`docs/SMOKE_TIMINGS.md`](docs/SMOKE_TIMINGS.md) |
+
## Performance smoke
We publish **self-timing smoke numbers**, not competitive rankings. Full method
@@ -330,34 +368,21 @@ cvxpy + Clarabel.
polishing step; those solves return ADMM-tolerance (~1e-5) residuals
instead of polished (~1e-11) ones. `SolveResult.polished` reports which.
-## Roadmap
+## Project status and roadmap
-Public technical plan and milestones: [`docs/PLAN.md`](docs/PLAN.md),
-[`docs/ROADMAP.md`](docs/ROADMAP.md).
+Release `0.2.0` ships the Rust crates, Python wheels, documentation site,
+factor-aware benchmarks, scaling, exact L1 costs, polishing, certificates,
+rolling sequences, and account batching.
-Near-term theme: **complete the public-release gate without weakening
-trust**. Immediate priorities:
-
-1. ~~Automatic scaling (Ruiz / equilibration) and large-instance reliability~~
- — landed; smoke matrix now passes at $n=5000, k=100$
-2. **PyPI wheels**; ~~fair OSQP/Clarabel comparison~~ — first protocol
- report published under `benchmarks/results/2026-07/`
-3. ~~Exact L1 turnover~~ — landed, dedicated soft-threshold prox block
- with audited subgradients; ~~polishing~~ — landed, audit-gated
- active-set refinement to ~1e-11 residuals by default; ~~infeasibility
- certificates~~ — landed with auditable Farkas / descent-ray proofs and
- portfolio-vocabulary hints; ~~solve sequences~~ — `PortfolioSequence` /
- `solve_sequence` landed (Rust + Python); ~~tracking-error objectives
- and rolling example~~ — landed (`docs/examples/`)
-4. M3 vertical productization: ~~constraint templates~~ — landed
- (industry / group / style / concentration / short-limit builders);
- ~~problem & solution serialization~~ — landed (`serde` feature,
- Python `to_json` / `from_json`); ~~multi-thread batch over the
- account axis~~ — landed (`solve_batch`, non-default `rayon` feature,
- published 1×500×250 throughput); ~~docs site~~ — landed as an mdBook
- built in CI (`docs/book/`; public deployment waits on the
- public-release gate)
-5. Later: 1.0 API/MSRV/deprecation review after external workload evidence
+The next priorities are evidence and stability rather than a broader solver
+scope:
+
+1. collect redacted real workloads and turn failures into regression tests;
+2. evaluate sparse factor storage only when those workloads justify it;
+3. complete the API, MSRV, semver, and deprecation review before 1.0.
+
+Detailed milestones: [`docs/PLAN.md`](docs/PLAN.md) and
+[`docs/ROADMAP.md`](docs/ROADMAP.md).
## Non-goals
@@ -369,8 +394,10 @@ trust**. Immediate priorities:
## Security
-Please do not file public issues for sensitive reports. See
-[`SECURITY.md`](SECURITY.md).
+Ledge is not hardened for untrusted inputs in a multi-tenant service. Please
+do not file public issues for sensitive reports; follow
+[`SECURITY.md`](SECURITY.md). The latest repository audit is recorded in
+[`docs/SECURITY_AUDIT.md`](docs/SECURITY_AUDIT.md).
## Repository layout
@@ -383,18 +410,14 @@ docs/book/ mdBook docs site source (built in CI; `mdbook build docs/
docs/examples/ rolling backtest example + published warm-start numbers
docs/assets/ reproducible README diagrams, chart, terminal GIF + provenance
benchmarks/ comparison protocol, adapters (non-default features), results
-.github/workflows/ CI + manual release/docs deploy (strict-gated on public release)
+.github/workflows/ CI + manual release/docs deployment
.github/ISSUE_TEMPLATE/ bug / performance / problem-instance templates
```
-Further reading: [`docs/PLAN.md`](docs/PLAN.md),
-[`docs/ROADMAP.md`](docs/ROADMAP.md),
+Project governance and release controls:
[`docs/DECISIONS.md`](docs/DECISIONS.md),
-[`docs/OPEN_CORE.md`](docs/OPEN_CORE.md),
-[`docs/algorithm.md`](docs/algorithm.md),
-[`docs/factor_structure_note.md`](docs/factor_structure_note.md),
-[`docs/cvxpy_migration.md`](docs/cvxpy_migration.md),
-[`docs/SMOKE_TIMINGS.md`](docs/SMOKE_TIMINGS.md).
+[`docs/OPEN_CORE.md`](docs/OPEN_CORE.md), and
+[`docs/PUBLIC_RELEASE_CHECKLIST.md`](docs/PUBLIC_RELEASE_CHECKLIST.md).
## License
diff --git a/docs/OPEN_CORE.md b/docs/OPEN_CORE.md
index 9997547..011984f 100644
--- a/docs/OPEN_CORE.md
+++ b/docs/OPEN_CORE.md
@@ -2,11 +2,11 @@
**This document is the single source of truth for "what is open" in Ledge.**
-> **Current state (2026-07-22): release gate applied.** The reviewed tree is
-> licensed Apache-2.0, targets version `0.2.0`, and uses Rust package
-> `ledge-portfolio` (library `ledge`). The existing repository is retained as
-> the historical private archive; creating the clean-root public repository
-> and publishing registry artifacts remain explicit maintainer actions in §5.
+> **Current state (2026-07-23): `0.2.0` published.** The reviewed Apache-2.0
+> tree is live in the clean-root public repository, Rust packages
+> `ledge-core` / `ledge-portfolio` and Python distribution `ledge-portfolio`
+> are published, and the documentation site is deployed. Section 5 is
+> retained as the release-control runbook for future versions.
It answers three questions the maintainer keeps asking:
diff --git a/docs/PLAN.md b/docs/PLAN.md
index 54c0f33..86fde2e 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -82,8 +82,8 @@ daily, weekly, or monthly portfolio rebalances.
| Trust | `n=2000, k=50` and the declared smoke matrix solve under defaults; comparison protocol and raw data published | Done |
| Rebalancing | Exact L1 turnover, certificates, polishing, tracking error, and measured rolling warm-start effect | Done |
| Workflows | Constraint templates, serialization, and 1 model × 500 accounts × 250 dates batch evidence | Done |
-| Distribution | Public OSS license, publishable crates, PyPI wheels, public docs | Pending maintainer action |
-| Stability | 1.0 API review, semver/MSRV/deprecation policy | Pending |
+| Distribution | Public OSS license, crates.io packages, PyPI wheels, public docs | Done — `0.2.0` published |
+| Stability | 1.0 compatibility, semver/MSRV, and deprecation review | Pending |
The current measured evidence is indexed in
[`docs/book/src/reference/benchmarks.md`](book/src/reference/benchmarks.md).
@@ -164,9 +164,9 @@ the primary comparative story; cold-start results must still be shown.
## 7. Release policy
-- Before public distribution, complete every step in
+- Every release follows the controls in
[`OPEN_CORE.md` §5](OPEN_CORE.md#5-public-release-runbook-roadmap-16-gate)
- and make `./scripts/check_open_core.sh --release` pass.
+ and must make `./scripts/check_open_core.sh --release` pass.
- Each release follows: changelog → tests → package checks → tag → crates.io
(`ledge-core`, then `ledge-portfolio`) → PyPI wheels → GitHub Release
linking the evidence used for claims.
@@ -179,11 +179,9 @@ the primary comparative story; cold-start results must still be shown.
## 8. Current priorities
-1. Export the verified gate tree as the selected clean-root public repository.
-2. Configure PyPI Trusted Publishing and publish the tagged release wheels.
-3. Collect redacted real workloads for the regression set.
-4. Evaluate sparse factor storage only if those workloads demonstrate need.
-5. Review the API, MSRV, and deprecation policy before 1.0.
+1. Collect redacted real workloads for the regression set.
+2. Evaluate sparse factor storage only if those workloads demonstrate need.
+3. Review the API, MSRV, semver, and deprecation policy before 1.0.
---
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index 569956c..c072ce9 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -54,16 +54,16 @@ honest comparison.
| 1.3 | Unscaled residual reporting: `check_kkt` always on original data | Done — termination and all reported residuals use original data |
| 1.4 | Expand smoke matrix to \(n=5000\), \(k=100\); update [`SMOKE_TIMINGS.md`](SMOKE_TIMINGS.md) and README support statement | Done — full matrix Solved under defaults (2026-07-21) |
| 1.5 | Over-relaxation (\(\alpha \approx 1.6\)) and optional vector ρ | Over-relaxation done — default `over_relaxation: 1.6`, smoke-matrix iterations cut 1.7–2.9x (n=5000: 1680 → 660); see Technical notes §1a. Vector ρ re-evaluated 2026-07-22 with a measured prototype and **stays deferred**: no static per-block factor wins across the smoke matrix (1.6x gains at n=1000/2000 with explicit rows, 1.6x losses at n=5000); see [`DECISIONS.md`](DECISIONS.md) |
-| 1.6 | **Public-release gate** — Apache-2.0, resolve the occupied crates.io `ledge` package name, enable publishing, publish a clean-root GitHub repository | Gate tree done; clean-root repository creation is the remaining maintainer action; follow [`OPEN_CORE.md`](OPEN_CORE.md) §5 |
-| 1.7 | `maturin-action` wheels + PyPI Trusted Publishing → `ledge-portfolio 0.2.0` | Workflow done — manual `release.yml`, strict-gate/tag guarded; first publish waits on Trusted Publisher/environment setup |
+| 1.6 | **Public-release gate** — Apache-2.0, resolve the occupied crates.io `ledge` package name, enable publishing, publish a clean-root GitHub repository | Done — clean-root public repository and `v0.2.0` tag published |
+| 1.7 | `maturin-action` wheels + PyPI Trusted Publishing → `ledge-portfolio 0.2.0` | Done — abi3 wheels and sdist published to PyPI |
| 1.8 | OSQP / Clarabel adapters (non-default features) + first protocol report | Done — `benchmarks/adapters`, report in `benchmarks/results/2026-07/` |
| 1.9 | Short technical note: “why factor structure is worth exploiting” using 1.8 data | Done — [`factor_structure_note.md`](factor_structure_note.md): measured dense-Q vs lifted gap (1–2 orders of magnitude inside the same solver), SMW cost model, what native factor form adds beyond lifting, honest large-n limits; every number cited from published reports |
**Exit criteria:**
- `n=2000, k=50` Solved under defaults.
-- Repository licensed Apache-2.0 — done; clean-root publication and
- `pip install ledge-portfolio` registry verification remain maintainer steps.
+- Repository licensed Apache-2.0 and published from a clean root — done;
+ `pip install ledge-portfolio==0.2.0` verified against PyPI.
- Comparison report published and satisfies all rules in
[`../benchmarks/README.md`](../benchmarks/README.md).
@@ -109,13 +109,12 @@ keeping the repository boundary mechanically checkable.
| 3.2 | Multi-thread batch over the account axis (`rayon`, feature-gated); publish “1×500×250” throughput | Done — `solve_batch(&[BatchAccount], settings)` runs one `PortfolioSequence` per account, in parallel over accounts behind the non-default `rayon` feature (serial with identical results without it); optional backtest anchor chaining (`chain_previous_weights`); per-account error isolation; Python `ledge.solve_batch(problems, steps, ...)`. Published: 1 model × 500 accounts × 250 dates (n=200, k=15) in 12.9 s on 4 vCPUs = 9.7k solves/s, 4.0x over serial ([`benchmarks/results/2026-07-batch/`](../benchmarks/results/2026-07-batch/README.md)); see Technical notes §8 |
| 3.3 | Problem / solution serialization (serde + JSON/binary) for bug reproduction | Done — non-default `serde` feature on `ledge-core` / `ledge-portfolio`: `QpProblem`, `PortfolioProblem`, `SolverSettings`, `WarmStart`, `Solution` (duals, residuals, certificates included) work with any serde format; deserialization re-runs construction validation; Python `PortfolioProblem.to_json()` / `from_json()` and `SolveResult.to_json()`; see Technical notes §7 |
| 3.4 | Evaluate sparse `F`; implement CSR path only if real workloads need it | |
-| 3.5 | mdBook + GitHub Pages docs site (tutorial, migration, API, tuning) | Done (build) — `docs/book/` with tutorial / guide / reference chapters; CI builds it on every push (`docs` job, artifact). **Public Pages deployment is deliberately manual-only** (`docs-deploy.yml`) until the 1.6 public-release gate; see [`DECISIONS.md`](DECISIONS.md) |
+| 3.5 | mdBook + GitHub Pages docs site (tutorial, migration, API, tuning) | Done — `docs/book/` with tutorial / guide / reference chapters; CI builds it on every push, and `docs-deploy.yml` publishes the public Pages site manually |
| 3.6 | **Open-core boundary review** — verify new workflows remain in-core and no private implementation entered this tree | Done — [`OPEN_CORE.md`](OPEN_CORE.md) + `scripts/check_open_core.sh` |
**Exit criteria:** batch throughput published — done
([`benchmarks/results/2026-07-batch/`](../benchmarks/results/2026-07-batch/README.md));
-docs site built in CI — done (public deployment intentionally waits on the
-1.6 gate); repository boundary recorded in
+docs site built in CI and deployed to GitHub Pages — done; repository boundary recorded in
[`OPEN_CORE.md`](OPEN_CORE.md) — done.
---
diff --git a/docs/algorithm.md b/docs/algorithm.md
index 5adc2ec..0840004 100644
--- a/docs/algorithm.md
+++ b/docs/algorithm.md
@@ -14,7 +14,7 @@ The solver accepts
+\sum_i c_i\,|x_i-a_i|,\\
\text{s.t.}\quad
&A_e x=b_e,\quad A_i x\le b_i,\quad \ell\le x\le u,\\
-&Q=F\Omega F^\mathsf{T}+\operatorname{diag}(d),
+&Q=F\Omega F^\mathsf{T}+\mathrm{diag}(d),
\end{aligned}
\]
@@ -97,7 +97,7 @@ the weighted L1 distance to the anchor — elementwise soft-thresholding:
\[
(z_t^{k+1})_i=a_i+S_{c_i/\rho}\!\left((\hat z_t^{k+1}+y_t^k/\rho)_i-a_i\right),
\qquad
-S_\kappa(v)=\operatorname{sign}(v)\max(|v|-\kappa,0),
+S_\kappa(v)=\mathrm{sign}(v)\max(|v|-\kappa,0),
\]
with the same over-relaxed blend and dual update as the other blocks. The
@@ -131,7 +131,7 @@ rebuilding it.
With default settings (`scaling_iterations = 10`; `0` disables), the solver
iterates on an equilibrated copy of the data. Each Ruiz pass computes a
-variable scaling \(E=\operatorname{diag}(e)\), row scalings \(D_e, D_i\) for
+variable scaling \(E=\mathrm{diag}(e)\), row scalings \(D_e, D_i\) for
the two constraint blocks, and a cost scalar \(c\), accumulated over passes:
\[
@@ -163,7 +163,7 @@ and returned iterates are always evaluated on the original data. A reported
Let
\[
-B=\operatorname{diag}(d)+(\sigma+\rho)I,\qquad
+B=\mathrm{diag}(d)+(\sigma+\rho)I,\qquad
U=[G,\sqrt{\rho}A^\mathsf{T}].
\]
@@ -218,7 +218,7 @@ usual multiplicative ladder), so lookups are bit-exact, the iterate path is
identical to a fresh `Solver::solve` of the same data, and a warm rolling
sequence factorizes each visited penalty exactly once per workspace. A cache
miss remains a full \(O(nr^2)\) recomputation:
-\(B=\operatorname{diag}(d)+(\sigma+\rho)I\) reweights every entry of \(S\),
+\(B=\mathrm{diag}(d)+(\sigma+\rho)I\) reweights every entry of \(S\),
so no rank-one update shortcut exists in this formulation.
## 4b. Rolling sequences (`solve_sequence`)
@@ -250,7 +250,7 @@ Returned multipliers use these sign conventions:
nonnegative at the upper bound;
- L1 duals \(y_t\) (present only with an L1 term) are subgradients of the
weighted L1 cost: \(|(y_t)_i|\le c_i\) always, and
- \((y_t)_i=c_i\operatorname{sign}(x_i-a_i)\) where the asset trades.
+ \((y_t)_i=c_i\mathrm{sign}(x_i-a_i)\) where the asset trades.
Stationarity is
diff --git a/docs/book/src/introduction.md b/docs/book/src/introduction.md
index 952e879..a877456 100644
--- a/docs/book/src/introduction.md
+++ b/docs/book/src/introduction.md
@@ -7,7 +7,7 @@ It solves continuous convex mean-variance portfolio QPs whose covariance has
factor structure
\\[
-\Sigma = F \Omega F^\mathsf{T} + \operatorname{diag}(d)
+\Sigma = F \Omega F^\mathsf{T} + \mathrm{diag}(d)
\\]
**without ever forming the dense \\(n \times n\\) matrix**. Inputs are the
@@ -50,5 +50,7 @@ but the project makes no default-settings promise.
Alpha (`0.2.x`). APIs and defaults may change before 1.0; every change is
recorded in the repository
[CHANGELOG](https://github.com/Jiangki/ledge/blob/main/CHANGELOG.md). Source is
-licensed Apache-2.0; crates.io/PyPI availability is verified separately for
-each tagged release on the [roadmap](reference/design.md).
+licensed Apache-2.0. Release `0.2.0` is available from
+[PyPI](https://pypi.org/project/ledge-portfolio/) and
+[crates.io](https://crates.io/crates/ledge-portfolio); see
+[Installation](tutorial/installation.md) for package names and platforms.
diff --git a/docs/factor_structure_note.md b/docs/factor_structure_note.md
index aee6180..9f11dca 100644
--- a/docs/factor_structure_note.md
+++ b/docs/factor_structure_note.md
@@ -11,7 +11,7 @@ independent `check_kkt` on the original data. Nothing here is extrapolated.*
A factor risk model writes the asset covariance as
\[
-\Sigma = F \Omega F^\mathsf{T} + \operatorname{diag}(d),
+\Sigma = F \Omega F^\mathsf{T} + \mathrm{diag}(d),
\qquad F \in \mathbb{R}^{n \times k},\; k \ll n .
\]
@@ -39,14 +39,14 @@ dense \(n \times n\) algebra.
**Lifted (the sophisticated manual fix).** Add \(k\) auxiliary variables
\(y = \Omega^{1/2} F^\mathsf{T} w\) and \(k\) equality rows; the objective
becomes sparse-diagonal
-(\(\tfrac{1}{2} w^\mathsf{T}\!\operatorname{diag}(d)\,w +
+(\(\tfrac{1}{2} w^\mathsf{T}\!\mathrm{diag}(d)\,w +
\tfrac{1}{2}\lVert y\rVert^2\)). A general sparse solver handles this well
— it is the strongest external baseline and every published Ledge
comparison includes it.
**Native factor form (Ledge).** Take \(F, \Omega, d\) directly. The ADMM
x-update solves a system of the form
-\(\bigl(\operatorname{diag}(\tilde d) + G G^\mathsf{T}\bigr) x = r\) with
+\(\bigl(\mathrm{diag}(\tilde d) + G G^\mathsf{T}\bigr) x = r\) with
\(G \in \mathbb{R}^{n \times r}\), and Sherman–Morrison–Woodbury reduces it
to an \(r \times r\) Gram factorization with
diff --git a/python/README.md b/python/README.md
index e66e3a4..72e4e33 100644
--- a/python/README.md
+++ b/python/README.md
@@ -8,12 +8,16 @@ workflow builds abi3 wheels for the supported platforms.
Each distribution includes the Apache-2.0 project license and the generated
Rust dependency notices in `THIRD_PARTY_LICENSES.html`.
-Registry install after the `0.2.0` artifacts are published:
+Install the published `0.2.0` release:
```bash
python -m pip install ledge-portfolio==0.2.0
```
+Release `0.2.0` provides abi3 wheels for Linux x86-64/aarch64, macOS
+universal2, and Windows x86-64. Other platforms build from the source
+distribution and require a Rust toolchain.
+
From the repository root:
```bash
From 4c383431ea17c6e06400a8e0cfb72811a09c3692 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Thu, 23 Jul 2026 15:10:35 +0000
Subject: [PATCH 2/3] security: harden release workflows and document audit
Co-authored-by: Jiangki
---
.github/dependabot.yml | 20 ++++++
.github/workflows/ci.yml | 40 ++++++-----
.github/workflows/docs-deploy.yml | 21 +++---
.github/workflows/release.yml | 30 ++++----
.gitleaks.toml | 8 +++
CHANGELOG.md | 31 ++++++---
docs/SECURITY_AUDIT.md | 111 ++++++++++++++++++++++++++++++
7 files changed, 212 insertions(+), 49 deletions(-)
create mode 100644 .github/dependabot.yml
create mode 100644 .gitleaks.toml
create mode 100644 docs/SECURITY_AUDIT.md
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..b0e19f9
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,20 @@
+version: 2
+
+updates:
+ - package-ecosystem: cargo
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+
+ - package-ecosystem: pip
+ directory: /python
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b01e374..1ba1486 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,16 +8,19 @@ on:
env:
CARGO_TERM_COLOR: always
+permissions:
+ contents: read
+
jobs:
rust:
name: Rust tests
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
components: rustfmt, clippy
- - uses: Swatinem/rust-cache@v2
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
@@ -45,9 +48,9 @@ jobs:
name: Comparison adapters
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
- - uses: Swatinem/rust-cache@v2
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Separate key prefix so this job does not thrash the default-feature
# rust-job cache with OSQP/Clarabel native build artifacts.
@@ -55,19 +58,22 @@ jobs:
- name: Adapter tests (OSQP + Clarabel)
run: cargo test -p ledge-bench-adapters --features osqp,clarabel
- # Builds the mdBook docs site and uploads it as an artifact. Public
- # GitHub Pages deployment is deliberately not wired up: it waits on the
- # public-release gate (roadmap 1.6) because Pages would expose the site.
+ # Builds the mdBook docs site and uploads it as an artifact. Public Pages
+ # deployment remains an explicit action in docs-deploy.yml.
docs:
name: Docs site (build only)
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install mdBook
run: |
mkdir -p "$HOME/.local/bin"
- curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz \
- | tar xz -C "$HOME/.local/bin"
+ archive="$RUNNER_TEMP/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz"
+ curl --fail --silent --show-error --location --output "$archive" \
+ https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz
+ echo "c0b903f01dd8f4edc644372ad2b80b1fdddd12552d37b6a098657cbd8eddd768 $archive" \
+ | sha256sum --check
+ tar xzf "$archive" -C "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Build book
run: |
@@ -75,7 +81,7 @@ jobs:
python scripts/generate_demo_assets.py --check
mdbook build docs/book
- name: Upload site artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: docs-site
path: docs/book/book
@@ -85,12 +91,12 @@ jobs:
name: Python binding
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
- - uses: dtolnay/rust-toolchain@stable
- - uses: actions/setup-python@v5
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
+ - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- - uses: Swatinem/rust-cache@v2
+ - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install package
run: |
python -m pip install --upgrade pip maturin
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index c397342..2836fcb 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -1,9 +1,8 @@
# Deploys the mdBook docs site to GitHub Pages.
#
-# DELIBERATELY manual-only (workflow_dispatch): GitHub Pages makes the site
-# public. Keep this manual until the clean-root public repository exists
-# (roadmap 1.6); after its first verified deployment, either continue manual
-# releases or add a `push: branches: [main]` trigger.
+# DELIBERATELY manual-only (workflow_dispatch) so documentation deployments
+# remain explicit. Add a `push: branches: [main]` trigger if continuous
+# deployment becomes preferable.
#
# Prerequisite: repository Settings -> Pages -> Source = "GitHub Actions".
name: Deploy docs
@@ -27,19 +26,23 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install mdBook
run: |
mkdir -p "$HOME/.local/bin"
- curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz \
- | tar xz -C "$HOME/.local/bin"
+ archive="$RUNNER_TEMP/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz"
+ curl --fail --silent --show-error --location --output "$archive" \
+ https://github.com/rust-lang/mdBook/releases/download/v0.4.52/mdbook-v0.4.52-x86_64-unknown-linux-gnu.tar.gz
+ echo "c0b903f01dd8f4edc644372ad2b80b1fdddd12552d37b6a098657cbd8eddd768 $archive" \
+ | sha256sum --check
+ tar xzf "$archive" -C "$HOME/.local/bin"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Build book
run: mdbook build docs/book
- name: Upload Pages artifact
- uses: actions/upload-pages-artifact@v3
+ uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: docs/book/book
- name: Deploy to GitHub Pages
id: deployment
- uses: actions/deploy-pages@v4
+ uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 28fc67c..f856bab 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,17 +24,19 @@ jobs:
name: Strict release preflight
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Check version and open-core gate
shell: bash
+ env:
+ RELEASE_TAG: ${{ inputs.tag }}
run: |
version="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | sed -n '1p')"
- test "${{ inputs.tag }}" = "v${version}" || {
- echo "::error::input tag ${{ inputs.tag }} does not match Cargo version v${version}"
+ test "$RELEASE_TAG" = "v${version}" || {
+ echo "::error::input tag $RELEASE_TAG does not match Cargo version v${version}"
exit 1
}
- test "$GITHUB_REF" = "refs/tags/${{ inputs.tag }}" || {
- echo "::error::select tag ${{ inputs.tag }} as the workflow ref (got $GITHUB_REF)"
+ test "$GITHUB_REF" = "refs/tags/$RELEASE_TAG" || {
+ echo "::error::select tag $RELEASE_TAG as the workflow ref (got $GITHUB_REF)"
exit 1
}
test "$(sed -n 's/^version = "\(.*\)"/\1/p' python/pyproject.toml | sed -n '1p')" = "$version"
@@ -65,20 +67,20 @@ jobs:
manylinux: "off"
runs-on: ${{ matrix.runner }}
steps:
- - uses: actions/checkout@v7
- - uses: actions/setup-python@v7
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
with:
# pyo3 abi3-py39: one wheel supports CPython 3.9 and newer.
python-version: "3.9"
- name: Build wheel
- uses: PyO3/maturin-action@v1
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
working-directory: python
target: ${{ matrix.target }}
manylinux: ${{ matrix.manylinux }}
args: --release --locked --out dist
- name: Upload wheel
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-wheel-${{ matrix.platform }}
path: python/dist/*.whl
@@ -89,9 +91,9 @@ jobs:
needs: preflight
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- name: Build sdist
- uses: PyO3/maturin-action@v1
+ uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1
with:
working-directory: python
command: sdist
@@ -99,7 +101,7 @@ jobs:
# workspace Cargo.lock is included and wheel builds are locked.
args: --out dist
- name: Upload sdist
- uses: actions/upload-artifact@v7
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: python-sdist
path: python/dist/*.tar.gz
@@ -117,10 +119,10 @@ jobs:
id-token: write
steps:
- name: Download distributions
- uses: actions/download-artifact@v8
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
pattern: python-*
path: dist
merge-multiple: true
- name: Publish with PyPI Trusted Publishing
- uses: pypa/gh-action-pypi-publish@release/v1
+ uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # release/v1
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..7194792
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,8 @@
+[extend]
+useDefault = true
+
+[[allowlists]]
+description = "Historical roadmap wording is not an API credential"
+regexTarget = "match"
+paths = ['''docs/PLAN\.md''']
+regexes = ['''API review, semver/MSRV/deprecation policy''']
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9f3fc4..ab16b61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,11 +4,30 @@ All notable changes to Ledge are documented here. The project follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and intends to use
[Semantic Versioning](https://semver.org/) once the public API stabilizes.
-Tagged GitHub releases will be created when publishing artifacts; until then,
-version links below point at the repository history.
+Version links below point to the corresponding GitHub tags or comparisons.
## [Unreleased]
+### Fixed
+
+- Replaced unsupported operator-name math macros with GitHub/KaTeX-compatible
+ roman notation across the README and mathematical documentation.
+- Updated package, documentation-site, and roadmap copy to reflect the
+ published `0.2.0` artifacts.
+
+### Security
+
+- Pinned third-party GitHub Actions to immutable commits, restricted default
+ workflow permissions to read-only, verified the downloaded mdBook archive,
+ and moved workflow-dispatch input handling out of interpolated shell source.
+- Added Dependabot coverage for Cargo, Python, and GitHub Actions plus a
+ repository security-audit report.
+
+### Documentation
+
+- Reworked the README quick start, navigation, feature overview, documentation
+ map, Rust example, platform guidance, and current roadmap.
+
## [0.2.0] - 2026-07-22
### Fixed
@@ -335,10 +354,6 @@ version links below point at the repository history.
`max(primal_tolerance, dual_tolerance)`.
- Convergence hints for badly scaled data now point at the
`scaling_iterations` setting instead of announcing planned equilibration.
-- Made the GitHub repository private and switched from Apache-2.0 to a
- proprietary license (`LicenseRef-Proprietary`). Crates are marked
- `publish = false`.
-
### Documentation
- Recorded the measured vector-ρ re-evaluation in `docs/DECISIONS.md`
@@ -346,8 +361,7 @@ version links below point at the repository history.
matrix, so vector ρ stays deferred with an explicit reopening condition;
roadmap 1.5 is closed for M2.
- Added `docs/PLAN.md` — vertical product plan (niche, expectations, stack,
- open-core vs paid boundary, commercialization gates). Notes current
- proprietary status and the M1 public-release gate.
+ open-core boundary, and technical acceptance gates).
- Rewrote `docs/ROADMAP.md` as executable milestones M0–M4 with exit criteria
and technical notes (scaling, certificates, L1 prox, workspace).
- Added `docs/DECISIONS.md` ADR log for plan adoption.
@@ -360,7 +374,6 @@ version links below point at the repository history.
- Removed pre-publish checklist docs; kept product roadmap and algorithm notes.
- Clarified scope and non-goals without historical repository-name framing.
- Added `SECURITY.md`, `docs/SMOKE_TIMINGS.md`, README diagrams, and CI.
-- Renamed GitHub repository from `mip-solver-lab` to `ledge`; updated URLs.
## [0.1.0] - 2026-07-14
diff --git a/docs/SECURITY_AUDIT.md b/docs/SECURITY_AUDIT.md
new file mode 100644
index 0000000..45249b9
--- /dev/null
+++ b/docs/SECURITY_AUDIT.md
@@ -0,0 +1,111 @@
+# Repository security and open-source audit
+
+Audit date: 2026-07-23
+
+Reviewed base: `9a68cf99fcbf667a27b415522f36442073e50874`
+
+Scope: public Git refs, tracked source and assets, package metadata,
+dependencies, release archives/configuration, documentation, and GitHub
+Actions workflows.
+
+## Executive conclusion
+
+No credential, customer data, private dependency, private repository URL, or
+proprietary implementation was found in the publicly reachable repository
+history or current tree.
+
+The public remote exposed only `main` and annotated tag `v0.2.0` during this
+review. Deleted pre-release branch names seen in an existing local clone were
+confirmed absent from the remote, no pull-request refs were exposed, and a
+pre-release commit ID was not readable through the public GitHub API. A normal
+push of a branch based on the clean public root does not transfer unrelated
+local objects.
+
+The repository does intentionally document its generic open-core boundary and
+the existence of a separately retained historical archive. Those documents do
+not contain an archive name/URL, customer identity, credential, or private
+package coordinate. An obsolete former repository name and private-state
+transition wording in `CHANGELOG.md` were unnecessary public detail and were
+removed during this audit.
+
+## Method
+
+- Enumerated tracked files, Git authors, local and remote refs, tags, public
+ branches, pull requests, and recent CI runs.
+- Scanned the working tree and public history with Gitleaks `8.30.1`, plus
+ targeted searches for tokens, keys, credentials, email addresses, private
+ hosts/IPs, absolute workstation paths, private package coordinates, and
+ commercial/customer markers.
+- Ran `scripts/check_open_core.sh --release`.
+- Inspected Cargo/Python metadata, lockfile sources, packaged legal notices,
+ generated-asset provenance, and registry availability.
+- Audited all 163 locked Rust dependencies with `cargo-audit 0.22.2` and the
+ current RustSec database.
+- Audited the published Python package and minimum declared NumPy runtime
+ (`ledge-portfolio==0.2.0`, `numpy==1.24.0`) with `pip-audit 2.10.1`.
+- Reviewed workflow permissions, event triggers, input handling, artifact
+ flow, external downloads, and third-party Action references.
+- Ran the repository's Rust, Python, documentation, asset, attribution, and
+ packaging checks listed below.
+
+## Findings and remediation
+
+| ID | Severity | Finding | Resolution |
+|---|---|---|---|
+| A-01 | Medium | Workflows referenced mutable Action tags; mdBook was streamed from the network directly into `tar` without an integrity check. | Third-party Actions are pinned to full commit SHAs. The mdBook archive is downloaded with fail-closed `curl`, checked against the release SHA-256 digest, then extracted. |
+| A-02 | Medium | CI had no explicit top-level token permission, so behavior depended on repository defaults. | CI now declares `permissions: contents: read`; publishing/deployment jobs retain only their narrowly required job permissions. |
+| A-03 | Low | A dispatch input was interpolated directly into shell source in the release preflight. Dispatch is write-restricted, but interpolation is avoidable. | The value is passed through the step environment and referenced as a quoted shell variable. |
+| A-04 | Low | README, Python package docs, roadmap, and release-state copy still said artifacts or public deployment were pending although `0.2.0` was live. | Updated to the verified PyPI, crates.io, docs, tag, and clean-root status. |
+| A-05 | Low | `CHANGELOG.md` exposed an obsolete former repository name and private-state transition details that were not useful to public users. | Removed the identifying/stale transition text; the generic public boundary documentation remains. |
+| A-06 | Low | RustSec reports `atomic-polyfill 1.0.3` as unmaintained. It is target-specific and reachable only through the `postcard` dev-dependency (`postcard -> heapless -> atomic-polyfill`), not the published runtime graph. | No forced replacement: there is no known vulnerability and `postcard` is used only for serialization tests. Track upstream and remove/replace it if the warning reaches a runtime target. |
+| A-07 | Informational | Gitleaks' generic-key heuristic treated a historical compatibility-roadmap phrase as a secret. Manual review confirmed plain prose. | Added a path- and exact-text-scoped allowlist; default Gitleaks rules remain enabled. |
+| A-08 | Informational | GitHub rejected read-only API queries for repository-wide Actions/security settings with the current integration identity. | Branch protection was observable and enabled; secret scanning, push protection, environment approvers, and default token settings still require an owner/admin check. |
+
+No known Rust or Python vulnerability was reported. Cargo sources resolve from
+crates.io only; the Python runtime resolves from PyPI; workspace `path`
+dependencies are local public crates and no private Git/registry dependency is
+present.
+
+## Additional hardening applied
+
+- Added weekly Dependabot checks for Cargo, Python, and GitHub Actions.
+- Added a repository Gitleaks configuration that extends the default rules.
+- Replaced unsupported operator-name macros throughout public Markdown math,
+ not only the first README formula.
+- Added the package/docs status, limitations, platform coverage, and security
+ posture to the README so users do not infer production guarantees.
+
+## Verification
+
+The completed change was checked with:
+
+```text
+gitleaks dir .
+gitleaks git . --log-opts=origin/main
+./scripts/check_open_core.sh --release
+cargo audit
+cargo fmt --all -- --check
+cargo clippy --workspace --all-targets --all-features -- -D warnings
+cargo test --workspace --all-features
+cargo package -p ledge-core --allow-dirty
+cargo package -p ledge-portfolio --allow-dirty --list
+python -m pytest python/tests -q
+python scripts/generate_demo_assets.py --check
+./scripts/generate_third_party_licenses.sh --check
+mdbook build docs/book
+maturin sdist
+actionlint
+lychee README.md docs/SECURITY_AUDIT.md
+```
+
+## Residual owner checks
+
+Repository administrators should confirm that GitHub secret scanning and push
+protection, private vulnerability reporting, Dependabot alerts, Actions
+default read permissions, and release-environment approvers are enabled.
+These settings are not stored in Git and could not all be read by the audit
+identity.
+
+This review does not establish copyright ownership, assess unpublished
+systems, or make Ledge safe for hostile multi-tenant inputs. Security reports
+should follow [`../SECURITY.md`](../SECURITY.md).
From 8e832ac0b23d624a08a7d609c81b87f74bf600f1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 23 Jul 2026 15:24:08 +0000
Subject: [PATCH 3/3] build(deps): bump actions/upload-pages-artifact from
3.0.1 to 5.0.0
Bumps [actions/upload-pages-artifact](https://github.com/actions/upload-pages-artifact) from 3.0.1 to 5.0.0.
- [Release notes](https://github.com/actions/upload-pages-artifact/releases)
- [Commits](https://github.com/actions/upload-pages-artifact/compare/56afc609e74202658d3ffba0e8f6dda462b719fa...fc324d3547104276b827a68afc52ff2a11cc49c9)
---
updated-dependencies:
- dependency-name: actions/upload-pages-artifact
dependency-version: 5.0.0
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
---
.github/workflows/docs-deploy.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 2836fcb..8d16808 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -40,7 +40,7 @@ jobs:
- name: Build book
run: mdbook build docs/book
- name: Upload Pages artifact
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
+ uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: docs/book/book
- name: Deploy to GitHub Pages