Repository navigation
Conversation
…ack out is always the one its program comes from Every spec model is frozen, each section and mapping is a Sealed mapping, and each list is a tuple that is written back out as a list. `override` is the edit. The Expression docstring says that lowering states a node's dims; a node built by hand holds the dims it was given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsnGSGGQso2HHaNdgWeAt6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsnGSGGQso2HHaNdgWeAt6
…dy are Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsnGSGGQso2HHaNdgWeAt6
brynpickering
left a comment
There was a problem hiding this comment.
Seems reasonable to do it, leaving override as the safe way to make changes.
Only change I would suggest is Sealed -> Frozen since frozen is used in this context already in Python, so it's more readily understood.
…s called Frozen `frozen` is the word Python uses for this, in `frozenset`, `dataclass(frozen=True)` and pydantic's `frozen=True`. The module is `_frozen.py`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019j1TmBS3MvzmuarWjhCHHu
Each validator that sorts now returns a tuple or a Frozen mapping, so a sorted field is as frozen as the rest of the spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7L7U7Av5v8wnKV6eDpXfi
Note The following content was generated by AI. Superseded by #924. This branch was merged into #924 at Generated by Claude Code |
Note
The following content was generated by AI.
What this changes
A
Speccannot be changed after it loads. Each field is frozen, each section and mapping is a read-onlyFrozen, and each list is a tuple. To change a spec, lay a patch over it withoverride.How
_StrictSpecisfrozen=True, so assigning a field raisesValidationErrorat every level.Frozen[str, X].Frozengains__get_pydantic_core_schema__: it validates as thedictit freezes, then freezes it, and it is written back out as adict.cases:checks its length before it freezes, so the schema keepsminProperties: 1and the refusal keeps pydantic's own words.Frozenwas calledSealed, in_sealed.py. It is nowFrozen, in_frozen.py, becausefrozenis the word Python uses for this (frozenset,dataclass(frozen=True), pydantic'sfrozen=True). The program's groups and a parsed node's keyword arguments use the same class, so the rename reaches them too.Listed[X]: a tuple once the spec loads, and a list again into_dict()andto_yaml(). Plain data stays plain, somerge,overrideand the canonical form read what they read before.dictnow tests forMapping:side_columns, the relation check in_validation,Spec._names_are_names, andOWNED_SECTIONSin_composition. Three refusals print a field's dims withlist(...), so they quote[]as the file wrote it, not().9a6fd86).schema/mathspec.schema.jsonchanges only in theSpecdescription.Expressiondocstring now says that lowering states each node'sdims, and that a node built by hand holds the dims it was given. The constructors check nothing, as the module docstring already says.docs/reference/reading.mdsays that neither a spec nor its program changes after it loads, and namesoverrideas the edit.Why
spec.programis lowered once, when the spec loads. Onmain,spec.variables['x'].dims.append('nope')andspec.objective.expression = 'sum(c * y, over=g)'both succeed. Afterwardsspec.programstill holds the model as it loaded, butspec.to_yaml()writes the edited text, which does not load. specsolve writesspec.to_yaml()beside each answer it archives, so the archive could hold a model that no one solved.Verification
On
5622b26; the next commit adds only the changelog line:pixi run lintpasses.pixi run test: 3063 passed, 1 skipped.pixi run docs-buildpasses.pixi run compile-texdid not run: this container's proxy blocks the TeX bundle download (relay.fullyjustified.net). CI runs it. The golden typesetter output did not change.On
f6d2e27, the rename:pixi run lintpasses,pixi run testgives 3063 passed and 1 skipped,pixi run docs-buildpasses, and every generator re-run gives no diff. Nosealis left insrc/,tests/ordocs/, apart from the history inCHANGELOG.md.Mutation table. A script deleted each guard and ran the full suite. It restored the file with
git checkout --, dropped__pycache__before and after each run, and the tree came back clean every time. It ran on5622b26, before the rename, which changes no logic.frozen=Truetest_a_loaded_spec_cannot_be_written_to[a-top-level-field],[a-field-of-an-entry],[a-field-two-levels-down]Listedis a tuple[a-name-appended-to-a-list],test_the_loader_yields_plain_typesand 12 tests that compare a frameFrozenfreezes what it validates[an-entry-added],[an-entry-removed]cases:is frozen[a-case-removed][a-given-entry-added]writes into agiven:section that defaults toFrozen({}). That default is never validated, so it is frozen whatever the hook does.Breaking, for the release notes
override(spec, [patch]).spec.variables['p'].dims == ('g',), not['g'].to_dict()andto_yaml()still write lists.Frozen[str, X], aMapping, not adict. Code that testsisinstance(section, dict)must testMapping.Tests that compared a spec field with a list now compare it with a tuple:
test_composition,test_given,test_piecewise,test_terms,test_validation,test_yaml_loadingandtypesetting/test_walk.Not done
Programis unchanged, apart from the rename. It was already frozen dataclasses over these read-only mappings and tuples.dims:lists and relation sides with validators that check forlistanddict. Whichever of feat(spec)!: a loaded spec cannot be changed, so the file it writes back out is always the one its program comes from #923 and feat(spec)!: a loaded spec is sorted by name and cannot be changed, writes back the fields its file wrote, and prints one canonical text for two files that differ only in order or spelling #924 merges second must sort before it freezes.🤖 Generated with Claude Code
https://claude.ai/code/session_019j1TmBS3MvzmuarWjhCHHu