Skip to content

docs: the ten laws at the head of SPEC, one evidence page, the ceiling as its own note - #333

Merged
FBumann merged 4 commits into
mainfrom
docs/spec-laws
Jul 30, 2026
Merged

docs: the ten laws at the head of SPEC, one evidence page, the ceiling as its own note#333
FBumann merged 4 commits into
mainfrom
docs/spec-laws

Conversation

@FBumann

@FBumann FBumann commented Jul 29, 2026

Copy link
Copy Markdown
Owner

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 laws

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:308, relational/compiler.py:461, test_roll.py:264 and test_arithmetic_laws.py:200 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 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.
  • A where operand's missing row reads as false because the coordinate cannot be claimed to exist, not because false is 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.md says 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 twicedocs/models/index.md and docs/ports.md carried the same eleven rows verbatim. Both now render 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. Adding a port is a JSON entry and a regenerate.

docs/ports.md merges 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 pushed ARCHITECTURE.md past the "kept short on purpose" line it sets for itself. The module map stays put — tests/test_architecture.py reads that file by path.

Duplication removed: the alpha notice is included from README.md rather than restated; docs/README.md drops 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.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.

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_model hardcoded its four symbols in the test file, so editing dispatch.md changed 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 modeldispatch and transport now 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, and typeset/ is what is under test.

Four diverge deliberately, and forcing a match would have been wrong rather than laborious:

page why
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 — the generator writes the cyclic
transport_dantzig 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: 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:end markers rather than a heading — tsp_mtz states its math under "What genuinely is refused", so no heading name holds for all six — and on $$ blocks only, because across a whole page p_max and group_sum parse as subscripts.

Also restores the docs/README.md paragraph 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

File Before After
docs/ARCHITECTURE.md 499 365
docs/models/index.md 107 204 (absorbed ports.md)
docs/design/ceiling.md 148 (new)
docs/README.md 42 20
docs/ports.md 117 merged away
docs-organisation-proposal.md 146 removed
docs/SPEC.md 606 617 (laws +23, body −12)

Net ≈ −200 lines, one file fewer. SPEC.md grows: 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 §0 must 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::TRACKED gains docs/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 xfailed
  • uv run mkdocs build --strict — zero warnings
  • ruff check / ruff format --check / pyrefly check clean

Not in this PR

The tutorial and task recipes, deferred deliberately: guide.md changed 4 times in the last 120 commits while SPEC.md changed 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

    • Clarified the language “ceiling” and admissibility rules, including refined error/refusal behavior at load time.
    • Undeclared bare names now produce load-time errors (rather than allowing empty/unsolved outcomes).
    • Tightened explanations for absence propagation, shifting, and filling/roll restrictions.
  • Documentation

    • Reorganized the documentation navigation into teaching models, a PyPSA ladder, published optima, and design notes; removed the old ports gallery page.
    • Expanded the new “ceiling” documentation and improved cross-links/status messaging.
    • Enhanced model verification pages with regenerated evidence/reference tables and clearer math/reference guidance.

FBumann and others added 2 commits July 29, 2026 18:25
…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>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f0caa3fc-8966-4732-8249-aba11732d735

📥 Commits

Reviewing files that changed from the base of the PR and between bed7f37 and 4f51366.

📒 Files selected for processing (7)
  • CLAUDE.md
  • CONTRIBUTING.md
  • docs/ROADMAP.md
  • docs/benchmarks.md
  • docs/guide.md
  • docs/models/index.md
  • tests/test_typeset.py
📝 Walkthrough

Walkthrough

The 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.

Changes

Documentation architecture and navigation

Layer / File(s) Summary
Ceiling documentation and site navigation
CLAUDE.md, CONTRIBUTING.md, README.md, docs/ARCHITECTURE.md, docs/README.md, docs/ROADMAP.md, docs/benchmarks.md, docs/design/ceiling.md, docs/guide.md, docs/index.md, mkdocs.yml
The ceiling is documented and linked through repository guidance, while documentation navigation and generated-content conventions are reorganized.

Specification laws and validation

Layer / File(s) Summary
Specification laws and validation
docs/SPEC.md, tests/test_doc_examples.py, tests/test_docs_site.py
The specification adds ten laws and clarifies absence, masking, undeclared names, and shift behavior; tests validate law citations and track the ceiling document.

