Skip to content

docs: each fact about the Python API is stated once, in the docstring of the name it belongs to - #1769

Merged
FBumann merged 4 commits into
claude/api-reference-from-docstringsfrom
claude/reference-dedup
Sep 25, 2026
Merged

FBumann merged 4 commits into
claude/api-reference-from-docstringsfrom
claude/reference-dedup

Conversation

@FBumann

@FBumann FBumann commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Prompt: Is there even more duplication in the docs now? Especially in Reference? As a stacked PR

Note

The following content was generated by AI.

The API page is now the rendered reference plus two rules that every verb shares. Each task-section fact that no docstring held moved into the docstring of its name. The keep= guidance moved to the debugging how-to. Four drifts are fixed.

What moved where
api.md section (deleted) Its unique facts now live in
The spec argument, Checking a spec check: the spec shapes, "a Spec is not read again", "no verb takes a Program", the expand('piecewise') / expand() choice, and the CI-verb sentence
Checking against a sink check: "solve and write read the same table", and a refusal names the construct and the sinks that take it. The intro of "What each sink takes"
The sources argument build sources, which links the data contract
Building a model Model (already covered)
Reading one row Model.row (a label its dimension cannot hold, no reader for a column). ConstraintRow, now rendered (linopy's format, display_terms summary)
Reading a result dual (an expanded set, duals only where a solver ran, no reduced costs or slacks). dual_ray (InfUnbdInfo / presolve, live only). Result.evaluate (an undeclared expression is not a kind, the archive model check, a new parameter is a build). to_pandas (pandas is not installed)
Writing a file write out
Re-solving with new numbers Model.update (a raise releases the model, the reason behind DataError, solve_over is the loop written for you)
How much of the session a solve keeps The contract was already in Model.solve keep and Result.kept. The when-to-use guidance, the #815 numbers and #382 moved to howto/debug §6
Archiving a model, Loading or scanning howto/archiving (already covered), Model.solve archive (sources through the build door, uncompressed), solve_over archive (a sliced source archived whole, a hand-built axis refused), SolveArchive (spec as written, digest caveats), scan_result (re-read at every collect)
Diagnostics the Diagnostics and Metrics attributes (already covered)
Choosing a solver Model.solve solver_name / solver_options (xpress, a time limit in three vocabularies, Gurobi environment options). solve now says "As Model.solve takes it"

Other duplicates:

Drifts fixed:

  • solver_name omitted xpress.
  • sweep.record columns were listed in three forms (6, 8 and 6). The real record has 9 columns. Sweep.record, sweeps.md and howto/parallel now say "one Record per slice", so the column list has one home.
  • check's Returns said every verb takes the Program back. No verb does.
  • howto/data said index levels attach by name. A MultiIndex is refused.
  • The sink table said HiGHS rewrites SOS to binaries. It refuses and names Spec.expand().
  • Two docstrings linked ../../docs/reference/sweeps.md, which is broken when rendered. They now use the site URL. ConstraintRow linked a private helper; that is now plain text.

Gates on the merge of #1766 (which now carries main through #1771), in a uv environment with the dev and docs groups and the gurobi and xpress extras:

  • ruff check . and ruff format --check .: clean.
  • pyrefly check: 0 errors.
  • zensical build --strict: no issues. Every api.md#… anchor used under docs/ exists in the rendered page.
  • Docs tests (test_docs_math, test_docstring_links): 70 passed.
  • Full suite: 4115 passed, 250 skipped, 1 xfailed. It collects fewer tests than docs: the Python API page renders every public name from its docstring #1766 (4153) because test_doc_examples runs one test per fence, and the deleted sections held fences.

Not done: the task sections were not rewritten as new how-tos. Their examples already exist in howto/archiving, howto/debug and interactive.md. #1765 edits the api.md archive table that this PR deletes, so whichever PR merges second resolves that conflict.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw

… of the name it belongs to

The Python API page kept its task sections beside the rendered reference, so
most rules were stated twice. Their facts that no docstring held move into the
docstrings, the keep= guidance moves to the debugging how-to, and the page
keeps only the two rules every verb shares: names that differ only by case,
and what each sink takes. Inbound links point at the rendered entries.

Four drifts fixed on the way: xpress missing from solver_name, sweep.record's
columns listed three different ways, check's Program described as something a
verb takes back, and a how-to promising that a MultiIndex attaches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
@FBumann
FBumann added this pull request to stack #1770 September 25, 2026 13:13
@codspeed

codspeed Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 82 skipped benchmarks1


Comparing claude/reference-dedup (75e9f3d) with claude/api-reference-from-docstrings (8e79d37)

Open in CodSpeed

Footnotes

  1. 82 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@read-the-docs-community

read-the-docs-community Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

@FBumann
FBumann merged commit c7ad4f2 into main Sep 25, 2026
15 checks passed
@FBumann
FBumann deleted the claude/reference-dedup branch September 25, 2026 16:29
FBumann added a commit that referenced this pull request Sep 25, 2026
… version that wrote it, and one written by 0.1.0 or earlier is refused by name (#1765)

> **Prompt:** we should add some sort of archive version or specsolve
version to the archives to ensure backwards compat if we change sth
later

Later: "use layout: 1".

> [!NOTE]
> The following content was generated by AI.

Every saved answer, archives included, carried `format.json` as
`{"answer": 0}`, whatever build wrote it. It now reads `{"layout": 1,
"specsolve": "<version>"}`: a layout number that covers everything
specsolve writes, and the version that wrote it. **Breaking:** 0.1.0
shipped without this, so an answer 0.1.0 wrote is refused.

<details><summary>What changes, why these choices, tests, gates, not
done</summary>

**What was there.** `relational/parquet.py` stamped every answer
directory with `format.json`. Every reader refused any other number with
a `LayoutError`: `load_result`, `scan_result`, a sweep, a spill resume,
and an archive's `answer/`. The number was held at 0 "while the layout
is still moving", so every answer since the stamp was added, 0.1.0 and
the alphas included, read as current. A layout change from here on could
not have been told apart from any of them.

**What changes**
- **The key is `layout` and the constant is `LAYOUT = 1`.** The number
covers what a result, a sweep and an archive write. A release that
changes any of them raises it, and its notes name the change. The
constant's comment, `Result.save`'s docstring (which `LayoutError`
points to) and `AGENTS.md` (under "Breaking changes are free") say so.
- **`format.json` records `specsolve`**: the version from the installed
metadata, or `null` from a source tree nothing installed. It is written
for the refusal message and for a human reading the file; no reader
branches on it.
- **The refusal names what it found:** `… holds a saved answer in layout
0, written by specsolve 0.0.1a359, and this package reads layout 1. …` A
stamp with no `layout` (what 0.1.0 and every earlier build wrote,
`{"answer": 0}`) and a missing `format.json` both read `… with no layout
stamp …`. Either way the message says to solve again and save.
- **Answers written by 0.1.0 or earlier are refused by name**, instead
of being read as though their layout were current. Before 1.0 that is
the stated rule, and the `!` in the title marks the break for the
release notes. An archive that 0.1.0 wrote still holds the spec and the
data to solve again.

**Why one stamp.** An archive's `answer/` is a saved answer and carries
the same `format.json`, and reading an archive reads it through
`load_result`. So one number covers results, sweeps and archives, and a
second stamp at the archive root would be a second home for the same
fact. The key is `layout`, not `answer`, because the number covers the
archive's other members too.

**Why the version comes from `importlib.metadata`:** `relational/` may
import nothing from the package but `errors`
(`test_engine_is_isolated`). This reads the same installed metadata as
`specsolve.__version__`.

**Tests**
-
`test_a_saved_answer_is_stamped_with_its_layout_and_the_specsolve_that_wrote_it`
(new): a fresh save holds `{"layout": 1, "specsolve": sps.__version__}`.
- `test_an_answer_in_another_layout_is_refused_by_name` covers three
cases: layout 0 from `0.0.1a359` (names the layout and the writer),
0.1.0's `{"answer": 0}`, and no `format.json` (both "with no layout
stamp").
- On the first commit, both tests failed against the previous
`parquet.py` and passed on the new one. That was checked by swapping the
file back and restoring it from git.

**Mutation table**, from `tools/mutate.py` on fa8fc6a; the tree was
clean after the run:

| mutation | result |
|---|---|
| a stamp other than this layout is refused (`parquet.py:73-82`) |
**caught** |

**Gates** on the merge of main after #1769, in a `uv` environment (dev +
docs groups, `gurobi` and `xpress` extras), because pixi cannot be
installed here:
- `ruff check .` and `ruff format --check .` pass.
- `pyrefly check`: 0 errors.
- `zensical build --strict` builds, and the rendered API page carries
the stamp paragraph under `Result.save`.
- `pytest tests/test_docs_math.py tests/test_docstring_links.py`: 70
passed.
- `pytest -n auto`: 4114 passed, 250 skipped, 1 xfailed.

**Not done**
- **No reader for another layout.** Before 1.0 nothing reads another
layout back, and an archive holds the spec and the data to solve again.
- **No mathspec version in the stamp.** `spec.yaml` states the language
version the file is written in.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw

---------

Co-authored-by: Claude <noreply@anthropic.com>
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.

2 participants