feat(api)!: a saved answer is stamped with layout 1 and the specsolve version that wrote it, and one written by 0.1.0 or earlier is refused by name - #1765
Merged
Conversation
…version that wrote it, and one written before 0.1.0 is refused by name
`format.json` was `{"answer": 0}` for every answer written so far, alpha
builds included, so a later layout change could not be told from any of
them. It now counts from 1, the layout 0.1.0 writes, and carries the
version that wrote it: `{"answer": 1, "specsolve": "0.1.0"}`. A layout
other than 1 is refused as before, and the message names the writer.
AGENTS.md says a change to what a result, a sweep or an archive writes
raises ANSWER_FORMAT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
Merging this PR will not alter performance
Comparing Footnotes
|
… and says so when a file has none
`format.json` holds `{"layout": 1, "specsolve": "<version>"}`: the key
names what the number covers, a result, a sweep or an archive, rather
than the answer directory it happens to sit in. The constant is LAYOUT.
A stamp with no `layout`, which is every answer written before 0.1.0, is
refused as having no layout stamp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
0.1.0 shipped before this, writing {"answer": 0}, so the layout 1 stamp is
what the next release writes and an answer 0.1.0 wrote is refused. The
comment, the reference row, the test messages and the changelog line say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
FBumann
added a commit
that referenced
this pull request
Sep 25, 2026
… of the name it belongs to (#1769) > **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. <details><summary>What moved where</summary> | 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: - sweeps.md: "The axes" table became links to `EachCoordinate` and `EachWindow` plus the hand-built row. The `SliceMetrics` and "asked before it is sliced" rows point at the entries. `EachWindow` now states the window checks rather than naming a private method. The remote-Gurobi sentence that #1771 added now links `Model.solve`. - glossary: "Row types" became one-liners that link the entries. **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 #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. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw --------- Co-authored-by: Claude <noreply@anthropic.com>
The API page's archive table is gone, so the layout stamp is stated in Result.save's docstring, and LayoutError points there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw
Merged
FBumann
added a commit
that referenced
this pull request
Sep 25, 2026
> **Prompt:** Prepare the release. make it 0.1.1 Then: "It should be 0.2.0!! Its more truthfull". > [!NOTE] > The following content was generated by AI. Renames `## Upcoming version` to `## 0.2.0 (2026-09-25)` and edits it into release notes: a summary paragraph, the one break (#1765 refuses answers and archives that 0.1.0 wrote), and the four PRs since 0.1.0. Merging tags `v0.2.0` and publishes to PyPI. <details><summary>Checks</summary> **The version is a minor bump** because #1765 breaks reading archives that 0.1.0 wrote, and AGENTS.md says a release that breaks a model file raises the minor version. **Checked:** - `python -m tools.changelog check`: `releases 0.2.0 on merge`. - `python -m tools.changelog pending`: `0.2.0`. - `main` at 9870a3b carries all four PRs. Its CI state is whatever GitHub shows; I did not re-run the suite for a changelog-only diff. **Not done:** the branch keeps its name `claude/release-0.1.1`, which nothing reads. The merge and the `pypi` environment approval are yours. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw --------- Co-authored-by: Claude <noreply@anthropic.com>
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.
Later: "use layout: 1".
Note
The following content was generated by AI.
Every saved answer, archives included, carried
format.jsonas{"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.What changes, why these choices, tests, gates, not done
What was there.
relational/parquet.pystamped every answer directory withformat.json. Every reader refused any other number with aLayoutError:load_result,scan_result, a sweep, a spill resume, and an archive'sanswer/. 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
layoutand the constant isLAYOUT = 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 (whichLayoutErrorpoints to) andAGENTS.md(under "Breaking changes are free") say so.format.jsonrecordsspecsolve: the version from the installed metadata, ornullfrom a source tree nothing installed. It is written for the refusal message and for a human reading the file; no reader branches on it.… holds a saved answer in layout 0, written by specsolve 0.0.1a359, and this package reads layout 1. …A stamp with nolayout(what 0.1.0 and every earlier build wrote,{"answer": 0}) and a missingformat.jsonboth read… with no layout stamp …. Either way the message says to solve again and save.!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 sameformat.json, and reading an archive reads it throughload_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 islayout, notanswer, because the number covers the archive's other members too.Why the version comes from
importlib.metadata:relational/may import nothing from the package buterrors(test_engine_is_isolated). This reads the same installed metadata asspecsolve.__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_namecovers three cases: layout 0 from0.0.1a359(names the layout and the writer), 0.1.0's{"answer": 0}, and noformat.json(both "with no layout stamp").parquet.pyand passed on the new one. That was checked by swapping the file back and restoring it from git.Mutation table, from
tools/mutate.pyon fa8fc6a; the tree was clean after the run:parquet.py:73-82)Gates on the merge of main after #1769, in a
uvenvironment (dev + docs groups,gurobiandxpressextras), because pixi cannot be installed here:ruff check .andruff format --check .pass.pyrefly check: 0 errors.zensical build --strictbuilds, and the rendered API page carries the stamp paragraph underResult.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
spec.yamlstates the language version the file is written in.🤖 Generated with Claude Code
https://claude.ai/code/session_016Pv7LzSgzt7Yn2K3ioyJXw