Skip to content

docs: make every module page say something true and compile-checked - #21

Merged
Sean-Koval merged 9 commits into
mainfrom
docs/moduledocs-substance
Aug 31, 2026
Merged

docs: make every module page say something true and compile-checked#21
Sean-Koval merged 9 commits into
mainfrom
docs/moduledocs-substance

Conversation

@Sean-Koval

Copy link
Copy Markdown
Contributor

Stacked on #18 — merge that first; this targets its branch so the diff stays clean.

Why

Three defect classes all lived in one file, docs-site/src/data/moduleDocs.ts, which generates the
39 module pages. Splitting them across agents would have meant three-way conflicts on 1750 lines, so
they were done together.

1 — Every documented Rust example now compiles (765d762)

The audit found 9 non-compiling examples. Compiling all 33 found 7 more — signature-correct but
referencing variables that were never defined (data, pnl, clf, corr, prices…), so nothing
a reader pasted would build. 16 rewritten. All 39 rust blocks now compile and are self-contained.

Worst case fixed: hrp and hcaa showed allocate(&prices)? returning weights. The real signatures
take 7 and 10 arguments and return Result<(), _> — the examples were binding () to a variable
named weights.

2 — A gate so it cannot regress (eba61b4)

npm run check:examples extracts every rust fence from the generated pages, emits a throwaway crate
at target/doc-examples/ depending on crates/openquant by path, and cargo-checks it. It
auto-discovers every pub enum *Error to build a universal ? target, since only 6 of 16 implement
std::error::Error.

Proven against the original hrp defect — reproduces both E0425 and E0061.

Not wired into .github/workflows — that file is contended by #17; wiring follows separately.

3 — Maths (0346c31)

All 6 flagged issues fixed, plus the deflated Sharpe the audit noted three pages promised and none
delivered. Formulas gained a where: field so symbols can actually be defined.

One documented disagreement with the audit, and it changed the fix. The audit called the
feature-diagnostics MDI formula wrong. It is not — feature_diagnostics.py:286 really does average
normalised |coefficient|. The name is wrong, not the formula. Substituting the textbook tree
formula would have made the page describe something the code does not compute. Disambiguated instead.

That check also turned up an unreported defect: the MDA denominator was wrong for the default
neg_log_loss scoring.

Four formulas the author was not confident about are listed in the report and left unchanged
flagged rather than guessed.

4 — The 27 stub modules (7e58f41)

All 27 filled with conceptOverview / whenToUse / relatedModules, none deferred. Those three
fields are now required and asserted by the generator — including that every relatedModules
slug resolves — so a future stub is a loud build failure, not silent filler.

The duplicated ## Implementation Notes block, which reprinted risk_notes verbatim on 39 pages,
is gone; it now renders once as Risk Notes and Caveats.

Examples: hcaa 129 → 504 words, cross-validation 140 → 472.

Bonus: fixed a cwd trap in the generator — outDir now resolves from the script's own path, so
running it from the repo root writes the real pages instead of a stray src/ tree.

Verification (real exit codes, no pipes)

npm install 0 · astro build 0 · check-content-schema 0 · check:examples 0 (39 examples) ·
check-links 0 · generator idempotent (0 changes on second run).

Independently re-verified, including a negative test: injecting a type error into a generated page's
rust block makes check:examples exit non-zero with error[E0308], and restoring it returns to 0.

Environment note: cargo cannot populate ~/.cargo/registry from inside a sandboxed shell on the
dev box, so check:examples needs an unsandboxed shell there. Irrelevant to CI.

Sean-Koval and others added 9 commits August 30, 2026 11:02
generate-module-doc-pages.mjs hardcoded 'status: validated' and
last_validated: '2026-03-02' into every page it emitted. Nothing in the
script consults a reviewer, a date, or git history, so all 40 generated
pages (39 modules + the index) carried an identical review stamp that no
human ever produced.

