docs: the ten laws at the head of SPEC, one evidence page, the ceiling as its own note - #333
Conversation
…file The reference stated several rules as their own consequences. The clearest case: a missing parameter row had four listed meanings by position, but the codebase already knows why — `lowering.py`, `relational/compiler.py`, `test_roll.py` and `test_arithmetic_laws.py` all say a value takes the identity of the position it sits in. SPEC carried that as one table cell and half a sentence, then rederived its cases as if they were separate rules. So: §0 states ten laws, each naming the section that elaborates it, under one preamble — nothing is guessed. §6's position table becomes a derivation of law 8 rather than four independent rules, and the `fill=` prose in §6 and §7 stops restating what the law says. Two things deliberately not compressed. `sum(x + y)` ≠ `sum(x) + sum(y)` is a theorem of law 7, folded into it as a clause but kept at the top of the file, because it is the most surprising rule in the language. And a `where` operand's missing row reads as false because the coordinate cannot be claimed to exist, not because false is an identity — the table says so rather than forcing the algebra. One law per row, one row per source line: a semantic change is a one-line diff at the head of a file every reviewer already opens. Body −12 lines, laws +23. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… everything Follows the laws commit. Four moves, one theme: a fact should have one home, and where a machine can keep it there, it should. **The gallery's reference table is now generated.** It was hand-written, and hand-written *twice* — docs/models/index.md and docs/ports.md carried the same eleven rows verbatim. Both are now rendered by tools/constructs.py straight from examples/ports/references.json, the file tests/test_ports.py already asserts against, so a published optimum and an asserted one cannot disagree. The generated form also carries what the hand version dropped: rtol per port, which ports check duals, and the full provenance string rather than a summary of it. Adding a port is a JSON entry and a regenerate. **docs/ports.md is merged into the gallery**, which now answers both showcase questions in order — what can it say (the construct matrix), how do you know it is right (the references, the ladder, the ledger). The ladder and the ledger are argument and stay hand-written. **The ceiling moves to docs/design/ceiling.md.** "Two tiers", "Capability is not the ceiling" and "Composition" are design policy, not structure, and they are what pushed ARCHITECTURE.md past the "kept short on purpose" line it sets for itself: 499 -> 365. The module map stays put, because tests/test_architecture.py reads that file by path. **Duplication removed:** the alpha notice is included from README.md rather than restated on the home page; docs/README.md drops its third copy of the nav table; the 16 flat gallery nav entries become three groups; and the seven port pages that still inlined a reference script now collapse it, matching the two that already did. docs-organisation-proposal.md is deleted on its own terms — "Not a repo file: it describes work, and once the work lands the repo's own docs are the record." That work landed: CONTRIBUTING.md exists, the site is published, and its three questions are the gallery's opening line. New guard: every law in SPEC §0 must cite a section that exists. mkdocs already fails on a dead anchor, but a law citing *nothing* resolves fine and quietly becomes a second, drifting home for the rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 51 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThe PR reorganizes documentation around a new ceiling design page, expands specification semantics and validation, moves model evidence into generated gallery tables, updates model-page presentation, and redirects port documentation links. ChangesDocumentation architecture and navigation
Specification laws and validation
Generated model evidence and gallery pages
Port reference migration
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Documentation build overview
27 files changed ·
|
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CLAUDE.md`:
- Line 16: Update the feature-triage reference in CLAUDE.md to point from
docs/ARCHITECTURE.md to docs/design/ceiling.md, keeping the moved ceiling
document as the single referenced location. Also update the visible link label
in CONTRIBUTING.md to docs/design/ceiling.md.
In `@docs/models/index.md`:
- Around line 114-116: In the objective and dual-vector explanation, update the
inequality-sign wording to “what sign an inequality carries,” removing the
incorrect possessive construction while leaving the surrounding text unchanged.
In `@docs/README.md`:
- Around line 8-12: Update the CONTRIBUTING.md link in the documentation text to
use the repository’s canonical full GitHub URL instead of the relative
../CONTRIBUTING.md path, while preserving the existing `#the-docs` fragment and
surrounding text.
In `@docs/ROADMAP.md`:
- Line 5: Synchronize the visible link labels with the relocated ceiling page:
update docs/ROADMAP.md lines 5-5 to label the link as docs/design/ceiling.md or
“the ceiling page”; update docs/benchmarks.md lines 336-336 to replace
docs/ARCHITECTURE.md with the new page name; and update docs/guide.md lines
133-133 to replace ARCHITECTURE with “The ceiling” or equivalent.
In `@tools/constructs.py`:
- Around line 129-157: Update tools/constructs.py in references_table and the
reference-loading path to define structured published-value metadata, compare
each published value with its asserted objective using that entry’s rtol, and
raise an actionable error during loading when they disagree; retain the existing
table rendering. In tests/test_models_gallery.py, add a negative test covering
contradictory published and asserted values and verifying they are rejected.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0cc386f7-709f-4926-82a2-30b8e063d6df
📒 Files selected for processing (49)
CLAUDE.mdCONTRIBUTING.mdREADME.mddocs-organisation-proposal.mddocs/ARCHITECTURE.mddocs/README.mddocs/ROADMAP.mddocs/SPEC.mddocs/benchmarks.mddocs/design/ceiling.mddocs/guide.mddocs/index.mddocs/models/facility_location.mddocs/models/index.mddocs/models/pypsa_cyclic_storage.mddocs/models/pypsa_kvl.mddocs/models/pypsa_ramp.mddocs/models/pypsa_storage.mddocs/models/pypsa_transport.mddocs/models/pypsa_unit_commitment.mddocs/models/stigler_diet.mddocs/models/transport_dantzig.mddocs/models/transport_pwl.mddocs/models/tsp_mtz.mddocs/ports.mdexamples/ports/facility_location.yamlexamples/ports/pypsa_cyclic_storage.yamlexamples/ports/pypsa_kvl.yamlexamples/ports/pypsa_ramp.yamlexamples/ports/pypsa_storage.yamlexamples/ports/pypsa_transport.yamlexamples/ports/pypsa_unit_commitment.yamlexamples/ports/references/pypsa_cyclic_storage.pyexamples/ports/references/pypsa_kvl.pyexamples/ports/references/pypsa_ramp.pyexamples/ports/references/pypsa_storage.pyexamples/ports/references/pypsa_transport.pyexamples/ports/references/pypsa_unit_commitment.pyexamples/ports/references/transport_pwl.pyexamples/ports/stigler_diet.yamlexamples/ports/transport_dantzig.yamlexamples/ports/transport_pwl.yamlexamples/ports/tsp_mtz.yamlmkdocs.ymltests/test_doc_examples.pytests/test_docs_site.pytests/test_models_gallery.pytests/test_ports.pytools/constructs.py
💤 Files with no reviewable changes (2)
- docs-organisation-proposal.md
- docs/ports.md
| Two link rules make one set of files serve both places, and | ||
| `tests/test_docs_site.py` enforces them: **inside `docs/`, link relatively**; | ||
| **outside it, write the full GitHub URL** — the relative form resolves in the | ||
| repo and 404s on the site, silently. The rest is in *the docs* in | ||
| [CONTRIBUTING.md](../CONTRIBUTING.md#the-docs). |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a full GitHub URL for CONTRIBUTING.md.
../CONTRIBUTING.md escapes docs/ and becomes a site 404; tests/test_docs_site.py explicitly checks for this case. Replace it with the repository’s canonical full GitHub URL.
As per coding guidelines, links to files outside docs/ must use full GitHub URLs rather than relative paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/README.md` around lines 8 - 12, Update the CONTRIBUTING.md link in the
documentation text to use the repository’s canonical full GitHub URL instead of
the relative ../CONTRIBUTING.md path, while preserving the existing `#the-docs`
fragment and surrounding text.
Source: Coding guidelines
| def references_table() -> str: | ||
| """One row per verified port, straight from ``references.json``. | ||
|
|
||
| The optimum is written as ``repr`` rather than rounded: this is the number | ||
| the assertion uses, and a table that rounds it is a different claim from | ||
| the one the test makes. | ||
|
|
||
| ``rtol`` is a column rather than a footnote even though every port shares | ||
| one today — a footnote saying "all matched to 1e-09" becomes quietly false | ||
| the first time one does not, and nothing would catch it. | ||
| """ | ||
| lines = [ | ||
| '| port | optimum | `rtol` | duals | reference |', | ||
| '|---|---|---|---|---|', | ||
| ] | ||
| notes = [] | ||
| for name, entry in sorted(REFERENCES.items()): | ||
| duals = '**✔**' if entry.get('duals') else '·' | ||
| mark = f'[^{name}]' if entry.get('corroborated_by') else '' | ||
| lines.append( | ||
| f'| [{name}]({name}.md) | {entry["objective"]!r} | {entry["rtol"]:g} | ' | ||
| f'{duals} | {entry["provenance"]}{mark} |' | ||
| ) | ||
| if corroborated := entry.get('corroborated_by'): | ||
| notes.append(f'[^{name}]: {corroborated}') | ||
| # Corroboration runs to a paragraph, so it goes under the table rather than | ||
| # in a cell — `footnotes` is already on in mkdocs.yml and renders in the | ||
| # repo view as plain text that still reads. | ||
| return '\n'.join(lines) + ('\n\n' + '\n\n'.join(notes) if notes else '') |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Machine-check published and asserted reference values together.
The generator and its freshness test currently validate rendered text, not the truth of the evidence claims.
tools/constructs.py#L129-L157: add structured published-value metadata, compare it withobjectiveusingrtol, and fail at load time with an actionable error.tests/test_models_gallery.py#L104-L116: add a negative test proving contradictory published and asserted values are rejected.
📍 Affects 2 files
tools/constructs.py#L129-L157(this comment)tests/test_models_gallery.py#L104-L116
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tools/constructs.py` around lines 129 - 157, Update tools/constructs.py in
references_table and the reference-loading path to define structured
published-value metadata, compare each published value with its asserted
objective using that entry’s rtol, and raise an actionable error during loading
when they disagree; retain the existing table rendering. In
tests/test_models_gallery.py, add a negative test covering contradictory
published and asserted values and verifying they are rejected.
Source: Coding guidelines
…t is not
The gallery's hand-written math was already checked against the generator, but
only on `dispatch` and only in a way that mostly did not read the page: the
four symbols were a literal tuple in the test, so editing dispatch.md changed
nothing, and the one assertion that did read the page reached it by
`.split('$$')[1]` — positional, and silently checking a different equation the
moment anything is added above.
Six pages carry hand-written math. Two of them state the model: `dispatch` and
`transport` now assert that every subscripted quantity the summary writes is
one the generator can reach. That is the oracle direction the format exists to
claim — the notation a gallery reader expects is the spec, and `typeset/` is
what is under test.
The other four diverge deliberately, and forcing them to match would have been
wrong rather than laborious:
tsp_mtz states DFJ subtour elimination — the formulation the
language *refuses*, which is the point of the section it
sits in. The model is MTZ.
storage writes soc_{s-1}. The model rolls, and a roll wraps, which
the generator writes as the cyclic ⊖. Matching would mean
dropping the wrap or opening with a symbol nobody has met.
transport_dantzig is the textbook transportation problem with an abstract
c_{ij}; the model is the GAMS instance and its real cost.
piecewise shows one generator's curve; the model carries the
snapshot dim through λ too.
So the relationship is declared per page rather than assumed: a page in neither
list fails, which is what stops a new summary opting out by saying nothing.
Anchoring is on the `math:begin`/`math:end` markers, not a heading — `tsp_mtz`
states its math under "What genuinely is refused", so no heading name holds for
all six, while the markers are machine-maintained. And on `$$` blocks only:
across a whole page, `p_max` and `group_sum` parse as subscripts.
Restores the note in docs/README.md explaining what stays hand-written and what
bounds it, dropped in the previous commit's rewrite of that file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_typeset.py`:
- Around line 427-428: Update the generated-math handling around
gallery_math.BEGIN to validate that gallery_math.END exists after the opening
marker before slicing page. If the closing marker is missing or out of order,
raise a clear error identifying the model path and gallery_math.END; otherwise
preserve the existing page removal behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 11c55089-fbf1-4d91-9d41-dc6e5bfe0488
📒 Files selected for processing (2)
docs/README.mdtests/test_typeset.py
| if gallery_math.BEGIN in page: | ||
| page = page[: page.index(gallery_math.BEGIN)] + page[page.index(gallery_math.END) :] |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Report an unmatched generated-math marker clearly.
A missing gallery_math.END raises a generic ValueError. Validate that the closing marker occurs after BEGIN and fail with the model path and missing marker.
Proposed fix
- if gallery_math.BEGIN in page:
- page = page[: page.index(gallery_math.BEGIN)] + page[page.index(gallery_math.END) :]
+ begin = page.find(gallery_math.BEGIN)
+ if begin != -1:
+ end = page.find(gallery_math.END, begin + len(gallery_math.BEGIN))
+ if end == -1:
+ pytest.fail(
+ f'docs/models/{stem}.md has {gallery_math.BEGIN} without '
+ f'a matching {gallery_math.END}'
+ )
+ page = page[:begin] + page[end + len(gallery_math.END):]As per coding guidelines, validation must provide clear, actionable error messages.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if gallery_math.BEGIN in page: | |
| page = page[: page.index(gallery_math.BEGIN)] + page[page.index(gallery_math.END) :] | |
| begin = page.find(gallery_math.BEGIN) | |
| if begin != -1: | |
| end = page.find(gallery_math.END, begin + len(gallery_math.BEGIN)) | |
| if end == -1: | |
| pytest.fail( | |
| f'docs/models/{stem}.md has {gallery_math.BEGIN} without ' | |
| f'a matching {gallery_math.END}' | |
| ) | |
| page = page[:begin] + page[end + len(gallery_math.END):] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_typeset.py` around lines 427 - 428, Update the generated-math
handling around gallery_math.BEGIN to validate that gallery_math.END exists
after the opening marker before slicing page. If the closing marker is missing
or out of order, raise a clear error identifying the model path and
gallery_math.END; otherwise preserve the existing page removal behavior.
Source: Coding guidelines
…essive - `_summary` validated the opening math marker but not the closing one. A missing `math:end` was a bare ValueError naming no file; one sitting *above* `math:begin` spliced the generated block back in and checked it as hand-written math. `find(END, begin)` collapses both into one assertion that names the page. - Three visible link labels still read `docs/ARCHITECTURE.md` after the target moved to `docs/design/ceiling.md` (ROADMAP, benchmarks, guide), as did CONTRIBUTING's, and CLAUDE.md's feature-triage paragraph still sent readers to ARCHITECTURE for the ceiling. The module-map references there are correct and stay. - "what sign an inequality's carries" -> "an inequality carries", in the two places that sentence lives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es (#339) * fix(relational): foreach: [] is one scalar row, not an error The same file was two languages: `foreach: []` built a scalar constraint and solved on the eager lane, and raised `constraint 'budget' has no dims` on the native one. Hard rule 3 says both lanes accept exactly the same language, and here they demonstrably did not. #320 framed this as a language decision on the grounds that SPEC §2 is silent about whether `[]` satisfies a required `foreach`. Since #333 it is not silent: law 5 requires `dims(lhs) ∪ dims(rhs)` to *equal* `foreach`, and `sum(x, over=f) <= 120` has no free dims, so `[]` is precisely the signature that satisfies it. Refusing needs a rule the spec never states — so this is the native lane being wrong against §0, not a preference between two readings. The fix is where the assertion was, and it is the honest reading of it: assert out is not None, 'a declaration with no dims is rejected before ...' The empty cross join is not nothing, it is the *unit* — one row of no columns — and `_positional` labels that row by selecting a bare literal against it, which already worked. `math.prod(())` was already 1, so the row count needed nothing. The language also already had the concept: `_check_parameter` says "a parameter with no dims has exactly one coordinate — the empty one", which is the same reading applied to a value instead of a row. A **variable** with no dims stays refused, keeping its hint to use a dim of size 1: there the scalar-ness would be declared, where a constraint's is derived from a reduction it did not choose. SPEC says so rather than leaving the asymmetry to be discovered. Verified against the eager lane end to end — objective, primal and the dual, which is the part that pins *one* row rather than zero or one per `f`. Closes #320 * fix(relational): a scalar variable too — the empty coordinate, uniformly Fixing `foreach: []` for constraints and not variables left the identical hard rule 3 violation one declaration kind away: the eager lane builds and solves a scalar variable, the native lane raised variable 'slack' has no dims (scalars: use dims of size 1) and that hint pointed at exactly the dummy dimension the SPEC paragraph added in the previous commit says is never how a scalar is written. The justification offered there — that a constraint's scalar-ness is derived from a reduction while a variable's would be declared — does not survive contact with that: the argument against the workaround is the same argument in both positions. Nothing was holding it up. The guard had no test behind it, and with `_coordinate_product` returning the unit the whole path works unchanged: native now matches eager to the number, `slack` reads back as a single row. So §2 states it once for all of them rather than per declaration, since it is one rule: a product over nothing has one coordinate, not none — one value for a parameter, one column for a variable, one row for a constraint, and an objective needs no `foreach` because it is scalar by definition. The parity test carries all three positions in one model, so a future guard on any of them fails here. * fix(relational): the unit needs a column to exist in Review finding on the previous commits, and it was a silent wrong answer rather than a style point. `pl.LazyFrame()` is not the unit relation: polars cannot hold one row and no columns — collecting such a frame reports `(0, 0)` — so the row I claimed to be returning did not exist. It survived only the one path that selects a bare literal against it. Everything else lost it. A `where` on a scalar declaration filters the frame, and filtering nothing yields nothing, so `where: "budget > 100"` with budget 120 — true — dropped the constraint and the variable and solved anyway: objective 600 where 360 is right, with the budget row silently unenforced. So the unit gets a column to exist in (`UNIT`), and the masked path selects it where there are no dims to select instead. Every `_label_frame` path is now checked for a scalar declaration: unmasked, where-true, where-false, and a masked dimensional variable restricting a scalar row. One case cannot be fixed here and is refused instead: a **masked scalar variable** (#340). Presence is built as `select()` over no dims and hits the same polars limitation at the point it is constructed, so present and absent become the same frame and the restriction is lost — law 7 then fails to reach the row. Teaching presence to carry the empty coordinate touches four sites and changes how presence is represented, which is its own change. The refusal names the issue and the workaround. That refusal is a divergence, since the eager lane accepts it and gets it right. It is a smaller one than this lane refusing every scalar variable, which is what it did before, and it is loud either way. * fix(validation): refuse a masked scalar variable at load, not at build Law 2: everything decidable without data is decided without data. `foreach: []` and the presence of a `where` are both declared, so the executor was the wrong place — `lps.check()` passed the model, and check() is what a model repository runs in CI with nothing bound. Moves to validation.py, where it joins the error list rather than raising, so a model with several problems still reports them together.
Two commits. The theme is the same in both: a rule should have one home, and where a machine can keep it there, it should.
1.
SPEC §0— the ten lawsThe reference stated several rules as their own consequences. The clearest case: a missing parameter row had four listed meanings by position — but the codebase already knows why.
lowering.py:308,relational/compiler.py:461,test_roll.py:264andtest_arithmetic_laws.py:200all say a value takes the identity of the position it sits in. SPEC carried that as one table cell and half a sentence, then rederived its cases as if they were separate rules.So §0 states ten laws, each citing the section that elaborates it, under one preamble — nothing is guessed. §6's position table is now a derivation of law 8 with a "why that reading" column; the
fill=prose in §6 and §7 stops restating it.One law per table row, one row per source line: a semantic change is a one-line diff at the head of a file every reviewer already opens.
Two things deliberately not compressed:
sum(x + y)≠sum(x) + sum(y)is a theorem of law 7, folded in as a clause but kept at the top of the file — it is the most surprising rule in the language.whereoperand's missing row reads asfalsebecause the coordinate cannot be claimed to exist, not becausefalseis an identity. Three of the four cases are identity arguments; the fourth isn't, and the table says so rather than forcing the algebra.Also dropped the two "it used to evaluate to scalar
False" notes —CLAUDE.mdsays historical notes belong in git.2. One evidence page, and the ceiling as its own note
The reference table is generated now. It was hand-written, and hand-written twice —
docs/models/index.mdanddocs/ports.mdcarried the same eleven rows verbatim. Both now render fromexamples/ports/references.json, the filetests/test_ports.pyalready asserts against, so a published optimum and an asserted one cannot disagree. The generated form also carries what the hand version dropped:rtolper port, which ports check duals, and the full provenance string rather than a summary. Adding a port is a JSON entry and a regenerate.docs/ports.mdmerges into the gallery, which now answers both showcase questions in order — what can it say (the construct matrix), how do you know it is right (references, ladder, ledger).The ceiling moves to
docs/design/ceiling.md. "Two tiers", "Capability is not the ceiling" and "Composition" are design policy, not structure, and they are what pushedARCHITECTURE.mdpast the "kept short on purpose" line it sets for itself. The module map stays put —tests/test_architecture.pyreads that file by path.Duplication removed: the alpha notice is included from
README.mdrather than restated;docs/README.mddrops its third copy of the nav table; 16 flat gallery nav entries become three groups; seven port pages that still inlined a reference script now collapse it, matching the two that already did.docs-organisation-proposal.mdis deleted on its own terms — "Not a repo file: it describes work, and once the work lands the repo's own docs are the record." That work landed.3. Every hand-written summary is an oracle, or says why it is not
Added after review asked whether the hand-written math on model pages already tests the generator. It did — but thinner than it looked.
test_the_gallery_notation_is_reproducible_from_the_modelhardcoded its four symbols in the test file, so editingdispatch.mdchanged nothing; the one assertion that read the page reached it by.split('$$')[1], positional into the document.Six pages carry hand-written math. Two state the model —
dispatchandtransportnow assert every subscripted quantity the summary writes is one the generator can reach. That is the oracle direction the format exists to claim: the notation a gallery reader expects is the spec, andtypeset/is what is under test.Four diverge deliberately, and forcing a match would have been wrong rather than laborious:
tsp_mtzstoragesoc_{s-1}; the model rolls, and a roll wraps — the generator writes the cyclic⊖transport_dantzigc_{ij}; the model is the GAMS instance and its real costpiecewiseSo the relationship is declared per page: a page in neither list fails, which is what stops a new summary opting out by saying nothing. Mutation-tested both ways.
Anchoring is on the
math:begin/math:endmarkers rather than a heading —tsp_mtzstates its math under "What genuinely is refused", so no heading name holds for all six — and on$$blocks only, because across a whole pagep_maxandgroup_sumparse as subscripts.Also restores the
docs/README.mdparagraph on what stays hand-written and what bounds it, which commit 2 dropped. That omission was the real regression, and the review question is what caught it.Shape
docs/ARCHITECTURE.mddocs/models/index.mdports.md)docs/design/ceiling.mddocs/README.mddocs/ports.mddocs-organisation-proposal.mddocs/SPEC.mdNet ≈ −200 lines, one file fewer.
SPEC.mdgrows: the law block costs 23 lines and buys the scannable rulebook. Worth stating plainly rather than claiming a shrink.New guard
Every law in
SPEC §0must cite a section that exists. mkdocs already fails the strict build on a dead anchor, but a law citing nothing resolves fine and quietly becomes a second, drifting home for the rule. Mutation-tested both ways.tests/test_doc_examples.py::TRACKEDgainsdocs/design/ceiling.md— it has no python/yaml fences today, but an unlisted page is silently unchecked if one is ever added.Verification
663 passed, 4 skipped, 1 xfaileduv run mkdocs build --strict— zero warningsruff check/ruff format --check/pyrefly checkcleanNot in this PR
The tutorial and task recipes, deferred deliberately:
guide.mdchanged 4 times in the last 120 commits whileSPEC.mdchanged 52, and two of the drafted recipes document patterns Track 1 items 1–4 are committed to replacing. Revisit trigger is those items landing.🤖 Generated with Claude Code
Summary by CodeRabbit
Spec / Behavior Changes
Documentation