Skip to content

One language, two lanes: closed helper set, no monkey-patch, compat as a pure-producer shim - #43

Merged
FBumann merged 2 commits into
mainfrom
feat/api-v01
Jul 24, 2026
Merged

One language, two lanes: closed helper set, no monkey-patch, compat as a pure-producer shim#43
FBumann merged 2 commits into
mainfrom
feat/api-v01

Conversation

@FBumann

@FBumann FBumann commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Three commits, rebased onto main after #36 and #37 landed. Two lanes that
accept different languages cannot be an oracle for each other; this closes
that gap and stops patching linopy.

No helper registry

@register is gone. The built-in set is closed, so the streaming lane and the
compat lane accept exactly the same language — which is what makes the
differential tests an oracle rather than a comparison of two dialects.
Compositions belong in macros:; math the language cannot say belongs in a
declared escape: island (#38), which is visible in the file instead of
reading like a built-in on the page.

No monkey-patch

_patch.py and YamlAccessor are deleted. compat exposes build() and
extend(), both pure producers: YAML in, model out, nothing retained. The
result is a plain linopy.Model that stands for itself. This drops the
dependency on Model.__slots__ carrying __weakref__, so "pure consumer of
linopy's public API" is true rather than aspirational.

No accumulated state

The accessor's merged parameter dataset let a YAML file reference parameters
declared by an earlier file — meaning through Python-side state, which hard
rule 5 forbids. Every file now declares what it uses, and the caller supplies
that data per call. State derived from the model argument (its variables, its
coords) stays.

Also: [oracle] extra → [compat]; extend() expands piecewise: blocks
like build() does; lowering rejections name the construct and its rewrite
instead of pointing at "the eager backend", which hard rule 3 abolished; and
the linopy-free architecture check now sees through module-level try:
guards, which could previously hide a forbidden import.

Dissolves #12 — accessor state cannot be lost across to_netcdf/pickle/
deepcopy when nothing is attached in the first place.

Docs record the decisions, not just the API delta

  • ARCHITECTURE: hard rule 3 restated as one language, two lanes — the
    property the code now enforces. New expressive ceiling section defining
    the admissible closure as degree 1 ∩ relational ∩ local, with the
    sink-/budget-/design-bounded split that says which exclusions can ever move.
  • ROADMAP (new): how capability is measured — the closure as spec,
    Calliope and PyPSA's constraint modules as corpora (Streaming-subset completeness audit: every lowering rejection is now a product gap #27), scale as the
    second axis — plus the primitive backlog, the degree axis (linear stays the
    goal, recorded with its revisit conditions), and the deliberate
    non-primitives with their rewrites.
  • GLOSSARY (new): one entry per construct, with the name it carries at
    every layer. Section 0 separates five things that "primitive" was covering:
    only constructs with an IR node and a lowering case carry the two-backend
    tax, while piecewise: is taxed but expands to declarations before either
    lane sees it. The tier-1 diagram is corrected to match.
  • SPEC: §2 and §9 describe the native path first and the shim second;
    §7.5 replaces the custom-helper section; non-goals name
    parity-with-other-languages and xarray-operation-parity explicitly; §12's
    fallback framing is marked superseded; §12.4's IR listing had drifted from
    relational/ir.py (Div, Shift, Defined, Bool were missing).

Three known gaps, recorded not fixed

Added to ROADMAP Track 1 rather than patched here, since each needs a decision:

  • 5bwhere comparisons on dimension coordinates (where: "snapshot > 0")
    are accepted eagerly and rejected by lowering. The README's own ramp example
    is affected.
  • 5c — dimension-name kwargs are never name-checked. sum(p, over=snapshto)
    builds a model that solves and is wrong: the typo makes the sum a no-op,
    and lowering deliberately mirrors that no-op for eager parity. Silent on both
    lanes, so the most severe of the three.
  • 5d** is evaluated eagerly and rejected by lowering; membership is
    also dubious, since a variable base breaks degree 1 and a parameters-only
    power is data prep.

Verification

146 tests pass; ruff, pre-commit and CI (native, full 3.11, full 3.13)
green; mypy unchanged at 8 pre-existing errors.

Deferred packaging work is filed as #42. Note the correction there: pyarrow
is a runtime dependency — the solver_direct sink reaches it through duckdb's
.to_arrow_reader(), so it never appears as an import pyarrow.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@FBumann, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f3a05e8-f7fb-40ec-8fcc-3800950f34cc

📥 Commits

Reviewing files that changed from the base of the PR and between cb8bae5 and 5d33581.

📒 Files selected for processing (34)
  • .github/workflows/ci.yml
  • ARCHITECTURE.md
  • CLAUDE.md
  • GLOSSARY.md
  • README.md
  • ROADMAP.md
  • SPEC.md
  • linopy_yaml/__init__.py
  • linopy_yaml/_patch.py
  • linopy_yaml/accessor.py
  • linopy_yaml/compat.py
  • linopy_yaml/expansion.py
  • linopy_yaml/helpers.py
  • linopy_yaml/loader.py
  • linopy_yaml/lowering.py
  • linopy_yaml/piecewise.py
  • linopy_yaml/validation.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_accessor.py
  • tests/test_api.py
  • tests/test_architecture.py
  • tests/test_compat.py
  • tests/test_dispatch.py
  • tests/test_error_notes.py
  • tests/test_expansion.py
  • tests/test_group_sum.py
  • tests/test_language_boundary.py
  • tests/test_lowering.py
  • tests/test_milp.py
  • tests/test_piecewise_block.py
  • tests/test_piecewise_convex.py
  • tests/test_roll.py
  • tests/test_validation.py
📝 Walkthrough

Walkthrough

The project replaces monkey-patched YAML integration with explicit native and compat APIs, removes custom helper registration, formalizes bounded language and escape semantics, adds parquet solution streaming and cleanup, and updates documentation, dependencies, and tests.

Changes

Language and API transition

Layer / File(s) Summary
Language contract and project documentation
ARCHITECTURE.md, CLAUDE.md, GLOSSARY.md, README.md, ROADMAP.md, SPEC.md
Documentation defines one bounded language for both lanes, explicit escape: islands, closed helpers, native APIs, and opt-in compat operations.
Closed helpers and language enforcement
linopy_yaml/expansion.py, linopy_yaml/helpers.py, linopy_yaml/lowering.py, linopy_yaml/validation.py, pyproject.toml
Custom helper registration is removed; unsupported helpers and dimension comparisons now produce load/language-boundary guidance.
Native API and compat producers
linopy_yaml/__init__.py, linopy_yaml/api.py, linopy_yaml/compat.py
The package adds check, write, and LanguageError; compat.build and compat.extend provide explicit model construction and extension with coordinate validation.
Solution parquet output and cleanup
linopy_yaml/relational/executor.py
Solutions can stream variables to parquet, support context-manager usage, and clean DuckDB resources through explicit and finalizer-backed cleanup.
Compat migration and regression coverage
tests/*, .github/workflows/ci.yml
Tests migrate from patched model accessors to compat APIs and cover public APIs, coordinate behavior, language errors, architecture rules, lifecycle cleanup, and solver parity.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant linopy_yaml
  participant StreamingEngine
  participant DuckdbExecutor
  participant Solver
  Caller->>linopy_yaml: solve(model, sources)
  linopy_yaml->>StreamingEngine: load, validate, and lower YAML
  StreamingEngine->>DuckdbExecutor: execute relational model
  DuckdbExecutor->>Solver: solve generated model
  Solver-->>DuckdbExecutor: solution tables
  Caller->>DuckdbExecutor: to_parquet(directory)
  DuckdbExecutor-->>Caller: parquet paths
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: new native entry points, a closed helper set, and compat as a pure-producer shim.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-v01

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…cer shim

Two lanes that accept different languages cannot be an oracle for each other.
Two changes close that gap, and a third follows from it.

- Remove `@register` and the helper registry. The built-in set is closed, so
  the streaming lane and the compat lane accept exactly the same language.
  Compositions belong in `macros:`; math the language cannot say belongs in a
  declared `escape:` island (#38), which is visible in the file rather than
  reading like a built-in on the page.

- Remove the `linopy.Model` monkey-patch (`_patch.py`) and `YamlAccessor`.
  `compat` now exposes `build()` and `extend()`, both pure producers: YAML in,
  model out, nothing retained. The returned object is a plain `linopy.Model`
  that stands for itself. This also drops the dependency on `Model.__slots__`
  carrying `__weakref__`, so "pure consumer of linopy's public API" is now
  true rather than aspirational.

- The accessor's accumulated parameter dataset let a YAML file reference
  parameters declared by an earlier file — meaning through Python-side state,
  which hard rule 5 forbids. Gone: every file declares what it uses, and the
  caller supplies that data per call. State derived from the model argument
  (its variables, its coords) stays. `validate_expressions(known_parameters=)`
  goes with it.

Also: `[oracle]` extra -> `[compat]`; `extend()` now expands `piecewise:`
blocks like `build()` does; lowering rejections name the construct and its
rewrite instead of pointing at "the eager backend", which hard rule 3
abolished; the linopy-free architecture check now sees through module-level
`try:` guards, which could previously hide a forbidden import.

Docs record the decisions this came out of, not just the API delta:

- ARCHITECTURE: hard rule 3 restated as "one language, two lanes" (the
  property the code now enforces); new "expressive ceiling" section defining
  the admissible closure as degree 1 ∩ relational ∩ local, with the
  sink-/budget-/design-bounded split that says which exclusions can ever move.
- ROADMAP (new): how capability is measured — the closure as spec, Calliope
  and PyPSA constraint modules as corpora (#27), scale as the second axis —
  plus the primitive backlog, the degree axis, and the deliberate
  non-primitives with their rewrites.
- SPEC: §2 and §9 describe the native path first and the shim second; §7.5
  replaces the custom-helper section; non-goals now name parity-with-other-
  languages and xarray-operation-parity explicitly; §12's fallback framing
  marked superseded; Q3 restated as a capability gap.

Dissolves #12 — accessor state cannot be lost across to_netcdf/pickle/
deepcopy when nothing is attached in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sdist already carries ARCHITECTURE.md and SPEC.md; ROADMAP.md joins them.
CLAUDE.md's install comment still named the pre-rename `oracle` extra.

pyarrow stays a runtime dependency: the solver_direct sink reaches it through
duckdb's `.to_arrow_reader()`, so it never appears as an `import pyarrow` —
now noted next to the dependency so the next reader does not mistake it for
dead weight.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FBumann
FBumann merged commit 653d8c1 into main Jul 24, 2026
3 of 4 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 35-51: Update CLAUDE.md’s module map to remove the duplicate
compat.py entry and describe the pure-producer compat lane. Update README.md’s
overview and Mermaid diagram to document that unsupported constructs fail during
loading rather than route to eager fallback.
- Line 37: Update the module-tree fenced code block in CLAUDE.md to include an
appropriate language tag, preferably text, immediately after the opening fence
while preserving its contents.
- Around line 55-61: Update every public solve example to use the
context-managed Solution: in CLAUDE.md lines 55-61, README.md lines 37-41, and
README.md lines 100-113, wrap each ly.solve(...) assignment in a with block and
keep the existing solution access inside it.

In `@GLOSSARY.md`:
- Around line 96-100: Update ROADMAP.md to include the exponentiation (`**`)
streaming parity gap described in GLOSSARY.md, aligning it with the existing
roadmap item for related language-parity gaps; preserve the glossary’s
description and classification.
- Around line 267-271: Update SPEC §12.4 in GLOSSARY.md to include the
implemented IR nodes Div, Shift, Defined, and Bool in its IR listing. Also align
the where-AST and IR terminology for equivalent concepts by mapping
ExistenceCheck to Defined, Comparison to Cmp, and BoolLiteral to Bool, keeping
the documentation consistent with relational/ir.py.
- Around line 323-329: Update load-time validation in validation.py to validate
dimension-valued kwargs, including sum’s over=, into=, and roll/shift dimension
keys, against the declared dimensions before either execution lane dispatches.
Do not skip these kwarg NameNodes; emit clear, actionable errors for unknown
dimension names, while preserving existing validation for data references and
valid dimensions.

In `@linopy_yaml/compat.py`:
- Around line 125-149: Update the coordinate validation flow around
_infer_coords and build_master_coords so YAML dimension values are compared
against the model’s inferred coordinates before applying explicit coords
overrides. Preserve the inferred mapping for consistency checks, then merge
coords into the mapping used for master-coordinate resolution, retaining the
existing mismatch error behavior and actionable message.

In `@linopy_yaml/relational/executor.py`:
- Around line 777-789: Update _solution_to_parquet so the destination path used
in the DuckDB COPY statement is safely escaped as a SQL string literal before
interpolation, preserving valid paths containing apostrophes and preventing
injection. Apply the escaping specifically to out while keeping the existing
parquet output and written mapping behavior unchanged.

In `@README.md`:
- Around line 47-49: Add the linopy_yaml compat import to the README example and
explicitly identify m as an existing linopy.Model before the compat.extend call,
keeping the example otherwise unchanged.

In `@tests/test_api.py`:
- Around line 169-182: Update test_solution_context_manager_and_to_parquet to
import pyarrow.parquet through pytest.importorskip, so the test is skipped when
pyarrow is unavailable while preserving the existing parquet assertions when it
is installed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7df1535-37ec-4c96-b3d6-44e6b66bc49f

📥 Commits

Reviewing files that changed from the base of the PR and between 391b660 and cb8bae5.

📒 Files selected for processing (37)
  • .github/workflows/ci.yml
  • ARCHITECTURE.md
  • CLAUDE.md
  • GLOSSARY.md
  • README.md
  • ROADMAP.md
  • SPEC.md
  • linopy_yaml/__init__.py
  • linopy_yaml/_patch.py
  • linopy_yaml/accessor.py
  • linopy_yaml/api.py
  • linopy_yaml/compat.py
  • linopy_yaml/expansion.py
  • linopy_yaml/helpers.py
  • linopy_yaml/loader.py
  • linopy_yaml/lowering.py
  • linopy_yaml/piecewise.py
  • linopy_yaml/relational/__init__.py
  • linopy_yaml/relational/executor.py
  • linopy_yaml/validation.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_accessor.py
  • tests/test_api.py
  • tests/test_architecture.py
  • tests/test_compat.py
  • tests/test_dispatch.py
  • tests/test_error_notes.py
  • tests/test_expansion.py
  • tests/test_group_sum.py
  • tests/test_language_boundary.py
  • tests/test_lowering.py
  • tests/test_milp.py
  • tests/test_piecewise_block.py
  • tests/test_piecewise_convex.py
  • tests/test_roll.py
  • tests/test_validation.py
💤 Files with no reviewable changes (3)
  • tests/test_accessor.py
  • linopy_yaml/accessor.py
  • linopy_yaml/_patch.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 35-51: Update CLAUDE.md’s module map to remove the duplicate
compat.py entry and describe the pure-producer compat lane. Update README.md’s
overview and Mermaid diagram to document that unsupported constructs fail during
loading rather than route to eager fallback.
- Line 37: Update the module-tree fenced code block in CLAUDE.md to include an
appropriate language tag, preferably text, immediately after the opening fence
while preserving its contents.
- Around line 55-61: Update every public solve example to use the
context-managed Solution: in CLAUDE.md lines 55-61, README.md lines 37-41, and
README.md lines 100-113, wrap each ly.solve(...) assignment in a with block and
keep the existing solution access inside it.

In `@GLOSSARY.md`:
- Around line 96-100: Update ROADMAP.md to include the exponentiation (`**`)
streaming parity gap described in GLOSSARY.md, aligning it with the existing
roadmap item for related language-parity gaps; preserve the glossary’s
description and classification.
- Around line 267-271: Update SPEC §12.4 in GLOSSARY.md to include the
implemented IR nodes Div, Shift, Defined, and Bool in its IR listing. Also align
the where-AST and IR terminology for equivalent concepts by mapping
ExistenceCheck to Defined, Comparison to Cmp, and BoolLiteral to Bool, keeping
the documentation consistent with relational/ir.py.
- Around line 323-329: Update load-time validation in validation.py to validate
dimension-valued kwargs, including sum’s over=, into=, and roll/shift dimension
keys, against the declared dimensions before either execution lane dispatches.
Do not skip these kwarg NameNodes; emit clear, actionable errors for unknown
dimension names, while preserving existing validation for data references and
valid dimensions.

In `@linopy_yaml/compat.py`:
- Around line 125-149: Update the coordinate validation flow around
_infer_coords and build_master_coords so YAML dimension values are compared
against the model’s inferred coordinates before applying explicit coords
overrides. Preserve the inferred mapping for consistency checks, then merge
coords into the mapping used for master-coordinate resolution, retaining the
existing mismatch error behavior and actionable message.

In `@linopy_yaml/relational/executor.py`:
- Around line 777-789: Update _solution_to_parquet so the destination path used
in the DuckDB COPY statement is safely escaped as a SQL string literal before
interpolation, preserving valid paths containing apostrophes and preventing
injection. Apply the escaping specifically to out while keeping the existing
parquet output and written mapping behavior unchanged.

In `@README.md`:
- Around line 47-49: Add the linopy_yaml compat import to the README example and
explicitly identify m as an existing linopy.Model before the compat.extend call,
keeping the example otherwise unchanged.

In `@tests/test_api.py`:
- Around line 169-182: Update test_solution_context_manager_and_to_parquet to
import pyarrow.parquet through pytest.importorskip, so the test is skipped when
pyarrow is unavailable while preserving the existing parquet assertions when it
is installed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7df1535-37ec-4c96-b3d6-44e6b66bc49f

📥 Commits

Reviewing files that changed from the base of the PR and between 391b660 and cb8bae5.

📒 Files selected for processing (37)
  • .github/workflows/ci.yml
  • ARCHITECTURE.md
  • CLAUDE.md
  • GLOSSARY.md
  • README.md
  • ROADMAP.md
  • SPEC.md
  • linopy_yaml/__init__.py
  • linopy_yaml/_patch.py
  • linopy_yaml/accessor.py
  • linopy_yaml/api.py
  • linopy_yaml/compat.py
  • linopy_yaml/expansion.py
  • linopy_yaml/helpers.py
  • linopy_yaml/loader.py
  • linopy_yaml/lowering.py
  • linopy_yaml/piecewise.py
  • linopy_yaml/relational/__init__.py
  • linopy_yaml/relational/executor.py
  • linopy_yaml/validation.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_accessor.py
  • tests/test_api.py
  • tests/test_architecture.py
  • tests/test_compat.py
  • tests/test_dispatch.py
  • tests/test_error_notes.py
  • tests/test_expansion.py
  • tests/test_group_sum.py
  • tests/test_language_boundary.py
  • tests/test_lowering.py
  • tests/test_milp.py
  • tests/test_piecewise_block.py
  • tests/test_piecewise_convex.py
  • tests/test_roll.py
  • tests/test_validation.py
💤 Files with no reviewable changes (3)
  • tests/test_accessor.py
  • linopy_yaml/accessor.py
  • linopy_yaml/_patch.py
🛑 Comments failed to post (10)
CLAUDE.md (3)

35-51: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the documentation with the new explicit compat/strict-language architecture.

The changed sections still leave the old behavior documented: CLAUDE.md describes monkey-patching/oracle behavior, while README.md retains an eager-fallback overview.

  • CLAUDE.md#L35-L51: remove the duplicate compat.py entry and describe the pure-producer compat lane.
  • README.md#L69-L69: update the earlier overview and Mermaid diagram so unsupported constructs fail at load time rather than route to eager fallback.
🧰 Tools
🪛 LanguageTool

[style] ~35-~35: ‘In brief’ might be wordy. Consider a shorter alternative.
Context: ...E.md` for the authoritative module map. In brief: ``` linopy_yaml/ ├── api.py ...

(EN_WORDINESS_PREMIUM_IN_BRIEF)

🪛 markdownlint-cli2 (0.23.0)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

📍 Affects 2 files
  • CLAUDE.md#L35-L51 (this comment)
  • README.md#L69-L69
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 35 - 51, Update CLAUDE.md’s module map to remove the
duplicate compat.py entry and describe the pure-producer compat lane. Update
README.md’s overview and Mermaid diagram to document that unsupported constructs
fail during loading rather than route to eager fallback.

37-37: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to the module-tree fence.

Use ```text (or another suitable language) to satisfy markdownlint MD040 and keep documentation checks clean.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 37-37: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` at line 37, Update the module-tree fenced code block in CLAUDE.md
to include an appropriate language tag, preferably text, immediately after the
opening fence while preserving its contents.

Source: Linters/SAST tools


55-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use the context-managed Solution in every public solve example.

These examples leave the DuckDB-backed solution open until garbage collection.

  • CLAUDE.md#L55-L61: wrap ly.solve(...) in a with block or call close().
  • README.md#L37-L41: use with ly.solve(...) as sol: in the short example.
  • README.md#L100-L113: use the same context-managed form in the quick example.
📍 Affects 2 files
  • CLAUDE.md#L55-L61 (this comment)
  • README.md#L37-L41
  • README.md#L100-L113
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 55 - 61, Update every public solve example to use the
context-managed Solution: in CLAUDE.md lines 55-61, README.md lines 37-41, and
README.md lines 100-113, wrap each ly.solve(...) assignment in a with block and
keep the existing solution access inside it.
GLOSSARY.md (3)

96-100: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Track the exponentiation gap in ROADMAP.md.

This section documents ** as a streaming parity gap while explicitly saying it is unlisted in the roadmap. Add the gap to the roadmap or remove this cross-document claim before shipping the language contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GLOSSARY.md` around lines 96 - 100, Update ROADMAP.md to include the
exponentiation (`**`) streaming parity gap described in GLOSSARY.md, aligning it
with the existing roadmap item for related language-parity gaps; preserve the
glossary’s description and classification.

267-271: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Synchronize SPEC §12.4 with the IR map.

The glossary says the specification omits Div, Shift, Defined, and Bool, although those nodes are implemented. Leaving the “full specification” stale makes supported and rejected constructs ambiguous across the documented contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GLOSSARY.md` around lines 267 - 271, Update SPEC §12.4 in GLOSSARY.md to
include the implemented IR nodes Div, Shift, Defined, and Bool in its IR
listing. Also align the where-AST and IR terminology for equivalent concepts by
mapping ExistenceCheck to Defined, Comparison to Cmp, and BoolLiteral to Bool,
keeping the documentation consistent with relational/ir.py.

323-329: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate dimension-valued kwargs during load.

sum(p, over=snapshto) can currently be accepted and silently lowered as a no-op: validation skips kwarg NameNodes, and the lowering path only checks whether the operand carries that name. Validate over=, into=, and roll/shift dimension keys against dimensions: before either lane dispatches.

As per coding guidelines, all validation should happen at load time with clear, actionable error messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@GLOSSARY.md` around lines 323 - 329, Update load-time validation in
validation.py to validate dimension-valued kwargs, including sum’s over=, into=,
and roll/shift dimension keys, against the declared dimensions before either
execution lane dispatches. Do not skip these kwarg NameNodes; emit clear,
actionable errors for unknown dimension names, while preserving existing
validation for data references and valid dimensions.

Source: Coding guidelines

linopy_yaml/compat.py (1)

125-149: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate declared coordinates against the actual model before applying overrides.

coords= overwrites known before the consistency check, so it can mask a mismatch between YAML values: and existing model coordinates. Preserve inferred coordinates for the comparison, then apply explicit overrides for master-coordinate resolution.

Proposed fix
-        known = _infer_coords(model)
-        if coords is not None:
-            known.update({k: pd.Index(v, name=k) for k, v in coords.items()})
+        inferred = _infer_coords(model)
 
         # If this YAML declares values: for a dim the model already has, they
         # must match. Silent override would hide real bugs.
         for dim_name, dim_def in schema.dimensions.items():
-            if dim_def.values is None or dim_name not in known:
+            if dim_def.values is None or dim_name not in inferred:
                 continue
             declared = pd.Index(dim_def.values, name=dim_name)
-            existing = known[dim_name]
+            existing = inferred[dim_name]
             if not declared.equals(existing):
                 ...
 
+        known = inferred.copy()
+        if coords is not None:
+            known.update({k: pd.Index(v, name=k) for k, v in coords.items()})
         master_coords = build_master_coords(schema, known)

As per coding guidelines, “Perform all validation at load time and provide clear, actionable error messages.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        inferred = _infer_coords(model)

        # If this YAML declares values: for a dim the model already has, they
        # must match. Silent override would hide real bugs.
        for dim_name, dim_def in schema.dimensions.items():
            if dim_def.values is None or dim_name not in inferred:
                continue
            declared = pd.Index(dim_def.values, name=dim_name)
            existing = inferred[dim_name]
            if not declared.equals(existing):
                msg = (
                    f"Extension declares dimension '{dim_name}' with values "
                    f'that differ from the existing model.\n'
                    f'  Existing: {list(existing)}\n'
                    f'  Declared: {list(declared)}\n'
                    f"Either omit 'values:' for '{dim_name}' in the "
                    f'extension, or make them match.'
                )
                raise ValueError(msg)

        # ``known`` is the override, so any dim it covers beats this YAML's
        # ``values:``. Dims still missing fall through to ``values:`` or raise.
        known = inferred.copy()
        if coords is not None:
            known.update({k: pd.Index(v, name=k) for k, v in coords.items()})
        master_coords = build_master_coords(schema, known)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@linopy_yaml/compat.py` around lines 125 - 149, Update the coordinate
validation flow around _infer_coords and build_master_coords so YAML dimension
values are compared against the model’s inferred coordinates before applying
explicit coords overrides. Preserve the inferred mapping for consistency checks,
then merge coords into the mapping used for master-coordinate resolution,
retaining the existing mismatch error behavior and actionable message.

Source: Coding guidelines

linopy_yaml/relational/executor.py (1)

777-789: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)executor\.py$|linopy_yaml/relational'

echo "== outline =="
ast-grep outline linopy_yaml/relational/executor.py --match _solution_to_parquet --view expanded || true

echo "== relevant lines =="
sed -n '740,805p' linopy_yaml/relational/executor.py

echo "== search copy/to usage =="
rg -n "COPY \(SELECT|TO '[^']*'|format parquet|parquet" linopy_yaml/relational/executor.py

echo "== duckdb available? show api if =="
python3 - <<'PY'
try:
    import duckdb
    print("duckdb", getattr(duckdb, "__version__", "<no ver>"))
except Exception as e:
    print("duckdb_import_error:", repr(e))
PY

Repository: FBumann/linopy-yaml

Length of output: 4201


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== behavioral probe: single-quoted COPY path with apostrophe and quote-like payloads =="
python3 - <<'PY'
import duckdb
import tempfile
from pathlib import Path

con = duckdb.connect(":memory:")
con.execute("CREATE TABLE t (x VARCHAR)")
con.execute("INSERT INTO t VALUES ('a')")

for filename in [
    "good.parquet",
    "path'containing'apostrophe.parquet",
    "path'; DROP TRIGGER x;--.parquet",
    'path"double"quote.parquet',
    "path\ntab.parquet",
]:
    with tempfile.TemporaryDirectory() as td:
        dirp = Path(td)
        out = dirp / filename
        try:
            con.execute("COPY (SELECT x FROM t) TO '{}' (FORMAT parquet) RETURN FILE_SIZE".format(out).replace("'", "''"))
            print("escaped_ok", str(out), out.exists())
        except Exception as e:
            print("escaped_err", str(out), type(e).__name__, str(e).splitlines()[0])

        try:
            con.execute(f"COPY (SELECT x FROM t) TO '{out}' (FORMAT parquet) RETURN FILE_SIZE")
            print("literal_ok", str(out), out.exists())
        except Exception as e:
            print("literal_err", str(out), type(e).__name__, str(e).splitlines()[0])
PY

echo "== DuckDB parameterized file output/docs =="
python3 - <<'PY'
import inspect
import duckdb
for name in ["DuckDBPyConnection", "DuckDBPyRelation"]:
    obj = getattr(duckdb, name, None)
    if obj is None:
        continue
    print(f"## {name}")
    for attr in dir(obj):
        if attr in ("to_csv_file", "to_csv", "to_parquet", "write_parquet", "write_csv"):
            m = getattr(obj, attr, None)
            if m is None:
                continue
            try:
                print(attr, inspect.signature(m))
                print(inspect.getdoc(m))
            except Exception as e:
                print(attr, "inspect_err", repr(e))
PY

Repository: FBumann/linopy-yaml

Length of output: 1925


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== corrected COPY TO behavior with properly escaped absolute path =="
python3 - <<'PY'
import duckdb
import tempfile
from pathlib import Path

con = duckdb.connect(":memory:")
con.execute("CREATE TABLE t (x VARCHAR)")
con.execute("INSERT INTO t VALUES ('a')")

for filename in [
    "good.parquet",
    "path'containing'apostrophe.parquet",
    "path"; DROP QUOTE x="--.parquet",
    'path"double"quote.parquet',
]:
    with tempfile.TemporaryDirectory() as td:
        dirp = Path(td)
        out = dirp / filename
        sql = f"COPY (SELECT x FROM t) TO '{out}' (FORMAT parquet) RETURN FILE_SIZE"
        print("sql:", repr(sql))
        try:
            res = con.execute(sql).fetchall()
            print("success", str(out), "exists", out.exists(), "rows", len(res))
        except Exception as e:
            print("error", type(e).__name__, str(e).splitlines()[0])

print("\n== parameterized/relational write APIs ==")
con.execute("CREATE TABLE t (x VARCHAR)")
con.execute("INSERT INTO t VALUES ('a')")
with tempfile.TemporaryDirectory() as td:
    out = Path(td) / "rel.parquet"
    rel = con.execute("SELECT x FROM t")
    rel.write_parquet(out)
    print("rel.write_parquet", out.exists())
PY

Repository: FBumann/linopy-yaml

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import duckdb
import tempfile
from pathlib import Path

con = duckdb.connect(":memory:")
con.execute("CREATE TABLE t (x VARCHAR)")
con.execute("INSERT INTO t VALUES ('a')")

filenames = [
    "good.parquet",
    "path'containing'apostrophe.parquet",
    "path'; DROP TRIGGER x;--.parquet",
    'path"double"quote.parquet',
]

for filename in filenames:
    with tempfile.TemporaryDirectory() as td:
        dirp = Path(td)
        out = dirp / filename
        sql_literal = f"COPY (SELECT x FROM t) TO '{out}' (FORMAT parquet) RETURN FILE_SIZE"
        sql_escaped = f"COPY (SELECT x FROM t) TO '{str(out).replace(chr(39), chr(39) + chr(39))}' (FORMAT parquet) RETURN FILE_SIZE"
        print("*", filename)
        print("  literal sql:", repr(sql_literal))
        try:
            res = con.execute(sql_literal).fetchall()
            print("  literal success:", out.exists())
        except Exception as e:
            print("  literal error:", type(e).__name__, str(e).splitlines()[0])
        print("  escaped sql:", repr(sql_escaped))
        try:
            res = con.execute(sql_escaped).fetchall()
            print("  escaped success:", out.exists(), "file_size", res[0][0] if res else None)
        except Exception as e:
            print("  escaped error:", type(e).__name__, str(e).splitlines()[0])

print("\n== parameterized/write API ==")
con.execute("CREATE TABLE t (x VARCHAR)")
con.execute("INSERT INTO t VALUES ('a')")
with tempfile.TemporaryDirectory() as td:
    out = Path(td) / "rel.parquet"
    rel = con.execute("SELECT x FROM t")
    rel.write_parquet(out)
    print("rel.write_parquet", out.exists())
PY

Repository: FBumann/linopy-yaml

Length of output: 2082


🌐 Web query:

DuckDB PYTHON COPY TO RETURN FILE_SIZE returns file size output option

💡 Result:

To obtain information about file sizes after a COPY TO operation in DuckDB, you should use the RETURN_STATS option [1][2]. When you execute a COPY ... TO statement with (RETURN_STATS TRUE), DuckDB returns a result set containing detailed information about the files written, including a file_size_bytes column [1][2]. Example usage in Python: con.execute("COPY (SELECT * FROM my_table) TO 'output.parquet' (RETURN_STATS TRUE);").fetchall The output of this query will be a result set containing the following columns for each file generated: - filename - count - file_size_bytes - footer_size_bytes - column_statistics - partition_keys - extra_info Note that while there is an option called FILE_SIZE_BYTES, it is a configuration parameter used to split output into multiple files of a specific size (often used with PER_THREAD_OUTPUT), not an output option to return the file size [1][2][3]. To get the size of the generated files, you must use RETURN_STATS [1][2]. Additionally, if you only need the list of filenames, you can use RETURN_FILES TRUE [4][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import duckdb
import tempfile
from pathlib import Path

con = duckdb.connect(":memory:")

filenames = [
    "good.parquet",
    "path'containing'apostrophe.parquet",
    "path'; CREATE TABLE x AS SELECT 1; DROP TRIGGER x;--.parquet",
    'path"double"quote.parquet',
]

for filename in filenames:
    with tempfile.TemporaryDirectory() as td:
        dirp = Path(td)
        out = dirp / filename
        sql_escaped = (
            "COPY (SELECT 'a') TO '"
            + str(out).replace("'", "''")
            + "' (FORMAT parquet) (RETURN_FILES TRUE)"
        )
        print("filename:", filename)
        print("sql:", sql_escaped)
        try:
            res = con.execute(sql_escaped).fetchall()
            print("success:", out.exists())
        except Exception as e:
            print("error:", type(e).__name__, str(e).splitlines()[0])

print("\n== parameterized write API ==")
with tempfile.TemporaryDirectory() as td:
    out = Path(td) / "rel.parquet"
    rel = con.sql("SELECT 'a'")
    rel.write_parquet(out)
    print("rel.write_parquet", out.exists())
PY

Repository: FBumann/linopy-yaml

Length of output: 1834


🌐 Web query:

DuckDB Python write_parquet Path incompatible function arguments Path str

💡 Result:

The error "incompatible function arguments" when passing a pathlib.Path object to DuckDB's Python functions typically occurs because the function expects a string [1]. While DuckDB has updated several functions to accept path-like objects (such as pathlib.Path) [2], inconsistencies have existed across different API methods (e.g., read_parquet vs write_parquet) [1][3]. To resolve this, you must explicitly convert your Path object to a string before passing it to the function: import duckdb from pathlib import Path path = Path("output.parquet") # Use str(path) to avoid the TypeError duckdb.sql("SELECT 42").write_parquet(str(path)) Alternatively, you can use the SQL COPY statement, which is a robust alternative for exporting data and is generally less sensitive to Python-side type restrictions: import duckdb from pathlib import Path path = Path("output.parquet") # SQL COPY handles the file path natively duckdb.sql(f"COPY (SELECT 42) TO '{path}'") DuckDB continues to improve its Python API to achieve better parity with standard Python file handling [3], but string conversion remains the most reliable workaround if you encounter this error in current versions.

Citations:


Escape the parquet destination path before embedding it in SQL.

OUT is interpolated directly into TO '{out}', so a valid path containing an apostrophe breaks the COPY; if the directory name is externally controlled, this also creates an SQL injection surface. Escape the path string literal or use DuckDB’s Python write-api with the path passed separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@linopy_yaml/relational/executor.py` around lines 777 - 789, Update
_solution_to_parquet so the destination path used in the DuckDB COPY statement
is safely escaped as a SQL string literal before interpolation, preserving valid
paths containing apostrophes and preventing injection. Apply the escaping
specifically to out while keeping the existing parquet output and written
mapping behavior unchanged.
README.md (1)

47-49: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the compat example self-contained.

The snippet calls compat.extend(...) without importing compat; it also relies on an implicit existing m. Add from linopy_yaml import compat and clearly state that m is an existing linopy.Model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 47 - 49, Add the linopy_yaml compat import to the
README example and explicitly identify m as an existing linopy.Model before the
compat.extend call, keeping the example otherwise unchanged.
tests/test_api.py (1)

169-182: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the pyarrow import with importorskip.

pyarrow.parquet is imported unconditionally inside this test, but per the PR objective pyarrow is no longer a runtime dependency, and this module isn't gated on any dependency in conftest.py (unlike the linopy-gated modules). On an environment without pyarrow installed this test will error rather than skip.

🧪 Proposed fix
 def test_solution_context_manager_and_to_parquet(dispatch_yaml, dispatch_inputs, tmp_path):
-    import pyarrow.parquet as pq
+    pq = pytest.importorskip('pyarrow.parquet')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

def test_solution_context_manager_and_to_parquet(dispatch_yaml, dispatch_inputs, tmp_path):
    pq = pytest.importorskip('pyarrow.parquet')

    sources, coords = dispatch_inputs
    with ly.solve(dispatch_yaml, sources, coords=coords) as sol:
        assert sol.status == 'Optimal'
        written = sol.to_parquet(tmp_path / 'solution')
        assert set(written) == {'p'}
        table = pq.read_table(written['p'])
        assert set(table.column_names) == {'snapshot', 'generator', 'value'}
        assert table.num_rows == sol.primal('p').shape[0]
    # closed by the with-block: the workdir is gone
    with pytest.raises(Exception):  # noqa: B017 — any error is fine, it must not silently work
        sol.primal('p')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_api.py` around lines 169 - 182, Update
test_solution_context_manager_and_to_parquet to import pyarrow.parquet through
pytest.importorskip, so the test is skipped when pyarrow is unavailable while
preserving the existing parquet assertions when it is installed.

@FBumann FBumann changed the title API v0.1: native entry points, closed helper set, compat as a pure-producer shim One language, two lanes: closed helper set, no monkey-patch, compat as a pure-producer shim Jul 24, 2026
FBumann added a commit that referenced this pull request Jul 24, 2026
…erge

`sum(p, over=snapshto)` used to build a model that solved and was wrong. The
typo made the sum a no-op: `_helper_sum` returns the array unchanged when it
does not carry the named dim, and `lowering.py` deliberately mirrors that for
eager parity — so both lanes agreed, and nothing downstream caught it.

`validation.py` skipped kwarg NameNodes as "dimension names, not data
references" and no other pass checked them. It now checks that they name a
*declared* dimension, in both positions the language uses:

- value position — `sum(x, over=d)`, `group_sum(x, m, into=d)`
- key position — `roll(x, d=1)`, `shift(x, d=1)`

Macro formals stay legal in a dim position: the template body is checked
against `dimensions | formals`, the same widening already applied to
variables and parameters. The error names the consequence, since that is the
part that bites: "an unknown dimension makes sum() a silent no-op rather than
an error".

Also restores four doc fixes that #43 merged without — GLOSSARY.md landed via
an earlier commit, but the corrective one did not:

- ARCHITECTURE/CLAUDE: pointers to GLOSSARY.md, and the tier-1 diagram no
  longer calls `piecewise:` a primitive (it expands to declarations and has no
  IR node — glossary section 0, SPEC §12.4).
- SPEC §12.4: the IR listing had drifted from `relational/ir.py` — `Div` and
  `Shift` missing from expressions, `Defined` and `Bool` from predicates.
- ROADMAP: row 5d, `**` evaluated eagerly and rejected by lowering.
- SPEC §7: dimension arguments are name-checked at load time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Jul 24, 2026
…erge (#48)

`sum(p, over=snapshto)` used to build a model that solved and was wrong. The
typo made the sum a no-op: `_helper_sum` returns the array unchanged when it
does not carry the named dim, and `lowering.py` deliberately mirrors that for
eager parity — so both lanes agreed, and nothing downstream caught it.

`validation.py` skipped kwarg NameNodes as "dimension names, not data
references" and no other pass checked them. It now checks that they name a
*declared* dimension, in both positions the language uses:

- value position — `sum(x, over=d)`, `group_sum(x, m, into=d)`
- key position — `roll(x, d=1)`, `shift(x, d=1)`

Macro formals stay legal in a dim position: the template body is checked
against `dimensions | formals`, the same widening already applied to
variables and parameters. The error names the consequence, since that is the
part that bites: "an unknown dimension makes sum() a silent no-op rather than
an error".

Also restores four doc fixes that #43 merged without — GLOSSARY.md landed via
an earlier commit, but the corrective one did not:

- ARCHITECTURE/CLAUDE: pointers to GLOSSARY.md, and the tier-1 diagram no
  longer calls `piecewise:` a primitive (it expands to declarations and has no
  IR node — glossary section 0, SPEC §12.4).
- SPEC §12.4: the IR listing had drifted from `relational/ir.py` — `Div` and
  `Shift` missing from expressions, `Defined` and `Bool` from predicates.
- ROADMAP: row 5d, `**` evaluated eagerly and rejected by lowering.
- SPEC §7: dimension arguments are name-checked at load time.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Jul 24, 2026
The two divergences #43 recorded and left. Both were the same defect: a file
meant different things depending on which lane built it, which hard rule 3
forbids. They are fixed in opposite directions, because the language should
contain one and not the other.

ROADMAP 5b — `where: "snapshot > 0"` now lowers. A dimension comparison needs
no join: the dim table is already in the frame, so it is a filter on its own
column. New `ir.DimCmp(dim, op, value)` predicate, one executor branch, and
the lowering case that used to raise. The README's own ramp example was
affected — a time-coupling constraint that skips the first snapshot is the
canonical use, and it was eager-only.

A dimension outside the component's `foreach` is still rejected, with the
reason: the mask would have to be `any`-reduced over an unlisted dim, which
mirrors the existing guard for out-of-frame parameters.

ROADMAP 5d — `**` is now rejected by the eager evaluator too, matching the
lowering that already refused it. It stays in the grammar and out of the
language: a variable base breaks degree 1, and a parameters-only power is
data prep. The error says which of the two rewrites applies.

Adds a differential test over the ramp pattern: same YAML and data through
both lanes, matching objectives, with an assertion that the mask actually
bites (the first snapshot is dropped per generator) so the test cannot pass
by ignoring the where entirely.

SPEC: predicate listing gains `DimCmp`; the precedence table and §5.2 now say
`**` parses but is refused at load time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FBumann added a commit that referenced this pull request Jul 24, 2026
…*` (#52)

The two divergences #43 recorded and left. Both were the same defect: a file
meant different things depending on which lane built it, which hard rule 3
forbids. They are fixed in opposite directions, because the language should
contain one and not the other.

ROADMAP 5b — `where: "snapshot > 0"` now lowers. A dimension comparison needs
no join: the dim table is already in the frame, so it is a filter on its own
column. New `ir.DimCmp(dim, op, value)` predicate, one executor branch, and
the lowering case that used to raise. The README's own ramp example was
affected — a time-coupling constraint that skips the first snapshot is the
canonical use, and it was eager-only.

A dimension outside the component's `foreach` is still rejected, with the
reason: the mask would have to be `any`-reduced over an unlisted dim, which
mirrors the existing guard for out-of-frame parameters.

ROADMAP 5d — `**` is now rejected by the eager evaluator too, matching the
lowering that already refused it. It stays in the grammar and out of the
language: a variable base breaks degree 1, and a parameters-only power is
data prep. The error says which of the two rewrites applies.

Adds a differential test over the ramp pattern: same YAML and data through
both lanes, matching objectives, with an assertion that the mask actually
bites (the first snapshot is dropped per generator) so the test cannot pass
by ignoring the where entirely.

SPEC: predicate listing gains `DimCmp`; the precedence table and §5.2 now say
`**` parses but is refused at load time.

Co-authored-by: Claude Opus 5 (1M context) <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.

1 participant