Emit 'status: generated' + generated_from + last_generated instead. A
machine-emitted page has by definition not been reviewed; 'reviewed' and
'validated' are now reachable only by a person editing a page deliberately.

last_generated is date-only so repeated runs on the same day are
byte-identical and the generator stays idempotent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ucq4aRivrPdTxkqQpgMePd
The gate checked that title/description/status/last_validated existed and
were non-empty. It never looked at what they said, so 'status: banana'
passed and CI's green 'content schema check passed' certified nothing.

Now it:
  - restricts status to generated | draft | reviewed | validated, and ties
    each status to the date field that backs it (generated -> last_generated,
    reviewed/validated -> last_validated, draft -> no date claim at all);
  - requires those dates to be real ISO dates, so 2026-02-31 is rejected
    rather than merely matching the shape;
  - fails when a page's last change (git commit date, or filesystem mtime
    when the working copy is dirty or untracked) is NEWER than the date it
    claims.

That last rule is the one that makes the stamp mean something. A page can no
longer claim review from before its content last moved, which makes bulk
stamping a whole corpus with one date impossible. Failure messages say what
to do, including the honest option of lowering the status rather than
bumping the date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ucq4aRivrPdTxkqQpgMePd
All 19 claimed 'validated' (16) or 'in_review' (3) against a single
hardcoded date of 2026-03-02 that no per-page review ever produced.

Restamped from Pam's per-page verdicts in the content audit's section (B)
inventory: 'solid' and 'thin' pages deliver on their own title and become
'reviewed' (6); 'stub' and 'redundant' pages do not and become 'draft' (13).
Draft pages lose last_validated entirely rather than carrying a weaker date,
because a draft has not been validated by anyone.

Deviation from the audit, called out deliberately: section (D) P0 heads its
row '13 reviewed, 7 draft', but that is inverted with respect to Pam's own
section (B) verdicts, which mark 13 of these pages stub or redundant and only
6 solid or thin. The per-page judgement is the evidence and it points the
other way, so this follows the inventory. It is also the conservative
direction: this pass under-claims rather than over-claims, which is the
entire point of P0.

Note the site total is 59 pages, of which 40 are generator-owned (39 modules
plus modules/index.md, which section (B) lists among the hand-written pages
but which generate-module-doc-pages.mjs in fact emits). So 19 pages are
hand-written here, not 20.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ucq4aRivrPdTxkqQpgMePd
A status nobody can see does not protect a reader. Every page now renders
its status in the Starlight banner slot: a coloured pill (GENERATED / DRAFT
/ REVIEWED) plus one line saying what the status means, styled in the
existing palette in src/styles/starlight.css with light and dark variants.

Wired through frontmatter banner.content rather than a Starlight component
override: an override must be registered in astro.config.mjs, which this
branch was told not to touch. The generator emits the banner for the 40
generated pages; the 19 hand-written pages carry it directly. A ':has()'
guard keeps the existing site-wide banner gradient intact for any other
banner use.

Because the badge lives beside the status rather than being derived from it,
check-content-schema.mjs now also fails when the two disagree, so they
cannot drift apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ucq4aRivrPdTxkqQpgMePd
Two problems the build surfaced once the statuses changed.

1. src/content/config.ts carried its own independent status enum
   ('draft' | 'in_review' | 'validated') that nobody had kept in step with the
   generator or the schema gate, so 'reviewed' and 'generated' failed the
   build. It now declares the same four values, plus last_generated and
   generated_from, and makes status REQUIRED rather than optional — a page
   with no status is a page making an unexamined claim by omission.

2. index.md was already using its banner slot for the Quickstart call to
   action, so adding a status banner produced a duplicate YAML key and broke
   the content sync. The badge and the call to action now share one banner,
   with a scoped CSS rule giving links there the status colours instead of
   the gradient banner's near-white.

