Conversation
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…no two regions claiming one coordinate Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`examples/commitment.yaml` is the formulation #2 factors. The state a unit carries into a snapshot has three regimes — a unit that is never off, the first snapshot, and every later one — and writing them at the constraint would fork `ramp_up` three ways. With the regimes named once, the inequality is written once. `tools/render_tex.py` picks it up like every other model, so the LaTeX gate compiles it, and it gets a page in the example gallery. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Documentation build overview
22 files changed ·
|
FBumann
marked this pull request as draft
August 29, 2026 14:17
This was referenced Aug 29, 2026
FBumann
force-pushed
the
claude/spec-merge
branch
from
August 30, 2026 07:13
dd31dee to
8d053c6
Compare
Resolved import-block conflicts in lowering, program and test_lowering. boundedness now walks program nodes, so the Cases arm is ported from CasesNode to program.Cases. `_lower_where` is `resolution.where_of` on main, which folds an always-true mask to None, so the exclusivity tests build their masks the way validation does, unfolded.
…outside the cases (#251) The value wherever no `when` holds moves out of `cases:` and becomes the block's own `otherwise:`, so no name inside `cases:` is reserved and the cases carry no order. Five hand-written errors policing `default` are replaced by the closed schema's own: a required `otherwise:`, a required `when:` on every case, and `min_length=1` on `cases:`. The fallback's dims are checked where the cases' are, which the loop over `cases:` used to give it for free.
… is set as one the solver decides (#252) * fix(typesetting): a cased expression whose otherwise holds a variable is set as one the solver decides Reading only `cases:` for what a block is left the fallback out, so a quantity whose only variable sits there printed upright — the notation for something the model is handed rather than something it solves for. `previous_status` in the commitment example is exactly that shape, and its generated page disagreed with the hand-written one in the language reference. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJMiQWNo9b5VD3Aac5gTXX * fix(language): an error inside a case names the expression that declares it, and is printed once A cased expression is expanded where its name stood, so a fault in one of its arms was reported against the constraint that pulled it in — as `Constraint 'ramp_up', case 'boundary'`, a case on a constraint that has none, once per constraint naming the expression. The arm now names its declaration, and the arm with no `when` is named `otherwise` rather than a case, which is what every other check already calls it. Two errors that differ at all carry different contexts, so an exact repeat is one fault seen twice and is printed once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJMiQWNo9b5VD3Aac5gTXX * fix(language): two integer bands with no integer between them are proved apart The cells for an ordered subject put a representative in every gap between the literals a pair of masks names, and the midpoint it chose was one no integer can be. So `n < 1` against `n > 0` on an `int` was refused with a witness of `n is 0.5` — a coordinate the data cannot produce, naming a rewrite the file had already made. A date was already handled this way; an integer is discrete for the same reason and now says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJMiQWNo9b5VD3Aac5gTXX * test(exclusivity): the soundness fuzz draws two masks apart rather than one and its negation Every pair it generated was a complement by construction, so the grid assertion was `X and not X` — false at every point, under every implementation, for every set of cells. Collapsing each subject to a single cell, which is the most a cell-coverage bug could ever be, left both seeds green while nine other tests in the file went red. Drawn independently and filtered to the pairs the check proves apart, the same mutation now fails it 1295 times over. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJMiQWNo9b5VD3Aac5gTXX * chore: drop an unreachable lookup guard and the default spelling the otherwise rename left behind `_lower_expression` looked its name up in the very mapping its only caller iterates, so the `KeyError` and its `did_you_mean` were a branch nothing reaches — and the one place in the file raising something other than `LanguageError`. Deleting it leaves the suite green, which is what the guard was worth. Beside it, five docstrings and a test name still called the fallback `default`, which the language no longer has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XJMiQWNo9b5VD3Aac5gTXX --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…o whichever engine reads the data Three facts decide which model a file and a table make together — where a dimension's members come from, the order they stand in, and that a coordinate appears at most once. Each was real and each was written down in a consumer, so a second consumer would have inherited none of them. Co-Authored-By: Claude <noreply@anthropic.com>
…oordinate, so a row lost in preparation is not read as a mask A table short of a coordinate and a table that never had one are identical in the data and mean opposite things. `coverage:` says which was meant, and a parameter declaring itself a mask is refused as a bound or a divisor — the two positions where absence has no reading — before any data arrives. Co-Authored-By: Claude <noreply@anthropic.com>
…library is templates rather than generated YAML merge takes the fragments and hands back one mapping to validate, resolve and lower exactly once. A fragment is merged before it is validated, so a template may name what a sibling declares without being a model on its own; the coordinate space is shared where fragments agree, the math is owned, and the objectives are summed. Co-Authored-By: Claude <noreply@anthropic.com>
…g being a path rather than a second convention merge read a string as YAML text where to_spec reads one as a path, so the front door and the composition disagreed about what a caller had handed over. A composed model is also held to round-tripping through to_yaml and printing in all three formats: composing may not reach a model no reviewer can open. Co-Authored-By: Claude <noreply@anthropic.com>
…one document they print The gallery showed one model per page, which a composition has none of. It now shows each fragment as written and then the document the merge prints, so the claim a reader checks is that four small files and one model are the same thing — and that the balance is the row it was before storage was added. Co-Authored-By: Claude <noreply@anthropic.com>
…k writes them The new files were the only YAML in the stack the formatter had not already written, and the check that missed it globbed markdown alone where the hook globs md, yml and yaml alike. Co-Authored-By: Claude <noreply@anthropic.com>
…are not models and say so render_tex renders every file under examples/, and a fragment naming what a sibling declares is a load error alone — which is the thing examples/composed/ exists to show. The model they make is rendered by the gallery and typeset in all three formats by the merge tests, so nothing goes unrendered. Co-Authored-By: Claude <noreply@anthropic.com>
FBumann
force-pushed
the
claude/parameter-coverage
branch
from
August 30, 2026 18:19
92c8b6e to
9a8045c
Compare
FBumann
force-pushed
the
claude/spec-merge
branch
from
August 30, 2026 18:19
8d053c6 to
839e7ef
Compare
FBumann
force-pushed
the
claude/parameter-coverage
branch
from
August 31, 2026 18:29
9a8045c to
981aa95
Compare
…g one fragment is that fragment merge wrapped every objective in parentheses, so a lone objective came back parenthesised: composing one fragment differed from the fragment, and every nesting added another pair. It is returned as written now, and the idempotence and associativity a composition verb has to keep are pinned by tests. Moved here from the what-counts-as-a-verb docs PR, where the fix does not belong — the behaviour is merge's, and so is its test. Co-Authored-By: Claude <noreply@anthropic.com>
FBumann
added a commit
that referenced
this pull request
Aug 31, 2026
…ge's surface The language has a test for what belongs in it and the ceiling has one for what may enter it; the API had neither, which is how a small surface grows a verb per feature. A verb decides nothing the language has not stated and needs nothing but the file, and the three properties it keeps are pure, total at load, and closed under composition. Reframed to stand on main: the merge.py bugfix and its tests that were bundled here now live on the composition PR (#244), where the behaviour and its test belong, and the page states composition-closure as the property a verb earns rather than a past defect in shipped code. Co-Authored-By: Claude <noreply@anthropic.com>
FBumann
force-pushed
the
claude/parameter-coverage
branch
from
September 2, 2026 08:28
003ea7c to
200b831
Compare
FBumann
force-pushed
the
claude/parameter-coverage
branch
from
September 2, 2026 08:44
200b831 to
4ada6e5
Compare
FBumann
added a commit
that referenced
this pull request
Sep 2, 2026
…ge's surface (#246) The language has a test for what belongs in it and the ceiling has one for what may enter it; the API had neither, which is how a small surface grows a verb per feature. A verb decides nothing the language has not stated and needs nothing but the file, and the three properties it keeps are pure, total at load, and closed under composition. Reframed to stand on main: the merge.py bugfix and its tests that were bundled here now live on the composition PR (#244), where the behaviour and its test belong, and the page states composition-closure as the property a verb earns rather than a past defect in shipped code. Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
Note The following content was generated by AI. Closed at the author's request, in a sweep of the open PRs. Closed rather than retargeted, because the base branch never landed. The facts behind that:
The design question is the valuable part here, and it lives on #12 and #30, where it is still open. The implementation is a fragment merger written against a language that has since renamed the block it reads. Not lost: the collision-error wording, the port-convention example, and the argument that a fragment is not a Generated by Claude Code |
This was referenced Sep 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
What this changes
ms.merge({...})takes fragments and hands back one mapping to validate, resolve and lower exactly once.One decision makes it cheap: a fragment is not a
Spec. It is merged before it is validated, so a template may name what a sibling declares — a sharedbus, the flow every component writes into — without being loadable alone. No free-name concept, no import list, no new resolution rule: validation still runs once, over one flat namespace, exactly as for a file somebody typed.The split that gives merging its meaning:
dimensionsandlookupsare the coordinate space, shared on purpose. Declared twice and agreeing, one declaration; disagreeing, the disagreement is the error.⚠ This takes a side in an open design question
Read this before the rest. The third bullet above is not settled, and an earlier version of this description presented it as though it were.
#12 is open and contested, and #30 is named in it. @brynpickering there:
That is a different composition model from this one, and the two are not compatible:
Associativity is a property of the model chosen here, not a neutral win. A layered-override merge needs order to matter, so the test pinning associativity would be wrong under that design rather than merely unnecessary.
So: this is a complete implementation of disjoint composition, which is what
ceiling.md's component-library section describes (topology is data, templates bounded by type). #250 later builds the other half as a second verb,override, on the argument that the two obey opposite laws and neither is a mode of the other.The collision error names the rewrite it usually is
A collision refuses, and the question is what the author should do about it. Two of the same kind of thing — a battery and a pumped hydro — are the same math with different numbers, so they are two rows of a dimension, not two fragments, and the message says that first:
The earlier message only offered the rename, which is the right advice for genuinely different math and the wrong advice for the case a component library hits most. The #29 promise moved out of the message and into the module docstring, where a maintainer reads it and a file's author does not need it.
The same pass fixed a message defect in both
mergeandoverride: the singular noun wassection[:-1], so asoscollision was reported as asoand apiecewiseone as apiecewis. There is a parametrized case asserting the English now.The worked example
examples/composed/is four files and a gallery page.base.yamldeclares the coupling surface — oneflowper port, one balance per bus — and the other three nameflowwithout declaring it, so each is a load error alone.The balance does not grow when a component type is added. Adding
storage.yamlas a fourth fragment:That is what the port convention buys, and it needs no language this repository does not already have — each component pins its own port's flow with
at(flow, by=…)rather than owning a flow variable the composer must then sum by hand.Why
#30, under the reading above.
ceiling.mdalready argues the shape — topology is data, compose-then-build producing oneSpecbefore a single lower pass, structure bounded by component types while cardinality lives in data. What was missing was the function.Names are not rewritten, the deliberate boundary. Qualified names are #29 and need an unparser this repo does not have. Until they land, a library keeps templates apart by naming them apart — which is what
examples/pypsa.yamlalready does withGenerator_p_nomandLink_ramp_limit_up.What lpspec removes or changes
Nothing. Not one line. Every verb already takes
str | Path | dict | Specandmergereturns a mapping, so this works today against an unchanged engine:Because names are not rewritten the composed declarations keep the fragments' spellings, so the sources mapping is just the union and no manifest, binder or diagnostic moves. That is the layering's strongest evidence: the piece enabling component libraries costs the engine nothing, being a
Spec → Specfunction on the far side of the waist.Verified
Toolchain reconstructed at the versions
pixi.tomlpins:ruff format --check/ruff check(0.16.1, pinned) clean;pyrefly(1.2.0, pinned) 0 errors;pytest -q854 passed, 0 skipped.reuse,typos,prettier --checkover**/*.{md,yml,yaml},render_tex(27 models) andmkdocs build --strictwere run on the top of the stack (#250), which contains these commits.Not run:
zizmor,taplo(neither's files changed), and thetectonichalf ofcompile-tex.Two CI failures this PR earned, and what they say
Both were in gates I had claimed I could not run. Recording them because the claim was the problem, not the code.
Prettier reformatted the new YAML.
{dtype: int}→{ dtype: int }. My local check globbed**/*.md;.lefthook.yamlglobs*.{md,yml,yaml}, and these were the first YAML files the stack added.render_textypesets every file underexamples/— and a composition fragment is a load error alone, which is the entire point of the directory.tools/render_tex.pyalready hadNOT_MODELSfor exactly this category (the symbol tables), so the fragments join it. It still renders 27 models, and the composition itself is covered twice over: the gallery page renders it, andtest_a_composed_model_prints_as_mathtypesets it in all three formats.That second one is the useful lesson.
compile-texwas on my "not run" list, and it caught a real defect the other seven gates could not.Mutation table
Every guard deleted in turn on a clean tree, restored with
git checkout --:The last is the quiet one:
a + bwherebissum(x) * kreassociates, so an unparenthesised join composes a different objective and nothing else in the suite would notice.A composed model is still one a reviewer can open
Hard rule 5 wants the model to be the file you review and diff, and "a construct that cannot be printed is not in the language." Both are asserted:
to_yamlon a merged model writes a file that loads back to the sameSpec, and all three formats render it.Writing that round-trip test found the bug in the third commit:
mergeread astrfragment as YAML text whereto_specreads one as a path.mergenow takes exactly what every other verb takes.Deliberately not done — and one correction
No qualified names (#29); needs an unparser, and merging is useful without it.
No provenance returned. With no renaming every declaration keeps its fragment's name, so the origin is recoverable by looking, and collisions name both fragments when refused. It becomes essential the day #29 renames things.
No
active:. #12 is open; nothing here implements or precludes it, though the collision rule above is the part that would have to give — and #250 is the verb that gives it, without touching this one.Correction. An earlier version of this description claimed
exclusivity.overlapping()from #168, pointed atwheremasks on a shared variable, would catch "two components claiming the same port". Wrong, and I withdraw it: two constraints bearing on one variable is ordinary linear programming, and because each declaration is owned by exactly one fragment a cross-fragment mask overlap is not expressible.The residual gap is real, and building the example above gave it a name:
port_busis a lookup, a partial lookup is legal, andsum(by=)places an unmapped port's terms nowhere — so a mis-wired system solves and is quietly wrong. That is #245, stacked on this.Stack: third of nine, on #243 → #242 → #168.