Generated model evidence and gallery pages

Layer / File(s) Summary
Generated model evidence and gallery pages
docs/models/*, tools/constructs.py, tests/test_models_gallery.py, tests/test_typeset.py
The gallery gains generated reference evidence, verification and ledger content, collapsible reference snippets, and tests for generated tables and model-summary notation.

Port reference migration

Layer / File(s) Summary
Port reference migration
examples/ports/*, examples/ports/references/*, tests/test_ports.py
Port example comments, reference docstrings, and test documentation now point to docs/models/index.md.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and accurately captures the main doc changes: ten laws in SPEC, a single evidence page, and the new ceiling note.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/spec-laws

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@read-the-docs-community

read-the-docs-community Bot commented Jul 29, 2026

Copy link
Copy Markdown

Documentation build overview

📚 farkas | 🛠️ Build #33829734 | 📁 Comparing 4f51366 against latest (130d49e)

  🔍 Preview build  

27 files changed · + 1 added · ± 25 modified · - 1 deleted

+ Added

± Modified

- Deleted

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 130d49e and cd963b7.

📒 Files selected for processing (49)
  • CLAUDE.md
  • CONTRIBUTING.md
  • README.md
  • docs-organisation-proposal.md
  • docs/ARCHITECTURE.md
  • docs/README.md
  • docs/ROADMAP.md
  • docs/SPEC.md
  • docs/benchmarks.md
  • docs/design/ceiling.md
  • docs/guide.md
  • docs/index.md
  • docs/models/facility_location.md
  • docs/models/index.md
  • docs/models/pypsa_cyclic_storage.md
  • docs/models/pypsa_kvl.md
  • docs/models/pypsa_ramp.md
  • docs/models/pypsa_storage.md
  • docs/models/pypsa_transport.md
  • docs/models/pypsa_unit_commitment.md
  • docs/models/stigler_diet.md
  • docs/models/transport_dantzig.md
  • docs/models/transport_pwl.md
  • docs/models/tsp_mtz.md
  • docs/ports.md
  • examples/ports/facility_location.yaml
  • examples/ports/pypsa_cyclic_storage.yaml
  • examples/ports/pypsa_kvl.yaml
  • examples/ports/pypsa_ramp.yaml
  • examples/ports/pypsa_storage.yaml
  • examples/ports/pypsa_transport.yaml
  • examples/ports/pypsa_unit_commitment.yaml
  • examples/ports/references/pypsa_cyclic_storage.py
  • examples/ports/references/pypsa_kvl.py
  • examples/ports/references/pypsa_ramp.py
  • examples/ports/references/pypsa_storage.py
  • examples/ports/references/pypsa_transport.py
  • examples/ports/references/pypsa_unit_commitment.py
  • examples/ports/references/transport_pwl.py
  • examples/ports/stigler_diet.yaml
  • examples/ports/transport_dantzig.yaml
  • examples/ports/transport_pwl.yaml
  • examples/ports/tsp_mtz.yaml
  • mkdocs.yml
  • tests/test_doc_examples.py
  • tests/test_docs_site.py
  • tests/test_models_gallery.py
  • tests/test_ports.py
  • tools/constructs.py
💤 Files with no reviewable changes (2)
  • docs-organisation-proposal.md
  • docs/ports.md

Comment thread CLAUDE.md
Comment thread docs/models/index.md Outdated
Comment thread docs/README.md
Comment on lines +8 to +12
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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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

Comment thread docs/ROADMAP.md Outdated
Comment thread tools/constructs.py
Comment on lines +129 to +157
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 '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 with objective using rtol, 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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between cd963b7 and bed7f37.

📒 Files selected for processing (2)
  • docs/README.md
  • tests/test_typeset.py

Comment thread tests/test_typeset.py Outdated
Comment on lines +427 to +428
if gallery_math.BEGIN in page:
page = page[: page.index(gallery_math.BEGIN)] + page[page.index(gallery_math.END) :]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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>
@FBumann
FBumann enabled auto-merge (squash) July 30, 2026 07:19
@FBumann
FBumann merged commit 71ae5f7 into main Jul 30, 2026
4 checks passed
FBumann added a commit that referenced this pull request Jul 30, 2026
…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.
@FBumann
FBumann deleted the docs/spec-laws branch July 31, 2026 10:51
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