Verified: 'npx astro build' exits 0 and the badge renders on 58 of 59 pages.
The one page without it is modules/index.md, which astro.config.mjs shadows
with a /modules redirect so it is unreachable in production regardless — a
pre-existing defect this branch does not own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ucq4aRivrPdTxkqQpgMePd
Sixteen of the 33 Rust usage examples were written against a symbol's name
without ever being checked against its signature. Nine could not compile at
all; the rest referenced variables that were never defined, so nothing a
reader pasted into a file would build.

The two flagship allocator pages were the worst: both hrp and hcaa showed a
one-argument `allocate(&prices)?` returning weights. `HierarchicalRiskParity::
allocate` takes six arguments, `HierarchicalClusteringAssetAllocation::allocate`
takes nine, and both return `Result<(), _>` and mutate the struct in place — so
the examples bound `()` to a variable named `weights`. Also corrected:

- `RegressionModelFingerprint::new()` takes no arguments and the accessor is
  `get_effects()`, not `linear_effects()`; `fit` needs `&mut self`.
- `RiskMetrics::calculate_*` are `&self` methods on a unit struct, so the call
  needs a receiver.
- `PurgedKFold::new` takes the label spans and returns a `Result`.
- `get_weights_by_time_decay` takes triple-barrier events and a close series,
  not a return vector.
- `get_sadf` takes `model` and `add_const`; `mean_decrease_accuracy` takes the
  purged splits, feature names, weights and scoring, and a `&mut` model.
- `drawdown_and_time_under_water` runs over a timestamped equity curve.
- `SequentiallyBootstrappedBaggingClassifier::new(100)` sets `random_state`,
  not `n_estimators`, which silently defaults to 10 — now stated and set.

Every example is now self-contained: the inputs it needs are constructed in
the block, and the argument each magic number binds to is named in a comment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DNfTVPyifBs1E4hweKi47
Nine non-compiling examples shipped green because no gate compiled a single
line of the documented Rust: check:content-schema validates frontmatter shape,
and check:links validates hrefs. Nothing checked that a symbol's arity, its
receiver, or its argument types matched the source.

