docs: make every module page say something true and compile-checked - #21
Merged
Conversation
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
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 the39 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 nothinga reader pasted would build. 16 rewritten. All 39 rust blocks now compile and are self-contained.
Worst case fixed:
hrpandhcaashowedallocate(&prices)?returning weights. The real signaturestake 7 and 10 arguments and return
Result<(), _>— the examples were binding()to a variablenamed
weights.2 — A gate so it cannot regress (
eba61b4)npm run check:examplesextracts every rust fence from the generated pages, emits a throwaway crateat
target/doc-examples/depending oncrates/openquantby path, and cargo-checks it. Itauto-discovers every
pub enum *Errorto build a universal?target, since only 6 of 16 implementstd::error::Error.Proven against the original
hrpdefect — reproduces bothE0425andE0061.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:286really does averagenormalised |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_lossscoring.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 threefields are now required and asserted by the generator — including that every
relatedModulesslug resolves — so a future stub is a loud build failure, not silent filler.
The duplicated
## Implementation Notesblock, which reprintedrisk_notesverbatim on 39 pages,is gone; it now renders once as Risk Notes and Caveats.
Examples:
hcaa129 → 504 words,cross-validation140 → 472.Bonus: fixed a cwd trap in the generator —
outDirnow resolves from the script's own path, sorunning it from the repo root writes the real pages instead of a stray
src/tree.Verification (real exit codes, no pipes)
npm install0 ·astro build0 ·check-content-schema0 ·check:examples0 (39 examples) ·check-links0 · 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:examplesexit non-zero witherror[E0308], and restoring it returns to 0.Environment note:
cargocannot populate~/.cargo/registryfrom inside a sandboxed shell on thedev box, so
check:examplesneeds an unsandboxed shell there. Irrelevant to CI.