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
25 files changed ·
|
0e49394 to
ac04893
Compare
3faa0a0 to
c2b0544
Compare
ac04893 to
9f69c57
Compare
c2b0544 to
54dd7db
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>
… so a port nobody wired is not a term that vanishes A map short of a label lands its terms in no group at all, which is a shape a model wants and a wiring mistake in equal measure. `coverage:` says which, and a composed model declaring its coupling map total turns the second from a plausible answer into an error. The vocabulary is now one Coverage, shared with the parameter key it repeats. Co-Authored-By: Claude <noreply@anthropic.com>
…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. merge lost the third and nobody noticed: a lone objective came back parenthesised, so composing one fragment differed from the fragment and every nesting added a pair. It is returned as written now, and the idempotence and associativity the page argues from are pinned by tests. Co-Authored-By: Claude <noreply@anthropic.com>
9f69c57 to
cf0fe7c
Compare
54dd7db to
3036298
Compare
…at a time, before a driver cuts one A rolling horizon or a myopic pathway asks one thing of a model before it starts, and nothing answered it: storage carried over a snapshot survives being cut into overlapping windows and an annual budget does not, while both window into pieces that solve. separable reports the overlap two windows need and, where there is no overlap that would do, names each declaration and the construct tying the axis together. The care is that a reduction means opposite things by position: in a constraint a sum over the axis ties every window to every other, and in the objective it is additively separable. A verdict treating the two alike would refuse every windowable model there is. Co-Authored-By: Claude <noreply@anthropic.com>
…de the footprint it already answers It was a module and a verb, on the reasoning that a pass belongs outside the module that defines the AST and that a dimension argument ruled out an accessor. Both are wrong: footprint is a walk that lives there already, and dimension(name) is a parameterised accessor. Nor was there a cycle to dodge — where_parser reaches errors and expression_parser and neither reaches program. So it reads off the program like every other question about one, and the package exports one name fewer. Co-Authored-By: Claude <noreply@anthropic.com>
… the measurement Co-Authored-By: Claude <noreply@anthropic.com>
…ing for one already walked the whole program It was a method taking a dimension, which meant a walk per axis and a caller that had to know which axis to ask about. Every construct that ties an axis names the axis it ties, so one walk answers for all of them: measured at 831us against 844us for the single axis it replaced, on the largest model shipped (#248). So it is a cached mapping like the footprint beside it, complete over the declared dimensions — an axis nothing mentions is trivially windowable rather than absent. A reduction collapsing several axes now couples each of them, which the per-axis form could not get wrong and this one could. Co-Authored-By: Claude <noreply@anthropic.com>
…bproblem is a call rather than a second file A myopic pathway, a rolling horizon and a Benders subproblem share one move, and written by hand it is a second file to keep in step with the first. fix makes it a function: the name does not move, so every expression naming it goes on reading. It reads like a rename and is not. A variable masked by where: has rows that do not exist, so as a parameter it is coverage: masked, where the obvious rewrite claims a number everywhere and binds cleanly against data that has none; and a binary decision becomes an int rather than a float, a flag being a mask and not something a constraint multiplies by. Co-Authored-By: Claude <noreply@anthropic.com>
…red math travels as files rather than as a script merge composes peers and refuses a name two of them declare, which is the wrong law for a framework's math that a project extends. override lays patches over a base in order: a declaration is replaced field by field, so a patch says only what it changes, and one set to null is removed. The removal marker is what an ordered list of files cannot supply for itself — a declaration a patch does not mention is left alone, so a deletion has no other spelling. It is positional and reaches no deeper than a declaration, where a null is the value the schema already takes. A patch is not a model, so nothing in the schema gains a key. Co-Authored-By: Claude <noreply@anthropic.com>
The verb had a design argument and no example of the workflow it argues about. examples/composed/ gains a capacity decision and a four-line patch that takes it away, which is the base-to-operate move the issue is about: every constraint expression and the objective are identical either side of it, and the cost of capacity becomes the sunk constant it is. file.md showed both verbs against paths that did not exist, so nothing could have run them. They compose that library now, and the page's claims are executed. Reading a claim off the line an expression *ends* on is what lets one wrap: prettier formats python inside a fence too, and the page test read the line it started on. Co-Authored-By: Claude <noreply@anthropic.com>
cf0fe7c to
163000f
Compare
3036298 to
ed85bb7
Compare
163000f to
3348d70
Compare
|
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 whole stack under it is closed.
Bringing this up to date means resolving three dead stack levels and then a rename across every example it adds. Rebuilding #12 stays open, and the three decisions in this description are the reason to keep it open: two verbs rather than a mode on Generated by Claude Code |
Note
The following content was generated by AI.
What this changes
A framework ships math; a project extends it. #12 is where this is argued, and #244 took the other side of it — this is the side it did not take.
A patch says only what it changes, because declarations are laid over field by field:
A declaration set to
nullis removed:examples/composed/operate.yamlis that patch in four lines, laid over the four-fragment library from #244: capacity stops being a decision and every constraint expression and the objective are identical either side of it. It is executed on thefile.mdpage rather than described there.The three decisions, and why
Two verbs, not a mode on
merge. Composition and override obey opposite laws — a name two peers declare is a collision, a name a patch declares is the point — and erroring on a shared name cannot also mean winning with it. Amode=flag would make one call mean two things. The collision error is a real safety property for component libraries (two templates both declaringcostby accident), and a layered model must not have it.mergeoverrideA marker in the file is genuinely needed — but only for removal. This is the part I think is under-appreciated in #12: an ordered list of files gets you replacement but not deletion. A declaration a patch does not mention is left alone, so "absent" means unchanged and a deletion has no other spelling. That is the irreducible core of the issue, and @brynpickering is right that it has to travel with the file rather than sit in a script.
nullrather thanactive:, and on @FBumann's own objection. His complaint is that twenty deactivated components make a file harder to read than not having them.nullis one line carrying no lingering declaration to skip past, where{active: false}is one line and givesactivea meaning in ordinary model files — where it buys nothing and costs exactly the readability he named. @brynpickering offerednullhimself in the thread; this is that suggestion, with the ambiguity he worried about resolved below.The ambiguity
nullhad, and how it is resolvedHe noted that a parameter-to-variable swap gets murky:
The problem underneath is real and sharper than stated:
where: nullis already a legal value — a variable with no mask — so ifnullmeant "remove" everywhere, one spelling would mean two things.The marker is positional.
constraints: {ramp: null}removes the constraint;variables: {p: {where: null}}sets that variable's mask to none. Removal is a declaration-level marker and reaches no deeper. That is rule 4's instinct — position decides which kinds of name are legal — applied one level out, and it makes the swap above unambiguous rather than merely conventional.Why nothing in the schema changes
A patch is not a model, for the same reason #244 established a fragment is not: it is read before validation. So
nullnever appears in a file anyone loads as a model, no block gains a key, and a validatedSpeccannot contain one.That is also the answer to #13, which was closed
design: declinedon "I'm not sure if we should widen the language this way". Its motivating case — capacity a variable in expansion, a parameter in dispatch — is now sayable at file level, and the language is not widened:variables:andparameters:stay separate blocks and the schema is untouched. Three routes now reach that outcome and none is redundant: #249'sfixis programmatic and mid-workflow, a patch is file-level and shareable, and the declinedcomponent_type:was neither.The risk, and the answer to it
overrideis dangerous in a waymergeis not: it is designed to collide, so a patch that quietly replaces a constraint the base relied on yields a valid model meaning something else, with no error anywhere.The mitigation needs nothing new. #244 proved a composed model round-trips through
to_yaml, so the artifact to review is the output and a patch's real effect is a diff of base against result. That is asserted here too. I would document that as the workflow rather than build provenance reporting, until someone hits a case where the diff is not enough.The one thing it does refuse: removing a declaration the base does not have, named with the near miss. A removal is a claim about what is there, so a stale one is a patch that no longer describes what it lands on — a base that moved on, or a section confused for another.
What lpspec removes or changes
Nothing.
overridereturns a mapping and every verb already takes one, solps.solve(ms.override(base, patches), sources)works against an unchanged engine — same asmerge.Verified
Toolchain at the versions
pixi.tomlpins:ruff format --check/ruff check(0.16.1) clean,pyrefly(1.2.0) 0 errors,reusecompliant,typosclean,prettier --checkover**/*.{md,yml,yaml}clean,pytest -q926 passed, 0 skipped (915 on the base — 11 new),mkdocs build --strictbuilds,python -m tools.schemaandpython -m tests.typesetting.goldenregenerate to no diff.Not run:
zizmor,taplo, and thetectonichalf ofcompile-tex.mkdocs build --strictneededdocs.python.org/objects.invdropped frommkdocs.yml, which this environment's proxy refuses; that affects cross-links into CPython's docs and nothing in this diff.Mutation table
nullremoves at any depth, not just a declarationThe first needed a second attempt:
over is not Noneinside aisinstance(over, dict)branch is always true, so my first mutation changed nothing and passed the whole suite — a catch that did not happen. Re-expressed as popping the key on a nestedNone.Deliberately not done
No
active:. #12 stays open; this implements the removal it needs by the spelling that costs the model schema nothing. If deactivate-in-place is wanted for its own sake — keeping a switched-off declaration so it can be switched back — that is a separate argument this does not make.No provenance for which patch won which key. The composed model is
to_yaml-able and the diff is the answer; a report would be a second way to learn the same thing.Order is not made explicit in the files. A patch carries its removals, but which file is the base and what order the rest apply in is still the caller's — and I think rightly, since a patch that named its own position could not be reused against a different base.
Changed in review.
_lay_overreturnedNoneand wrote into its argument, which madeoverride's "the base is never mutated" guarantee a property of the caller rather than of the function; it now takes a base and returns the laid result. And the stale-removal message called asoscollision asoand apiecewiseone apiecewis, which #244 now fixes for both verbs.Stack: ninth, on #249 → #248 → #247 → #246 → #245 → #244 → #243 → #242 → #168.
Generated by Claude Code