`npm run check:examples` (docs-site/scripts/check-doc-examples.mjs) extracts
every ```rust fence from the generated pages under src/content/docs/modules/,
wraps each in its own module in a throwaway crate under target/doc-examples/
that depends on crates/openquant by path, and runs `cargo check`. Wrong arity,
a missing receiver, a renamed method or a wrong argument type is now a compile
error.

Two details worth knowing:

- Most openquant error enums do not implement std::error::Error, so
  `Box<dyn Error>` is not available as a universal `?` target. The generated
  crate discovers every `pub enum *Error` under crates/openquant/src and emits
  a `From` impl into one local `DocError`, so a new module's error type is
  picked up without touching this script.
- It shares the workspace target dir, so openquant's dependency graph is not
  rebuilt.

Compile-only by design: the snippets are never executed (several read files or
spawn threads), so this proves the API surface is real, not that the numbers
are right.

Not wired into .github/workflows/ — CI wiring is being done separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DNfTVPyifBs1E4hweKi47
Formulas carry an optional `where:` field now, rendered under the equation.
Undefined symbols were the norm on these pages — the equations named `k`, `p`,
`T`, `⊕`, `P_seq` and `u` and defined none of them — so a slot for definitions
had to exist before any of them could be fixed.

Corrections, each checked against the implementation:

- **filters** — the CUSUM was one-sided: `S_t = max(0, ·)` is non-negative, so
  the stated `|S_t| > h` trigger was vacuous and the filter only detected
  upward breaks. `cusum_filter_indices_checked` runs both arms and resets the
  breached one, which is now what the page says.
- **hcaa** — `w_left, w_right ∝ 1/σ²_left, 1/σ²_right` was not a well-formed
  proportionality. `recursive_bisection` computes α = 1 − m_left/(m_left +
  m_right) over the selected `allocation_metric`, which generalises the HRP
  split; the sharpe_ratio branch inverts the sign and equal_weighting skips
  the split, both now stated.
- **feature-diagnostics vs feature-importance** — these gave incompatible
  definitions of MDI. Pam's audit read the Python page as wrong, but the code
  says otherwise: `mdi_importance` really does average normalised |coefficient|
  over bootstrap replicas of a linear probability model. The formula was right
  and the *name* was wrong, so the fix is to say so on the page rather than
  substitute a tree formula the function does not compute. Both pages now name
  which estimator they document and link to the other. The MDA denominator was
  also wrong for the default scoring: `_score_with_perm_groups` divides by −S
  under neg_log_loss and by 1−S only under accuracy.
- **hpc-parallel / streaming-hpc** — `throughput = atoms/seconds` is a unit
  definition, not a foundation. Replaced with the equal-cost condition that
  actually derives the nested boundary `b_i = N√(i/M)` (it makes every
  molecule cost N²/2M), and with the two-threshold alert rule that
  `StreamingPipeline::update` implements.
- **sb-bagging** — `S_b ~ P_seq(u)` defined neither symbol. Replaced with the
  draw probability `seq_bootstrap` computes, and the constructor's real
  n_estimators default noted on the bagging predictor.
- **VPIN** — the two pages disagreed, and neither matched its code. The
  equal-volume-bucket form belongs to streaming_hpc; the bar-based `get_vpin`
  normalises by the *current* bar's volume, outside the sum, because bars are
  not equal-volume. Both now use V^B/V^S and each explains the other.
- **util-volatility** — `k` in Yang–Zhang was undefined; it is
  0.34/(1.34 + (n+1)/(n−1)), a variance-minimising weight, not a free knob.
- **cross-validation** — `p`, `T` and `⊕` were undefined; the purge is now
  stated as label-span overlap, which is what makes it different from
  dropping neighbours.
- **backtest-statistics** — three pages advertise deflated Sharpe and none
  gave a formula. Added PSR and DSR as `deflated_sharpe_ratio` computes them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DNfTVPyifBs1E4hweKi47
Twenty-seven of the 39 module pages were 129–457-word heading skeletons —
"## Subject" rendering one bolded taxonomy string, one sentence of motivation,
and a closing section that reprinted the frontmatter. They were thin for a
structural reason: `conceptOverview`, `whenToUse` and `relatedModules` were
optional in the ModuleDoc type, so omitting them produced a thin page rather
than an error.

All three are now required and the generator asserts them, including that
every `relatedModules` slug resolves to a real module (a dangling one would
have generated a 404 link). A new module that arrives without them fails the
build instead of shipping as a stub. The two dead fall-through branches are
gone with them, and `whyItExists` is marked deprecated — `conceptOverview`
now says the same thing with substance behind it.

The 27 overviews are written from the source, not from the module name: what
the algorithm actually does, when to reach for it, and — the part a generated
page can never supply — when to reach for something else instead. hrp vs cla
vs portfolio_optimization, hcaa vs hrp, streaming_hpc vs
microstructural_features, strategy_risk vs risk_metrics, and MDI vs MDA vs SFI
each say plainly which one to pick and why. Several record a fact only the
source reveals: sb_bagging's n_estimators defaulting to 10, VPIN's differing
normalisation across two modules, synthetic_backtesting's most useful output
being `no_stable_optimum`.

Also drops the duplicated notes emission. `doc.notes` was written twice per
page — once as the `risk_notes` frontmatter key, once verbatim as a body
section mislabelled "Implementation Notes" (they are risk caveats; nothing on
any page describes an implementation). Nothing reads the frontmatter key — it
is optional in src/content/config.ts and referenced by no component or script
— so the array is now rendered once, as "Risk Notes and Caveats".

And the generator resolves its output directory from its own path rather than
from process.cwd(), so running it from the repo root writes the real pages
instead of silently creating a stray src/ tree there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DNfTVPyifBs1E4hweKi47
@Sean-Koval
Sean-Koval changed the base branch from docs/p0-honest-status to main August 31, 2026 03:13
@Sean-Koval
Sean-Koval merged commit b527eb2 into main Aug 31, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant