pse-arrow
An Arrow-native process systems engineering core in Rust: typed relations as the
only model authority, a relational math IR, DataFusion-based inference, native
NLP solving, and a generated Pyomo backend. It is a clean-room re-implementation
of core IDAES-PSE capabilities, parity-tested against idaes-pse 2.12.0, and it
is not affiliated with IDAES — see
Relationship to IDAES.
The distribution is pse-arrow on PyPI (the name pse is taken); the import
name is pse and the crate prefix is pse-. Everything is licensed
MIT OR Apache-2.0.
Where authority lives
| Question | Authority |
|---|---|
| What is the design? | authoritative_design/blueprint.md — one file, stable section numbers, revised in git |
| Why does it read that way? | adr/ — one record per decision, with the charter §H fields |
| What was deliberately deferred? | adr/register.md — one row per deferral, with its trigger and next check |
| How is the work sequenced? | plans/ — living until done, then an appended Outcome |
| Is the design any good? | design_review/reviews/ — evidence, not authority |
| What can the libraries actually do? | capability-maps/ and their evidence — probes, lockfiles and captured outputs |
| What runs in CI, and who answers when it goes red? | dev/ci.md |
When the code and a plan disagree, the code is what runs. When the code and the blueprint disagree, one of them is a bug, and the ADR says which.
Reading order
- Orientation — this page, then Relationship to IDAES.
- The design — blueprint §0 (purpose and reading guide), then §2 (the fourteen binding decisions D1–D14), then the section §0.3 points you at for what you are about to do.
- The decisions — the ADR index. ADR-0004 … ADR-0017 are D1–D14 one record each; ADR-0018 … ADR-0038 are the repository, supply-chain and process decisions.
- The evidence — the capability maps before
believing any claim about what Arrow, DataFusion or a supporting crate does at
the pinned version. Every claim in this book carries a charter §D evidence
label;
Proposedmeans exactly that.
Evidence vocabulary
Every claim in these documents is labelled with the charter §D vocabulary —
Proposed, Interface-checked, Implemented, Tested, Measured,
Formally established. The labels describe different claims, not one ladder: a
measured implementation can still be incorrect, and an interface-checked design
can still need substantial engineering. The definitions are in
design_review/design_principles/DATA_MODEL_DESIGN_CHARTER.md
§D.
Relationship to IDAES
Not affiliated
pse-arrow is not affiliated with, endorsed by, or sponsored by the IDAES
project, the U.S. Department of Energy, or any of the institutions of the IDAES
consortium. “IDAES” is used here only to name the project this one is compared
against, which is nominative use; any trademarks are the property of their
owners. Nothing in this repository is an IDAES release, an IDAES fork, or an
IDAES-supported product, and questions about it should come here rather than to
the IDAES project.
IDAES-PSE is Apache-2.0 licensed. pse-arrow is MIT OR Apache-2.0.
Clean room
The architecture is a re-implementation, not a translation:
external/idaes-pseis a reading copy, checked out at the pinned parity tag byscripts/fetch-external.shand gitignored. It is read to understand behaviour — what a control volume writes, which defaults a solver profile carries, what an initializer does in what order.- No IDAES code is copied into this repository. No file, no function body, no
docstring. The pull-request template carries a clean-room attestation, and
external/is on the agent configuration’s deny list for edits. - The only coupling is the parity harness (
python/pse/parity/), which importsidaesin a separate environment and compares results. - Names are preserved where parity requires a two-way mapping — the property
name
enth_mol_phase, the enumerations listed in blueprint §6.14, the IDAES default thresholds in §15.1, and the external-function symbol names in §26. Those are interface facts, and each is enumerated in the blueprint rather than absorbed silently.
The architectural decisions are recorded in ADR-0003 (clean-room relationship and the parity pin) and ADR-0032 (external checkouts are fetched, never vendored).
The parity pin
Parity is measured against exactly one IDAES release:
idaes-pse == 2.12.0
It is declared once, in the parity dependency group of pyproject.toml, and
it is the version scripts/fetch-external.sh checks out. A lint in
governance / adr-lint asserts the two agree, so the reading copy can never
drift from what the tests measure against.
The parity environment is a separate resolution (.venv-parity, CPython
3.13) because idaes-pse classifies 3.10–3.13 while the platform package
targets ≥ 3.11 and is verified on 3.14. Keeping them separate is what stops
IDAES’s transitive dependencies — pydantic, sympy, networkx, pandas —
from becoming platform dependencies. The parity pre-flight fails rather than
skips when the interpreter, the IDAES version or ipopt is not what the pin
says.
How the pin moves
Moving it is a decision, not a dependency bump:
- Open a PR labelled
adrwith a new ADR that supersedes the parity-pin part of ADR-0003, stating what changed upstream and what parity results moved. - Update the
paritygroup pin and the tag inscripts/fetch-external.shin the same PR; the lint checks they agree. - Re-run
python / parityon every supported interpreter and record the differences in the ADR’s Consequences section with a charter §D evidence label.
Dependabot never moves it: idaes-pse is in the ignore list of the uv
configuration for exactly this reason (ADR-0035).
Citing IDAES
When citing the project this one is compared against:
Lee, A., Ghouse, J. H., Eslick, J. C., Laird, C. D., Dowling, A. W., Bhattacharyya, D., Biegler, L. T., Burgard, A. P., Miller, D. C., & Omell, B. P. (2021). The IDAES process modeling framework and model library — Flexibility for process simulation and optimization. Journal of Advanced Manufacturing and Processing, 3(3), e10095. https://doi.org/10.1002/amp2.10095
CITATION.cff references the same paper as the parity reference.
What “core IDAES-PSE capabilities” means
The scope is the modeling framework, not the example libraries: the process
block lifecycle, unit models and ports, control volumes, the physical and
modular property frameworks, reactions, the generic unit model library, costing,
initialization, scaling, diagnostics, solvers and DAE discretization,
serialization and units, and parameter sweeps. Surrogates, the DMF, the UI,
Pecos, the apps and the models_extra libraries are out of scope; surrogate
embedding is covered by the kernel contract. Blueprint §0.2 carries the full
table, and Appendix A maps every capability to its platform realization and its
delivery phase.
Authoritative design
What “authoritative” means
blueprint.md is the design. When an implementation and the
blueprint disagree, one of them is a bug, and an ADR says which. Nothing else in
this repository has that status:
| Document | Status |
|---|---|
blueprint.md | Authoritative. One file, revised in git. Revision 5 is the current Proposed contract amendment; tags design-rev2 and design-rev3 mark the seeded history. |
proposal.md | Historical. The thesis the blueprint makes concrete; superseded by it and kept for the record. Front matter says so. |
../adr/ | Why. One record per decision. A record binds; it does not restate the design. |
../design_review/reviews/ | Evidence, not authority. A review finds defects and cites measurements. It does not change the design; the ADR that responds to a finding does. |
../capability-maps/ | Evidence. What the pinned libraries actually do, with reproducible probes under evidence/. |
Section numbers are citation targets
ADRs, reviews, plans, the agent instructions and the lint all cite the blueprint
by section: blueprint §14.3, §D7, §3.1. Those numbers are stable. New
material is inserted as a sub-section (§14.3.1), never by renumbering, and
scripts/adr.py lint resolves every § citation in every ADR against the
headings of blueprint.md — so a renumbering fails governance / adr-lint
rather than quietly rotting every record that cited it.
The decision behind that rule is ADR-0033.
The amendment rule
The blueprint changes only through a pull request that:
- is labelled
adrand titledadr: ADR-NNNN <title>(or is a named follow-updesign:PR referenced by that ADR); - adds or changes exactly one decision record under
../adr/; - adds a row to the blueprint’s Revision history table naming the revision, the date, the change and the git tag or pull request;
- adds an inline
> Decision: ADR-NNNNline under the heading of every section the decision governs, so a reader of the section finds the record without searching; - keeps every existing section number exactly where it was.
An ADR may merge with status: proposed only if the PR is also labelled
needs-review. Changes that alter D1–D14, add or remove a crate, add, drop or
major-bump a dependency family, change the hashing contract, the Python boundary
contract, the metadata conventions or the commit contract, or that deviate from
a SHOULD, need an ADR and a design review with an Accept or Accept-scoped
verdict before the record becomes accepted. The full table is in
../plans/01-repository-configuration.md
§7 and in .codex/skills/adr/SKILL.md.
Revisions so far
| Revision | What it settled |
|---|---|
| 1 | The initial blueprint following the proposal. |
| 2 | The first review’s priority-1 findings (F1–F7, F9, F15); the dependency anchors moved to DataFusion 55.1.0 / Arrow 59.3.0 with family-wide = pins. Tag design-rev2. |
| 3 | Every finding of both reviews, including the items revision 2 parked (F8, F10, F12–F14, F16, F17) and R2-1 – R2-8, using the four capability maps’ measurements. Tag design-rev3. |
| 4 | Repository conventions: MSRV equals the pinned toolchain (ADR-0018); workspace layout additions (ADR-0038); documents under docs/ (ADR-0033). |
| 5 | Proposed: library-backed semantic/quantity admission, complete demand/stage/reuse contracts, guarded kernels/numerics, canonical v2 versus encoded integrity, shared accounted resources and bounded relational/import operators. ADR-0039–ADR-0048; review, implementation handoff. |
Evidence labels are mandatory
Every claim in the blueprint, in an ADR, in a plan’s verification section and in
a register row carries a charter §D label — Proposed, Interface-checked,
Implemented, Tested, Measured, Formally established. Tested and
Measured must name the test or benchmark and the conditions. Proposed is not
a failure state; an unlabelled claim is.
Architecture blueprint
status: proposed revision: 6 date: 2026-09-13
Arrow-native IDAES core: detailed design architecture blueprint
Status: Proposed design blueprint, revision 6 (2026-09-13); implementation and numerical acceptance remain open. One file, revised in git: revisions 2 and 3 are the tags design-rev2 and design-rev3 (ADR-0033); the Revision history table below lists every revision.
Reviews: revisions 1 and 2 have historical reviews under docs/design_review/reviews/. The revision-4 library-contract review overturned several earlier enforcement claims. Revision 5 incorporates R4-01–R4-13 and L1–L9; §26 records disposition and plan 02 names the remaining implementation gates. Library characterization is evidence about the pinned libraries, not acceptance of the skeletal platform.
Follows: docs/authoritative_design/proposal.md (the proposal this document makes concrete)
Governing doctrine: docs/design_review/design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01–DM-60, gates G1–G7) applied through AGENT_DESIGN_DIRECTIVE.md. The P-numbered principles cited in §2.1 and §22.2 come from the doctrine texts this document was drafted against (semantic_design_principles_holistic, Inference-Complete Process Metamodel Design Principles, semantic_math_basis), which are not part of this repository.
Library evidence: the capability maps under docs/capability-maps/, pinned DataFusion 55.1.0 / Arrow 59.3.0 source and rustdoc, and the retained review characterization probes. Context7 is a discovery aid; pinned interfaces and reproducible behavior determine capability claims. The external/idaes-pse reading copy at tag 2.12.0 is for behavior only (just fetch-external; never copy implementation or prose).
Contents
- Revision history
- 0. Purpose, scope, and how to read this document
- 1. Architectural summary
- 2. Architectural decisions
- 3. Workspace, crates, and dependency pins
- 4. The semantic schema registry
- 5. Identity, versions, snapshots, and the catalog
- 6. The canonical relation families
- 7. The mathematical IR
- 8. Physical typing
- 9. Material systems and the property framework
- 10. Balance laws and control volumes
- 11. The unit model library as templates
- 12. Connectivity
- 13. Flowsheets, time, and dynamics
- 14. The compiler
- 15. Structural analysis and diagnostics
- 16. Scaling as an explicit transformation
- 17. Initialization plans
- 18. Backends
- 19. Cases, results, and analytics
- 20. Persistence, provenance, and reproducibility
- 21. The Python boundary and the Pyomo adapter
- 22. Authoring and the extension model
- 23. Observability and failure semantics
- 24. Testing and acceptance
- 25. Delivery phases
- 26. Risks and open decisions
- Appendix A. IDAES capability coverage matrix
- Appendix B. Relation index
- Appendix C. Glossary
Revision history
| Revision | Date | Change | git |
|---|---|---|---|
| 1 | 2026-09-13 | Initial blueprint following the proposal. | — |
| 2 | 2026-09-13 | Resolves the priority-1 findings of the design review of the same date. F1 authored identity is assigned at creation and stored in the document; qualified_name is an attribute; change_ops.op = rename; case targets resolve to identities at commit (§5.1, §6.1, §6.10, §22.2). F2 every pse.expr_dsl column is the sole authored form of an expression; parsed graphs live in normalized.*_expr_* produced by P3 (§6.6, §7.7). F3 solver profiles and discretization policies are authored policy relations; compiled holds only bound plans (§6.10, §6.11, §14.1). F4 undecided rule outcomes are written to inferred.undecided, never to a head relation (§6.7, §7.6, §14.2, §14.5). F5 affine_kind is replaced by scale_kind ∈ {point, difference}; multiplication and division are unrestricted after canonical-unit normalization; the addition algebra is unchanged (§6.2, §7.2, §7.4, §8). F6 a dedicated pass P12 (index expansion) turns indexed equation instances into scalar rows; P12–P15 of revision 1 are now P13–P16; §7 and §8.2 pass references corrected (§6.9, §7.1, §7.2, §14.1). F7 P10 never folds parameter symbols; static attributes are computed on a case-bound view whose substitutions are recorded (§7.4, §7.5, §14.1, §14.4). F9 the DataFusion engine is a declared input: reference.engine_profiles, explicit rule lists, semantic-settings hash and a datafusion-proto plan fingerprint enter the memo key and the pass record (§6.11, §14.2, §14.3, §20.2). F15 a lifecycle test layer and the FFI/subprocess interruption rules (§18.3, §18.4, §20.1, §24.1). Also: dependency anchors raised to DataFusion 55.1.0 / Arrow 59.3.0 with family-wide = pins (§3.1), per the project owner’s direction. | design-rev2 |
| 3 | 2026-09-13 | Resolves every finding of the second review and every item revision 2 had parked, using the four capability maps’ measurements. R2-1 the plan fingerprint leaves the memo key; schema and field metadata are canonicalized at construction; the codec obligations, the analyzer rule list and a versioned settings allow-list are stated (§4.3, §14.2, §14.3, §6.11). R2-2 -0.0 preservation is qualified; Float64 columns are never distinct or join keys in rule plans (§5.3, §14.2, §19.2). R2-3 §4.3 corrected; the ten pse.* types are registered in the engine’s extension-type registry (§4.3, §4.4, §5.4, §14.3). R2-4 the Python boundary is rewritten as enforced contracts: pint validates and never defines, contract classes are generated, strict structuring, an Any lint, the msgspec/attrs division, shipped pyarrow extension classes with an idempotent registrar, the numpy null rule, pre-flight checks, exact pins and two interpreter ranges (§3.1, §3.3, §4.4, §21). R2-5 every crate and Python library is pinned, a committed lockfile and cargo deny are required, arrow-flight and uom are dropped, §3.3 roles match use sites, serde-saphyr replaces serde_yaml, num-dual is 0.15 with FeOs conditional, blake3 belongs to pse-ids with derive_key (§3.1, §3.2, §3.3, §5.1, §9.8). R2-6 object_store, petgraph and faer statements corrected (§15.3–§15.5, §20.1). R2-7 §5.4 states the measured pushdown shapes, purity, the wrapper test, Precision and selective statistics. R2-8 feedback_arc_set, the formatter factory, force_validate, a datafusion.pse.* config extension, pgjson plans, the optimizer observer, toml::Spanned, implicit_derivative, and the Ipopt C names are adopted (§12.5, §14.2, §18.2, §18.3, §22.1, §23, §24.1). Parked items: F8 rule bodies, invariant specs and selectors are typed relations (§4.1, §6.7, §6.10, §6.11); F10 capability probes are runtime operations with recorded resolved options (§6.13, §18.3); F12 the hashing contract names its constants and its scope (§5.3, §18.2); F13 salsa is deferred behind an artifact-hash memo with a stated trigger (§14.3); F14 derivation granularity is declared per relation (§4.1, §14.2); F16 the bundle carries quantity types and a loss profile (§21.1); F17 the SVD and condition-number routes are named and the pushdown precondition is discharged (§15.4, §15.5, §5.4). Also decided: FairSpillPool with an explicit limit (§14.3), pyo3-arrow retained (§21.1), LogicalPlan::Extension rule nodes deferred with a trigger (§14.2), IDAES 2.12.0 as the parity reference (§3.1, §6.14, §25), the commit contract stated (§22.2). | design-rev3 |
| 4 | 2026-09-13 | Repository conventions: MSRV = pinned toolchain (ADR-0018); layout additions (ADR-0038); document paths under docs/ (ADR-0033). | repository setup history |
| 5 | 2026-09-13 | Proposed: active semantic admission and complete quantity operations; normalized demand seeds and stage bundles; conservative complete cache keys; guarded kernel/numeric policies; canonical logical identity separated from encoded integrity (pse.canon.v2); noncanonical plan evidence; shared accounted budgets; standard predicate, aggregate, unnest, buffer and explicit-schema reader bindings. Addresses R4-01–R4-13 and L1–L9 under ADR-0039–ADR-0048. | this design revision; maintainer PR acceptance pending |
| 6 | 2026-09-13 | Wave-1 implementation conventions: direct pins for serde_json and uuid (ADR-0049); identity framing constants, canonical frames and the phase-1 envelope (ADR-0050); generated-tree additions and regeneration-equivalence check (ADR-0051); small contract clarifications listed under §3.1, §4.2, §4.4, §6.1, §6.9, §7.2, §7.7, §22.2 and Appendix B. | wave-1 keel |
0. Purpose, scope, and how to read this document
0.1 What this document is
The proposal established the thesis: make mathematical and physical semantics the data model, then compile every executable representation from it. This document is the blueprint that turns that thesis into things an implementer can build and test:
- the exact relation families (Arrow schemas) that constitute the authoritative model;
- the mathematical IR and its operator contracts;
- the compiler passes, each with a declared contract;
- the kernel contract for constitutive computation and how it generates DataFusion, native, and Pyomo adapters;
- the backends (native evaluation program, Ipopt FFI, NL file, generated Pyomo);
- the coverage map from every core IDAES-PSE capability to its platform realization;
- the workspace layout, dependency pins, and delivery phases with acceptance tests.
It is written for engineers and programming agents who will implement the platform. Where a design decision could reasonably go two ways, the decision is stated with its rationale rather than left open.
This file is the authoritative design. Decisions that changed it are recorded under docs/adr/ (ADR-NNNN); design reviews under docs/design_review/reviews/ are evidence, not authority. Section numbers are stable citation targets: new material is inserted as a sub-section, never by renumbering.
0.2 What “core IDAES-PSE capabilities” means here
The scope is the modeling framework, not the example libraries. Concretely, the platform must reproduce the capability of these IDAES subsystems (source paths are relative to idaes-pse/idaes/):
| IDAES subsystem | Source | In scope |
|---|---|---|
| Process block lifecycle, config system, flowsheet and time domain | core/base/process_block.py, process_base.py, flowsheet_model.py | Yes |
| Unit model base, ports, arcs, translation | core/base/unit_model.py, Pyomo network | Yes |
| Control volumes 0D and 1D, all balance variants, geometry, reactions, holdup | core/base/control_volume_base.py, control_volume0d.py, control_volume1d.py, extended variants | Yes |
| Physical property framework: parameter blocks, state blocks, metadata, property sets, phases, components | core/base/property_base.py, property_meta.py, property_set.py, phases.py, components.py | Yes |
| Modular (generic) property framework: state definitions, EOS, pure-component methods, phase equilibrium, transport, bubble/dew | models/properties/modular_properties/** | Yes |
| Reaction framework and generic reaction package | core/base/reaction_base.py, models/properties/modular_properties/reactions/** | Yes |
| Generic unit model library | models/unit_models/*.py | Yes (as templates) |
| Costing framework and SSLW library | core/base/costing_base.py, models/costing/SSLW.py | Yes |
| Initialization framework and initializers, homotopy | core/initialization/**, core/util/initialization.py, core/solvers/homotopy.py | Yes (as plans) |
| Scaling toolbox (new and classic) | core/scaling/**, core/util/scaling.py | Yes (as transformations) |
| Model statistics and diagnostics toolbox, SVD, degeneracy hunter, convergence analysis | core/util/model_statistics.py, model_diagnostics.py, diagnostics_tools/** | Yes (as queries and native analyses) |
| Solver configuration, PETSc DAE integration, solver features | core/solvers/** | Yes (as solver profiles and backends) |
| Dynamic utilities and DAE discretization | core/util/dyn_utils.py, Pyomo dae | Yes (as lowering) |
| Serialization, tables, tags, units, constants, smooth math | core/util/model_serializer.py, tables.py, tags.py, units_of_measurement.py, constants.py, math.py | Yes |
| Parameter sweeps, utility minimization, convergence evaluation | core/util/parameter_sweep.py, utility_minimization.py, convergence/** | Yes |
| Helmholtz and CoolProp property backends | models/properties/helmholtz/**, general_helmholtz/**, modular_properties/coolprop/** | Provider adapters only |
Surrogates (core/surrogate), DMF (core/dmf), UI (core/ui), Pecos, apps (apps/**), power-generation and gas-solid libraries (models_extra/**) | various | Out of scope; surrogate embedding is covered by the kernel contract |
Detailed capability-by-capability coverage appears in Appendix A.
0.3 Reading guide
| If you need to | Read |
|---|---|
| Understand the non-negotiable decisions | §2 |
| Set up the workspace and pins | §3 |
| Declare a new relation | §4, §6 |
| Understand identity, versions, snapshots | §5 |
| See every authoritative relation | §6 |
| Write or lower an equation | §7, §8 |
| Model a material system or property package | §9 |
| Build a balance, control volume, or unit template | §10, §11, §12 |
| Understand time, dynamics, discretization | §13 |
| Implement or extend a compiler pass | §14 |
| Implement diagnostics, scaling, initialization | §15, §16, §17 |
| Implement a backend or the Python adapter | §18, §21 |
| Persist, hash, or reproduce anything | §20 |
| Add a unit, property method, kernel, or costing method | §22 |
| Plan delivery and acceptance | §24, §25 |
0.4 Conventions used in this document
- Relation schemas are written as
namespace.relation @versionfollowed by columns asname : ArrowType [constraints] -- meaning. Arrow types usearrow-schemanames. Semantic extension types are namedpse.<name>and defined in §4.4. - Equations are written in plain text;
d/dtis the time derivative,Σ_jis a reduction over the named domain. - “Authored” means written by a model author or package; “derived” means produced by a compiler pass and never authored; “reference” means shipped library data.
- IDAES names are quoted where the platform deliberately preserves them (for example the property name
enth_mol_phase) so that parity tests can map both ways.
1. Architectural summary
1.1 The one-paragraph design
A process model is a versioned snapshot of typed Arrow relations organized into seven catalog namespaces (reference, authored, normalized, inferred, compiled, runtime, provenance). Authors write packages (material systems, property methods, unit templates, laws, costing methods) and cases (instances, connections, specifications) in a declarative authoring language that parses into authored relations. A staged compiler with contracted passes derives everything else: type and topology closure, property-demand closure, law expansion into equations, discretization, structural analysis, scaling and initialization plans, and finally a backend-neutral CanonicalMathProblem. DataFusion is the relational engine for the compiler’s set-oriented passes, the snapshot catalog, kernel batch evaluation, and all analytics. Native Rust numerics (an evaluation program with automatic differentiation, sparse linear algebra, and structural graph algorithms) execute the problem in-process with Ipopt, or lower it to the AMPL NL format or to a generated Pyomo model. Every artifact is content-addressed, every derived row carries a derivation, and every run records the exact inputs that produced it.
1.2 The three representations, made concrete
| Representation | Namespace(s) | Authority | Produced by | Consumed by |
|---|---|---|---|---|
CanonicalModel (engineering intent) | authored, normalized, inferred | Authored facts plus deterministic inference | Authoring parser, passes P0–P6 | Law expansion, projection, UI |
CanonicalMathGraph (indexed symbolic mathematics) | compiled.math_* | Derived | Passes P7–P12 | Structural analysis, scaling, backends |
CanonicalMathProblem (ordered executable problem) | compiled.problems, variable_order, equation_order, derived structure | Derived | Passes P13–P16 | Native backend, NL writer, Pyomo adapter, diagnostics |
The runtime namespace holds results and evidence; the provenance namespace holds derivations and pass records. No pass writes backward into authored.
1.3 Where each library sits
authoring documents (YAML/TOML/Python builders)
│ load (serde-saphyr, toml; both with source spans) + expression DSL parse (winnow) → authored relations
▼
Semantic schema registry ──generates──► Arrow schemas · typed Rust views · serde_arrow adapters
│ validators · docs · migrations · TableProviders
▼
Snapshot catalog (DataFusion CatalogProvider/SchemaProvider/TableProvider over immutable artifacts)
│
├─ Rule compiler: typed RulePlanSpec → LogicalPlanBuilder → RecordBatch (closure, joins, selectors)
├─ Fixed-point executor (recursive closure with provenance) (Rust over DataFusion plans)
├─ Native graph algorithms (petgraph + own matching/DM/BTD) (structural analysis)
├─ Math IR passes (Rust; memoized on declared-input content hashes) (canonicalization, lowering)
├─ Kernel registry → generated ScalarUDFImpl wrappers (batch property evaluation)
▼
CanonicalMathProblem
├─ Native evaluation program (instruction tape, per-equation reverse AD, faer sparse LA)
│ └─ Ipopt C-API FFI in-process
├─ AMPL NL writer / SOL reader (ipopt, bonmin, couenne, cbc, scip, petsc_snes, petsc_ts, k_aug)
└─ Generated Pyomo model via pyo3 + pyo3-arrow bundle (parmest, PyROS, GDPopt, parity)
▼
runtime relations (solutions, residuals, duals, iterations, diagnostics) + provenance
└─ DataFusion analytics: stream tables, KPIs, diagnostics reports, sweeps
2. Architectural decisions
Each decision below is binding for the implementation. “Because” states the rationale; “Consequence” states what it forces.
D1. Typed relations are the only authority
Decision: ADR-0004
Every durable fact about a model lives in a typed Arrow relation declared in the semantic schema registry (§4). There is no entity–attribute–value table, no JSON column that carries required behavior, and no Rust struct that is the “real” model with Arrow as an export.
Because the proposal’s central claim (§2.1 of the proposal) is that structure declared once removes five parallel maintenance surfaces. Consequence: Rust structs that hold model data are generated views over relations, never hand-written mirrors.
D2. Author causes, derive consequences
Decision: ADR-0005
The authored model holds only primitive facts (topology, materials, quantities, phenomena, laws, options, specifications). Participation, roles, boundary classification, property requirements, method selection, equations, and orderings are derived by contracted passes and materialized with derivation provenance.
Because the metamodel doctrine (Principles 1–3, 10–12, 20) and because IDAES’s largest maintenance burden is imperative build() methods that restate consequences. Consequence: an IDAES build() method becomes a template plus inference rules; a derived relation that an author tries to write is rejected or downgraded to an assertion.
D3. Three artifact levels, one schema system
Decision: ADR-0006
CanonicalModel, CanonicalMathGraph, and CanonicalMathProblem are stages in one catalog, share identity conventions, extension types, hashing, and provenance. They are not three applications.
D4. Semantic IDs, artifact-local ordinals, and content hashes are distinct
Decision: ADR-0007
Stable 128-bit semantic IDs identify authored and derived entities across revisions; UInt32/UInt64 ordinals index compiled artifacts; blake3 content hashes identify immutable artifact versions. Names, row positions, dictionary codes, and solver positions are never identity (§5). An authored entity’s identity is assigned when it is created and stored in its document; its qualified_name is an attribute that a rename change op may alter without touching any identity (§5.1, §22.2).
D5. Physical type is more than a unit string
Decision: ADR-0039
Every quantity-bearing column and every symbol carries a quantity_type_id resolving dimension vector, quantity kind, basis, reference state, affine semantics, and index shape (§8). Heterogeneous value columns require a per-row quantity_type_id. Null never means “unknown to be solved” (§7.6).
D6. The math IR is richer than DataFusion Expr
Decision: ADR-0009, ADR-0047
Expression nodes, arguments, and typed operator payloads are relations; indexed operators (SumOver, Gather, Broadcast, Derivative, Integral, ImplicitSystem, KernelCall) survive until a backend requires scalarization. DataFusion Expr is used to compute over the IR, not to be the IR (§7).
D7. Laws are templates over contributions
Decision: ADR-0010
Material, energy, momentum, element, and charge balances, and the costing and utility-minimization laws, are instances of law templates expanded over a contributions substrate by the generic compiler (§10). Control volumes are template compositions that declare which contributions exist.
D8. Property demand is resolved explicitly
Decision: ADR-0040
There is no lazy attribute construction. P6 closes demand extracted from normalized template, law, port, display and initializer declarations before P7–P9 realize equations or kernels (§9.6). Selection, unresolved demand and provision checks are explicit. Inspection never constructs physics.
D9. Kernels have one contract and generated adapters
Decision: ADR-0043
Every constitutive computation implemented in code is a KernelSpec with identity, signature (physical types), mathematical behavior, derivative availability, execution forms, failure behavior, and backend bindings. Scalar, batched Arrow, derivative, DataFusion UDF, NL external-function, and Pyomo adapters are generated only for its declared implementation-backed bindings (§18.5, §21.4). A signature does not supply an algorithm or prove support.
D10. DataFusion has four roles and no more
Decision: ADR-0013
Relational assembly and inference (rule plans), snapshot catalog (providers), batch kernel evaluation (generated UDFs), and analytics (queries over runtime). It does not run inside Newton iterations, and a solve is never a scalar function call.
D11. Native numerics own execution layouts
Decision: ADR-0047
The evaluation program, sparse Jacobian structures, and solver workspaces are compiled artifacts derived from relations. They borrow Arrow buffers when layouts permit and copy when they do not. Zero-copy is a preference, not an obligation.
D12. Pyomo is a generated, generic, coarse-grained backend
Decision: ADR-0015
One adapter consumes a CanonicalMathProblem bundle over the Arrow C stream interface and constructs Pyomo objects. No IDAES class hierarchy is recreated. The adapter exists for parity testing and for Pyomo-ecosystem tools (parmest, PyROS, GDPopt, DAE utilities) until native equivalents exist.
D13. Cases and results never mutate the model
Decision: ADR-0016
A model revision defines structure; a case revision defines values, bounds, fixed status, objectives, and overlays; a run records what happened. Initialization stages, homotopy steps, and “fix then release” operations are immutable case overlays, not mutations.
D14. Incrementality follows declared dependencies
Decision: ADR-0041, ADR-0042
Every pass declares its complete input ports; memoization is keyed by their logical contents, absence states, pass version, and — for passes that execute plans — the engine profile (§14.3). Initial reuse is at complete stage granularity; fine-grained invalidation is deferred until measured and qualified (§14.4, §14.5). No stage may read outside its declared input bundle.
2.1 Doctrine crosswalk
| Doctrine principle | Realized by |
|---|---|
| Platform-independent semantics (holistic P9) | authored/normalized/inferred namespaces contain no backend concepts; backends bind in compiled.backend_bindings |
| Declarative single-sourcing (P10) | Schema registry generates all derived code and docs (§4) |
| Parse, don’t validate; illegal states unrepresentable (P11, P12) | Authoring parser produces typed relations; enums are closed dictionaries; option conflicts are schema invariants |
| Stable semantic identity (P13) | §5 |
| Staged compilation, canonicalization before optimization (P14, P15) | Pass pipeline §14; canonicalization P3 precedes any lowering |
| Design by contract (P16) | PassSpec, KernelSpec, RelationSpec are relations with declared preconditions and postconditions |
| Functional core, imperative shell (P17) | Passes are pure functions of snapshots; the run controller and object store are the shell |
| Durable domain truth vs temporal control truth (P19) | Model/case revisions vs runtime.run_state |
| Unified mutation model (P20) | Change sets (§22.2) are the only write path into authored |
| Explicit failure semantics (P23) | Failure taxonomy §23 |
| Reproducibility and hermeticity (P25) | Manifests §20 |
| Primitive-fact minimality, roles as views, contributions substrate, laws as templates (metamodel P1, P3, P10, P12) | §6.4–§6.9, §10 |
| Unknown is not false (metamodel P18) | Inference predicates are four-valued; unknown becomes a diagnostic, never an exclusion (§14.3) |
| Rules typed, stratified, deterministic (metamodel P19) | RuleSpec relation and fixed-point executor (§14.2) |
| Solver strategy from static attributes (math basis §0.2) | Static analysis report and solve-plan selection (§18.7) |
3. Workspace, crates, and dependency pins
3.1 Version anchors
Decision: ADR-0049
Engine and runtime (Rust). Every crate in both families is pinned with =; Cargo.lock is committed and CI asserts one version per family (cargo tree grouped by family) — = pins bind direct dependencies only, and cargo tree -d does not report a mixed family because it is not a duplicate.
| Component | Pin | Note |
|---|---|---|
datafusion and every datafusion-* subcrate | 55.1.0 | Requires arrow ^59.2.0 and object_store ^0.13.2 (required by DataFusion, not chosen). scan_with_args/ScanArgs, PhysicalPlanningContext, EnsureRequirements, is_strict, convert_to_state are the 55 forms to code against; convert_to_state has no consumer unless a UDAF is written. Default features plus nothing else; serde, avro, parquet_encryption, backtrace off. |
arrow, arrow-*, parquet | 59.3.0 | DataFusion 55.1.0 builds and tests against arrow 59.2.0 (caret), so the platform runs one minor ahead of the Arrow the engine is exercised against — the first place to look if an Arrow-level behaviour surprises the engine. Features: ipc, ffi, canonical_extension_types; force_validate in the test and CI profiles only (§24.1); ipc_compression and prettyprint never on an identity path. arrow-flight and arrow-avro are not dependencies (no consumer). |
object_store | 0.13.2 | The 0.13 line exposes *_opts methods only (put_opts, get_opts, copy_opts, rename_opts, delete_stream); §20.1 is written against that surface. |
pyo3 / pyo3-arrow | 0.29 / 0.19.0 | Retained over arrow-pyarrow: the platform needs the PyCapsule stream protocol and the numpy bridge that pyo3-arrow provides, and arrow-pyarrow requires a Python interpreter at build time, which breaks the offline build. pyo3-arrow depends on thiserror 1.x beside the platform’s 2.x; no type crosses that boundary. abi3-py311 wheels. |
tokio | 1.53.1 | DataFusion runtime. |
| Rust edition / MSRV | 2024 / pinned stable toolchain (1.98.1 at revision 4; ADR-0018) | DataFusion 55’s own floor is 1.94.0; the workspace declares only what CI exercises, and tests/governance keeps rust-version equal to rust-toolchain.toml. Every supporting crate below builds under it (verified by the capability-map extraction on a 1.100 nightly; the declared floors of the pre-1.0 crates are re-checked at each upgrade). |
Supporting crates (Rust). All pinned with =; the resolved set is the one the supporting-library capability map committed as a lockfile under docs/capability-maps/evidence/rust/.
| Crate | Pin | Owning crate | Note |
|---|---|---|---|
serde | 1.0.229 | all | derive, rc |
serde_json | 1.0.151 | pse-catalog, pse-relations, xtask | manifest and extension-metadata JSON codec; never a hash input and never relation data (ADR-0049) |
serde_arrow | 0.15.0 | pse-relations (adapters) | feature arrow-59; SchemaLike::from_type/from_samples banned on platform paths; any SERDE_ARROW:* field-metadata key is a contract violation (§5.3) |
syn / quote / proc-macro2 / prettyplease | 3.0.5 / 1.0.47 / 1.0.107 / 0.3.0 | pse-schema | syn 3, not the ecosystem’s syn 2; proc-macro2 span-locations; a prettyplease upgrade is a regeneration commit |
serde-saphyr | 1.2.0 | pse-authoring | replaces the unmaintained serde_yaml (0.9.34+deprecated, last release 2024-03): typed errors with line/column, hostile input refused without panics, a budget bound on nesting, aliases and allocation |
toml | 1.1.6 | pse-authoring | Spanned<T> supplies pse.source_span for package.toml |
winnow | 1.0.4 | pse-authoring | already in the graph via toml |
uuid | 1.26.1 | pse-authoring | v7 for explicit-policy entity ids; assign-ids only, P1 never mints one (ADR-0049) |
petgraph | 0.8.3 | pse-structural, pse-compiler | pre-1.0; SCC intra-component order is arbitrary and is sorted by semantic ID (§15.3); greedy_feedback_arc_set for tear search (§12.5) |
num-dual | 0.15.0 | pse-kernels, pse-numerics | every kernel body is written against the 0.15 trait shape (DualNum<Primitive = f64>); implicit_derivative* for implicit kernels (§18.2) |
faer | 0.24.4 | pse-numerics, pse-diagnostics | sparse LU, dense SVD, pseudoinverse_from_svd_with_tolerance, matrix_free (§15.4, §15.5) |
feos-core / feos | 0.10.1 — conditional | pse-kernels-ext | requires num-dual ^0.14, whose trait shape differs from 0.15; the provider is enabled only when its upstream tracks 0.15 (§9.8). Imports the quantity crate, which is never an authority for units |
diffsol | 0.16.2 — optional | pse-backend-native | carries its own linear algebra (diffsol-la/diffsol-nl); DiffSL never used (§13.6) |
egglog | 3.0.0 — optional, phase 4 | pse-mathir | three majors in ten months; never hash its Debug rendering (§5.3) |
blake3 | 1.8.7 | pse-ids (sole hasher) | derive_key contexts for every 128-bit ID (§5.1); rayon feature under the §18.8 budget |
rayon / tokio | 1.12.0 / 1.53.1 | pse-numerics, pse-compiler | one thread budget (§18.8) |
tracing | 0.1.44 | all | late fields pre-declared as field::Empty (§23.1) |
thiserror / miette | 2.0.20 / 7.6.0 | all / CLI only | miette::Result in the CLI and driver only; every pse-* crate returns concrete error enums |
| Ipopt C API | ≥ 3.14 | pse-backend-native (own -sys crate, bindgen output committed) | GetIpoptCurrentIterate/GetIpoptCurrentViolations (§18.3) |
Python. Every library pinned ==. Two interpreter ranges: the platform package targets CPython ≥ 3.11 (3.14 verified); the parity environment is 3.10–3.13 (3.13 verified) because idaes-pse classifies only those. The two are separate dependency sets, so what IDAES drags in (pydantic, sympy, networkx, pandas) cannot become a platform dependency.
| Library | Pin | Role |
|---|---|---|
pyarrow | 25.0.1 | capsule producer/consumer; the ten pse.* ExtensionType classes (§4.4) |
pyomo | 6.10.1 | generated backend (§21.2); pyomo.util.check_units; pyomo.contrib.* recorded per run (§21.3) |
pint | 0.26.1 | required by Pyomo’s units machinery; validates, never defines (§3.3, §21.2) |
attrs / cattrs | 26.1.0 / 26.2.0 | generated contract classes and strict structuring (§21.5) |
msgspec | 0.21.1 | the manifest and every JSON/TOML the Python package reads or writes (§21.5) |
numpy / scipy | 2.5.3 / 1.18.1 | array boundary (§21.6); test-time oracles only (§24.1) |
idaes-pse | 2.12.0 (parity environment only) | the parity reference; the vendored idaes-pse/ tree is a reading copy on the 2.10 line (§6.14) |
Supply chain. cargo deny (advisories, bans, maintenance) runs in CI in addition to cargo audit, because a deprecation encoded only in semver build metadata (serde_yaml’s) is invisible to cargo audit.
The vendored arrow-rs/ checkout in this repository is on the 60.0.0 development line and is a reading copy only. The API facts extracted under build/facts/arrow59-default and build/facts/df55-default are the pinned surfaces to code against.
3.2 Cargo workspace layout
Crate names use the placeholder prefix pse- (process systems engineering); the prefix is a naming decision to confirm, the boundaries are not.
Cargo.toml workspace; [workspace.dependencies] carries every pin of §3.1 once; Cargo.lock committed; CI asserts one version per family
crates/
pse-schema/ RelationSpec/LogicalType/ExtensionType registry; codegen (syn/quote/prettyplease)
pse-relations/ generated typed relation views, builders, validators, migrations (build output committed)
pse-ids/ SemanticId, ContentHash, ordinals, derived-ID rules (blake3 derive_key), canonical IPC serializer and hashing; the only crate that depends on blake3
pse-quantity/ dimensions, units, quantity kinds/types, bases, reference states, conversions
pse-material/ species, elements, phases, phase-species, reactions, stoichiometry, material systems
pse-authoring/ package/case document loader (serde-saphyr, toml), expression DSL parser (winnow), change sets
pse-mathir/ expression graph, operator catalog and contracts, canonicalization, unit inference
pse-templates/ template instantiation, feature guards, law templates, contributions
pse-rules/ RuleSpec → DataFusion LogicalPlan compiler; fixed-point executor; provenance rows
pse-catalog/ snapshot catalog: CatalogProviderList/SchemaProvider/TableProvider, artifact store
pse-compiler/ pass registry, PassSpec contracts, artifact-hash memo (§14.3), pipeline driver
pse-structural/ incidence, bipartite matching, Dulmage–Mendelsohn, SCC/BTD, DOF, tear selection
pse-kernels/ KernelSpec registry; ideal/cubic EOS, pure-component correlations, Henry, transport, reactions
pse-kernels-ext/ optional providers: CoolProp FFI, Helmholtz external libraries; FeOs when its upstream tracks num-dual 0.15
pse-numerics/ evaluation program, AD (per-equation reverse; num-dual for kernels), faer sparse LA
pse-plans/ discretization, scaling transformations, initialization plans, solver profiles
pse-backend-native/ Ipopt C-API FFI driver over the evaluation program
pse-backend-nl/ AMPL NL writer, SOL reader, external-function library exporter
pse-backend-pyomo/ bundle assembly for the Python adapter (schemas, ordering, source maps)
pse-diagnostics/ structural/numerical diagnostics as queries and native analyses; findings relations
pse-runtime/ run controller, case overlays, sweeps, result ingestion, lifecycle/cancellation
pse-py/ pyo3 + pyo3-arrow extension module (cdylib)
python/
pse/ Python package: authoring builders, Pyomo adapter, parity harness (attrs/cattrs contracts)
packages/
reference/ shipped reference packages: units, elements, species, property methods, unit templates
tests/
golden/ golden relation snapshots and fingerprints per vertical slice (data, not a crate)
governance/ workspace member crate pse-tests-governance (§24.1 governance layer)
engine/ workspace member crate pse-tests-engine
conformance/ workspace member crate pse-tests-conformance
lifecycle/ workspace member crate pse-tests-lifecycle
structural/ workspace member crate pse-tests-structural
crates/pse-ipopt-sys/ links = "ipopt"; build.rs emits link directives only; bindgen output committed (§18.3)
crates/pse-buildinfo/ lockfile hashes, rustc version, profile, git sha; feeds the manifest and pse.build_info()
xtask/ codegen, family-check, governance, doc-lint, probe-host, release — the logic behind the justfile
benches/ crate pse-benches: criterion, harness = false, one bench per §24.3 group
Decision: ADR-0038 — the five
tests/*crates,pse-ipopt-sys,pse-buildinfo,xtaskandbencheswere added to this layout at revision 4; the phase-0ipoptfeature is default-on withpse-ipopt-sysoptional.
Dependency direction is strictly downward in the list above within each layer; pse-mathir, pse-quantity, pse-material, and pse-ids depend on no engine crate; pse-catalog and pse-rules are the only crates that depend on DataFusion planning types; backends depend on compiled relation views only.
3.3 Supporting libraries and the boundary each must respect
Decision: ADR-0021, ADR-0022, ADR-0026, ADR-0037
| Library | Role (matches its use sites) | Boundary |
|---|---|---|
serde, serde_arrow | Schema-governed conversion between generated Rust records and Arrow batches at import/export boundaries (§20.5, §21) | Explicit schemas from the registry (SchemaLike for Vec<FieldRef>); from_type/from_samples banned by a governance grep; no SERDE_ARROW:* key may reach a relation schema |
syn, quote, proc_macro2, prettyplease | Code generation from RelationSpec into typed views/builders and the Python contract classes (non-macro pipeline, emitted source committed) | Generated code is derived; the registry is authoritative |
serde-saphyr, toml, winnow | Document loading (YAML/TOML, both with source spans) and expression DSL parsing | Parser output is authored relations; no evaluation during parsing; a parsing budget bounds hostile documents |
petgraph | Two use sites: the unit-level topology multigraph and tear search (§12.5); Tarjan SCC and condensation for block triangularization (§15.3) | Graph facts persist in Arrow; matching and Dulmage–Mendelsohn are platform code; node indices are never identity; intra-component order is sorted by semantic ID |
egglog (optional, phase 4) | Equality-saturation rewrites under explicit type/domain guards | Only rewrites justified by physical type, domain, and numerical policy; hand-written canonicalization (§7.4) stays complete on its own; adoption gated on cross-version extraction determinism |
num-dual | Forward-mode and hyper-dual derivatives for kernels written generically over DualNum; implicit_derivative* for implicit kernels | Kernel-local; never a dense dual vector over the whole problem; one trait shape (0.15) for every kernel body |
faer | Sparse LU for the structural and numerical diagnostics (§15.4), dense SVD and pseudoinverse_from_svd_with_tolerance, matrix_free operators with shift-invert for the smallest singular values (§15.5) | Per-diagnostic scratch built from compiled.incidence; not model truth; Par chosen by the §18.8 budget. The evaluation-program workspace and the KKT solves are not faer’s (§18.2, §18.3) |
feos-core/feos (conditional) | SAFT and cubic Helmholtz-energy providers; Helmholtz reference fluids are expression templates and a platform kernel, not FeOs (§9.8) | Capability providers under KernelSpec; enabled only when feos-core tracks num-dual 0.15; its quantity crate is never a units authority |
diffsol (optional) | Native DAE trajectory backend through residual/Jacobian callbacks | Lowered from the IR; no second model language (DiffSL unused); brings its own linear-algebra stack, accepted for the trajectory backend only |
blake3 | Content hashing and derived identity, owned by pse-ids | derive_key contexts; 16-byte XOF prefix for 128-bit IDs; the canonicalizer, not the hash, is what tests cover (§5.3) |
object_store | Artifact storage with atomic single-object writes and conditional updates (put_opts with PutMode) | Snapshot protocol is platform-owned (§20); no copy-then-delete sequence is a commit |
rayon, tokio | Batch kernel evaluation and pass parallelism; DataFusion runtime | One thread budget covering kernels, the solver, hashing and diagnostics (§18.8) |
tracing | Structured observability aligned to pass and run boundaries | Provenance is separate from observability (§23); DataFusion emits no spans of its own |
thiserror, miette | Typed failure taxonomy with source anchors | Failure classes are data (§23); miette::Result only in the CLI and driver |
Ipopt C API (own -sys crate) | In-process NLP solve | Called only by pse-backend-native; no panic or unwind crosses a callback (§18.3) |
Python: pyomo (+pint), pyarrow, attrs/cattrs, msgspec, numpy/scipy (tests) | Generated backend, Arrow boundary, generated typed contracts, manifest codec, array boundary | §21: a given contract lives in exactly one class system; pint validates and never defines; numpy never receives a nullable column |
Deferred with a stated trigger (not dependencies today): salsa — adopt only if sub-pass memoization inside P7 proves necessary after measurement (§14.3); LogicalPlan::Extension rule nodes — adopt when rule attribution inside EXPLAIN is required by the diff report or agent tooling (§14.2); datafusion-tracing/instrumented-object-store — when a release matching the pinned engine exists (§23.1); datafusion-ffi — only if §22’s extension model admits compiled third-party kernel packages.
Explicitly not added: a second dataframe engine, a graph database, a vector database, a second linear-algebra stack outside the trajectory backend, compile-time unit typing in kernels (uom was dropped: no kernel needs it — KernelSpec signatures are checked by P10’s unit inference and by the per-kernel parity suites — and its stock SI system cannot express the currency dimension), external-source TableProviders (a live database in the catalog is a second mutable authority), datafusion-spark/enable_ansi_mode (silently different arithmetic), Parquet virtual columns and input_file_name (position-based identity), FunctionFactory (SQL-defined kernels), AsyncScalarUDFImpl (I/O inside expression evaluation), parquet-variant (a typed hole in D1), parquet_derive/typed-arrow (they invert the registry→Rust direction), and any Python-side units library as an authority: pint is present because Pyomo’s units machinery is pint; it validates what the adapter emits, it never defines a quantity, and no relation is derived from a pint object (§21.2).
4. The semantic schema registry
4.1 What is declared once
Decision: ADR-0039, ADR-0040
A relation is declared exactly once as a RelationSpec. The registry itself is stored as relations in reference.schema_* so that the platform can query its own schema, diff versions, and generate migrations.
reference.schema_relations @1
relation_id : pse.semantic_id PK
namespace : pse.enum(Namespace) -- reference|authored|normalized|inferred|compiled|runtime|provenance
name : Utf8 -- e.g. "stoichiometry"
version : UInt32
authority : pse.enum(Authority) -- authored|reference|derived
snapshot_class : pse.enum(SnapshotClass) -- model|case|derived|sidecar; explicit membership (§5.3)
primary_key : List<Utf8> -- column names
derivation_granularity : pse.enum(DerivationGranularity) -- row|rule (§14.2 rule 4); required when authority = derived
stability : pse.enum(Stability) -- stable|evolving|internal
doc : Utf8
reference.schema_columns @1
relation_id : pse.semantic_id FK schema_relations
ordinal : UInt16
name : Utf8
logical_type_id : pse.semantic_id FK schema_logical_types
nullable : Boolean
quantity_type_id : pse.semantic_id [nullable] -- one quantity contract for the whole column
per_row_quantity : Boolean -- true ⇒ a sibling column named <name>_quantity_type_id exists
fk_relation_id : pse.semantic_id [nullable]
fk_column : Utf8 [nullable]
role : pse.enum(ColumnRole) -- key|reference|measure|label|payload|provenance
doc : Utf8
reference.schema_logical_types @1
logical_type_id : pse.semantic_id PK
name : Utf8 -- "semantic_id", "content_hash", "dimension_vector", "enum:MaterialBalanceType", ...
arrow_storage : Utf8 -- canonical Arrow DataType rendering
extension_name : Utf8 [nullable] -- ARROW:extension:name when an extension type
metadata_schema : Utf8 [nullable] -- JSON Schema for ARROW:extension:metadata
reference.schema_enums @1
enum_id : pse.semantic_id
member_ordinal : UInt16
member : Utf8
idaes_name : Utf8 [nullable] -- parity mapping (e.g. "componentPhase")
deprecated : Boolean
doc : Utf8
reference.schema_invariants @1
invariant_id : pse.semantic_id PK
relation_id : pse.semantic_id
kind : pse.enum(InvariantKind) -- unique|foreign_key|check|cardinality|domain|closure|acyclic
rule_id : pse.semantic_id -- the typed rule plan (reference.rule_plan_nodes, §6.11) that returns violating keys
severity : pse.enum(Severity) -- error|warning
doc : Utf8
reference.schema_migrations @1
relation_id, from_version, to_version, plan_spec, doc
A relation schema has one meaning and may occur at several immutable compiler stages. Producer ownership belongs to pass_output_ports (§6.11), not to one relation-wide producing_pass_id. Each output port has one producer; every compiled read names its source stage and port. Runtime operations own their result bundles under the same rule. P1 stages authored changes; compiler output ports cannot target authored or reference.
4.2 What is generated from it
Decision: ADR-0051
pse-schema runs as a build step (a non-macro syn/quote/prettyplease pipeline; emitted source is committed under pse-relations/src/generated/ and diffed in CI) and produces, for every relation:
| Generated artifact | Form | Consumer |
|---|---|---|
Arrow Schema with field and schema metadata (§4.3) | fn schema() -> SchemaRef | providers, writers, validators |
| Typed column view | struct StoichiometryView<'a> { reaction_id: &'a FixedSizeBinaryArray, ... } with try_from(&RecordBatch) that checks the schema fingerprint | passes, backends |
| Typed builder | StoichiometryBuilder with push(row: StoichiometryRow) and finish() -> RecordBatch | passes, authoring parser |
serde_arrow schema | SerdeArrowSchema derived from the registry (never inferred from samples) | import/export adapters, Python contracts |
| Validators | recursive field/batch admission plus one DataFusion LogicalPlan per relational invariant | P2, bundle/provider admission, semantic analyzer and output admission |
TableProvider | read-only provider over the snapshot artifact with exact key-filter pushdown and statistics | catalog |
| Documentation | markdown table per relation, cross-linked | docs/generated/ |
| Migrations | fn migrate_v1_to_v2(batch) -> batch | artifact loader |
| Authoring JSON Schema | for editor validation of package documents | tooling |
Two further artifacts are generated into committed trees: crates/pse-authoring/src/generated/documents.rs, the serde document structs the package loader parses into, generated from the registry’s DocumentSpec; and the docs/generated/ layout — README.md, relations/<ns>.md, enums.md, extension_types.md, passes.md, rules.md and schema/authoring.schema.json (ADR-0051).
Nothing hand-written may duplicate a column list. A hand-written struct that mirrors a relation is a governance failure (tests/governance/tests/no_shadow_structs.rs greps generated symbol names against hand-written code).
4.3 Metadata conventions
Decision: ADR-0039, ADR-0044, ADR-0050
Schema-level metadata keys (attached by the provider and preserved through IPC/Parquet):
pse.contract.id relation_id
pse.contract.version relation version
pse.contract.fingerprint blake3 of the RelationSpec rows
pse.namespace namespace
pse.snapshot_id snapshot the batch belongs to
pse.producer_pass_id pass that produced a derived relation
Field-level metadata keys:
pse.semantic.logical_type logical type name
pse.semantic.quantity_type quantity_type_id when the column carries one quantity contract
pse.semantic.role ColumnRole
pse.semantic.fk "<relation>.<column>"
pse.semantic.enum enum_id for dictionary-encoded enums
ARROW:extension:name for extension types (§4.4)
ARROW:extension:metadata the extension's serialized metadata, mandatory beside the name when the type has metadata
pse.* and ARROW:extension:* metadata carry declarations. Neither arbitrary semantic keys nor extension registration automatically validates an ordinary query. One generated recursive validator interprets storage, extension name/version/metadata, nullability, field roles and the declared quantity contract. DataFusion’s extension registry supplies factories that the validator explicitly calls; Arrow’s try_extension_type supplies the typed non-panicking field check. P2 and batch admission separately check row values, cross-field references, keys and foreign keys. Constraints are planner information, never proof of validation.
Generated constructors reject unknown metadata and produce the same key/value set for the same contract. Sorted insertion into a Rust HashMap does not stabilize its iteration order or protobuf encoding. Canonical hashing serializes an ordered metadata representation under §5.3; ordinary Arrow fields retain their normal maps. SERDE_ARROW:* and other unregistered keys are errors, not silently discarded. Only the explicitly named contextual keys in §5.3 are excluded from logical identity.
4.4 Extension types
Decision: ADR-0039
All extension types have a standard storage type so that an unaware consumer reads them safely.
| Extension name | Storage | Meaning |
|---|---|---|
pse.semantic_id | FixedSizeBinary(16) | 128-bit semantic identity (§5.1) |
pse.content_hash | FixedSizeBinary(32) | blake3 digest |
pse.dimension_vector | FixedSizeList<Struct<num: Int16, den: Int16>, 8> | rational exponents over (length, mass, time, temperature, amount, current, luminous_intensity, currency) |
pse.quantity_value | Struct<value: Float64, quantity_type_id: FixedSizeBinary(16), unit_id: FixedSizeBinary(16)> | a value in a heterogeneous column with explicit contract |
pse.bound | Struct<kind: Dictionary(Int8, Utf8), value: Float64> with kind ∈ {finite, unbounded} | explicit bound; never NaN or null sentinels |
pse.index_tuple | List<FixedSizeBinary(16)> | ordered domain member identities |
pse.ordinal_ref | UInt64 | artifact-local reference; metadata names the target relation |
pse.source_span | Struct<document_id: FixedSizeBinary(16), start: UInt32, end: UInt32> | authoring provenance |
pse.enum | Dictionary(Int32, Utf8) | closed enumeration; metadata carries enum_id; dictionary codes are presentation only |
pse.expr_dsl | Utf8 | authored expression text: the only authored form of an expression. Its parsed graph is a normalized.*_expr_* relation produced by P3 with a derivation to the source span (§7.7); no authored relation stores a parsed graph |
pse.target_path | Utf8 | an authored selector path naming instances, ports, symbols, or equations (fs.H101.inlet.flow_mol, fs.H101.inlet.*). P1 resolves it to entity identities at commit (case_spec_targets and kin, §6.10); the text is the serialization of those rows and is re-rendered by rename |
pse.enum metadata is {"v":1,"enum_id":"<32 hex>"} and pse.ordinal_ref metadata is {"v":1,"target_relation_id":"<32 hex>"}; every other type carries {"v":1}; a factory rejects any other shape.
Rust implementations use arrow_schema::extension::ExtensionType — NAME, typed Metadata, metadata, serialize_metadata, deserialize_metadata (which must error on unexpected metadata), supports_data_type, try_new, and the provided validate and try_new_from_field_metadata, which are the read path. The generated try_from(&RecordBatch) calls try_extension_type::<E>() on every extension column beside the schema-fingerprint check; Field::extension_type() (the panicking variant) is banned by a governance grep. Every type’s serialized metadata carries a v field, and the registration factory (below) accepts the metadata generations it knows and rejects the rest — that is the extension-type migration mechanism (DM-44, DM-51). One generated conformance test per type covers the metadata round trip, supports_data_type accept and reject, and reconstruction from a Field.
Engine registration and active admission. Every type in this table is registered in the DataFusion session’s ExtensionTypeRegistry at session construction (ExtensionTypeRegistration::new_arc(name, factory) on a MemoryExtensionTypeRegistry preloaded with the seven canonical arrow.* types), generated from this table; the generator asserts that no registration replaces an earlier one. Registration alone is passive: review E1 demonstrates that an ordinary SELECT can bypass its factories. The generated validator runs when a provider or bundle is admitted, on the complete logical plan before any rewrite can remove an invalid field, as the first and final platform AnalyzerRule around the ordered engine analyzers, after logical optimization before physical planning, and on every published output batch. Traversal includes subqueries (LogicalPlan::apply_with_subqueries), expressions and nested child fields. Rule expressions additionally use the platform’s quantity inference; an untyped computed quantity fails head admission (§14.2). A session exposes only these validated planning/execution entry points.
Native borrowing and Python ingestion invoke the same contract outside DataFusion (§18.2, §21). Metadata parsing can check the syntax/version of ordinal_ref.target_relation_id; only bundle admission can verify that the named target exists and every ordinal is in range. Unknown names/versions, malformed nested metadata, invalid values or incompatible quantities produce schema.* / rule.head_schema_mismatch before consumers assume validity. Structural Arrow validation is necessary and remains distinct from these semantic checks.
Rendering. One ArrayFormatterFactory (Arrow) that also serves DFExtensionType::create_array_formatter (DataFusion) renders every pse.* value meaningfully — a semantic ID as its short form and owner name, a bound as [lo, hi], a quantity value with its unit — in EXPLAIN, error messages and diagnostics, instead of raw storage bytes (§23.2).
Python. The platform package ships and registers the matching pyarrow.ExtensionType classes, generated from this table by the same generator, through an idempotent register-once helper (pyarrow.register_extension_type is process-global and raises on a second registration). An unregistered pse.* type in Python degrades silently to its storage type while ARROW:extension:name survives in the field metadata, so the contract layer asserts, per column, that every field claiming a pse.* extension resolved to a registered class (§21.1). The platform Rust admission layer errors on an unregistered name; an unaware Python consumer degrades — §21.1’s loss profile records that asymmetry.
4.5 Logical type catalog (physical scalars)
| Logical type | Arrow | Used for |
|---|---|---|
f64 | Float64 | all numerical values in compiled and runtime |
i64 | Int64 | counts, ordinals when signed needed |
u16/u32/u64 | UInt16/32/64 | opcodes, ordinals, positions |
bool | Boolean | flags |
text | Utf8 | names, docs, paths |
ts | Timestamp(Nanosecond, "UTC") | run and revision timestamps |
list<T>, struct{...} | List, Struct | variable-length payloads whose element semantics are uniform |
Dictionary encoding is applied at write time for enums and repeated labels; it never carries identity.
5. Identity, versions, snapshots, and the catalog
5.1 Three forms of identity
Decision: ADR-0045, ADR-0049, ADR-0050
| Form | Type | Assigned by | Survives |
|---|---|---|---|
| Semantic ID | pse.semantic_id | authoring (package entities), inference/compilation (derived entities) | revisions, reordering, re-batching, projection, publication |
| Artifact-local ordinal | UInt32/UInt64 | a compiled pass, deterministically | only within the artifact that defined it |
| Content hash | pse.content_hash | canonical serialization (§5.3) | forever; identifies an immutable version |
Authored entity IDs. Identity is assigned when an entity is created and is stored in the document that declares it (id: on every entity); it is never computed from the entity’s name, so a rename changes an attribute and nothing else. Two policies exist, declared per package (authored.packages.id_policy):
explicit(the default for authored packages): the authoring tool assigns a UUIDv7 when the entity is first written. P1 rejects an explicit-policy entity that lacks anid(parse.missing_id);pse authoring assign-idsinserts missing ones so that hand-written and agent-written documents stay valid.named(reference packages whose qualified names are the public contract: units, elements, constants, property kinds, the standard templates and methods):semantic_id = blake3_128("pse:named:v1" ‖ package_id ‖ qualified_name). Under this policy a rename is by definition a new entity; the old one is deprecated with areference.aliasesrow, andchange_ops.op = renameis rejected.
Both forms are 128-bit and opaque; the policy is recorded on the package, never in the ID. References between entities — template submodels, connections, case targets — are stored by identity (§6.10), so no authored fact depends on a name.
Derived entity IDs. Anything a pass creates has an ID computed from what it was created from, never from row position or numerical values:
symbol instance = blake3_128("pse:symbol:v1" ‖ instance_id ‖ symbol_decl_id ‖ index_tuple)
equation instance = blake3_128("pse:equation:v1" ‖ instance_id ‖ equation_decl_id ‖ index_tuple)
law-expanded term = blake3_128("pse:term:v1" ‖ law_instance_id ‖ contribution_id ‖ index_tuple)
connection equation = blake3_128("pse:conn:v1" ‖ connection_id ‖ member_ordinal ‖ index_tuple)
mesh node = blake3_128("pse:node:v1" ‖ domain_id ‖ policy_id ‖ node_ordinal)
discretized symbol = blake3_128("pse:symbol:v1" ‖ parent_symbol_id ‖ node_id)
blake3_128(context ‖ parts) denotes blake3::Hasher::new_derive_key(context), updated with each part length-prefixed, and the first 16 bytes of the extendable output — a defined 128-bit digest (BLAKE3 is an XOF), not a truncation of an unrelated hash. derive_key and a truncated plain hash produce different bytes, so this choice is frozen with the v1 context strings. pse-ids owns the blake3 dependency; no other crate hashes.
Consequence: re-running compilation on an unchanged snapshot reproduces identical IDs; changing mesh resolution changes only mesh-dependent IDs.
Ordinals. Passes assign ordinals by sorting on semantic ID (or on a declared stable order such as domain member ordinal) so that ordinals are a pure function of the snapshot. Ordinals are stored alongside semantic IDs in compiled.* relations; cross-artifact references always use the semantic ID.
5.2 Model revision, case revision, run
authored.model_revisions @1
model_revision_id : pse.semantic_id PK
parent_revision_id : pse.semantic_id [nullable]
snapshot_id : pse.content_hash -- of the authored+reference relations that define structure
created_at, author, message
authored.case_revisions @1
case_revision_id : pse.semantic_id PK
model_revision_id : pse.semantic_id FK
parent_case_id : pse.semantic_id [nullable] -- overlay chain (§6.10)
snapshot_id : pse.content_hash
created_at, author, message
runtime.runs @1 (see §6.13)
A change to a case never changes the model revision; a change to a template or connection creates a new model revision and invalidates compiled artifacts by dependency (§14.5), not wholesale.
5.3 Canonical serialization and hashing
Decision: ADR-0045, ADR-0030, ADR-0050
Logical content and physical encoding have different identities. pse-ids owns both hashing entry points. pse.canon.v2 is a new contract: v1’s ignored-null payload and stream/file ambiguity cannot be repaired while retaining its name. No implemented v2 canonicalizer is claimed by this document.
- Admit and order. Validate the complete relation against its registry contract. Sort unique primary keys with
arrow_row::RowConverterand explicitASC NULLS FIRST; a null or duplicate required key is invalid. Preserve list element order and named domain order; dictionary codes and record-batch boundaries carry no identity. - Normalize a hashing copy recursively. Decode dictionaries to values after checking key bounds and declared enum membership. Rebuild zero-offset, exactly sized arrays; null primitive/Boolean/fixed-binary slots have zero payload, null variable-width slots have empty payload with repeated offsets, and masked struct/fixed-list descendants have recursively zero/empty placeholders with their own validity forced valid. This recursion replaces all hidden descendants, including nested child validity; it does not preserve an arbitrary hidden null pattern. Canonical validity is omitted when all values are valid; otherwise unused bitmap bits are zero. Rebuild offsets and drop unreferenced child/storage capacity. Only registered logical types and their declared canonical storage are supported; an unknown Arrow layout is rejected, never hashed opportunistically.
- Preserve logical metadata separately. The canonical payload schema is generated from the declared schema with dictionaries decoded and all metadata removed. The original semantic metadata is a separate canonical metadata relation with columns
(field_path, key, value), all non-null Utf8, no metadata, sorted by UTF-8 byte order. Field paths are zero-based registry column/child ordinals rendered in ASCII decimal without leading zeros, joined by/(for example0/2); the empty path denotes the schema.(field_path, key)is unique. Keep every registered contract/semantic/extension key and its value; exclude onlypse.snapshot_idandpse.producer_pass_id. This preserves the declared dictionary/extension contract without attaching incompatible extension storage claims to the decoded hashing copy. Registry fingerprint, relation ID and schema version also enter the hash frame. Runtime maps are never presumed ordered. - Freeze the physical hashing representation. Write the metadata relation and the normalized data relation as two separately length-framed, finished Arrow IPC streams, each with one batch (including a zero-row batch),
IpcWriteOptions::try_new(64, false, MetadataVersion::V5)with its uncompressed default (compression = None); the compression mutator remains banned by the Rust rules. All padding bytes are zero. Every integer frame length is unsigned 64-bit little-endian; variable-length components have a preceding length, fixed-width IDs/hashes retain their declared width. The complete preimage is the framed version stringpse.canon.v2, relation ID, schema version (u32 little-endian), registry fingerprint, metadata stream and data stream. There is noDebug/display serialization. - Valid floats retain the ADR-0030 policy. Before writing the data stream, map valid NaNs to bits
0x7fc00000(Float32) or0x7ff8000000000000(Float64) and preserve valid-0.0; hidden null payload was already zeroed. Apply this only to the hashing copy. DataFusion grouping/distinct/hash joins collapse signed zero and treat NaN as self-equal, so rule keys remain non-floating (§14.2); analytics records that loss (§19.2). - Hash by role.
logical_hash = blake3(framed canonical preimage)identifies the logical relation under its contract.encoding_checksum = blake3(finished stored bytes)checks one IPC-file, Parquet or evidence object. Neither checksum is substituted for the other; the manifest records both and the encoding descriptor (§20.2). Serialization helpers share the sole hashing owner and an explicit format version. - Name membership without self-reference.
snapshot_idhashes apse.snapshot.v2frame: version string, registry fingerprint, snapshot kind, counted semantic-parent entries(role, snapshot_id)sorted by role, then counted(port, namespace, relation_id, schema_version, logical_hash)members sorted by that tuple’s first four fields. Text sorts by UTF-8 bytes, IDs by raw bytes and versions numerically. Strings/counts use the u64 little-endian framing above; schema versions use u32 and IDs/hashes retain fixed widths. Duplicate parent roles or member ports are rejected. Required empty relations appear explicitly.schema_relations.snapshot_classgenerates membership: structural authored/reference contracts aremodel; §6.10 case/observation relations arecase; compiler/runtime outputs arederived; revision catalogs, change logs, mutable refs, stage catalogs and pass-attempt/plan-diagnostic records aresidecar. Registry admission requires one class for every relation; no runtime namespace wildcard or implicit omission chooses membership. Model/case snapshots include every relation of their class in the admitted package/case scope, including empties; their port names arenamespace/followed by the relation ID as 32 lowercase hexadecimal digits. A member is a complete relation artifact; any row-selection transformation is declared before snapshot assembly. A case has amodelsemantic parent and separately named overlay parents. Stage/run membership comes from complete declared output ports and semantic derivations, with parent roles naming their input bindings; those declarations and execution contracts enter the dependency environment. Sidecars and containing manifests never enter their own membership. Derivationsnapshot_idnames an input snapshot. A stagebundle_idis this output snapshot ID, not a hash column in its own payload. Initial reuse requires equal complete inputs; identical relation content can share storage, but changed lineage does not automatically backdate a whole stage snapshot. - Bound construction before allocation. Phase 1 supports at most 1,000,000 rows and 256 MiB of normalized value, offset and validity buffers per relation, and every Utf8/List offset must fit i32. Deployments may lower these limits. Preflight uses checked arithmetic and reserves all concurrently live input, normalized-copy and IPC buffers under §14.3; exceeding a bound is
runtime.resource_limit, never partial publication. Raising the supported envelope or adopting deterministic fixed-batch streaming requires the R-23 measurement/format decision.
Equal declared logical content hashes identically across row/batch layout, null payload, dictionary encoding and supported file round trips. Renaming preserves entity IDs but changes relation hashes when label columns change. A changed reference convention changes its contract/reference relations and the dependent snapshot. Metamorphic fixtures must cover nested masked values, slices, all-valid/absent validity, empty relations and Parquet round trips; E4 refutes v1 assumptions and does not establish v2 correctness.
5.4 The snapshot catalog
Decision: ADR-0048
The catalog is a DataFusion CatalogProviderList whose catalogs are snapshots (named by snapshot_id, with mutable aliases such as head resolved to a snapshot at session creation), whose schemas are the seven namespaces, and whose tables are relations:
SELECT r.name, s.coefficient
FROM "<snapshot_id>".authored.stoichiometry s
JOIN "<snapshot_id>".authored.reactions r USING (reaction_id)
Provider contract (df §18, §51, §40A.5), stated against what DataFusion 55.1 was measured to do:
schema()is the generated schema; cheap and stable; a session pins a snapshot so schemas cannot change mid-query.SchemaProvider::table()is async by signature but resolves from the manifest loaded at session creation and performs no I/O; registration methods reject calls after the session is sealed.scan_with_argsis implemented;scandelegates. Projection is applied at read withSchema::projectso metadata travels with the projected fields.- Pushdown. Advertise
Exactonly for the registered, differentially tested predicate subset over key/enum columns; unsupported shapes returnUnsupported. Equality, conjunction, disjunction, null semantics and supportedInListforms are explicit contracts. A two-elementINbecoming anORin a probe is an observation, not a universal planner guarantee.supports_filters_pushdownis pure. Compile predicates using DataFusion physical expressions and evaluate with Arrowfilter_record_batch; do not write a second SQL predicate evaluator. Projection retains filter columns until evaluation; limits apply after all required exact/residual filtering. Candidate Parquet pruning may useProjectionMask,ArrowPredicate/RowFilteronly under the same completeness contract and measured R-24 trigger. The test oracle compares complete values and row multiplicities against an otherwise identicalUnsupportedprovider; an intentionally empty/over-pruning provider must fail. Rechecking returned rows alone cannot detect lost matches. - Statistics are returned with explicit
Precisionper value: row counts from the manifest areExact; min/max of key columns from the artifact footer areExactfor whole artifacts andInexactfor any filtered scan; everything not stored isAbsent.ScanArgs::statistics_requests()is answered selectively from cached metadata;SumandDistinctCountareAbsent.Statistics::new_unknownis the starting point. - Constraints.
Constraints::new_unverified([PrimaryKey])on every relation;Uniqueonly where P2 validates auniqueinvariant;Constraints::projectunder projection. DataFusion does not validate these — P2 does. - No mutation. None of
insert_into,delete_from,update,truncate,merge_into(all defaulted on the trait, and the set grows) is implemented; a governance test asserts the set of implementedTableProvidermethods.get_column_defaultis not implemented (defaults are authored facts and overlays).LogicalPlan::{Dml, Ddl, Copy, Statement}are never constructed. - Semantic admission. Provider construction validates schema and batches, and the platform plan entry points execute §4.4 validation. Registration supplies factories; it supplies no automatic universal query check.
- Configuration. Session settings are assembled through
ConfigOptions::set(typed errors);SessionConfig::set_strpanics on an invalid value and is banned by a governance grep. - The provider never prunes rows of a coupled mathematical problem for analytical convenience; analytical queries run over
runtime/compiledrelations, not over the problem the solver sees.
Artifacts behind providers are Arrow IPC files for compiled and runtime relations (hot path, zero-copy mmap) and Parquet for durable authored/reference/runtime history (§20).
6. The canonical relation families
This section is the schema catalog. Every relation is declared in the registry (§4) and appears here with its columns. Column types use the logical names of §4.4–§4.5; sid abbreviates pse.semantic_id, hash abbreviates pse.content_hash, enum(X) abbreviates pse.enum with enum X. [n] marks nullable. Enumerations are listed once, in §6.14.
6.1 Identity and packages (authored, reference)
authored.packages @1
package_id : sid PK · name : text · version : text (semver) · kind : enum(PackageKind)
id_policy : enum(IdPolicy) -- explicit|named (§5.1)
dependencies : list<struct<package_id: sid, version_req: text>> · content_hash : hash · doc : text
authored.documents @1
document_id : sid PK · package_id : sid · path : text · content_hash : hash
authored.entities @1
entity_id : sid PK · package_id : sid · kind : enum(EntityKind) · name : text · qualified_name : text -- attributes, never identity
parent_entity_id : sid [n] -- containment (flowsheet ⊃ unit ⊃ control volume ⊃ state block)
source_span : pse.source_span [n]
reference.aliases @1 -- deprecated qualified names of named-policy entities (§5.1)
alias_id : sid PK · entity_id : sid · old_qualified_name : text · deprecated_in : text (package version)
authored.model_revisions @1, authored.case_revisions @1 (§5.2)
normalized.package_graph @1 -- P0 output; exact version requirements only in phase 0
package_id : sid PK · version : text · content_hash : hash · depth : u16
dependency_package_ids : list<sid> · derivation_id : sid
Every other authored relation’s primary key is also present in authored.entities; the invariant closure:entity_registered enforces it.
6.2 Physical types (reference, extendable by packages)
Decision: ADR-0039
reference.dimensions @1
ordinal : u16 PK · name : text -- length, mass, time, temperature, amount, current, luminous_intensity, currency
reference.units @1
unit_id : sid PK · symbol : text · name : text · dimension : pse.dimension_vector
scale_to_canonical : f64 · offset_to_canonical : f64 · is_affine : bool · system : text · doc : text
-- canonical units are SI base units; USD_<year> units have dimension currency and scale by CE index (§19.5)
reference.unit_sets @1
unit_set_id : sid PK · name : text
time_unit_id, length_unit_id, mass_unit_id, amount_unit_id, temperature_unit_id,
current_unit_id [n], luminous_intensity_unit_id [n], currency_unit_id [n] : sid
-- IDAES UnitSet; derived units (pressure, energy, ...) are computed, not stored
reference.quantity_kinds @1
quantity_kind_id : sid PK · name : text · dimension : pse.dimension_vector
extensive : bool · addition_kind : enum(QuantityAdditionKind) -- additive|origin_sensitive
doc : text
-- e.g. molar_flow, mass_flow, pressure, temperature, molar_enthalpy, molar_entropy,
-- molar_density, mass_density, mole_fraction, phase_fraction, area, volume, length, heat_duty, work, ...
reference.bases @1
basis_id : sid PK · kind : enum(BasisKind) -- molar|mass|volume|energy|standard_volume|dimensionless
composition_basis : enum(CompositionBasis) [n] -- mole_fraction|mass_fraction|volume_fraction|molality|molarity
rate_basis : enum(RateBasis) [n]
reference_conditions_id : sid [n]
reference.reference_states @1
reference_state_id : sid PK · kind : enum(ReferenceStateKind)
-- elemental_at_conditions | compound_at_conditions | ideal_gas_at_conditions | custom
temperature : f64 [n] (K) · pressure : f64 [n] (Pa) · include_enthalpy_of_formation : bool · phase_id : sid [n] · doc : text
reference.quantity_types @1
quantity_type_id : sid PK · quantity_kind_id : sid · basis_id : sid [n]
reference_state_id : sid [n] · scale_kind : enum(ScaleKind) -- point|difference (§8.1); gauge pressure is a point quantity whose reference state names the datum
shape : list<enum(DomainKind)> -- e.g. [phase, species] for x_{p,j}
subject_kind : enum(SubjectKind) [n] -- species|phase|element|reaction|none
canonical_unit_id : sid · nominal_magnitude : f64 [n] -- default scaling hint (§16.2)
doc : text
reference.conversion_rules @1
conversion_id : sid PK · from_quantity_type_id : sid · to_quantity_type_id : sid
kind : enum(ConversionKind) -- scale|affine|kernel
kernel_id : sid [n] -- e.g. molar↔mass via molecular weight
required_parameters : list<text> -- e.g. ["mw"]
reference.quantity_operations @1 -- physical composition policy, distinct from dimension arithmetic (§8.3)
operation_id : sid PK · opcode : enum(Opcode)
input_kind_ids : list<sid> · result_kind_id : sid
basis_rule : enum(BasisRule) -- preserve|require_equal|registered_conversion|cancel|declared_result
reference_rule : enum(ReferenceRule) -- preserve|require_equal|registered_conversion|cancel|declared_result
scale_rule : enum(QuantityScaleRule) -- preserve|point|difference|addition|weighted_mean
shape_rule : enum(QuantityShapeRule) -- scalar|preserve|same_indices|explicit_broadcast|reduce_bound_index
basis_source : u16 [n] · reference_source : u16 [n] · scale_source : u16 [n] · shape_source : u16 [n]
subject_rule : enum(SubjectRule) -- preserve|require_equal|declared_result
subject_source : u16 [n] · result_subject_kind : enum(SubjectKind) [n]
result_basis_id : sid [n] · result_reference_state_id : sid [n]
input_conversions : list<struct<operand: u16, conversion_id: sid>> · precondition_invariant_ids : list<sid>
-- *_source is an explicit zero-based operand ordinal, required for preserve; bounds are validated.
-- require_equal checks all relevant operands and returns that common component; cancel requires
-- matching non-absent tags under its invariant and returns no tag. declared_result uses only the
-- explicit result field and a required invariant. Per-operand conversions occur once before matching.
-- Every kind-changing operation resolves one rule; absent/ambiguous rules fail. No dimension-only fallback.
reference.constants @1
constant_id : sid PK · name : text · idaes_name : text [n] · value : f64 · unit_id : sid · quantity_kind_id : sid · doc : text
-- gas_constant R, boltzmann k_B, avogadro N_A, faraday F, gravity g, planck h, speed_of_light c, stefan_boltzmann σ, ...
6.3 Domains, index sets, coordinates
authored.domains @1
domain_id : sid PK · owner_entity_id : sid · kind : enum(DomainKind)
-- time|length|species|phase|phase_species|element|reaction|port_set|stage|cell|face|node|custom
continuous : bool · unit_id : sid [n] · parent_domain_id : sid [n] · doc : text
authored.domain_members @1
domain_id : sid · member_id : sid PK · ordinal : u32 · label : text
coordinate : f64 [n] -- for discrete points of a continuous domain
ref_entity_id : sid [n] -- species_id, phase_id, reaction_id, ...
authored.continuous_domains @1
domain_id : sid PK · lower : f64 · upper : f64 · unit_id : sid
initial_points : list<f64> -- authored breakpoints (IDAES time_set / length_domain)
discretization_policy_id : sid [n] -- FK authored.discretization_policies; cases may override (case_policies)
normalized.domain_products @1
product_id : sid PK · domain_ids : list<sid> -- ordered factor domains of a symbol shape
inferred.valid_index_tuples @1
product_id : sid · tuple : pse.index_tuple PK · derivation_id : sid
-- e.g. valid (phase, species) pairs from phase_species; valid (time, length) grid after discretization
compiled.meshes @1
mesh_id : sid PK · domain_id : sid · policy_id : sid · node_count : u32 · nodes : list<f64>
compiled.mesh_nodes @1
node_id : sid PK · mesh_id : sid · ordinal : u32 · coordinate : f64 · kind : enum(NodeKind) -- element_boundary|collocation|interior
compiled.stencils @1
stencil_id : sid PK · mesh_id : sid · derivative_order : u8 · scheme : enum(DiscretizationScheme)
node_id : sid · neighbor_node_id : sid · weight : f64
compiled.quadrature_rules @1
rule_id : sid PK · mesh_id : sid · node_id : sid · weight : f64
6.4 Material systems (authored; elements are reference)
reference.elements @1
element_id : sid PK · symbol : text · name : text · atomic_mass : f64 (kg/mol)
authored.species @1
species_id : sid PK · package_id : sid · name : text · formula : text [n]
mw : f64 [n] (kg/mol) · component_type : enum(ComponentType)
-- Component|Solute|Solvent|Ion|Anion|Cation|Apparent (a platform enum mirroring the IDAES class hierarchy)
charge : i64 · dissociation_species : list<struct<species_id: sid, coefficient: f64>> [n] -- ions / apparent species
valid_phase_types : list<enum(PhaseType)> -- IDAES valid_phase_types
doc : text
authored.species_elements @1
species_id : sid · element_id : sid · count : f64 -- elemental_composition
authored.phases @1
phase_id : sid PK · package_id : sid · name : text · phase_type : enum(PhaseType) -- liquid|vapor|solid|aqueous|undefined
is_solvent_phase : bool · doc : text
authored.phase_species @1 -- explicit restriction (IDAES per-phase component_list); optional
phase_id : sid · species_id : sid
authored.henry_declarations @1
species_id : sid · phase_id : sid · henry_type : enum(HenryType) -- Kpx|Kpc|Hxp|Hcp
method_id : sid -- reference.method_specs (ConstantH, ...)
authored.material_systems @1
material_system_id : sid PK · package_id : sid · name : text
species_ids : list<sid> · phase_ids : list<sid> · doc : text
inferred.phase_species @1 -- closure: species valid in phase (valid_phase_types ∩ phase_type, restricted by explicit lists)
material_system_id : sid · phase_id : sid · species_id : sid · henry : bool · derivation_id : sid
authored.reactions @1
reaction_id : sid PK · package_id : sid · name : text
kind : enum(ReactionKind) -- rate|equilibrium|inherent
basis : enum(BasisKind) -- molar|mass (reaction_basis)
concentration_form : enum(ConcentrationForm) [n] -- molarity|molality|moleFraction|massFraction|partialPressure|activity
reaction_phase_id : sid [n] · doc : text
authored.stoichiometry @1 -- one row per (reaction, phase, species) with a non-zero coefficient; zero rows are not stored
reaction_id : sid · phase_id : sid · species_id : sid · coefficient : f64
-- invariant closure:stoichiometry_species_in_phase ; element balance check is a diagnostic, not an invariant (IDAES allows unbalanced authored data)
authored.reaction_methods @1
reaction_id : sid PK · rate_form_method_id : sid [n] · rate_constant_method_id : sid [n]
equilibrium_form_method_id : sid [n] · equilibrium_constant_method_id : sid [n] · heat_of_reaction_method_id : sid [n]
authored.reaction_packages @1
reaction_package_id : sid PK · package_id : sid · property_package_id : sid · reaction_ids : list<sid>
unit_set_id : sid · default_arguments : list<struct<key: text, value: text>> · doc : text
authored.parameter_values @1 -- parameter_data for species, phases, reactions, packages, methods
owner_entity_id : sid · parameter_kind : text -- method parameter name, e.g. "cp_mol_ig_comp_coeff"
index : pse.index_tuple -- e.g. ["A"] or [phase, species]
value : f64 · unit_id : sid · source : text [n] · std_dev : f64 [n] · estimable : bool
6.5 Property capability registry (reference, extended by packages)
reference.property_kinds @1
property_kind_id : sid PK · idaes_name : text -- "enth_mol_phase", "dens_mass", "fug_phase_comp", ...
quantity_kind_id : sid · basis_id : sid [n] · shape : list<enum(DomainKind)>
category : enum(PropertyCategory) -- state|thermo|transport|reaction|derived
doc : text
reference.method_specs @1 -- a submodel that a package can select (IDAES "method" objects and EOS/state-definition modules)
method_id : sid PK · family : enum(MethodFamily) · name : text · version : text
-- MethodFamily: state_definition|eos|pure_component|phase_equilibrium_form|phase_equilibrium_state|bubble_dew|henry|
-- transport_mixing|reaction_rate_form|rate_constant|equilibrium_form|equilibrium_constant|heat_of_reaction|
-- enthalpy_transport|custom
provides : list<sid> -- property_kind_ids
requires : list<sid> -- property_kind_ids or state symbols needed
parameter_kinds : list<struct<name: text, quantity_kind_id: sid, indexed_by: list<enum(DomainKind)>, required: bool>>
realization : enum(MethodRealization) -- equation_template|kernel
template_id : sid [n] · kernel_id : sid [n]
validity : list<struct<input: text, lower: pse.bound, upper: pse.bound>>
doc : text
authored.property_packages @1 -- IDAES GenericParameterBlock configuration, typed
property_package_id : sid PK · package_id : sid · material_system_id : sid · unit_set_id : sid
state_definition_method_id : sid -- FTPx|FcTP|FpcTP|FPhx|FcPh|...
temperature_ref : f64 (K) · pressure_ref : f64 (Pa) · include_enthalpy_of_formation : bool
bubble_dew_method_id : sid [n] · doc : text
authored.state_bounds @1
property_package_id : sid · state_symbol : text · lower : pse.bound · initial : f64 · upper : pse.bound · unit_id : sid
authored.phase_equilibrium_pairs @1
property_package_id : sid · phase_a_id : sid · phase_b_id : sid
state_method_id : sid -- SmoothVLE|SmoothVLE2 (CubicComplementarityVLE)|...
form_method_id : sid -- fugacity|log_fugacity per pair (may be overridden per species)
authored.method_selections @1 -- "which submodel computes what, for which scope"
property_package_id : sid · scope_kind : enum(ScopeKind) -- package|phase|species|phase_species|reaction
scope_ids : pse.index_tuple · property_kind_id : sid [n] · family : enum(MethodFamily)
method_id : sid · options : list<struct<key: text, value: text>>
authored.default_scaling @1
property_package_id : sid · property_kind_id : sid · index : pse.index_tuple · scaling_factor : f64
inferred.property_requirements @1 -- explicit demand closure (§9.6)
requirement_id : sid PK · state_scope_id : sid · property_kind_id : sid · index : pse.index_tuple
requested_by : sid -- equation_id or requirement_id (transitive)
derivation_id : sid
inferred.method_resolutions @1
requirement_id : sid PK · method_id : sid · realization : enum(MethodRealization)
template_id : sid [n] · kernel_binding_id : sid [n] · status : enum(ResolutionStatus) -- resolved|unresolved|ambiguous
derivation_id : sid
6.6 Templates (authored; reference packages ship the standard library)
A template is the declarative replacement for an IDAES *Data class with CONFIG and build(). One template kind covers unit models, control volumes, state blocks, reaction blocks, connection rules, costing methods, laws, initializers, and scalers.
authored.templates @1
template_id : sid PK · package_id : sid · name : text · version : text · kind : enum(TemplateKind)
-- unit|control_volume|state_block|reaction_block|connection_rule|costing_method|law|initializer|scaler|flowsheet|helper
default_initializer_template_id : sid [n] · default_scaler_template_id : sid [n]
idaes_class : text [n] -- parity mapping, e.g. "idaes.models.unit_models.Heater"
doc : text
authored.template_params @1 -- CONFIG options
template_id : sid · name : text · logical_type_id : sid · enum_id : sid [n]
default : text [n] · required : bool · domain_spec : text [n] -- validator (e.g. "is_property_package", "positive")
doc : text
authored.template_features @1 -- typed choice structures (dynamic, has_holdup, has_pressure_change, thermodynamic_assumption, ...)
template_id : sid · name : text · kind : enum(FeatureKind) -- bool|enum|choice
enum_id : sid [n] · default : text [n] · inherit_from : text [n] -- "parent.dynamic" style inheritance
doc : text
authored.template_feature_rules @1
template_id : sid · rule : enum(FeatureRuleKind) -- implies|excludes|requires
antecedent : text · consequent : text -- predicates over features/params ("dynamic=true" ⇒ "has_holdup=true")
authored.template_guards @1
guard_id : sid PK · template_id : sid · predicate : pse.expr_dsl · doc : text
authored.template_domains @1 -- local index sets (length_domain, inlet_list, outlet_list, element lists, ...)
template_id : sid · name : text · kind : enum(DomainKind) · continuous : bool
members_from : text [n] -- param that supplies members ("inlet_list") or bound package domain ("phase_list")
bounds : struct<lower: f64, upper: f64> [n] · unit_id : sid [n]
authored.template_symbols @1 -- Var / Param / Expression / DerivativeVar declarations
template_id : sid · symbol_decl_id : sid PK · name : text · role : enum(SymbolRole) -- variable|parameter|expression|derivative|reference
quantity_type_id : sid · indexed_by : list<text> -- template domain names ("time", "phase", "species", "length")
default_lower : pse.bound [n] · default_upper : pse.bound [n] · default_initial : f64 [n]
reference_to : text [n] -- for role=reference: path to aliased symbol (IDAES Reference)
wrt_domain : text [n] -- for role=derivative
guard_id : sid [n] · idaes_name : text [n] · doc : text
authored.template_equations @1
template_id : sid · equation_decl_id : sid PK · name : text
indexed_by : list<text> · filter : pse.expr_dsl [n] -- e.g. "j in phase_species[p]"
expression : pse.expr_dsl · sense : enum(Sense) · family_hint : enum(EquationFamily) [n] · role_hint : enum(EquationRole) [n]
guard_id : sid [n] · idaes_name : text [n] · doc : text
-- the text is the authored fact. Its parsed graph is normalized.template_expr_* (P3; same schema as compiled.math_* with
-- template-scoped references; every node carries a derivation to the source span). No authored relation stores a parsed graph.
authored.template_submodels @1 -- composition (Heater ⊃ control_volume ⊃ properties_in/out ⊃ ...)
template_id : sid · name : text · child_template_id : sid [n] -- null ⇒ resolved from a param (property_package → its state_block template)
child_from_param : text [n] · multiplicity_domain : text [n] -- indexed submodels (state blocks over time × length)
bindings : list<struct<child_param: text, value: pse.expr_dsl>> -- e.g. dynamic ← parent.dynamic, has_phase_equilibrium ← ...
guard_id : sid [n]
authored.template_ports @1
template_id : sid · name : text · kind : enum(PortKind) -- material|heat|work|signal
direction : enum(Direction) · bound_to : text -- submodel path whose state defines members ("control_volume.properties_in")
guard_id : sid [n] · doc : text
authored.template_contributions @1 -- declared phenomena (heat, work, mass transfer, pressure change, reaction, custom terms)
template_id : sid · contribution_decl_id : sid PK · name : text
law_family : enum(LawFamily) -- material|energy|momentum|element|charge|cost|utility
subject : pse.expr_dsl [n] -- selector: species j, phase p, element e, "total"
expression : pse.expr_dsl · orientation : enum(Orientation) -- into_scope|out_of_scope|generation|accumulation
scope : text -- control region / submodel path
guard_id : sid [n] · doc : text
authored.template_law_instances @1
template_id : sid · law_instance_decl_id : sid PK · law_template_id : sid
scope : text · subject_selector : pse.expr_dsl · options : list<struct<key: text, value: text>> -- balance_type, ...
guard_id : sid [n]
authored.template_requirements @1 -- capabilities the bound packages must provide
template_id : sid · requirement : enum(CapabilityRequirement) · detail : text [n]
-- material_flow_terms|enthalpy_flow_terms|material_density_terms|energy_density_terms|diffusion_terms|reaction_rate_basis|phase_equilibrium|...
authored.template_display @1 -- _get_performance_contents, define_display_vars, stream-table membership
template_id : sid · kind : enum(DisplayKind) · label : text · expression : pse.expr_dsl · display_unit_id : sid [n] · format : text [n]
Non-expression template consumers (initializer stages, opaque law operations and display adapters) declare required properties once:
authored.template_property_requirements @1
requirement_id : sid PK · template_id : sid · operation_id : sid
scope_selector_id : sid · property_kind_id : sid · guard : pse.expr_dsl [n]
index_domain_bindings : list<struct<index_name: text, domain_id: sid>>
normalized.property_demand_seeds @1
seed_id : sid PK · source_id : sid · source_kind : enum(DemandSource)
scope_id : sid · property_kind_id : sid · index : pse.index_tuple
guard_node_id : u64 [n] · derivation_id : sid
P3 derives expression demands directly from its parsed graphs and normalizes these opaque-operation declarations into the same seed schema. P4/P5 decide guards and bind scope/index identities before P6. The seed set contains no P7 equation or P15 stage identifier.
6.7 Instances, flowsheets, connectivity
authored.instances @1
instance_id : sid PK · parent_instance_id : sid [n] · template_id : sid · name : text
param_values : list<struct<name: text, value: text>> · feature_values : list<struct<name: text, value: text>>
property_package_id : sid [n] · reaction_package_id : sid [n] · doc : text
authored.flowsheets @1
instance_id : sid PK · time_domain_id : sid · dynamic : enum(TriState) -- true|false|inherit
default_property_package_id : sid [n]
authored.scopes @1 -- intensional selectors (metamodel P4); the selector is a typed tree, not text
scope_id : sid PK · root_term_id : sid
authored.selector_terms @1
term_id : sid PK · scope_id : sid · parent_term_id : sid [n] · ordinal : u16
op : enum(SelectorOp) -- descendant_of|kind_is|tagged_with|union|intersection|difference|include|exclude
entity_id : sid [n] · entity_kind : enum(EntityKind) [n] · tag : text [n]
authored.connections @1 -- Arcs
connection_id : sid PK · from_port_id : sid · to_port_id : sid · rule_template_id : sid -- default: equality connection rule
tear_cost : f64 [n] -- tear policy for sequential initialization (§12.5); null ⇒ tearable at unit cost
doc : text
inferred.instance_tree @1 -- transitive containment
ancestor_id : sid · descendant_id : sid · depth : u16 · derivation_id : sid
inferred.instance_features @1 -- resolved features after inheritance and rules (dynamic ⇒ has_holdup, useDefault resolution)
instance_id : sid · name : text · value : text · derivation_id : sid
inferred.instances @1 -- expanded submodel instances (control volumes, state blocks per (t,x), reaction blocks)
instance_id : sid PK · parent_instance_id : sid · template_id : sid · path : text · index : pse.index_tuple · derivation_id : sid
inferred.ports @1
port_id : sid PK · instance_id : sid · name : text · kind : enum(PortKind) · direction : enum(Direction) · state_instance_id : sid
inferred.port_members @1
port_id : sid · ordinal : u16 · symbol_group : text -- "flow_mol", "mole_frac_comp", "temperature", "pressure"
symbol_decl_id : sid · quantity_type_id : sid · derivation_id : sid
inferred.scope_members @1 -- scope_id, entity_id, derivation_id
inferred.boundary_crossings @1 -- scope_id, connection_id, classification : enum(Crossing) (internal|external|inbound|outbound), derivation_id
inferred.topology_edges @1 -- unit-level directed graph: from_instance_id, to_instance_id, connection_id
inferred.tear_candidates @1 -- cycle-breaking edges for sequential initialization (§17.4)
inferred.undecided @1 -- rule outcomes that are unknown or in conflict (§14.2 rule 3); head relations hold only decided-true rows
undecided_id : sid PK · rule_id : sid · head_relation_id : sid · key : pse.index_tuple · truth : enum(TruthValue) -- unknown|conflict
reason : text · supporting : list<struct<relation_id: sid, row_key: text>> · derivation_id : sid
6.8 Symbols (compiled)
compiled.symbols @1
symbol_id : sid PK · ordinal : u64 · owner_instance_id : sid · symbol_decl_id : sid
qualified_name : text -- fs.H101.control_volume.properties_out[0.0].temperature
index : pse.index_tuple · quantity_type_id : sid · unit_id : sid -- canonical unit of the package unit set
role : enum(SymbolRole) · solver_type : enum(SolverVariableType) -- continuous|binary|integer
semantic_role : enum(VariableSemanticRole) -- state|design_capacity|allocation|slack|aux_reformulation|reporting_only|...
lifecycle : enum(VariableLifecycle) -- authored|generated_discretization|generated_reformulation|generated_relaxation|runtime_artifact
default_lower : pse.bound · default_upper : pse.bound · default_initial : f64 [n]
derivation_id : sid
compiled.symbol_references @1 -- alias links (IDAES Reference / VarLikeExpression)
alias_symbol_id : sid · target_symbol_id : sid · kind : enum(AliasKind) -- reference|display_alias
compiled.symbol_groups @1 -- named indexed families for ports, display, and lowering (flow_mol_phase_comp[p,j] over its product)
group_id : sid PK · owner_instance_id : sid · name : text · product_id : sid
compiled.symbol_group_members @1 -- group_id, tuple, symbol_id
6.9 Mathematics (compiled.math_*)
compiled.math_expr_nodes @1
node_id : u64 PK · opcode : enum(Opcode) · quantity_type_id : sid [n] -- null until unit inference has run; never null after P10
scope_instance_id : sid [n] · subtree_hash : hash -- hash-consing key (§7.4)
compiled.math_expr_args @1
parent_node_id : u64 · argument_ordinal : u16 · child_node_id : u64
-- typed operator payloads (one relation per operator family that needs data beyond children)
compiled.math_symbol_refs @1 node_id, symbol_id
compiled.math_float_constants @1 node_id, value : f64, unit_id : sid
compiled.math_int_constants @1 node_id, value : i64
compiled.math_affine @1 node_id, constant : f64, terms : list<struct<coefficient: f64, child_node_id: u64>> -- ordered; children need not be linear
compiled.math_weighted_means @1 node_id, pairs : list<struct<weight_node_id: u64, value_node_id: u64>>,
normalization : enum(WeightNormalization) (divide_by_sum|certified_unit_sum), unit_sum_invariant_id : sid [n]
compiled.math_reductions @1 node_id, kind : enum(ReductionKind) (sum|prod|min|max), domain_id : sid,
bound_index_id : sid, filter_node_id : u64 [n]
compiled.math_gathers @1 node_id, group_id : sid, coordinate_map : list<struct<bound_index_id: sid, position: u16>>
compiled.math_broadcasts @1 node_id, domain_id : sid, bound_index_id : sid
compiled.math_derivatives @1 node_id, wrt_domain_id : sid, order : u8
compiled.math_integrals @1 node_id, domain_id : sid, quadrature_policy_id : sid [n]
compiled.math_smooth_ops @1 node_id, eps : f64 -- smooth_max, smooth_min, smooth_abs, safe_sqrt, safe_log
compiled.math_conditionals @1 node_id, guard_node_id : u64 -- piecewise with explicit guard; lowered per backend (§7.2)
compiled.math_kernel_calls @1 node_id, kernel_binding_id : sid, output_ordinal : u16
compiled.math_implicit_refs @1 node_id, implicit_system_id : sid, unknown_ordinal : u16
compiled.math_unit_converts @1 node_id, scale : f64, offset : f64, from_unit_id : sid, to_unit_id : sid
compiled.math_piecewise_linear @1 node_id, breakpoints : list<struct<x: f64, y: f64>>,
input_quantity_type_id : sid, output_quantity_type_id : sid
compiled.math_quantity_selections @1 -- the P10 derivation of the §8.3 selection made at each node
node_id : u64 · operation_id : sid [n] · builtin_rule : enum(BuiltinQuantityRule) [n]
operand_permutation : list<u16> · conversions : list<struct<operand: u16, conversion_id: sid>>
deferred_static_check : bool
compiled.math_indexed_equations @1 -- one row per (instance, equation declaration): the indexed form produced by P7–P9 and consumed by P12
indexed_equation_id : sid PK · owner_instance_id : sid · equation_decl_id : sid [n] · qualified_name : text
product_id : sid [n] · filter_node_id : u64 [n] · body_node_id : u64 · sense : enum(Sense) · lower_node_id : u64 [n] · upper_node_id : u64 [n]
law_instance_id : sid [n] · derivation_id : sid
compiled.math_free_indices @1 -- the free indices of an indexed equation; P12 binds them to domain members
indexed_equation_id : sid · bound_index_id : sid · domain_id : sid · position : u16
compiled.math_equations @1 -- scalar: one row per valid index tuple, produced by P12
equation_id : sid PK · ordinal : u64 · owner_instance_id : sid · equation_decl_id : sid [n] · parent_indexed_equation_id : sid [n]
qualified_name : text · index : pse.index_tuple
body_node_id : u64 · sense : enum(Sense) -- eq|le|ge|range|definition
lower_node_id : u64 [n] · upper_node_id : u64 [n]
residual_quantity_type_id : sid
family : enum(EquationFamily) · role : enum(EquationRole) -- semantic_math_basis §1.2, §1.3
differentiability : enum(Differentiability) · convexity : enum(Convexity) · monotonicity : enum(Monotonicity)
default_active : bool · group_id : sid [n]
law_instance_id : sid [n] · derivation_id : sid
compiled.math_objectives @1
objective_id : sid PK · owner_instance_id : sid · body_node_id : u64 · sense : enum(ObjectiveSense) · quantity_type_id : sid · default_active : bool
compiled.math_implicit_systems @1
implicit_system_id : sid PK · unknown_symbol_ids : list<sid> · equation_ids : list<sid>
branch_policy : text · kernel_binding_id : sid [n] -- when encapsulated as an implicit kernel
compiled.math_complementarity @1
pair_id : sid PK · expr_a_node_id : u64 · expr_b_node_id : u64 · formulation : enum(ComplementarityForm) -- smooth_eps|binary|sos1
compiled.math_alternative_sets @1 / math_alternatives @1 -- typed optionality (mode, installed/absent, technology choice) for GDP lowering
compiled.math_dae_links @1 derivative_symbol_id : sid, state_symbol_id : sid, wrt_domain_id : sid
compiled.math_static_analysis @1 -- per-equation and per-variable static attributes (§18.7), computed by P14 on the case-bound view (§7.5)
... · parameter_dependence : list<sid> -- fixed/parameter symbols whose substituted values the classification used
6.10 Cases, specifications, observations (authored)
Decision: ADR-0048
authored.cases @1
case_id : sid PK · model_revision_id : sid · name : text · parent_case_id : sid [n] -- overlay chain; child overrides parent
kind : enum(CaseKind) -- base|overlay|initialization_stage|scenario|sweep_sample
doc : text
authored.case_specs @1
spec_id : sid PK · case_id : sid · target : pse.target_path -- serialization of case_spec_targets (identity-based); re-rendered by rename
treatment : enum(Treatment) [n] -- fixed|free|parameter
value : f64 [n] · unit_id : sid [n] -- fixed or parameter value; null ⇒ untouched
initial : f64 [n] -- initial guess; null ⇒ no guess supplied
lower : pse.bound [n] · upper : pse.bound [n]
scaling_factor : f64 [n]
priority : i32 -- overlay precedence within a case
authored.case_spec_targets @1 -- resolved by P1 at commit from case_specs.target; rename-stable because it references identities
spec_id : sid · ordinal : u16 · instance_id : sid · member_kind : enum(TargetKind) -- symbol|port|group|equation|instance_wildcard
symbol_decl_id : sid [n] · port_decl_id : sid [n] · index : pse.index_tuple [n] · wildcard : bool
authored.case_activations @1 activation_id PK, case_id, target : pse.target_path, active : bool -- resolved rows: case_activation_targets (same shape as case_spec_targets)
authored.case_objectives @1 case_id, objective_id, active : bool, weight : f64
authored.case_policies @1 case_id, discretization_policy_id [n] (FK authored.discretization_policies), scaler_template_id [n], numerical_policy_id [n] (FK reference.numerical_policies), initializer_template_id [n] (FK authored.templates), solver_profile_id [n] (FK authored.solver_profiles)
authored.datasets @1 dataset_id, name, source, content_hash
authored.observations @1
observation_id : sid PK · dataset_id : sid · target : pse.target_path · value : f64 [n] · unit_id : sid -- null means a missing measurement; resolved rows: observation_targets
std_dev : f64 [n] · timestamp : ts [n] · tag : text [n]
authored.measurement_models @1 measurement_id, observation_id, model_expression : pse.expr_dsl, error_model : enum(ErrorModel), parameters
authored.scenarios @1 scenario_id, case_id, weight : f64, doc
authored.case_sets @1 case_set_id, base_case_id, generator_kind : enum(GeneratorKind) (grid|latin_hypercube|uniform_random|list), generator_params : list<struct<key: text, value: text>>, seed : u64 [n], sample_count
authored.case_set_samples @1 case_set_id, sample_ordinal, case_id
Tabular observation imports use a declared schema with Arrow CSV/JSON ReaderBuilder::new(schema), strict unknown-column handling and CSV header validation. Required columns, missing-value spellings, units, target resolution and source-row spans are specified by a versioned import descriptor; schema inference is disabled. Missing measurements remain rows with null values and never become zero. Structural/type/finite-value/FK validation precedes a change set. This applies to the §19.4 observation interface, not to the YAML/TOML modeling language or IDAES state-file format.
6.11 Numerical infrastructure (reference, authored, compiled)
Decision: ADR-0040, ADR-0043, ADR-0047, ADR-0048, ADR-0041
reference.kernel_specs @1 -- §18.5; generated registrations and adapters consume these fields
kernel_id : sid PK · name : text · version : text · provider : text · artifact_digest : hash
behavior : enum(KernelBehavior) -- explicit|implicit
inputs : list<struct<name: text, quantity_type_id: sid, natural_unit_id: sid, nullable: bool, shape: list<enum(DomainKind)>>>
outputs : list<struct<name: text, quantity_type_id: sid, natural_unit_id: sid, shape: list<enum(DomainKind)>>>
parameters : list<struct<name: text, quantity_type_id: sid, natural_unit_id: sid, indexed_by: list<enum(DomainKind)>>>
null_input_policy : enum(KernelNullPolicy) -- reject|propagate_missing; no null-as-zero mode
failure_policy : enum(KernelFailurePolicy) -- typed_error; scalar and UDF default, no invalid-to-null fallback
tolerant_batch : bool -- true enables the separate mandatory outcome relation (§6.13)
effects : enum(KernelEffects) -- pure; stateful/ambient kernels are unsupported by this contract
argument_evaluation : enum(ArgumentEvaluation) -- eager|guarded_expression; guarded requires an expression lowering
smoothness : enum(Differentiability) · validity : list<struct<input: text, lower: pse.bound, upper: pse.bound>>
monotonicity : list<struct<input: text, direction: enum(Monotonicity)>> · convexity : enum(Convexity)
derivative_bindings : list<struct<kind: enum(DerivativeKind), implementation_id: sid, validity_invariant_ids: list<sid>>>
execution_forms : list<struct<form: enum(ExecutionForm), implementation_id: sid>> -- scalar|batch_arrow|dual|hyperdual
thread_safe : bool · failure_classes : list<enum(KernelFailure)>
bindings : list<struct<backend: enum(BackendBinding), implementation_id: sid, conformance_suite: text>>
-- native|datafusion_udf|nl_external_function|pyomo_external_function|pyomo_expression; no row means unsupported
nl_function_name : text [n] · doc : text · test_suite : text
reference.numerical_policies @1
policy_id : sid PK · name : text · version : text
reassociation : enum(ReassociationPolicy) -- preserve_order|canonical_order
fma : enum(FmaPolicy) -- disabled|explicit
nonfinite : enum(NonfinitePolicy) -- evaluation_error
rewrite_mode : enum(RewriteMode) -- guarded_floating; no unconditional real-algebra identity rewriting
absolute_tolerance : f64 · relative_tolerance : f64 · signed_zero_equal : bool
-- strict.default: preserve_order, disabled, evaluation_error, guarded_floating, 1e-12, 1e-10, false
-- policy selection is case-bound (§7.3); package graph canonicalization always uses ordered strict semantics
compiled.kernel_bindings @1
binding_id : sid PK · kernel_id : sid · scope_instance_id : sid
parameter_bindings : list<struct<name: text, symbol_id: sid [n], value: f64 [n], unit_id: sid [n]>>
input_bindings : list<struct<name: text, node_id: u64>>
reference.pass_specs @1 -- §14.1; ports supply the only input/output declarations
pass_id : sid PK · name : text · version : text
preconditions : list<sid> · postconditions : list<sid> -- invariant_ids
determinism : enum(Determinism) · diagnostics : list<enum(FailureClass)>
-- every input port is keyed initially; no separate incomplete cache_key_inputs list
reference.pass_input_ports @1
pass_id : sid · port : text · relation_id : sid
source_pass_id : sid [n] · source_port : text [n] -- both set for derived input, otherwise pinned snapshot input
required : bool -- optional absence is a typed input value and enters the key
reference.pass_output_ports @1
pass_id : sid · port : text · relation_id : sid
-- PK (pass_id, port); one producer per port; relation schemas can occur at successive stages
compiled.stage_bundles @1 -- sidecar catalog of completed immutable outputs
bundle_id : hash PK · pass_id : sid · pass_version : text
members : list<struct<port: text, relation_id: sid, schema_version: u32, logical_hash: hash>>
-- bundle_id is the stage snapshot ID (§5.3), including semantic parents/derivations; this sidecar is excluded
reference.rule_specs @1 -- §14.2; the body is the typed plan below, never text
rule_id : sid PK · version : text · stratum : u16 · head_relation_id : sid · root_node_id : sid
negation : enum(NegationPolicy) · monotonic : bool · conflict_policy : enum(ConflictPolicy)
reference.rule_plan_nodes @1 -- the RulePlanSpec algebra as rows (one relation, typed payload columns)
node_id : sid PK · rule_id : sid · op : enum(RulePlanOp)
-- scan|filter|project|equi_join|anti_join|union|distinct|aggregate|unnest|kernel_call|recursive
relation_id : sid [n] -- required for scan
input_port : text [n] -- required for scan; names its pass_input_ports binding, never an implicit latest producer
join_keys : list<struct<left: text, right: text>> [n] -- equi/anti join; key columns must be key-role (never Float64, rule 7)
predicate_expr_id : sid [n] -- reference.rule_expr_* graph (same schema as math_* restricted to boolean ops)
projection : list<struct<name: text, expr_id: sid>> [n] · kernel_binding_id : sid [n] · doc : text
reference.rule_aggregates @1 node_id, ordinal : u16, function : enum(RuleAggregate) (count|sum|min|max|collect_ordered),
input_expr_id : sid [n], output_name : text, order_by : list<struct<column: text, ascending: bool>>,
null_policy : enum(AggregateNullPolicy) (reject|skip_missing), empty_policy : enum(AggregateEmptyPolicy) (zero|empty_list|error)
reference.rule_group_keys @1 node_id, ordinal : u16, column : text
reference.rule_unnest @1 node_id, column : text, value_name : text,
null_list_policy : enum(NullListPolicy) (reject|no_members), empty_list_policy : enum(EmptyListPolicy) (no_members)
reference.rule_plan_edges @1 parent_node_id : sid · ordinal : u16 · child_node_id : sid
reference.rule_dependencies @1 -- derived at registry build from rule_plan_nodes; the queryable dependency facts
rule_id : sid · relation_id : sid · input_port : text [n] · mode : enum(DependencyMode) -- read|negate|write; reads/negations retain the named port
stratum : u16 · derivation_id : sid
authored.discretization_policies @1 -- a policy (DM-13): authored or shipped by the reference package; never produced by a pass
policy_id : sid PK · package_id : sid · name : text · method : enum(DiscretizationMethod) -- finite_difference|collocation
scheme : enum(DiscretizationScheme) -- BACKWARD|FORWARD|CENTRAL|LAGRANGE_RADAU|LAGRANGE_LEGENDRE
finite_elements : u32 · collocation_points : u8 [n]
compiled.scaling_plans @1 plan_id, case_id, template_id (scaler), options
compiled.variable_scales @1 symbol_id, scale : f64, offset : f64, source : enum(ScaleSource), derivation_id
compiled.equation_scales @1 equation_id, scale : f64, scheme : enum(ConstraintScalingScheme), derivation_id
compiled.initialization_plans @1 plan_id, case_id, template_id (initializer), doc
compiled.init_stages @1
stage_id : sid PK · plan_id : sid · ordinal : u16 · kind : enum(StageKind)
-- solve_subset|solve_blocks|propagate|apply_overlay|continuation|call_plan|check|restore
target_kind : enum(StageTargetKind) -- instance|equation_set|blocks|connection|all
target_ids : list<sid> -- identities (instances, equations, blocks, connections); never paths
overlay_case_id : sid [n] · solver_profile_id : sid [n] · tolerance : f64 [n]
failure_policy : enum(FailurePolicy) -- abort|continue|retry_with_profile
doc : text
authored.solver_profiles @1 -- a policy: the reference package ships ipopt.default and ipopt.user_scaled (§18.3); users add their own
profile_id : sid PK · package_id : sid · name : text · backend : enum(Backend) -- native_ipopt|nl_ipopt|nl_bonmin|nl_couenne|nl_cbc|nl_scip|nl_petsc_snes|nl_petsc_ts|pyomo
solver : text · options : list<struct<key: text, value: text>>
scaling_mode : enum(ScalingMode) -- user|gradient_based|none
derivative_mode : enum(DerivativeMode) -- exact_hessian|limited_memory
compiled.solve_plans @1 plan_id, problem_id, class : enum(SolvePlanClass), justification : text, modifiers : list<text>
reference.engine_profiles @1 -- the relational engine as a declared input (§14.2 rule 5)
engine_profile_id : sid PK · datafusion_version : text · arrow_version : text
analyzer_rules : list<text> · optimizer_rules : list<text> · physical_rules : list<text> -- ordered; installed explicitly
semantic_settings : list<struct<key: text, value: text>> -- an explicit, versioned key allow-list (§14.2 rule 5), never a namespace match
setting_allow_list_version : text -- bumped when the allow-list changes
-- the engine-profile artifact logical hash is supplied by its bundle handle, not stored in its own row
6.12 Derived structure (compiled)
compiled.problems @1
problem_id : sid PK · model_revision_id : sid · case_id : sid · discretization_policy_ids : list<sid>
variable_count, equation_count, inequality_count, objective_count : u64 · degrees_of_freedom : i64 · input_bundle_hash : hash -- P12 math plus resolved case/policy inputs; never this row's own hash
compiled.variable_order @1
problem_id : sid · symbol_id : sid · position : u64 [n] -- null when not in the solver vector (fixed/parameter/eliminated)
treatment : enum(Treatment) · lower : pse.bound · upper : pse.bound · initial : f64 [n] · scale : f64 · offset : f64
compiled.equation_order @1
problem_id : sid · equation_id : sid · position : u64 [n] · active : bool · scale : f64 · kind : enum(RowKind) -- equality|inequality|objective
compiled.bound_values @1 -- fixed and parameter symbols, separate from free-variable initial guesses
problem_id : sid · symbol_id : sid · treatment : enum(Treatment) · value : f64 · unit_id : sid · value_hash : hash · source_spec_id : sid [n]
compiled.case_bound_substitutions @1 -- fixed/parameter symbols substituted on the case-bound view (§7.4 step 3, §14.1 P13/P14)
problem_id : sid · symbol_id : sid · value : f64 · unit_id : sid · value_hash : hash · source : enum(ValueSource) -- case_spec|package_default|template_default
compiled.incidence @1
problem_id : sid · equation_id : sid · symbol_id : sid · linear : bool · coefficient : f64 [n] -- coefficient when affine in this symbol
compiled.dm_partition @1
problem_id : sid · kind : enum(DMKind) (equation|variable) · id : sid
block : enum(DMBlock) -- underconstrained|square|overconstrained
matched_id : sid [n] -- matching partner; null ⇒ unmatched
compiled.blocks @1 problem_id, block_id, kind : enum(BlockKind) (scc|connected_component|bordered_diagonal), order : u32, size : u32
compiled.block_members @1 block_id, kind (equation|variable), id
compiled.sparsity_patterns @1 problem_id, kind (jacobian|hessian), row_ptr : list<u32>, col_idx : list<u32>, content_hash
compiled.evaluation_programs @1 problem_id, program_id, artifact_hash, instruction_count, workspace_size
compiled.backend_bindings @1 problem_id, backend, status : enum(BindingStatus) (supported|unsupported), unsupported_opcodes : list<text>, artifact_hash [n]
6.13 Execution and evidence (runtime, provenance)
Decision: ADR-0044
runtime.runs @1
run_id : sid PK · problem_id : sid · case_id : sid · model_revision_id : sid
backend : enum(Backend) · solver_profile_id : sid · plan_id : sid [n] · stage_id : sid [n] · parent_run_id : sid [n]
attempt : u16 -- 1 for the first attempt of (plan run, stage); retry_with_profile creates a new row with attempt + 1
resolved_options : list<struct<key: text, value: text>> -- the options actually passed to the solver after capability resolution (§18.3)
started_at, finished_at : ts · status : enum(TerminationStatus)
-- optimal|locally_infeasible|infeasible|unbounded|max_iterations|max_time|restoration_failed|solver_error|cancelled|user_interrupt
environment : struct<platform_version: text, compiler_version: text, solver_version: text, kernel_digests: list<hash>, host: text>
wall_seconds : f64 · iterations : u32 [n]
runtime.solutions @1 run_id, symbol_id, value : f64, unit_id, bound_status : enum(BoundStatus) (interior|at_lower|at_upper|violated)
runtime.duals @1 run_id, equation_id, dual : f64, bound_multiplier_lower : f64 [n], bound_multiplier_upper : f64 [n]
runtime.residuals @1 run_id, equation_id, residual : f64, scaled_residual : f64, relative_residual : f64 [n]
runtime.iterations @1 run_id, iteration : u32, objective, inf_pr, inf_du, mu, step_size, regularization, restoration : bool
runtime.solver_events @1 run_id, ordinal, kind : enum(SolverEvent), message : text
runtime.diagnostics_findings @1 finding_id, subject_snapshot : hash, run_id [n], check_id : sid, severity : enum(FindingSeverity), subjects : list<sid>, values : text (JSON), message, next_steps : list<text>
runtime.kernel_evaluations @1 evaluation_id, kernel_binding_id, input_hash, output_batch_hash -- batch evaluations outside solves
runtime.kernel_evaluation_outcomes @1 -- mandatory for tolerant batch evaluation, one row per requested input/output coordinate
evaluation_id : sid · row_ordinal : u64 · output_ordinal : u16
outcome : enum(KernelOutcome) (success|missing_input|domain_failure|implementation_failure)
value : f64 [n] · quantity_type_id : sid · unit_id : sid · reason_code : enum(KernelFailure) [n]
-- success iff value is non-null; every non-success has a reason_code; failed values cannot feed ordinary aggregations
runtime.host_capabilities @1 -- written by the explicit `probe_host` operation (§18.3); read when a run resolves its profile
host : text PK · probed_at : ts · ipopt_version : text · linear_solvers : list<text> · hsl_available : bool · petsc_version : text [n]
python_env : struct<interpreter: text, pyomo: text, pint: text, pyarrow: text, numpy: text, scipy: text, idaes: text [n], pyomo_contrib: list<struct<name: text, version: text>>> [n]
provenance.derivations @1
derivation_id : sid PK · relation_id : sid · row_key : text · rule_id : sid [n] · pass_id : sid [n]
supporting : list<struct<relation_id: sid, row_key: text>> · snapshot_id : hash · fingerprint : hash
provenance.pass_records @1 pass_run_id, pass_id, version, snapshot_in : hash, snapshot_out : hash, engine_profile_hash : hash [n],
plan_evidence : list<struct<encoding_checksum: hash, encoding: text, codec_version: text>> (noncanonical diagnostic bytes — §14.2 rule 5),
plan_explain : list<text> (EXPLAIN in pgjson format), rules_fired : list<struct<plan_ordinal: u16, rule_name: text, ordinal: u16>>,
duration_ms, finding_count, status
provenance.assertions @1 assertion_id, package_id, expected : text (typed JSON), status : enum(AssertionStatus) (pass|fail|obsolete), reason
provenance.refs @1 name : text PK, snapshot_id : hash, manifest_checksum : hash, updated_at : ts
6.14 Enumerations preserved from IDAES
These enumerations are closed dictionaries whose members carry idaes_name for parity. Member lists were taken from the vendored IDAES sources (2.10 line) and are re-verified against the pinned parity package, IDAES 2.12.0, by a governance test that compares schema_enums to the package’s enum classes before phase 1; where IDAES declares members without an implementation, the platform declares them too and reports a missing law binding rather than silently accepting them.
| Enum | IDAES source | Members |
|---|---|---|
MaterialBalanceType | core/base/control_volume_base.py | useDefault, none, componentPhase, componentTotal, elementTotal, total |
EnergyBalanceType | same | useDefault, none, enthalpyPhase, enthalpyTotal, energyPhase, energyTotal, isothermal (only enthalpyTotal, none, isothermal have law bindings) |
MomentumBalanceType | same | none, pressureTotal, pressurePhase, momentumTotal, momentumPhase (only pressureTotal, none bound; no useDefault) |
FlowDirection | same | notSet, forward, backward |
MaterialFlowBasis | core/base/process_base.py | molar, mass, other |
DistributedVars | core/base/control_volume1d.py | variant, uniform |
PhaseType | core/base/phases.py | undefined, liquidPhase, vaporPhase, solidPhase, aqueousPhase |
ComponentType (platform enum) | core/base/components.py class hierarchy | Component, Solute, Solvent, Ion, Anion, Cation, Apparent |
StateIndex | modular_properties/base/utility.py | true, apparent |
HenryType | modular_properties/phase_equil/henry.py | Hcp = 1, Hxp = 2, Kpc = 51, Kpx = 52 (values below 51 are constants c/P, above are volatilities P/c) |
ConcentrationForm | modular_properties/base/utility.py | molarity, activity, molality, moleFraction, massFraction, partialPressure |
CubicType | modular_properties/eos/ceos_common.py | PR, SRK |
FlashType | models/unit_models/feed_flash.py | isothermal, isenthalpic |
MixingType, MomentumMixingType | models/unit_models/mixer.py | none, extensive; none, minimize, equality, minimize_and_equality |
SplittingType, EnergySplittingType | models/unit_models/separator.py | totalFlow, phaseFlow, componentFlow, phaseComponentFlow; none, equal_temperature, equal_molar_enthalpy, enthalpy_split |
ThermodynamicAssumption | models/unit_models/pressure_changer.py | isothermal, isentropic, pump, adiabatic |
ValveFunctionType | models/unit_models/valve.py | linear, quick_opening, equal_percentage (a custom callback becomes an authored helper template) |
HeatExchangerFlowPattern | models/unit_models/heat_exchanger.py | countercurrent, cocurrent, crossflow |
ControllerType, ControllerMVBoundType, ControllerAntiwindupType | models/control/controller.py | P, PI, PD, PID; NONE, SMOOTH_BOUND, LOGISTIC; NONE, CONDITIONAL_INTEGRATION, BACK_CALCULATION |
ConstraintScalingScheme | core/scaling/custom_scaler_base.py | harmonicMean, inverseSum, inverseRSS, inverseMaximum, inverseMinimum |
DefaultScalingRecommendation | same | userInputRecommended, userInputRequired, userSetManually |
InitializationStatus | core/initialization/initializer_base.py | Ok, none, Failed, DoF, PrecheckFailed, Error |
DaeVarTypes | core/solvers/petsc.py | ALGEBRAIC, DIFFERENTIAL, DERIVATIVE, TIME |
DiscretizationScheme | Pyomo DAE | BACKWARD, FORWARD, CENTRAL, LAGRANGE_RADAU, LAGRANGE_LEGENDRE |
| SSLW costing enums | models/costing/SSLW.py | listed in §19.5 |
7. The mathematical IR
7.1 Design constraints
- The expression graph is a DAG stored in
compiled.math_expr_nodesandmath_expr_args; the authoritative argument order isargument_ordinal. Packed adjacency arrays for evaluation are generated (§18.2), never maintained separately. - Indexed mathematics is preserved until a backend requires scalarization. A species balance over 1,000 cells is one indexed equation instance (
compiled.math_indexed_equations) whose free index ranges over the cell domain until P12 (index expansion, §14.1) produces the scalar rows that structural analysis and the backends consume. - Every node carries a resolved
quantity_type_idafter P10. Unit inference is a pass, not a runtime check. - Every operator carries a contract (§7.3). A backend that cannot lower an operator fails explicitly at P16 with the unsupported opcodes listed in
compiled.backend_bindings. - Free unknowns are symbols. A null in a value column never means “to be solved”.
7.2 Operator catalog
| Opcode | Arity / payload | Dimensional rule | Derivative rule | Domain restriction | Smoothness | Native | NL | Pyomo | DataFusion |
|---|---|---|---|---|---|---|---|---|---|
Const | payload value, unit | given | 0 | — | C∞ | ✓ | ✓ | ✓ | ✓ |
SymbolRef | payload symbol | symbol’s type | 1 w.r.t. itself | — | C∞ | ✓ | ✓ | ✓ | ✓ (column) |
Add, Sub | 2, ordered | complete addition rule §8.3 | sum/difference rule | quantity compatibility | as children | ✓ | ✓ | ✓ | ✓ |
Affine | ordered constant + (coef, child) list | complete quantity rule §8.3 | chain rule through children | declared numerical policy | as children | ✓ | ✓ | ✓ | ✓ |
WeightedMean | ordered weight/value pairs, normalization policy | values same full quantity type; weights dimensionless | quotient/chain rule | nonzero weight sum, or certified sum of one | as children off restriction | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ (CASE-guarded expression) |
Mul, Div | 2 | product/quotient of dimensions | product/quotient rule | Div: divisor ≠ 0 | C∞ off restriction | ✓ | ✓ | ✓ | ✓ |
Pow | 2 | base dimension^exponent; exponent must be dimensionless constant unless base is dimensionless | standard | fractional exponent: base > 0; negative exponent: base ≠ 0 | C∞ off restriction | ✓ | ✓ | ✓ | ✓ |
Neg, Abs | 1 | same | −1; sign | — ; Abs nonsmooth at 0 | C∞ ; C⁰ | ✓ | ✓ | ✓ | ✓ |
Exp, Log, Log10, Sqrt | 1 | dimensionless in, dimensionless out (Sqrt: half dimension) | standard | Log: > 0; Sqrt: ≥ 0 (nonsmooth at 0) | C∞ off restriction | ✓ | ✓ | ✓ | ✓ |
Sin, Cos, Tan, Asin, Acos, Atan, Sinh, Cosh, Tanh, Erf | 1 | dimensionless | standard | Asin/Acos: [−1, 1] | C∞ | ✓ | ✓ | ✓ | ✓ |
SmoothMax, SmoothMin, SmoothAbs | 2 (or 1) + eps | same dimension for all | closed form (IDAES math.py: 0.5*(a+b+sqrt((a-b)^2+eps^2))) | eps > 0 | C∞ | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ |
SafeSqrt, SafeLog | 1 + eps | as Sqrt/Log | closed form | eps > 0 | C∞ | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ |
Conditional | guard, then, else | then/else same dimension | branchwise | guard is a boolean node; nonsmooth at switch | C⁰ or discontinuous | ✓ | ✗ (must be reformulated, §7.5) | ✓ (Expr_if) | ✓ (CASE) |
SumOver, ProdOver, MinOver, MaxOver | body + domain + bound index + optional filter | body dimension (SumOver), product (ProdOver) | termwise | — | as body (MinOver/MaxOver nonsmooth) | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ (aggregate) |
Gather | group + coordinate map | group’s type | 1 | index must be valid tuple | — | ✓ | ✓ | ✓ | ✓ (join) |
Broadcast | body + domain | body’s | — | — | — | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ |
Derivative | body + domain + order | body dimension / domain unit^order | linear | continuous domain only | — | via discretization (P11) | via P11 | via P11 or DerivativeVar | ✗ |
Integral | body + domain + quadrature | body dimension × domain unit | linear | continuous domain | — | via quadrature (P11) | via P11 | via P11 | ✗ |
KernelCall | binding + output ordinal | kernel’s output type | from KernelSpec.derivative_bindings | kernel validity | kernel’s | ✓ | ✓ only with nl_external_function binding | ✓ ExternalFunction or expression expansion | ✓ generated UDF |
ImplicitRef | implicit system + unknown ordinal | unknown’s type | implicit function theorem dz/du = −G_z⁻¹ G_u on the selected branch | G_z nonsingular | branchwise C¹ | ✓ | ✗ (must remain equations) | ✗ (must remain equations) | ✗ |
UnitConvert | 1 + scale, offset | from → to | scale | offset ≠ 0 only when a point quantity is converted from an affine unit (°C, psig) to its canonical unit; inserted by P3/P9, so no node carries an affine unit after P10 | C∞ | ✓ | ✓ | ✓ | ✓ |
PiecewiseLinear | breakpoints payload | given | slopes | — | C⁰ | ✓ | ✓ (SOS2 lowering) | ✓ (Piecewise) | ✓ |
Phase 0 admits as a Conditional guard only a SymbolRef whose kind is the registered boolean kind or an IntConst 0/1 decided by feature selection; comparison and logical opcodes are a later amendment.
“✓ (expanded)” means the backend receives scalar forms after P12 (index expansion) and P16 (backend lowering). Conditional is admitted in authored templates only under a guard whose value is decided at compile time (feature flags) or when the case selects a backend that supports it; otherwise it must be reformulated (smooth approximation, complementarity, or disjunction).
7.3 Operator contract record
Decision: ADR-0047
Each opcode has a row in reference.operator_specs (generated from the pse-mathir operator table so that the code and the relation cannot drift):
reference.operator_specs @1
opcode : enum(Opcode) PK · arity : enum(Arity) (fixed n|variadic|payload)
quantity_operation_ids : list<sid> · shape_rule : text · derivative_rule : text
argument_evaluation : enum(ArgumentEvaluation) · failure_classes : list<enum(KernelFailure)>
domain_restrictions : list<struct<argument: u16, relation: enum(RelationOp), bound: f64>>
smoothness : enum(Differentiability) · convexity_rule : text · monotonicity_rule : text
sparsity_rule : text · rewrite_conditions : list<text>
lowering : list<enum(BackendBinding)>
The operator table is the authority for evaluation order, quantity rules, derivatives and lowerings. P10 always preserves ordered guarded floating-point semantics, so the model graph is independent of case policy. x/x → 1 is not an unconditional rewrite: the relevant contract must establish finite nonzero inputs and preserve observable failure/signed-zero behavior. log(exp(x)) → x is disabled by default because intermediate overflow/underflow and rounding can change behavior; any future rule requires a checked interval/error bound under the selected policy.
P13 resolves case_policies.numerical_policy_id, defaulting to strict.default, and persists the selected policy in problem/preparation and run evidence. P16 may reorder terms or contract multiply-add only when that policy explicitly permits it; those choices enter preparation keys. Backend support includes numerical-policy support: an adapter that cannot preserve selected ordering, branch failures or comparison semantics returns backend.unsupported_numeric_policy, never silently relaxes them. Domain violations and non-finite intermediates are typed evaluation failures, not accepted approximate answers. Comparison of finite values is abs(a-b) <= atol + rtol*max(abs(a),abs(b)); policy determines signed-zero equivalence. NaNs never pass a finite comparison; non-finite/error outcomes must match their declared class. No backend gets a bitwise-equivalence claim from passing a tolerance test.
7.4 Canonicalization (pass P10)
Decision: ADR-0039, ADR-0047
- Ordered hash-consing. Hash framed opcode, typed payload, quantity contract and child hashes through
pse-ids;argument_ordinaland reduction domain order are significant. Do not sort commutative children under the default contract. Sharing a node does not authorize hoisting its evaluation outside a conditional region. - Preserve arithmetic structure. P3 emits ordered
Add/Sub/Mulnodes. P10 does not flatten, merge coefficients, delete zero-weight evaluated terms or reassociate them just because the real expressions are algebraically equal. ExplicitAffinenodes retain ordered terms; their children may be nonlinear. A case-bound optimization with a declared policy and complete assumptions belongs to P16, not the model graph. - Guarded literal folding. Fold literal-only subtrees only when domain, finite result, selected branch, signed zero and comparison behavior are preserved. Do not pre-evaluate a failing excluded branch. Never fold symbol references in P10, regardless of role; case-bound substitutions are separate P13 outputs.
- Complete physical inference. Apply §8.3 and record the selected quantity operation/conversion. Infer kind, basis, reference convention, scale kind, shape, subject kind and canonical unit together. Dimension equality alone cannot attach a result type.
WeightedMeanhas its own payload and checked normalization; it is not a free-form label on an arbitrary sum. - Acyclicity and indices. Reject cycles; resolve reduction/gather binders and retain the free-index domains. Deterministic topological numbering is storage order, not a command to execute excluded branches.
The pse.canon.v2 relation serializer preserves this ordered IR. Reordering arithmetic under an opted-in case policy changes the prepared program and its policy-qualified hash, not the authoritative graph.
7.5 Equation records
compiled.math_equations follows the EquationRecord shape of semantic_math_basis.md §1.1. Family and role are derived by P14 on the case-bound view: the canonical graph with fixed and parameter symbols replaced by recorded case_bound_substitutions values from bound_values. A coefficient is constant when it is a literal or a recorded fixed/parameter substitution; math_static_analysis.parameter_dependence records which fixed/parameter substitutions the classification used, so the same canonical graph may classify differently in a case that estimates a parameter (§19.4) than in one that fixes it, and the derivation says why:
| Detection | Family |
|---|---|
| single symbol with constant bound | BOUND |
body proved affine in active decision variables, eq | AFFINE_EQUALITY; if every coefficient ∈ {−1, +1} and the symbols are flows sharing a quantity type: NETWORK_BALANCE |
body proved affine in active decision variables, le/ge/range | AFFINE_INEQUALITY |
| affine-in-variables sum of fractions with constant 1 | SIMPLEX_ALLOCATION (sum of mole fractions) |
| product of two symbol-dependent subtrees | BILINEAR (flow × composition, flow × enthalpy) |
| transcendental of symbol-dependent subtree | SMOOTH_TRANSCENDENTAL (Antoine, Arrhenius, ideal-gas cp) |
contains SmoothMax/SmoothMin | NONSMOOTH_CONVEX approximated; recorded as SMOOTH_TRANSCENDENTAL with a smooth_approximation flag |
contains KernelCall without expression expansion | BLACK_BOX with derivative contract reference |
definition sense | REPORTING_DEFINITION when no active equation depends on it, else DEFINITION |
Roles (HARD_FEASIBILITY, DEFINITION, LINKING for connection equations, DOMAIN_GUARD, REPORTING, APPROXIMATION) are assigned by the pass that created the equation and are part of the derivation.
7.6 Null, bound, and unknown semantics
Decision: ADR-0043
| Situation | Representation |
|---|---|
| Variable to be solved | symbol with treatment = free in compiled.variable_order |
| No initial guess | initial null in case_specs; P13 substitutes the template default or the quantity type’s nominal magnitude and records ScaleSource/InitialSource |
| Unbounded | pse.bound{kind: unbounded}; never ±inf, NaN, or null |
| Missing measurement | null value in observations (a row may exist for provenance); estimation excludes it under an explicit missing-data policy and reports the count |
| Invalid kernel evaluation | scalar/UDF typed error, or a mandatory typed row in kernel_evaluation_outcomes; never an unexplained null |
| Inference predicate could not decide | a row in inferred.undecided (truth = unknown or conflict) plus a diagnostic; the head relation holds only decided-true rows, so an undecided fact can never be consumed as true, and the closure report (§14.5) stays open until the row is resolved |
7.7 The expression DSL
Authored equations are written in a small textual language. The text, stored in pse.expr_dsl columns, is the authored fact and the only form a change set may edit; its parsed graph is a derived normalized.*_expr_* relation produced by P3 with a derivation to the source span (§6.6), and a parse → render → parse round trip is the identity on the graph (a test in §24.1).
expr := affine ;
affine := term { ("+" | "-") term } ;
term := factor { ("*" | "/") factor } ;
factor := unary { "^" unary } ;
unary := ["-"] primary ;
primary := number [unit]
| path [index]
| function "(" [expr {"," expr}] ")"
| "sum" "(" binder "|" expr ")"
| "prod" "(" binder "|" expr ")"
| "d(" expr ")/d" ident (* derivative w.r.t. a continuous domain *)
| "integral" "(" binder "|" expr ")"
| "kernel" "." ident "(" [args] ")"
| "if" expr "then" expr "else" expr (* Conditional; guard must be compile-time or backend-supported *)
| "(" expr ")" ;
binder := ident "in" path [ "where" expr ] ;
path := ident { "." ident } ;
index := "[" expr { "," expr } "]" ;
unit := "{" unit_expr "}" ; (* e.g. 320{K}, 2{bar}, 1{mol/s} *)
function := "exp" | "log" | "log10" | "sqrt" | "abs" | "smooth_max" | "smooth_min" | "smooth_abs" | "safe_sqrt" | "safe_log"
| "sin" | "cos" | "tan" | "tanh" | "erf" | "min" | "max" | "convert" | "weighted_mean" ;
Accepted superset (wave 1). The parser accepts a documented superset of the grammar above. There are three entry points: an expression; an equation, with ==, <= or >=; and a predicate, with and, or, not, != and in. expr where name = expr, … binds local names as in §9.3. eps= is the last, named argument of the smooth and safe functions. ^ is right-associative, and -x^2 is refused as ambiguous — write (-x)^2 or -(x^2). The comprehension form f(x_i for i in set) written in the mixer example below is not accepted; the chained smooth_min form of §11.1 is the authored form. min and max parse and are refused by P3 until their opcodes exist.
Examples (heater energy balance and a mixer’s smooth minimum pressure):
sum(p in phase | control_volume.properties_out[t].enth_flow_phase[p])
- sum(p in phase | control_volume.properties_in[t].enth_flow_phase[p]) - heat_duty[t] == 0
mixed_state[t].pressure == smooth_min(inlet_state[t, i].pressure for i in inlet_list, eps=1e-3{Pa})
weighted_mean(w1, x1, w2, x2, ...) requires a nonempty even argument list and lowers to ordered pairs with divide_by_sum; only generated templates with an explicit unit-sum invariant may select certified_unit_sum. An empty set or zero denominator is a typed domain error.
The parser (winnow) produces nodes with unresolved paths; P3 (canonicalization) resolves paths against template symbols, submodels, and bound package domains. Any path that does not resolve is an authoring error with a source span.
8. Physical typing
8.1 QuantityType
A quantity_type_id resolves to the tuple (quantity kind, dimension vector, basis, reference state, scale kind, shape, subject kind, canonical unit, nominal magnitude). This is what allows the compiler to distinguish:
| Distinction | Encoded by |
|---|---|
| temperature vs temperature difference | scale_kind = point vs difference on kind temperature. Both use canonical unit K; neutral scaling preserves point/difference, while kind-changing products require a registered composition (§8.3) |
| absolute vs gauge pressure | both point quantities of kind pressure; the gauge type carries a reference_state_id naming the datum, and conversion is a conversion_rules.kind = affine rule whose offset is the datum |
| molar vs mass-specific enthalpy | basis_id molar vs mass; conversion rule molar↔mass requires the mw kernel with the species or phase molecular weight |
| enthalpies with incompatible reference states | reference_state_id; addition of two enthalpies with different reference states is a P10 error |
| species composition vs phase composition | shape = [phase, species] with subject_kind = species vs shape = [phase] with subject_kind = phase |
| vector indexed by species vs by cells | shape domain kinds differ even when lengths match |
8.2 Units and unit sets
- Units are SI-anchored:
scale_to_canonical,offset_to_canonical, and a dimension vector. Units with offsets (°C, °F, psig) carryis_affine = true: a point conversion applies the offset, while a difference conversion applies scale only. P3 normalizes authored constants to package canonical units. P9 explicitly converts to and from each kernel’s natural-unit ports; P10 checks those declaredUnitConvertedges, including affine ports, instead of banning the conversion needed by the kernel. - A property package selects a
unit_set(IDAESUnitSet: time, length, mass, amount, temperature, plus optional current, luminous intensity, currency). Every symbol declared by templates bound to that package is stored in the canonical unit of the set; theunit_idcolumn ofcompiled.symbolsrecords it. - Method natural units: a correlation kernel (Antoine in bar and °C, a Perry’s cp in J/(kmol·K)) declares its own input/output units in
KernelSpec; P9 insertsUnitConvertnodes to and from the package canonical units. This reproduces the IDAES generic framework behavior of mixing methods written in different units, but every conversion is a visible node. - Currency units (
USD_2018,USD_2023, …) have dimensioncurrency; conversion between years is a scale by the ratio of Chemical Engineering cost indices fromreference.cost_indices, standardized through the CE=500 basis exactly as IDAESregister_idaes_currency_unitsdoes (§19.5).
8.3 Unit inference algorithm (P10 step 4)
Decision: ADR-0039
Inference returns the complete QuantityType and the selected operation/conversion, never only a dimension vector. quantity_kinds.addition_kind distinguishes additive quantities (flows, transported energy, amounts) from origin-sensitive points (temperature, pressure, specific enthalpy/entropy). For an additive kind, signed sums retain its ordinary point representation; difference is reserved for origin-sensitive kinds. This avoids treating the sum of two material flows as an invalid affine-point sum.
| Operation | Result and rejection rule |
|---|---|
| Literal / symbol | A symbol supplies its complete type. A literal’s explicit kind/context plus unit must resolve uniquely; ambiguity fails. A bare dimension match never chooses between torque/energy, mass/molar basis or reference states. |
| Additive sums | Require equal kind, basis, reference convention and index identities after declared unit conversion; preserve them. Opposite signs do not erase a reference convention. |
| Origin-sensitive addition/subtraction | Point plus/minus difference gives point; difference plus/minus difference gives difference; point minus point gives difference only with the same datum. Point plus point and difference minus point fail. Ordinary scalar weighting of a point does not authorize an arbitrary point sum. |
| Neutral dimensionless scaling | A scalar dimensionless operand with no basis/reference/subject obligations preserves the other operand’s complete type, including difference; division also requires a nonzero divisor. Thus T + alpha*delta_T remains well typed. A composition fraction is not automatically a semantically neutral scalar. |
| Other Mul/Div, Pow, roots | Dimensions combine arithmetically, then exactly one registered quantity_operations rule determines kind, basis, reference propagation/cancellation, scale, shape and subject. Preservation names its operand ordinal; no adapter chooses which operand supplies a component. No rule or multiple matches is math.quantity_operation_unsupported. Rational powers and roots require a declared result and domain conditions; variable powers require a dimensionless base. |
| WeightedMean | Values have identical complete type after explicit conversions; weights are dimensionless with compatible index identity. divide_by_sum evaluates an ordered numerator/denominator with nonzero denominator; certified_unit_sum requires its named invariant. Result preserves value kind, basis, datum, point/difference and shape. This operation alone authorizes averaging origin-sensitive points. |
| Reduction | Bind the named domain and preserve remaining index identities. Sum requires additive or difference values; a sum of points needs WeightedMean. Empty sum has a typed zero only where the operator contract permits it; min/max on an empty domain fail. ProdOver requires dimensionless body or an explicitly registered finite-cardinality product rule. |
| Exp/Log/trigonometric | Require the declared dimensionless kind and domain; result is the declared dimensionless type. Dimensional cancellation cannot silently discard incompatible basis/reference obligations. |
| Derivative / Integral | Combine dimensions with the domain unit and use a registered quantity rule for the result kind, basis and reference; keep remaining index identities. An unavailable rule fails before lowering. |
| KernelCall | Match complete input/parameter contracts, insert named natural-unit conversions, and use the declared output contract. Check conversion direction and difference-versus-point offsets. |
| Gather / Broadcast / Conditional | Gather checks actual domain member identity; broadcast is explicit. Conditional branches have identical complete types after declared conversions, and the guard is Boolean. |
The standard quantity package must declare and test at least flow times specific enthalpy to energy flow (preserving enthalpy reference state), flow times composition to component flow (preserving material basis), gas constant times temperature to molar energy, pressure divided by molar energy to molar density, temperature divided by an explicitly typed temperature scale to dimensionless, and activation energy divided by gas-constant-times-temperature to dimensionless. cancel policies require matching tagged basis/reference inputs; declared_result must name its result and invariant, never discard context by default. Package authors add a quantity rule for a genuinely new composition; adapters do not invent one.
A relation of operation selections/conversions is included in the typed stage’s derivations. Equation sides must have compatible complete types; the residual uses the applicable subtraction rule. Cross-package enthalpy reference changes and molar/mass conversions require a registered conversion with its parameter dependencies. Tests exercise composed expressions, weighted means, gauge/absolute pressure, scaled temperature differences and the standard package equations.
8.4 Bases and reference states
- Material flow basis (
molar,mass,other) is a property of the state definition and of each reaction package; conservation-law expansion (§10) converts contributions to the law’s basis by inserting the conversion rule’s kernel (mw) rather than summing incompatible quantities. - Reference states record
T_ref,P_ref,include_enthalpy_of_formation, and phase. Enthalpy and entropy quantity types carry the reference state of the package that computed them; aTranslatortemplate between packages with different reference states must author the offset explicitly (the compiler refuses to equate them).
8.5 Nominal magnitudes
quantity_types.nominal_magnitude supplies the default scaling hint (ScaleSource = units) used when no better information exists (§16). Packages override per property in authored.default_scaling (IDAES default_scaling_factors).
9. Material systems and the property framework
This section maps the IDAES Generic (Modular) Property Framework (models/properties/modular_properties/**) and the property base classes onto relations, templates, and kernels. The governing change: IDAES resolves properties lazily by attribute access (build_on_demand in core/base/util.py); the platform resolves them once, explicitly, in pass P6, and persists the resolution.
9.1 From GenericParameterBlock configuration to relations
| IDAES configuration | Platform relation / column |
|---|---|
base_units | property_packages.unit_set_id → reference.unit_sets |
components[j].type (Component, Solute, Solvent, Ion, Anion, Cation, Apparent) | species.component_type (an enum mirroring the class hierarchy; charge for ions; dissociation_species rows for Apparent) |
components[j].valid_phase_types | species.valid_phase_types |
components[j].elemental_composition | species_elements |
components[j].parameter_data (mw, pressure_crit, temperature_crit, omega, dens_mol_crit, correlation coefficients) | parameter_values rows keyed by species and parameter kind, with units |
components[j].<method> (cp_mol_ig_comp: RPP4, pressure_sat_comp: NIST, …) | method_selections rows with scope_kind = species and family = pure_component |
components[j].henry_component[p] = {method, type, basis} | henry_declarations |
components[j].phase_equilibrium_form[(p1,p2)] | method_selections with scope_kind = phase_species, family = phase_equilibrium_form |
phases[p].type, equation_of_state, equation_of_state_options | phases.phase_type; method_selections with family = eos and options |
phases[p].component_list | authored.phase_species |
phases[p].visc_d_phase, therm_cond_phase, surf_tens_phase, transport_property_options | method_selections with family = transport_mixing |
state_definition, state_bounds, state_components | property_packages.state_definition_method_id, state_bounds, options.state_components |
pressure_ref, temperature_ref, include_enthalpy_of_formation | property_packages columns; a reference_states row |
phases_in_equilibrium, phase_equilibrium_state, bubble_dew_method | phase_equilibrium_pairs, property_packages.bubble_dew_method_id |
inherent_reactions, reaction_basis | reactions rows with kind = inherent owned by the package |
default_scaling_factors | default_scaling |
The IDAES rule that every component must be valid in at least one phase, and the derived phase_component_set, become the inference rule inferred.phase_species (§14.2) plus invariant closure:species_has_phase.
9.2 State definitions are templates
Each IDAES state-definition module (FTPx, FcTP, FpcTP, FPhx, FcPh, FpTPxpc) becomes a state_block template shipped in the reference package. Its declarations are exactly the IDAES ones:
| Template | State symbols (role = variable) | Supporting symbols | Supporting equations | always_flash |
|---|---|---|---|---|
state.FTPx | flow_mol, mole_frac_comp[j], temperature, pressure | flow_mol_phase[p], mole_frac_phase_comp[p,j], phase_frac[p]; expression flow_mol_phase_comp[p,j] = flow_mol_phase[p]·mole_frac_phase_comp[p,j] | 1 phase: flow_mol_phase[p0] = flow_mol, mole_frac_comp[i] = mole_frac_phase_comp[p0,i], phase_frac[p] = 1. 2 phases: Σ_p flow_mol_phase[p] = flow_mol, flow_mol·mole_frac_comp[i] = Σ_p flow_mol_phase[p]·x[p,i], Σ_i x[p_first,i] − Σ_i x[p_last,i] = 0, phase_frac[p]·flow_mol = flow_mol_phase[p]. >2 phases: per-phase Σ_i x[p,i] = 1. Outlet only (defined_state = false): sum_mole_frac_out: 1 = Σ_i mole_frac_comp[i] | true |
state.FcTP | flow_mol_comp[j], temperature, pressure | mole_frac_comp[j] (variable) with flow_mol_comp[j] = mole_frac_comp[j]·Σ_k flow_mol_comp[k]; flow_mol expression | as FTPx per phase count | true |
state.FpcTP | flow_mol_phase_comp[p,j], temperature, pressure | expressions flow_mol, flow_mol_phase[p], flow_mol_comp[j], mole_frac_comp[j], phase_frac[p]; variable mole_frac_phase_comp[p,j] with x[p,j]·flow_mol_phase[p] = flow_mol_phase_comp[p,j] | — | false |
state.FPhx | flow_mol, mole_frac_comp[j], enth_mol, pressure | temperature becomes a supporting variable; enth_mol_eqn: enth_mol = Σ_p enth_mol_phase[p]·phase_frac[p] | as FTPx | true |
state.FcPh | flow_mol_comp[j], enth_mol, pressure | as FcTP plus enth_mol_eqn | as FcTP | true |
state.FpTPxpc | flow_mol_phase[p], mole_frac_phase_comp[p,j], temperature, pressure | flow_mol expression; mole_frac_comp[j] variable with mole_frac_comp[j]·flow_mol = Σ_p x[p,j]·flow_mol_phase[p]; phase_frac[p]·flow_mol = flow_mol_phase[p]; outlet-only per-phase Σ_j x[p,j] = 1 | — | false |
Every state template declares the four interface contributions IDAES requires of a state block (get_material_flow_terms(p,j), get_enthalpy_flow_terms(p), get_material_density_terms(p,j), get_energy_density_terms(p)) as named expressions in template_requirements/template_display:
material_flow_term[p,j] = flow_mol_phase_comp[p,j]
enthalpy_flow_term[p] = flow_mol_phase[p] · enth_mol_phase[p]
material_density_term[p,j] = dens_mol_phase[p] · mole_frac_phase_comp[p,j]
energy_density_term[p] = dens_mol_phase[p] · energy_internal_mol_phase[p]
plus material_flow_basis = molar, default_material_balance_type = componentTotal, default_energy_balance_type = enthalpyTotal, define_port_members = state symbols, define_display_vars labels. Mole and phase fractions carry the IDAES bounds (mole_frac: [1e-20, 1.001], phase_frac: [0, ∞)) as template defaults.
The defined_state/always_flash rule of IDAES becomes one inference rule producing inferred.state_flash_required(state_instance):
flash_required(S) ← state_instance(S, pkg) ∧ has_phase_equilibrium_pairs(pkg)
∧ ( ¬defined_state(S) ∨ always_flash(template_of(S)) )
Only state instances with flash_required = true receive _teq[pair], the phase-equilibrium constraints, and the smooth-VLE machinery.
9.3 The method registry
Every IDAES “method object” (a class exposing build_parameters and return_expression) becomes a reference.method_specs row. The parameter kinds it needs, in its natural units, are declared once; P9 inserts unit conversions to the package unit set (§8.2).
| Family | Shipped methods (IDAES name) | Realization | Provides | Parameter kinds (natural units) |
|---|---|---|---|---|
pure_component | NIST (Shomate cp/h/s, Antoine Psat in bar), RPP3 (cal-based polynomial cp/h/s, Antoine in mmHg), RPP4 (J-based polynomial; Wagner Psat), RPP5 (R-scaled polynomial; Antoine in °C), Perrys (liquid cp/h/s polynomial in kmol; density eqn 1 and 2), ConstantProperties (constant cp, ρ, μ, λ), ChapmanEnskogLennardJones (viscosity), ChungViscosityPure, Eucken (thermal conductivity), CoolProp exponential/non-exponential/polynomial forms | equation template | cp_mol_{ig,liq,sol}_comp, enth_mol_*_comp, entr_mol_*_comp, dens_mol_liq_comp, vol_mol_liq_comp, pressure_sat_comp, visc_d_phase_comp, therm_cond_phase_comp | e.g. NIST A..H with units J/mol/K, J/mol/K/kK, …, kJ/mol; Antoine A (dimensionless), B, C (K); Wagner A..D dimensionless; Perry’s C1..C5 J/kmol/K^n |
eos | Ideal, Cubic (PR, SRK; van der Waals one-fluid mixing rules), ENRTL (Song–Chen symmetric eNRTL; phase 4) | equation template plus one implicit kernel (cubic root) | the full EOS interface (§9.4) | omega, pressure_crit, temperature_crit, mw per species; PR_kappa[i,j]/SRK_kappa[i,j] per package; eNRTL alpha, tau |
phase_equilibrium_form | fugacity, log_fugacity | equation template | equilibrium constraint per (pair, species) | — |
phase_equilibrium_state | SmoothVLE, CubicComplementarityVLE (SmoothVLE2) | equation template | _teq constraints, slacks | eps_1 = 0.01 K, eps_2 = 0.0005 K; eps_t, eps_z (flow units) |
bubble_dew | IdealBubbleDew, LogBubbleDew (default) | equation template | temperature_bubble/dew, pressure_bubble/dew and the transition compositions | — |
henry | ConstantH with HenryType ∈ {Hcp, Hxp, Kpc, Kpx} | equation template | henry[p,j] | henry_ref[p] in the type’s units |
transport_mixing | ViscosityWilke (wilke or herning_zimmer φ_ij), ThermalConductivityWMS, NoMethod | equation template | visc_d_phase, therm_cond_phase | — |
reaction_rate_form | power_law_rate | equation template | reaction_rate[r] | reaction_order[p,j] |
rate_constant | arrhenius | equation template | k_rxn[r] | arrhenius_const (units derived from basis and concentration form), energy_activation (J/mol) |
equilibrium_form | power_law_equil, log_power_law_equil, solubility_product, log_solubility_product | equation template (solubility forms use SmoothMax) | equilibrium constraint | eps, s_norm, s_scale |
equilibrium_constant | ConstantKeq, van_t_hoff, gibbs_energy | equation template | k_eq[r], log_k_eq[r] | k_eq_ref, T_eq_ref, ds_rxn_ref |
heat_of_reaction | constant_dh_rxn | equation template | dh_rxn[r] | dh_rxn_ref |
| external providers | Helmholtz (IDAES general_helmholtz parameter files), CoolProp coefficient source, FeOs models | kernel (KernelSpec) | per provider capability list | provider-specific |
Two representative method templates in the authoring language:
method: pure.NIST.cp_mol_ig_comp@1
family: pure_component
provides: [cp_mol_ig_comp]
parameters:
- {name: A, quantity: heat_capacity_molar, unit: "J/mol/K"}
- {name: B, unit: "J/mol/K/kK"} # ... C, D, E, F, G, H as in NIST.py
inputs: [{name: T, quantity: temperature, unit: K}]
output_unit: "J/mol/K"
expression: "A + B*t + C*t^2 + D*t^3 + E/t^2 where t = T/1000{K}"
method: rate_constant.arrhenius@1
family: rate_constant
provides: [k_rxn]
parameters:
- {name: arrhenius_const, unit: derived} # unit derived by P9 from basis and concentration form
- {name: energy_activation, unit: "J/mol"}
expression: "arrhenius_const * exp(-energy_activation / (R * state.temperature))"
Unit derivation for arrhenius_const follows the IDAES rule exactly: for moleFraction, massFraction, activity forms the rate unit is amount·volume⁻¹·time⁻¹ (or mass basis); otherwise the concentration unit is raised to the total reaction order. This rule is a conversion_rules.kind = kernel entry, not code inside the template.
9.4 The equation-of-state contract
An eos template must provide expression declarations for the property kinds IDAES’s EoSBase enumerates. The reference eos.ideal template ships the complete equation set:
| Property kind | Ideal expression |
|---|---|
act_phase_comp[p,j], act_coeff_phase_comp[p,j] | x[p,j], 1 |
compress_fact_phase[p] | vapor 1, else 0 |
cp_mol_phase[p], cp_mol_phase_comp[p,j] | Σ_j x[p,j]·cp_comp[p,j]; comp from the pure method selected for the phase type (ig, liq, sol) |
cv_mol_phase_comp[p,j] | vapor cp_ig − R; liquid/solid cp |
dens_mol_phase[p], vol_mol_phase[p] | vapor P/(R·T), R·T/P; liquid/solid 1/Σ_j x[p,j]·v_j(T) with v_j from vol_mol_*_comp or 1/dens_mol_*_comp |
dens_mass_phase[p] | dens_mol_phase[p]·mw_phase[p] |
enth_mol_phase[p], enth_mol_phase_comp[p,j] | vapor Σ_j x·h_ig,j(T); liquid/solid Σ_j x·h_j(T) + (P − P_ref)/dens_mol_phase[p] |
entr_mol_phase_comp[p,j] | vapor s_ig,j(T) − R·ln(x[p,j]·P/P_ref); liquid/solid s_j(T) |
energy_internal_mol_phase_comp[p,j] | h_ig(T) − R(T − T_ref) + Δn·R·T_ref (vapor, with the IDAES Δn rule over noble and diatomic elements); h(T) + dU_form (liquid/solid) |
gibbs_mol_phase_comp[p,j] | h_comp − T·s_comp |
fug_phase_comp[p,j], fug_coeff_phase_comp[p,j] | vapor x·P; liquid Raoult x·Psat_j(T), Henry henry_pressure(p,j,T); coefficient 1 |
log_fug_phase_comp_eq[p,j,pair] | the same evaluated at _teq[pair] in log form |
pressure_osm_phase[p] | R·T·Σ_{j∉solvents} i_j·conc_mol_phase_comp[p,j] |
The reference eos.cubic template carries the Peng–Robinson and Soave–Redlich–Kwong parameterization (u, w, ΩA, Ωb), Soave alpha and its two temperature derivatives, the one-fluid mixing rules for am, bm, dam/dT, d²am/dT², delta[p,i], the dimensionless A[p], B[p], and the departure functions for enthalpy, entropy, internal energy, cp, cv, Gibbs energy, partial molar volume, speeds of sound, and the fugacity coefficient kernel ln φ_j = (b_j/bm)(Z − 1) − ln(Z − B) + (A/(B·p̂))·(b_j/bm − δ_j)·ln((2Z + B(u + p̂))/(2Z + B(u − p̂))), all as expressions over the IR. Equilibrium-state duplicates (_eq quantities at _teq) are the same expressions with T := _teq[pair], generated by template instantiation rather than hand-copied.
The compressibility root is the one non-algebraic step. It is a KernelSpec:
kernel cubic.compress_fact@1
behavior: implicit -- z³ + b z² + c z + d = 0 with b, c, d from (A, B, u, w)
inputs: A (dimensionless), B (dimensionless), eos_type (enum)
outputs: z_liq (lowest real root), z_vap (highest real root)
branch_policy: {liquid: min_real_root, vapor: max_real_root, single_root: that_root}
derivatives: jacobian (implicit function theorem on the selected root), hessian (available)
bindings: native, datafusion_udf, nl_external_function ("cubic_root_l", "cubic_root_h"), pyomo_external_function
The native evaluation solves the cubic in closed form with Newton polishing and returns the branch and its derivatives; the NL binding ships a compiled external-function library with the same two symbols IDAES uses so that existing solver binaries work unchanged.
9.5 Phase equilibrium
Relations and generated mathematics per property package:
- Pairs and species.
phase_equilibrium_pairslists(p1, p2);inferred.phase_equilibrium_species(pair, j)holds every species present in both phases (this is IDAES’sphase_equilibrium_listwith keysPE<n>). The classificationidentify_VL_component_listbecomes derived columnsrole ∈ {vl, henry, l_only, v_only}. - Equilibrium temperature. For each flash-required state instance and pair: symbol
_teq[pair](temperature, bounded liketemperature). - Equilibrium constraint. For each pair and species with role
vlorhenry:equilibrium_constraint[pair, j]from the pair’s form (fugacity:fug_phase_comp_eq(p1) = fug_phase_comp_eq(p2);log_fugacity: the log form), familySMOOTH_TRANSCENDENTAL, roleHARD_FEASIBILITY. - SmoothVLE (Burgard et al.): optional
_t1[pair]with_t1 = smooth_max(T, temperature_bubble[pair], eps_1)when there are no vapor-only species, and_teq = smooth_min(_t1, temperature_dew[pair], eps_2)when there are no liquid-only species; otherwise the degenerate forms_teq = _t1or_teq = T. - CubicComplementarityVLE (Dabadghao et al.): temperature slacks
s[p] ≥ 0,_teq − T − s[vap] + s[liq] = 0,smooth_min(s[p]·(mol/s)/K, flow_mol_phase[p], eps_t) = 0; cubic second-derivative expression6Z + 2b, splitgp − gn, andsmooth_min(gn·f, flow_mol_phase[vap], eps_z) = 0,smooth_min(gp·f, flow_mol_phase[liq], eps_z) = 0. Requires both phases to use the same cubic template (an invariant onmethod_selections). - Bubble and dew points. Symbols
temperature_bubble[pair],temperature_dew[pair],pressure_bubble[pair],pressure_dew[pair]and the transition compositions_mole_frac_{tbub,tdew,pbub,pdew}[pair, j](plus log forms) are created only when the property demand closure requests them (initialization plans do).IdealBubbleDewgenerates the Raoult/Henry sums (Σ_j z_j·Psat_j(Tbub) + Σ_henry z_j·H_j(Tbub) − P = 0,P·(Σ z_j/Psat_j(Tdew) + …) − 1 = 0, and the pressure forms);LogBubbleDewgenerates per-specieslog_fug_phase_comp_Tbub(liq, j) = log_fug_phase_comp_Tbub(vap, j)andΣ_j _mole_frac_tbub[j] = 1. - Henry’s law.
HenryTypevalue encodes direction (Hcp = 1,Hxp = 2are constants;Kpc = 51,Kpx = 52are volatilities). The concentration term is selected by type (conc_mol_phase_compforHcp/Kpc,mole_frac_phase_compforHxp/Kpx, with_true/_apparentsuffix for electrolytes), andhenry_pressure = c/Horc·K. Cubic EOS packages reject Henry species (invariant).
9.6 Property demand resolution (pass P6)
Decision: ADR-0040
P6 reads normalized demand seeds and the pinned method candidate universe, not equations produced by P7/P8 or plans produced by P15. P3 extracts member/property references from the authored expression ASTs; P4 resolves their feature guards; P5 binds finite scope/domain identities. Law contributions, ports and displays expose their property signatures at that same declarative boundary. Initializer and other opaque template operations declare template_property_requirements (§6.6); expression-derived requirements are generated rather than independently authored twice.
seeds := bound, decided requirements from normalized templates/laws/ports/displays/initializers
queue := seeds in semantic-key order
while an unprocessed requirement remains:
form candidates from the complete pinned selections/defaults and state declarations
select by the declared applicability and preference order, independent of arrival order
zero candidates -> explicit unsupported; tied candidates -> explicit ambiguous
one candidate -> persist its resolution and add its declared requires with provenance
stop when the finite set of (scope, property kind, bound index) keys is unchanged
P6 materializes inferred.property_requirements, inferred.method_resolutions and unresolved/ambiguous findings. Recursive requirements are monotone additions over the finite P5 scope; a declaration that creates an unbounded new scope or alternates selection is rejected. Negative selection reads the complete lower-stratum candidate set, including absence, and therefore invalidates when a new candidate appears. Default/explicit selection precedence is fixed in the registry, not chosen by execution order.
P7–P9 realize the selected state/method declarations into symbols, indexed equations and kernel bindings. Every advertised provision must produce its declared output; every property read by a realized equation or P15 plan must already have a resolution. A missing output is prop.incomplete_provider; late demand is prop.undeclared_requirement. Compilation fails rather than silently extending P6 or claiming that declarations make implementation mistakes impossible. The Property Interrogator remains a read-only projection of the requirement/resolution relations. Variable-plus-constraint versus expression provision is preserved by the templates and symbol roles.
A changed initializer selection changes its declarative demand input and reruns the affected stages before P15 binds a plan. This is one acyclic compiler schedule with a bounded internal P6 fixed point, not an implicit cycle across passes.
9.7 Reaction packages
GenericReactionParameterBlock configuration maps to reactions, stoichiometry, reaction_methods, parameter_values (including reaction_order[p,j] overrides; defaults: rate reactions order = −ν for reactants and 0 for products, equilibrium reactions order = +ν, zeroed for solid phases), and reaction_packages (which also carries reaction_basis). The reaction block template declares:
| Symbol / equation | Form |
|---|---|
dh_rxn[r] | expression from heat_of_reaction method |
k_rxn[r] | expression from rate_constant method |
reaction_rate[r] | expression from rate_form: k_rxn[r]·Π_{(p,j): order≠0} C[p,j]^order[p,j] with C selected by ConcentrationForm (molarity → conc_mol_phase_comp, activity → act_phase_comp, molality → molality_phase_comp, moleFraction → mole_frac_phase_comp, massFraction → mass_frac_phase_comp, partialPressure → pressure_phase_comp) |
k_eq[r] / log_k_eq[r] | expression / variable+constraint from equilibrium_constant (van_t_hoff: log_k_eq − ln k_eq_ref = −(dh_rxn/R)(1/T − 1/T_eq_ref); gibbs_energy: log_k_eq = −Δh_ref/(R·T) + Δs_ref/R) |
equilibrium_constraint[r] | from equilibrium_form (power_law_equil: k_eq = Π C^order; solubility product: Q − smooth_max(0, Q − s, eps) = 0 with the IDAES s = s_scale·S/(S + s_norm) over total solid flow) |
reaction_rate_basis | package reaction_basis (must be molar or mass; other blocks conversions) |
The “reaction package must match its property package’s unit set and required properties” check (_validate_property_parameter_units, _validate_property_parameter_properties) becomes two invariants on reaction_packages.
9.8 Provider kernels: Helmholtz, CoolProp, FeOs
- Helmholtz. IDAES computes pure-component Helmholtz properties through a compiled library that evaluates AMPL NL expression files (
phi_ideal,phi_residual, their derivatives, saturation approximations) with a density root solve. The platform authors the sameφ⁰(δ, τ)andφʳ(δ, τ)forms as expression templates (phi_ideal_type01..04,phi_residual_type01..05,surface_tension_type01), loads the per-fluid coefficient JSON intoparameter_values, and exposes one implicit kernelhelmholtz.state@1whose unknowns are(δ, τ)given the chosen state pair (PH,PS,PU,TPX) and whose outputs are the mass-basis properties IDAES exposes (h,s,u,g,f,cv,cp,w,v, plus transport when available). Mole basis is a conversion rule withmw. Derivatives come from the implicit function theorem on the templates’ own derivatives, so no hand-coded derivative tables are needed. This removes the external-library dependency for the native backend; the NL backend keeps a generated external-function library with the IDAES symbol names. - CoolProp. The
CoolPropWrappersupplies coefficients for the exponential, non-exponential, and polynomial forms; the platform treats it as a parameter source producingparameter_valuesrows (with provenance), never as a runtime dependency of the solve. - FeOs. Conditional provider crate under
KernelSpecfor SAFT and cubic models written overnum-dual, giving derivatives natively. It is enabled only whenfeos-coretracksnum-dual0.15:feos-core0.10.1 requiresnum-dual ^0.14, whoseDualNumtrait has a different shape from 0.15 (DualNum<f64>versusDualNum<Primitive = f64>), so one kernel source cannot serve both, and the platform writes every kernel against 0.15. FeOs also imports thequantitycrate, a third units representation, which is never an authority for anything. Coverage (which property kinds, which conventions, which reference states) is declared per kernel and validated by parity tests; nothing is assumed.
9.9 Electrolytes and inherent reactions
The electrolyte extensions (true/apparent species bases, AqueousPhase, ENRTL, ion sets, apparent_inherent_reaction_extent, true_to_appr_species and appr_to_true_species constraints) are supported by the schema (component_type, charge, dissociation_species, StateIndex, _true/_apparent property variants) but are scheduled for phase 4 (§25). Inherent reactions are property-package-owned reactions (reactions.kind = inherent); their generation terms are emitted by law expansion exactly like rate and equilibrium reactions, but the extents live in the state scope.
9.10 Scaling defaults and validity
default_scaling rows (IDAES default_scaling_factors) and method_specs.validity intervals (IDAES metadata valid_range) are consumed by the scaling pass (§16) and by the diagnostics “values outside valid range” check (§15).
10. Balance laws and control volumes
10.1 The conservation law template
IDAES control volumes assemble balances procedurally (add_material_balances, add_energy_balances, add_momentum_balances and their variants in control_volume0d.py and control_volume1d.py). The platform replaces the procedure with one law template expanded over contributions (decision D7):
law_template conservation@1
inputs: scope (control region), subject selector (species j | element e | total | phase p | energy | momentum),
basis, temporal policy (accumulation | none), balance options
expansion:
1. collect contributions whose law_family matches and whose subject matches the selector (semantic match, §8.4)
2. classify each by orientation: into_scope (+), out_of_scope (−), generation (+), accumulation (−)
3. convert each to the law's basis (insert conversion kernels, never sum incompatible quantities)
4. group by the balance index (t; t,x; and p,j | j | e | none depending on balance type)
5. emit one equation per index: Σ signed contributions = 0, family NETWORK_BALANCE or AFFINE_EQUALITY when affine,
role HARD_FEASIBILITY, with derivation listing every contribution
6. emit negative-completeness rows for candidate contributions that were excluded and why
Material, element, energy, and momentum balances are bindings of this template with different subjects and contribution kinds. The control-volume templates declare the contributions; the compiler does the bookkeeping.
10.2 Contribution kinds
The table lists every contribution IDAES control volumes can produce, the condition under which the template declares it, and its orientation. F(p,j) is the state’s material_flow_term, H(p) its enthalpy_flow_term.
| Law family | Contribution (IDAES symbol) | Declared when | Orientation | Notes |
|---|---|---|---|---|
| material | inlet flow F_in(p,j) | always (0D: from properties_in; 1D: −fdt·d(_flow_terms)/dξ) | into_scope | 1D uses the derivative form (§10.4) |
| material | outlet flow F_out(p,j) | always | out_of_scope | |
| material | rate_reaction_generation[t,(p,j)] = Σ_r ν[r,p,j]·rate_reaction_extent[t,r] | has_rate_reactions | generation | converted by _rxn_rate_conv: mass↔molar via mw_comp[j]; other basis is an error |
| material | equilibrium_reaction_generation | has_equilibrium_reactions (requires reactions.has_equilibrium) | generation | |
| material | inherent_reaction_generation | property package has inherent reactions and state includes them | generation | |
| material | phase-equilibrium transfer Σ_r sd[r]·phase_equilibrium_generation[t,r] with sd = +1 if PE_r = (j,(p,*)), −1 if (j,(*,p)) | has_phase_equilibrium and balance type componentPhase | generation (antisymmetric across phases) | |
| material | mass_transfer_term[t,(p,j)] | has_mass_transfer | generation | |
| material | custom molar / mass terms | authored | generation | basis conversion by mw_comp[j] |
| material | accumulation d(material_holdup)/dt with material_holdup = volume·phase_fraction[p]·material_density_term(p,j) | dynamic (holdup symbol when has_holdup) | accumulation | |
| element | elemental flows Σ_j conv(j)·F(p,j)·element_comp[j][e] in and out | balance type elementTotal | into/out | conv = 1 (molar) or 1/mw_comp[j] (mass) |
| element | elemental_mass_transfer_term[t,e] | has_mass_transfer | generation | reactions are forbidden with element balances (invariant) |
| element | accumulation d(element_holdup)/dt | dynamic | accumulation | |
| energy | H_in(p), H_out(p) | always | into / out | |
| energy | heat[t] | has_heat_transfer | into_scope (sign is authored: positive into the volume) | |
| energy | work[t] | has_work_transfer | into_scope | |
| energy | enthalpy_transfer[t] | has_enthalpy_transfer | into_scope | |
| energy | heat_of_reaction[t] = −Σ_r extent[t,r]·dh_rxn[r] (rate and equilibrium) | has_heat_of_reaction (requires extents to exist) | generation | |
| energy | custom term | authored | generation | |
| energy | accumulation Σ_p d(energy_holdup[p])/dt, energy_holdup = volume·phase_fraction·energy_density_term(p) | dynamic | accumulation | |
| momentum | P_in, P_out | always (pressureTotal) | into / out | |
| momentum | deltaP[t] | has_pressure_change | generation | |
| momentum | custom term | authored | generation |
Balance type selects the grouping and which contributions participate:
MaterialBalanceType | Balance index | Participation |
|---|---|---|
componentPhase | (t, p, j) over the phase-species set | all material contributions including phase-equilibrium transfer |
componentTotal | (t, j) summing over phases with (p, j) valid | phase-equilibrium transfer cancels and is omitted |
elementTotal | (t, e) over elements minus linearly dependent ones | elemental contributions only; element_balances index excludes elements made dependent by the IDAES rule (an element unique to one species, when that species already contributed one unique element) |
total | (t) | not supported by IDAES control volumes (BalanceTypeNotSupportedError); supported by mixers/separators and by add_state_material_balances |
useDefault | resolved from the representative state’s default_material_balance_type | inference rule |
EnergyBalanceType.enthalpyTotal and isothermal (extended control volumes: T_in = T_out, or T[t, prev(x)] = T[t, x] in 1D) and MomentumBalanceType.pressureTotal are the implemented bindings; enthalpyPhase, energyTotal, energyPhase, pressurePhase, momentumTotal, momentumPhase are declared enum members whose law bindings are absent, so selecting them raises law.unsupported_binding at P8, exactly where IDAES raises BalanceTypeNotSupportedError.
10.3 The lumped control volume template (cv.lumped@1, IDAES ControlVolume0DBlock)
template: cv.lumped@1
kind: control_volume
params:
property_package: {type: property_package, required: true}
reaction_package: {type: reaction_package, default: null}
property_package_args, reaction_package_args: {type: options}
features:
dynamic: {kind: bool, inherit_from: parent.dynamic}
has_holdup: {kind: bool, default: "= dynamic"} # rule: dynamic ⇒ has_holdup
has_phase_equilibrium: {kind: bool, required_explicit: true}
information_flow: {kind: enum, enum: FlowDirection, default: forward}
has_rate_reactions, has_equilibrium_reactions, has_mass_transfer, has_heat_transfer,
has_work_transfer, has_enthalpy_transfer, has_heat_of_reaction, has_pressure_change: {kind: bool, default: false}
material_balance_type, energy_balance_type, momentum_balance_type: {kind: enum}
domains:
time: {from: flowsheet.time}
phase, species, phase_species: {from: property_package}
submodels:
properties_in: {template: "= property_package.state_block", multiplicity: time,
bindings: {defined_state: "= information_flow == forward", has_phase_equilibrium: "= has_phase_equilibrium"}}
properties_out: {template: "= property_package.state_block", multiplicity: time,
bindings: {defined_state: "= information_flow == backward", has_phase_equilibrium: "= has_phase_equilibrium"}}
reactions: {template: "= reaction_package.reaction_block", multiplicity: time, guard: "reaction_package != null",
bindings: {state: properties_out, has_equilibrium: "= has_equilibrium_reactions"}}
symbols:
volume[t] : {quantity: volume, guard: has_holdup, default_initial: 1}
phase_fraction[t,p] : {quantity: phase_fraction, role: "= n_phases > 1 ? variable : expression(1)", default_initial: 1/n_phases}
material_holdup[t,p,j] : {quantity: amount (or mass by basis), guard: has_holdup}
material_accumulation[t,p,j] : {role: derivative, of: material_holdup, wrt: time, guard: dynamic}
rate_reaction_extent[t,r] : {quantity: molar_flow (reaction basis), guard: has_rate_reactions}
rate_reaction_generation[t,p,j] : {guard: has_rate_reactions}
equilibrium_reaction_extent[t,r], equilibrium_reaction_generation[t,p,j]: {guard: has_equilibrium_reactions}
inherent_reaction_extent[t,r], inherent_reaction_generation[t,p,j]: {guard: "properties_out.include_inherent_reactions"}
phase_equilibrium_generation[t,pe]: {guard: "has_phase_equilibrium and material_balance_type == componentPhase"}
mass_transfer_term[t,p,j] : {guard: has_mass_transfer}
energy_holdup[t,p], energy_accumulation[t,p]: {guard: has_holdup / dynamic}
heat[t], work[t], enthalpy_transfer[t]: {quantity: power, guards: has_heat_transfer / has_work_transfer / has_enthalpy_transfer}
heat_of_reaction[t] : {role: expression, guard: has_heat_of_reaction}
deltaP[t] : {quantity: pressure_difference, guard: has_pressure_change}
element_holdup[t,e], element_accumulation[t,e], elemental_flow_in[t,p,e], elemental_flow_out[t,p,e], elemental_mass_transfer_term[t,e]:
{guard: material_balance_type == elementTotal (and holdup/dynamic/mass-transfer as applicable)}
equations:
sum_of_phase_fractions[t]: "1 == sum(p in phase | phase_fraction[t,p])" guard: n_phases > 1 and has_holdup
material_holdup_calculation[t,p,j]: "material_holdup[t,p,j] == volume[t]*phase_fraction[t,p]*properties_out[t].material_density_term[p,j]"
rate_reaction_stoichiometry[t,p,j]: "rate_reaction_generation[t,p,j] == sum(r in rate_reactions | stoich[r,p,j]*rate_reaction_extent[t,r])"
equilibrium_reaction_stoichiometry, inherent_reaction_stoichiometry: analogous
energy_holdup_calculation[t,p]: "energy_holdup[t,p] == volume[t]*phase_fraction[t,p]*properties_out[t].energy_density_term[p]"
elemental_holdup_calculation[t,e]: "element_holdup[t,e] == volume[t]*sum((p,j) in phase_species | conv(j)*phase_fraction[t,p]*material_density_term[p,j]*element_comp[j,e])"
contributions: (as in §10.2, each guarded by its feature)
law_instances:
material: {law: conservation, subject: "= material_balance_type", scope: self, options: {basis: properties_out.material_flow_basis}}
energy: {law: conservation, subject: energy, guard: energy_balance_type != none}
momentum: {law: conservation, subject: momentum, guard: momentum_balance_type != none}
requirements: [material_flow_terms, enthalpy_flow_terms, material_density_terms (if has_holdup), energy_density_terms (if has_holdup)]
The generated equations reproduce IDAES’s forms exactly, for example the componentPhase material balance:
accum[t,p,j] == F_in[t,p,j] − F_out[t,p,j] + rate_gen[t,p,j]·rxn_conv(t,j) + equil_gen[t,p,j] + inherent_gen[t,p,j]
+ phase_equilibrium_term(t,p,j) + mass_transfer_term[t,p,j] + user_mol(t,p,j) + user_mass(t,p,j)
with accum = convert(material_accumulation → flow units) when dynamic and 0 otherwise; the total enthalpy balance Σ_p accum_E[t,p] == Σ_p H_in[t,p] − Σ_p H_out[t,p] + heat + work + enthalpy_transfer + heat_of_reaction + custom; and the pressure balance 0 == P_in − P_out + deltaP + custom.
Units follow the IDAES derivations (flow_units from the flow basis, acc_units = amount/time or mass/time, holdup_units, rxn_flow_units from the reaction basis); a mismatch between flowsheet time units and package time units is invariant cv.time_units_consistent.
10.4 The distributed control volume template (cv.distributed_1d@1, IDAES ControlVolume1DBlock)
Additional parameters and features: area_definition (DistributedVars.uniform ⇒ scalar area; variant ⇒ area[t,x]), transformation_method (dae.finite_difference | dae.collocation), transformation_scheme (BACKWARD, FORWARD for finite difference; LAGRANGE_RADAU, LAGRANGE_LEGENDRE for collocation), finite_elements, collocation_points, flow_direction, length_domain_set, optional external length_var.
Structural facts preserved from IDAES:
length_domainis a normalized, dimensionless continuous domain on [0, 1]; the physical length is the scalar symbollength(or an external reference). The mathematics is written per unit ξ, so every per-unit-length source term is multiplied bylength._flow_direction_term = −1forforward,+1forbackward; ports bind toproperties[:, first]/properties[:, last]accordingly (§12.1).- One indexed state block
properties[t, x];defined_state = trueonly at the inlet end (firstfor forward,lastfor backward), which is anidx_mapin IDAES and a per-member binding rule here. - Linking symbols
_flow_terms[t,x,p,j] = properties[t,x].material_flow_term[p,j]and_enthalpy_flow[t,x,p] = properties[t,x].enthalpy_flow_term[p]exist so that the spatial derivativesmaterial_flow_dx,enthalpy_flow_dx,elemental_flow_dx,pressure_dxare derivatives of symbols, not of expressions. - Per-unit-length quantities:
material_holdup(amount/length),rate_reaction_generation(flow/length),heat,work,enthalpy_transfer(power/length),deltaP(pressure/length). - Boundary skip rule. Balance and stoichiometry equations are skipped at
x = firstunless the scheme isFORWARD, in which case they are skipped atx = last. This is a filter on the equation template index (filter: "x != boundary(scheme)"), evaluated by P12 (index expansion) against the mesh that P11 produced.
Generated equations (componentPhase form):
material_flow_linking[t,x,p,j]: _flow_terms[t,x,p,j] == properties[t,x].material_flow_term[p,j]
material_balances[t,x,p,j]: L·convert(material_accumulation[t,x,p,j]) == fdt·material_flow_dx[t,x,p,j]
+ L·rate_gen·rxn_conv + L·equil_gen + L·inherent_gen + L·pe_term + L·mass_transfer + L·custom
material_holdup_calculation: material_holdup[t,x,p,j] == area(t,x)·phase_fraction[t,x,p]·material_density_term[p,j]
enthalpy_balances[t,x]: L·convert(Σ_p energy_accumulation[t,x,p]) == fdt·Σ_p enthalpy_flow_dx[t,x,p] + L·(heat + work + enthalpy_transfer + heat_of_reaction + custom)
pressure_balance[t,x]: 0 == fdt·pressure_dx[t,x] + L·deltaP[t,x] + L·custom
element_balances[t,x,e]: L·convert(element_accumulation) == fdt·elemental_flow_dx[t,x,e] + L·elemental_mass_transfer + L·custom
The IDAES apply_transformation() step is the discretization policy attached to length_domain (§13.4); the transformation must be applied to a domain owned by the control volume (external domains are rejected, invariant cv.length_domain_owned).
10.5 What the template model changes structurally
The unit-model survey identified five mechanisms IDAES uses that a declarative template must express without code:
| IDAES mechanism | Template mechanism |
|---|---|
Config fragments re-used textually (_make_heater_config_block invoked for hot_side and cold_side) | Named parameter groups (param_group: heater_side) included by reference; hx_process_config name aliasing becomes template_ports.alias rows |
Narrowing an inherited option (Turbine forces compressor = false, thermodynamic_assumption = isentropic) | template_feature_rules with rule = requires on a derived template; the derived template’s param domain is the narrowed set |
Conditional component declaration (heat_duty exists only if has_heat_transfer and energy_balance_type != none) | guard_id on symbols, equations, ports, submodels |
Callback extension points (delta_temperature_callback, valve_function_callback, pressure_flow_callback, performance-curve build_callback) | Helper templates selected by an enum feature (delta_temperature_method ∈ {lmtd, lmtd2, lmtd3, lmtd_smooth, amtd, underwood}); a custom callback is an authored helper template |
Sub-model embedding with re-exported ports (Flash ⊃ Separator, SLSeparator ⊃ Separator) | template_submodels plus template_ports.bound_to = "split.Vap" |
The recurring IDAES initialization idiom (fix a transfer term to a guess, deactivate its defining equation, solve, then release) is a two-stage initialization_plan (§17).
11. The unit model library as templates
11.1 Catalog
Every IDAES generic unit becomes a reference template. The table records composition, the features that switch structure, extra symbols and equations beyond the control volume, ports, and the default plans. Equations are summarized; full forms are in the survey catalogs and are reproduced in the reference package sources.
| Template (IDAES class) | Composition | Structural features / params | Extra symbols and equations | Ports | Default plans |
|---|---|---|---|---|---|
unit.heater@1 (Heater) | one cv.lumped with has_heat_transfer = true | material/energy/momentum_balance_type, has_phase_equilibrium, has_pressure_change | references heat_duty[t] ≡ cv.heat[t], deltaP[t] ≡ cv.deltaP[t] (guarded) | inlet, outlet | init single_cv; scaler cv |
unit.feed@1, unit.product@1, unit.state_junction@1 | one state block properties[t] (defined_state = true, no phase equilibrium) | property_package | references to every state symbol; StateJunction binds inlet and outlet to the same state | outlet / inlet / both | init: the state block’s plan |
unit.feed_flash@1 (FeedFlash) | cv.lumped with has_phase_equilibrium = true, no energy balance | flash_type ∈ {isothermal, isenthalpic} | isothermal[t]: T_in = T_out or isenthalpic[t]: Σ_p H_in = Σ_p H_out; references to inlet state symbols | outlet only | fix inlet state |
unit.translator@1 (Translator) | properties_in (defined_state = true), properties_out (defined_state = outlet_state_defined, has_phase_equilibrium) | inlet_property_package, outlet_property_package, outlet_state_defined, has_phase_equilibrium (mutually exclusive with defined outlet) | none; the author adds mapping equations; the compiler refuses to equate quantities with different reference states without them | inlet, outlet | init block_triangularization; solve only when DOF = 0 |
unit.cstr@1 (CSTR) | cv.lumped with geometry, reactions, has_rate_reactions = true | reaction and balance features, has_heat_of_reaction, has_heat_transfer, has_pressure_change | cstr_performance_eqn[t,r]: cv.rate_reaction_extent[t,r] = volume[t]·cv.reactions[t].reaction_rate[r] | inlet, outlet | scaler defaults deltaP: 1e-5, volume: 1e3 |
unit.pfr@1 (PFR), unit.pipe@1 (Pipe) | cv.distributed_1d (PFR with reactions) | discretization params, length_domain_set | performance_eqn[t,x,r]: extent[t,x,r] = reactions[t,x].reaction_rate[r]·area; volume with geometry: volume = area·length | inlet, outlet | scaler requires user input for length, area |
unit.stoichiometric_reactor@1 | cv.lumped with reactions, has_rate_reactions = true | has_heat_of_reaction, has_heat_transfer, has_pressure_change | reference rate_reaction_extent[t,r] (user-fixed; no performance equation) | inlet, outlet | |
unit.equilibrium_reactor@1 | cv.lumped with reactions | has_rate_reactions (default true), has_equilibrium_reactions (default true) | rate_reaction_constraint[t,r]: reactions[t].reaction_rate[r] = 0 | inlet, outlet | |
unit.gibbs_reactor@1 | cv.lumped with element balances (elementTotal hard-coded) | inert_species, has_heat_transfer, has_pressure_change | domain lagrange_set (elements touched by non-inert species); lagrange_mult[t,e] (energy/mol); gibbs_minimization[t,p,j]: 0 = gibbs_scaling·(gibbs_mol_phase_comp[p,j] + Σ_e lagrange_mult[t,e]·element_comp[j,e]) for non-inert j; inert_species_balance[t,p,j]: F_in = F_out (skipped when linearly dependent on an element balance) | inlet, outlet | scaler: lagrange_mult by 1/(R·T_nominal) |
unit.flash@1 (Flash) | cv.lumped (has_phase_equilibrium = true, heat and pressure change default true) ⊃ unit.separator (outlet_list = [Vap, Liq], split_basis = phaseFlow, ideal_separation, ideal_split_map by phase type, mixed_state_block = cv.properties_out) | ideal_separation, energy_split_basis | split_fraction_eq[t,o]: split.split_fraction[t,o,o] = 1 when not ideal | inlet; vap_outlet, liq_outlet re-exported from the separator | |
unit.mixer@1 (Mixer) | no control volume; inlet_state[i][t] per inlet (defined_state = true), mixed_state[t] (or an external state) | inlet_list/num_inlets (domain inlet_idx), material_balance_type (elementTotal invalid), has_phase_equilibrium, energy_mixing_type ∈ {none, extensive}, momentum_mixing_type ∈ {none, minimize, equality, minimize_and_equality}, construct_ports | law instances: material mixing per balance type (0 = Σ_i F_in,i − F_mixed + PE terms + inherent gen), enthalpy_mixing[t]: 0 = Σ_i Σ_p H_i − Σ_p H_mixed; minimum_pressure[t,i] with minimum_pressure_constraint chain mp[t,i] = smooth_min(mp[t,i−1], P_i, eps_pressure) and mixture_pressure: P_mixed = mp[t,last]; pressure_equality[t,i]: P_mixed = P_i; minimize_and_equality builds both with equality inactive by default | one port per inlet name, outlet | init mixer (§17.3); scaler mixer |
unit.separator@1 (Separator) | no control volume; mixed_state[t] (defined_state = true) plus per-outlet states unless ideal | outlet_list/num_outlets, split_basis ∈ {totalFlow, phaseFlow, componentFlow, phaseComponentFlow}, material_balance_type, momentum_balance_type ∈ {none, pressureTotal}, energy_split_basis ∈ {none, equal_temperature, equal_molar_enthalpy, enthalpy_split}, ideal_separation, ideal_split_map, has_phase_equilibrium | split_fraction[t,o,(index by basis)] with sum_split_frac; material_splitting_eqn per balance type (sf·F_mixed = F_out + PE/inherent terms); temperature_equality_eqn, molar_enthalpy_equality_eqn, or molar_enthalpy_splitting_eqn (forbidden with component-based bases); pressure_equality_eqn. Ideal path: no outlet states; port members are expressions (mixed value or eps_flow/eps_frac by ideal_split_map) | inlet; one port per outlet | init separator; scaler separator |
unit.pressure_changer@1 and derived unit.compressor@1, unit.pump@1, unit.turbine@1 | cv.lumped with has_work_transfer = true, has_pressure_change = true | compressor, thermodynamic_assumption ∈ {isothermal, isentropic, pump, adiabatic}, support_isentropic_performance_curves | work_mechanical ≡ cv.work, deltaP ≡ cv.deltaP, ratioP[t] with ratioP·P_in = P_out; isothermal: T_in = T_out; adiabatic: work = 0; pump: work_fluid = (P_out − P_in)·flow_vol, efficiency_pump, actual_work (work_fluid = work_mech·η for compressors, reversed for expanders); isentropic: properties_isentropic[t] state, isentropic_pressure, state_material_balances, isentropic: s_isen = s_in, isentropic_energy_balance: work_isentropic = Σ_p H_isen − Σ_p H_in, actual_work with efficiency_isentropic, optional performance-curve helper with head_isentropic, head expressions | inlet, outlet | init isentropic_pressure_changer for isentropic; scaler defaults ratioP: 1, efficiencies 10 |
unit.valve@1 (Valve) | derives from pressure_changer with compressor = false, adiabatic, material_balance_type = componentTotal | valve_function ∈ {linear, quick_opening, equal_percentage, custom}, pressure_flow_method | valve_opening[t] ∈ [0,1] (fixed by default), valve_function[t] (opening, sqrt(opening), alpha^(opening − 1)), Cv, pressure_flow_equation[t]: F² = Cv²·(P_in − P_out)·valve_function² | inlet, outlet | init guesses outlet pressure (0.95·P_in fallback) |
unit.heat_exchanger@1 (HeatExchanger 0D) | two cv.lumped (hot_side, cold_side, each with has_heat_transfer = true) | side param groups, hot_side_name/cold_side_name aliases, flow_pattern ∈ {countercurrent, cocurrent, crossflow}, delta_temperature_method | overall_heat_transfer_coefficient[t], area, delta_temperature_in/out[t] with the pattern-dependent definitions, crossflow_factor[t], heat_duty ≡ cold_side.heat, unit_heat_balance: 0 = hot.heat + convert(cold.heat), delta_temperature[t] from the helper (lmtd: (dT1 − dT2)/ln(dT1/dT2); lmtd2, lmtd3 variants; lmtd_smooth (Kazi et al.) with eps_lmtd_smoothing; amtd; underwood ((cbrt(dT1) + cbrt(dT2))/2)³), heat_transfer_equation: convert(heat_duty) = [crossflow_factor·] U·A·ΔT | hot_side_inlet, hot_side_outlet, cold_side_inlet, cold_side_outlet plus aliases | init hx_0d; scaler hx |
unit.heat_exchanger_ntu@1, unit.heat_exchanger_lumped_capacitance@1 | two cv.lumped | as IDAES (effectiveness–NTU relations; wall temperature and heat holdup with ua_hot_side, ua_cold_side) | per IDAES source | four ports | |
unit.heat_exchanger_1d@1 (HeatExchanger1D), unit.shell_and_tube_1d@1 | two cv.distributed_1d with flow directions from flow_type (cocurrent: both forward; countercurrent: cold backward) | per-side param groups; common finite_elements, collocation_points, flow_type; shell-and-tube adds shell_is_hot and geometry | area, length ≡ hot_side.length, length_equality, heat_transfer_coefficient[t,x], heat_transfer_eq[t,x]: hot.heat = −U·area/length·(T_hot − T_cold), heat_conservation[t,x]: convert(cold.heat) = −hot.heat; shell-and-tube: shell_diameter, tube_outer/inner_diameter, number_of_tubes, cross-section constraints, temperature_wall[t,x], hot/cold heat-transfer equations through the wall | four ports plus Shell_*/Tube_* aliases | init hx_1d (duty guess then release) / shell_and_tube (wall temperature fixed then released) |
unit.ms_contactor@1 (MSContactor) | no control volume; per stream: stream[t,e] over elements = 1..n, optional inlet_state[t], side-stream states, reaction blocks; optional heterogeneous reaction blocks | streams map (per stream: property/reaction packages, flow_direction, has_feed, has_rate_reactions, has_equilibrium_reactions, has_energy_balance, has_heat_transfer, has_heat_of_reaction, has_pressure_balance, has_pressure_change, side_streams), number_of_finite_elements, interacting_streams, heterogeneous_reactions | material_transfer_term[t,e,(s1,s2,j)] (positive into s1), energy_transfer_term[t,e,(s1,s2)], per-stream holdups, extents, generation terms, stage-wise material/energy/pressure balances with side-stream feeds and the upstream-element rule by flow direction, volume[e], volume_frac_stream[t,e,s] | <stream>_inlet, <stream>_outlet | init ms_contactor (transfer terms fixed, per-stream SCC solve, holdup back-calculation) |
unit.skeleton@1 (SkeletonUnitModel) | nothing | authored symbols/equations; add_ports(name, member map) | user-declared | user-declared | init: solve when DOF = 0 |
unit.stream_scaler@1 | one state block; expression port members | — | multiplier; outlet members multiplier·var for flow-like members, pass-through otherwise (expression members, IDAES VarLikeExpression) | inlet, outlet | |
unit.sl_separator@1, unit.thickener_0d@1 | separate solid and liquid states; embedded separators | solid and liquid packages, energy_split_basis | liquid_recovery ≡ split.split_fraction[t, recovered]; thickener adds the Kynch flux, Stokes law, continuity and conservation equations with Conditional flux functions | five or six ports | |
control.pid@1 (PIDController) | references to process_var[t] and manipulated_var[t] | controller_type ∈ {P, PI, PD, PID}, mv_bound_type ∈ {NONE, SMOOTH_BOUND, LOGISTIC}, antiwindup_type, derivative_on_error, calculate_initial_integral | setpoint, gain_p/i/d/b, mv_ref, error (variable or expression), negative_pv, derivative symbols, mv_integral_component and its derivative, mv_unbounded expression, mv_eqn (smooth bound or logistic), mv_integration_eqn (with conditional-integration Heaviside or back-calculation), initial_integral_error_eqn; equations at t0 deactivated by default | none | scaler as IDAES |
Costing methods (FlowsheetCostingBlock, UnitModelCostingBlock, SSLW) are templates of kind costing_method (§19.5).
11.2 Worked template: the heater
template: unit.heater@1
kind: unit
idaes_class: idaes.models.unit_models.Heater
params:
include: [param_group.unit_base, param_group.heater_side] # dynamic, has_holdup; balance types, has_phase_equilibrium, has_pressure_change, property_package(+args)
submodels:
control_volume:
template: cv.lumped@1
bindings:
dynamic: "= dynamic"; has_holdup: "= has_holdup"
property_package: "= property_package"; property_package_args: "= property_package_args"
has_phase_equilibrium: "= has_phase_equilibrium"
material_balance_type: "= material_balance_type"; energy_balance_type: "= energy_balance_type"; momentum_balance_type: "= momentum_balance_type"
has_heat_transfer: true; has_pressure_change: "= has_pressure_change"
geometry: "= has_holdup"
symbols:
heat_duty[t]: {role: reference, reference_to: control_volume.heat[t], quantity: power}
deltaP[t]: {role: reference, reference_to: control_volume.deltaP[t], guard: "has_pressure_change and momentum_balance_type != none"}
ports:
inlet: {kind: material, direction: inlet, bound_to: control_volume.properties_in}
outlet: {kind: material, direction: outlet, bound_to: control_volume.properties_out}
display:
performance: [{label: "Heat Duty", expr: heat_duty[t]}, {label: "Pressure Change", expr: deltaP[t], guard: has_pressure_change}]
plans:
initializer: init.single_control_volume@1
scaler: scale.heater@1
Nothing in this template computes; the compiler instantiates the control volume, resolves the property package’s state template into properties_in[t] and properties_out[t], expands the three law instances, derives port members from the state template, and emits the equations of §10.3 with derivations back to this file.
11.3 Worked template fragment: isentropic pressure change
template: unit.pressure_changer@1
features:
compressor: {kind: bool, default: true}
thermodynamic_assumption: {kind: enum, enum: ThermodynamicAssumption, default: isothermal}
support_isentropic_performance_curves: {kind: bool, default: false}
submodels:
control_volume: {template: cv.lumped@1, bindings: {has_work_transfer: true, has_pressure_change: true, ...}}
properties_isentropic: {template: "= property_package.state_block", multiplicity: time, guard: "thermodynamic_assumption == isentropic",
bindings: {defined_state: false, has_phase_equilibrium: "= has_phase_equilibrium"}}
performance_curve: {template: helper.isentropic_performance_curve@1, guard: support_isentropic_performance_curves}
symbols:
work_mechanical[t]: {role: reference, reference_to: control_volume.work[t]}
ratioP[t]: {quantity: dimensionless, default_initial: 1}
efficiency_isentropic[t]: {quantity: dimensionless, default_initial: 0.8, guard: "thermodynamic_assumption == isentropic"}
work_isentropic[t]: {quantity: power, guard: "thermodynamic_assumption == isentropic"}
equations:
ratioP_calculation[t]: "ratioP[t] * control_volume.properties_in[t].pressure == control_volume.properties_out[t].pressure"
isothermal[t]: {expr: "properties_in[t].temperature == properties_out[t].temperature", guard: "thermodynamic_assumption == isothermal"}
zero_work_equation[t]:{expr: "control_volume.work[t] == 0", guard: "thermodynamic_assumption == adiabatic"}
isentropic_pressure[t]: {expr: "properties_isentropic[t].pressure == control_volume.properties_out[t].pressure", guard: isentropic}
isentropic[t]: {expr: "properties_isentropic[t].entr_mol == control_volume.properties_in[t].entr_mol", guard: isentropic}
isentropic_energy_balance[t]:
{expr: "work_isentropic[t] == sum(p in phase | properties_isentropic[t].enthalpy_flow_term[p]) - sum(p in phase | control_volume.properties_in[t].enthalpy_flow_term[p])", guard: isentropic}
actual_work[t]:
{expr: "if compressor then work_isentropic[t] == work_mechanical[t]*efficiency_isentropic[t] else work_mechanical[t] == work_isentropic[t]*efficiency_isentropic[t]", guard: isentropic}
law_instances:
isentropic_state_material: {law: state_material_balance, subject: "= material_balance_type", states: [properties_isentropic, control_volume.properties_out], guard: isentropic}
if compressor then … else … is a compile-time conditional on a feature, so P7 selects one branch and no Conditional node reaches the backend.
11.4 Derived templates
template: unit.turbine@1
derives_from: unit.pressure_changer@1
feature_rules:
- {rule: requires, antecedent: "true", consequent: "compressor == false"}
- {rule: requires, antecedent: "true", consequent: "thermodynamic_assumption == isentropic"}
plans: {initializer: init.isentropic_pressure_changer@1}
A derived template narrows features and parameters; it cannot add code. The three IDAES wrappers (Compressor, Pump, Turbine) and the valve are four such rows.
12. Connectivity
12.1 Ports
A port is a typed interface derived from a state instance, never a hand-maintained list of variables.
- Members come from the bound state template’s
define_port_membersdeclaration (default: the state symbols). For an IDAES-compatible FTPx state a material port has membersflow_mol,mole_frac_comp[j],temperature,pressure; each member row recordsquantity_type_idand the symbol group. - Kinds.
material(state-defined members),heat(a single power quantity),work,signal(a dimensionless or typed scalar). Heat and signal ports are how heat integration and controllers connect without inventing pseudo-streams. - Direction is declared (
inlet,outlet,bidirectional) and used only for topology inference and stream tables; equations never depend on it. - Distributed control volumes bind ports to
properties[:, first]for the inlet andproperties[:, last]for the outlet whenflow_direction = forward, reversed forbackward. The binding is aGatherwith a fixed coordinate on the length domain. - Expression members (IDAES
VarLikeExpression, used by the ideal separator and the stream scaler) are legal: a member may reference an expression symbol; the connection rule then produces an equation between an expression and a variable, which is still an affine equality when the expression is affine.
12.2 Connection rules
authored.connections records an arc; rule_template_id selects the physics of the junction. The reference package ships:
| Rule template | Generated equations | Use |
|---|---|---|
connection.equality@1 (default; Pyomo Port.Equality / IDAES expand_arcs) | for every member m and index i: from.m[i] == to.m[i]; family AFFINE_EQUALITY, role LINKING | one-to-one connections between identical state templates |
connection.translate@1 | none by itself; requires the destination to be a unit.translator instance whose authored mapping equations link properties_in to properties_out | different property packages |
connection.heat@1 | from.heat[t] == to.heat[t] with sign convention into the destination is positive | heat-integration graphs, wall models |
connection.signal@1 | from.value[t] == to.value[t] | controllers |
A connection whose ports have mismatched member sets or incompatible quantity types (different reference states, bases, or shapes) is invariant conn.member_mismatch; IDAES surfaces this only at arc expansion as a Pyomo error. Pyomo’s Port.Extensive rule (automatic splitting and mixing at multi-arc ports) is intentionally not provided: mixers and separators are templates with explicit physics (proposal §5.3).
12.3 Arc expansion is a pass, not a transformation call
P8 generates inferred.connection_equations (one row per generated equation with connection_id, member_ordinal, index, equation_id) and the corresponding compiled.math_indexed_equations rows, which P12 expands to scalar math_equations. There is no separate “expand arcs” step to forget: a snapshot with connections always compiles to a problem that contains them. Deactivating a connection for an initialization stage is a case_activations overlay (§17).
12.4 Value propagation is a case operation
IDAES propagate_state(arc, direction, overwrite_fixed) copies port member values from source to destination. The platform defines it as a runtime operation producing an overlay case:
propagate(connection, direction = forward | backward, overwrite_fixed = false):
for each member m and index i of the connection's ports:
if destination symbol is free, or overwrite_fixed:
case_specs.initial[destination symbol] := current value of source symbol
The operation writes rows, never mutates a model; initialization plans (§17) sequence such operations explicitly.
12.5 Topology closure
Pass P5 materializes:
inferred.instance_tree(transitive containment) andinferred.instances(expanded submodels, including per-time and per-node state instances);inferred.topology_edges: a directed multigraph of unit instances from connections (built inpetgraph, persisted as rows);inferred.scope_membersandinferred.boundary_crossingsfor everyauthored.scopesrow, using the cut-set rule (internal,external,inbound,outbound) of the metamodel doctrine; flowsheet-level element and energy audits and the diagnostics “external variables” check are queries over these relations;inferred.tear_candidates: for each cycle family of the unit graph, the edge set that breaks all cycles, computed as a feedback arc set:heuristicusespetgraph::algo::greedy_feedback_arc_setover the unit multigraph (it returns exactly the recycle edges on flowsheet-shaped graphs, and reproduces the FOQUS cycle-count choice under the platform’s edge weights), andmipsolves the minimum feedback arc set through the NL backend and a MILP solver. The search is library work; the tear policy — which connections may be torn and at what cost — is authored data onconnections.tear_cost. Tears feed the sequential-modular initialization plan (§17.4).
12.6 Translators, state junctions, and scalers
- Translator. The template declares two states and no equations; the author supplies mapping equations in the instance’s
instance_equations(an authored relation of the same shape astemplate_equationsbut instance-scoped). The compiler’s unit inference refusesenth_mol_in == enth_mol_outacross packages with different reference states unless the author converts explicitly, which is the failure mode IDAES leaves to the user. - StateJunction. One state, two ports; connection equations on both sides make it a named stream. Stream tables treat it as a stream anchor.
- StreamScaler. Outlet members are expressions
multiplier · inlet memberfor flow-like members (members whose quantity kind is extensive) and pass-through otherwise; the “flow-like” test is by quantity kind (extensive = true), not by name substring as in IDAES.
13. Flowsheets, time, and dynamics
13.1 The flowsheet template and time domain
unit.flowsheet@1 (IDAES FlowsheetBlock) declares:
| Parameter / feature | Rule |
|---|---|
dynamic (true, false, inherit) | top-level inherit resolves to false with a warning; nested flowsheets inherit the parent’s value; dynamic = true inside a steady-state parent is flowsheet.dynamic_in_steady_parent (IDAES DynamicError) |
time_set (breakpoints) | default [0]; a dynamic top-level flowsheet with the default set is promoted to [0, 1] |
time_units | required when dynamic; must be a time unit |
time (external domain reference) | if supplied, must be continuous when dynamic |
default_property_package | fallback for children with property_package = useDefault; resolved by walking instance_tree upward (IDAES _get_default_prop_pack) |
The time domain is an authored.domains row of kind time, continuous = dynamic, with its members in domain_members (steady state: one member t = 0; dynamic: the breakpoints, extended by discretization in P11). Every state instance and time-indexed symbol is indexed by this domain even in steady state, so state[0.0] addressing survives unchanged.
13.2 Dynamic and holdup inference
IDAES _setup_dynamics becomes four rules in inferred.instance_features:
dynamic(I) ← authored dynamic(I) = true
dynamic(I) ← authored dynamic(I) = inherit ∧ parent(I, P) ∧ dynamic(P)
has_holdup(I) ← authored has_holdup(I) = inherit ∧ dynamic(I) -- default follows dynamic
error(I, dynamic_in_steady_parent) ← dynamic(I) ∧ parent(I, P) ∧ ¬dynamic(P)
error(I, holdup_required) ← dynamic(I) ∧ authored has_holdup(I) = false
13.3 Accumulation terms and derivative symbols
When an instance is dynamic, the control-volume templates declare material_accumulation[t,(p,j)], energy_accumulation[t,p], element_accumulation[t,e] with role = derivative, wrt = time. P7 creates the symbol and a math_dae_links row linking it to its state (material_holdup, …). The balance equations reference the derivative symbol; nothing else changes. Steady-state instances substitute 0 for the accumulation contribution (a compile-time guard, not a runtime branch).
The IDAES steady-state initial condition convention (fix_initial_conditions: fix every accumulation at t0 to zero) is the reference case overlay overlay.steady_initial_conditions@1.
13.4 Discretization is a lowering pass (P11)
An authored.discretization_policies row (selected per domain by continuous_domains.discretization_policy_id or overridden by case_policies) attaches to a continuous domain (time or a control volume’s length domain). P11 implements Pyomo DAE’s semantics as data:
- Mesh.
finite_elements(nfe) equal-width elements over the domain bounds, retaining authored breakpoints; collocation addsncpinterior points per element (Radau roots include the element end, Legendre roots do not). Rows incompiled.meshes,compiled.mesh_nodes. - Symbol expansion. Every symbol indexed by the domain is expanded from its authored members to all mesh nodes (
lifecycle = generated_discretization, IDs from §5.1). - Discretization equations. For every derivative symbol
dof statevwith respect to the domain, one equation per node per the scheme, named<d>_disc_eq[...]:
| Scheme | First derivative at node k | Skipped at |
|---|---|---|
BACKWARD | d[k] = (v[k] − v[k−1]) / (t_k − t_{k−1}) | first node |
FORWARD | d[k] = (v[k+1] − v[k]) / (t_{k+1} − t_k) | last node |
CENTRAL | d[k] = (v[k+1] − v[k−1]) / (t_{k+1} − t_{k−1}) | first and last nodes |
LAGRANGE_RADAU | d[k] = Σ_{j=0..ncp} v[l+j]·adot[j][k−l] / (t_{l+ncp} − t_l) with l the element’s lower boundary | first node |
LAGRANGE_LEGENDRE | d[k] = Σ_{j=0..ncp} v[l+j]·adot[j][k−l] / (t_{l+ncp+1} − t_l); element-boundary nodes instead get continuity v[k] = Σ_j v[l'+j]·afinal[j] named <v>_<domain>_cont_eq | first node and element boundaries (for the derivative form) |
Second derivatives use adotdot (collocation) or the standard three-point stencils (finite difference); mixed derivatives are unsupported (diagnostic disc.mixed_derivative, as in IDAES). Stencil weights are stored in compiled.stencils so that the equations are data-generated Affine nodes.
- Integrals.
Integralnodes lower to trapezoid sums over the mesh (PyomoIntegralsemantics) unless a quadrature rule is attached. - Boundary filters on 1D balances (§10.4) are evaluated against the mesh.
- Scaling propagation. The IDAES post-discretization step (
scale_time_discretization_equations) is the rulesf(dv/dt) = sf(v) / sf(t)andsf(disc_eq) = sf(dv/dt),sf(cont_eq) = sf(v), applied by the scaling pass because P11 has already produced the rows; the “propagate after build” failure cannot occur.
13.5 Dynamic operations as case operations
| IDAES utility | Platform operation | Result |
|---|---|---|
copy_values_at_time(fs_tgt, fs_src, t_target, t_source) | copy_at_time over runtime.solutions and case_specs | new overlay case with initial values |
copy_non_time_indexed_values | copy_non_time_indexed | overlay |
copy_values_from_point(blk, t0) | broadcast_from_point | overlay |
deactivate_model_at(b, time, points) | case_activations rows for equations indexed by those points | overlay |
deactivate_constraints_unindexed_by, fix_vars_unindexed_by | selector-based activation/treatment rows | overlay |
initialize_by_time_element | plan.time_element_march@1 (§17.4) | plan |
get_derivatives_at, find_comp_in_block_at_time | queries over math_dae_links and symbols.index | — |
13.6 Trajectory backends
- Fully discretized NLP (default). The problem after P11 is an algebraic system; any NLP backend solves it. Consistent initial conditions are a first stage of the dynamic plan (solve at
t0with everything else inactive). - PETSc TS through the NL backend. The IDAES
petsc_dae_by_time_elementworkflow is reproduced as a plan: per time interval, the NL writer emits the subsystem at the target node with thedae_suffix(ALGEBRAIC = 0,DIFFERENTIAL = 1,DERIVATIVE = 2,TIME = 3) anddae_linksuffixes generated frommath_dae_links, runspetsc_tswith--ts_init_time/--ts_max_time, reads the trajectory, and writesruntime.solutionsfor the interval, interpolating skipped nodes. A fixed derivative with a non-zero value and a fixed differential variable paired with a free derivative are the same errors IDAES raises (dae.fixed_derivative,dae.fixed_state_free_derivative). diffsol(optional). A native DAE integrator fed directly from the evaluation program (residual and Jacobian-vector-product callbacks; the evaluation program’s sparse rows supply the product). It is a backend binding; the IR is unchanged; DiffSL, its own model language, is never used.diffsolcarries its own linear-algebra stack (diffsol-la/diffsol-nl, notfaer), accepted for the trajectory backend only; whether every discretized process model of §13 is a semi-explicit DAE thatdiffsolaccepts is verified per model by P16 (backend.unsupported_dae_class).
14. The compiler
14.1 Pass pipeline and contracts
Decision: ADR-0040, ADR-0041
Every pass is a reference.pass_specs row with pass_input_ports and pass_output_ports. The driver derives its DAG from named stage/port edges; reading a relation schema without an input binding is invalid. P0 resolves package headers from an input document bundle; P1 stages authored rows and change operations; P2 validates the candidate snapshot for atomic commit (§22.2). P1 does not mutate an existing committed snapshot. Compilation of a committed P2-valid snapshot starts at P3; P3 onward cannot output authored/reference relations.
An output bundle contains all declared output ports, including explicit empty relations. One port has one producer. The same compiled.math_* schema can appear in P7’s template bundle, P8’s law-completed bundle, P9’s method-completed bundle and P10’s typed bundle. Each stage reads its predecessor explicitly, preserves unaffected rows and publishes a complete immutable replacement bundle; it does not overwrite another stage’s artifact. Sidecar pass records and stage catalog rows describe these outputs and are excluded from the bundle they describe. Runtime operations similarly publish a complete named result bundle. All read dependencies, including registry and reference data, enter initial stage keys.
| Pass | Name | Reads (namespaces) | Writes | Preconditions → postconditions | Determinism | Main diagnostics |
|---|---|---|---|---|---|---|
| P0 | Package and schema resolution | package headers in the input document bundle, pinned reference.schema_* | normalized.package_graph | dependency versions satisfiable → every referenced package pinned by content hash | deterministic | pkg.unresolved, pkg.version_conflict, schema.version_mismatch |
| P1 | Authoring parse | package documents | authored.* (via change sets, §22.2), including the identity-based target rows (case_spec_targets, case_activation_targets, observation_targets) | documents parse → every entity registered with an id (explicit policy) or a name-derived id (named policy); every pse.expr_dsl syntactically valid with a source span; every pse.target_path resolved to entity identities | deterministic | parse.syntax, parse.unknown_key, parse.missing_id, parse.unresolved_target |
| P2 | Primitive-fact validation | authored.*, reference.* | provenance.pass_records, findings | — → all invariants of reference.schema_invariants hold | deterministic | one class per invariant kind |
| P3 | Canonicalization | authored.* | normalized.*, including parsed normalized.*_expr_*, finite candidate scopes and property_demand_seeds | validated → aliases resolved, defaults materialized, authored constants normalized to package unit sets; natural-unit kernel conversions remain explicit P9 edges, expression paths resolved against template scopes, derived IDs assigned | deterministic | canon.unresolved_path, canon.unit_mismatch |
| P4 | Type and capability closure | normalized.* | inferred.phase_species, inferred.instance_features, method compatibility | — → every feature resolved; every method selection compatible with its scope | deterministic (fixed point) | feature.rule_violation, method.incompatible |
| P5 | Topology and containment closure | normalized.*, inferred.instance_features | inferred.instances, instance_tree, ports, port_members, topology_edges, scope_members, boundary_crossings, tear_candidates | — → every port bound; every connection endpoint exists | deterministic | conn.member_mismatch, topology.dangling_port |
| P6 | Property demand closure | P3 seeds, P4 decided features/candidate compatibility, P5 bound scopes/domains, complete method selections/defaults and reference.method_specs | inferred.property_requirements, method_resolutions | — → every requirement resolved or explicitly unresolved | deterministic (fixed point) | prop.unsupported, prop.ambiguous, prop.unknown_subject |
| P7 | Template instantiation | normalized templates/parameters/bindings, P4 features, P5 scopes/domains, P6 resolutions and package quantity/reference contracts | compiled.symbols, symbol_groups, template equations as math_* (indexed), contributions | — → every guard decided; every symbol has quantity type and canonical unit | deterministic | template.guard_undecidable |
| P8 | Law expansion and connection equations | P7 indexed bundle/contributions, normalized laws, P5 connections, P6 resolutions | complete indexed math_* bundle (balances, connection equations), negative-completeness rows | — → every contribution included exactly once per applicable law | deterministic | law.unsupported_binding, law.basis_conversion_missing, law.excluded_candidate (info) |
| P9 | Method realization and kernel binding | P8 indexed bundle, P6 resolutions, method/kernel declarations and package bindings | complete indexed math_* bundle, kernel_bindings, explicit conversions | — → every KernelCall bound; every natural-unit method converted | deterministic | kernel.unbound_parameter |
| P10 | Math canonicalization and unit inference | compiled.math_* | canonical indexed math_* (ordered, hash-consed, typed) with residual quantity contracts | — → every node typed; DAG acyclic | deterministic | math.unit_inconsistent, math.cyclic_expression, math.domain_violation_static |
| P11 | Discretization lowering | math_*, authored.discretization_policies (via normalized), math_dae_links | meshes, nodes, stencils, expanded symbols, *_disc_eq, *_cont_eq (as indexed equations) | policies attached → no Derivative/Integral nodes remain for discretized domains | deterministic | disc.mixed_derivative, disc.missing_policy |
| P12 | Index expansion | math_indexed_equations, math_free_indices, valid_index_tuples, symbol_groups, meshes | scalar math_equations (one row per valid index tuple that passes the filter, with parent_indexed_equation_id); Gather nodes whose coordinates are fully bound rewritten to SymbolRef; boundary filters of §10.4 evaluated | every free index has a domain with materialized members → every equation is scalar; no Broadcast node and no free index remains; IDs per §5.1 | deterministic (tuples in domain-member order) | expand.unbound_index, expand.empty_domain (info) |
| P13 | Case binding | P12 scalar bundle, authored case specs/targets/activations/objectives/overlays and numerical policy | compiled.problems, variable_order, equation_order, bound_values, case_bound_substitutions | case overlays resolve → every symbol has a treatment, bounds, and an initial value or an explicit “none”; every fixed/parameter symbol has a bound-value row; parameter substitutions are explicit | deterministic (ordinals by semantic ID) | case.unknown_target, case.conflicting_overlay |
| P14 | Structural analysis and static attributes | problem, math_*, case_bound_substitutions | incidence, dm_partition, blocks, math_static_analysis, problems.degrees_of_freedom | — → DOF, DM partition, SCC order, per-equation families and roles, per-variable roles, all computed on the case-bound view with parameter_dependence recorded | deterministic | findings of §15.2 |
| P15 | Plan generation | problem, static analysis, package defaults, authored.solver_profiles, scaler and initializer templates | scaling_plans, variable_scales, equation_scales, initialization_plans, init_stages, solve_plans | — → a solve plan selected from static attributes only (§18.7) | deterministic | plan.no_feasible_solve_plan |
| P16 | Backend lowering | problem, plans | evaluation_programs, sparsity_patterns, backend_bindings, NL text, Pyomo bundle | backend chosen → every opcode supported or the binding is unsupported with the opcode list | deterministic per backend | backend.unsupported_opcode, backend.derivative_unavailable |
The useDefault resolution of balance types (representative state block’s defaults), IDAES’s _get_representative_property_block, is part of P4.
P12 is the pass that scalarizes indexed equation domains: P7–P11 keep every equation in its indexed form (one row per instance and declaration), and P12 is where the compact form is expanded, why, and with what IDs (doctrine P18 of the metamodel: expansion rules, domains, and generated identities are inspectable).
14.2 The rule compiler
Decision: ADR-0039, ADR-0044, ADR-0048
Set-oriented inference (P4–P6, P8’s contribution matching, P5’s scope closure) is written as reference.rule_specs, not as Rust loops over rows. A rule body is a typed RulePlanSpec (a bounded algebra: scan, filter, project, equi-join, anti-join for stratified negation, union, distinct, aggregate and unnest) compiled to a DataFusion LogicalPlan through LogicalPlanBuilder, optimized, and executed against the snapshot session. Example (rule for valid phase–species pairs):
rule phase_species_valid @1 stratum 1 head inferred.phase_species(material_system_id, phase_id, species_id, henry)
← material_system_species(M, J) ⋈ material_system_phases(M, P)
⋈ species(J, valid_phase_types) ⋈ phases(P, phase_type)
where phase_type ∈ valid_phase_types -- or valid_phase_types is null and phase is not aqueous
▷ authored.phase_species restricts (P, J) when the phase declares an explicit list
left ⋈ henry_declarations(J, P) → henry := present
Execution rules:
-
Strata run in order; within a stratum rules are evaluated to a least fixed point. Recursive closures (containment, subtype, reachability) use the generic fixed-point executor: seed, evaluate delta plans, deduplicate by semantic key, append, repeat until no new rows. DataFusion’s recursive CTE support may be used for closures whose provenance can be reconstructed from the result; where per-row derivations are required the external loop is used.
-
Negation is stratified: a rule may negate only relations fully computed in a lower stratum.
-
Four-valued predicates. Matching predicates return
true | false | unknown | conflict.truerows are written to the head relation.unknownandconflictrows are written toinferred.undecidedwith the rule, the key, a reason and the supporting rows, and produce diagnostics; a candidate is never silently dropped (metamodel P18), and no consumer can mistake an undecided fact for a true one because head relations hold only decided-true rows. Inside a plan,Expr::IsUnknown/IsNotUnknowncover the three SQL truth values;conflict(two rules asserting incompatible values for one key underconflict_policy = reject) is detected by the executor after the stratum’s fixed point. -
Every head row carries a derivation (
provenance.derivations: rule id, supporting rows, fingerprint) at the granularity the registry declares for the head relation (schema_relations.derivation_granularity):rowfor inference, law expansion and method resolution, where negative completeness is the deliverable;rulefor mechanically expanded relations (index expansion, discretization, connection equations), where the derivation is the rule plus the ID formula of §5.1 and is reconstructed on demand rather than stored per row. A benchmark in §24.3 measures derivation rows and bytes per equation so the granularity assignment is evidence-based. -
Determinism, and the engine as a declared input. Output relations are sorted by primary key before hashing (arrival order across partitions is nondeterministic and never assumed). The DataFusion session is built from a
reference.engine_profilesrow (§6.11): the DataFusion and Arrow crate versions; the ordered analyzer rule list installed withAnalyzer::with_rulesand the ordered optimizer rule list installed withOptimizer::with_rules(never the engine defaults, whose pipeline changes between releases;Analyzer::add_function_rewriteis never called after construction); the physical-optimizer rule list; and the semantic settings as an explicit, versioned key allow-list — everydatafusion.optimizer.*anddatafusion.sql_parser.*key,datafusion.execution.time_zone,datafusion.execution.skip_physical_aggregate_schema_check(assertedfalse) anddatafusion.execution.enable_ansi_mode(assertedfalse) — read back frominformation_schema.df_settings; a namespace match would miss the twoexecution.*keys. Platform policy that can change a plan (bound kernel outcome-policy identifiers and the null policy of §14.2 rule 3) is exposed as aConfigExtensionunderdatafusion.pse.*, so it appears indf_settingsand enters the same hash. These entries are generated views of the bound contract values; a session setting cannot independently override kernel null/strictness semantics. The profile artifact’s logical hash, the function registry hash (kernel digests) and the catalog snapshot are part of every rule-executing pass’s memo key (§14.3). The plan fingerprint is not part of the memo key: the plan is a pure function of the rule plan, the catalog snapshot and the engine profile, all of which are already in the key, so hashing the plan adds no information to reuse validity. Plan bytes are noncanonical diagnostic evidence under ADR-0044. Storelogical_plan_to_bytes_with_extension_codecoutput with its physicalencoding_checksum, engine version and platform codec version. The codec identifies snapshot providers by(snapshot_id, relation_id, schema_version, logical_hash)and UDFs by the complete kernel binding digest; decoding resolves only admitted pinned objects. Sorted HashMap insertion does not make encoding deterministic, and no two-process byte-equality assertion is allowed for equivalent plans.pgjsonEXPLAIN and ordered analyzer/optimizer observations accompany the artifact for attribution. They are outside semantic stage outputs and memo inputs. Round-trip and attribution tests, rather than byte-equality tests, validate the diagnostic route. -
Head semantic admission. Before execution the rule compiler infers complete field/quantity contracts for the typed expression graph, including UDF arguments and returns. Losing semantic metadata is not permission to reconstruct meaning from storage alone: the compiler must retain the source-to-expression typing derivation. At output admission compare the inferred contract and actual arrays with the generated head schema. Exact type, nullability and semantics are the default. A declared numeric conversion may use Arrow casting only after exact representability checks (including fractional-to-integer and large-integer-to-float cases); physical unit/basis/reference conversion invokes its named conversion and dependencies.
can_cast_typesandCastOptions { safe: false }do not prove losslessness. Attach the destination metadata only after these checks; validate nested values and required-field nulls, then execute the head invariants. Failure isrule.head_schema_mismatch, never silent relabeling. -
Key discipline. Distinct, union-dedup and join keys in a rule plan are key-role columns (semantic IDs, ordinals, enums) — never
Float64, because the engine’s value equality merges-0.0with+0.0and treats NaN as self-equal (§5.3). Null-bearing key comparisons usedistinct/not_distinctsemantics and Kleene booleans (and_kleene/or_kleene) so that a null-versus-null candidate is neither dropped nor matched by accident. The rule compiler rejects a violatingrule_plan_nodesrow (rule.float_key). -
Rule nodes in plans (deferred). Wrapping each rule body in a
LogicalPlan::Extensionnode that carriesrule_idsurvives the optimizer intact and would make rule attribution visible inEXPLAIN; it costs the 14-methodUserDefinedLogicalNodecontract and anExtensionPlanner. It is not adopted now: derivations andrules_firedalready answer “which rule produced this row”. Trigger for adoption: the §22.4 diff report or agent tooling needs per-rule attribution inside a plan rendering. -
Aggregate and unnest.
LogicalPlanBuilder::aggregategroups P8 contribution descriptors by declared law/scope/domain keys;collect_orderedsupplies terms in stable contribution-ID/index order, not physical arrival order. The generated operation includes an explicit order expression and required provenance inputs. Compilersumis exact checked integer/count arithmetic initially; floating value reductions require a separately supported numerical policy, while P8 groups expression descriptors instead of summing solver values.count,min,maxand collection have explicit null and empty-group rules from §6.11; missing input is not an evaluation failure. Unnest one declared list at a time throughunnest_columns_with_options, retaining the parent key; reject or produce no members for null lists as declared, and no members for empty lists. Where member order is semantic, the input carries explicit(ordinal, value)structs; unnest does not invent stable identities from physical row positions. Duplicate memberships and domain validity are checked by invariants. New operations require a real consumer and an independent expansion fixture.
Generated ScalarUDFImpl wrappers for kernels (§18.5) are registered in the session so that rule bodies can evaluate kernels in batch (for example, estimating bubble-point temperatures for every state instance in one plan during initialization-plan generation).
14.2.1 Optional predicate preimages
Decision: ADR-0048
ScalarUDFImpl::preimage can provide a contiguous input range for supported output comparisons. It is disabled for platform UDFs until R-25 qualifies a concrete consumer. A replacement predicate must be equivalent, including half-open endpoints, nulls, selected branch and floating representability; monotonicity alone is insufficient. A conservative range may be used only as candidate pruning with the original residual predicate retained. Start with exact discrete/affine cases, compare full query results with pruning disabled, then measure avoided decoding and total cost. No general thermodynamic inverse is inferred from this hook.
14.3 The pass engine
Decision: ADR-0041, ADR-0042, ADR-0044, ADR-0046, ADR-0050
- Complete stage memo. Key a pass by ID/version, every named input port’s schema and logical hash or explicit absence, registry/operator/quantity contracts, relevant package/domain/binding/provider-candidate artifacts, kernel implementation digests, and selected policies. Plan-executing passes also key the complete engine profile and generated function registry. The driver binds this immutable environment; undeclared file/catalog/config reads fail the contract. A changed candidate universe or absent optional relation is a changed input. Initially P7 and every other stage are cached as complete bundles, not per-instance tuples. Attempt metadata is excluded from semantic output membership; changed semantic lineage remains in the stage snapshot. Reusing equal relation storage does not authorize skipping a dependent stage whose complete inputs changed.
- Measured refinement. A complete per-instance input bundle may be considered only when representative cold/warm measurements show whole-stage recomputation dominates preparation and the candidate passes full incremental-versus-clean comparisons (R-22).
salsaremains deferred to the further R-01 trigger; neither a second dependency engine nor automatic read tracking is needed for initial execution. - Snapshot sessions, shared runtime. Each snapshot has a sealed catalog and its own
SessionContext, extension/UDF registries and ordered engine profile. The process driver injects a sharedRuntimeEnvwithSessionStateBuilder::with_runtime_env; compatible snapshots share one deploymentFairSpillPoolandDiskManager. Limits, spill settings, time provider and thread budget are explicit. Passes obtain sessions from the driver and cannot create private full-budget pools. - Accounted allocation. Platform canonicalization, binding, transfer and result buffers register a
MemoryConsumerand callMemoryReservation::try_growbefore allocation; estimates include temporary/coexisting copies and serialization buffers. Shared immutable buffers have one reservation owner for their lifetime, so readers do not double-charge them. Reserve before replacement, release with the final owner, and release temporary reservations on failure/cancellation.TrackConsumersPoolandPeakRecordingPoolprovide attribution; Arrow tracking pools have infallible reserve/resize and cannot enforce a rejection limit. DataFusiongrowand unregistered/native/solver allocations are outside the fallible guarantee. Pool accounting is reported alongside measured process peaks; no whole-process OOM immunity is claimed. - Failure and parallelism. Independent DAG stages may run concurrently within the shared memory/thread budget (§18.8). Reservation exhaustion becomes typed
ResourcesExhausted/runtime.resource_limitnaming the affected consumer and configuration; the supported relation envelope is §5.3. A pass publishes all its output ports after validation or none. Failed/cancelled attempts record terminal findings separately and cannot be mistaken for an empty successful result.
14.4 Incrementality
Decision: ADR-0041, ADR-0042
The initial contract promises complete dependency correctness and reuse at stage granularity. It does not promise minimal per-row or per-instance recomputation. Every reused bundle must equal a clean execution under the same complete inputs.
| Change | Recomputed or refreshed | Eligible reuse |
|---|---|---|
| Free-variable initial guess | P13 initial values and downstream stages whose inputs change | P0–P12; unchanged structural/program bundles after complete key comparison |
| Fixed-symbol value | P13 bound_values, applicable parameter/static substitutions and all dependent P14–P16 inputs; value-dependent scaling | P0–P12; tape topology/sparsity only if their complete inputs/assumptions are unchanged |
| Parameter value or treatment | P13 bindings/substitutions; P14 classification/incidence and dependent plans/preparation | P0–P12; equal complete input keys permit reuse |
| Species/domain membership, unit set, package or child-template binding | P3 onward where the declared package/domain/binding input changes; P7 entire bundle initially | Earlier/independent bundles with identical full keys |
| Feature flag or new connection | Resolved features/scopes and downstream template/law/method stages as declared by the DAG | Complete bundles whose inputs remain equal; no per-instance promise |
| Method selection, new candidate, or provider absence change | P4 compatibility, P6 closure and all consumers of changed resolutions | Only complete bundles with equal input keys |
| Initializer selection | Seed normalization, guarded/bound demand closure, realization and P15 plan binding | Unchanged model inputs and equal resulting bundles |
| Discretization policy | P11 onward | P0–P10 |
| Numerical policy | Case binding and preparation/affected analysis; policy-qualified backend validation | Ordered P10 model graph |
| Engine profile or generated function registry | Every plan-executing stage with that input and changed descendants | Non-plan stages with unchanged complete inputs |
| Kernel implementation or declared unit/failure/derivative contract | Capability resolution, bindings, analyses and prepared consumers | Unrelated complete bundles |
Fixed and parameter values are in compiled.bound_values; free initial guesses are in variable_order.initial. Binding refresh and program-topology reuse are separate decisions. P10 never substitutes case values. P14 records every fixed/parameter substitution or value-bound assumption used in classification, including zeros that alter incidence; derivatives and prepared programs retain the corresponding dependencies. A policy or value change cannot reuse stale bound segments simply because opcode topology is unchanged.
14.5 Closure proofs
At the end of P14 the compiler writes provenance.closure_report (one row per closure category of the metamodel doctrine §31: type, topology, scope, quantity, method, balance, optionality, equation, runtime) with status ∈ {closed, open} and the list of open items. A category is open while any inferred.undecided row belongs to it. A snapshot is publishable as a compiled problem only when every category is closed or the case explicitly declares the open items as intentional (unconnected optional ports, free decision variables).
15. Structural analysis and diagnostics
IDAES’s diagnostics (core/util/diagnostics_tools/**, core/util/model_statistics.py) become (a) predicates and aggregates over compiled and runtime relations, (b) graph algorithms over compiled.incidence, (c) linear-algebra analyses over the evaluation program’s Jacobian, (d) walks over the expression graph, and (e) auxiliary optimization problems built from the Jacobian or the problem. Every check writes runtime.diagnostics_findings rows; reports are views over findings. Thresholds are a relation (reference.diagnostic_thresholds) whose defaults are the IDAES DiagnosticsToolbox.CONFIG values.
15.1 Thresholds (defaults preserved from IDAES)
| Key | Default | Used by |
|---|---|---|
variable_bounds_absolute_tolerance / _relative_tolerance | 1e-4 / 1e-4 | near-bounds caution |
variable_bounds_violation_tolerance | 0 | at-or-outside-bounds warning |
constraint_residual_tolerance | 1e-5 | large residuals warning |
constraint_term_mismatch_tolerance | 1e6 (ratio) | mismatched terms |
constraint_term_cancellation_tolerance | 1e-4 (relative) | canceling terms |
max_canceling_terms | 5 | cancellation search |
constraint_term_zero_tolerance | 1e-10 | term-is-zero cutoff |
variable_large_value_tolerance / _small_ / _zero_ | 1e4 / 1e-4 / 1e-8 | extreme values |
jacobian_large_value_caution / _warning | 1e4 / 1e8 | extreme Jacobian rows, columns, entries |
jacobian_small_value_caution / _warning | 1e-4 / 1e-8 | same |
warn_for_evaluation_error_at_bounds | true | closed vs open interval tests |
parallel_component_tolerance | 1e-8 | near-parallel pairs |
absolute_feasibility_tolerance | 1e-6 | infeasibility explanation |
15.2 Structural checks (no numerical values required)
| Check id | Definition (relational) | IDAES method |
|---|---|---|
struct.degrees_of_freedom | DOF = |{free variables appearing in active equalities}| − |{active equalities}|; an equality is an equation with sense = eq; inequalities and definitions are excluded; fixed variables are excluded from the count but not from incidence | degrees_of_freedom |
struct.inconsistent_units | P10 rejects unit-inconsistent authored equations, so on compiled problems this is empty by construction; the check remains for authored assertions | display_components_with_inconsistent_units |
struct.singularity | Dulmage–Mendelsohn on the equality incidence graph (§15.3). Under-constrained set = unmatched ∪ underconstrained variables with underconstrained equations; over-constrained set = overconstrained variables with overconstrained ∪ unmatched equations; each reported per weakly connected component | report_structural_issues, display_underconstrained_set, display_overconstrained_set |
struct.evaluation_error | interval propagation (FBBT semantics, using fixed values as bounds) over each active equation body and objective; flag log/log10 when lb ≤ 0 (or < 0 when not warning at bounds), sqrt when lb < 0, tan when the node’s own bounds are unbounded, asin/acos when outside [−1, 1], Div when the denominator interval contains 0, Pow unless the exponent is provably a non-negative integer or the base is provably positive (IDAES integer-exponent rule: integer-domain variable, fixed integer, or integer-coefficient linear or quadratic form over integer variables) | display_potential_evaluation_errors |
struct.fixed_to_zero | fixed variables with value exactly 0 | display_variables_fixed_to_zero |
struct.unused_variables | variables not appearing in any active equation (count fixed separately) | display_unused_variables |
struct.external_variables | variables in a scope’s active equations whose owner instance is outside the scope | display_external_variables |
struct.no_free_variables | equations whose leaves are all parameters or fixed variables | display_constraints_with_no_free_variables |
stats.* | every model_statistics count (total, activated, deactivated blocks; total, activated, deactivated equalities and inequalities; variables total, fixed, unfixed, in activated equalities, only in inequalities, unused, fixed unused, derivative variables) as one SQL view with the IDAES definitions: “total” means on active scopes regardless of the item’s own activity; a ranged inequality with lower ≠ upper is neither equality nor inequality | report_statistics |
15.3 Native structural algorithms (pse-structural)
- Incidence construction. From
compiled.incidencerestricted to active equalities and free variables (IDAESIncidenceGraphInterface(include_inequality=False, include_fixed=False)). Linear-only participation is known from the canonical form: a symbol occurring only inAffineterms with a non-zero coefficient participates linearly; coefficients that fold to exactly zero after canonicalization do not create an edge (thestandard_repnrule). - Maximum matching: Hopcroft–Karp, own implementation.
petgraphships only general-graph matching (maximum_matching, Gabow’s algorithm, O(|V|³), input treated as undirected, and it panics on a degenerate node bound); the incidence graph is bipartite, so Hopcroft–Karp’s O(|E|√|V|) is required.is_bipartite_undirectedis asserted on the graph in debug builds. In CI the matching cardinality and structural rank are cross-checked againstscipy.sparse.csgraph.maximum_bipartite_matching/structural_rankand Pyomo’sIncidenceGraphInterfaceover the golden snapshots (§24.1). - Coarse Dulmage–Mendelsohn: projected digraphs over the matching, alternating-path reachability from unmatched nodes on both sides, giving the row partition (unmatched, overconstrained, underconstrained, square) and column partition (unmatched, underconstrained, overconstrained, square), with the matching order preserved.
- Block triangularization: requires a perfect matching; Tarjan SCC on the matching-projected digraph (reversed), condensation DAG, topological order made deterministic by ordering ties on semantic ID. Output
compiled.blockswithkind = sccandorder. - Connected components for independent subsystems.
- Tear selection (§12.5).
15.4 Numerical checks (require values)
The Jacobian is produced by the evaluation program (§18.2) at the current point and scaled as IDAES does: J_scaled = diag(sf_equation) · J · diag(1/sf_variable); an “Ipopt gradient-based” emulation (max_grad = 100, min_scale = 1e-8) is available for comparison.
| Check id | Definition | IDAES |
|---|---|---|
num.condition_number | Default: a Hager–Higham 1-norm condition estimate (a handful of sparse LU solves), reported as an estimate. On request: the IDAES-parity Frobenius form ‖J‖_F·‖J⁻¹‖_F — for square J through faer sparse LU solves against the identity (O(n) solves, so it is an opt-in expensive diagnostic with the cost stated in the finding), for non-square J through faer::linalg::svd::pseudoinverse_from_svd_with_tolerance with the tolerance a declared threshold (pseudo_inverse_tolerance, default 1e-12 relative). “Exactly singular” is reported as undefined; a 2-norm estimate via the smallest singular values (§15.5) is also stored | jacobian_cond |
num.large_residuals | per active equation: r = max(lb − body, 0), then max(r, body − ub); flag if r·sf > tol; unevaluable bodies are flagged with a null residual | display_constraints_with_large_residuals |
num.bounds_violation | sf·v ≤ sf·lb − tol or sf·v ≥ sf·ub + tol | display_variables_at_or_outside_bounds |
num.near_bounds | mag = ub − lb, or the single bound magnitude, or 0; tol = max(abs_tol/sf, mag·rel_tol); flag if within tol of either bound | display_variables_near_bounds |
num.extreme_values | m = sf·|v|; flag if m > large or (m < small and m > zero) | display_variables_with_extreme_values |
num.near_zero, num.none_value | sf·|v| ≤ zero_tol; value missing | display_variables_with_value_near_zero, display_variables_with_none_value |
num.extreme_jacobian_rows / _columns | row (column) 2-norms outside [small, large]; an empty row has norm 0 and is always flagged small | display_constraints_with_extreme_jacobians, display_variables_with_extreme_jacobians |
num.extreme_jacobian_entries | |J_ij| ≥ large or 0 < |J_ij| ≤ small | display_extreme_jacobian_entries |
num.parallel_rows / _columns | bucket vectors by the sorted support of entries with |a| > tol and |a|/max > tol; within a bucket, flag pairs with | |u·v| − ‖u‖‖v‖ | ≤ tol or ≤ tol·max(‖u‖, ‖v‖) (anti-parallel counts) | display_near_parallel_constraints, _variables |
num.mismatched_terms | for every sum node (with the equation sides negated into one sum): if max|term| / min|term| ≥ 1e6 over terms above the zero cutoff | display_constraints_with_mismatched_terms |
num.canceling_terms | combinations of 2..max_canceling_terms terms whose sum is ≤ tol·max|term|; products distribute a sum child; divisions divide numerator terms by the denominator value; equality nodes are analysed only when at least one side is a sum | display_constraints_with_canceling_terms, display_problematic_constraint_terms |
num.outside_valid_range | values outside method_specs.validity for the property | list_components_with_values_outside_valid_range |
15.5 Advanced analyses
- SVD toolbox. Smallest
ksingular values of the scaled equality Jacobian: densefaerSVD belowsvd_dense_limit(default 2,000 rows); above it, shift-invert Lanczos overfaer::matrix_freewith the operator(JᵀJ)⁻¹applied through a sparse LU factorization ofJ(partial_svdalone targets the largest singular values and cannot be used for this); problems whose LU factorization fails fall back to a declaredsvd.unavailablefinding rather than a dense attempt; rank deficiency count belowsingular_value_tolerance(1e-6); for each small singular value, the variables and equations whose singular-vector components exceedsize_cutoff_in_singular_vector(0.1). - Degeneracy Hunter (Dowling and Biegler). Stage 1 candidate MILP:
Jᵀν = 0,ν ∈ [−M − m_small, M + m_small], binariesy_pos,y_negforcing|ν| ≥ m_smallwhen set,Σ(y_pos + y_neg) ≥ 1, minimizeΣ|ν|; columns withΣ|J_ij| ≤ 1e-6skipped. Stage 2 irreducible degenerate set MILP per candidate: fixν_k = 1,Jᵀν = 0,−M·y ≤ ν ≤ M·y, minimizeΣy; members are equations withy > 0.9. Both are problems built ascompiledartifacts and solved through the NL backend with a MILP solver profile (scipdefault,cbc). - Ill-conditioning certificate (Klotz): LP
Jᵀy = res,Σy_pos − Σy_neg = 1, minimize‖res‖₁, then minimize‖y‖₁at fixed residual norm; report components by|y|down toratio_cutoffof the largest. - Infeasibility explanation (minimal intractable system). Elasticize in three rounds (variable bounds; inequalities; everything) with slack variables, recording violated items and re-fixing their slacks; then a deletion filter over the elastic set (deactivate each; if the subproblem becomes feasible the item is essential). Output: the MIS and the guard set. Implemented as a plan over case overlays and slack symbols, not as model cloning.
- Convergence analysis. A case set (§19.3) solved with Ipopt; per sample record iterations, iterations in restoration, iterations with regularization, CPU time, and whether the numerical checks raise warnings; compare to a baseline with
rel_tol = 0.1,abs_tol = 1.
15.6 Reports
report_structural_issues and report_numerical_issues are projection views: model statistics, warnings (ordered as IDAES orders them: DOF, units, structural singularity, evaluation errors; residuals, bounds, extreme Jacobian norms at warning thresholds, parallel components), cautions (fixed-to-zero, unused; near bounds, near zero, extreme values, none values, mismatched, canceling, no free variables, extreme Jacobian at caution thresholds), and next-step check ids. assert_no_structural_warnings and assert_no_numerical_warnings are the same views with an error policy, usable as CI gates.
16. Scaling as an explicit transformation
16.1 Model
Scaling is data in compiled.variable_scales (scale, offset, source) and compiled.equation_scales (scale, scheme), both with derivations. The transformation is
x = x_ref + D_x x̂ (D_x = diag(1/scale_v), x_ref = offset)
r̂(x̂) = D_r r(x_ref + D_x x̂) (D_r = diag(scale_e))
Ĵ = D_r J D_x
Backends consume it in one of two ways, recorded in solver_profiles.scaling_mode: the native and NL backends pre-scale the problem (equivalent to Pyomo’s scale_model = true writer option) and unscale results on read-back; the NL backend can alternatively emit the scaling_factor suffix and set nlp_scaling_method = user-scaling for Ipopt. Expression scaling hints (IDAES scaling_hint on named expressions) are variable_scales rows for expression symbols and serve only the nominal-value algebra.
Validity: a scale is strictly positive and finite; values are clamped to [min, max] per component kind (1e-10, 1e10) with a debug finding when clamping occurred.
16.2 Sources of scaling factors, in precedence order
Source (ScaleSource) | Rule | IDAES origin |
|---|---|---|
user | case_specs.scaling_factor overlay | set_scaling_factor, JSON scaling files |
template_default | template_scaling_defaults rows (IDAES DEFAULT_SCALING_FACTORS), including the markers user_input_required (error unless a user value exists), user_input_recommended, user_set_manually | CustomScalerBase.DEFAULT_SCALING_FACTORS |
package_default | authored.default_scaling by property kind and index | default_scaling_factors |
units | UNIT_SCALING_FACTORS: per quantity kind a reference unit and factor (temperature: 1e-2 per K; pressure: 1e-5 per Pa), converted to the symbol’s unit | scale_variable_by_units |
bounds | mag = 0.5(ub + lb) when both bounds, else the single bound; sf = 1 if mag = 0 else 1/|mag| | scale_variable_by_bounds |
nominal_magnitude | quantity_types.nominal_magnitude (§8.5); IDAES’s SI table (temperature 100 K, pressure 1e5 Pa, molar density 100 mol/m³, molar enthalpy 1e4 J/mol, entropy 100 J/mol/K, fugacity 1e4 Pa, mole fractions 1e-3, mw 1e-3 kg/mol), rounded to the nearest power of ten in the package unit | populate_default_scaling_factors |
definition_constraint | for v == f(w…): substitute every other symbol by its nominal value 1/sf_w (or 1/value clamped, or 1), solve the scalar equation for v (Newton; nonlinear-in-v or zero-derivative cases are errors), sf_v = 1/|v| | scale_variable_by_definition_constraint |
propagated | state-to-state by symbol name and index (propagate_state_scaling); derivative sf(dx/dt) = sf(x)/sf(t); discretization equations inherit the derivative’s factor, continuity equations the state’s | propagate_state_scaling, scale_time_discretization_equations |
magnitude | sf = 1/|value| when |value| > zero_tolerance, else 1 | AutoScaler.scale_variables_by_magnitude |
16.3 Nominal value algebra (constraint scaling without a solved point)
get_sum_terms_nominal_values(expr) returns one signed nominal value per additive term. It is an operator-indexed evaluation over the IR (a native visitor, generated from the operator table so it cannot drift from the opcode catalog):
| Node | Result |
|---|---|
| symbol (variable) | sign/sf where sign is +1 if lb ≥ 0 or the domain is non-negative, −1 if ub ≤ 0 or the domain is non-positive, else the sign of the current value (+1 if none); if the symbol has no scale but has a value, the value itself |
| symbol (parameter) | its value |
| expression symbol with a hint | ±1/hint (sign from the current value, + if unevaluable); the subtree is not descended |
Affine, equality/inequality/range | concatenation of children’s term lists (coefficients applied) |
Mul | outer product of the two lists |
Div | [Σ(num)/Σ(den)], denominator 0 replaced by 1 with a finding |
Pow | [|Σ(base)|^Σ(exp)] |
Neg, Abs | applied termwise |
| transcendental | function applied to Σ(child) |
Conditional | concatenation of both branches |
KernelCall | kernel evaluated at Σ of each input |
| constant, unit | the value; 1 |
Constraint schemes over the non-zero nominal terms n_i (default inverse_maximum):
| Scheme | sf |
|---|---|
harmonic_mean | Σ_i 1/|n_i| |
inverse_sum | 1/Σ_i |n_i| |
inverse_root_sum_squared | 1/√(Σ_i n_i²) |
inverse_maximum | 1/max_i |n_i| |
inverse_minimum | 1/min_i |n_i| |
Nominal derivative norm (scale_constraint_by_nominal_derivative_norm(norm)): for each variable v in the equation, set every other variable to its nominal value 1/sf_w, set v to 1 (its perfectly scaled value), evaluate ∂body/∂v · (1/sf_v) with the evaluation program’s derivative; sf = 1/‖(pjac_v)‖_norm. The Jacobian-norm autoscaler is the same computation at the current point, composing with existing factors: sf_new = sf_old/‖row‖.
16.4 Scaler templates
A scaler template declares the four-stage pipeline IDAES’s CustomScalerBase.scale_model runs (variable routine, first fill-in, constraint routine, second fill-in), the defaults, and the submodel dispatch order (user-supplied scaler for the submodel → package default state or reaction scaler → the template’s default scaler). Reference scaler templates exist for every unit template of §11 with the IDAES routines (for example scale.cstr: control-volume routine, deltaP by default, heat[t] by 1/(0.1·Σ_p nominal enthalpy inflow), volume forced to 1e3, reaction rates 1e-3, constraints by inverse_maximum; scale.gibbs: lagrange_mult[t,e] by 1/(R·T_nominal), constraints by inverse_sum).
16.5 Profiling and persistence
The scaling profiler is a plan that runs the case under a matrix of scaler methods (variables only; each nominal scheme; nominal L1 and L2 norms; actual L1 and L2 norms), with perfect (magnitude-based) and user variable scaling, and records the condition number and the Ipopt iteration statistics (iterations, restoration, regularization) per cell in runtime.profile_results. Scaling factors are relations; the IDAES JSON scaling-file format is an import/export adapter.
17. Initialization plans
17.1 Plan model
An initialization plan is a compiled.initialization_plans row with ordered init_stages. Stage kinds:
| Kind | Semantics |
|---|---|
apply_overlay | activate an overlay case (treatments, fixes, activations, initial values); overlays stack |
propagate | copy values along connections (§12.4) |
estimate | run an estimation kernel (bubble/dew Newton estimates, Rachford–Rice vapor fraction, outlet-state estimation) writing initial values |
solve_subset | solve the equations selected by the target with the stage’s solver profile; DOF must be 0 unless the stage says otherwise |
solve_blocks | block-triangular sequence: 1×1 blocks by scalar Newton (calculate_variable_from_constraint), larger blocks by the profile’s solver; inputs of each block fixed to values from earlier blocks |
continuation | homotopy schedule (§17.5) |
call_plan | run a submodel’s plan (state blocks, reaction blocks, plug-ins) |
check | DOF, perfect matching, residual, or missing-value check with a failure policy |
restore | discard overlays down to a named level, keeping solution values |
State snapshot and restore follow IDAES StoreState semantics: at the end of a plan the fixed status of every variable and the active status of every equation are restored to the pre-plan case, values of variables that were fixed are restored, and values of free variables keep their solved values. Because overlays are immutable rows, “restore” is discarding overlays, not replaying mutations.
Every stage run is a runtime.runs row with parent_run_id = the plan run and stage_id; failures carry the stage.
17.2 Standard plan templates
| Plan template | Stages | IDAES source |
|---|---|---|
init.block_triangularization@1 | check(DOF = 0); check(perfect matching) (failure ⇒ structural singularity); solve_blocks over the SCC order with the block solver profile (tol = 1e-8, max_iter = 200, scaled); solve_subset(all) unless skip_final_solve; postcheck | BlockTriangularizationInitializer |
init.single_control_volume@1 | for 0D: call_plan(properties_in); estimate(outlet ← inlet, with the IDAES pressure/temperature special cases when deltaP/deltaT are fixed) or copy values when copy_inlet_state; call_plan(properties_out); reactions: apply_overlay(fix external variables), call_plan(reactions), restore; solve_subset(unit). For 1D: estimate(states along x), call_plan(properties). Plug-ins: prepare (deactivate) before, initialize and finalize after, then solve_subset(all) when plug-ins exist | SingleControlVolumeUnitInitializer, ModularInitializerBase |
init.from_data@1 | apply_overlay(values from a solution or file, free variables only); postcheck | FromDataInitializer |
init.modular_properties@1 | 0 apply_overlay(fix state symbols; deactivate sum_mole_frac_out, equilibrium constraints when state is phase-component flows, inherent equilibrium); 1 estimate(critical properties; Tbub, Tdew, Pbub, Pdew by damped Newton with the IDAES TOL = 0.1, 30 iterations, ±50 K step clamp; transition compositions) then solve_subset(bubble/dew/critical equations only); 2 estimate(_teq by the smooth max/min clipping); 3 estimate(state_initialization: K-values from Psat/Henry, vapor fraction by Tbub/Tdew position or modified Rachford–Rice, phase compositions); 4 apply_overlay(fix temperature), activate flow balances, phase fraction and mole-fraction equations, log_* equations with back-calculated values, equilibrium constraints (unless state is phase-component flows), the pair’s VLE constraints; solve_subset when DOF = 0; 5 activate all except do_not_initialize equations, unfix temperature, seed log_* symbols as log(max(value, 1e-8)), solve_subset; 6 restore | ModularPropertiesInitializer |
init.mixer@1 | call_plan(each inlet state); estimate(mixed: pressure ← min inlet, flow-like ← sum, others ← mean); call_plan(mixed); if pressure equality active: overlay deactivating it and fixing P_mixed to the first inlet, solve_subset, restore; else solve_subset | MixerInitializer |
init.separator@1 | call_plan(mixed); overlay deactivating everything but sum_split_frac, solve_subset if DOF ≠ 0, restore; ideal ⇒ done; estimate(outlet states: intensive unchanged, flows × split fraction); call_plan(outlets); solve_subset | SeparatorInitializer |
init.hx_0d@1, init.hx_1d@1, init.shell_and_tube@1 | duty guess 0.25·U·A·(T_hot,in − T_cold,in) fixed on both sides with the heat-transfer equations inactive, solve, then release and solve; the shell-and-tube variant fixes temperature_wall to the mean inlet temperature and solves the hot side alone first | HX0DInitializer, HX1DInitializer, ShellAndTubeInitializer |
init.isentropic_pressure_changer@1 | performance curves deactivated with ratioP (seeded 1.8 / 0.7) and efficiency (0.8) fixed; call_plan(control volume); copy outlet values to the isentropic state; fix isentropic temperature (or enthalpy) and deactivate isentropic, solve; release and solve; reactivate curves, unfix, solve | IsentropicPressureChangerInitializer |
init.valve@1 | estimate(outlet pressure from ratioP, deltaP, or 0.95·P_in), then the adiabatic pressure-changer plan | Valve.initialize_build |
init.ms_contactor@1 | snapshot; fix transfer terms and volume fractions, extents to 0; deactivate non-balance equations; solve_blocks per stream; restore; if holdup: fix holdups with holdup equations inactive, solve, back-calculate holdups, solve | MSContactorInitializer |
init.feed@1 | call_plan(properties) | FeedInitializer |
The postcheck (check(residuals ≤ constraint_tolerance·sf; no missing values)) uses scaled residuals exactly as IDAES’s large_residuals_set does. Status values are Ok, Failed, DoF, PrecheckFailed, Error.
17.3 Plug-ins and initialization order
inferred.initialization_order(instance, ordinal) lists the instance followed by its plug-ins (costing blocks register themselves). Plans run prepare in order and finalize in reverse, as ModularInitializerBase does.
17.4 Flowsheet and dynamic plans
plan.sequential_modular@1: order units topologically with tears frominferred.tear_candidates; for each unit in order run its plan thenpropagatealong outgoing connections; iterate tear streams by direct substitution or Wegstein (accelclamp[−5, 0],iterLim = 40,tol = 1e-5, absolute or relative); finish withsolve_subset(all).plan.time_element_march@1(IDAESinitialize_by_time_element): supported schemesLAGRANGE_RADAUandBACKWARD(others rejected); overlay deactivating all butt0, solve consistent initial conditions; deactivate time-unindexed equations and fix time-unindexed variables; for each element, reactivate its nodes, fix the differential variables and derivatives at the element start (or every variable at or before it whenfix_diff_only = false), copy values forward, solve, then deactivate again; restore.- PETSc time stepping and consistent-initial-condition solves (§13.6) are plan templates over the NL backend.
17.5 Continuation (homotopy)
stage.kind = continuation reproduces the IDAES homotopy meta-solver: for fixed symbols v_i with targets t_i, the continuation parameter n runs from 0 to 1 with v_i = t_i·n + v_i,0·(1 − n); parameters step_init = 0.1, step_cut = 0.5, iter_target = 4, step_accel = 0.5, max_step = 1, min_step = 0.05, max_eval = 200 with the IDAES validity ranges; after a successful step s ← s·(1 + step_accel·(iter_target/iterations − 1)) clamped to [min_step, max_step]; on failure roll back to the last solved overlay and s ← max(min_step, s·step_cut); terminations optimal, minStepLength, maxEvaluations, infeasible, other (converged only with regularization). Each step is a runtime.runs row; the progress fraction is recorded.
18. Backends
18.1 The CanonicalMathProblem
A problem (compiled.problems plus variable_order, equation_order, canonical math_*, incidence, sparsity_patterns, variable_scales, equation_scales, kernel_bindings, solve_plans) is what every backend consumes. Ordinals are assigned by sorting on semantic ID; positions in the solver vectors are variable_order.position and equation_order.position. Fixed and parameter symbols are not in the solver vector; their values are bound parameters of the evaluation program.
18.2 The native evaluation program
Decision: ADR-0047, ADR-0046
pse-numerics lowers the ordered expression DAG into control-flow regions with owned numerical workspaces:
- Compile reachable expressions in deterministic order, but represent every runtime
Conditionalas a guard and separate then/else regions with a result merge. Only the selected region executes for a scalar; batched execution carries the corresponding row mask. A shared expression is computed once within a valid execution region, or reused across regions only with proof that doing so preserves guard/error behavior. A flat unconditional topological sweep is not a conditional evaluator. - Instructions name opcode, output/input slots and typed payload. Preserve operation and reduction order by default;
Affineevaluates its ordered children and coefficients without incidental FMA. Opted-in reordering/contraction follows the persisted numerical policy and enters the program key (§7.3). Never usetarget-cpu=nativeto choose semantics implicitly. - Residual, inequality and objective segments use the declared complete quantity contracts. Fixed/parameter values come from the separately refreshable
bound_valuessegment, not free initial guesses. - Reverse-mode Jacobian sweeps and forward-over-reverse Hessian sweeps follow only the executed branch; branch-boundary nonsmoothness and implicit-system singularities use declared outcomes. Sparse patterns may conservatively include both branches, but excluded branches cannot execute merely to fill derivative slots. Kernel derivative implementations are selected from
derivative_bindingsand validated under their conditions. Exact-Hessian requests fail when unavailable unless the profile explicitly selects a limited-memory fallback. - Domain violations or non-finite intermediate/results produce a recoverable typed evaluation error carrying equation, node and selected policy. Ipopt callbacks translate it without unwinding (§18.3). Invalid output is never an ordinary null or a successful last iterate.
- Batch evaluation uses the same operation/branch contracts. DataFusion
Expr::Case/CaseExprprovides guarded relational execution; opaque eager UDFs are called only within selected branches. Tolerant analysis uses the explicit outcome relations in §18.5.
Borrow null-free contiguous primitive arrays through PrimitiveArray::values and an owning ScalarBuffer/array handle whose lifetime covers the compiled consumer. These safe views incorporate the slice offset: an eligible one-chunk slice is not rejected solely for a nonzero offset. Multiple chunks, coercion/layout requirements or invalid values take a checked, reserved copy path; null fixed/parameter values are rejected, not silently copied into a numerical slot. Never construct the view by indexing a raw base pointer. Retain immutable owners through callbacks, cancellation and Python stream draining. Measure the benefit against the copy path before claiming zero-copy performance.
Implicit-kernel derivatives may use the pinned num-dual implicit-derivative APIs behind a declared implementation binding and independent fixtures. Arrow supplies storage and views; it does not own solver mutation or certify the derivative contract.
18.3 In-process Ipopt
Decision: ADR-0028
pse-backend-native binds the Ipopt C API (CreateIpoptProblem, IpoptSolve, AddIpoptNumOption/StrOption/IntOption) with callbacks for eval_f, eval_grad_f, eval_g, eval_jac_g, eval_h over the evaluation program. Options come from solver_profiles; the IDAES defaults are shipped as profiles ipopt.default (tol = 1e-6, max_iter = 200, nlp_scaling_method = gradient-based) and ipopt.user_scaled (nlp_scaling_method = user-scaling, or pre-scaled problem with nlp_scaling_method = none), with linear_solver resolved when a run starts from runtime.host_capabilities (§6.13), which the explicit runtime operation probe_host writes (Ipopt version, available linear solvers, HSL, PETSc, the Python environment); a profile naming ma57 on a host without HSL fails with capability.backend unless the profile declares fallback_linear_solver = mumps, in which case the fallback is a selected policy, and the options actually passed to CreateIpoptProblem are recorded in runs.resolved_options so that a reproduction on a different host is either identical or visibly different. Results: primal values (unscaled), duals, bound multipliers (zL, zU), termination status mapped to TerminationStatus, iteration statistics taken as data inside the intermediate callback through GetIpoptCurrentIterate and GetIpoptCurrentViolations (Ipopt ≥ 3.14; probe_host records the version and an older Ipopt degrades to parsing the solver’s text output, recorded as such) into runtime.iterations (objective, primal and dual infeasibility, mu, step size, regularization, restoration flag). The -sys crate is generated with bindgen from IpStdCInterface.h, with the generated bindings committed and diffed in CI as §4.2 treats generated relation code; index_style is C (0) and asserted.
Interruption across the C boundary: the driver checks the cancellation token inside the intermediate callback and returns false to stop the solve. No Rust panic and no unwind ever crosses an Ipopt callback; a callback catches the failure, records it in the driver state, and returns false, and the driver converts the recorded failure into solve.evaluation_error or runtime.cancelled after IpoptSolve returns. The last iterate is written to runtime.solutions under the run’s terminal status, so a cancelled or failed run is distinguished from a converged one by runs.status, never by the absence of rows.
18.4 The NL backend
pse-backend-nl writes the AMPL NL format directly from the problem (linear and nonlinear parts, common subexpressions as defined variables, bounds, objective, suffixes) and reads SOL files. It is the solver-neutral route and the only route to ipopt_sens, k_aug, dot_sens, bonmin, couenne, cbc, clp, scip, petsc_snes, and petsc_ts. Suffixes emitted: scaling_factor (when the profile asks for user scaling), dae_suffix and dae_link (PETSc), dual, ipopt_zL_out, ipopt_zU_out on read-back. Symbolic labels (.row, .col files) are always written so that solver messages and PETSc trajectories map back to semantic IDs.
Subprocess lifecycle: a solver process belongs to the run that started it. Cancellation or a wall-time limit kills the process group; a SOL file is ingested only when the process exited normally and the file’s trailer is complete, otherwise it is discarded and the run ends runtime.cancelled or solve.solver_error. A partial SOL file is never parsed.
Kernels that are not expression-expandable reach NL solvers only through an AMPL external-function library. The platform exports one compiled library per kernel package (funcadd entry points generated from KernelSpec, including the IDAES-compatible symbols cubic_root_l, cubic_root_h, cbrt, and the Helmholtz function names) so that the same binaries IDAES uses work unchanged; KernelSpec.bindings must list nl_external_function for the binding to be valid, and P16 refuses otherwise.
18.5 Kernel adapters generated from KernelSpec
Decision: ADR-0043, ADR-0047
The generator derives adapters only for KernelSpec.bindings entries with a compatible implementation and named conformance suite. It validates natural units, null policy, effects, argument evaluation and derivative routes before emitting code. There is no independent adapter default that can change mathematical behavior.
| Adapter | Generated mechanism | Semantic boundary |
|---|---|---|
| Scalar | Result-returning call to the declared scalar implementation | Complete input/parameter types and natural units; missing input follows its declared policy; domain/implementation failures are typed errors |
| Dual / hyper-dual | Declared generic or explicit derivative implementation | Availability and validity come from derivative_bindings; a scalar signature cannot manufacture derivatives |
| Batch Arrow, fail-fast | Declared batched body or mechanical scalar mapping | Missing inputs propagate only if declared; an invalid computation fails the operation and cannot turn into a value null |
| Tolerant batch analysis | Separate outcome-producing operation | Emit one kernel_evaluation_outcomes row for every requested coordinate/output; failure reason is mandatory and value is absent. Consumers must select an explicit outcome policy before aggregation |
| DataFusion scalar UDF | Exact storage signature; field-aware argument checks through ReturnFieldArgs.arg_fields and ScalarFunctionArgs.arg_fields; return field from the declared output | Validate complete quantity types before attaching metadata. Derive is_strict from propagation policy and is_nullable from admitted missing input; pure declarations alone permit Immutable. Preserve ColumnarValue::Scalar inputs and use number_rows for cardinality; allocate arrays only when required |
| Guarded expression | Expr::Case/CaseExpr or the native branch regions | short_circuits and conditional_arguments describe optimizer restrictions; they do not make generic UDF arguments lazy. A guarded opaque signature without an expression/branch implementation is unsupported |
| NL external function | Generated funcadd binding to the declared exported implementation | Check library, units, failure and derivative support before P16 acceptance; no unsupported binding inferred from a function name |
| Pyomo | Declared expression expansion or exported ExternalFunction | Same natural-unit, outcome and numerical-policy preflight; Python callback bodies are not ASL solve implementations |
Constant simplification must obey §7.3 and excluded-branch behavior. Ordering/interval hooks are generated only from an explicit sound bound/ordering contract, not from an unqualified monotonicity label. Kernel bodies read no ambient session configuration. A kernel’s natural-unit conversions appear in P9 once; adapters consume those bindings and must not apply the same conversion again. Scalar and batch output admission rechecks the generated contract.
The operator and aggregate capability tables are human-readable views of these declarations. Generated conformance checks require every supported entry to name its implementation; no entry is marked Tested until its actual backend route passes. ImplicitRef itself has no direct DataFusion/NL/Pyomo binding; an explicit implicit-system KernelCall may expose a separately declared and tested UDF.
18.6 Solver-neutral results
Every backend writes the same relations (runtime.runs, solutions, duals, residuals, iterations, solver_events). Residuals are always recomputed natively from the returned primals so that the record does not depend on the solver’s reporting.
18.7 Solve-plan selection
Static attributes only (semantic math basis §0.2): problem class from math_static_analysis and problems.degrees_of_freedom.
| Static signature | Solve plan | Backend and modifiers |
|---|---|---|
DOF = 0, no objective, smooth | SQUARE_NLE | native Ipopt (or petsc_snes through NL); modifiers: continuation on failure |
DOF > 0, objective, smooth, continuous | NLP_LOCAL | native Ipopt; multistart optional |
| discretized dynamic problem with DAE links, no objective | DAE_INTEGRATE or SQUARE_NLE on the fully discretized system | PETSc TS plan, diffsol, or Ipopt |
integer or binary symbols (number_of_units) | MINLP | bonmin/couenne/scip through NL |
| alternative sets present | GDP | Pyomo adapter with Big-M, hull, or GDPopt (phase 3); native reformulation later |
complementarity pairs with formulation = smooth_eps | as NLP | smooth reformulation already in the IR |
BLACK_BOX equations without derivative contracts | none | plan.no_feasible_solve_plan |
The selected class, its justification, and the modifiers are rows in compiled.solve_plans.
18.8 Threading
Decision: ADR-0046
One configuration owns the thread budget: the DataFusion tokio runtime and target_partitions, the rayon pool for batch kernels and pass parallelism, artifact hashing (blake3::update_rayon), diagnostics linear algebra (faer’s Par), and the solver’s linear-algebra threads (HSL, MUMPS, PETSc). Defaults leave the solver single-threaded while a solve is running and give the pool to DataFusion otherwise; hashing and diagnostics never take pool threads during a solve; target_partitions is validated against the pool size when the session is built; oversubscription is a configuration error, not a runtime surprise.
18.9 Backend capability matrix
Decision: ADR-0043, ADR-0047
Entries are Proposed contract scope, conditional on the declared implementation, selected numerical policy and conformance suite; they are not implementation certification. Generate this view from the operation/binding declarations when codegen exists.
| Operator or feature | Native Ipopt | NL | Pyomo | DataFusion batch |
|---|---|---|---|---|
scalar arithmetic, Affine, WeightedMean, reductions, gathers under supported numerical policy | ✓ | ✓ | ✓ | ✓ |
| smooth approximations | ✓ | ✓ (expanded) | ✓ (expanded) | ✓ |
Conditional with runtime guard | ✓ | ✗ | ✓ (Expr_if) | ✓ |
KernelCall (expandable) | ✓ | ✓ | ✓ | ✓ |
KernelCall (opaque) | ✓ | ✓ with external library | ✓ with ExternalFunction | ✓ |
ImplicitRef | ✓ | ✗ (kept as equations) | ✗ (kept as equations) | ✗ (a separately bound implicit KernelCall is distinct) |
| exact Hessian | ✓ when every derivative binding exists; otherwise refuse or use an explicitly selected limited-memory profile | solver-dependent | solver-dependent | — |
| integer variables | ✗ | ✓ (MILP/MINLP solvers) | ✓ | — |
| DAE suffixes | — | ✓ | ✓ | — |
19. Cases, results, and analytics
19.1 Cases and overlays
A case is a chain of overlays over a model revision. Precedence: the child’s case_specs rows override the parent’s for the same target; within a case, higher priority wins; targets are the identity-based case_spec_targets rows resolved at commit (§6.10), so a rename between two case revisions changes nothing; P13 expands each target to symbols, and a wildcard expands over the port’s or group’s members at that time. treatment = fixed with a value, free, or parameter decides variable_order.treatment; initial supplies guesses; bounds override template defaults. A specification that names a symbol with role = expression is an error (IDAES VarLikeExpression semantics: expressions cannot be fixed or bounded; use an equation instead).
19.2 Results and reporting
- Stream tables. A view joining
inferred.portson connections (streams named by connection, plus additional ports for feeds and products), the state’s display declaration (or the state symbols whentrue_state),runtime.solutions, and the reporting unit set (IDAESreporting_unitsconfig): output columns are streams, rows are display quantities with a units column, missing rows shown as-. The interactive variant adds the variable kind (fixed,unfixed,parameter,expression) per cell. - Unit reports.
template_display.performancerows per instance joined to solutions; the IDAESreport()layout is a projection. - KPI tables.
generate_tableis a DataFrame query over any attribute paths. - Tags.
authored.tags(name,expression,display_unit_id,format,doc,group) with group defaults (str_include_units,set_in_display_units); reading a tag converts to the display unit; setting a tag writes acase_specsrow (in display units when the group says so). Tag tables (table_heading,table_row) and SVG label substitution are projections. - Model statistics and diagnostics reports are the views of §15.
- Float semantics in analytics.
GROUP BY,DISTINCTand joins on aFloat64column merge-0.0with+0.0and treat NaN as self-equal (§5.3); ordering does not. Analytics overruntimevalues that group on a float record this as a selected loss in the report’s metadata; the platform’s own views never group on floats.
19.3 Sweeps and convergence studies
authored.case_sets hold a generator (grid, latin_hypercube, uniform_random, explicit list; the IDAES PySMO sampling methods map to these) and produce sample cases; a runner (sequential, parallel over the pool) executes each sample with rebuild_model semantics replaced by immutable snapshots, records per-sample outputs and solver statistics, and stores results in runtime.sweep_results. Baseline comparison and the convergence-summary report (successes, failures, restoration, regularization, numerical issues) are queries.
19.4 Parameter estimation and data reconciliation
observations and measurement_models bind measured values (with std_dev) to model expressions. The reference problem templates:
problem.weighted_least_squares@1: objectiveΣ_k ((expr_k − obs_k)/σ_k)²over the bound observations; estimated parameters are symbols whose treatment is switched fromparametertofreeby the case; ports left free where reconciliation requires it.problem.multi_scenario_estimation@1: stacks one problem instance per scenario (shared θ symbols, scenario-local everything else) into one problem viascenarios; this is the parmest experiment model expressed as data.
Covariance and confidence regions use reduced-Hessian sensitivities from k_aug/sIpopt through the NL backend or the native Hessian. Until native estimation is complete, the Pyomo adapter exposes the stacked problem to parmest.
19.5 Costing
The costing framework (core/base/costing_base.py, models/costing/SSLW.py) is templates and reference data:
costing.flowsheet@1(IDAESFlowsheetCostingBlock): parametersbase_currency(a currency unit),base_period(default year); flow types registered asflow_typesrows with a fixed cost symbol<flow>_costper type; aggregate symbols and equationsaggregate_capital_cost = Σ_u convert(u.capital_cost),aggregate_fixed_operating_cost,aggregate_variable_operating_cost(per period),aggregate_flow_<f> = Σ convert(flows),aggregate_flow_costs[f] = convert(aggregate_flow_f · f_cost), only for flow types with at least one registered flow. Plug-in ordering registers each unit costing instance in the unit’s initialization order.costing_methodtemplates (IDAESUnitModelCostingBlockmethods): applied to a unit instance, creatingcapital_cost,fixed_operating_cost,variable_operating_costsymbols (each must be non-negative; the compiler warns otherwise) and their constraints;unit_mappingis a relationcosting_method_defaults(unit_template_id, method_template_id)with inheritance-aware lookup.- Currency.
USD_CE500is the base currency unit (dimensioncurrency);USD_CE394 = (500/394)·USD_CE500;USD_<year> = (500/CE(year))·USD_CE500for 1990 onward fromreference.cost_indices; location factors are reference data. - SSLW methods (
cost_heat_exchanger,cost_vesselwith platforms, ladders, and trays,cost_fired_heater,cost_compressor,cost_fan,cost_blower,cost_turbine,cost_pumpwith motor, and thecost_pressure_changerdispatcher) are templates with their enumerations (HXType,HXMaterial,HXTubeLength,VesselMaterial,TrayType,TrayMaterial,HeaterMaterial,HeaterSource,CompressorType,CompressorDriveType,CompressorMaterial,PumpMaterial,PumpType,PumpMotorType,FanType,FanMaterial,BlowerType,BlowerMaterial) as reference enums, the correlation coefficients asparameter_values, and the cost-basis quantities (area in ft², work in hp, pressure in psig, duty in BTU/hr, flow in gpm or ft³/min) asUnitConvertnodes.number_of_unitsis an integer symbol (solver_type = integerwhen theintegeroption is set, otherwise continuous and fixed to 1). Two IDAES defects noted by the survey (the vertical and horizontal vessel wrappers discarding their arguments; the blower dispatch routing to the pump method) are corrected in the reference package with parity notes. - Costing initialization is the plan stage “solve each cost symbol from its defining constraint” (1×1 blocks).
19.6 Utility minimization (Duran–Grossmann)
law.heat_integration@1 takes the heating and cooling unit instances, DTmin, and eps, and generates: Tin[i], Tout[i] expressions from the units’ control volumes; Theta[i] (the smoothed Q/ΔT); pinch candidates T_[p] = Tin[p] + dT[p] (dT = DTmin for heating units, 0 for cooling); QAh[p], QAc[p] (bounded below by 1e-8) with the smooth-max heat-content equations; Qs, Qw; heating_utility[p]: Qs ≥ QAc[p] − QAh[p]; cooling_utility: Qw = −Σ_i Q_i + Qs. Initial values come from the pinch calculation kernel. Composite curves are an analytics query.
19.7 Optionality
math_alternative_sets (installed or absent, mode selection, technology choice) lower to disjunctions. Phase 3 lowers through the Pyomo adapter (Big-M, hull, GDPopt); native reformulation to indicator constraints or binaries through the NL backend follows.
19.8 Uncertainty
First-order propagation (IDAES uncertainty_propagation) is sensitivities ds/dp from k_aug/sIpopt (NL) or the native reduced Hessian, combined with the parameter covariance from estimation: σ²_f = (df/dθ)Σ_θ(df/dθ)ᵀ. Robust optimization (PyROS) is reached through the Pyomo adapter.
20. Persistence, provenance, and reproducibility
20.1 Artifact store
Decision: ADR-0045, ADR-0046
<store root>/
refs/<name>.json -- {snapshot_id, manifest_checksum, updated_at}; conditional publication
manifests/<manifest_checksum>.json -- immutable encoded manifest; snapshot identity is a field, not its byte checksum
relations/<namespace>/<relation>@<version>/<encoding_checksum>.arrow
relations/<namespace>/<relation>@<version>/<encoding_checksum>.parquet
evidence/<encoding_checksum> -- noncanonical plan/diagnostic encodings
kernels/<kernel_id>@<version>/<digest>/ -- versioned implementation artifacts
documents/<document_id>/<encoding_checksum> -- exact authored source bytes
Publication is a local single-writer protocol over atomic object creation and conditional ref updates. Prepare and semantically validate complete output bundles, compute logical hashes (§5.3), serialize each selected physical encoding with an explicit successful finish, and verify its size/format/checksum. Store the encoded object with PutMode::Create. A physical checksum verifies physical bytes; it cannot substitute for the logical relation hash.
Before accepting AlreadyExists, verify the existing object’s checksum, length and encoding contract against the intended manifest member. A cached verified-object handle may supply that proof only while its immutability/ownership guarantee remains valid; a restored or externally writable store requires rereading. Truncated, corrupt or incompatible existing bytes are an error requiring explicit repair, never a successful idempotent write. Untrusted imports/restores additionally decode, restore only the declared encoding projection, validate the complete semantic contract and recompute the logical hash. Missing or conflicting metadata is not repaired by guessing units.
Write the complete manifest under its own encoded checksum, then publish (snapshot_id, manifest_checksum) with PutMode::Update using the ref’s prior version (or Create for an absent ref). Readers pin both values, verify the manifest checksum, recompute its semantic snapshot identity, and resolve only its complete member set. Two valid encodings of the same logical snapshot may have different manifest checksums. No self-hashing JSON object or timestamp-dependent logical identity is needed.
An interrupted operation leaves the old ref valid and at most unreachable complete immutable objects; incomplete local staging is never a committed artifact. A CAS conflict re-reads the head and applies the declared conflict/retry policy, never merges arbitrary members. Use the pinned *_opts object-store methods, not rename_opts or copy-then-delete as a commit. Multi-writer/cloud qualification stays behind R-10. Reservations cover staging and serialization (§14.3); successful puts do not excuse an incomplete logical output bundle.
20.2 Manifest
Decision: ADR-0044, ADR-0045, ADR-0049
The versioned manifest is the physical envelope around the explicit semantic membership of §5.3. Its own checksum is held by the ref, not embedded recursively in itself. A representative member is:
{
"manifest_version": "pse.manifest.v2",
"snapshot_kind": "stage",
"snapshot_id": "blake3:…",
"membership_profile": "pse.snapshot.v2",
"created_at": "…",
"schema_registry_fingerprint": "blake3:…",
"relations": [{
"port": "typed_math", "namespace": "compiled", "relation_id": "…",
"name": "math_expr_nodes", "version": 1, "logical_hash": "blake3:…", "rows": 12,
"encodings": [{"format": "arrow_ipc_file", "writer_version": "…", "encoding_checksum": "blake3:…", "bytes": 1234, "path": "…"}]
}],
"packages": [{"package_id": "…", "version": "1.2.0", "logical_hash": "…"}],
"compiler": {"version": "…", "passes": [{"pass_id": "…", "version": "…"}]},
"engine_profile": {"engine_profile_id": "…", "content_hash": "blake3:…"},
"numerical_policy": {"policy_id": "…", "content_hash": "blake3:…"},
"toolchain": {"lockfile_hash": "blake3:…", "canonicalization": "pse.canon.v2"},
"kernels": [{"kernel_id": "…", "version": "…", "digest": "…"}],
"semantic_parents": [{"role": "input.typed_math", "snapshot_id": "blake3:…"}],
"evidence": [{"kind": "datafusion_proto", "canonical": false, "encoding_checksum": "blake3:…", "codec_version": "…", "path": "…"}]
}
Schema, member uniqueness/completeness, path containment, row counts, format versions, checksums and registry compatibility are validated before exposure. Evidence, timestamps and alternative encodings are excluded from logical membership; every model/case/policy/kernel dependency that changes meaning is either a semantic member or a declared semantic parent. The snapshot-kind profile, not optional JSON fields, decides which dependencies are required.
20.3 What a run references
runtime.runs.environment, runs.resolved_options, runtime.host_capabilities and the run’s manifest capture: model and case revisions; property, reaction, and template package versions; compiler and pass versions; kernel digests; discretization, scaling, and initialization plan artifacts; solver profile, the options actually used, and backend versions (Ipopt, HSL, PETSc; Pyomo, pint and every pyomo.contrib.* package used, numpy and scipy versions, and the IDAES version for parity runs); host and thread configuration; the lockfile hash; result and diagnostic artifact hashes. The questions in the proposal (§10.2) are joins: “which correlation produced this enthalpy term” walks math_equations → derivations → method_resolutions → method_specs.
20.4 Reproduction
Decision: ADR-0045
pse reproduce <run_id> verifies the pinned manifest and encoded objects, validates decoded logical identities, then re-executes the declared pass DAG and solve under recorded policies. Deterministic semantic outputs must reproduce their logical hashes; physical encodings, pass-attempt records and plan evidence need not be byte-identical. Solver values and failures use §7.3 comparison rules and the profile tolerances; unsupported runtime capabilities are explicit refusals. A cached result is accepted only with its complete dependency key and validated artifact contract.
20.5 Compatibility and migration
Decision: ADR-0045
Relation and canonicalization versions are explicit. Unknown canonicalization/manifest versions are rejected; v1 artifacts require a separately declared importer that decodes, validates and rehashes into v2, never relabels or copies old hashes. No v1 migration is claimed implemented. Loaders apply declared generated relation migrations forward; a snapshot whose schema registry fingerprint is unknown is rejected with schema.unknown_registry. IDAES JSON state files (to_json format version 4) and scaling JSON files are importable through adapters that map local names to semantic IDs by qualified path.
21. The Python boundary and the Pyomo adapter
21.1 Extension module
Decision: ADR-0024, ADR-0039, ADR-0048
pse-py (pyo3 0.29, pyo3-arrow 0.19) exposes a small API; every table crosses as an Arrow C stream (__arrow_c_stream__) that the consumer drains as a RecordBatchReader, never as row objects and never as a materialized Table on both sides at once. The capsule protocol, not pyarrow, is the contract: any Arrow implementation (pyarrow, polars, datafusion-python at whatever version) can consume it, and none of the typed classes of §21.5 names a pyarrow type.
snap = pse.open(store_path).head() # SnapshotHandle
snap.table("compiled", "math_equations") # object exposing __arrow_c_stream__ → pyarrow / polars / datafusion-python
problem = pse.compile(snap, case="base") # ProblemHandle
bundle = problem.bundle() # ProblemBundle: dict of named Arrow streams + manifest
result = pse.solve(problem, profile="ipopt.default") # RunHandle with .tables()
Bundle contents: manifest (problem id, ordering hashes, unit set, and the per-column loss profile: for every table and field, whether a pse.* extension type is carried and whether the consumer registered it — platform Rust admission rejects an unregistered extension while an unaware Python consumer may expose the storage type, so the declaration is asymmetric and says which side enforces what), variables (ordinal, semantic id, qualified name, quantity_type_id, unit_id, bounds, initial, treatment, scale), equations (ordinal, sense, body node, bounds, scale, active, residual_quantity_type_id), objectives, expr_nodes, expr_args, the typed payload tables, kernel_bindings with their Pyomo binding descriptors, source_map (ordinal → qualified name and source span). Transfer is one stream per table, not per-node calls. Both ingress and egress invoke generated recursive semantic validation; a storage-only consumer cannot re-enter through a checked API without restoring a verified declared contract. Stream owners retain immutable buffer and reservation lifetimes. BatchCoalescer may bound small transfer batches under R-24 measurements; it never collects an entire dataset merely to cross the boundary.
21.2 Adapter algorithm (Python)
Decision: ADR-0047
- Pre-flight:
SolverFactory(name).available(False)for the profile’s solver and, for every kernel bound aspyomo_external_function, that the exported library resolves on this host — both before any model object is built; a failure iscapability.backendnaming the solver or kernel id (Pyomo itself only warns when anExternalFunctionlibrary is missing and fails later from the solver). CreateConcreteModel; oneVarper free variable (indexed by ordinal), oneParam(mutable=True)per parameter (a generated-code invariant, not a convention), bounds and initial values from the bundle; units attached from the bundle’s unit set throughpyomo.units, which is backed bypint.pyomo.util.check_units.identify_inconsistent_units(not a bareassert_units_consistent) runs on the built model and names the offending components in acompile.mathfinding; the registry is the authority for every unit — pint validates what the adapter built and, if the two disagree, the adapter has a bug. No relation is ever derived from a pint object. - Lower the ordered graph with numerical-policy and capability preflight. Use
LinearExpressiononly for a body proved affine in active decision variables after recorded substitutions; coefficients must be decision-independent and children actual variable terms. Verify the expected degree (0 for constants, 1 for a nonconstant affine expression), not an unconditional degree-one assertion. AnAffinecombination containing nonlinear children lowers as ordinary ordered expressions; its opcode does not imply a linear optimization problem. PreserveAdd/Substructure unless the selected policy permits changing it.WeightedMeanexpands with the declared normalization/domain guard; smooth functions and reductions follow their contracts.ConditionalconstructsExpr_if, but evaluation support is certified for the actual writer/solver route; building a Pyomo node alone is not proof of lazy solver evaluation. Expand declared kernels or bind the exportedExternalFunction;ImplicitRefremains equations. Refuse unsupported numerical policies or guarded routes before solve. - Constraints and objectives from the equation rows;
scaling_factorsuffix from the bundle when the profile uses user scaling. - Solve through
SolverFactorywith the profile’s options; results (values,dual,ipopt_zL_out,ipopt_zU_out, termination) are returned as Arrow tables keyed by ordinal; residuals are recomputed natively when ingested. The versions of Pyomo, pint and everypyomo.contrib.*package touched are recorded inruns.environment.
The adapter builds no IDAES classes. Engineering names live in the source map.
21.3 Uses of the adapter
Parity testing (§24), and the Pyomo ecosystem: parmest, PyROS, GDPopt, pyomo.dae utilities, pyomo.contrib.iis as a cross-check oracle for §15.5’s minimal intractable system, and IncidenceGraphInterface-based cross-checks of the native structural results, run as standing CI parity tests over the golden snapshots. Five of these six live in pyomo.contrib, the least stable surface in the dependency set; §18.9’s matrix records that standing, and every result obtained through one records the package version (§20.3).
21.4 Opaque kernels and ASL
Decision: ADR-0043
A kernel bound as pyomo_external_function requires the exported AMPL external-function library; the same library serves the NL backend. Python callback functions are not used for solves because ASL-based solvers cannot execute them. Every kernel with this binding has an end-to-end test that solves a tiny problem through Ipopt from Pyomo and compares to the native result.
21.5 Python contracts
The Python package follows the contract discipline (formerly docs/library_ref/contract_substrate_discipline.md, now stated here so the reference cannot dangle):
- Contract classes are generated. The attrs classes for every relation and bundle table are emitted by the §4.2 generator from
RelationSpec— the same projection as the Rust views, in another language — and shipped inpython/pse/contracts/. A hand-written class that mirrors a relation’s column list is the governance failure §4.2 names; theno_shadow_structscheck covers Python too. - Two class systems, one contract each.
msgspec.Struct(frozen=True, forbid_unknown_fields=True)withMetaconstraints is the codec for the §20.2 manifest and for every JSON or TOML file the Python package reads or writes (strict decode, located errors, JSON Schema emitted only for these wire types — never for authoring documents, whose schema the Rust registry generates).attrs+cattrsstructure Arrow rows (already-decoded material) into the generated classes. No type is defined in both systems. - Strict structuring. Every converter is built with the hook factory
register_structure_hook_factory(has, …make_dict_structure_fn(…, _cattrs_forbid_extra_keys=True))so an unknown key raisesForbiddenExtraKeysError(cattrs ignores extra keys by default, which would turn a version drift between Rust and Python into silent data loss);detailed_validationis never disabled;ClassValidationErrorgroups are rendered withcattrs.transform_errorinto §23.2 findings rather than reduced to their first message. Classes areattrs.frozen. - No
Any. A governance lint walksattrs.fields()of every contract class at import time and fails onAny, baredictor barelist; nothing at runtime forbids them otherwise. - Extension types. The generated
pyarrow.ExtensionTypeclasses of §4.4 are registered once, idempotently, when the package is imported; the contract layer asserts per column that every field claiming apse.*extension resolved to a registered class, using theARROW:extension:namekey that survives even when the type does not. - No Python-side model classes duplicate relations; engineering names live in the source map.
21.6 The array boundary (numpy)
A nullable Arrow column converted to a bare ndarray turns null into NaN, collapsing the distinction §4.4 exists to protect. The adapter therefore converts with zero_copy_only=True by default (which refuses nulls and chunked columns rather than copying silently), asserts null_count == 0 before any conversion (true for solver vectors by construction), and carries the validity mask alongside where a nullable column must cross. A copy is requested explicitly (zero_copy_only=False) only where the call site records why. -0.0 survives the conversion. numpy and scipy are pinned because the parity harness compares numbers through them; neither is used on a production path.
22. Authoring and the extension model
22.1 Package layout
<package>/
package.toml -- id, version, kind, dependencies
materials/*.yaml -- species, phases, elements, reactions, stoichiometry, parameter values
methods/*.yaml -- method_specs for equation-template methods (kernels are Rust and only referenced)
properties/*.yaml -- property_packages, method_selections, state bounds, equilibrium pairs
templates/*.yaml -- unit, control volume, state, reaction, connection rule, helper, initializer, scaler templates
laws/*.yaml -- law templates and law instances
costing/*.yaml -- costing methods and reference cost data
cases/*.yaml -- instances, connections, cases, observations, case sets
assertions/*.yaml -- expected derived facts (tests, not truth)
P1 parses every document into authored rows via a change set; the document’s content hash is recorded on each row’s source span. YAML documents are parsed by serde-saphyr (typed errors with line and column, hostile input refused without a panic, a parsing budget on nesting, aliases and allocation) and TOML by toml with Spanned<T>, so every row’s pse.source_span comes from the parser. Every entity declared under id_policy = explicit carries an id: field; pse authoring assign-ids inserts missing ones and P1 rejects a document that still lacks one (§5.1). YAML is one surface; the same change sets can be produced by Python builders (pse.authoring) and by agents.
22.2 The change-set model
Decision: ADR-0027
authored.change_sets @1 change_set_id, base_revision_id, author, message, created_at
authored.change_ops @1 change_set_id, ordinal, op : enum (insert|update|delete|rename), relation_id, row_key,
row : struct<staged_port: text, staged_ordinal: u64>, precondition : text [n]
-- row points into the change set's per-relation staged members, never at JSON text (D1)
-- rename: the staged row carries entity_id and the new name/qualified_name; rejected for named-policy entities (§5.1)
A change set is validated (P2 on the resulting snapshot), applied atomically, and yields a new model or case revision. The commit contract is P2-level validity: a committed revision is structurally and referentially valid (every invariant of reference.schema_invariants holds, every target resolves to an identity, every expression parses); it may still fail P3–P10 (an unresolvable template path, a unit inconsistency). Publication as a compiled problem is gated separately by the closure report (§14.5), and every revision carries last_closure_status so a reader can tell a committed revision from a compilable one. There is no other write path into authored (decision D2 and doctrine P20). Attempts to write a derived relation are rejected; an “expected” derived fact goes to provenance.assertions.
rename changes an entity’s name and qualified_name attributes and re-renders every retained pse.target_path and pse.expr_dsl text that serialized a reference to the old path from the identity-based rows (case_spec_targets and kin, normalized.*_expr_*); references by identity are untouched, every derived ID is unchanged, and the §22.4 diff report shows one rename row. A change op that edits a retained target text without the corresponding identity rows is rejected by P2 (case.target_text_mismatch): the text is a serialization, not a second authority.
22.3 What is data and what is code
| Extension | Realized as | Code change needed |
|---|---|---|
| New unit model | template document | no |
| New pure-component correlation, EOS variant, phase-equilibrium form, reaction form | method document with an expression template | no |
| New costing method or costing library | costing template and reference data | no |
| New initializer or scaler strategy | plan template composed from stage kinds | no, unless a new stage kind is required |
| New diagnostic check expressible over relations | a RulePlanSpec and thresholds | no |
| New material system, property package, flowsheet, case | documents | no |
| New constitutive computation that cannot be expressed as an expression (root finders, tabulated data, external libraries) | KernelSpec plus Rust implementation and tests | yes, in pse-kernels or a provider crate |
| New operator (a new universal computational primitive) | opcode with a full contract in pse-mathir and all backend lowerings | yes |
| New law family | law template if expressible over contributions; otherwise a compiler rule | usually no |
This is the litmus test of the doctrine: new scope enters as semantics and plug-ins, not as runtime branches.
22.4 Agent change sets
An agent proposes a change set (for example: replace the heat-transfer correlation on one exchanger, regenerate, compare). The platform compiles the resulting snapshot, produces a diff report (added and removed equations, changed sparsity, changed scaling, changed DOF, closure report), and the change is accepted or rejected as a whole. Agents never edit build()-style code.
23. Observability and failure semantics
23.1 Observability
tracing spans are opened per pass run, per rule evaluation, per plan stage, and per solve; events carry the semantic IDs involved. Fields that are known only at the end of a span (output hashes, row counts, finding counts, cache hit or miss) are declared as field::Empty when the span opens, because a later record of an undeclared field is silently discarded; #[instrument] is used with skip(...) and explicit fields(...), never with its default of recording every argument. DataFusion emits no spans of its own: rule-level spans are platform code around plan execution, operator-level visibility comes from the EXPLAIN pgjson output and rules_fired stored in the pass record (§14.2 rule 5), and JoinSetTracer may be installed for per-task tracing when needed (datafusion-tracing is adopted only when a release matching the pinned engine exists). Solver output is captured as data through the Ipopt iterate API (§18.3) with text capture only for messages that have no structured equivalent. Metrics (pass durations, row counts, cache hits, solver iterations) are emitted with the same identifiers. Provenance (provenance.*) explains derivation; observability explains execution; they are linked by pass run and run IDs but never merged.
23.2 Failure taxonomy
| Failure class | Examples | IDAES counterpart |
|---|---|---|
authoring.parse, authoring.reference | syntax error, unknown path | ConfigurationError |
validation.invariant | foreign key missing, cardinality, domain | ConfigurationError, PropertyPackageError |
compile.feature | dynamic in a steady-state parent, holdup without dynamics | DynamicError, ConfigurationError |
compile.property | unsupported/ambiguous property, unresolved method, undeclared demand or incomplete provider | PropertyNotSupportedError, PropertyPackageError |
compile.law | unsupported balance binding, basis conversion missing | BalanceTypeNotSupportedError |
compile.math | incompatible physical contracts, unsupported quantity operation, cyclic expression | Pyomo UnitsError |
compile.discretization | mixed derivative, missing policy | NotImplementedError |
capability.backend | unsupported opcode or numerical policy, missing derivative or external library | PropertyPackageError, ASL errors |
plan.initialization | DOF ≠ 0, structural singularity, postcheck failure | InitializationError with InitializationStatus |
solve.infeasible, solve.locally_infeasible, solve.unbounded, solve.limit, solve.evaluation_error, solve.solver_error | termination statuses | Pyomo TerminationCondition |
runtime.cancelled, runtime.timeout, runtime.resource_limit, runtime.infrastructure | cancellation token, wall limit, reservation/size limit, store I/O or integrity failure | — |
internal.invariant | a pass postcondition failed | BurntToast |
user.model | authored assertion failed, user equation error | UserModelError |
Failures carry the relation rows and source spans involved; they are never flattened into “run failed”. Every pse-* crate returns concrete error enums (thiserror with miette::Diagnostic derived, the class names above as #[diagnostic(code(...))] in Rust path form); miette::Result and the graphical reporter exist only in the CLI and the driver. Findings are relations; a rendered diagnostic is a projection of a finding, never its storage. pse.* values inside a diagnostic are rendered by the formatter factory of §4.4, not as raw storage bytes.
DataFusion errors map into the taxonomy at one place, never per call site:
DataFusionError variant | Platform class |
|---|---|
ResourcesExhausted | runtime.resource_limit (recoverable; names the consumer and configuration keys) |
SchemaError, Plan from a rule-compiler input | internal.invariant (a rule plan that does not type is a platform bug) |
SchemaError, Plan from an analytics query | user.model |
Execution inside a generated kernel UDF | solve.evaluation_error or compile.property, from the kernel’s declared failure class |
Execution elsewhere, ArrowError, ParquetError, ObjectStore, IoError | runtime.infrastructure |
Configuration | config.invalid (typed; SessionConfig::set_str, which panics instead, is banned) |
Diagnostic, Collection | unpacked into one finding per contained error (validators return violating keys plural; the first error is never the only one reported) |
NotImplemented, Substrait, Ffi, External, Context, Shared, SQL, ExecutionJoin | internal.invariant unless wrapped by a platform class |
24. Testing and acceptance
24.1 Test layers
Decision: ADR-0048, ADR-0051
| Layer | What it proves | Mechanism |
|---|---|---|
| Schema governance | every relation is registered; no hand-written struct shadows a relation; every invariant has a positive and a negative fixture; generated code is current; no authored or reference column has a foreign key into compiled or runtime; no authored relation stores a parsed expression graph (no compiler output port targets authored/reference relations); every crate in Cargo.toml and every Python library appears in §3.1’s tables; the TableProvider implementations expose exactly the read-path method set; governance greps ban Field::extension_type(), SessionConfig::set_str, SchemaLike::from_type/from_samples, config_options reads in kernels, and SERDE_ARROW:* keys; the Python Any lint and no_shadow_structs cover the generated contract classes; the schema_enums members match IDAES 2.12.0’s enum classes | tests/governance/*, CI diff of generated sources |
| Structural validity | Arrow structural checks execute with pse-relations/force-validate explicitly enabled; semantic checks remain separate | just test / CI feature argument, never a profile-only claim |
| Engine reproducibility | Complete engine/registry inputs affect stage keys; plan bytes remain noncanonical evidence with codec round-trip/attribution checks; ordinary query/import paths reject malformed known/unknown/nested extension fields before optimization; computed quantities retain a derivation and pass loss-aware admission | tests/engine/; generated boundary matrix |
| Pushdown truthfulness | Exact and Unsupported providers yield the same complete values and multiplicities, including nulls, long IN forms, projections and limits; a deliberately empty/over-pruning provider fails | tests/engine/, pushdown_vs_unpruned |
| Python boundary | an extra key in a bundle row raises ForbiddenExtraKeysError; an Any field fails the lint; all registered extension types survive Rust → Python → Rust with registration and are reported degraded without it; a nullable column refuses ndarray conversion; a missing external-function library or solver raises capability.backend before model construction; the manifest rejects an unknown registry fingerprint | python/pse/tests |
| Cross-check oracles | matching cardinality and structural rank agree with scipy.sparse.csgraph and IncidenceGraphInterface; the native minimal intractable system agrees with pyomo.contrib.iis; the smallest singular values agree with scipy.sparse.linalg.svds on the dense-tractable fixtures — invariants compared, never object identity | parity harness |
| Supply chain | cargo deny (advisories, bans, maintenance) and cargo audit are green; cargo tree shows one version per family; the committed lockfile matches the manifest | CI |
| Canonicalization | pse.canon.v2 ignores batch/row layout and hidden null storage, preserves valid signed zero and full registered semantic metadata, distinguishes changed valid values, and survives declared encoding round trips; physical checksums can differ while logical identity agrees. Renaming keeps entity IDs but changes hashes of changed labels. Nested masks/slices/offsets/empty relations/dictionaries are covered | tests/conformance/ metamorphic fixtures and tests/lifecycle/ corruption cases |
| Quantity and numerical contracts | Standard package compositions, scaled differences, reference/basis conversion and WeightedMean are typed; ordered versus opted-in preparation policies are distinguished; overflow, cancellation, signed zero, nonlinear Affine children and excluded-branch failure/derivatives are checked | tests/conformance/, quantity_composition, numerical_policy_conformance |
| Rule engine | Finite normalized demand closure and typed stage-port DAG; no late demand or incomplete provisions; strata and four-valued outcomes; aggregate/unnest null, empty, order and provenance behavior matches an independent reference | golden inferred relations; demand/realization and relational-expansion fixtures |
| Kernels | values match IDAES or literature at test points; first and second derivatives match finite differences and dual-number results; validity violations fail as declared; batch and scalar paths agree; NL external functions agree with native | per-kernel test suites referenced from KernelSpec.test_suite |
| Structural algorithms | matching, DM partition, SCC order, and connected components agree with reference fixtures (generated once from Pyomo incidence analysis on the IDAES tutorials) | fixture files under tests/structural/ |
| Backend conformance | native/Pyomo residuals and Jacobians agree at valid points under the named policy (default atol 1e-12, rtol 1e-10); invalid/non-finite/branch outcomes match their declared class; NL round trip solves with Ipopt to the same solution as native; SOL parsing | parity harness in python/pse/parity |
| Diagnostics parity | the IDAES diagnostics tutorial models produce the same warnings and cautions | fixtures from idaes-examples diagnostics notebooks |
| Initialization and scaling | plans reach the same converged points as IDAES initializers on the reference units; scaling factors equal IDAES’s for the reference scalers | parity fixtures |
| Incrementality | Incremental execution equals clean compilation for every declared dependency change, including absence, added species/candidates, package units, template bindings, fixed/parameter values, initializer and numerical policy; complete-key reuse preserves current semantic lineage. No assertion requires fine-grained reuse before its adoption gate | incremental_vs_clean_binding_matrix, full output/lineage comparison |
| Resource envelope | Fallible query/canonicalization/binding/result reservations, supported row/byte/offset limits, two concurrent snapshot sessions, shared-buffer ownership and release after cancellation; compare accounted peaks with process peaks | tests/engine/, tests/lifecycle/, recorded resource conditions |
| Provenance | every equation, symbol, and inferred row has a derivation chain to authored rows; the negative-completeness report lists excluded candidates | closure tests |
| Doctrine conformance | the metamodel conformance tests (automatic participation, material change, scope change, internal cancellation, unknown composition, rename and reorder, redundant relation rejection, domain replacement) pass on the process packages; rename in particular: after renaming a unit only name/qualified_name columns differ, every derived ID, case binding and derivation join is unchanged, and the diff report contains exactly one rename row | tests/conformance/ |
| Lifecycle | a truncated/corrupt artifact under a correct-looking checksum name is rejected, including AlreadyExists and restore; logical and encoding checks are distinct; faults before artifact/manifest/ref publication expose no mixed snapshot; a ref CAS conflict retries and never publishes a mixed snapshot; cancellation mid-pass leaves no partial artifact; cancellation mid-solve returns false from the Ipopt callback without unwinding; an NL solver process killed mid-run leaves no ingested SOL file; a retried stage is a new runs row with attempt + 1; two change sets on one base revision conflict explicitly; a snapshot with an unknown engine profile is recompiled, never served from the memo | tests/lifecycle/ |
24.2 Vertical slices and acceptance criteria
Slice A — steady-state heater and mixer (phase 1). Benzene–toluene ideal package with FTPx, feed, heater, mixer, product; equality connections; case fixing feed state and heater duty, and a second case fixing outlet temperature instead. Accept when: the compiled problem has the same variable and equation counts and DOF as the IDAES model; native and Pyomo residuals and Jacobians agree; Ipopt converges to IDAES values under a recorded solution comparison (default atol 1e-8, rtol 1e-6; non-finite/error outcomes checked separately); the stream table matches; reload from the artifact store reproduces identical hashes; the block-triangularization plan initializes both cases; renaming the heater between the two cases changes no derived ID and no case binding.
Slice B — recycle with flash (phase 2). Peng–Robinson package, SmoothVLE, LogBubbleDew, flash, separator, compressor (isentropic), heat exchanger (LMTD), recycle with tear stream; modular-properties initialization plan; full diagnostics including SVD and Degeneracy Hunter on a deliberately degenerate variant. Accept when: cubic root kernel derivatives agree with finite differences and the NL external function; the sequential-modular plan converges the recycle; structural and numerical reports match IDAES on the same flowsheet; homotopy moves the feed specification across a phase boundary with the IDAES step statistics; a value-only case change recompiles only P13 onward.
Slice C — distributed dynamic reactor with costing and estimation (phase 3). CSTR and PFR (backward finite difference and Radau collocation) with a generic reaction package (Arrhenius, power law), dynamic flowsheet with time discretization, PID controller, SSLW costing, utility minimization, a parameter-estimation case over observations, and a parameter sweep. Accept when: discretization equations equal Pyomo DAE’s; the time-element plan and the PETSc plan reproduce IDAES trajectories; costing aggregates match SSLW values; estimated parameters and covariance match parmest; sweep statistics match the IDAES convergence analysis format.
24.3 Benchmarks
Decision: ADR-0042, ADR-0046
Measure the same semantic workload and report cold/warm preparation separately: parse, admission, demand closure, each stage, rule optimization, canonicalization, physical persistence, restore/recovery, Python transfer/construction, residual/Jacobian/Hessian evaluation, solve, inspection and total wall time. Include derivation rows/bytes per equation, accounted consumer peaks, whole-process peak, temporary copies, spill bytes, scanned/decoded rows and buffer copy rate. Record pins, profiles, numerical policy, thread/memory/size limits, dataset and case hashes and result equivalence.
Compare complete stage reuse against any proposed finer input bundle before R-22/R-01 adoption. Compare shared predicates with storage pruning, scalar UDF fast paths with materialization, and safe borrows/coalescing with checked copies before claiming improvement (R-24). Predicate preimages require full-result equivalence before decoding savings are measured (R-25). No timing gate or performance gain is established by the design or by the standalone characterization probes.
25. Delivery phases
| Phase | Scope | Exit criteria |
|---|---|---|
| 0 — Foundations (design and implementation gates remain distinct) | pse-schema registry and codegen; pse-ids; pse-quantity; pse-material; pse-mathir with the operator catalog and canonicalization; pse-catalog over a local store; pse-authoring parser and change sets; passes P0–P3 and P10 | schema governance tests green; expression DSL round-trips; v2 canonicalization metamorphic fixtures and active semantic query admission; storage identity/integrity fault matrix; quantity/ordered-IR contract fixtures; a complete snapshot can be published, read back and queried |
| 1 — Slice A | reference packages: units, elements, ideal EOS, NIST/RPP/Perry’s methods, FTPx/FcTP, lumped control volume, heater, feed, product, mixer, state junction, equality connections; passes P4–P9, P12–P16; native evaluation program with Jacobian; Ipopt FFI; NL writer and SOL reader; Pyomo adapter; structural diagnostics; nominal scaling; block-triangularization and single-control-volume plans | slice A plus complete-stage reuse, demand/provision, guarded backend, numerical-policy and resource-envelope acceptance from plan 02 |
| 2 — Slice B | cubic EOS with the root kernel and external library; smooth VLE, bubble and dew; flash, separator, pressure changers, valve, 0D heat exchangers; translator; topology closure with tear selection; modular-properties, mixer, separator, HX, isentropic plans; sequential-modular plan; homotopy; full numerical diagnostics, SVD, Degeneracy Hunter, MIS; autoscaler and scaling profiler; exact Hessian | slice B acceptance |
| 3 — Slice C | distributed control volume, PFR, pipe, 1D exchangers; discretization lowering; dynamic flowsheets, time-element and PETSc plans; PID controller; generic reaction package; costing framework and SSLW; utility minimization; observations, estimation, reconciliation; sweeps and convergence analysis; GDP through the Pyomo adapter; uncertainty propagation | slice C acceptance |
| 4 — Breadth | electrolytes (aqueous phase, eNRTL, true and apparent species), Helmholtz templates and native state kernel, CoolProp parameter import, FeOs provider (conditional on feos-core tracking num-dual 0.15), MSContactor, solid–liquid units, NTU and lumped-capacitance exchangers, shell-and-tube, diffsol backend, optional egglog rewrites, native GDP lowering | parity on the corresponding IDAES tests |
Each phase ends with the parity harness green against IDAES 2.12.0 (the pinned parity package, §3.1) for its scope and with the doctrine conformance tests passing. The current phase-0 skeleton and environment preflight do not satisfy these behavioral exits; revision 5 is Proposed design, not a claim that its mechanisms exist.
26. Risks and open decisions
Decision: ADR-0042, ADR-0048
| Risk or question | Position taken here | Fallback |
|---|---|---|
| Exact Hessian cost for large distributed models | per-equation forward-over-reverse with sparsity from incidence; kernels declare second derivatives | Ipopt limited-memory Hessian profile |
| External-function libraries must be built per platform for NL solvers | one generated library per kernel package with the IDAES symbol names, built in CI for Linux, macOS, Windows | native Ipopt path needs no library |
| Pyomo construction cost dominates parity runs | the adapter is coarse-grained and memoized; used for parity and ecosystem tools only | none needed for production solves |
| Floating-point/backend differences | ordered default, explicit numerical policies and absolute/relative/non-finite comparison (§7.3) | refuse unsupported policy; an alternate policy must be explicitly selected and recorded |
| Canonical hashing of floats | IEEE bits with NaN canonicalization; −0.0 preserved | — |
| Electrolyte and eNRTL complexity | schema supports it from phase 0; implementation in phase 4 | — |
| Recursive closures: DataFusion recursive CTE versus the external fixed-point loop | external loop when per-row derivations are required; CTE allowed otherwise | — |
Expressiveness of the template DSL versus IDAES’s escape hatch (SkeletonUnitModel) | authored instance equations remain IR, so the escape hatch stays inspectable | — |
| Crate naming and repository layout | decided by ADR-0002 and ADR-0038 | a changed decision requires a superseding ADR |
| Which HSL linear solvers are available on target machines | probe_host records them in runtime.host_capabilities; a run resolves its profile against that record and stores resolved_options (§18.3) | a profile-declared fallback_linear_solver = mumps, otherwise a typed capability.backend failure |
Revision-4 review disposition in revision 5 (Proposed contract corrections; implementation acceptance is open):
| Finding / opportunity | Current decision and authoritative sections | Remaining gate |
|---|---|---|
| R4-01 / L1 | ADR-0039; active recursive admission and analyzer boundaries (§4.3–§4.4, §14.2) | Supported query/import/output negative cases |
| R4-02 / L2 | ADR-0043/0047; built-in CASE and native branch regions (§7.3, §18.2, §18.5) | Mixed-row/nested/derivative branch equivalence |
| R4-03 / R4-04 | ADR-0045; v2 null normalization, explicit membership and encoded integrity (§5.3, §20) | Metamorphic, round-trip and interrupted-publication fixtures |
| R4-05 / L6 | ADR-0039/0043; semantic/representability admission and field-aware scalar/batch adapters (§14.2, §18.5) | Lossy numeric/unit/reference rejection and cardinality checks |
| R4-06 | ADR-0040; finite normalized demand seeds and stage ports (§6.6, §6.11, §9.6, §14.1) | DAG, closure, late-demand and incomplete-provider checks |
| R4-07 | ADR-0041/0042; complete stage keys, bound-value refresh (§14.3–§14.4) | Incremental equals clean across the dependency matrix |
| R4-08 | ADR-0039; complete quantity rules and WeightedMean (§6.2, §7.2, §8.3) | Standard and adversarial composed physical types |
| R4-09 / L7 | ADR-0047; ordered IR, persisted numerical policy, safe owning buffer views (§7.3–§7.4, §18.2, §21.2) | Numerical/failure conformance and borrow lifetime tests |
| R4-10 | ADR-0043; complete kernel/outcome contracts and supported binding matrix (§6.11, §6.13, §18.5–§18.9) | Actual backend-route conformance; mandatory outcome handling |
| R4-11 / L3 | ADR-0048; shared predicates and complete pushdown oracle (§5.4, §24.1) | Deliberate over-pruning detected; R-24 for storage optimization |
| R4-12 | ADR-0044; explicitly noncanonical plan evidence (§4.3, §14.2, §20.2) | Round-trip/attribution and exclusion from semantic keys |
| R4-13 / L5 | ADR-0046; shared accounted budget and explicit size envelope (§5.3, §14.3) | Concurrent consumers, cancellation, pool/process peaks |
| L4 | ADR-0048; bounded aggregate/unnest rule algebra (§6.11, §14.2) | Independent relational expansion with null/empty/order/provenance cases |
| L8 | ADR-0048; predicate preimages deferred to R-25 | Equivalent predicate proof/fixtures, then measured benefit |
| L9 | ADR-0048; explicit-schema tabular observation readers (§6.10) | Import descriptor, header/unknown-field and unit/target validation |
Earlier review dispositions remain historical in the revision log. In particular, earlier claims that registration validates all plans, sorted metadata insertion stabilizes protobuf, or a query pool bounds all process allocations are withdrawn. The accepted arguments are preserved by superseding ADRs, not edited into a different history. The revision-5 review assesses the revised document scope; plan 02 separates that assessment from runtime acceptance.
| Deferred decision / residual risk | Current control and observable trigger |
|---|---|
| Finer memoization and salsa | R-22 first requires measured whole-stage cost and complete input fixtures; R-01 then considers automatic finer tracking |
| Larger canonical relations or fixed-boundary streaming | R-23: supported workload exceeds §5.3 row/byte/offset envelope; new canonical format requires an ADR and equivalence fixtures |
| Storage pruning / bounded coalescing | R-24: representative scan/transfer cost dominates and equivalent results plus lower total cost are measured |
| Predicate preimages | R-25: repeated computed-column analytics supplies an exact supported preimage; a conservative pruning range alone cannot replace the predicate |
egglog, FeOs, compiled plugins, observability extensions | Existing R-03–R-06 triggers remain; none supplies missing semantic contracts |
| Cloud/multi-writer storage | R-10: qualify the exact backend’s CAS/durability contract before widening deployment scope |
| Solver platforms, derivatives and numerical parity | Existing solver/register gates and slice fixtures; documentation cannot certify numerical behavior |
| Dependency floors and plan encodings after upgrades | R-07 and R-11; pins and codec support are checked, plan-byte reproducibility is not asserted |
Appendix A. IDAES capability coverage matrix
The left column is the IDAES mechanism (source path relative to idaes-pse/idaes/); the middle column names the platform realization; the right column is the delivery phase.
| IDAES mechanism | Platform realization | Phase |
|---|---|---|
core/base/process_block.py: declare_process_block_class, CONFIG, build(), scalar/indexed blocks, initialize/idx_map per-index config | templates (authored.templates, template_params), submodel multiplicity with per-member bindings (idx_map), instantiation pass P7 | 0–1 |
process_base.py: flowsheet(), _setup_dynamics, default property package search, initialization_order, default scaling, report, performance and stream-table contents | instance_tree, feature inference rules (§13.2), inferred.initialization_order, default_scaling, display views | 1 |
flowsheet_model.py: time domain creation, nesting rules, model_check, stream_table | unit.flowsheet@1, domains of kind time, stream-table view | 1 |
unit_model.py: add_inlet_port/add_outlet_port/add_port, add_state_material_balances, fix_initialization_states, plug-in initialization | template_ports, law.state_material_balance, plan stages, plug-in order | 1 |
control_volume_base.py: enums, CONFIG_Template, balance dispatch, _rxn_rate_conv, _estimate_next_state | §6.14 enums, param_group.cv_options, law expansion (§10), conversion rules, estimate stage | 1 |
control_volume0d.py: all balance variants, holdup, phase fractions, reactions, custom terms, initialization, scaling | cv.lumped@1 (§10.3), scale.cv_lumped@1, plan stages | 1 |
control_volume1d.py: length domain, area definition, flow direction, linking variables, spatial derivatives, transformation | cv.distributed_1d@1 (§10.4), discretization policy, P11 | 3 |
extended_control_volume0d.py/1d.py: isothermal energy balance | EnergyBalanceType.isothermal law binding | 1/3 |
property_base.py: parameter block, state block contract, ports from define_port_members, build_on_demand | property_packages, state templates (§9.2), port derivation, demand closure P6 | 1 |
property_meta.py, property_set.py: unit sets, derived units, standard and electrolyte property sets, metadata flags | unit_sets, quantity_kinds, property_kinds (with idaes_name), method_specs.provides/requires | 0–1 (electrolyte set: 4) |
reaction_base.py: reaction parameter and reaction blocks, get_reaction_rate_basis, required-property validation | reaction_packages, reaction block template, invariants | 3 |
phases.py, components.py: phase types, component classes, validity rules, electrolyte sets | phases, species, inferred.phase_species rule | 1 (ions: 4) |
var_like_expression.py | expression symbols; case_specs targeting an expression is an error | 1 |
costing_base.py, models/costing/SSLW.py | costing.flowsheet@1, costing method templates, currency units, cost indices | 3 |
models/properties/modular_properties/base/generic_property.py: config, build order, property builders, log forms, initialization | property_packages, method_selections, P6 realization, init.modular_properties@1 | 1–2 |
state_definitions/* | state templates (§9.2) | 1 (FpcTP, FpTPxpc: 2) |
eos/ideal.py, eos/ceos.py, eos/ceos_common.py, eos/enrtl*.py | eos.ideal@1, eos.cubic@1 with cubic.compress_fact@1 kernel and external library, eos.enrtl@1 | 1 / 2 / 4 |
pure/* (NIST, RPP3–5, Perry’s, constants, Chapman–Enskog, Chung, Eucken, electrolyte permittivity) | pure-component method templates | 1–2 (transport: 3) |
phase_equil/* (bubble/dew, forms, Henry, SmoothVLE, SmoothVLE2) | phase-equilibrium method templates (§9.5) | 2 |
transport_properties/* (Wilke, WMS, NoMethod) | transport mixing method templates | 3 |
reactions/* (rate forms, rate constants, equilibrium forms and constants, heat of reaction) | reaction method templates (§9.7) | 3 |
interrogator/* | query over inferred.property_requirements | 1 |
helmholtz/, general_helmholtz/, coolprop/ | Helmholtz expression templates plus helmholtz.state@1 kernel; CoolProp parameter import | 4 |
models/unit_models/* (all units in §11.1) | unit templates | 1–4 per §11.1 |
models/control/controller.py | control.pid@1 | 3 |
core/initialization/* (initializer base, block triangularization, single control volume, from data) | plan model and standard plans (§17) | 1 |
core/util/initialization.py (fix_state_vars, revert_state_vars, propagate_state, solve_indexed_blocks, initialize_by_time_element) | overlays, propagate operation, solve_subset, plan.time_element_march@1 | 1 / 3 |
core/solvers/homotopy.py | continuation stage | 2 |
core/solvers/get_solver.py, config.py, solvers/config.py (option merging), ipopt_l1, features.py | solver_profiles with the IDAES defaults, capability probes | 1 |
core/solvers/petsc.py (petsc_dae_by_time_element, DAE suffixes, trajectories) | PETSc plan over the NL backend, math_dae_links, trajectory ingestion | 3 |
core/scaling/* (scaler base, custom scaler methods, nominal value tools, autoscaler, arc scaler, profiler, util) and core/util/scaling.py | scaling transformation model (§16), scaler templates, profiler plan, JSON adapters | 1–2 |
core/util/model_statistics.py | stats.* views (§15.2) | 1 |
core/util/diagnostics_tools/* (toolbox, SVD, Degeneracy Hunter, term analysis, evaluation errors, ill-conditioning, convergence analysis, bounds, halt on error) | checks of §15 | 1 (structural, basic numerical) / 2 (advanced) |
core/util/model_serializer.py (to_json, from_json, StoreSpec) | overlays and result relations; JSON adapter for import | 1 |
core/util/dyn_utils.py | case operations (§13.5) | 3 |
core/util/tables.py, tags.py | stream-table and tag views, authored.tags | 1 |
core/util/units_of_measurement.py, constants.py, math.py | reporting unit set, reference.constants, smooth operators in the opcode catalog | 0–1 |
core/util/misc.py (set_param_from_config, StrEnum, compact expression printing) | parameter ingestion with units, enums as dictionaries, expression rendering | 0 |
core/util/config.py validators, exceptions.py | parameter domains in template_params, failure taxonomy (§23) | 0 |
core/util/parameter_sweep.py, convergence/* | case sets, runners, convergence analysis | 3 |
core/util/utility_minimization.py | law.heat_integration@1 | 3 |
core/util/phase_equilibria.py (Txy diagrams) | a case set over compositions with bubble and dew symbols plus a plotting projection | 3 |
core/util/structfs/* (step runner) | plans and pass records are the structured runner | 1 |
core/util/env_info.py | runtime.runs.environment | 1 |
Pyomo network (Port, Arc, expand_arcs, SequentialDecomposition) | ports, connections, P8, tear selection and sequential plan | 1–2 |
Pyomo dae (ContinuousSet, DerivativeVar, Integral, finite difference, collocation, flatten utilities) | continuous domains, derivative symbols, Integral opcode, P11 | 3 |
Pyomo contrib.incidence_analysis | pse-structural | 1 |
Pyomo contrib.pynumero (PyomoNLP Jacobian route) | native evaluation program | 1 |
Pyomo expression visitors, differentiate, replace_expressions, units container | IR traversal, AD, canonicalization, quantity types | 0–1 |
Pyomo common.config (ConfigBlock) | template_params, template_features, template_property_requirements, parameter domains | 0 |
Pyomo suffixes (scaling_factor, dual, ipopt_zL_out, ipopt_zU_out, DAE suffixes) | scaling relations, result relations, NL suffix emission | 1–3 |
ExternalFunction and IDAES binary libraries (functions, cubic_roots, general_helmholtz_external) | KernelSpec bindings and the generated external-function library | 2–4 |
core/surrogate/* (trainers, SurrogateBlock) | a trained surrogate is a KernelSpec (PySMO polynomial, RBF, kriging as expression templates; ALAMO expressions parsed into the IR; ONNX/Keras through OMLT via the Pyomo adapter) embedded by a unit.surrogate@1 template with input-bound intersection; training itself is out of scope | 4 (embedding), out of scope (training) |
core/dmf/*, core/ui/*, apps/*, models_extra/* | out of scope; the artifact store and provenance relations replace DMF’s workspace, resource, and relation concepts | — |
Appendix B. Relation index
| Namespace | Relations |
|---|---|
reference | schema_relations, schema_columns, schema_logical_types, schema_enums, schema_invariants, schema_migrations, dimensions, units, unit_sets, quantity_kinds, bases, reference_states, quantity_types, conversion_rules, quantity_operations, constants, elements, property_kinds, method_specs, kernel_specs, pass_specs, pass_input_ports, pass_output_ports, numerical_policies, rule_specs, operator_specs, rule_plan_nodes, rule_plan_edges, rule_aggregates, rule_group_keys, rule_unnest, rule_dependencies, rule_expr_*, engine_profiles, aliases, diagnostic_thresholds, cost_indices, location_factors |
authored | packages, documents, entities, model_revisions, case_revisions, change_sets, change_ops, domains, domain_members, continuous_domains, species, species_elements, phases, phase_species, henry_declarations, material_systems, reactions, stoichiometry, reaction_methods, reaction_packages, parameter_values, property_packages, state_bounds, phase_equilibrium_pairs, method_selections, default_scaling, templates, template_params, template_features, template_property_requirements, template_feature_rules, template_guards, template_domains, template_symbols, template_equations, template_submodels, template_ports, template_contributions, template_law_instances, template_requirements, template_display, template_scaling_defaults, instances, instance_equations, flowsheets, scopes, selector_terms, connections, cases, case_specs, case_spec_targets, case_activations, case_activation_targets, case_objectives, case_policies, discretization_policies, solver_profiles, datasets, observations, observation_targets, measurement_models, scenarios, case_sets, case_set_samples, tags |
normalized | package_graph, domain_products, property_demand_seeds, the parsed expression graphs template_expr_*, instance_expr_*, display_expr_*, contribution_expr_*, guard_expr_* (one family per pse.expr_dsl column), canonical copies of authored relations after P3 |
inferred | valid_index_tuples, phase_species, phase_equilibrium_species, state_flash_required, property_requirements, method_resolutions, instance_tree, instance_features, instances, ports, port_members, scope_members, boundary_crossings, topology_edges, tear_candidates, connection_equations, initialization_order, undecided, law negative-completeness rows |
compiled | meshes, mesh_nodes, stencils, quadrature_rules, symbols, symbol_references, symbol_groups, symbol_group_members, math_expr_nodes, math_expr_args, math_symbol_refs, math_float_constants, math_int_constants, math_affine, math_weighted_means, math_reductions, math_gathers, math_broadcasts, math_derivatives, math_integrals, math_smooth_ops, math_conditionals, math_kernel_calls, math_implicit_refs, math_unit_converts, math_piecewise_linear, math_quantity_selections, math_indexed_equations, math_free_indices, math_equations, math_objectives, math_implicit_systems, math_complementarity, math_alternative_sets, math_alternatives, math_dae_links, math_static_analysis, kernel_bindings, scaling_plans, variable_scales, equation_scales, initialization_plans, init_stages, solve_plans, problems, variable_order, equation_order, bound_values, case_bound_substitutions, stage_bundles, incidence, dm_partition, blocks, block_members, sparsity_patterns, evaluation_programs, backend_bindings |
runtime | runs, solutions, duals, residuals, iterations, solver_events, diagnostics_findings, kernel_evaluations, kernel_evaluation_outcomes, host_capabilities, sweep_results, profile_results, run_state |
provenance | derivations, pass_records, assertions, refs, closure_report |
Appendix C. Glossary
| Term | Meaning |
|---|---|
| Authored | written by an author or package; a primitive fact |
| Derived | produced by a pass or rule; never authored; always carries a derivation |
| Reference | shipped library data (units, elements, methods, templates) |
| Semantic ID | 128-bit stable identity of an authored or derived entity |
| Ordinal | artifact-local integer position |
| Content hash | blake3 of a canonically serialized artifact |
| Snapshot | a versioned semantic membership and parent set; one or more physical manifests can encode the same snapshot (§5.3, §20.2) |
| Encoding checksum | blake3 of finished stored bytes; physical integrity, distinct from logical content identity |
| Model revision, case revision, run | structure, values, execution (never mixed) |
| Template | declarative replacement for an IDAES *Data class: parameters, features, symbols, equations, submodels, ports, contributions, laws |
| Contribution | a typed physical term (flow, generation, transfer, accumulation) that laws collect |
| Law template / instance | a universal balance or accounting rule and its scoped application |
| Kernel | a constitutive computation implemented in code under a KernelSpec contract |
| Method | an IDAES-style property or reaction submodel, realized as an equation template or a kernel |
| Quantity type | dimension, kind, basis, reference state, scale kind, shape, subject |
| Scale kind | point or difference on origin-sensitive quantities; additive kinds retain point representation, and complete composition rules preserve scale distinctions (§8.3) |
| Undecided | a rule outcome of unknown or conflict, stored in inferred.undecided rather than in any head relation |
| Case-bound view | the canonical graph with recorded fixed/parameter substitutions from bound_values; the input of structural analysis (P14) |
| Index expansion | pass P12: the indexed equation instances of P7–P11 become one scalar equation row per valid index tuple |
| Engine profile | the declared DataFusion version, analyzer/optimizer/physical rule lists and the versioned semantic-settings allow-list that a rule-executing pass depends on (§14.2 rule 5) |
| Plan evidence | Noncanonical protobuf/EXPLAIN artifacts with physical checksums and codec/engine context; never semantic identity or a memo input (ADR-0044) |
| Derivation granularity | per-relation declaration of whether derivations are stored per row or reconstructed from the rule and the ID formula |
| Host capabilities | the recorded result of the explicit probe_host operation; the input from which a run’s solver options are resolved |
| Loss profile | the bundle manifest’s per-column statement of which pse.* extension types survive the Python boundary and which degrade to storage types |
| Overlay | an immutable case that overrides its parent |
| Plan | an ordered list of stages (initialization, scaling, solve, sweep) executed by the runtime |
| Pass | a contracted compiler transformation between relation versions |
| Rule | a typed, stratified inference producing derived rows |
| Closure report | proof that every closure category (type, topology, scope, quantity, method, balance, optionality, equation, runtime) is satisfied |
status: historical supersededBy: blueprint.md
Proposed architecture: an Arrow-native mathematical compiler
I would build this as a Rust-native mathematical modeling platform whose authoritative representation is a versioned collection of typed Arrow relations, with DataFusion providing relational planning, model assembly, validation, and analytical execution.
The crucial change would be this:
The model would no longer be a collection of objects that construct equations. The model would be a structured specification of domains, quantities, equations, dependencies, and numerical policies—from which object graphs and executable programs are generated.
Pyomo would become one generated execution backend. It would no longer determine the platform’s data structures, extension mechanisms, or model-authoring conventions.
Your requested version pairing is coherent: DataFusion 55.0.0’s workspace uses Arrow 59.2.0, and declares Rust 1.94.0 as its minimum supported version. I would pin the associated Arrow, Parquet, and DataFusion crates together rather than independently selecting their versions.
There is one important boundary to preserve: Arrow should unify the representation, but not dictate every numerical algorithm’s internal memory layout. Sparse factorizations, expression evaluation, and graph algorithms should operate on efficient native structures derived from the canonical relations. Those structures must be reproducible execution artifacts—not competing sources of model truth.
The architecture below is a proposed platform design. Names such as CanonicalMathGraph, KernelSpec, and CompilationPlan describe abstractions to implement, not existing Arrow or DataFusion classes.
1. One authoritative substrate, three levels of mathematical representation
I would organize the platform around three related representations.
Declarative model packages + engineering datasets + problem specification
│
▼
CanonicalModel
Physical entities, domains, model templates,
property capabilities, connections, specifications
│
Semantic compilation
▼
CanonicalMathGraph
Typed, indexed symbolic equations and operators;
explicit dependencies, units, assumptions, provenance
│
Instantiation and numerical lowering
▼
CanonicalMathProblem
Ordered unknowns, equations, objectives, derivative
structure, initialization plans, scaling, backend bindings
│ │
▼ ▼
Native Rust kernels Generated Pyomo model
│ │
└─────────┬─────────┘
▼
Results and diagnostic relations
All three representations would use the same schema system, identity conventions, artifact model, and provenance mechanisms. They are different compilation stages, not separate application databases.
CanonicalModel: engineering intent
This contains statements such as:
- A reactor uses a specified material system and reaction model.
- A conservation law applies over a spatial domain.
- Two ports are connected through an explicitly selected connection rule.
- A quantity is specified, estimated, optimized, observed, or left to be solved.
- A numerical policy calls for a particular discretization or initialization strategy.
It should remain relatively compact. A distributed reactor should not require the author to enumerate every mesh-point variable.
CanonicalMathGraph: mathematical meaning
This represents the selected physics as typed, indexed symbolic expressions.
For example:
[ \frac{\partial n_j}{\partial t} + \frac{\partial F_j}{\partial x}
\sum_r \nu_{r,j} R_r. ]
The graph records the meanings of (n_j), (F_j), the indexing domains, derivative operators, stoichiometry, and constitutive dependencies. It does not yet need to expand every expression into scalar solver objects.
CanonicalMathProblem: an executable mathematical problem
This is the fully specified problem delivered to a backend: variable ordering, equation ordering, bounds, parameter bindings, sparse derivative structure, numerical transformations, and backend capabilities.
The primary architectural advantage is late commitment. Physical meaning survives independently of mesh resolution, solver choice, Pyomo class structure, or current numerical values.
2. Make the semantic schema—not merely the Arrow schema—the foundation
Arrow provides an excellent physical representation, but an Arrow schema alone does not express everything required for an engineering modeling system.
I would define a semantic schema registry that generates:
Semantic relation definitions
├── Arrow schemas and field metadata
├── Typed Rust column views and builders
├── Serialization and import adapters
├── Referential and scientific invariant checks
├── Authoring-language schemas
├── Documentation and inspection interfaces
└── Version migrations and compatibility tests
This is where much of the boilerplate disappears: a relation’s structure is declared once, rather than separately maintained in Rust structs, Python classes, Arrow schemas, validation code, and documentation.
serde_arrow is useful at this boundary and supports Arrow 59. I would use explicit schemas for production interchange, while reserving generated column builders for performance-sensitive paths. Schema inference from representative records would not be the production contract.
2.1 The canonical relation families
I would start with the following model.
| Relation family | Representative relations | Meaning |
|---|---|---|
| Identity and versions | model_versions, package_versions, entities, source_anchors | Stable identities, ownership, names, package versions, source locations. |
| Physical types | quantity_types, units, reference_states, conversion_rules | Dimensions, quantity kinds, measurement conventions, material bases. |
| Domains and coordinates | domains, typed membership relations, meshes, cells, faces, quadrature_rules | Valid indexing combinations and spatial/temporal structure. |
| Material systems | species, phases, phase_species, stoichiometry, material_systems | Chemical and phase structure, admissible combinations, reactions. |
| Model composition | templates, instances, feature_bindings, capability_requirements | Declarative unit models, property formulations, composition, structural options. |
| Symbols | symbols, symbol_instances, aliases | Variables, parameters, derived quantities, references, indexing. |
| Mathematics | expr_nodes, expr_args, typed operator-payload relations | Symbolic operations and their dependencies. |
| Problem statements | constraints, objectives, implicit_systems, events, complementarity_pairs | Mathematical conditions to satisfy or optimize. |
| Connectivity | ports, port_members, connections, connection_rules | Physical interfaces and the rules that generate connection equations. |
| Cases and observations | parameter_bindings, variable_specs, observations, measurement_models | Operating conditions, initial guesses, estimation data, uncertainty. |
| Numerical infrastructure | kernel_specs, pass_specs, initialization_plans, scaling_plans | Registered computation and numerical strategies. |
| Derived structure | incidence, partitions, sparse_patterns, backend_bindings | Compilation products used by numerical backends. |
| Execution and evidence | runs, solutions, residuals, diagnostics, derivations | Results, statuses, explanations, and lineage. |
These should be typed relations, not a generic entity–attribute–JSON store.
For instance, stoichiometry deserves an explicit relation with reaction, phase, species, and coefficient columns. Hiding it inside a general-purpose metadata map would sacrifice exactly the structural advantages you are pursuing.
2.2 Separate identity from location and numerical state
I would use three distinct forms of identity:
Stable semantic IDs, such as 128-bit identifiers, identify authored entities and declarations across revisions.
Artifact-local integer IDs provide efficient indexing inside a compiled expression graph or solver vector.
Content hashes identify immutable versions of relations, packages, and compiled artifacts.
Names, Arrow row positions, dictionary codes, and memory addresses must never serve as semantic identity.
For generated quantities, identity should derive from the parent instance, template declaration, and domain-member identities—not from current numerical values.
2.3 Separate structure, specifications, and results
Three independently versioned concepts are necessary:
Model revision: What equations and physical entities exist?
Case revision: What values, bounds, fixed statuses, and objectives apply?
Run: What happened when a particular backend executed that case?
This separation permits a compiled model to support many operating cases without mixing results back into its definition.
It also avoids an important cache error: a change in numerical specifications might leave the equation topology unchanged while invalidating constant folding, initialization assumptions, or a solver-specific compiled representation.
3. Use Arrow’s richer structures deliberately
The substrate should exploit Arrow’s capabilities without mechanically using every available datatype.
| Arrow capability | Recommended architectural use |
|---|---|
| Primitive numeric arrays | Coefficients, coordinate values, operating data, residuals, solution vectors. |
Struct | Small, well-defined composite records such as tagged bounds or measurement specifications. |
List / FixedSizeList | Variable-length connectivity and fixed-size vector quantities where their semantics are uniform. |
| Dictionary encoding | Repeated categorical labels and enum presentation—not persistent identity. |
| Field and schema metadata | Schema versions, semantic type references, quantity annotations, interpretation contracts. |
| Extension types | Domain-specific logical types with explicit Arrow storage representations. |
| IPC and C Data/C Stream interfaces | Interchange between Rust components and the Python backend. |
| Parquet | Durable, partitioned relations and large result datasets. |
Arrow 59.2 includes Rust support for extension types, while the Arrow format specifies how extension names and metadata accompany their storage types. These mechanisms support the representation of domain-specific semantics; they do not, by themselves, implement those semantics in a query engine.
3.1 Physical meaning requires more than a unit string
I would define a QuantityType containing:
Numeric representation
Dimension exponents
Quantity kind
Material basis
Index/shape definition
Reference-state convention
Affine or coordinate semantics, where applicable
This allows the compiler to distinguish:
- Absolute temperature from temperature difference.
- Absolute pressure from gauge pressure.
- Molar enthalpy from mass-specific enthalpy.
- Enthalpies using incompatible reference states.
- Species composition from phase composition.
- A vector indexed by species from an equally sized vector indexed by spatial cells.
A mass-to-molar conversion is not a numeric cast. It requires a molecular-mass relationship. A gauge-pressure conversion requires a reference pressure.
Those dependencies should become explicit mathematical operations.
3.2 Avoid the heterogeneous-value-column trap
A relation like:
variable_id | value
can legitimately contain temperatures, pressures, and flow rates. But its value field cannot truthfully carry one common unit annotation.
I would therefore require either a per-row quantity_type_id or type-specialized derived relations. Engineering-facing aggregate queries would use these typed views.
Otherwise, ordinary SQL could accidentally compute the sum of a pressure, a temperature, and a flow rate.
3.3 Null is not an optimization variable
An unknown in a mathematical problem must be represented by a symbol, not a null floating-point value.
Null can mean “no initial guess was supplied” or “this measurement is missing.” It must not mean “the nonlinear solver should determine this value.”
Similarly, an unbounded variable should use an explicit bound representation, not a NaN sentinel.
These distinctions make validation and cross-language behavior substantially more reliable.
4. Build a mathematical IR that is richer than DataFusion expressions
I would not make DataFusion’s Expr the canonical mathematical representation.
DataFusion provides logical and physical expression systems for query execution, and exposes extensibility through its planner and execution interfaces. Those are valuable implementation mechanisms, but a process-modeling IR also needs free unknowns, indexed quantification, differential operators, implicit systems, and backend-specific mathematical constraints.
4.1 A normalized expression representation
A simplified portion of the proposed schema could look like this:
expr_nodes
node_id: UInt64
opcode: UInt16
quantity_type_id: UInt32
scope_id: UInt64
expr_args
parent_node_id: UInt64
argument_ordinal: UInt32
child_node_id: UInt64
symbol_refs
node_id: UInt64
symbol_id: UInt64
float_constants
node_id: UInt64
value: Float64
kernel_calls
node_id: UInt64
kernel_binding_id: UInt64
constraints
equation_id: UInt64
owner_id: UInt64
body_node_id: UInt64
lower_bound_id: UInt64
upper_bound_id: UInt64
This is illustrative schema notation. Integer IDs here are artifact-local; stable source identities are maintained separately.
Operator-specific payloads belong in typed relations. An expression node should not contain twenty nullable fields, most of which are irrelevant to its operator.
The authoritative argument representation would be expr_args. Packed adjacency arrays can be generated for execution, rather than independently maintained as a second graph definition.
4.2 Preserve indexed mathematics until expansion is necessary
The IR should directly represent operations such as:
SumOver(domain, expression)
Gather(symbol, coordinate_mapping)
Broadcast(expression, domain)
Derivative(expression, continuous_domain)
Integral(expression, domain, quadrature_policy)
ImplicitSystem(equations, unknowns)
KernelCall(binding, arguments)
This matters enormously for distributed and multicomponent models.
A model containing a species balance over 1,000 cells should initially retain one indexed balance declaration—not immediately become thousands of manually assembled expression trees.
Scalarization becomes a compiler decision dictated by the selected backend.
4.3 Make every operator carry its mathematical contract
An operator definition should declare:
| Contract | Purpose |
|---|---|
| Input/output physical types | Reject dimensionally or semantically invalid expressions. |
| Index and shape rules | Validate reductions, broadcasting, and coordinate mappings. |
| Evaluation semantics | Define the numerical operation. |
| Derivative rules | Generate Jacobian, directional derivative, and adjoint computations. |
| Domain and smoothness information | Detect invalid inputs and select compatible numerical methods. |
| Sparsity/dependency rules | Generate structural analysis and efficient derivative layouts. |
| Rewrite conditions | Permit only justified simplifications. |
| Backend lowerings | Define how the operation becomes native code or Pyomo expressions. |
This creates a single extensibility point from which multiple capabilities follow.
For example, registering a new constitutive relation should not require unrelated handwritten implementations for evaluation, dependency reporting, unit inference, DataFusion wrapping, and Pyomo adaptation.
However, an opaque native function still needs a trustworthy derivative implementation or an explicitly supported differentiation route. A schema cannot invent the derivative of arbitrary foreign code.
4.4 Distinguish expression acyclicity from equation coupling
The expression DAG should be acyclic.
The mathematical problem can still contain tightly coupled equations, recycles, and implicit thermodynamics. Those cycles appear in the variable–equation incidence structure, not as cycles in expression ownership.
Keeping that distinction explicit simplifies serialization, evaluation ordering, and structural diagnostics.
5. Replace model-building methods with declarative templates
The central authoring abstraction should be a model template, not a class with a build() method.
A template would specify:
Required parameters and capabilities
Declared quantities and indexing domains
Ports and interface contracts
Equation templates
Feature guards
Validity assumptions
Numerical hints
A heater, a reactor, a property model, and a costing correlation would all use this same mechanism.
5.1 Conservation laws become reusable operators
Rather than separate procedural builders for many balance variants, define a balance structurally:
[ \frac{d}{dt} \left( \text{inventory of conserved quantity} \right)
\text{net boundary flux} + \text{internal generation}. ]
Its specification identifies the conserved quantity, domain, inventory expression, flux laws, source terms, and boundary conditions.
For a lumped unit, the domain has one control region.
For a distributed unit, the same abstract law is combined with cells, faces, geometry, and a discretization policy.
Material, energy, elemental, and charge balances become different bindings of the same underlying abstraction.
5.2 Property selection becomes explicit dependency resolution
A unit’s equations might require enthalpy flow, density, and viscosity.
The compiler would resolve those requirements against registered property capabilities, select the configured providers, and expand the required expression or kernel dependencies.
This replaces lazy attribute construction with an explicit sequence:
Required engineering quantity
↓
Selected provider and version
↓
Required state formulation and parameters
↓
Dependency closure
↓
Generated equations and kernel bindings
Inspecting the model would never construct additional physics.
The selected provider must be persisted. A future package installation must not silently change which equation of state or correlation an existing model uses.
5.3 Connections must declare their physical rule
A connection is not merely a join between similarly named variables.
The platform should distinguish equality connections, conservation junctions, mixing relations, heat-transfer interfaces, and signal connections.
A mixer, for example, requires conservation and appropriate outlet-state relationships. It cannot be implemented by equating every inlet temperature.
Connection templates should therefore generate explicit equations with documented orientation and sign conventions.
5.4 Example: one heater template, different problem specifications
A simplified heater template could generate:
[ F_{\mathrm{out},j}=F_{\mathrm{in},j}, ]
[ \sum_p \dot H_{\mathrm{out},p}
\sum_p \dot H_{\mathrm{in},p} -Q=0, ]
[ P_{\mathrm{out}}=P_{\mathrm{in}}. ]
One case fixes (Q) and solves for outlet temperature. Another fixes outlet temperature and solves for (Q).
No new equipment script is needed.
A proposed authoring syntax might look like:
model:
material_system: benzene_toluene@1
instances:
heater:
template: heater.lumped@1
material_system: benzene_toluene@1
features:
dynamic: false
pressure_change: false
case:
specifications:
heater.inlet.temperature: {fixed: "320 K"}
heater.inlet.pressure: {fixed: "2 bar"}
heater.outlet.temperature: {fixed: "370 K"}
numerical_policy:
initialization: structural_blocks_then_continuation@1
scaling: physical_nominals@1
This is schematic, not a complete executable model. Flow and composition specifications would also be required.
The important point is that the authoring document compiles into canonical relations. It is not interpreted as a sequence of imperative operations against Pyomo objects.
6. Give DataFusion a substantial—but precise—role
DataFusion should be deeply integrated, not relegated to reporting after the model has been built elsewhere.
I would use it for four major responsibilities.
6.1 Relational model assembly
DataFusion joins and projections would assemble:
- Valid phase–species combinations.
- Template instances over domains.
- Property coefficient bindings.
- Reaction source terms.
- Connection mappings.
- Scenario parameter overlays.
- Measurement-to-symbol bindings.
For example, reaction generation can be assembled by joining stoichiometry with local reaction instances and then creating the corresponding indexed symbolic reduction.
That is different from numerically summing current reaction-rate values. The assembly stage creates mathematics; the evaluation stage computes values.
6.2 A snapshot-aware model catalog
Implement CatalogProvider and TableProvider interfaces over immutable model snapshots and derived artifacts.
A table scan would be pinned to a specific model or case revision. Projection and filter pushdown could reduce the data read for a requested compilation or analytical query.
DataFusion’s TableProvider exposes schema, scan planning, and pushdown contracts, including distinctions between exact and inexact filter handling. Those contracts must be implemented truthfully.
A backend must not silently prune equations from a coupled mathematical problem merely because an analytical query requests a subset of outputs.
6.3 Domain-aware logical and physical operators
I would implement deterministic operators such as:
ExpandIndexDomain
ResolvePropertyRequirements
GenerateStencil
AssembleIncidence
EvaluateConstitutiveKernel
ProjectSolution
DataFusion provides extension mechanisms for custom planning and execution operators. These make it possible to retain relational integration while invoking specialized Rust algorithms where ordinary SQL is unsuitable.
An important implementation detail: each DataFusion execution plan still produces a declared output schema. A compiler pass that produces symbols, equations, and provenance would publish a typed artifact bundle containing several relations, rather than pretending that one output stream can contain arbitrary heterogeneous tables.
6.4 Semantically informed UDFs
DataFusion 55’s ScalarUDFImpl exposes useful hooks beyond evaluation, including return_field_from_args, simplification, interval evaluation, constraint propagation, and output-ordering information. I would generate appropriate wrappers from the kernel and quantity-type registries.
For example, an enthalpy function could expose its output field semantics and supported validity information.
These hooks are useful integration points, not an existing thermodynamic type checker, automatic differentiator, or certified nonlinear feasibility engine. Those domain implementations remain part of the proposed platform.
What I would explicitly keep out of ordinary UDFs
I would not expose an effectful nonlinear solver as an ordinary scalar function such as:
SELECT solve_process(model_id);
Instead, a typed execution plan would launch the solve, persist its run artifacts, and expose completed results as queryable relations.
This gives solver execution explicit lifecycle, cancellation, retry, provenance, and resource semantics rather than allowing those concerns to become hidden inside query evaluation.
7. Make compilation, initialization, scaling, and discretization first-class plans
The replacement for ad hoc workflow scripts should be a versioned compiler and execution-plan system.
7.1 Each compiler pass has a declared contract
A PassSpec would identify:
Pass implementation and version
Required input relation schemas
Produced output relation schemas
Preconditions and postconditions
Dependency footprint
Determinism and numerical assumptions
Cache-key definition
Diagnostic and provenance outputs
The compiler derives a dependency DAG from these contracts.
A typical compilation sequence would be:
Validate semantic schema
→ resolve packages and physical types
→ instantiate templates and valid domains
→ resolve property dependencies
→ generate connection equations
→ lower differential and spatial operators
→ perform domain-safe simplification
→ analyze incidence and equation structure
→ generate derivative and sparse-layout artifacts
→ generate scaling and initialization plans
→ lower to a selected backend
Individual stages can contain DataFusion plans, native graph algorithms, or symbolic transformations. Their inputs and outputs still belong to the same substrate.
7.2 Initialization becomes a generated plan over immutable overlays
An InitializationPlan would contain stages identifying equation subsets, temporary specifications, tear variables, continuation schedules, tolerances, and failure policies.
A stage could:
- Solve a selected property subsystem.
- Propagate its values into a downstream subsystem.
- Solve strongly connected equation blocks.
- Apply a continuation schedule to difficult terms.
- Solve the complete problem.
Temporary fixings and deactivations would be case overlays, not mutations to the authoritative model.
Restoring the original problem means discarding the overlay—not remembering which Python objects were changed and reversing those mutations.
This makes initialization reproducible and inspectable. It does not guarantee that every nonlinear system admits an automatically successful initialization strategy.
7.3 Scaling becomes an explicit mathematical transformation
Store scaling as a transformation:
[ x=x_{\mathrm{ref}}+D_x\hat{x}, ]
[ \hat r(\hat{x})=D_r,r(x_{\mathrm{ref}}+D_x\hat{x}), ]
so that
[ \hat J=D_rJD_x. ]
The transformation records the variable and residual scales, offsets, assumptions, and mappings required to return results and sensitivities to physical coordinates.
Scaling is then no longer a collection of scattered annotations. It is a compiled artifact with a defined mathematical effect.
7.4 Discretization is another lowering pass
A continuous-domain model would retain derivative, flux, boundary, and geometry semantics until a policy selects finite-volume, finite-difference, or collocation rules.
The resulting mesh, stencil, interpolation, quadrature, and boundary relations become explicit artifacts.
Adaptive refinement produces a new discretization artifact and a recorded state-transfer mapping.
The core physical template remains unchanged.
7.5 Incrementality must follow scientific dependencies
I would cache separately:
| Artifact | Typical invalidation condition |
|---|---|
| Template expansion | Structural options, domain membership, package selection. |
| Property dependency closure | Required outputs, state formulation, provider capabilities. |
| Symbolic simplification | Expressions and any assumptions or parameter values used by the rewrite. |
| Sparse structure | Equation dependencies, variable treatment, discretization. |
| Numerical evaluation | Current values and parameter bindings. |
| Solver preparation | Backend settings, ordering, scaling, bounds, fixed-variable treatment. |
A value change should not automatically trigger a full rebuild. But it must invalidate any compiled result that depended on that value being constant or satisfying a particular condition.
8. Use a unified kernel contract, with native numerical layouts inside execution
The most important supporting abstraction is a KernelSpec.
It should govern a constitutive function’s participation in the entire system:
| Kernel contract | Contents |
|---|---|
| Identity | Provider, version, artifact digest. |
| Signature | Physical input/output types, shapes, indexing, parameter requirements. |
| Mathematical behavior | Explicit function or implicit system; smoothness and validity regions. |
| Derivatives | Available Jacobians, directional derivatives, adjoints, Hessian information. |
| Execution | Scalar and batch implementations, thread safety, resource requirements. |
| Failure behavior | Domain errors, nonconvergence, invalid states, recoverable conditions. |
| Backend bindings | Native execution, DataFusion wrapper, Pyomo-compatible lowering. |
8.1 One implementation contract, several generated adapters
The goal is not three independently maintained enthalpy implementations.
The preferred pattern is:
Constitutive equation or registered Rust kernel
├── Scalar evaluation adapter
├── Batched Arrow adapter
├── Derivative adapter
├── DataFusion wrapper
└── Pyomo/backend binding
For kernels written generically over supported numerical types, automatic differentiation can reduce derivative boilerplate. num-dual provides scalar and higher-order dual-number machinery, including facilities for implicit differentiation; it is well suited to relatively small constitutive calculations. It is not a reason to construct a huge dense dual vector for every unknown in an industrial flowsheet.
8.2 Treat implicit thermodynamics explicitly
A flash calculation should not automatically be modeled as an ordinary pure scalar function.
It can instead remain an equation subsystem:
[ G(z,u)=0, ]
or be encapsulated as an implicit kernel with a declared branch-selection and convergence contract.
When (G_z) is nonsingular on the selected smooth branch,
[ \frac{dz}
-G_z^{-1}G_u. ]
That derivative relationship can be part of the kernel’s implementation strategy. Phase transitions, root changes, and singular states need explicit handling rather than being hidden behind a nominally differentiable API.
8.3 Do not run relational queries inside every Newton iteration
The native numerical backend should compile the mathematical graph into an evaluation program operating on contiguous numerical arrays.
It would use:
Ordered variable vector
Bound parameter arrays
Compiled evaluation instructions
Precomputed sparse derivative pattern
Reusable numerical workspaces
DataFusion is valuable for assembling these inputs, evaluating independent batches, and analyzing results.
It should not repeatedly rediscover the expression graph through joins during each residual evaluation.
For sparse linear algebra, faer provides native sparse structures, including separate symbolic and numerical compressed-column representations. Those are appropriate derived execution layouts for Jacobians and related calculations.
Arrow buffers can be borrowed when layouts and ownership permit. Reordering, mutable solver workspaces, or incompatible layouts may require deliberate copies. “Arrow-native” should not become an obligation to preserve zero-copy at the expense of correctness or numerical performance.
8.4 Parallelize where the mathematics permits it
I would batch independent states, scenarios, coefficient evaluations, and compatible constitutive calls.
A tightly coupled nonlinear system does not become embarrassingly parallel because its inputs are columnar.
The runtime should also coordinate DataFusion, native worker pools, and solver-library threading to avoid oversubscription.
9. Reduce Python to one generic, coarse-grained Pyomo adapter
The Python boundary should be model-independent.
A single adapter would consume a CanonicalMathProblem, construct the necessary Pyomo objects, invoke the selected backend, and return Arrow result relations.
9.1 Transfer model bundles, not individual operations
Use PyO3 and the Arrow Python bridge to exchange batches or streams.
arrow-pyarrow 59.2 supports mappings for schemas, arrays, record batches, and readers through the Arrow interoperability interfaces. The PyCapsule interface standardizes the relevant C Data and C Stream exchange conventions.
The bridge should transfer something like:
Problem manifest
Symbol and parameter relations
Expression graph relations
Constraint and objective relations
Numerical specifications
Kernel bindings
Source maps
It should not call Python once per coefficient or Rust once per expression node.
For isolated worker processes, use an IPC-based transport. An in-process C interface does not make raw pointers valid across process boundaries.
9.2 Generate a solver model, not an IDAES object hierarchy
The adapter would:
Construct symbols. Map canonical IDs to Pyomo variables and parameters.
Lower expressions. Traverse the already ordered expression graph and construct symbolic expressions.
Attach equations and objectives. Use the canonical problem’s definitions, with efficient treatment of affine and other recognized structures.
Apply the case and numerical plan. Bind parameters, fix variables, set bounds and starting values, and configure the solve.
Return results. Map primal values, supported duals, residuals, termination conditions, and diagnostics back to canonical IDs.
There is no reason to recreate the full IDAES class hierarchy merely to produce an equivalent mathematical problem. Semantic ownership and engineering names remain in the substrate and source maps.
9.3 Preserve symbolic meaning across the boundary
The adapter must not evaluate an expression at its initial guess and substitute that number into the Pyomo model.
The symbolic dependence on decision variables must survive lowering.
Pyomo has its own expression representation, so transferring Arrow data efficiently does not eliminate the cost of constructing those symbolic objects.
9.4 External Rust functions need actual solver integration
For expressions lowered entirely into Pyomo algebra, the solver interface can work with the generated expression structure.
For opaque Rust property kernels, the binding is more demanding. Pyomo’s external-function documentation explicitly distinguishes Python callbacks from compiled AMPL-compatible external libraries; expressing a function in Pyomo does not imply every solver interface can execute it. ASL-based nonlinear solvers require the corresponding compiled external-function route.
I would therefore make external-function ABI compatibility, derivative availability, and backend support mandatory parts of KernelSpec, with end-to-end tests.
A generic Rust extern "C" function alone is not a complete solution.
10. Make persistence and reproducibility part of the same architecture
I would use immutable Arrow/Parquet artifacts plus a versioned snapshot manifest.
A manifest identifies the exact relation artifacts, schema versions, package versions, and compilation dependencies that define a model or problem.
10.1 Publish complete snapshots, not unrelated table updates
A write would proceed as follows:
Construct proposed changes
→ validate all relation and scientific invariants
→ write immutable relation artifacts
→ write the snapshot manifest
→ conditionally update the branch/head reference
Readers remain pinned to their existing manifest.
object_store supplies useful primitives including atomic individual-object writes and conditional operations. The multi-relation snapshot protocol would be implemented by this platform; it is not automatically provided by Arrow, Parquet, or DataFusion.
For an initial local implementation, a single writer with explicit snapshot publication is a reasonable simplification.
10.2 Record everything necessary to explain a run
A run should reference:
Model and case revisions
Material/property package versions
Compiler and pass versions
Kernel artifact hashes
Discretization and scaling artifacts
Initialization plan
Backend and solver versions
Solver settings
Execution environment information
Result and diagnostic artifacts
This supports questions such as:
“Which property correlation produced the enthalpy term in this failed equation?”
“Which assumption allowed this variable to be eliminated?”
“What changed between these two cases?”
The answers become relational queries over provenance—not archaeology through execution logs.
10.3 Hash semantic content, not incidental batch layout
Content addressing requires canonicalization rules for row ordering, dictionary encodings, schema metadata, and floating-point representations.
Two logically identical relations split into different Arrow batches should not accidentally be treated as different scientific models.
Conversely, a change in reference-state convention must change the semantic identity of the relevant artifact, even when the numeric storage type remains Float64.
11. Supporting Rust libraries: a focused selection
I would keep the supporting stack deliberately focused. Each library should provide an algorithm or integration mechanism without becoming another authoritative representation.
| Library | Proposed role | Boundary to maintain |
|---|---|---|
serde + serde_arrow | Schema-governed import/export and authoring adapters. Arrow 59 support is available. | Explicit schemas remain authoritative; no production inference from sample data. |
petgraph | Temporary graphs for topology, dependency ordering, and structural algorithms. | Persist graph facts in Arrow; graph objects are derived views. Additional matching/decomposition algorithms may require implementation. |
salsa | Incremental compilation and dependency-aware memoization. | Cache compiler computations, not the authoritative engineering model. |
egglog | Optional rule-based inference and equality-saturation optimization. | Only admit rewrites justified by physical types, domains, and numerical policy. |
num-dual | Derivatives for constitutive and implicit kernels. | Use appropriately sized derivative calculations; preserve sparse global structure. |
faer | Dense/sparse numerical linear algebra, diagnostics, and factorization workspaces. | Native matrices are compiled numerical artifacts, not another model definition. |
uom | Compile-time dimensional checking inside selected Rust kernels. | The dynamic quantity registry remains necessary for arbitrary user-defined models. |
FeOs / feos-core | Selected native thermodynamic model implementations. | Treat as capability providers; validate coverage and conventions rather than assuming complete IDAES replacement. |
diffsol / DiffSL | Optional native trajectory backend for supported differential-equation formulations. | Lower the canonical IR into it; do not introduce a second independently authored model language. |
blake3 + object_store | Content addressing, artifact storage, snapshot publication. | Hashing and object storage do not themselves supply semantic equivalence or database transactions. |
Two qualifications are especially important.
First, symbolic rewrites need conditions. For example, (x/x\rightarrow1) requires a nonzero-domain assumption. Floating-point reassociation also needs an explicit numerical policy; mathematical equivalence does not always mean identical floating-point behavior.
Second, the platform should not add a second dataframe engine, graph database, or vector database merely for completeness. Those additions would need a demonstrated capability gap. They are not prerequisites for the substrate described here.
12. What this replaces—and what would demonstrate that it works
The proposal is a wholesale alternative at the modeling layer:
| Existing architectural pattern | Proposed replacement |
|---|---|
| Classes constructing mutable model objects | Declarative templates compiled into immutable relations. |
| Construction-time Python configuration | Typed structural specifications with explicit dependency effects. |
| State-block attribute access triggering construction | Explicit property-demand resolution and dependency closure. |
| Separate mathematical and metadata representations | Typed expression IR linked directly to physical semantics and provenance. |
| Model-specific initialization scripts | Generated initialization plans with immutable specification overlays. |
| Scattered scaling annotations | Explicit, invertible scaling transformations. |
| Backend-specific model construction | Generic lowering from a backend-neutral mathematical problem. |
| Saved values requiring the original construction code | Complete model packages, specifications, and derived artifacts sufficient to rebuild the problem. |
| Agent-authored imperative edits | Validated structural change sets against the canonical schema. |
A particularly valuable consequence for agent support
An agent could propose a structured change such as:
Replace the selected heat-transfer correlation, verify its required properties, regenerate affected equations, compare sparsity and scaling, and report the resulting model differences.
That request could be resolved into a typed change set and compilation plan.
The agent would not need to locate and edit a collection of build(), initialization, scaling, and reporting methods. Nor would the platform need to trust arbitrary generated scripts as its normal extension mechanism.
Build it through complete vertical slices
I would validate the architecture with three progressively demanding examples.
A steady-state heater/mixer system should demonstrate declarative templates, physical typing, connection generation, Arrow persistence, generic Pyomo lowering, and reproducible reload.
A recycle system with an implicit flash calculation should demonstrate dependency closure, structural analysis, initialization plans, derivative correctness, and explicit phase/validity behavior.
A distributed dynamic reactor with costing or parameter estimation should demonstrate continuous-domain lowering, reaction structure, sparse derivatives, case reuse, and the ability to combine physical and economic equations without another modeling framework.
The principal acceptance tests should include:
- Agreement between native and Pyomo residuals and derivatives at valid test points.
- Conservation, unit, basis, reference-state, and scaling consistency.
- Stable semantic identity despite changed batch boundaries or row ordering.
- Correct incremental invalidation when specifications or assumptions change.
- Complete provenance for generated equations and failed computations.
- Explicit failure when a selected backend cannot support an operator or derivative requirement.
I would benchmark construction, compilation, Python materialization, residual evaluation, derivative evaluation, and solver time separately. The strongest early gains are likely to be less repeated construction work, less boilerplate, better inspection, and more reliable reuse. Overall solve-time improvement must be measured rather than inferred from replacing Python data structures.
Bottom line
The advantageous pivot is not “represent IDAES objects in Arrow.” It is “make mathematical and physical semantics the data model, then compile every executable representation from it.”
Arrow would provide the common typed representation and interchange. DataFusion would provide relational assembly, planning, validation, batch computation, and analysis. A Rust mathematical compiler would supply equation semantics, transformations, differentiation, and numerical plans. Pyomo would remain a replaceable generated backend.
That combination provides a credible route to the unified design you are seeking: specialized physics still requires specialized mathematics, but the machinery for declaring, connecting, validating, initializing, scaling, executing, and explaining that mathematics can become largely schema-driven rather than model-specific scripting.
Decision records
Each record states one decision, the blueprint sections it governs, the charter
§H fields (principles, level, evidence, revisit trigger, verification), and the
design-review finding that motivated it. A record is immutable once accepted:
it changes only by superseding it with a new record, or by a status edit and an
appended ## Status history line. docs/authoritative_design/blueprint.md is
the authoritative design; these records say why it reads the way it does.
Run just adr-new <slug> to start one and just adr-lint before opening the PR.
Open items that were deliberately deferred live in register.md.
| ID | Title | Status | Date | Blueprint | Revisit |
|---|---|---|---|---|---|
| ADR-0001 | Record decisions as ADRs with the charter §H fields | accepted | 2026-09-13 | §0.1 | A second maintainer joins, or two consecutive months pass in which ADRs are written after the change rather than before it |
| ADR-0002 | Name the project pse-arrow, prefix crates pse-, dual-license MIT OR Apache-2.0 | accepted | 2026-09-13 | §3.2, §26 | The PyPI name pse becomes available, or a crates.io name collision forces a different crate prefix at the phase-0 publish |
| ADR-0003 | Re-implement IDAES clean-room and pin parity to idaes-pse 2.12.0 | accepted | 2026-09-13 | §0.2, §3.1, §6.14, §25 | IDAES releases a version that changes an enumeration preserved by name in blueprint §6.14, or drops a CPython version the parity environment needs |
| ADR-0004 | Adopt D1: typed Arrow relations are the only model authority | accepted | 2026-09-13 | §D1, §4.1, §6 | A relation family needs content no typed schema can express and a JSON column is proposed for it a second time |
| ADR-0005 | Adopt D2: authors write primitive facts, passes derive everything else | accepted | 2026-09-13 | §D2, §14.1, §22.2 | A derived relation acquires an authored write path, or derivation rows exceed the §24.3 benchmark’s budget per equation |
| ADR-0006 | Adopt D3: one catalog holds CanonicalModel, CanonicalMathGraph and CanonicalMathProblem | accepted | 2026-09-13 | §D3, §5, §14.1 | A fourth artifact level is proposed, or a level needs identity, hashing or provenance conventions the other two do not share |
| ADR-0007 | Adopt D4: semantic IDs, artifact ordinals and content hashes are three different things | accepted | 2026-09-13 | §D4, §5.1, §5.2 | A relation needs an identity that is neither a semantic ID, an ordinal nor a content hash |
| ADR-0008 | Adopt D5: every quantity carries a quantity_type_id, and pse.* extension types are registered | superseded | 2026-09-13 | §D5, §4.4, §8 | A quantity-bearing column appears without a quantity_type_id, or the engine’s extension-type registry stops validating storage types at planning time |
| ADR-0009 | Adopt D6: the math IR is relations, and DataFusion Expr computes over it | accepted | 2026-09-13 | §D6, §7.1, §7.2 | A backend requires an operator the IR cannot express without scalarizing earlier than §7.1 allows |
| ADR-0010 | Adopt D7: balance, costing and utility laws are templates expanded over a contributions substrate | accepted | 2026-09-13 | §D7, §10.1, §10.2 | A law family cannot be expressed over contributions and needs a compiler rule of its own |
| ADR-0011 | Adopt D8: a pass computes the property closure; inspection never constructs physics | superseded | 2026-09-13 | §D8, §9.6 | A property provider needs to be selected at evaluation time rather than at P6, for example because a method’s applicability depends on a solved value |
| ADR-0012 | Adopt D9: every constitutive computation is a KernelSpec with generated bindings | superseded | 2026-09-13 | §D9, §18.5, §21.4 | A kernel needs a binding form the six generated ones cannot express, or a third-party compiled kernel package is admitted (§22, ADR-0037) |
| ADR-0013 | Adopt D10: DataFusion has four roles and never runs inside a Newton iteration | accepted | 2026-09-13 | §D10, §5.4, §14.2 | A fifth role is proposed for DataFusion, or Exact pushdown stops holding for the key shapes §5.4 depends on |
| ADR-0014 | Adopt D11: execution layouts are compiled artifacts and zero-copy is a preference | superseded | 2026-09-13 | §D11, §18.2, §15.4 | A borrow precondition is relaxed, or a measurement shows the copy path dominates an evaluation benchmark |
| ADR-0015 | Adopt D12: one generated Pyomo adapter, no IDAES class hierarchy | accepted | 2026-09-13 | §D12, §21.1, §21.2 | A Pyomo-ecosystem tool needs a per-node API the coarse-grained bundle cannot serve, or a native equivalent makes the adapter redundant |
| ADR-0016 | Adopt D13: initialization, homotopy and fix-then-release are immutable case overlays | accepted | 2026-09-13 | §D13, §19.1, §17.1 | An operation genuinely needs to change structure mid-run, for example a native GDP lowering that adds equations during a solve |
| ADR-0017 | Adopt D14: memoize on declared inputs, the pass version and the engine profile | superseded | 2026-09-13 | §D14, §14.3, §14.4 | A measurement shows sub-pass granularity finer than per-instance is needed (the same trigger as ADR-0020), or an undeclared read is found in a pass |
| ADR-0018 | Pin both families with =, commit the lockfile, run cargo deny, and set MSRV to the pinned toolchain | accepted | 2026-09-13 | §3.1, §3.2, §3.3 | A DataFusion or Arrow release requires a Rust version above the pinned toolchain, or a pre-1.0 crate raises its declared floor above ours |
| ADR-0019 | Record the DataFusion plan fingerprint as evidence; keep it out of the memo key | superseded | 2026-09-13 | §14.2, §14.3, §20.2 | datafusion-proto gains a documented byte-stability guarantee, or the two-process byte-equality test fails after a DataFusion upgrade |
| ADR-0020 | Defer salsa; the artifact-hash memo is the only memoization mechanism | superseded | 2026-09-13 | §14.3 | A measurement shows sub-pass granularity finer than per-instance is needed inside P7 |
| ADR-0021 | Load YAML with serde-saphyr; serde_yaml is banned | accepted | 2026-09-13 | §3.1, §3.3 | serde-saphyr becomes unmaintained, or a document form the authoring language needs is not expressible through it |
| ADR-0022 | Write every kernel against num-dual 0.15; FeOs stays conditional | accepted | 2026-09-13 | §3.1, §3.3, §9.8 | feos-core releases a version that depends on num-dual 0.15 (checked monthly with cargo info) |
| ADR-0023 | Give pse-ids sole ownership of hashing, with derive_key contexts and pse.canon.v1 | superseded | 2026-09-13 | §5.1, §5.3, §20.1 | A canonicalization constant changes (alignment, metadata version, legacy flag, compression), which is a new pse.canon.vN and invalidates every stored hash |
| ADR-0024 | Cross the Python boundary with pyo3-arrow and the PyCapsule stream protocol | accepted | 2026-09-13 | §3.1, §21.1 | arrow-pyarrow drops its build-time interpreter requirement, or pyo3-arrow stops tracking the pinned arrow family |
| ADR-0025 | Defer LogicalPlan::Extension rule nodes; derivations already answer the attribution question | accepted | 2026-09-13 | §14.2 | The §22.4 diff report or agent tooling needs per-rule attribution inside a plan rendering |
| ADR-0026 | Drop uom and arrow-flight; pint validates what the adapter emits and never defines a quantity | accepted | 2026-09-13 | §3.1, §3.3, §21.2 | A kernel genuinely needs compile-time unit typing, or a remote Arrow transport becomes a requirement |
| ADR-0027 | Define the commit contract as P2-level validity; compilability is gated separately | accepted | 2026-09-13 | §22.2, §14.5 | A consumer is found treating a committed revision as compilable, or P2 grows a check expensive enough to make commits slow |
| ADR-0028 | Build Ipopt 3.14.20 with MUMPS and ASL from pinned sources in a container; probe HSL at runtime | accepted | 2026-09-13 | §18.3, §6.13, §26 | A platform’s recipe cannot be reproduced (macOS or Windows at phase 1b), or a wheel needs the native backend and the linkage question comes due |
| ADR-0029 | Give every session a FairSpillPool sized from a declared memory limit | superseded | 2026-09-13 | §14.3 | A workload’s spill rate makes the declared limit the binding constraint on compile time, or a deployment needs a different pool discipline |
| ADR-0030 | Canonicalize NaN and preserve -0.0 on the hashing path; never use Float64 as a distinct or join key | accepted | 2026-09-13 | §5.3, §14.2, §19.2 | DataFusion changes its grouping or join equality for floats, or a rule genuinely needs a float-valued key |
| ADR-0031 | Commit every generated source and diff it in CI; never generate in build.rs | accepted | 2026-09-13 | §4.2, §18.3, §21.5 | Generation becomes slow enough that an explicit step is a friction point, or a generated artifact needs a host-specific input a checkout cannot supply |
| ADR-0032 | Fetch external reading copies into a gitignored external/ rather than vendoring them | accepted | 2026-09-13 | §3.1, §0.2 | A CI job needs an external tree that a network fetch cannot supply reproducibly |
| ADR-0033 | Keep one blueprint file, revised in git, with section numbers as stable citation targets | accepted | 2026-09-13 | §0.1 | A revision needs to remove a section entirely, or the file grows past the point where one document is readable |
| ADR-0034 | Govern with a single maintainer, PR-only squash merges onto a linear signed main | accepted | 2026-09-13 | §0.1 | A second maintainer joins, which changes the approval count, the bypass policy and the project board question |
| ADR-0035 | Update dependencies with Dependabot, grouped by the pinned families | accepted | 2026-09-13 | §3.1 | Dependabot cannot express a grouping the family rule needs, in which case Renovate is the declared fallback (register row R-13) |
| ADR-0036 | Publish the documentation as an mdBook on GitHub Pages | accepted | 2026-09-13 | §0.1, §0.3 | The Python package grows a public surface that needs generated API documentation (register row R-14), or the book outgrows a single SUMMARY.md |
| ADR-0037 | Defer datafusion-tracing, instrumented-object-store, datafusion-ffi and egglog, each with a trigger | accepted | 2026-09-13 | §3.3, §14.2, §23.1, §22.1 | Any one of the four triggers fires: a datafusion-tracing release matching the pinned engine; an instrumented-object-store release matching it; a third-party compiled kernel package under §22; a cross-process, cross-version egglog extraction-determinism test passing |
| ADR-0038 | Add pse-ipopt-sys, pse-buildinfo, xtask, benches and five test crates to the workspace layout | accepted | 2026-09-13 | §3.2, §24.1, §24.3, §18.3 | A sixth test family is needed, or xtask grows logic that belongs in a shipped crate |
| ADR-0039 | Enforce semantic admission and complete physical quantity operations | accepted | 2026-09-13 | §D5, §4.1, §4.3, §4.4, §6.2, §8.3, §14.2 | A new quantity operation cannot be expressed by the registered composition policies, or a supported query bypasses generated admission. |
| ADR-0040 | Resolve property demand from normalized seeds and name immutable stage outputs | accepted | 2026-09-13 | §D8, §4.1, §6.11, §9.6, §14.1 | A selected method requires demand that cannot be declared over the finite normalized scope before realization. |
| ADR-0041 | Reuse complete stage inputs before introducing finer memoization | accepted | 2026-09-13 | §D14, §6.11, §14.3, §14.4 | Representative cold/warm measurements show whole-stage recomputation dominates total preparation cost and a complete finer dependency bundle passes differential checks. |
| ADR-0042 | Defer finer memoization until whole-stage measurements justify it | accepted | 2026-09-13 | §D14, §14.3, §24.3, §26 | A measured workload still needs finer dependency tracking after complete per-instance reuse is characterized. |
| ADR-0043 | Declare kernel outcomes and generate only supported bindings | accepted | 2026-09-13 | §D9, §6.11, §7.6, §18.5, §18.9, §21.4 | A current kernel needs an execution or outcome policy that the descriptor cannot express without backend-specific semantic invention. |
| ADR-0044 | Record plan encodings as noncanonical diagnostic evidence | accepted | 2026-09-13 | §4.3, §6.13, §14.2, §20.2 | A consumer demonstrates a need for canonical plan bytes, or upstream supplies a versioned byte-stability contract worth adopting. |
| ADR-0045 | Separate canonical logical content from encoded artifact integrity | accepted | 2026-09-13 | §5.1, §5.3, §20.1, §20.2, §20.4, §20.5 | Any canonical constant or normalization rule changes, or a measured workload exceeds the supported relation-size envelope. |
| ADR-0046 | Share runtime budgets and reserve platform allocations explicitly | accepted | 2026-09-13 | §14.3, §18.2, §18.8, §20.1, §24.3 | Measured pool/process peaks diverge materially or a supported consumer cannot reserve before allocation. |
| ADR-0047 | Preserve guarded numerical semantics and borrow through safe Arrow views | accepted | 2026-09-13 | §D6, §D11, §6.11, §7.3, §7.4, §18.2, §21.2 | A backend cannot express an adopted numerical policy or measured buffer-transfer cost justifies a new layout. |
| ADR-0048 | Use standard relational operators and explicit-schema readers | accepted | 2026-09-13 | §5.4, §6.10, §6.11, §14.2, §21.1, §24.1, §26 | A representative supported scan/import needs an operation outside the bounded algebra or measured optimization triggers in R-24/R-25 fire. |
| ADR-0049 | Pin serde_json and uuid as direct dependencies for the manifest codec and explicit identities | accepted | 2026-09-13 | §3.1, §5.1, §20.2 | a hash frame, a relation payload or a second document codec is routed through serde_json, or a pass other than the authoring assign-ids command mints a UUID |
| ADR-0050 | Freeze the identity framing constants, canonical frames and the phase-1 relation envelope | accepted | 2026-09-13 | §4.3, §5.1, §5.3, §14.3 | any constant listed here has to change — that is a new version string and a superseding record, never an edit |
| ADR-0051 | Extend the generated-tree list and make codegen –check a regeneration-equivalence check | accepted | 2026-09-13 | §4.2, §24.1 | the Ipopt bindgen arm starts running its generator in CI’s solver container, or the hand-written Rust manifest struct is generated (register row R-27) |
Deferred-decision register
Every item here was deliberately deferred with a stated trigger rather than decided. A deferral that nobody revisits is an omission, so each row carries the observable trigger, a check that can be run, an owner, and the date the check is next due.
| Column | Meaning |
|---|---|
R-NN | Row id. Cited from ADRs and from issues; ids are never reused. |
item | What was deferred, in one line. |
ADR | The decision record that deferred it, or — when the deferral is a blueprint position with no separate record. |
trigger | The observable event that ends the deferral. Not a date. |
check | How to tell whether the trigger has fired. A cell starting with $ is a shell command that scripts/check_register.py --due runs and reports; anything else is a manual check. |
owner | Who answers for the row. |
last-checked | The date the check was last actually run. |
next-check | When it is due again. Monthly for automatable checks; phase-gated otherwise. |
status | open (deferred, waiting), watch (trigger may fire soon), closed (decided — the ADR that decided it is in the ADR column). |
scripts/check_register.py --lint runs in governance / adr-lint: it validates
the dates, checks that every referenced ADR exists, and fails when a row that is
not closed has a next-check in the past. register-review.yml runs
--due on a monthly cron and opens or updates one Register review YYYY-MM
issue from its output, including whatever the shell checks printed. A row is
closed by the ADR that decides it, never by deleting it.
| R-NN | item | ADR | trigger | check | owner | last-checked | next-check | status |
|---|---|---|---|---|---|---|---|---|
| R-01 | Proposed: salsa for finer dependency tracking (§14.3) | ADR-0042 | complete per-instance reuse has met R-22, but measured preparation still requires finer automatically tracked dependencies | manual: compare declared-input memo and salsa on the same semantic outputs and cancellation contract; no adoption measurement exists yet | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-02 | LogicalPlan::Extension rule nodes for per-rule attribution (§14.2 rule 8) | ADR-0025 | the §22.4 diff report or agent tooling needs rule attribution inside a plan rendering | manual: is a plan rendering in the diff report on the current slice’s scope? | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-03 | datafusion-tracing and instrumented-object-store (§23.1) | ADR-0037 | a release matching the pinned DataFusion 55.1.0 exists | $ python3 -c "import json,urllib.request as u; print(json.load(u.urlopen('https://crates.io/api/v1/crates/datafusion-tracing'))['crate']['max_version'])" | paul-heyse | 2026-09-13 | 2026-10-13 | watch |
| R-04 | datafusion-ffi for compiled third-party kernel packages (§3.3, §22) | ADR-0037 | the §22 extension model admits compiled third-party kernel packages | manual: has a third-party kernel package been proposed? | paul-heyse | 2026-09-13 | 2027-03-01 | open |
| R-05 | feos-core / feos provider, blocked on num-dual 0.15 (§9.8) | ADR-0022 | feos-core releases a version depending on num-dual 0.15 | $ cargo info feos-core | paul-heyse | 2026-09-13 | 2026-10-13 | watch |
| R-06 | egglog equality-saturation rewrites (§3.3, phase 4) | ADR-0037 | a cross-process, cross-version extraction-determinism test passes | manual: run the determinism spike before any phase-4 adoption | paul-heyse | 2026-09-13 | 2027-03-01 | open |
| R-07 | declared MSRV floors of the pre-1.0 crates re-checked at every upgrade (§3.1, §26) | ADR-0018 | any Cargo.lock change | $ cargo metadata --locked --format-version 1 --no-deps | paul-heyse | 2026-09-13 | 2026-10-13 | open |
| R-08 | Ipopt / HSL / MUMPS build recipe per platform (§26, §18.3) | ADR-0028 | phase 1 exit, or a macOS or Windows recipe fails to reproduce | $ ls docker/solvers/checksums.sha256 | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-09 | wheel solver linkage: bundle Ipopt/MUMPS versus runtime libloading | ADR-0028 | the first wheel that must ship the native backend | manual: does a release need pse-py built with the ipopt feature? | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-10 | object_store multi-writer commit coordination (§20.1, §26) | — | any deployment that is not single-writer local | manual: re-validate the ref CAS against the target object store backend | paul-heyse | 2026-09-13 | 2027-03-01 | open |
| R-11 | Proposed: canonical plan-byte contract beyond diagnostic encodings (§14.2) | ADR-0044 | a consumer requires canonical plan bytes or upstream supplies a usable versioned byte-stability contract | manual: review need and codec/engine compatibility at dependency changes; E5 refutes sorted-insertion determinism; current diagnostic bytes stay noncanonical | paul-heyse | 2026-09-13 | 2026-10-13 | open |
| R-12 | Miri re-adoption (dropped: no unsafe outside the FFI crates) | — | the first unsafe block outside the four allowlisted crates | $ cargo tree --workspace --depth 0 | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-13 | Renovate as the fallback for Dependabot | ADR-0035 | Dependabot cannot express a grouping the family rule needs | manual: has a family-group PR arrived ungrouped? | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-14 | Python API documentation tool (mkdocstrings or equivalent) | ADR-0036 | python/pse grows a public surface beyond open/compile/solve/build_info | $ ls python/pse | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-15 | crates.io release tooling: release-plz versus cargo-workspaces | ADR-0002 | phase-0 exit, when the library crates first flip publish | manual: decide by ADR before the first v0.1.0 tag | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-16 | a project board and a triage rotation | ADR-0034 | a second contributor | $ gh api repos/paul-heyse/pse-arrow/collaborators --jq length | paul-heyse | 2026-09-13 | 2027-03-01 | open |
| R-17 | Windows symlink policy for .claude/skills -> .codex/skills | ADR-0034 | the first Windows contributor, or a Windows CI job that checks out the agent config | $ git config --get core.symlinks | paul-heyse | 2026-09-13 | 2027-03-01 | open |
| R-18 | promote cargo shear / cargo machete from advisory to a PR gate | ADR-0038 | every crate has code — the phase-0 skeleton declares its blueprint dependencies before using them, so unused-dependency tools would fail the whole workspace | $ cargo shear | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-19 | Python pins for CPython 3.11: numpy 2.5.3, scipy 1.18.1 and pint 0.26.1 declare requires-python >= 3.12, so pyproject carries an exact second pin per library under python_full_version < '3.12' (numpy 2.4.6, scipy 1.17.1, pint 0.25.3) | ADR-0024 | either raise requires-python to 3.12 (abi3-py312, parity 3.12–3.13) by ADR, or keep the split once parity on 3.11 is exercised in phase 1 | manual | paul-heyse | 2026-09-13 | 2026-12-01 | open |
| R-20 | cargo xtask doc-lint is a stub (exit 2) and rust / doc-lint is continue-on-error until the path indexes under docs/capability-maps/facts/ exist | ADR-0031 | the first just evidence-regen run that writes docs/capability-maps/facts/*.paths.txt.zst | $ ls docs/capability-maps/facts | paul-heyse | 2026-09-13 | 2026-11-01 | open |
| R-21 | the solver-image pin pull request needs PIN_PR_TOKEN (a fine-grained PAT with contents, pull requests and workflows write) because GITHUB_TOKEN cannot touch .github/workflows; until it is set the workflow prints the manual pin command | ADR-0028 | the secret is created, or the pin moves to a repository variable read through vars.SOLVER_IMAGE (decided by ADR) | $ gh secret list --repo paul-heyse/pse-arrow | paul-heyse | 2026-09-13 | 2026-11-01 | open |
| R-22 | Proposed: complete per-instance memoization before any finer dependency framework (§14.3) | ADR-0042 | representative cold/warm measurements show whole-stage recomputation dominates total preparation, and a complete finer input bundle passes incremental-versus-clean checks | manual: compare complete outputs, lineage, preparation time and memory on the same slice; report conditions and the changed dependency matrix | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-23 | Proposed: larger canonical relation envelope or deterministic fixed-boundary streams (§5.3) | ADR-0045 | a supported workload exceeds the phase-1 row/byte/offset envelope and measurements justify a larger format | manual: measure process/reservation peaks; design the next canonical version and metamorphic/round-trip compatibility fixtures before adoption | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-24 | Proposed: Parquet pruning and bounded transfer coalescing (§5.4, §21.1) | ADR-0048 | a representative scan or stream-transfer cost is material and the candidate improves total cost under identical semantic results | manual: compare full results/multiplicities, decoded bytes, allocations, process/pool peaks and wall time; qualify each optimization separately | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-25 | Proposed: predicate preimages for computed-column analytics (§14.2) | ADR-0048 | a repeated analytic predicate has an exact supported preimage and measurement shows reduced total scan cost | manual: prove or exhaustively characterize equivalent replacement semantics including half-open endpoints, nulls and floating bounds; compare against an unpruned query before measuring savings | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-26 | mmap for IPC artifacts in pse-catalog (read via get_opts → Bytes → zero-copy Buffer in phase 0) | ADR-0046 | a measured artifact-read cost that dominates a representative open/query path | manual: compare read-copy versus mmap on the golden stores under recorded conditions; an adoption needs an ADR and an unsafe-allowlist entry | paul-heyse | 2026-09-13 | 2027-01-15 | open |
| R-27 | generate the Rust manifest struct from the registry ManifestSpec (hand-written in phase 0 with a parity test) | ADR-0051 | wave 2 starts, or the parity test between the hand-written struct and ManifestSpec fails | $ ls crates/pse-catalog/src/store/manifest.rs | paul-heyse | 2026-09-13 | 2026-12-01 | open |
id: ADR-NNNN title: TITLE status: proposed date: YYYY-MM-DD deciders: [paul-heyse] level: decision principles: [DM-00] blueprint: [§0.1] review: not-required: REASON evidence: Proposed supersedes: [] superseded-by: null revisit: An observable trigger, not a date. verification: The test, lint or CI job that shows this holds.
ADR-NNNN: TITLE
Delete the guidance under each heading as you fill it in. A small deviation
fills each section in one line — charter §H: “a short, concrete decision record
is sufficient”. Cite blueprint §N.M rather than restating the design.
Context
Two or three sentences: the situation that forces a decision, the blueprint section it governs, and the design-review finding that raised it, if any.
Scope
What this record binds and what it deliberately leaves open. A must-gap
narrows the supported scope here; it never claims compliance. If the record
amends the blueprint rather than deviating from it, say so — that is why its
level is decision.
Drivers
The forces, one line each: correctness, reproducibility, supply chain, parity,
cost. The charter principle IDs go in the principles: field, not here.
Options
Each option in one line, with the reason it was not taken. “Do nothing” counts.
Outcome
The decision, stated as a rule someone can follow or break.
Consequences
What this forces on the implementation, including the unpleasant parts.
Compensating controls
What keeps the downside bounded: a lint, a pin, a register row, a fallback.
Confirmation
How anyone can tell the decision still holds — the same thing named in
verification:, plus where its output lands.
Pros and cons
A short list, or a two-row table, only when the options were genuinely close.
More information
Links: blueprint sections, the design-review finding, the capability-map evidence, the register row, the plan that implements it.
Status history
- YYYY-MM-DD — proposed.
id: ADR-0001
title: Record decisions as ADRs with the charter §H fields
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-55, DM-59, DM-60]
blueprint: [§0.1]
review: not-required: this record establishes the review process; it governs no design surface
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A second maintainer joins, or two consecutive months pass in which ADRs are written after the change rather than before it
verification: governance / adr-lint runs scripts/adr.py lint and scripts/check_register.py --lint on every PR
ADR-0001: Record decisions as ADRs with the charter §H fields
Context
The blueprint states decisions but not why they beat the alternatives, and the two design reviews are evidence that expires. Charter §H asks a deviation to record principles, scope, reason, alternatives, consequences, compensating controls, evidence, owner and a revisit trigger; nothing in the repository held those fields.
Scope
Binds where decisions live (docs/adr/), what a record must contain, and when one is required (the table in docs/plans/01-repository-configuration.md §7). Plans (docs/plans/) are a separate, living lifecycle and are not covered here.
Drivers
Decisions must outlive the session that took them; the charter’s §H fields must be mechanically checkable; the cost of the process must stay under the cost of the risk (charter §H: “a short, concrete decision record is sufficient”).
Options
adr-tools — rejected: unmaintained shell, no §H fields. A DECISIONS.md append-only log — rejected: no per-decision status, no supersession, no lint. Issue labels only — rejected: not in the repository, not reviewable in a PR.
Outcome
MADR-4 body sections plus the charter §H fields in YAML front matter, one file per decision under docs/adr/NNNN-kebab.md, linted by scripts/adr.py. A record is immutable once accepted except status, superseded-by and an appended ## Status history line; it changes by being superseded.
Consequences
Every decision that alters D1–D14, a crate, a dependency family, a hashing or boundary contract, or governance now needs a PR labelled adr titled adr: ADR-NNNN <title>. The blueprint gains > Decision: ADR-NNNN one-liners at the governed sections.
Compensating controls
scripts/adr.py lint checks the filename, contiguous numbering, required keys, the enums, DM-NN principle IDs, that every § citation resolves to a real blueprint heading, that the review path exists, that supersession is symmetric, and that accepted records are unchanged against origin/main.
Confirmation
governance / adr-lint is a required check on main; docs / build runs adr.py index --check so the generated index cannot drift.
Pros and cons
The cost is one short file per decision and one required check. The alternative — decisions living in commit messages and chat — is what produced the two design reviews’ “unstated dependency” findings.
More information
Charter §H (scoped exception record) and §D (evidence vocabulary) in docs/design_review/design_principles/DATA_MODEL_DESIGN_CHARTER.md; docs/adr/template.md; .codex/skills/adr/SKILL.md.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0002
title: Name the project pse-arrow, prefix crates pse-, dual-license MIT OR Apache-2.0
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-11, DM-55]
blueprint: [§3.2, §26]
review: not-required: naming, licensing and distribution carry no semantic surface; blueprint §26 lists the prefix as “placeholder, to confirm”
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: The PyPI name pse becomes available, or a crates.io name collision forces a different crate prefix at the phase-0 publish
verification: tests/governance/tests/every_crate_registered.rs (crate prefix); governance / reuse (reuse lint over the SPDX headers)
ADR-0002: Name the project pse-arrow, prefix crates pse-, dual-license MIT OR Apache-2.0
Context
Blueprint §3.2 leaves the pse- prefix as a placeholder and §26 carries “crate naming and repository layout” as an open decision. The repository needs one name on GitHub, on crates.io and on PyPI before anything is published.
Scope
Binds the repository name, the crate prefix, the Python import name, the PyPI distribution name and the license expression. It does not bind when anything is published (ADR-0034 and the register cover that).
Drivers
One name that reads the same in three registries; a license that lets the Rust and Python ecosystems both consume the result; a prefix short enough for 23 crates.
Options
idaes-rs or anything with idaes in it — rejected: implies affiliation (ADR-0003). Import name pse_arrow — rejected: the import name is what appears in every example; pse is short and unambiguous inside the package. Apache-2.0 only — rejected: the Rust ecosystem’s default is the dual license.
Outcome
Repository paul-heyse/pse-arrow; crate prefix pse-*; Python import name pse; PyPI distribution pse-arrow because PyPI pse is already taken; license MIT OR Apache-2.0 with LICENSE-MIT, LICENSE-APACHE, LICENSES/ and a two-line SPDX header on every authored source file.
Consequences
The distribution name and the import name differ, which every install instruction has to state. REUSE.toml covers headerless file types by glob so Markdown needs no header; the code generator emits the header itself.
Compensating controls
reuse lint in python / lint; tests/governance/tests/every_crate_registered.rs asserts every workspace member carries the prefix and is listed in the blueprint §3.2 layout.
Confirmation
pip install pse-arrow then import pse is the documented pair; README.md and docs/README.md state it in the first paragraph.
Pros and cons
The mismatch between distribution and import name is a real papercut, accepted because pse is the name the blueprint uses throughout §3.2 and renaming 23 crates later is worse.
More information
Blueprint §3.2 (workspace layout) and §26 (open decisions); REUSE.toml; ADR-0003 for the non-affiliation statement.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0003
title: Re-implement IDAES clean-room and pin parity to idaes-pse 2.12.0
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-42, DM-48, DM-59]
blueprint: [§0.2, §3.1, §6.14, §25]
review: not-required: a provenance and scope statement; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: IDAES releases a version that changes an enumeration preserved by name in blueprint §6.14, or drops a CPython version the parity environment needs
verification: python / parity pre-flight test_00_preflight asserts idaes.__version__ == "2.12.0"; governance / adr-lint asserts the IDAES tag in scripts/fetch-external.sh equals the parity dependency-group pin
ADR-0003: Re-implement IDAES clean-room and pin parity to idaes-pse 2.12.0
Context
The platform reproduces core IDAES-PSE capabilities (blueprint §0.2) and is parity-tested against a pinned IDAES release (§3.1, §25), which makes the relationship to an existing Apache-2.0 project a licensing and honesty question, not only a technical one.
Scope
Binds how IDAES source may be used, which IDAES release parity is measured against, and how that pin moves. It does not bind the parity tolerances (blueprint §24) or which subsystems are in scope (§0.2).
Drivers
Clean-room provenance must be defensible; parity results are meaningless without a pinned reference; users must not read the project as an IDAES product.
Options
Fork idaes-pse — rejected: the architecture shares no structure with it (D1, D2), and a fork inherits an import graph the platform deliberately does not have. Float the parity pin to the latest release — rejected: a parity failure would be indistinguishable from an upstream change.
Outcome
external/idaes-pse is checked out at the pinned tag and read for behaviour; no IDAES code is copied. The only coupling is the parity harness. Parity is pinned at idaes-pse==2.12.0 in the parity dependency group and moves only by a new ADR. docs/relationship-to-idaes.md carries the non-affiliation and trademark statement and cites Lee et al. 2021.
Consequences
external/ is gitignored and fetched by scripts/fetch-external.sh; the parity environment is a separate resolution (.venv-parity) so that IDAES’s transitive dependencies (pydantic, sympy, networkx, pandas) can never become platform dependencies.
Compensating controls
Enumerations deliberately preserved by name are enumerated in blueprint §6.14 so a reader can see exactly where names are shared and why; the PR template carries a clean-room attestation.
Confirmation
The parity job fails — never skips — when the interpreter, the IDAES version or ipopt is not what the pin says (root conftest.py, --parity).
Pros and cons
Clean-room costs re-deriving behaviour that could be read off a source file; that cost is the price of an architecture that is not a translation of IDAES’s object graph.
More information
Blueprint §0.2 (scope), §6.14 (preserved enumerations), §25 (phase exits); docs/relationship-to-idaes.md; Lee et al. 2021, J. Adv. Manuf. Process. 3(3) e10095.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0004
title: Adopt D1: typed Arrow relations are the only model authority
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-01, DM-02, DM-06, DM-10]
blueprint: [§D1, §4.1, §6]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A relation family needs content no typed schema can express and a JSON column is proposed for it a second time
verification: tests/governance/tests/no_shadow_structs.rs; tests/governance/tests/banned_patterns.rs
ADR-0004: Adopt D1: typed Arrow relations are the only model authority
Context
Blueprint D1 forbids entity-attribute-value tables, JSON columns that carry required behaviour, and hand-written Rust structs that are the “real” model. The first design review’s F8 found the blueprint violating its own rule: rule bodies, invariant specs and selectors were opaque JSON text.
Scope
Binds every durable model fact to a declared relation in the schema registry (§4.1). It does not forbid JSON for genuinely open-ended evidence: diagnostics_findings.values and provenance.assertions.expected stay JSON by name.
Drivers
One authority per fact; a rule change must be diffable at the level of meaning; agents must be able to query “which rules read this relation”.
Options
Keep rule bodies as JSON — rejected by F8: stratification cannot be checked as a relational invariant and impact analysis has to parse text. A Rust struct model with Arrow as an export — rejected by D1: it is two authorities.
Outcome
Every durable fact lives in a typed relation declared in pse-schema. Rust structs that hold model data are generated views. reference.rule_plan_nodes/rule_plan_edges/rule_dependencies, selector_terms and typed case_sets generators replace the JSON that F8 found.
Consequences
The registry becomes the single point of change for the model, and code generation (ADR-0031) becomes load-bearing. A new relation is a registry edit plus a regeneration commit, not a struct.
Compensating controls
no_shadow_structs fails on a hand-written mirror of a relation; banned_patterns greps for the JSON escape hatches; P2 invariant rules.stratified_negation is itself expressed over rule_dependencies.
Confirmation
rust / test runs the governance crate on every PR; a golden snapshot per vertical slice pins the relation content.
Pros and cons
The cost is that everything mechanical must be generated. The benefit is the one the proposal claims: structure declared once removes five parallel maintenance surfaces.
More information
Blueprint §D1, §4.1 (what is declared once), §6 (the relation families); review finding F8.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0005
title: Adopt D2: authors write primitive facts, passes derive everything else
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-02, DM-13, DM-23, DM-46]
blueprint: [§D2, §14.1, §22.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A derived relation acquires an authored write path, or derivation rows exceed the §24.3 benchmark’s budget per equation
verification: tests/governance/tests/banned_patterns.rs (no write path into derived relations); the §24.3 derivation-cost benchmark
ADR-0005: Adopt D2: authors write primitive facts, passes derive everything else
Context
Blueprint D2 confines the authored model to primitive facts (topology, materials, quantities, phenomena, laws, options, specifications) and derives participation, roles, boundary classification, property requirements, method selection, equations and orderings by contracted passes. The first review’s F14 questioned the cost of per-row derivations.
Scope
Binds what may be authored and what must be derived, and requires every derived row to carry a derivation. It does not fix the derivation granularity: schema_relations.derivation_granularity declares row or rule per relation (§14.2 rule 4).
Drivers
IDAES’s largest maintenance burden is imperative build() methods that restate consequences; a derived fact that can also be authored is two authorities; provenance must survive the pipeline.
Options
Author everything and validate — rejected: that is IDAES’s position and the maintenance surface the proposal exists to remove. Store no derivations — rejected: negative completeness (“why is this row absent”) becomes unanswerable.
Outcome
An IDAES build() method becomes a template plus inference rules. A derived relation an author tries to write is rejected or downgraded to provenance.assertions. Every head row carries a derivation at the granularity its relation declares.
Consequences
Authoring gets less expressive on purpose; escape hatches become change ops (§22.2) or assertions. Per-row derivations cost storage, which is why rule-granularity relations reconstruct theirs from the rule plus the §5.1 ID formula.
Compensating controls
The §24.3 benchmark measures derivation rows and bytes per equation so the granularity assignment stays evidence-based rather than a guess.
Confirmation
Golden snapshots per slice include the derivation relations; the closure report (§14.5) shows which categories are still open.
Pros and cons
Deriving more means an author cannot express an exception locally; the compensating control is that exceptions become declared assertions the compiler can contradict.
More information
Blueprint §D2, §14.1 (pass pipeline), §22.2 (change sets); review finding F14; ADR-0017 for what invalidation follows.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0006
title: Adopt D3: one catalog holds CanonicalModel, CanonicalMathGraph and CanonicalMathProblem
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-13, DM-21, DM-23]
blueprint: [§D3, §5, §14.1]
review: not-required: neither review raised a finding against D3
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A fourth artifact level is proposed, or a level needs identity, hashing or provenance conventions the other two do not share
verification: tests/governance/tests/every_crate_registered.rs; the lifecycle test layer in tests/lifecycle
ADR-0006: Adopt D3: one catalog holds CanonicalModel, CanonicalMathGraph and CanonicalMathProblem
Context
Blueprint D3 states that the three artifact levels are stages in one catalog sharing identity conventions, extension types, hashing and provenance, not three applications.
Scope
Binds the artifact levels to one schema system and one catalog. It does not bind the pass boundaries between them (§14.1 does).
Drivers
Three catalogs would mean three identity schemes and three hashing contracts; the reproducibility contract (§20) needs one manifest shape.
Options
Separate stores per level — rejected: the snapshot hash of §5.3 step 7 could not span them. A single flat namespace — rejected: the seven catalog namespaces are what makes authority legible.
Outcome
One catalog, seven namespaces, one identity and hashing contract (ADR-0023), one manifest (§20.2).
Consequences
A change to the hashing contract or to the extension-type set touches all three levels at once, which is the intent.
Compensating controls
The snapshot catalog (§5.4) is the only reader of artifact bytes; tests/lifecycle exercises publish, read back and query for each level.
Confirmation
docs / build publishes the relation index (Appendix B) so the level of every relation is visible.
Pros and cons
Coupling the levels is the point: the alternative is three products.
More information
Blueprint §D3, §5 (identity and catalog), §14.1 (pass pipeline).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0007
title: Adopt D4: semantic IDs, artifact ordinals and content hashes are three different things
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-11, DM-12, DM-15]
blueprint: [§D4, §5.1, §5.2]
review: not-required: revision 2 resolved the identity finding (F1); revision 3 records no further finding against D4
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A relation needs an identity that is neither a semantic ID, an ordinal nor a content hash
verification: tests/governance/blake3_owner.rs; the canonicalization property tests in tests/conformance
ADR-0007: Adopt D4: semantic IDs, artifact ordinals and content hashes are three different things
Context
Blueprint D4 separates stable 128-bit semantic IDs, artifact-local ordinals and blake3 content hashes, and forbids names, row positions, dictionary codes and solver positions from ever being identity.
Scope
Binds the three identity kinds and their uses. The derived-ID formulas and derive_key contexts are ADR-0023’s subject.
Drivers
A rename must not change an identity; a re-batched relation must hash identically; a solver position is an artifact of ordering, not a fact.
Options
Name-based identity — rejected by F1: a rename would then be a delete plus an insert across every referencing row. Row position as identity — rejected: it is not stable under re-batching.
Outcome
Authored identity is assigned at creation and stored in the document; qualified_name is an attribute a rename change op may alter. Ordinals index compiled artifacts. Content hashes identify immutable artifact versions.
Consequences
Every reference is by identity; retained target texts (pse.target_path, pse.expr_dsl) are serializations that P2 re-checks against the identity rows (case.target_text_mismatch).
Compensating controls
rename is a first-class change op (§22.2); the §22.4 diff report shows one rename row rather than a scatter of deletes and inserts.
Confirmation
Canonicalization property tests cover re-batching, metadata re-insertion order and dictionary re-encoding (§24.1).
Pros and cons
Three identity kinds is more machinery than one; the review’s F1 shows what one kind costs.
More information
Blueprint §D4, §5.1 (three forms of identity), §5.2 (revisions and runs); ADR-0023.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0008
title: Adopt D5: every quantity carries a quantity_type_id, and pse.* extension types are registered
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-06, DM-07, DM-08, DM-44]
blueprint: [§D5, §4.4, §8]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0039
revisit: A quantity-bearing column appears without a quantity_type_id, or the engine’s extension-type registry stops validating storage types at planning time
verification: python / test test_extension_round_trip; the wrong-storage-type planning rejection test in tests/conformance
ADR-0008: Adopt D5: every quantity carries a quantity_type_id, and pse.* extension types are registered
Context
Blueprint D5 requires every quantity-bearing column and every symbol to carry a quantity_type_id resolving dimension vector, quantity kind, basis, reference state, affine semantics and index shape. The second review’s R2-3 found §4.3’s claim that “nothing in DataFusion acts on these keys” false: the engine’s ExtensionTypeRegistry resolves ARROW:extension:name and rejects a wrong storage type during planning.
Scope
Binds the physical-typing contract and the registration of the ten pse.* extension types on both sides of the Python boundary. Unit inference itself is §8.3.
Drivers
A unit string cannot express basis, reference state or affine semantics; heterogeneous value columns need a per-row type; an unvalidated extension type is a typed hole.
Options
Unit strings on columns — rejected: enth_mol_phase in two bases is the same string. Compile-time unit typing with uom — rejected by ADR-0026.
Outcome
Every quantity-bearing column and symbol carries a quantity_type_id; heterogeneous value columns carry a per-row one. The ten pse.* extension types are registered in the engine’s extension-type registry, which becomes a declared validity mechanism rather than inert metadata.
Consequences
Null never means “unknown to be solved” (§7.6); an unregistered extension name is an engine error on the Rust side while Python degrades to the storage type, so the bundle carries a per-column loss profile (§21.1).
Compensating controls
The registration is one generated function, not new machinery; a batch with the wrong storage type fails at planning, not at read time.
Confirmation
test_extension_round_trip on the Python side and the planning-rejection test on the Rust side run in python / test and rust / test.
Pros and cons
Carrying a type id on every quantity column is verbose; the alternative is the class of bug that the basis/reference-state distinction exists to prevent.
More information
Blueprint §D5, §4.3 (metadata conventions), §4.4 (extension types), §8 (physical typing); review finding R2-3.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0039.
id: ADR-0009
title: Adopt D6: the math IR is relations, and DataFusion Expr computes over it
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-21, DM-22, DM-10]
blueprint: [§D6, §7.1, §7.2]
review: not-required: neither review raised a finding against D6
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A backend requires an operator the IR cannot express without scalarizing earlier than §7.1 allows
verification: tests/conformance operator-contract tests; the expression DSL round-trip test at the phase-0 exit
ADR-0009: Adopt D6: the math IR is relations, and DataFusion Expr computes over it
Context
Blueprint D6 keeps expression nodes, arguments and typed operator payloads as relations, and keeps indexed operators (SumOver, Gather, Broadcast, Derivative, Integral, ImplicitSystem, KernelCall) alive until a backend requires scalarization.
Scope
Binds the representation of mathematics. It does not bind the operator catalog’s contents (§7.2) or canonicalization (§7.4).
Drivers
DataFusion Expr has no indexed reduction, no derivative and no kernel call with declared derivative availability; scalarizing early destroys the structure discretization and index expansion need.
Options
Use Expr as the IR — rejected: the seven indexed operators have no representation and the IR would be tied to the engine version. A hand-written Rust enum — rejected by D1.
Outcome
Expression nodes, arguments and typed payloads are relations; DataFusion Expr is used to compute over the IR, never to be it.
Consequences
Every operator needs a contract record (§7.3) and a lowering per backend; adding one is a code change by §22.3’s own table.
Compensating controls
Canonicalization (P10) is hand-written and complete on its own; egglog stays optional and phase 4 (ADR-0037).
Confirmation
Operator-contract conformance tests run per backend; the DSL round-trip is a phase-0 exit criterion (§25).
Pros and cons
Keeping indexed operators alive costs a scalarization pass (P12); folding them early costs the ability to discretize.
More information
Blueprint §D6, §7.1 (design constraints), §7.2 (operator catalog), §14.1 (P12).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0010
title: Adopt D7: balance, costing and utility laws are templates expanded over a contributions substrate
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-16, DM-17, DM-18, DM-25]
blueprint: [§D7, §10.1, §10.2]
review: not-required: neither review raised a finding against D7
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A law family cannot be expressed over contributions and needs a compiler rule of its own
verification: tests/conformance law-expansion golden tests per control-volume variant
ADR-0010: Adopt D7: balance, costing and utility laws are templates expanded over a contributions substrate
Context
Blueprint D7 makes material, energy, momentum, element and charge balances, and the costing and utility-minimization laws, instances of law templates expanded over a contributions substrate by the generic compiler. Control volumes are template compositions that declare which contributions exist.
Scope
Binds how conservation laws are expressed. The contribution kinds themselves are §10.2.
Drivers
IDAES writes one build() per balance variant; the variants differ only in which contributions exist; extension locality (charter §E) asks for one authoritative addition per new law.
Options
One template per balance variant — rejected: the combinatorics of 0D/1D by phase/element/charge is exactly what IDAES pays for. A general equation authoring surface with no law concept — rejected: the balance structure is what diagnostics and scaling reason over.
Outcome
Laws are templates; control volumes compose them by declaring their contributions; the generic compiler expands them (§10).
Consequences
A new contribution kind is a registry addition; a new law family is usually a template, not code (§22.3).
Compensating controls
Golden expansion tests per control-volume variant; the closure report’s balance category is open while any law has unmatched contributions.
Confirmation
rust / test runs the conformance layer; slice A and slice B acceptance (§24.2) exercise the lumped and distributed variants.
Pros and cons
Templates are indirection; the alternative is the maintenance surface D2 exists to remove.
More information
Blueprint §D7, §10.1 (conservation law template), §10.3-§10.4 (control volumes), §11 (unit model library).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0011
title: Adopt D8: a pass computes the property closure; inspection never constructs physics
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-20, DM-19, DM-26]
blueprint: [§D8, §9.6]
review: not-required: neither review raised a finding against D8
evidence: Proposed
supersedes: []
superseded-by: ADR-0040
revisit: A property provider needs to be selected at evaluation time rather than at P6, for example because a method’s applicability depends on a solved value
verification: python / test test_preflight_capability_backend (raises before ConcreteModel construction); P6 golden tests in tests/conformance
ADR-0011: Adopt D8: a pass computes the property closure; inspection never constructs physics
Context
Blueprint D8 removes lazy attribute construction: a pass computes the closure of required properties from equations, selects providers by type and configured preference, persists the selection, and generates the property equations or kernel bindings.
Scope
Binds when property demand is resolved and where the selection is stored. Provider implementations are §9.8 and ADR-0022.
Drivers
IDAES builds properties on attribute access, which makes inspection mutating (charter DM-20) and makes “what does this model need” unanswerable without running it.
Options
Lazy construction on access — rejected: inspection would construct physics and the model would not be a value. Require authors to list properties — rejected: the closure is derivable (D2).
Outcome
P6 resolves property demand explicitly and persists the selection; inspection is semantically non-mutating.
Consequences
A property that cannot be provided is a typed refusal at compile time naming the capability, not a runtime attribute error.
Compensating controls
test_preflight_capability_backend asserts the refusal happens before any Pyomo object is built; the closure report’s method category records unresolved demand.
Confirmation
P6 golden relations pin the selection for each reference package; python / parity compares against IDAES’s chosen methods.
Pros and cons
Resolving eagerly costs compile time for properties a case may never evaluate; the benefit is that the model is inspectable without being executed.
More information
Blueprint §D8, §9.6 (property demand resolution), §9.3 (method registry).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0040.
id: ADR-0012
title: Adopt D9: every constitutive computation is a KernelSpec with generated bindings
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-19, DM-25, DM-44, DM-52]
blueprint: [§D9, §18.5, §21.4]
review: not-required: neither review raised a finding against D9
evidence: Proposed
supersedes: []
superseded-by: ADR-0043
revisit: A kernel needs a binding form the six generated ones cannot express, or a third-party compiled kernel package is admitted (§22, ADR-0037)
verification: tests/conformance kernel-adapter equivalence tests; rust / codegen-diff
ADR-0012: Adopt D9: every constitutive computation is a KernelSpec with generated bindings
Context
Blueprint D9 gives every code-implemented constitutive computation a KernelSpec with identity, signature in physical types, mathematical behaviour, derivative availability, execution forms, failure behaviour and backend bindings, from which scalar, batched Arrow, derivative, DataFusion UDF, NL external-function and Pyomo bindings are generated.
Scope
Binds the kernel contract and the generated binding set. Which kernels exist is §9 and the reference packages.
Drivers
Six hand-written bindings per kernel is five too many; a kernel whose derivative availability is undeclared forces the Hessian profile to limited memory silently.
Options
Hand-written adapters per backend — rejected: they drift, and the NL and Pyomo forms must agree bit for bit with the native one. A trait per backend — rejected: the spec is data (D1), so the adapters are generated from it.
Outcome
One KernelSpec per computation; all six binding forms are generated; derivative availability and failure behaviour are declared, not inferred.
Consequences
Adding a kernel is a spec plus a Rust body plus tests; the adapters come from the generator and are committed and diffed (ADR-0031).
Compensating controls
Kernel-adapter equivalence tests compare scalar, batched and generated-UDF evaluation on the same inputs; KernelSpec digests enter the plan fingerprint (ADR-0019).
Confirmation
rust / test runs the equivalence layer; rust / codegen-diff fails if a generated adapter is edited by hand.
Pros and cons
The spec is verbose; it is also the only place a kernel’s derivative and failure contract is written down.
More information
Blueprint §D9, §18.5 (generated kernel adapters), §21.4 (opaque kernels and ASL), §22.3.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0043.
id: ADR-0013
title: Adopt D10: DataFusion has four roles and never runs inside a Newton iteration
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-38, DM-20, DM-43, DM-39]
blueprint: [§D10, §5.4, §14.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: A fifth role is proposed for DataFusion, or Exact pushdown stops holding for the key shapes §5.4 depends on
verification: tests/conformance wrapper-provider test (re-applies every Exact filter and fails on a survivor); the §24.3 benchmarks
ADR-0013: Adopt D10: DataFusion has four roles and never runs inside a Newton iteration
Context
Blueprint D10 confines DataFusion to relational assembly and inference, the snapshot catalog, batch kernel evaluation and analytics. The second review’s R2-7 measured the filter shapes the catalog provider actually receives and changed how §5.4 must be implemented.
Scope
Binds where the engine may appear. The engine profile as a declared input is ADR-0019’s subject; the memory pool is ADR-0029’s.
Drivers
A solve must not be a scalar function call; an engine version must not be able to change a numerical result; Exact pushdown causes the optimizer to delete the filter, so an untruthful provider silently returns excluded rows.
Options
Use DataFusion for expression evaluation inside the solver — rejected: per-iteration planning cost and an engine-version dependency on the numerics. Avoid DataFusion entirely — considered in the first review’s simpler alternative; rejected because set semantics, stratified negation and per-row provenance are the deliverable of P4-P6.
Outcome
Four roles and no more. Key equality arrives as rel.id = FixedSizeBinary(16, …) and is matchable; IN lists arrive rewritten to OR; conjunctions are split; the provider claims Exact only for the shapes it actually matches.
Consequences
The catalog provider must be conservative: Inexact is always safe, Exact is a guarantee the engine does not verify.
Compensating controls
A test-only wrapper provider re-applies every filter the real provider claimed Exact and fails on a surviving row.
Confirmation
The wrapper-provider test runs over golden snapshots in rust / test; §24.3 benchmarks the pushdown path end to end.
Pros and cons
Four roles is a discipline, not a mechanism; the wrapper provider is what makes it checkable.
More information
Blueprint §D10, §5.4 (snapshot catalog), §14.2 (rule compiler); review finding R2-7.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0014
title: Adopt D11: execution layouts are compiled artifacts and zero-copy is a preference
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-36, DM-37, DM-39, DM-59]
blueprint: [§D11, §18.2, §15.4]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0047
revisit: A borrow precondition is relaxed, or a measurement shows the copy path dominates an evaluation benchmark
verification: tests/engine borrow-precondition tests; the §24.3 evaluation-program benchmarks
ADR-0014: Adopt D11: execution layouts are compiled artifacts and zero-copy is a preference
Context
Blueprint D11 makes the evaluation program, sparse Jacobian structures and solver workspaces compiled artifacts that borrow Arrow buffers when layouts permit and copy when they do not. The second review’s R2-6 corrected stale claims about faer, petgraph and object_store surfaces at the pinned versions.
Scope
Binds the ownership rules at the Arrow-to-native boundary and the numerical routes the diagnostics use. It does not bind the solver interface (§18.3, ADR-0028).
Drivers
Charter §G lists “it is zero-copy” as a claim to check; a sliced, filtered or nullable array cannot be borrowed; a claimed numerical route that no library provides is a capability lie (DM-43).
Options
Borrow unconditionally — rejected: a sliced or nullable array would be read past its validity. Claim an iterative SVD without a route — rejected by F17; the route is now shift-invert over faer’s matrix_free with a dense limit.
Outcome
Arrow buffers are borrowed only when the array is contiguous (ptr_offset() == 0, one chunk) and null-free; otherwise the data is copied through a checked slice. The workspace is native. Every numerical route names the library function it uses.
Consequences
The boundary code has two paths and both must be tested; the condition-number default becomes a Hager-Higham estimate with the exact Frobenius form opt-in.
Compensating controls
Borrow preconditions are asserted, not assumed; tests/engine constructs sliced, filtered and nullable arrays and asserts the copy path is taken.
Confirmation
rust / test runs the engine layer; the §24.3 benchmarks record the copy rate under recorded conditions so the claim stays Measured rather than asserted.
Pros and cons
Two paths cost code; one path costs correctness.
More information
Blueprint §D11, §18.2 (native evaluation program), §15.4-§15.5 (numerical diagnostics); review finding R2-6.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0047.
id: ADR-0015
title: Adopt D12: one generated Pyomo adapter, no IDAES class hierarchy
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-41, DM-42, DM-43, DM-52]
blueprint: [§D12, §21.1, §21.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: A Pyomo-ecosystem tool needs a per-node API the coarse-grained bundle cannot serve, or a native equivalent makes the adapter redundant
verification: python / parity bundle round-trip; python / test test_forbidden_extra_keys and test_any_lint
ADR-0015: Adopt D12: one generated Pyomo adapter, no IDAES class hierarchy
Context
Blueprint D12 has one adapter consume a CanonicalMathProblem bundle over the Arrow C stream interface and construct Pyomo objects, recreating no IDAES class hierarchy. The second review’s R2-4 found the Python boundary specified by prose the named libraries do not enforce.
Scope
Binds the adapter’s shape and its purpose (parity and Pyomo-ecosystem tools until native equivalents exist). The units authority question is ADR-0026’s.
Drivers
Per-node calls across the boundary are the cost model that kills a parity run; an unenforced contract is a convention, not a contract.
Options
Recreate IDAES’s classes in Python — rejected: a second model authority. Row-oriented transfer — rejected: one stream per table is the coarse, typed unit charter DM-37 asks for.
Outcome
One adapter, one bundle, one stream per table. Contract classes are generated; structuring is strict (forbid_extra_keys); an import-time lint fails on Any, bare dict or bare list fields; the numpy boundary refuses nullable columns.
Consequences
The adapter is memoized and coarse-grained; every table crosses as __arrow_c_stream__, so pyarrow is a consumer rather than the contract.
Compensating controls
The bundle carries a per-column loss profile saying which side enforces what (F16); pre-flight checks run before any ConcreteModel is constructed.
Confirmation
python / test runs the boundary tests; python / parity runs the adapter against IDAES 2.12.0 in the solver container.
Pros and cons
A coarse adapter cannot express incremental model edits; the platform’s answer to an edit is a new case overlay (D13), not a mutation.
More information
Blueprint §D12, §21.1 (extension module), §21.2 (adapter algorithm), §21.5 (Python contracts); review finding R2-4.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0016
title: Adopt D13: initialization, homotopy and fix-then-release are immutable case overlays
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-13, DM-29, DM-20]
blueprint: [§D13, §19.1, §17.1]
review: not-required: neither review raised a finding against D13
evidence: Proposed
supersedes: []
superseded-by: null
revisit: An operation genuinely needs to change structure mid-run, for example a native GDP lowering that adds equations during a solve
verification: tests/lifecycle overlay-immutability tests; python / test golden fixtures open stores read-only
ADR-0016: Adopt D13: initialization, homotopy and fix-then-release are immutable case overlays
Context
Blueprint D13 separates a model revision (structure), a case revision (values, bounds, fixed status, objectives, overlays) and a run (what happened), and makes initialization stages, homotopy steps and fix-then-release operations immutable case overlays.
Scope
Binds the write paths during execution. The change-set model for authored data is ADR-0027.
Drivers
IDAES’s initialization mutates the model and restores it, which makes a failed initialization leave a model in an undefined state; reproducing a run requires knowing what was mutated.
Options
Mutate and restore — rejected: an interrupted restore is unrecoverable and unreproducible. Copy the model per stage — rejected: overlays are the cheap, inspectable form of the same thing.
Outcome
Cases and results never mutate the model. Every stage is an overlay with its own identity; a run records the overlay chain it applied.
Consequences
Every operation that IDAES expresses as mutation becomes an overlay with a name, which is more objects but a legible history.
Compensating controls
tests/lifecycle asserts that an interrupted run leaves the model revision byte-identical; the golden-store fixture opens read-only so a Python test cannot write one.
Confirmation
The last iterate of a cancelled or failed run is written under the run’s terminal status, so a cancelled run is distinguished by runs.status, never by absent rows (§18.3).
Pros and cons
Overlays cost storage and a resolution step; mutation costs reproducibility.
More information
Blueprint §D13, §17.1 (plan model), §19.1 (cases and overlays), §23 (failure semantics).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0017
title: Adopt D14: memoize on declared inputs, the pass version and the engine profile
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-31, DM-32, DM-33, DM-28]
blueprint: [§D14, §14.3, §14.4]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0041
revisit: A measurement shows sub-pass granularity finer than per-instance is needed (the same trigger as ADR-0020), or an undeclared read is found in a pass
verification: tests/engine memo-key tests; the two-process byte-equality test for the plan fingerprint (ADR-0019)
ADR-0017: Adopt D14: memoize on declared inputs, the pass version and the engine profile
Context
Blueprint D14 keys memoization on the content hashes of a pass’s declared inputs, the pass version and — for plan-executing passes — the engine profile. The first review’s F13 asked whether salsa should own this, and the second review’s R2-1 found the plan fingerprint not reproducible as specified.
Scope
Binds the memo key’s shape and the invalidation granularity. Whether salsa is adopted is ADR-0020; what the fingerprint is for is ADR-0019.
Drivers
An undeclared read is a contract violation, not something to track; a value change must not invalidate structure; an engine upgrade must invalidate exactly the plan-executing passes.
Options
Track dependencies automatically with salsa — deferred by ADR-0020: it duplicates the declared inputs. Key on file mtimes — rejected: not semantic (DM-32).
Outcome
The memo key is (pass_id, pass_version, content hashes of cache_key_inputs, engine profile hash for plan-executing passes). Backdating is inherent: byte-identical outputs leave the next pass’s key unchanged.
Consequences
Every pass must declare its inputs exhaustively, and pass_specs.cache_key_inputs becomes part of the reviewed surface of a pass.
Compensating controls
The §14.4 invalidation table is explicit per change kind; the canonical graph never depends on a parameter value, so a parameter change invalidates exactly the case-bound rows.
Confirmation
tests/engine asserts that a re-run on changed inputs producing identical bytes does not invalidate downstream passes.
Pros and cons
Declared inputs are work; automatic tracking is a second dependency system beside pass_specs.
More information
Blueprint §D14, §14.3 (pass engine), §14.4 (incrementality); review findings F13 and R2-1.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0041.
id: ADR-0018
title: Pin both families with =, commit the lockfile, run cargo deny, and set MSRV to the pinned toolchain
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-31, DM-48, DM-51, DM-59]
blueprint: [§3.1, §3.2, §3.3]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: A DataFusion or Arrow release requires a Rust version above the pinned toolchain, or a pre-1.0 crate raises its declared floor above ours
verification: tests/governance/tests/pins_match_blueprint.rs, tests/governance/tests/toolchain_matches_msrv.rs, tests/governance/tests/dependency_floors.rs; rust / family-check; rust / deny
ADR-0018: Pin both families with =, commit the lockfile, run cargo deny, and set MSRV to the pinned toolchain
Context
Blueprint §3.1 pins every crate in both families with =, requires a committed Cargo.lock and cargo deny, and states MSRV as 1.94.0 — DataFusion 55’s own floor. The second review’s R2-5 found 23 supporting crates unpinned; a floor the project never compiles against is a claim nothing checks.
Scope
Binds the pinning rule, the lockfile, the supply-chain gates and the MSRV. This record amends blueprint §3.1’s MSRV row rather than deviating from it, which is why its level is decision and not should-deviation; the amendment is carried in the blueprint’s revision history as revision 4.
Drivers
= pins bind direct dependencies only and cargo tree -d cannot see a mixed family; a deprecation encoded in semver build metadata is invisible to cargo audit; an MSRV nothing exercises is not a supported configuration.
Options
Caret pins with a lockfile — rejected: a family can go mixed without a duplicate. MSRV 1.94.0 as DataFusion’s floor — rejected as the project’s MSRV: CI never builds it, so it would be an unverified claim; it stays in the blueprint as DataFusion’s floor. A floating toolchain — rejected: nightly is this machine’s default.
Outcome
Both families are =-pinned family-wide with features fixed once in [workspace.dependencies]; Cargo.lock is committed; cargo deny and cargo audit and cargo shear run in rust / deny. rust-version equals the pinned stable toolchain — 1.98.1 at revision 4 — and rust-toolchain.toml is the single source of both.
Consequences
Every dependency move is a deliberate uv lock --upgrade-package or a Dependabot PR in a family group (ADR-0035); consumers of a future published crate inherit a high MSRV, which is acceptable while the project is pre-1.0.
Compensating controls
cargo xtask family-check asserts one resolved version per family and cross-checks the capability-map evidence lockfiles; dependency_floors asserts every resolved package’s declared rust_version is at or below ours; deny.toml bans serde_yaml, uom, arrow-flight and the rest of the §3.1/§3.3 list with a reason each.
Confirmation
rust / family-check and rust / deny are required checks; toolchain_matches_msrv fails if rust-version and rust-toolchain.toml diverge.
Pros and cons
Pinning family-wide costs a wider Dependabot blast radius per upgrade; the alternative is the mixed-family failure mode that neither cargo tree -d nor cargo deny reports.
More information
Blueprint §3.1 (version anchors, revision 4 MSRV row), §3.3 (library boundaries); review finding R2-5; docs/capability-maps/evidence/rust/ lockfiles.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0019
title: Record the DataFusion plan fingerprint as evidence; keep it out of the memo key
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-48, DM-31, DM-15, DM-59]
blueprint: [§14.2, §14.3, §20.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0044
revisit: datafusion-proto gains a documented byte-stability guarantee, or the two-process byte-equality test fails after a DataFusion upgrade
verification: tests/engine two-process plan-encoding byte-equality test; rust / test
ADR-0019: Record the DataFusion plan fingerprint as evidence; keep it out of the memo key
Context
Revision 2 put a datafusion-proto plan fingerprint into the rule passes’ memo key. The second review’s R2-1 measured six distinct encodings of one plan over a schema with five field-metadata keys, because datafusion-proto serializes metadata in map iteration order.
Scope
Binds what the fingerprint is for and what it is not. The rest of the memo key is ADR-0017’s subject.
Drivers
The plan is a pure function of the rule plan, the catalog snapshot and the engine profile, all already in the key, so hashing it adds no information to reuse validity; an unstable key is a silent cache miss or, worse, a false hit across versions.
Options
Keep the fingerprint in the key and canonicalize metadata — rejected: it still assumes cross-release protobuf stability the project cannot verify. Drop the fingerprint entirely — rejected: it is genuinely useful evidence in the pass record.
Outcome
The fingerprint leaves the memo key and lives in provenance.pass_records.plan_fingerprints as evidence, defined as pse.planfp.v1 and versioned like pse.canon.v1. Schema and field metadata are canonicalized at construction (§4.3) so the bytes are reproducible within a release.
Consequences
Beside it the pass record stores the plan’s EXPLAIN in pgjson and the ordered list of rules that actually fired, from the analyzer and optimizer observer closures — queryable structure, never identity.
Compensating controls
The platform LogicalExtensionCodec encodes a snapshot table as (snapshot_id, relation_id, version, content_hash) and a kernel UDF by its KernelSpec digest rather than by name, so the bytes do not depend on in-process naming.
Confirmation
A CI test encodes the same plan in two fresh processes and asserts byte equality; cross-release stability is explicitly not asserted.
Pros and cons
Evidence that is not a key can drift without breaking correctness, which is exactly the property wanted here.
More information
Blueprint §14.2 rule 5, §14.3 (pass engine), §20.2 (manifest); review finding R2-1; DataFusion capability map PROBE C (measured).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0044.
id: ADR-0020 title: Defer salsa; the artifact-hash memo is the only memoization mechanism status: superseded date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-58, DM-32, DM-28] blueprint: [§14.3] review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings evidence: Proposed supersedes: [] superseded-by: ADR-0042 revisit: A measurement shows sub-pass granularity finer than per-instance is needed inside P7 verification: The §24.3 compile-time benchmark for P7 on a flowsheet-scale model; register row R-01
ADR-0020: Defer salsa; the artifact-hash memo is the only memoization mechanism
Context
Blueprint §14.3 defers salsa behind the artifact-hash memo with a stated trigger. The first review’s F13 asked whether the two mechanisms duplicate each other; the supporting-library capability map measured that salsa’s backdating and accumulators work as described, which does not change the question.
Scope
Binds the memoization mechanism for phases 0-3. It does not reject salsa permanently; the trigger is a measurement, and the register row carries it.
Drivers
salsa‘s distinctive value is automatic dependency tracking, which duplicates pass_specs’ declared inputs; an undeclared read is a contract violation, not something to track; its unwind-based cancellation cannot cross the Ipopt boundary.
Options
Adopt salsa now — rejected: a second dependency system and a second cancellation model with no measured need (charter DM-58). Adopt it only for P7 — deferred to the same trigger.
Outcome
salsa is not a dependency. Per-instance memoization inside P7 uses the same key shape over (instance_id, template hash, resolved features).
Consequences
If the trigger fires, adoption means reconciling two cancellation models at the Ipopt boundary, which is the part to design before adopting rather than after.
Compensating controls
Register row R-01 carries the trigger, the owner and a phase-gated review date so the deferral does not become an omission.
Confirmation
The §24.3 benchmark measures P7 compile time per instance; the register review reads it monthly.
Pros and cons
Deferring costs a possible re-plumb later; adopting now costs a mechanism nothing has asked for.
More information
Blueprint §14.3 (pass engine); review finding F13; docs/adr/register.md row R-01.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0042.
id: ADR-0021
title: Load YAML with serde-saphyr; serde_yaml is banned
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-51, DM-30, DM-45]
blueprint: [§3.1, §3.3]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: serde-saphyr becomes unmaintained, or a document form the authoring language needs is not expressible through it
verification: rust / deny (deny.toml bans serde_yaml); tests/governance/tests/banned_patterns.rs; the hostile-document budget tests in tests/conformance
ADR-0021: Load YAML with serde-saphyr; serde_yaml is banned
Context
Blueprint §3.3 loads package and case documents from YAML and TOML with source spans. serde_yaml is deprecated (0.9.34+deprecated, last release 2024-03), and the deprecation is encoded only in semver build metadata, so cargo audit does not see it — the reason §3.1 requires cargo deny as well.
Scope
Binds the YAML parser and the hostile-input posture of the document loader. TOML stays toml 1.1.6 with Spanned<T> supplying pse.source_span.
Drivers
Authoring documents come from users and agents; a parser that panics on hostile input is a denial-of-service surface; typed errors with line and column are what diagnostics (§23.2) need.
Options
Keep serde_yaml — rejected: unmaintained and invisible to cargo audit. yaml-rust2 — rejected: no serde integration, so the typed-error property would be hand-built. JSON only — rejected: the authoring language is meant to be written by hand.
Outcome
serde-saphyr 1.2.0 is the YAML loader: typed errors with line and column, hostile input refused without panics, and a budget bound on nesting, aliases and allocation.
Consequences
The budget must be configured per document class and tested with adversarial inputs (charter DM-54); serde_yaml is banned in deny.toml so it cannot return through a transitive dependency.
Compensating controls
deny.toml names serde_yaml with the reason and the review date; the conformance layer feeds the loader deeply nested and alias-bombed documents and asserts a typed refusal.
Confirmation
rust / deny is a required check; the budget tests run in rust / test.
Pros and cons
A less-used parser carries its own risk; the register carries the row that re-checks its maintenance status.
More information
Blueprint §3.1 (supporting crates), §3.3 (document loading); review finding R2-5; supporting-libraries capability map.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0022
title: Write every kernel against num-dual 0.15; FeOs stays conditional
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-25, DM-43, DM-40, DM-31]
blueprint: [§3.1, §3.3, §9.8]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: feos-core releases a version that depends on num-dual 0.15 (checked monthly with cargo info)
verification: tests/conformance kernel derivative tests against hand derivations; rust / family-check; register row R-05
ADR-0022: Write every kernel against num-dual 0.15; FeOs stays conditional
Context
Blueprint §3.1 pins num-dual at 0.15.0 and every kernel body is written against its 0.15 trait shape (DualNum<Primitive = f64>). feos-core 0.10.1 requires num-dual ^0.14, whose trait shape differs, so the two cannot coexist in one kernel dialect.
Scope
Binds the AD library and trait shape for kernels, and the condition under which the FeOs provider is enabled. Implicit-kernel derivatives are included: implicit_derivative* is library capability, not platform code.
Drivers
One trait shape per kernel body or every kernel is written twice; a provider that forces a second AD generation into the graph would also force a second num-dual version past the family rule (ADR-0018).
Options
Pin num-dual 0.14 to keep FeOs — rejected: 0.15 is the shape the operator catalog and the hyper-dual Hessian path are written against. Vendor a shim — rejected: two trait shapes in one workspace is the problem, not the fix.
Outcome
num-dual 0.15.0; every kernel body is generic over DualNum<Primitive = f64>; implicit kernels use num-dual’s implicit_derivative* / ImplicitDerivative. feos-core/feos enter pse-kernels-ext only when their upstream tracks 0.15, which is a phase-4 gate.
Consequences
SAFT providers are unavailable until the gate opens; Helmholtz reference fluids are expression templates and a platform kernel regardless (§9.8), so the gap is bounded to SAFT-family equations of state.
Compensating controls
Register row R-05 carries the monthly cargo info feos-core check; feos never appears in [workspace.dependencies] until it does.
Confirmation
Kernel derivative tests compare num-dual results against hand derivations in the kernel suites, which is what makes the implicit-function-theorem use auditable.
Pros and cons
Waiting costs a provider; adopting early costs two AD generations in one graph.
More information
Blueprint §3.1, §3.3 (num-dual and feos rows), §9.8 (provider kernels), §18.2; review finding R2-5; register row R-05.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0023
title: Give pse-ids sole ownership of hashing, with derive_key contexts and pse.canon.v1
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-15, DM-11, DM-48, DM-40]
blueprint: [§5.1, §5.3, §20.1]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0045
revisit: A canonicalization constant changes (alignment, metadata version, legacy flag, compression), which is a new pse.canon.vN and invalidates every stored hash
verification: tests/governance/blake3_owner.rs; the canonicalization property tests in tests/conformance (re-batching, metadata re-insertion order, dictionary re-encoding, Parquet round trip)
ADR-0023: Give pse-ids sole ownership of hashing, with derive_key contexts and pse.canon.v1
Context
Blueprint §5.3 defines content identity over the canonical IPC encoding only and versions the serializer as pse.canon.v1. The first review’s F12 required the hashing contract to name its constants and its scope; §3.1 makes pse-ids the sole blake3 dependant.
Scope
Binds who may hash, what is hashed, and the four IPC constants that are part of the contract. Float canonicalization is ADR-0030; the plan fingerprint is ADR-0019 and is not a content hash.
Drivers
A hash computed in two places will diverge; Parquet embeds created_by, so its bytes change on upgrade while the data does not; alignment alone changes IPC bytes (872 versus 1032 for identical rows).
Options
Hash Parquet artifacts — rejected: not stable across writer versions. Let each crate hash what it owns — rejected: the canonicalizer, not the hash, is what tests cover, and it must exist once.
Outcome
pse-ids is the only crate that depends on blake3 and holds the canonical IPC serializer. Every 128-bit semantic ID uses a derive_key context. pse.canon.v1 names alignment 64, MetadataVersion::V5, no legacy format and no compression; changing any of them is a new version.
Consequences
Every artifact hash is tied to the IPC path; SERDE_ARROW:* or otherwise unregistered metadata keys are an error rather than stripped; nothing may hash a Debug rendering, because three libraries in the dependency set leak hash-container iteration order into theirs.
Compensating controls
blake3_owner fails if any other crate declares blake3; banned_patterns greps for {:?} reaching a hash input in pse-ids.
Confirmation
Canonicalization property tests assert that two logically identical relations split into different batches hash identically, and that a Parquet round trip does not change the hash.
Pros and cons
One hashing crate is a bottleneck for changes; it is also the only way the contract can be tested in one place.
More information
Blueprint §5.1 (identity), §5.3 (canonical serialization and hashing), §20.1 (artifact store); review finding F12.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0045.
id: ADR-0024
title: Cross the Python boundary with pyo3-arrow and the PyCapsule stream protocol
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-37, DM-41, DM-42, DM-43]
blueprint: [§3.1, §21.1]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: arrow-pyarrow drops its build-time interpreter requirement, or pyo3-arrow stops tracking the pinned arrow family
verification: python / test test_native_stub_surface and test_extension_round_trip; rust / family-check (pyo3 family)
ADR-0024: Cross the Python boundary with pyo3-arrow and the PyCapsule stream protocol
Context
Blueprint §3.1 retains pyo3-arrow 0.19.0 over arrow-pyarrow because the platform needs the PyCapsule stream protocol and the numpy bridge, and because arrow-pyarrow requires a Python interpreter at build time, which breaks the offline build. The second review’s R2-4 sharpened the measured Rust/Python asymmetry at that boundary.
Scope
Binds the Rust-side Arrow-to-Python mechanism and the contract that crosses it. The Python-side contract classes and their strictness are ADR-0015’s subject.
Drivers
The capsule protocol, not pyarrow, is the contract: any Arrow implementation can consume it, and none of the typed contract classes names a pyarrow type; an interpreter at build time would make the Rust build depend on the Python environment.
Options
arrow-pyarrow — rejected for the build-time interpreter and the narrower protocol. Hand-rolled FFI over the C data interface — rejected: pyo3-arrow already carries the capsule and numpy bridges.
Outcome
pyo3 0.29 with pyo3-arrow 0.19.0; every table crosses as __arrow_c_stream__, drained as a RecordBatchReader, never as row objects and never materialized on both sides at once.
Consequences
pyo3-arrow depends on thiserror 1.x beside the platform’s 2.x, which is an accepted deny.toml skip with a reason: no type crosses that boundary. Wheels are abi3-py311.
Compensating controls
The bundle’s loss profile declares, per table and field, whether a pse.* extension type is carried and whether the consumer registered it — the enforcement is asymmetric and the declaration says which side enforces what.
Confirmation
test_native_stub_surface diffs the hand-written _native.pyi against dir(_native); the extension round-trip test runs on both interpreters.
Pros and cons
Accepting a duplicate thiserror generation is the price of the capsule protocol and an interpreter-free build; the alternative is a build that cannot run offline.
More information
Blueprint §3.1 (pyo3 row), §21.1 (extension module); review finding R2-4; ADR-0018 for the family rule and the skip.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0025 title: Defer LogicalPlan::Extension rule nodes; derivations already answer the attribution question status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-58, DM-46, DM-49] blueprint: [§14.2] review: not-required: recorded in the second review’s observations as an adopt-candidate, not as a finding evidence: Proposed supersedes: [] superseded-by: null revisit: The §22.4 diff report or agent tooling needs per-rule attribution inside a plan rendering verification: Register row R-02; the §22.4 diff-report acceptance test when it is written
ADR-0025: Defer LogicalPlan::Extension rule nodes; derivations already answer the attribution question
Context
Blueprint §14.2 rule 8 defers wrapping each rule body in a LogicalPlan::Extension node carrying rule_id. The DataFusion capability map measured that such a node survives the optimizer intact (PROBE B), so the deferral is a cost decision, not a feasibility one.
Scope
Binds whether rule attribution lives in the plan. It does not affect provenance.derivations or rules_fired, which answer “which rule produced this row” today.
Drivers
The mechanism costs the 14-method UserDefinedLogicalNode contract and an ExtensionPlanner; the question it answers is already answered at the data level (charter DM-58).
Options
Adopt now — rejected: machinery ahead of a demonstrated consumer. Never adopt — rejected: EXPLAIN-level attribution is genuinely the better answer once a diff report renders plans.
Outcome
Not adopted. Derivations and the optimizer observer’s rules_fired list are the attribution mechanism for phases 0-3.
Consequences
If the trigger fires, the node must be added for every rule body at once, because partial attribution in an EXPLAIN is worse than none.
Compensating controls
Register row R-02 carries the trigger and a phase-gated review date.
Confirmation
The observer closures of Analyzer::execute_and_check and Optimizer::optimize already record which rules fired on each plan, which is recorded in the pass record (ADR-0019).
Pros and cons
Deferring costs a later retrofit across all rule bodies; adopting now costs a 14-method contract with no consumer.
More information
Blueprint §14.2 rule 8; the second review’s observations; docs/adr/register.md row R-02.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0026
title: Drop uom and arrow-flight; pint validates what the adapter emits and never defines a quantity
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-02, DM-42, DM-58, DM-43]
blueprint: [§3.1, §3.3, §21.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: A kernel genuinely needs compile-time unit typing, or a remote Arrow transport becomes a requirement
verification: rust / deny (deny.toml bans uom and arrow-flight); python / lint banned-api rule on assert_units_consistent; tests/governance/tests/banned_patterns.rs
ADR-0026: Drop uom and arrow-flight; pint validates what the adapter emits and never defines a quantity
Context
Blueprint §3.3’s “explicitly not added” list drops uom and arrow-flight, and the second review’s R2-4 found §3.3 and §21.2 contradicting each other about whether a Python units library is an authority.
Scope
Binds the units authority and removes two dependencies with no consumer. The quantity-type contract itself is ADR-0008.
Drivers
One authority per fact (DM-02): the registry defines quantities and the unit set; uom’s stock SI system cannot express the currency dimension costing needs; arrow-flight has no consumer, and a dependency with no consumer is a supply-chain surface for nothing.
Options
Compile-time unit typing in kernels with uom — rejected: KernelSpec signatures are checked by P10’s unit inference and by the per-kernel parity suites, and the currency dimension does not fit. Keep arrow-flight for a future service — rejected: add it when a service exists.
Outcome
uom and arrow-flight are banned in deny.toml. pint is present only because Pyomo’s units machinery is pint: it validates what the adapter emits and never defines a quantity, and no relation is ever derived from a pint object.
Consequences
The adapter calls pyomo.util.check_units.identify_inconsistent_units — not a bare assert_units_consistent — so a disagreement names the offending components in a compile.math finding; if the two disagree, the adapter has a bug.
Compensating controls
A ruff banned-api rule rejects assert_units_consistent in the Python package; deny.toml carries both bans with reasons and review dates.
Confirmation
python / lint and rust / deny are required checks; the unit-inference tests in tests/conformance are the positive side.
Pros and cons
Dropping uom means unit errors are caught by inference rather than by the type system; the currency dimension makes the type-system route unavailable anyway.
More information
Blueprint §3.1, §3.3 (“explicitly not added”), §21.2 (adapter algorithm step 1); review findings R2-4 and R2-5.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0027
title: Define the commit contract as P2-level validity; compilability is gated separately
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-14, DM-29, DM-30, DM-08]
blueprint: [§22.2, §14.5]
review: not-required: stated as a revision-3 decision; neither review raised it as a finding
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A consumer is found treating a committed revision as compilable, or P2 grows a check expensive enough to make commits slow
verification: tests/lifecycle commit-contract tests; the closure-report tests in tests/conformance
ADR-0027: Define the commit contract as P2-level validity; compilability is gated separately
Context
Blueprint §22.2 makes a change set validated by P2 on the resulting snapshot, applied atomically, yielding a new model or case revision. Revision 3 states the commit contract explicitly because “valid” was previously ambiguous between structurally valid and compilable.
Scope
Binds what a committed revision guarantees. It does not bind what a publishable compiled problem guarantees — that is the closure report (§14.5).
Drivers
A commit that required full compilation would make authoring unusable; a commit that guaranteed nothing would make every consumer re-validate; charter DM-14 asks for explicit commit boundaries.
Options
Require full P0-P10 validity to commit — rejected: an author could not save work in progress. Require nothing — rejected: every reader would need to re-check referential integrity.
Outcome
A committed revision is structurally and referentially valid: every invariant of reference.schema_invariants holds, every target resolves to an identity, every expression parses. It may still fail P3-P10. Publication as a compiled problem is gated by the closure report, and every revision carries last_closure_status.
Consequences
Change sets are the only write path into authored; an attempt to write a derived relation is rejected and an “expected” derived fact goes to provenance.assertions.
Compensating controls
last_closure_status on every revision lets a reader tell a committed revision from a compilable one without running the compiler.
Confirmation
tests/lifecycle commits a revision that fails P10 and asserts it commits, is readable, and is refused as a compiled problem.
Pros and cons
Two gates are more concepts than one; one gate is either too slow or too weak.
More information
Blueprint §22.2 (change-set model), §14.5 (closure proofs), §D13.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0028
title: Build Ipopt 3.14.20 with MUMPS and ASL from pinned sources in a container; probe HSL at runtime
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-28, DM-43, DM-48, DM-45]
blueprint: [§18.3, §6.13, §26]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A platform’s recipe cannot be reproduced (macOS or Windows at phase 1b), or a wheel needs the native backend and the linkage question comes due
verification: python / parity test_00_preflight (Ipopt 3.14.x, MUMPS, ASL, SolverFactory("ipopt").available(False)); solvers-image-rebuild-check (weekly, reproducibility of library checksums)
ADR-0028: Build Ipopt 3.14.20 with MUMPS and ASL from pinned sources in a container; probe HSL at runtime
Context
Blueprint §18.3 requires Ipopt 3.14 or newer for GetIpoptCurrentIterate/GetIpoptCurrentViolations, and §6.13 requires an explicit probe_host operation that records the available linear solvers. The first review’s F10 asked when the capability probe runs and where its result is persisted; the host’s system Ipopt is 3.11.9.
Scope
Binds how solvers are acquired for CI and development, and the fallback policy when a profile names an unavailable linear solver. Wheel linkage is deliberately left open (register row R-09, a phase-1 ADR).
Drivers
Iteration statistics as data require Ipopt 3.14; parity iteration counts require MUMPS ordering comparable to IDAES’s binaries; HSL cannot be redistributed; a capability claimed but unprobed is a lie (DM-43).
Options
idaes get-extensions in CI — rejected: it fetches IDAES’s binaries, which is neither clean-room nor pinned. conda-forge ipopt on Linux — rejected for CI: the recipe would not be ours to reproduce; it stays the documented Windows route. Bundle HSL — impossible: licensing.
Outcome
docker/solvers/ builds Ipopt releases/3.14.20, ThirdParty-Mumps releases/3.0.14 and ThirdParty-ASL releases/2.1.0 from checksum-verified tarballs on a digest-pinned ubuntu:24.04, with netlib reference BLAS/LAPACK and --without-metis so MUMPS ordering matches IDAES’s. HSL is never built; probe_host records ma27/ma57/ma86/ma97 availability at runtime.
Consequences
A profile naming ma57 on a host without HSL fails with capability.backend unless it declares fallback_linear_solver = mumps, in which case the fallback is a selected policy recorded in runs.resolved_options.
Compensating controls
runtime.host_capabilities persists the probe; runs.resolved_options records the options actually passed to CreateIpoptProblem, so a reproduction on a different host is either identical or visibly different.
Confirmation
python / parity runs in the container and fails the pre-flight rather than skipping; the weekly rebuild-without-cache job compares library checksums.
Pros and cons
Building from source costs a container image and a weekly job; using someone’s binaries costs reproducibility and the clean-room story.
More information
Blueprint §18.3 (in-process Ipopt), §6.13 (execution and evidence), §26 (residual risks); review finding F10; register rows R-08 and R-09.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0029
title: Give every session a FairSpillPool sized from a declared memory limit
status: superseded
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-30, DM-28, DM-43]
blueprint: [§14.3]
review: not-required: adopted in revision 3 from the DataFusion capability map’s measured pool behaviour; no review finding
evidence: Interface-checked
supersedes: []
superseded-by: ADR-0046
revisit: A workload’s spill rate makes the declared limit the binding constraint on compile time, or a deployment needs a different pool discipline
verification: tests/engine memory-exhaustion test (asserts a typed ResourcesExhausted naming the keys to change); the §24.3 compile benchmarks
ADR-0029: Give every session a FairSpillPool sized from a declared memory limit
Context
Blueprint §14.3 builds one DataFusion SessionContext per snapshot with a FairSpillPool sized from datafusion.runtime.memory_limit, never an UnboundedMemoryPool.
Scope
Binds the memory pool, the disk manager configuration and the failure mode of memory exhaustion. Thread budgets are §18.8.
Drivers
A bounded pool turns exhaustion into a typed, actionable error naming the keys to change; an unbounded one turns it into process death, which loses the run, the diagnostics and the partial outputs.
Options
UnboundedMemoryPool — rejected: an OOM kill is not a failure mode the failure taxonomy (§23.2) can describe. GreedyMemoryPool — rejected: one operator can starve the rest of the plan.
Outcome
FairSpillPool sized from a per-deployment datafusion.runtime.memory_limit, with an explicitly configured DiskManager directory, declared spill_compression and max_spill_file_size_bytes, and target_partitions validated against the rayon budget at construction.
Consequences
Deployments must declare a memory limit; there is no “unlimited” setting, which is the point.
Compensating controls
Exhaustion raises ResourcesExhausted naming the configuration keys; the pass record notes the failure class and partial outputs are discarded (§14.3).
Confirmation
tests/engine drives a plan past the limit and asserts the typed error; the §24.3 benchmarks record spill behaviour under recorded conditions.
Pros and cons
A limit that is too low fails runs that would have succeeded; a limit that does not exist fails them less legibly.
More information
Blueprint §14.3 (pass engine, session), §18.8 (threading), §23.2 (failure taxonomy).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
- 2026-09-13 — superseded by ADR-0046.
id: ADR-0030
title: Canonicalize NaN and preserve -0.0 on the hashing path; never use Float64 as a distinct or join key
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-40, DM-42, DM-08, DM-15]
blueprint: [§5.3, §14.2, §19.2]
review: docs/design_review/reviews/design_review_arrow-native-idaes-core-blueprint-rev2_2026-09-13.md#7-principle-findings
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: DataFusion changes its grouping or join equality for floats, or a rule genuinely needs a float-valued key
verification: tests/engine rule-compiler rejection test (rule.float_key); the canonicalization property tests in tests/conformance
ADR-0030: Canonicalize NaN and preserve -0.0 on the hashing path; never use Float64 as a distinct or join key
Context
Blueprint §5.3 step 5 hashes floats as IEEE-754 bits after mapping every NaN to the quiet NaN with zero payload and positive sign, preserving -0.0. The second review’s R2-2 measured that the preservation claim fails under GROUP BY, DISTINCT and hash joins, which merge -0.0 with +0.0 and treat NaN as self-equal.
Scope
Binds float canonicalization on the hashing path and the key discipline in rule plans. It does not change what reaches a solver: a NaN’s provenance matters for diagnostics (§18.2).
Drivers
An unqualified “-0.0 is preserved” is a claim the engine contradicts (charter DM-59); a float join key silently merges rows a reader believes are distinct.
Options
Canonicalize -0.0 to +0.0 too — rejected: the sign is meaningful in Arrow’s totalOrder and in stored data. Allow float keys with a tolerance — rejected: tolerance is a physical decision, not a key semantics decision.
Outcome
canonical_f64_bits maps every NaN to the quiet NaN with zero payload and positive sign and preserves -0.0, applied on the hashing path only. Preservation holds for the content hash and for Arrow ordering (null, -1, -0, 0, NaN, nulls first); it does not hold under engine grouping or joining, and §5.3 now says so. Rule plans never use a Float64 column as a distinct or join key.
Consequences
Analytics that group on floats collapse -0.0 with +0.0; §19.2 records the collapse rather than hiding it. Null-bearing key comparisons use distinct/not_distinct semantics and Kleene booleans.
Compensating controls
The rule compiler rejects a violating rule_plan_nodes row with rule.float_key, so the discipline is enforced at compile time rather than reviewed by eye.
Confirmation
tests/engine asserts the rejection; the canonicalization property tests assert the hash is invariant under re-batching and stable across NaN payloads.
Pros and cons
Refusing float keys costs expressiveness in rules; the measured engine behaviour is why it costs less than allowing them.
More information
Blueprint §5.3 step 5, §14.2 rule 7, §19.2; review finding R2-2; DataFusion capability map PROBE D (measured).
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0031
title: Commit every generated source and diff it in CI; never generate in build.rs
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-52, DM-23, DM-48, DM-53]
blueprint: [§4.2, §18.3, §21.5]
review: not-required: a convention stated in §4.2; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: Generation becomes slow enough that an explicit step is a friction point, or a generated artifact needs a host-specific input a checkout cannot supply
verification: rust / codegen-diff (cargo xtask codegen --check); scripts/check_generated.py in python / lint and pre-commit
ADR-0031: Commit every generated source and diff it in CI; never generate in build.rs
Context
Blueprint §4.2 generates typed relation views, builders, validators, migrations, the Python contract classes, documentation and the Ipopt bindgen output from the registry, and requires the emitted source to be committed.
Scope
Binds where generated code lives, how it is regenerated and how drift is detected. What is generated is §4.2’s subject.
Drivers
A build.rs generator makes the build depend on the host and hides the diff; a generated file that is not committed cannot be reviewed, and the Python contract classes are part of the public API.
Options
Generate in build.rs — rejected: no reviewable diff, and the bindgen step needs the solver container’s headers. Generate on demand and gitignore — rejected: the diff is the review.
Outcome
cargo xtask codegen [--only relations|python|docs|bindgen] writes the four generated paths; every generated file carries // @generated by pse-schema <fingerprint>, #![rustfmt::skip] and a blanket lint allow with a reason. cargo xtask codegen --check runs git diff --exit-code plus an untracked-file check.
Consequences
A registry change is a two-part commit: the registry edit and the regeneration. A prettyplease upgrade is itself a regeneration commit.
Compensating controls
.gitattributes marks the generated paths linguist-generated; the agent settings deny edits under them; check_generated.py holds a sha256 manifest of python/pse/contracts/.
Confirmation
rust / codegen-diff is a required check and runs in the solver container so the bindgen output is regenerated against the pinned headers.
Pros and cons
Committing generated code inflates diffs; not committing it removes the only place the generator’s output is reviewed.
More information
Blueprint §4.2 (what is generated), §18.3 (bindgen output), §21.5 (Python contracts); ADR-0012.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0032
title: Fetch external reading copies into a gitignored external/ rather than vendoring them
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-31, DM-48, DM-02]
blueprint: [§3.1, §0.2]
review: not-required: a repository-hygiene decision; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A CI job needs an external tree that a network fetch cannot supply reproducibly
verification: governance / adr-lint asserts the IDAES tag in scripts/fetch-external.sh equals the parity group pin; .gitignore covers /external/
ADR-0032: Fetch external reading copies into a gitignored external/ rather than vendoring them
Context
The blueprint reads idaes-pse/, arrow-rs/ and a DataFusion checkout for behaviour and for API facts (§3.1’s closing note). Vendoring them would add hundreds of megabytes and three trees with their own histories to a repository whose own content is text.
Scope
Binds how external source is obtained and where it lives. Which tags are used is derived from the pins, not chosen here.
Drivers
Reading copies are derived artifacts; the authoritative statement of which version is read is the pin, not a checkout; a vendored tree invites edits that no pin describes.
Options
Git submodules — rejected: they pin a commit that can diverge from the dependency pin, giving two authorities. Vendor the trees — rejected: size, and a second copy that can be edited.
Outcome
external/ is gitignored and populated by scripts/fetch-external.sh, which clones idaes-pse at the parity tag, arrow-rs at the arrow pin and datafusion at the datafusion pin, reading the arrow and datafusion tags from Cargo.lock.
Consequences
A fresh clone cannot read external source until just fetch-external runs; just doctor reports it as missing rather than failing silently.
Compensating controls
A lint asserts the script’s IDAES tag equals the parity dependency-group pin, so the reading copy cannot drift from what parity tests against.
Confirmation
governance / adr-lint runs the tag check on every PR; agent settings deny edits under external/.
Pros and cons
Fetching costs network in a fresh environment; vendoring costs a second authority for three versions.
More information
Blueprint §3.1 (closing note on the vendored checkout), §0.2 (source paths); ADR-0003.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0033
title: Keep one blueprint file, revised in git, with section numbers as stable citation targets
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-02, DM-49, DM-55]
blueprint: [§0.1]
review: not-required: a documentation-authority decision; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A revision needs to remove a section entirely, or the file grows past the point where one document is readable
verification: governance / adr-lint (every ADR § citation must resolve to a heading in blueprint.md); docs / build
ADR-0033: Keep one blueprint file, revised in git, with section numbers as stable citation targets
Context
The design existed as …-blueprint.md and …-blueprint-rev3.md side by side, with reviews citing line numbers into whichever file they were written against. A citation that does not survive the next revision is not a citation.
Scope
Binds the blueprint’s file layout, its revision mechanism and its citation form. The amendment procedure for a decision is ADR-0001’s decision-PR rule.
Drivers
ADRs, reviews, plans and agent instructions all cite the blueprint; git already versions files; section numbers are the only stable handle a reader and a linter can share.
Options
One file per revision — rejected: every citation rots at the next revision. A docs site with anchors only — rejected: anchors change with titles; numbers do not.
Outcome
One docs/authoritative_design/blueprint.md, revised in git with tags design-rev2 and design-rev3 marking the seeded history. Section numbers are stable citation targets: new material is inserted as a sub-section, never by renumbering. §0.1 states that the file is authoritative, that ADRs record why, and that reviews are evidence, not authority.
Consequences
A section that becomes wrong is rewritten in place with a revision-history row and a > Decision: ADR-NNNN line, not deleted and renumbered.
Compensating controls
scripts/adr.py lint resolves every § citation against the file’s headings, so a renumbering breaks the build rather than silently rotting the records.
Confirmation
The revision-history table gained a git column so each revision names its tag; docs / build publishes the file with the ADR index beside it.
Pros and cons
Stable numbers constrain how the document can be reorganised; rotating citations constrain everything that cites it.
More information
Blueprint §0.1 (what this document is), revision history; docs/authoritative_design/README.md.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0034
title: Govern with a single maintainer, PR-only squash merges onto a linear signed main
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-14, DM-45, DM-60]
blueprint: [§0.1]
review: not-required: a repository-governance decision; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A second maintainer joins, which changes the approval count, the bypass policy and the project board question
verification: governance / pr-title; the main ruleset declared in .github/setup/*.json and applied by just gh-setup (re-running it is a no-op)
ADR-0034: Govern with a single maintainer, PR-only squash merges onto a linear signed main
Context
The project has one maintainer, is public, and will accept outside contributions. The rules have to protect history and provenance without requiring a reviewer who does not exist.
Scope
Binds branch protection, merge strategy, approvals, signing and where the rules are declared. Labels, milestones and issue forms follow from it.
Drivers
A linear signed history is what makes a release tag mean something; zero approvals is honest for a sole maintainer where pretending otherwise would mean self-approval theatre; declared configuration is reviewable, clicked configuration is not.
Options
Require one approval — rejected: the sole maintainer would approve their own PRs. Allow direct pushes to main — rejected: no check would ever run before a change lands. Merge commits — rejected: squash keeps the Conventional-Commit title as the changelog unit.
Outcome
Single maintainer; every change arrives as a PR; squash merge only with the PR title as the commit subject; main requires linear history and signatures and forbids deletion and non-fast-forward; the ruleset is declared in .github/setup/*.json and applied idempotently.
Consequences
Bypass is restricted to the admin role and every bypass push requires a governance follow-up issue. Required checks are staged: a check that never reports would block every PR (§6).
Compensating controls
governance / pr-title enforces the Conventional-Commit types and scopes from cliff.toml, which is what git-cliff reads to build the changelog.
Confirmation
just gh-setup re-applies the declared configuration; a drift shows up as a diff in the applied JSON rather than as a surprise.
Pros and cons
Zero approvals is weak; the compensating control is that every check is required and history is protected.
More information
GOVERNANCE.md; blueprint §0.1; .github/setup/; ADR-0001’s decision-PR rule.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0035
title: Update dependencies with Dependabot, grouped by the pinned families
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-31, DM-51, DM-48]
blueprint: [§3.1]
review: not-required: a supply-chain process decision; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: Dependabot cannot express a grouping the family rule needs, in which case Renovate is the declared fallback (register row R-13)
verification: rust / family-check on every Dependabot PR; rust / deny; governance / pr-title
ADR-0035: Update dependencies with Dependabot, grouped by the pinned families
Context
ADR-0018 pins both families = family-wide, so an upgrade is never a single-crate bump: every arrow* and parquet* crate must move together, and so must every datafusion* crate plus object_store.
Scope
Binds the update mechanism and its grouping. Which upgrades are accepted is a per-PR decision with family-check as the gate.
Drivers
Ungrouped updates would open a dozen PRs that each fail family-check; a grouped PR is reviewable as one decision; github-actions pins by commit SHA and needs its own cadence.
Options
Renovate — kept as the declared fallback rather than the default: Dependabot needs no extra app installation on a single-maintainer public repository. Manual upgrades — rejected: nothing would notice a yanked crate.
Outcome
Cargo monthly with versioning-strategy: increase and groups arrow-family (arrow*, parquet*), datafusion-family (datafusion*, object_store), pyo3-family (pyo3*, numpy), codegen (syn, quote, proc-macro2, prettyplease) and numerics. uv monthly with quality-tools and runtime groups and idaes-pse ignored. GitHub Actions weekly, grouped. Docker for the solver base image.
Consequences
A codegen group PR must carry a regeneration commit (ADR-0031); the parity pin never moves by bot, because moving it is an ADR (ADR-0003).
Compensating controls
rust / family-check fails a PR that moves one crate of a family; pinact and Dependabot together keep every action pinned by SHA with a version comment.
Confirmation
Every bot PR runs the full required-check set before it can merge; cargo deny catches a newly yanked or newly advisory-carrying version.
Pros and cons
Monthly cadence trades freshness for review load; the weekly actions cadence exists because action SHAs move for security reasons.
More information
Blueprint §3.1 (pins and families); .github/dependabot.yml; ADR-0018; register row R-13.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0036
title: Publish the documentation as an mdBook on GitHub Pages
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-55, DM-49]
blueprint: [§0.1, §0.3]
review: not-required: a documentation-tooling decision; neither review raised a finding against it
evidence: Proposed
supersedes: []
superseded-by: null
revisit: The Python package grows a public surface that needs generated API documentation (register row R-14), or the book outgrows a single SUMMARY.md
verification: docs / build (adr.py index --check, mdbook build docs, lychee --offline); docs / deploy on main
ADR-0036: Publish the documentation as an mdBook on GitHub Pages
Context
The repository’s authority lives in Markdown: the blueprint, the charter, the reviews, the capability maps, the decision records. A reader needs one navigable surface, and the ADR index has to be generated rather than hand-maintained.
Scope
Binds the documentation tool, the site source layout and the build gate. Rust API documentation is cargo doc and is copied in when the artifact exists; Python API documentation is deferred.
Drivers
The source is already Markdown; the book must build without the Rust extension so docs / build stays cheap; internal links must be checked, because a docs site with broken links is worse than a directory listing.
Options
mdBook — selected: Markdown in place, SUMMARY.md as the one navigation authority, a cargo binstall-able pinned binary. MkDocs or Sphinx — rejected: a Python environment on the docs path for a project whose docs are not Python. A directory of files on GitHub — rejected: no link checking and no generated index.
Outcome
mdBook with docs/book.toml (src = ".") and docs/SUMMARY.md, deployed to GitHub Pages from main. The ADR block in SUMMARY.md is generated by scripts/adr.py index between <!-- adr:begin --> and <!-- adr:end -->.
Consequences
Every document that should be readable must be listed in SUMMARY.md; docs/generated/ is committed so the book never needs the extension module built.
Compensating controls
docs / build runs adr.py index --check before mdbook build, so a stale index fails the PR rather than the deploy; lychee --offline checks internal links.
Confirmation
docs / deploy uses actions/deploy-pages from main only; the built book is gitignored.
Pros and cons
mdBook’s single SUMMARY.md is a bottleneck for a large site; it is also the reason nothing can be published unlisted.
More information
Blueprint §0.1, §0.3 (reading guide); docs/book.toml; docs/SUMMARY.md; register row R-14.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0037
title: Defer datafusion-tracing, instrumented-object-store, datafusion-ffi and egglog, each with a trigger
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-58, DM-43, DM-40, DM-59]
blueprint: [§3.3, §14.2, §23.1, §22.1]
review: not-required: recorded in the second review’s observations; the deferrals themselves are blueprint §3.3 positions, not findings
evidence: Proposed
supersedes: []
superseded-by: null
revisit: Any one of the four triggers fires: a datafusion-tracing release matching the pinned engine; an instrumented-object-store release matching it; a third-party compiled kernel package under §22; a cross-process, cross-version egglog extraction-determinism test passing
verification: Register rows R-03, R-04, R-06; rust / family-check (a tracing crate at 55.0.0 against a 55.1.0 engine fails it)
ADR-0037: Defer datafusion-tracing, instrumented-object-store, datafusion-ffi and egglog, each with a trigger
Context
Blueprint §3.3’s “deferred with a stated trigger” list and §14.2 rule 8 defer four mechanisms. The second review observed that datafusion-tracing and instrumented-object-store top out at 55.0.0 against a 55.1.0 engine and should not be added yet.
Scope
Binds four deferrals into one record so the register has one place to point. salsa (ADR-0020) and LogicalPlan::Extension (ADR-0025) have their own records because each carries its own design argument.
Drivers
A dependency one patch behind the pinned engine would violate the family rule (ADR-0018); a rewrite engine whose extraction is nondeterministic across processes cannot be on a correctness path; an FFI extension surface with no third-party consumer is machinery for nothing.
Options
Add datafusion-tracing now and pin around the family rule — rejected: the family rule exists precisely to prevent that. Adopt egglog as an optimization behind a flag — that is the position: optional, phase 4, never a correctness dependency.
Outcome
None of the four is a dependency today. datafusion-tracing and instrumented-object-store enter when a release matching the pinned engine exists; datafusion-ffi enters only if §22’s extension model admits compiled third-party kernel packages; egglog stays optional, phase 4, gated on cross-version extraction determinism, and its renderings are never hashed.
Consequences
Observability in the meantime is tracing spans at pass and run boundaries (§23.1); DataFusion emits no spans of its own, which is a known gap rather than a hidden one.
Compensating controls
Register rows R-03, R-04 and R-06 carry the triggers, owners and check commands, so each deferral is revisited monthly rather than forgotten.
Confirmation
rust / family-check mechanically prevents the first two from being added at a mismatched version; hand-written canonicalization (§7.4) stays complete on its own so egglog can never become load-bearing.
Pros and cons
Four deferrals in one record risks burying an argument; three register rows and two sibling ADRs keep each trigger individually visible.
More information
Blueprint §3.3 (deferred with a stated trigger), §14.2 rule 8, §23.1 (observability), §22.1; the second review’s observations; register rows R-03, R-04, R-06.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
id: ADR-0038
title: Add pse-ipopt-sys, pse-buildinfo, xtask, benches and five test crates to the workspace layout
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-31, DM-53, DM-60, DM-48]
blueprint: [§3.2, §24.1, §24.3, §18.3]
review: not-required: the layout additions follow from decisions the reviews already drove (R2-5 pins, F10 probes); no finding names §3.2’s crate list
evidence: Proposed
supersedes: []
superseded-by: null
revisit: A sixth test family is needed, or xtask grows logic that belongs in a shipped crate
verification: tests/governance/tests/every_crate_registered.rs (every workspace member appears in the blueprint §3.2 layout and carries the pse- prefix); rust / test
ADR-0038: Add pse-ipopt-sys, pse-buildinfo, xtask, benches and five test crates to the workspace layout
Context
Blueprint §3.2 lists the 23 pse-* crates, python/, packages/reference/ and tests/golden/. The §24.1 test matrix names six test families and §18.3 requires a -sys crate with committed bindgen output, none of which appear in the layout block.
Scope
Binds the workspace membership. This record amends blueprint §3.2 rather than deviating from it — the additions are written into the layout block with a > Decision: ADR-0038 marker — which is why its level is decision and not should-deviation; the amendment is carried as revision 4 in the revision history.
Drivers
A test family that is not a crate cannot be run by cargo nextest; governance tests need cargo_metadata and so need a crate; xtask is how the justfile stays a one-line surface over logic that needs Rust APIs; the Ipopt links key must live in its own -sys crate.
Options
Put governance tests in each crate’s tests/ — rejected: they are workspace-wide assertions and would be duplicated 23 times. Make xtask a shell script — rejected: cargo metadata parsing, JSON and cross-platform behaviour are Rust’s job.
Outcome
The workspace adds crates/pse-ipopt-sys/ (links = "ipopt", bindgen output committed), crates/pse-buildinfo/ (lockfile hashes, rustc version, profile, git sha), xtask/, benches/ (pse-benches, criterion), and tests/{governance,engine,conformance,lifecycle,structural}/ as member crates named pse-tests-<name>. tests/golden/ stays data, not a crate. The ipopt feature is default-on in pse-backend-native, pse-runtime and pse-py, with pse-ipopt-sys optional so --no-default-features checks cleanly; phase-0 wheels build without it.
Consequences
None of the added crates is ever published: publish = false stays set for xtask, benches, tests/* and pse-py.
Compensating controls
every_crate_registered asserts the workspace membership matches the blueprint layout block, so the two cannot drift.
Confirmation
rust / clippy runs once with default features and once with --no-default-features, which is what keeps the optional -sys crate honest.
Pros and cons
Five test crates is more manifests than one; it is also what makes the §24.1 matrix runnable and separately profiled in .config/nextest.toml.
More information
Blueprint §3.2 (workspace layout, revision 4), §24.1 (test layers), §24.3 (benchmarks), §18.3 (Ipopt -sys crate); ADR-0028.
Status history
- 2026-09-13 — accepted with the repository-seeding pull request (backfilled from blueprint revision 3).
ADR-0039: Enforce semantic admission and complete physical quantity operations
id: ADR-0039 title: Enforce semantic admission and complete physical quantity operations status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-06, DM-07, DM-24, DM-41, DM-42, DM-43] blueprint: [§D5, §4.1, §4.3, §4.4, §6.2, §8.3, §14.2] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0008] superseded-by: null revisit: A new quantity operation cannot be expressed by the registered composition policies, or a supported query bypasses generated admission. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0039: Enforce semantic admission and complete physical quantity operations
Context
R4-01, R4-05 and R4-08 show that registration is passive, successful casts can lose values, and dimensional arithmetic alone does not determine a physical quantity type.
Scope
Amends semantic admission, metadata interpretation and quantity-operation contracts. Registry declarations remain the sole type authority; Arrow and DataFusion execute their mechanical checks.
Drivers
Reject malformed or incompatible meaning before a consumer assumes validity. Preserve basis, reference, scale, shape and subject through composed expressions without introducing another unit authority.
Options
Keep passive registration and castability as admission: rejected by E1/E3. Add a second physical type library: rejected because it would duplicate the registry. Use generated validation and explicit operation rules: selected.
Outcome
Generate a recursive validator and invoke it at bundle/provider admission, before optimization, after relevant plan transformations and at output admission. Match complete quantity contracts before attaching destination metadata; numeric and physical conversions require a declared, checked conversion. Declare composite quantity-operation rules and a WeightedMean operator in the registry.
Consequences
Validation must cover nested fields, versioned metadata, cross-field references and row-level constraints. Unsupported physical combinations are typed errors; dimensional equality never silently chooses a basis or reference state.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
E1 and E3 characterize the pinned libraries; future boundary and quantity-composition fixtures in plan 02 must exercise supported import/query/output paths. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
One generated validator and quantity contract serves every boundary. The cost is explicit operation/conversion declarations and admission work; extension metadata alone would be cheaper but cannot enforce the required meaning.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0008 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0040: Resolve property demand from normalized seeds and name immutable stage outputs
id: ADR-0040 title: Resolve property demand from normalized seeds and name immutable stage outputs status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-18, DM-20, DM-21, DM-22, DM-31] blueprint: [§D8, §4.1, §6.11, §9.6, §14.1] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0011] superseded-by: null revisit: A selected method requires demand that cannot be declared over the finite normalized scope before realization. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0040: Resolve property demand from normalized seeds and name immutable stage outputs
Context
R4-06 identifies P6 reading P7/P8 equations and P15 initialization requirements despite preceding those stages. A relation-wide single producer also cannot describe successive math artifacts.
Scope
Amends demand discovery and stage ownership within the existing three artifact levels. No fourth model authority or database is introduced.
Drivers
Make the pass DAG decidable before realization, including law-only and initializer-only demand. Allow several stages to carry one schema without competing mutable producers.
Options
Discover demand from realized equations through an implicit backward loop: rejected. Contract a larger mutually recursive compiler stage: viable but unnecessary for declared demands. Resolve normalized seeds and check realization completeness: selected.
Outcome
Extract property-demand seeds from normalized template, law, port, display and initializer declarations. P6 closes selected method requirements over a finite bound scope before realization. Pass contracts name immutable input/output ports; successive stages may carry the same relation schema under distinct artifact handles.
Consequences
P7 through P15 cannot invent new property demand. They reject undeclared requirements and incomplete providers; stage-output ownership and acyclicity become registry checks.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
Plan 02 specifies demand_seed_closure, stage_bundle_graph and realization-completeness fixtures, including law-only and initializer-only requirements. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Finite seeds and named ports make missing requirements and producer ambiguity observable. Authors must declare opaque requirements early; undeclared late demands are refused instead of triggering an invisible extra compile loop.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0011 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0041: Reuse complete stage inputs before introducing finer memoization
id: ADR-0041 title: Reuse complete stage inputs before introducing finer memoization status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-31, DM-32, DM-33] blueprint: [§D14, §6.11, §14.3, §14.4] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0017] superseded-by: null revisit: Representative cold/warm measurements show whole-stage recomputation dominates total preparation cost and a complete finer dependency bundle passes differential checks. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0041: Reuse complete stage inputs before introducing finer memoization
Context
R4-07 changes a bound package species set without changing the published P7 tuple. Fixed values also occupy a different segment from initial guesses.
Scope
Amends cache dependency completeness and the initial granularity, preserving the artifact-hash memo and model/case distinction.
Drivers
Prevent reuse of old equations after a domain, package, provider or bound-value change. Distinguish fixed numerical bindings from free guesses and retain the lineage that explains the output.
Options
Keep the instance/template/features tuple: rejected as incomplete. Automatically track arbitrary reads: deferred by ADR-0042. Complete whole-stage keys: selected.
Outcome
Initially memoize complete immutable stage inputs, including absence, registry/package/domain/binding artifacts, provider candidates, policies and relevant binary digests. Recompute at stage granularity and reuse equal complete input keys. Equal relation content may share storage; changed semantic lineage does not justify skipping a dependent stage. Refresh fixed/parameter bindings separately from free initial guesses; finer reuse needs complete dependency fixtures and measured benefit.
Consequences
Some changes recompute more than the minimum. P7 is not initially cached per instance, and the invalidation table promises no unimplemented row-level reuse.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
Plan 02 requires incremental_vs_clean_binding_matrix and compares full values, membership and lineage, including provider absence and newly eligible candidates. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Complete keys simplify the correctness argument and support restart reuse. They may cause extra stage recomputation; equal relation storage is still shareable, and finer skipping requires a separate complete dependency contract.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0017 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0042: Defer finer memoization until whole-stage measurements justify it
id: ADR-0042 title: Defer finer memoization until whole-stage measurements justify it status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-32, DM-33, DM-56, DM-58] blueprint: [§D14, §14.3, §24.3, §26] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0020] superseded-by: null revisit: A measured workload still needs finer dependency tracking after complete per-instance reuse is characterized. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0042: Defer finer memoization until whole-stage measurements justify it
Context
The simpler compiler in review section 8 starts with complete stage keys. ADR-0020 assumed an incomplete per-instance cache and therefore needs a different deferral boundary.
Scope
Amends the granularity and adoption trigger; salsa remains absent from runtime dependencies.
Drivers
Keep one dependency mechanism until a measured workload needs finer tracking. Dependency correctness and cancellation behavior must precede claims of incremental speed.
Options
Adopt salsa or a custom fine-grained dependency engine now: rejected without a measured consumer. Keep the old incomplete tuple: rejected. Refine one existing memo in measured steps: selected.
Outcome
Use the artifact-hash memo at whole-stage granularity first. Evaluate a complete per-instance bundle only after the R-22 measurement trigger. Consider salsa under R-01 only if that measured case requires automatic finer dependency tracking and its cancellation model has a complete boundary contract.
Consequences
Compile latency may initially be higher, but there is one dependency declaration and one cancellation boundary to validate.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
Register R-01 and R-22 track the decisions; plan 02 names the cold/warm and incremental-versus-clean experiments. No speedup is claimed. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Deferral avoids a second revision/cancellation model and premature per-instance keys. Whole-stage preparation can cost more; R-22 measures that cost before refinement, and R-01 separately evaluates salsa.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0020 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0043: Declare kernel outcomes and generate only supported bindings
id: ADR-0043 title: Declare kernel outcomes and generate only supported bindings status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-08, DM-19, DM-25, DM-28, DM-43, DM-44, DM-47] blueprint: [§D9, §6.11, §7.6, §18.5, §18.9, §21.4] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0012] superseded-by: null revisit: A current kernel needs an execution or outcome policy that the descriptor cannot express without backend-specific semantic invention. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0043: Declare kernel outcomes and generate only supported bindings
Context
R4-02 and R4-10 show that conditional flags are not execution, null outputs lack mandatory meaning, natural units are absent from the descriptor, and backend capability tables disagree.
Scope
Amends kernel/result and generated-binding contracts; no arbitrary compiled plugin service is added.
Drivers
Keep missing inputs, invalid computations and unsupported derivatives distinct across adapters. A declared signature must not be mistaken for an implementation of units, laziness or a backend route.
Options
Generate every adapter from a signature alone: rejected. Let each backend decide nulls and units: rejected. Generate mechanical adapters from complete declarations and checked implementation bindings: selected.
Outcome
Declare natural units, null-input handling, domain/failure outcomes, effects, argument evaluation, derivatives and supported binding implementations once. Generate only bindings backed by an implementation and conformance scope. Scalar/UDF failure is a typed error; tolerant batch analysis returns mandatory typed per-row outcomes. Runtime conditionals use actual branch execution, not eager UDF flags.
Consequences
Kernel authors still write the actual algorithm and supported derivative implementations. An ImplicitRef has no direct DataFusion binding; an explicitly bound implicit KernelCall is a separate capability.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
E2 supplies the eager-UDF counterexample. Plan 02 requires scalar/batch/native/NL/Pyomo value, failure, unit and derivative conformance; absent bindings fail preflight. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Generation removes repeated adapter decisions while allowing specialized algorithms. Complete binding descriptors and actual conformance fixtures cost more than a broad capability checkbox, but make unsupported routes explicit.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0012 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0044: Record plan encodings as noncanonical diagnostic evidence
id: ADR-0044 title: Record plan encodings as noncanonical diagnostic evidence status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-15, DM-31, DM-48, DM-59] blueprint: [§4.3, §6.13, §14.2, §20.2] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0019] superseded-by: null revisit: A consumer demonstrates a need for canonical plan bytes, or upstream supplies a versioned byte-stability contract worth adopting. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0044: Record plan encodings as noncanonical diagnostic evidence
Context
E5 produces different protobuf encodings after sorted metadata insertion. The existing semantic cache key already makes plan byte identity unnecessary for correctness.
Scope
Amends plan evidence and metadata determinism claims. Semantic stage keys remain ADR-0041 authority.
Drivers
Retain useful plan attribution without turning diagnostic serialization order into semantic identity. Do not maintain a canonical protobuf contract without a consumer that requires it.
Options
Recursively canonicalize all protobuf maps: possible but adds an encoding contract without a current consumer. Keep the false equality test: rejected. Preserve diagnostic bytes and explicitly bound their meaning: selected.
Outcome
Retain protobuf and EXPLAIN as noncanonical diagnostic artifacts with physical checksums, engine/codec identifiers and ordered rule observations. Do not require byte equality for equivalent plans, use their checksums as relation identity, or claim sorted HashMap insertion canonicalizes encoding.
Consequences
Equivalent runs can carry different plan-evidence checksums without a cache miss or a reproduction failure. Reproduction compares declared semantic outputs and recorded configuration.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
E5 in the retained review runner establishes the distinction. Plan 02 requires plan-evidence round-trip/attribution checks and verifies that plan bytes never enter semantic memo inputs. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Existing codecs and EXPLAIN remain useful evidence with checksum integrity. Equivalent plans can have different bytes; consumers compare decoded meaning/attribution and cannot use the diagnostic checksum as a semantic key.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0019 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0045: Separate canonical logical content from encoded artifact integrity
id: ADR-0045 title: Separate canonical logical content from encoded artifact integrity status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-11, DM-12, DM-14, DM-15, DM-42, DM-48] blueprint: [§5.1, §5.3, §20.1, §20.2, §20.4, §20.5] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0023] superseded-by: null revisit: Any canonical constant or normalization rule changes, or a measured workload exceeds the supported relation-size envelope. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0045: Separate canonical logical content from encoded artifact integrity
Context
R4-03 and R4-04 show that stream content identity conflicts with IPC-file/Parquet byte checks and that ignored values under nulls affect the proposed canonical bytes.
Scope
Amends hashing and artifact publication as pse.canon.v2 and a versioned manifest. Semantic-ID derivation contexts and ADR-0030 valid-float rules remain unchanged.
Drivers
Make equal declared relations retain one identity despite ignored storage bytes or alternate physical encodings. Publication must distinguish a complete verified object from a corrupt object with a plausible name.
Options
Use one digest for logical and encoded bytes: rejected by E4. Canonicalize Parquet writer bytes as model identity: rejected. Separate logical identity from physical integrity: selected.
Outcome
Compute logical relation identity from a framed, versioned canonical stream after recursive normalization of hidden physical content. Store each physical encoding under its own byte checksum and bind logical identity, encoding, size and checksum in the manifest. Snapshot membership is explicit and excludes self-referential revision/ref/pass bookkeeping. Validate existing objects before reuse and publish the ref only after complete artifacts and manifest exist.
Consequences
The canonicalization change requires v2 even though the model implementation is skeletal. Unknown/v1 artifacts are refused until an explicit importer decodes, validates and rehashes them; byte copying is not migration. Phase 1 has an explicit bounded canonical relation size.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
E4 is the counterexample. Plan 02 names canonical_null_equivalence, encoding_roundtrip_identity and publication fault fixtures. Both checksum and logical verification are required on untrusted artifact admission. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Separate logical and encoded identities support IPC/Parquet interchange and explicit integrity checks. Recursive normalization and restore verification have real memory/CPU cost; the bounded envelope makes that cost a stated implementation obligation.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0023 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0046: Share runtime budgets and reserve platform allocations explicitly
id: ADR-0046 title: Share runtime budgets and reserve platform allocations explicitly status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-30, DM-35, DM-39, DM-50, DM-59] blueprint: [§14.3, §18.2, §18.8, §20.1, §24.3] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0029] superseded-by: null revisit: Measured pool/process peaks diverge materially or a supported consumer cannot reserve before allocation. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0046: Share runtime budgets and reserve platform allocations explicitly
Context
R4-13 identifies Arrow/native allocations outside the query pool and separate snapshot sessions multiplying deployment budgets. Arrow tracking reservations are not fallible allocation limits.
Scope
Amends resource ownership and the scope of typed exhaustion guarantees, preserving snapshot-local catalogs.
Drivers
Prevent concurrent snapshot sessions from each consuming an entire deployment budget. Cover live platform buffers with reservations and state exactly which allocations the library pool cannot control.
Options
Give every session its own full deployment budget: rejected. Treat Arrow tracking as a rejection mechanism: rejected. Share runtime accounting with explicit platform reservations and a supported size envelope: selected.
Outcome
Share one configured RuntimeEnv and accounted budget across snapshot sessions in a process. Query consumers use FairSpillPool; platform-owned canonicalization, binding and result buffers reserve through MemoryConsumer/MemoryReservation::try_grow before allocating. Track consumers and peaks, and report process peak separately. Limit guarantees apply to accounted consumers, not arbitrary allocator/solver memory.
Consequences
Reservations must cover temporary copies and coexistence peaks, release on error/cancellation, and avoid double counting shared buffers. External solvers retain separate declared process budgets.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
Pinned memory API source is interface-checked in the review. Plan 02 names query/canonicalization/result budget tests and concurrent-snapshot cancellation checks; whole-process protection is not claimed. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Shared accounting exposes consumer limits and release behavior. It requires correct ownership and estimates for coexisting buffers; solver/global allocator memory remains outside the fallible guarantee and must be measured separately.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0029 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0047: Preserve guarded numerical semantics and borrow through safe Arrow views
id: ADR-0047 title: Preserve guarded numerical semantics and borrow through safe Arrow views status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-24, DM-28, DM-29, DM-36, DM-37, DM-40] blueprint: [§D6, §D11, §6.11, §7.3, §7.4, §18.2, §21.2] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [ADR-0014] superseded-by: null revisit: A backend cannot express an adopted numerical policy or measured buffer-transfer cost justifies a new layout. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0047: Preserve guarded numerical semantics and borrow through safe Arrow views
Context
R4-02 and R4-09 expose eager tape evaluation and unguarded floating rewrites/FMA. L7 shows that safe primitive buffer views already account for array slice offsets.
Scope
Amends numerical lowering, control flow and Arrow-to-native borrowing; the math IR and native workspace remain separate representations.
Drivers
Preserve observable numerical values and failures under rewriting, branching and backend lowering. Use Arrow ownership and slice APIs where they meet the native consumer contract.
Options
Unconditional real-algebra rewrites and FMA: rejected by E6. Reject all sliced arrays: safe but needlessly restrictive. Explicit numerical policy plus safe slice-aware views: selected.
Outcome
Preserve ordered operations and guarded failure behavior by default. Persist any reassociation, FMA and comparison policy in the case-bound preparation contract; a backend that cannot honor it refuses it. Compile explicit conditional blocks/masks, scope shared values to the executed branch, and differentiate only executed paths. Borrow null-free contiguous primitive values through owning safe Arrow views, including eligible slices; otherwise perform checked, reserved copies.
Consequences
Affine structure must be established over decision variables before choosing Pyomo LinearExpression. Lifetime and cancellation tests are required for borrows; no performance gain is claimed without measurements.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
E2/E6 are counterexamples. Plan 02 requires guarded_branch_execution, numerical_policy_conformance, nonlinear_affine_lowering and arrow_borrow_lifetime fixtures. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Ordered branch-aware execution makes failure semantics explicit and safe views permit eligible slices. Some algebraic optimizations and backend routes must be refused; copying remains the supported fallback, with performance decided by measurement.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — proposes superseding ADR-0014 for the corrected contract above; accepted arguments remain immutable.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
ADR-0048: Use standard relational operators and explicit-schema readers
id: ADR-0048 title: Use standard relational operators and explicit-schema readers status: accepted date: 2026-09-13 deciders: [paul-heyse] level: decision principles: [DM-18, DM-25, DM-38, DM-41, DM-53, DM-54, DM-56, DM-58] blueprint: [§5.4, §6.10, §6.11, §14.2, §21.1, §24.1, §26] review: docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates evidence: Proposed supersedes: [] superseded-by: null revisit: A representative supported scan/import needs an operation outside the bounded algebra or measured optimization triggers in R-24/R-25 fire. verification: just adr-lint; just docs; docs/plans/02-blueprint-revision-5-contracts.md Verification; bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
ADR-0048: Use standard relational operators and explicit-schema readers
Context
L3/L4/L9 identify existing physical predicates, aggregation/unnesting and explicit-schema readers for current or declared consumers. R4-11 shows the exact-pushdown test cannot detect missing rows.
Scope
Adds bounded rule-algebra and ingestion bindings inside D10. It does not add a crate family, new query engine or new modeling language.
Drivers
Use existing operator implementations for actual scan, contribution and observation consumers. Keep exact filtering, physical meaning and import policy explicit while avoiding another general-purpose engine.
Options
Reimplement predicate semantics or reject all Arrow readers because inference exists: rejected. Add a general query/ingestion platform: rejected. Bind existing pinned mechanisms to explicit consumers: selected.
Outcome
Use DataFusion physical predicates and Arrow filters as the shared exact evaluator; compare pushed scans with complete unpruned results. Add typed aggregate/unnest operations for contribution grouping and membership projection. Use explicit-schema Arrow CSV/JSON readers only for declared tabular observation imports. Keep Parquet pruning, bounded coalescing and predicate preimages behind workload and equivalence checks where their benefit is unmeasured.
Consequences
Aggregate null/empty/order behavior, provenance, reader schema/units and filter completeness remain platform contracts. Predicate preimages must be equivalent replacements; conservative ranges can only prune with a residual filter.
Compensating controls
The implementation gates are explicit in plan 02. Document acceptance does not establish runtime behavior. Deferred optimizations remain disabled until their register trigger and conformance evidence are satisfied.
Confirmation
Pinned interfaces were inspected in review L3/L4/L8/L9. Plan 02 names pushdown_vs_unpruned, relational_expansion_reference and explicit_schema_observations; register R-24/R-25 records deferred optimization decisions. just adr-lint and just docs check documentation structure only; the plan distinguishes these checks from future behavioral acceptance.
Pros and cons
Shared predicates, aggregates, unnesting and declared-schema readers reduce custom mechanisms. The platform still owns null/empty/order/provenance and physical conversions; optional pruning and coalescing require separate consumer measurements.
More information
See the governed blueprint sections, the revision-4 review, its reproducible library evidence, the revision-5 contract review, and plan 02.
Status history
- 2026-09-13 — proposed before the revision-5 blueprint amendment; user requested reconciliation of all review findings and library opportunities.
- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope; evidence remains Proposed until the wave-1 implementation review).
id: ADR-0049
title: Pin serde_json and uuid as direct dependencies for the manifest codec and explicit identities
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-02, DM-41, DM-52]
blueprint: [§3.1, §5.1, §20.2]
review: not-required: a pin addition inside ADR-0018’s accepted policy, with no new dependency family and no change to the resolved graph
evidence: Interface-checked
supersedes: []
superseded-by: null
revisit: a hash frame, a relation payload or a second document codec is routed through serde_json, or a pass other than the authoring assign-ids command mints a UUID
verification: tests/governance/tests/pins_match_blueprint.rs (the §3.1 rows equal [workspace.dependencies]); just family-check
ADR-0049: Pin serde_json and uuid as direct dependencies for the manifest codec and explicit identities
Context
Wave 1 implements the snapshot manifest (blueprint §20.2), the canonical extension metadata of §4.4 and the explicit id policy of §5.1. The manifest and the extension metadata are JSON documents, and an explicit-policy entity’s id is a UUIDv7 minted by the authoring tool. serde_json 1.0.151 and uuid 1.26.1 already resolve in the committed Cargo.lock as transitive dependencies, so promoting them to direct = pins adds two rows and no packages.
Scope
Binds which crates may declare the two pins and what they may be used for. It does not add a dependency family (blueprint §3.1 “Supply chain”), does not change the resolved graph, and leaves the authoring document formats (serde-saphyr for YAML, toml for package.toml, ADR-0021) untouched.
Drivers
Correctness: one codec authority per serialization, not a hand-rolled writer beside serde. Reproducibility: the manifest and extension metadata are byte-compared in tests, so the codec must be the pinned one. Supply chain: both crates are already in the graph, so the direct pin costs nothing and makes the ownership visible to pins_match_blueprint.
Options
Hand-roll the manifest and extension-metadata JSON — rejected: a second codec authority beside serde, and the one that would drift. Encode the manifest as TOML through the existing toml pin — rejected: the Python side reads the same document with msgspec JSON (§21.5) and would need a second parser. Mint explicit ids only in Python — rejected: pse authoring assign-ids is a Rust command and P1 must reject an unidentified entity without an interpreter present.
Outcome
serde_json = "=1.0.151" is a direct dependency of pse-catalog (the pse.manifest.v2 codec, §20.2), pse-relations (the canonical ARROW:extension:metadata codec, §4.4) and xtask (generator and report output). It is never a hash input and never carries relation data: every hashed byte goes through the pse-ids framing of §5.3, and every relation value is an Arrow array. uuid = { version = "=1.26.1", features = ["v7"] } is a direct dependency of pse-authoring only, used by ids::assign_ids to mint an explicit-policy identity at authoring time; P1 never mints an id — it rejects a missing one with parse.missing_id (§5.1).
Consequences
Two more = pins to move in a kind/library-upgrade PR, and two more rows that pins_match_blueprint asserts against blueprint §3.1. A serde_json::Value must not reach pse-ids: the crate-boundary rule that pse-ids owns hashing is what keeps JSON off the identity path.
Compensating controls
The §3.1 rows name the owning crates and state the non-uses (“never a hash input”, “assign-ids only”), so an owner that is not listed is a visible manifest diff. blake3_owner keeps hashing in pse-ids, where serde_json is not a dependency.
Confirmation
just governance runs pins_match_blueprint, which fails if a [workspace.dependencies] entry has no §3.1 row or a row disagrees with the manifest; just family-check asserts the resolved graph still holds one version per family.
Pros and cons
Promoting an already-resolved transitive crate to a direct pin is cheap and makes an implicit dependency explicit; the cost is two more rows that a library upgrade has to move together with the rest of §3.1.
More information
Blueprint §3.1 (supporting crates), §5.1 (id policies), §20.2 (manifest), §4.4 (extension metadata); ADR-0018 (pins and lockfile), ADR-0021 (serde-saphyr for YAML); plan 03 packet K-2.
Status history
- 2026-09-13 — accepted. A short record within ADR-0018’s accepted pin policy; review not required at maintainer discretion.
id: ADR-0050
title: Freeze the identity framing constants, canonical frames and the phase-1 relation envelope
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-11, DM-15, DM-48]
blueprint: [§4.3, §5.1, §5.3, §14.3]
review: not-required: the constants make ADR-0045’s and ADR-0007’s accepted contracts implementable without changing them; the revision-5 review already carries the Accept verdict for the contracts themselves
evidence: Tested
supersedes: []
superseded-by: null
revisit: any constant listed here has to change — that is a new version string and a superseding record, never an edit
verification: crates/pse-ids/tests/golden_vectors.rs (frozen hexadecimal vectors for every derive context, derive_hash, snapshot_id and encoding_checksum); the canonical metamorphic fixtures under tests/conformance/tests/
ADR-0050: Freeze the identity framing constants, canonical frames and the phase-1 relation envelope
Context
Blueprint §5.1 and §5.3 state the identity and canonicalization contracts; ADR-0045 separates logical content from encoded integrity and ADR-0023 gives pse-ids sole ownership of hashing. Several wave-1 packets (pse-ids, the canonicalizer, pse-schema, pse-catalog, pse-mathir, pse-compiler) have to produce the same bytes independently, and two of them are written in parallel. The constants those bytes depend on are stated in prose across three sections and nowhere as one list.
Scope
Freezes the numeric and textual constants of the identity contract and nothing else. It adds no identity kind, changes no rule about what is hashed (§5.3 steps 1–3 are unchanged), and does not raise the envelope — R-23 owns that. Float handling is ADR-0030’s and is cited, not restated.
Drivers
Reproducibility: a hash is a contract between processes, so its framing has to be a written constant, not an implementation detail. Correctness: derive_key and a truncated plain hash produce different bytes, so the context strings are part of the wire format. Parallel implementation: two packets coding against the same frame need one authority for it.
Options
Leave the constants in each implementation and compare with a golden test — rejected: the golden test would record whatever the first implementation did. Version each constant independently — rejected: the frame is one contract; a single version string per frame is what a reader can check. Defer the envelope to a runtime setting — rejected: an unbounded preflight cannot reserve before allocating (§14.3).
Outcome
Framing. Every framed part is a u64 little-endian length followed by that many bytes. Fixed-width identities and hashes keep their declared width (16 and 32 bytes); a schema version is u32 little-endian. There is no Debug or display serialization anywhere on a hashing path.
Derive contexts. blake3::Hasher::new_derive_key(context) is the only construction. The frozen contexts are the six of §5.1 — pse:named:v1, pse:symbol:v1, pse:equation:v1, pse:term:v1, pse:conn:v1, pse:node:v1 — plus pse:registry:v1 (registry fingerprint), pse:stage_key:v1 (the §14.3 memo key), pse:settings:v1 (the semantic-settings hash) and pse:mathir:node:v1 (structural and subtree hashes). A 128-bit identity is the first 16 bytes of the extendable output, a defined digest rather than a truncation.
Registry identity bootstrap. REGISTRY_PACKAGE_ID = named_id(SemanticId::NIL, "pse.schema"). Within it, a relation is named relation:<ns>.<name>@<v>, an enumeration enum:<Name>, an invariant invariant:<relation>:<name>, a pass pass:<P>@<v> and a rule rule:<name>@<v>.
pse.canon.v2 preimage. len‖"pse.canon.v2", then relation_id, then the schema version as u32 little-endian, then the registry fingerprint, then len‖metadata stream and len‖data stream. Each stream is a finished Arrow IPC stream written with IpcWriteOptions::try_new(64, false, MetadataVersion::V5) at its uncompressed default, carrying exactly one batch — including a zero-row batch for an empty relation. All padding bytes are zero.
pse.snapshot.v2 frame. The version string, the registry fingerprint, the snapshot kind, a counted list of (role, snapshot_id) semantic parents sorted by role, then a counted list of (port, namespace, relation_id, schema_version, logical_hash) members sorted by the first four fields. Text sorts by UTF-8 bytes, identities by raw bytes, versions numerically. A duplicate parent role or member port is an error; required empty relations appear explicitly; a sidecar never enters its own membership.
Envelope. Envelope::PHASE1 is 1,000,000 rows and 256 MiB of normalized value, offset and validity buffers per relation, and every Utf8/List offset must fit i32. A deployment may only tighten it. Preflight uses checked arithmetic and reserves before allocating; exceeding a bound is runtime.resource_limit, never a partial publication.
Floats. Unchanged from ADR-0030 and applied only to the hashing copy.
Consequences
Changing any of these constants is a new version string (pse.canon.v3, pse:named:v2) and a superseding record, because every stored logical_hash and snapshot_id in every golden store is a function of them. The 64-byte IPC alignment and MetadataVersion::V5 are part of the identity, so an Arrow upgrade that changes either is a format decision, not a pin bump.
Compensating controls
crates/pse-ids/tests/golden_vectors.rs holds frozen hexadecimal vectors for every context and every frame, so a change to the framing is a failing test with a diff rather than a silently different store. The canonical metamorphic fixtures assert that identity-preserving transforms keep the hash and that distinguishing ones change it.
Confirmation
just test-package pse-ids -p pse-relations runs the golden vectors; just test-package pse-tests-conformance -p pse-relations runs the metamorphic and round-trip fixtures. Both are named in verification:.
The vectors below are frozen by crates/pse-ids/tests/golden_vectors.rs (Tested,
2026-09-13: just test-package pse-ids -p pse-relations, dev profile,
pse-relations/force-validate, 87 passed, 0 failed). Inputs are the literals
A = [0x01; 16], B = [0x02; 16], C = [0x03; 16], D = [0x04; 16], index tuple (C, D),
Ordinal(7); the case frame uses registry fingerprint [0x11; 32], parent
("model", [0x22; 32]) and members ("case/aaaa", "case", C, 1, [0x33; 32]),
("case/bbbb", "case", D, 2, [0x44; 32]). derive_id(ctx, parts) equals the first
16 bytes of derive_hash(ctx, parts) because BLAKE3’s finalize is the XOF prefix; a
test records that equality so no later change separates the two.
| Vector | Hex |
|---|---|
derive_id(NAMED, [NIL, "pse.schema"]) | a409aa6be295e9f374cf4b829f193f49 |
named_id(^, "relation:authored.stoichiometry@1") | 4af05b3e65e854a58198776b3cddaa8d |
derive_hash(REGISTRY, [b"a", b"bc"]) | e1745e73a5c5b0a583f4cf13428c2e7696e97101b6a4abb64a8ddfb83969a8b6 |
symbol_instance_id(A, B, (C, D)) | 70dde4aa3dfe04a4886a7b59df389070 |
equation_instance_id(A, B, (C, D)) | a56c9c4f2710d4ee6160789909a9c33a |
law_term_id(A, B, (C, D)) | bbacd7698075d85247763217db444ee3 |
connection_equation_id(A, Ordinal(7), (C, D)) | 19ae7d5c7570254f981df87da0451540 |
mesh_node_id(A, B, Ordinal(7)) | eb00e870f92170e9ac71856dca721e01 |
discretized_symbol_id(A, B) | 7d39e0dbf3fea05cb118af04f299257a |
snapshot_id(case frame) | f778ddfdfc8864bd7c10168528f35bf164c14b184ae55d29b4ca8a76427693f8 |
encoding_checksum(b"pse") | b183159a276933fcc7170f73b6e21ff751344a2769b9669736ff4ffa47c689ee |
canonical_f64_bits(NaN) / (-0.0) | 0x7ff8000000000000 / 0x8000000000000000 |
canonical_f32_bits(NaN) / (-0.0) | 0x7fc00000 / 0x80000000 |
No independent BLAKE3 implementation was available in the build environment, so the
vectors are self-consistent rather than cross-checked; an external confirmation with
b3sum is a wave-exit item.
Pros and cons
One list of constants is a single point of change and therefore a single point of failure; it is also the only way two independently written implementations can be checked against the same bytes. The alternative — constants distributed over prose — is what made pse.canon.v1 unrepairable under its own name.
More information
Blueprint §5.1 (identity forms and derived-id formulas), §5.3 (canonical serialization and hashing, steps 4–8), §4.3 (metadata conventions), §14.3 (the pass engine and stage keys); ADR-0007, ADR-0023, ADR-0030, ADR-0045, ADR-0046; register row R-23 (a larger envelope); plan 03 §I.1 and packets K-1 and B-canon.
Status history
- 2026-09-13 — accepted. Evidence is
Proposed: the constants are written, the implementation and its golden vectors land with packet K-1.
id: ADR-0051
title: Extend the generated-tree list and make codegen –check a regeneration-equivalence check
status: accepted
date: 2026-09-13
deciders: [paul-heyse]
level: decision
principles: [DM-52, DM-53]
blueprint: [§4.2, §24.1]
review: not-required: an extension of ADR-0031 inside its accepted convention; the generated set grows and the check gets stronger, neither changes where generated code lives
evidence: Proposed
supersedes: []
superseded-by: null
revisit: the Ipopt bindgen arm starts running its generator in CI’s solver container, or the hand-written Rust manifest struct is generated (register row R-27)
verification: just codegen-check (cargo xtask codegen --check); tests/governance/tests/codegen_regeneration.rs
ADR-0051: Extend the generated-tree list and make codegen –check a regeneration-equivalence check
Context
ADR-0031 commits generated sources and checks them with git diff --exit-code plus an untracked-file check. That is hygiene: it proves the working tree is clean, not that re-running the generator reproduces the committed tree. Wave 1 adds two generated artifacts blueprint §4.2 implies but does not name — the serde document structs pse-authoring parses package documents into, and the docs/generated/ layout — and phase 0’s deferral of regeneration equivalence (register row R-20’s neighbourhood) ends when the generators exist.
Scope
Extends ADR-0031’s generated-path list and strengthens its check. It does not move a generated path, does not change the @generated header convention and does not reintroduce build-script generation. The Ipopt bindgen arm keeps its phase-0 behaviour until its generator can run.
Drivers
Correctness: a committed generated tree that the generator no longer reproduces is a silent fork of the authority. Reviewability: a document struct that is hand-written beside the registry’s DocumentSpec is a second column list, which no_shadow_structs exists to forbid. Determinism: a two-way diff catches a generator that drops a file as well as one that adds one.
Options
Keep hygiene-only checking — rejected: it cannot distinguish a current tree from a stale one. Regenerate in place and diff the working tree — rejected: a failing check would leave the checkout modified. Check only one direction — rejected: a generator that stops emitting a file would pass.
Outcome
Generated paths are ADR-0031’s four plus crates/pse-authoring/src/generated/documents.rs — the serde document structs generated from the registry’s DocumentSpec, consumed by pse_authoring::document::load_package. docs/generated/ has a declared layout: README.md, relations/<ns>.md, enums.md, extension_types.md, passes.md, rules.md and schema/authoring.schema.json, each carrying the @generated marker.
cargo xtask codegen --check regenerates every target into a temporary directory and diffs it against the committed tree in both directions — a file in one and not the other is a failure either way — and keeps ADR-0031’s untracked-file check. It never writes into the working tree.
python/pse/contracts/GENERATED.sha256 is written by cargo xtask codegen --only python, not by a Python script: one generator owns the tree and its manifest.
The bindgen arm keeps a hygiene-only diff for crates/pse-ipopt-sys/src/bindings.rs until the generator runs inside the solver container (--only bindgen without IPOPT_DIR exits 2 with a notice). That exception is named here so it is not read as regeneration equivalence.
The Rust Manifest struct in pse-catalog is hand-written in phase 0 and guarded by a parity test against the registry’s ManifestSpec, which is the authority for the generated Python msgspec struct and the documentation. Generating it is register row R-27.
Consequences
codegen --check costs a full regeneration instead of a git diff, so it is a local recipe and a CI job, not a pre-commit hook. Every generator must be deterministic — no HashMap iteration reaching output — because the check now compares two runs, not a run against a commit.
Compensating controls
crates/pse-schema/tests/codegen_determinism.rs asserts two runs are byte-equal before the tree is ever written; tests/governance/tests/codegen_regeneration.rs runs the two-way check inside just test; no_shadow_structs keeps a hand-written mirror of a generated struct out of the tree.
Confirmation
just codegen-check and just governance; the rust / codegen-diff job runs the same command. A failure names the differing path and the direction.
Pros and cons
A stronger check costs build time on every run; the weaker one cannot tell a current tree from one whose generator changed underneath it, which is exactly the failure ADR-0031 was written to prevent.
More information
Blueprint §4.2 (what is generated), §24.1 (test layers), §20.2 (the manifest), §22.1 (authoring documents); ADR-0004, ADR-0031; register rows R-20 and R-27; plan 03 packets R-1, A-6, A-8, A-9 and R-2.
Status history
- 2026-09-13 — accepted. Evidence is
Proposed: the generated-path list and the check contract are written;codegen_regenerationlands with packet R-2.
Plans
A plan records how work is sequenced and verified. A decision record
(../adr/) records what was decided and why. The two
have different lifecycles, which is why they live in different directories.
| Plan | Covers | ADRs | Status |
|---|---|---|---|
| 01 — Repository configuration | Repository identity, layout and migration, the Rust workspace and supply chain, the solver container, Python packaging and tests, the CI job graph, decision records, GitHub setup, the agent environment, the docs site | ADR-0001 – ADR-0038 | in-progress |
| 02 — Blueprint revision 5 contracts | Design amendment for R4-01–R4-13 and L1–L9; separate Proposed implementation handoff | ADR-0039 – ADR-0048 | done (documentation; runtime handoff Proposed) |
| 03 — Wave 1 foundations | Phase-0 foundations and load-bearing interfaces as parallel sub-agent packets | ADR-0039 – ADR-0051 | in-progress |
Norms
- Naming.
docs/plans/NN-kebab-case.md, numbered in the order they were started. Numbers are never reused. - Front matter.
title,status(draft|in-progress|done|abandoned),date,adrs(the decision records the plan implements), andphase(the delivery phase from blueprint §25). - Living until done. A plan is edited while it is being executed. That is the difference from an ADR, which is immutable once accepted.
- Outcome. When the work lands, append
## Outcome (recorded after implementation)with three sub-headings and fill all three:- What was built — what actually exists now, with the charter §D evidence label for each claim.
- A mistake made and corrected — at least one. A plan whose outcome records no mistake was either not executed or not read honestly.
- Deviations from the plan, deliberate — what was done differently and why. A deviation that changed a decision needs an ADR, not a paragraph here.
- Authority. When the code and a plan disagree, the code is what runs. A plan is never cited as the reason something behaves the way it does; the blueprint and the ADRs are.
- Plans live here, in the repository, not in an agent’s scratch directory.
just planstarts one.
01 — Repository configuration
title: Repository configuration and working environment for pse-arrow status: in-progress date: 2026-09-13 adrs: [ADR-0001 … ADR-0038] phase: 0
Plan: repository configuration and working environment for pse-arrow
Completion ledger (2026-09-13)
The later blueprint revision-5 contract amendment is tracked in plan 02. This setup ledger retains its own acceptance scope; its historical design summaries do not override revision 5.
The initial design below is historical planning context. The repository is now public
at paul-heyse/pse-arrow; the starting commit for completion is 3c9c95f, already
pushed to main, with a clean working tree. Completion uses signed commits and a
pull request, followed by verification of the merged commit. No release is published.
| Work | Status / acceptance |
|---|---|
| Environment | Tested: bootstrap in a fresh clone with relative and absolute venv paths (including spaces); doctor clean; directory entry made zero network connections |
| Solver access | Tested: immutable CI/dev pins agree with consumers; container preflight passes; no-cache rebuild matches published library checksums |
| Claude/Codex | Implemented: nine native roles, shared skills/rules/hooks; Tested: ten setup fixtures and Codex role/skill discovery; interactive hook trust and Claude usage-limit recovery remain |
| CI / packaging | Implemented: parity install, coverage artifact upload, unique check names, editable dev builds for automatic Python/parity CI; full wheels/sdist are manual or release-time |
| GitHub | Tested: main/tag rulesets active; settings re-applied without duplicate objects; two consecutive comparisons report zero differences |
| Qualification | Tested: local Rust/Python gates, depth-two feature combinations and release tests, fresh bootstrap, six negative controls; final PR/main checks remain the merge acceptance record |
Tested (starting state): just doctor, local mode, reports 1 blocking failure
and 3 warnings, baseline zero. just lint-agents and just adr-lint, read-only local
mode, report 0 failures, baseline zero. These are structural checks, not runtime
certification. The latest Python CI fails selecting the parity venv; Rust CI is pending.
Keep the recorded phase boundaries: real model generators, API-reference doc lint, release tooling and numerical parity beyond the existing preflight are deferred. Neither a clean generated tree nor a successful preflight proves those capabilities. Registry publishing and new release tags are outside this completion pass.
Original context (historical)
The revision-3 blueprint (docs/design_review/Arrow-native-idaes-core-architecture-blueprint-rev3.md) is the authoritative architecture: a Cargo workspace of 23 pse-* crates, a pyo3/pyo3-arrow extension, a Python package pse, shipped reference packages, and the §24.1 test/governance matrix that the repository has to be able to run. Nothing in the current working tree is under version control, no toolchain file exists, and the only pinned artifacts are the capability-map evidence lockfiles under docs/design_review/evidence/. The user asked for a best-in-class repo configuration and working environment (Rust and Python toolchains, testing, public GitHub setup, design-decision/ADR process) aligned to rev 3.
Decisions the user has made in this session
| Decision | Choice |
|---|---|
| Repository home | New public repo under paul-heyse; idaes-arrow (the fork) stays as-is |
| Naming | Crate prefix pse-*, import name pse; PyPI distribution pse-arrow (pse is taken) |
| License | MIT OR Apache-2.0 |
| Solvers in CI | Ipopt 3.14.x + MUMPS + ASL built from pinned sources in a cached container image; HSL is a local opt-in recorded by probe_host |
| MSRV | rust-version == pinned stable toolchain (1.98.1 today); blueprint §3.1’s “1.94.0” becomes “DataFusion’s floor”, amended by ADR |
| Docs tool | mdBook on GitHub Pages; Python API docs deferred (register row) |
Constraints inherited from the blueprint (verified against the text): =-pinned arrow 59.3.0 / datafusion 55.1.0 families, committed Cargo.lock, CI one-version-per-family assertion (cargo tree -d cannot detect a mixed family); edition 2024; cargo deny + cargo audit; generated sources committed and diffed in CI (relations, Python contracts, docs/generated/, Ipopt bindgen output) via an explicit step, never build.rs; force_validate in test/CI builds only; every Python library ==-pinned with two interpreter ranges (platform ≥ 3.11; parity 3.11–3.13 with idaes-pse==2.12.0); test families tests/{governance,engine,conformance,lifecycle,structural,golden}, python/pse/tests, python/pse/parity; residual risks CI must re-check (pre-1.0 crate floors, Ipopt/HSL/MUMPS recipe per platform, FeOs tracking num-dual 0.15, datafusion-proto byte stability).
Environment facts: stable 1.98.1 current, nightly 1.100 is the machine default (a rust-toolchain.toml is mandatory); tooling/dfarrow-apiex keeps its own nightly-2026-08-18 pin (rustdoc JSON format 61); uv 0.12.13, maturin 1.15.0, ruff, pyrefly (author’s checker), cargo-{nextest,deny,audit,hack,llvm-cov,semver-checks,insta,msrv,machete,shear,mutants,geiger,binstall}, just, taplo, typos, ast-grep, docker/podman installed; system Ipopt is 3.11.9 (too old); gh authenticated as paul-heyse; uv workspaces enforce one requires-python (intersection), so the parity set cannot be a narrower workspace member.
Conventions reused from paul-heyse/idaes-arrow: AGENTS.md canonical + short real CLAUDE.md starting @AGENTS.md; justfile recipes grouped by cost tier (env/discovery/local/pr/scheduled/mutating) with [group]/[doc]/[confirm], mutating recipes never a gate dependency; .envrc “fast, no network” + stdlib scripts/doctor.py; scripts/bootstrap.sh with exact cargo-binstall pins; deny.toml/clippy.toml/.config/nextest.toml/.taplo.toml/.editorconfig; .cargo/config.toml that never sets target-cpu=native; panic = "unwind"; tools from .venv/bin never $PATH; .python-version as the single interpreter source of truth; Dependabot family groups; .claude/settings.json allow/ask/deny + hooks + path-scoped rules; .claude/skills → .codex/skills symlink; plans/NN-*.md with a README index. Dropped because the repo is greenfield: the .quality/*-baseline.json ratchet (zero tolerance instead).
1. Repository identity
paul-heyse/pse-arrow. Description: “Arrow-native process systems engineering core in Rust: typed relations as the only model authority, a relational math IR, DataFusion-based inference, native NLP solving, and a generated Pyomo backend. Clean-room re-implementation of core IDAES-PSE capabilities, parity-tested against idaes-pse 2.12.0. Not affiliated with IDAES.” Topics: process-systems-engineering, apache-arrow, datafusion, rust, pyo3, chemical-engineering, flowsheet-modeling, equation-oriented-modeling, nonlinear-optimization, ipopt, pyomo, idaes.docs/relationship-to-idaes.md(the analogue of idaes-arrow’sFORK.md): clean-room (readexternal/idaes-psefor behaviour, never copy; the parity harness is the only coupling; enumerations preserved by name are listed in blueprint §6.14), the 2.12.0 parity pin and how it moves (ADR), trademark/non-affiliation statement, citation of the IDAES paper. Summarised inREADME.md.
2. Layout and migration
pse-arrow/
├── Cargo.toml Cargo.lock rust-toolchain.toml deny.toml clippy.toml rustfmt.toml .cargo/config.toml .config/nextest.toml
├── pyproject.toml uv.lock .python-version conftest.py
├── justfile .envrc .editorconfig .gitattributes .gitignore .pre-commit-config.yaml .taplo.toml typos.toml sgconfig.yml sgrules/
├── AGENTS.md CLAUDE.md README.md CONTRIBUTING.md GOVERNANCE.md SECURITY.md CODE_OF_CONDUCT.md CHANGELOG.md CITATION.cff
├── LICENSE-MIT LICENSE-APACHE LICENSES/{MIT.txt,Apache-2.0.txt} REUSE.toml cliff.toml
├── crates/pse-*/ (23 per blueprint §3.2) crates/pse-ipopt-sys/ xtask/ benches/
├── tests/{governance,engine,conformance,lifecycle,structural}/ # workspace member crates pse-tests-<name>
├── tests/golden/ # data: golden stores + fingerprints (not a crate)
├── python/pse/ (contracts/ GENERATED, codec/, authoring/, adapters/pyomo/, parity/, tests/) python/stubs/
├── packages/reference/
├── docker/solvers/{Dockerfile, build.sh, checksums.sha256, conda/env.yml, test/hs071_c.c, LICENSES.md, README.md} .devcontainer/
├── docs/{book.toml, SUMMARY.md, README.md, relationship-to-idaes.md, dev/ci.md, doc-lint-allow.toml}
│ ├── authoritative_design/{README.md, blueprint.md, proposal.md}
│ ├── adr/{README.md (generated), template.md, register.md, NNNN-*.md}
│ ├── plans/{README.md, NN-*.md}
│ ├── design_review/{design_principles/, reviews/} # unchanged: the design-review skill's output contract
│ ├── capability-maps/{arrow-rust,datafusion-rust,supporting-rust-libraries,python-libraries}.md
│ │ ├── evidence/{README.md, rust/ (lockfiles, probes, outputs, gen scripts), python/ (probes, apidump.py, apiq.py, exported requirements)}
│ │ └── facts/{arrow593,df551,support}.paths.txt.zst # compact path indexes for doc-lint (full facts are 364 MB: not committed)
│ └── generated/ # pse-schema output; committed; linguist-generated
├── external/ # gitignored: scripts/fetch-external.sh clones idaes-pse@2.12.0, arrow-rs@59.3.0, datafusion@55.1.0
├── scripts/{doctor.py, bootstrap.sh, adr.py, check_register.py, check_agent_config.py, check_generated.py, fetch-external.sh, labels-sync.sh}
├── tooling/dfarrow-apiex/ # kept; workspace `exclude`; own nightly rust-toolchain.toml; hard-coded /home/paul paths replaced
├── .github/{CODEOWNERS, PULL_REQUEST_TEMPLATE.md, ISSUE_TEMPLATE/, dependabot.yml, labels.yml, setup/*.json, actions/setup-rust/, workflows/}
├── .claude/{settings.json, hooks/, rules/, agents/ (canonical), skills -> ../.codex/skills}
└── .codex/{skills/ (canonical: design-review, adr), agents -> ../.claude/agents}
Migration from /home/paul/idaes-pse-rust-datafusion (new repo initialised in a fresh directory; the old tree is left untouched):
| Current | New | Action |
|---|---|---|
| proposal | docs/authoritative_design/proposal.md | front matter status: historical |
| blueprint rev 2, then rev 3 | docs/authoritative_design/blueprint.md | two seeding commits, tags design-rev2, design-rev3; revision table gains a git column; no -revN files from now on |
design_principles/, reviews/ | unchanged paths | skill contract |
four capability maps + evidence/ | docs/capability-maps/ (+ evidence/) | fix relative links in the two evidence READMEs; drop the self-declared superseded top-level apisurface-Cargo.toml/gen_rustdoc.sh; keep arrow_probe.rs |
tooling/dfarrow-apiex/ | same | ROOT from git rev-parse --show-toplevel / Path(__file__).parents[2]; verify/Cargo.toml path deps → external/; README anchors 55.1.0/59.3.0 |
build/, target/ | not committed | regenerable: just evidence-regen |
idaes-pse/ (2.13.0rc0), .worktrees/ (59.2.0/55.0.0) | external/ at the pinned tags | not submodules; tags read from Cargo.lock (arrow/datafusion) and the parity group (IDAES); a lint checks the script’s IDAES tag equals the pin |
.claude/ == .codex/ | canonical + symlinks | check_agent_config.py |
3. Rust workspace, toolchain, and supply chain
Cargo.toml (root). [workspace] resolver = "3", members = ["crates/*", "tests/governance", "tests/engine", "tests/conformance", "tests/lifecycle", "tests/structural", "xtask", "benches"], exclude = ["tooling/dfarrow-apiex", "tooling/dfarrow-apiex/verify", "docker"]. [workspace.package]: version = "0.0.1", edition = "2024", rust-version = "1.98.1" (== toolchain by governance test), license = "MIT OR Apache-2.0", repository, authors, publish = false (flipped per library crate at phase-1 exit; never for pse-py, xtask, benches, tests/*). [workspace.dependencies] carries every §3.1 pin exactly once with features fixed there: arrow family =59.3.0 (arrow with ipc, ffi, canonical_extension_types; arrow-ipc/arrow-cast default-features = false; parquet with arrow, async, object_store), datafusion family =55.1.0 (defaults only), object_store =0.13.2, pyo3 =0.29.x + pyo3-arrow =0.19.0, tokio =1.53.1 (rt-multi-thread, sync, macros, time), serde =1.0.229 (derive, rc), serde_arrow =0.15.0 (arrow-59), serde-saphyr =1.2.0, toml =1.1.6, winnow =1.0.4, syn =3.0.5/quote =1.0.47/proc-macro2 =1.0.107 (span-locations)/prettyplease =0.3.0, petgraph =0.8.3, num-dual =0.15.0, faer =0.24.4, blake3 =1.8.7 (rayon), rayon =1.12.0, tracing =0.1.44, thiserror =2.0.20, miette =7.6.0; dev/tooling pins (proptest, insta, criterion, tempfile, tracing-subscriber, cargo_metadata, clap, regex, ignore, bindgen, pkg-config, anyhow for xtask only) listed in tests/governance/tooling_deps.toml with reasons so the “every crate appears in §3.1” test can exempt them; feos, diffsol, egglog enter only with their consumer crate and phase. Patch versions not yet verified are pinned to whatever the seeded lockfile resolves. [workspace.metadata.pse]: blueprint path, validate-features = ["pse-relations/force-validate"], unsafe-allowlist = ["pse-ipopt-sys", "pse-backend-native", "pse-kernels-ext", "pse-py"], [workspace.metadata.pse.families] (arrow 59.3.0 over arrow*/parquet*; datafusion 55.1.0 over datafusion*; object_store 0.13.2; pyo3 0.29 minor-match over pyo3*) consumed by cargo xtask family-check. No [patch]/[replace] ever (governance test).
Lints ([workspace.lints], every crate lints.workspace = true). rust: unsafe_code = "deny" (the four allowlisted crates carry #![allow(unsafe_code, reason = "...")]), missing_docs, unreachable_pub, unused_qualifications, trivial_casts, rust_2018_idioms, missing_debug_implementations warn (-D warnings in CI), non_ascii_idents = "forbid"; rustdoc broken_intra_doc_links = "deny"; clippy all + pedantic + cargo warn (priority −1), multiple_crate_versions/module_name_repetitions/must_use_candidate/cast_precision_loss allowed, panic policy unwrap_used/expect_used/panic/unimplemented/todo/exit/mem_forget/print_stdout/print_stderr/dbg_macro deny, disallowed_methods/disallowed_types/disallowed_macros deny, undocumented_unsafe_blocks, multiple_unsafe_ops_per_block, allow_attributes_without_reason deny, float_cmp warn. clippy.toml: allow-{unwrap,expect,panic,dbg,print}-in-tests = true, avoid-breaking-exported-api = false, doc-valid-idents (DataFusion, PyO3, IDAES, Ipopt, MUMPS, …), disallowed-methods = the type-aware layer of the §24.1 bans (Field::extension_type, SessionConfig::set_str, SchemaLike::from_type/from_samples, IpcWriteOptions::try_with_compression; definition paths confirmed against the extracted facts at implementation), disallowed-types = [anyhow::Error] (xtask opts out at crate level).
Profiles. dev: debug = "line-tables-only", [profile.dev.package."*"] opt-level = 2 (arrow/datafusion/faer at −O0 make the suite crawl; float semantics unchanged); debugging (inherits dev, debug = "full"); release: opt-level = 3, lto = "thin", codegen-units = 16, panic = "unwind" (required by PyO3 and the Ipopt catch_unwind callbacks), debug = "line-tables-only", strip = "none"; profiling (inherits release, full debug, for samply); dist (inherits release, lto = "fat", codegen-units = 1; used by maturin --profile dist and release binaries). force_validate mechanism: it is an arrow feature, not a profile; pse-relations declares force-validate = ["arrow/force_validate"], never in defaults; every test invocation (justfile test/coverage, the CI composite action, cargo llvm-cov) passes --features pse-relations/force-validate explicitly (no alias, so workflows stay readable); a governance test asserts the feature exists and is named in validate-features.
Toolchain and cargo config. rust-toolchain.toml: channel = "1.98.1", profile = "minimal", components rustfmt, clippy, rust-src, rust-analyzer, llvm-tools; comment states nightly is never the default and lives only in tooling/dfarrow-apiex/. .cargo/config.toml: deliberately minimal — [alias] xtask = "run --quiet --package xtask --", [future-incompat-report] frequency = "always"; no linker override (rustc ≥ 1.90 already links x86_64 Linux with rust-lld; mold stays a per-user opt-in in ~/.cargo/config.toml rather than a bootstrap requirement), never target-cpu=native (FMA contraction would change float results between dev and CI), sccache only via RUSTC_WRAPPER in .envrc/justfile, no --locked key exists so every gate passes it explicitly; a governance test forbids linker =, target-cpu, [build] rustflags, [patch], paths = in this file. rustfmt.toml: style_edition = "2024", max_width = 100, field-init and try shorthand, Unix newlines; generated files carry #![rustfmt::skip].
Per-crate template. version/edition/rust-version/license/repository/authors/publish.workspace = true, [lints] workspace = true, deps via .workspace = true. Features from phase 0: pse-relations/force-validate; ipopt (default on) in pse-backend-native, pse-runtime, pse-py with pse-ipopt-sys optional so --no-default-features checks cleanly without Ipopt; pse-kernels-ext/{coolprop,feos} off; pse-mathir/egglog off (phase 4); pse-backend-native/diffsol off (phase 4).
deny.toml. [graph] targets = the five wheel targets, all-features = true; [advisories] yanked = "deny", unmaintained = "workspace", ignore = [] (every entry needs id, reason, owner, review date); [licenses] allow = MIT, MIT-0, Apache-2.0 (+LLVM exception), BSD-2/3, ISC, Zlib, 0BSD, BSL-1.0, Unicode-3.0/DFS-2016, CC0-1.0, CDLA-Permissive-2.0, MPL-2.0; [bans] multiple-versions = "deny", wildcards = "deny", allow-wildcard-paths = true, deny list = serde_yaml, uom, arrow-flight, arrow-avro, datafusion-spark, arrow-pyarrow, parquet-variant, parquet_derive, openssl-sys (each with the §3.1/§3.3 reason), skip seeded from the first cargo deny check bans run (the evidence lockfiles predict syn 1/2 beside syn 3, thiserror 1 under pyo3-arrow, hashbrown/rand/getrandom generations, zstd 0.13/0.14, …; every entry reason + owner + review date), skip-tree = [windows-sys]; [[bans.features]] crate = "datafusion" deny = ["serde", "avro", "parquet_encryption", "backtrace"]; [sources] unknown registry/git denied. Plus cargo audit and cargo shear in the same job.
.config/nextest.toml. Test groups solver (max-threads 2; pse-backend-native, lifecycle, solve_* tests; 300 s slow timeout) and serial (object-store/golden writers). default: fail-fast, 60 s slow timeout. ci: fail-fast = false, retries = 0 (a flaky test is a red test), 120 s slow timeout × 3, global-timeout = "2h", failure-output = "immediate-final", JUnit to target/nextest/ci/junit.xml. flaky-hunt: opt-in exponential retries with flaky-result = "fail" for diagnosis only. Every gate pairs cargo nextest run with cargo test --doc --workspace --locked (nextest does not run doctests).
xtask (logic) vs justfile (surface). The justfile is the one-line command surface in cost tiers; xtask owns anything needing Rust APIs, JSON, or cross-platform behaviour. xtask subcommands: codegen [--only relations|python|docs|bindgen] (calls pse_schema::codegen::generate; emits // @generated by pse-schema <fingerprint>, #![rustfmt::skip], #![allow(clippy::all, clippy::pedantic, missing_docs, reason = "generated")]; Python emitted final-form; bindgen from $IPOPT_DIR/include/coin-or/IpStdCInterface.h with an allowlist of the §18.3 functions, runs inside the solver container), codegen --check (git diff --exit-code plus untracked-file check on the four generated paths), family-check [--evidence docs/capability-maps/evidence/rust/*.lock] (one resolved version per family from cargo metadata --locked; packages shared with an evidence lock must match), governance, doc-lint (backticked a::b::c and [rustdoc:…] identifiers in docs/** resolve in the committed path indexes or in workspace rustdoc JSON generated with the apiex nightly pin; allowlist file), probe-host [--json] (thin wrapper over pse_runtime::probe_host()), release <version> (bump workspace version, git cliff, commit, tag). tests/governance/ (crate pse-tests-governance): no_shadow_structs, banned_patterns (regex layer of the §24.1 greps incl. config_options( under kernels, SERDE_ARROW:, {:?} as hash input in pse-ids), pins_match_blueprint, every_crate_registered, toolchain_matches_msrv, dependency_floors (every resolved package’s declared rust_version ≤ ours — the blueprint’s “pre-1.0 floors re-checked at every upgrade”), no_patch_tables, unsafe_allowlist, ffi_callbacks_catch_unwind, error_taxonomy (every pub enum *Error derives thiserror + miette::Diagnostic with §23.2 codes; no anyhow/miette::Result in crates/*), blake3_owner. benches/ = pse-benches (criterion, harness = false, one bench per §24.3 group, saved baselines per snapshot; CI smoke = cargo test --benches with no timing gate).
Justfile recipes (all gates --locked): env bootstrap, bootstrap-venv|quality|solvers|rust-tools|linters, doctor, doctor-json, fetch-external; discovery versions, lib-outline, metadata; local check, clippy, fmt-check, test, doctest, docs, codegen-check, family-check, governance, ci-fast (= fmt-check check clippy test doctest); pr policy (deny+audit+shear), coverage, bench-smoke, doc-lint, adr-lint, quality (python), parity, ci-pr (= ci-fast governance policy docs bench-smoke quality adr-lint); scheduled features-powerset, test-release, udeps, mutants-file, unsafe-surface, floors-latest; decisions adr-new, adr-index, adr-supersede, plan, register-check; mutating ([confirm]) fmt, codegen, snapshots-accept, release, solver-image, evidence-regen, labels-sync, gh-setup. scripts/bootstrap.sh --rust-only binstalls the exact CI tool set (nextest 0.9.143, deny 0.20.2, audit 0.22.2, shear, machete, llvm-cov 0.9.0, insta 1.48.0, hack 0.6.45, msrv, mutants, geiger, udeps, semver-checks 0.50.0, git-cliff, pinact, zizmor, taplo, typos, maturin 1.15.0).
Release/versioning. Single workspace version (0.0.x in phase 0, v0.1.0 at phase-1 exit), one v* tag per release on main; cargo-semver-checks from the first tag (informational on PRs, gating on PRs labeled release); changelog by git-cliff from Conventional-Commit squash titles (cliff.toml; types feat/fix/perf/refactor/docs/test/build/ci/chore/deps/adr/design, scopes = crate short names), title convention enforced by governance / pr-title; crates.io publishing tooling (release-plz vs cargo-workspaces) decided by ADR at phase-0 exit.
4. Solver container and native linkage
docker/solvers/ builds Ipopt releases/3.14.20 (tag commit 1e71ba4…, tarball sha256 43bddd6f…, corroborated by Homebrew and conda-forge), ThirdParty-Mumps releases/3.0.14 (MUMPS 5.9.1 + its patches), ThirdParty-ASL releases/2.1.0 (solvers-20241108), from tarballs verified against checksums.sha256; base ubuntu:24.04@sha256:<digest>. Choices inside the recipe: netlib reference BLAS/LAPACK (deterministic; no threaded BLAS), --without-metis like IDAES’s compile_solvers.sh so MUMPS ordering matches IDAES binaries and iteration counts stay comparable, no HSL ever (Ipopt 3.14’s hsllib option dlopens a local libhsl.so; probe_host records ma27/ma57/ma86/ma97 availability; verify hsllib behaviour at implementation), -O2 -fPIC, no -march=native; post-build test compiles hs071_c.c via pkg-config and runs bin/ipopt on a tiny .nl. Stages: solvers (runtime libs, headers, .pc), ci (+ build-essential, git, pkg-config, libclang for bindgen, uv with managed CPython 3.11/3.12/3.13 for the parity job; IPOPT_DIR=/opt/pse-solvers, PKG_CONFIG_PATH, LD_LIBRARY_PATH, PATH), dev (+ rustup pinned toolchain, cargo tool set, just, uv) used by .devcontainer/devcontainer.json. solvers-image.yml: build on PRs touching docker/solvers/**; on main build + push ghcr.io/paul-heyse/pse-solvers:{ipopt3.14.20-mumps5.9.1-asl20241108-r1, ci-<tree-hash>, dev-<tree-hash>} and open a PR updating the SOLVER_IMAGE@sha256 env in the workflows; weekly solvers-image-rebuild-check rebuilds without cache and compares library checksums (recipe reproducibility). LICENSES.md records Ipopt EPL-2.0, MUMPS CeCILL-C, ASL, netlib.
crates/pse-ipopt-sys: links = "ipopt"; build.rs emits link directives only (IPOPT_DIR or pkg_config ... atleast_version("3.14.0"); cargo:version= → DEP_IPOPT_VERSION recorded by probe_host; actionable error naming the container, brew install ipopt, or the conda env); bindings committed by xtask codegen --only bindgen. index_style = 0 assertion and catch_unwind per callback live in pse-backend-native.
Phased OS coverage: phase 1a Linux container gating; phase 1b macOS (Homebrew ipopt 3.14.20) and Windows (conda-forge ipopt 3.14.20 via micromamba, CARGO_TARGET_DIR=C:\t, shell: bash) non-gating until a green week, trajectory-parity tests filtered off-Linux; phase 2 run build.sh natively on macOS and under MSYS2 on Windows, cached by recipe hash, and build the NL external-function libraries in the same jobs (blueprint §26). Wheel linkage (bundle libipopt/MUMPS via auditwheel/delocate/delvewheel vs runtime libloading) is a phase-1 ADR + register row; phase-0 wheels build pse-py without the ipopt feature.
5. Python packaging, environments, and tests
Decisions. Distribution pse-arrow, import pse. One root pyproject.toml, no uv workspace, committed uv.lock. maturin backend, dynamic = ["version"] from crates/pse-py/Cargo.toml (workspace-inherited: one version authority). pyomo/pint/numpy are the pyomo extra, not core (pse.open/compile/solve are native; the adapter serves parity and ecosystem tools, §21.3); numpy/scipy otherwise only in the test group. pyrefly type-checks ([tool.pyrefly] in pyproject), ruff lints/formats, import-linter enforces module boundaries, REUSE enforces headers. Zero tolerance, no baselines.
pyproject.toml (key content). [build-system] requires = ["maturin>=1.15,<2"]; [project] name = "pse-arrow", requires-python = ">=3.11", license = "MIT OR Apache-2.0", license-files = ["LICENSES/*.txt"], dependencies = ["pyarrow==25.0.1", "attrs==26.1.0", "cattrs==26.2.0", "msgspec==0.21.1"] (§3.1 == rule; O1 below), [project.optional-dependencies] pyomo = ["pyomo==6.10.1", "pint==0.26.1", "numpy==2.5.3"]. [dependency-groups]: quality (ruff, pyrefly, import-linter, reuse, taplo, typos, pre-commit, maturin; all ==), test (pytest, pytest-cov, pytest-xdist, numpy==2.5.3, scipy==1.18.1), parity (includes test + "idaes-pse==2.12.0; python_full_version < '3.14'"), evidence (includes parity), docs (empty for mdBook; reserved), dev (quality + test). The marker keeps uv sync --group parity on 3.14 honest; if a future IDAES pin conflicts with the platform pins uv lock fails, which is the right signal. [tool.uv]: package = true, required-version = "==0.12.13", python-preference = "managed", default-groups = ["dev"], cache-keys on pyproject.toml, Cargo.toml, Cargo.lock, uv.lock, rust-toolchain.toml, crates/**/Cargo.toml, crates/**/*.rs; [[tool.uv.index]] name = "testpypi" with publish-url, explicit = true. [tool.maturin]: bindings = "pyo3", manifest-path = "crates/pse-py/Cargo.toml", module-name = "pse._native", python-source = "python", python-packages = ["pse"], locked = true, profile = "release", editable-profile = "dev", strip = true, auditwheel = "repair", sdist include of uv.lock, rust-toolchain.toml, REUSE.toml; wheel exclude of python/pse/tests/**, python/pse/parity/tests/**; abi3-py311 is a pyo3 feature in the crate, not a maturin key. [tool.ruff]: target-version = "py311", line-length = 88, explicit select = idaes-arrow’s set (E,W,F,I,B,C4,UP,SIM,RUF,PT,PERF,N,ARG,PTH,TRY,PLC,PLE,PLW,ISC) + ANN, TC, TID, D (google), S, T20, ERA, PGH, PIE, RET, SLF, FA, LOG, G, DTZ, PYI, NPY, FURB, FLY, INP, EXE, ICN, TD; ignore = ["ISC001", "TD002"]; format.exclude = ["python/pse/contracts/**"]; runtime-evaluated-base-classes = ["msgspec.Struct"] + attrs decorators; banned-api (TID251): typing.Any, cattrs.Converter/GenConverter/structure/unstructure (use pse.codec.converter()), pyarrow.register_extension_type (use register_all()), assert_units_consistent (use identify_inconsistent_units), json, tomllib (msgspec), pickle; per-file ignores for tests, generated contracts, codec, tools. [tool.pyrefly]: includes, search-path = ["python", "python/stubs"], python-version = "3.11", check-unannotated-defs = true, ignore-errors-in-generated-code = false, sub-configs replace-imports-with-any for idaes.*/pyomo.*/pint.* under parity/** and adapters/pyomo/** until typed stubs land in phase 1 (confirm key names with pyrefly dump-config; rely on venv auto-discovery rather than an OS-specific interpreter path). [tool.importlinter]: numpy/scipy only in pse._array, pse.parity.**, pse.tests.**; idaes, pandas, pydantic, sympy, networkx, matplotlib, click never outside pse.parity; pyomo/pint only in pse.adapters.pyomo.**, pse.parity.**, tests; pse.contracts depends on nothing in pse; layers pse.parity > pse.adapters | pse.authoring > pse.codec > pse.contracts. [tool.pytest.ini_options]: --strict-markers --strict-config --import-mode=importlib -ra --durations=100 --durations-min=2, xfail_strict = true, filterwarnings = ["error"] + commented allow-list policy (scoped entries with upstream issue links), required_plugins, markers unit|component|integration|performance|parity|golden(name); [tool.coverage] branch, source_pkgs = ["pse"], fail_under = 90, site-packages path mapping.
Package layout. python/pse/{__init__.py, py.typed, _native.pyi (hand-written; a test diffs it against dir(_native)), _build.py (BuildInfo msgspec struct), _array.py (only non-parity numpy importer; zero_copy_only=True; copy_reason mandatory), contracts/ (GENERATED: attrs classes, msgspec manifest structs, ten ExtensionTypes, enums, GENERATED.sha256), codec/ (converter factory with forbid_extra_keys; msgspec helpers; transform_error → findings), governance.py (import-time attrs.fields walk failing on Any/bare dict/bare list), authoring/, adapters/pyomo/, parity/ (+tests), tests/}. No from __future__ import annotations under python/pse (PEP 563 breaks the import-time Any lint); ast-grep enforces.
Build info. A pse-buildinfo crate (used by pse-catalog for the manifest’s toolchain.lockfile_hash and by pse-py) whose build.rs hashes Cargo.lock and uv.lock (blake3 for the manifest; sha256 twins for Python verification), plus rustc version, profile, git sha; pse.build_info() returns a frozen msgspec BuildInfo; BUILD_INFO.json is attached to releases; a test fails when the built extension is stale relative to the checkout’s lockfiles.
uv workflow. uv sync --locked --extra pyomo (platform dev on 3.14.7 from .python-version); uv run maturin develop --uv --release for a release-profile extension; MATURIN_PEP517_ARGS="--profile release" uv sync --reinstall-package pse-arrow in CI test jobs; parity env UV_PROJECT_ENVIRONMENT=.venv-parity uv sync --locked --extra pyomo --group parity --python 3.13; uv lock --check in pre-commit and CI; uv lock --upgrade-package <name> is the only sanctioned way to move a pin; uv sync --check feeds doctor.py. .envrc = idaes-arrow’s minus IDAES vars, plus PSE_GOLDEN_DIR and a guarded PATH_add /opt/pse-solvers/bin. Evidence probes run from the evidence group; the hand-maintained requirements-py31x.txt become uv export --frozen … outputs written by just evidence-regen.
Test plugins (root conftest.py, stdlib + pytest). (1) exactly one of unit|component|integration|performance per item, enforced at collection with one UsageError listing all offenders; (2) --performance with IDAES semantics; (3) --parity: without it parity items are deselected with a one-line summary; with it a session autouse fixture fails (never skips) unless Python < 3.14, idaes.__version__ == "2.12.0", and ipopt is on PATH; pytest.skip/importorskip under parity/** banned by ast-grep; (4) VerifyCleanup ported from IDAES and extended to fail when a test modifies a tracked file. Fixtures (python/pse/tests/conftest.py): golden(name) opens tests/golden/<name> read-only (stores are produced by cargo xtask golden; Python never writes them), registered_extension_types, no_numpy_on_import (subprocess), build_info_matches_checkout. Boundary tests per §24.1: test_forbidden_extra_keys, test_any_lint, test_extension_round_trip, test_nullable_ndarray_refusal, test_preflight_capability_backend (raises before ConcreteModel construction), test_manifest_fingerprint, test_native_stub_surface, test_versions_agree; parity test_00_preflight asserts probe_host reports Ipopt 3.14.x, mumps, ASL, IDAES 2.12.0 and SolverFactory("ipopt").available(False), and writes artifacts/probe_host.json.
Wheels and release. wheels.yml via PyO3/maturin-action@v1 (command: build, --release --out dist --locked, sccache: true): ubuntu-24.04 x86_64 and ubuntu-24.04-arm aarch64 at manylinux: 2_28 (pyarrow 25.0.1 ships only 2_28), macos-15 arm64 + macos-15-intel x86_64 (thin wheels), windows-2022 x64; assert every wheel tag is cp311-abi3; sdist via uv build --sdist then a PEP 517 install from the sdist in a fresh venv; verify-clean-install on 3.11 and 3.14 from a checkout. release.yml on v*: version gate (cargo metadata pse-py version == tag == every wheel filename; CHANGELOG.md heading exists) → wheels/sdist → PEP 740 attestations (astral-sh/attest-action) → uv publish --index testpypi --trusted-publishing always (environment test-release) → TestPyPI install smoke with retry → uv publish (environment release) → GitHub Release with dist/*, uv.lock, Cargo.lock, uv export snapshots (platform, parity), BUILD_INFO.json, probe_host.json, solver-image digest.
Pre-commit (default_install_hook_types: [pre-commit, pre-push]). ruff-format, ruff-check (no --fix); local language: system hooks from .venv/bin: taplo fmt –check, typos, reuse lint, uv lock --check, scripts/check_generated.py (sha256 manifest of python/pse/contracts/), ast-grep scan, actionlint, shellcheck; pre-push: pyrefly, import-linter, cargo fmt –check, cargo clippy. scripts/bootstrap.sh --linters-only fetches ast-grep/actionlint/shellcheck.
ast-grep rules (sgrules/). Python: no-future-annotations, to-numpy-only-in-array-boundary, no-direct-native-import, identify-not-assert-units, no-skip-in-parity, no-zero-copy-false-without-reason. Rust: arrow-through-datafusion, pyarray-new-panics, plus structural forms of the §24.1 bans.
6. CI job graph and required checks
Workflow files: rust.yml, rust-scheduled.yml, python.yml, wheels.yml (workflow_call + PR paths), parity.yml, solvers-image.yml, release.yml, docs.yml, governance.yml, register-review.yml, repo-hygiene.yml; composite .github/actions/setup-rust (rustup with --default-toolchain none → rustup show installs the pinned toolchain; Swatinem/rust-cache with shared-key per job kind and save-if on main; taiki-e/install-action with exact tool versions from env). Common: permissions: contents: read at top, widened per job; concurrency per PR/ref with cancel-in-progress on PRs only; CARGO_INCREMENTAL=0, CARGO_TERM_COLOR=always, RUST_BACKTRACE=1, tool versions as env vars, SOLVER_IMAGE=ghcr.io/paul-heyse/pse-solvers:ci-<hash>@sha256:<digest>; no RUSTFLAGS in CI; sccache off in CI; every action pinned by commit SHA with a version comment and kept current by Dependabot github-actions + pinact; zizmor + actionlint in repo-hygiene.
Check context (required on main) | Job | Runs |
|---|---|---|
rust / fmt | fmt | cargo fmt --all --check; taplo fmt --check |
rust / clippy | clippy (solver container) | cargo clippy --workspace --all-targets --locked -- -D warnings; again with --no-default-features |
rust / test | test (solver container) | cargo nextest run --workspace --locked --profile ci --features pse-relations/force-validate; cargo test --doc --workspace --locked; cargo test --benches -p pse-benches --locked; JUnit artifact |
rust / codegen-diff | codegen-diff (container: Ipopt headers + libclang) | cargo xtask codegen --check |
rust / family-check | family-check | cargo xtask family-check --evidence docs/capability-maps/evidence/rust/*.lock; cargo metadata --locked |
rust / deny | deny | cargo deny check; cargo audit; cargo shear |
python / lint | lint | uv sync --locked --group quality --no-install-project; ruff format –check, ruff check, pyrefly, import-linter, reuse lint, typos, taplo, uv lock --check, check_generated.py |
python / test | test (3.11 and 3.14) | MATURIN_PEP517_ARGS="--profile release" uv sync --locked --extra pyomo --group test --no-default-groups; pytest -m "unit or component" -n auto --cov |
python / parity | parity (container ci stage, 3.13 on PRs; 3.11–3.13 nightly) | installs the Linux x86_64 wheel artifact, uv run --no-sync pytest --parity |
docs / build | build | adr.py index --check; mdbook build docs; lychee --offline on the book; pages artifact |
governance / adr-lint | adr-lint | adr.py lint; check_register.py --lint; needs-adr label rule; IDAES tag in fetch-external.sh == parity pin |
governance / pr-title | pr-title | amannn/action-semantic-pull-request (types/scopes from cliff.toml) |
Non-required but run on PRs: rust / docs (RUSTDOCFLAGS=-Dwarnings cargo doc --no-deps), rust / doc-lint (paths-filtered), rust / coverage (cargo llvm-cov nextest … --lcov, Codecov OIDC, informational in phase 0), rust / semver (from the first tag; gating only with the release label), rust / msrv-floors (cargo hack check --rust-version only when Cargo.lock changed), rust / test-macos and rust / test-windows (phase 1b, continue-on-error until a green week), wheels / * (paths-filtered), governance / reuse, governance / agent-config, repo-hygiene / *. docs / deploy on main via actions/deploy-pages. rust-scheduled.yml weekly (cron 0 6 * * 1, each job with an owner and a documented response in docs/dev/ci.md): feature powerset (cargo hack --feature-powerset --depth 2), test-release (--cargo-profile release), udeps (pinned nightly), mutants on pse-ids/pse-mathir (continue-on-error, artifact), unsafe-surface (geiger), full doc-lint, floors-latest (cargo update --dry-run + floors against a temp lock), solvers-image-rebuild-check. Miri is dropped (no unsafe outside FFI crates); re-adoption trigger = first non-FFI unsafe (register row). codecov.yml: informational, after_n_builds = number of uploads.
Required-check staging: phase-0 set = the six rust / *, docs / build, governance / adr-lint, governance / pr-title; python / * added by a just gh-setup re-run once the Python package first reports (a check that never reports blocks every PR).
7. Decision records and the design process
Two directories, two lifecycles. docs/adr/NNNN-kebab.md records decisions (immutable after acceptance except status fields; changed by superseding). docs/plans/NN-kebab.md records how work is sequenced and verified (living until done, then an appended ## Outcome with “a mistake made and corrected” and “deviations, deliberate”; “when the code and a plan disagree, the code is what runs”). Plans list the ADRs they implement in front matter.
ADR template (docs/adr/template.md): MADR 4 sections plus charter §H fields in YAML front matter: id, title, status (proposed | accepted | rejected | deprecated | superseded), date, deciders, level (decision | should-deviation | must-gap — a must-gap narrows scope, never claims compliance), principles (DM-xx), blueprint (sections governed), review (path#finding or not-required: <reason>), evidence (charter §D label), supersedes/superseded-by, revisit (observable trigger), verification (test/lint/benchmark that shows the decision holds). Body: Context · Scope · Drivers · Options · Outcome (Consequences, Compensating controls, Confirmation) · Pros/cons · More information · Status history (append-only). A small deviation fills each section in one line.
Tooling: scripts/adr.py (stdlib, ~200 lines): new, lint (file-name pattern, contiguous numbering, required keys, enums, DM-\d{2}, blueprint § anchors resolve to headings in blueprint.md, review path exists, symmetric supersession, immutability of accepted ADRs vs origin/main, register rows valid), index [--check] (regenerates docs/adr/README.md and the ADR block in docs/SUMMARY.md), supersede. just adr-new|adr-lint|adr-index|adr-supersede|plan|register-check. A small .codex/skills/adr/SKILL.md navigator explains when an ADR is required and how to fill §H fields. adr-tools rejected (unmaintained shell, no §H fields).
Decision-PR rule (GOVERNANCE.md): an ADR enters or changes status only in a PR labeled adr, titled adr: ADR-NNNN <title>; the same PR (or a named follow-up design: PR) amends blueprint.md with a revision row and an inline > Decision: ADR-NNNN at the governed section; maintainer merge is the approval; a PR may merge with status: proposed only if also labeled needs-review.
When an ADR / review is required:
| Change | Needs |
|---|---|
| Alters D1–D14; adds/removes a crate; adds/drops/majors a dependency family; changes hashing contract, Python boundary contract, metadata conventions, or commit contract; any SHOULD deviation; governance changes | ADR + design review (needs-review; verdict Accept/Accept-scoped before accepted) |
| New relation family, pass, kernel contract, or backend binding within an accepted decision; small local SHOULD deviation; moving the parity pin; a deferred trigger fires | ADR (short); review at maintainer discretion |
| Bug fixes, refactors within contracts, tests, docs wording, patch bumps inside a pinned family, tooling | neither; ordinary PR with the evidence field filled |
Blueprint convention: one file, revised in git; section numbers are stable citation targets (insert §14.3.1, never renumber); §0.1 states “this file is authoritative; ADRs record why; reviews are evidence, not authority”.
Register (docs/adr/register.md): rows R-NN | item | ADR | trigger | check | owner | last-checked | next-check | status. Initial rows: salsa (§14.3); LogicalPlan::Extension (§14.2); datafusion-tracing/instrumented-object-store release matching the pinned engine (crates.io API check); datafusion-ffi; egglog determinism (phase 4); feos-core tracking num-dual 0.15 (cargo info check); pre-1.0 crate floors per upgrade (dependency_floors); object_store multi-writer commit; Ipopt/HSL/MUMPS recipe per platform (phase 1 exit); wheel solver linkage (phase 1 ADR); datafusion-proto byte stability per bump; Miri re-adoption; crates.io release tooling (phase 0 exit); Renovate fallback; Python API docs tool; project board at second contributor; Windows symlink policy. register-review.yml (monthly cron, issues: write) runs check_register.py --due, executes automatable checks, and opens/updates one Register review YYYY-MM issue.
Backfill ADRs in the seeding PR (short §H records citing the blueprint; evidence mostly Proposed/Interface-checked): 0001 ADR process; 0002 name/license/prefix/PyPI distribution; 0003 clean-room relationship + IDAES 2.12.0 parity pin; 0004–0017 D1–D14; 0018 family-wide = pins, lockfile, cargo deny, MSRV = pinned toolchain (amends §3.1); 0019 plan fingerprint is evidence, not a memo key; 0020 salsa deferred; 0021 serde-saphyr; 0022 num-dual 0.15 + FeOs conditional; 0023 blake3 derive_key in pse-ids + pse.canon.v1; 0024 pyo3-arrow over arrow-pyarrow; 0025 LogicalPlan::Extension deferred; 0026 uom/arrow-flight dropped, pint validates never defines; 0027 commit contract = P2 validity; 0028 solver acquisition (source-built Ipopt+MUMPS, HSL probed with declared fallback); 0029 FairSpillPool with explicit limit; 0030 canonical float hashing, no Float64 keys; 0031 generated sources committed and diff-checked; 0032 external checkouts not vendored; 0033 one blueprint file, stable section numbers; 0034 governance (single maintainer, PR-only, squash-only, linear signed main); 0035 Dependabot with family groups; 0036 mdBook on Pages; 0037 remaining deferred-with-trigger items; 0038 workspace layout additions (crates/pse-ipopt-sys, pse-buildinfo, xtask/, benches/, tests/* crates; amends §3.2).
Doc conventions: new files lowercase kebab-case (exceptions: the three UPPER_SNAKE principle files, root governance files, the skill’s design_review_{slug}_{date}.md); YAML front matter on ADRs, plans, capability maps (pins:, regenerated:), authoritative design (status:, revision:); citations blueprint §14.3, ADR-0020, DM-33, G4; charter §D evidence labels mandatory in ADR evidence:, the PR template, plan Verification sections, register rows, with Tested/Measured naming the test/benchmark and conditions; generated docs carry <!-- @generated by pse-schema; do not edit -->.
8. GitHub setup
Declared config in .github/setup/*.json, applied idempotently by just gh-setup (gh api PUT/PATCH; rulesets updated by name; gh ruleset in gh 2.45 is read-only).
gh repo create paul-heyse/pse-arrow --public --disable-wiki --description … --homepage https://paul-heyse.github.io/pse-arrow; push seeded history + tags.gh repo edit: defaultmain, squash merge only (title = PR title, body = PR body), delete branch on merge, allow update branch, Discussions on, wiki/projects off; topics via PUT.- Security: vulnerability alerts, automated security fixes, private vulnerability reporting, secret-scanning push protection.
- Ruleset
main(~DEFAULT_BRANCH): deletion, non_fast_forward, required_linear_history, required_signatures (squash commits are GitHub-signed; SSH signing documented for bypass pushes), pull_request (0 approvals — sole maintainer; dismiss stale; require thread resolution; squash only), required_status_checks (strict; the §6 list, staged). Bypass: RepositoryRole adminalways(fallbackUserid if the role id is rejected); every bypass requires agovernancefollow-up issue. Rulesettagsonrefs/tags/v*: creation/update/deletion/required_signatures. Branch naming documented, not enforced:<kind>/<kebab>. - Labels (
.github/labels.yml,just labels-sync):kind/{bug,feature,design-decision,parity-gap,library-upgrade,docs,chore,governance},area/{schema-registry,identity-catalog,physical-typing,materials-properties,math-ir,compiler-passes,rules-inference,numerics-solvers,backends,python-boundary,diagnostics,docs,governance,ci,tooling},phase/{0-foundations,1-slice-a,2-slice-b,3-slice-c,4-breadth},adr,needs-review,needs-adr,register,release,blocked,dependencies. Milestones per delivery phase 0–4. - Issue forms:
bug-report.yml,design-decision.yml,parity-gap.yml(IDAES reference, tolerance, which side is believed correct and why),library-upgrade.yml(family, capability-map regeneration, register rows);config.ymlwith blank issues off and links to Discussions/SECURITY. - PR template: Summary;
Implements: ADR-NNNN/Plan:/Closes:; Evidence (§D label + named tests/benchmarks); checklist (ADR referenced or “no ADR needed because…”, generated sources regenerated, lockfiles updated deliberately, capability map regenerated if a family moved, register rows touched, no edits under generated/external/build); legal acknowledgement (MIT OR Apache-2.0) + clean-room attestation. - Root files:
SECURITY.md(private reporting, 7-day acknowledgement, FFI/untrusted-document scope),CODE_OF_CONDUCT.md(Contributor Covenant 2.1, contact paul@heyse.io),CONTRIBUTING.md(environment, repo map, change workflow, when an ADR/review is required, the “proves / does not prove” table, generated code and pins, docs conventions, licensing, release pointer),GOVERNANCE.md,LICENSE-MIT/LICENSE-APACHE+LICENSES/for REUSE, SPDX two-line header on every authored source file (SPDX-License-Identifier: MIT OR Apache-2.0,Copyright (c) 2026 Paul Heyse; the generator emits it) enforced byreuse lint(REUSE.tomlcovers headerless types by glob),CITATION.cff(references the IDAES paper as the parity reference),CHANGELOG.mdvia git-cliff. dependabot.yml: cargo monthly,versioning-strategy: increase, groupsarrow-family(arrow*,parquet*),datafusion-family(datafusion*,object_store),pyo3-family(pyo3*,numpy),codegen(syn,quote,proc-macro2,prettyplease; PR must carry a regeneration commit),numerics(num-dual,faer*,petgraph,diffsol,feos*,egglog); uv monthly with groupsquality-tools,runtime,ignore: idaes-pse; github-actions weekly grouped; docker for the solver base image. Renovate is the fallback (register row).- Environments
test-release(branch policymain) andrelease(tag policyv*, reviewer = maintainer,prevent_self_review: false); PyPI trusted publisher forrelease.yml; Pages source = workflow.
9. Agent working environment
AGENTS.mdcanonical (~150 lines): identity + clean-room rule; “start here” (just doctor→just bootstrap→just --list; “just --listis the contract”); prime directives (baseline is zero; never edit generated dirs, fix the generator; one authoritative declaration per meaning; label every claim with §D vocabulary; ADR before changing a D-decision or a family; tools from.venv/pinned toolchain, never$PATH; report failure counts with baseline); repository map; where authority lives (blueprint, ADR index, register,Cargo.tomlheader,[dependency-groups], capability maps viajust lib-outline); invariants; gotchas seeded from real incidents (umbrella pin does not pin the family; deleted extraction made rustdoc markers unreproducible; PyPIpsetaken;/home/paulpaths; YAML/Actions/PowerShell items from idaes-arrow); “what each command proves / does not prove” table; documentation and decision rules; off-limits paths; runtimes.CLAUDE.md: real file starting@AGENTS.md, then settings/hooks/rules summary, “use plan mode when the change touches D1–D14, adds a crate or dependency, touches codegen or the Python boundary”, subagents, skills, “plans go indocs/plans/, never~/.claude/plans”..claude/settings.json: tool-level allow (Bash,Edit,Write,Read,WebFetch,WebSearch) so development never waits on approval, noasklist (it is inert in an unprompted session, so a real gate belongs indenyor the edit hook, and the justfile is the single authority on the command surface); deny force-push and Edit/Write under.git,target,build,external,docs/generated,crates/**/generated; envUV_PROJECT_ENVIRONMENT=.venv; hookssession-start.sh(emitsjust doctor),guard-protected-paths.sh(PreToolUse, exit 2; also blocks accepted ADRs anddocs/authoritative_design/**unlessPSE_DESIGN_EDIT=1),format-after-edit.sh(PostToolUse; cargo fmt / ruff format / taplo fmt; never fails). No per-editcheck-fasthook..claude/rules/{rust,python,docs,decisions,generated,ci}.mdwithpaths:globs. Keep the nine agents and the design-review skill; add theadrskill;just lint-agentsrunscheck_agent_config.py..editorconfig(idaes-arrow’s; 88 for.py, 100 for.rs),.gitattributes(* text=auto eol=lf;*.arrow/*.parquet/*.feather/*.ipcbinary;*.nl/*.solLF;linguist-generatedfor lockfiles,docs/generated/**,crates/*/src/generated/**,*.snap, evidence lockfiles/logs/outputs),.gitignore(/target/,/build/,/external/,/docs/book/,/dist/,.venv*/, caches,python/pse/_native*.so,*.pyd,.envrc.local,.claude/settings.local.json, tooling targets,/.worktrees/,/idaes-pse/,artifacts/).
10. Docs site
mdBook (latest 0.5.x pinned exact at implementation; installed by cargo-binstall). docs/book.toml (src = ".", site-url = "/pse-arrow/", repo/edit URLs) + docs/SUMMARY.md (overview, relationship-to-idaes, authoritative design, decisions index + register + generated ADR block, plans, design reviews, capability maps + evidence READMEs, generated reference, contributing/governance/security includes). docs / build never needs the Rust extension (docs/generated/ is committed); rustdoc output is copied into the book when the rust / docs artifact exists. Python API docs (mkdocstrings) revisited via register row when python/pse grows a public surface.
11. Execution order and verification
Each step is one PR after the seeding commits (a fresh repo cannot require checks that do not yet exist, hence the staged rulesets in §6/§8).
- Init and seed history. New directory (e.g.
~/pse-arrow),git init -b main; commit 1: design principles + proposal + blueprint rev 2 + first review (tagdesign-rev2); commit 2: blueprint rev 3 + second review (tagdesign-rev3); commit 3: capability maps + evidence (links fixed) +tooling/dfarrow-apiex(paths fixed). - Root scaffolding. Licenses,
LICENSES/,REUSE.toml, README, relationship doc, CONTRIBUTING/GOVERNANCE/SECURITY/CoC/CITATION/CHANGELOG,.editorconfig,.gitattributes,.gitignore,.taplo.toml,typos.toml, justfile,.envrc,scripts/doctor.py,scripts/bootstrap.sh,scripts/fetch-external.sh. - Rust workspace skeleton. Root
Cargo.tomlwith every pin, toolchain/cargo/rustfmt/clippy/deny/nextest files; 23 crates +pse-ipopt-sys+pse-buildinfo+xtask+benches+ fivetests/*crates as minimal libs with full manifests;cargo generate-lockfile, seeddeny.tomlskips from the firstcargo deny check bans;cargo xtask family-check --evidence; the governance tests that run on an empty workspace (pins match blueprint, every crate registered, toolchain matches msrv, dependency floors, no patch tables, unsafe allowlist, blake3 owner, error taxonomy). - Python skeleton.
pyproject.toml,uv.lock,.python-version, rootconftest.py, minimalpython/pse(init,_native.pyi,_build.py, governance lint), minimalpse-pypymodule withbuild_info;uv sync --locked;test_versions_agree,test_native_stub_surface,build_info_matches_checkout; pre-commit config; sgrules. - Solver image.
docker/solvers/, local build, push to GHCR,.devcontainer;pse-ipopt-sysbuild.rs against the image;cargo xtask codegen --only bindgeninside the container;probe_hostsmoke viacargo xtask probe-host. - Decision records and docs.
scripts/adr.py, template, 38 backfill ADRs, register,docs/plans/README.md+ plan 01 (this plan, transcribed),book.toml/SUMMARY.md,adrskill, blueprint §0.1 authority note +gitcolumn + ADR citations for the amendments (§3.1 MSRV, §3.2 layout additions). - Agent environment.
AGENTS.md,CLAUDE.md,.claude/settings.json, hooks, rules, symlinks,check_agent_config.py. - CI. Workflows, composite action,
codecov.yml,dependabot.yml, labels, issue forms, PR template,.github/setup/*.json,cliff.toml;pinact run+zizmorclean. - GitHub.
gh repo create, push,just gh-setup(phase-0 ruleset), environments, Pages, milestones, labels; PyPI pending publisher. - Verification pass (recorded in plan 01’s Outcome):
- fresh clone →
just doctorreports what is missing →just bootstrap→just doctorclean →just ci-fastandjust ci-prgreen locally;direnventry touches no network. cargo xtask family-checkfails on a deliberately mixed family in a scratch branch;rust / codegen-difffails on a hand edit undercrates/pse-relations/src/generated/;governance / adr-lintfails on an ADR with a bad§anchor and on an edit to an accepted ADR;python / lintfails on atyping.Anyfield; a parity run on 3.14 fails (not skips) the pre-flight.- a direct push to
mainis rejected by the ruleset; a PR without a conventional title failsgovernance / pr-title;just gh-setupre-run is a no-op. python / parityin the container findsipopt3.14.20 and passes the pre-flight;solvers-image-rebuild-checkreproduces library checksums.docs / build+docs / deploypublish the book;lycheefinds no broken internal links.wheels.ymlproducescp311-abi3wheels on all five targets andverify-clean-installpasses on 3.11 and 3.14.
- fresh clone →
Open questions for the maintainer (recommended default in bold)
- O1
==pins in[project.dependencies]of the published wheel (blueprint §3.1 rule; hostile to consumers): keep through phases 0–3; revisit by ADR at the first PyPI release. - O2 Wheel solver linkage (bundle Ipopt/MUMPS vs runtime
libloading): phase-1 ADR; phase-0 wheels ship without the native backend. - O3 Reserve
pse-*names on crates.io now: no; publish at phase-0 exit. - O4 Two arrow/datafusion Dependabot groups vs one: two (
family-checkguards). - O5 Backfill all 38 ADRs in the seeding PR vs D1–D14 only: all (short records).
- O6 Free-threaded (
cp314t) wheels: defer; nightly non-blocking job later. - O7
pyo3-stub-genfor_native.pyivs hand-written + surface test: hand-written now. - O8 CoC contact
paul@heyse.iovs a Discussions category: email.
Outcome and remaining runtime activation
What was built
Implemented: setup completion is delivered through PR #1. The shared framework has nine repository-specific Claude roles and generated native Codex definitions, canonical skills with runtime aliases, and shared session/edit/ format hooks. AGENTS.md routes both runtimes to the same scoped rules. Pyrefly is the only configured Python type checker; the unused alternative was removed from the quality group, lockfile, environment and Dependabot configuration.
Tested: all counts below use a zero-failure baseline. Local Rust qualification
uses toolchain 1.98.1; Rust test commands explicitly enable
pse-relations/force-validate.
| Command / condition | Result and limit |
|---|---|
just ci-fast, dev | 23 Rust tests passed, zero skipped; doctests passed (phase-zero crates contain no doctest cases) |
just test-release, release | 23 Rust tests passed, zero skipped |
IPOPT_DIR=<prefix> just features-powerset, libraries extracted from the pinned CI image | 47 depth-two feature configurations and 31 no-default-feature checks passed |
just ci-pr, local | Composite Rust, governance, policy, docs, benchmark smoke, Python quality and 36 Python tests passed |
just quality | Ruff, Pyrefly, import contracts and repository checks passed; ten setup fixtures passed |
just parity-container, pinned dev image, Python 3.13 | 41 existing package/preflight tests passed; numerical modeling parity is not established |
Fresh-clone just bootstrap, just doctor, just quality | Relative and absolute venv selection passed; an absolute path containing a space was exercised |
strace -f -e connect direnv exec . true | Zero IPv4/IPv6 connections during directory entry |
just solver-rebuild-check | No-cache source build reproduced published shared-library checksums byte for byte |
just gh-setup-check twice after re-applying setup | Zero configuration differences; main and tag rulesets remain unique and active |
Tested: disposable fixtures rejected a staged generated-file edit (just codegen-check),
a mixed family (just family-check), an invalid blueprint citation (just adr-lint),
missing/double pytest cost markers, and parity on Python 3.14. The setup fixtures also
reject accepted-ADR edits against a base ref; existing Python tests reject typing.Any.
A temporary non-conventional title on PR #1 failed governance / pr-title; restoring
the conventional title restored the check. No negative fixture remains in the source tree.
Interface-checked: a fresh Codex runtime listed all nine custom roles; the app server listed both skills and all three hooks without configuration errors. The hooks are reported as untrusted, so automatic execution is not certified. Shared hook behavior is covered directly by the setup fixtures.
Mistakes found and corrected
Implemented: the previous Python parity job selected no environment when installing
the wheel; it now names .venv-parity. Coverage uploads now run on a host runner with
its verification tools. Windows fixture paths are normalized, directory skill aliases
are excluded from sdists, and benchmark smoke selects the relations crate when enabling
its validation feature. Doctor reads package metadata because invoking a tool wrapper’s
--version attempted network access. Dependabot’s Cargo strategy is auto; GitHub
rejected the earlier increase value. The wheel action’s explicit stable override
was removed so it reads the pinned toolchain; clean installs assert the embedded compiler
version matches that pin. Full distribution qualification is invoked manually or at release time; a passing
development PR does not claim packaged-artifact acceptance.
Deliberate boundaries and handoff
Implemented (maintainer clarification): full wheel and sdist builds are removed
from ordinary PR triggers and required parity dependencies. Automatic Python tests
and parity use editable dev-profile builds. just py-sync, just py-test and
just quality are the local development loop; just wheels-check <ref> explicitly
requests the five-platform distribution matrix. Release workflows still qualify
artifacts before publishing. Packaging completion is not a development merge gate.
Implemented (maintainer clarification): committing and pushing do not require
native builds or full test suites. Local Git hooks retain static checks; Clippy is
available through just clippy and remains a GitHub check. just ci-pr and parity
recipes are optional local validation. Required GitHub checks continue to gate merge.
Implemented: source/configuration parity is complete; this plan stays in progress
until the remaining runtime activation is verified. In Codex, review and trust the three
project hooks with /hooks, then start a fresh session and exercise an ordinary edit
and a protected-path rejection. In Claude Code, accept the repository trust dialog and
repeat the plan-scout smoke check when the account limit permits: the attempted live
check returned HTTP 429 with the existing monthly spend-limit message.
Interface-checked: local commits carry SSH signatures, but GitHub reports the local
key as unknown. Registering that public signing key needs the account’s
admin:ssh_signing_key scope, which the current credential does not have. The squash
merge uses GitHub’s verified signature; no admin bypass is used to waive required checks.
The declared admin-role bypass remains, so an admin credential cannot prove ordinary
contributors’ direct pushes are rejected. No test push to main is attempted.
Implemented: real model generators, API doc lint, numerical parity, automated solver
pin PR credentials, and package publication retain their existing phase boundaries.
The API-doc job reports its R-20 prerequisite explicitly while indexes are absent.
Generic secret scanning is unavailable for this personal repository; supported secret
scanning and push protection are enabled (see .github/setup/README.md). No registry
publisher is activated, package uploaded, or release tag created by this completion pass.
02 — Blueprint revision 5 contracts
title: Reconcile blueprint revision 5 with library-backed contracts status: done date: 2026-09-13 adrs: [ADR-0039, ADR-0040, ADR-0041, ADR-0042, ADR-0043, ADR-0044, ADR-0045, ADR-0046, ADR-0047, ADR-0048] phase: 0
Blueprint revision 5 contract reconciliation
Context
The revision-4 review identified thirteen contract findings and nine library opportunities. The user authorized updating the blueprint, decision records and related documentation. This plan delivers that design amendment and an implementation handoff. Its completion does not mean the proposed compiler or solver contracts are built.
The reviewed baseline is revision 4 at commit
fb0717d9151ada4884bd7ccb5efc27422bb92f7a; the historical review and its
reproducible characterization receipts
remain evidence for that baseline. The current architecture is
blueprint revision 5, with the bounded
follow-up review.
Decisions
All new records have Proposed evidence and status: proposed. The accepted
records’ arguments are preserved; supersession changes only lifecycle fields and
status history. No dependency pin or family is moved.
| New record | Replaces | Decision / review coverage |
|---|---|---|
| ADR-0039 | ADR-0008 | Active recursive admission, lossless head conversion and complete quantity algebra; R4-01/05/08, L1/L6 |
| ADR-0040 | ADR-0011 | Demand seeds before realization and named immutable stage outputs; R4-06 |
| ADR-0041 | ADR-0017 | Complete stage keys, candidate absence and separate bound-value refresh; R4-07 |
| ADR-0042 | ADR-0020 | Whole-stage reuse first; R-22 before finer reuse and R-01 before salsa |
| ADR-0043 | ADR-0012 | Units, effects, missing input, mandatory failure outcomes and actual derivative/backend bindings; R4-02/10, L2/L6 |
| ADR-0044 | ADR-0019 | Noncanonical plan evidence outside semantic identity and reuse; R4-12 |
| ADR-0045 | ADR-0023 | Canonical v2, recursive null normalization, membership, physical checksums and publication; R4-03/04 |
| ADR-0046 | ADR-0029 | Shared runtime and fallible reservations for accounted platform buffers; R4-13, L5 |
| ADR-0047 | ADR-0014 | Ordered numerical semantics, true branch execution and safe buffer ownership; R4-02/09, L2/L7 |
| ADR-0048 | Supplemental | Shared predicates, aggregate/unnest and explicit-schema readers; R4-11, L3/L4/L7/L8/L9 |
The implementation starts with the simpler compiler from the review: complete stage keys and a bounded canonicalizer. Changed semantic lineage stays in the stage snapshot; equal relation storage does not authorize skipping a dependent stage. No per-instance minimal invalidation or automatic whole-stage backdating is promised. This is the smallest trustworthy initial granularity, not a measured claim that finer reuse would be slower.
Plan
Documentation deliverables
- Amend the blueprint in place, retain stable section numbers, and add the revision row and governed-section decision markers.
- Allocate ten proposed ADRs and record nine symmetric supersessions without rewriting accepted arguments.
- Reconcile all four capability maps’ current recommendations, gates and open items while retaining original library evidence conditions.
- Update the deferred register: R-01/R-11 and new R-22–R-25.
- Provide the bounded follow-up design review and the implementation sequence below; update navigation and stale scaffold citations.
- Complete the documentation/governance verification record below.
Implementation handoff — Proposed, not executed by this plan
Each packet implements the cited blueprint contract. Test identifiers below are proposed fixture names, not claims that those tests already exist. The owning implementation change should place them in the indicated existing test workspace and run its normal repository recipes. No newly added framework is needed.
| Order / packet | Depends on | Responsibility and consumer | Required acceptance before the next dependent packet |
|---|---|---|---|
| A — Registry and operation contracts | Normal maintainer decision process | pse-schema, pse-relations, pse-quantity, pse-mathir: admission, snapshot classes, ports, quantity compositions, WeightedMean, numerical/kernel outcomes; blueprint §4, §6–§8 | Generated contracts and supported/rejected physical-type fixtures agree; generator output remains the sole mechanical declaration |
| B — Bounded identity and local publication | A; reservation interface from E | pse-ids, pse-catalog: v2 canonical bytes, complete membership, physical encoding checksums and conditional refs; blueprint §5/§20 | Canonical metamorphic fixtures and corrupt/interrupted publication fail safely; no v1 hash relabeling |
| C — Demand, stage graph and rule execution | A–B | pse-authoring, pse-rules, pse-compiler: P3 demand seeds, finite P6 closure, explicit stage ports, shared predicate evaluation and aggregate/unnest; blueprint §9.6/§14 | Ordinary semantic admission, complete pushdown oracle, expansion reference and missing/late demand rejection |
| D — Complete stage reuse and plan evidence | C | pse-compiler, pse-rules, pse-catalog: all input/absence/policy/binary dependencies, immutable outputs, fresh lineage and noncanonical diagnostics; blueprint §14.3–§14.4/§20.2 | Every supported dependency mutation matches clean compilation; encoded plan variation cannot change semantic keys |
| E — Resource ownership and native preparation | A; allocation interface precedes B’s writers | pse-runtime, pse-numerics, kernel/backend crates: shared RuntimeEnv, reservations, safe Arrow owners and actual branch regions; blueprint §14.3/§18 | Budget/cancellation release, borrowed-slice lifetime and guarded residual/derivative fixtures; integration with B/C before their acceptance |
| F — Backend and Python conformance | A–E | Native/NL/Pyomo bindings and pse-py: complete kernel contracts, numerical-policy refusal, affine qualification and coarse streams; blueprint §18/§21 | Each advertised route has a real conformance fixture; excluded-branch and invalid/missing outcomes remain distinct |
| G — Observation import and measured refinements | A–F as needed by the actual consumer | pse-authoring observation reader; existing Arrow CSV/JSON interfaces via deliberately enabled pinned features; later scan/transfer optimization | Explicit schema, fields, units and target tests first. R-22–R-25 require separate equivalence and end-to-end measurements before optional adoption |
Implement E’s small reservation/ownership interface with A, then its native execution body after D. This avoids treating memory accounting as a later retrofit to B’s allocations. Arrow CSV/JSON reader availability in the library does not mean their features are enabled in the current direct Arrow dependency; G must wire only the required existing-family features through workspace policy.
| Proposed fixture / owner test workspace | Required cases and observable result |
|---|---|
semantic_admission_paths — tests/conformance, tests/engine | Wrong storage, unknown/malformed/versioned/nested extension metadata, ordinal targets/ranges; ordinary query/provider/import/output paths reject before assuming validity. Direct factory invocation is only a positive control. |
head_conversion_exactness — tests/engine | Fractional integer cast, large integer Float64 loss, same-storage wrong units/basis/datum, nested/nullability mismatch; only named admissible conversions reach the head. |
quantity_composition — tests/conformance | Additive material/energy sums, scaled temperature differences, gauge/absolute pressure, WeightedMean, molar/mass basis, enthalpy transport and standard gas-law/Arrhenius compositions; no dimensional-only inference. |
canonical_null_equivalence — tests/conformance | Hidden primitive and nested payload/validity, sliced arrays, dictionary changes, batch/row order, all-valid bitmap, NaN/signed-zero rules and empty relations; same declared content has the same v2 digest. |
encoding_roundtrip_identity, snapshot_membership — tests/conformance | IPC stream/file and Parquet share logical identity with separately checked bytes; explicit empty members, changed contract/parent, excluded sidecars and duplicate/missing ports. |
publication_fault_matrix — tests/lifecycle | Existing truncated/corrupt object, failed serialization finish, interruption before each object/manifest/ref, CAS conflict and untrusted restore; only a complete validated snapshot is visible. |
demand_seed_closure, stage_bundle_graph, realization_completeness — tests/engine | Law-only, initializer-only and transitive method demand; candidate addition/removal, ambiguity and unsupported demand; unique bound ports, acyclic stages, no P6 dependency on realized P7/P15 IDs; late demands fail. |
incremental_equals_clean — tests/engine | Species/domain add/remove, package units, child/template binding, candidate universe/absence, kernel contract/binary, policies, engine settings, fixed/parameter values/treatment and free guesses; complete outputs and lineage agree. |
plan_evidence_roundtrip — tests/engine | Resolve only pinned admitted providers/bindings; preserve attributable decoded plan meaning. Fresh-map/process byte differences are allowed and cannot affect memo keys. |
pushdown_vs_unpruned — tests/engine | Compare complete values and multiplicities with an Unsupported provider; aliases, nulls, IN/disjunction, dictionary keys, filters omitted by projection and limits. Deliberately over-pruning/empty providers fail the oracle. |
relational_expansion_reference — tests/engine | Aggregate/unnest grouping, ordinal preservation, provenance, empty/null lists and duplicate membership; checked count/integer sums; no undeclared floating reduction. |
kernel_outcome_conformance — tests/conformance | Scalar/batch/UDF/native/exported adapters: natural units, null rejection/propagation, invalid domain and unsupported derivatives; tolerant results include every requested coordinate and mandatory reason. |
guarded_branch_execution, numerical_policy_conformance, nonlinear_affine_lowering — tests/conformance | Mixed/nested masks, excluded log/division/overflow, CSE on/off, derivative branches, cancellation, signed zero, explicit FMA/reassociation policy, constant-degree-zero and nonlinear children in Affine; actual backend routes preserve supported behavior or refuse. |
arrow_borrow_lifetime — tests/lifecycle | Eligible nonzero-offset slices, owners outliving callbacks/stream drains, mutation isolation, cancellation and reserved-copy fallback; null numerical inputs are rejected. |
query_memory_budget, canonicalization_memory_budget, result_memory_budget — tests/lifecycle | Single and concurrent snapshots, non-spillable consumers, all live copies and failed reservations; release on cancellation/failure, no partial publication. Report whole-process peaks beside pool accounting. |
explicit_schema_observations — tests/conformance | CSV headers and JSON unknown fields, duplicate/missing required columns, declared null spellings, finite/exact values, units, target IDs and located errors; no schema inference or null-to-zero fallback. |
Verification
Proposed: all runtime fixtures and measurement packets above. Implementation
acceptance requires the appropriate Rust tests with explicit
--features pse-relations/force-validate (owned by just test), then the named
slice/conformance/lifecycle gates. Python/native changes require just py-sync,
just py-test, just quality; solver-backed parity uses just parity-container
against IDAES 2.12.0. Baseline is zero failures for each command and mode.
Tested — historical characterization only:
bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh completed six
characterization groups with zero failed assertions, baseline zero, in the
revision-4 review’s dev/force-validate environment (Rust 1.98.1, Arrow 59.3.0,
DataFusion 55.1.0, offline locked resolution). It is not rerun for prose changes
and does not test the proposed v2 canonicalizer or any production integration.
Interface-checked: pinned source and rustdoc routes are recorded in that review. Additional Context7 queries during this amendment returned broad analyzer/runtime/reader documentation; they are discovery evidence, not a substitute for the pinned call-shape checks or executable fixtures.
| Current amendment check | Mode / baseline | Result |
|---|---|---|
just doctor | Environment preflight; zero failed checks | Passed at session start |
just adr-index, just adr-lint | Generated index, front matter, section links, supersession and accepted-record immutability; zero findings | Passed: 48 records, 25 register rows; index current |
just docs | mdBook build; zero errors; links additionally checked by the focused audit | Passed |
just lint-typos, just lint-license, just lint-agents, just fmt-check | Repository static checks; zero findings | Passed; REUSE covered 368/368 files |
just family-check, just codegen-check | Current-checkout dependency families/evidence agreement and generated-tree hygiene; zero failures | Passed; 54 shared evidence-family packages agree; 4 generated scopes clean |
just test-package pse-tests-governance -p pse-relations | Nextest default/test profile, locked, Arrow force-validate explicitly enabled; baseline zero | 19 passed, 0 failed, 0 skipped across 13 binaries; run e60383cf-16e5-4c73-a3da-175ff0e30279 |
git diff --check and focused document consistency audit | Whitespace, original heading sequence, P0–P16, decision markers, table shape, manifest JSON and preserved evidence inputs; zero findings | Passed: 181 original headings retained, one inserted subsection; 17 pass rows; 55 governed-section references; 9 immutable supersession pairs; 271 tables and 257 local links at initial audit |
Validation corrections: initial just governance/family results were not
credited because a cached xtask binary retained the root of a temporary setup
checkout. A fresh task-specific Cargo target verified this tree; refreshing only
the driver’s source timestamp then made ordinary just family-check resolve this
checkout too, without changing driver contents. A package-only force-validation
attempt failed before tests because the governance crate does not contain the
relations feature. Selecting pse-relations alongside governance, as in the
successful command above, supplies the required feature. The current xtask governance recipe omits that feature, so the test, codegen and family components
were verified explicitly; no driver/governance implementation was changed here.
The host shell currently emits /etc/bash.bashrc: line 7: PS1: unbound variable
during recipe startup. Record command exit status and gate findings separately;
a successful check is not a claim that startup is warning-free. API-reference doc
lint remains deferred under R-20; no source generator or numerical acceptance is
inferred from a book build.
Open items
- Runtime implementation and scientific parity remain the Proposed handoff above.
- R-22: measured complete finer dependency bundles; R-01: salsa only after that.
- R-23: larger canonical relation envelope or a new fixed-boundary format.
- R-24: qualify Parquet pruning and bounded coalescing separately by their consumers.
- R-25: exact supported predicate preimages, then measured total benefit.
- Maintainer publication remains the existing decision-PR process. This local
amendment does not create a PR, merge a branch or accept an ADR. Use each
record’s
adr: ADR-NNNN <title>andadr/needs-reviewlabels as required by the repository; a nameddesign:follow-up may carry the integrated blueprint. Its description must state thatPSE_DESIGN_EDIT=1was used for the user’s expressly authorized blueprint amendment and cite the revision-5 review.
Outcome (recorded after implementation)
What was built
The design/documentation amendment described above. Blueprint revision 5 and ADRs remain Proposed; library receipts retain their original evidence labels. No process-model implementation, new library family or solver behavior is added. Scaffold changes are contract citations, diagnostic wording and the benchmark placeholder label; the parsed Cargo manifest is identical to the baseline. Current static verification is recorded separately in Verification.
A mistake made and corrected
An edit selector matched the P3 table’s Canonicalization cell instead of the
acceptance-table cell and removed intervening sections in an intermediate local
draft. The task’s own edits were reconstructed from the baseline, selectors were
bounded to their intended section, and the entire original heading sequence plus
all 17 pass rows were checked. No unrelated work was overwritten. The final audit
also corrected self-referential hash descriptions, incomplete port naming and
stale current/historical guidance.
Deviations from the plan, deliberate
The canonicalizer is a bounded v2 contract instead of promising arbitrary-size one-batch construction. Whole-stage keys and explicit lineage replace premature fine reuse; plan bytes remain noncanonical evidence. Additional optimization is deferred by measured triggers. These are explicit ADR-0041/0042/0044/0045 decisions, not undocumented SHOULD exceptions or implementation performance claims.
03 — Wave 1 foundations
title: Wave 1 — foundations, executed as parallel sub-agent packets status: in-progress date: 2026-09-13 adrs: [ADR-0004, ADR-0005, ADR-0006, ADR-0007, ADR-0009, ADR-0027, ADR-0030, ADR-0031, ADR-0039, ADR-0040, ADR-0041, ADR-0042, ADR-0043, ADR-0044, ADR-0045, ADR-0046, ADR-0047, ADR-0048, ADR-0049, ADR-0050, ADR-0051] phase: 0
Wave 1 — foundations, executed as parallel sub-agent packets
Context
The repository has finished design and planning (blueprint revision 5, ADR-0001–0048, two
implementation plans, four capability maps with probes) but almost no implementation:
21 of 23 pse-* crates are 8–16-line declared boundaries, the schema generator and every
generated tree (crates/pse-relations/src/generated/, docs/generated/, the non-placeholder
half of python/pse/contracts/) do not exist, cargo xtask golden does not exist, and four
of the five test families hold one tautological placeholder each. What is real: the
governance machine (workspace pins, family-check, 11 governance tests, codegen --check
hygiene), the build-provenance chain (pse-buildinfo → pse-py::build_info →
pse._build), the Python extension-type registrar, codec, Any lint, numpy boundary and
host probe, the Ipopt build.rs, and the ADR/plan/register tooling.
Blueprint §25 defines phase 0 — “Foundations” — as: pse-schema registry and codegen;
pse-ids; pse-quantity; pse-material; pse-mathir with the operator catalog and
canonicalization; pse-catalog over a local store; pse-authoring parser and change sets;
passes P0–P3 and P10. Exit criteria: schema governance tests green; expression DSL
round-trips; pse.canon.v2 metamorphic fixtures and active semantic query admission;
storage identity/integrity fault matrix; quantity/ordered-IR contract fixtures; a complete
snapshot can be published, read back and queried.
Plan 02 hands off seven dependency-ordered packets A–G with fifteen named fixtures. The
revision-5 review (Accept, bounded proposed-design scope) names the “smaller Slice A
runtime” — whole-stage recompilation, only currently consumed backend bindings — as the
viable implementation sequence. This plan cuts that sequence into packets that Opus 5
sub-agents (impl-plan-exec-subagent role) execute in parallel with disjoint file
ownership. The packet designs were produced by three design passes (registry/codegen/
authoring/compiler; identity/storage/catalog/runtime; quantity/math IR/DSL) and reconciled
here; the reconciled interface contracts are in §“Interface contracts”.
Decisions
Taken with the maintainer while planning:
| Question | Decision |
|---|---|
| Wave scope | Phase 0 plus the load-bearing interfaces plan 02 says must land early: runtime memory reservations (packet E interface and body), the minimal rule compiler for P2 invariants, the compiler pass/port/stage-key skeleton for P0–P3 + P10, generated Python contracts. Excluded: P4–P9, P11–P16, kernels, numerics, backends, the Pyomo adapter, pse.open/compile/solve. |
| Governance | Merge PR #1, then land the uncommitted revision-5 amendment as one adr: PR that also flips ADR-0039–0048 to accepted (the revision-5 review carries the Accept verdict the decision rule requires). |
| Topology | One serial keel lands shared interfaces; then one PR per packet from its own worktree branch to main; required checks gate every packet; the maintainer squash-merges in tier order. |
| New dependencies | One short ADR (ADR-0049) promotes serde_json (manifest codec, extension metadata JSON) and uuid (v7, explicit-policy IDs) to direct = pins with §3.1 rows. Both already resolve in Cargo.lock (1.0.151, 1.26.1); the graph does not change. |
Taken by the orchestrator while reconciling the three designs (routine choices within the accepted decisions; each is reversible inside the wave):
| Choice | Decision and reason |
|---|---|
Where Opcode lives | pse_quantity::enums::Opcode (the registry types quantity_operations.opcode as enum(Opcode) and pse-quantity sits below pse-mathir); pse-mathir re-exports it; a conformance test asserts member parity with the registry enum. |
| Hashing from the registry | pse-schema gains pse-ids as a dependency (pse-ids depends on nothing internal, so the edge is downward); the registry fingerprint is derive_hash("pse:registry:v1", …). |
| Reservation interface | pse_ids::resource::{MemoryReserver, Reservation, ReserveError, CancellationToken}; pse-runtime implements it over DataFusion’s MemoryPool (packet E interface lands with the keel, body later — plan 02). |
| Manifest struct | Hand-written Rust wire type in pse-catalog for phase 0; the registry declares ManifestSpec as the authority for the generated Python msgspec struct and docs/generated; a parity test guards the two. Generating the Rust struct is a wave-2 item (Open items). |
| Extension-type registry for DataFusion | Hand-written in pse-catalog::session::registry over the eleven pse_relations::ext impls; not generated. |
| Math IR persistence | pse-mathir stays Arrow-free; it exposes MathRelationSink/Source traits; the adapter over generated builders lives in pse-compiler (mathir_relations.rs). |
lib.rs and manifest ownership | Keel packets write every crate’s lib.rs module list, every Cargo.toml, and empty documented stub files; fan-out packets only fill module bodies and add test files. No two packets edit one manifest. |
| DSL grammar | Parser accepts a documented superset of §7.7 (equation/predicate entry points, where let, eps= last argument, ^ right-associative, -x^2 refused as ambiguous, comprehension form … for i in … refused); each item is a revision-6 amendment row. |
Non-negotiables every packet inherits (AGENTS.md, .claude/rules/rust.md, lints,
governance tests): zero baseline; unwrap/expect/panic/todo/print/dbg denied outside tests;
missing_docs with -D warnings; every #[allow] carries reason =; bans on
Field::extension_type(), SessionConfig::set_str, SchemaLike::from_type/from_samples,
IpcWriteOptions::try_with_compression, anyhow in crates/*; every pub enum *Error
derives thiserror::Error + miette::Diagnostic with a §23.2 code in Rust-path form
(compile::math::unit_inconsistent, runtime::resource_limit, …); pse-ids is the sole
blake3 owner and no {:?} reaches a hash; pse-ids/quantity/material/mathir depend on no
engine crate; engine crates import Arrow through datafusion::arrow; generated paths are
written only by cargo xtask codegen; every test invocation passes
--features pse-relations/force-validate (just test does; for packages that do not depend
on pse-relations use just test-package <pkg> -p pse-relations); tools from .venv,
never $PATH; BTreeMap/sorted Vec wherever order reaches output or a hash.
Superseded behaviours that must not be implemented by accident (they still read as
prose in older accepted ADRs): a single RelationSpec.producing_pass_id (ports own
production); a cache_key_inputs list (every input port is keyed, absence included); P6
reading P7/P8/P15 outputs; the two-process plan-byte equality test; one digest for logical
content and encoded bytes (pse.canon.v1); the Exact-pushdown wrapper oracle used alone
(compare against a complete unpruned result); passive extension registration as
validation; per-session unbounded/greedy pools; HashMap metadata iteration order
reaching any hash; object_store put/get/copy_if_not_exists/rename (only *_opts exist
at 0.13.2, and a rename is never a commit).
Plan
Stage 0 — landing (maintainer plus one agent, serial)
- Merge PR #1 (
chore/complete-repository-setup, 9 commits ahead oforigin/main). - Land the revision-5 amendment from the current working tree (41 changed/untracked
files: blueprint, ADR-0039–0048, nine supersession edits, capability maps, plan 02, two
reviews, evidence) as one PR:
- branch
adr/0039-revision-5-contracts; titleadr: ADR-0039 enforce semantic admission and complete physical quantity operations; labelsadr,kind/design-decision.governance / adr-lintrequires theadrlabel and that title form whenever anydocs/adr/NNNN-*.mdchanges; it does not limit a PR to one record. - before committing, on each of ADR-0039–0048:
status: accepted;review:→docs/design_review/reviews/design_review_blueprint-rev5-contracts_2026-09-13.md#6-acceptance-gates;evidence: Proposedstays (the design is accepted, the runtime is not); append- 2026-09-13 — accepted (revision-5 review, Accept for bounded proposed scope).to## Status history. The records are not onorigin/main, so the immutability lint has nothing to compare. - PR description states
PSE_DESIGN_EDIT=1was used for the authorised blueprint amendment and cites the revision-5 review (plan 02 open item). - checks:
just adr-lint,just adr-index,just docs,just lint-repo.
- branch
- This plan file (
docs/plans/03-wave-1-foundations.md) gets itsdocs/plans/README.mdrow anddocs/SUMMARY.mdentry in packet K-2.
Execution topology
- Worktrees. Each packet runs in its own worktree and branch
(
git worktree add ../pse-arrow-wt/<packet> -b wave1/<packet> main; the Agent tool’sisolation: "worktree"). Packets own disjoint file sets; a packet that must touch a file it does not own reports the conflict instead of editing (theimpl-plan-exec-subagentcontract). - One PR per packet to
main, Conventional-Commit title with the crate short name as scope (feat(ids): pse.canon.v2 canonicalizer), the PR template’s Evidence section filled with §D labels naming tests and exactjustcommands with failure counts against baseline zero, and “Not verified” filled honestly. - Tiers. A packet starts when every
depends-onpacket is merged tomain. The orchestrator rebases waiting worktrees ontomainafter each merge. Registry changes and their regenerated trees land in the same PR (just codegenis anaskpermission; the packet reports the regeneration). - Integration checks before requesting a merge:
just ci-fast,just governance,just codegen-check,just family-check;just qualityfor Python-touching packets;just docsfor docs-touching packets.just ci-prat every tier boundary. - Roles.
plan-scoutpreflights each tier’s packets againstmainbefore dispatch;impl-plan-exec-subagentexecutes a packet;precision-integratorhandles a packet that must change a contract another packet consumes;plan-auditoraudits each tier before the next starts;design-reviewerreviews the implementation at wave exit;architecture-docs-writerwith theadrskill executes K-2’s decision records. - Reports. Every packet returns packet ID, changed files, command/mode/failure counts, unresolved dependencies, evidence labels, and discoveries outside scope (surfaced, not acted on). A stub or a focused green check is never reported as terminal acceptance.
- Decision routing. A packet that finds it must alter a D-decision, a crate boundary, a
dependency family or a SHOULD stops and routes through the
adrskill; the orchestrator decides whether to fork the packet.
Hand-off packet (what every dispatch prompt contains)
- The packet row below (ID, title, blueprint sections, ADRs, owned files, deliverables, acceptance) and the relevant excerpts of §“Interface contracts”.
- The non-negotiables and superseded-behaviour lists above, verbatim.
- Prerequisite reading:
AGENTS.md,.claude/rules/rust.md(orpython.md), the cited blueprint sections, the cited ADRs, the cited capability-map sections (just lib-outlinefirst). - The exact acceptance commands and the report format.
- Commit authorisation: commit on the packet branch; never push to
main; open the PR with the Conventional-Commit title given in the row; do not merge.
Tier 0 — keel
| ID | Title | § / ADR | Depends on | Owned files | Deliverables and acceptance | Size |
|---|---|---|---|---|---|---|
| K-1 | pse-ids core: identities, framing, floats, encoding checksum, reservation interface, snapshot membership | §5.1, §5.3 steps 4–7, §14.3 · ADR-0007, 0030, 0045, 0046 | — | crates/pse-ids/src/{lib,error,id,derive,float,frame,encoding,resource,contract,snapshot}.rs, crates/pse-ids/tests/golden_vectors.rs | Everything in contracts §I.1 except canon/; frozen hex golden vectors for every derive context, derive_hash, snapshot_id, encoding_checksum; SemanticId::NIL. just test-package pse-ids -p pse-relations; just governance (blake3_owner, banned_patterns, error_taxonomy). | M |
| K-2 | Decision records, revision-6 amendment batch, manifests, test-crate scaffolding | §3.1, §3.2, §4.2, §4.4, §6.3, §6.9, §7.7, §22.2 · new ADR-0049, 0050, 0051 | Stage 0 | docs/adr/{0049,0050,0051}-*.md, docs/authoritative_design/blueprint.md (revision-6 rows only, PSE_DESIGN_EDIT=1), docs/adr/register.md (rows for deferred items below), Cargo.toml, every crates/*/Cargo.toml touched by the wave, xtask/Cargo.toml, benches/Cargo.toml, tests/{conformance,engine,lifecycle}/{Cargo.toml,src/lib.rs,src/*.rs stubs}, tests/governance/tooling_deps.toml, docs/plans/03-wave-1-foundations.md (this plan, committed with status: in-progress), docs/plans/README.md, docs/SUMMARY.md | ADR-0049 serde_json (pse-catalog, pse-relations, xtask) and uuid v7 (pse-authoring) as direct = pins + §3.1 rows. ADR-0050 identity framing: u64-LE length-prefixed parts; derive_key for IDs/fingerprints; contexts pse:named:v1, pse:registry:v1, pse:stage_key:v1, pse:settings:v1, pse:mathir:node:v1; pse.canon.v2 and pse.snapshot.v2 frame layouts; Envelope::PHASE1; IPC alignment 64 / MetadataVersion::V5. ADR-0051 generated trees: crates/pse-authoring/src/generated/documents.rs added to ADR-0031’s list; codegen --check is regeneration equivalence; GENERATED.sha256 written by xtask. Revision-6 blueprint rows: §3.1 rows; §4.4 pse.enum/pse.ordinal_ref metadata carry enum_id/target_relation_id; §22.2 change_ops.row : struct<staged_port: text, staged_ordinal: u64>; normalized.package_graph columns; §6.9 math_piecewise_linear, math_quantity_selections, math_broadcasts.bound_index; §7.7 grammar superset; §7.2 phase-0 boolean guards note; §4.2 generated paths. Manifests: pse-schema += pse-ids; pse-rules += pse-schema; pse-compiler += pse-authoring, pse-mathir, pse-quantity, pse-schema; pse-runtime += datafusion-execution, datafusion-common; xtask += pse-schema, pse-compiler, pse-catalog, pse-authoring, tokio, tempfile, sha2; dev-deps proptest/insta on pse-quantity, pse-mathir, pse-authoring; features fixtures on pse-quantity/pse-material, arbitrary on pse-authoring; test crates get the union of dev-deps and pub mod stubs (conformance::{canon_fixtures,fixture}, engine::oracle, lifecycle::fault_store). Title adr: ADR-0049 pin serde_json and uuid as direct dependencies, labels adr. just adr-lint; just family-check; just governance; just docs. | M |
| K-3 | pse-schema registry model, builder, assembly, fingerprint, codegen API; skeleton lib.rs/error.rs for the A/C crates | §4.1–4.5, §5.1, §6.1, §20.2, §22.2 · ADR-0004, 0031, 0039, 0045 | K-1, K-2 | crates/pse-schema/src/{lib,error,builder,fingerprint,membership,arrow,ext_metadata}.rs, crates/pse-schema/src/model/**, crates/pse-schema/src/catalog/{mod,enums_platform,s4_schema,s6_1_identity,s22_change_sets,manifest}.rs + empty declare() stubs for every other catalog/*.rs listed in tiers 1–2, crates/pse-schema/src/codegen/mod.rs (+ stub rust/mod.rs, python/mod.rs, markdown/mod.rs, jsonschema.rs), crates/pse-schema/tests/{registry_assembles,fingerprint_stable}.rs; crates/pse-relations/src/{lib,error}.rs + stubs (ext/mod.rs, validate/mod.rs, cells.rs, registry_relations.rs, migrate.rs, generated/mod.rs placeholder); crates/pse-authoring/src/{lib,error,span}.rs + stubs (dsl/mod.rs, document/mod.rs, ids.rs, change_set/mod.rs, targets.rs, p0.rs, p1/mod.rs); crates/pse-compiler/src/{lib,error}.rs, passes/mod.rs (the Pass trait and bundle types) + stubs; crates/pse-rules/src/{lib,error}.rs + stubs | Contracts §I.2 model types, RegistryBuilder, catalog::assemble() with the fixed call list, the six reference.schema_* relations, EXTENSION_TYPES (11), logical-type catalog, platform enums, ManifestSpec, fingerprint/membership/arrow/ext-metadata modules, codegen::generate() returning empty trees; every error enum with its §23.2 codes so error_taxonomy is non-vacuous. registry_assembles; fingerprint_stable (insta). just test-package pse-schema -p pse-relations; just check; just clippy; just governance. | L |
| K-4 | Leaf keels: pse-quantity, pse-mathir, pse-material | §4.4, §6.2, §6.4, §6.9, §6.14, §7.1–7.2, §23.2 · ADR-0009, 0030, 0039, 0047 | K-1, K-2 | crates/pse-quantity/src/{lib,dimension,ids,enums,error,index}.rs + stubs for every module in §I.6; crates/pse-mathir/src/{lib,node,payload,graph,error}.rs + stubs; crates/pse-material/src/{lib,ids,enums,error}.rs + stubs | Ratio, DimensionVector (8 rationals, canonical bytes), Opcode (43, as_str, ALL), registry enums with as_str, IndexSet, QuantityError; NodeId, Payload, ExprGraph::insert with arity/finiteness checks, MathIrError; material ids/enums. In-file tests (reduced form, overflow → error, Opcode::ALL.len() == 43, NaN literal rejected, arity mismatch). just test-package pse-quantity -p pse-relations; just test-package pse-mathir -p pse-relations; just governance. | M |
| K-5 | Engine keels: pse-catalog types and manifest, pse-runtime types | §5.4, §20.2, §23.2, §14.3, §18.8 · ADR-0044, 0045, 0046, 0048 | K-1, K-2 | crates/pse-catalog/src/{lib,error,failure,contract,snapshot}.rs, crates/pse-catalog/src/store/{mod,layout,manifest,clock}.rs, crates/pse-catalog/src/provider/mod.rs + stubs for store/{encode,verify,refs,publish,membership,open}.rs, provider/{list,catalog,schema,table,pushdown,statistics}.rs, session/{mod,profile,registry,admission,config}.rs, evidence.rs; crates/pse-runtime/src/{lib,error,budget}.rs + stubs env,reserve,peak,cancel,session_factory | Contracts §I.4 CatalogError + classify (the one §23.2 mapping site), RelationContract, Snapshot/LoadedRelation, Manifest serde with validate/frame/encode/decode, layout paths, RefName, Clock, BoxFut; ResourceBudget, RuntimeError. In-file tests: manifest round trip, validate negatives, classify table. just test-package pse-catalog -p pse-relations; just test-package pse-runtime -p pse-relations. | M |
Order: K-1 and K-2 in parallel (K-1 codes against the ADR-0050 constants stated here);
then K-3, K-4, K-5 in parallel. Fan-out begins when all five are on main.
Tier 1 — parallel fan-out (14 packets)
| ID | Title | § / ADR | Depends on | Owned files | Deliverables and acceptance | Size |
|---|---|---|---|---|---|---|
| A-1 | Registry §6.2–§6.5 | §6.2–6.5, §8.1 · ADR-0039 | K-3 | crates/pse-schema/src/catalog/{s6_2_physical,s6_3_domains,s6_4_material,s6_5_property}.rs | 33 relations incl. normalized.domain_products, inferred.valid_index_tuples; section enums. registry_assembles; just test-package pse-schema -p pse-relations. | M |
| A-2 | Registry §6.6–§6.7 and §6.14 IDAES enums | §6.6, 6.7, 6.14 · ADR-0040 | K-3 | catalog/{s6_6_templates,s6_7_instances,s6_14_idaes_enums}.rs | Templates (17 relations), template_property_requirements, normalized.property_demand_seeds, instances/scopes/connections, inferred.* incl. undecided; all 27 §6.14 enums with idaes_name. Same command. | M |
| A-3 | Registry §6.8–§6.9, §7.2–7.3, §6.12, expression families | §6.8, 6.9, 6.12, 7.2, 7.3 · ADR-0009, 0047 | K-3 | catalog/{s6_8_symbols,s6_9_math,s7_operators,expr_family,s6_12_derived}.rs | math_* incl. revision-6 additions, symbols*, operator_specs, Opcode and related enums; declare_expr_family(b, ns, prefix) for normalized.{template,instance,display,contribution,guard}_expr_* and reference.rule_expr_*; declare_normalized_copy() for P3. Same command. | M |
| A-4 | Registry §5.2, §6.10, §6.11, §6.13, passes P0–P16 | §5.2, 6.10, 6.11, 6.13, 14.1, 14.5 · ADR-0040, 0041, 0043, 0044 | K-3 | catalog/{s5_2_revisions,s6_10_cases,s6_11_numerical,s6_13_runtime,s14_passes}.rs | Cases/targets, kernel_specs, numerical_policies, pass_specs/pass_input_ports/pass_output_ports, stage_bundles, rule_*, engine_profiles, runtime/provenance incl. closure_report; all 17 PassSpecs with ports per §14.1 (P1 outputs every authored relation; P3+ never authored/reference). Same command. | L |
| A-7 | pse-relations hand-written engine | §4.3, §4.4, §5.3 step 1, §20.5 · ADR-0039, 0045 | K-3 | crates/pse-relations/src/{ext/**,validate/**,cells.rs,registry_relations.rs,migrate.rs}, crates/pse-relations/tests/**, tests/conformance/tests/semantic_admission_paths.rs, tests/conformance/fixtures/extension_metadata.json | Eleven arrow_schema::extension::ExtensionType impls (v versioned; deserialize_metadata errors on unknown), canonical metadata codec (serde_json), PseFormatterFactory, the recursive validator (validate_field, validate_schema, validate_batch; plural errors), cells↔batch, registry materialization, migration loader. semantic_admission_paths validator half (wrong storage, unknown/malformed/versioned/nested metadata, unregistered pse.* name, SERDE_ARROW:* key, ordinal syntax). just test-package pse-relations; just test-package pse-tests-conformance -p pse-relations. | L |
| A-6 | Rust codegen (starts on the keel’s example relations; regenerates the full tree once A-1–A-5 merge) | §4.2, §4.4, §22.1 · ADR-0031, 0039, 0051 | K-3, A-7; full tree after A-5 | crates/pse-schema/src/codegen/rust/**, crates/pse-schema/src/catalog/documents.rs, crates/pse-schema/tests/codegen_determinism.rs; outputs (via just codegen) crates/pse-relations/src/generated/**, crates/pse-authoring/src/generated/documents.rs | Per relation: RELATION_ID/NAME/NAMESPACE/VERSION/FINGERPRINT, schema(), <Name>Row, <Name>View::try_from_batch (fingerprint + try_extension_type + validate), <Name>Builder, validate; enums with as_str/from_str/idaes_name; migrations; generated conformance tests; serde_arrow_fields() only after the serde_arrow_roundtrip spike passes; @generated header prepended after prettyplease::unparse. codegen_determinism (two runs byte-equal; no HashMap in codegen sources); no_shadow_structs non-vacuous. cargo xtask codegen --only relations && just test-package pse-relations; just codegen-check. | L |
| R-1 | xtask codegen for real | §4.2 · ADR-0031, 0051 | K-3 | xtask/src/{main,codegen,golden(stub signature),bindgen(dispatch)}.rs, .claude/rules/generated.md (drop the phase-zero paragraph), justfile (codegen-check doc string) | codegen writes every generated file and removes stale ones; --check regenerates into a temp dir and diffs both ways plus the untracked-file check; Python target writes GENERATED.sha256 (sha2); bindgen arm skipped with a notice when IPOPT_DIR is unset (--only bindgen exits 2), hygiene-only diff for bindings.rs until R-3. cargo xtask codegen && cargo xtask codegen --check; just governance. | S |
| Q-1 | Units, unit sets, conversions, kinds, bases, reference states, quantity types, registry | §6.2, §8.1, §8.2, §8.4 · ADR-0039 | K-4 | crates/pse-quantity/src/{unit,unit_set,conversion,kind,basis,reference_state,quantity_type,registry}.rs | convert_spec (the single source for P3/P9/P10 unit-convert edges; compared by canonical_f64_bits), convert_value without FMA, UnitSet rejecting affine base units, DerivedUnit, ConversionRule, QuantityTypeKey, QuantityRegistryBuilder::build with every load-time invariant (positive and negative test each), resolve_key, operations_for, neutral_dimensionless. just test-package pse-quantity -p pse-relations. | M |
| M-1 | Operator table, operator_specs emission, hash frames, ordered hash-consing, acyclicity, deterministic numbering | §7.2, §7.3, §7.4 steps 1 and 5, §16.3 · ADR-0009, 0030, 0047 | K-4, K-1 | crates/pse-mathir/src/{opspec,catalog,hash,canonical,topo}.rs | OPERATOR_TABLE (43 rows; foldable set = Add, Sub, Mul, Div, Neg, Abs, Sqrt, UnitConvert; rewrite_conditions naming the disabled rewrites), emit_operator_specs via OperatorSpecSink; structural_hash/subtree_hash (pse:mathir:node:v1, floats via canonical_f64_bits, scope excluded), CanonicalGraph with post-order numbering and listing(), cycle detection with path. Tests: table order equals Opcode::ALL; commutative children not sorted; -0.0 ≠ 0.0; cycle → cyclic_expression. just test-package pse-mathir -p pse-relations. | L |
| C-dsl | Expression DSL: AST, parser, renderer, round trip | §4.4 pse.expr_dsl, §7.7 (+ revision-6 superset), §9.3, §10.3, §11.3 · ADR-0009 | K-3 | crates/pse-authoring/src/dsl/{mod,span,ast,error,lexer,parser,unit_expr,render,arbitrary}.rs, crates/pse-authoring/tests/{dsl_roundtrip,dsl_examples}.rs, crates/pse-authoring/tests/snapshots/ | parse_expr/parse_equation/parse_predicate, render_*, Ast with byte spans and structural_eq, member_references, winnow over LocatingSlice with cut_err after keywords, budget (≤ 65 535 bytes, depth ≤ 64), even-arity weighted_mean, DslError with offsets. dsl_examples: every blueprint example parses and snapshots. dsl_roundtrip: 1024 proptest cases parse(render(a)) ≡ a, persistence file committed. just test-package pse-authoring -p pse-relations; just doctest. | L |
| B-canon | pse.canon.v2 canonicalizer | §5.3 steps 1–5, 8 · ADR-0030, 0045 | K-1 | crates/pse-ids/src/canon/{mod,order,normalize,metadata,envelope,ipc}.rs, crates/pse-ids/tests/canon_unit.rs | canonicalize, logical_hash, stages::{admit_and_order, normalize, metadata_relation, frame_and_hash}, supported-layout matrix (§I.1), envelope preflight with checked arithmetic and reservations, RowConverter ordering ASC NULLS FIRST, recursive normalization (dictionary decode with bounds + enum membership, zero-offset rebuild, null payload zeroing incl. nested validity, validity omitted when all valid, i32 offsets), NaN canonicalization on the hashing copy only, zero-row batch emitted. Crate-local proptests incl. E4 verbatim. just test-package pse-ids -p pse-relations. | L |
| B-providers | Catalog/schema/table providers with truthful pushdown | §5.4 · ADR-0013, 0048 | K-5 | crates/pse-catalog/src/provider/{list,catalog,schema,table,pushdown,statistics}.rs, tests/governance/tests/no_mutating_providers.rs | Sealed SnapshotCatalogList/SnapshotCatalog/NamespaceSchema (register_* → Sealed), one generic RelationTable (scan_with_args, Schema::project, classify pure and idempotent, apply_filters via physical expr + filter_record_batch, Exact only for key/enum equality, IS [NOT] NULL, non-negated InList, AND/OR of Exact; any Cast/float → Unsupported), statistics_for with Precision, Constraints::new_unverified. Governance test asserts the implemented TableProvider method set equals {schema, constraints, table_type, scan, scan_with_args, supports_filters_pushdown, statistics}. just governance; just test-package pse-catalog -p pse-relations. | L |
| E-1 | Shared runtime and DataFusion-backed reservations | §14.3, §18.8 · ADR-0046 | K-5, K-1 | crates/pse-runtime/src/{env,reserve,peak,cancel}.rs | SharedRuntime::build (FairSpillPool → TrackConsumersPool → PeakRecordingPool; MemoryLimit::Finite asserted; DiskManager with directory and max size), PoolReserver implementing pse_ids::resource::MemoryReserver (try_grow before allocation; ResourcesExhausted → ReserveError::Exhausted naming owner and keys; release on drop/cancel), ResourceReport with process peak RSS beside pool peak, CancelSource. just test-package pse-runtime -p pse-relations. | M |
| Q-material | Elements, phase-validity predicate, stoichiometry helpers | §6.4, §6.14 · ADR-0039 | K-4 | crates/pse-material/src/{element,phase_validity,stoichiometry,testdata}.rs, crates/pse-material/tests/phase_validity.rs | ElementTable, species_valid_in_phase (four-valued minus conflict), molecular_weight, element_balance (diagnostic, not invariant). Deferred: material systems, Henry, apparent species, basis conversion. just test-package pse-material -p pse-relations. | S |
Tier 2 (10 packets)
| ID | Title | § / ADR | Depends on | Owned files | Deliverables and acceptance | Size |
|---|---|---|---|---|---|---|
| A-5 | Invariants, assembly checks, rule dependencies | §4.1, §6.1, §6.4, §14.2 rules 2/7 · ADR-0004, 0030 | A-1–A-4 | crates/pse-schema/src/{checks,rule_deps}.rs, catalog/{invariants,inv}.rs | Auto unique:*:pk and foreign_key:*; closure:entity_registered:*, closure:stoichiometry_species_in_phase, closure:material_system_species_exist (unnest), closure:packages.dependencies_resolved, acyclic:{entities,instances,cases,model_revisions} (recursive, depth-bounded), check:*, domain:*, cardinality:*, rules.stratified_negation, stage_graph.*; rule_dependencies rows; float-key and stratification checks at assembly. just test-package pse-schema -p pse-relations. | M |
| A-8 | Python codegen and Python tests | §4.4, §21.5, §6.14 · ADR-0004, 0031 | A-5, A-7 | crates/pse-schema/src/codegen/python/**, python/pse/codec/__init__.py, python/pse/tests/{test_no_shadow_contracts,test_generated_manifest_current,test_extension_metadata_canonical,test_relation_rows_structure,test_manifest_fingerprint}.py, python/pse/parity/tests/test_enum_parity.py; output python/pse/contracts/** | attrs row classes per relation, enums with IDAES_NAMES, msgspec Manifest from ManifestSpec, eleven pyarrow extension types with the revision-6 metadata shapes, idempotent registrar, REGISTRY_FINGERPRINT, GENERATED.sha256; pse.codec.converter() hooks for SemanticId/ContentHash/datetime; pse.codec.Manifest re-exported from contracts. Parity test compares idaes_name members with IDAES 2.12.0 (container only). cargo xtask codegen --only python; just py-sync && just py-test && just quality. | M |
| A-9 | Markdown docs and authoring JSON Schema | §4.2, §22.1 · ADR-0031, 0036 | A-5, A-6 | crates/pse-schema/src/codegen/{markdown/**,jsonschema.rs}, docs/SUMMARY.md (generated section entries); output docs/generated/** | docs/generated/{README.md, relations/<ns>.md, enums.md, extension_types.md, passes.md (mermaid DAG), rules.md, schema/authoring.schema.json} with the @generated marker. cargo xtask codegen --only docs; just docs. | S |
| Q-3 | Quantity operations and the §8.3 inference algebra | §6.2 quantity_operations, §8.3 (every row), §7.4 step 4 · ADR-0039 | Q-1 | crates/pse-quantity/src/{operation,infer,literal}.rs | infer(&OpRequest, &[Operand], &QuantityRegistry) -> Inferred implementing the decision table (built-in rules for literals, additive sums, origin-sensitive ±, neutral scaling, WeightedMean, reductions, gather/broadcast, conditional, unit convert, kernel/implicit; registered rules for Mul/Div/Pow/transcendental/derivative/integral with ordered-then-swapped matching and component rules). Tests per row: T+ΔT ok, ΔT+ΔT ok, T1−T2 same datum → difference, different datum rejected, T1+T2 rejected, α·ΔT stays difference, ambiguous rule rejected, swapped match recorded, sum of points rejected, certified_unit_sum needs invariant. just test-package pse-quantity -p pse-relations. | L |
| M-3 | Guarded literal folding | §7.3, §7.4 step 3, §18.2 · ADR-0047 | M-1, Q-1 | crates/pse-mathir/src/fold.rs | fold_literals with the seven-clause fold-safety predicate (foldable opcodes only; direct literal children only; never under a Conditional branch; finite result; domain checked; unit agreement; bitwise-equal to ordered evaluation), FoldReport with deferred_static_checks. Tests: 1e16 + (−1e16 + 1) → 0 (ordered), Add(Add(x,1),2) untouched, Mul(1e308,10) untouched, Div(1,0) static violation at root but untouched under a branch, Mul(−1,0) → −0.0, Log(Exp(1000)) untouched. just test-package pse-mathir -p pse-relations. | M |
| C-1 | Authoring loaders, spans, ids | §22.1, §5.1 · ADR-0021, 0049 | K-2, A-6 | crates/pse-authoring/src/{document/**,ids.rs}, crates/pse-authoring/tests/{load_package,hostile_yaml,ids}.rs, tests/fixtures/packages/{minimal_explicit,minimal_named}/** | load_package (serde-saphyr with budget, toml::Spanned, SpanIndex path → SourceSpan), generated document structs consumed, entity_id for explicit/named policies, assign_ids (uuid v7, text edits at the entity span), parse.missing_id; four hostile YAML inputs refused without panic. just test-package pse-authoring -p pse-relations. | M |
| B-store | Artifact store, publication protocol, refs, open/restore | §20.1, §20.2, §20.5, §5.3 step 7 · ADR-0045, 0046 | B-canon, K-5 | crates/pse-catalog/src/store/{encode,verify,refs,publish,membership,open}.rs | Protocol: validate batches → canonicalize (keep sorted) → complete_members from registry snapshot classes (empties explicit) → snapshot_id → encode IPC file (+ Parquet for authored/reference) with finish and decode-verify → PutMode::Create (verify checksum/length/format on AlreadyExists) → manifest under its own checksum → ref CAS (PutMode::Update with prior version; Create if absent; RefUpdatePolicy); Catalog::{open, publish_bundle, set_ref, head, list_refs, snapshot, resolve}; TrustLevel::Untrusted decodes, validates and rehashes; stage sidecar stage_lookup/stage_record under stages/<key>.json (excluded from membership); RelationContract::from_spec. In-file tests over object_store::memory::InMemory. just test-package pse-catalog -p pse-relations. | L |
| B-session | Session construction, active admission, engine profile, settings | §4.4, §14.2 rule 5, §14.3, §23.2 · ADR-0039, 0044, 0046 | B-providers, A-7 | crates/pse-catalog/src/session/{mod,profile,registry,admission,config}.rs, tests/engine/tests/{semantic_admission_paths,engine_profile_settings}.rs | build_session (with_default_features then typed ConfigOptions::set, information_schema = true, explain.format = pgjson, asserted enable_ansi_mode = false and skip_physical_aggregate_schema_check = false, shared RuntimeEnv, sealed catalog list, extension registry over the eleven impls asserting add_extension_type_registration returned None, analyzer rules [pse.admission.first] ++ profile ++ [pse.admission.final], explicit optimizer/physical lists, PseOptions under datafusion.pse.*); admit_plan (apply_with_subqueries, expressions, nested fields, TableScan must be a RelationTable); SnapshotSession::{sql, execute_plan, table_reference, read_back_settings, settings_hash, profile_hash, function_registry_hash} with observer closures and bounded collection; phase0_reference_profile; RuleCatalog by name → config::invalid on unknown; ThreadBudget::validate. semantic_admission_paths (E1 regression: ordinary SELECT over wrong storage / unknown name / malformed / versioned / nested / foreign source rejected before execution; direct factory call as positive control); engine_profile_settings. just test-package pse-tests-engine -p pse-relations. | L |
| B-fixtures | Canonicalization conformance fixtures and benchmark | §5.3, §24.1 “Canonicalization”, §24.3 · ADR-0045 | B-canon (encoding part after B-store) | tests/conformance/src/canon_fixtures.rs, tests/conformance/tests/{canonical_null_equivalence,encoding_roundtrip_identity,snapshot_membership}.rs, benches/src/lib.rs, benches/benches/canonicalization.rs | Generators and the identity-preserving/distinguishing transform matrix (hidden null payload, nested masks, slices, dictionary re-encoding, batch/row order, all-valid bitmap, NaN/signed zero, empty relations); IPC-file/Parquet round trips share logical identity with distinct checksums; membership: empties explicit, changed contract/parent, sidecar excluded, duplicate/missing ports rejected; criterion group per canonicalization stage replacing the placeholder. just test-package pse-tests-conformance -p pse-relations; just bench-smoke. | M |
| R-3 (optional) | Ipopt bindgen arm | §18.3 · ADR-0031 | R-1; solver image | xtask/src/bindgen.rs, output crates/pse-ipopt-sys/src/bindings.rs | bindgen over IpStdCInterface.h with the eleven-function allowlist, run with IPOPT_DIR (dev image); --check becomes regeneration equivalence for bindings.rs. If this slips, bindings.rs keeps the hygiene-only check and a register row records it. just parity-container -- cargo xtask codegen --only bindgen --check. | S |
Tier 3 (9 packets)
| ID | Title | § / ADR | Depends on | Owned files | Deliverables and acceptance | Size |
|---|---|---|---|---|---|---|
| Q-4 | Standard package fixture, admission and numeric helpers, proptests | §8.3, §8.2, §14.2 rule 6, §7.3 · ADR-0039, 0026 | Q-3 | crates/pse-quantity/src/{standard,admission,numeric}.rs, crates/pse-quantity/tests/{dimension_props,standard_package}.rs | standard_registry() (feature fixtures; named-policy ids; SI + °C/°F/psig/USD_<year>; kinds with addition_kind; point/difference variants; the six §8.3 required compositions plus swapped Mul forms; Log[mole_fraction] declared result), require_same_contract, exact i64↔f64 representability, approx_eq per §7.3, is_zero. Doc states the YAML units package is the authority and this is its test double. dimension_props; standard_package (all required compositions positive and negative). just test-package pse-quantity -p pse-relations --features pse-quantity/fixtures. | M |
| M-4 | Unit inference driver, index model, equation records, canonicalize() | §7.4 (all), §7.5 residual, §8.3, §6.9 · ADR-0039, 0047 | Q-3, M-1, M-3 | crates/pse-mathir/src/{index,infer,equation,canonicalize,walk}.rs | Node → OpRequest mapping, SymbolTypeSource, free-index resolution (UnboundIndex), residual typing, QuantitySelection rows, canonicalize(input, Policy::Strict) in the order acyclicity → indices → bottom-up inference → guarded folding → typed hash-consing → numbering; non-canonical unit may only feed UnitConvert/kernel inputs; NodeVisitor by operator family. just test-package pse-mathir -p pse-relations --features pse-quantity/fixtures. | L |
| C-2 | Change sets, P0, P1, target resolution | §22.2, §14.1 P0/P1, §6.10 · ADR-0005, 0027 | C-1, C-dsl, A-6 | crates/pse-authoring/src/{change_set/**,p0.rs,p1/**,targets.rs}, crates/pse-authoring/tests/{change_set,p1_stage,targets}.rs | ChangeSet/ChangeOp (row per revision 6), AuthoredReader trait, apply (rejects derived writes, named-policy rename, unknown row keys), p0::resolve (exact =x.y.z requirements only), p1::stage (documents → rows via generated builders; every expr_dsl parsed with span; entities; targets resolved after staging to case_spec_targets/case_activation_targets/observation_targets; wildcards; parse.unresolved_target). Import-path half of semantic_admission_paths. just test-package pse-authoring -p pse-relations. | L |
| C-3 | Minimal rule compiler and P2 | §14.2 rules 3/5/6/7/9, §23.2, §6.11 · ADR-0030, 0044, 0048 | A-5, A-7, B-session, B-store | crates/pse-rules/src/{plan/**,exec/**,invariants,derivations,errmap}.rs, crates/pse-rules/tests/**, tests/engine/tests/relational_expansion_p2.rs, tests/conformance/tests/invariant_fixtures.rs, tests/conformance/src/fixture.rs, tests/conformance/fixtures/invariants/<name>/{valid,violating}.yaml | compile(RuleSpec, PortBinding, &SnapshotSession) lowering scan/filter/project/equi_join/anti_join/union/distinct/aggregate(count)/unnest/recursive (UNION ALL + depth column bounded by seed rows); rule::float_key at compile; exact head admission (rule::head_schema_mismatch); four-valued root rewrite → head/undecided split; PK sort; errmap::classify at one site; run_invariants (P2 body) producing findings/undecided/derivations/explain/rules_fired; every registry invariant has a valid+violating fixture pair (test iterates registry().invariants()). Deferred: strata scheduling across rules, conflict, non-count aggregates, kernel_call, per-row derivations of recursive results. relational_expansion_reference P2 subset vs an independent Rust reference; invariant_fixtures. just test-package pse-rules -p pse-relations; just test-package pse-tests-engine -p pse-relations; just test-package pse-tests-conformance -p pse-relations. | L |
| B-faults | Publication fault matrix | §20.1, §24.1 “Lifecycle” · ADR-0045 | B-store | tests/lifecycle/src/fault_store.rs, tests/lifecycle/tests/publication_fault_matrix.rs | FaultStore: ObjectStore wrapper with FaultPlan (fail-before, truncate/flip-byte, one-shot precondition, cancel-at-step); cases: truncated/corrupt existing object under a correct name, failed writer finish, interruption before each object/manifest/ref, CAS conflict (retry and fail policies), cancellation mid-publish, untrusted restore of a corrupted artifact; assertions: old ref valid, no mixed snapshot visible, reserved() == 0 after failure. just test-package pse-tests-lifecycle -p pse-relations. | M |
| B-oracle | pushdown_vs_unpruned | §5.4, §24.1 “Pushdown truthfulness” · ADR-0013, 0048 | B-providers, B-store | tests/engine/src/oracle.rs, tests/engine/tests/pushdown_vs_unpruned.rs | UnprunedTable (same data, all Unsupported), OverPruningTable, EmptyTable, ExtraRowTable; multiset comparison via RowConverter rows; query matrix: key equality, IN (2 and 20), OR, AND with residual, IS [NOT] NULL on a nullable reference column, enum equality (dictionary), projection omitting the filter column, LIMIT, aliases; the three negative wrappers must fail the oracle. just test-package pse-tests-engine -p pse-relations. | M |
| E-2 | Budget fixtures and session factory | §14.3, §24.1 “Resource envelope” · ADR-0046 | E-1, B-session, B-store, B-canon | crates/pse-runtime/src/session_factory.rs, tests/lifecycle/tests/{query_memory_budget,canonicalization_memory_budget,result_memory_budget}.rs | SessionFactory::open_session (the only path handing a RuntimeEnv to pse-catalog); query_memory_budget (64 KiB pool; ORDER BY over a published 200k-row relation → ResourceLimit naming consumer and keys; two concurrent sessions share one pool; cancel one → its reservations return to zero; report shows pool peak beside process peak); canonicalization_memory_budget (reserver below the preflight estimate fails before allocation; publish_bundle under budget publishes no ref; reserved() == 0 after failure/cancel); result_memory_budget. just test-package pse-tests-lifecycle -p pse-relations. | M |
| B-evidence (optional) | Plan evidence codec and round trip | §14.2 rule 5, §20.2, §6.13 · ADR-0044 | B-session, B-store | crates/pse-catalog/src/evidence.rs, tests/engine/tests/plan_evidence_roundtrip.rs | PsePlanCodec (try_encode_table_provider writes snapshot_id ‖ relation_id ‖ version ‖ logical_hash; decode resolves only pinned snapshots; extension nodes/UDFs unsupported in phase 0), PlanEvidence with checksum, codec/engine versions, pgjson EXPLAIN and rules_fired, write_evidence under evidence/<checksum> marked canonical: false. plan_evidence_roundtrip asserts nothing about bytes. just test-package pse-tests-engine -p pse-relations. | M |
| A-6 (final) | Full-tree regeneration after A-5 | — | A-5, A-7, A-8, A-9 | as A-6 | just codegen over all four targets; just codegen-check clean; just ci-fast; just governance. | S |
Tier 4 (4 packets) and wave exit
| ID | Title | § / ADR | Depends on | Owned files | Deliverables and acceptance | Size |
|---|---|---|---|---|---|---|
| Q-5 | quantity_composition conformance fixture | §8.3, §24.1 · ADR-0039 | Q-4, M-4 | tests/conformance/tests/quantity_composition.rs | Composed expressions through canonicalize against standard_registry(): heater energy balance, F·h preserving the enthalpy reference state, F·x preserving basis, R·T, P/(R·T), T/1000{K} polynomial, Ea/(R·T) in exp, T + α·ΔT, WeightedMean of temperatures, gauge/absolute mix rejected, molar/mass mix rejected, unregistered length×mass rejected although dimensions combine. just test-package pse-tests-conformance -p pse-relations. | M |
| M-5 | Relation sink/source, snapshots, proptests, numerical_policy_conformance (P10 subset) | §6.9, §7.3, §24.1 · ADR-0047, 0030 | M-4 | crates/pse-mathir/src/relations/{mod,vec_sink}.rs, crates/pse-mathir/tests/{hash_consing_props,canonical_snapshots}.rs, crates/pse-mathir/tests/snapshots/, tests/conformance/tests/numerical_policy_conformance.rs | MathRelationSink/Source, emit, emit_untyped, load_untyped, load_canonical, VecSink; shuffled insertion orders → identical listings and hashes; emit→load→emit identical; excluded-branch Div(1,0)/Log(0) never folded or flagged; no reassociation; signed zero survives; NaN refused; Policy::Strict is the only value. just test-package pse-mathir -p pse-relations; just test-package pse-tests-conformance -p pse-relations; just snapshots-accept once, reviewed. | M |
| C-4 | Compiler skeleton and driver: DAG, stage keys, memo, atomic commit, P0–P3 + P10 adapters | §14.1, §14.3, §14.4, §20.1 · ADR-0040, 0041, 0042, 0044 | A-4, C-2, C-3, B-store, B-session, M-5, Q-4 | crates/pse-compiler/src/{passes/{dag,key,bundle,registry,p0,p1,p2,p3,p10}.rs,driver.rs,memo.rs,records.rs,mathir_relations.rs}, crates/pse-compiler/tests/**, tests/engine/tests/{stage_bundle_graph,incremental_equals_clean_p0_p3,demand_seed_closure_seeds}.rs | StageDag::{build, validate, topo_order} (acyclic; one producer per port; every derived input names an existing output; no P3+ output to authored/reference; P2 reads only authored/reference + registry); stage_key (pse:stage_key:v1: pass id/version, registry fingerprint, every port sorted with logical hash or explicit absence, policies, engine profile + function registry hashes; plan bytes never); Driver::commit (P0 → P1 → publish model/case bundle without ref → P2 → ref CAS only on zero error findings, ADR-0027) and Driver::run (memo via stage sidecar; every output batch validated; all-or-nothing publication with parents = input bindings; pass_records); P3 adapter (normalized copies, parsed normalized.*_expr_* via MathRelationSink adapter, constants to package units via convert_spec, demand seeds) and P10 adapter (canonicalize); PipelineRequest.external_bindings for tests only. stage_bundle_graph; incremental_equals_clean phase-0 (clean twice → equal hashes and keys; add a species → P1–P3 keys change, P0 reused; absent optional port changes the key; changed engine profile changes P2’s key); demand_seed_closure seeds only. just test-package pse-compiler -p pse-relations; just test-package pse-tests-engine -p pse-relations. | L |
| R-2 | Golden stores and remaining governance tests | §24.1, §5.4, §25 · ADR-0004, 0027, 0031 | R-1, C-4, A-7, B-store | xtask/src/golden.rs, tests/golden/{registry,minimal_explicit}/**, tests/governance/tests/{codegen_regeneration,registry_governance}.rs, tests/governance/appendix_b_deferred.toml, python/pse/tests/test_golden_registry.py | cargo xtask golden <name> publishes a store from a fixture (registry snapshot; minimal_explicit package + case through P0–P3, P10) and --check compares logical hashes and snapshot ids (physical checksums may differ); codegen_regeneration (in just test); registry_governance (every Appendix B relation registered or listed with a reason; no authored/reference FK into compiled/runtime; no compiler output port to authored/reference; every relation has a snapshot class); Python reads the golden registry store through the generated contracts. cargo xtask golden registry && cargo xtask golden minimal_explicit; just governance; just py-test. | M |
| W-exit | Wave verification, implementation design review, plan outcome | §25 · all | everything above | docs/plans/03-wave-1-foundations.md (Outcome), docs/design_review/reviews/design_review_wave-1-foundations_<date>.md, ADR evidence-label edits by supersession rules (status fields only) | Run the Verification table below in the stated modes; design-reviewer reviews the implementation against DM/G gates; plan-auditor verifies every packet landed with wiring, not symbols; record what was built, a mistake corrected, deliberate deviations; move status: done. | S |
Interface contracts
Signatures other packets code against. Each is owned by the packet named in brackets; a
change to one of these is a contract change and goes through precision-integrator, never
a silent edit. Bodies, private helpers and tests are the owning packet’s business.
I.1 pse-ids [K-1, B-canon]
#![allow(unused)]
fn main() {
// id.rs — Copy, Eq, Ord, Hash, Debug; Display = lowercase hex; Debug never reaches a hash
pub struct SemanticId([u8; 16]); // NIL, from_bytes, as_bytes, try_from_slice, to_hex, parse_hex
pub struct ContentHash([u8; 32]); // same; to_prefixed() -> "blake3:<hex>", parse_prefixed
pub struct LogicalHash(pub ContentHash); pub struct EncodingChecksum(pub ContentHash); pub struct SnapshotId(pub ContentHash);
pub struct Ordinal(pub u64); pub struct SchemaVersion(pub u32);
// derive.rs — Hasher::new_derive_key(context); each part = u64le(len) ‖ bytes; XOF first 16 bytes for ids
pub mod context { NAMED = "pse:named:v1"; SYMBOL = "pse:symbol:v1"; EQUATION = "pse:equation:v1"; TERM = "pse:term:v1"; CONN = "pse:conn:v1"; NODE = "pse:node:v1";
REGISTRY = "pse:registry:v1"; STAGE_KEY = "pse:stage_key:v1"; SETTINGS = "pse:settings:v1"; MATHIR_NODE = "pse:mathir:node:v1"; }
pub fn derive_id(context: &'static str, parts: &[&[u8]]) -> SemanticId;
pub fn derive_hash(context: &'static str, parts: &[&[u8]]) -> ContentHash;
pub struct FramedHasher; // new(context); part(&[u8]); str(&str); u16/u32/u64 (LE); bool; id(&SemanticId); hash(&ContentHash); finish_hash(); finish_id()
pub struct IndexTuple<'a>(pub &'a [SemanticId]);
pub fn named_id(package_id: SemanticId, qualified_name: &str) -> SemanticId;
pub fn symbol_instance_id(..) / equation_instance_id(..) / law_term_id(..) / connection_equation_id(..) / mesh_node_id(..) / discretized_symbol_id(..);
// float.rs — hashing copy only (ADR-0030)
pub fn canonical_f64_bits(v: f64) -> u64; // NaN → 0x7ff8_0000_0000_0000; -0.0 preserved
pub fn canonical_f32_bits(v: f32) -> u32;
// frame.rs / encoding.rs
pub trait FrameSink { put_len_prefixed, put_u32_le, put_u64_le, put_fixed } // impl for Vec<u8> and blake3::Hasher
pub fn encoding_checksum(bytes: &[u8]) -> EncodingChecksum; pub struct EncodingHasher; // impl io::Write (tee for writers)
// resource.rs — implemented by pse-runtime over MemoryPool
pub trait MemoryReserver: Send + Sync + Debug { fn open(&self, owner: &str) -> Box<dyn Reservation>; }
pub trait Reservation: Send + Debug { fn try_grow(&mut self, bytes: usize) -> Result<(), ReserveError>; fn shrink(&mut self, bytes: usize); fn size(&self) -> usize; fn release(&mut self); }
pub enum ReserveError { Exhausted { owner, requested, reserved, limit_hint } } // runtime::resource_limit
pub struct FixedBudget; // reference impl for tests/benches
pub struct CancellationToken; // cancel(), is_cancelled(), checkpoint() -> Result<(), CanonError>
// contract.rs — what the canonicalizer consumes (plain values; no registry dependency)
pub struct CanonicalContract { relation_id, schema_version, registry_fingerprint, schema: SchemaRef /* declared, with metadata */, primary_key: Vec<usize>, layouts: Vec<Layout> }
impl CanonicalContract { pub fn try_new(.., primary_key: &[&str], enum_domains: &BTreeMap<FieldPath, Arc<[String]>>) -> Result<Self, CanonError>; pub fn check_batch_schema(&self, &Schema) -> Result<(), CanonError>; }
pub enum Layout { Boolean, Int8..Int64, UInt8..UInt64, Float32, Float64, TimestampNsUtc, Utf8, Binary, FixedSizeBinary(i32), Enum { key: Int8|Int32, members }, List, FixedSizeList, Struct } // everything else rejected at try_new
pub struct FieldPath(String); // "" = schema; "0/2" = registry ordinals joined by '/'
pub struct Envelope { max_rows: u64, max_normalized_bytes: u64 } // PHASE1 = 1_000_000 rows, 256 MiB; lowered() may only tighten
// snapshot.rs — pse.snapshot.v2, plain blake3 over the frame
pub enum SnapshotKind { Model, Case, Stage, Run }
pub struct SnapshotParent { role: String, snapshot_id: SnapshotId }
pub struct SnapshotMember { port, namespace, relation_id, schema_version, logical_hash }
pub struct SnapshotFrame { registry_fingerprint, kind, parents, members }
pub fn snapshot_id(frame: &SnapshotFrame) -> Result<SnapshotId, SnapshotError>; // duplicate role/port → error; Case requires one "model" parent
pub fn model_port_name(namespace: &str, relation_id: SemanticId) -> String; // "<namespace>/<32 hex>"
// canon/ [B-canon]
pub const CANON_VERSION = "pse.canon.v2"; IPC_ALIGNMENT = 64; IPC_METADATA_VERSION = V5;
pub struct CanonicalizeOptions { envelope, keep_preimage, keep_sorted, cancel: Option<CancellationToken> }
pub struct CanonicalOutput { logical_hash, row_count, preimage: Option<Vec<u8>>, sorted: Option<RecordBatch> /* original values, PK-sorted */, reservation: Box<dyn Reservation> }
pub fn canonicalize(contract: &CanonicalContract, batches: &[RecordBatch], reserver: &dyn MemoryReserver, opts: CanonicalizeOptions) -> Result<CanonicalOutput, CanonError>;
pub fn logical_hash(contract, batches, reserver) -> Result<LogicalHash, CanonError>;
pub mod stages { admit_and_order, normalize, metadata_relation, frame_and_hash }
// preimage: len‖"pse.canon.v2" · relation_id · u32le version · fingerprint · len‖metadata_stream · len‖data_stream (each StreamWriter(64,false,V5), one batch, finish())
}
I.2 pse-schema [K-3, A-*]
#![allow(unused)]
fn main() {
pub fn registry() -> Result<&'static Registry, SchemaError>; // OnceLock; assembled from catalog::assemble()
pub struct RelationKey { namespace: Namespace, name: &'static str, version: u32 } // Display "authored.stoichiometry@1"
pub struct RelationSpec { id: SemanticId /* named_id(REGISTRY_PACKAGE_ID, "relation:<ns>.<name>@<v>") */, key, authority, snapshot_class, derivation_granularity: Option<_>, stability, primary_key: Vec<&str>, columns: Vec<ColumnSpec>, doc, fingerprint: ContentHash }
pub struct ColumnSpec { name, logical_type: LogicalType, nullable, quantity: QuantityContract /* None|Column(id)|PerRow */, fk: Option<ForeignKey>, role: ColumnRole, doc }
pub enum LogicalType { F64, I64, I32, U8, U16, U32, U64, Bool, Text, Timestamp, List(Box<_>), FixedList(Box<_>, i32), Struct(Vec<(&str, LogicalType, bool)>), Ext(ExtensionUse) }
pub enum ExtensionUse { SemanticId, ContentHash, DimensionVector, QuantityValue, Bound, IndexTuple, OrdinalRef { target }, SourceSpan, Enum(&str), ExprDsl, TargetPath }
pub struct ExtensionTypeSpec { name, storage: fn() -> DataType, metadata: ExtensionMetadataShape /* VersionOnly|Enum|OrdinalRef */, metadata_version: u32, doc }
pub const EXTENSION_TYPES: [ExtensionTypeSpec; 11];
pub struct EnumSpec { id, name, idaes_source, members: Vec<EnumMember { name, idaes_name, deprecated, doc }> }
pub struct InvariantSpec { id, name, relation, kind: InvariantKind, rule: &str, severity, doc }
pub struct MigrationSpec { relation, from_version, to_version, steps: Vec<MigrationStep>, doc }
pub struct PassSpec { id, name /* "P2" */, version, inputs: Vec<InputPort { port, relation, source: Pinned | Derived { pass, port }, required }>, outputs: Vec<OutputPort { port, relation }>, preconditions, postconditions, determinism, diagnostics, executes_plans: bool }
pub struct RuleSpec { id, name, version, stratum: u16, head: RuleHead /* Relation(name) | Violations { of, key_columns } */, plan: RulePlan, negation, monotonic, conflict_policy }
pub enum RulePlan { Scan { relation, port }, Filter { input, predicate: RuleExpr }, Project, EquiJoin { keys, null_equality }, AntiJoin, Union, Distinct, Aggregate { group, aggregates /* phase 0: Count */ }, Unnest { column, value_name, null_list, empty_list }, Recursive { name, seed, step, is_distinct, depth_bound } }
pub enum RuleExpr { Col, Lit(Cell), And, Or, Not, Cmp, IsNull, IsNotNull, IsDistinctFrom, IsNotDistinctFrom, InList, Field, ListLen, IsTrue, IsFalse, IsUnknown }
pub enum Cell { Null, Bool, I64, U64, F64, Text, Id, Hash, Enum(&str), List(Vec<Cell>), Struct(Vec<Cell>) } // the registry's own row model
pub struct Registry; // BTreeMap-backed: package_id(), fingerprint(), relations() sorted, relation(name), relation_by_id, enums(), enum_spec, logical_types(), invariants(), invariants_for, passes(), pass, rules(), rule, rule_dependencies(), schema_rows() -> Vec<(RelationKey, Vec<Vec<Cell>>)>
pub struct ManifestSpec; // the authority for the Python msgspec Manifest and docs; Rust struct hand-written in pse-catalog (phase 0)
pub mod arrow { pub fn relation_schema(reg, spec) -> Result<Schema, SchemaError>; pub fn field_for(reg, col) -> Result<Field, SchemaError>; } // §4.3 metadata attached once at construction
pub mod membership { pub fn members(reg, class: SnapshotClass) -> Vec<&RelationSpec>; pub fn port_name(spec) -> String; }
pub mod fingerprint { pub const FRAME_VERSION = "pse.schema.fingerprint.v1"; pub fn registry(rows) -> ContentHash; pub fn relation(reg, spec) -> ContentHash; } // derive_hash(context::REGISTRY, ..)
pub mod codegen { pub enum Language { Rust, Python, Markdown } pub struct GeneratedTree { files: BTreeMap<PathBuf, Vec<u8>>, roots: Vec<PathBuf> } pub fn generate(reg, language) -> Result<GeneratedTree, SchemaError>; }
// SchemaError codes: schema::duplicate_declaration, schema::unknown_reference, schema::missing_snapshot_class, schema::missing_granularity, schema::stage_graph, schema::rule_float_key, schema::rule_stratification, schema::codegen
// Registry identity: REGISTRY_PACKAGE_ID = named_id(SemanticId::NIL, "pse.schema"); enums "enum:<Name>"; logical types "logical_type:<rendered>"; invariants "invariant:<relation>:<name>"; passes "pass:<P>@<v>"; rules "rule:<name>@<v>"; rule nodes "rule_node:<rule>@<v>:<preorder path>"
}
I.3 pse-relations [A-7 hand-written; A-6 generated]
#![allow(unused)]
fn main() {
// generated/<namespace>/<relation>.rs — the symbols no_shadow_structs greps
pub const RELATION_ID: SemanticId; pub const NAME: &str; pub const NAMESPACE: Namespace; pub const VERSION: u32; pub const FINGERPRINT: [u8; 32];
pub fn schema() -> SchemaRef; pub fn serde_arrow_fields() -> Vec<FieldRef>; // the latter only after the serde_arrow_roundtrip spike passes
pub struct <Name>Row { .. } // serde derive; enums typed; nested <Name><Field>Item structs
pub struct <Name>View<'a> { .. } impl <Name>View<'a> { pub fn try_from_batch(&'a RecordBatch) -> Result<Self, RelationError>; } // fingerprint metadata + try_extension_type per ext column + validate_batch
pub struct <Name>Builder; // new(), with_capacity(n), push(Row) -> Result<(), RelationError>, finish() -> Result<RecordBatch, RelationError>
pub fn validate(batch: &RecordBatch) -> Result<(), Vec<RelationError>>;
// generated/mod.rs: REGISTRY_FINGERPRINT, relation_by_id(id) -> Option<&'static RelationDescriptor>; generated/enums.rs: as_str/from_str/ordinal/idaes_name/ALL; generated/migrations.rs; generated/conformance_tests.rs
// ext/ — hand-written
pub trait PseExtension: ExtensionType { const SPEC: &'static ExtensionTypeSpec; }
pub struct PseSemanticId; PseContentHash; PseDimensionVector; PseQuantityValue; PseBound; PseIndexTuple; PseOrdinalRef; PseSourceSpan; PseEnum; PseExprDsl; PseTargetPath;
pub struct ExtMetadata { v: u32, enum_id: Option<SemanticId>, target_relation_id: Option<SemanticId> } // canonical_json(); parse(&str) strict
pub struct PseFormatterFactory; // impl arrow display ArrayFormatterFactory
// validate/ — hand-written, driven by the runtime Registry
pub fn validate_field(field: &Field) -> Result<(), Vec<RelationError>>; // recursive; used by the analyzer rule
pub fn validate_schema(spec: &RelationSpec, schema: &Schema) -> Result<(), Vec<RelationError>>;
pub fn validate_batch(spec: &RelationSpec, batch: &RecordBatch) -> Result<(), Vec<RelationError>>; // + fingerprint, dictionary bounds and enum membership, bound kinds, finite values, ordinal syntax, required nulls in structs
pub fn batch_from_cells(reg, spec, rows: &[Vec<Cell>]) -> Result<RecordBatch, RelationError>; pub fn cells_from_batch(..);
pub fn registry_relations::materialize(reg) -> Result<BTreeMap<RelationKey, RecordBatch>, RelationError>; // reference.schema_*, pass_*, rule_* batches
pub trait Migration { relation_id, from_version, to_version, apply(&RecordBatch) -> Result<RecordBatch, RelationError> }; pub fn migrate_to_current(..);
// RelationError codes: schema::fingerprint_mismatch, schema::unknown_registry, schema::storage, schema::extension_type, schema::extension_metadata, schema::unknown_metadata, schema::nullability, schema::enum_member, schema::ordinal_range, schema::arrow
}
I.4 pse-catalog [K-5, B-*]
#![allow(unused)]
fn main() {
pub enum CatalogError { ResourceLimit { consumer, config_keys }, Cancelled, Infrastructure { op, source }, CorruptObject { path, expected, actual }, RefConflict { name }, ManifestInvalid, UnknownRegistry, UnknownVersion, Admission { path, reason }, ForeignSource { table }, Sealed, LogicalHashMismatch, Membership, ConfigInvalid { key, reason }, UserModel, EvaluationError, CompileProperty, Canon(#[from]), Snapshot(#[from]), Reserve(#[from]), Internal }
pub enum PlanOrigin { RuleCompiler, Analytics, KernelUdf }
pub fn classify(err: DataFusionError, origin: PlanOrigin) -> Vec<CatalogError>; // the ONE §23.2 mapping site; unpacks Collection/Diagnostic/Context/Shared
pub struct RelationContract { canonical: CanonicalContract, namespace, name, key_columns: Vec<usize>, enum_columns, unique_sets, encodings: EncodingPolicy /* IpcFile | IpcFileAndParquet */ }
impl RelationContract { pub fn try_new(..); pub fn from_spec(spec: &RelationSpec, fingerprint: ContentHash) -> Result<Self, CatalogError>; pub fn constraints(&self) -> Constraints; pub fn df_schema(&self) -> Result<DFSchema, _>; }
pub enum TrustLevel { Owned, Untrusted }
pub struct LoadedRelation { contract: Arc<RelationContract>, batch: RecordBatch /* one PK-sorted batch */, member: RelationMember }
pub struct Snapshot { manifest: Arc<Manifest>, relations: BTreeMap<(String, String), Arc<LoadedRelation>> } // snapshot_id(), relation(ns, name)
pub struct RefName(String); // ^[a-z0-9][a-z0-9._-]{0,63}$
pub trait Clock { fn now_rfc3339_utc(&self) -> String; } pub struct SystemClock; pub struct FixedClock(pub String);
pub const MANIFEST_VERSION = "pse.manifest.v2";
pub struct Manifest { manifest_version, snapshot_kind, snapshot_id, membership_profile, created_at, schema_registry_fingerprint, relations: Vec<RelationMember>, packages: Vec<PackageRef>, compiler: CompilerRef, engine_profile: Option<ArtifactRef>, numerical_policy: Option<ArtifactRef>, toolchain: ToolchainRef, kernels: Vec<KernelRef>, semantic_parents: Vec<SnapshotParent>, evidence: Vec<EvidenceRecord> }
pub struct RelationMember { port, namespace, relation_id, name, version, logical_hash, rows, encodings: Vec<EncodingRecord { format: ArrowIpcFile|Parquet, writer_version, encoding_checksum, bytes, path }> }
impl Manifest { pub fn validate(&self, registry_fingerprint) -> Result<(), CatalogError>; pub fn frame(&self) -> SnapshotFrame; pub fn encode(&self) -> Result<Vec<u8>, _>; pub fn decode(&[u8]) -> Result<Self, _>; }
// store/ [B-store]
pub struct MemberDraft { port: Option<String>, contract: Arc<RelationContract>, batches: Vec<RecordBatch> }
pub struct BundleDraft { kind: SnapshotKind, members, parents: Vec<SnapshotParent>, packages, compiler, engine_profile, numerical_policy, toolchain, kernels, evidence, set_ref: Option<(RefName, RefUpdatePolicy)> }
pub struct Published { snapshot_id, manifest_checksum, manifest: Arc<Manifest>, ref_head: Option<RefHead> }
pub enum RefUpdatePolicy { Fail, RetryWhileUnchanged { expected_parent: Option<SnapshotId>, attempts: u8 } }
pub struct Catalog;
impl Catalog {
pub fn open(store: Arc<dyn ObjectStore>, registry: Arc<Registry>, trust: TrustLevel, clock: Arc<dyn Clock>) -> Self;
pub async fn publish_bundle(&self, draft: BundleDraft, reserver: &dyn MemoryReserver, cancel: CancellationToken) -> Result<Published, CatalogError>;
pub async fn set_ref(&self, name: &RefName, target: (SnapshotId, EncodingChecksum), policy: RefUpdatePolicy) -> Result<RefHead, CatalogError>;
pub async fn head(&self, name: &RefName) -> Result<RefHead, _>; pub async fn list_refs(&self) -> Result<Vec<RefHead>, _>;
pub async fn snapshot(&self, id: SnapshotId, manifest_checksum: EncodingChecksum) -> Result<Arc<Snapshot>, _>; pub async fn resolve(&self, name: &RefName) -> Result<Arc<Snapshot>, _>;
pub async fn stage_lookup(&self, key: &ContentHash) -> Result<Option<(SnapshotId, EncodingChecksum)>, _>; pub async fn stage_record(&self, key: &ContentHash, target: (SnapshotId, EncodingChecksum)) -> Result<(), _>; // sidecar stages/<key>.json
}
pub fn complete_members(kind, registry, provided: &[(String, &RelationContract, LogicalHash)]) -> Result<Vec<SnapshotMember>, CatalogError>;
// provider/ [B-providers]
pub struct SnapshotCatalogList; pub struct SnapshotCatalog; pub struct NamespaceSchema; pub struct RelationTable; // sealed; RelationTable::new(Arc<LoadedRelation>, SnapshotId), identity() -> ProviderIdentity { snapshot_id, relation_id, schema_version, logical_hash }
pub enum FilterVerdict { Exact, Unsupported } pub fn classify(expr: &Expr, contract: &RelationContract) -> FilterVerdict; // pure
pub fn apply_filters(state: &dyn Session, contract, batch, filters: &[Expr]) -> Result<RecordBatch, CatalogError>;
pub fn statistics_for(relation: &LoadedRelation) -> Statistics; // num_rows Exact; total_byte_size Inexact; rest Absent
// session/ [B-session]
extensions_options! { pub struct PseOptions { null_policy: String = "reject", kernel_outcome_policy: String = "typed_error" } } // prefix datafusion.pse
pub struct ThreadBudget { pool_threads: NonZeroUsize, target_partitions: NonZeroUsize } // validate(): target_partitions ≤ pool_threads
pub struct ExecutionSettings { batch_size, spill_compression: "uncompressed", max_spill_file_size_bytes, sort_spill_reservation_bytes, time_zone }
pub fn settings_hash(allow_list_version: &str, pairs: &[(String, String)]) -> ContentHash; // derive_hash(SETTINGS, name-sorted)
pub struct EngineProfile { engine_profile_id, datafusion_version, arrow_version, analyzer_rules, optimizer_rules, physical_rules, semantic_settings, setting_allow_list_version }
pub fn phase0_reference_profile() -> EngineProfile;
pub struct RuleCatalog; // analyzer(name) / optimizer(name) / physical(name) -> Result<Arc<dyn Rule>, CatalogError>; unknown → ConfigInvalid
pub fn build_extension_registry() -> Result<ExtensionTypeRegistryRef, CatalogError>; // canonical types + eleven pse.* factories; replacement asserted None
pub enum AdmissionPosition { First, Final } pub struct AdmissionRule; // AnalyzerRule "pse.admission.first" | "pse.admission.final"
pub fn admit_plan(plan: &LogicalPlan, registry: &dyn ExtensionTypeRegistry) -> Result<(), CatalogError>; // apply_with_subqueries + expressions; validate_field recursively; TableScan must be a RelationTable
pub fn admit_batch(batch: &RecordBatch, registry) -> Result<(), CatalogError>;
pub struct SessionSpec<'a> { profile: &'a EngineProfile, profile_hash: Option<LogicalHash>, catalogs: SnapshotCatalogList, runtime: Arc<RuntimeEnv>, execution: ExecutionSettings, threads: ThreadBudget, pse: PseOptions, reserver: Arc<dyn MemoryReserver> }
pub fn build_session(spec: SessionSpec<'_>) -> Result<SnapshotSession, CatalogError>;
pub struct SnapshotSession; // sql(query, origin) / execute_plan(plan, origin) -> QueryReport { batches, rules_fired: Vec<RuleFired>, reservation }; table_reference(snapshot_id, ns, name) -> TableReference; read_back_settings(); settings_hash(); profile_hash(); function_registry_hash(); state(); registry()
}
I.5 pse-runtime [K-5, E-*]
#![allow(unused)]
fn main() {
pub struct ResourceBudget { memory_limit_bytes: NonZeroUsize, spill_dir: PathBuf, max_temp_dir_bytes: u64, top_consumers: NonZeroUsize, threads: ThreadBudget, execution: ExecutionSettings, hashing_may_use_pool: bool /* false */ } // validate()
pub enum RuntimeError { ConfigInvalid, Infrastructure, Cancelled, Catalog(#[from]), Reserve(#[from]), Internal }
pub struct SharedRuntime { env: Arc<RuntimeEnv> } // build(&ResourceBudget); reserver() -> PoolReserver; report() -> ResourceReport; pool()
pub struct PoolReserver(Arc<dyn MemoryPool>); // impl MemoryReserver: MemoryConsumer::new(owner).with_can_spill(false).register(pool)
pub struct ResourceReport { limit_bytes, pool_peak_bytes, pool_reserved_now, top_consumers: Vec<(String, usize)>, process_peak_rss_bytes: Option<u64> }
pub struct CancelSource; // new(), token() -> CancellationToken, cancel(), cancelled().await
pub struct SessionFactory; // new(Arc<SharedRuntime>, ResourceBudget); open_session(&Catalog, &[RefName], &EngineProfile, Option<LogicalHash>) -> Result<SnapshotSession, RuntimeError>
}
I.6 pse-quantity [K-4, Q-*]
#![allow(unused)]
fn main() {
pub struct Ratio { num: i16, den: i16 } // reduced; den > 0; checked_add/sub/mul; ZERO, ONE
pub struct DimensionVector([Ratio; 8]); // length, mass, time, temperature, amount, current, luminous_intensity, currency; mul/div/pow/root checked; canonical_bytes() -> [u8; 32]; DIMENSIONLESS
pub enum Opcode { Const, SymbolRef, Add, Sub, Affine, WeightedMean, Mul, Div, Pow, Neg, Abs, Exp, Log, Log10, Sqrt, Sin, Cos, Tan, Asin, Acos, Atan, Sinh, Cosh, Tanh, Erf, SmoothMax, SmoothMin, SmoothAbs, SafeSqrt, SafeLog, Conditional, SumOver, ProdOver, MinOver, MaxOver, Gather, Broadcast, Derivative, Integral, KernelCall, ImplicitRef, UnitConvert, PiecewiseLinear } // ALL: [Opcode; 43]; as_str(); parse()
pub struct UnitId(SemanticId); UnitSetId; QuantityKindId; BasisId; ReferenceStateId; QuantityTypeId; ConversionId; OperationId; DomainId; BoundIndexId; InvariantId;
pub struct BoundIndexRef { bound_index, domain, kind: DomainKind } pub struct IndexSet(BTreeSet<BoundIndexRef>);
pub struct Unit { id, symbol, dimension, scale_to_canonical, offset_to_canonical, is_affine }
pub struct UnitConvertSpec { from, to, scale, offset }
pub fn convert_spec(from: &Unit, to: &Unit, scale_kind: ScaleKind) -> Result<UnitConvertSpec, QuantityError>; // the single source for every UnitConvert edge
pub fn convert_value(spec: &UnitConvertSpec, v: f64) -> f64; // (v*scale)+offset, two roundings, no FMA
pub struct UnitSet { id, base: [Option<UnitId>; 8] } // new(..) rejects affine/wrong-dimension base units; derived_unit(dim) -> DerivedUnit
pub struct ConversionRule { id, from, to, kind: ConversionKind, kernel, required_parameters, scale, offset }
pub struct QuantityKind { id, dimension, extensive, addition_kind: Additive | OriginSensitive }
pub struct QuantityTypeKey { kind, basis, reference_state, scale_kind: Point | Difference, shape: Vec<DomainKind>, subject_kind }
pub struct QuantityType { id, key, canonical_unit, nominal_magnitude } // is_neutral_scalar(reg)
pub struct QuantityRegistryBuilder; // unit/kind/basis/reference_state/quantity_type/conversion/operation/unit_set; build() runs load-time invariants
pub struct QuantityRegistry; // unit(), unit_by_symbol(), quantity_type(), resolve_key(&QuantityTypeKey) -> Result<QuantityTypeId>, operations_for(Opcode), conversions_between(), neutral_dimensionless()
pub struct QuantityOperation { id, opcode, input_kinds, result_kind, basis_rule, reference_rule, scale_rule, shape_rule, *_source, subject_rule, result_*, input_conversions, precondition_invariants }
pub struct Operand<'a> { quantity_type: QuantityTypeId, indices: &'a IndexSet }
pub enum OpRequest<'a> { Literal { unit, context: LiteralContext }, Add, Sub, Neg, Abs, Affine { term_signs, has_constant }, WeightedMean { normalization, certified_invariant }, Mul, Div, Pow { exponent: Rational(Ratio) | Symbolic }, Sqrt, Transcendental(Opcode), Smooth { opcode, eps }, Conditional, Reduce { kind, bound }, Gather { group_shape, coordinates }, Broadcast { index }, Derivative { domain_unit, domain_kind, order }, Integral { .. }, UnitConvert { spec }, KernelCall { declared_inputs, declared_output }, ImplicitRef { unknown }, PiecewiseLinear { .. } }
pub enum LiteralContext { Free, Additive { sibling: QuantityTypeId }, EquationSide { side: QuantityTypeId } }
pub enum OperationSelection { BuiltIn(BuiltInRule), Registered { operation: OperationId, operand_permutation: Vec<u16> } }
pub struct Inferred { result: QuantityTypeId, indices: IndexSet, selected: OperationSelection, conversions: Vec<OperandConversion { operand, conversion }> }
pub fn infer(request: &OpRequest<'_>, operands: &[Operand<'_>], reg: &QuantityRegistry) -> Result<Inferred, QuantityError>;
pub fn require_same_contract(expected: QuantityTypeId, actual: QuantityTypeId, reg) -> Result<(), QuantityError>; // quantity half of head_conversion_exactness
pub mod numeric { exact_i64_from_f64, exact_f64_from_i64, approx_eq(a, b, atol, rtol) /* §7.3 */, is_zero }
#[cfg(feature = "fixtures")] pub mod standard { pub fn standard_registry() -> QuantityRegistry; pub mod ids; }
// QuantityError codes: validation::invariant (registry, dimension, unknown id), compile::math::quantity_operation_unsupported, compile::math::unit_inconsistent, compile::math::domain_violation_static
}
I.7 pse-mathir [K-4, M-*]
#![allow(unused)]
fn main() {
pub use pse_quantity::Opcode;
pub struct NodeId(pub u64);
pub struct Node { opcode, payload: Payload, children: Vec<NodeId>, quantity_type: Option<QuantityTypeId> /* None until P10 */, scope: Option<SemanticId> }
pub enum Payload { None, SymbolRef { symbol }, FloatConst { value /* finite */, unit }, IntConst { value }, Affine { constant, terms: Vec<AffineTerm { coefficient, child }> }, WeightedMean { pairs: Vec<(weight, value)>, normalization, unit_sum_invariant }, Reduction { kind, domain, bound_index, filter }, Gather { group, coordinate_map }, Broadcast { domain, bound_index }, Derivative { wrt_domain, order }, Integral { domain, quadrature_policy }, SmoothOp { eps }, Conditional { guard }, KernelCall { kernel_binding, output_ordinal }, ImplicitRef { implicit_system, unknown_ordinal }, UnitConvert(UnitConvertSpec), PiecewiseLinear { breakpoints, input, output } }
pub struct ExprGraph; // new(); insert(opcode, payload, children, scope) -> Result<NodeId, MathIrError> (arity per OPERATOR_TABLE, literal finiteness, consing on structural hash); node(id); iter(); set_quantity_type() (P10 only); convenience constructors
pub struct OperatorSpec { opcode, family: OperatorFamily, arity, shape_rule, derivative_rule, argument_evaluation, failure_classes, domain_restrictions, smoothness, convexity_rule, monotonicity_rule, sparsity_rule, rewrite_conditions, lowering, foldable }
pub static OPERATOR_TABLE: [OperatorSpec; 43]; pub fn operator_spec(op) -> &'static OperatorSpec;
pub trait OperatorSpecSink { fn operator_spec(&mut self, opcode, arity, quantity_operation_ids: &[OperationId], ..); } pub fn emit_operator_specs(reg: &QuantityRegistry, sink: &mut dyn OperatorSpecSink);
pub fn structural_hash(node, child_hashes) -> ContentHash; pub fn subtree_hash(node, quantity_type, child_hashes) -> ContentHash; // derive_hash(MATHIR_NODE, ..); scope excluded
pub struct CanonicalGraph; // node(id) -> &CanonicalNode { opcode, payload, children, quantity_type: QuantityTypeId, scope, subtree_hash, free_indices }; roots(); equations(); selections(); listing() -> String
pub fn fold_literals(graph: &mut ExprGraph, reg) -> Result<FoldReport { folded, deferred_static_checks }, MathIrError>;
pub trait SymbolTypeSource { symbol_type(symbol) -> Option<QuantityTypeId>; group_shape(group); domain(domain) -> Option<DomainFacts>; kernel_contract(binding); implicit_unknown(system, ordinal) }
pub struct FreeIndex { bound_index, domain, position }
pub struct EquationRecord { indexed_equation_id, owner_instance, equation_decl, qualified_name, product, filter, body, sense, lower, upper, free_indices, residual_quantity_type, law_instance, derivation }
pub struct QuantitySelection { node, selected, conversions, deferred_static_check }
pub enum Policy { Strict }
pub struct CanonicalizeInput<'a> { graph, equations, roots, symbols: &'a dyn SymbolTypeSource, registry: &'a QuantityRegistry }
pub fn canonicalize(input: CanonicalizeInput<'_>, policy: Policy) -> Result<CanonicalGraph, MathIrError>;
pub trait MathRelationSink { expr_node, expr_arg, symbol_ref, float_constant, int_constant, affine, weighted_mean, reduction, gather, broadcast, derivative, integral, smooth_op, conditional, kernel_call, implicit_ref, unit_convert, indexed_equation, free_index, quantity_selection } // primitive-typed methods; no row structs
pub trait MathRelationSource { /* mirror iterators */ }
pub fn emit(graph: &CanonicalGraph, sink); pub fn emit_untyped(graph: &ExprGraph, roots, sink); pub fn load_untyped(src) -> Result<(ExprGraph, Vec<EquationRecord>), _>; pub fn load_canonical(src) -> Result<CanonicalGraph, _>;
pub struct VecSink; // test double implementing both traits
// MathIrError codes: compile::math::cyclic_expression, compile::math::unit_inconsistent (Quantity is #[diagnostic(transparent)]), compile::math::domain_violation_static, compile::math::quantity_operation_unsupported (UnboundIndex, UnknownBinding), internal::invariant (Malformed)
}
I.8 pse-authoring [K-3, C-dsl, C-1, C-2]
#![allow(unused)]
fn main() {
pub struct SourceSpan { document_id: SemanticId, start: u32, end: u32 }
pub struct ParseBudget { max_depth, max_aliases, max_bytes }
pub struct Document { document_id /* named_id(package_id, path) */, package_id, path, kind, bytes, content_hash }
pub struct DocumentBundle { header: PackageHeaderDoc /* generated */, header_span: SpanIndex, documents: Vec<(Document, SpanIndex)> }
pub fn load_package(dir: &Path, budget: &ParseBudget) -> Result<DocumentBundle, AuthoringError>;
pub struct SpanIndex; // span(&DocPath) -> Option<SourceSpan>; DocPath = "/templates/0/symbols/3"
pub mod ids { pub fn entity_id(policy: IdPolicy, package_id, explicit: Option<&str>, qualified_name, at: SourceSpan) -> Result<SemanticId, AuthoringError>; pub fn assign_ids(bundle, next: &mut dyn FnMut() -> SemanticId) -> Result<Vec<DocumentEdit>, AuthoringError>; pub fn uuid_v7() -> SemanticId; }
pub mod dsl {
pub struct Span { start: u32, end: u32 }
pub struct Expr { kind: ExprKind, span } pub enum ExprKind { Number(Number), Path(Path), Neg, Binary { op: Add|Sub|Mul|Div|Pow, lhs, rhs }, Call { function: Function, args, named: Vec<NamedArg /* eps */> }, Kernel { name, args }, Reduce { kind: Sum|Prod|Integral, binder: Binder { var, domain: Path, filter: Option<Predicate> }, body }, Derivative { body, wrt }, Conditional { guard: Predicate, then, otherwise }, Let { bindings, body } }
pub struct Predicate { kind: Compare | In | And | Or | Not | Atom | Null, span } pub struct Equation { lhs, sense: Eq|Le|Ge, rhs, span }
pub fn parse_expr(&str) -> Result<Expr, DslError>; parse_equation; parse_predicate; render_expr(&Expr) -> String; render_equation; render_predicate;
impl Expr { walk, paths() -> Vec<&Path>, structural_eq (spans ignored, f64 by bits), strip_spans }
pub enum DslError { Syntax { offset, span, expected, found }, WeightedMeanArity, AmbiguousUnaryPower, NonFiniteNumber, Budget } // authoring::parse
}
pub mod change_set { pub struct ChangeSet { change_set_id, base_revision_id, author, message, created_at_ns, ops: Vec<ChangeOp>, staged: BTreeMap<SemanticId, RecordBatch> } pub enum ChangeOp { Insert { relation_id, row_key, staged_ordinal }, Update, Delete, Rename { entity_id, name, qualified_name } }
pub trait AuthoredReader { fn batch(&self, relation_id) -> Result<Option<RecordBatch>, AuthoringError>; } // implemented by pse-compiler over a Snapshot
pub struct CandidateSnapshot { relations: BTreeMap<SemanticId, RecordBatch>, change_sets, change_ops }
pub fn apply(base: &dyn AuthoredReader, cs: &ChangeSet, reg) -> Result<CandidateSnapshot, AuthoringError>; }
pub mod p0 { pub fn resolve(bundles: &[DocumentBundle], reg) -> Result<PackageGraph, AuthoringError>; }
pub mod p1 { pub fn stage(bundle, base: &dyn AuthoredReader, reg) -> Result<ChangeSet, AuthoringError>; }
pub mod targets { pub fn parse(text, at) -> Result<TargetPath, _>; pub fn resolve(&TargetPath, &TargetContext) -> Result<Vec<TargetRow>, _>; }
// AuthoringError codes: authoring::parse::{syntax, unknown_key, missing_id, unresolved_target, budget}, authoring::reference::{derived_write, rename_named, unknown_row_key}, authoring::pkg::{unresolved, version_conflict}, schema::version_mismatch
}
I.9 pse-rules and pse-compiler [K-3, C-3, C-4]
#![allow(unused)]
fn main() {
// pse-rules
pub struct PortBinding { ports: BTreeMap<&'static str, (SnapshotId, SemanticId)> }
pub struct CompiledRule { rule_id, plan: LogicalPlan, head_schema: SchemaRef, key_columns: Vec<String> }
pub fn compile(rule: &RuleSpec, binding: &PortBinding, session: &SnapshotSession, reg) -> Result<CompiledRule, RuleError>;
pub struct RuleOutcome { head: RecordBatch /* PK-sorted */, undecided: RecordBatch, explain_pgjson: String, rules_fired: Vec<RuleFired> }
pub async fn execute(rule: &CompiledRule, session, reg) -> Result<RuleOutcome, RuleError>;
pub struct InvariantReport { findings, undecided, derivations, explain, rules_fired }
pub async fn run_invariants(snapshot: &Snapshot, session, reg, scope: Model | Case | Registry) -> Result<InvariantReport, RuleError>; // the P2 body
pub mod errmap { pub fn classify(err: DataFusionError, origin) -> RuleError; } // delegates to pse_catalog::classify
// RuleError codes: rule::float_key, rule::head_schema_mismatch, internal::invariant, runtime::resource_limit, runtime::infrastructure, config::invalid
// pse-compiler
pub trait Pass: Send + Sync { fn spec(&self) -> &'static PassSpec; fn run(&self, ctx: &PassContext<'_>, inputs: &InputBundle) -> Result<PassOutput, CompilerError>; }
pub struct PassContext<'a> { registry, session: Option<&'a SnapshotSession>, policies: &'a PolicySet, runtime: &'a tokio::runtime::Handle, external: &'a ExternalInputs }
pub struct InputBundle { ports: BTreeMap<&'static str, Option<BoundInput { snapshot, relation_id, schema_version, logical_hash, handle }>> } // None = explicit absence (enters the key)
pub struct PassOutput { ports: BTreeMap<&'static str, Vec<RecordBatch>> /* every declared port, empties explicit */, findings, record: PassRecordDraft }
pub struct StageKey(pub ContentHash);
pub fn stage_key(spec, inputs, reg, policies, engine: Option<(ContentHash, ContentHash)>) -> StageKey; // derive_hash(STAGE_KEY, pass_id, version, registry_fingerprint, ∀port sorted: port, relation_id, version, 0x01‖hash | 0x00, ∀policy sorted, [profile_hash, function_registry_hash])
pub struct StageDag; // build(reg); validate() -> Result<(), Vec<CompilerError>>; topo_order(); producer(relation, port)
pub struct Driver; // new(Arc<Catalog>, Arc<SharedRuntime>); commit(&DocumentBundle, base_ref) -> CommitReport; run(PipelineRequest { through: "P3"|"P10", snapshot, policies, engine_profile, external_bindings /* tests only */ }) -> PipelineReport
// CompilerError codes: compile::stage_graph, internal::invariant, runtime::resource_limit, runtime::infrastructure + re-exported authoring/rule errors
}
Progress ledger (living; updated 2026-09-14)
Landed on main (2026-09-14, squash commit d375a6a, PR #9). Everything the rows
below describe — the revision-5 amendment with ADR-0039–0048 accepted, ADR-0049–0051, the
five keel packets K-1…K-5, the maintainer’s prompt-free permissions and hook update, and
the docs/book.toml link fix — is on main in one squash. The stacked PRs #2–#8 were
consolidated because GitHub closed the amendment PR when PR #1’s branch was deleted and
marked the packet PRs as merged into their intermediate bases; the merges used the
maintainer’s admin bypass without awaiting CI (governance issue #12). Conflict resolution
was proved lossless (main equalled the branch base for every conflicting file). Tier 1
starts from origin/main: one branch and one PR per packet, base main, never stacked.
| Item | State | Evidence / where |
|---|---|---|
| Stage 0 — merge PR #1 | awaiting maintainer | chore/complete-repository-setup, every reported check green |
| Stage 0 — revision-5 amendment, ADR-0039–0048 accepted | PR #2 open (stacked on #1) | adr/0039-revision-5-contracts; just adr-lint, just docs, just lint-repo, just lint-agents at zero |
K-1 pse-ids core | PR #4 open (stacked on #2) | wave1/k1-ids; Tested: just test-package pse-ids -p pse-relations 87 passed / 0 failed; golden vectors frozen and recorded in ADR-0050 |
| K-2 decisions, revision-6 rows, manifests, scaffolding | PR #3 open (stacked on #2) | wave1/k2-decisions; ADR-0049/0050/0051 accepted; just governance 19/19, just test 23/23, clippy both legs clean; Cargo.lock gained dependency edges only |
| Keel integration branch | pushed | wave1/keel-int = #2 + K-2 + K-1 + K-4 + the ADR-0050 vector table; Tested there: cargo fmt --check, just check, just clippy (both legs), just test 110 passed / 0 failed before K-4, just doctest; taplo’s fmt-check from the main checkout walks into nested worktrees (false positive) |
K-3 pse-schema keel | PR #7 open (base wave1/keel-int; merged into the integration branch) | wave1/k3-schema; Tested: just test-package pse-schema -p pse-relations 52 passed / 0 failed; error_taxonomy now sees five real error enums |
| K-4 leaf keels | PR #5 open (base wave1/keel-int; merged into the integration branch) | wave1/k4-leaf; Tested: just test-package pse-quantity/pse-mathir/pse-material -p pse-relations 36 + 34 + 11 passed, 0 failed; workspace just test 191 passed |
| K-5 engine keels | PR #6 open (base wave1/keel-int; merged into the integration branch) | wave1/k5-engine; Tested: just test-package pse-catalog -p pse-relations 71 passed, pse-runtime 7 passed, 0 failed |
| Keel integration (all five packets) | green | wave1/keel-int at 2026-09-14: cargo fmt --check, just check, just clippy (both legs), just test 324 passed / 0 failed (force-validate), just doctest, just governance, just docs + offline lychee (0 errors) |
| Tiers 1–4 | not started | dispatch after the keel PRs reach main |
Resuming after an interruption. The three K-3/K-4/K-5 worktrees keep their files on disk; re-dispatch the same packet prompt with the instruction to continue from the worktree’s current state (git status, existing files) rather than starting over. Once PR #1 → #2 → #3/#4 are merged, rebase wave1/keel-int and every open wave1/* branch onto main (git rebase --onto main <old-base> <branch>) and retarget their PRs to main (gh pr edit --base main).
Findings surfaced by the keel packets (not yet acted on):
tests/governance/tests/banned_patterns.rs: theformat!("{:?}"ban cannot fire becausecommon::code_onlystrips string literals before matching — fix in packet R-2 (owner of the governance tests).just adr-new <slug> --title "multi word"splits the title ({{ args }}unquoted in the recipe); usepython3 scripts/adr.py new …directly until achore(just)fix lands.CanonicalContract::try_newadmits an empty primary key; the registry (K-3/A-5) must reject keyless relations.- Worktrees have no
.venv; pre-commit hooks and Python recipes needUV_PROJECT_ENVIRONMENT=/home/paul/pse-arrow/.venv; every packet must use a worktree-localCARGO_TARGET_DIRbecausextaskembeds its root at compile time and the shared target directory uplifts one binary. - No independent BLAKE3 implementation is available locally; the ADR-0050 vectors are self-consistent, not cross-checked (
b3sumat wave exit). - Agent-tool worktrees start at
origin/main, not the current branch; every packet prompt must state its base branch explicitly. - mdBook’s default
indexpreprocessor renderedREADME.mdchapters asindex.html, so cross-links to the revision-4 evidence README faileddocs / build;docs/book.tomlnow keeps only thelinkspreprocessor (fixed on PR #2’s branch and merged forward). - The
mainruleset requires verified signatures; local commits are SSH-signed with a key GitHub does not know (UNKNOWN_KEY), which blocks every merge until the signing key is registered on the account. - Keel contract deviations recorded by the packets for their consumers:
schema_columns.quantity_type_idis minted from the qualified name (A-1 reconciles withreference.quantity_types);rule_expr_idformula fixed in K-3 (A-3 adopts it);RateBasismembersper_volume | per_mass | per_area(A-1 declares the same);MathIrError::Quantitycarriescompile::math::unit_inconsistentwith the quantity error asdiagnostic_source;ArtifactRefserializes its identity asid.
Verification
Phase-0 exit per blueprint §25, each with the command that proves it. Baseline zero for
every command and mode; Tested claims name the test and its conditions.
| Exit criterion (§25) | Proof | Command |
|---|---|---|
| schema governance tests green | no_shadow_structs non-vacuous, codegen_regeneration, no_mutating_providers, registry_governance, error taxonomy over real enums, pins incl. the new §3.1 rows | just governance |
| expression DSL round-trips | dsl_roundtrip proptest (1024 cases, committed seed) and dsl_examples snapshots | just test-package pse-authoring -p pse-relations |
pse.canon.v2 metamorphic fixtures + active semantic query admission | canonical_null_equivalence, encoding_roundtrip_identity, snapshot_membership, semantic_admission_paths (E1 as a regression) | just test-package pse-tests-conformance -p pse-relations; just test-package pse-tests-engine -p pse-relations |
| storage identity/integrity fault matrix | publication_fault_matrix; the three budget fixtures | just test-package pse-tests-lifecycle -p pse-relations |
| quantity/ordered-IR contract fixtures | quantity_composition, head_conversion_exactness (quantity half in Q-4, head half in C-3), numerical_policy_conformance (P10 subset) | just test-package pse-tests-conformance -p pse-relations |
| a complete snapshot published, read back and queried | golden stores registry and minimal_explicit: publish → open → SELECT through the catalog; pushdown_vs_unpruned; incremental_equals_clean (P0–P3) | cargo xtask golden registry && cargo xtask golden minimal_explicit; just test-package pse-tests-engine -p pse-relations |
| generated trees current | the four generated paths regenerate byte-identically | just codegen-check |
| Python boundary | generated contracts pass the Any lint, extension round trip with the revision-6 metadata shapes, GENERATED.sha256, golden registry readable through generated contracts | just py-sync && just py-test && just quality |
| benchmarks build and run | canonicalization criterion group replaces the placeholder | just bench-smoke |
just ci-pr green locally at every tier boundary and at exit; every required check green
on each packet PR; just features-powerset and just test-release run once at exit
(scheduled jobs, run locally for the record).
Open items
Decided by recommendation in K-2 unless the maintainer overrides at plan approval:
- Extension metadata shapes.
pse.enumcarries{"v":1,"enum_id":…}andpse.ordinal_refcarries{"v":1,"target_relation_id":…}in the extension’s own metadata (revision 6); the Python placeholder types change accordingly. authored.change_ops.rowbecomesstruct<staged_port, staged_ordinal>pointing into per-relation staged members of the change-set bundle (revision 6); JSON text rejected (D1).- Appendix B relations without column lists (
instance_equations,sweep_results,profile_results,run_state; partialtemplate_scaling_defaults,diagnostic_thresholds,cost_indices,location_factors) are listed intests/governance/appendix_b_deferred.tomlwith reasons, not invented — exceptnormalized.package_graph, which P0 needs and revision 6 specifies. - Boolean guards. Phase 0 accepts a
SymbolRefof a registered boolean kind or anIntConst0/1 as aConditionalguard; comparison/logical opcodes are a later §7.2 amendment. - Literal ambiguity (
T − 300{K}): a unit literal beside an origin-sensitive sibling is that kind’s difference; beside an equation side it takes the side’s type; a bare literal resolves only when exactly one registered type matches. Documented as a rule. - Derived units per unit set (slice A): register the derived units a unit set uses as
reference.unitsrows in the units package; no mintedinferred.derived_unitsin phase 0. Affineis constructible and typed but not emitted by P3 or synthesised by P10 in phase 0 (§7.4 step 2); it is P8’s substrate later.- Scope on shared nodes is excluded from the node hash; conflicting scopes collapse to
None. Revisit before P7 if per-scope ownership is needed. - Rust
Manifeststruct is hand-written in phase 0 with a parity test against the registry’sManifestSpec; generating it is a wave-2 item under ADR-0051. - mmap for IPC artifacts is not adopted (read via
get_opts→Bytes→ zero-copyBuffer, nounsafe); register row under R-24 with a measured trigger. datafusion-sessionis not declared; the umbrella’sdatafusion::catalog::*re-exports suffice.- B-evidence and R-3 (bindgen arm) are optional in this wave; if they slip they get
register rows, and
bindings.rskeeps the hygiene-only check. - Wave 2 preview (not planned here): P4–P9 rule execution with strata and conflict
detection,
pse-templates, the reference units/elements YAML package as the authority for Q-4’s fixture (with thestandard_fixture_matches_yamlgovernance test),pse.openover the catalog throughpyo3-arrow, Rust manifest generation,renamere-rendering ofexpr_dslfrom normalized graphs.
Outcome (recorded after implementation)
What was built
A mistake made and corrected
Deviations from the plan, deliberate
Data Model–Based Design Charter
Version 1.0 · September 13, 2026
Purpose: A technology-neutral design and review framework for systems that make explicit data models the source of structure, behavior, constraints, and execution intent.
Make meaning authoritative and explicit; derive implementation mechanisms from that meaning; preserve the contract across every representation and lifecycle stage.
0. How to use this charter
Read the governing objective and the scope of the proposed change first. Consult the relevant principle groups, then use the review rubric and companion review template. The stable IDs are intended for architecture decisions, agent instructions, issue descriptions, code reviews, and regression checks.
This is a normative design proposal, not a claim that any storage format, query engine, programming language, or framework supplies these guarantees automatically. The principles apply at different scales: a small library can satisfy them without building a compiler platform or deploying multiple services.
MUST means an applicable required contract. A design must not claim alignment while violating it. When an indispensable requirement cannot yet be met, narrow the supported scope or mark the design incomplete against that requirement. SHOULD is a strong default; deviations require an explicit rationale and a safer or more proportionate alternative. Examples of machinery are conditional options, not mandatory architecture.
A principle can be not applicable only for a stated reason tied to scope. An unimplemented requirement is not the same as an irrelevant principle. Distinguish design-time claims from implemented and tested guarantees.
The governing objective
Optimize for semantic explicitness, enforceable correctness, consistent authority, reusable derivations, local changes, and explainable execution, subject to the system’s actual performance, operational, and maintenance requirements.
Minimize the number of independently maintained semantic decisions. Do not maximize the number of tables, eliminate every handwritten function, or minimize line count at the expense of meaning. A specialized algorithm behind a complete contract is aligned; a declarative-looking wrapper around hidden business rules is not.
What “data model–based” means
| Concept | Required interpretation |
|---|---|
| Semantic model | The explicit concepts, types, relationships, constraints, operations, and policies that define what the system means. |
| Schema | A machine-readable contract for a representation, supplemented by semantic and relational invariants where a physical schema alone is insufficient. |
| Canonical authority | The designated source for a fact within a declared scope and revision; not necessarily one universal file, table, service, or physical layout. |
| Declaration | A statement of structure, requirement, relationship, or permitted behavior, independent of incidental construction sequence. |
| Binding/specification | A selection or assignment that applies a reusable definition to a particular context or case. |
| Derived representation | A view, plan, graph, index, layout, or artifact produced from declared inputs through a versioned transformation. |
| Execution state | Mutable or immutable state belonging to an identified attempt, with explicit ownership and publication rules. |
| Evidence | Observations, tests, derivations, measurements, or verified contracts supporting a design claim. |
The aim is one coherent logical substrate and authority model, with as many physical representations as justified. Observations and external effects are not magically derivable from declarations: they must enter as explicit inputs or recorded outcomes.
Five design questions to answer before choosing technology
- What meanings and distinctions must survive every representation?
- Which facts are authoritative, which are bindings, and which are derived or observed?
- Which invariants and effects govern valid changes and executions?
- Which transformations create execution representations, and under what assumptions?
- Which ordinary changes should become declarations rather than coordinated procedural edits?
Principle index
| Topic | Principle IDs |
|---|---|
| 1. Semantic authority and the modeling boundary | DM-01–DM-05 |
| 2. Semantic types, schemas, and invariants | DM-06–DM-10 |
| 3. Identity, versions, and consistency | DM-11–DM-15 |
| 4. Declarative composition and reusable structure | DM-16–DM-20 |
| 5. Compilation, derivation, and semantic preservation | DM-21–DM-25 |
| 6. Planning, execution, mutable state, and effects | DM-26–DM-30 |
| 7. Dependencies, incrementality, and concurrency | DM-31–DM-35 |
| 8. Execution representations and performance | DM-36–DM-40 |
| 9. Boundaries, providers, and extensibility | DM-41–DM-45 |
| 10. Provenance, reproducibility, and explainability | DM-46–DM-50 |
| 11. Evolution, generation, and verification | DM-51–DM-55 |
| 12. Architectural leverage and disciplined improvement | DM-56–DM-60 |
Principles
1. Semantic authority and the modeling boundary
DM-01 — Make meaning—not storage shape—the primary model
MUST. Define the concepts, distinctions, relationships, obligations, and permitted operations that give the system meaning before selecting their physical representation. A field list is not a complete model: the interpretation of those fields and the rules governing their combinations belong to the contract. Preserve distinctions that affect correctness, even when their values share the same primitive representation.
Assessment test: Can a reviewer determine what each important fact means, which operations are valid, and what constitutes an invalid state without reverse-engineering implementation code?
Warning sign: A richly structured table whose important interpretation exists only in comments, naming conventions, or application branches.
DM-02 — Assign one authority to each semantic fact and revision
MUST. Identify one authoritative definition for every semantic fact within a declared scope and revision. Other representations must be derived, reconciled through an explicit authority protocol, or declared independent observations. Do not let caches, adapters, runtime objects, and configuration files become independently editable definitions of the same fact.
Assessment test: For any disputed value or rule, can the system identify the authoritative source, its revision, and the permitted update path?
Warning sign: Two individually reasonable components maintain different versions of the same business rule or structural definition.
DM-03 — Unify logical contracts without mandating one physical structure
MUST. A unified substrate means coherent semantics, identity, versioning, and interchange across the system. It does not require one universal table, one object graph, one process, or one storage format. Permit specialized physical representations when their relationship to the canonical model is explicit and they cannot silently become competing authorities.
Assessment test: Can each representation state what it owns, what it derives, and how it relates to canonical identities and versions?
Warning sign: Either a universal key-value structure erases meaning, or specialized subsystems invent incompatible definitions.
DM-04 — Declare the semantic boundary and expose opaque behavior
MUST. Specify what the model describes and what remains an external input, specialized implementation, or intentionally opaque component. Decision-relevant behavior outside the declarative language must enter through a registered contract with explicit dependencies, effects, assumptions, and limitations. Do not claim complete model coverage while hiding material rules inside callbacks.
Assessment test: Can a reviewer enumerate the important behavior that is not inspectable in the model and explain the contract governing each exception?
Warning sign: An architecture claims to be fully declarative, but arbitrary scripts determine outputs behind an opaque operation name.
DM-05 — Separate intent from mechanisms and incidental technology
SHOULD. Represent the desired structure, relationships, constraints, and outcomes independently of a particular class hierarchy, query engine, deployment topology, or execution backend. Select mechanisms through explicit policy and lowering decisions. Technology-specific facts are legitimate at the appropriate stage; they should not leak upward and redefine portable intent.
Assessment test: Could another conforming backend consume the same intent without changing its meaning, even if that backend has different capability limits?
Warning sign: Public model definitions mirror one framework’s internal objects or require callers to reproduce its construction sequence.
2. Semantic types, schemas, and invariants
DM-06 — Type semantic distinctions, not only machine representations
MUST. Use types or explicit semantic references for distinctions that change interpretation: role, category, coordinate system, reference convention, ordering, basis, scope, and shape. Define the operations and conversions these distinctions allow. When a distinction cannot be statically enforced, validate it before the affected operation executes.
Assessment test: Could two values with the same primitive type be interchanged incorrectly without the system rejecting or explicitly converting them?
Warning sign: Strings, floats, or integer IDs are treated as interchangeable because their physical encodings match.
DM-07 — Make validity rules explicit and enforce them at identified boundaries
MUST. Declare local, relational, cross-field, conditional, and lifecycle invariants. Identify the layer that enforces each invariant and the point at which it must hold. Structural validation alone is insufficient when semantic compatibility or cross-record consistency affects correctness. A schema registry must not be credited with enforcement that no implementation performs.
Assessment test: For each critical invariant, can the reviewer point to an enforcement mechanism, failure behavior, and negative test?
Warning sign: An invariant is documented or annotated but ordinary write or execution paths can bypass it.
DM-08 — Represent absence, unknowns, uncertainty, invalidity, and failure distinctly
MUST. Do not overload nulls, empty strings, zeros, NaNs, or missing records with incompatible meanings. Model distinctions such as not supplied, not applicable, not yet computed, deliberately unknown, uncertain, invalid, and failed when they affect behavior. Separate an unknown to be determined from the absence of an initial value or observation.
Assessment test: Can callers distinguish all materially different states without inferring meaning from a sentinel or undocumented convention?
Warning sign: A missing value is interpreted as a default in one subsystem, an unknown in another, and a failure elsewhere.
DM-09 — Model relationships and valid domains explicitly
MUST. Represent ownership, membership, connectivity, ordering, dependencies, and correspondence with explicit roles, cardinalities, and valid indexing domains. Do not assume that all combinations of otherwise valid values are themselves valid. Prefer normalized authoritative facts; use denormalized execution views when beneficial and derive them through explicit rules.
Assessment test: Are valid combinations and relationship meanings declared, or inferred from coincidental row positions, matching labels, or unrestricted Cartesian products?
Warning sign: Parallel arrays, position-based joins, or string parsing encode relationships that should have their own contracts.
DM-10 — Keep important structure typed and queryable
SHOULD. Use dedicated fields, relations, and tagged alternatives for stable, decision-relevant structure. Reserve maps, free text, and opaque blobs for genuinely open-ended content or well-defined boundaries. Promote a previously opaque field when validation, joins, transformations, or execution decisions begin to depend on its internal meaning.
Assessment test: Which important queries or checks require parsing arbitrary text or untyped payloads, and why has that structure not been modeled?
Warning sign: An apparently schema-first system is an entity-attribute-value store or JSON envelope with application-specific interpretation everywhere.
3. Identity, versions, and consistency
DM-11 — Use stable semantic identity independent of physical location
MUST. Give persistent entities identities whose meaning does not depend on names, file paths, row order, memory addresses, storage partitions, or backend-local indices. Treat those properties as attributes or implementation details. Define identity behavior under rename, move, duplication, split, merge, and regeneration where those operations exist.
Assessment test: Do identities and references survive harmless reordering, repartitioning, renaming, and serialization round trips?
Warning sign: A row index, dictionary encoding, mutable path, or process-local pointer is used as a durable entity identifier.
DM-12 — Distinguish entity, revision, artifact, and execution identity
MUST. An enduring entity, a particular revision, an encoded artifact, and an execution attempt answer different questions. Represent them separately and define their relationships. Content equality does not necessarily imply entity identity, and a repeated run does not necessarily create a new model revision. Make identity scope explicit.
Assessment test: Can the system separately answer “which thing,” “which definition,” “which stored representation,” and “which attempt”?
Warning sign: One UUID or hash is expected to identify both a logical entity and every evolving state and execution associated with it.
DM-13 — Separate definitions, specifications, policies, observations, and results
MUST. Distinguish what exists, how it is bound or configured, which policy selects behavior, what has been observed, and what an execution produced. Version these concepts according to their own lifecycles while recording compatibility and dependency links. Do not let a computed result silently overwrite the declaration or input from which it was obtained.
Assessment test: Can a new case or execution reuse the same definition, and can a result be traced without mutating that definition?
Warning sign: Configuration, runtime state, user intent, measurements, and cached output occupy one mutable object with unclear update semantics.
DM-14 — Publish semantically consistent revisions through explicit commit boundaries
MUST. Readers must know which coherent revision they are observing. When a change spans several authoritative structures, validate and publish it as a consistent unit at the level required by the application. Use snapshots, transactions, manifests, or equivalent protocols; do not imply that individually atomic writes provide multi-structure consistency.
Assessment test: What can a reader observe during failure or concurrent publication, and can it accidentally combine incompatible revisions?
Warning sign: Related tables are updated separately and readers have no way to detect or avoid a mixed-version model.
DM-15 — Define canonicalization and equivalence before using content identity
MUST. Specify which differences are semantically meaningful and which are merely representational. Canonicalization must address ordering, encoding, defaults, metadata, numeric edge cases, and schema versions as applicable. Distinguish byte equality, structural equality, declared semantic equivalence, and approximate numerical agreement. Do not claim general semantic equivalence from hashing.
Assessment test: Would harmless encoding changes preserve the intended identity while a meaningful change to interpretation reliably invalidate it?
Warning sign: Hashes vary with batch layout, or silently ignore a change in reference convention that alters meaning.
4. Declarative composition and reusable structure
DM-16 — Represent material structure and policy as declarations
SHOULD. Express reusable structures, constraints, selection rules, and important workflows as typed declarations where this improves validation, composition, or explanation. Keep algorithmic implementation inside well-defined interpreters and kernels. Model the semantic choices an author needs to control, not every loop, allocation, or implementation branch.
Assessment test: Can an ordinary domain change be expressed as a validated model change instead of coordinated edits to procedural construction code?
Warning sign: Either scripts conceal all meaningful decisions, or every implementation detail becomes an elaborate configuration language.
DM-17 — Use templates and bindings instead of copied construction logic
SHOULD. Represent repeated structures once with declared parameters, required capabilities, feature conditions, and instantiation rules. Separate a template’s meaning from particular instances and their bindings. Generated instances must retain their origin and bound arguments. Prefer composition over duplicating nearly identical constructors or inheritance branches.
Assessment test: Does a new instance require new semantic information, or duplicated code whose differences could have been typed bindings?
Warning sign: A family of components repeats construction and validation logic with small variations encoded in custom subclasses.
DM-18 — Preserve high-level structure until expansion is required
SHOULD. Keep collections, indexed operations, patterns, constraints, and other compact structures explicit until a consumer requires their expansion. Make expansion rules, domains, and generated identities inspectable. Lazy materialization is acceptable when requests and dependency closure are explicit; it must not depend on accidental inspection order.
Assessment test: Can a large instance retain a compact definition, and can the compiler explain when and why that definition was expanded?
Warning sign: The model is immediately flattened into millions of low-level objects, or hidden lazy construction depends on attribute access.
DM-19 — Select behavior through declared capabilities and explicit bindings
MUST. Describe what an operation requires and what a provider guarantees. Resolve provider selection through validated capabilities and explicit policy, then persist the selected identity and version. Avoid dispatch based on accidental names, import order, reflection side effects, or undocumented type hierarchies. Account for ambiguity and absence.
Assessment test: Can the system explain why a provider was selected, prove it satisfies the required contract, and reproduce that selection later?
Warning sign: Installing a package or changing import order silently selects a different implementation for an existing model.
DM-20 — Make inspection and validation semantically non-mutating
MUST. Reading, reporting, inspecting, and validating must not silently change the authoritative model or select new behavior. Explicit demand resolution and materialization may produce declared derived artifacts. Internal caches may mutate only under a documented contract that does not change the logical definition or externally observable interpretation.
Assessment test: Does inspecting a field, generating a report, or asking whether a capability exists leave the model’s meaning unchanged?
Warning sign: A getter constructs new rules, a diagnostic changes specifications, or a report chooses a provider.
5. Compilation, derivation, and semantic preservation
DM-21 — Use explicit intermediate representations and progressive lowering
SHOULD. Separate portable intent, resolved structure, instantiated details, and execution-specific plans when those stages have different responsibilities. Give each representation a schema, invariants, identity scope, and source mapping. Do not force one structure to be simultaneously an authoring format, validation model, execution layout, and result store.
Assessment test: Can the reviewer identify what information is introduced, resolved, retained, or discarded at each stage?
Warning sign: A backend-specific object graph becomes the only surviving representation of the user’s original intent.
DM-22 — Give every meaningful transformation a contract
MUST. Declare each transformation’s inputs, outputs, versions, preconditions, postconditions, dependencies, effects, and failure modes. A transformation with several output schemas should publish an explicit artifact bundle. Apply the same contract discipline whether its implementation uses queries, generated code, a specialized algorithm, or a foreign library.
Assessment test: Can a pass be invoked, validated, cached, tested, and explained without knowing its internal implementation?
Warning sign: Pipeline functions mutate shared structures and communicate through undocumented ordering or ambient state.
DM-23 — Make derived representations traceable and non-competing
MUST. Every derived structure must identify its source revisions and derivation procedure. Rebuilding should be possible from the complete declared inputs when the derivation contract promises it. Editable projections require an explicit inverse or command translation with conflict checks; otherwise they are read-only. Do not confuse derived results with independently acquired observations.
Assessment test: Can a cache or backend object be discarded and recreated, or can the system explain the external evidence needed to reproduce it?
Warning sign: A materialized view acquires manual corrections that cannot be expressed in or reconciled with the authoritative model.
DM-24 — Preserve semantics across rewrites and lowerings
MUST. Define the equivalence promised by each rewrite, optimization, conversion, or approximation. Attach assumptions and validity conditions, enforce them, and invalidate dependent artifacts if they cease to hold. Where a transformation deliberately changes behavior, model the change as a selected policy rather than calling it a semantics-preserving optimization.
Assessment test: What observable behavior is preserved, under which assumptions, and which tests, derivations, or proofs support that claim?
Warning sign: An optimizer assumes nonzero values, stable ordering, commutativity, or identical precision without recording the condition.
DM-25 — Unify operation contracts while allowing specialized implementations
SHOULD. Define reusable operation contracts that support validation, planning, execution, and any required auxiliary operations. Generate adapters where possible, but allow specialized kernels and algorithms inside the contract. A unified substrate must not force unsuitable operations into generic queries or limit the system to whatever one engine already implements.
Assessment test: Can one operation definition support multiple execution paths without separately reimplementing its meaning in each?
Warning sign: Either every operation is squeezed into a universal interpreter, or each backend independently defines its semantics.
6. Planning, execution, mutable state, and effects
DM-26 — Separate preparation from repeated execution
SHOULD. Resolve structure, validate compatibility, determine dependencies, and prepare execution layouts before repeatedly evaluating a workload when those facts are stable. Bind changing values through explicit interfaces. Record which assumptions justify reuse so that preparation is repeated only when required, rather than rediscovered inside every inner-loop operation.
Assessment test: Which work depends on structure and which depends on current inputs, and is that distinction reflected in the execution lifecycle?
Warning sign: Each evaluation rebuilds the model, repeats joins to rediscover dependencies, or re-crosses language boundaries per primitive operation.
DM-27 — Represent important workflows as inspectable plans or state machines
SHOULD. Model material sequencing, dependencies, temporary specifications, transitions, retries, cancellation points, and completion criteria explicitly. Use an appropriate structure: a plan, DAG, state machine, or protocol—not one universal graph shape. Generate the routine orchestration from that structure while leaving specialized step implementations behind typed contracts.
Assessment test: Can the system explain the next valid action, why it is enabled, and how interruption or resumption changes the state?
Warning sign: A long script contains the only definition of workflow order, recovery rules, and temporary modifications.
DM-28 — Declare effects, ambient inputs, and nondeterminism
MUST. Mark operations that read external state, use time or randomness, perform I/O, mutate authoritative state, or depend on environment-specific behavior. Give optimizers and schedulers truthful purity and ordering contracts. Capture or bind external inputs when reproducibility is required. Never hide an effectful operation inside an interface treated as pure.
Assessment test: Could an optimizer safely duplicate, reorder, cache, or omit this operation under its declared contract?
Warning sign: An apparently deterministic query function launches a job, reads the current clock, changes a file, or performs a write.
DM-29 — Isolate mutable workspaces and commit their outcomes explicitly
MUST. Allow efficient mutable runtime state within a clearly owned execution context. Define how it differs from authoritative definitions and which outputs become committed facts. Temporary overrides should be scoped overlays or equivalent controlled state, not hidden edits requiring fragile manual reversal. Do not require a full immutable snapshot for every inner-loop update.
Assessment test: What state may change during execution, who owns it, and what remains visible after success, cancellation, or failure?
Warning sign: A failed attempt leaves the reusable model partly fixed, partially deactivated, or otherwise altered for the next caller.
DM-30 — Make partial failure and recovery explicit
MUST. Distinguish planned, running, completed, partial, stale, cancelled, and failed outcomes as needed. Define retry and idempotency semantics for effectful actions, and distinguish an execution attempt from its logical operation. Validate output completeness before publication. Do not imply rollback or exactly-once behavior when the underlying protocol cannot guarantee it.
Assessment test: After a crash or retry, can the system identify what happened, which outputs are valid, and which effects may already have occurred?
Warning sign: A partially written artifact looks complete, or an automatic retry duplicates an external action.
7. Dependencies, incrementality, and concurrency
DM-31 — Expose every dependency that can affect meaning or output
MUST. Track structural inputs, parameters, policies, provider versions, interpretation rules, assumptions, and relevant environmental facts. Include dependencies on absence or selection decisions when they matter. Dependencies may be represented conservatively when exact analysis is impractical; hidden dependencies must not be ignored simply to obtain more reuse.
Assessment test: Could any undeclared change alter this artifact or provider selection without changing its recorded inputs?
Warning sign: A cache key includes data files but omits a policy, default provider, locale, or assumption that changes interpretation.
DM-32 — Key reuse to semantic dependencies rather than convenience
MUST. Cache artifacts only when their complete declared inputs and applicable equivalence conditions match. Separate a cache lookup mechanism from its correctness argument. Test reuse against clean recomputation after relevant changes. Caches must remain disposable unless explicitly promoted to authoritative state with a separate persistence contract.
Assessment test: For each reused artifact, why is reuse valid, and what evidence shows invalidation catches meaningful changes?
Warning sign: A filename, timestamp, object identity, or broad model ID is used as a sufficient cache key without dependency analysis.
DM-33 — Invalidate at the smallest trustworthy semantic granularity
SHOULD. Distinguish changes to structure, bindings, policies, assumptions, numerical values, and execution settings. Recompute only affected artifacts when dependency evidence supports doing so. Prefer conservative invalidation to unsound reuse; measure invalidation precision before introducing complicated mechanisms intended to reduce rebuilding.
Assessment test: Does a small case change trigger a needless full rebuild, and does a meaningful structural or assumption change invalidate all affected products?
Warning sign: Everything recompiles for every value change, or structural artifacts survive changes to assumptions used during their construction.
DM-34 — Keep different relationship structures semantically distinct
MUST. Do not conflate ownership, physical connectivity, dependency, dataflow, control flow, provenance, equivalence, or conflict relationships. Represent their roles and direction explicitly. Preserve legitimate cycles where the domain permits them; use derived acyclic scheduling structures only where their interpretation justifies it.
Assessment test: What does each edge mean, and would reversing, removing, or traversing it have the same meaning across all edge types?
Warning sign: One generic graph is used for ownership, scheduling, and dependency analysis despite incompatible edge semantics.
DM-35 — Make concurrency respect dependencies, ownership, and declared ordering
MUST. Parallelize independent work or use explicit synchronization and reduction contracts for shared work. Specify ownership, thread safety, cancellation, resource limits, and ordering sensitivity. Coordinate nested execution pools where necessary. A columnar or graph representation does not by itself establish that operations can execute independently.
Assessment test: Can concurrent execution change correctness, visible ordering, effect counts, or reproducibility beyond the stated contract?
Warning sign: Parallelism is added based on data layout while hidden shared state, dependent stages, or competing thread pools are ignored.
8. Execution representations and performance
DM-36 — Choose physical layouts for demonstrated access patterns
SHOULD. Use columnar batches, dense arrays, sparse structures, adjacency layouts, trees, or other representations according to actual operations. Keep the mapping to canonical identities and semantics explicit. Treat physical layouts as compiled or materialized views unless they truly own independent facts. Avoid both object-per-cell overhead by default and a universal table mandate.
Assessment test: Which measured access pattern motivates the layout, and how can it be replaced without rewriting the semantic model?
Warning sign: An inappropriate layout is defended as architectural purity, or a fast layout becomes the only place meaning is recorded.
DM-37 — Cross expensive boundaries in coarse, typed units
SHOULD. Transfer validated bundles, batches, plans, or artifacts across process, language, storage, and accelerator boundaries when that suits the workload. Declare schema, ownership, lifetime, ordering, encoding, and cancellation behavior. Accept necessary copies for correctness or performance; zero-copy is an optimization with preconditions, not a design objective that overrides semantics.
Assessment test: Is boundary overhead proportional to useful work, and are representation, lifetime, and transfer costs accounted for?
Warning sign: Cross-language calls occur per primitive operation, or a zero-copy claim ignores reordering, mutable workspaces, or process boundaries.
DM-38 — Match the execution mechanism to the operation’s semantics
SHOULD. Use relational engines for relational work, specialized algorithms for specialized work, and generated evaluation programs for repeated computation where appropriate. Integrate them through the same typed operation and artifact contracts. Shared representation should enable cooperation, not require every algorithm to be expressed in one execution language.
Assessment test: Is an operation placed in an engine because its semantics and workload fit, or merely because that engine is already present?
Warning sign: Inner-loop computation repeatedly performs relational reconstruction, or relational assembly is rewritten as avoidable per-record scripts.
DM-39 — Evaluate performance end to end and distinguish evidence from expectation
MUST. Measure construction, validation, compilation, transfer, execution, storage, recovery, and inspection costs where material. Include memory use, cold and warm behavior, representative scale, and contention. Attribute gains to measured mechanisms. Do not infer whole-system speedups from language choice, a datatype, vectorization, or an isolated kernel benchmark.
Assessment test: Which bottleneck improves, what new costs appear, and what representative evidence supports the claimed benefit?
Warning sign: A proposal promises major speedups while ignoring model construction, copies, invalidation, external execution, or memory growth.
DM-40 — Declare precision, approximation, ordering, and determinism requirements
MUST. State the required equivalence and accuracy class for calculations and outputs. Specify permitted approximation, precision changes, reduction order, stochastic behavior, and tolerance policies. Record when backend or hardware choices can affect outcomes. Require bitwise reproducibility only when needed and supported; otherwise define an honest weaker contract.
Assessment test: Can two conforming executions differ, by how much or in what ways, and how will conformance be assessed?
Warning sign: Mathematical equivalence, structural equality, approximate agreement, and identical bytes are treated as the same guarantee.
9. Boundaries, providers, and extensibility
DM-41 — Keep adapters mechanical and domain conversions explicit
MUST. Adapters may translate representations, bind identifiers, and implement declared conversions. They must not become independent owners of domain rules, hidden defaults, provider selection, or policy. When a conversion changes interpretation, represent it as a named, validated transformation rather than burying it in serialization or glue code.
Assessment test: Does crossing a boundary merely realize a known contract, or introduce a new interpretation that exists nowhere else?
Warning sign: A Python bridge, serializer, API handler, or storage adapter quietly adds rules that the canonical model does not contain.
DM-42 — Make interchange loss-aware and reject silent semantic degradation
MUST. Preserve or explicitly translate required type information, metadata, identities, ordering, null semantics, and provenance across boundaries. Define how receivers handle unknown versions and unsupported constructs. A projection or lossy export may be legitimate, but it must declare what is omitted and must not masquerade as a complete authoritative representation.
Assessment test: Can an exported and re-imported artifact preserve its promised semantics, and does the system detect information the target cannot represent?
Warning sign: Metadata disappears during a query, cast, export, or bridge operation and downstream code continues as if meaning were preserved.
DM-43 — Negotiate capabilities and expose unsupported behavior
MUST. Require execution providers to declare supported operations, auxiliary functionality, limitations, and versions. Validate the requested plan against that contract before execution. A fallback must preserve the required semantics or be explicitly selected as a changed policy. Never interpret successful registration as proof of end-to-end executability.
Assessment test: Which required capability is supplied by which provider, and what happens when an operation, shape, or guarantee is unsupported?
Warning sign: A feature is accepted at construction but fails deep in execution, or silently runs with different semantics on a fallback backend.
DM-44 — Make extensions complete, versioned, and conformance-testable
MUST. An extension contract must cover its signature, semantics, validity, dependencies, effects, failures, execution bindings, and required auxiliary operations. Auxiliary operations may include validation, inversion, dependency analysis, differentiation, or explanation, depending on the system. Generate wrappers from the contract where possible; do not pretend a schema supplies missing implementations.
Assessment test: Can an extension be independently tested for every capability it claims, and do generated wrappers share one underlying semantic definition?
Warning sign: Adding an operation requires disconnected implementations in several backends, each with incomplete or inconsistent behavior.
DM-45 — Treat trust and authority as explicit execution constraints
MUST. Validate external inputs and agent-proposed changes through the same semantic gates as other writes. Distinguish data from executable authority, and declare which operations may read, execute, or modify external resources. Match isolation, permissions, and resource limits to actual risks. Schema conformance alone does not authorize an effect or establish input trust.
Assessment test: Can untrusted content, a plugin, or an agent bypass model validation or trigger effects beyond the explicitly granted authority?
Warning sign: A valid payload can inject arbitrary code, invoke hidden effects, or alter authoritative state through an ungoverned escape hatch.
10. Provenance, reproducibility, and explainability
DM-46 — Preserve source-to-result lineage through transformations
MUST. Connect authored declarations, imported observations, selected policies, transformations, generated structures, executions, and outputs. Record many-to-many derivation where needed rather than forcing one parent per output. Distinguish provenance from ownership and ordinary dependency edges. Preserve enough origin information to explain generated artifacts and diagnose failures.
Assessment test: Can a problematic output or generated rule be traced to the specific inputs, selections, and transformations responsible for it?
Warning sign: Generated objects have identifiers but no link to the authored declaration or policy that caused them to exist.
DM-47 — Represent diagnostics as structured evidence
MUST. Use stable diagnostic codes, severity, affected identities, stage, observed values, violated rules, causal context, and remediation hints where appropriate. Distinguish model invalidity, unsupported capability, numerical or algorithmic failure, infrastructure failure, and inconclusive checks. Human-readable messages should be projections of structured evidence, not the only source of meaning.
Assessment test: Can a tool classify and act on a failure without parsing prose or guessing whether the model or execution infrastructure is at fault?
Warning sign: Everything becomes a generic exception or log string, including unsupported features and actual correctness failures.
DM-48 — Define and support the required reproducibility contract
MUST. Record the versions, input snapshots, specifications, policies, execution settings, assumptions, provider artifacts, and environmental conditions needed for the promised replay class. Capture external observations when they cannot be reproduced. Distinguish recomputation from replay of recorded evidence, and disclose nondeterministic or unavailable dependencies.
Assessment test: What exactly can be reproduced later, with which inputs and guarantees, and what is explicitly outside that promise?
Warning sign: A result is labeled reproducible because input values were saved, while provider versions, policies, or external state were omitted.
DM-49 — Make changes understandable at the level of meaning
SHOULD. Provide semantic diffs that identify changed declarations, constraints, bindings, policies, provider selections, assumptions, and affected derived artifacts. Separate these from harmless physical reorderings or serialization changes. Use dependency information to explain the expected impact without claiming certainty about untested numerical or external outcomes.
Assessment test: Can a reviewer explain what changed and why it matters without comparing raw bytes or reverse-engineering generated code?
Warning sign: A small meaningful change produces an unreadable generated diff, or a major semantic change looks like an innocuous metadata edit.
DM-50 — Observe the model lifecycle, not only low-level operations
SHOULD. Expose inspectable plans, validity status, dependency selections, cache decisions, stage costs, artifact sizes, and outcomes linked to semantic identities. Provide accessible projections for both humans and agents. Instrumentation must respect non-mutating inspection and should be budgeted so that explanation does not accidentally dominate execution.
Assessment test: Can a user see why the system constructed, selected, reused, rejected, or executed something and where its cost arose?
Warning sign: There are abundant infrastructure logs but no explanation of the model-level decision that produced the behavior.
11. Evolution, generation, and verification
DM-51 — Evolve schemas and semantics through explicit migrations
MUST. Version structural and semantic contracts, classify compatibility, and supply validated migration paths where required. Preserve origin and migration provenance. Generated tooling can handle mechanical changes, but meaning-changing migrations need explicit decisions and tests. Unknown or incompatible versions must not be silently reinterpreted using current defaults.
Assessment test: Can an older artifact be interpreted under its original contract or migrated with documented effects and verified invariants?
Warning sign: A renamed field, changed default, or altered semantic convention silently changes the meaning of stored models.
DM-52 — Generate repeated mechanical artifacts from shared contracts
SHOULD. Generate types, column accessors, builders, validators, serialization bindings, schemas, documentation, and conformance scaffolding when they express the same contract. Keep handwritten logic for genuinely semantic or algorithmic work. Define ownership and regeneration rules so that generated files do not become independently edited authorities.
Assessment test: When a contract changes, which artifacts update automatically and which require a genuinely new semantic decision?
Warning sign: A field or operation is separately declared in application types, transport schemas, validators, documentation, and adapters.
DM-53 — Verify invariants and equivalence across representations
MUST. Test authoritative invariants, serialization round trips, backend conformance, transformation conditions, and declared equivalence. Use property-based, metamorphic, differential, or reference tests where appropriate. Independent execution paths are useful evidence, but agreement alone is not proof if both share the same faulty transformation.
Assessment test: What would detect semantic drift in an adapter, rewrite, physical layout, generated implementation, or alternate backend?
Warning sign: Tests check a few expected outputs while identity, metadata, invalid inputs, and representation changes remain untested.
DM-54 — Test adversarial lifecycle and boundary conditions
MUST. Exercise malformed inputs, invalid relationships, empty domains, unknown tags, missing capabilities, precision boundaries, interrupted publication, stale caches, concurrent changes, cancellation, partial results, and retry behavior as applicable. Include negative tests showing that prohibited states or actions are rejected at their declared boundaries.
Assessment test: Which failures occur between happy-path stages, and are their effects on state, authority, and recovery actually tested?
Warning sign: The design is validated only through successful small examples and assumes construction, publication, and execution always complete.
DM-55 — Make contracts and extension paths discoverable to humans and agents
SHOULD. Expose stable names, schema documentation, operation contracts, capability registries, examples, diagnostics, and source mappings in a navigable form. Document the sanctioned change path: declaration, binding, transformation, kernel, adapter, or runtime policy. Prefer a compact map with focused references over duplicated narratives that drift from implementation.
Assessment test: Can a competent agent find the authoritative concept and correct extension point without inferring conventions from unrelated examples?
Warning sign: Correct changes require tribal knowledge of hidden hooks, special filenames, reflection behavior, or undocumented construction order.
12. Architectural leverage and disciplined improvement
DM-56 — Optimize for fewer independent semantic decisions—not fewer lines
SHOULD. Judge an abstraction by how much duplicated meaning, coordinated editing, and exception handling it removes. Boilerplate generated from one contract is less harmful than compact code that hides several conflicting rules. Measure extension locality, semantic duplication, and failure surface; do not use line count or the number of tables as the primary design score.
Assessment test: Does an ordinary extension require one new semantic declaration plus focused implementation and tests, or coordinated policy edits across the system?
Warning sign: A shorter implementation is praised despite moving important meaning into implicit conventions or scattered generic callbacks.
DM-57 — Prefer a small coherent core with explicit extension mechanisms
SHOULD. Keep the core semantic vocabulary and lifecycle understandable. Add new concepts when they capture recurring distinctions or remove real exceptions. Support new algorithms through registered contracts rather than uncontrolled escape hatches. Avoid both a closed language that cannot express legitimate needs and a universal engine whose every case requires custom scripts.
Assessment test: Does the extension use existing concepts coherently, justify a new concept, or merely route around the model?
Warning sign: A growing collection of flags and arbitrary callbacks turns a nominally generic system into many hidden special cases.
DM-58 — Scale architectural machinery to demonstrated needs
SHOULD. Implement the smallest coherent design that preserves the important semantic boundaries and required guarantees. Introduce elaborate registries, distributed storage, incremental compilers, code generation, or orchestration only when their leverage is demonstrated or a concrete requirement demands them. Preserve useful extension seams without building hypothetical platforms.
Assessment test: Which present requirement or tested extension justifies each layer, service, engine, and abstraction?
Warning sign: The system needs an entire platform before one representative end-to-end use case can work.
DM-59 — Make design claims falsifiable and label uncertainty
MUST. For proposed benefits, state the mechanism, assumptions, baseline, validation method, and current evidence. Distinguish an implemented capability, a checked library interface, a tested prototype, a measured benefit, and a hypothesis. Present tradeoffs and uncertainty directly. Do not convert architectural preference into an unsupported correctness or performance claim.
Assessment test: What observation could show that this proposal fails to improve its stated objective, and has that observation been sought?
Warning sign: A design is called unified, declarative, scalable, or faster without operational definitions or supporting evidence.
DM-60 — Turn the principles into change-level review and regression controls
MUST. For each material design change, identify applicable principles, record evidence and deviations, and add tests or automated checks for guarantees that can regress. Give exceptions explicit scope, rationale, consequence, compensating controls, owner, and revisit conditions. A high aggregate score cannot compensate for a violated mandatory correctness boundary.
Assessment test: Can a future change reintroduce the defect this design removed, and what review or automated control would catch it?
Warning sign: The charter is praised once, then implementation and later extensions are reviewed without reference to its contracts.
Review and improvement framework
A. Acceptance gates
These gates apply to the supported behavior and correctness requirements actually in scope. They do not require implementing hypothetical features. A conceptual proposal can record an unresolved gate, but must not be presented as verified or ready for an affected use case. No weighted score compensates for a failed gate.
| Gate | Reject or revise the design when… | Main principles |
|---|---|---|
| G1 — Authority | Two independently mutable definitions can disagree about the same authoritative fact without an explicit reconciliation protocol. | DM-02, DM-11–15, DM-23 |
| G2 — Semantic fidelity | Required meaning is ambiguous, silently dropped, reinterpreted, or represented by an indistinguishable sentinel. | DM-01, DM-06–10, DM-24, DM-40, DM-42 |
| G3 — Validity | An invalid state can reach an operation that assumes it is valid, without a defined rejection or safe handling path. | DM-07, DM-22, DM-43–45, DM-53–54 |
| G4 — Hidden behavior | Inspection, optimization, provider selection, or an apparently pure operation can introduce undeclared effects or alter the model’s meaning. | DM-04, DM-19–20, DM-28 |
| G5 — Consistency and recovery | Readers can mistake an inconsistent revision or partial failed output for a valid committed result, or retries can produce ungoverned effects. | DM-14, DM-29–30, DM-35 |
| G6 — Transformation and reuse | A rewrite, cache hit, projection, or alternate backend changes required behavior without a valid contract or selected approximation policy. | DM-24, DM-31–33, DM-40–43 |
| G7 — Truthful capability claims | A required capability is claimed without an actual implementation route, or an unsupported operation silently falls back to different semantics. | DM-43–44, DM-59 |
B. Assessment dimensions
Use this rubric to compare alternatives or track improvement, not to manufacture precision. Score only dimensions relevant to the change and state any reweighting. Show an evidence-supported current score separately from a proposed target; a narrative promise is not an implemented guarantee. For small changes, a qualitative assessment is preferable to a numerical total.
| Dimension | Suggested weight | What to inspect |
|---|---|---|
| Semantic completeness and validity | 20 | Important distinctions are typed, relationships are explicit, invariants are enforced, transformations preserve the declared meaning. |
| Authority, identity, and lifecycle consistency | 15 | One authority per fact; stable IDs; clear revisions, specifications, executions, commit boundaries, and derived artifacts. |
| Declarative leverage and extension locality | 15 | Reusable templates, explicit capabilities, generated mechanical artifacts, few independent semantic decisions. |
| Derivation, dependency, and reuse correctness | 10 | Typed stages, complete dependencies, explainable invalidation, separation of preparation from repeated execution. |
| Execution, boundaries, and failure behavior | 10 | Effects, mutable workspaces, concurrency, adapters, unsupported features, and partial failures have explicit contracts. |
| Verification and evolution | 10 | Negative and conformance tests, schema migrations, equivalence checks, and regressions are covered. |
| Provenance and inspectability | 10 | Source-to-result evidence, structured diagnostics, semantic diffs, and realistic reproducibility guarantees. |
| Measured cost and proportionality | 10 | Appropriate layouts and mechanisms, end-to-end measurements, no unsupported benefits or unjustified platform complexity. |
Suggested maturity scale:
| Rating | Evidence level |
|---|---|
| 0 | Missing, contradictory, or outside the claimed supported behavior without disclosure. |
| 1 | Stated in prose or convention; no reliable implementation or enforcement. |
| 2 | Explicitly represented and partly implemented, with significant manual coordination or verification gaps. |
| 3 | Implemented, enforced at defined boundaries, and supported by representative tests or other suitable evidence. |
| 4 | Systematically reusable across the relevant scope, conformance-tested across applicable paths, and protected against regression. |
Do not equate rating 4 with formal proof or unlimited generality. A small design can reach rating 4 within a narrow, well-tested scope.
C. A repeatable design review
Frame the change. Describe the observable outcome, the existing baseline, scope, non-goals, required guarantees, and uncertainty. Separate semantic changes from mechanical refactoring and performance changes.
Map authority and lifecycles. Identify definitions, bindings, policies, observations, execution state, results, and derived views. State their identities, version boundaries, owners, and permitted update paths.
Specify contracts before interfaces. Describe semantic types, invariants, valid domains, important operations, effects, capabilities, and failure modes. Identify what remains deliberately opaque and why.
Trace representative journeys. Walk an ordinary extension, a meaningful change, a representation boundary, and an interrupted execution. Track each piece of meaning and state through the lifecycle. Select the journeys relevant to the change rather than completing irrelevant paperwork.
Compare with a simpler viable alternative. Explain the proposed abstraction’s leverage, costs, and failure surface. Count independent semantic decisions and coordinated edits, not merely source files. A declaration plus a genuinely new algorithm and tests is a legitimate extension.
Check gates and record evidence. Cite specific artifacts, schemas, tests, code paths, measurements, or documented gaps. Differentiate verified behavior from expected benefits and assumptions.
Choose changes and regression controls. Prioritize correctness and authority defects, then semantic duplication and extension difficulty, then measured performance costs. Record accepted tradeoffs and make recurring regressions mechanically detectable where practical.
D. Evidence vocabulary
| Label | Meaning |
|---|---|
| Proposed | Described architecture or intended behavior; not implemented. |
| Interface-checked | A required interface or integration mechanism has been inspected; end-to-end behavior is not yet established. |
| Implemented | Code exists for the stated path; testing scope must still be described. |
| Tested | Named tests exercise the stated behavior over specified cases. |
| Measured | A stated benchmark or operational observation supports a quantitative claim under recorded conditions. |
| Formally established | A specific property follows from an identified formal argument or verified method with explicit assumptions; do not use this label for ordinary testing. |
Evidence labels describe different claims, not a single universal ladder. A measured implementation can still be incorrect; an interface-checked design can still need substantial engineering.
E. The extension-locality test
Ask where a typical new rule, entity type, provider, or policy must be expressed. The desired pattern is:
One authoritative semantic addition, any genuinely new specialized implementation, and focused conformance tests; routine adapters, validation scaffolding, documentation, and execution bindings derive from existing contracts where feasible.
A change spanning several files is not automatically a failure. A change requiring the same meaning to be independently re-expressed in several subsystems is the problem. Introducing a genuinely new semantic concept may legitimately require changes to the core language and its backends, with explicit versioning and conformance work.
F. What belongs in the model and what belongs in code?
| Question | Default placement |
|---|---|
| Does this distinguish valid from invalid meaning or change an observable rule? | Canonical semantic declaration or a registered operation contract. |
| Is this a reusable structural pattern or important selection policy? | Typed template, binding, or policy. |
| Is this a specialized algorithm implementing an existing contract? | Ordinary well-tested code behind that contract. |
| Is this representation translation with no new domain decision? | Mechanical adapter or generated binding. |
| Is this an optimized layout or queryable projection? | Versioned derived artifact, with explicit dependencies and identity mapping. |
| Is this an effectful action or important execution sequence? | Typed action or workflow contract, with an implementation appropriate to the task. |
| Is this repeated mechanical expression of an existing schema? | Generated artifact. |
| Is this speculative flexibility with no demonstrated consumer? | Defer it; retain only an inexpensive, justified extension seam. |
G. Common false positives
A system is not meaningfully data model–based merely because it uses tables, a graph, a schema language, immutable objects, a compiled language, or code generation. Inspect the meaning and authority, not the branding.
| Attractive claim | Hidden defect to check |
|---|---|
| “Everything is declarative.” | Arbitrary scripts or callbacks still decide important behavior. |
| “There is one substrate.” | Multiple independently editable definitions are stored in the same format. |
| “The schema enforces correctness.” | Critical semantic or relational invariants are only metadata. |
| “All state is immutable.” | Runtime services hide mutable state, or the design needlessly snapshots inner-loop work. |
| “It is zero-copy.” | Ownership, process boundaries, casting, reordering, or mutable workspaces invalidate the claim. |
| “Every backend is supported.” | Lowerings or auxiliary capabilities exist only for a subset of accepted operations. |
| “All behavior is generic.” | Every real use case adds more flags, special cases, or untyped escape hatches. |
| “It is faster because it is data-oriented.” | End-to-end transfer, compilation, construction, and recovery costs were never measured. |
| “The outputs match.” | Both implementations share the same incorrect transformation or ignore semantic metadata. |
H. Scoped exception record
A SHOULD-level deviation should record: principle IDs; scope; reason; alternatives considered; consequences; compensating controls; evidence; accountable owner or role; and a revisit trigger. A MUST-level gap must be identified as unresolved or handled by narrowing supported scope—not hidden in an exception that still claims full compliance.
Do not add review machinery that costs more than the risk and decision warrant. A short, concrete decision record is sufficient for a small deviation.
Closing standard
A successful design makes valid changes easier, invalid states harder to introduce, meaning easier to inspect, and execution mechanisms easier to replace. Its structure explains its behavior. Its extensions add meaning rather than duplicate machinery. Its claims are supported by evidence, and its remaining uncertainty is visible.
Design review template
Reference: DATA_MODEL_DESIGN_CHARTER.md. Replace bracketed prompts with evidence. Remove irrelevant sections with a brief scope note rather than inventing requirements. For a small change, use a compact version of this template.
1. Decision and scope
Proposal: [Name and short description.]
Status: [Proposed / interface-checked / implemented / tested; use claim-specific labels.]
Reviewer / author: [Person, agent, or accountable role.]
Affected revisions: [Model/schema/implementation identifiers as applicable.]
Observable outcome: [What becomes possible, safer, simpler, or measurably better?]
Baseline: [Existing design and relevant limitations.]
Supported scope and non-goals: [State which capabilities and guarantees are actually in scope.]
Constraints and uncertainty: [Scale, deployment, compatibility, correctness, external interfaces, unknowns.]
2. Authority and lifecycle map
| Concept or fact | Semantic type and identity | Authority / owner | Revision or snapshot boundary | Permitted update path | Derived representations |
|---|---|---|---|---|---|
| [Definition] | |||||
| [Binding/specification] | |||||
| [Policy] | |||||
| [Observation/input] | |||||
| [Execution state] | |||||
| [Result] |
Deliberately opaque behavior: [What is outside the declarative model, why, and which contract governs it?]
Identity behavior: [Rename, reorder, reserialize, regenerate, duplicate, split/merge if applicable.]
3. Semantic contracts and invariants
| Contract or invariant | Representation | Enforcement boundary | Failure behavior | Verification evidence |
|---|---|---|---|---|
| [Semantic compatibility] | ||||
| [Relationship/cardinality] | ||||
| [Lifecycle consistency] | ||||
| [Capability requirement] |
Absence and uncertainty: [Distinguish unspecified, unknown, not applicable, uncertain, invalid, partial, failed, or other relevant states.]
Equivalence requirements: [Byte / structural / semantic / approximate; tolerances and assumptions.]
4. Derivation and execution design
| Stage or operation | Input revisions and dependencies | Output contract | Preconditions / assumptions | Effects and mutable ownership | Provenance / invalidation |
|---|---|---|---|---|---|
| [Declaration/binding] | |||||
| [Resolution/validation] | |||||
| [Transformation/preparation] | |||||
| [Execution] | |||||
| [Publication] |
Relationship structures: [Distinguish ownership, connectivity, dependency, scheduling, provenance, and other graph meanings.]
Provider selection and limitations: [Selected version, required capabilities, supported subsets, unsupported behavior.]
Boundary contracts: [Schema, identity, metadata, encoding, ownership/lifetime, batching, conversions, and possible loss.]
Coherent publication: [What becomes visible, when, and under which consistency protocol?]
5. Representative journeys
Ordinary extension
[Trace one realistic new rule, entity, provider, or policy. Identify new semantic decisions, specialized implementation, generated artifacts, and tests. Distinguish multiple files from duplicated meaning.]
Meaningful change
[Trace a changed specification, policy, structural definition, or assumption. Explain identity, invalidation, retained artifacts, and semantic diff.]
Boundary or alternate representation
[Trace meaning through serialization, a language bridge, a backend, or another physical layout. State what is preserved or deliberately omitted.]
Interruption or failure
[Trace cancellation, crash, retry, unsupported capability, or invalid input. Explain observable state, valid partial outputs, effects already performed, and recovery.]
6. Acceptance gates
| Gate | Pass / fail / unresolved / not applicable | Evidence or scope rationale | Required action |
|---|---|---|---|
| G1 — Authority | |||
| G2 — Semantic fidelity | |||
| G3 — Validity | |||
| G4 — Hidden behavior | |||
| G5 — Consistency and recovery | |||
| G6 — Transformation and reuse | |||
| G7 — Truthful capability claims |
An unresolved gate is not a pass. A high score elsewhere cannot offset a failed required gate.
7. Principle findings
| Finding | Principle IDs | Concrete evidence or gap | Consequence | Proposed correction | Verification |
|---|---|---|---|---|---|
| [One substantive finding per row] |
Applicability: [Which groups are relevant and why? Do not restate all 60 principles for every change.]
Optional maturity assessment: [Use charter dimensions only when helpful. Separate current evidence-supported scores from proposed targets and disclose reweighting.]
8. Alternatives and architectural leverage
| Alternative | Semantic duplication and extension locality | Correctness and operational risks | Implementation / maintenance cost | Performance evidence | Why selected or rejected |
|---|---|---|---|---|---|
| Current baseline | |||||
| Proposed design | |||||
| Simpler viable alternative |
Abstractions justified by current needs: [Explain registries, compilers, generators, services, stores, and engines that add material complexity.]
What remains ordinary code: [Specialized algorithms and mechanisms that should not be turned into an unnecessary DSL.]
9. Verification and measurement plan
| Claim or risk | Evidence label | Test / analysis / benchmark | Conditions and expected result | Current result or remaining gap |
|---|---|---|---|---|
| [Invariant enforcement] | ||||
| [Representation preservation] | ||||
| [Reuse/invalidation correctness] | ||||
| [Failure/recovery] | ||||
| [End-to-end performance] |
Include relevant round-trip, differential, metamorphic, negative, migration, concurrency, and interrupted-publication checks. Do not assume independent-looking backends constitute independent correctness oracles.
Cost accounting: [Construction, validation, preparation, transfers, execution, storage, recovery, inspection, memory; only the material categories.]
10. Exceptions and unresolved decisions
Principle IDs: [IDs.]
Scope: [Where the deviation applies.]
Reason and alternatives: [Why the preferred default is not suitable.]
Consequence and compensating controls: [What is weakened and how risk is managed.]
Evidence: [Support for this choice.]
Owner / accountable role: [Who maintains the exception.]
Revisit trigger: [A concrete change, scale, capability, or other condition.]
A SHOULD-level exception can be accepted with justification. A MUST-level gap requires narrowing the supported claim or recording the design as unresolved for the affected requirement.
11. Decision and implementation changes
Decision: [Accept / accept scoped design with documented SHOULD deviations / revise / reject.]
Reason: [The strongest evidence and remaining limitations.]
| Priority | Change | Principle IDs | Acceptance evidence | Regression protection |
|---|---|---|---|---|
| [Correctness first] | ||||
| [Semantic leverage next] | ||||
| [Measured performance where justified] |
Final check: The design’s claims match the evidence; the scope matches the implemented guarantees; and later extensions have a clear, validated path.
Agent directive: data model–based design
Use DATA_MODEL_DESIGN_CHARTER.md as the normative reference for architecture, implementation, and refactoring. Refer to stable principle IDs in your findings. Apply the principles to the task’s actual scope; do not build an unnecessary platform to satisfy a checklist.
Governing objective
Make domain meaning, relationships, constraints, capabilities, and important behavior explicit in a canonical, typed, versioned model. Derive routine construction, execution representations, adapters, and inspection views from shared contracts. Minimize independently maintained semantic decisions and coordinated procedural edits—not source lines or the amount of handwritten algorithmic code.
One unified logical substrate does not mean one universal table, one execution engine, or one memory layout. Specialized kernels and physical representations are appropriate when their contracts, dependencies, and mappings to the canonical model are explicit.
Required approach
Before designing APIs or selecting libraries, identify the authoritative concepts and their semantic types, relationships, invariants, identities, and lifecycles. Separate definitions, bindings, policies, observations, mutable execution state, results, and derived artifacts.
Represent reusable structure and important selection policies as declarations when doing so improves correctness, composability, or change locality. Preserve high-level structure until a consumer needs expansion. Use explicit capability resolution, typed transformations, and inspectable plans. Do not hide important meaning in reflection, naming conventions, ad hoc scripts, implicit defaults, arbitrary callbacks, or serialization glue.
Keep inspection semantically non-mutating. Declare side effects, ambient inputs, nondeterminism, and failure behavior. Scope mutable runtime workspaces and publish coherent outcomes. Make interrupted execution, unsupported capabilities, invalid states, retries, and partial outputs distinguishable.
Trace transformations and results to their sources. Track every dependency needed for safe reuse, including policies, provider versions, and assumptions. Ensure adapters preserve meaning or explicitly declare a selected loss or approximation. Validate capability support end to end rather than assuming a library or datatype supplies missing semantics.
Use workload-appropriate execution mechanisms and data layouts. Hoist stable preparation out of repeated execution. Cross expensive boundaries in suitably coarse, typed units. Evaluate performance end to end; describe unmeasured benefits as hypotheses.
Generate repeated mechanical artifacts from schemas and operation contracts where useful. Keep specialized algorithms as ordinary code behind complete contracts. Compare major proposals with a simpler viable alternative and justify each additional architectural layer.
Review output
Use DESIGN_REVIEW_TEMPLATE.md for substantial changes. For smaller changes, compress the same reasoning into a short assessment. Report the objective and scope, canonical model and authority, lifecycle and transformations, applicable principle findings, failure behavior, verification evidence, tradeoffs, and recommended changes.
For each important finding, cite the relevant principle IDs and concrete evidence or an explicitly stated gap. Do not claim that a design satisfies a principle merely because it uses a fashionable technology or vocabulary. Distinguish proposed, interface-checked, implemented, tested, measured, and formally established claims.
Check the charter’s acceptance gates before calculating any optional score. Do not allow lower code volume or better performance to compensate for lost meaning, competing authority, hidden effects, inconsistent revisions, invalid reuse, or unsupported behavior.
The practical extension test is: does a normal change add one authoritative declaration, any genuinely new implementation, and focused tests—or require the same meaning to be re-expressed across several subsystems? Prefer the former, while keeping the architecture proportionate to actual needs.
Design review — Arrow-native IDAES core architecture blueprint (revision 1)
Standard: docs/design_review/design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01–DM-60, gates G1–G7), applied through AGENT_DESIGN_DIRECTIVE.md and DESIGN_REVIEW_TEMPLATE.md.
Subject: docs/design_review/Arrow-native-idaes-core-architecture-blueprint.md, revision 1 (2026-09-13), 2,716 lines. A design document; no code exists yet.
Depth: standard, with the adversarial checks recorded in §1 (Method).
Date: 2026-09-13.
1. Decision and scope
Proposal. Re-create the IDAES-PSE core modelling framework as a versioned snapshot of typed Arrow relations across seven catalog namespaces, compiled by sixteen contracted passes (P0–P15) into a backend-neutral CanonicalMathProblem, with DataFusion 55 as the relational engine for inference rules, catalog, batch kernels and analytics, and native Rust numerics (evaluation program, AD, faer, in-process Ipopt) plus AMPL NL and a generated Pyomo adapter as backends.
Status. Proposed throughout. Five engine facts are Interface-checked in this review (Appendix A). Claims the three capability maps established by rustdoc extraction or probe are labelled Interface-checked (capability map) — inspected by those documents, not re-derived here.
Reviewer. Claude (Fable 5.1), acting as design reviewer for the project owner.
Affected revisions. Blueprint revision 1; the doctrine documents it cites; the three capability maps compiled 2026-09-13; the decided dependency anchors DataFusion 55.1.0 and Arrow 59.3.0 (project owner’s instruction during this review; the blueprint still says 55.0.0 / 59.2.0).
Observable outcome claimed. A process model whose structure, physics, mathematics and execution artifacts are queryable relations with one authority each, deterministic identities, content-addressed snapshots, contracted passes, and declared backend capabilities — reproducing IDAES’s core capabilities without its imperative build() methods and lazy property construction.
Baseline. IDAES 2.10 on Pyomo: Python class hierarchies with CONFIG blocks and build(), build_on_demand property construction, ad hoc initialization scripts, scaling and diagnostics as utilities over a live Pyomo model.
Supported scope and non-goals. In scope: the modelling framework (§0.2 of the blueprint). Out of scope: example libraries, surrogates training, DMF, UI. Electrolytes, Helmholtz, MSContactor, GDP-native lowering are phase 4.
Constraints and uncertainty. Nothing is measured (§24.3 says so explicitly). Library surfaces are pre-1.0 for salsa, faer, petgraph, num-dual, egglog, diffsol. Parity depends on Pyomo and IDAES being installed, which they are not in this repository.
Method and coverage
What was examined:
- The blueprint in full (all sections and appendices), the charter, directive and template in full.
DataFusion-rust-capability-map.md,Arrow-rust-capability-map.md,Supporting-rust-library-capability-map.mdin full, anddocs/design_review/evidence/README.md.- Five context7 queries against
/apache/datafusion(Appendix A) to verify the engine facts on which gate verdicts depend and to look for capabilities the maps did not cover.
Guarantees attacked directly (each produced a finding or was cleared):
| Guarantee attacked | Result |
|---|---|
| “Names are never identity” (D4) against the authored-ID formula in §5.1 | Finding F1 |
| “The graph is the truth” for template expressions (§7.7) against the two authored representations | Finding F2 |
| Authored relations reference only authored/reference relations (implied by “no pass writes backward”) | Finding F3 |
“Unknown is never silent exclusion” (§7.6, §14.2) against the inferred.* schemas | Finding F4 |
The §8.3 unit-inference rule applied to the §9.4 reference expression P/(R·T) | Finding F5 |
| Every transformation between P7’s indexed equations and P13’s scalar incidence has a pass contract | Finding F6 |
| Static families and incidence linearity are a function of the snapshot alone | Finding F7 |
| The memo key contains every input that can change a pass output | Finding F9 |
| The snapshot publication protocol (§20.1) under interruption | Cleared — write artifacts, then manifest, then CAS the ref; readers pin a manifest |
| Overlay precedence (§19.1) | Cleared at document level; tie behaviour is a named diagnostic |
| Null / NaN / bound sentinels (§4.4, §7.6) | Cleared; the Arrow map’s probe independently confirms null, NaN and −0.0 are distinct through IPC |
Not examined, so silence is not assurance:
- The physical and mathematical correctness of the IDAES-derived equations in §9–§11, §13, §15–§17 (state definitions, EOS, balances, discretization stencils, diagnostics formulas, initialization sequences). This review judges meaning and authority, not thermodynamics; parity tests are the right instrument.
- Supporting-crate API claims (salsa, faer, num-dual, winnow, toml, miette, blake3, object_store, Ipopt). Taken from the supporting-library map as leads and labelled accordingly.
- The Dulmage–Mendelsohn, Hopcroft–Karp, block-triangularization and NL-writer algorithms as algorithms.
- Hash-consing collision behaviour across scopes; the
scope_instance_idcolumn on shared nodes (an observation only).
2. Authority and lifecycle map
Reconstructed from the blueprint. Cells the document does not settle are marked [invented]; each is evidence for a finding.
| Concept or fact | Semantic type and identity | Authority / owner | Revision or snapshot boundary | Permitted update path | Derived representations |
|---|---|---|---|---|---|
| Relation schema (columns, types, invariants) | reference.schema_* rows; relation_id; pse.contract.fingerprint | The registry (§4.1) | Registry fingerprint in every manifest | Registry edit + regeneration + migration row | Arrow schemas, typed views, builders, validators, providers, docs, JSON Schema (§4.2) |
| Operator contract | reference.operator_specs | The Rust operator table in pse-mathir (§7.3) — the relation is generated from code | Compiler version | Code change | The relation, nominal-value visitor, backend lowerings |
| Authored entity (unit, species, template, case) | pse.semantic_id = blake3_128("pse:authored:v1" ‖ package_id ‖ qualified_name) (§5.1) | Package document via change set | Model revision | Change set only (§22.2) | Everything in normalized/inferred/compiled |
| Identity under rename | [invented] — the ID formula makes a rename a new entity; no rename op exists; conformance tests (§24.1) require rename stability | — | — | — | — |
| Template equation | authored.template_equations.expression : pse.expr_dsl and authored.template_expr_* graph (§6.6 line 831) | [invented] — §7.7 says “the graph is the truth”; both are in authored and both are reachable by change_ops | Model revision | Change set | compiled.math_* |
| Solver profile, discretization policy, plan template | compiled.solver_profiles, compiled.discretization_policies (§6.11) | [invented] — written by P14/P11 yet referenced by authored.case_policies and authored.continuous_domains | [invented] | [invented] — a case cannot reference a row a later pass creates | Bound plans per problem |
| Method selection | authored.method_selections | Property package document | Model revision | Change set | inferred.method_resolutions (P6) with preference order persisted |
| Inference outcome (true/false/unknown/conflict) | prose only (§7.6, §14.2 rule 3) | [invented] — no inferred.* schema except method_resolutions carries a status column | — | — | — |
| Case specification | authored.case_specs (target by DSL path, priority, overlay chain) | Case document / runtime operation producing an overlay | Case revision | Change set; propagate, copy_at_time produce new overlay cases | compiled.variable_order, equation_order |
| Compiled mathematics | compiled.math_*, symbols, math_equations (IDs derived from instance × decl × index) | Passes P7–P11 | Snapshot content hash | Never edited; regenerated | Evaluation program, NL text, Pyomo bundle |
| Run and results | runtime.runs (run_id), solutions, duals, residuals, iterations | The run controller | Run | Append only | Stream tables, reports, sweeps |
| Derivation | provenance.derivations per derived row | The pass or rule that produced the row | Snapshot | Append only | Closure report, diff report |
Ref (head) | refs/<name>.json (§20.1) and provenance.refs (§6.13) | [invented] — two representations; the CAS object is presumably authoritative and the relation a projection | Ref version (e-tag) | Conditional put | — |
Deliberately opaque behaviour. Kernels (KernelSpec, §18.5): identity, signature, validity, derivatives, failure classes and bindings are declared; the body is Rust. External solver binaries reached through NL. The Pyomo adapter. Each has a registered contract; this satisfies DM-04.
Identity behaviour. Reorder, re-batch, reserialize: handled by §5.3 canonicalization. Regenerate: derived IDs are pure functions of the snapshot (§5.1). Rename: undefined and contradictory (F1). Duplicate / split / merge of units: not addressed (an ordinary extension would need “clone unit with new name”, which under F1 is a new entity with no link to its origin).
3. Semantic contracts and invariants
| Contract or invariant | Representation | Enforcement boundary | Failure behaviour | Verification evidence |
|---|---|---|---|---|
Every authored PK is registered in authored.entities | closure:entity_registered invariant row | P2 | validation.invariant | Proposed; negative fixture required by §24.1 |
| Unit consistency of every equation | residual_quantity_type_id after P10 | P10 | math.unit_inconsistent | Proposed — but the §8.3 rule rejects valid physics (F5) |
| Species valid in at least one phase | closure:species_has_phase | P2/P4 | validation.invariant | Proposed |
| Balance-type enum member has a law binding | P8 lookup | P8 | law.unsupported_binding (typed Unsupported — aligned with DM-43) | Proposed |
| Backend can lower every opcode | compiled.backend_bindings.status | P15 | backend.unsupported_opcode | Proposed |
| Connection member sets and quantity types match | conn.member_mismatch | P5 | validation.invariant | Proposed |
| Reference-state equality across packages requires explicit conversion | P10 refusal | P10 | math.unit_inconsistent | Proposed |
Pushdown truthfulness (Exact only where the scan guarantees it) | supports_filters_pushdown verdicts | Provider | None — DataFusion deletes the filter on trust (Interface-checked, Appendix A) | Unverified for FixedSizeBinary(16) key shapes (F17) |
| Snapshot coherence | Manifest lists every relation hash; ref CAS | Store protocol | runtime.infrastructure; readers never see a half snapshot | Proposed; interrupted-publication test absent from §24.1 (F15) |
| Memo validity | Key = input content hashes + pass version | Pass engine | Stale hit is silent | Incomplete key (F9) |
Change sets are the only write path into authored | change_ops | P1/P2 | Rejection of writes to derived relations | Proposed; governance test named |
Absence and uncertainty. The design distinguishes: free unknown (a symbol, never null), no initial guess (case_specs.initial null with a recorded substitution source), unbounded (pse.bound{unbounded}), missing measurement (null value with a provenance row), unresolved and ambiguous method resolution (typed status). It does not give “inference could not decide” a representation in any inferred.* head relation other than method_resolutions (F4). Overlay “untouched” is null in case_specs; there is no “reset to template default” value, which is acceptable because reset is expressed by discarding overlays (§17.1) — worth stating.
Equivalence requirements. Byte equality: canonical IPC bytes (§5.3) — under-specified on alignment, metadata version and metadata key order (F12). Structural equality: schema fingerprint. Semantic equivalence: hash-consed canonical form after P10, with commutative reordering by child hash declared as not ulp-preserving (§7.3) — an honest DM-40 declaration. Approximate numerical agreement: parity tolerances per slice (§24.2). These four are kept distinct, which satisfies DM-15 in principle.
4. Derivation and execution design
| Stage | Input revisions and dependencies | Output contract | Preconditions / assumptions | Effects and mutable ownership | Provenance / invalidation |
|---|---|---|---|---|---|
| P1 parse / change set | Package documents | authored.* rows with source spans | Documents parse | Writes a new model or case revision | Document content hash on rows |
| P2 validate | authored.*, reference.* | Findings; pass record | — | None | — |
| P3 normalize | authored.* | normalized.* (“canonical copies”) | Paths resolve | None | Derivations |
| P4–P6 closures (rule compiler) | normalized.*, reference.method_specs | inferred.* with derivations | Stratified rules; DataFusion session pinned | None declared; engine version and optimizer rule set are undeclared inputs (F9) | Per-row derivations |
| P7 instantiate | inferred.* | compiled.symbols, indexed math_* | Guards decidable | None | Derivations |
| P8 law expansion | Contributions, connections | Balance and connection equations; negative-completeness rows | Every contribution matched once | None | Derivations |
| P9 method realization | method_resolutions | Property equations, kernel bindings, UnitConvert | Kernels bound | None | — |
| P10 canonicalize | math_* | Hash-consed, typed DAG | Acyclic | None | Rewrite conditions recorded |
| P11 discretize | Policies, DAE links | Meshes, expanded symbols, stencils | Policies attached | None | IDs from parent × node |
| Index expansion | [invented] — no pass produces one scalar math_equations row per valid index tuple, yet P13’s incidence requires it | — | — | — | — (F6) |
| P12 case binding | Cases, overlays | problems, variable_order, equation_order | Overlays resolve | None | Ordinals by semantic ID |
| P13 structural analysis | Problem | Incidence, DM partition, blocks, static attributes | Scalar equations | None | — |
| P14 plans | Problem, static attributes, package defaults | Scaling, initialization, solve plans, profiles | Selection from static attributes only | None | — |
| P15 lowering | Problem, plans | Evaluation program, NL, bundle, backend_bindings | Backend chosen | None | Artifact hashes |
| Solve (runtime) | Problem, profile, environment | runtime.* rows | Backend supports the problem | Subprocess and file effects for NL solvers; Ipopt callbacks — not declared as effects in a contract | Run manifest; resolved solver options after capability probe not recorded (F10) |
| Publish | Artifacts | Manifest, ref | All artifacts written | Conditional put | Manifest lists everything |
Relationship structures. Containment (instance_tree), connectivity (topology_edges, connections), dependency (pass DAG, requested_by chains), scheduling (blocks.order, initialization_order, tear_candidates), provenance (derivations), equivalence (hash-consing) are separate relations with declared meaning. DM-34 satisfied at document level.
Provider selection and limitations. Methods: P6 with a persisted preference order (DM-19 aligned). Backends: solver_profiles.backend plus backend_bindings.status (DM-43 aligned). Linear solver: capability probe — timing and persistence unspecified (F10).
Boundary contracts. Python: Arrow C stream, one bundle (DM-37 aligned); per-column loss of pse.* extension types into storage types undeclared, and variables in the bundle carry no quantity type (F16). Native: buffer borrowing declared as a preference; offset and nullability preconditions absent (noted by the Arrow map; folded into F12).
Coherent publication. Snapshot manifest + ref CAS. Sound. Committed authored revisions are guaranteed valid only to the P2 level (structural and referential); a revision that fails P3 path resolution is publishable. This is acceptable but should be stated as the commit contract (§10 exception record).
5. Representative journeys
Ordinary extension — a new unit model
An author writes templates/absorber.yaml composing cv.distributed_1d@1, declares symbols, contributions, ports and a plan. New semantic decisions: one template document. Specialized implementation: none. Generated: nothing new (the registry is unchanged). Tests: a golden snapshot and a parity fixture. Extension locality is excellent — the charter §E pattern exactly. The same holds for a new pure-component method and a new costing method (§22.3). A new kernel adds one KernelSpec and one Rust body; adapters are generated. A new opcode legitimately touches the IR and every backend.
Meaningful change — switching a property package’s EOS
method_selections row changes → new model revision → P6 onward recomputed for states bound to that package (§14.4). Derived IDs of untouched instances are stable because they derive from instance × declaration × index, not from row position. The diff report (§22.4) lists added and removed equations and changed sparsity. This journey works as designed. The one weakness: if the author also renames the package while changing it, F1 turns the change into a delete-plus-create and the diff report cannot pair old and new.
Boundary — the Pyomo parity bundle
Ordinals, bounds, initial values, expression nodes and a source map cross as Arrow streams. Pyomo units are attached from the unit set, so dimension survives; basis, reference state and affine kind do not, and pse.semantic_id arrives as FixedSizeBinary(16) unless a pyarrow.ExtensionType is registered. For a solve this loss is harmless; for the parity harness comparing enthalpy residuals against IDAES it is exactly the information needed to explain a mismatch. The loss is real and undeclared (F16, DM-42).
Interruption — cancellation during a sequential-modular initialization plan
Stage k is a runtime.runs row with parent_run_id; cancellation arrives as a token. Overlays are immutable, so the model is untouched (DM-29 satisfied by construction — this is the design’s strongest recovery property). What is observable: runs up to k−1 complete, run k cancelled, the plan run cancelled. What is unspecified: whether a native solve can be interrupted between Ipopt callbacks without unwinding through the C boundary (the supporting map records this as the most important rule in its Ipopt section, and the blueprint does not state it); whether an NL solver subprocess is killed and its partial SOL file discarded; and whether a retried stage (retry_with_profile) is distinguishable from its first attempt other than by timestamp. Tests for none of these appear in §24.1 (F15).
6. Acceptance gates
| Gate | Verdict | Evidence or scope rationale | Required action |
|---|---|---|---|
| G1 — Authority | Fail | Two independently mutable representations of a template equation, both in authored (§6.6 line 831 vs §7.7). Policies referenced by authored rows live in compiled (§6.10 line 1000, §6.11 lines 1044, 1062). Authored identity derives from the qualified name (§5.1 line 478) while D4 (line 183) says names are never identity. Refs exist as a JSON object and a relation. | F1, F2, F3; one-line reconciliation for refs |
| G2 — Semantic fidelity | Fail | The four-valued inference outcome has no column in any inferred.* head relation except method_resolutions (§7.6 line 1249, §14.2 line 2024 vs §6.4–§6.7 schemas). The §8.3 Mul/Div rule (line 1324) rejects the reference expression P/(R·T) (line 1463). | F4, F5 |
| G3 — Validity | Unresolved | No pass owns index expansion between P7 and P13 (F6); P2 cannot validate a foreign key from authored into compiled (F3); the commit contract is P2-level validity but this is not stated. | F3, F6; state the commit contract |
| G4 — Hidden behaviour | Unresolved | Linear-solver capability probe timing and persistence (F10). Rule bodies are opaque JSON, so “which relations does this rule read” is not a declared, queryable dependency (F8). Everything else — inspection non-mutating, passes pure, overlays immutable — is explicitly designed. | F8, F10 |
| G5 — Consistency and recovery | Pass (with notes) | Manifest-then-CAS publication; immutable overlays; per-stage run rows; typed failure taxonomy. Notes: retry attempts unnumbered; FFI unwind rule unstated; adversarial tests absent (F15). | F15 |
| G6 — Transformation and reuse | Unresolved | Memo key omits the DataFusion version, optimizer rule set and session config (F9 — mechanisms to close it verified in Appendix A). Parameter folding is ambiguous and changes static families and incidence linearity (F7). Canonical reordering’s equivalence class is honestly declared. Hash contract under-specified on IPC parameters (F12). | F7, F9, F12 |
| G7 — Truthful capability claims | Unresolved | Exact pushdown on FixedSizeBinary(16) keys unverified; “iterative SVD for large” has no library route; condition-number cost is O(n) sparse solves without a budget; “pseudo-inverse via sparse LU” is not a route (F17). Enum members without law bindings, unsupported opcodes and missing derivatives are typed refusals — aligned. | F17 |
No weighted score offsets these. G1 and G2 fail on narrow, correctable points; nothing found requires a change to the architecture.
7. Principle findings
Ranked: correctness and authority defects first, then semantic duplication and extension difficulty, then measured cost and proportionality.
| # | Finding | Principle IDs | Concrete evidence or gap | Consequence | Proposed correction | Verification |
|---|---|---|---|---|---|---|
| F1 | Authored identity is a function of the qualified name, so rename is undefined and destructive | DM-11, DM-12, DM-02 | §5.1 line 478: semantic_id = blake3_128("pse:authored:v1" ‖ package_id ‖ qualified_name). D4 line 183: “Names … are never identity”. §22.2 change_ops.op ∈ {insert, update, delete} — no rename. case_specs.target is a DSL path (§6.10). §24.1 lists “rename and reorder” among conformance tests that must pass. | Renaming H101 to heater_1 creates a new entity: every derived ID under it changes, every case_specs row targeting the old path fails at P12 with case.unknown_target, prior runs and derivations no longer join to the model, and the §22.4 diff report shows a deletion and an addition rather than a rename. | Assign authored entity IDs at creation (UUIDv7, or blake3 of a creation nonce) and persist the ID in the document; keep qualified_name as an attribute. Reserve name-derived IDs for reference-package entities where the name is the contract, and say so. Add change_ops.op = rename and resolve case_specs.target to symbol_id at commit, storing both. Surface area: §5.1, §6.1, §6.10, §22.2, the parser. | Conformance test: rename a unit; assert all derived IDs, case bindings, and derivation joins are unchanged except qualified_name columns; assert the diff report contains one rename row. |
| F2 | Template equations have two authored representations | DM-02, DM-23, DM-14 | §6.6 line 831: “parsed form lives in authored.template_expr_*”; §7.7: “the graph is the truth and the text is retained … for display and re-parse tests”. Both are authored relations and both are valid change_ops targets. | A change set updating template_equations.expression without re-parsing leaves the graph stale: the display shows one equation and the compiler instantiates another; a change set updating graph rows directly bypasses the DSL’s source spans. “Re-parse tests” is a test, not a reconciliation protocol. | Make the text the only authored fact; produce the graph in normalized.template_expr_* from P1/P3 with a derivation. Alternatively make the graph authored and the text a rendered projection — but then the DSL loses its source spans. Surface area: one namespace move plus P1’s output list. | Governance test: no authored.* relation has a producing_pass_id; round-trip test parse→render→parse is the identity on the graph. |
| F3 | Policies and profiles are placed in compiled while referenced from authored | DM-13, DM-09, DM-07 | §6.11 lines 1044 and 1062 put discretization_policies and solver_profiles in compiled (written by P11/P14 per §14.1). §6.10 line 1000 authored.case_policies and §6.3 line 647 continuous_domains.discretization_policy_id reference them. §1.2: “No pass writes backward into authored.” | P2 cannot check the foreign key (the target does not exist until compilation); a profile regenerated by P14 gets a new identity and every case that named it dangles; ipopt.default is described as “shipped”, i.e. reference data, yet lives in a derived namespace. | Split: reference.solver_profiles / authored.solver_profiles (policy definitions), authored.discretization_policies, reference.plan_templates; keep in compiled only the bound instances per problem (compiled.problem_profiles, compiled.bound_plans). Surface area: §6.11, §14.1 P11/P14 output lists, Appendix B. | Governance test: no authored.* or reference.* column has fk_relation_id into compiled/runtime. |
| F4 | “Unknown is never silent exclusion” has no representation in the inferred relations | DM-08, DM-47 | §7.6 line 1249 and §14.2 rule 3 (line 2024): rows “are written to the head relation with status”. No status column exists on inferred.phase_species, instance_features, instances, ports, port_members, scope_members, boundary_crossings, topology_edges, state_flash_required (§6.4, §6.7, §9.2). Only method_resolutions has one. | A rule evaluating dynamic(I) where the parent’s dynamic is itself undecided has nowhere to write unknown; the implementer either drops the row (the forbidden outcome) or writes false, which then silently disables holdup terms downstream. | Either add status : enum(TruthValue) to every rule-produced head relation (generated from the registry, so one declaration), or add one relation inferred.undecided(rule_id, head_relation_id, key, reason, derivation_id) and require rules to write there. State which. | Fixture: a species with valid_phase_types = null and a phase of type undefined; assert an unknown row and a diagnostic, and assert the species is absent from phase_species rather than present with a guessed value. |
| F5 | The Mul/Div affine-kind rule rejects valid thermodynamics written in the reference templates | DM-06, DM-40, DM-24 | §8.3 line 1324: “affine kinds must be absolute-free (an absolute temperature may not be multiplied unless the author wraps it as convert(T, K))”. §9.4 line 1463: dens_mol_phase[p] = P/(R·T) with no wrapper; §9.3 NIST method t = T/1000{K}; every Arrhenius and EOS expression divides by T. | Either P10 rejects the entire reference property library, or every template wraps every temperature — noise that is itself an error source — and two implementers will pick different readings of “absolute”. The conflation is between point vs difference (T vs ΔT, which governs addition) and affine units (°C, gauge), which is a unit property already handled by is_affine and P3’s conversion to canonical units. | Redefine affine_kind ∈ {point, difference} on quantity types; restrict Mul/Div only to values still carried in an affine unit (impossible after P3 normalizes to canonical SI units); keep the addition algebra (point ± difference → point; point − point → difference; point + point → error unless declared as a weighted mean). Gauge pressure is a reference-state distinction, not an affine kind. Surface area: §8.1, §8.3, one enum. | P10 test set: P/(R·T) types without wrapping; T_out − T_in yields difference; T + ΔT yields point; T1 + T2 errors; 25{degC} literals convert to K at P3 before inference. |
| F6 | Index expansion (scalarization) has no pass and no contract; §7’s pass numbers are stale | DM-22, DM-21, DM-18 | §7.1 line 1166: “one indexed equation declaration … until P13 lowers it”; §7.2 lines 1189–1196: “via discretization (P13)”, “after P13/P14”; §7.1 line 1168: “fails explicitly at P14”. §14.1: P11 is discretization, P13 is structural analysis, P15 is backend lowering. §14.1 lists no pass that turns indexed math_equations (P7) into one row per valid index tuple, yet compiled.incidence (line 1085), equation_order and DOF need scalar rows. §8.2 line 1311 says P8 inserts UnitConvert; §9.3 and §14.1 say P9. | Implementer A flattens in P7 (losing DM-18’s compact form and making P7’s per-instance memo huge); implementer B flattens in P15 (then P13’s DOF and DM partition run over indexed rows and are wrong). The pass contracts in reference.pass_specs cannot be written from the document as it stands. | Add “P12a index expansion” (or fold into P12 with its own postcondition): input indexed equations + valid_index_tuples + meshes; output scalar math_equations rows with parent_equation_id and index, Broadcast/SumOver bodies rewritten per instance, IDs per §5.1. Renumber §7 and §8.2 to match §14.1. Surface area: §7.1, §7.2, §8.2, §14.1, §6.9 (add parent_equation_id). | Golden test on slice A: math_equations row count after expansion equals Σ over declarations of |valid tuples|; P13 preconditions assert every equation is scalar. |
| F7 | Whether parameter symbols are folded is undecided, and it changes static families, incidence linearity and the memo | DM-24, DM-40, DM-32 | §7.4 step 3 line 1218: folding “only among literal constants”. §14.4 line 2050: “constant folding and simplification that used a parameter value record the value’s hash”. §19.4 line 2366: estimated parameters are symbols “switched from parameter to free by the case” — so parameters must survive as symbols to P12. §6.12 incidence.linear : bool; §7.5 AFFINE_EQUALITY requires constant coefficients; §18.7 selects solve plans “from static attributes only”. | flow_mass = mw·flow_mol is BILINEAR if mw stays symbolic and AFFINE_EQUALITY if folded; DM partition, block order, nominal-value scaling and the solve-plan class differ between the two readings, and a case that estimates mw needs the unfolded form while a case that fixes it benefits from the folded one. | State: P10 never folds role = parameter symbols (the canonical graph is case-independent and hashes identically across cases). P12 produces a case-bound view in which treatment = parameter symbols are substituted, and P13’s static attributes are computed on that view with the substitution recorded in the derivation. Surface area: §7.4, §14.4, §14.1 P12/P13 contracts. | Test: same model, two cases (mw fixed vs mw estimated): identical P10 hashes; different math_static_analysis.family for the affected equations; incidence.linear differs accordingly. |
| F8 | Rule bodies, invariant specs and selectors are opaque JSON text, contrary to D1 | DM-10, DM-31, DM-49, DM-01 | D1 (§2): “no JSON column that carries required behavior”. §4.1 line 378 schema_invariants.spec : Utf8 (JSON); §6.11 line 1042 rule_specs.body : text (typed RulePlanSpec JSON); §6.7 line 878 scopes.selector : text (typed JSON); §6.10 line 1008 case_sets.generator; §6.11 line 1057 init_stages.target; §6.13 lines 1115, 1123. The rule bodies are the compiler’s inference logic — the most decision-relevant behaviour in the design. | Stratification (“a rule may negate only relations in a lower stratum”) cannot be checked by a P2 relational invariant; “which rules read authored.species” cannot be queried; impact analysis for §22.4’s diff report and §14.4’s invalidation must parse text; a semantic diff of a rule change is a JSON diff. | Promote the load-bearing part: reference.rule_plan_nodes / rule_plan_edges typed like math_expr_* (scan, filter, project, join, anti-join, union, distinct with typed payloads), or at minimum a derived reference.rule_dependencies(rule_id, relation_id, mode ∈ {read, negate}, stratum) generated when the registry is built. Selectors: a small selector_terms relation. Leave diagnostics_findings.values as JSON (genuinely open-ended). Surface area: §4.1, §6.11, one generator. | P2 invariant rules.stratified_negation expressed as a RulePlanSpec over rule_dependencies; a query “rules reading relation X” used by the diff report. |
| F9 | The DataFusion engine is an undeclared input of every inferred relation: version, optimizer rule set and session config are outside the memo key, and the plan fingerprint has no mechanism | DM-31, DM-32, DM-48 | §14.2 rule 5 pins “config snapshot, function registry hash, catalog snapshot” and requires a plan fingerprint; §14.3 keys memos on “content hashes of its declared inputs and its own version”. Neither names the DataFusion version or the optimizer/analyzer rule list. Interface-checked (Appendix A): Optimizer::with_rules(vec![…]) accepts an explicit rule list; information_schema.df_settings exposes the full config as a table; datafusion-proto serializes a LogicalPlan to protobuf bytes with a LogicalExtensionCodec for UDFs and extension nodes. | A DataFusion patch upgrade changes a rewrite (or a kernel UDF’s simplify), the pass record says “unchanged inputs”, and the memo returns a stale inferred.* artifact whose hash no longer matches what recomputation would produce — an invalid reuse that nothing detects until a clean rebuild. | Add to every rule-executing pass’s key: DataFusion crate version, the ordered optimizer/analyzer rule names (and pin the list with Optimizer::with_rules so an upgrade cannot add rules silently), and blake3 of information_schema.df_settings. Define the plan fingerprint as blake3 of the datafusion-proto encoding under the platform’s LogicalExtensionCodec, re-verified per upgrade. Surface area: §14.2 rule 5, §14.3, pass_specs.cache_key_inputs. | Test: bump the pinned rule list; assert every rule-pass memo misses. Test: two sessions with different batch_size produce identical plan fingerprints (the fingerprint must not include execution-only settings) — state which settings are semantic. |
| F10 | Solver capability probing happens at an unstated time and the resolved options are not recorded per run | DM-19, DM-28, DM-48 | §18.3 line 2285: linear_solver = ma57 “when HSL is available and mumps otherwise (a capability probe …)”; §26 line 2617: “capability probe at profile creation”. runtime.runs.environment records solver_version but not the linear solver actually used; solver_profiles.options is a shared row. | pse reproduce on a host without HSL runs the same solver_profile_id with MUMPS, takes a different iterate path, and the manifest claims the same profile. The selection depended on ambient state that no record captured. | Make the probe an explicit runtime operation whose result is a runtime.host_capabilities row; record runs.resolved_options (the options actually passed to CreateIpoptProblem) on every run; a profile that names ma57 on a host without HSL is a typed capability.backend failure unless the profile declares a fallback policy. | Reproduction test: replay a run’s manifest on a host lacking HSL; assert a typed failure or, with a declared fallback, a run row whose resolved_options differ visibly from the original. |
| F11 | Dependency anchors are stale and the pin mechanism does not achieve the stated intent; one authoring-boundary crate is unmaintained; one dual-number split is unaddressed | DM-48, DM-51, DM-45 | §3.1 pins DataFusion 55.0.0 / Arrow 59.2.0; the decided anchors are 55.1.0 / 59.3.0 (project owner, this review) and the evidence extraction under docs/design_review/evidence/ is at those versions. Both maps report (Interface-checked, capability map) that pinning only the umbrella crate lets sub-crates float and that cargo tree -d cannot detect a mixed family. The supporting map reports serde_yaml 0.9.34+deprecated (“no longer maintained”) at the authoring boundary, and feos-core 0.10.1 requiring num-dual ^0.14 against num-dual 0.15. | Mixed-family ArrayRefs are distinct Rust types with identical names; the failure message never says “version”. An unmaintained parser fed untrusted authoring documents is a DM-45 trust gap. Two num-dual copies make DualNum two traits and silently break §18.5’s monomorphization. | §3.1: pin every arrow-* and datafusion-* crate with = at 59.3.0 / 55.1.0 (or commit and enforce a lockfile); add num-dual to the pin table with cargo tree -d coverage; replace serde_yaml (the supporting map names three candidates; choose on span support and panic-freedom); enable ffi and canonical_extension_types features. | CI: cargo tree asserts one version per family; a governance test that the YAML crate is the chosen one. |
| F12 | The canonical hashing contract leaves three byte-affecting choices to library defaults, and the native borrow path omits two preconditions | DM-15, DM-48, DM-37 | §5.3 step 3: “deterministic alignment” — no value; no MetadataVersion; schema and field metadata are HashMaps whose iteration order is unspecified, yet pse.contract.fingerprint hashes them. The Arrow map’s probe (Interface-checked, capability map) measured that alignment changes the IPC bytes (872 vs 1032 for identical rows) and that concatenating batches is what makes split-invariant hashing true. §20.1 stores the same relation as .arrow and .parquet without saying which is hashed. The supporting map notes serde_arrow writes its own field-metadata key. D11/§18.2 borrow Arrow buffers without stating the ptr_offset() == 0 and null_count() == 0 preconditions. | A change of Arrow default alignment or a different metadata insertion order produces a different content_hash for identical semantics, so snapshot_ids in history stop reproducing; a sliced or nullable array borrowed by the tape reads wrong rows with plausible numbers. | §5.3: name the alignment constant and MetadataVersion and version them with the contract; sort metadata keys before hashing; state that content identity is defined over the canonical IPC encoding only and Parquet is never hashed; classify foreign metadata keys as volatile. §18.2: state the borrow preconditions. | Canonicalization property tests: identical relations under re-batching, metadata re-insertion order and dictionary re-encoding hash equal; a Parquet round trip reproduces the IPC hash. Negative test: a sliced array is copied, not borrowed. |
| F13 | salsa duplicates the memoization the artifact store already provides (over-construction) | DM-58, DM-57, DM-32 | §14.3 line 2032: passes are salsa queries “keyed by the content hashes of its declared inputs and its own version; the memo stores the output artifact hashes”. reference.pass_specs already declares inputs and cache_key_inputs (§6.11). Salsa’s distinctive value is automatic dependency tracking and backdating; here dependencies are declared by contract and backdating falls out of content-hash keying (an unchanged output hash is an unchanged input hash for the next pass). | A second incremental mechanism with its own database, macro dialect, unwind-based cancellation (which cannot cross the Ipopt FFI), pre-1.0 API churn and an unstated MSRV, whose safety net (catching an undeclared read) is precisely the case the contract already forbids. Sixteen passes do not need a query engine to memoize. | Defer salsa. Implement the pass engine as a map from (pass_id, pass_version, input hashes, engine key from F9) → output hashes, backed by the artifact store; per-instance memoization inside P7 uses the same key shape. Keep a seam so salsa can be adopted if sub-pass granularity proves necessary. Removes machinery; no verification needed beyond the existing incrementality tests (§24.1). | — |
| F14 | Per-row derivations for mechanically expanded rows are mandatory and unmeasured | DM-39, DM-58, DM-46 | §1.1 line 120: “every derived row carries a derivation”; §14.2 rule 4; provenance.derivations.supporting : list<struct> per row (line 1118). Discretization-expanded symbols and connection equations are pure functions already encoded in their IDs (§5.1). §24.3 lists no provenance-cost benchmark. | For a distributed dynamic model (10⁴ nodes × 10² symbols) the derivation relation is larger than the mathematics it explains; compile time and store growth carry a large constant nobody has measured, and the closure report depends on scanning it. | Declare derivation granularity per relation in the registry: row (law expansion, method resolution, inference — where negative completeness is the deliverable) or rule (discretization, connection expansion — reconstructible from the rule and the ID formula). Add a provenance-size and pass-time benchmark to §24.3. | Benchmark on slice C at increasing mesh size: derivation rows and bytes per equation; a reconstruction test that regenerates rule-granularity derivations from IDs. |
| F15 | Adversarial lifecycle tests are absent from the test plan | DM-54, DM-30 | §24.1 covers governance, canonicalization, rules, kernels, structural, conformance, diagnostics, initialization, incrementality, provenance, doctrine. It does not list: truncated or partially written artifact under a correct hash name; ref CAS conflict; cancellation mid-pass and mid-solve; NL subprocess kill; stale memo after an engine upgrade; concurrent change sets on one base revision. The Arrow map notes a StreamWriter dropped without finish() yields a truncated stream with no error at the call site. | The design’s recovery story (§20.1, §17.1, §23.2) is asserted, not protected; the first regression will be discovered in production. | Add a “lifecycle” layer to §24.1 with the six cases above, each with a negative assertion (rejected at its declared boundary) and, for publication, a hash-verify-before-put check. | The tests themselves. |
| F16 | The Python boundary’s loss profile is undeclared | DM-42 | §21.1 line 2455: bundle variables carry ordinal, id, name, bounds, initial, treatment, scale — no quantity_type_id. §4.4: Python consumers register matching extension types; unknown consumers see storage. Nothing states, per column, whether pse.* survives or degrades. | A parity mismatch on an enthalpy residual cannot be explained from the bundle because reference state and basis are absent; a consumer reading FixedSizeBinary(16) cannot tell a semantic ID from a content hash. | Add quantity_type_id and unit_id to bundle variables; publish a per-column loss declaration in the bundle manifest; register the ten pyarrow.ExtensionType classes in python/pse. | Round-trip test Rust → Python → Rust preserving extension names; parity harness test that reads reference state from the bundle. |
| F17 | Three numerical-analysis capabilities are claimed without a route or a budget | DM-43, DM-59, DM-39 | §15.5: “dense faer SVD for small problems, iterative for large” — the supporting map finds no iterative SVD in faer 0.24 (Interface-checked, capability map). §15.4: ‖J‖_F·‖J⁻¹‖_F “via faer sparse LU (pseudo-inverse for non-square)” — a sparse LU is not a pseudo-inverse route, and the Frobenius inverse norm needs O(n) sparse solves. §5.4: Exact pushdown on pse.semantic_id keys — whether DataFusion presents FixedSizeBinary(16) equality and IN lists in a shape the provider can match is unverified; Exact causes the optimizer to delete the filter (Interface-checked, Appendix A). | An “iterative SVD” that does not exist blocks the SVD toolbox above dense size; a condition number that costs a full solve per column is a diagnostic nobody runs; a wrongly Exact filter returns rows the query excluded, silently. | §15.5: either implement Lanczos over faer’s matrix_free, add a crate, or restrict the toolbox to dense-tractable sizes and say so. §15.4: use QR least-squares for non-square; add a Hager–Higham 1-norm estimator or declare the exact Frobenius form opt-in. §5.4: spike the filter shapes before claiming Exact; add the test-only wrapper provider that re-applies every Exact filter and fails on a survivor. | The spike; a CI wrapper-provider test over golden snapshots; a benchmark for the condition-number path. |
Observations (not findings). normalized holds “canonical copies of authored relations after P3” — a full duplicate namespace; consider views for relations P3 does not change. pse.ordinal_ref carries its target relation in platform metadata rather than in the extension type’s own metadata, so the type cannot enforce it. compiled.math_expr_nodes.scope_instance_id is ambiguous for a hash-consed node shared across scopes. runtime.runs lacks an attempt ordinal for retried stages. provenance.refs and refs/<name>.json need one sentence naming the CAS object as authoritative.
Applicability
| Group | Applied | Reason |
|---|---|---|
| 1 Semantic authority | Yes | The design’s thesis is D1; G1 findings F1–F3 |
| 2 Types, schemas, invariants | Yes | Physical typing and four-valued inference; F4, F5 |
| 3 Identity, versions, consistency | Yes | Three identity forms and content hashing; F1, F12 |
| 4 Declarative composition | Yes | Templates, laws, plans; the extension journey is the design’s strength |
| 5 Compilation and preservation | Yes | Sixteen passes; F6, F7 |
| 6 Planning, execution, effects | Yes | Overlays, runs, backends; F10 |
| 7 Dependencies and incrementality | Yes | Memo keys and salsa; F9, F13 |
| 8 Execution representations and performance | Yes, lightly | Native layouts and DataFusion’s four roles are well placed; no measurements exist (§24.3 says so) |
| 9 Boundaries and providers | Yes | Kernels, backends, Python; F16, F17 |
| 10 Provenance and reproducibility | Yes | Derivations and manifests; F14 |
| 11 Evolution and verification | Yes | Registry migrations, test layers; F15 |
| 12 Leverage and proportionality | Yes | F13, F14 and the §8 alternative |
Maturity assessment. Not scored. Everything is at rating 1 (stated in prose) by construction; a numeric total would manufacture precision. The proposed target is rating 3 at the end of each vertical slice, which §24.2’s acceptance criteria are designed to demonstrate.
8. Alternatives and architectural leverage
| Alternative | Semantic duplication and extension locality | Correctness and operational risks | Implementation / maintenance cost | Performance evidence | Why selected or rejected |
|---|---|---|---|---|---|
| Current baseline (IDAES on Pyomo) | Every unit re-expresses construction, validation, initialization and scaling in Python; five parallel maintenance surfaces (proposal §2.1) | Lazy construction by attribute access; inspection mutates; identity by name path; no snapshots | Established; large community | Known to be construction-bound at scale | The reason the project exists |
| Proposed design (blueprint rev. 1) | One declaration per unit, method, kernel, plan; adapters generated; inference as data | The findings above — all correctable within the architecture | High: ~150 relations, a registry-driven generator, a rule compiler, a native AD program, three backends | None yet (honestly stated) | Selected, subject to the §11 changes |
Simpler viable alternative — typed relations and generated views as proposed, but compiler passes as ordinary Rust functions over those views (no RulePlanSpec, no fixed-point executor, no salsa); DataFusion only for the snapshot catalog, batch kernels and analytics; whole-pass memoization keyed by declared-input content hashes; provenance rows written by the pass code | Extension locality for units, methods, kernels and plans is identical (those are documents and KernelSpecs either way). Inference rules become Rust functions — a SHOULD-level deviation from DM-16 — but with typed views they remain readable and are still tested against golden inferred relations | Loses the guarantee that every inferred row’s derivation is produced mechanically by the rule engine (it becomes a discipline); loses four-valued semantics as a framework property. Gains: no engine-version dependency inside the compiler (F9 largely disappears), no JSON rule bodies (F8 disappears), no second memo mechanism (F13 disappears) | Materially lower for phases 0–1; the rule compiler and fixed-point executor are the largest pieces of platform machinery with no analogue in IDAES | None — but it removes the unmeasured per-plan optimizer cost from the compile path | Partially adopted in the recommendation: keep the rule compiler for the closure passes P4–P6 and P8’s contribution matching, where set semantics, stratified negation and per-row provenance are the deliverable and where agents are expected to add rules; drop salsa (F13); type the rule bodies (F8) so that what remains is genuinely data. For everything else the passes are already ordinary Rust in the blueprint. |
Abstractions justified by current needs. The schema registry and generator (≈150 relations; five generated artifact kinds — DM-52 leverage is real). The math IR with typed payload relations (DataFusion Expr has no place for a quantity type; the map confirms). KernelSpec with generated adapters (six bindings per kernel). The contributions substrate and law templates (replaces the largest duplicated construction logic in IDAES). Immutable case overlays (DM-29 by construction). Content-addressed snapshots with manifests (DM-14, DM-48).
Abstractions not yet justified. salsa (F13). Per-row derivations for mechanically expanded relations (F14). egglog is correctly gated to phase 4 and correctly required to be an optimization, never a correctness dependency (the supporting map reaches the same conclusion). LogicalPlan::Extension nodes for rule bodies (DataFusion map item 26) would be additional machinery; evaluate only after F8’s typed rule relations exist, since they answer the same provenance question at the data level.
What remains ordinary code. Hopcroft–Karp, Dulmage–Mendelsohn, Tarjan, block triangularization; the evaluation program and reverse-mode adjoint; the NL writer and SOL reader; the Ipopt driver; the cubic-root and Helmholtz-state kernels; bubble/dew and Rachford–Rice estimators. The blueprint keeps all of these behind contracts and does not try to make them declarative — correctly.
9. Verification and measurement plan
| Claim or risk | Evidence label | Test / analysis / benchmark | Conditions and expected result | Current result or remaining gap |
|---|---|---|---|---|
| Rename preserves identity, bindings and provenance (F1) | Proposed | Conformance test “rename and reorder” (§24.1) | After the F1 correction: only qualified_name columns differ | Fails by construction today |
| One authority per template equation (F2) | Proposed | Governance test: no authored.* has producing_pass_id; parse→render→parse identity | Passes after moving the graph to normalized | Not yet expressible |
No authored FK into compiled (F3) | Proposed | Registry query over schema_columns.fk_relation_id | Zero rows | Two violations today |
| Unknown ≠ false (F4) | Proposed | Fixture with an undecidable phase validity | An unknown row and a diagnostic; no guessed row | No column to assert on |
| Unit inference accepts reference physics (F5) | Proposed | P10 over every reference template expression | Zero math.unit_inconsistent on the shipped library; the five algebra cases in F5 | Rule contradicts §9.4 |
| Index expansion contract (F6) | Proposed | Golden row counts; P13 precondition | Scalar rows = Σ valid tuples | No pass |
| Static attributes independent of parameter treatment (F7) | Proposed | Two-case test | Identical P10 hashes; families differ only on the case-bound view | Undecided |
| Memo invalidates on engine change (F9) | Interface-checked (mechanisms) / Proposed (test) | Bump pinned rule list → all rule-pass memos miss; plan fingerprint stable across execution-only settings | As stated | Key incomplete |
| Canonical hash invariance (F12) | Interface-checked (capability map probe) / Proposed | Property tests: re-batch, metadata order, dictionary re-encode, Parquet round trip | Equal hashes | Alignment and key-order unspecified |
Exact pushdown truthfulness (F17) | Interface-checked (semantics) / unverified (shapes) | Spike on FixedSizeBinary(16) filter shapes; CI wrapper provider re-applying Exact filters | Zero survivors | Spike not done |
| Interrupted publication and cancellation (F15) | Proposed | Six lifecycle tests | Rejected at the declared boundary; no partial artifact under a correct name | Not in the plan |
| Backend conformance (native vs Pyomo residuals and Jacobians) | Proposed | §24.1 parity harness, 1e-10 relative at random valid points | As stated | Requires Pyomo and IDAES installed; neither is in this repository |
| End-to-end performance | Hypothesis (DM-39) | §24.3 per-stage benchmarks at increasing mesh and component counts, plus provenance size (F14) | No claim before measurement — the blueprint already says this | Nothing measured |
Cost accounting (material categories). Construction and compilation: sixteen passes, the rule plans’ optimization time, provenance rows (unmeasured; F14). Transfer: one bundle per Python crossing (bounded). Execution: native tape; Ipopt; NL subprocesses. Storage: dual .arrow/.parquet artifacts per relation plus derivations. Recovery: idempotent artifact writes by hash. Inspection: SQL over the snapshot catalog. Memory: DataFusion pool (choice unmade — the DataFusion map’s item 19) plus native workspaces.
10. Exceptions and unresolved decisions
Exception E1 — commit contract is P2-level validity. Principle IDs: DM-14, DM-07. Scope: authored model and case revisions. Reason: path resolution and unit inference need the whole snapshot and are legitimately compiler work; requiring P3–P10 success before commit would block incremental authoring. Consequence: a committed revision may not compile. Compensating control: the closure report (§14.5) gates publication as a compiled problem; the commit boundary must be documented as “structurally and referentially valid” and the UI/agent surface must show the last closure status per revision. Evidence: §22.2, §14.5. Owner: platform architecture. Revisit trigger: an agent workflow that needs compile-clean commits.
Exception E2 — inference rules are relational data (DM-16 default retained) but only for the closure passes.
Principle IDs: DM-16, DM-58. Scope: P4–P6 and P8 contribution matching. Reason: set semantics, stratified negation and per-row provenance are the deliverable there; elsewhere passes are ordinary Rust. Consequence: two ways to write a pass. Compensating control: pass_specs.determinism and the same contract shape for both. Owner: compiler lead. Revisit trigger: a third party needs to add inference outside P4–P8.
Unresolved MUST-level items (the design is incomplete against these until revised): F1, F2, F3 (DM-02/DM-11/DM-13); F4 (DM-08); F5 (DM-06); F6 (DM-22); F7 (DM-24); F9 (DM-31/DM-32); F15 (DM-54). Each has a correction with small surface area.
Unresolved decisions the author must make (recorded, not resolved here): the YAML crate; num-dual version; the MemoryPool implementation and limit; the blake3 derivation mode for 128-bit IDs (derive_key vs truncation — the supporting map notes truncation safety is unverified in the docs it read); arrow-pyarrow vs pyo3-arrow; derivation granularity per relation; the iterative-SVD route; the condition-number budget; the Ipopt -sys binding strategy and linking.
11. Decision and implementation changes
Decision: Revise (document revision 2 before phase-0 code).
Reason. The architecture is sound and unusually well specified for a document at this stage: three representations with one identity system, typed relations with declared authority, contracted passes, immutable overlays, content-addressed snapshots, typed refusals for unsupported behaviour, and an honest stance on unmeasured performance. The failures are narrow and local: identity derived from names (F1), a duplicated authored representation (F2), policies in the wrong namespace (F3), a missing column for the design’s own four-valued logic (F4), a unit rule that contradicts the design’s own reference expressions (F5), a missing pass (F6), an undecided folding policy (F7), an incomplete memo key (F9), and a missing test layer (F15). None requires structural change; all must be fixed before the registry and pass contracts are generated, because each is cheap to change now and expensive after tests/golden exists.
| Priority | Change | Principle IDs | Acceptance evidence | Regression protection |
|---|---|---|---|---|
| 1 | Creation-time authored IDs; rename change op; case targets resolved to symbol IDs at commit (F1) | DM-11, DM-12, DM-02 | Rename conformance test passes | The conformance test |
| 1 | Move template_expr_* to normalized, produced by P1/P3 (F2) | DM-02, DM-23 | No authored relation with producing_pass_id | Governance test |
| 1 | Move profiles, discretization policies and plan templates to reference/authored; keep bound instances in compiled (F3) | DM-13, DM-09 | No authored FK into compiled | Registry governance query |
| 1 | status on rule-produced head relations or an inferred.undecided relation (F4) | DM-08 | Undecidable-fixture test | Fixture |
| 1 | Redefine affine kinds as point/difference; restrict Mul/Div by affine unit only (F5) | DM-06, DM-40 | Reference library passes P10; five algebra cases | P10 test set |
| 1 | Add the index-expansion pass and renumber §7/§8.2 (F6) | DM-22, DM-21 | Golden row counts; P13 precondition | Pass contract |
| 1 | State parameter-folding policy: symbolic at P10, substituted on the P12 case-bound view (F7) | DM-24, DM-32 | Two-case test | Test |
| 1 | Engine version, pinned optimizer rule list and df_settings hash in rule-pass keys; plan fingerprint via datafusion-proto (F9) | DM-31, DM-32, DM-48 | Rule-list bump invalidates memos | Test |
| 1 | Lifecycle test layer (F15) | DM-54 | Six tests exist and pass | The tests |
| 2 | Type the rule bodies and selectors, or at least generate rule_dependencies (F8) | DM-10, DM-31 | Stratification as a P2 invariant | Invariant |
| 2 | Runtime capability probe as a recorded operation; runs.resolved_options (F10) | DM-19, DM-48 | Reproduction test on a host without HSL | Test |
| 2 | §3.1: 55.1.0 / 59.3.0 family-wide = pins; num-dual pinned; replace serde_yaml; enable ffi, canonical_extension_types (F11) | DM-48, DM-51, DM-45 | cargo tree one version per family | CI |
| 2 | §5.3: alignment, MetadataVersion, sorted metadata keys, IPC-only identity, foreign-key classification; §18.2 borrow preconditions (F12) | DM-15, DM-48 | Canonicalization property tests | Tests |
| 2 | Bundle carries quantity types; per-column loss declaration; Python extension types registered (F16) | DM-42 | Round-trip test | Test |
| 2 | Resolve the three unbacked numerical routes and spike Exact shapes (F17) | DM-43, DM-59 | Spike report; wrapper-provider CI test | Test |
| 3 | Defer salsa; artifact-hash memo keyed by declared inputs (F13) | DM-58 | Incrementality tests unchanged | Existing tests |
| 3 | Declare derivation granularity per relation; add the provenance-size benchmark (F14) | DM-39, DM-58 | Benchmark on slice C | Benchmark |
| 3 | Adopt the capability maps’ gate-closing items not already covered above: validate extension types in generated try_from; RowConverter for keys containing pse.index_tuple; distinct/Kleene kernels in the rule compiler; CastOptions { safe: false } at import; governance test that no TableProvider mutation method is implemented; bounded MemoryPool; TimeProvider injection; conditional_arguments/short_circuits on conditional kernels; no config_options reads in kernels; Ipopt_get_curr_iterate over text parsing; the never-unwind-through-FFI rule | DM-07, DM-15, DM-08, DM-42, DM-02, DM-30, DM-28, DM-24 | Each named test in the maps | As named |
Final check. The design’s claims exceed its evidence in the places named above and nowhere else that this review inspected; the supported scope matches the phased delivery; the extension path for units, methods, kernels and plans is clear and validated by the journeys. After revision 2, re-review §5, §6.6, §6.10–§6.11, §7–§8, §14 only.
Appendix A — Library capability verification ledger
Facts verified in this review through context7 against /apache/datafusion (High reputation, 5,032 snippets). Everything else about libraries is taken from the three capability maps and labelled capability map.
| # | Fact | Source seen | Used by |
|---|---|---|---|
| A1 | datafusion-proto converts logical plans to and from protobuf bytes; a LogicalExtensionCodec serializes UDFs and extension nodes (the FFI table-provider path uses serialize_exprs with such a codec) | datafusion/proto/README.md; ffi/src/table_provider.rs | F9 — plan fingerprint mechanism; supersedes the DataFusion map’s “no stable plan hash” with a serialization contract |
| A2 | Optimizer::with_rules(vec![…]) builds the optimizer from an explicit rule list; the physical optimizer likewise holds a rules vector (22 built-in) | docs/source/library-user-guide/query-optimizer.md; physical-optimizer/src/optimizer.rs | F9 — pin the rule set rather than merely record it |
| A3 | information_schema.df_settings exposes every session setting as a queryable table, including datafusion.optimizer.* | docs/source/user-guide/sql/information_schema.md | F9 — a hashable config snapshot for the pass record |
| A4 | Field metadata on expressions is explicit: Expr::Literal(scalar, metadata) (48.0.0), with_metadata()/arrow_metadata() functions, and Expr::unalias_nested preserving aliases with metadata (54.0.0) | upgrading/48.0.0.md, upgrading/54.0.0.md, scalar_functions.md | §4.3 — metadata does not propagate through computed expressions by itself; rule outputs must be re-typed against the head relation’s generated schema (a rule to add to §14.2) |
| A5 | TableProviderFilterPushDown::Exact means “DataFusion will not add a FilterExec”; Inexact keeps one; the provider’s guarantee is unverified by the engine | library-user-guide/custom-table-providers.md | F17, G7 |
| A6 | UserDefinedLogicalNodeCore carries check_invariants(InvariantLevel), prevent_predicate_push_down_columns() (default: all columns), supports_limit_pushdown() (default false) | datafusion/expr/src/logical_plan/extension.rs | §8 — the DataFusion map’s item 26 is confirmed as available; kept at evaluate |
Additional capabilities surfaced by this review that the maps did not list: A1 (plan serialization as the fingerprint contract), A2 (explicit rule-list pinning), A3 (config snapshot as a table), A4 (explicit metadata carriers, and therefore the head-schema re-typing rule). Capabilities from the maps that this review endorses as gate-closing are listed in §11 priority 3.
Design review — Arrow-native IDAES core architecture blueprint (revision 2)
Standard: docs/design_review/design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01–DM-60, gates G1–G7), through AGENT_DESIGN_DIRECTIVE.md and DESIGN_REVIEW_TEMPLATE.md.
Subject: docs/design_review/Arrow-native-idaes-core-architecture-blueprint.md, revision 2 (2026-09-13), 2,792 lines. A design document; no code exists.
Prior review: design_review_arrow-native-idaes-core-blueprint_2026-09-13.md (revision 1; decision Revise; findings F1–F17). This review checks that revision 2 closed F1–F7, F9 and F15, and re-adjudicates the design against the four capability maps compiled after that review.
Depth: standard. Date: 2026-09-13.
1. Decision and scope
Proposal. Unchanged in architecture from revision 1: typed Arrow relations in seven namespaces, sixteen contracted passes (now P0–P16 with P12 index expansion), DataFusion 55.1 for inference rules, catalog, batch kernels and analytics, native numerics with in-process Ipopt, plus NL and generated-Pyomo backends. Revision 2 changed identity assignment, expression authority, policy placement, undecided-outcome representation, the unit algebra, pass structure, parameter-folding policy, the engine-as-input memo key, and the test plan.
Status. Proposed throughout. Where this review says Interface-checked it inspected the library surface itself through context7 (Appendix A). Where it says capability map, measured the claim rests on a probe program and output committed under docs/design_review/evidence/ by the four maps — inspected as documents here, not re-run.
Reviewer. Claude (Fable 5.1), design reviewer for the project owner.
Affected revisions. Blueprint revision 2; the four capability maps (Arrow-rust-, DataFusion-rust-, Supporting-rust-library-, Python-library-capability-map.md), all compiled 2026-09-13 against DataFusion 55.1.0, Arrow 59.3.0, and the current PyPI releases (Pyomo 6.10.1, pyarrow 25.0.1, IDAES 2.12.0).
Observable outcome claimed. As revision 1, with the nine priority-1 defects removed.
Supported scope and non-goals. Unchanged (§0.2 of the blueprint).
Constraints and uncertainty. Nothing is measured on the platform side. The maps now carry twenty-one measured probes of the libraries; several of them contradict blueprint sentences, which is the substance of this review.
Method and coverage
Examined:
- Every section of revision 2 that revision 2 changed, at the grain cited (§3.1, §4.3–§4.4, §5.1, §6.1, §6.6–§6.13, §7.1–§7.7, §8.1–§8.3, §14.1–§14.5, §18.3–§18.4, §19.1, §20.1–§20.2, §22.1–§22.2, §24.1, §26, Appendices B–C), plus the unchanged sections the maps now contradict (§3.2, §3.3, §5.3, §12.5, §15.3–§15.5, §21.2–§21.5, §25).
- The Python map in full. In the three Rust maps: the anchor sections, every cluster the maps mark as new or as measured against revision 2 (DataFusion D2, D3, D9, D10, Pass 3; Arrow Pass 3; Supporting §1 and Pass 3), and each map’s register, gate review, leverage matrix, and open items. The evidence README.
- Five context7 queries (Appendix A).
Guarantees attacked this round:
| Guarantee | Result |
|---|---|
| F1–F7, F9, F15 are closed as specified in revision 2 | Closed at the document level. F9’s mechanism is specified but, as the DataFusion map measured, not reproducible as written (finding R2-1) |
| §14.2 rule 5’s plan fingerprint is reproducible across processes | Fails (capability map, measured, PROBE C): HashMap metadata order enters the datafusion-proto bytes |
“-0.0 is preserved” (§5.3, §26) holds through the engine | Holds for ordering and the content hash; fails under GROUP BY and hash joins (capability map, measured, PROBE D) |
| “Nothing in DataFusion acts on these keys” (§4.3) | False for ARROW:extension:name at 55.1.0 (capability map, measured, PROBE X1; the TypePlanner pattern Interface-checked) |
| §3.3’s “no Python-side units library” is consistent with §21.2’s units requirement | Contradictory: Pyomo’s units machinery is pint (capability map, measured, PROBE 4) |
§21.5’s “no Any fields” and strict structuring are enforced by the named libraries | Not by default: cattrs ignores extra keys unless forbid_extra_keys is set (Interface-checked) and accepts Any (capability map, measured) |
| §3.2’s “carries every pin once” | One of twenty-four supporting crates and none of the Python libraries is anchored (blueprint text) |
§20.1’s commit rule names a method that exists at the pinned object_store | It does not: rename is not on the 0.13.2 trait (capability map, docs-checked) |
§5.4’s Exact pushdown is reachable on FixedSizeBinary(16) keys | Reachable for equality (capability map, measured, PROBE A); IN arrives rewritten to OR |
Not examined: physics and algorithms as before; the maps’ probe programs were not re-run; the Python API dumps were not opened.
2. Authority and lifecycle map
Revision 1’s invented cells are now filled: authored identity is creation-time (§5.1), template expression graphs are normalized (§6.6), policies are authored (§6.11), undecided outcomes have inferred.undecided (§6.7), the engine is a reference.engine_profiles row (§6.11). Two authority questions remain, both at boundaries:
| Concept or fact | Semantic type and identity | Authority / owner | Revision boundary | Permitted update path | Derived representations |
|---|---|---|---|---|---|
| Units in a generated Pyomo model | Pyomo/pint unit objects attached per Var | [ambiguous] — §8’s registry by intent; §3.3 forbids a Python units library; §21.2 requires one (finding R2-4) | — | — | pint validates; must never define |
| Python contract classes (§21.5) | attrs classes | [unstated] — hand-written or generated from RelationSpec? A hand-written column list is what §4.2 calls a governance failure (R2-4) | Registry fingerprint | Regeneration | cattrs structuring |
| Plan fingerprint (§14.2 rule 5) | blake3 of datafusion-proto bytes | Platform contract (implicitly) — its stability depends on platform-constructed schema metadata and the platform codec, which §14.2 does not say (R2-1) | Engine profile hash | — | pass_records.plan_fingerprints |
Deliberately opaque behaviour. Unchanged. The DataFusion extension-type registry is a newly identified place where the engine enforces §4.4’s storage types; it is not yet claimed (R2-3).
3. Semantic contracts and invariants
Revision 2’s new contracts, with the verification the maps supply:
| Contract or invariant | Representation | Enforcement boundary | Failure behaviour | Verification evidence |
|---|---|---|---|---|
| Rename changes attributes only | change_ops.op = rename; identity-based case_spec_targets | P1/P2 | case.target_text_mismatch | Proposed; conformance test named in §24.1 |
No authored FK into compiled/runtime | Registry query | Governance test | CI failure | Proposed |
| Undecided never consumed as true | Head relations hold decided-true rows only | Rule executor | inferred.undecided row + diagnostic | Proposed |
| Mul/Div unrestricted after canonical units | §8.3 algebra | P10 | math.unit_inconsistent | Proposed; reference-library test set in §24.1 |
| Every equation scalar before P14 | P12 postcondition | P12 | expand.unbound_index | Proposed |
| P10 graph independent of parameter values | §7.4 step 3 | P10/P13 | — | Proposed; two-case hash test |
| Engine version and rule set in the memo key | engine_profiles.content_hash | Pass engine | Memo miss | Proposed; the fingerprint component is non-reproducible (R2-1) |
pse.* storage types valid in every plan | [unclaimed] — DataFusion ExtensionTypeRegistry | Planning | Typed planning error | Capability map, measured (PROBE X1) |
| Python rows structured strictly | [asserted] — §21.5 prose | None by default | Unknown keys silently dropped | Interface-checked (cattrs docs) |
Absence and uncertainty. Revision 2 added the undecided representation; the remaining collapse is at the Python boundary, where a nullable Arrow column converted to ndarray turns null into NaN (capability map, measured, PROBE 8) and §21 says nothing about it.
Equivalence requirements. §5.3’s content hash is now better evidenced than revision 1 could say: the Arrow map measured that canonical IPC bytes are stable regardless of metadata key count (PROBE 6) and that Parquet preserves metadata and is deterministic within a writer version (PROBE 7). The equivalence class the engine applies to float values in grouping and joining is different from ordering (R2-2), and §5.3 does not say so.
4. Derivation and execution design
Only the rows revision 2 changed or the maps affected:
| Stage | Input revisions and dependencies | Output contract | Preconditions / assumptions | Effects and mutable ownership | Provenance / invalidation |
|---|---|---|---|---|---|
| P4–P6, P8 (rule passes) | normalized.*, reference.rule_specs, engine_profiles | inferred.*, undecided | Session built from the profile; Optimizer::with_rules (and analyzer rules — R2-1) | None | plan_fingerprints — non-reproducible as specified (R2-1) |
| P12 index expansion | indexed equations, tuples, meshes | scalar math_equations | Every free index has members | None | IDs per §5.1 |
| P13/P14 | cases, case_bound_substitutions | problem, static attributes on the case-bound view | — | None | parameter_dependence |
| Python adapter (§21) | bundle streams | Pyomo model | pint present (contradicts §3.3); external-function library resolves (only a warning today — capability map, measured) | Pyomo model in process | Loss profile undeclared (parked F16) |
Provider selection and limitations. Unchanged, with one correction from the maps: DataFusion enforces no relational constraint except Field nullability, so §5.4’s “Constraints: primary key only” is a declaration the platform’s P2 makes true (capability map, docs-checked).
Boundary contracts. The engine errors on an unregistered extension name while Python silently degrades to the storage type — the same design decision enforced on one side and not the other (both capability map, measured). §21 must declare the asymmetry.
5. Representative journeys
Meaningful change — a rule-pass memo hit across two processes
Process A compiles snapshot S: P4 runs its plans, records plan_fingerprints, memoizes on (inputs, pass version, engine profile hash). Process B, same binary, same S: the memo key’s engine profile and input hashes match, but the plan fingerprint — if it is part of the key or merely of the record — differs, because every pse.* relation carries two or more field-metadata keys and datafusion-proto serializes them in HashMap iteration order (capability map, measured, PROBE C: six encodings, six distinct byte strings at five keys). If the fingerprint is in the key, process B recomputes everything; if it is only in the record, pse reproduce finds two byte-different pass records for identical runs and §20.4’s assertion fails. Either way F9’s mechanism does not deliver what §14.2 promises until schema metadata is canonicalized at construction (R2-1).
Boundary — a parity run through the Python adapter
The bundle crosses as capsule streams (confirmed by the Python map). The adapter attaches units — which requires pint, which §3.3 excludes (R2-4). Rows are structured into attrs classes with a default cattrs converter: an extra key from a newer bundle version is silently dropped (Interface-checked), so a schema drift between Rust and Python is invisible. A KernelCall lowered to ExternalFunction with a library that does not resolve constructs successfully with a warning and fails from the solver later (capability map, measured, PROBE 3). Solution values pulled into numpy for comparison with IDAES convert null to NaN if any nullable column is touched (PROBE 8). None of these is architectural; all of them are contract statements §21 lacks.
Interruption — unchanged from revision 1 and now specified
§18.3’s no-unwind rule, §18.4’s subprocess rule, §20.1’s hash-verify-before-put and the lifecycle test layer are in the text. The Supporting map still lists its amendment F as parked; it is applied.
6. Acceptance gates
| Gate | Verdict | Evidence or scope rationale | Required action |
|---|---|---|---|
| G1 — Authority | Pass | Revision 1’s three failures are closed: creation-time IDs with a rename op (§5.1, §22.2); expression graphs derived into normalized (§6.6, §7.7); policies authored (§6.11) with a governance test. No second authority remains in the core. The units question at the Python boundary is a consistency defect, not a second definition, as long as pint validates and never defines (R2-4). | Reword §3.3 to authority; state that Python contract classes are generated. |
| G2 — Semantic fidelity | Unresolved | Revision 1’s failures (undecided outcomes, unit algebra) are closed. Two claims are now measured false or unqualified: “-0.0 is preserved” fails under grouping and joining (R2-2); the Python boundary collapses null into NaN and degrades extension types silently (R2-4). Both are boundary paths with no declared loss policy. | Qualify §5.3; forbid Float64 distinct/join keys in RulePlanSpec; declare the Python loss profile and the null_count == 0 rule. |
| G3 — Validity | Unresolved | Index expansion and the FK direction are fixed. The engine-level storage-type validation DataFusion offers is unclaimed (R2-3); §21.5’s strictness is asserted but the default converter drops unknown keys and accepts Any (R2-4); SessionConfig::set_str panics on invalid input (capability map, measured). | Register the pse.* types; forbid_extra_keys and an Any lint; ban set_str. |
| G4 — Hidden behaviour | Unresolved | F10 (probe timing) parked. New: supports_filters_pushdown is called more than once per plan and must be pure (capability map, measured); a missing external-function library only warns (R2-7, R2-4). | Purity note; library resolution check; F10. |
| G5 — Consistency and recovery | Pass (with notes) | Lifecycle tests, FFI and subprocess rules, hash-verify-before-put are specified. Notes: MemoryPool choice still unmade (the map measured that a bounded pool yields an actionable typed error — PROBE H); Python extension registration is process-global and non-idempotent. | Choose the pool; idempotent register-once helper. |
| G6 — Transformation and reuse | Unresolved | F7 and the engine-profile key are applied. F9’s fingerprint is non-reproducible as specified, its table-provider codec obligation is unstated, the analyzer rule list is captured but its installation is unnamed, and the semantic-settings selection by namespace misses two execution.* keys (R2-1). F12, F13 parked. | R2-1 corrections, or the simpler alternative in §8. |
| G7 — Truthful capability claims | Unresolved | F17 half-resolved (Exact reachable; SVD and condition-number routes now known). New: §3.2 and §3.1 disagree on pinning; §3.3 role text overshoots use sites for three crates; two library methods named in the text do not exist at the pinned versions; §4.3 erratum; Python pins and IDAES version wrong; a cited discipline file is deleted (R2-5, R2-6). | Adopt the maps’ pin tables; correct the errata. |
No gate fails. Five are unresolved on claim accuracy and boundary contracts; none requires an architectural change.
7. Principle findings
Ranked by severity. Revision 1 findings are referenced as Fn; this review’s findings are R2-n.
| # | Finding | Principle IDs | Concrete evidence or gap | Consequence | Proposed correction | Verification |
|---|---|---|---|---|---|---|
| R2-1 | F9’s plan fingerprint is not reproducible as specified, and three of its dependencies are unstated | DM-48, DM-31, DM-15, DM-59 | §14.2 rule 5 (line 2071): “blake3 of the plan’s datafusion-proto encoding under the platform’s LogicalExtensionCodec”. DataFusion map D9, PROBE C (capability map, measured): six independent encodings of one plan over a schema with five field-metadata keys gave six distinct byte strings; one shared Schema object gave one — the cause is Field::metadata HashMap order. Same cluster: logical_plan_to_bytes fails without a codec on a custom provider, so try_encode_table_provider/try_decode_table_provider (required methods) are platform obligations §14.2 never names; rule 5 names Optimizer::with_rules but not the analyzer’s own explicit list (Interface-checked: the analyzer rules are a separate ordered list and “the default pipeline may change between releases”); and the settings hash selects by namespace while datafusion.execution.skip_physical_aggregate_schema_check and enable_ansi_mode are semantic keys under execution.* (DataFusion map E3). | Identical runs record different plan_fingerprints, so §20.4’s byte-identical pass-record assertion fails on the first reproduction; if the fingerprint is ever folded into the memo key, no rule-pass memo hits. A DataFusion release adding a semantic key under execution.* escapes the hash. | Canonicalize schema and field metadata at construction (a generated constructor inserting keys in sorted order — the same fix serves pse.contract.fingerprint); define the fingerprint as a platform contract with its own version, stating that it covers the platform’s table-reference encoding and the kernel digests (rule 5 already says UDFs are encoded by digest); install analyzer rules explicitly too; replace the namespace rule with a versioned allow-list and assert the two execution.* keys stay false. Surface area: §14.2 rule 5, §6.11 engine_profiles, one constructor. See §8 for the simpler alternative. | CI test encoding the same plan in two fresh processes and asserting byte equality; a test that adding a rule to the engine profile misses every rule-pass memo. |
| R2-2 | “-0.0 is preserved” is stated without qualification and fails under grouping and joining | DM-40, DM-42, DM-08 | §5.3 step 4 (line 535) and the §26 risk row. DataFusion map D3, PROBE D (capability map, measured): ORDER BY keeps -0.0 before +0.0 and NaN last; GROUP BY returns four groups from five rows, merging the zeros; a self equi-join matches NaN to itself. §14.2’s rule algebra includes distinct and equi-join with no restriction on key columns. | A rule whose head relation is produced through distinct over rows containing a Float64 column, or an analytics query grouping on one, merges -0.0 with +0.0 and picks a sign the engine chooses; the content hash of that relation then differs from what §5.3’s “preserved” promises, silently. | State in §5.3 that preservation holds for ordering and hashing only; add a RulePlanSpec invariant that distinct and join keys are key-role columns (semantic IDs, ordinals, enums), never Float64; record the grouping collapse in §19.2 as a selected loss for analytics. | A rule-compiler test rejecting a distinct over a Float64 column; an analytics test documenting the collapse. |
| R2-3 | §4.3’s “nothing in DataFusion acts on these keys” is false, and the engine’s extension-type registry is an unclaimed validity mechanism | DM-06, DM-07, DM-44 | §4.3 (line 437). DataFusion map D10, PROBE X1 (capability map, measured): a pse.semantic_id registration over FixedSizeBinary(16) resolves from ARROW:extension:name; a field with the wrong storage type is rejected during planning; an unregistered pse.* name errors; seven arrow.* types are preloaded by default. The FixedSizeBinary(16) + ARROW:extension:name pattern is upstream’s own TypePlanner example (Interface-checked). | As written, an implementer expects extension metadata to be inert in plans; at 55.1.0 a mismatched storage type produces a planning error the design never anticipated, and an unregistered name errors rather than degrades — behaviour that differs from the Python side and is documented nowhere. Conversely, the cheapest G3 closure available (validation of every plan, not only generated views) is unused. | Narrow §4.3 to non-ARROW: keys. Add to §4.4/§14.3: the ten pse.* types are registered in the session’s ExtensionTypeRegistry from §4.4’s table (generated), duplicates asserted absent; note that the registry validates storage type and metadata only, and that paths outside a session (D11 borrowing, Python) still need the generated try_from check. Record that DataFusion validates no constraint but field nullability (§5.4). | Test: a batch with pse.semantic_id over Utf8 is rejected at planning; a test that the registered set equals §4.4’s table. |
| R2-4 | The Python boundary is specified by prose the named libraries do not enforce, and two of its sentences contradict each other | DM-42, DM-06, DM-08, DM-02, DM-52, DM-59 | (a) §3.3 line 333 “Explicitly not added: … a Python-side units library” vs §21.2 step 1 (line 2512) “units attached from the unit set so that assert_units_consistent passes” — Pyomo’s units container defers to pint and raises without it (capability map, measured, PROBE 4). (b) §21.5 (line 2529): “typed attrs classes … cattrs for structuring, no Any fields” and cites docs/library_ref/contract_substrate_discipline.md, which no longer exists; cattrs by default ignores extra keys (Interface-checked: “By default, cattrs ignores extra keys in unstructured input dictionaries”) and accepts Any fields (capability map, measured, PROBE 6). (c) Arrow → numpy with nulls converts null to NaN unless zero_copy_only=True refuses (PROBE 8); an unregistered pse.* type in Python becomes its storage type silently while ARROW:extension:name survives in field metadata (PROBE 1). (d) ExternalFunction with an unresolvable library only warns (PROBE 3). (e) §3.1 line 268 floors pyarrow at 15 (current 25.0.1) and names IDAES 2.10 (current 2.12.0, interpreter 3.10–3.13 only); attrs, cattrs, msgspec, numpy, scipy unpinned; msgspec’s role unstated. | Under §3.3 as written the adapter cannot be built; under §21.2 pint is present and nothing says it may not define a quantity. A bundle from a newer platform version structures without error, dropping the new columns. A parity comparison through numpy cannot distinguish “not computed” from NaN. A kernel bound to a missing library fails from Ipopt with no kernel id. Parity “against IDAES” is undefined without an exact version. | Reword §3.3: no Python-side units library is an authority; pint validates what the adapter emits and never defines a quantity. Generate the contract classes from RelationSpec (they join §4.2’s artifact list); forbid_extra_keys via the hook factory, detailed_validation kept on, transform_error into §23.2 findings, an Any/bare-dict lint over attrs.fields(). Assign msgspec to the manifest and all JSON/TOML the platform reads, attrs+cattrs to Arrow rows, no type in both. Ship and register the ten pyarrow.ExtensionType classes from the registry with an idempotent helper; declare the per-column loss profile (parked F16); assert null_count == 0 or carry the mask, zero_copy_only=True by default. Verify the external-function library resolves before building the model; SolverFactory(name).available(False) pre-flight; record pyomo.contrib.*, numpy and scipy versions in runs.environment. Pin every Python library == and state two interpreter ranges (platform ≥3.11; parity 3.10–3.13); pin IDAES 2.12.0 for parity. Replace the dangling citation with the discipline itself. | Python tests: extra key rejected; Any field fails the lint; extension names survive Rust→Python→Rust; a nullable column refuses ndarray conversion; a missing library raises capability.backend before construction. |
| R2-5 | Dependency declarations are incomplete or inaccurate at the pinned versions | DM-31, DM-48, DM-51, DM-59, DM-43 | §3.2 (line 277): “[workspace.dependencies] carries every pin once”; §3.1 anchors tokio and none of the other 23 supporting crates the Supporting map enumerates (its §1.2 table resolves all of them into a committed lockfile). §3.1 still pins arrow-flight, which nothing consumes (Arrow map §10.1). uom is named in §3.3 (line 323) and appears nowhere in §8 (Supporting map Q13). §3.3 role text overshoots the body: faer “Sparse LU/QR … solver workspaces” (line 322) with no QR or workspace use site; petgraph “dependency ordering, SCC, DAG checks” (line 318) while matching and DM are carved out; feos “Native Helmholtz/SAFT/cubic” (line 324) while §9.8 delivers Helmholtz by expression template. serde_yaml still named; the num-dual split is source-level (DualNum<f64> vs DualNum<Primitive = f64> — capability map, rustdoc). blake3 is assigned to no crate and its 128-bit derivation is unnamed (derive_key vs truncation give different bytes — capability map, measured, PROBE 1). No lockfile requirement; cargo audit cannot see serde_yaml’s deprecation (capability map, RustSec-checked). | A build resolves whatever is current for 23 crates and every Python library, so §20.4’s reproduction claim is false for the toolchain; a transitive second Arrow or num-dual version is undetected; a reader justifies faer QR or uom from §3.3 for work that does not exist; permanent §5.1 IDs are frozen with an unstated derivation. | Adopt the Supporting map §1.2 and Python map §1.1 pin tables into §3.1; require a committed, CI-enforced lockfile with a one-version-per-family assertion; add cargo deny maintenance and bans checks to §24; drop arrow-flight; either name the rule for uom-typed kernels or remove uom; narrow the three §3.3 rows; choose serde-saphyr (the map’s evidence is one-sided); pin num-dual 0.15 and mark feos blocked on its upstream; assign blake3 to pse-ids and name derive_key as the 128-bit route in §5.1. | CI: one version per family; cargo deny green; a governance test that every crate in Cargo.toml appears in §3.1’s table. |
| R2-6 | Four statements about library surfaces are false or stale at the pinned versions | DM-59, DM-43 | §20.1 (line 2461, added in revision 2): “rename is never used as a commit primitive” — object_store 0.13.2 has no rename; 0.13.0 merged the plain methods into *_opts (capability map, docs-checked at the pinned version). §15.3 (line 2145): “Hopcroft–Karp (own implementation; petgraph lacks it)” — petgraph ships maximum_matching (Gabow, general graph, O(|V|³), input treated as undirected; capability map, rustdoc). §15.4 (line 2157): “pseudo-inverse for non-square” via sparse LU — the route is QR least-squares or pseudoinverse_from_svd_with_tolerance (capability map, rustdoc). §15.5 (line 2172): “iterative for large” — faer’s matrix_free::partial_svd targets the largest singular values; the smallest need shift-invert through the sparse LU (capability map, rustdoc). | An implementer reads a method that does not exist, or implements Hopcroft–Karp believing no alternative exists (the reasoning is right, the stated reason wrong), or reaches for partial_svd and gets the wrong end of the spectrum. | §20.1: “the final path is written once with PutMode::Create/Update; no copy-then-delete sequence is a commit”. §15.3: “petgraph ships only general-graph matching (Gabow, O(|V|³)); the incidence graph is bipartite, so Hopcroft–Karp’s O(|E|√|V|) is required”. §15.4/§15.5: name the QR/SVD-with-tolerance route, shift-invert plus matrix_free for the smallest singular values, or restrict the toolbox to dense-tractable sizes. | Doc-lint: every backticked library identifier in §15, §18, §20 resolves in build/facts/*. |
| R2-7 | Exact pushdown is reachable, but two facts change how §5.4 must be implemented | DM-43, DM-20, DM-53 | DataFusion map D2, PROBE A (capability map, measured): key equality arrives as rel.id = FixedSizeBinary(16, …) (matchable); IN (a, b) arrives already rewritten to OR; conjunctions are split; IS NOT NULL on a non-nullable column is eliminated; supports_filters_pushdown is called more than once per plan. §5.4 promises Exact on key columns and says nothing about IN or purity. | A provider matching Expr::InList never fires and silently falls back to a full scan; a provider that counts or logs in supports_filters_pushdown double-counts; nothing verifies an Exact claim (the optimizer deletes the filter — Interface-checked in the prior review). | §5.4: match binary equality and OR chains of equalities, not InList; declare supports_filters_pushdown pure; add the test-only wrapper provider that re-applies every Exact filter; use Precision per statistic and answer StatisticsRequests selectively (parked from revision 1). §26 F17’s pushdown precondition is discharged for key equality. | Wrapper-provider CI test over golden snapshots; a test that an IN on a key column reaches the scan as Exact. |
| R2-8 | Library mechanisms the design re-implements or renders poorly (leverage) | DM-38, DM-49, DM-47, DM-16, DM-46 | §12.5 (line 1936) and §15.3 list tear selection as hand-written; petgraph greedy_feedback_arc_set returns exactly the recycle edge on a flowsheet-shaped graph (capability map, measured, PROBE 3c). §23.2 wants failures that carry rows; a pse.semantic_id renders as sixteen raw bytes without an ArrayFormatterFactory / DFExtensionType::create_array_formatter (both maps). §24.1 does not use Arrow’s force_validate feature in the test profile. §14.2 rule 5 hashes engine settings but platform policy travels separately — a ConfigExtension (datafusion.pse.*) would enter the same df_settings hash. Pass records store plan text; EXPLAIN has a pgjson format. The optimizer observer closure records which rules fired. toml::Spanned supplies pse.source_span for free; num-dual::implicit_derivative matches a hand-derived implicit function theorem (PROBE 4); the Ipopt C exports are GetIpoptCurrentIterate/GetIpoptCurrentViolations. | Hand-written search where a library primitive exists; diagnostics that satisfy §23.2’s letter and not its purpose; a validity gate available for one profile line left unused; platform policy outside the reproducibility hash. | Adopt: feedback_arc_set as the tear search (policy stays ours); one formatter factory driven from §4.4’s table; force_validate in test/CI profiles; a datafusion.pse.* config extension for platform policy; pgjson for stored plans; the observer for fired-rule evidence; toml::Spanned; implicit_derivative as the implicit-kernel binding; correct the Ipopt names when §23.1 is amended. | Each adoption’s own test; the formatter has a snapshot test per pse.* type. |
Status of revision 1’s parked items. F8 (JSON rule bodies), F10 (probe timing), F11 (now subsumed by R2-5), F13 (salsa; the Supporting map measured backdating and accumulators work as described — PROBE 6 — which does not change the DM-58 question), F14 (derivation cost), F16 (loss profile; now sharpened by the measured Rust/Python asymmetry), F17 (half-resolved by PROBE A and the faer findings). F12 gained precision: canonical IPC bytes are stable regardless of metadata key order (PROBE 6), so the exposure is platform-side fingerprints and datafusion-proto, which is R2-1; the alignment and MetadataVersion constants are still unnamed.
Observations (not findings). The Supporting map’s amendment F (no unwind through Ipopt) is listed as parked; it is applied in §18.3. datafusion-tracing and instrumented-object-store top out at 55.0.0 against a 55.1.0 engine and should not be added yet. LogicalPlan::Extension for rule bodies survived the optimizer in the map’s PROBE B and is now an adopt-candidate on cost grounds alone; it remains a DM-58 decision, not a defect.
Applicability
| Group | Applied | Reason |
|---|---|---|
| 1 Semantic authority | Yes | G1 re-checked after F1–F3; R2-4 units authority |
| 2 Types, schemas, invariants | Yes | R2-2, R2-3, R2-4 |
| 3 Identity, versions | Yes | R2-1 fingerprint, R2-5 blake3 derivation |
| 4 Declarative composition | Lightly | Unchanged since revision 1; no map finding bears on it |
| 5 Compilation | Yes | P12, parameter folding verified as specified |
| 6 Execution, effects | Yes | R2-7 purity; G5 notes |
| 7 Dependencies, incrementality | Yes | R2-1, R2-5 |
| 8 Execution representations | Lightly | R2-6 faer routes |
| 9 Boundaries, providers | Yes | R2-3, R2-4, R2-7 |
| 10 Provenance | Yes | R2-1 pass records, R2-8 observer |
| 11 Evolution, verification | Yes | R2-5 lockfile and cargo deny; R2-8 force_validate |
| 12 Leverage | Yes | R2-8; §8 |
Maturity assessment. Not scored; nothing is implemented. The library side is now at Interface-checked or capability map, measured for most of the surfaces the design depends on, which is a materially stronger evidentiary position than revision 1 had.
8. Alternatives and architectural leverage
| Alternative | Semantic duplication and extension locality | Correctness and operational risks | Implementation / maintenance cost | Performance evidence | Why selected or rejected |
|---|---|---|---|---|---|
| Current baseline (revision 1) | As reviewed | Nine priority-1 defects | — | None | Superseded |
| Proposed (revision 2) | Same locality; identity and policy placement now sound | R2-1 through R2-7 — all boundary or claim-accuracy defects | As revision 1 plus one pass | None | Selected, subject to §11 |
Simpler alternative for F9: drop the plan fingerprint from the memo key; key rule passes on (input hashes, rule_specs hash, pass version, engine profile hash) only; record a plan rendering in the pass record as evidence, never as an identity | No change to extension locality | Sound, because the plan is a pure function of the rule spec, the catalog snapshot and the engine profile — all already in the key — so the fingerprint adds no information to reuse validity. Removes a dependency on platform-controlled metadata order and on a required codec from the correctness path. The pass record still needs a reproducible rendering, which R2-1’s canonicalization supplies for datafusion-proto bytes and which pgjson supplies for humans | Lower: no fingerprint in the key, no codec on the reuse path | None needed | Recommended. Keep the datafusion-proto bytes (canonicalized) in pass_records for explainability; take them out of the reuse argument. |
Abstractions justified by current needs. Unchanged from revision 1’s review. The ExtensionTypeRegistry registration (R2-3) is one generated function, not new machinery. The Python contract generation (R2-4) extends an existing generator rather than adding one.
What remains ordinary code. Unchanged, with one transfer: tear-set search moves to petgraph’s feedback_arc_set; the tear policy stays platform code.
9. Verification and measurement plan
| Claim or risk | Evidence label | Test / analysis / benchmark | Conditions and expected result | Current result or remaining gap |
|---|---|---|---|---|
| Plan fingerprint reproducible (R2-1) | Capability map, measured (negative) | Two-process byte equality | Equal after metadata canonicalization | Six distinct encodings today |
-0.0 under distinct/join (R2-2) | Capability map, measured | Rule-compiler rejection of Float64 keys | Rejected | Unrestricted today |
pse.* storage validated by the engine (R2-3) | Capability map, measured | Wrong-storage batch rejected at planning | Typed error | Mechanism unclaimed |
| Python strictness (R2-4) | Interface-checked / measured | Extra-key rejection; Any lint; null→NaN refusal; library-resolution check | All reject | Defaults permit all four |
| Pins and lockfile (R2-5) | Blueprint text | cargo tree one version per family; cargo deny | Green | 23 crates and all Python libraries unpinned |
Exact on key equality (R2-7) | Capability map, measured | Wrapper-provider CI test | Zero survivors | Not yet written |
| F1–F7, F15 closures | Proposed | The tests revision 2 named in §24.1 | As named | Not implemented |
| End-to-end performance | Hypothesis (DM-39) | §24.3 | No claim | Nothing measured |
Cost accounting. Unchanged. One addition: the ExtensionTypeRegistry is consulted during planning and its overhead is unmeasured (DataFusion map, unverified) — trivial to measure once a session exists.
10. Exceptions and unresolved decisions
Exception E1 (carried). Commit contract is P2-level validity — still not stated in the blueprint; record it in §22.2.
Exception E2 (carried). Rule passes are relational data; other passes are Rust.
Unresolved MUST-level items: R2-1 (DM-48), R2-2 (DM-40/DM-42), R2-4 items (a)–(c) (DM-42, DM-06, DM-08), R2-5 pins (DM-48), R2-6 (DM-59). R2-3 is a SHOULD-level leverage item with a MUST-level documentation erratum in §4.3.
Decisions the author must make (recorded, not resolved): whether the plan fingerprint stays in the memo key (§8 recommends not); YAML crate (evidence favours serde-saphyr); num-dual 0.15 with feos blocked; blake3 derive_key; MemoryPool and its limit; arrow-pyarrow vs pyo3-arrow; whether to adopt LogicalPlan::Extension for rule bodies; F13 salsa; derivation granularity (F14); the SVD toolbox scope (F17).
11. Decision and implementation changes
Decision: Revise (document revision 3), narrower than revision 1’s. The architecture is settled and every revision-1 priority-1 finding is closed in the text. What remains is one applied fix whose mechanism the maps measured as non-reproducible (R2-1), two claims measured false or unqualified (R2-2, R2-3), a Python boundary specified by prose its libraries do not enforce (R2-4), and dependency and library-surface statements that are wrong at the pinned versions (R2-5, R2-6). Foundational code that does not touch these — the schema registry, identities, quantities, the math IR, canonicalization — can begin against revision 2; the rule-pass memo key, the Python package, pse-backend-native’s FFI surface and §5.3’s hashing constants must wait for revision 3.
| Priority | Change | Principle IDs | Acceptance evidence | Regression protection |
|---|---|---|---|---|
| 1 | Canonicalize schema/field metadata at construction; take the fingerprint out of the memo key (or make it reproducible); state the codec obligations, analyzer-rule installation and a versioned settings allow-list (R2-1) | DM-48, DM-31, DM-15 | Two-process byte equality | CI test |
| 1 | Qualify -0.0 preservation; forbid Float64 distinct/join keys in RulePlanSpec; declare the analytics collapse (R2-2) | DM-40, DM-42 | Rule-compiler rejection test | Test |
| 1 | Reword §3.3 units exclusion to authority; generate Python contract classes; strict cattrs with Any lint; msgspec/attrs division; ship and register pse.* pyarrow types; loss profile; null→NaN rule; library and solver pre-flight checks; Python pins and interpreter ranges; IDAES 2.12.0 (R2-4, F16) | DM-42, DM-06, DM-08, DM-52 | Python boundary tests | Tests |
| 1 | Adopt the maps’ pin tables; committed enforced lockfile; cargo deny; drop arrow-flight; resolve uom; narrow §3.3 rows; serde-saphyr; num-dual 0.15; blake3 to pse-ids with derive_key (R2-5) | DM-31, DM-48, DM-51 | One version per family; every crate in §3.1 | CI |
| 1 | Correct §20.1 rename, §15.3 petgraph, §15.4/§15.5 faer routes (R2-6) | DM-59 | Identifiers resolve in build/facts/* | Doc-lint |
| 2 | Narrow §4.3; register the pse.* types in the engine registry; record that DataFusion validates only field nullability (R2-3) | DM-06, DM-07 | Wrong-storage rejection test | Test |
| 2 | §5.4: OR-chain matching, purity, wrapper-provider test, Precision, selective StatisticsRequests (R2-7) | DM-43, DM-20 | Wrapper test | CI |
| 2 | Parked revision-1 items F8, F10, F12 (name the IPC constants), F13, F14, F17 (choose the SVD route) | as listed in §26 | — | — |
| 3 | Leverage adoptions (R2-8): feedback_arc_set, formatter factory, force_validate, ConfigExtension, pgjson, observer, toml::Spanned, implicit_derivative, Ipopt names | DM-38, DM-49, DM-47, DM-16 | Per-adoption tests | Tests |
| 3 | Decide LogicalPlan::Extension for rule bodies on cost (its survival is measured) | DM-18, DM-58 | Decision record | — |
Final check. Revision 2’s claims match its evidence except where the four maps measured otherwise, and each such place is named above with the line it occurs on; the supported scope is unchanged; the extension path is unchanged and still clear.
Appendix A — Library verification ledger for this review
| # | Fact | Source seen | Label | Used by |
|---|---|---|---|---|
| A1 | Upstream’s TypePlanner example maps a SQL type to FixedSizeBinary(16) with ARROW:extension:name metadata and is installed via SessionStateBuilder::with_type_planner; only one TypePlanner is active at a time | docs/source/library-user-guide/extending-sql.md via context7 | Interface-checked | R2-3 (the shape the engine expects for pse.semantic_id) |
| A2 | The analyzer rules are a separate ordered list from the optimizer rules; “the default pipeline may change between releases” | datafusion/core/src/optimizer_rule_reference.md, query-optimizer.md via context7 | Interface-checked | R2-1 (install analyzer rules explicitly) |
| A3 | cattrs: “By default, cattrs ignores extra keys in unstructured input dictionaries”; forbid_extra_keys via make_dict_structure_fn and a hook factory raises ForbiddenExtraKeysError; transform_error renders ClassValidationError groups | docs/customizing.md, docs/validation.md via context7 (/python-attrs/cattrs) | Interface-checked | R2-4 |
| A4 | Optimizer::with_rules(vec![…]) and information_schema.df_settings (prior review) | as before | Interface-checked | R2-1 |
| A5 | ExtensionTypeRegistry, ExtensionTypeRegistration::new_arc, SessionStateBuilder::with_extension_type_registry; PROBE X1 rejection of a mismatched storage type | DataFusion map D10 (rustdoc extraction and probe under docs/design_review/evidence/rust/) | Capability map, measured — not re-run here | R2-3 |
| A6 | datafusion-proto byte instability with ≥2 field-metadata keys; codec required methods | DataFusion map D9, PROBE C | Capability map, measured | R2-1 |
| A7 | GROUP BY/join merge of -0.0 and NaN self-equality | DataFusion map D3, PROBE D | Capability map, measured | R2-2 |
| A8 | Pyomo units raise without pint; ExternalFunction warns on a missing library; cattrs accepts Any; null→NaN on ndarray conversion; unregistered extension types degrade with metadata retained | Python map PROBES 1, 3, 4, 6, 8 (docs/design_review/evidence/python/) | Capability map, measured | R2-4 |
| A9 | object_store 0.13.2 trait surface (no rename); petgraph maximum_matching docs; faer partial_svd targets the largest singular values; blake3 derive_key vs truncation; feedback_arc_set result | Supporting map §1, §13, ledger rows 57–61 | Capability map, docs- and probe-checked | R2-5, R2-6, R2-8 |
Additional capabilities surfaced by this review beyond the maps: none. The maps’ pass-3 sweeps enumerated the library surfaces with stated denominators; this review adjudicated their findings against the charter rather than adding to the inventory.
Design review: blueprint revision 4 and library contracts
Historical assessment of revision 4, identified by the digest below. The revision-5 follow-up reviews the resulting amendment; the findings and probe receipts here remain unchanged.
1. Decision and scope
Decision: Revise. Retain the Arrow relation authority, separate mathematical IR, explicit case overlays, and native numerical execution. The largest immediate improvement is to make existing library mechanisms enforce the proposed contracts. Several mechanisms currently credited with enforcement provide only an interface or a declaration. Adding more infrastructure before correcting that distinction would increase the number of places that can disagree.
Proposal: blueprint revision 4. Standard: Data Model–Based Design Charter, version 1.0, including DM-01–DM-60 and independent gates G1–G7. Reviewer: Codex. Date: 2026-09-13. Evidence status: the platform design is Proposed. Selected library routes are Interface-checked; six standalone characterization groups are Tested under the conditions in §9. This is not an implementation acceptance review.
Observable outcome: a compiler whose model meaning survives validation, inference, numerical preparation, storage, and backend transfer; ordinary process model extensions add declarations and any genuinely new algorithm, with mechanical adapters derived from those declarations.
Baseline and constraints: the repository is skeletal. Existing foundation checks do not establish a functioning modeling or solving lifecycle. The review covers the proposed local snapshot/compiler/native/NL/Pyomo architecture and its stated delivery phases. It does not require a distributed runtime or a new query engine. Scientific parity with IDAES 2.12.0 remains a future behavioral gate.
Method and coverage
The analysis reconstructed authority, pass dependencies, invariants, and failure paths from blueprint §§2, 4–9, 12–24, then compared them with the charter. The heater and lumped control-volume examples, property-demand algorithm, and phase exits provided concrete consumers for the extension and proportionality tests. Blueprint §§10–11 and Appendix A were sampled for architectural integration; individual thermodynamic correlations, all unit templates, and the entire IDAES coverage matrix were not independently certified.
The Arrow, DataFusion, and supporting Rust libraries maps supplied discovery leads. Their relevant claims were compared with the current blueprint, existing probe source, and pinned upstream source. Earlier reviews were consulted for context; their acceptance statements were not reused as current proof. The Python contract in blueprint §21 was inspected as a design; the Python capability map and its behavioral probes were not re-audited.
Context7 was used for DataFusion provider contracts, Arrow extension validation, and UDF predicate inversion. Versioned upstream documentation supplemented it. Retrieved examples were treated as leads: Context7 returned some historical or generic snippets that cannot establish a 55.1.0 contract. Final library findings rest on the pinned source and executable probes, not snippet ranking.
Local evidence included just metadata, Cargo resolution of the scratch probe,
Arrow 59.3.0 and DataFusion 55.1.0 source checkouts, and cached rustdoc JSON whose
crate_version and format version 61 were checked. The cached JSON was not
regenerated; no completeness claim is made for that extraction. The source
checkouts matched tags 59.3.0 and 55.1.0. The probe resolved 292 packages, with
zero third-party versions absent from the repository lockfile.
The inspected blueprint has SHA-256
fa3e959a0005f97c284818eaeae4af94720fa6f001d4b9f10f98c4a44f5b4cca.
Review began at 4becdebe856cd52cac944c5114dd48f1e0e5048e; unrelated concurrent
commits advanced HEAD to fb0717d9151ada4884bd7ccb5efc27422bb92f7a during probing.
The blueprint, charter, Cargo manifest, and lockfile were confirmed unchanged
across that movement. Probe receipts bind the actual execution inputs.
Adversarial checks attacked extension admission, conditional evaluation, casting, canonical bytes, protobuf determinism, floating-point rewrites, pass scheduling, cache dependencies, and exact-filter verification. Crash recovery, solver FFI, numerical derivatives, actual provider scans, and end-to-end performance remain Proposed guarantees, not behavior verified by this review.
2. Authority and lifecycle map
All enforcement descriptions in this table are proposed platform mechanisms.
| Concept or fact | Semantic type and identity | Authority / owner | Revision or snapshot boundary | Permitted update path | Derived representations |
|---|---|---|---|---|---|
| Relation and operation contracts | Relation IDs, schema versions, operator and kernel IDs | Registry and versioned operation declarations; blueprint §§4, 7.3, 6.11 | Registry/package revision | Registered declaration change; generated artifacts follow | Schemas, views, validators, adapters, documentation |
| Model definition | Authored semantic IDs; explicit versus named policy | Authored/reference relations; §§5.1, 6.1 | Model revision, P2-valid commit | Change set with base revision and conflict checks | Normalized facts, inference, indexed mathematics |
| Case bindings and policy | Case revision; identity-based target rows | Case specifications, discretization policies, solver profiles; §§6.10–6.11 | Case/overlay revision | Change set; child overlay and priority resolution | Bound variables, substitutions, scaling and solve plans |
| Observations | Observation/dataset IDs and source references | Authored observations; §6.10 | Dataset/case snapshot | Explicit import and target resolution | Estimation objectives and diagnostics |
| Selected providers | Method/kernel identities and artifact digests | Persisted resolution from declarations; §§9.6, 18.3 | Compiled artifact and run | Explicit selection policy; runtime host probe | Kernel bindings, resolved solver options |
| Mutable execution state | Run ID, attempt, stage and parent run | Run controller and owned numerical workspace; §§17–18 | Individual execution attempt | Declared stage actions and cancellation | Last iterate, residuals, events, terminal status |
| Results and provenance | Run-keyed rows; derivation IDs | Recorded observations of execution; §§6.13, 20.3 | Published result bundle | Append/publish through the artifact protocol | Reports, stream tables, semantic diffs |
| Stored representation | Canonical content hash versus encoded bytes | Canonicalizer and artifact writer; §§5.3, 20.1 | Inconsistent as written: stream identity versus file/Parquet byte verification | Manifest publication after validation | Storage objects and pinned catalog tables; R4-03/R4-04 |
Deliberately opaque behavior: constitutive kernels, numerical solvers,
structural algorithms, and external-function libraries can remain ordinary code.
Their selection, units, domains, derivatives, effects, failure policy, and source
digests must be explicit. KernelSpec does not yet contain every policy that its
generated adapters are instructed to derive (R4-10).
Identity behavior: creation-time IDs and explicit rename semantics are a real strength: an ordinary rename cannot retarget an old case through a reused name. Artifact ordinals remain local. Hashes, however, still require corrections for physical encodings and ignored null payloads. Stable entity identity should not be confused with identical whole-relation hashes when labels or provenance change.
3. Semantic contracts and invariants
| Contract or invariant | Representation | Enforcement boundary | Failure behavior | Verification evidence |
|---|---|---|---|---|
| A relation has its declared structure and valid relationships | RelationSpec, extension fields, invariant plans | Generated readers/builders and P2 | Typed invariant errors | Proposed; registry registration alone does not validate ordinary queries, E1 |
| Computed values retain their physical meaning | Quantity types, conversion rules, rule-head fields | P10 and rule-output admission | Reject incompatible interpretation or apply a named conversion | Unresolved for composite quantity typing; numeric casts demonstrably lose information, E3 |
| A guard suppresses an excluded computation | Conditional expression and kernel contract | Physical execution, including native tape | No evaluation error from an unselected branch | Tested library counterexample E2; built-in CASE is an available route |
| Content identity ignores irrelevant representation differences | pse.canon.v1 | Canonicalizer, before reuse/publication | Reject malformed content; normalize irrelevant bytes | Tested counterexample E4 for hidden primitive values under nulls |
| Every required property is resolved before realization | Requirements and persisted method resolutions | P6 followed by P7–P9 | Explicit unresolved/ambiguous diagnostics | Unresolved scheduling contract: P6 also reads later outputs, R4-06 |
| Reused artifacts have complete dependencies | Pass inputs and memo keys | Memo lookup and per-instance expansion | Miss/recompute when any semantic input changes | Violated as specified by the explicit P7 key, R4-07 |
| Readers observe a complete revision/result | Immutable artifacts, manifest, conditional ref update | Publication and run terminalization | Conflict/failure leaves old ref valid | Proposed protocol is sound in outline; digest roles require a decision, R4-03 |
| Advertised backend capabilities are complete | Operator/kernel contracts and backend bindings | P16 plus runtime capability resolution | Typed unsupported capability | Unresolved descriptor/table disagreements, R4-10 |
Absence and uncertainty: free unknowns, absent initial guesses, and unbounded
values are deliberately distinct. Four-valued inference also has an explicit
undecided relation. Preserve those choices. Two remaining ambiguities need closure:
blueprint §18.5 permits invalid kernel evaluations to become null while diagnostics
are optional; §7.6 permits missing observations as null although §6.10 declares
observations.value without its nullable marker. A consumer must not infer
invalidity, missing input, or failed evaluation from the same unqualified null.
Equivalence: maintain separate contracts for entity identity, canonical logical
content, physical artifact integrity, symbolic mathematical equivalence, and
floating-point agreement. Blueprint §24.1’s 1e-10 relative backend comparison
and §24.2’s 1e-6 relative solution comparison do not authorize arbitrary
overflow-eliminating rewrites, precision-losing casts, or an unstated fused
multiply-add policy. Near-zero and non-finite cases also need explicit comparison
rules rather than an unspecified relative-only check.
4. Derivation and execution design
| Stage or operation | Input revisions and dependencies | Output contract | Preconditions / assumptions | Effects and mutable ownership | Provenance / invalidation |
|---|---|---|---|---|---|
| Author and commit | Documents, registry, base revision | P2-valid authored snapshot | Targets resolve; structural and relational invariants hold | Atomic change-set publication | Source spans and revision parent |
| Normalize and resolve | Authored facts, package contracts, feature and domain declarations | Normalized facts, decided requirements, selected methods | Demand seeds exist independently of their realization | Pure computation over snapshot inputs | Rule/supporting-row evidence; stage inputs must be complete |
| Instantiate and expand | Resolutions, template bindings, domains, laws | Indexed then scalar mathematical artifacts | Guard and domain decisions complete | Local staging; no authored mutation | Semantic IDs and explicit source-to-output derivations |
| Bind and prepare | Graph, case, parameter/fixed values, numerical policy | Ordered problem, analysis, tape, backend bundle | Valid units, capabilities and selected approximation | Owned preparation workspace | Value/assumption dependencies; R4-07/R4-09 |
| Execute | Prepared artifacts, resolved options, provider binaries | Typed run outcome and result bundle | Domains guarded; derivatives available as selected | Mutable numerical workspace, solver/subprocess effects | Attempt IDs and source-linked events |
| Publish and inspect | Complete artifacts and manifest | Pinned immutable snapshot; read-only projections | Verified logical content and physical integrity | Conditional ref update; inspection changes no model meaning | Reproduction inputs and report lineage |
There is a concrete cycle in the documented stage dependencies:
P6 property-demand closure -> P7 template equations -> P8 law equations
^ | |
+---- equation/contribution demand per §9.6 -----+
^
+---- initialization-plan requirements from P15
P15 itself consumes the problem produced downstream of P6. This is different from the specified fixed point inside a rule stratum. A bounded correction is to derive demand seeds from normalized templates, laws, ports, displays, and declared initializer requirements, then realize their closure. Alternatively, explicitly contract a mutually recursive resolution stage with monotonicity and termination conditions. The generic DAG driver cannot infer either decision from §14.1.
The shared compiled.math_* names also need stage-qualified artifact inputs and
outputs. P7–P10 produce or replace overlapping relations, while RelationSpec
offers one producing_pass_id. A pass-local artifact bundle or versioned stage
handle is enough; this does not require a new database or a new physical layout.
Relationship structures: containment, process connectivity, expression dependencies, structural incidence, scheduling, and provenance remain distinct. The use of petgraph for specific derived graphs is appropriate. A process recycle must not be rejected because its scheduling projection needs a tear or SCC.
Provider and boundary contract: the catalog correctly pins a manifest; the numerical problem remains whole; unsupported backend bindings fail before solve. Add active semantic validation before DataFusion optimization and at output admission. Preserve the complete input row set when pushing filters: physical pruning may exclude only rows proved irrelevant to the selected query.
Coherent publication: retain immutable object creation, a complete manifest, and one conditional ref update. Specify separately the checksum of each stored object and the canonical relation hash it represents. An existing content-named object can be reused only under a stated integrity/trust contract; its name alone is not an integrity check after corruption or incomplete external restoration.
5. Representative journeys
Ordinary extension: add a property method
A new equation-based heat-capacity correlation should add its method declaration, parameter quantities and natural units, expression template, and independent value/derivative fixtures. P6 selects it; P9 instantiates it; generated boundary code follows the same contract. No new provider framework is needed.
For a genuinely new opaque method, one Rust algorithm is legitimate additional
work. Its descriptor must also declare the null/domain policy, purity, branch
semantics, derivative conditions and actual backend routes. Merely setting
Immutable, is_strict, monotonicity, or Hessian availability in an adapter would
introduce an independent semantic decision. A method claiming a property but
omitting its realization must fail conformance even if demand closure terminates.
Meaningful change: add a species to a bound package
Keep the instance ID, template contents and Boolean features unchanged; extend the material package with a third species. Its state symbols and balance domains must expand. The explicit per-instance P7 memo tuple in §14.3 is unchanged, although the required output differs. A fresh compile and an incremental compile therefore have no specified reason to agree. Include the resolved domain/package and binding artifacts in the expansion input contract, or use a conservative complete-stage key until finer invalidation is justified.
A related value journey must distinguish a fixed value from its initial guess.
Blueprint §18.1 binds fixed and parameter symbols outside the solver vector, but
§14.4 describes a fixed-value change as changing only variable_order.initial.
The bound-value segment must be refreshed explicitly even when its tape and
sparsity can be reused. Conservatively rebuilding that segment is inexpensive
compared with silently solving the prior case.
Boundary: rule output and stored artifact
An engine expression emits Float64(1.9) for an integer target. Both
can_cast_types and safe: false accept the conversion; the result is 1 (E3).
Reattaching the target field metadata cannot restore the lost fraction. The same
storage type is even less informative for quantities: attaching a Pa field to a
bar-valued result performs no factor-of-100000 conversion.
For storage, the same two-row relation has canonical-option IPC stream, IPC file, and Parquet encodings of 648, 954, and 773 bytes in E4. The file and Parquet digests both differ from the canonical stream digest. Blueprint §§5.3 and 20.1 currently ask that one target hash mean both things. These are compatible only after their roles are separated or storage verification explicitly decodes and canonicalizes.
Interruption and guarded failure
The cancellation design is useful: a failed native solve retains its last iterate under a terminal failure status, and a killed NL process contributes no partial SOL result. That is Proposed, not exercised here.
An earlier failure occurs without cancellation: a conditional kernel selects a
valid branch, yet the generic DataFusion UDF evaluator evaluates the other branch
first. E2 reproduces this with a failing argument and both conditional flags set.
Built-in CASE succeeds on the same input. The native tape needs the corresponding
control-flow contract; an unconditional topological sweep cannot implement lazy
guards simply because the opcode is named Conditional.
6. Acceptance gates
These are document-stage verdicts. A pass means the inspected design has an identified authority/mechanism, not that a skeleton implementation has passed it.
| Gate | Pass / fail / unresolved / not applicable | Evidence or scope rationale | Required action |
|---|---|---|---|
| G1 — Authority | Pass, Proposed | D1/D2/D13, §§5.1, 6.10, 22.2 identify one write path, stable entity identity, case ownership, and non-authoritative projections. No independently editable competing model was identified in the inspected scope. | Preserve these boundaries; separate the two artifact identities in R4-03. |
| G2 — Semantic fidelity | Fail | Lossy casts, invalid/null collapse, incomplete composite quantity rules, and noncanonical null bytes; R4-04/05/08/09/10. | Specify and enforce semantic conversions, state distinctions, canonicalization, and numerical equivalence. |
| G3 — Validity | Fail | The claimed automatic extension validation route does not execute on an ordinary query; E1, R4-01. Generated views remain useful but do not establish the broader claimed plan boundary. | Invoke generated validators at every supported admission path and recheck outputs. |
| G4 — Hidden behavior | Fail | Conditional flags are optimizer declarations; generic UDF argument execution remains eager. Native control flow is unspecified; E2, R4-02. | Use actual guarded execution and declare kernel effects/failures. |
| G5 — Consistency and recovery | Unresolved | Manifest/CAS and terminal-run separation are specified, but physical-byte verification conflicts with logical content naming; R4-03. Actual interruption paths were not run. | Decide the identity/integrity protocol and exercise interrupted publication. |
| G6 — Transformation and reuse | Fail | P7’s declared key omits semantic inputs; rewrites lack an adequate numeric contract; retyping loses information; R4-05/07/09. | Complete dependency keys and transformation conditions; compare reuse with clean recomputation. |
| G7 — Truthful capability claims | Fail | Registration, conditional flags, and sorted HashMap insertion are credited with guarantees they do not provide; E1/E2/E5. Backend tables also disagree; R4-10. | Correct the claims and derive executable capability checks from one contract. |
An unresolved gate is not a pass. No maturity total is computed.
7. Principle findings
Rows are grouped by structural cause. P1 denotes correctness work required before implementing the affected contract; P2 denotes verification or operational work that must precede the associated acceptance claim.
| Finding | Principle IDs | Concrete evidence or gap | Consequence | Proposed correction | Verification |
|---|---|---|---|---|---|
| R4-01 — P1: extension registration is mistaken for active validation | DM-07, DM-43, DM-59 | Blueprint §§4.3–4.4 claim validation of storage and metadata “for every plan.” Existing map probe X1 directly invokes create_extension_type_for_field; it does not establish query admission. E1 registers the factory, then successfully queries both a malformed known extension and an unknown name; factory calls during query: zero. | An implementer relying on the stated session guarantee admits a semantically invalid field into an operation that assumes its extension contract. | Generate one recursive field validator; invoke it at provider/bundle admission, relevant plan boundaries and result admission. Install a platform AnalyzerRule using the existing registry to reject invalid plan fields; retain generated Arrow view validation outside the engine. Small generator and catalog/rule integration surface. | An ordinary supported query/import path must reject wrong storage, unknown name/version, malformed metadata, and nested extension errors before execution. Include a direct-helper positive control; do not count that control as admission coverage. |
| R4-02 — P1: conditional declarations do not implement conditional execution | DM-24, DM-28, DM-43 | Blueprint §18.5 says conditional_arguments/short_circuits ensure an excluded branch is never evaluated. E2 and the pinned ScalarFunctionExpr::evaluate show eager argument evaluation. §18.2 describes one topological tape with no branch/mask execution contract. | A valid conditional expression fails on a domain error in an unselected branch; hoisted common subexpressions can reproduce the same error. | Lower relational conditionals to built-in Expr::Case/CaseExpr; make native branch/mask control explicit. Keep UDF flags as truthful optimizer restrictions. Custom physical expressions are needed only if built-in control flow cannot express an actual kernel requirement. | Mixed batches selecting different branches; excluded division/log failure; nested conditionals; common-subexpression optimization on/off; native and DataFusion outcomes agree on values and errors. E2 supplies the minimal counterexample. |
| R4-03 — P1: logical content identity and stored-byte integrity conflict | DM-12, DM-15, DM-14, DM-42 | Blueprint §5.3 hashes canonical IPC streams and excludes Parquet bytes. §20.1 names IPC files and Parquet objects with that content hash but requires each write to hash “the bytes actually serialized” against its target name. E4 confirms the three encodings have different digests. | A conforming physical writer rejects valid artifacts, or substitutes a physical hash and loses the specified logical identity and reuse behavior. Readers cannot implement the two checks consistently. | Distinguish canonical relation hash from encoding checksum/format in the manifest, or explicitly decode and canonicalize for logical verification while checking physical completeness separately. Define existing-object validation and the snapshot’s relation membership, excluding self-referential bookkeeping. Local manifest/writer/reader contract change. | Persist one logical relation as stream, file and Parquet: common logical hash, distinct valid encoding checksums, identical decoded values. Corrupt/truncate each representation and interrupt before manifest/ref publication; no invalid snapshot becomes visible. |
| R4-04 — P1: canonicalization leaves physically hidden values significant | DM-15, DM-32, DM-48 | Blueprint §5.3 specifies sort, concatenate, dictionary expansion and NaN normalization, but no normalization of values beneath null validity bits. E4 gives equal nullable Int64 columns different null payloads, takes rows in PK order, concatenates, and writes with the stated IPC options: bytes differ. | Equivalent relations get different hashes after construction or an encoding round trip. Cache reuse and deterministic replay depend on buffer history. | Define recursive canonical representation of null slots, nested masked children, offsets, dictionary metadata and other ignored physical content. Normalize the hashing copy, preserving runtime data separately. Treat canonicalization as a typed algorithm, not an IPC writer option. | Metamorphic null-payload, all-valid bitmap, sliced/nested array, empty-domain, dictionary and Parquet-round-trip fixtures. Equal declared content must hash identically; changed valid values must differ. E4 is one counterexample, not full coverage of these types. |
| R4-05 — P1: castability is not lossless or semantically compatible retyping | DM-06, DM-24, DM-41, DM-42 | Blueprint §14.2 rule 6 uses can_cast_types, CastOptions { safe: false }, then reattaches target metadata. E3 silently converts 1.9 to 1 and rounds 9007199254740993 through Float64. Neither API checks quantity kind, basis, reference state or units. | A bad rule can produce numerically altered values or relabel incompatible quantities as a valid head relation. Overflow rejection does not prevent either case. | Derive an explicit admissibility policy from the head contract: exact type/semantic agreement by default; named conversions for units/bases; narrowly allowed numeric conversions with representability checks. Check original semantic evidence before attaching the destination schema. | Reject fractional-to-integer, large-integer-to-float precision loss, same-storage wrong units/reference state, nested metadata loss and nullable-to-required violations. Admit only explicitly declared exact/converting cases. |
| R4-06 — P1: the pass graph and property-demand seeds are not decidable as written | DM-18, DM-21, DM-22, DM-31 | Blueprint §9.6 takes instantiated P7 equations, P8 contributions and initializer requirements as P6 inputs; §14.1 requires P6 before those consumers and derives a DAG. P7–P10 also write overlapping math_* relations despite one producing-pass field in §4.1. | Two implementers must invent different sequencing, staging or hidden repeat loops; a requirement introduced by a realized method or initializer can be missed or discovered too late. | Specify normalized demand-seed extraction or one explicitly contracted joint fixed point. Name stage artifact bundles and their producer/version boundaries. Retain the external fixed-point executor for provenance where needed. | Validate the stage dependency graph mechanically; exercise a method requiring another property, a law-only property and an initializer-only bubble-point requirement. Reordering independent stages must preserve complete resolved output. |
| R4-07 — P1: expansion reuse omits bound semantic inputs | DM-31, DM-32, DM-33 | Blueprint §14.3 names P7’s key as (instance_id, template hash, resolved features), while §6.6 and §11.2 bind domains and child models from property packages and parameters. §14.4 also describes fixed-value changes as only changing initial values although §18.1 binds fixed values separately. | Adding a species, changing a bound unit set or a non-feature template binding can reuse old symbols/equations. A fixed-value change can retain stale bound values under an overly literal implementation. | Define a complete instantiation input bundle, including binding/domain/package contracts and relevant reference data; key by its content. Specify bound-value refresh independently of tape reuse. Start with conservative whole-stage invalidation. | For each dependency change, compare incremental output with a clean compile, including added/removed species, domain members, package units, child bindings, fixed values and parameter treatment. Identical hashes for untouched outputs alone are insufficient. |
| R4-08 — P1: composite physical typing is not closed over the declared distinctions | DM-01, DM-06, DM-07, DM-24 | Blueprint §8.3 makes every Mul/Div result a point and mostly combines dimensions, while §8.1 requires quantity kind, basis, reference state, scale kind and shape. The result rules for those other components are absent. weighted_mean is required by §§7.4/8.3 but has no explicit operator/payload discriminator in §6.9/§7.2. | With symbolic dimensionless α, α·ΔT becomes a point, making T + α·ΔT an illegal point-plus-point sum. Multiplying a referenced enthalpy into an energy-flow term leaves implementers to invent how its reference convention survives. | Complete the quantity-operation algebra, including multiplication by dimensionless factors, reference/basis propagation and explicit weighted-mean recognition. Keep dimension checks as one component. One operator/quantity contract addition should drive all adapters. | Positive/negative physical-type fixtures for scaled differences, weighted means, gauge/absolute pressure, molar/mass flows, and enthalpy transport across incompatible reference states. Test through composed expressions, not only direct additions. |
| R4-09 — P1: symbolic rewrites lack a consistent floating-point policy | DM-24, DM-40, DM-48 | Blueprint §7.3 calls log(exp(x)) → x unconditional; §7.4 merges/reorders affine terms; §18.2 emits fused multiply-add sequences. §7.3 simultaneously requires opt-in reassociation, and repository Rust rules prohibit incidental FMA changes. E6 produces infinity versus 1000 and 1 versus 0 under these algebraic changes. | A rewrite changes domain/error behavior or finite results far beyond the stated tolerance, with no persisted selection explaining the difference. | Decide whether each stage preserves guarded real semantics or a specified floating-point evaluation. Guard range-sensitive rewrites; make reassociation/FMA and approximation policy explicit, versioned, and part of preparation/reuse inputs. Distinguish an affine combination of arbitrary subexpressions from a problem affine in decision variables before §21.2’s LinearExpression assertion. | Overflow/underflow, cancellation, signed zero, boundary derivatives and affine combinations containing nonlinear children; compare policies and backends with stated absolute/relative/non-finite rules. E6 is a numerical counterexample, not a benchmark. |
| R4-10 — P1: the kernel/result contract cannot supply all promised adapter semantics | DM-08, DM-28, DM-43, DM-44, DM-47 | Blueprint §18.5 derives strictness and validity-to-null behavior from KernelSpec, but §6.11 lacks a strictness/null-output policy; invalid outputs have diagnostics only “when requested.” §8.2 requires kernel natural units not present explicitly in that record. ImplicitRef is unavailable in DataFusion in §7.2 and supported in §18.9. Missing observations are nullable in §7.6 and non-null in §6.10. | Missing input and invalid computation collapse into null; an aggregate can ignore failed evaluations and return an apparently ordinary result. Independent adapters must invent unit/failure/capability behavior. | Extend one authoritative operation/result contract with the actually required policies and units; default to a typed error for scalar evaluation failures, or return mandatory typed outcome data for tolerant batch analysis. Generate capability matrices from binding declarations and conformance scope. Do not infer auxiliary implementations from a signature. | Compare scalar, batch, UDF, native, NL and Pyomo behavior for missing inputs, invalid domains, unsupported derivatives, natural-unit conversions and each conditional binding. Reconcile matrix entries and observation nullability mechanically. |
| R4-11 — P2: exact-pushdown verification detects extra rows but not missing rows | DM-53, DM-54, DM-60 | Blueprint §§5.4/24.1 reapply the advertised predicate to returned batches and check for survivors. A provider returning an empty batch passes that test for every filter. The recorded two-element IN rewrite also does not establish a universal expression-shape guarantee. | An implementation can silently prune matching facts; the prescribed test reports success, and incomplete inference propagates downstream. | Compare complete query results with pushdown disabled, including row multiplicities and values. Use DataFusion physical predicates/Arrow filters to avoid a second predicate semantics implementation; advertise unsupported shapes conservatively. | Exact versus Unsupported providers on empty/nonempty inputs, disjunctions, nulls, long IN lists, aliases, dictionary keys, projection omitting filter columns and limits. A deliberately over-pruning provider must fail the test. |
| R4-12 — P2: sorted insertion does not canonicalize protobuf metadata | DM-15, DM-48, DM-59 | Blueprint §§4.3/14.2 claim sorted insertion into HashMap makes plan bytes reproducible. E5 creates 32 fresh maps in sorted key order: 32 protobuf encodings, one IPC encoding. The previous review/map recommendation did not fix the actual iteration-order problem. | Deterministic plan-evidence tests remain flaky; fingerprints differ across equivalent plans. This is now an evidence/replay defect, not a stale memo-key defect: the blueprint correctly removed plan fingerprints from reuse keys. | Keep semantic memo keys. Either make plan bytes explicitly noncanonical evidence, or canonicalize the protobuf representation recursively before encoding using a controlled representation. Sorting map insertion alone is insufficient. | Encode with independently constructed schemas and fresh processes; compare field and schema metadata at nested levels. Do not assert cross-version protobuf stability. |
| R4-13 — P2: a query memory pool is credited with a broader resource guarantee | DM-30, DM-35, DM-39, DM-59 | Blueprint §14.3 says a bounded FairSpillPool turns exhaustion into a typed error, but §5.3 collects an entire relation into one batch and native/Arrow allocations have no stated reservation path. DataFusion try_grow is fallible; grow is infallible. Arrow’s optional pool has infallible reserve/resize and is not a rejection mechanism. | Query operators can stay within their pool while canonicalization, results or concurrent snapshot sessions exhaust process memory; concatenation can also exceed List/Utf8 offset capacity. | Bound the guarantee to accounted consumers. Share a RuntimeEnv/budget where appropriate; reserve before platform allocations and track peaks. State a phase-1 relation-size limit or design a versioned fixed-boundary canonical streaming format before claiming larger scale. | Budget-failure tests for query, canonicalization and result ingestion separately; two concurrent snapshots; non-spillable consumers; cancellation and release. Report whole-process peak memory beside pool reservations. |
Applicability and principle verdicts
All twelve groups apply because this proposal defines a semantic model, compiler, execution lifecycle, persistence protocol, interchange boundaries and extension system. That broad scope is the reason for the coverage below. Distributed coordination, an untrusted binary-plugin service and an accelerator backend are not added to scope. Their absence is not a defect.
“Satisfied” means the inspected Proposed design identifies the mechanism, boundary and rejection behavior for the stated scope. It does not upgrade the platform to Implemented or Tested. “Unresolved” identifies a decision or evidence gap; it is not an implicit pass. The grouped rows provide a verdict for each applicable principle without using a numerical score.
| Group | Satisfied | Violated | Unresolved | Mechanism or limiting evidence |
|---|---|---|---|---|
| 1 — Meaning and authority | DM-02, DM-03, DM-05 | DM-01 | DM-04 | Change-set authority and separate physical layouts are explicit; composite meaning and opaque-kernel contract remain incomplete, R4-08/10. |
| 2 — Types and invariants | DM-09 | DM-06, DM-07, DM-08 | DM-10 | Membership/domain relations reject invalid combinations; extension/cast/null boundaries fail, R4-01/05/08/10. Several policy-bearing text/options fields still require fully specified interpretation. |
| 3 — Identity and revisions | DM-11, DM-13 | DM-12, DM-15 | DM-14 | Stable IDs and separate cases/results are specified; identity/integrity and null bytes are inconsistent, R4-03/04. |
| 4 — Declarations and composition | DM-16, DM-17, DM-19, DM-20 | — | DM-18 | Templates, explicit provider selection and non-mutating inspection have mechanisms; demand versus expansion ordering needs resolution, R4-06. |
| 5 — Derivation and lowering | — | DM-24 | DM-21, DM-22, DM-23, DM-25 | Stage ownership, complete pass inputs, and generated operation contracts need R4-02/05/06/07/08/09/10. Provenance declarations alone cannot repair an invalid derivation. |
| 6 — Execution and effects | DM-26, DM-27, DM-29 | DM-28 | DM-30 | Preparation, typed stages and owned workspaces are explicit; conditional effects and complete failure outcomes are not, R4-02/10/13. |
| 7 — Reuse and concurrency | DM-34 | DM-31, DM-32 | DM-33, DM-35 | Graph meanings are distinct; incomplete per-instance keys and cross-consumer budgets limit reuse/concurrency claims, R4-07/13. |
| 8 — Execution representation | DM-37, DM-38, DM-39 | DM-40 | DM-36 | Coarse typed transfer and native numerics fit their consumers; §24.3 explicitly withholds performance claims. Layout benefits remain unmeasured; R4-09/13 qualify precision and cost. |
| 9 — Boundaries and extensions | DM-45, scoped to authored data and built-in/explicitly selected kernels | DM-41, DM-42, DM-43 | DM-44 | Agent edits share change-set gates; arbitrary compiled plugin loading is deferred. Mechanical conversion and truthful capabilities still need R4-01/02/05/10. |
| 10 — Provenance and explanation | DM-46, DM-49, DM-50 | DM-48 | DM-47 | Source maps, derivations, semantic diffs and lifecycle spans are identified; deterministic evidence and mandatory failure distinctions need R4-04/10/12. |
| 11 — Evolution and verification | DM-51, DM-52, DM-55 | DM-53, DM-54 | — | Explicit versions, unknown-registry rejection, generated artifacts and discoverable entry points are specified. Tests must exercise the actual boundary and complete result equality, R4-01/11. Future meaning-changing migrations still need their own contract. |
| 12 — Architectural discipline | DM-56, DM-57 | DM-59, DM-60 | DM-58 | Shared declarations and deferred optional engines reduce duplicate decisions. Overstated evidence and ineffective regression controls need correction; §8 compares a smaller viable compiler. |
8. Alternatives and architectural leverage
| Alternative | Semantic duplication and extension locality | Correctness and operational risks | Implementation / maintenance cost | Performance evidence | Why selected or rejected |
|---|---|---|---|---|---|
| Current skeletal repository | Intended boundaries exist; no working process-model lifecycle establishes extension locality | Foundation checks cannot certify engineering behavior | Lowest present implementation, but does not deliver Slice A | No end-to-end modeling measurement | Baseline only |
| Blueprint revision 4 as written | Strong declaration/generation intent; incomplete contracts force adapter and pass authors to make hidden semantic choices | R4-01–R4-13; especially admission, conditional execution, hashing and reuse | Broad staged compiler plus three backends and fine reuse before those guarantees are settled | Benefits are hypotheses under §24.3 | Revise before implementing affected contracts |
| Simpler viable compiler with the same semantic model and phased capabilities | Keep typed relations, indexed math IR, native/NL/Pyomo boundaries and contracted kernels; use complete immutable stage bundles and conservative whole-stage memoization first; generate only adapters with a current consumer | Easier dependency/recovery argument; still requires physical/numerical semantics and conformance tests | Less fine-grained invalidation and plan-fingerprint machinery; reporting uses pinned views where materialization is unnecessary | Potential extra recomputation; must be measured against its reduced preparation/memory cost | Recommended starting implementation after correctness corrections; refine per-instance reuse when a representative workload establishes value |
The simpler alternative preserves the stated observable outputs and future capability scope. It changes internal granularity and ordering of investment. It does not replace the semantic model with Pyomo objects or force Newton iterations through a relational engine. An initially coarser memo is a deliberate DM-33 tradeoff and should be recorded if adopted, with refinement triggered by measured rebuild cost and complete dependency fixtures.
Library capabilities to use more extensively
The following recommendations are Proposed. “Interface-checked” establishes the available mechanism; it does not establish its benefit or complete domain semantics. Most use already pinned families and do not call for another engine.
| Priority / capability | Concrete consumer and principle | What the library can supply | Platform-owned contract and limit | Recommendation and verification |
|---|---|---|---|---|
| L1 — active analyzer and recursive validation | Rule/catalog and bundle admission; DM-07, DM-42 | AnalyzerRule, LogicalPlan::apply_with_subqueries, Arrow try_extension_type, and the DataFusion extension registry | Recursive field validity, semantic compatibility, admission ordering and rejection of unsupported versions; the registry must be called | Adopt next. Interface-checked, E1. Generate one validator from registry contracts and install it in the ordered engine profile; test query paths, nested fields and projections. Analyzer source, plan traversal. |
| L2 — built-in conditional physical execution | Guarded property evaluation; DM-24, DM-28 | Expr::Case lowers to CaseExpr; E2 exercises row exclusion | Native branch/mask semantics and truthful optimization restrictions; UDF arguments do not become lazy from flags | Adopt next. Tested in E2’s two-row case. Reuse the existing conditional execution path before inventing a custom physical operator. Physical planner, generic UDF evaluator. |
| L3 — physical predicates and Arrow/Parquet filter machinery | Snapshot scans and runtime analytics; DM-24, DM-38, DM-53 | SessionContext::create_physical_expr, Arrow filter_record_batch, Parquet ProjectionMask, ArrowPredicate, RowFilter, and reader projection/filter options | Exact versus candidate pruning, required filter columns, null semantics and limit placement; complete equivalence with an unpruned scan | Adopt shared predicate evaluation; evaluate storage pushdown by workload. Interface-checked. Durable Parquet is already a scan source in blueprint §5.4, despite the Arrow map’s deferred-item rationale saying otherwise. Parquet reader, predicate contract. |
| L4 — relational aggregation and explicit unnesting | P8 contribution grouping and membership projections from list columns; DM-18, DM-25, DM-38, DM-56 | LogicalPlanBuilder::aggregate and unnest_columns_with_options; built-in aggregate expressions | Valid membership, empty/null-list treatment, grouping keys, ordering of collected terms and derivation links | Specify the missing rule-algebra route now; adopt for these consumers. Interface-checked. §14.2’s small algebra lacks an aggregate/unnest operation although §10.1 groups contributions and §6 stores list membership. Extend only those required operations or explicitly assign them to a contracted native pass. Compare to an independent small reference expansion. Builder source. |
| L5 — shared runtime, reservations and memory attribution | Concurrent snapshots, canonicalizer and result assembly; DM-30, DM-35, DM-39, DM-50 | SessionStateBuilder::with_runtime_env, MemoryConsumer/MemoryReservation::try_grow, TrackConsumersPool, PeakRecordingPool | Accounting before allocation, non-spillable behavior, shared ownership and a whole-process envelope; an Arrow pool alone is infallible tracking | Adopt explicit accounting and measure peaks. Interface-checked. Keep separate snapshot catalogs on a shared budget where appropriate. Test release after failure/cancellation. Memory API, Arrow pool source. |
| L6 — field-aware UDF typing and scalar fast paths | Generated constitutive adapters; DM-06, DM-25, DM-37 | ReturnFieldArgs.arg_fields, ScalarFunctionArgs.arg_fields, ColumnarValue::Scalar, number_rows | Input quantity checks and declared conversions before output metadata; required per-row quantity handling; all-scalar output cardinality | Deepen the already adopted mechanism. Interface-checked. Derive input rejection as well as return metadata; preserve scalar inputs rather than expanding every constant into an array. Measure allocations for scalar/mixed/batch cases. UDF API. |
| L7 — safe buffer views and bounded coalescing | Parameter binding and Python streams; DM-29, DM-36, DM-37 | PrimitiveArray::values exposes a slice-aware ScalarBuffer; BatchCoalescer controls transfer batches | Ownership/lifetime, null rejection, immutable publication and explicit copying; a nonzero slice offset alone does not imply noncontiguity | Evaluate after correctness. Interface-checked. Relax §18.2’s offset-zero rule only through safe buffer APIs and lifetime tests. Coalesce transfer batches without concatenating the entire dataset. Primitive arrays, coalescer. |
| L8 — narrowly proved predicate preimages | Repeated analytics over computed quantities; DM-24, DM-32, DM-43 | ScalarUDFImpl::preimage supplies an input interval for a supported output predicate; existing interval/ordering hooks can aid planning | Equivalent predicate semantics, half-open endpoints, selected branch, floating precision and absence behavior; a conservative pruning range is insufficient for a replacement predicate | Evaluate later, starting with exact discrete/affine cases. Interface-checked. This is not a general thermodynamic inversion or automatic support for arbitrary inequalities. Differentially compare full results before enabling pruning; measure skipped decoding. Pinned UDF source. |
| L9 — explicit-schema Arrow readers | Future tabular observations/imports already contemplated in §6.10; DM-41, DM-52, DM-56 | Arrow CSV and JSON ReaderBuilder::new(schema) accept a declared schema; JSON strict mode and CSV header validation are available | Units, target binding, source spans, required columns, exact numeric policy and foreign-key validation remain platform responsibilities | Correct the map’s blanket rejection; adopt only for a declared tabular interface. Interface-checked. Schema inference is optional, not inherent. This does not replace the YAML/TOML modeling language or a specialized IDAES state-file adapter. JSON reader, CSV reader. |
Already adopted and worth retaining: RowConverter for composite key ordering,
Schema::project, extension metadata and generated Arrow/Python registrations,
explicit engine rule lists and configuration, immutable snapshot providers,
structured DataFusion error translation, native graph algorithms, and kernel-local
num-dual. Registration, method availability, and selected configuration should
be described at exactly the strength they establish.
Do not add Flight, distributed query orchestration, a second dataframe engine,
or a general plugin service to solve these findings. salsa, egglog, FeOs and
additional physical encodings should retain their explicit consumer/measurement
triggers. Arrow RunEndEncoded, view arrays and custom allocation can be valuable
later, but none repairs lost meaning at an admission boundary.
What remains ordinary code
Physical quantity algebra, semantic admissibility, canonicalization, four-valued inference and provenance, demand closure, sparse derivative preparation, matching, branch-aware numerical execution, solver callbacks and the snapshot commit protocol remain platform responsibilities. Arrow supplies representations and kernels; DataFusion supplies plans and execution machinery. Neither library defines process-engineering meaning on behalf of the registry.
9. Verification and measurement plan
The retained probe source, runner and receipt make the library counterexamples reproducible. The full regeneration recipe would rewrite unrelated capability evidence and Python exports; no recipe isolates these characterization questions. The bounded runner therefore creates an ephemeral crate from workspace pins and a copied lockfile, checks resolution against that lock, and runs it offline with the pinned Rust toolchain.
Executed command:
bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
Mode and baseline: Rust 1.98.1; dev profile; Arrow 59.3.0 with
arrow/force_validate explicitly enabled by the scratch force-validate feature;
DataFusion 55.1.0; offline locked execution; failure baseline zero. Final result:
6 characterization groups completed, 0 assertion failures. This proves the
named library behavior, including failures of the proposed assumptions. It does
not mean six platform acceptance gates passed.
The initial scratch run stopped at the prototype’s missing protobuf provider codec after E1–E4; that harness defect was corrected before recording the final successful receipt. No full Rust suite, Python suite, solver parity, or benchmark was run as part of this documentation review.
Document and evidence checks: just docs, just lint-typos and
just lint-license completed with exit status 0 in documentation/static mode
(failure baseline: zero). The pinned Rust formatter’s
rustfmt --check --edition 2024 accepted probe.rs; /usr/bin/shellcheck
accepted run.sh. A focused check found 0 failures across the five review/evidence
files, ten local links, eleven required sections and three recorded input digests;
git diff --check also succeeded. The recipes emitted the existing host-shell
diagnostic /etc/bash.bashrc: line 7: PS1: unbound variable before succeeding;
host startup configuration was outside this review’s edit scope.
| Claim or risk | Evidence label | Test / analysis / benchmark | Conditions and expected result | Current result or remaining gap |
|---|---|---|---|---|
| Extension registration validates all plans | Tested, library only | E1 extension_validation | Registered validator; known malformed and unknown extension fields; ordinary SELECT | Direct checks reject; query succeeds; factory invoked zero times during query. Claimed universal mechanism refuted. |
| UDF flags enforce lazy branches | Tested, library only | E2 conditional_evaluation | Two positive integer rows; excluded failing branch; Immutable custom UDF with both flags | Custom UDF errors; built-in CASE succeeds. |
safe: false means no numeric loss | Tested, library only | E3 cast_fidelity | Fractional Float64 and integer above exact Float64 range | Both casts succeed with altered values. |
| Canonical bytes ignore null payload and match storage encoding | Tested, library only | E4 representation_identity | Same PK order, take and concatenate, equal nullable values, V5/alignment-64/no-compression stream | Null payload affects bytes; IPC file and Parquet digests differ from stream. |
| Sorted metadata insertion stabilizes protobuf | Tested, library only | E5 sorted_metadata_proto | 32 independently built sorted-insertion maps; fixed provider codec | 32 protobuf encodings and one IPC encoding in the retained run. The count may vary; more than one refutes the guarantee. |
| Real-algebra rewrites preserve required floating results | Tested, arithmetic only | E6 floating_point_rewrites | log(exp(1000)); addition of 1e16, -1e16 and 1 | Infinity versus 1000; 1 versus 0. No platform optimizer was run. |
| Complete and terminating compiler schedule | Proposed | demand_seed_closure, stage_bundle_graph | Method/law/initializer-only demands, finite recursion, explicit stage inputs | Needs normalized seed or joint-stage decision before implementation. |
| Reuse equals clean recomputation | Proposed | incremental_vs_clean_binding_matrix | Change one package/domain/binding/value/policy at a time, including absence and provider candidates | Compare full declared outputs and lineage, not only cache-hit counts. |
| Semantic conversion and composite physical types | Proposed | head_semantic_admission, quantity_composition | Lossy numeric conversions; different unit/reference/basis; scaled differences and weighted means | Must reject unsupported conversions and preserve declared result types. |
| Exact pushdown is complete | Proposed | pushdown_vs_unpruned | Deliberately over-pruning provider plus representative filters/projections/limits | Full results and multiplicities match reference; malicious empty output is detected. |
| Crash, retry and cancellation preserve a coherent result | Proposed | publish_fault_matrix, cancelled_stage_outcomes | Fault after each artifact/manifest/ref step; native callback and NL process interruption | Old/new complete snapshots only; explicit terminal status and retry attempt; no mixed or partial accepted result. |
| Backend values, derivatives and failures conform | Proposed | Slice A plus adversarial operator/kernel cases | Independent analytic/finite-difference fixtures and native/NL/Pyomo comparisons | Shared lowering agreement alone is not an independent oracle. Include invalid/branch/nonsmooth cases and unsupported modes. |
| Added library use reduces total cost | Proposed | Slice A cold/warm and scale benchmarks | Same output contract; whole-stage versus fine memo, predicate pushdown, buffer views, scalar/batch inputs | Report parse, validation, preparation, canonicalization, transfer, solve, persistence, recovery, inspection, pool/process peak memory, and provenance size. No gain measured here. |
10. Exceptions and unresolved decisions
No MUST-level exception is accepted by this review. The gate failures require revision or explicit narrowing of the affected supported behavior. Existing deferrals for distributed coordination, optional kernels and additional engines remain outside this review’s implementation demand.
| Decision required | Scope / reason and alternatives | Consequence and compensating control | Evidence | Accountable role / revisit trigger |
|---|---|---|---|---|
| Logical versus physical artifact identity | Canonical stream hash plus encoding checksum, or decode-and-canonicalize verification | Readers must know which identity they check; test all supported encodings and interrupted commits | E4; R4-03/04 | Architecture and persistence maintainers; before canonicalizer/store implementation |
| Demand-seed stage and stage artifact ownership | Pre-realization seed extraction versus an explicit joint fixed point | No implicit late discovery or hidden backward stage writes; validate dependency graph and closure | Blueprint §§9.6/14.1; R4-06 | Compiler maintainer; before P6/P7 contracts are implemented |
| Numeric and quantity-operation policy | Guarded symbolic semantics and selected evaluation policy; complete result-type rules | Persist assumptions, reject unsupported combinations, test failure behavior and numerical comparison classes | E6; R4-08/09 | Math/quantity maintainer; before P10 and tape contracts are frozen |
| Tolerant batch failure mode | Fail operation versus mandatory typed per-row outcomes | A null by itself cannot denote both missing input and invalid evaluation | R4-10 | Kernel/runtime maintainers; before adapter generation |
| Coarse initial reuse and relation-size envelope | Complete whole-stage keys initially; bounded one-batch canonicalization or a new canonical streaming version | Potential extra recomputation or explicit size rejection; no silent stale hits or unbounded promise | R4-07/13; §8 alternative | Compiler/runtime maintainers; refine after Slice A measurements and dependency conformance |
| Deterministic plan-evidence contract | Canonical protobuf representation versus explicitly noncanonical diagnostic bytes | Preserve useful semantic memo keys; no false cross-process fingerprint claim | E5; R4-12 | Rule-engine maintainer; before engine reproducibility acceptance |
Owner entries identify the responsible role for a future decision; this review does not assign a person or authorize a dependency/design change. Adoption that changes D1–D14, identity, boundary or commit contracts must follow the repository’s ADR and design-review process. The blueprint and accepted ADRs were not amended.
11. Decision and implementation changes
Decision: Revise. G1 has a coherent proposed authority model; G2, G3, G4, G6 and G7 fail on concrete contract or capability evidence, and G5 remains unresolved. The architecture’s central choices are useful, but the current blueprint is not ready to serve as an unambiguous specification for its affected boundaries.
| Priority | Change | Principle IDs | Acceptance evidence | Regression protection |
|---|---|---|---|---|
| 1 — Semantic boundaries | Active extension validation, loss-aware rule admission, complete kernel/result semantics; R4-01/05/10; L1/L6 | DM-06–08, DM-41–44 | Actual import/query/output paths reject invalid data and preserve semantic distinctions | Generated boundary matrix from the same contracts as adapters |
| 1 — Execution semantics | Guarded physical execution and complete numeric/quantity rules; R4-02/08/09; L2 | DM-01, DM-24, DM-28, DM-40 | Conditional and numerical adversarial cases agree under named policies | Per-operator and per-backend value/error/derivative conformance |
| 1 — Identity and consistency | Separate canonical content from encoding integrity; normalize ignored physical data; R4-03/04 | DM-12, DM-14, DM-15, DM-48 | Encoding round trips and publication fault matrix | Canonicalization metamorphic fixtures, format/version compatibility checks |
| 1 — Compilation and reuse | Resolve demand-stage cycle and complete input bundles; R4-06/07 | DM-18, DM-22, DM-31, DM-32 | Stage graph validity, complete property closure, incremental equals clean output | Declared-read enforcement and dependency-change fixtures |
| 2 — Library reuse and regression quality | Shared filter evaluation, bounded aggregate/unnest operations, complete pushdown testing, honest plan evidence; R4-11/12; L3/L4 | DM-25, DM-38, DM-53, DM-54, DM-59, DM-60 | Over-pruning detected; standard relational operations preserve keys and provenance; fingerprint policy holds | Differential query fixtures and descriptor-derived capability tables |
| 2 — Operational envelope | Account for platform allocations and shared budgets; R4-13; L5 | DM-30, DM-35, DM-39 | Typed limit failure within the supported envelope; measured process/pool peaks | Memory and cancellation fixtures under concurrent snapshot workloads |
| 3 — Measured refinements | Buffer views, bounded coalescing, predicate preimages and tabular readers where a consumer exists; L7–L9 | DM-36, DM-37, DM-56, DM-58 | Same semantic outputs and a measured end-to-end improvement | Keep conservative paths as differential references; revisit with workload changes |
The recommended next design revision should settle the priority-1 contracts first, then express their library bindings and acceptance tests. It should also correct the capability-map claims identified by E1, E2, E3 and E5; otherwise future agents will inherit the same unsupported assumptions. More library use is valuable when it removes a duplicated mechanism while leaving one explicit semantic authority.
Blueprint revision 4: library characterization evidence
This evidence accompanies the
design review.
It characterizes library mechanisms used by the proposed design; it does not
exercise an implemented pse modeling pipeline.
| File | Purpose |
|---|---|
| probe.rs | Six characterization groups, including positive controls and counterexamples |
| run.sh | Reproduction from repository pins and lockfile, with an ephemeral Cargo project |
| output.txt | Final successful execution receipt, input hashes, versions and observations |
From the repository root:
bash docs/design_review/evidence/blueprint-rev4-2026-09-13/run.sh
The runner uses .venv/bin/python, the Rust toolchain declared by
rust-toolchain.toml, workspace dependency declarations and a copy of Cargo.lock.
The scratch root requires a local lockfile adjustment; every resulting third-party
version is checked against the repository lock before offline, locked execution.
The repository manifest and lockfile are not modified. Compilation uses the
repository target cache by default; CARGO_TARGET_DIR can select another cache.
The temporary project is removed on exit. A later dependency change constitutes
a different experiment; the receipt records the relevant hashes and versions.
Tested conditions: Rust 1.98.1, Arrow 59.3.0, DataFusion 55.1.0, dev profile,
Arrow force_validate explicitly enabled. The scratch force-validate feature
maps directly to arrow/force_validate; it is not a platform test-package feature.
Failure baseline: zero. The final run completed 6 characterization groups
with 0 assertion failures. Some assertions deliberately establish that a claimed
guarantee is false. The counts are not platform acceptance results.
E1 uses default DataFusion features plus a registered extension factory; it calls
the validator directly as a control and then executes an ordinary query. E2 uses
nonconstant input columns to prevent constant folding from disguising eager
evaluation. E3 distinguishes successful casting from exact representation. E4
uses equal nullable integer values with different hidden payloads, ordered primary
keys, take_record_batch, concatenation and canonical-option IPC streams, then
compares complete physical file encodings. E5 inserts eight metadata keys in
sorted order into 32 independently built maps; the provider codec contributes
fixed bytes, so random provider identities do not explain the differences. E6
uses ordinary floating-point arithmetic, not a platform optimizer.
The retained E5 run produced 32 distinct protobuf encodings and one IPC encoding; the exact first count can vary. Its assertion requires more than one, which is enough to refute reproducibility from sorted insertion. No throughput, latency, memory benchmark, solver behavior or scientific parity is established here.
The first scratch attempt lacked the custom table-provider protobuf codec and stopped after E4. That harness error was corrected; the retained source and receipt are from the completed version. No existing capability-map receipts were overwritten or reclassified as newly executed evidence.
Design review: blueprint revision 5 contract amendment
1. Decision and scope
Decision: Accept for the bounded proposed-design scope below. The revised
contracts address R4-01–R4-13 and assign a concrete consumer or deferred trigger
to L1–L9. Acceptance means the reviewed mechanisms, boundaries and refusal paths
are specified. It does not certify their implementation, scientific validity or
performance, and does not change any ADR to accepted.
Proposal: blueprint revision 5, ADRs 0039–0048 and their capability-map/register reconciliation. Standard: Data Model–Based Design Charter, gates G1–G7. Author/reviewer: Codex, including review of its own amendment; this is not an independent second-person review. Date: 2026-09-13. Evidence: Proposed architecture, selected Interface-checked library mechanisms and the explicitly bounded historical Tested characterizations in the revision-4 review.
Observable outcome: implementers have one declared route for admitting physical meaning, resolving demand, binding complete stage inputs, handling guarded failures, and preserving content through alternate encodings. Existing Arrow/DataFusion operators perform their standard jobs; domain rules remain in the platform contracts that invoke them.
Method and coverage. Inspected the changed semantic contracts in blueprint §§4–9, 14, 18, 20–21 and 24–26, their declarations in §6, affected decisions and glossary, all new ADRs and their supersession boundaries, and the four capability maps’ current recommendations. Replayed the earlier review’s counterexamples against the proposed rules by inspection. Followed the concrete journeys in §5; checked section/port ownership, membership framing, numerical/failure policy and the implementation handoff. Static validation is recorded in plan 02.
This is a follow-up on the changed contracts. Unchanged thermodynamic correlations, every unit template, the full IDAES coverage matrix, all specialized diagnostics and later-phase solver/provider routes were not re-certified. No product compiler, solver, canonicalizer or Python adapter was implemented or executed by this amendment. The retained probe receipts were not regenerated. Context7 returned broad analyzer/runtime/reader material during reconciliation; the pinned source/probe evidence in the earlier review remains the basis for precise library claims. Unbuilt behavior is outside this acceptance claim, rather than silently being scored as tested.
The reviewed blueprint SHA-256 is
63629cd7221d7083f95b7e39f916cc6a6d3395599a2faadf5674e98c175c1a5b.
Dependency pins/features, both lockfiles, the toolchain, charter and original
probe source are unchanged; Cargo.toml has only a citation-comment amendment.
Supported limits: local single-writer publication; explicitly admitted Arrow layouts; the row/byte/offset envelope in blueprint §5.3; complete stage input keys; pure contracted kernels; and only numerical/backend routes whose actual implementation and conformance binding is available. Larger canonical streams, finer dependency tracking, Parquet optimization, coalescing and preimages retain register triggers. The design supplies rejection paths for unsupported cases.
2. Authority and lifecycle map
| Concept | Identity and authority | Revision boundary / permitted update | Derived representations |
|---|---|---|---|
| Relation and physical quantity meaning | Registry declarations, semantic IDs and complete quantity-operation rules; blueprint §4/§6.2/§8 | Versioned contract/package amendment admitted through P2 | Arrow fields/views, recursive validators, analyzer checks and generated adapters |
| Model / case | Stable authored identity; separate snapshot classes and explicit model/overlay parents | Change set and atomic P2-valid commit; compiler cannot write authored/reference facts | Normalized seeds, indexed/scalar math and case-bound views |
| Stage inputs and results | Named input/output ports; one producer per port | Complete immutable output snapshot after postconditions; an earlier stage is never overwritten | Catalog handles, physical encodings, memo entries and attempt sidecars |
| Kernel and numerical policy | One KernelSpec, implementation/derivative bindings and selected versioned numerical policy | New contract or selected case policy changes dependency inputs | UDF, native, exported and Pyomo bindings that pass the declared conformance scope |
| Logical / encoded identity | pse-ids; canonical v2 relation/snapshot preimages versus finished-byte checksums | New logical meaning changes content identity; alternate valid encodings can share that identity | Physical manifest, checksum-named objects and conditional ref |
| Evidence and failure | Required typed outcomes, terminal attempt/run state and noncanonical diagnostic objects | Immutable outcome publication; mutable workspaces remain run-owned | EXPLAIN/protobuf, structured diagnostics and provenance queries |
Opaque behavior: specialized kernel bodies, numerical workspaces, solver callbacks and storage I/O remain ordinary code behind the declared contracts. Registration or a function signature cannot supply a missing derivative, conditional implementation or physical conversion.
Renames preserve semantic IDs while changing label-bearing relation content. Row/batch order, supported dictionary encodings and hidden null payload do not change declared logical identity. Changed semantic parents remain visible even when an unchanged relation shares storage; initial reuse does not discard lineage.
3. Semantic contracts and invariants
| Contract | Representation and boundary | Defined failure | Evidence / decisive fixture |
|---|---|---|---|
| Active semantic admission | Generated recursive validator at provider/bundle, pre-rewrite, analyzer, post-optimizer and output boundaries; blueprint §4.4 | schema.* or head mismatch before use | Proposed; E1 refutes passive registration; semantic_admission_paths |
| Complete quantity and head conversion | Quantity-operation selection/derivation, exact types by default and checked explicit conversions; §8.3/§14.2 | Unsupported/ambiguous composition or lossy/incompatible head rejected | Proposed; E3 plus head_conversion_exactness, quantity_composition |
| Decidable stage ownership and demand | P3 seeds, finite bound scopes, P6 closure and port-bound P7–P10 bundles; §9.6/§14.1 | Unknown/ambiguous demand, undeclared port or late realization demand fails | Proposed; stage_bundle_graph, demand_seed_closure |
| Guarded operations and outcomes | Actual CASE/branch regions, complete kernel outcome relation and selected numerical policy; §7.3/§18 | Excluded branches do not execute; selected invalid operations return typed failure; unsupported routes refuse | Proposed; E2/E6 plus actual backend conformance |
| Stable content with checked physical envelopes | Recursive canonical copy, explicit metadata/validity/framing/membership and separate checksums; §5.3/§20 | Unknown layout/version, size excess, incomplete member or corrupt encoding rejected | Proposed; E4/E5 are counterexamples, not a v2 implementation proof |
| Complete reuse and accounted resources | All named inputs/absence/policies/binaries; shared runtime with fallible reservations; §14.3 | Changed key cannot hit; failed reservation cannot publish a partial successful stage | Proposed; incremental/clean and concurrent resource fixtures |
Missing measurements remain nullable values. Invalid evaluation is a typed error, or a mandatory per-coordinate outcome in the separate tolerant operation; a null alone cannot stand for both. An absent optional input is part of the cache key. An empty required output is explicit, while a failed output is never an empty successful bundle. Unknown/conflicting rule facts remain outside decided heads.
Equivalence is role-specific: canonical content identity, physical checksum integrity, full values/multiplicities for pushed scans, guarded numerical outcomes under the selected absolute/relative/signed-zero policy, and attributable decoded meaning for noncanonical plan evidence. These are not interchangeable tests.
4. Derivation and execution design
| Operation | Declared inputs / assumptions | Output, effects and invalidation |
|---|---|---|
| Admit / normalize | P2-valid authored/reference snapshot; registry and quantity contracts | Typed P3 expressions and finite demand seeds; no inspection-driven construction |
| Resolve / instantiate | Features, scopes/domains, candidate universe including absence, method preferences and bindings | Closed or explicitly unresolved demand, then complete named math bundles; real requirements cannot appear as an undeclared P6 feedback loop |
| Match / group / expand | Bound rule ports, key discipline, null/empty/order policies and contribution provenance | Shared DataFusion/Arrow predicate execution, aggregate/unnest descriptors and indexed equations until the expansion consumer |
| Bind / prepare / execute | Case values separately from guesses, numerical policy, actual kernel/backend contracts and shared budget | Owned prepared/branch workspaces, mandatory outcomes and policy-qualified derivatives; refresh bound values independently of topology reuse |
| Encode / publish / reuse | Admitted complete relations, reserved buffers, membership and semantic parents | Canonical logical hashes; separate finished physical bytes; verified manifest then conditional ref. Complete-key memo hits only; attempts/evidence remain sidecars |
The library opportunities are bounded: AnalyzerRule and recursive traversal for admission; built-in CASE for relational guards; physical predicates and Arrow filters for exact scans; standard aggregation/unnesting for P8 and memberships; shared RuntimeEnv/reservation APIs for accountable allocations; field-aware UDF arguments and scalar paths; safe ScalarBuffer owners; and explicit-schema readers for observations. No new query engine or general inversion/ingestion framework is justified. CSV/JSON feature wiring remains part of the future import consumer.
5. Representative journeys
Ordinary extension. A method requiring a second property declares the requirement and implementation/quantity contract. P3/P6 bind the finite scope and candidate set before P9 realization. Its adapters come from the admitted binding; an unavailable derivative or undeclared late dependency is rejected. Extension work is the new declaration, any new algorithm and its focused conformance fixture.
Meaningful change. Add a species, change a unit set, or remove a provider.
The domain/package/candidate input changes, so the complete P7/P6-dependent keys
change. Fresh output snapshots preserve the new lineage. A fixed-value change
refreshes bound_values and any value-dependent analysis/preparation even if the
instruction topology is the same. Clean compilation is the equivalence oracle.
Alternate representation. Construct equal nested nullable relations with different hidden payload/child validity and serialize each as IPC file or Parquet. Canonical v2 specifies normalization before logical hashing. Each finished encoding has its own checksum; restore decodes and validates meaning instead of substituting a byte checksum for logical identity. A Python consumer that loses an extension cannot re-enter a checked platform path by silently relabeling storage.
Failure. A mixed batch selects different branches and includes a log-domain error only in an excluded branch. CASE/native masks prevent its evaluation; a selected invalid branch instead returns the declared error/outcome. Cancellation or reservation failure releases owned workspaces and prevents successful stage publication. A truncated existing object fails verification before a new ref can expose it. These are proposed journeys with the named acceptance fixtures in plan 02.
6. Acceptance gates
Each Pass below is Proposed document scope, not runtime assurance.
| Gate | Verdict | Evidence or scope rationale | Required action |
|---|---|---|---|
| G1 — Authority | Pass | Registry meaning, named stage producers and explicit snapshot classes separate model, derived output and sidecars; §4/§5.3/§14.1 | Generate the projections; preserve accepted ADR arguments |
| G2 — Semantic fidelity | Pass | Complete quantity rules, exact head admission, explicit missing/failure states, recursive canonical representation and numerical policy; §7–§8/§14.2/§18 | Execute semantic/representation fixtures before claiming correctness |
| G3 — Validity | Pass | Every supported admission route invokes the validator; unsupported layouts, conversions, ports and bindings have rejection paths; §4.4/§5.3/§18.5 | Test actual entry points, including nested and optimized-away fields |
| G4 — Hidden behavior | Pass | Guarded execution is a real control-flow route; kernels are declared pure; candidate and policy inputs are explicit; §9.6/§14.3/§18.2 | Branch/effect and late-demand negatives |
| G5 — Consistency and recovery | Pass | Complete stage publication, distinct logical/encoded identity, verified existing objects, complete manifest then ref; §14.3/§20 | Local interruption, corruption, CAS and cancellation fixtures; wider deployment stays deferred |
| G6 — Transformation and reuse | Pass | Complete keys, explicit ordered numerical contracts, lossless/converting head policy and full pushdown oracle; §7.3/§14.2–§14.4/§24 | Incremental/clean, branch/numerical and complete-result differential checks |
| G7 — Truthful capability claims | Pass | False registration/conditional/protobuf/resource guarantees withdrawn; explicit implementation bindings and unsupported refusal; §18.5/§18.9/§26 | Keep entries Proposed until actual routes pass; no performance claim from API availability |
7. Principle findings
No residual MUST gap was identified in the revised, bounded contract decisions. The table records the decisive corrections, not new assertions that the runtime already enforces them. Verification names are Proposed fixtures in plan 02.
| Finding / disposition | Principle IDs | Concrete evidence | Consequence addressed | Correction in revision 5 | Verification |
|---|---|---|---|---|---|
| R4-01/05 — Addressed in design | DM-06, DM-07, DM-41–DM-43, DM-59 | §4.4/§14.2; E1 ordinary query bypass and E3 lossy casts | Invalid quantities can no longer be admitted by the specified route merely because storage is castable or registered | Active recursive admission and source-aware explicit conversion | semantic_admission_paths, head_conversion_exactness |
| R4-02/09 — Addressed in design | DM-24, DM-28, DM-40 | §7.3–§7.4/§18.2/§21.2; E2/E6 | Excluded-branch errors, reordered cancellation and nonlinear Affine bodies had inconsistent behavior | CASE/regions, guarded ordered default, selected numerical policy and qualified LinearExpression | Guarded/numerical/nonlinear-affine fixtures |
| R4-03/04 — Addressed in design | DM-12, DM-14, DM-15, DM-48 | §5.3/§20; E4 | Equal relations differed by hidden null bytes, while valid file bytes contradicted logical object names | Recursive v2, explicit membership/framing and independent encoded checksums | Canonical/encoding/membership/publication fixtures |
| R4-06/07 — Addressed in design | DM-18, DM-21–DM-23, DM-31–DM-33 | §6.11/§9.6/§14.1–§14.4 | Ambiguous producer order and omitted domain/binding/value inputs could create stale models | Early seeds, bound ports, complete output snapshots and conservative stage keys | Demand/stage graph and incremental/clean fixtures |
| R4-08/10 — Addressed in design | DM-01, DM-06, DM-08, DM-25, DM-43, DM-44, DM-47 | §6.2/§6.11/§6.13/§8.3/§18.5 | Adapters invented quantity, missing/failure or derivative semantics | Complete composition and kernel contracts, explicit WeightedMean and mandatory outcomes | Composed quantity and actual kernel/backend fixtures |
| R4-11/12 — Addressed in design | DM-15, DM-48, DM-53, DM-54, DM-59, DM-60 | §5.4/§14.2/§24.1; E5 | An empty pushed result passed the old oracle; noncanonical protobuf was presented as stable | Full values/multiplicities oracle; noncanonical diagnostic encodings outside reuse | Deliberate over-pruning negative; decoded-plan attribution/round trip |
| R4-13 — Addressed in design | DM-30, DM-35, DM-39, DM-59 | §5.3/§14.3/§18.2 | A query pool did not account for other live allocations or oversized relation construction | Shared budget, fallible platform reservations and explicit supported envelope | Separate query/canonicalization/result budgets, concurrent release and process peaks |
| Proportionality — Accepted simplification | DM-33, DM-56–DM-58 | §14.3/§24.3/§26; ADR-0041/0042 | Early fine invalidation and canonical-plan machinery added unproved semantic choices | Complete keys first; exact input equality for skipping, measured triggers for refinement | R-22/R-01 before adoption; full dependency fixtures precede timing |
Applicability. All twelve groups bear on these cross-boundary changes, but this review reassesses only the principles listed below and their revised mechanisms. Other principles and unchanged subsystems retain the earlier review’s coverage limits; they are not silently recertified or declared irrelevant. “Satisfied” is a verdict on specified Proposed design, with the boundary and rejection behavior identified above. No numerical maturity score is assigned.
| Applicable principles | Verdict | Mechanism / limiting scope |
|---|---|---|
| DM-01–DM-04 | Satisfied | Complete domain meaning; registry authority; separate physical layouts; contracted opaque bodies |
| DM-06–DM-10 | Satisfied | Typed operations/outcomes, active admission, bound domains/ports and queryable declarations |
| DM-12–DM-15 | Satisfied | Explicit model/case/output/attempt roles, coherent publication and role-specific identity |
| DM-18–DM-20 | Satisfied | Seeds before realized IDs, candidate closure and declared bindings; inspection does not construct physics |
| DM-21–DM-25 | Satisfied | Stage ownership, complete transformation contracts, traceable outputs and guarded lowering |
| DM-28–DM-30 | Satisfied | Pure kernel contract, owned workspaces, typed partial/terminal outcomes and publication boundary |
| DM-31–DM-33, DM-35 | Satisfied | Complete inputs and absence, conservative initial granularity, shared resource/ordering contracts |
| DM-36–DM-40 | Satisfied | Native layouts and safe owners for concrete consumers, coarse streams, explicit numerical policy; benefits remain hypotheses |
| DM-41–DM-44 | Satisfied | Mechanical adapters, explicit losses/conversions, actual binding requirements and unsupported refusal |
| DM-46–DM-48 | Satisfied | Fresh semantic lineage, mandatory typed failure evidence, logical reproducibility distinct from diagnostic bytes |
| DM-51–DM-54 | Satisfied | Explicit v2 refusal/migration path, generated contracts and adversarial/differential acceptance specifications |
| DM-56–DM-60 | Satisfied | Existing mechanisms with consumers, smaller initial reuse, falsifiable evidence and named change-level fixtures |
8. Alternatives and architectural leverage
| Alternative | Semantic duplication / locality | Risks and cost | Performance evidence | Decision |
|---|---|---|---|---|
| Revision 4 as written | Adapters/pass authors must invent missing validation, type, dependency and failure meaning | Unsupported library guarantees and premature fine reuse | No end-to-end measurement | Rejected by the earlier review |
| Revision 5 | Shared contracts invoke existing Arrow/DataFusion mechanisms; kernel bodies remain ordinary code | More explicit boundary work; bounded canonicalizer and coarse keys simplify correctness | Benefits and recomputation costs remain hypotheses | Selected for the stated architecture |
| Smaller Slice A runtime with whole-stage recompilation and only currently consumed backend bindings | Same semantic registry/IR; omit memo hits and defer every optional adapter/optimization until a consumer needs it | Extra recomputation, smaller implementation and failure surface; still requires active admission, quantity semantics and coherent publication | Measure before enabling memo/transfer refinements | Viable implementation sequence within revision 5, provided unsupported routes remain explicit |
Registry and operation generation are justified by repeated Rust/Python/storage boundaries; typed aggregate/unnest operations have P8/membership consumers. A new distributed service, universal inversion engine, extra dataframe system or automatic dependency framework is not justified by these findings. Canonical normalization, sparse numerical algorithms, masks and buffer ownership remain ordinary typed code, not new DSLs.
9. Verification and measurement plan
The dependency-ordered packets and exact proposed fixtures are in plan 02.
| Claim / risk | Evidence | Check and conditions | State |
|---|---|---|---|
| Documentation consistency | Tested, static/governance scope only | Plan 02’s exact recipe/results table; baseline zero; original headings, pass rows, governed markers, links and immutable ADR bodies | Results recorded in plan 02; no runtime inference |
| Library counterexamples | Tested, historical | Retained revision-4 runner/output: six groups, zero failed assertions, Rust 1.98.1, Arrow 59.3.0/DataFusion 55.1.0, dev, force validation, locked offline resolution | Not rerun for prose; does not test the new integration |
| Active admission, quantity, kernel and numerical preservation | Proposed | Real import/query/output/backend fixtures, including excluded branches, lossy casts and missing-versus-invalid outcomes | Required before behavioral acceptance |
| Canonicalization, identity and recovery | Proposed | Nested/null/metadata metamorphisms; IPC/Parquet differential; corrupt existing objects and interruption at every publication step | Required before storage acceptance |
| Complete reuse and exact scans | Proposed | Mutate each named semantic dependency versus clean compile; full pushed/unpruned values and multiplicities | Required before reuse/pushdown acceptance |
| Cost and resources | Proposed | Cold/warm construction through solve/restore; pool and process peaks, copies, spills, decoded bytes and transfer/solver time under recorded limits | No speedup or process-wide OOM immunity claimed |
No ordinary full Rust/Python/solver suite can establish unimplemented behavior. Current runs are scoped static/governance checks; future implementation changes must run their actual test/slice recipes. API-reference lint remains deferred under R-20 and is not credited as interface verification.
10. Exceptions and unresolved decisions
No SHOULD deviation is needed for these revised contracts: whole-stage reuse is the smallest currently trustworthy granularity, supported boundaries refuse unavailable behavior, and optional features have explicit triggers. This review does not close unimplemented behavioral acceptance by treating it as an exception.
| Decision outside the initial supported scope | Control / owner | Observable revisit trigger |
|---|---|---|
| Finer reuse, then salsa | R-22/R-01; paul-heyse | Complete finer input fixtures plus measured whole-stage preparation cost |
| Larger canonical relations / fixed-boundary streams | R-23; paul-heyse | Actual workload exceeds declared bounds and justifies a versioned format decision |
| Parquet pruning / bounded coalescing | R-24; paul-heyse | Each consumer independently shows full equivalence and lower total cost |
| Predicate preimages | R-25; paul-heyse | Exact supported predicate equivalence, including null/endpoint/floating cases, then measured benefit |
| Cloud/multi-writer stores and later providers | Existing register rows including R-10 | Concrete deployment/provider requirements and their capability/conformance checks |
11. Decision and implementation changes
Accept the revised, bounded Proposed design contracts. All seven gates pass for that document scope. The review does not accept runtime behavior, every unchanged blueprint detail, an ADR status transition or a performance claim.
| Priority | Change / next acceptance boundary | Principles | Acceptance evidence | Regression protection |
|---|---|---|---|---|
| 1 | Generate/admit complete quantity, kernel, snapshot and port contracts | DM-06–DM-08, DM-22, DM-43 | Plan 02 packets A–C; actual boundary negatives | E1/E3 counterexamples become integration fixtures |
| 1 | Implement guarded numerical outcomes and coherent identity/publication | DM-14–DM-15, DM-24, DM-30, DM-40 | Packets B/E/F; full lifecycle and backend routes | E2/E4/E6, corruption and excluded-branch regressions |
| 1 | Establish complete reuse and pushdown correctness | DM-31–DM-33, DM-53–DM-54 | Packets C/D; clean differential and full-result oracle | Domain/candidate/absence/fixed-value mutations and over-pruning negative |
| 2 | Adopt bounded standard operators and real observation imports | DM-18, DM-25, DM-41, DM-56 | Packets C/G; independent expansion/import fixtures | Null/empty/order/units/target cases |
| 3 | Refine only after measuring the complete workload | DM-39, DM-58–DM-60 | R-22–R-25 experiments under identical meaning and resource limits | Recorded conditions, equivalence and end-to-end cost |
Arrow (Rust)
status: evidence-map blueprint_revision: 5 pins: Cargo.lock regenerated: null reviewed: 2026-09-13
Arrow (Rust) — capability map
Current binding: blueprint revision 5 and ADR-0039–ADR-0048. The inventories and original extraction/probe receipts below retain their historical scope and dates; this update did not regenerate that corpus. Historical comparisons to revision 2 are not current acceptance claims. Corrected interpretations are recorded in place. The current design remains Proposed; pinned library mechanisms are Interface-checked, and only the named characterization experiments are Tested.
[review:E1]–[review:E6] refer to the retained six-group characterization
and its conditions: Rust 1.98.1, Arrow 59.3.0, DataFusion 55.1.0, dev,
Arrow force_validate, zero failure baseline. The revision-4 review
explains the counterexamples; these markers do not certify a platform implementation.
| Current decision | Library mechanism and boundary | Evidence / adoption gate |
|---|---|---|
| Active semantic admission | Recursive try_extension_type / generated validators; registry factories are explicitly invoked | ADR-0039; [review:E1]; actual query/import rejection remains a platform gate |
| Canonical content and encoded integrity | Recursive typed normalization plus framed metadata/data IPC streams; separate stored-byte checksums | ADR-0045; [review:E4]; v2 metamorphic/round-trip fixtures remain open |
| Loss-aware conversion | Arrow casts only after exact representability and full quantity/conversion checks | ADR-0039; [review:E3]; castability alone is insufficient |
| Safe buffers and bounded transfer | PrimitiveArray::values / owning ScalarBuffer; eligible slices are contiguous; BatchCoalescer for bounded transfers | ADR-0047/0048; review L7; lifetime tests and R-24 measurement |
| Shared filters and explicit-schema readers | Arrow filters, Parquet projection/row filters, CSV/JSON ReaderBuilder::new(schema) | ADR-0048; review L3/L9; shared predicate correctness now, R-24 for storage optimization |
Companion to docs/authoritative_design/blueprint.md and to supporting-rust-libraries.md, which covered the §3.3 supporting crates and deliberately excluded Arrow and DataFusion. The DataFusion half of this pair is datafusion-rust.md. Each map now carries its own principle-alignment register (§11) and its own acceptance-gate review (§12); in the previous edition both lived only in the DataFusion map, which meant neither document could be updated on its own.
Compiled 2026-09-13 against arrow 59.3.0.
Historical survey adjudicated blueprint revision 2 (2026-09-13). The previous edition of this map adjudicated revision 1 and never said so; §0.4 lists what revision 2 changed underneath it.
0. Purpose, sourcing constraint, and evidence rules
0.1 What this document is
Per crate cluster: what Arrow can actually do, which of its APIs the blueprint’s stated requirements land on, what we deliberately will not use, and where the blueprint assumes a capability that does not exist or is under-specified. A second pass asks a different question — what does Arrow offer that the blueprint has not claimed, which would improve alignment with design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01…DM-60, gates G1–G7)?
0.2 The sourcing constraint, and how it is enforced
This document was written without using any of the repository’s existing Arrow or DataFusion material — not the docs/library_ref/ prose references, not the datafusion-pyarrow-* skills, not the pre-existing build/facts/arrow59-default surface (which is at 59.2.0 and would misrepresent the pin), and not the vendored arrow-rs/ checkout.
Two of those are now moot rather than merely avoided: docs/library_ref/ has been deleted, and the vendored checkouts survive only as orphaned git worktrees under .worktrees/ whose parent repositories are gone, so their tags can no longer be verified. The constraint is now structural as well as chosen.
Everything here comes from four fresh lanes:
| Lane | Marker | What it is |
|---|---|---|
| context7 | [c7:/id] | /websites/rs_arrow_arrow (docs.rs mirror, 12171 snippets). Never /apache/arrow — that entry is the multi-language project at apache_arrow_19_0_0, i.e. the C++/Python line, and citing it would be a category error. |
| docs.rs | [docs.rs:crate@version] | version-addressed pages |
| locally generated rustdoc JSON | [rustdoc:crate@version] | the authoritative lane — see the receipt in §1 |
| locally run probe program | [probe] | a small Rust binary compiled against the pinned crates, used where documentation asserts behaviour that can simply be measured |
| upstream source at a release tag | [gh:repo@tag/path] | workspace manifests, feature lists and changelogs read at the exact pinned tag — used for the §10 enumerations, where a complete list matters and rustdoc gives only what was extracted |
| crates.io registry API | [crates.io:crate] | release dates and current-version checks |
The constraint is mechanically checked by counting citations, not mentions: no provenance marker in this document is of the form [ref:…] (the companion map’s marker for local references) and none cites /apache/arrow. Both strings appear in this section and in the evidence ledger, where they record the rule and the rejected candidate respectively — that is the intended behaviour of the check, which greps for the bracketed citation forms. A claim carrying no marker is a defect.
0.3 Status vocabulary
leverage (the library provides it, use it) · build on top (foundation only; platform code required, cost stated) · confirmed (a specific blueprint claim is true as stated) · refined / erratum (claim wrong or imprecise; correction given) · adopt / evaluate / reject (Pass-2 recommendations) · open item (a question this lookup surfaced).
Pass-2 recommendations are disciplined by charter §G (common false positives) and DM-58 (scale architectural machinery to demonstrated needs): a recommendation that names no principle and replaces no identified hand-written code does not appear.
Pass 3 (§10) is new in this edition and answers a third question: taking the complete inventory of the family — every crate, every public trait, every feature flag, every release delta — what is this design not leveraging at all?
0.4 What changed underneath this map, and what it got wrong
The previous edition was compiled at 14:35 on 2026-09-13; the blueprint was rewritten at 15:24. Four things follow.
| # | Change | Consequence for this map |
|---|---|---|
| 1 | Blueprint revision 2 absorbed this map’s amendment A and must-settle items #4 and #6 verbatim — family-wide = pins, the cargo tree -d blind spot, the ipc/ffi/canonical_extension_types feature set, and dropping arrow-avro | §1.2’s erratum is resolved; it is retained as a record, not a recommendation. §15 now marks every amendment applied / parked in §26 / new. |
| 2 | Blueprint line 7 cites “the three capability maps” as a library authority | The citation was circular while the maps adjudicated revision 1. Naming the revision (header) closes it. |
| 3 | The rustdoc extraction behind the previous edition’s 32 [rustdoc:] citations no longer exists on disk | Every marker was unreproducible. §1.4’s new extraction is committed with its lockfile so this cannot recur. |
| 4 | Coverage was 13 of 28 arrow-family crates, and the map said so only implicitly | §1.4 now extracts 20 publishable crates, and §10 states the denominator explicitly. |
And one correction to this document’s own earlier reasoning. The previous edition asserted, under A1’s “Gaps and risks” and as register row #8, that because Schema.metadata is a HashMap the canonical IPC encoding is non-reproducible unless the platform sorts metadata keys first. That is wrong for IPC. Measured across six independently constructed schemas carrying 0, 1, 2, 3 and 5 metadata keys, the IPC byte stream was identical every time [probe]. Arrow’s IPC writer does not inherit the HashMap’s iteration order. The concern is real, but it belongs to two other paths — a platform-side fingerprint that iterates the map directly, and datafusion-proto, which does inherit it and is measurably unstable (companion map D9). §2 and §10 carry the corrected finding.
1. Version anchors, pin discipline, and the extraction receipt
1.1 Anchors
| Component | This document | Blueprint §3.1 (rev 2) | Note |
|---|---|---|---|
arrow and the whole arrow-* family | 59.3.0 (2026-09-01) | 59.3.0, each crate =-pinned | Now agreed. Revision 2 raised the pin and adopted family-wide = pinning. 59.3.0 is still the current release as of this compile [crates.io:arrow], so no re-pin is needed. |
parquet and the parquet-variant* crates | 59.3.0 | parquet named; the variant crates are not | moves with the family; see §10 E1 |
pyo3 / pyo3-arrow | 0.29 / 0.19.0 | same | pyo3-arrow 0.19.0 requires arrow-* ^59, so 59.3.0 is compatible with no change [docs.rs:pyo3-arrow@0.19.0] |
object_store | 0.13.2 | 0.13.2 | resolved by cargo from DataFusion’s requirement — §3.1’s pin is correct |
| What DataFusion 55.1.0 itself builds against | arrow = "59.2.0" (caret) | not recorded | New fact. The DataFusion 55.1.0 workspace declares arrow = "59.2.0" [gh:apache/datafusion@55.1.0/Cargo.toml]. A caret requirement admits 59.3.0, so the =59.3.0 pin resolves cleanly — but the platform runs one minor release ahead of the Arrow version DataFusion is tested against. Not a defect; a fact §3.1 should record, because it is the first place to look when an Arrow-level behaviour change surprises the engine. |
1.2 Pin-discipline erratum — §3.1 — resolved in revision 2
Retained as a record of why §3.1 reads as it now does, not as a live recommendation. Revision 2 adopted this finding verbatim, including the cargo tree -d reasoning.
Revision 1 of §3.1 stated the intent well: “arrow, arrow-*, parquet, arrow-flight, arrow-avro | 59.2.0 | One release family; never mix Arrow majors in the graph (cargo tree -d in CI)”. The stated mechanism does not achieve the stated intent.
Resolving a project that pins only the umbrella crate:
arrow = "=59.2.0" -> arrow 59.2.0
arrow-array 59.3.0 <-- floated
arrow-schema 59.3.0 <-- floated
The umbrella depends on arrow-* ^59.2.0, so every sub-crate floats to the newest compatible release. This is not a duplicate, so cargo tree -d does not catch it — it is a silently mixed family, which is what §3.1 says it wants to prevent. The same applies to DataFusion, where the umbrella at 55.0.0 resolves sub-crates at 55.1.0.
Recommended amendment (now applied): pin every crate in both families with =, or commit a Cargo.lock and enforce it in CI. This document’s extraction does both — every crate =-pinned and the resolved Cargo.lock committed under evidence/rust/ — which is why its receipt shows a single version per family. Committing the lockfile is the stronger half and §3.1 still does not require it: = pins bind direct dependencies, but nothing stops a transitive path from pulling a second Arrow version, and only a lockfile plus a CI assertion catches that.
1.3 Feature flags (fresh, from crates.io/docs.rs)
arrow 59.3.0 is an umbrella over required arrow-arith, arrow-array, arrow-buffer, arrow-cast, arrow-data, arrow-ord, arrow-row, arrow-schema, arrow-select, arrow-string, plus optional arrow-csv, arrow-ipc, arrow-json, arrow-pyarrow [docs.rs:arrow@59.3.0].
The complete feature list of the arrow umbrella at 59.3.0, so §10’s E3 enumeration has a stated denominator [gh:apache/arrow-rs@59.3.0/arrow/Cargo.toml]:
| Feature | Default | §3.1 rev 2 | Bearing on this design |
|---|---|---|---|
ipc | ✅ | named | §5.3 canonical serialization, §20.1 hot artifacts — required |
csv, json | ✅ | — | not used for platform data (§9) |
canonical_extension_types | ❌ | named | applied. §3 explains why; recognising inbound arrow.* columns (DM-42) |
ffi | ❌ | named | applied. Required for §21/D12’s C stream boundary |
pyarrow (pulls arrow-pyarrow) | ❌ | — | an alternative to pyo3-arrow; still an unmade choice — §8, §15 item 5 |
force_validate | ❌ | — | new in §10. Runs full structural validation on every array constructed. Too expensive for release builds, which is why it is off — but it is exactly a G3 gate for cargo test and CI. Not mentioned anywhere in the blueprint. |
pool | ❌ | — | new in §10. Enables arrow_buffer::MemoryPool / MemoryReservation — Arrow-level allocation accounting, distinct from DataFusion’s MemoryPool. Bears on D11’s native workspaces and G5. |
ipc_compression | ❌ | — | must stay off on the hashing path (§7) |
prettyprint | ❌ | — | diagnostics only, never on an identity path |
chrono-tz | ❌ | — | required only if Timestamp(_, tz) arithmetic is used; §4.5 fixes "UTC", so not required |
async | ❌ | — | not required by anything in scope |
1.4 Extraction receipt
rustdoc JSON generated locally from a scratch project with every crate =-pinned, resolved offline from the crates.io mirror, one toolchain:
toolchain rustc 1.100.0-nightly (809936eac 2026-09-12)
cargo 1.100.0-nightly (7941be6fb 2026-09-11)
flags -Z unstable-options --output-format json
format_version 61
run 2026-09-13T16:15:24-04:00 .. 16:16:22-04:00
targets 57 requested, 57 OK, 0 FAIL, 60 JSON documents
arrow family 20 crates, every crate_version 59.3.0
arrow, arrow_arith, arrow_array, arrow_avro, arrow_buffer, arrow_cast,
arrow_csv, arrow_data, arrow_flight, arrow_ipc, arrow_json, arrow_ord,
arrow_row, arrow_schema, arrow_select, arrow_string,
parquet, parquet_variant, parquet_variant_compute, parquet_variant_json
datafusion 37 crates, every crate_version 55.1.0 (companion map)
lockfile 361 packages; one version per family, asserted
This is a substantially wider extraction than the previous edition’s, which covered 13 arrow crates and preserved nothing. Newly covered here: arrow-avro, arrow-csv, arrow-flight, arrow-json, and the three parquet-variant* crates. The normalized form is written to build/facts/arrow593/ — 30,920 declarations, 101 public traits, 18,268 impl relations — alongside, and never overwriting, the older arrow59-default profile at 59.2.0.
Every [rustdoc:…@59.3.0] marker in this document resolves against that extraction.
Committed under docs/capability-maps/evidence/rust/: the extraction manifest (apisurface-Cargo.toml), the resolved apisurface-Cargo.lock, the generation script, the generation log, the five probe programs, and their captured output (probe_output.txt). The lockfile is the part the previous edition lacked — without it “re-derivable” meant “re-resolvable to whatever is current”, which is not the same claim.
1.5 Probe receipt
Measurements in this document come from five programs compiled against the pinned crates. Arrow-side probes 1–5 are unchanged from the previous edition; 6 and 7 are new, and 6 corrects a claim this map previously made (§0.4).
| Probe | Program | Measures |
|---|---|---|
| 1–5 | arrow_probe.rs | IPC metadata round-trip; null/NaN/-0.0 distinctness; totalOrder sort; batch-split byte equality; alignment sensitivity |
| 6 | arrow_probe2.rs | whether metadata key count/order changes the canonical IPC bytes |
| 7 | arrow_probe2.rs | Parquet metadata survival, extension-name survival, and write determinism — closes the previous edition’s open item |
2. Type system, Field, Schema, and metadata (A1)
Role: §4.3 metadata conventions; §4.5 logical type catalog; §4.2’s generated fn schema() -> SchemaRef and the try_from(&RecordBatch) that checks a schema fingerprint.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Type enumeration | DataType with 41 variants: Null, Boolean, Int8…64, UInt8…64, Float16/32/64, Timestamp, Date32/64, Time32/64, Duration, Interval, Binary, FixedSizeBinary, LargeBinary, BinaryView, Utf8, LargeUtf8, Utf8View, List, ListView, FixedSizeList, LargeList, LargeListView, Struct, Union, Dictionary, Decimal32, Decimal64, Decimal128, Decimal256, Map, RunEndEncoded | [rustdoc:arrow-schema@59.3.0] |
| Field | Field::new(name, DataType, nullable); typed constructors new_struct, new_list, new_large_list, new_fixed_size_list, new_map, new_union, new_dictionary, new_dict(…, dict_id, dict_is_ordered), new_list_field | [rustdoc:arrow-schema@59.3.0] |
| Field metadata | metadata() -> &HashMap<String, String>, metadata_mut, set_metadata, with_metadata | [rustdoc:arrow-schema@59.3.0] |
| Schema | pub struct Schema { pub fields: Fields, pub metadata: HashMap<String, String> }; Schema::new, new_with_metadata, with_metadata, metadata() | [c7:/websites/rs_arrow_arrow], [rustdoc:arrow-schema@59.3.0] |
| Schema operations | project(&[usize]), try_merge(iter), normalize(separator, max_level), flattened_fields(), fields_with_dict_id(i64), contains(&Schema) -> bool | [rustdoc:arrow-schema@59.3.0] |
| Batch-level metadata mutation | RecordBatch::schema_metadata_mut() — inserts into schema metadata in place, cloning the schema if shared | [c7:/websites/rs_arrow_arrow] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§4.3 schema-level keys (pse.contract.id, pse.contract.version, pse.contract.fingerprint, pse.namespace, pse.snapshot_id, pse.producer_pass_id) | Schema.metadata: HashMap<String, String> | confirmed — arbitrary string keys at schema level are a first-class field of Schema, not a convention bolted on |
§4.3 field-level keys (pse.semantic.logical_type, …quantity_type, …role, …fk, …enum) | Field::with_metadata / metadata() | confirmed |
| §4.3 “metadata is a carrier, not the metamodel: nothing in DataFusion acts on these keys unless a platform component explicitly reads them” | — | confirmed by construction: Arrow assigns no meaning to non-ARROW:-prefixed keys. The one exception is the ARROW:extension:* pair, which Arrow does interpret (A2). |
§4.5 logical type catalog (Float64, Int64, UInt16/32/64, Boolean, Utf8, Timestamp(Nanosecond,"UTC"), List, Struct) | all present | confirmed — the catalog is a conservative subset of the 41 variants |
§4.2 generated try_from(&RecordBatch) “that checks the schema fingerprint” | the fingerprint check is platform code; but Schema::contains(&other) exists for structural compatibility | refined — see alignment note below |
Pass 2 — alignment opportunities not in the blueprint
| Capability | DM | What it would replace | Recommendation |
|---|---|---|---|
Schema::contains(&Schema) -> bool | DM-53 (verify equivalence across representations), G3 | a hand-written field-by-field comparison in the generated try_from | adopt as a second check. The fingerprint check answers “is this the contract I expect”; contains answers “is this batch structurally usable” — they fail differently, and a projected batch legitimately fails the first while passing the second. §4.2 currently specifies only the fingerprint. |
Schema::project(&[usize]) | DM-22 (every transformation has a contract) | hand-built projected schemas in the provider | adopt — projection is applied at read in §5.4; deriving the projected schema from the authoritative one keeps metadata attached rather than reconstructed |
RunEndEncoded DataType | DM-36 (layouts for demonstrated access patterns) | nothing yet | evaluate, do not adopt now. compiled.* relations repeat instance/domain keys heavily, and REE is the Arrow-native encoding for run-structured columns. But DM-58 applies: no access-pattern measurement exists yet. Record as a measurement to take, not a design change. |
Utf8View / BinaryView | DM-36 | Utf8 for doc, name, path columns | evaluate. View types trade a wider header for cheap slicing and no re-copy on take. Relevant only if profiling shows string handling matters; §4.5 should not change speculatively. |
Decimal32 / Decimal64 | DM-40 (declare precision) | Float64 for exact-decimal quantities | reject for physical quantities — §4.5 is right that Float64 is the type for numerical values a solver consumes. Worth a note only for costing (§19.5), where exact decimal arithmetic is arguably more honest than binary floating point. |
Schema::normalize / flattened_fields | DM-10 (keep structure typed and queryable) | — | reject — these flatten nested structure into dotted names, which is the opposite of the blueprint’s direction (structure stays typed, §4.5). Named here so nobody adopts them for convenience. |
Gaps and risks
-
Fieldsis anArc-backed shared slice, so schemas are cheap to clone but shared;RecordBatch::schema_metadata_mutclones on write[c7:/websites/rs_arrow_arrow]. Mutating schema metadata after a batch is built is therefore possible but not free, and it silently diverges the batch’s schema from the registry’s. §4.3 says metadata is “attached by the provider” — that should be read as attached at construction, never patched later. -
Metadata is
HashMap<String, String>— unordered — but the IPC writer does not inherit that disorder. This is a correction to the previous edition, which claimed the canonical IPC encoding was non-reproducible for this reason. Measured[probe]:PROBE 6 canonical IPC bytes vs number of metadata keys (6 independently built schemas each) 0 keys: len=312 distinct encodings = 1 STABLE 1 key : len=416 distinct encodings = 1 STABLE 2 keys: len=488 distinct encodings = 1 STABLE 3 keys: len=560 distinct encodings = 1 STABLE 5 keys: len=704 distinct encodings = 1 STABLESo §5.3’s
content_hashis not at risk from metadata ordering, and the previous edition’s register row #8 overstated the problem. The risk is real but confined to two other paths, and the second one is serious:- Any platform code that fingerprints by iterating
metadata()directly —pse.contract.fingerprintas §4.3 describes it — is exposed, because that iteration is theHashMap’s. Sorting there is still required (DM-48). - Protobuf metadata remains unordered. Sorted insertion into field/schema HashMaps does not stabilize
datafusion-protobytes [review:E5]. Blueprint §14.2 now records noncanonical diagnostic encodings outside semantic memo keys (ADR-0044); §5.3 separately canonicalizes metadata for logical relation identity.
- Any platform code that fingerprints by iterating
-
fields_with_dict_id/Field::new_dict(dict_id, …)expose Arrow’s legacy dictionary-id mechanism. §4.5 says “dictionary encoding … never carries identity” — consistent, but the generated code must not setdict_id, and a governance check is cheap.
3. Extension types and the canonical_extension_types feature (A2)
The densest blueprint↔library coupling in the Arrow surface: §4.4 declares ten pse.* extension types and states exactly which Arrow machinery implements them.
This cluster is where DM-06 (“type semantic distinctions, not only machine representations”) is either satisfied or lost. FixedSizeBinary(16) is a machine representation; pse.semantic_id and pse.content_hash are two different semantic types that share it. Arrow’s extension mechanism is the only place in the format where that distinction can be recorded and carried, so everything below is DM-06 infrastructure.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| The trait | arrow_schema::extension::ExtensionType — const NAME: &str; type Metadata; fn metadata(&self) -> &Metadata; fn serialize_metadata(&self) -> Option<String>; fn deserialize_metadata(Option<&str>) -> Result<Metadata, ArrowError>; fn supports_data_type(&self, &DataType) -> Result<(), ArrowError>; fn try_new(&DataType, Metadata) -> Result<Self, ArrowError>; provided fn validate(&DataType, Metadata) -> Result<(), ArrowError>; provided fn try_new_from_field_metadata(&DataType, &HashMap<String,String>) -> Result<Self, ArrowError> | [rustdoc:arrow-schema@59.3.0] |
| Naming rule | “Names should use namespace-style prefixes (e.g. myorg.name_of_type). Names beginning with arrow. are reserved for canonical types.” | [docs.rs:arrow-schema@59.2.0] |
| Metadata keys | EXTENSION_TYPE_NAME_KEY = "ARROW:extension:name", EXTENSION_TYPE_METADATA_KEY = "ARROW:extension:metadata" | [rustdoc:arrow-schema@59.3.0] |
| Field integration | Field::with_extension_type<E>(self, E) -> Self, try_with_extension_type<E>(&mut self, E) -> Result<(), ArrowError>, try_extension_type<E>(&self) -> Result<E, ArrowError>, extension_type<E>(&self) -> E (panics), has_valid_extension_type<E>(&self) -> bool, extension_type_name() -> Option<&str>, extension_type_metadata() -> Option<&str>, try_canonical_extension_type() -> Result<CanonicalExtensionType, ArrowError> | [rustdoc:arrow-schema@59.3.0], [c7:/websites/rs_arrow_arrow] |
| Canonical types (7) | Uuid = arrow.uuid; Json = arrow.json; Bool8 = arrow.bool8; Opaque = arrow.opaque; FixedShapeTensor = arrow.fixed_shape_tensor; VariableShapeTensor = arrow.variable_shape_tensor; TimestampWithOffset = arrow.timestamp_with_offset; plus the CanonicalExtensionType enum and the *Metadata structs | [rustdoc:arrow-schema@59.3.0] |
| Feature gate | all seven canonical types and CanonicalExtensionType require the canonical_extension_types cargo feature (off by default) | [docs.rs:arrow-schema@59.2.0], confirmed by building with it enabled [rustdoc:arrow-schema@59.3.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§4.4 “Rust implementations use arrow_schema::extension::ExtensionType with NAME, typed Metadata, serialize_metadata, deserialize_metadata, and supports_data_type, exactly as documented for Arrow 59” | the trait as quoted above | confirmed, and incomplete. The blueprint names five members; the trait also requires try_new and metadata, and provides validate and try_new_from_field_metadata. The two provided methods are the ones that matter operationally — they are the read path. |
§4.3 ARROW:extension:name as a field-metadata key | EXTENSION_TYPE_NAME_KEY | confirmed verbatim, and ARROW:extension:metadata is its mandatory companion — §4.3’s field-key table lists only ARROW:extension:name, so the metadata key should be added for completeness |
§4.4 pse.* naming for ten extension types | the myorg.name_of_type convention; arrow. reserved | confirmed — pse.semantic_id, pse.bound etc. are correctly namespaced and cannot collide with canonical types |
| §4.4 “All extension types have a standard storage type so that an unaware consumer reads them safely” | supports_data_type is the enforcement point, called by try_new/validate | confirmed — but only if the platform calls it; see the validation gap below |
§4.4 Python consumers register matching pyarrow.ExtensionType classes | out of scope here — see A7 | deferred |
Pass 2 — alignment opportunities
| Capability | DM / gate | What it would replace | Recommendation |
|---|---|---|---|
Field::try_extension_type::<E>() + validate called inside the generated try_from(&RecordBatch) | DM-07 (enforce validity at identified boundaries), G3 (invalid state must not reach an operation that assumes validity) | nothing today — §4.2’s generated view checks the schema fingerprint only | adopt. A fingerprint match proves the schema is the contract we published; it does not prove a given field’s extension metadata deserializes or that its storage type is supported. Those are exactly what try_extension_type checks, and the check is generated code, not hand-written. This is the single highest-value item in the Arrow surface. |
Field::extension_type::<E>() — the panicking variant | G3, DM-30 (partial failure explicit) | — | reject and ban. A panic in a pass body is not a typed failure (§23.2) and cannot become a validation.invariant row. Add it to the governance greps beside no_shadow_structs.rs. |
canonical_extension_types feature on | DM-42 (loss-aware interchange; reject silent semantic degradation) | — | adopt. Not for what we write — for what we read. §20.5 imports IDAES JSON and §21 exchanges with Python; an inbound column carrying arrow.json, arrow.uuid or arrow.bool8 is meaningful, and without the feature try_canonical_extension_type does not exist and the platform cannot even recognise it. Silently treating arrow.bool8 storage (Int8) as a plain integer is precisely the degradation DM-42 forbids. The blueprint does not mention this feature at all. |
Opaque (arrow.opaque) canonical type | DM-04 (declare the semantic boundary and expose opaque behavior), DM-42 | a bare storage type plus a pse.semantic.* note | evaluate. §18.5 and §9.8 admit kernels whose values the platform does not interpret; arrow.opaque is the standard, cross-language way to say “this came from another system and we do not interpret it”, carrying the originating type name in its metadata. Cheaper and more honest than inventing pse.opaque. |
Uuid (arrow.uuid) for pse.semantic_id | DM-42, DM-44 | pse.semantic_id | reject, with reason. §5.1 puts two kinds of 128-bit value in one opaque column: blake3_128 derived IDs and UUIDv7 interactive IDs. A blake3 digest is not a UUID and labelling it arrow.uuid would assert a structure (version/variant nibbles) it does not have — a G2 semantic-fidelity failure in exchange for cosmetic interoperability. The blueprint’s opaque pse.semantic_id is correct. Worth recording why, since arrow.uuid looks like an obvious win. |
A generated conformance test per pse.* extension type | DM-44 (extensions complete, versioned, conformance-testable), DM-53 | ad-hoc tests | adopt. ExtensionType gives the exact contract to test: round-trip serialize_metadata → deserialize_metadata, supports_data_type accepts the declared storage and rejects a near-miss, and try_new_from_field_metadata reconstructs from a Field. Ten types × one generated test each, driven from §4.4’s table — which is §4.2’s existing generation pattern, extended. |
Gaps and risks
-
Arrow has no extension-type registry — but DataFusion 55.1.0 does, and it changes this cluster’s conclusion. The Arrow trait is per-type and field-local; nothing in
arrow-schemamaps a name back to an implementation. That much is unchanged. What is new is thatdatafusion_expr::registry::ExtensionTypeRegistryprovides exactly that mapping, keyed on theARROW:extension:namestring, and it is wired intoSessionStateBuilder. Measured[probe]:PROBE X1 ExtensionTypeRegistry canonical types preloaded (7): arrow.timestamp_with_offset, arrow.opaque, arrow.fixed_shape_tensor, arrow.uuid, arrow.variable_shape_tensor, arrow.bool8, arrow.json register pse.semantic_id -> 8 registrations resolve from a good Field -> storage_type=FixedSizeBinary(16) Field with the WRONG storage type -> REJECTED: "pse.semantic_id requires FixedSizeBinary(16), got Utf8" unregistered pse.* name -> Err("Logical type not found")Scope correction: X1 invokes
create_extension_type_for_fielddirectly; it does not validate query admission. E1 shows malformed and unknown extension fields survive an ordinary SELECT with zero factory calls [review:E1]. Generated recursive admission plus a platform AnalyzerRule supplies the missing boundary (blueprint §4.4). -
Extension-type metadata versioning remains unresolved. An inbound field carrying
pse.quantity_valuewritten by a different version of the platform deserializes only if our currentMetadatatype still parses it. §4.4 has no versioning story —pse.contract.versionversions the relation, not the extension type. The registry gives a place to put a resolution rule; it does not supply one. Open item (DM-44, DM-51). -
deserialize_metadatamust error on unexpected metadata, not ignore it[docs.rs:arrow-schema@59.2.0]. For the blueprint’s metadata-free types (pse.content_hash,pse.ordinal_ref) the implementation must therefore reject a field that carries anARROW:extension:metadatakey rather than shrug — otherwise two different producers disagree silently. -
pse.ordinal_refis the weak link in §4.4. Its storage isUInt64and “metadata names the target relation” — but that target is carried inpse.semantic.fk-style platform metadata, not in the extension’s ownMetadata. Making the target relation part of the extension type’sMetadatawould letsupports_data_type/deserialize_metadataenforce it. Evaluate (DM-09, DM-44). -
has_valid_extension_typereturnsbooland discards the reason[rustdoc:arrow-schema@59.3.0]; usetry_extension_typewherever the failure must become a diagnostic (§23.2).
4. Arrays, buffers, and the zero-copy boundary (A3)
Role: §4.2 generated typed views (StoichiometryView<'a> { reaction_id: &'a FixedSizeBinaryArray, … }) and builders; D11 — “the evaluation program, sparse Jacobian structures, and solver workspaces … borrow Arrow buffers when layouts permit and copy when they do not. Zero-copy is a preference, not an obligation.”
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Buffer construction from Rust memory | Buffer::from_vec<T>(Vec<T>), Buffer::from_slice_ref | [rustdoc:arrow-buffer@59.3.0] |
| Buffer over foreign memory | Buffer::from_custom_allocation(ptr: NonNull<u8>, len: usize, owner: Arc<dyn Allocation>) | [rustdoc:arrow-buffer@59.3.0] |
| Buffer inspection | as_slice() -> &[u8], data_ptr() -> NonNull<u8>, ptr_offset() -> usize, len(), slice(offset) | [rustdoc:arrow-buffer@59.3.0] |
| Typed arrays | arrow_array — 4597 indexed items including the concrete array types the generated views name | [rustdoc:arrow-array@59.3.0] |
| Batch | RecordBatch::try_new(SchemaRef, Vec<ArrayRef>), schema(), column(i), slice(offset, len), schema_metadata_mut() | [c7:/websites/rs_arrow_arrow], verified by probe |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§4.2 typed column views holding &'a FixedSizeBinaryArray etc. | downcast from ArrayRef | confirmed — the generated view is a borrow of the batch’s columns, which is what makes it free |
| D11 “borrow Arrow buffers when layouts permit” — native code reading Arrow memory | Buffer::as_slice() / data_ptr() + ptr_offset() | confirmed, with a hazard (below) |
| D11 “copy when they do not” | Buffer::from_vec for the reverse direction | confirmed |
| §18.2 “Arrow buffers of parameter values are borrowed when contiguous and copied otherwise; the workspace is native” | as above | confirmed — the contiguity test is real: a ChunkedArray-style multi-batch column has no single contiguous buffer, and a sliced array has an offset |
Pass 2 — alignment opportunities
| Capability | DM | What it would replace | Recommendation |
|---|---|---|---|
Buffer::from_custom_allocation(ptr, len, owner) | DM-37 (cross expensive boundaries in coarse typed units), DM-29 (isolate mutable workspaces, commit outcomes) | copying the evaluation program’s native f64 workspace into a fresh Arrow array to publish results | evaluate. This is the reverse of D11’s stated direction: it lets a native workspace be published as an Arrow buffer with an explicit ownership token, no copy. Attractive for §18.6’s solution/residual relations where the solver already owns a dense f64 vector. The cost is a lifetime obligation — the Arc<dyn Allocation> must outlive every batch derived from it — which is exactly the “scope mutable runtime workspaces and publish coherent outcomes” discipline DM-29 asks for. Worth a spike; not free. |
RecordBatch::slice(offset, len) | DM-36 | manual re-batching | adopt where batches are windowed; it is O(1) and metadata-preserving. |
Gaps and risks
- Borrow through safe views.
PrimitiveArray::valuesincorporates the logical slice offset. A null-free contiguous one-chunk slice can be borrowed with its owning handle retained; raw base-pointer indexing is invalid. A filter need not produce a nonzero offset. Layout/coercion/multi-chunk cases use checked reserved copies (blueprint §18.2; review L7). - Nullability: a borrowed
Float64Arraymay carry a validity bitmap. §18.2 treats parameter values as densef64; borrowing a nullable column without checkingnull_count() == 0reads uninitialised slots as data. State the precondition.
5. Null, NaN, and -0.0 — semantic distinctness (A4)
The cluster with the highest gate stakes: G2 — semantic fidelity (“required meaning is ambiguous, silently dropped, reinterpreted, or represented by an indistinguishable sentinel”) and DM-08 (“represent absence, unknowns, uncertainty, invalidity, and failure distinctly”).
Role: §7.6 null/bound/unknown semantics; §4.4’s pse.bound (“explicit bound; never NaN or null sentinels”); §5.3 step 4 (“floats hash as IEEE-754 bits after canonicalizing NaN to the quiet NaN with zero payload; -0.0 is preserved (it is distinguishable and can matter to solvers)”).
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Validity is structural | every Arrow array carries an optional validity bitmap separate from its values; is_null(i), null_count() | [rustdoc:arrow-array@59.3.0], verified by probe |
| Comparison null semantics | arrow::compute::kernels::cmp::{lt, lt_eq, gt, gt_eq, eq, neq} — “comparing null values on either side will yield a null in the corresponding slot” | [c7:/websites/rs_arrow_arrow] |
| Null-aware equality | arrow_ord::distinct (and not_distinct) — the IS DISTINCT FROM semantics where null equals null | [rustdoc:arrow-ord@59.3.0] |
| Float ordering | sort and cmp kernels order f32/f64 by the IEEE 754 (2008) totalOrder predicate | [c7:/websites/rs_arrow_arrow] |
| Null placement | SortOptions { descending, nulls_first }; default is ASC NULLS FIRST; make_comparator(left, right, SortOptions) | [c7:/websites/rs_arrow_arrow] |
Empirical verification
Run locally against the pinned build (arrow 59.3.0, program and output in the evidence ledger, row 16):
PROBE 2 null vs NaN vs -0.0 after an IPC round-trip
is_null(1)=true value(2)=-0 signbit(2)=true
PROBE 3 sort order of [0.0, -0.0, NaN, -1.0, null] with nulls_first
sorted: ["null", "-1", "-0", "0", "NaN"]
Three facts follow, and all three matter to the blueprint:
- Null is structurally distinct from NaN and survives serialization. DM-08 and G2 are satisfied by the format, not by convention — there is no sentinel to confuse.
-0.0sorts strictly before+0.0. totalOrder treats them as distinct and ordered, so §5.3’s “-0.0is preserved” is not merely a hashing choice — Arrow’s own ordering already distinguishes them, and a canonicalisation that collapsed them would contradict the sort.- NaN sorts last, after all finite values. Deterministic placement, so §5.3 step 1’s key ordering is well defined even if a NaN reaches a sort column.
What the blueprint binds to
| Blueprint requirement | Status |
|---|---|
§4.4 pse.bound = Struct<kind: Dictionary(Int8,Utf8), value: Float64> with kind ∈ {finite, unbounded} — “never NaN or null sentinels” | confirmed as the right call, and now with a second reason. Arrow would let you encode unboundedness as null; the blueprint refuses. Beyond G2, the mechanical reason is that comparison kernels propagate null (lt(null, x) = null), so a null-encoded bound silently produces null rather than a decision — an indistinguishable-sentinel failure at exactly the place a solver needs an answer. |
| §5.3 step 4 “canonicalizing NaN to the quiet NaN with zero payload” | confirmed necessary. Because ordering and hashing are bit-level under totalOrder, two NaNs with different payloads or sign bits are distinct. Without canonicalisation, two logically identical relations could hash differently — a DM-48 reproducibility break. The blueprint is right and the mechanism is now explicit. |
§5.3 step 4 “-0.0 is preserved” | confirmed — and note the tension resolved: NaN is canonicalised, -0.0 is not. That asymmetry is correct: NaN payload carries no meaning, the zero’s sign does. |
| §7.6 null / bound / unknown as distinct | confirmed — Arrow supplies null; “unknown” and “conflict” (§14.2 rule 3’s four-valued predicates) are platform-level status columns, not Arrow concepts, which is the right layering |
Pass 2 — alignment opportunities
| Capability | DM / gate | Recommendation |
|---|---|---|
arrow_ord::distinct / not_distinct | DM-08, G2 | adopt wherever the rule compiler (§14.2) compares nullable key columns. Ordinary eq yields null for null-vs-null, which in a join or dedup context silently drops candidate rows — exactly what §14.2 rule 3 forbids (“they never silently drop a candidate”). distinct gives the null-equals-null semantics that deduplication actually wants. Not mentioned in the blueprint. |
Explicit SortOptions at every sort site | DM-40 (declare ordering and determinism requirements) | adopt as a rule. The default is ASC NULLS FIRST; relying on a default makes the canonical ordering of §5.3 depend on an Arrow default rather than on a written contract. Name it. |
CastOptions { safe } (A5) for any numeric narrowing | DM-42 (reject silent degradation) | see A5 |
Gaps and risks
- The NaN canonicalisation is ours to implement and is easy to get wrong. Arrow will not do it;
f64::to_bits()on a signalling NaN, a negative NaN, or a NaN with a payload gives different bytes. §5.3 step 4 must be a named, tested function applied to everyFloat64column before hashing — and it must not be applied to data on the way to a solver, where a NaN’s provenance may matter for diagnostics (§18.2 step 6). make_comparatorreturnsBox<dyn Fn(usize, usize) -> Ordering>and does not support nested types;lt/gtlikewise state “nested types (e.g. lists) are not supported”[c7:/websites/rs_arrow_arrow]. §4.4’spse.index_tuple(List<FixedSizeBinary(16)>) therefore cannot be compared or sorted by the ordinary kernels — see the row format in A5, which can. This is a real constraint on §5.3 step 1 that the blueprint does not anticipate.
6. Compute kernels, ordering, and the row format (A5)
Role: §5.3 step 1 (order rows by primary key) and step 2 (concatenate, expand dictionaries); §15.4 numerical checks over Jacobian values; §16 scaling transformations.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Selection | arrow_select::{concat, concat_batches, filter, filter_record_batch, filter_nulls, take, interleave, zip, nullif}; FilterBuilder, FilterPredicate, TakeOptions, SlicesIterator, BatchCoalescer, ScalarZipper | [rustdoc:arrow-select@59.3.0] |
| Dictionary hygiene | arrow_select::{garbage_collect_dictionary, garbage_collect_any_dictionary} | [rustdoc:arrow-select@59.3.0] |
| Ordering | arrow_ord::{sort, sort_to_indices, lexsort, lexsort_to_indices, partition, rank}; SortColumn, LexicographicalComparator, FixedLexicographicalComparator, Partitions, make_comparator | [rustdoc:arrow-ord@59.3.0], [c7:/websites/rs_arrow_arrow] |
| Comparison | arrow_ord::cmp::{eq, neq, lt, lt_eq, gt, gt_eq, distinct, not_distinct} | [rustdoc:arrow-ord@59.3.0] |
| Row format | arrow_row::{RowConverter, Rows, Row, OwnedRow, RowParser, SortField}; convert_columns, convert_rows, encode_dictionary_values | [rustdoc:arrow-row@59.3.0] |
| Casting | arrow_cast::{cast, cast_with_options, can_cast_types}; CastOptions, FormatOptions, ArrayFormatter, array_value_to_string | [rustdoc:arrow-cast@59.3.0] |
| Arithmetic | arrow_arith::{add, sub, mul, div, add_wrapping, …}, boolean and/or plus Kleene variants and_kleene/or_kleene, bitwise family, and the generic binary/binary_mut/unary builders | [rustdoc:arrow-arith@59.3.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §5.3 step 1 “Order rows by primary key (byte order of semantic IDs, then ordinals)” | lexsort_to_indices + SortColumn, or the row format | leverage — multi-column ordering is built in |
| §5.3 step 2 “Concatenate all batches into one” | concat_batches | confirmed load-bearing — see A6’s PROBE 4: without it, identical data in different batch splits produces different IPC bytes |
| §5.3 step 2 “expand dictionaries” | cast from Dictionary(K,V) to V | leverage |
| §15.4 row/column norms, extreme-value scans over the Jacobian | arrow_arith + reductions | partially — these run over the native evaluation program’s Jacobian (D11), not over Arrow arrays, so the kernels matter only where a diagnostic is expressed as a relation query |
Pass 2 — alignment opportunities
| Capability | DM / gate | What it would replace | Recommendation |
|---|---|---|---|
arrow_row::RowConverter | DM-15 (define canonicalization and equivalence before using content identity), DM-38 | a hand-written multi-column comparator for §5.3’s primary-key ordering | adopt — the strongest single find in the Arrow surface. RowConverter encodes a set of columns into a byte-comparable row format: sorting becomes memcmp, and two rows are equal iff their bytes are equal. That is precisely the canonical-ordering-and-equivalence primitive §5.3 needs, and it handles nested types, which make_comparator, lt and gt explicitly do not (A4). §4.4’s pse.index_tuple (List<FixedSizeBinary(16)>) is a primary-key component in several compiled.* relations, so this is not hypothetical — it is the difference between §5.3 step 1 working on those relations and not. |
arrow_ord::partition / Partitions | DM-38 | hand-written run detection over a sorted key column | adopt for grouped passes (per-instance, per-domain processing), which the rule compiler and §18.2’s tape construction both need. |
arrow_cast::CastOptions { safe } and can_cast_types | DM-42, G2 | unchecked or precision-losing conversion | Adopt as implementation tools after admission checks. safe: false can still truncate fractions and round integers [review:E3]; exact representability and complete quantity compatibility are platform checks (§14.2). |
and_kleene / or_kleene | DM-08 | ordinary and/or | adopt in the rule compiler. Kleene logic is three-valued (true AND null = null, false AND null = false), which is the semantics §14.2 rule 3’s four-valued predicates need underneath. Ordinary and propagates null unconditionally and loses the false short-circuit — changing which rows a rule emits. |
garbage_collect_dictionary | DM-15, DM-36 | — | evaluate. After filtering, a dictionary column retains unreferenced values; two logically identical relations can then carry different dictionary payloads. §5.3 step 2 expands dictionaries before hashing so the content hash is safe — but any other comparison of two batches is not. Worth knowing the kernel exists. |
BatchCoalescer | DM-37 (coarse typed units) | manual re-batching before crossing the FFI boundary | evaluate for §21’s Python handoff, where batch size affects per-call overhead. |
Gaps and risks
- The arithmetic kernels are checked by default (
adderrors on overflow;add_wrappingis the opt-in wrapping form)[rustdoc:arrow-arith@59.3.0]. Good default; worth stating so nobody reaches for the wrapping variants to silence an error. ArrayFormatter/array_value_to_stringexist for display only. They must never appear in a hashing or identity path — string rendering is locale- and option-dependent (FormatOptions).
7. IPC, Parquet, and canonical serialization determinism (A6)
The cluster that decides whether §5.3 — and therefore every content hash, snapshot id, and reproduction claim in the platform — actually holds.
Role: §5.3 canonical serialization and hashing; §20.1 artifact store (.arrow IPC hot artifacts, .parquet durable artifacts); §4.3 “metadata … preserved through IPC/Parquet”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Write options | IpcWriteOptions::try_new(alignment: usize, write_legacy_ipc_format: bool, metadata_version: MetadataVersion) -> Result<Self, ArrowError>; try_with_compression(Option<CompressionType>); try_with_compression_level(Option<i32>) | [rustdoc:arrow-ipc@59.3.0] |
| Writers | StreamWriter::{try_new, try_new_buffered, try_new_with_options, write, finish, into_inner}; FileWriter likewise | [rustdoc:arrow-ipc@59.3.0] |
| Readers | StreamReader, FileReader | [rustdoc:arrow-ipc@59.3.0] |
| Compression | opt-in via the ipc_compression feature and try_with_compression; None is expressible explicitly | [rustdoc:arrow-ipc@59.3.0], [docs.rs:arrow@59.3.0] |
| Parquet | parquet 59.3.0, 6507 indexed items | [rustdoc:parquet@59.3.0] |
Empirical verification — the four facts that matter
Run locally against the pinned build (evidence ledger row 16):
PROBE 1 metadata through an IPC round-trip
schema md preserved : Some("rel-1") <- pse.contract.id
field md preserved : Some("qt-demo") <- pse.semantic.quantity_type
ext name preserved : Some("pse.semantic_id") <- ARROW:extension:name
PROBE 4 batch splitting
1-batch len=872 2-batch len=1080 concat-then-write len=872
1-batch == 2-batch : false
1-batch == concat-first : true
PROBE 5 alignment
align8 len=872 align64 len=1032 equal=false
PROBE 7 Parquet — survival and determinism [new in this edition]
two writes of identical data: len=1093 identical=true
schema md survives : Some("rel-1")
field md survives : Some("qt-demo")
ext name survives : Some("pse.semantic_id")
null preserved=true -0.0 sign preserved=true
'created_by' writer string embedded in the file: true
| Blueprint claim | Verdict |
|---|---|
| §4.3 “Schema-level metadata keys … preserved through IPC/Parquet”; field-level likewise | confirmed empirically for both, including the ARROW:extension:name key — see PROBE 7 below. The previous edition could only confirm IPC. |
| §5.3 “Two logically identical relations split into different batches hash identically” | confirmed — but only because of step 2. Writing the same four rows as one batch versus two produces different byte streams (872 vs 1080). Concatenating first reproduces the single-batch bytes exactly. Step 2 (“concatenate all batches into one”) is therefore not housekeeping; it is the step that makes the claim true. Anyone who “optimises” it away breaks every content hash. |
| §5.3 step 3 “Serialize to Arrow IPC stream with no compression and deterministic alignment” | confirmed expressible: IpcWriteOptions::try_new(alignment, false, MetadataVersion::V5).try_with_compression(None). |
Pass 2 — alignment opportunities and a specification gap
| Item | DM / gate | Recommendation |
|---|---|---|
| Pin the alignment constant in the hashing contract | DM-48 (define and support the required reproducibility contract), DM-40 | adopt — this is a specification gap in §5.3. Alignment changes the bytes (872 vs 1032 for the same rows). §5.3 says “deterministic alignment” without naming a value. If the platform ever calls IpcWriteOptions::default(), the content hash silently depends on an Arrow default that can change between releases — and every snapshot_id in history becomes unreproducible. The canonical-serialization function must pass an explicit alignment and MetadataVersion, and those constants must be versioned alongside pse.contract.fingerprint. |
Record the IPC MetadataVersion in the hash contract | DM-48, DM-51 | adopt for the same reason: try_new takes it explicitly, so it is a contract term, not an ambient default. |
write_legacy_ipc_format: false | DM-42 | adopt explicitly rather than by default. |
| Parquet metadata + determinism | DM-48, DM-42 | now measured — and the previous edition’s reasoning was half right. See PROBE 7. Parquet does preserve schema metadata, field metadata and ARROW:extension:name, and two writes of identical data are byte-identical. So the feared metadata loss does not occur, and Parquet is deterministic within a fixed writer version. What remains true is the version dependence: the file embeds a created_by writer string, so the bytes change when parquet is upgraded even though the data has not. The recommendation therefore stands but for a narrower reason — state in §5.3/§20.1 that content identity is defined over the canonical IPC encoding only, not because Parquet loses meaning, but because its bytes carry the writer’s version. |
Gaps and risks
- Identity and integrity are separate. Blueprint §5.3 defines a versioned logical hash; §20 names each stored encoding by its own byte checksum. IPC-file/Parquet encodings cannot be verified against the canonical stream digest [review:E4].
- Finish and validate every writer before publication. Verify physical checksum/length/format, then the decoded semantic contract and logical hash where required. An existing checksum-named object is not automatically trusted (blueprint §20.1; [review:E4]).
8. The C Data / C Stream FFI boundary and Python interop (A7)
Role: §21.1 extension module (pyo3 + pyo3-arrow, cdylib); D12 “one adapter consumes a CanonicalMathProblem bundle over the Arrow C stream interface”; §21.5 typed Python contracts.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| C Data interface | arrow_schema::FFI_ArrowSchema; arrow_array::ffi::{to_ffi, from_ffi, export_array_into_raw} | [rustdoc:arrow-schema@59.3.0], [rustdoc:arrow-array@59.3.0] |
| C Stream interface | arrow_array::FFI_ArrowArrayStream and ArrowArrayStreamReader — the Rust side of the protocol Python exposes as __arrow_c_stream__ | [rustdoc:arrow-array@59.3.0] |
| Feature gate | the umbrella re-exports these under the ffi cargo feature | [docs.rs:arrow@59.3.0] |
| First-party Python bridge | arrow-pyarrow — now its own crate, enabled through the umbrella’s pyarrow feature | [docs.rs:arrow@59.3.0] |
| Third-party Python bridge | pyo3-arrow 0.19.0 (2026-06-15) — “zero-copy FFI conversions between Python objects and Rust representations … leveraging the Arrow PyCapsule Interface”; requires pyo3 ^0.29, arrow-* ^59, numpy ^0.29; default feature buffer_protocol (needs abi3-py311 or non-abi3 wheels) | [docs.rs:pyo3-arrow@0.19.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§3.1 pins pyo3 0.29 / pyo3-arrow 0.19.0 as “the Arrow 59 compatibility line” | pyo3-arrow 0.19.0 requires pyo3 ^0.29 and arrow-* ^59 | confirmed — and the ^59 (not ^59.2) requirement means the move to 59.3.0 is compatible, no change needed |
D12 / §21.1 “over the Arrow C stream interface”, __arrow_c_stream__ | FFI_ArrowArrayStream + ArrowArrayStreamReader on the Rust side; PyCapsule on the Python side | confirmed |
§21.1 the extension module is a cdylib | pyo3 mechanics, out of scope here | — |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
arrow-pyarrow vs pyo3-arrow — an unmade choice | DM-57 (small coherent core with explicit extension mechanisms), DM-41 (adapters mechanical) | evaluate and decide explicitly. Two routes now exist and the blueprint names only one. arrow-pyarrow is first-party and moves in lockstep with the Arrow family — one fewer independently-versioned compatibility edge, which is exactly the class of risk that produced the num-dual/feos-core split in the companion map. pyo3-arrow adds the PyCapsule protocol and numpy integration, which §21.2’s adapter may actually want. The decision should be recorded with its reason rather than inherited. |
PyCapsule (__arrow_c_stream__) over pyarrow-object conversion | DM-37 (cross expensive boundaries in coarse typed units), DM-42 | adopt — already the blueprint’s direction. The capsule protocol is implementation-neutral: any Python Arrow implementation can consume it, whereas converting to concrete pyarrow objects binds the contract to one library version. Worth stating as the reason. |
| Stream, not batch-at-a-time, across the boundary | DM-37 | adopt — FFI_ArrowArrayStream amortises one FFI crossing over the whole relation. §21’s bundle should cross as a stream per relation, not a call per batch. |
| Declare the loss profile of the Python adapter | DM-42 (loss-aware interchange), G7 (truthful capability claims) | adopt. §4.4’s pse.* extension types reach Python as storage types unless matching pyarrow.ExtensionType classes are registered (§4.4 says they are). Where a class is not registered, meaning is lost silently — a Python consumer sees FixedSizeBinary(16) and cannot tell a semantic id from a content hash. The adapter should declare, per column, whether the extension survived or degraded. |
Gaps and risks
pyo3-arrowdepends onthiserror ^1[docs.rs:pyo3-arrow@0.19.0]while the wider ecosystem has moved tothiserror 2.x. Two major versions ofthiserrorin one graph is legal (no shared types cross the boundary) but worth knowing before §23.2’s failure taxonomy is built onthiserror.- The
buffer_protocoldefault feature “requires either theabi3-py311pyo3 feature or building non-abi3 wheels”[docs.rs:pyo3-arrow@0.19.0]. §3.1 targets Python 3.11+, soabi3-py311is available — but this is a wheel-building constraint that must be settled before packaging, not after. - FFI ownership:
export_array_into_rawandto_ffitransfer ownership across the boundary under the C Data contract. A panic unwinding across that boundary is undefined behaviour — the same rule the companion map records for the Ipopt callbacks. State it once, apply it at every FFI edge.
9. Flight, Flight SQL, and the file-format readers (A8) — survey
Surveyed for completeness and rejected, with reasons, so the rejection is on the record rather than an omission.
| Capability | Status for this design | Reason |
|---|---|---|
arrow-flight / Flight SQL | not used | §20.1’s artifact store is object_store over immutable content-addressed files, and §21’s Python boundary is in-process FFI. Flight solves network transport between processes, which this architecture does not have. Adopting it would add a service boundary the blueprint explicitly does not want (§3.3’s “explicitly not added”). Revisit only if a remote execution tier appears. |
arrow-csv, arrow-json (umbrella features csv, json) | adopt for declared tabular observations | ReaderBuilder::new(schema) accepts an explicit schema; inference is optional. Use JSON strict mode and CSV header validation plus generated unit/target/invariant admission (blueprint §6.10; review L9). |
arrow-avro | not used | No Avro source in scope. Listed in §3.1’s pin table; it can be dropped from the dependency set entirely, which is one fewer crate in the version-pinning surface. |
prettyprint feature | development and diagnostics only | Useful for EXPLAIN-style output and test failure messages; must never appear in an identity or hashing path (A5). |
10. Pass 3 — the under-leverage sweep
Passes 1 and 2 both start from the blueprint: Pass 1 asks whether what it claims is true, Pass 2 asks what would improve alignment in the areas it already touches. Neither can find a capability the blueprint never gestured at, because neither enumerates the library independently.
This pass does. Four complete enumerations, each with a stated denominator, each row adjudicated bound (already used), adopt, evaluate or reject, with reason. Charter §G and DM-58 still apply: a row naming no principle and replacing no identified work does not appear. Where a whole group is uninteresting it is dismissed as a group rather than padded out.
The denominator matters because of the rule this corpus inherits: absence from the extraction is not evidence that a feature does not exist.
10.1 E1 — the crate inventory
arrow-rs 59.3.0 declares 27 workspace members [gh:apache/arrow-rs@59.3.0/Cargo.toml]. Four are build or test scaffolding (arrow-flight/gen, arrow-integration-test, arrow-integration-testing, parquet_derive_test), leaving 23 publishable crates. This map extracts 20; the three gaps are named below rather than silently omitted.
| Crate | Status | Adjudication |
|---|---|---|
arrow (umbrella) | bound | §4.2’s generated code, everything downstream |
arrow-schema | bound | §4.3, §4.4 — the densest coupling in the family (A1, A2) |
arrow-array, arrow-data, arrow-buffer | bound | §4.2 typed views, D11 buffer borrowing (A3) |
arrow-ipc | bound | §5.3 canonical serialization — the identity path (A6) |
arrow-ord, arrow-select, arrow-cast, arrow-arith | bound | §5.3 ordering, §14.2 rule evaluation, §20.5 import (A4, A5) |
arrow-row | adopt | register #4 — required, not optional, wherever a key contains pse.index_tuple |
arrow-string | bound (incidentally) | pulled in by the umbrella; no direct requirement. Utf8 columns in the platform are identifiers and documentation, not text to be searched |
parquet | bound | §20.1 durable artifacts (A6) |
arrow-csv, arrow-json (umbrella features csv, json) | adopt for declared tabular observations | ReaderBuilder::new(schema) accepts an explicit schema; inference is optional. Use JSON strict mode and CSV header validation plus generated unit/target/invariant admission (blueprint §6.10; review L9). |
arrow-avro | reject | no Avro source in scope; §3.1 rev 2 already says “not used” |
arrow-flight | reject | no network tier (A8). Note the residue: §3.1 rev 2 still lists arrow-flight in the pin row, so the dependency set carries a crate with no consumer — §15 |
parquet-variant, parquet-variant-compute, parquet-variant-json | reject, with reason | The Variant type is a self-describing semi-structured value — a JSON-like tree in a binary encoding. It is genuinely useful for schema-on-read data, and it is genuinely wrong here: D1 makes typed relations the only authority, and a Variant column is a typed hole in that authority. Adopting it would let a future contributor park unmodelled structure in a relation instead of declaring it. Recorded as a rejection precisely because it looks convenient (DM-01, DM-10, G1) |
parquet-geospatial | reject | no geospatial data in scope |
parquet_derive | reject | derives Parquet schemas from Rust structs — the inverse of §4.2’s direction, where the registry is authoritative and Rust types are generated from it (DM-02, G1) |
arrow-pyarrow | not extracted — open | needs a Python interpreter with pyarrow at build time, so it is outside this extraction’s offline constraint. This is §15 item 5’s crate and the decision is still unmade (A7) |
10.2 E2 — the extension-point inventory
101 public traits across the 20 extracted crates [rustdoc:arrow593@59.3.0]. Most are type-level machinery — ArrowPrimitiveType, ByteArrayType, OffsetSizeTrait, the Sealed markers — which exist to make the generic kernels work and are not extension points in any useful sense. Roughly a dozen are places where a platform can insert its own behaviour. Those are the interesting rows.
| Trait | DM / gate | Adjudication |
|---|---|---|
arrow_schema::extension::ExtensionType | DM-06, DM-07, G3 | bound — §4.4’s ten types (A2) |
arrow_cast::display::ArrayFormatterFactory | DM-49 (make changes understandable at the level of meaning), DM-47 | adopt. A factory that produces a custom ArrayFormatter for a given array and field. Today a pse.semantic_id renders in any diagnostic as 16 raw bytes and a pse.bound as an anonymous struct. §23.2 wants failures that carry “the relation rows and source spans involved”; rows that render as byte soup satisfy the letter and not the purpose. One factory, driven from §4.4’s table, makes every diagnostic surface render semantic types meaningfully — and DataFusion has the matching hook (DFExtensionType::create_array_formatter), so one implementation serves both. |
arrow_buffer::MemoryPool / MemoryReservation | DM-30, G5 | evaluate. Arrow-level allocation accounting (reserve, available, used, capacity), gated behind the pool feature. Distinct from DataFusion’s MemoryPool, which governs query memory: this governs array construction, including arrays the platform builds outside any query — §18.6’s result ingestion, §4.2’s builders. Those are exactly the allocations DataFusion’s pool cannot see. Worth evaluating once there is a memory budget to enforce; DM-58 says not before. |
arrow_json::EncoderFactory | DM-42, G7 | evaluate. Overrides how specific types are encoded to JSON. §19 and §23 export diagnostics and results; an extension column exported through the default encoder loses its semantic type silently. This is the export-side twin of the import-side CastOptions { safe: false } rule (register #6). Only worth it if JSON export becomes a supported interface rather than a debugging convenience. |
parquet::arrow::arrow_reader::ArrowPredicate + RowGroups | DM-36, DM-26 | evaluate. Row-level filtering pushed into the Parquet reader, with page-index support. §5.4’s provider answers key-filters from cached metadata; if a durable Parquet artifact ever becomes the scan source for a large relation, this is the mechanism that makes a filter cheap. Not yet — §5.4 reads IPC. |
parquet::file::metadata::memory::HeapSize | DM-39 | reject — accounting for Parquet metadata memory; the platform’s artifacts are small enough that this is noise. |
arrow_array::RecordBatchReader / RecordBatchWriter | DM-37 | bound (implicitly) — the stream shape at §20.1 and §21’s boundary |
arrow_buffer::alloc::Allocation | DM-37, DM-29 | evaluate — the ownership token behind Buffer::from_custom_allocation (register #11) |
arrow_flight::sql::server::FlightSqlService | — | reject — 46 provided methods for a service tier this architecture does not have (A8) |
parquet::geospatial::*, parquet_variant*::* | — | reject — see E1 |
| ~85 type-level and sealed traits | — | not extension points — dismissed as a group |
10.3 E3 — the declared-policy surface
Arrow’s policy surface is small and almost entirely compile-time, which is itself worth stating: unlike DataFusion (155 runtime config keys), Arrow’s behaviour is fixed at build time by feature flags, so it enters the provenance record as the dependency manifest rather than as a settings hash.
The full feature enumeration is §1.3. Two rows are new findings:
force_validate— adopt for test and CI profiles. It runs full structural validation on every array constructed, which is too expensive for release builds and is exactly right for a gate. G3 asks whether an invalid state can reach an operation that assumes validity; this turns “we believe our builders are correct” into a checked property across the entire test suite, at the cost of one line in a cargo profile. The blueprint’s §24.1 test layers do not mention it.pool— evaluate, per E2.
Beyond features, one runtime policy surface matters and the blueprint already half-specifies it: IpcWriteOptions. Its three constructor arguments (alignment, legacy format, MetadataVersion) plus compression are the complete set of knobs that change the bytes §5.3 hashes. They are policy in the DM-16 sense — declared, versioned, and part of the reproducibility contract — and §5.3 currently names none of them. That is §15 item 1, unchanged from the previous edition and still open.
10.4 E4 — the release delta, 59.0 → 59.3
Features landed in the 59 line [gh:apache/arrow-rs/dev/changelog]. Most are performance work invisible to a consumer. These are the ones that touch a principle:
| Change | Release | Adjudication |
|---|---|---|
| Parquet ↔ extension-type conversion fixed and tested (via the GeoArrow work) | 59.1.0 | explains PROBE 7. Extension metadata surviving a Parquet round-trip is a recent property, not an ancient one. Since §20.1 stores durable Parquet artifacts, this is load-bearing, and it is a reason to treat the =59.3.0 pin as a floor rather than an arbitrary choice. |
arrow-ipc sans-IO stream encoder | 59.2.0 | evaluate. Separates encoding from writing, so the platform controls buffering and can hash the encoded bytes without a second pass. Relevant to A6’s “hash the bytes actually written” recommendation. |
arrow-ipc compression-level configuration | 59.1.0 | reject — compression stays off on the identity path (A6) |
DictionaryArray::is_normalized flag | 59.0.0 | adopt as a check. A normalized dictionary has no unreferenced values. §5.3 step 2 expands dictionaries before hashing, so identity is safe — but any other comparison of two batches is not (A5). This flag makes the property assertable instead of assumed. |
arrow-csv header validation against a Schema | 59.1.0 | adopt at the explicit tabular observation boundary, not as an authoring-language replacement (ADR-0048; review L9) |
| Writing RunEndEncoded arrays directly to Parquet | 59.1.0 | folds into register #14’s REE evaluation — REE is now viable end-to-end, not just in memory |
uuid extension type from FixedSizeBinary(16) | 59.1.0 | reinforces register #17’s rejection — the canonical arrow.uuid is now easier to adopt and still wrong for a blake3 digest |
arrow-flight skip_validation, zero-copy tonic path | 59.1.0–59.2.0 | reject — no Flight tier |
product aggregate kernel; MapArray concat/lengths; i256 numeric traits; Float16 CSV parsing | 59.1.0–59.2.0 | reject — no requirement in scope |
10.5 Third-party extenders — bounded survey
Surveyed so that a future “we should use X” is already answered. None is adopted.
| Crate | Latest | Adjudication |
|---|---|---|
typed-arrow / typed-arrow-derive | 0.7.1 | reject, and worth explaining. It derives Arrow schemas from Rust types at compile time — superficially very close to this platform’s spirit. It is the wrong direction: §4.2 generates Rust types from the registry, which is the authority (D1). typed-arrow makes the Rust type the authority, which would invert the dependency and create exactly the second definition G1 forbids. |
serde_arrow | 0.15.0 | bound elsewhere — already §3.3’s, covered by the supporting-library map; the boundary there (“explicit schemas from the registry; no inference from sample data”) is the right one |
quiver | 0.6.1 | evaluate, low priority. A schema specification and validator for record batches. The platform generates its own validators from the registry (§4.2), so this would duplicate an authority; the only interesting part is its failure reporting. |
arrow-udf-wasm / arrow-udf-python | 0.5.1 / 0.4.2 | reject. Sandboxed UDF runtimes. §18.5’s kernels are native Rust behind KernelSpec; adding a WASM or Python execution tier inside kernel evaluation contradicts D10’s boundary and DM-58. |
minarrow, lance-arrow-scalar, delta_kernel | — | reject — alternative Arrow implementations and table formats; the platform’s storage protocol is its own (§20.1) |
11. Principle-alignment register (Arrow)
Capabilities the blueprint does not claim, which would improve alignment with DATA_MODEL_DESIGN_CHARTER.md. Every row names a principle and the hand-written work or risk it removes. Rejections are included, because a recorded rejection is worth as much as an adoption.
In the previous edition these rows lived in the DataFusion map as one combined 47-row table. They are split here so each map stands alone; the DataFusion rows are in that map’s §13. Numbering is local to this map.
Adopt
| # | Capability | Cluster | DM / gate | What it removes or prevents |
|---|---|---|---|---|
| 1 | Field::try_extension_type::<E>() / validate inside the generated try_from(&RecordBatch) | A2 | DM-07, G3 | A fingerprint match does not prove a field’s extension metadata deserializes or that its storage type is supported. Generated, not hand-written. |
| 2 | Enable canonical_extension_types | A2 | DM-42 | Without it the platform cannot recognise an inbound arrow.json/arrow.uuid/arrow.bool8 column and reads it as bare storage. Applied in revision 2 — retained because the register row is what justified it. |
| 3 | Enable ffi | A7 | — | Required for D12’s C stream boundary to compile at all. Applied in revision 2. |
| 4 | arrow_row::RowConverter for canonical ordering | A5 | DM-15 | A hand-written multi-column comparator — and the only option for keys containing pse.index_tuple, since the ordinary comparison kernels reject nested types. |
| 5 | distinct / not_distinct and Kleene booleans in the rule compiler | A5, A4 | DM-08 | Ordinary eq yields null for null-vs-null, silently dropping candidate rows — which §14.2 rule 3 explicitly forbids. |
| 6 | CastOptions { safe: false } + can_cast_types at import | A5 | DM-42, G2 | safe: true turns an out-of-range cast into a null. Silent degradation at the §20.5 import edge. |
| 7 | Name the IPC alignment constant and MetadataVersion in the §5.3 contract | A6 | DM-48 | Measured: alignment changes the bytes (872 vs 1032). An unnamed constant makes every content_hash depend on an Arrow default. |
| 8 | Canonicalize the hashing representation explicitly | A1 | DM-48, DM-15 | Metadata key/value sets are validated at construction; HashMap insertion order is not encoding order. v2 hashes a separate ordered metadata representation; plan encodings remain noncanonical [review:E5]. |
| 9 | Generated conformance test per pse.* extension type | A2 | DM-44, DM-53 | Ad-hoc tests; the ExtensionType trait defines exactly what to assert. |
| 10 | force_validate in the test and CI cargo profiles | §10.3 | G3, DM-54 | Turns “we believe the generated builders produce valid arrays” into a property checked on every array in the whole suite. One profile line. New. |
| 11 | ArrayFormatterFactory for pse.* rendering in diagnostics | §10.2 | DM-49, DM-47 | Hand-written display helpers per semantic type, or diagnostics that render a pse.semantic_id as 16 raw bytes. One factory, driven from §4.4’s table, shared with DataFusion’s matching hook. New. |
| 12 | DictionaryArray::is_normalized as an assertion | §10.4 | DM-15 | Makes “two logically identical batches carry identical dictionaries” checkable rather than assumed outside the hashing path. New. |
Evaluate
| # | Capability | Cluster | DM | Why it is not yet an adopt |
|---|---|---|---|---|
| 13 | Buffer::from_custom_allocation to publish a native workspace as Arrow | A3 | DM-37, DM-29 | Removes a copy when publishing solver results; costs a lifetime obligation on the Arc<dyn Allocation>. |
| 14 | RunEndEncoded, Utf8View for repeated/string columns | A1 | DM-36 | DM-58 applies — no access-pattern measurement exists. 59.1.0 made REE writable to Parquet, so the route is now end-to-end; the missing input is still a measurement. |
| 15 | arrow-pyarrow vs pyo3-arrow | A7 | DM-57 | One is first-party and version-locked to the Arrow family; the other adds PyCapsule and numpy. An unmade choice, and the compatibility-edge class of risk. |
| 16 | arrow.opaque for genuinely uninterpreted kernel values | A2 | DM-04, DM-42 | The standard cross-language way to say “from another system, not interpreted” — cheaper and more honest than inventing pse.opaque. |
| 17 | arrow_buffer::MemoryPool (the pool feature) | §10.2 | DM-30, G5 | Accounts for allocations DataFusion’s pool cannot see. Wait until there is a budget to enforce. New. |
| 18 | arrow_json::EncoderFactory for extension-aware export | §10.2 | DM-42, G7 | Only if JSON export becomes a supported interface rather than a debugging convenience. New. |
| 19 | arrow-ipc sans-IO stream encoder | §10.4 | DM-48 | Would let A6’s “hash the bytes actually written” happen in one pass instead of two. New. |
| 20 | parquet::ArrowPredicate and page indexes | §10.2 | DM-36, DM-26 | Durable Parquet is already a blueprint §5.4 scan source. Evaluate under R-24 with full-result equivalence and measured decoding/total-cost improvement (ADR-0048). |
Reject, with reason
| # | Capability | Cluster | Why rejected |
|---|---|---|---|
| 21 | arrow.uuid for pse.semantic_id | A2 | §5.1 puts blake3 digests and UUIDv7 in one opaque column. Labelling a blake3 digest arrow.uuid asserts version/variant structure it does not have — a G2 failure traded for cosmetic interoperability. |
| 22 | Field::extension_type::<E>() (the panicking variant) | A2 | A panic is not a typed failure (§23.2) and cannot become a validation.invariant row. G3. Ban by governance grep. |
| 23 | Schema::normalize / flattened_fields | A1 | They flatten nested structure into dotted names — the opposite of §4.5’s direction. Named so nobody adopts them for convenience. |
| 24 | Decimal32/Decimal64 for physical quantities | A1 | §4.5 is right that Float64 is what a solver consumes. Worth a note only for costing (§19.5). |
| 25 | Sample-inferred schemas at platform ingestion | A8 | Reject inference as authority; explicit-schema Arrow CSV/JSON readers are adopted for the declared §6.10 observation interface (ADR-0048). |
| 26 | parquet-variant family | §10.1 | A Variant column is a typed hole in D1’s “typed relations are the only authority”. Rejected because it looks convenient, not because it is obscure. New. |
| 27 | parquet_derive | §10.1 | Derives schemas from Rust structs; §4.2 generates Rust from the registry. Adopting it inverts the authority direction — G1. New. |
| 28 | typed-arrow | §10.5 | Same inversion as row 27, at compile time. New. |
| 29 | arrow-udf-wasm / arrow-udf-python | §10.5 | A sandboxed execution tier inside kernel evaluation contradicts D10’s boundary and DM-58. New. |
| 30 | arrow-flight / Flight SQL | A8 | No network tier. Note the residue: §3.1 still pins a crate with no consumer. |
12. Acceptance-gate review (Arrow)
This map establishes library surfaces and bounded observations, not platform gate acceptance. The earlier gate summary credited registration and incomplete canonicalization too strongly; the revision-4 review supersedes it.
| Gate concern | Current mechanism | Remaining evidence |
|---|---|---|
| G1 / G2 / G3 | Registry authority, complete physical admission and v2 typed canonicalization | Supported boundary rejection and metamorphic fixtures; E1/E3/E4 are counterexamples, not implementation acceptance |
| G4 / G6 | Safe owning views, explicit conversion and ordered numerical contracts | Lifetime, conversion and backend conformance |
| G5 / G7 | Logical/encoded identity split and truthful interface scope | Corrupt/partial/existing-object publication fixtures and complete provider comparisons |
Use the revision-5 design review for current document-stage gates and plan 02 for implementation acceptance.
13. Leverage matrix
| Blueprint requirement | API | Status |
|---|---|---|
§4.3 schema- and field-level pse.* metadata keys | Schema.metadata, Field::with_metadata (both HashMap<String,String>) | confirmed |
| §4.3 “preserved through IPC/Parquet” | — | confirmed empirically for both [probe] — schema md, field md and ARROW:extension:name survive IPC (PROBE 1) and Parquet (PROBE 7) |
§4.3 pse.contract.fingerprint reproducibility | Schema.metadata iteration | refined — IPC is stable regardless of key count [probe] (PROBE 6), so the previous edition’s claim was wrong; the exposure is platform-side fingerprints and datafusion-proto (§15 item 3) |
| §4.4 extension admission | try_extension_type, ExtensionTypeRegistry factories and generated validation | X1 is a direct-helper check; ordinary query enforcement requires the active boundary [review:E1] |
| §4.5 logical type catalog | DataType (41 variants; the catalog is a conservative subset) | confirmed |
§4.2 try_from(&RecordBatch) schema check | fingerprint is platform code; Schema::contains available | refined — add a structural check beside the fingerprint |
§4.4 ExtensionType with NAME/Metadata/serialize_metadata/deserialize_metadata/supports_data_type | the trait, verbatim | confirmed, incomplete — try_new also required; validate + try_new_from_field_metadata provided and are the read path |
§4.4 pse.* namespacing | arrow. reserved for canonical types | confirmed |
| §4.4 extension validation at the boundary | try_extension_type / validate | adopt — highest-value Arrow item (DM-07, G3) |
| §4.4 recognising inbound canonical types | canonical_extension_types feature | adopt — unmentioned; DM-42 |
| D11 borrow Arrow buffers | PrimitiveArray::values, ScalarBuffer | Interface-checked safe slice-aware route; owner lifetime, null rejection and reservation checks are platform obligations (ADR-0047; review L7) |
| D11 publish native workspace as Arrow | Buffer::from_custom_allocation | evaluate (DM-37, DM-29) |
§4.4 pse.bound never null/NaN sentinels | null propagates through cmp kernels | confirmed, with a second reason |
§5.3 step 4 canonicalise NaN, preserve -0.0 | IEEE 754 totalOrder in sort/cmp | confirmed necessary [probe] — sorted order was null, -1, -0, 0, NaN |
| §5.3 step 1 order by primary key | lexsort_to_indices, or RowConverter | leverage; RowConverter required where keys include pse.index_tuple (nested types are unsupported by make_comparator/lt) |
| §5.3 step 2 concatenate batches | concat_batches | confirmed load-bearing [probe] — without it, identical data in different splits yields different bytes |
| §5.3 step 2 expand dictionaries | cast from Dictionary(K,V) to V | leverage |
| §5.3 step 3 IPC, no compression, deterministic alignment | IpcWriteOptions::try_new(alignment, false, MetadataVersion) + try_with_compression(None) | confirmed expressible; alignment value is unspecified in §5.3 [probe] |
| §14.2 rule compiler null-aware joins/dedup | distinct / not_distinct, and_kleene / or_kleene | adopt — unmentioned; DM-08 |
| §20.5 declared import conversions | cast_with_options, can_cast_types | Interface-checked mechanisms; exact numeric/physical admissibility must be established first [review:E3] |
| §21.1 / D12 Arrow C stream boundary | FFI_ArrowArrayStream, ArrowArrayStreamReader, ffi feature | confirmed |
§3.1 pyo3-arrow 0.19.0 on the Arrow 59 line | requires arrow-* ^59, pyo3 ^0.29 | confirmed — compatible with 59.3.0 |
| §3.1 one Arrow release family | umbrella-only pinning does not pin the family | erratum — resolved in revision 2 (§1.2); the lockfile half is still open (§15 item 6) |
§3.1 arrow-flight in the pin row | — | residue — pinned, no consumer (§10.1, amendment A′) |
| §24.1 test layers | force_validate cargo feature | adopt, unmentioned — a whole-suite structural-validity gate for one profile line (register #10, G3) |
| §23.2 diagnostics render semantic values | arrow_cast::display::ArrayFormatterFactory | adopt, unmentioned — today a pse.semantic_id renders as 16 raw bytes (register #11, DM-49) |
14. Evidence ledger
Rows 1–22 are the previous edition’s, retained so its claims stay traceable. Rows 23–32 are this edition’s. Row 15’s extraction no longer exists on disk and is superseded by row 26; row 14’s open question is closed by row 28.
| # | Cluster | Instrument | Source | Query / target | Outcome |
|---|---|---|---|---|---|
| 1 | setup | WebSearch | crates.io, docs.rs, github.com, datafusion.apache.org | latest Apache DataFusion release | returned 55.0.0 — later found stale; see row 12 |
| 2 | setup | WebFetch | https://docs.rs/crate/arrow/latest | latest arrow version, feature flags, sub-crate deps | 59.3.0 (2026-09-01); features incl. ffi, pyarrow, canonical_extension_types; umbrella over 10 required + 4 optional sub-crates |
| 3 | setup | c7 resolve | DataFusion | Rust query engine, TableProvider, LogicalPlan, ScalarUDFImpl | /apache/datafusion (5032 snippets, High) |
| 4 | setup | c7 resolve | arrow-rs | Rust Arrow, RecordBatch, schema, extension types, IPC, FFI | /websites/rs_arrow_arrow (12171); /apache/arrow-rs (291); /apache/arrow = wrong language line |
| 5 | setup | c7 query-docs | /websites/rs_datafusion (direct ID probe) | TableProvider::scan_with_args | not found — no docs.rs mirror for DataFusion on context7 |
| 6 | D1 | c7 query-docs | /apache/datafusion | custom TableProvider with projection, filter pushdown, statistics | served upstream library-user-guide/custom-table-providers.md + upgrading/48.0.0.md; API-accurate |
| 7 | setup | cargo | cargo generate-lockfile --offline | resolve arrow "=59.2.0" + datafusion "=55.0.0" | finding: arrow-array/arrow-schema floated to 59.3.0 inside an arrow 59.2.0 build |
| 8 | A2 | c7 query-docs | /websites/rs_arrow_arrow | ExtensionType trait, attaching to a Field | extension_type_name, try_extension_type, EXTENSION_TYPE_NAME_KEY/_METADATA_KEY |
| 9 | A2 | WebFetch | docs.rs/arrow-schema/59.2.0/…/extension/index.html | module contents, canonical types, feature gate | 7 canonical types + CanonicalExtensionType, gated by canonical_extension_types |
| 10 | A2 | WebFetch | docs.rs/arrow-schema/59.2.0/…/trait.ExtensionType.html | full trait definition and method contracts | NAME namespacing rule (arrow. reserved); deserialize_metadata error obligations |
| 11 | A4 | c7 query-docs | /websites/rs_arrow_arrow | null vs NaN in comparison/sort kernels, float ordering | IEEE 754 totalOrder for sort and cmp; null propagates to null; SortOptions, make_comparator |
| 12 | setup | WebFetch | https://docs.rs/crate/datafusion-expr/latest | latest datafusion-expr version | 55.1.0 (2026-09-11) — sub-crates ahead of the assumed 55.0.0 |
| 13 | setup | WebFetch | https://docs.rs/crate/datafusion/latest | latest umbrella version, deps, features | 55.1.0 (2026-09-11) — corrects row 1. Depends on arrow ^59.2.0, object_store ^0.13.2; full default/optional feature list |
| 14 | A1 | c7 query-docs | /websites/rs_arrow_arrow | is schema/field custom metadata preserved through IPC? | Schema{fields, metadata}, new_with_metadata, RecordBatch::schema_metadata_mut; preservation not directly evidenced — see open items |
| 15 | all | rustdoc | locally generated, arrow 59.3.0 + datafusion 55.1.0, format_version 61 | full API surface of 30 crates | authoritative lane; receipt in §1 |
| 16 | A4, A6 | probe | locally compiled src/bin/probe.rs against arrow 59.3.0 | IPC metadata round-trip; null/NaN/-0.0 distinctness; totalOrder sort; batch-split byte equality; alignment sensitivity | all five results quoted inline in §5 and §7 |
| 17 | A6 | rustdoc | arrow_ipc@59.3.0 | IpcWriteOptions, StreamWriter, FileWriter | try_new(alignment, write_legacy_ipc_format, metadata_version), try_with_compression(Option<_>) |
| 18 | A5 | rustdoc | arrow_select/arrow_ord/arrow_cast/arrow_row/arrow_arith@59.3.0 | full function and struct inventories | RowConverter, distinct, and_kleene, CastOptions, partition, garbage_collect_dictionary, BatchCoalescer |
| 19 | A3, A7 | rustdoc | arrow_buffer/arrow_array/arrow_schema@59.3.0 | buffer zero-copy surface; FFI types | from_custom_allocation, ptr_offset; FFI_ArrowSchema, FFI_ArrowArrayStream, ArrowArrayStreamReader, to_ffi/from_ffi |
| 20 | A2 | rustdoc | arrow_schema@59.3.0 | canonical extension NAME constants | arrow.uuid, arrow.json, arrow.bool8, arrow.opaque, arrow.fixed_shape_tensor, arrow.variable_shape_tensor, arrow.timestamp_with_offset |
| 21 | A7 | WebFetch | https://docs.rs/crate/pyo3-arrow/latest | version, arrow/pyo3 requirements, features | 0.19.0; arrow-* ^59, pyo3 ^0.29, numpy ^0.29, thiserror ^1; buffer_protocol default |
| 22 | setup | cargo | cargo generate-lockfile / cargo fetch | coherent =-pinned resolution | Arrow family all 59.3.0; DataFusion family all 55.1.0; object_store 0.13.2; tokio 1.53.1 |
| 23 | §1.1 | crates.io API | crates.io/api/v1/crates/{arrow,datafusion} | is either pin stale? | no — arrow 59.3.0 (2026-09-01) and datafusion 55.1.0 (2026-09-11) are both still the current releases at this compile [crates.io:arrow], [crates.io:datafusion] |
| 24 | §1.1 | GitHub raw | apache/datafusion@55.1.0/Cargo.toml | which Arrow does DataFusion build against? | arrow = "59.2.0" (caret) — the =59.3.0 pin is one minor ahead of DataFusion’s tested Arrow [gh:apache/datafusion@55.1.0/Cargo.toml] |
| 25 | §10.1 | GitHub raw | apache/arrow-rs@59.3.0/Cargo.toml | the E1 denominator | 27 workspace members; 23 publishable after removing build/test scaffolding [gh:apache/arrow-rs@59.3.0/Cargo.toml] |
| 26 | all | rustdoc | locally generated, full family, =-pinned, lockfile committed | complete API surface | 57 targets, 57 OK, 0 FAIL, 60 JSON documents, format_version 61; arrow family 20 crates all 59.3.0. Supersedes row 15, whose output was not preserved. Receipt in §1.4 |
| 27 | §10.2 | facts corpus | build/facts/arrow593 (normalized from row 26) | the E2 denominator | 101 public traits, 30,920 declarations, 18,268 impl relations [rustdoc:arrow593@59.3.0] |
| 28 | A1, A6 | probe | arrow_probe2.rs against arrow 59.3.0 | does metadata key count change canonical IPC bytes? does Parquet preserve metadata and write deterministically? | PROBE 6 — IPC stable at 0/1/2/3/5 keys, which refutes this map’s previous claim (§0.4). PROBE 7 — Parquet preserves schema md, field md and ARROW:extension:name; two writes byte-identical; created_by embedded. Closes row 14’s open question and the previous edition’s unresolved item 1 |
| 29 | A2 | probe | df_probe_x.rs against datafusion 55.1.0 | can a pse.* type be registered in DataFusion’s ExtensionTypeRegistry, and what happens to an unregistered one? | PROBE X1, direct helper only — 7 canonical types preloaded; pse.semantic_id registers and resolves; wrong storage type rejected with a typed error; unregistered name errors rather than degrading |
| 30 | §10.4 | GitHub | apache/arrow-rs dev/changelog/59.{0,1,2}.0.md | the E4 delta | Parquet/GeoArrow extension-type round-trip (59.1.0) — explains PROBE 7; sans-IO IPC stream encoder (59.2.0); DictionaryArray::is_normalized (59.0.0); REE→Parquet (59.1.0) [gh:apache/arrow-rs/dev/changelog] |
| 31 | §10.5 | crates.io API | crates.io search, q=arrow | third-party extenders worth adjudicating | typed-arrow 0.7.1, serde_arrow 0.15.0, quiver 0.6.1, arrow-udf-wasm 0.5.1, arrow-udf-python 0.4.2, pyo3-arrow 0.19.0 |
| 32 | §1.3 | GitHub raw | apache/arrow-rs@59.3.0/arrow/Cargo.toml | the E3 denominator | the complete umbrella feature list, including the undocumented-in-blueprint force_validate and pool [gh:apache/arrow-rs@59.3.0/arrow/Cargo.toml] |
15. Open items and recommended blueprint amendments
The reviewed amendments are now Proposed blueprint revision-5 contracts. The existing probe receipts remain historical observations; no library receipt proves their platform integration.
| Item | Current disposition | Remaining gate |
|---|---|---|
| Active extension validation | §4.3–§4.4, ADR-0039; generated recursive admission | Actual provider/query/bundle/output paths and nested values |
| Canonical bytes and physical encodings | §5.3/§20, ADR-0045; v2 framing and null normalization | Metamorphic/round-trip/integrity tests; R-23 for larger relations |
| Metadata ordering | Ordered hashing representation, noncanonical protobuf evidence | No sorted-insertion equivalence claim; [review:E5] |
| Numeric/physical casts | §14.2 semantic admission, ADR-0039 | Fractional/large-integer and wrong-unit/basis/reference rejection |
| Buffer views and transfer | §18.2/§21.1, ADR-0047/0048 | Lifetime/cancellation checks; R-24 for coalescing benefit |
| Shared predicates / Parquet | §5.4, ADR-0048 | Full pushed/unpruned results; R-24 before additional pruning |
| Explicit-schema readers | §6.10, ADR-0048 | Declared tabular descriptor, strict fields/headers and unit/target validation |
| Optional layouts / custom allocation | Remain consumer/measurement gated | No new layout admitted merely because Arrow exposes it |
The revision-4 evidence supplies E1/E3/E4/E5; review L3/L7/L9 records the pinned source interfaces for the added bindings. Regeneration of the original evidence corpus was not part of this update.
DataFusion (Rust)
status: evidence-map blueprint_revision: 5 pins: Cargo.lock regenerated: null reviewed: 2026-09-13
DataFusion (Rust) — capability map
Current binding: blueprint revision 5 and ADR-0039–ADR-0048. The inventories and original extraction/probe receipts below retain their historical scope and dates; this update did not regenerate that corpus. Historical comparisons to revision 2 are not current acceptance claims. Corrected interpretations are recorded in place. The current design remains Proposed; pinned library mechanisms are Interface-checked, and only the named characterization experiments are Tested.
[review:E1]–[review:E6] refer to the retained six-group characterization
and its conditions: Rust 1.98.1, Arrow 59.3.0, DataFusion 55.1.0, dev,
Arrow force_validate, zero failure baseline. The revision-4 review
explains the counterexamples; these markers do not certify a platform implementation.
| Current decision | Library mechanism and boundary | Evidence / adoption gate |
|---|---|---|
| Active semantic analyzer | Generated recursive validator before/after relevant plan rewrites, including subqueries; explicit provider/output admission | ADR-0039; [review:E1]; registry registration alone is passive |
| Guarded execution and field-aware UDFs | Built-in CASE, complete argument/return contracts, scalar fast paths | ADR-0043/0047; [review:E2]; flags do not implement lazy arguments |
| Standard relational mechanisms | Physical expressions/Arrow filters; typed aggregate and unnest for contributions/membership | ADR-0048; review L3/L4; full-result and expansion-reference fixtures |
| Complete stage reuse and noncanonical evidence | Complete input ports plus engine/function registry; diagnostic protobuf checksums outside semantic keys | ADR-0040–0044; [review:E5] and dependency-matrix tests |
| Shared accounted runtime | Shared RuntimeEnv, fallible platform reservations and consumer/peak attribution | ADR-0046; review L5; query pool is not a process-wide allocation limit |
| Predicate preimages | Exact supported replacement semantics with half-open bounds | ADR-0048; review L8; R-25 proof/equivalence and measurement trigger |
Companion to arrow-rust.md; together they cover the engine stack that supporting-rust-libraries.md deliberately excluded. The register (§13) and gate review (§14) here are now DataFusion-only; the Arrow map carries its own. In the previous edition both were combined here, which meant neither document could be updated alone.
Compiled 2026-09-13 against datafusion 55.1.0 on arrow 59.3.0.
Historical survey adjudicated blueprint revision 2 (2026-09-13). The previous edition adjudicated revision 1 and never said so; §0.1 lists what changed underneath it.
0. Purpose, sourcing constraint, and evidence rules
Per cluster: what DataFusion can actually do, which APIs the blueprint’s requirements land on, what we deliberately will not use, and where the blueprint assumes a capability that does not exist or is under-specified. A second pass asks what the engine offers that the blueprint has not claimed, which would improve alignment with design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01…DM-60, gates G1–G7).
Sourcing constraint. Written without the repository’s existing Arrow/DataFusion material — no docs/library_ref/ prose, no datafusion-pyarrow-* skills, no pre-existing build/facts/df55-default surface (which is at 55.0.0 and would misrepresent the pin), no vendored datafusion/ checkout. Two of those are now moot rather than merely avoided: docs/library_ref/ has been deleted, and the vendored checkouts survive only as orphaned worktrees whose parent repositories are gone, so their tags can no longer be verified. Five fresh lanes:
| Lane | Marker | What it is |
|---|---|---|
| locally generated rustdoc JSON | [rustdoc:crate@55.1.0] | the authoritative lane — exact signatures from the pinned crates; receipt in §1 |
| locally run probe program | [probe] | new in this edition. The previous edition had no measurements: every claim was API shape. Six probes now measure behaviour the blueprint asserts. |
| context7 | [c7:/apache/datafusion] | the upstream docs/source/library-user-guide/ tree and the per-version upgrading/ guides — idiom and intent |
| docs.rs | [docs.rs:crate@version] | version-addressed pages, feature flags, release dates |
| upstream source at a release tag | [gh:repo@tag/path] | workspace manifests, config references and changelogs read at the exact pinned tag — used for the §12 enumerations, where a complete list matters and rustdoc gives only what was extracted |
Checked mechanically: no provenance marker is of the form [ref:…], and none cites /apache/arrow (the multi-language C++/Python line). A claim carrying no marker is a defect.
Status vocabulary and the Pass-2 discipline (charter §G, DM-58) are as defined in the Arrow map §0.3. Pass 3 (§12) is new and answers a third question: taking the complete inventory of the family — every crate, every public trait, every config key, every release delta — what is this design not leveraging at all?
0.1 What changed underneath this map
The previous edition was compiled at 14:35 on 2026-09-13; the blueprint was rewritten at 15:24.
| # | Change | Consequence |
|---|---|---|
| 1 | Revision 2 absorbed amendment A (pin discipline) and C/D with must-settle #4 and #6 — the engine became a declared input: reference.engine_profiles, ordered rule lists, a semantic-settings hash, and a plan fingerprint all enter the memo key and the pass record (F9) | §1.2’s erratum is resolved. Register row #18 is no longer a recommendation but a binding to verify — and §5 does verify it, with a result that is not good news. |
| 2 | F9 chose datafusion-proto as the fingerprint mechanism | The previous edition never evaluated datafusion-proto: its open item 6 offered only “a formatting surface” or “a platform structural walk”, and §10 rejected Substrait without mentioning proto at all. The crate was absent from the extraction corpus. It is now extracted, and measured — see D9, which is the most consequential finding in this edition. |
| 3 | Revision 2 added pass P12 (index expansion) and renumbered P12–P15 to P13–P16 | Every pass reference in this map re-checked; §4 and §6 note where the new cardinality-changing pass lands. |
| 4 | The rustdoc extraction behind 60 [rustdoc:] citations no longer exists on disk | §1.4’s new extraction is committed with its lockfile so this cannot recur. |
| 5 | Coverage was 20 of ~40 DataFusion crates | §1.4 now extracts 37, including datafusion-proto, -ffi, -substrait, the -datasource-* family and -physical-expr-adapter. §12 states the denominator. |
1. Version anchors and the extraction receipt
1.1 Anchors
| Component | This document | Blueprint §3.1 (rev 2) | Note |
|---|---|---|---|
datafusion and every datafusion-* sub-crate | 55.1.0 (2026-09-11) | 55.1.0, each crate =-pinned | Now agreed. Still the current release at this compile [crates.io:datafusion] — no re-pin. |
arrow family | 59.3.0 | 59.3.0 | DataFusion 55.x requires arrow ^59.2.0 [docs.rs:datafusion@55.1.0], so 59.3.0 resolves. But its workspace builds and tests against arrow = "59.2.0" [gh:apache/datafusion@55.1.0/Cargo.toml], so the platform runs one minor ahead of the Arrow the engine is exercised against. Not a defect; a fact §3.1 should record. |
object_store | 0.13.2 | 0.13.2 | Resolved by cargo, not chosen: DataFusion 55.1.0 requires object_store ^0.13.2 [docs.rs:datafusion@55.1.0]. §3.1’s pin is correct and 0.14.1 must not be used — this closes open item #3 of the supporting-library map. |
tokio | 1.53.1 | 1.52+ | resolved |
| MSRV / edition | 1.94.0 / 2024 | 1.94.0 / 2024 | confirmed at the tag [gh:apache/datafusion@55.1.0/Cargo.toml] |
1.2 Why 55.1.0, and a pin-discipline erratum — resolved in revision 2
Retained as a record of why §3.1 reads as it now does, not as a live recommendation.
Two corrections to earlier reconnaissance, both worth recording because they are easy to repeat:
-
55.1.0, not 55.0.0, is current. A web search reported 55.0.0 as latest; the crate pages show
datafusion55.1.0 released 2026-09-11[docs.rs:datafusion@55.1.0], withdatafusion-expr55.1.0 the same day[docs.rs:datafusion-expr@55.1.0]. Search results were stale by two days. -
Pinning the umbrella does not pin the family. Resolving
datafusion = "=55.0.0"produceddatafusion55.0.0 with every sub-crate at 55.1.0, because the umbrella depends ondatafusion-* ^55.0.0. That mixed state is worse than either coherent choice, andcargo tree -ddoes not detect it — it is not a duplicate. The identical problem exists in the Arrow family (arrow59.2.0 resolvingarrow-schema59.3.0).
Recommended §3.1 amendment (half applied): pin every crate in both families with =, or commit a Cargo.lock and enforce it in CI. Revision 2 adopted the = pins; it did not adopt the lockfile. = pins bind direct dependencies only — nothing stops a transitive path from introducing a second version, and only a committed lockfile plus a CI assertion catches that. This extraction does both, and its lockfile is committed under evidence/rust/.
1.3 Features (datafusion 55.1.0)
Default: nested_expressions, compression (xz2/bzip2/flate2/zstd), crypto_expressions, datetime_expressions, encoding_expressions, parquet, sql, regex_expressions, unicode_expressions, unparser, recursive_protection. Optional: avro, backtrace, parquet_encryption, serde [docs.rs:datafusion@55.1.0]. Dispositions in §10.
1.4 Extraction receipt
toolchain rustc 1.100.0-nightly (809936eac 2026-09-12)
cargo 1.100.0-nightly (7941be6fb 2026-09-11)
flags -Z unstable-options --output-format json
format_version 61
run 2026-09-13T16:15:24-04:00 .. 16:16:22-04:00
targets 57 requested, 57 OK, 0 FAIL, 60 JSON documents
DataFusion 37 crates, every crate_version 55.1.0
datafusion, _catalog, _catalog_listing, _common, _common_runtime,
_datasource, _datasource_arrow, _datasource_csv, _datasource_json,
_datasource_parquet, _doc, _execution, _expr, _expr_common, _ffi,
_functions, _functions_aggregate, _functions_aggregate_common,
_functions_nested, _functions_table, _functions_window,
_functions_window_common, _macros, _optimizer, _physical_expr,
_physical_expr_adapter, _physical_expr_common, _physical_optimizer,
_physical_plan, _proto, _proto_common, _proto_models, _pruning,
_session, _spark, _sql, _substrait
Arrow 20 crates, every crate_version 59.3.0 (companion map)
lockfile 361 packages; one version per family, asserted
Newly covered relative to the previous edition’s 20 crates, and each for a reason: datafusion-proto / -proto-common / -proto-models (revision 2’s F9 depends on them and they were absent), datafusion-ffi, datafusion-substrait, the whole -datasource-* family, -physical-expr-adapter, -functions-{nested,table,window}, -catalog-listing, -spark, -common-runtime, -macros, -doc.
The normalized form is written to build/facts/df551/ — 76,771 declarations, 169 public traits, 48,968 impl relations — alongside, and never overwriting, the older df55-default profile at 55.0.0. Trait coverage more than doubled: 169 against the previous 148, from nearly twice the crates.
Committed under docs/capability-maps/evidence/rust/: the extraction manifest, the resolved Cargo.lock, the generation script and log, the five probe programs, and their captured output (probe_output.txt).
1.5 §3.1’s “55 forms” — all confirmed at 55.1.0
§3.1 states that scan_with_args/ScanArgs, PhysicalPlanningContext, EnsureRequirements, is_strict and convert_to_state “are the 55 forms to code against”. Every one resolves:
| Identifier | Crate | Kind |
|---|---|---|
scan_with_args, ScanArgs | datafusion-session | trait method, struct |
PhysicalPlanningContext | datafusion-expr | struct |
EnsureRequirements | datafusion-physical-optimizer | struct |
is_strict | datafusion-expr (ScalarUDFImpl) | trait method |
convert_to_state | DM-25 | Note only: the revised algebra uses built-in aggregation, but declares no custom UDAF requiring this state-conversion route. Do not infer a consumer merely from the aggregate family (ADR-0048). |
[rustdoc:*@55.1.0]. §3.1’s further note that “planner contracts moved toward datafusion-session” is also confirmed: TableProvider, CatalogProvider, CatalogProviderList, SchemaProvider, Session and ScanArgs all live there.
1.6 Probe receipt — new in this edition
The previous edition made no measurements. These six programs test assertions the blueprint makes, rather than trusting the API shape.
| Probe | Settles | Result in |
|---|---|---|
| A | what filter shapes actually reach a provider on a FixedSizeBinary(16) key column | §3 (D2) |
| B | whether a UserDefinedLogicalNode survives the optimizer carrying a rule_id | §4 (D3) |
| C | whether datafusion-proto encoding is usable and stable enough to be a plan fingerprint | §10 (D9) |
| D | how null, NaN and -0.0 behave through sort, GROUP BY and equi-join | §4, §7 |
| X1 | whether a pse.* extension type can be registered and resolved by the engine | §11 (D10) |
| X2/X3 | which EXPLAIN formats exist, and how an invalid config value is reported | §5, §9 |
| H | whether a bounded MemoryPool yields a typed failure or a process death | §8 (D7) |
2. Catalog, providers, and scan_with_args (D1)
Role: §5.4 — “the catalog is a DataFusion CatalogProviderList whose catalogs are snapshots …, whose schemas are the seven namespaces, and whose tables are relations”; the provider contract that follows it.
Where the contracts live. In 55.1.0 TableProvider, CatalogProvider, CatalogProviderList, SchemaProvider, Session and ScanArgs are all defined in datafusion-session [rustdoc:datafusion-session@55.1.0]. §3.1’s note — “planner contracts moved toward datafusion-session” — is confirmed, and it means pse-catalog depends on datafusion-session, not on the umbrella crate, which is a smaller and more stable dependency edge.
Capability inventory
| Trait | Methods | Provenance |
|---|---|---|
CatalogProviderList | register_catalog(String, Arc<dyn CatalogProvider>), catalog_names() -> Vec<String>, catalog(&str) -> Option<Arc<dyn CatalogProvider>> | [rustdoc:datafusion-session@55.1.0] |
CatalogProvider | schema_names(), schema(&str), register_schema, deregister_schema(name, cascade) | [rustdoc:datafusion-session@55.1.0] |
SchemaProvider | owner_name(), table_names(), table(&str) — async, table_type(&str) — async, register_table, deregister_table, table_exist(&str) | [rustdoc:datafusion-session@55.1.0] |
TableProvider — read path | schema() -> SchemaRef, constraints() -> Option<&Constraints>, table_type() -> TableType, get_table_definition(), get_logical_plan(), get_column_default(&str), scan(state, projection, filters, limit) async, scan_with_args(state, ScanArgs) async, supports_filters_pushdown(&[&Expr]), statistics() -> Option<Statistics> | [rustdoc:datafusion-session@55.1.0] |
TableProvider — mutation path | insert_into(state, input, InsertOp), delete_from(state, filters), update(state, assignments, filters), truncate(state), merge_into(state, source, merge_schema, on, clauses: Vec<MergeIntoClause>) — all async with default implementations | [rustdoc:datafusion-session@55.1.0] |
ScanArgs | with_projection(Option<&[usize]>), with_filters(Option<&[Expr]>), with_limit(Option<usize>), with_statistics_requests(&[StatisticsRequest]), and the matching getters | [rustdoc:datafusion-session@55.1.0] |
TableType | Base | View | Temporary | [rustdoc:datafusion-expr@55.1.0] |
| Async catalog adapters | AsyncCatalogProvider, AsyncCatalogProviderList, AsyncSchemaProvider | [rustdoc:datafusion-catalog@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §5.4 catalogs are snapshots, schemas are the seven namespaces, tables are relations | CatalogProviderList → CatalogProvider → SchemaProvider → TableProvider | confirmed — the three-level shape is exactly what the trait stack gives |
§5.4 “mutable aliases such as head resolved to a snapshot at session creation” | catalog(name) is a plain lookup; resolution happens in platform code before registration | confirmed — and correctly placed: resolving at session creation means catalog() stays pure, which §5.4’s “a session pins a snapshot” requires |
§5.4 “scan_with_args is implemented; scan delegates” | both exist as provided methods | confirmed, and the direction is right: ScanArgs is the extensible carrier, scan the legacy narrow form |
§5.4 “schema() is the generated schema; cheap and stable” | TableProvider::schema() is synchronous and infallible | confirmed |
| §5.4 “Constraints: primary key only” | constraints() -> Option<&Constraints> | confirmed — see D2 |
| §5.4 statistics from the artifact footer, “answered only from cached metadata” | statistics() -> Option<Statistics> plus ScanArgs::statistics_requests() | refined — see D2 |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Leave all five mutation methods unimplemented, and prove it | G1 (authority), DM-02, DM-13, DM-20 | adopt, as a governance test. TableProvider in 55.1.0 carries insert_into, delete_from, update, truncate and merge_into as defaulted methods. The blueprint’s artifacts are immutable and content-addressed (§20.1) and authored facts have exactly one authority (D1, D13). A provider that implemented any of these would create a second, mutable authority over the same facts, reachable through SQL — the precise condition G1 rejects. The defaults make this safe by omission, which is fragile: a future contributor “completing the trait” would breach the architecture with no test failing. Add tests/governance/tests/no_mutating_providers.rs. The blueprint does not mention this surface at all. |
SchemaProvider::table() is async | DM-28 (declare ambient inputs), DM-26 | note and constrain. An async table() may do I/O per lookup. §5.4 wants a pinned snapshot with cheap, stable schemas; the provider should resolve the manifest once at session creation and serve table() from memory, so the async signature is satisfied without actually performing I/O. State it, or planning latency becomes a function of catalog size. |
get_column_default(&str) -> Option<&Expr> | DM-16 (structure and policy as declarations) | reject. Column defaults are an authoring concept the blueprint deliberately does not have — values come from authored relations and case overlays (§6.10), not from provider-supplied defaults. Named so it stays unimplemented. |
get_logical_plan() / TableType::View | DM-18 (preserve high-level structure until expansion) | evaluate. A relation derived by a rule (§14.2) could in principle be exposed as a view whose LogicalPlan DataFusion inlines and optimises with its consumer, rather than as a materialised artifact. That is genuinely aligned with DM-18. But it conflicts with §5.3 content-addressing — a view has no content hash — so it is only viable for runtime analytics (§19.2), never for compiled artifacts. Worth a bounded look; not a change to the artifact model. |
AsyncSchemaProvider / AsyncCatalogProviderList | DM-26 | reject for now — these exist for catalogs that must resolve remotely during planning. The snapshot catalog resolves once, up front. Recording the rejection prevents a future “we should be async” drift. |
Gaps and risks
CatalogProviderList::register_catalogandSchemaProvider::register_tableare mutating methods on the catalog itself. Session setup uses them; nothing after setup should. Since a snapshot session is supposed to be immutable for its lifetime (§5.4), the platform’s implementations should reject registration after aseal()point rather than relying on nobody calling them (DM-29, G5).MergeIntoClauseandInsertOpin the trait signature are evidence that DataFusion is growing a full DML surface release over release. This is a standing upgrade hazard for this design: each upgrade may add more defaulted mutation methods. The governance test above should assert the set of implemented methods, not just the absence of today’s five.
3. Pushdown truthfulness, constraints, and statistics (D2)
The cluster where §5.4 makes its sharpest claim — “Truthfulness over ambition: a wrongly advertised exact filter is a correctness bug” — and where G7 (truthful capability claims) and G6 (transformation and reuse) are decided.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Pushdown verdict | TableProviderFilterPushDown = Unsupported | Inexact | Exact, returned per filter from supports_filters_pushdown(&[&Expr]) -> Result<Vec<_>> | [rustdoc:datafusion-expr@55.1.0] |
| Constraint kinds | Constraint = PrimaryKey | Unique; Constraints::new_unverified(Vec<Constraint>), extend, project(&[usize]) -> Option<Self> | [rustdoc:datafusion-common@55.1.0] |
| Statistics container | Statistics with new_unknown(&Schema), with_num_rows(Precision<usize>), with_total_byte_size(Precision<usize>), add_column_statistics(ColumnStatistics), calculate_total_byte_size(&Schema) | [rustdoc:datafusion-common@55.1.0] |
| Per-column statistics | ColumnStatistics { null_count, max_value, min_value, sum_value, distinct_count, byte_size } | [rustdoc:datafusion-common@55.1.0] |
| Precision wrapper | Precision<T> = Exact | Inexact | Absent | [rustdoc:datafusion-common@55.1.0] |
| Demand-driven statistics | StatisticsRequest = Min | Max | NullCount | DistinctCount | Sum | ByteSize | RowCount | TotalByteSize, delivered through ScanArgs::with_statistics_requests | [rustdoc:datafusion-expr-common@55.1.0], [rustdoc:datafusion-session@55.1.0] |
| Pruning | the datafusion-pruning crate (186 indexed items) | [rustdoc:datafusion-pruning@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§5.4 “filters on key columns and enum columns are handled Exact, everything else Inexact or unsupported” | the three-variant enum, one verdict per filter | confirmed exactly — DataFusion’s model is per-filter, so the provider can be precise rather than blanket |
| §5.4 the provider declares what it can do rather than being asked to guess | supports_filters_pushdown, constraints(), statistics(), table_type() | confirmed — this is DM-19 (“select behavior through declared capabilities and explicit bindings”) implemented by the engine: every optimisation DataFusion performs over a provider is gated on a capability the provider declared, never inferred. The design’s job is to make those declarations true. |
| §5.4 “a wrongly advertised exact filter is a correctness bug” | — | confirmed, and it is worth naming the mechanism. Exact tells the optimizer it may delete the filter from the plan. If the scan then returns a row the filter would have rejected, that row reaches the consumer with no second check. There is no runtime verification anywhere in DataFusion — the claim is taken on trust. This is the clearest G7 surface in the engine. |
| §5.4 “Constraints: primary key only” | Constraint::PrimaryKey | confirmed, and note the constructor is named new_unverified [rustdoc:datafusion-common@55.1.0] — DataFusion itself flags that it does not check. §5.4’s “declared only where the provider can guarantee them” is the right discipline and the API name is a standing reminder. |
| §5.4 “Statistics: row counts and min/max for key columns from the artifact footer; answered only from cached metadata” | Statistics + ColumnStatistics + Precision | refined — see below |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Use Precision::{Exact, Inexact, Absent} deliberately, per statistic | DM-59 (make design claims falsifiable and label uncertainty), DM-40, G7 | adopt, and write it into §5.4. The blueprint says statistics come “from the artifact footer” but never says whether they are exact. Precision forces the answer, per value: a row count read from a manifest is Exact; a min/max over a filtered artifact is Inexact; anything not stored is Absent. Returning Inexact where the truth is unknown is free and safe; returning Exact wrongly lets the optimizer eliminate work on a false premise. DataFusion built the uncertainty label into the type — the design should use it rather than default everything to one setting. |
Answer StatisticsRequests selectively | DM-26 (separate preparation from repeated execution), DM-58 | adopt. ScanArgs::statistics_requests() tells the provider exactly which statistics the planner will use. A provider can answer those from cached metadata and return Absent for the rest — which is precisely §5.4’s “answered only from cached metadata”, now expressible as a contract instead of a convention. Sum and DistinctCount in particular should be Absent for snapshot artifacts unless the manifest genuinely carries them. Unmentioned in the blueprint. |
Constraints::project(&[usize]) | DM-22, DM-24 | adopt — when a scan is projected, the primary-key constraint must be projected with it or dropped. Doing this by hand is an easy place to assert a key over a column set that no longer contains it. |
Declare Constraint::Unique where a relation genuinely has one | DM-09 (model relationships and valid domains explicitly), DM-10 | evaluate. §5.4 restricts to primary keys “until proven”, which is the right default. But §4.2 generates a unique invariant kind and P2 validates it — so for relations whose unique invariant is enforced at write time, the guarantee is real and declaring it would let the optimizer eliminate redundant aggregations over runtime analytics (§19.2). The gate is G7: declare it only where a validator actually runs. |
Pushdown of pse.* extension-typed filters | G7, DM-43 | measured — the spike is done, and the answer is favourable. See below. |
Measured (PROBE A) — the §5.4 pushdown spike
The previous edition listed this as must-settle item 1 and blueprint §26 F17 still says “the pushdown spike precedes any Exact claim”. It has now been run: a recording TableProvider over a FixedSizeBinary(16) key column, capturing every expression offered to supports_filters_pushdown [probe].
PROBE A filter shapes offered to a provider
q: WHERE id = arrow_cast(X'…03','FixedSizeBinary(16)')
offered: ["rel.id = FixedSizeBinary(16, "0,0,…,3")"] reached scan(): 1
q: WHERE id IN (arrow_cast(X'…01',…), arrow_cast(X'…02',…))
offered: ["rel.id = FixedSizeBinary(16, "…,1") OR rel.id = FixedSizeBinary(16, "…,2")"]
reached scan(): 1
q: WHERE kind = 'a'
offered: ["rel.kind = Utf8("a")"] reached scan(): 1
q: WHERE kind = 'a' AND v > 2.0
offered: ["rel.kind = Utf8("a")", "rel.v > Float64(2)"] reached scan(): 2
q: WHERE id IS NOT NULL
offered: [] reached scan(): 0
Five facts, and they change §5.4’s status from unverifiable to specifiable:
- Equality on a
FixedSizeBinary(16)key arrives as a plain binary expression with a literal on one side —rel.id = FixedSizeBinary(16, …). That is a shape a provider can match structurally.Exactis achievable on key equality, which is what §5.4 promises. The blocking item is resolved in the design’s favour. - The measured two-element
INbecame anOR. This is not a universal shape guarantee: support only qualified expression forms, handle supportedInListforms explicitly, and return Unsupported for the rest. Blueprint §5.4 now specifies that conservative contract. - Conjunctions are split.
A AND Barrives as two independent filters, so a provider can accept oneExactand decline the other. §5.4’s per-column policy is expressible exactly as written. IS NOT NULLon a non-nullable column is eliminated before the provider sees it — zero filters offered. The optimizer used the schema’s nullability declaration. This is a small, pleasing confirmation thatFieldnullability is load-bearing, and a reminder that it is the one constraint DataFusion actually enforces (see Gaps).supports_filters_pushdownis called more than once per plan — each filter appeared twice. The method must therefore be pure and idempotent; a provider that logged, counted, or mutated state there would double-count. Not documented in the signature.
Gaps and risks
- Exact requires completeness as well as soundness. Reapplying filters to returned rows cannot detect missing matches. Compare full values and multiplicities with a pushdown-disabled provider and include a deliberately empty/over-pruning negative control (review R4-11; blueprint §24.1).
supports_filters_pushdownmust be pure (PROBE A, fact 5). Worth a comment in the generated provider, since the obvious place to put instrumentation is exactly there.- DataFusion enforces no relational constraint except
Fieldnullability — and does not check that on ingest. Upstream is explicit: primary, unique and foreign keys and check constraints “are provided for informational purposes”, are “not validated or used during query planning”, and returning nulls for a non-nullable column “will result in runtime errors during execution”[c7:/apache/datafusion]. §5.4 declares “Constraints: primary key only”, which is sound as a declaration — but it must not be read as enforcement. The platform’s P2 invariant validation is the only thing that makes the claim true (G7, DM-43). Statistics::new_unknown(&Schema)is the honest default and should be the starting point for every provider, with statistics added only where the manifest supplies them — rather than constructingStatisticsoptimistically and hoping.ColumnStatisticshas no notion of which rows a statistic covers. For a snapshot artifact this is fine (it covers the whole artifact); it becomes wrong the moment a provider serves a subset. Another reason §5.4’s “the provider never prunes rows of a coupled mathematical problem” matters beyond its stated rationale.
4. Logical plans, expressions, and the rule compiler (D3)
Role: §14.2 — “a typed RulePlanSpec (a small algebra: scan, filter, project, equi-join, anti-join for stratified negation, union, distinct) compiled to a DataFusion LogicalPlan through LogicalPlanBuilder, optimized, and executed against the snapshot session”; D10’s first role; D6’s insistence that the math IR is not Expr.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Plan nodes (25) | Projection, Filter, Window, Aggregate, Sort, Join, Repartition, Union, TableScan, EmptyRelation, Subquery, SubqueryAlias, Limit, Statement, Values, Explain, Analyze, Extension, Distinct, Dml, Ddl, Copy, DescribeTable, Unnest, RecursiveQuery | [rustdoc:datafusion-expr@55.1.0] |
| Expressions (37) | Column, Literal, BinaryExpr, Not, Negative, Between, Case, Cast, TryCast, ScalarFunction, AggregateFunction, WindowFunction, InList, Exists, InSubquery, SetComparison, ScalarSubquery, GroupingSet, Placeholder, Unnest, Alias, Wildcard, ScalarVariable, OuterReferenceColumn, Like, SimilarTo, IsNull/IsNotNull/IsTrue/IsFalse/IsUnknown/IsNotTrue/IsNotFalse/IsNotUnknown, HigherOrderFunction, Lambda, LambdaVariable | [rustdoc:datafusion-expr@55.1.0] |
| Builder | LogicalPlanBuilder::{scan, scan_with_filters, scan_with_filters_fetch, values, values_with_schema, project, project_with_validation, filter, limit, limit_by_expr, sort, sort_by, sort_with_limit, union, union_by_name, to_recursive_query(name, recursive_term, is_distinct), with_options} and the join/distinct/aggregate family | [rustdoc:datafusion-expr@55.1.0] |
| Extension nodes | UserDefinedLogicalNode — name, inputs, schema, expressions, with_exprs_and_inputs, check_invariants(InvariantLevel), prevent_predicate_push_down_columns(), necessary_children_exprs, supports_limit_pushdown, fmt_for_explain, dyn_hash/dyn_eq/dyn_ord | [rustdoc:datafusion-expr@55.1.0] |
| Invariant levels | InvariantLevel = Always | Executable | [rustdoc:datafusion-expr@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§14.2 RulePlanSpec algebra → LogicalPlan via LogicalPlanBuilder | every primitive in the algebra has a builder method | confirmed — scan, filter, project, join (incl. anti-join via JoinType), union, distinct all present |
| §14.2 rule 1 “DataFusion’s recursive CTE support may be used for closures whose provenance can be reconstructed from the result” | LogicalPlan::RecursiveQuery + LogicalPlanBuilder::to_recursive_query(name, recursive_term, is_distinct); CteWorkTable provides the working table | confirmed — and the is_distinct flag is the fixed-point dedup §14.2 describes |
| §14.2 rule 5 “the plan fingerprint is part of the pass record” | no plan-hash API on LogicalPlan itself; UserDefinedLogicalNode::dyn_hash exists for extension nodes | superseded. Revision 2 chose datafusion-proto, which the previous edition did not consider. Measured and adjudicated in D9 — the mechanism works but is not reproducible as specified. |
D6 “the math IR is richer than DataFusion Expr” | 37 Expr variants, none of which carry physical type, unit, or domain | confirmed — Expr has no place to put a quantity type, so D6’s separate IR is necessary, not merely preferred |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
LogicalPlan::Extension + UserDefinedLogicalNode for rule bodies | DM-18 (preserve high-level structure until expansion is required), DM-21 (explicit IRs and progressive lowering), DM-49 | upgraded from evaluate to adopt-candidate — the prerequisite is now measured. §14.2 currently lowers a RulePlanSpec straight to relational primitives, so by the time a plan is optimised or explained, the rule has disappeared into a pile of joins. An extension node keeps rule_id visible through planning and into EXPLAIN, serving §14.2 rule 4 (“every head row carries a derivation”) from the plan rather than only from the result. The previous edition could not recommend it because whether such a node survives the optimizer was unknown. PROBE B settles it — it does. The remaining cost is the 14-method contract and an ExtensionPlanner for physical execution; both are known quantities. |
UserDefinedLogicalNode::check_invariants(InvariantLevel) | DM-07, G3, DM-53 | adopt if extension nodes are adopted. DataFusion will call this to verify a node’s invariants — Always (structural) and Executable (before execution). That is a free, engine-driven hook for the kind of checking §4.2’s invariants describe, at plan level rather than data level. |
prevent_predicate_push_down_columns() | G6 (a rewrite must not change required behavior), DM-24 | adopt if extension nodes are adopted. This is how a node tells the optimizer not to push a predicate below it. §5.4 already worries about the provider’s pushdown truthfulness; this is the same concern one level up, and the only declarative way to stop a semantically invalid pushdown. |
Expr::IsUnknown / IsNotUnknown | DM-08 | evaluate. §14.2 rule 3 defines four-valued predicates (true | false | unknown | conflict) and writes unknown/conflict rows with a status column. DataFusion’s IsUnknown covers the SQL three-valued case natively. conflict has no engine equivalent and stays platform-level — so the mapping is partial, which is worth stating so nobody assumes the engine models all four. |
project_with_validation | DM-22, G3 | adopt over plain project where the rule compiler builds projections from a spec — it validates the projection against the input schema rather than trusting the caller. |
Expr::Placeholder | DM-26 (separate preparation from repeated execution) | evaluate for §19.3 sweeps: one prepared plan with placeholders, executed per sample, instead of rebuilding a plan per case. Only worthwhile if sweeps prove plan-construction-bound. |
HigherOrderFunction / Lambda / LambdaVariable | — | reject. New in this line and appealing for expressing per-element logic, but D6 is explicit that the math IR is not Expr; pushing model semantics into lambdas inside plans would blur exactly the boundary D6 draws. |
Measured (PROBE B) — does an extension node survive the optimizer?
A UserDefinedLogicalNodeCore carrying a rule_id, wrapped around an ordinary filter-and-project plan, put through SessionState::optimize [probe]:
PROBE B UserDefinedLogicalNode survival
before optimize: after optimize:
PseRule: rule_id=rule-42 PseRule: rule_id=rule-42
Projection: rel.v Filter: rel.v > Float64(1)
Filter: rel.v > Float64(1) TableScan: rel projection=[v]
TableScan: rel
node survived : true
rule_id still visible: true
physical planning : FAILS without an ExtensionPlanner
-> "No installed planner was able to convert the
custom node to an execution plan: RuleNode"
Three facts. The node survives intact and rule_id is still readable in the rendered plan, so the DM-18 opportunity is real rather than hypothetical. The optimizer still optimises through it — the projection was pushed down into the TableScan beneath the extension node, so wrapping a rule body does not cost the rewrites. And physical execution requires registering a datafusion_session::ExtensionPlanner; the failure is a clean typed planning error, not a silent fallback.
Measured (PROBE D) — float and null semantics through the engine
§14.2 rule 3 and §5.3 both depend on how the engine treats null, NaN and -0.0. The Arrow map measured the kernel behaviour; this measures the engine behaviour over the same five values [0.0, -0.0, NaN, -1.0, null] [probe]:
PROBE D
ORDER BY x : ["-1", "-0", "0", "NaN", "null"]
GROUP BY x (distinct) : ["-1", "0", "NaN", "null"] <- 4 groups from 5 rows
self equi-join rows : 6
Sorting and grouping disagree about whether -0.0 and +0.0 are the same value. ORDER BY reproduces Arrow’s IEEE 754 totalOrder exactly — -0.0 strictly before +0.0, NaN after all finite values — which confirms the Arrow map’s PROBE 3 survives into the engine. But GROUP BY returns four groups from five rows: -0.0 and +0.0 are merged, and the surviving representative is 0, not -0. The self equi-join returns six rows, which decomposes as four from the {0.0, -0.0} pair matching each other, one from -1.0, and one from NaN matching itself — so the hash join also treats NaN as equal to NaN, which is not IEEE comparison semantics.
None of this is a DataFusion defect; grouping and joining use value-equality semantics by design. It matters because §5.3 states that “-0.0 is preserved (it is distinguishable and can matter to solvers)” and §26 repeats it. That is true of Arrow ordering and of the content hash — and false of any aggregation or join keyed on a Float64 column. A GROUP BY over a float key silently merges the two, and which sign survives is an engine choice. The design should either declare that float columns are never grouping or join keys, or state the collapse explicitly as a selected loss (DM-42, G2, DM-08).
Gaps and risks
- Plan fingerprinting is not a DataFusion capability, and revision 2’s choice of
datafusion-protodoes not change that — it relocates the problem rather than solving it.LogicalPlanoffers no stable hash; proto offers an encoding whose stability depends on inputs the platform controls. See D9, which is where this risk now lives (DM-48, DM-51). - A
Float64column used as a grouping or join key loses the-0.0distinction (PROBE D). Not a defect, but it contradicts a claim §5.3 and §26 both make without qualification. LogicalPlancarriesDml,Ddl,CopyandStatementvariants. Nothing in the blueprint should construct them, for the same authority reason as D1’s mutation methods — the rule compiler builds read-only plans only.
5. Optimizer, analyzer, and plan explainability (D4)
Role: §14.2 — plans are “optimized” before execution, and rule 5 requires determinism: “the DataFusion session is pinned (config snapshot, function registry hash, catalog snapshot) and the plan fingerprint is part of the pass record.”
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
AnalyzerRule | analyze(LogicalPlan, &ConfigOptions) -> Result<LogicalPlan>, name() — 3 built-in impls | [rustdoc:datafusion-optimizer@55.1.0] |
OptimizerRule | name(), apply_order() -> Option<ApplyOrder>, supports_rewrite() -> bool, rewrite(LogicalPlan, &dyn OptimizerConfig) -> Result<Transformed<LogicalPlan>> — 25 built-in impls | [rustdoc:datafusion-optimizer@55.1.0] |
| Traversal order | ApplyOrder = TopDown | BottomUp | [rustdoc:datafusion-optimizer@55.1.0] |
| Rewrite signalling | Transformed<T> — carries whether a rewrite fired, so the driver knows when a fixed point is reached | [rustdoc:datafusion-optimizer@55.1.0] |
| Explainability | LogicalPlan::{Explain, Analyze}; UserDefinedLogicalNode::fmt_for_explain | [rustdoc:datafusion-expr@55.1.0] |
| Explicit rule installation | Optimizer::with_rules(Vec<Arc<dyn OptimizerRule + Send + Sync>>), Analyzer::with_rules(Vec<Arc<dyn AnalyzerRule + Send + Sync>>); Optimizer::new() / Analyzer::new() take the defaults | [rustdoc:datafusion-optimizer@55.1.0] |
| Rule-firing observation | Optimizer::optimize(plan, config, observer) and Analyzer::execute_and_check(plan, config, observer) — both take an observer closure called per rule | [rustdoc:datafusion-optimizer@55.1.0] |
| Function rewrites | Analyzer::{add_function_rewrite, function_rewrites} over FunctionRewrite | [rustdoc:datafusion-optimizer@55.1.0] |
| Physical side | datafusion-physical-optimizer (840 items) including EnsureRequirements | [rustdoc:datafusion-physical-optimizer@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §14.2 “compiled … optimized, and executed” | the analyzer → optimizer → physical planner pipeline | confirmed |
§3.1 “EnsureRequirements … are the 55 forms to code against” | EnsureRequirements present in datafusion-physical-optimizer | confirmed at 55.1.0 |
§14.2 rule 5 “installed explicitly with Optimizer::with_rules rather than taken from the engine defaults” | Optimizer::with_rules(Vec<Arc<dyn OptimizerRule + Send + Sync>>); Analyzer::with_rules is its analyzer-side twin | confirmed, and the blueprint names only half of it. Optimizer::with_rules pins the 25 optimizer rules; the 3 AnalyzerRules are installed separately and are equally capable of changing a plan. §14.2 should name both. There is also a third surface — Analyzer::add_function_rewrite — which mutates an analyzer after construction and would escape any list captured at construction time; platform code should not use it. |
| §14.2 rule 5 pinned session (config snapshot, function registry hash, catalog snapshot) | ConfigOptions is threaded into every AnalyzerRule::analyze and OptimizerRule::rewrite | confirmed as necessary. Because config reaches every rule, an unpinned config means an unpinned plan — and therefore an unpinned result. §14.2 rule 5’s pinning is not belt-and-braces; it is what makes optimisation deterministic. |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Record the rule set and its versions in the pass record, not just the config | DM-31 (expose every dependency that can affect meaning or output), G6, DM-48 | adopt. §14.2 rule 5 pins config, function registry and catalog — but not the optimizer rule set. There are 25 built-in OptimizerRules plus 3 AnalyzerRules at 55.1.0, and that set changes between DataFusion releases. Two runs with identical inputs, identical config and different DataFusion patch versions can produce different plans and therefore different execution. Since the rules expose name(), capturing the ordered list of rule names alongside the DataFusion version is cheap and closes the gap. |
| Disable or pin optimizer rules that can change numerical semantics | G6 (“a rewrite … changes required behavior without a valid contract”), DM-24, DM-40 | evaluate, with a bias to caution. Relational rewrites (projection pushdown, join reordering) are semantics-preserving over sets. Expression-level rewrites are where risk lives: constant folding and simplification over Float64 can reassociate arithmetic and change the last bits of a result. §14.2’s rule bodies are set-oriented, so exposure is limited — but §18.5’s kernel UDFs are invoked inside plans, and simplify is a UDF method the design already plans to implement. The rule: any rewrite that touches floating-point arithmetic must be justified or disabled, and §5.3’s hashes make a violation loudly visible. |
Transformed<T> in the platform’s own rewrites | DM-22 | adopt if the platform ever writes an OptimizerRule; it makes “did this rule fire” explicit rather than inferred. |
The observer closure on Optimizer::optimize / Analyzer::execute_and_check | DM-46 (preserve source-to-result lineage), DM-50, DM-49 | adopt. Both entry points take a per-rule observer. That turns “the rule set was X” — which revision 2’s F9 already records — into “these rules actually fired, in this order, on this plan”. The first is a declaration of what could have happened; the second is evidence of what did, and it is what makes a plan difference explainable at the level of meaning rather than by diffing two renderings. Effectively free: the seam exists and the platform already drives optimisation explicitly. New. |
EXPLAIN output captured into the pass record — and pgjson, not indent | DM-49, DM-27 (represent workflows as inspectable plans), DM-47 | adopt, with a caveat and a correction. Storing the rendered plan alongside a pass run is excellent for explainability. The caveat is D3’s: the rendering is not a stable contract, so store it as evidence for humans, never as the fingerprint. The correction is that the previous edition treated EXPLAIN as a single text surface; it is four, and one of them is machine-readable — see the measurement below. |
Measured (PROBE X2) — EXPLAIN is four surfaces, not one
PROBE X2 EXPLAIN formats accepted by 55.1.0
indent -> OK "TableScan: rel projection=[v]"
tree -> OK box-drawing render of the physical plan
pgjson -> OK structured JSON
graphviz -> OK "// Begin DataFusion GraphViz Plan"
json -> rejected: "Expected 'indent', 'tree', 'pgjson' or 'graphviz'"
pgjson turns the plan from a string a human reads into a structure a pass can store, diff and query — which is what DM-27 actually asks for. §23.1’s observability and §19’s analytics both become considerably better if a pass record carries a parsed plan rather than a blob of text. And datafusion.explain.{analyze_level, analyze_categories, show_statistics, show_schema} select what it contains, so the verbosity is declared policy rather than a fixed cost (§12 E3).
Gaps and risks
- The optimizer is an ambient dependency of every derived relation. This is the cluster’s real finding. §2 D14 says “incrementality follows declared dependencies” and §14.4 keys memoization on input content hashes plus pass version. The DataFusion version and its rule set are inputs that affect output and are not in that key. A DataFusion upgrade should therefore invalidate compiled artifacts — which argues for including the engine version in the pass key (DM-31, DM-32).
- No physical-optimizer trait was exposed in the extraction’s
datafusion-physical-optimizerindex (the structEnsureRequirementsis present, but the rule trait did not surface), so the physical rule contract should be read directly before any custom physical rule is written. Open item.
6. Physical planning, streams, and partitioning (D5)
Role: §14.3 session and parallelism; §18.8 thread budget; D10’s boundary — DataFusion “does not run inside Newton iterations, and a solve is never a scalar function call”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Execution contract | ExecutionPlan trait; ExecutionPlanProperties; PlanProperties | [rustdoc:datafusion-physical-plan@55.1.0] |
| Output shape declarations | Partitioning, EmissionType, Boundedness | [rustdoc:datafusion-physical-plan@55.1.0], [c7:/apache/datafusion] |
| Streaming | SendableRecordBatchStream (type alias), RecordBatchStream trait | [rustdoc:datafusion-execution@55.1.0] |
| Planning context | PhysicalPlanningContext | [rustdoc:datafusion-expr@55.1.0] |
| Requirement enforcement | EnsureRequirements | [rustdoc:datafusion-physical-optimizer@55.1.0] |
| Aggregate state conversion | convert_to_state | [rustdoc:datafusion-expr-common@55.1.0], [rustdoc:datafusion-functions-aggregate@55.1.0] |
§3.1’s “55 forms” claim is confirmed in full at 55.1.0: scan_with_args, ScanArgs, PhysicalPlanningContext, EnsureRequirements, is_strict and convert_to_state all exist, in the crates named above.
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §14.3 “DataFusion partitions relational work internally” | Partitioning on PlanProperties; EnsureRequirements inserts repartitions to satisfy a plan’s requirements | confirmed |
§5.4 provider returns an ExecutionPlan from scan_with_args | Arc<dyn ExecutionPlan> | confirmed |
§14.2 execution to RecordBatch | SendableRecordBatchStream | confirmed — execution is a stream, so a pass consuming a rule result must collect it, which is where §5.3’s concatenation step belongs |
| D10 “does not run inside Newton iterations” | — | confirmed as a sound boundary. A DataFusion plan is async, streaming and partition-parallel, with a memory pool and spill machinery behind it (D7). Putting that inside a Newton iteration would make each residual evaluation a scheduled, allocating, potentially spilling operation. The blueprint’s boundary is not conservatism; it is the only workable choice. |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Declare EmissionType and Boundedness honestly on any custom ExecutionPlan | G7, DM-43, DM-30 | adopt. A snapshot-artifact scan is Boundedness::Bounded and (per the upstream example) EmissionType::Incremental. These are capability claims the engine plans against: declaring Bounded for something unbounded causes an operator to wait forever for an end that never comes. |
| Partitioning as a declared property, not an accident | DM-35 (concurrency respects dependencies and declared ordering), DM-40 | adopt. §5.3 requires canonical ordering, and a partitioned scan emits batches in nondeterministic order across partitions. That is fine — ordering is imposed later by the sort in §5.3 step 1 — but it must be stated, because a pass that reads a rule result and assumes arrival order is deterministic will be intermittently wrong. |
Single-partition providers for compiled relations | DM-36, DM-58 | evaluate. §5.4 says the provider “never prunes rows of a coupled mathematical problem”. A related discipline: relations consumed as a whole by a native pass gain nothing from partitioning and lose ordering determinism. Declaring Partitioning::UnknownPartitioning(1) for those is simpler and cheaper. Measure before generalising (DM-58). |
convert_to_state | DM-25 | Note only: the revised algebra uses built-in aggregation, but declares no custom UDAF requiring this state-conversion route. Do not infer a consumer merely from the aggregate family (ADR-0048). |
Gaps and risks
- Thread budget crosses three owners. §18.8 gives DataFusion the tokio runtime and rayon the batch kernels. DataFusion’s partition parallelism is driven by
target_partitionsinSessionConfig(D7) — so the budget is set in config, not by the pool. Getting this wrong oversubscribes silently, which §18.8 says must be a configuration error rather than a runtime surprise. The configuration must therefore be validated at session construction. - The
ExecutionPlantrait’s full method set was not enumerated here (the physical-plan crate indexes 6008 items); a custom physical operator is out of scope for the blueprint (D11 keeps numerics native), so only the provider’s returned plan matters. If a custom operator is ever written, re-derive this cluster.
7. Scalar UDFs and the generated kernel adapters (D6)
Role: §18.5 — the ScalarUDFImpl row of the kernel-adapter table; §14.2 — generated wrappers registered in the session so rule bodies can evaluate kernels in batch; D10’s third DataFusion role.
Capability inventory — ScalarUDFImpl has 25 methods in 55.1.0
| Group | Methods | Provenance |
|---|---|---|
| Identity | name(), aliases(), display_name(&[Expr]), schema_name(&[Expr]), documentation() | [rustdoc:datafusion-expr@55.1.0] |
| Typing | signature() -> &Signature, return_type(&[DataType]), return_field_from_args(ReturnFieldArgs) -> Result<FieldRef>, is_nullable(&[Expr], &dyn …), coerce_types(&[DataType]) | [rustdoc:datafusion-expr@55.1.0] |
| Evaluation | invoke_with_args(ScalarFunctionArgs) -> Result<ColumnarValue>, is_strict() | [rustdoc:datafusion-expr@55.1.0] |
| Rewriting | simplify(Vec<Expr>, &SimplifyContext) -> Result<ExprSimplifyResult> (Simplified | Original), preimage(&[Expr], &Expr, &SimplifyContext) -> Result<PreimageResult> (None | Range) | [rustdoc:datafusion-expr@55.1.0] |
| Conditional evaluation | short_circuits(), conditional_arguments(&[Expr]) -> Option<(Vec<&Expr>, Vec<&Expr>)> | [rustdoc:datafusion-expr@55.1.0] |
| Range analysis | evaluate_bounds(&[&Interval]) -> Result<Interval>, propagate_constraints(&Interval, &[&Interval]) -> Result<Option<Vec<Interval>>> | [rustdoc:datafusion-expr@55.1.0] |
| Ordering | output_ordering(&[ExprProperties]) -> Result<SortProperties>, preserves_lex_ordering, strictly_order_preserving | [rustdoc:datafusion-expr@55.1.0] |
| Misc | with_updated_config(&ConfigOptions), struct_field_mapping, placement(&[ExpressionPlacement]) | [rustdoc:datafusion-expr@55.1.0] |
Argument carriers: ReturnFieldArgs { arg_fields, scalar_arguments } and ScalarFunctionArgs { args, arg_fields, number_rows, return_field, config_options } [rustdoc:datafusion-expr@55.1.0].
What the blueprint binds to
| §18.5 requirement | API | Status |
|---|---|---|
name, signature (exact Arrow types, dictionary preservation off) | name(), signature(), and coerce_types() to refuse implicit coercion | confirmed |
return_field_from_args “attaches pse.semantic.* metadata and the output quantity type” | ReturnFieldArgs { arg_fields, … } -> Result<FieldRef> | confirmed, and better than the blueprint claims. The argument carrier supplies the input Fields, not just DataTypes — so the adapter can read each argument’s pse.semantic.quantity_type from its field metadata and derive the output quantity type, rather than hard-coding it. That is unit inference (§8.3) enforced at the plan boundary, and it is only possible because the API passes fields. |
is_strict = true when every input is required | is_strict() | confirmed |
volatility = Immutable | via Signature | confirmed |
§18.5 kernels selected through KernelSpec bindings rather than discovered | signature(), is_strict(), volatility, coerce_types() — the declared capability surface the planner binds against | confirmed — DM-19 again, one level down: a UDF is chosen and optimised entirely from its declarations. A declaration that overstates the kernel (e.g. Immutable for something config-dependent) is a G7 failure, not merely a performance bug. |
simplify for all-constant inputs | simplify(...) -> ExprSimplifyResult::{Simplified, Original} | confirmed |
evaluate_bounds / propagate_constraints “derived from monotonicity and validity where declared” | both present | confirmed |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
conditional_arguments() + short_circuits() | G4, DM-28, DM-08 | Optimizer declarations only. Generic UDF evaluation is eager even with both flags [review:E2]. Use built-in Expr::Case/CaseExpr for actual guarded execution; emit truthful flags only for a supported implementation. |
preimage() | DM-24, DM-43, G6 | Evaluate under R-25. A supported replacement must denote the equivalent input set with correct half-open bounds, nulls and floating behavior. Monotonicity alone is not proof; conservative ranges may only prune while retaining a residual predicate (review L8). |
output_ordering / preserves_lex_ordering / strictly_order_preserving | DM-24 (preserve semantics across rewrites), DM-26, DM-40 | adopt for monotone kernels. §5.3 depends on canonical ordering and §15.3’s block triangularisation depends on deterministic order. Declaring that a strictly increasing kernel preserves ordering lets DataFusion keep a sort through the call instead of re-sorting — and, more importantly, makes the claim explicit and testable rather than implicit. The blueprint already tracks monotonicity for evaluate_bounds; the same declaration feeds these three. |
is_nullable(&[Expr], schema) | DM-08, G2 | adopt. §18.5 says the batch adapter produces “nulls plus a diagnostic column when requested” for validity violations. If the UDF declares itself non-nullable while the batch path can emit nulls, the plan’s schema lies — a G2 failure. The declaration must be derived from the same KernelSpec strictness/validity data that drives the batch adapter, not written independently. |
with_updated_config(&ConfigOptions) and ScalarFunctionArgs.config_options | DM-28 (declare ambient inputs), G4 | reject, and assert it. A kernel whose result depends on session config is nondeterministic with respect to everything §5.3 hashes. §14.2 rule 5 already pins the session config and includes a function-registry hash in the pass record — good — but the generated adapters should simply never read config_options, and a governance check should say so. |
coerce_types() returning the identity | DM-42, G2 | adopt explicitly. §18.5 wants exact Arrow types. Implementing coerce_types as “accept exactly the declared types, else error” prevents DataFusion from silently widening an Int64 argument into a Float64 kernel and changing the numerics. Relying on the default is a silent-coercion risk. |
Gaps and risks
- The trait is large and growing — 25 methods in 55.1.0, several (
preimage,conditional_arguments,placement,struct_field_mapping) clearly newer than the blueprint’s description. BecauseKernelSpecgenerates the adapters (§18.5), the cost of adopting a new method is one generator change rather than N hand edits — which is DM-52 working as intended, and a good argument for keeping the generation path even though the trait is a moving target. invoke_with_argsreceivesnumber_rows, which matters for the all-scalar case (no arrays to infer length from). A generated adapter that infers length from its first array argument breaks on all-scalar invocations.Interval’s public surface did not resolve to a field list in the extraction[rustdoc:datafusion-expr-common@55.1.0]; the exact bound representation forevaluate_boundsshould be read from the crate before the range-analysis adapters are written. Open item.
8. Session, runtime, memory, and ambient inputs (D7)
Role: §14.3 — “one DataFusion SessionContext per snapshot with the catalog of §5.4, MemoryPool limits, and the generated UDF registry”; §18.8 thread budget; §20.4 reproduction.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Session and task | SessionConfig, RuntimeEnv, RuntimeEnvBuilder, TaskContext, TaskContextProvider | [rustdoc:datafusion-execution@55.1.0] |
| Memory accounting | MemoryPool trait — register, unregister, grow, shrink, try_grow -> Result<()>, reserved() -> usize, memory_limit() -> MemoryLimit; MemoryConsumer, MemoryReservation, MemoryConsumerMetrics | [rustdoc:datafusion-execution@55.1.0] |
| Pool implementations (5) | GreedyMemoryPool, FairSpillPool, UnboundedMemoryPool, TrackConsumersPool, PeakRecordingPool | [rustdoc:datafusion-execution@55.1.0] |
| Spill | DiskManager, DiskManagerBuilder, SpillFile, SpillWriter, FileSpillWriter, SpillingProgress, TempFileFactory, RefCountedTempFile | [rustdoc:datafusion-execution@55.1.0] |
| Injectable clock | TimeProvider trait — now() -> Instant; SystemTimeProvider | [rustdoc:datafusion-execution@55.1.0] |
| Metadata caching | CacheManager, CacheManagerConfig, Cache/CacheKey/CacheValue traits, DefaultCache, FileMetadata, CachedFileMetadata, CachedFileList, CacheEntryInfo, LruQueue | [rustdoc:datafusion-execution@55.1.0] |
| Object store wiring | ObjectStoreRegistry trait, DefaultObjectStoreRegistry, ObjectStoreUrl | [rustdoc:datafusion-execution@55.1.0] |
| Schema fingerprinting | SchemaFingerprint::from_schema(&Schema) | [rustdoc:datafusion-execution@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§14.3 “one SessionContext per snapshot … with MemoryPool limits” | RuntimeEnvBuilder + a chosen MemoryPool | confirmed |
| §14.3 “the generated UDF registry”; §14.2 rule 5 “function registry hash” | function registration on the session | confirmed — and the hash is platform code over the registered names/signatures |
§18.8 “one configuration owns the thread budget: the DataFusion tokio runtime, the rayon pool …” | SessionConfig (target_partitions) and the runtime | confirmed, and see D5’s note that the budget is expressed in config |
§20.1 artifact store on object_store | ObjectStoreRegistry / ObjectStoreUrl | confirmed — DataFusion consumes the same object_store 0.13.2 the artifact store uses, so one store instance can serve both |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Choose and share an accounted MemoryPool | DM-30, DM-35, DM-39 | Adopt shared RuntimeEnv and explicit reservations. FairSpillPool limits fallibly accounted consumers, not arbitrary Arrow/native/solver allocations; grow is infallible. Platform buffers reserve through try_grow and report pool/process peaks separately (ADR-0046). |
TrackConsumersPool / PeakRecordingPool for diagnostics | DM-50 (observe the model lifecycle), DM-39 | evaluate. These wrap a pool to attribute memory to consumers and record peaks. §24.3 benchmarks and §23.1 metrics both want this, and it converts “the pass used too much memory” into “this operator did”. |
TimeProvider — inject a deterministic clock | DM-28 (declare ambient inputs and nondeterminism), DM-48 (reproducibility contract) | adopt for tests and reproduction. Wall-clock time is an ambient input. §20.4’s pse reproduce asserts byte-identical relation hashes for deterministic passes; anything that reaches a timestamp through the engine breaks that. DataFusion already provides the seam — use SystemTimeProvider in production and a fixed provider under test. Unmentioned in the blueprint, and it is exactly the DM-28 mechanism the charter asks for. |
CacheManager for artifact metadata | DM-26 (separate preparation from repeated execution), DM-32 | evaluate. §5.4 requires statistics “answered only from cached metadata”; DataFusion ships a pluggable metadata cache with FileMetadata/CachedFileMetadata. Whether to use it or keep the manifest cache platform-side is a real choice — the engine’s cache is keyed to files, while the platform’s natural key is the content hash. Probably platform-side wins, but it should be a decision. |
SchemaFingerprint::from_schema(&Schema) | DM-15, DM-53 | evaluate, do not adopt blind. §4.2 defines the contract fingerprint as blake3 of the RelationSpec rows — derived from the registry, which is the authority. SchemaFingerprint hashes the Arrow schema instead. These answer different questions and must not be conflated: the registry fingerprint is the contract identity (stable across Arrow versions), while SchemaFingerprint’s stability across DataFusion releases is unknown. Keep §4.2’s definition; this is at most a cheap secondary check. |
Validate target_partitions against the rayon budget at session construction | G5, DM-35 | adopt — §18.8 says oversubscription “is a configuration error, not a runtime surprise”; that sentence only becomes true if something checks it. |
Measured (PROBE H) — what does a bounded pool actually produce?
A 64 KiB GreedyMemoryPool, two partitions, sorting 200,000 rows [probe]:
PROBE H
error: "Not enough memory to continue external sort. Consider increasing the
memory limit config: 'datafusion.runtime.memory_limit', or decreasing
the config: 'datafusion.execution.sort_spill_reservation_bytes'"
The recommendation is confirmed, and more strongly than expected: the failure is not merely typed, it is actionable — the message names the two configuration keys that would resolve it. That is precisely the shape §23.2 wants from a runtime.resources diagnostic, and it arrives for free simply by choosing a bounded pool. The counterfactual is an OOM kill with no message at all.
Gaps and risks
- Spill introduces a filesystem dependency the blueprint does not discuss.
DiskManagerwrites temp files when a pool spills. For a reproducible, content-addressed platform that is benign (spill is invisible in results) but it is a real resource and failure surface: a full temp filesystem becomes an execution failure mid-pass. §23.2’sruntime.infrastructureclass covers it; the disk manager should be configured explicitly rather than defaulted. 55.1.0 makes this pluggable —datafusion_execution::{SpillFile, SpillWriter, TempFileFactory}let the platform own spill placement and lifetime, anddatafusion.execution.{spill_compression, max_spill_file_size_bytes}govern its shape (§12 E2, E3). MemoryLimitas a return type means a pool can report “unbounded” — worth asserting at startup that the configured pool reports a finite limit.- Arrow allocations outside a query are invisible to this pool. §18.6’s result ingestion and §4.2’s builders construct arrays directly; DataFusion’s
MemoryPoolgoverns query operators only. Arrow’s ownMemoryPool(thepoolfeature) covers the remainder — Arrow map §11 register row 17.
9. Errors, diagnostics, and observability (D8)
Role: §23.1 observability and §23.2’s failure taxonomy — “failures carry the relation rows and source spans involved; they are never flattened into ‘run failed’”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Error enum (20 variants) | DataFusionError::{ArrowError, ParquetError, ObjectStore, IoError, SQL, NotImplemented, Internal, Plan, Configuration, SchemaError, Execution, ExecutionJoin, ResourcesExhausted, External, Context, Substrait, **Diagnostic**, **Collection**, Shared, Ffi} | [rustdoc:datafusion-common@55.1.0] |
| Structured diagnostic | Diagnostic::{new_error(message, Option<Span>), new_warning(message, Option<Span>), add_note(message, Option<Span>), add_help(message, Option<Span>), with_note, with_help} | [rustdoc:datafusion-common@55.1.0] |
| Multiple failures | DataFusionError::Collection | [rustdoc:datafusion-common@55.1.0] |
| Error chaining | DataFusionError::Context, External, Shared | [rustdoc:datafusion-common@55.1.0] |
| Memory metrics | MemoryConsumerMetrics, SpillingProgress (D7) | [rustdoc:datafusion-execution@55.1.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §23.2 mapping engine failures into the platform taxonomy | the 20 DataFusionError variants | confirmed as a mapping problem, and the mapping is not 1:1. ResourcesExhausted → runtime.infrastructure; SchemaError/Plan → validation.invariant or internal.invariant depending on origin; Execution → depends entirely on context; Internal → internal.invariant (§23.2’s BurntToast). The blueprint’s §23.2 table has no DataFusion column; it should, or the mapping will be invented per call site. |
| §23.2 “never flattened into ‘run failed’” | Collection carries multiple errors; Context preserves a chain | leverage — the engine supports non-flattening; the platform must not undo it by stringifying |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Add a DataFusion column to §23.2’s failure table | DM-47 (represent diagnostics as structured evidence), G3 | adopt. Twenty engine variants currently map to platform classes by convention at each call site. A declared mapping — ideally a single generated conversion function — makes the taxonomy data (§23.2’s own premise) rather than folklore, and prevents the common failure where every engine error becomes internal.invariant. |
DataFusionError::Collection for multi-error reporting | DM-47, DM-30 | adopt. §4.2’s validators return violating keys — plural — and §14.2’s rule evaluation can produce many diagnostics in one pass. Collapsing those into the first error loses exactly the evidence §23.2 wants preserved. |
Diagnostic with notes and helps | DM-47, DM-49 | evaluate. It is a good structured carrier, but its Span is a SQL source span, and the blueprint’s authoring source spans are pse.source_span over YAML/TOML/DSL text (§4.4). So Diagnostic is useful for engine-originated problems, while platform failures keep their own span type. Do not try to unify them; state the boundary. |
Distinguish ResourcesExhausted from other execution failures in runtime.solver_events/pass records | G5, DM-30 | adopt — it is the one engine error that is expected, recoverable and configuration-driven (D7’s pool choice). Treating it as a generic execution failure hides a tuning problem as a bug. |
DataFusionError::Ffi | DM-41, G3 | note — relevant if the Python boundary ever surfaces engine errors across FFI; another reason not to unwind across that boundary. |
Measured (PROBE X3) — how is an invalid configuration reported?
§14.3 constructs a session from a configuration the platform assembles, and §23.2 requires failures to be typed. Measured [probe]:
PROBE X3 setting datafusion.explain.format = "json" (not a valid value)
SessionConfig::set_str -> PANICS
thread panicked at datafusion-execution-55.1.0/src/config.rs:186
called `Result::unwrap()` on an Err value: Configuration("Invalid explain
format. Expected 'indent', 'tree', 'pgjson' or 'graphviz'. Got 'json'")
ConfigOptions::set -> typed Err: "Error setting config datafusion.explain.format"
The convenient API panics; the correct one returns a typed error. This is the exact shape of the Arrow map’s Field::extension_type() versus try_extension_type() finding, and it deserves the same treatment: a panic during session construction is not a config.invalid row in §23.2’s taxonomy, it is a process abort with a backtrace. Ban SessionConfig::set_str in platform code; assemble configuration through ConfigOptions::set and convert the Err into a typed failure. Governance grep, beside the Field::extension_type() one.
This also matters for §14.2 rule 5, which reads semantic settings back from information_schema.df_settings: the write path is where validation happens, and it is the path that panics.
Gaps and risks
- DataFusion emits no
tracingspans of its own by default. §23.1 wants spans “per rule evaluation”; the engine does not provide them, so rule-level spans are platform code around plan execution, and operator-level visibility requires an external instrumentation layer. This is a real gap between §23.1’s ambition and what the engine supplies out of the box. Two 55.1.0 mechanisms narrow it (§12 E2):datafusion_common_runtime::JoinSetTracerinjects instrumentation into every future and blocking closure the engine spawns, andCustomMetricValuelets platform-defined metrics travel with an operator and appear inEXPLAIN ANALYZE. Neither is a tracing span, but together they cover most of what §23.1 wants. Open item, now with named candidates. SessionConfig::set_strpanics on an invalid value (PROBE X3). Ban it.DataFusionError::Shared(anArc-wrapped error) exists so one error can be reported to several consumers — a hint that concurrent execution can surface the same failure repeatedly. Pass-level failure recording should deduplicate.
10. Plan serialization, Substrait, SQL, and the unparser (D9)
Promoted from a survey to a full cluster, because blueprint revision 2 moved a load-bearing decision into it. F9 now specifies that the memo key and the pass record carry “a datafusion-proto plan fingerprint”, computed as “blake3 of the plan’s datafusion-proto encoding under the platform’s LogicalExtensionCodec” (§14.2). The previous edition of this map never evaluated datafusion-proto — its open item 6 offered only “a formatting surface” or “a platform structural walk”, and the crate was absent from the extraction corpus. It is now extracted and measured, and the result is the most consequential finding in this edition.
Role: §14.2 rule 5 and the plan fingerprint; §14.3 memoization; §20.2 manifest; §20.4 reproduction; §5.4 the catalog the plan refers to.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Logical plan bytes | datafusion_proto::bytes::{logical_plan_to_bytes, logical_plan_from_bytes} and the _with_extension_codec variants | [rustdoc:datafusion-proto@55.1.0] |
| Physical plan bytes | physical_plan_to_bytes{,_with_extension_codec,_with_proto_converter} and the matching readers | [rustdoc:datafusion-proto@55.1.0] |
| Expression bytes | datafusion_proto::bytes::Serializeable — to_bytes, from_bytes_with_ctx, provided from_bytes | [rustdoc:datafusion-proto@55.1.0] |
| The codec contract | LogicalExtensionCodec — required: try_decode, try_encode, try_decode_table_provider, try_encode_table_provider. Provided (i.e. default, and by default not implemented): try_{decode,encode}_file_format, try_{decode,encode}_udf, try_{decode,encode}_higher_order_function, try_{decode,encode}_udaf, try_{decode,encode}_udwf | [rustdoc:datafusion-proto@55.1.0] |
| Physical counterpart | PhysicalExtensionCodec, PhysicalProtoConverterExtension, PhysicalPlanNodeExt; expression-level PhysicalExprEncode/PhysicalExprDecode | [rustdoc:datafusion-proto@55.1.0], [rustdoc:datafusion-physical-expr-common@55.1.0] |
| Model types | datafusion-proto-models — the generated protobuf message types, separately published | [rustdoc:datafusion-proto-models@55.1.0] |
| Substrait | datafusion_substrait::logical_plan::{consumer::SubstraitConsumer, producer::SubstraitProducer} | [rustdoc:datafusion-substrait@55.1.0] |
Measured (PROBE C) — is a datafusion-proto fingerprint sound?
Three questions, in the order they matter: can it encode our plans at all; is the encoding deterministic; is it stable across sessions [probe].
PROBE C datafusion-proto encoding
without a codec: FAILS -> NotImplemented("LogicalExtensionCodec is not provided")
("Error serializing custom table")
with a codec: len=220 same plan encoded twice: identical=true
equivalent plan, fresh session: identical=false
same query, different whitespace: identical=false
different predicate: differs=true
round-trip (encode -> decode -> re-encode): identical=false
That last line is the alarming one: encoding, decoding and re-encoding in the same process with the same codec does not reproduce the bytes. So the instability is not a session artefact. A second probe isolated the cause [probe]:
PROBE C2/C3 plan-byte stability vs the number of field-metadata keys
(6 independent encodings of the same query and schema)
0 metadata key(s): len=35 distinct encodings among 6 = 1 STABLE
1 metadata key(s): len=49 distinct encodings among 6 = 1 STABLE
2 metadata key(s): len=63 distinct encodings among 6 = 2 UNSTABLE
3 metadata key(s): len=77 distinct encodings among 6 = 3 UNSTABLE
5 metadata key(s): len=105 distinct encodings among 6 = 6 UNSTABLE
3 keys, one shared Schema object reused: distinct = 1 STABLE
The cause is Field::metadata’s HashMap iteration order. Rust seeds each HashMap independently, so two separately constructed but semantically identical schemas iterate their metadata in different orders, and datafusion-proto serialises them in iteration order. Reuse one Schema object and the instability vanishes; rebuild it and it returns. At five metadata keys, six encodings of the same plan produced six different byte strings.
What the blueprint binds to
| Blueprint requirement | Status |
|---|---|
§14.2 rule 5 / F9: “blake3 of the plan’s datafusion-proto encoding under the platform’s LogicalExtensionCodec” | erratum — as specified, the fingerprint is not reproducible. Every pse.* relation carries two or more field-metadata keys by §4.3’s own conventions (pse.semantic.logical_type, …quantity_type, …role, plus ARROW:extension:name on extension columns). The fingerprint therefore differs on essentially every process start. Since it is in the memo key (§14.3), memoized artifacts would never be reused; and since it is also recorded in provenance.pass_records.plan_fingerprints, two byte-identical runs would claim different provenance. This is fixable and cheap — see below — but it must be fixed before F9’s mechanism is written. |
§14.2 “the rendered EXPLAIN text … is never an identity” | confirmed, and revision 2 is right to say so. D4 and PROBE X2 both support it: the rendering is a display surface with four selectable formats, not a stability contract. |
§5.4’s catalog is entirely custom TableProviders | confirmed, with an obligation revision 2 does not state. logical_plan_to_bytes fails outright on a plan containing a custom provider — NotImplemented("LogicalExtensionCodec is not provided"). The platform must implement try_encode_table_provider / try_decode_table_provider, which are required methods. The fingerprint is therefore not “DataFusion’s encoding of the plan” but “DataFusion’s encoding plus the platform’s own encoding of every table reference” — and the stability of the second half is entirely the platform’s responsibility. §14.2 should say so. |
| §14.2 the fingerprint distinguishes different plans | confirmed — a changed predicate changed the bytes. |
Pass 2 and Pass 3 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Canonical metadata versus diagnostic plans | DM-48, DM-15, G6 | Corrected: sorted HashMap insertion does not stabilize protobuf [review:E5]. Canonicalize logical relation metadata under §5.3; preserve plan bytes as explicitly noncanonical diagnostic evidence outside memo keys (ADR-0044). |
Implement try_encode_udf on the platform codec | DM-31, G6 | adopt. try_encode_udf is a provided method, so a codec that does not override it encodes a UDF by name only. §18.5’s kernels are generated UDFs whose definitions change when KernelSpec changes. A fingerprint that captures only the name would be identical across two plans that compute different things — exactly the “cache hit reuses an artifact produced under different semantics” failure G6 describes. §14.2 already hashes the kernel digest separately, so the information exists; the codec must include it or §14.2 must state that the fingerprint deliberately excludes UDF bodies and the kernel digest covers them. Either is defensible; silence is not. |
| Verify fingerprint stability in CI | DM-48, DM-53 | adopt. Encode the same plan in two fresh processes and assert byte equality. This is a three-line test that would have caught the erratum above, and it is the only thing that keeps the canonicalisation from regressing. |
Serializeable for individual Exprs | DM-48 | evaluate. §6.6’s pse.expr_dsl columns are the authored form and normalized.*_expr_* the parsed graphs — both platform-owned, so this is not needed for the IR. It is useful for caching a compiled filter expression alongside a pass record. Low priority. |
datafusion-substrait | DM-48, DM-55, DM-57, DM-58 | reject as a plan format, and the reason is now stronger. Substrait would give §20.4 a portable, engine-independent record — attractive for reproducibility. Against it: RulePlanSpec is already the authoritative typed versioned plan description, and Substrait would be a second one (DM-57, DM-58). Revision 2’s choice of datafusion-proto is the better call provided the canonicalisation above lands: proto is an encoding of a plan the platform already has, not a new IR to maintain. Reconsider Substrait only if cross-engine execution becomes a requirement. |
SQL frontend (datafusion-sql) | DM-10, DM-16 | used only for human and analytic queries. §5.4 shows SQL over the snapshot catalog and §19.2 reporting is a natural SQL surface. §14.2 is explicit that rule bodies are typed RulePlanSpecs compiled through LogicalPlanBuilder, not SQL strings — the right call: a SQL string is untyped, unversioned, and not queryable as structure. Keep SQL at the analytics edge; never in the compiler. |
Unparser (default feature at 55.1.0 [docs.rs:datafusion@55.1.0]) | DM-49 | evaluate for explainability only. Rendering a LogicalPlan back to SQL lets a reviewer read what a rule compiled to in a language they know. Same caveat as EXPLAIN: evidence for humans, never an identity or contract. UserDefinedLogicalNodeUnparser extends it to the platform’s own nodes. |
LogicalPlan::{Dml, Ddl, Copy, Statement} | G1 | never constructed — see D1. |
datafusion optional features avro, parquet_encryption, serde, backtrace | — | not enabled. No Avro source; no encryption requirement stated; serde on plans is superseded by RulePlanSpec being the serialized form; backtrace is a debugging aid enabled ad hoc. |
recursive_protection (default on) | DM-30 | keep enabled — guards against stack overflow on deeply nested plans, relevant because §14.2’s fixed-point executor builds iterated plans. |
Gaps and risks
- The fingerprint’s stability is a platform property, not an engine guarantee. Between the
HashMapordering above and the platform-writtentry_encode_table_provider, most of what determines the bytes is ours. DataFusion offers no stable-plan-hash API and does not claim to. §14.2 should state the fingerprint’s definition as a platform contract with its own version, exactly as §5.3 does for content hashes. - Physical plans are a separate, larger surface.
physical_plan_to_bytesexists and takes its own codec. F9 speaks only of a logical-plan fingerprint, which is the right scope — the physical plan depends ontarget_partitionsand other execution-only settings that §14.2 deliberately excludes from the semantic-settings hash. datafusion-proto-modelsis separately published and separately versioned in principle; at 55.1.0 it moves with the family. Worth noting only because it is the crate whose wire format actually defines the bytes.
11. Logical types, extension types, and the type registry (D10) — new cluster
This is the largest single gap between the blueprint and DataFusion 55.1.0, and it did not exist in the previous edition because the surface did not exist in the extraction corpus.
DataFusion 55.1.0 provides a name-keyed extension registry and explicit validation helpers. X1 exercises a direct helper; it does not establish planning admission. E1 demonstrates the missing call on ordinary SELECT [review:E1]. The revised blueprint installs active semantic boundaries (§4.4).
Role: §4.4 (the ten pse.* extension types), §4.5 (the logical type catalog), §4.3 (metadata conventions), §5.4 (what the provider hands the engine), §18.5 (return_field_from_args and UDF typing).
This cluster is where DM-06 (“type semantic distinctions, not only machine representations”) stops being an Arrow-format property and becomes an engine property. The Arrow map’s A2 establishes that pse.semantic_id and pse.content_hash are two different semantic types over one storage type. What A2 could not offer was anywhere for the engine to enforce that. This is that place.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Logical type layer | datafusion_common::types::LogicalType — native() -> &NativeType, signature() -> TypeSignature, provided default_cast_for(&DataType); LogicalTypeRef = Arc<dyn LogicalType> | [rustdoc:datafusion-common@55.1.0] |
| Native type | NativeType with predicates is_numeric, is_integer, is_float, is_decimal, is_timestamp, is_date, is_time, is_duration, is_interval, is_binary, is_null | [rustdoc:datafusion-common@55.1.0] |
| Type signatures | TypeSignature::{arity, is_one_of, supports_zero_argument, get_example_types, to_string_repr_with_names}; TypeSignatureClass::{matches_native_type, default_casted_type} | [rustdoc:datafusion-common@55.1.0] |
| Extension type contract | datafusion_common::types::DFExtensionType — required storage_type() -> DataType, serialize_metadata() -> Option<String>; provided create_array_formatter(&dyn Array, &FormatOptions) -> Result<Option<ArrayFormatter>> | [rustdoc:datafusion-common@55.1.0] |
| The registry | datafusion_expr::registry::ExtensionTypeRegistry — extension_type_registration(name), extension_type_registrations(), add_extension_type_registration(..), remove_extension_type_registration(name); provided create_extension_type_for_field(&Field) -> Result<Option<DFExtensionTypeRef>> and extend(&[..]) | [rustdoc:datafusion-expr@55.1.0] |
| Registration | ExtensionTypeRegistration::new_arc(name: impl Into<String>, factory: impl Fn(&DataType, Option<&str>) -> Result<DFExtensionTypeRef> + Send + Sync + 'static); type_name(); create_df_extension_type(&DataType, Option<&str>) | [rustdoc:datafusion-expr@55.1.0] |
| Default implementation | MemoryExtensionTypeRegistry::{new_empty, new_with_canonical_extension_types, new_with_types}; From<HashMap<String, ExtensionTypeRegistrationRef>> | [rustdoc:datafusion-expr@55.1.0] |
| Session wiring | SessionStateBuilder::with_extension_type_registry(ExtensionTypeRegistryRef); Session::extension_type_registry(); SessionStateDefaults::default_extension_types() | [rustdoc:datafusion-session@55.1.0] |
| SQL-level type mapping | datafusion_expr::planner::TypePlanner — plan_type(&ast::DataType), plan_type_field(..) -> Result<Option<FieldRef>>; installed via SessionStateBuilder::with_type_planner | [rustdoc:datafusion-expr@55.1.0], [c7:/apache/datafusion] |
| Field/DataType helpers | datafusion_common::datatype::{FieldExt, DataTypeExt} — renamed, retyped, with_field_metadata, into_list, into_fixed_size_list, into_list_item | [rustdoc:datafusion-common@55.1.0] |
Measured (PROBE X1)
Registering a pse.semantic_id type over FixedSizeBinary(16), exactly as §4.4 declares it [probe]:
PROBE X1 ExtensionTypeRegistry
canonical types preloaded (7): arrow.timestamp_with_offset, arrow.opaque,
arrow.fixed_shape_tensor, arrow.uuid, arrow.variable_shape_tensor,
arrow.bool8, arrow.json
register pse.semantic_id -> 8 registrations
resolve from a Field carrying
ARROW:extension:name -> storage_type = FixedSizeBinary(16)
Field with the WRONG storage type -> REJECTED
"Error during planning: pse.semantic_id requires FixedSizeBinary(16), got Utf8"
unregistered pse.* name -> Err("Logical type not found")
And separately, that extension metadata survives the engine end to end [probe]:
PROBE E ARROW:extension:name through planning and execution
planned output field 'id' : Some("pse.semantic_id")
executed batch field 'id' : Some("pse.semantic_id")
after `SELECT id AS key_out`: Some("pse.semantic_id")
as a GROUP BY key : Some("pse.semantic_id")
Four consequences, in order of importance.
-
Direct validation is available, automatic plan validation is not established. Invoke the generated recursive validator at supported admission/plan/output boundaries; E1 proves registration alone is insufficient [review:E1].
-
An explicit registry lookup rejects an unknown name. An ordinary query may never perform that lookup. Python and Rust therefore both need an active platform boundary; the loss profile identifies the enforcing API rather than attributing enforcement to registration [review:E1].
-
ARROW:extension:namepropagates through projection, aliasing and aggregation (PROBE E). §4.3’s claim that metadata is “a carrier” understates it: the carrier is preserved by the engine across operations that rewrite schemas. That is what makes an end-to-end semantic-type discipline feasible at all. -
Seven canonical Arrow extension types are preloaded by default.
MemoryExtensionTypeRegistry::new_with_canonical_extension_types()is whatSessionStateDefaultsuses, soarrow.json,arrow.uuid,arrow.bool8,arrow.opaqueand the two tensor types are already recognised. The Arrow map’s §11 register row 2 (enablecanonical_extension_types) has an engine-side counterpart that is on by default.
What the blueprint binds to
| Blueprint requirement | Status |
|---|---|
| §4.3 “nothing in DataFusion acts on these keys unless a platform component explicitly reads them” | erratum. True of ordinary pse.* keys; false of ARROW:extension:name, which the engine resolves against its registry and uses to reject mismatched storage types. The sentence should be narrowed to non-ARROW: keys. |
§4.4 the ten pse.* extension types and their standard storage types | confirmed registrable, and enforceable. Each type’s supports_data_type logic has a direct home in the registration factory, which receives (&DataType, Option<&str>) — the storage type and the serialized metadata — and returns an error the planner surfaces. |
| §4.5 logical type catalog | refined. §4.5 is a catalog of Arrow storage types. DataFusion has its own LogicalType/NativeType layer above them, used for function signature resolution and coercion. The two are compatible but distinct, and §4.5 should say which layer it is describing. |
§18.5 return_field_from_args “attaches pse.semantic.* metadata and the output quantity type” | confirmed and strengthened. Because the field’s metadata survives (PROBE E), a UDF that attaches an extension name on output gets that name validated downstream by the registry — so §18.5’s claim becomes checkable rather than advisory. |
§4.4 “Python consumers register matching pyarrow.ExtensionType classes” | unchanged — see the Python map; the asymmetry in consequence 2 above is the finding |
Pass 2 / Pass 3 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Register all declared types and invoke generated validation | DM-06, DM-07, G3, DM-44 | Adopt: factories plus recursive provider/bundle/analyzer/output admission; one generated registration function alone does not close G3 (ADR-0039; [review:E1]). |
DFExtensionType::create_array_formatter | DM-49, DM-47 | adopt alongside the Arrow map’s §11 register row 11. The same formatter implementation satisfies Arrow’s ArrayFormatterFactory and DataFusion’s per-type hook, so pse.semantic_id renders meaningfully in EXPLAIN, in error messages and in diagnostics instead of as 16 raw bytes. |
TypePlanner for pse.* types in analytic SQL | DM-06, DM-55 | evaluate. §19.2’s reporting surface is SQL. A TypePlanner lets a query say CAST(x AS SEMANTIC_ID) — or any platform type name — and have it resolve to the right storage type with its extension metadata. Upstream’s own example does exactly this for UUID → FixedSizeBinary(16) + ARROW:extension:name [c7:/apache/datafusion], which is the same shape as pse.semantic_id. Only worth it if analytic SQL becomes a supported interface rather than an ad-hoc one. |
| Extension-type versioning via the registry | DM-44, DM-51 | evaluate — this is where the Arrow map’s open item can finally be answered. Arrow has no registry, so A2 could only record that pse.* metadata has no version-resolution story. A registration factory receives the serialized metadata string and may branch on a version field inside it, so a single registered name can accept several metadata generations and reject the rest. That is a migration mechanism (DM-51) in the place DM-44 wants it. |
LogicalType / NativeType for the platform’s own quantity types | DM-06, DM-58 | reject, for now. It is tempting to model QuantityType as a DataFusion LogicalType. But §8’s dynamic registry is the authority for quantity types and units, and DataFusion’s logical-type layer exists to drive function signature coercion — a different job. Implementing it would create a second place where a quantity type is defined (G1) for no gain the extension registry does not already provide. Recorded because it is the obvious next step and it is the wrong one. |
FieldExt / DataTypeExt helpers | DM-58 | note only — convenience methods (renamed, retyped, with_field_metadata, into_list). Worth knowing they exist so generated code does not re-implement them; not a design decision. |
Gaps and risks
- Session scope is not validation coverage. Both engine and non-engine paths must invoke the declared checks. The registry supplies factories; Arrow typed helpers and platform cross-field/row validators supply distinct parts of the admission contract (§4.4; [review:E1]).
create_extension_type_for_fieldis a provided method. A customExtensionTypeRegistryimplementation that overrides it could weaken the check silently. UseMemoryExtensionTypeRegistryunless there is a reason not to.- Registration order and duplicate names.
add_extension_type_registrationreturns the previous registration for that name, so a later registration silently replaces an earlier one. Generated code should assert the return isNone— a duplicatepse.*name is a build error, not a runtime surprise (G1). - The registry validates storage type and metadata. It cannot validate cross-field constraints such as
pse.ordinal_ref’s target relation, which the Arrow map records as an open item. That gap is unchanged.
12. Pass 3 — the under-leverage sweep
Passes 1 and 2 both start from the blueprint. Neither can find a capability the blueprint never gestured at, because neither enumerates the library independently. This pass does: four complete enumerations, each with a stated denominator, each row adjudicated bound, adopt, evaluate or reject, with reason. Charter §G and DM-58 still apply.
The denominator matters because of the rule this corpus inherits: absence from the extraction is not evidence that a feature does not exist. The previous edition covered 20 crates and 148 traits; this one covers 37 and 169.
12.1 E1 — the crate inventory
DataFusion 55.1.0 declares 44 workspace members [gh:apache/datafusion@55.1.0/Cargo.toml]; removing binaries, test harnesses, codegen helpers and example crates (datafusion-cli, datafusion-examples and its three FFI examples, datafusion-sqllogictest, datafusion-wasmtest, test-utils, benchmarks, the two */gen crates) leaves ~37 publishable library crates. All 37 are extracted.
| Crate group | Status | Adjudication |
|---|---|---|
datafusion (umbrella), -common, -expr, -expr-common, -session, -catalog, -execution, -physical-plan, -physical-expr, -physical-expr-common, -physical-optimizer, -optimizer, -sql, -pruning | bound | the surface D1–D8 already cover |
-proto, -proto-common, -proto-models | now bound — by revision 2 | F9’s plan fingerprint. Absent from the previous extraction, which is why D9 could not be written. Measured there. |
-datasource, -datasource-{arrow,csv,json,parquet}, -catalog-listing | reject for platform data, with one exception | §5.4’s providers are platform-written over content-addressed artifacts; listing tables and format inference are the opposite of D1’s model. The exception is SchemaAdapterFactory/PhysicalExprAdapterFactory (E2), which live here and are about schema evolution, not file discovery. |
-functions, -functions-aggregate, -functions-aggregate-common, -functions-nested, -functions-window, -functions-window-common, -functions-table | bound (as the default registry) | The session ships them; §14.2’s rule algebra uses comparison and boolean functions. §18.5’s kernels are additional UDFs, not replacements. -functions-table is the table-function surface (E2). |
-ffi | evaluate | A stable ABI for TableProvider, ScalarUDF, ExecutionPlan and even FFI_PhysicalOptimizerRule across a dynamic-library boundary. The blueprint has no plugin boundary — everything is one workspace — so DM-58 says no today. It becomes interesting only if §22’s extension model ever admits third-party kernel packages as compiled artifacts rather than source. Recorded so the option is known. |
-substrait | reject as a plan format | D9 — RulePlanSpec is already the authoritative typed plan description; a second one cuts against DM-57 and DM-58. |
-physical-expr-adapter | evaluate | Schema-evolution machinery — see E2. |
-spark | reject | Spark-compatible function semantics (monthname, SparkPow, ANSI mode). No Spark compatibility requirement exists, and adopting Spark semantics for arithmetic would silently change numerical behaviour (G2). |
-common-runtime | bound (transitively), one interesting trait | JoinSetTracer — see E2 |
-macros, -doc | bound (transitively) | the #[user_doc] machinery behind function documentation; no decision |
12.2 E2 — the extension-point inventory
169 public traits across the 37 crates [rustdoc:df551@55.1.0], against 148 in the previous 20-crate corpus. Type-level machinery, sealed markers and internal accumulator plumbing are dismissed as a group. What follows is every trait that is a genuine place to insert platform behaviour and is not already covered by D1–D10.
| Trait | DM / gate | Adjudication |
|---|---|---|
datafusion_common::config::ConfigExtension + ExtensionOptions | DM-16, DM-28, DM-31 | adopt. A typed, namespaced configuration extension (ConfigExtension::PREFIX, then set/entries over typed fields) that lives inside ConfigOptions and appears in information_schema.df_settings alongside the engine’s own. §14.2 rule 5 already reads semantic settings back from df_settings to build the settings hash — so platform policy registered as a datafusion.pse.* extension is captured by the existing mechanism with no new plumbing. Today platform policy travels separately from engine policy and only the engine half is hashed. |
datafusion_common::cse::{Normalizeable, NormalizeEq, CSEController} | DM-15 | evaluate — closer to §5.3’s needs than it looks. NormalizeEq::normalize_eq is DataFusion’s answer to “are these two nodes semantically equivalent after normalization”, used for common-subexpression elimination. §5.3 defines canonicalization and equivalence for relations; §7.4 (P10) defines hash-consing for the math IR. This is the engine’s version of the same idea for expression trees. Not directly reusable — it is Expr-shaped and the math IR is not Expr (D6) — but it is prior art worth reading before P10’s canonicalizer is written, and CSEController is the hook if the rule compiler ever wants CSE over its own plans. |
datafusion_pruning::UnhandledPredicateHook | DM-43, G7 | adopt. handle(&Arc<dyn PhysicalExpr>) -> Arc<dyn PhysicalExpr> — called for predicates the pruning rewriter cannot handle. This is capability negotiation made explicit: instead of a predicate silently falling through to a full scan, the platform sees exactly which expressions were not handled. Directly complements D2’s Exact discipline, and it is the mechanism that turns “we think pushdown fired” into “we know when it did not”. |
datafusion_physical_expr_common::metrics::CustomMetricValue | DM-50, DM-39 | adopt if any custom ExecutionPlan is written. Application-defined metric values that aggregate and display alongside the engine’s own in EXPLAIN ANALYZE. With pgjson output (D4) this makes platform-specific counters — rows derived per rule, kernels evaluated — machine-readable pass evidence rather than log lines. |
datafusion_common_runtime::JoinSetTracer | DM-50, DM-28 | evaluate. trace_future / trace_block wrap every future and blocking closure the engine spawns. This is the closest thing DataFusion has to the per-operation instrumentation §23.1 wants, and D8 records that the engine emits no spans of its own. Two methods; the cost is that it sits on a very hot path. |
datafusion_expr::var_provider::VarProvider | DM-28 (declare ambient inputs) | evaluate. Supplies values for @variable / @@variable references in a plan. If §19.3’s sweeps or §19.2’s reports ever want a parameterised constant, this is the declared, typed way to inject it — as against string interpolation into SQL, which would be an undeclared ambient input. Pairs with Expr::Placeholder (D3). |
datafusion_datasource::schema_adapter::{SchemaAdapter, SchemaAdapterFactory, SchemaMapper} | DM-51 | reject — deprecated. The trait’s own documentation at 55.1.0 reads: “Deprecated: … This trait has been removed. Use PhysicalExprAdapterFactory instead” [rustdoc:datafusion-datasource@55.1.0]. Recorded because it is the obvious search hit for “schema evolution” and is the wrong answer. |
datafusion_physical_expr_adapter::{PhysicalExprAdapter, PhysicalExprAdapterFactory} | DM-51, DM-42 | evaluate. The replacement: rewrites a PhysicalExpr to match a target schema, for the case where a stored artifact’s physical schema differs from the table schema. §22.2’s change-set model and §4.2’s pse.contract.version mean artifacts written under an older RelationSpec will exist. Today §20.5 would handle that by conversion at import; this would handle it at scan. A real migration mechanism — but only once schema evolution is a lived problem rather than an anticipated one (DM-58). |
datafusion_execution::{SpillFile, SpillWriter, TempFileFactory} | DM-28, DM-29, G5 | evaluate. Pluggable spill: the platform chooses where temp files live and how they are named and cleaned up. D7 records that spill is an undiscussed filesystem dependency; this is the seam that makes it an owned one. |
datafusion_execution::cache::{Cache, CacheKey, CacheValue} | DM-32 | evaluate — as D7’s CacheManager row already records. Note CacheKey requires table_ref() and size(), which reveals the cache is table-shaped; the platform’s natural key is the content hash. Probably platform-side wins. |
datafusion_common::rounding::FloatBits | DM-40 | note, do not adopt. to_bits / from_bits / float_is_nan / infinity — the bit-level float manipulation behind DataFusion’s interval arithmetic. §5.3 step 4 needs exactly this shape (IEEE bits, NaN canonicalisation) and the platform will write its own, because §5.3’s canonicalisation rules are the platform’s contract, not DataFusion’s. Recorded so nobody assumes the engine’s helper implements §5.3’s semantics — it does not. |
datafusion_expr::async_udf::AsyncScalarUDFImpl | DM-38 | reject. invoke_async_with_args for UDFs that call out to remote services. §18.5’s kernels are pure native functions and D10’s boundary forbids a solve inside a scalar call; an async UDF would put an await point inside expression evaluation. Recorded as a rejection because “our kernel needs I/O” is a plausible future request that should be answered no. |
datafusion_expr::HigherOrderUDFImpl | — | reject — as D3 already records for Lambda: model semantics belong in the math IR, not in Expr. |
datafusion_session::{TableFunctionImpl, ExtensionPlanner, PhysicalPlanner, QueryPlanner} | DM-19 | ExtensionPlanner: adopt if D3’s extension nodes are adopted — PROBE B shows it is the missing piece for physical execution. The others: reject, no requirement. |
datafusion_catalog::{AsyncCatalogProvider, AsyncSchemaProvider, UrlTableFactory} | — | reject — the snapshot catalog resolves once at session creation (D1). Recorded to prevent a future “we should be async” drift. |
datafusion_execution::context::FunctionFactory | G1 | reject — handles CREATE FUNCTION from SQL. §18.5’s UDFs are generated from KernelSpec; admitting SQL-defined functions would create a second authority over what a kernel is. |
datafusion_common::tree_node::{TreeNode, TreeNodeRewriter, TreeNodeVisitor, DynTreeNode, …} | DM-24 | evaluate as prior art. A complete, well-tested generic tree-rewriting framework with Transformed<T> fixed-point signalling. The math IR needs the same machinery (P10, P11, P12). Implementing TreeNode for the platform’s own IR nodes is plausible and would give the rewrite drivers for free; the cost is a dependency from pse-math onto datafusion-common, which §3.2 currently confines to pse-catalog and pse-rules. A real architectural question, not a free win. |
datafusion_datasource::morsel::{Morsel, MorselPlanner, Morselizer} | DM-35 | reject — morsel-driven I/O scheduling for file scans. §5.4 reads mmapped IPC artifacts; there is no I/O scheduling problem to solve. |
datafusion_physical_plan::operator_statistics::StatisticsProvider | DM-39 | evaluate — customises statistics computation per ExecutionPlan node, chained until one answers. Relevant only alongside D2’s Precision discipline, and only if a custom ExecutionPlan is written. |
datafusion_expr::registry::SerializerRegistry | DM-48 | bound by implication — D9’s codec work. |
datafusion_expr::planner::{ExprPlanner, RelationPlanner, ContextProvider} | — | reject — SQL-surface extension points; §14.2 builds plans structurally, not from SQL. TypePlanner is the exception and is adjudicated in D10. |
datafusion_sql::unparser::{Dialect, UserDefinedLogicalNodeUnparser} | DM-49 | evaluate for explainability — D9. |
datafusion_common::heap_size::DFHeapSize, utils::proxy::{VecAllocExt, HashTableAllocExt} | DM-39 | note — memory accounting helpers used by the engine’s own operators; relevant only if a custom operator is written. |
| ~130 type-level, sealed, accumulator and codec-internal traits | — | not extension points — dismissed as a group |
12.3 E3 — the declared-policy surface
DataFusion’s policy surface is 155 configuration keys across eight namespaces [gh:apache/datafusion@55.1.0/docs/source/user-guide/configs.md]:
| Namespace | Keys | Bearing |
|---|---|---|
datafusion.execution | 71 | mixed — some semantic, most execution-only |
datafusion.optimizer | 39 | semantic — rewrite enablement |
datafusion.sql_parser | 10 | semantic — dialect, identifier handling |
datafusion.format | 9 | display only |
datafusion.explain | 9 | display only, but see D4’s pgjson |
datafusion.runtime | 8 | resource limits (D7) |
datafusion.catalog | 8 | mostly disabled by D1’s model |
datafusion.spark | 1 | rejected (E1) |
§14.2 rule 5 already gets the core of this right: it captures datafusion.optimizer.*, datafusion.sql_parser.* and datafusion.execution.time_zone into the semantic-settings hash and excludes execution-only keys such as batch_size and target_partitions. The enumeration confirms that partition is sound, and surfaces four keys that deserve individual attention:
| Key | Default | Finding |
|---|---|---|
datafusion.execution.skip_physical_aggregate_schema_check | false | assert it stays false. When true, the engine skips verifying that the schema produced by planning an aggregate’s input matches the input plan’s schema. That is a G3 check being switched off. It is not in §14.2’s semantic-settings list because it lives under execution.* — an example of the namespace split not aligning perfectly with the semantic/non-semantic split. |
datafusion.execution.enable_ansi_mode | false | assert it stays false. Changes expression semantics for Spark built-ins; combined with E1’s rejection of -spark, this should be pinned rather than left to a default. |
datafusion.execution.spill_compression, max_spill_file_size_bytes | uncompressed, 128 MiB | declare explicitly alongside the MemoryPool choice (D7) |
datafusion.explain.{format, analyze_level, analyze_categories} | indent, dev, all | set format = pgjson where a pass record stores a plan (D4) |
The general finding: §14.2 rule 5’s semantic-settings hash is defined by namespace, but at least two semantically significant keys live under execution.*. The rule should be restated as an explicit allow-list of keys rather than a namespace prefix match, and the list versioned — otherwise a future DataFusion release adding a semantic key under execution.* silently escapes the hash (DM-31, G6).
Beyond config, the crate’s feature flags are the compile-time half (§1.3): defaults include unparser, recursive_protection, parquet, sql and the expression families; serde, avro, parquet_encryption and backtrace are off and stay off (D9).
12.4 E4 — the release delta, 54.0 → 55.1
Selected from the 55.0.0 changelog [gh:apache/datafusion@55.1.0/dev/changelog/55.0.0.md]. 55.1.0 itself is a bug-fix release with no new features.
| Change | Adjudication |
|---|---|
feat: Support IEEE 754 negative zero semantics | directly relevant, and measured. PROBE D shows ORDER BY distinguishes -0.0 from +0.0 while GROUP BY and hash joins merge them. The release note explains why ordering behaves correctly; it does not extend to grouping. This is the evidence behind D3’s finding that §5.3’s “-0.0 is preserved” needs qualification. |
feat: add strictness metadata for scalar UDF null propagation and use it in outer join elimination | adopt — this is is_strict earning its keep. §3.1 already names is_strict and §18.5 binds it. The 55.0 change makes the declaration load-bearing: the optimizer uses it to eliminate outer joins. A UDF that declares is_strict = true while emitting non-null output for null input would therefore change query results, not merely miss an optimisation. Reinforces register row on is_nullable/strictness derivation from KernelSpec. |
Add EnsureRequirements: merged EnforceDistribution + EnforceSorting with idempotent pushdown_sorts | confirms §3.1’s “55 forms” — already verified in §1.5 |
feat(sql): Postgres-style EXPLAIN (...) option list; pgjson format for EXPLAIN ANALYZE | adopt — D4 |
Add minimal APIs / hooks for granular statistics collection in TableProvider; StatisticsContext; partition_statistics | evaluate — extends D2’s Precision discipline to per-partition granularity. Only useful once §5.4’s statistics are richer than row counts and key min/max. |
Add Physical/logical Partitioning::Range + proto representation | evaluate. Range partitioning preserves ordering across partitions, where hash partitioning does not. D5 records that partitioned output arrives nondeterministically and §5.3 re-sorts. If a large compiled.* relation ever makes that sort expensive, range partitioning is the mechanism that avoids it (DM-35, DM-40). |
feat: introduce pluggable SpillFile trait and TempFileFactory | evaluate — E2, D7 |
feat: Plumb Parquet virtual columns (row_number); input_file_name UDF | reject. Provenance columns derived from physical storage position. §5.1 is explicit that row positions are never identity; adopting these would create a second, position-based identity (G1, DM-11). |
Add SQL planner, physical planner, and TableProvider hook for MERGE INTO | reject — a sixth mutation method on TableProvider. Strengthens D1’s governance-test recommendation: the trait keeps growing, so the test must assert the set of implemented methods rather than checking a fixed list. |
feat(catalog): expose InformationSchemataBuilder as public API | note — §14.2 rule 5 already reads information_schema.df_settings; this is the builder behind it. |
feat(physical-expr): DynamicFilterTracker; dynamic filter pushdown config keys | reject for now — runtime-adaptive filters are a scan-performance mechanism for large external datasets; §5.4’s artifacts are small and keyed. |
refactor: wrap HigherOrderUDFImpl in a concrete HigherOrderUDF struct; lambda proto + Substrait support | reject — D3 |
perf(logical-plan): box CreateExternalTable / CreateFunction in DdlStatement (-45% LogicalPlan size) | note — no action; illustrates that LogicalPlan’s representation changes between releases, which is the argument for not treating its rendering as an identity (D9) |
Allow datafusion-ffi to opt out of proto parquet; FFI_PhysicalOptimizerRule::optimize_with_context | note — E1’s -ffi evaluation |
12.5 Third-party extenders — bounded survey
| Crate | Latest | Adjudication |
|---|---|---|
datafusion-table-providers (+ the -postgres/-mysql/-sqlite/-duckdb/-flightsql/-odbc family) | 0.13.1 | reject. Providers for external databases. D1’s catalog is snapshots of content-addressed artifacts; a live external database inside the catalog would be a second, mutable authority (G1) and would break §20.4’s reproduction contract outright. |
datafusion-tracing | 55.0.0 | evaluate, with a caveat. Instruments execution plans with tracing spans — exactly D8’s gap. The caveat is that it is at 55.0.0 while the engine is at 55.1.0, so it is an independently-versioned compatibility edge of the kind that produced the num-dual/feos-core split in the supporting-library map. JoinSetTracer (E2) is the first-party alternative and is one trait. |
datafusion-distributed | 4.0.0 | reject — D10’s boundary keeps DataFusion out of the numerical inner loop; a distributed tier is machinery for a scale this design does not claim (DM-58). |
datafusion-postgres / -pg-catalog | 0.18.x | reject — exposes the engine over the Postgres wire protocol. That is a service boundary §3.3 explicitly does not want. |
datafusion-proto-models | 55.1.0 | in-family — E1 |
datafusion-federation, clickhouse-datafusion, zarr-datafusion, qdrant-datafusion, hudi-datafusion, datafusion-ducklake | — | reject — external-source integrations; same reason as datafusion-table-providers |
13. Principle-alignment register (DataFusion)
Capabilities the blueprint does not claim, which would improve alignment with DATA_MODEL_DESIGN_CHARTER.md. Every row names a principle and the hand-written work or risk it removes. Rejections are included, because a recorded rejection is worth as much as an adoption.
In the previous edition this table combined Arrow and DataFusion rows. The Arrow rows now live in that map’s §11; these are DataFusion’s. Numbering is local to this map, and rows marked new come from §12’s Pass-3 enumeration or from this edition’s probes.
Adopt
| # | Capability | Cluster | DM / gate | What it removes or prevents |
|---|---|---|---|---|
| 1 | Register types and install active semantic admission | D10 | DM-06, DM-07, G3, DM-44 | X1 is a direct-helper probe; E1 refutes universal plan enforcement. Generate recursive validation and install the declared analyzer/provider/output boundaries (ADR-0039). |
| 2 | Canonicalise field and schema metadata before a schema reaches the engine | D9 | DM-48, DM-15, G6 | Blocking for revision 2’s F9. datafusion-proto serialises field metadata in HashMap iteration order, so plan bytes differ on every process start once a field carries ≥2 metadata keys — which every pse.* relation does (PROBE C). As specified, F9’s fingerprint would never produce a memo hit and would record different provenance for identical runs. |
| 3 | Implement try_encode_udf on the platform’s LogicalExtensionCodec | D9 | DM-31, G6 | It is a provided method, so the default encodes a UDF by name only. Two plans calling the same-named kernel with different KernelSpec bodies would fingerprint identically — a cache hit reusing an artifact produced under different semantics. Either encode the kernel digest, or state in §14.2 that the fingerprint excludes UDF bodies and the kernel digest covers them. Silence is the one unacceptable option. |
| 4 | CI test: encode the same plan in two fresh processes, assert byte equality | D9 | DM-48, DM-53 | Three lines. It would have caught row 2, and it is the only thing that stops the canonicalisation from silently regressing. |
| 5 | Governance test: no TableProvider mutation method is implemented | D1 | G1, DM-02 | The mutation methods are defaulted, so the architecture is protected only by omission. 55.0 added a sixth (merge_into), which is the argument for asserting the set of implemented methods rather than checking a fixed list. |
| 6 | Precision::{Exact, Inexact, Absent} used deliberately per statistic | D2 | DM-59, G7 | An undifferentiated statistics answer. The uncertainty label is built into the type; using it is free. |
| 7 | Answer StatisticsRequests selectively, Absent otherwise | D2 | DM-26 | Makes §5.4’s “answered only from cached metadata” a contract rather than a convention. |
| 8 | Constraints::project() when a scan is projected | D2 | DM-22, DM-24 | Hand-projecting a primary key over a changed column set. |
| 9 | Record that DataFusion enforces no constraint but Field nullability | D2 | G7, DM-43 | §5.4 declares “Constraints: primary key only”. Upstream is explicit that primary, unique and foreign keys “are not validated or used during query planning” [c7:/apache/datafusion]. The declaration is sound; reading it as enforcement is not. Platform P2 validation is what makes it true. New. |
| 10 | Built-in CASE with truthful UDF optimization flags | D6 | G4, DM-28 | E2 shows flags alone leave generic evaluation eager; use real guarded physical execution (ADR-0043/0047). |
| 11 | output_ordering / preserves_lex_ordering / strictly_order_preserving for monotone kernels | D6 | DM-24, DM-40 | Makes a monotonicity claim explicit and testable instead of implicit. |
| 12 | is_nullable and is_strict derived from KernelSpec, not written independently | D6 | DM-08, G2 | A UDF declaring non-nullable while its batch path emits nulls makes the plan schema lie. Raised in stakes by 55.0: strictness metadata is now used for outer join elimination, so a wrong is_strict changes results rather than merely missing an optimisation (§12 E4). |
| 13 | coerce_types = accept declared types, else error | D6 | DM-42, G2 | Silent widening of an Int64 argument into a Float64 kernel. |
| 14 | Record the optimizer/analyzer rule set and engine version in the pass key | D4 | DM-31, DM-32, G6 | Applied in revision 2 as part of F9 (reference.engine_profiles, ordered rule lists, engine_profile_hash). Retained as the closed record of why. |
| 15 | Restate §14.2 rule 5’s semantic-settings hash as an explicit key allow-list | §12 E3 | DM-31, G6 | Rule 5 selects settings by namespace (datafusion.optimizer.*, sql_parser.*, execution.time_zone). At least two semantically significant keys live under execution.* — skip_physical_aggregate_schema_check and enable_ansi_mode — so a namespace match misses them, and a future release adding a semantic key under execution.* would escape the hash silently. New. |
| 16 | Assert skip_physical_aggregate_schema_check = false and enable_ansi_mode = false | §12 E3 | G3, G2 | The first switches off a schema-consistency check; the second changes expression semantics. Both default correctly today; neither is pinned. New. |
| 17 | Choose a bounded MemoryPool; never UnboundedMemoryPool | D7 | DM-30, G5 | Corrected scope: fallible reservations can return a typed resource error; unaccounted or infallible allocations can still exhaust process memory (ADR-0046). PROBE H characterizes the query error and its configuration hints, not process-wide protection. |
| 18 | Inject TimeProvider for tests and reproduction | D7 | DM-28, DM-48 | Wall-clock time is an ambient input that breaks §20.4’s byte-identical reproduction claim. The engine already provides the seam. |
| 19 | Validate target_partitions against the rayon budget at session construction | D7, D5 | G5, DM-35 | §18.8 says oversubscription “is a configuration error, not a runtime surprise” — only true if something checks. |
| 20 | Add a DataFusion column to §23.2’s failure table | D8 | DM-47, G3 | Twenty engine variants currently map to platform classes by convention at each call site. |
| 21 | DataFusionError::Collection for multi-error reporting | D8 | DM-47, DM-30 | Validators return violating keys plural; collapsing to the first error discards the evidence §23.2 wants. |
| 22 | Ban SessionConfig::set_str; assemble configuration through ConfigOptions::set | D8 | G3, DM-47 | Measured (PROBE X3): the convenient setter panics on an invalid value while ConfigOptions::set returns a typed error. A panic during session construction is a process abort, not a config.invalid row. Same shape as the Arrow map’s Field::extension_type() ban, and the same remedy — a governance grep. New. |
| 23 | Test-only wrapper that re-checks every Exact pushdown claim | D2 | G7, DM-53, DM-54 | Nothing in DataFusion verifies an Exact claim; the optimizer deletes the filter on trust. PROBE A shows the shapes are matchable, which makes the claim reachable — it does not make a given match correct. |
| 24 | A provider must treat supports_filters_pushdown as pure | D2 | DM-20, G4 | Measured (PROBE A): the engine calls it more than once per plan. Instrumentation or counters placed there — the obvious location — would double-count. Undocumented in the signature. New. |
| 25 | ConfigExtension / ExtensionOptions for a typed datafusion.pse.* namespace | §12 E2 | DM-16, DM-28, DM-31 | Platform policy registered as a config extension appears in information_schema.df_settings, which §14.2 rule 5 already reads — so it enters the settings hash with no new plumbing. Today platform policy travels separately and only the engine half is hashed. New. |
| 26 | UnhandledPredicateHook | §12 E2 | DM-43, G7 | Turns “we think pushdown fired” into “we know exactly which predicates were not handled”. Capability negotiation made explicit, complementing D2’s Exact discipline. New. |
| 27 | DFExtensionType::create_array_formatter | D10 | DM-49, DM-47 | Shares one implementation with the Arrow map’s ArrayFormatterFactory (its §11 register row 11) so pse.* values render meaningfully in EXPLAIN, error messages and diagnostics instead of as raw storage. New. |
| 28 | EXPLAIN format pgjson where a pass record stores a plan | D4 | DM-27, DM-49 | The previous edition treated EXPLAIN as one text surface; it is four, and one is structured JSON (PROBE X2). A stored plan becomes queryable evidence rather than a blob. New. |
| 29 | CustomMetricValue if any custom ExecutionPlan is written | §12 E2 | DM-50, DM-39 | Platform counters aggregate and display alongside engine metrics; with pgjson they become machine-readable pass evidence rather than log lines. New. |
| 30 | ExtensionPlanner if D3’s extension nodes are adopted | §12 E2 | DM-19 | PROBE B shows it is the one missing piece: the node survives the optimizer intact, and physical planning then fails with a clean typed error until a planner is registered. New. |
| 31 | Family-wide = pins, and a committed, CI-enforced lockfile | §1.2 | DM-48, DM-51 | Revision 2 adopted the pins; it did not adopt the lockfile. = pins bind direct dependencies only — a transitive path can still introduce a second version. |
| 32 | The per-rule observer closure on Optimizer::optimize / Analyzer::execute_and_check | D4 | DM-46, DM-50, DM-49 | Revision 2’s F9 records the rule set; the observer records which rules actually fired, in what order, on this plan. The first is what could have happened, the second is what did — and it is what makes a plan difference explainable at the level of meaning instead of by diffing two renderings. The seam exists and the platform already drives optimisation explicitly, so this is effectively free. New. |
Evaluate
| # | Capability | Cluster | DM | Why it is not yet an adopt |
|---|---|---|---|---|
| 33 | LogicalPlan::Extension + UserDefinedLogicalNode for rule bodies, with check_invariants and prevent_predicate_push_down_columns | D3 | DM-18, DM-21, DM-07 | Still the largest structural opportunity, and its blocking uncertainty is now resolved: PROBE B shows the node survives the optimizer with rule_id intact, and that the optimizer still pushes projections down through it. What remains is cost, not risk — a 14-method contract plus row 30’s planner. This is an adopt-candidate awaiting a decision, not an open question. |
| 34 | ScalarUDFImpl::preimage() | D6 | DM-24, DM-43, G6 | Deferred to R-25; exact equivalent predicate with half-open endpoints, null/floating policy and differential fixtures; a monotone inverse alone is insufficient. |
| 35 | TrackConsumersPool / PeakRecordingPool | D7 | DM-50, DM-39 | Turns “the pass used too much memory” into “this operator did”. |
| 36 | CacheManager for artifact metadata | D7 | DM-26, DM-32 | The engine cache is table/file-keyed (CacheKey requires table_ref()); the platform’s natural key is the content hash. Probably platform-side wins — but decide it. |
| 37 | Expr::Placeholder for §19.3 sweeps | D3 | DM-26 | One prepared plan executed per sample. Only if sweeps prove plan-construction-bound. Pairs with row 39. |
| 38 | EXPLAIN / unparser output stored as pass evidence | D4, D9 | DM-49 | Excellent for explainability; must never become an identity. Row 28 settles the format; whether to store it at all is still a decision. |
| 39 | VarProvider for declared parameter values | §12 E2 | DM-28 | The typed, declared way to inject a constant into a plan, as against string interpolation into SQL — which would be an undeclared ambient input. New. |
| 40 | JoinSetTracer | §12 E2 | DM-50, DM-28 | The closest first-party answer to D8’s “no engine spans” gap: two methods wrapping every future and blocking closure. Cost: it sits on a very hot path. New. |
| 41 | PhysicalExprAdapterFactory for schema evolution | §12 E2 | DM-51, DM-42 | Artifacts written under an older RelationSpec will exist (§22.2, §4.2’s pse.contract.version). This adapts at scan; §20.5 would convert at import. Real migration machinery — once evolution is a lived problem, not an anticipated one (DM-58). Note the deprecated SchemaAdapter is the wrong trait (row 52). New. |
| 42 | SpillFile / SpillWriter / TempFileFactory | §12 E2 | DM-28, DM-29, G5 | D7 records spill as an undiscussed filesystem dependency; this is the seam that makes it an owned one. New. |
| 43 | Partitioning::Range | §12 E4 | DM-35, DM-40 | Preserves ordering across partitions where hash partitioning does not. Relevant only if §5.3’s re-sort ever becomes expensive on a large compiled.* relation. New. |
| 44 | StatisticsContext / partition_statistics / StatisticsProvider | §12 E2, E4 | DM-39, DM-59 | Extends row 6’s Precision discipline to per-partition granularity. Only once §5.4’s statistics are richer than row counts and key min/max. New. |
| 45 | TreeNode implemented for the math IR nodes | §12 E2 | DM-24, DM-22 | A complete, tested generic rewriting framework with fixed-point signalling — exactly what P10, P11 and P12 need. The cost is a dependency from pse-math onto datafusion-common, which §3.2 currently confines to two crates. A genuine architectural question, not a free win. New. |
| 46 | NormalizeEq / Normalizeable / CSEController as prior art | §12 E2 | DM-15 | The engine’s answer to “are these two expression trees semantically equivalent after normalization”. Not directly reusable (it is Expr-shaped), but worth reading before P10’s canonicalizer is written. New. |
| 47 | Extension-type versioning via the registration factory | D10 | DM-44, DM-51 | The factory receives the serialized metadata string, so one registered name can accept several metadata generations and reject the rest. This is where the Arrow map’s long-standing open item can finally be answered. New. |
| 48 | datafusion-ffi | §12 E1 | DM-44, DM-58 | A stable ABI for providers, UDFs and even physical optimizer rules across a dynamic-library boundary. No plugin boundary exists today, so DM-58 says no — it matters only if §22’s extension model ever admits compiled third-party kernel packages. New. |
| 49 | datafusion-tracing (third party) | §12 E2, §12.5 | DM-50 | Instruments plans with tracing spans — D8’s gap, solved. But it is at 55.0.0 against an engine at 55.1.0: an independently-versioned compatibility edge of exactly the kind that produced the num-dual/feos-core split. JoinSetTracer (row 40) is the first-party alternative. New. |
Reject, with reason
| # | Capability | Cluster | Why rejected |
|---|---|---|---|
| 50 | ScalarUDFImpl::with_updated_config / reading config_options in a kernel | D6 | A kernel whose result depends on session config is nondeterministic with respect to everything §5.3 hashes. G4, DM-28. |
| 51 | TableProvider::get_column_default | D1 | Column defaults are an authoring concept the blueprint deliberately does not have — values come from authored relations and case overlays. |
| 52 | SchemaAdapter / SchemaAdapterFactory / SchemaMapper | §12 E2 | Deprecated at 55.1.0 — the trait’s own docs say “This trait has been removed. Use PhysicalExprAdapterFactory instead”. Recorded because it is the obvious search hit for “schema evolution” and is the wrong answer. New. |
| 53 | Substrait as a plan format | D9 | RulePlanSpec is already the authoritative typed versioned plan description. A second one cuts against DM-57 and DM-58. Revision 2’s choice of datafusion-proto is better: an encoding of a plan the platform already has, not a new IR. Reconsider only for cross-engine execution. |
| 54 | SQL strings in the compiler | D9 | Untyped, unversioned, not queryable as structure — against DM-10 and DM-16. SQL stays at the analytics edge. |
| 55 | HigherOrderFunction / Lambda / HigherOrderUDFImpl in plans | D3, §12 E2 | Would push model semantics into Expr, blurring exactly the boundary D6 draws. |
| 56 | AsyncSchemaProvider / async catalog resolution | D1 | The snapshot catalog resolves once at session creation. Recorded to prevent a future “we should be async” drift. |
| 57 | AsyncScalarUDFImpl | §12 E2 | An await point inside expression evaluation. §18.5’s kernels are pure native functions and D10’s boundary forbids I/O there. Recorded because “our kernel needs to call a service” is a plausible future request that should be answered no. New. |
| 58 | FunctionFactory (CREATE FUNCTION from SQL) | §12 E2 | §18.5’s UDFs are generated from KernelSpec. Admitting SQL-defined functions creates a second authority over what a kernel is — G1. New. |
| 59 | Parquet virtual columns (row_number) and the input_file_name UDF | §12 E4 | Provenance derived from physical storage position. §5.1 is explicit that row positions are never identity; these would create a second, position-based identity — G1, DM-11. New. |
| 60 | MERGE INTO (planner, physical planner and TableProvider hook) | §12 E4 | A sixth mutation method. Strengthens row 5 rather than weakening it. New. |
| 61 | datafusion-spark and enable_ansi_mode | §12 E1, E3 | Spark-compatible arithmetic and function semantics. No compatibility requirement exists, and adopting them would silently change numerical behaviour — G2. New. |
| 62 | Morsel / MorselPlanner / Morselizer | §12 E2 | Morsel-driven I/O scheduling for file scans. §5.4 reads mmapped IPC artifacts; there is no I/O scheduling problem. New. |
| 63 | LogicalType for the platform’s quantity types | D10 | Tempting, and wrong. §8’s dynamic registry is the authority for quantity types; DataFusion’s logical-type layer exists to drive function-signature coercion. Implementing it would create a second definition of a quantity type (G1) for no gain the extension registry does not already give. Recorded because it is the obvious next step after row 1. New. |
| 64 | datafusion-table-providers family, -distributed, -postgres, and the external-source integrations | §12.5 | A live external database inside the catalog is a second, mutable authority (G1) and breaks §20.4’s reproduction contract. A distributed tier is machinery for a scale this design does not claim (DM-58). A Postgres wire protocol is a service boundary §3.3 excludes. New. |
| 65 | Dynamic filter pushdown (DynamicFilterTracker and its config keys) | §12 E4 | Runtime-adaptive filters are a scan-performance mechanism for large external datasets; §5.4’s artifacts are small and keyed. New. |
14. Acceptance-gate review (DataFusion)
The previous gate interpretation overclaimed X1, UDF flags and sorted metadata insertion. E1/E2/E5 refute those mechanisms as universal enforcement. The revision-4 review replaces that interpretation; revision 5 specifies the active integrations.
| Gate concern | Revised mechanism | Remaining platform gate |
|---|---|---|
| G2 / G3 | Complete field/quantity admission and generated analyzer checks | Actual import/query/output rejection, including nested and computed fields |
| G4 / G7 | Built-in CASE; implementation-backed UDF policies and binding matrices | Branch/failure and real backend-route conformance |
| G5 / G6 | Shared accounted runtime, complete stage keys, noncanonical plan evidence | Resource/cancellation tests and incremental-versus-clean outputs |
| G7 / regression quality | Full pushed/unpruned result oracle | Deliberate over-pruning must be detected |
Pinned interface availability is Interface-checked; integration stays Proposed until the named behavior is exercised. No numerical maturity total is inferred from this map.
15. Leverage matrix (DataFusion)
| Blueprint requirement | API | Status |
|---|---|---|
| §5.4 catalog of snapshots → namespaces → relations | CatalogProviderList / CatalogProvider / SchemaProvider / TableProvider (all in datafusion-session) | confirmed |
§5.4 scan_with_args implemented, scan delegates | both provided methods; ScanArgs with projection/filters/limit/statistics-requests | confirmed |
| §5.4 exact vs inexact filters | TableProviderFilterPushDown::{Exact, Inexact, Unsupported} | confirmed; Exact is now shown reachable [probe] — key equality arrives as a matchable binary expression (PROBE A). Still unverified by the engine, so row 23’s test wrapper stands — G7 |
§5.4 IN on key columns | typed predicate contract | The two-element OR rewrite was observed; other InList/OR forms require their own supported-shape and full-result checks (review R4-11). |
| §5.4 “Constraints: primary key only” | Constraints | refined — DataFusion validates no constraint but Field nullability, and not on ingest [c7:/apache/datafusion]; the declaration is informational |
| §5.4 primary key only | Constraint::{PrimaryKey, Unique}, Constraints::new_unverified | confirmed; constructor name is the standing warning |
| §5.4 statistics from cached metadata | Statistics, ColumnStatistics, Precision, StatisticsRequest | refined — use Precision and answer requests selectively |
| §5.4 read-only provider over immutable artifacts | five defaulted mutation methods exist | erratum — must be provably unimplemented (G1) |
§14.2 RulePlanSpec → LogicalPlan via LogicalPlanBuilder | full builder surface | confirmed |
| §14.2 recursive closures via recursive CTE | LogicalPlan::RecursiveQuery, to_recursive_query(name, term, is_distinct), CteWorkTable | confirmed |
| §14.2 rule 4 every head row carries a derivation | — | evaluate LogicalPlan::Extension to keep rule_id in the plan (DM-18) |
| §14.2 rule 5 pinned session | ConfigOptions threaded into every analyzer/optimizer rule | confirmed as necessary |
§14.2 rule 5 plan fingerprint (rev 2 F9: datafusion-proto + LogicalExtensionCodec) | logical_plan_to_bytes_with_extension_codec | erratum [probe] — requires a platform codec to encode at all, and the bytes are not reproducible once a field carries ≥2 metadata keys (PROBE C). D9, register rows 2–4 |
| §4.3 “nothing in DataFusion acts on these keys” | ExtensionTypeRegistry, create_extension_type_for_field | erratum [probe] — ARROW:extension:name is acted on: registered types resolve, mismatched storage types are rejected during planning (PROBE X1). D10 |
§4.4 pse.* extension types enforceable by the engine | ExtensionTypeRegistration::new_arc, SessionStateBuilder::with_extension_type_registry | adopt, unmentioned — register row 1, the headline of this edition |
§5.3 “-0.0 is preserved” | engine ORDER BY vs GROUP BY / hash join | refined [probe] — true of ordering, false of grouping and joining, which merge -0.0 with +0.0 and treat NaN as self-equal (PROBE D) |
| §14.2 rule 5 semantic-settings hash by namespace | 155 config keys in 8 namespaces | refined — two semantic keys live under execution.*; use an explicit allow-list (register row 15) |
| §14.2 rule 3 four-valued predicates | Expr::IsUnknown/IsNotUnknown cover three; conflict stays platform-level | partial, by design |
§18.5 ScalarUDFImpl adapter row | 25 methods; all six named by §18.5 confirmed | confirmed |
§18.5 return_field_from_args attaches pse.semantic.* | ReturnFieldArgs { arg_fields, scalar_arguments } | confirmed and stronger — input Fields are supplied, so output quantity type can be derived from input metadata |
| §18.2 domain guards and conditional evaluation | Expr::Case / CaseExpr | Tested E2 library case; generic UDF conditional flags are not lazy execution (ADR-0043/0047). |
§14.3 one SessionContext per snapshot with MemoryPool limits | SessionConfig, RuntimeEnvBuilder, 5 pool implementations | confirmed; pool choice is unmade |
| §14.3 / §18.8 thread budget | target_partitions in SessionConfig | confirmed; budget lives in config, must be validated |
| §20.1 artifact store | ObjectStoreRegistry, ObjectStoreUrl, object_store 0.13.2 shared with the platform | confirmed |
| §20.4 reproduction | TimeProvider seam exists | adopt — unmentioned; DM-28/DM-48 |
| §23.2 failure taxonomy | 20 DataFusionError variants, Diagnostic, Collection | refined — §23.2 needs a DataFusion column |
| §23.1 spans per rule evaluation | engine emits no tracing spans by default | gap, with named candidates — JoinSetTracer and CustomMetricValue (register rows 39, 29) |
| §23.2 typed failures at session construction | SessionConfig::set_str vs ConfigOptions::set | erratum [probe] — the convenient setter panics on an invalid value (PROBE X3); register row 22 |
| §23.1 / §19 machine-readable plans | datafusion.explain.format = pgjson | adopt, unmentioned [probe] — four formats exist, one is structured JSON (PROBE X2); register row 28 |
| §3.1 the “55 forms” | all five confirmed | confirmed (§1.5) |
| §3.1 one release family | umbrella-only pinning mixes versions | erratum (§1.2) |
16. Evidence ledger
| # | Cluster | Instrument | Source | Target | Outcome |
|---|---|---|---|---|---|
| 1 | setup | c7 resolve | DataFusion | Rust query engine, providers, plans, UDFs | /apache/datafusion (5032, High); no docs.rs mirror exists |
| 2 | D1 | c7 query-docs | /apache/datafusion | custom TableProvider, pushdown, statistics | upstream library-user-guide/custom-table-providers.md + upgrading/48.0.0.md; PlanProperties, EmissionType, Boundedness |
| 3 | setup | WebSearch | crates.io/docs.rs/github | latest DataFusion release | reported 55.0.0 — stale, corrected by rows 4–5 |
| 4 | setup | WebFetch | docs.rs/crate/datafusion-expr/latest | latest sub-crate version | 55.1.0 (2026-09-11) |
| 5 | setup | WebFetch | docs.rs/crate/datafusion/latest | latest umbrella version, deps, features | 55.1.0; arrow ^59.2.0, object_store ^0.13.2; full feature list |
| 6 | setup | cargo | generate-lockfile / fetch | coherent =-pinned resolution | DataFusion family all 55.1.0; Arrow all 59.3.0; object_store 0.13.2; tokio 1.53.1 |
| 7 | D1 | rustdoc | datafusion_session@55.1.0 | TableProvider, CatalogProvider(List), SchemaProvider, ScanArgs | 15 TableProvider methods incl. five mutation methods; ScanArgs::with_statistics_requests |
| 8 | D1 | rustdoc | datafusion_catalog@55.1.0 | catalog implementations | MemoryCatalogProvider*, MemTable, ViewTable, CteWorkTable, Async*Provider |
| 9 | D2 | rustdoc | datafusion_expr@55.1.0, datafusion_common@55.1.0, datafusion_expr_common@55.1.0 | pushdown, constraints, statistics | TableProviderFilterPushDown, Constraint, Precision, ColumnStatistics, StatisticsRequest |
| 10 | D3 | rustdoc | datafusion_expr@55.1.0 | LogicalPlan, Expr, LogicalPlanBuilder, UserDefinedLogicalNode | 25 plan variants, 37 expr variants, to_recursive_query, check_invariants, InvariantLevel |
| 11 | D4 | rustdoc | datafusion_optimizer@55.1.0 | OptimizerRule, AnalyzerRule, ApplyOrder | 25 + 3 built-in rule impls; Transformed<T> |
| 12 | D5 | rustdoc | datafusion_physical_plan@55.1.0, datafusion_physical_optimizer@55.1.0, datafusion_expr@55.1.0, datafusion_expr_common@55.1.0 | the §3.1 “55 forms” | all five confirmed — see §1.5 |
| 13 | D6 | rustdoc | datafusion_expr@55.1.0 | ScalarUDFImpl and its argument carriers | 25 methods; ReturnFieldArgs{arg_fields,…}, ScalarFunctionArgs{…,config_options}, PreimageResult, ExprSimplifyResult |
| 14 | D7 | rustdoc | datafusion_execution@55.1.0 | runtime, memory, spill, cache, time | MemoryPool (+5 impls), DiskManager, CacheManager, TimeProvider, SchemaFingerprint, ObjectStoreRegistry |
| 15 | D8 | rustdoc | datafusion_common@55.1.0 | error and diagnostic surface | 20 DataFusionError variants incl. Diagnostic/Collection; Diagnostic::{new_error,new_warning,add_note,add_help} |
| 16 | §1.1 | crates.io API | crates.io/api/v1/crates/datafusion | is the pin stale? | no — 55.1.0 (2026-09-11) is still the current release at this compile [crates.io:datafusion] |
| 17 | §1.1, §12 E1 | GitHub raw | apache/datafusion@55.1.0/Cargo.toml | the E1 denominator; which Arrow does the engine build against? | 44 workspace members (~37 publishable libraries); arrow = "59.2.0" — the platform pin runs one minor ahead [gh:apache/datafusion@55.1.0/Cargo.toml] |
| 18 | all | rustdoc | locally generated, full family, =-pinned, lockfile committed | complete API surface | 57 targets, 57 OK, 0 FAIL, format_version 61; DataFusion family 37 crates all 55.1.0. Supersedes the previous edition’s 20-crate extraction, whose output was not preserved. Receipt §1.4 |
| 19 | §12 E2 | facts corpus | build/facts/df551 (normalized from row 18) | the E2 denominator | 169 public traits, 76,771 declarations, 48,968 impl relations [rustdoc:df551@55.1.0] |
| 20 | D2 | probe | df_probe.rs — a recording TableProvider | what filter shapes reach a provider on a FixedSizeBinary(16) key? | PROBE A — key equality arrives as a matchable binary expression (resolves the previous edition’s must-settle item 1 and §26 F17’s precondition); IN pre-rewritten to OR; conjunctions split; IS NOT NULL eliminated on a non-nullable column; the method is called more than once |
| 21 | D9 | probe | df_probe.rs, df_probe_c3.rs | is datafusion-proto usable and stable as a plan fingerprint? | PROBE C — fails without a codec on custom providers; unstable at ≥2 field-metadata keys (6 distinct encodings in 6 runs at 5 keys); stable when one Schema object is reused, isolating HashMap iteration order as the cause |
| 22 | D3 | probe | df_probe_b.rs | does a UserDefinedLogicalNode survive the optimizer carrying rule_id? | PROBE B — yes, intact and still rendered; the optimizer pushes projections down through it; physical planning needs an ExtensionPlanner and says so with a typed error |
| 23 | D3, §14 | probe | df_probe.rs | null / NaN / -0.0 through sort, GROUP BY and equi-join | PROBE D — ORDER BY preserves totalOrder; GROUP BY merges -0.0 with +0.0 (4 groups from 5 rows); hash join treats NaN as self-equal |
| 24 | D10 | probe | df_probe_x.rs | can a pse.* type be registered and resolved? what happens to an unregistered one? | PROBE X1, direct helper only — 7 canonical types preloaded; registration and resolution work; wrong storage type rejected with a typed planning error; unregistered name errors |
| 25 | D10 | probe | df_probe.rs | does ARROW:extension:name survive planning and execution? | PROBE E — survives projection, aliasing and use as a GROUP BY key |
| 26 | D4, D8 | probe | df_probe_x.rs | which EXPLAIN formats exist; how is an invalid config value reported? | PROBE X2 — indent, tree, pgjson, graphviz all accepted. PROBE X3 — SessionConfig::set_str panics; ConfigOptions::set returns a typed Err |
| 27 | D7 | probe | df_probe.rs | bounded MemoryPool — typed failure or process death? | PROBE H — a typed, actionable error naming datafusion.runtime.memory_limit and datafusion.execution.sort_spill_reservation_bytes |
| 28 | §12 E3 | GitHub raw | apache/datafusion@55.1.0/docs/source/user-guide/configs.md | the E3 denominator | 155 keys in 8 namespaces; datafusion.execution 71, .optimizer 39, .sql_parser 10, .format 9, .explain 9, .runtime 8, .catalog 8, .spark 1 [gh:apache/datafusion@55.1.0/docs/source/user-guide/configs.md] |
| 29 | §12 E4 | GitHub raw | apache/datafusion@55.1.0/dev/changelog/55.0.0.md | the E4 delta | IEEE 754 negative-zero semantics; UDF null-propagation strictness used for outer-join elimination; EnsureRequirements; pgjson EXPLAIN; Partitioning::Range; pluggable SpillFile; StatisticsContext; MERGE INTO; Parquet virtual columns [gh:apache/datafusion@55.1.0/dev/changelog/55.0.0.md] |
| 30 | D2, D10 | c7 query-docs | /apache/datafusion | table-constraint enforcement; TypePlanner idiom | constraints are “provided for informational purposes … not validated or used during query planning”; only Field nullability is enforced. TypePlanner upstream example maps SQL UUID → FixedSizeBinary(16) + ARROW:extension:name — the same shape as pse.semantic_id |
| 31 | §12.5 | crates.io API | crates.io search, q=datafusion | third-party extenders worth adjudicating | datafusion-table-providers 0.13.1, datafusion-tracing 55.0.0 (one minor behind the engine), datafusion-distributed 4.0.0, datafusion-postgres 0.18.0, datafusion-ffi 55.1.0 |
17. Open items and recommended blueprint amendments
| Item | Revision-5 decision | Remaining acceptance |
|---|---|---|
| Extension registration versus admission | ADR-0039; recursive validator and explicit analyzer/provider/output boundaries | E1-derived negative boundary matrix |
| Conditional UDF execution | ADR-0043/0047; CASE and native branch regions | Mixed-row, nested, optimizer and derivative conformance |
| Rule-head casting | ADR-0039; exact numeric and complete physical admissibility | E3-derived rejection and explicit conversion cases |
| Exact pushdown | ADR-0048; shared physical predicates and complete-result oracle | Soundness and completeness, including projections/limits/IN/nulls |
| Aggregate/unnest | ADR-0048; bounded typed payloads for current consumers | Independent expansion reference, empty/null/order/provenance fixtures |
| Plan fingerprints | ADR-0044; noncanonical checksummed diagnostics, outside semantic keys | Codec round-trip/attribution; E5 is not a determinism guarantee |
| Stage reuse | ADR-0040–0042; complete immutable inputs and initial whole-stage memo | Dependency matrix; R-22/R-01 before finer mechanisms |
| Memory pools | ADR-0046; shared runtime, platform try_grow, separate process metrics | Two snapshots, non-spillable/platform consumers, cancellation |
| UDF scalar fast paths | ADR-0043; field-aware input checks and scalar preservation | Scalar/mixed/batch cardinality, units, outcomes and allocations |
| Predicate preimages | R-25; exact equivalent supported predicates | Proof/fixtures before pruning; measured total benefit |
Review E1–E6 and L1–L9 have retained source and receipts. Context7 supplied discovery leads; the pinned source and executed characterization bound the claims. No original extraction/probe corpus was regenerated here.
Supporting Rust libraries
status: evidence-map blueprint_revision: 5 pins: Cargo.lock regenerated: null reviewed: 2026-09-13
Supporting Rust libraries — capability map
Current binding: blueprint revision 5. The original survey and version/probe receipts below retain their historical scope; they were not regenerated in this update. Earlier revision comparisons and parked recommendations are historical. The revision-4 review and ADR-0039–ADR-0048 determine the corrected design bindings below. Platform integration remains Proposed; interface and probe evidence are labeled at their original scope. No Python behavioral corpus was re-audited by this revision.
| Current binding | Decision and limit | Evidence / authority |
|---|---|---|
| Artifact-hash memo | Whole-stage inputs first; finer bundles and salsa require R-22/R-01 evidence | ADR-0040–0042; review R4-06/07 |
| Hash ownership and identity | v2 canonical logical hash and a separate encoded checksum; semantic-ID contexts retained | ADR-0045; review E4 |
| Object storage | Verify existing objects and complete manifest membership before conditional publication | ADR-0045; blueprint §20.1 |
| Kernel-local differentiation | Declared derivative implementation/validity; no inference from scalar signature | ADR-0043/0047; actual backend conformance remains open |
Companion to docs/authoritative_design/blueprint.md §3.3 (supporting libraries and the boundary each must respect), and the fourth of four capability maps — the other three are arrow-rust.md, datafusion-rust.md and python-libraries.md.
Compiled 2026-09-13 against the versions in §1. Historical survey adjudicated blueprint revision 2 (2026-09-13). The previous edition adjudicated revision 1 and never said so.
0. Purpose, scope, and evidence rules
0.1 What this document is
Blueprint §3.3 names sixteen supporting-library rows covering 24 Rust crates (the seventeenth row is Python and belongs to the Python map), each with a one-line role and a one-line boundary. That is enough to justify a dependency and not enough to build against. This document answers, per crate: what the library can actually do, which of its APIs the blueprint’s stated requirements land on, what we deliberately will not use, and where the blueprint has assumed a capability the library does not provide.
A second pass asks what these libraries offer that the blueprint has not claimed, which would improve alignment with design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01…DM-60, gates G1–G7). A third (§13) enumerates each library independently and asks what is being left on the table.
(The previous edition said “twenty-two crates” while its own anchor table had 25 rows. The count above is reconciled against §3.3: 24 Rust crates plus the Ipopt C API, which is not a crate.)
0.2 Evidence rules
Every API name, type, signature or behavioural claim carries a provenance marker. A claim with no marker is a defect in this document.
| Marker | Meaning |
|---|---|
[rustdoc:crate@version] | the authoritative lane — locally generated rustdoc JSON from the pinned crates; receipt in §1 |
[probe] | a program compiled against the pinned crates, used where documentation asserts behaviour that can simply be measured |
[c7:/org/project] | context7, naming the library ID that produced the answer |
[docs.rs:crate@version] | fetched directly from docs.rs — used where context7 has no coverage, or to pin a version |
[crates.io:crate] | registry metadata: release dates, version currency, reverse dependencies |
[rustsec:ID] | the RustSec advisory database |
[gh:repo@tag/path] | upstream source read at an exact tag — used for the Ipopt C headers, which have no crate |
[UNVERIFIED] | no authoritative source found; carried as a risk, never filled in from recall |
The previous edition also declared a [bp:§n] marker it never used, and used a [c7 resolve: …] form it never declared. Both are gone: blueprint assertions are named in prose, and context7 resolutions are ledger rows.
The [ref:] marker has been retired. It pointed at docs/library_ref/, which has been deleted. Every one of the previous edition’s 35 [ref:] citations has been re-sourced against live evidence or removed with its claim — §0.4 records what that cost.
Status words in the “what the blueprint binds to” tables mean:
- leverage — the library provides this; use it directly.
- build on top — the library provides a foundation but not the capability; platform code is required and its cost is stated.
- confirmed — a specific claim the blueprint makes about a library is true as stated.
- refined / erratum — the claim is wrong or imprecise; the correction is given and the affected blueprint section named.
- open item / open decision — a question this lookup surfaced that must be answered before or during implementation.
0.3 Status vocabulary
Status words in the “what the blueprint binds to” tables mean:
- leverage — the library provides this; use it directly.
- build on top — the library provides a foundation but not the capability; platform code is required and its cost is stated.
- confirmed — a specific claim the blueprint makes about a library is true as stated.
- refined / erratum — the claim is wrong or imprecise; the correction is given and the affected blueprint section named.
- adopt / evaluate / reject — Pass-2 and Pass-3 recommendations, disciplined by charter §G (common false positives) and DM-58: a recommendation that names no principle and replaces no identified hand-written work does not appear.
- open decision — a question this lookup surfaced. §18 gives each one a recommendation and the evidence; the decision itself remains the owner’s.
0.4 What the deleted reference corpus cost, and what replaced it
The previous edition drew on docs/library_ref/, a corpus of local deep-dives. That directory has been deleted. Thirty-five citations across eleven files became unresolvable, and with them the sole support for three of the ten recommended blueprint amendments.
Every one has been re-sourced or removed. The outcome:
| Deleted source | Citations | Outcome |
|---|---|---|
petgraph.md | 10 | re-sourced verbatim from petgraph 0.8.3 rustdoc, and measured by PROBE 3. Amendment B stands, with one fact the deleted reference did not carry. |
ipopt_standalone.md | 6 | re-sourced from IpStdCInterface.h at COIN-OR tag releases/3.14.16. Every claim confirmed; amendment H’s function names were wrong and are corrected. |
arrow_rust_59_datafusion55_advanced_reference_2026-08-23.md | 6 | re-sourced — and the claim did not survive. The object_store trait surface it described does not exist at the pinned 0.13.2. See C9.2 and §18. |
rust_codegen_syn_quote_proc_macro2_prettyplease.md | 4 | re-sourced from rustdoc and crates.io. Pins confirmed, one drifted (syn 3.0.3 → 3.0.5). |
datafusion-tracing.md | 2 | re-sourced — and amendment D is currently unsatisfiable. Both crates exist; neither has a 55.1.0 release to match the pinned engine. |
rust_parallel_concurrency_stack_reference_2026-08-19.md | 2 | re-sourced from crates.io. Pins confirmed. |
ipopt_solver_ontology.md, scip_ipopt_options_and_metrics.md, datafusion_calculations_rust.md, datafusion_55_arrow_59_2_…narrative, rust_mir_cpg_continuous_reference | 5 | deleted. The previous edition described these itself as “mentions only, not a deep-dive” and “a usage, not a deep-dive”. No claim rode on any of them. |
Two corrections this exercise forced on the previous edition’s own text, beyond the amendments:
- It claimed “this cluster produced the document’s one blueprint erratum” (C3,
serde_yaml) and then issued a second one eleven sections later (C4, petgraph). This edition carries five, and counts them. - Its
object_storetrait surface was taken from a reference describing a different version than the one §3.1 pins. That is precisely the failure mode an unresolvable citation hides.
0.5 Coverage
Where the previous edition relied on context7 breadth, this one leads with locally generated rustdoc: 26 crates extracted at the pinned versions, 190,463 declarations, 589 public traits (§1.5). context7 and docs.rs remain for idiom and intent. Three crates that context7 does not carry at all — num-dual, serde_arrow, feos-core — are now covered by the authoritative lane rather than by fallback.
One named exclusion: the Ipopt C API has no crate and needs a native library, so it is sourced from the upstream header at an exact tag [gh:coin-or/Ipopt@releases/3.14.16] rather than by extraction.
0.6 Version anchoring
Anchors in §1 are pinned exactly and resolved into a committed lockfile, not proposed. MSRV/edition-2024 compatibility against the platform’s Rust 1.94 floor (§3.1) is recorded where a crate states one.
1. Version anchors, the pin gap, and the receipts
1.1 The pin gap — the largest single finding in this map
Blueprint §3.2 line 277 states that the workspace manifest “carries every pin once”. Blueprint §3.1 pins five components: the DataFusion family, the Arrow family, object_store, pyo3/pyo3-arrow, and tokio.
Of the 24 supporting Rust crates §3.3 names, exactly one — tokio — has a version anchored anywhere in the blueprint. The other 23 have none. §26 F11 acknowledges two of the 23 (serde_yaml, num-dual); the remaining 21 are unrecorded.
That includes blake3, which is the most deeply wired supporting crate in the design — roughly twenty use sites spanning semantic-ID derivation (§5.1), content hashing (§5.3), hash-consing (§7.4), the plan fingerprint (§14.2) and the manifest (§20.2) — and which is additionally assigned to no crate in §3.2 and used at two digest widths (128-bit IDs, 256-bit content hashes) with no derivation mechanism named.
The table below is therefore offered as a §3.1 amendment the blueprint can adopt wholesale. Every version was resolved into the committed lockfile of §1.5, not proposed.
1.2 Anchors
| Crate | Pinned here | §3.1 today | Note |
|---|---|---|---|
serde | 1.0.229 | — | substrate; derive + rc |
serde_arrow | 0.15.0 | — | features arrow-53…arrow-59; no arrow-60 yet, so an Arrow 60 move waits on this crate [crates.io:serde_arrow]. Interposes marrow ^0.3.0 |
syn | 3.0.5 | — | previous edition said 3.0.3; syn 3, not the syn 2 most of the ecosystem still uses |
quote | 1.0.47 | — | |
proc-macro2 | 1.0.107 | — | span-locations for source spans in generated code |
prettyplease | 0.3.0 | — | matched to syn 3 |
serde_yaml | rejected | — | 0.9.34+deprecated, last released 2024-03-25. See C3.1 and §18 Q1 |
serde-saphyr | 1.2.0 | — | recommended replacement (§18 Q1); released 2026-08-30 |
toml | 1.1.6+spec-1.1.0 | — | 1.x stable; Spanned is load-bearing (C3.2) |
winnow | 1.0.4 | — | already in the graph via toml |
petgraph | 0.8.3 | — | pre-1.0; pin exactly (C4) |
salsa | 0.28.2 | — | §26 F13 is a live decision (C5) |
egglog | 3.0.0 | — | optional, phase 4. Three majors in ten months (C6) |
num-dual | 0.15.0 | — | conflicts with feos-core — §1.3 |
faer | 0.24.4 | — | |
uom | 0.38.0 | — | MSRV 1.68 |
feos-core / feos | 0.10.1 | — | optional, phase 4; pulls quantity 0.14.1 and num-dual 0.14.2 |
diffsol | 0.16.2 | — | optional; pulls diffsol-la/diffsol-nl, not faer (C8.2) |
blake3 | 1.8.7 | — | rayon feature for §18.8’s budget (C9.1) |
rayon | 1.12.0 | — | |
tokio | 1.53.1 | 1.52+ | the one anchored crate; consistent |
tracing | 0.1.44 | — | |
thiserror | 2.0.20 | — | |
miette | 7.6.0 | — | does not depend on thiserror [crates.io:miette], so the 1.x/2.x split is not a concern here |
| Ipopt C API | ≥ 3.14 | — | own -sys crate; sourced from IpStdCInterface.h [gh:coin-or/Ipopt@releases/3.14.16] |
1.3 Measured: the num-dual split is real, and worse than a duplicate
Resolving the manifest above produces 362 packages with 17 names at more than one version, and one of them is load-bearing [probe]:
num-dual 0.14.2, 0.15.0 <- type-identity hazard
thiserror 1.0.69, 2.0.20
syn 1.0.109, 2.0.119, 3.0.5
Reverse dependencies, from the lockfile:
feos-core 0.10.1 -> num-dual ^0.14 (resolved 0.14.2)
quantity 0.14.1 -> num-dual ^0.14 (resolved 0.14.2)
<this manifest> -> num-dual =0.15.0
Three facts follow, and the third is the one that matters.
-
The split is entirely feos’s. Only
feos-coreand its ownquantitydependency want 0.14. Nothing else in a 362-package graph does. -
feos-core0.10.1 sits one minor behind on both of its key dependencies —num-dual ^0.14against 0.15.0, andquantity ^0.14against 0.15.0. -
The trait’s shape changed between the two, so the kernel source differs, not merely the type. From the extraction
[rustdoc:num-dual@0.14.2],[rustdoc:num-dual@0.15.0]:0.14.2 trait DualNum<F>: … NumOps<F> … From<F> … DualStruct<F, Real = F> … 0.15.0 trait DualNum: … NumOps<Self::Primitive> … From<Self::Primitive> … DualStruct<Real = Self::Primitive> …§18.5 promises “the same body monomorphized over
num_dual::DualNum”. A kernel written for 0.14 is boundedD: DualNum<f64>; the same kernel for 0.15 is boundedD: DualNum<Primitive = f64>. Those are different source files. The choice is not “which version do we depend on” but “which signature does every kernel body inpse-kernelscarry”.A secondary confirmation, from this extraction’s own tooling:
cargo rustdoc -p num-dualfails outright with “specificationnum-dualis ambiguous” once both versions are present. The hazard is visible to cargo before it is visible to the type checker.
1.4 Supply chain and maintenance
A lane the Arrow and DataFusion maps do not need: those are one well-run Apache project each, whereas this map’s surface is 24 independently-owned crates.
RustSec advisories touching this set — three, all patched below the versions in play [rustsec:RUSTSEC-2018-0005], [rustsec:RUSTSEC-2023-0078], [rustsec:RUSTSEC-2024-0358]:
| Advisory | Crate | Patched | Our version |
|---|---|---|---|
| RUSTSEC-2018-0005 | serde_yaml | ≥ 0.8.4 | 0.9.34 — unaffected |
| RUSTSEC-2023-0078 | tracing | ≥ 0.1.40 | 0.1.44 — unaffected |
| RUSTSEC-2024-0358 | object_store | ≥ 0.10.2 | 0.13.2 — unaffected |
cargo audit is therefore clean today. And that is the finding, because:
serde_yaml’s deprecation is invisible tocargo audit. It carries no RustSec unmaintained advisory. The deprecation is encoded only in the semver build metadata —0.9.34+deprecated— and a repository banner, neither of which any standard tool interprets. It still takes ~90 million downloads a quarter[crates.io:serde_yaml].
So a clean cargo audit is not evidence that the dependency set is maintained. §24’s supply-chain gate needs cargo deny’s maintenance and ban checks, not cargo audit alone — otherwise the one dependency this map flags as an erratum passes CI silently.
Maintenance recency, worth stating because none of it is an advisory [crates.io:*]:
| Crate | Last release | Quiet |
|---|---|---|
serde_yaml | 2024-03-25 | ~30 months — rejected |
ipopt / ipopt-sys | 2024-12-14 | ~21 months — supports the blueprint’s “own -sys crate” (C11) |
miette | 2025-04-27 | ~16 months |
petgraph | 2025-09-30 | ~11 months |
1.5 Extraction receipt
toolchain rustc 1.100.0-nightly (809936eac 2026-09-12)
flags -Z unstable-options --output-format json
format_version 61
targets 25 requested; 3 needed explicit name@version specs (see below)
documents 26 (both num-dual majors extracted, for §1.3)
lockfile 362 packages, committed
facts build/facts/support — 190,463 declarations, 589 public traits,
113,713 impl relations
Three targets — syn, num-dual, thiserror — could not be documented by bare crate name because each resolves at more than one version; they were re-run as name@version. That is §1.3’s hazard showing up in the build tooling.
Committed under docs/capability-maps/evidence/rust/: support-Cargo.toml, support-Cargo.lock, gen_rustdoc_support.sh, rustdoc_gen_support.log, the five probe programs, and probe_output_support.txt.
1.6 Probe receipt
The previous edition made no measurements at all. Five programs, eight probes:
| Probe | Settles | Cluster |
|---|---|---|
| 1 | blake3 — derive_key vs truncation, and whether 16-byte truncation is sound | C9.1, §18 Q4 |
| 2 | serde_arrow — what a Rust record actually traces to, and what metadata it carries | C2.1, §18 Q5 |
| 3 | petgraph — SCC order stability, matching direction-independence, feedback arc sets | C4, amendment B |
| 4 | num-dual — implicit_derivative against a hand-derived implicit function theorem | C7.1, amendment G |
| 5 | faer — the matrix-free operator contract, and which end of the spectrum it targets | C7.2, §18 Q7 |
| 6 | salsa — backdating, accumulators, durability | C5, F13 |
| 7 | serde-saphyr and toml::Spanned — spans and panic-freedom at the authoring boundary | C3, §18 Q1 |
| 8 | egglog — extraction reproducibility | C6, §18 Q10 |
2. Code generation — syn, quote, proc_macro2, prettyplease (C1)
Re-sourced. The previous edition rested this cluster on a 172 KB local deep-dive that has since been deleted. Every claim below is now taken from the pinned crates’ own rustdoc [rustdoc:syn@3.0.5], [rustdoc:quote@1.0.47], [rustdoc:proc-macro2@1.0.107], [rustdoc:prettyplease@0.3.0] and from the registry [crates.io:syn]. One anchor had drifted: syn is at 3.0.5, not 3.0.3.
Version anchors [crates.io:syn]:
| Crate | Pinned version | Role |
|---|---|---|
syn | 3.0.3 | parse Rust tokens/source into typed syntax trees; inspect, visit, mutate, fold, print |
quote | 1.0.47 | produce proc_macro2::TokenStream with interpolation, repetition, identifier construction, span control |
proc-macro2 | 1.0.107 | compiler-compatible token trees and spans usable outside proc macros — in build.rs, libraries, tests and binaries |
prettyplease | 0.3.0 | pretty-print a syn 3 File into readable generated source |
Note the major version: syn 3 (with prettyplease 0.3 matched to it), not the syn 2 most of the ecosystem still uses. Any example found elsewhere may target syn 2; the local reference is the authority here.
What the blueprint binds to
| Blueprint requirement | API / pattern | Status |
|---|---|---|
§4.2 “pse-schema runs as a build step (a non-macro syn/quote/prettyplease pipeline)” | the non-macro codegen pipeline — proc_macro2::TokenStream built with quote!, parsed to syn::File, emitted with prettyplease::unparse | leverage; proc_macro2 exists precisely so this works outside a proc-macro crate [rustdoc:proc-macro2@1.0.107] |
§4.2 “emitted source is committed under pse-relations/src/generated/ and diffed in CI” | prettyplease’s “deterministic-enough” formatting contract (§20) | leverage, with a caveat: a formatting change in prettyplease produces a diff with no semantic change. Pin prettyplease exactly and treat its upgrade as a regeneration commit. |
| §4.2 generated views, builders, schemas, validators, providers, migrations, docs, JSON Schema | quote! with repetition (§16), format_ident! for generated names (§18) | leverage |
§4.2 the generated try_from(&RecordBatch) that “checks the schema fingerprint” | ordinary generated code | platform logic, not a library feature |
§23.2 authoring.* failures in generator input | syn::Error (§11), quote_spanned! for diagnostic span targeting (§19) | leverage for generator errors; note these are build-time developer errors, distinct from §23.2’s runtime failure taxonomy |
What we deliberately do not use
- No procedural macros. §4.2’s parenthetical “(non-macro pipeline)” is a deliberate rejection of the derive-macro model, and it is the right one for this platform: a derive macro hides generated code from review, defeats §4.2’s “emitted source is committed and diffed in CI”, and makes the
no_shadow_structs.rsgovernance check impossible to write against real symbols. The local reference covers derive/attribute/function-like pipelines (§22–§24) — those sections are explicitly out of scope. - The registry is authoritative, the generator is a projection. §3.3’s boundary. A generator that reads anything but
reference.schema_*has introduced a second source of truth.
Gaps and risks
prettypleaseformatting stability across versions is the only real operational risk (above).syn 3’s feature flags (§7) gate large parts of the AST; enable deliberately —fullis convenient and slow to compile, and a generator emitting only items may not need it.
3. Boundary serialization — serde, serde_arrow (C2)
C2.1 serde_arrow — Rust records ⇄ Arrow batches under a declared schema
Proposed anchor: 0.15.0 (released 2026-08-09) [docs.rs:serde_arrow@0.15.0]
Arrow compatibility: feature flags arrow-53 … arrow-59; when several are enabled the highest wins [docs.rs:serde_arrow@0.15.0]. The blueprint’s Arrow 59.2 pin (§3.1) is therefore supported — this was the single largest compatibility risk in the cluster and it clears.
Role: §4.2 — a SerdeArrowSchema is a generated artifact of the registry, consumed by import/export adapters and the Python contract layer; §1.3 places serde_arrow adapters directly beneath the schema registry.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Schema type | schema::SerdeArrowSchema — “a collection of fields as understood by serde_arrow” | [docs.rs:serde_arrow@0.15.0] |
| Schema construction | sealed trait schema::SchemaLike with from_value(), from_type::<T>(TracingOptions), from_samples() | [docs.rs:serde_arrow@0.15.0] |
Implementors of SchemaLike | Vec<FieldRef>, Vec<Field>, and SerdeArrowSchema itself | [docs.rs:serde_arrow@0.15.0] |
| Arrow conversion | to_record_batch(fields, items) -> RecordBatch, from_record_batch(batch) -> Vec<T>, to_arrow(fields, items), from_arrow(arrays) | [docs.rs:serde_arrow@0.15.0] |
| Engine-neutral conversion | to_marrow/from_marrow over marrow::array::Array — the recommended path for libraries that do not want to pin an arrow major | [docs.rs:serde_arrow@0.15.0] |
| Incremental building | ArrayBuilder with push(item) / build(), plus from_arrow()/from_marrow() initializers | [docs.rs:serde_arrow@0.15.0] |
| Serde plumbing | Serializer (wraps ArrayBuilder), Deserializer | [docs.rs:serde_arrow@0.15.0] |
| Type-mismatch escape hatch | schema::Strategy enum — “strategies for handling types without direct match between arrow and serde”, annotated per field | [docs.rs:serde_arrow@0.15.0] |
| Strategy metadata key | schema::STRATEGY_KEY — the Arrow field-metadata key under which a strategy is stored | [docs.rs:serde_arrow@0.15.0] |
| Per-field overrides | schema::Overwrites — opaque field-path → field-definition mapping | [docs.rs:serde_arrow@0.15.0] |
| Schema tracing control | schema::TracingOptions | [docs.rs:serde_arrow@0.15.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§4.2 “SerdeArrowSchema derived from the registry (never inferred from samples)” | SchemaLike is implemented for Vec<FieldRef> [docs.rs:serde_arrow@0.15.0], so the generated Arrow Schema’s fields are the serde_arrow schema — no tracing step exists in the path at all | confirmed, and cleaner than stated: the blueprint frames this as a discipline (“never inferred”); the API makes it the natural construction. from_samples/TracingOptions simply never appear in production code. |
| §4.2 import/export adapters over generated record types | to_record_batch / from_record_batch with the generated schema’s fields | leverage |
| §21.5 typed Python contracts across the Arrow boundary | Arrow RecordBatch is the handoff; serde_arrow is the Rust-side half only | leverage, boundary unchanged |
Row-at-a-time authoring output (§7.7 parser → authored relations) | ArrayBuilder::push / build | leverage — a serde-driven alternative to the generated typed builders, for adapter code only |
Measured (PROBE 2) — why §3.3’s boundary is not optional
§3.3’s boundary for this row reads “Explicit schemas from the registry; no inference from sample data in production”, and §4.2 says the SerdeArrowSchema is “derived from the registry (never inferred from samples)”. Measured against a Rust record shaped like a pse.* relation row [probe]:
PROBE 2 struct Row { id: [u8; 16], v: f64, label: String }
field id type = Struct([16 x UInt8 fields named "0".."15"])
metadata = {"SERDE_ARROW:strategy": "TupleAsStruct"}
field v type = Float64 metadata = {}
field label type = LargeUtf8 metadata = {}
round-trip equal : true
Two findings, and the first is the more serious.
Traced types are not the registry’s types. A [u8; 16] — the natural Rust shape for a pse.semantic_id — traces to a struct of sixteen UInt8 fields, not FixedSizeBinary(16). A String traces to LargeUtf8, not Utf8. Nothing here is wrong of serde_arrow; tracing infers from the Rust type, which is what §4.2 forbids. But it makes the boundary concrete: if anyone ever calls from_type/from_samples in a production path, the resulting schema is structurally incompatible with §4.4’s extension types, and the failure is a wrong storage type rather than an error. The governance grep §4.2 already implies should name SchemaLike::from_type and from_samples explicitly.
And the adapter stamps its own metadata. The traced field carries SERDE_ARROW:strategy = "TupleAsStruct", which survives into the RecordBatch. That is open question 5’s concern, confirmed: a batch that arrived through the adapter carries field metadata a registry-built batch does not. Since §5.3 hashes the canonical IPC encoding, which includes field metadata, two logically identical relations would hash differently depending on their provenance.
The resolution follows from the first finding rather than needing a new rule: because the registry supplies the schema, no SERDE_ARROW:* key should ever appear on a platform relation. Recommendation for Q5: treat any SERDE_ARROW:* key on a relation schema as a contract violation — a governance check, not a hashing-policy decision.
What we deliberately do not use
from_samplesandfrom_typeare banned in production paths. §3.3’s boundary (“explicit schemas from the registry; no inference from sample data in production”) maps exactly onto never calling these two constructors. A governance grep forfrom_samples(/from_type::<outside test code enforces it cheaply — recommend adding it beside the existingno_shadow_structs.rscheck (§4.2).- Not the
marrowpath.to_marrow/from_marrowexist so libraries can avoid pinning an Arrow major; the platform deliberately pins Arrow 59.2 workspace-wide (§3.1) and wants the realRecordBatch, so thearrow-59feature andto_record_batch/from_record_batchare the right surface. - serde_arrow is not the relation writer. Generated typed builders and views (§4.2) remain the interior path; serde_arrow serves import/export adapters at the edges, where the peer is a serde type rather than a relation.
Gaps and risks
- Field-metadata key collision — a real design item. serde_arrow writes its own key,
STRATEGY_KEY, into Arrow field metadata[docs.rs:serde_arrow@0.15.0], the same namespace §4.3 reserves forpse.semantic.*andARROW:extension:name. Two consequences to settle: (1) §4.3’s “metadata is a carrier” rule must tolerate a foreign key on fields that pass through serde_arrow; (2) §5.3 step 2 strips volatile metadata before hashing —STRATEGY_KEYmust be classified explicitly as volatile (stripped) or contractual (kept), or two logically identical relations could hash differently depending on whether they arrived through an adapter. Open decision. - Extension types. The docs reference support for field metadata, extension types,
FixedSizeBinaryand dictionary encoding, but the status detail lives in a separate support-matrix module that was not fetched. Since every blueprint extension type (§4.4) is backed byFixedSizeBinary,Struct,ListorDictionarystorage, the risk is low — but the round-trip ofpse.semantic_id(FixedSizeBinary(16)) andpse.quantity_value(a struct) through serde_arrow should be a spike before committing, withOverwritesas the fallback for forcing a field’s definition. TracingOptionsandStrategyare needed only when a Rust type has no direct Arrow match. If the generated record types are shaped from the registry, this should never fire; aStrategyappearing in production is a signal the generated type diverged from the relation.
C2.2 serde — the trait layer underneath
Role: §3.3 pairs serde with serde_arrow for schema-governed conversion at import/export boundaries; it is also the derive layer under serde_yaml/toml (C3) and appears as an optional feature of num-dual (C7) and uom (C7).
No capability lookup was performed and none is warranted: serde is the Rust serialization substrate, its surface (Serialize/Deserialize/derive) is stable and universally documented, and the blueprint asks nothing unusual of it.
What matters is the boundary, which is entirely a platform rule:
- A
#[derive(Serialize, Deserialize)]struct that mirrors a relation’s column list is a governance failure under §4.2 (“nothing hand-written may duplicate a column list”), regardless of which crate consumes it. Theno_shadow_structs.rscheck must therefore cover serde-derived types, not only plain structs. - serde types are legitimate for documents (package/case YAML and TOML, §22.1), manifests (§20.2 JSON), and external formats (IDAES
to_jsonv4 import, §20.5) — shapes that are genuinely not relations.
4. Document loading and the expression DSL — serde_yaml, toml, winnow (C3)
This cluster produced the document’s one blueprint erratum: serde_yaml is no longer maintained.
C3.1 serde_yaml — deprecated; a replacement decision is required
Current release: 0.9.34+deprecated [docs.rs:serde_yaml@0.9.34+deprecated]. The documentation carries a banner stating verbatim: “(This project is no longer maintained.)”
§3.3 names serde_yaml for package/case document loading and §22.1 makes YAML a first-class authoring format. Adopting an unmaintained crate for the authoring boundary — the platform’s outermost trust boundary, parsing user-supplied documents — is the one place where “unmaintained” is not a cosmetic concern: it means no security fixes for a parser fed untrusted input.
Verified replacement candidates (existence and coverage confirmed; selection not made):
| Candidate | Positioning | Coverage signal |
|---|---|---|
serde_norway | maintained fork of serde_yaml, near-drop-in | [c7:/websites/rs_serde_norway] — 5689 snippets, High reputation, benchmark 81.17 |
serde-saphyr | strongly typed, panic-free YAML deserializer; converts YAML directly into Rust types with no intermediate Value | [c7:/bourumir-wyngs/serde-saphyr] — 355 snippets, benchmark 83.96 |
serde_yml | fork of serde_yaml under active publication | [c7:/websites/rs_crate_serde_yml] — 69 snippets, benchmark 75.22 |
Recommendation to evaluate, not to adopt blind: serde-saphyr’s two stated properties — panic-free, and no intermediate structure — are exactly what an authoring-boundary parser wants, since §23.2 requires authoring.parse to be a typed failure with a source span, never a panic. serde_norway is the lower-effort path if drop-in compatibility outweighs that. Open decision requiring a spike; whichever wins, §3.3’s table needs updating.
Boundary (unchanged by the substitution): §3.3 — “parser output is authored relations; no evaluation during parsing.”
Measured (PROBE 7) — the replacement decision, settled by evidence
Open question 1 asked which YAML crate replaces serde_yaml, and set the criterion itself: §23.2 requires typed failures with spans rather than panics, so span support should decide it. Measured [probe]:
PROBE 7b serde-saphyr 1.2.0
valid input : parsed ok
type error : rejected, with
error: line 2 column 10: invalid u32
--> <input>:2:10
|
1 | id: pkg.demo
2 | version: three
| ^ invalid u32
-- hostile input --
unterminated flow seq -> typed Err (no panic)
tab indentation -> typed Err (no panic)
duplicate key -> typed Err (no panic)
deep nesting (200) -> typed Err (no panic)
serde-saphyr produces a caret diagnostic with line and column out of the box — the shape §23.2 wants, and better than serde_yaml offered — and refuses all four malformed inputs with typed errors rather than panics. It also ships a budget module for bounded parsing and a localizer for message control [rustdoc:serde-saphyr@1.2.0], which matter at a boundary that parses user documents.
Against the alternatives [crates.io:*]: serde_norway has not been released since 2024-12-21, so replacing one quiet crate with another quiet crate; serde_yml is explicitly deprecated in its own description. serde-saphyr was released 2026-08-30.
Recommendation for Q1: serde-saphyr 1.2.0. The decision remains the owner’s; the evidence is one-sided.
And the TOML half, which the previous edition correctly called free and unclaimed [probe]:
PROBE 7a toml::Spanned
id span = 5..15 value = "pkg.demo"
version span = 26..27 value = 3
source slice at the version span : "3"
type error span = Some(26..33), rendered with a caret at line 2 column 11
Byte offsets that slice back to the source exactly, plus spans on errors. §22.1’s package.toml and §4.4’s pse.source_span get this for nothing.
C3.2 toml — document loading with source spans
Proposed anchor: 1.1.6+spec-1.1.0 [docs.rs:toml@1.1.6] — a 1.x stable crate, one of only two in this document.
Role: §22.1 package layout and §22.2 change sets; the TOML half of §1.3’s “load (serde_yaml, toml)”.
| Capability | Surface | Provenance |
|---|---|---|
| Deserialization / serialization | from_str, from_slice, to_string, to_string_pretty; Deserializer, Serializer | [docs.rs:toml@1.1.6] |
| Document model | Table, Value | [docs.rs:toml@1.1.6] |
| Source spans | Spanned<T> — “a spanned value, indicating the range at which it is defined in the source” | [docs.rs:toml@1.1.6] |
| Features | parse, display, preserve_order | [docs.rs:toml@1.1.6] |
| Parser engine | depends on winnow ^1.0.0 and toml_parser ^1.1.3 | [docs.rs:toml@1.1.6] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§4.4 pse.source_span = Struct<document_id, start: UInt32, end: UInt32> populated from authored documents | wrap authored fields as Spanned<T>; its range is the start/end pair | leverage — this is free and the blueprint does not claim it. Authoring provenance for TOML documents requires no custom parsing at all. |
§23.2 authoring.parse / authoring.reference failures carrying source anchors | Spanned<T> for reference errors found after parsing (an unknown path is not a parse error — §7.7 resolves paths in P3, so the span must survive parsing to be reported later) | leverage, and it settles a real problem: the span must be captured at load time because the resolver runs much later |
| §22.2 change sets against document regions | Spanned<T>, preserve_order | leverage |
Asymmetry to resolve: TOML gets spans for free; the YAML path’s span support depends entirely on which replacement crate C3.1 selects. Since §4.4 defines one pse.source_span type for all authored documents, span support should be a selection criterion for the YAML crate, not an afterthought.
C3.3 winnow — the expression DSL parser
Proposed anchor: 1.0.4 (released 2026-07-13) [docs.rs:winnow@1.0.4] — 1.x stable; MSRV policy is “the last 6 months of rust releases”, no fixed number.
Already in the graph: toml depends on winnow ^1.0.0 [docs.rs:toml@1.1.6], so the DSL parser adds no new parser engine — one engine, one version, which is what §3.3’s “explicitly not added” discipline wants.
Role: §7.7 — parse the authored expression DSL into template expression-graph nodes with unresolved paths and source spans.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Modules | ascii, binary, combinator, error, stream, token, prelude, _topic, _tutorial | [docs.rs:winnow@1.0.4] |
| Operator-precedence parsing | combinator::expression — “parses an expression based on operator precedence” — with Prefix (struct), Infix (enum, incl. Left/Right), Postfix (struct), each carrying a binding power | [docs.rs:winnow@1.0.4 /combinator/index.html], [c7:/websites/rs_winnow_…] |
| Span capture | stream::LocatingSlice wraps the input to track byte offsets; Parser::span() and Parser::with_span() return the consumed range alongside the output as Range<usize> | [c7:/websites/rs_winnow_…] |
| Threaded state | stream::Stateful for carrying global state (e.g. a document id, an interner) through the parse | [c7:/websites/rs_winnow_…] |
| Other stream wrappers | Partial (streaming input), Bytes, BStr | [docs.rs:winnow@1.0.4] |
| Error position | ParseError::offset() -> usize — “the location in ParseError::input where parsing failed”; an offset, not an index, and may equal input.len() at EOF | [c7:/websites/rs_winnow_…] |
| Core combinators | alt, delimited, preceded, terminated, separated, repeat (+ Repeat::fold), opt, dispatch!, cut_err (backtrack → unrecoverable), trace | [docs.rs:winnow@1.0.4 /combinator/index.html] |
| Result types | Result, ModalResult | [docs.rs:winnow@1.0.4] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§7.7 grammar layering affine → term → factor → unary → primary to express + - / * / / ^ / unary - precedence | combinator::expression with binding powers: Infix::Left(5) for + -, Infix::Left(7) for * /, Infix::Right for ^ (right-associative), Prefix for unary - | leverage, and a simplification the blueprint does not use. The EBNF encodes precedence structurally through four nested rules; winnow can express it as a precedence table, which is easier to keep consistent with §7.2’s operator catalog. The EBNF stays the normative grammar; the implementation need not mirror its shape. |
| §7.7 “any path that does not resolve is an authoring error with a source span” | LocatingSlice + with_span() producing Range<usize> per node | confirmed — spans map directly onto §4.4’s pse.source_span start/end (UInt32), the same shape toml::Spanned yields |
| §7.7 “the parser produces nodes with unresolved paths” (no evaluation, no resolution) | plain combinators returning AST nodes | leverage; §3.3’s “no evaluation during parsing” is a code-review rule, not a library feature |
§23.2 authoring.parse with a position | ParseError::offset() | leverage |
§7.7 constructs beyond arithmetic — sum/prod/integral binders, d(expr)/dident, kernel.f(...), if/then/else, unit suffixes {K} | ordinary combinators (delimited, separated, dispatch!) as the operand parser handed to expression | leverage — expression(operand) takes an arbitrary operand parser, so the binder and function forms compose cleanly with the precedence table |
Threading the document_id through so every span becomes a complete pse.source_span | Stateful | leverage |
What we deliberately do not use
- Not
Partial. Authoring documents are read whole; streaming-input support adds failure modes for nothing. - The parser never resolves, evaluates, or type-checks. §3.3’s boundary and §7.7’s P3 split — unit inference (§8.3) and path resolution belong to canonicalization, not to the grammar.
cut_erris a discipline, not an optimization. Once a construct is unambiguously entered (sum(has been consumed), failing must be unrecoverable so the error reports the real problem rather than backtracking into a misleading alternative. This directly serves §23.2’s requirement that failures name the culprit.
Gaps and risks
- Both
tomlandwinnoware 1.x with stable APIs — the lowest-risk crates in this document. The cluster’s risk is concentrated entirely in theserde_yamlreplacement decision (C3.1). - winnow’s MSRV policy is a rolling window rather than a fixed floor; with the platform on Rust 1.94 this is not a constraint, but it does mean winnow may raise its floor without a major-version bump.
5. Graph algorithms — petgraph (C4)
Re-sourced and measured. The previous edition rested this cluster on a 598 KB local deep-dive that has since been deleted — including the only support for recommended amendment B. Every claim below is now taken from petgraph 0.8.3’s own rustdoc [rustdoc:petgraph@0.8.3] and from PROBE 3 [probe]. Amendment B survives intact, with one fact the deleted reference did not carry.
Role: §12.5 topology closure (inferred.topology_edges “built in petgraph, persisted as rows”); §15.2–15.3 structural analysis; §14.1 pass-DAG checks; §17.4 topological unit ordering for sequential-modular initialization.
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §15.3 “Block triangularization: Tarjan SCC on the matching-projected digraph (reversed), condensation DAG, topological order” | algo::tarjan_scc, algo::kosaraju_scc, algo::condensation, algo::toposort | leverage [rustdoc:petgraph@0.8.3] |
| §15.3 “topological order made deterministic by ordering ties on semantic ID” | tarjan_scc/kosaraju_scc return Vec<Vec<NodeId>> where “node order inside each component is arbitrary, while SCC order is postorder / reverse topological sort” | confirmed necessary, with a sharper reason than the blueprint gives. The non-determinism is not only in tie-breaking between components — it is inside each component, where petgraph guarantees nothing. §5.1’s “re-running compilation on an unchanged snapshot reproduces identical IDs” therefore requires sorting within every block by semantic ID, not just across blocks. [probe] — see the measurement below |
| §15.3 “Connected components for independent subsystems” | algo::connected_components | leverage |
§14.1 pass pipeline is a DAG; §12.5 acyclicity checks; §4.2’s acyclic invariant kind | algo::is_cyclic_directed, algo::toposort (which returns a cycle error) | leverage; note visit::Topo “only visits nodes that are not part of cycles”, so for possibly-cyclic input use toposort’s error or DfsPostOrder/SCC [rustdoc:petgraph@0.8.3] |
§17.4 plan.sequential_modular@1 — “order units topologically with tears” | toposort + the platform’s tear selection | leverage |
§15.3 “Maximum matching: Hopcroft–Karp (own implementation; petgraph lacks it)” | see claim audit below | refined — decision upheld, reason corrected |
Claim audit: “petgraph lacks maximum matching”
petgraph does ship maximum matching: algo::maximum_matching(graph) -> Matching<G> plus greedy_matching and the Matching type [rustdoc:petgraph@0.8.3]. But the decision to hand-write Hopcroft–Karp is still right, for a stronger reason than the blueprint states:
maximum_matchinguses Gabow’s algorithm for general graphs, documented complexity O(|V|³), auxiliary space O(|V| + |E|), and it treats the input as undirected regardless of orientation[rustdoc:petgraph@0.8.3].- The equation×variable incidence graph (§15.3) is bipartite, where Hopcroft–Karp runs in O(|E|√|V|). At process-model scale the difference between O(|E|√|V|) and O(|V|³) is the difference between a usable and an unusable structural check.
Recommended erratum to §15.3: replace “petgraph lacks it” with “petgraph ships only general-graph matching (Gabow, O(|V|³)); the incidence graph is bipartite, so Hopcroft–Karp’s O(|E|√|V|) is required.”
Measured (PROBE 3)
(a) The matching claim — amendment B — survives verbatim. petgraph 0.8.3’s own rustdoc for maximum_matching reads [rustdoc:petgraph@0.8.3]: “Compute the maximum matching using Gabow’s algorithm… The input graph is treated as if undirected. The algorithm runs in O(|V|³). An algorithm with a better time complexity might be used in the future.” Measured [probe]:
PROBE 3b directed eq->var graph, maximum_matching : [(0,3), (1,2)] size=2
every edge reversed : [(0,3), (1,2)] size=2
same matching either way : true
Reversing every edge changes nothing, which is what “treated as if undirected” means in practice. The decision to hand-write Hopcroft–Karp stands, for the stated reason: O(|E|√|V|) against O(|V|³) on a bipartite incidence graph.
One fact the deleted reference did not carry: maximum_matching panics if g.node_bound() is usize::MAX [rustdoc:petgraph@0.8.3]. Unreachable at realistic sizes, but a panic is not a typed failure (§23.2), so the platform’s own implementation should not inherit the habit.
(b) SCC ordering — the determinism rule is confirmed, with a sharper demonstration [probe]:
PROBE 3a graph with a 3-cycle {0,1,2}, a 2-cycle {3,4}, and an acyclic tail
tarjan_scc, insertion order 1 : [[5], [4, 3], [2, 1, 0]]
tarjan_scc, insertion order 2 : [[5], [4, 3], [2, 1, 0]]
kosaraju_scc, same graph : [[5], [3, 4], [0, 1, 2]]
tarjan_scc is stable across insertion orders — but kosaraju_scc returns the same components in the opposite intra-component order. The order within a component is therefore an artifact of the algorithm, not a property of the graph, and petgraph guarantees nothing about it. §15.3’s rule — order ties on semantic ID — is required, and §5.1’s “re-running compilation on an unchanged snapshot reproduces identical IDs” depends on sorting within every block, not only across blocks. Swapping one petgraph algorithm for another would otherwise silently change every derived ID.
(c) A capability §12.5 does not claim: feedback_arc_set [probe]:
PROBE 3c flowsheet-shaped recycle 0->1->2->3->1
greedy_feedback_arc_set : [(3, 1)] <- exactly the recycle edge
toposort before removal : Err(Cycle(NodeIndex(3)))
condensation : 4 nodes -> 2
A tear set is a feedback arc set. §12.5 and §15.3 list tear selection among the hand-written algorithms; petgraph ships greedy_feedback_arc_set, and on a recycle it returns precisely the tear. See §13.
What we deliberately do not use
- Graphs never persist. §3.3’s boundary — “graph facts persist in Arrow; matching and DM decomposition are implemented in
pse-structural”. petgraph structures are built per analysis fromcompiled.incidenceand dropped;inferred.topology_edgesis the relation of record (§12.5). - petgraph node indices are not identity. They are artifact-local at best and reused after removal; §5.1’s three identity forms admit no fourth. Map
NodeIndex ↔ semantic IDexplicitly at the boundary of every analysis.[rustdoc:petgraph@0.8.3] - Not
GraphMapkeyed by semantic ID, tempting as it looks:GraphMapimplementsNodeCompactIndexablebut the identity mapping is better kept explicit and sorted, since determinism (above) requires a canonical order anyway. - Not DM decomposition from petgraph — it has none; §15.3’s coarse Dulmage–Mendelsohn is platform code over the matching, as the blueprint says.
Gaps and risks
- petgraph 0.8.3 is pre-1.0 and was last released 2025-09-30
[crates.io:petgraph]; algorithms continue migrating toward graph-trait-based implementations, so signatures may shift across minor versions. Pin exactly. maximum_matching’s docs note “a better-complexity algorithm may be used in the future”[rustdoc:petgraph@0.8.3]— worth re-checking at upgrade time, though a bipartite-specialized implementation remains ours regardless.
6. Incremental compute — salsa (C5)
Proposed anchor: 0.28.2 (released 2026-08-03) [docs.rs:salsa@0.28.2]. MSRV not stated — open item against the 1.94 floor.
context7 coverage: the richest of any crate in this document — three sources, used for different purposes: /websites/salsa-rs_github_io_salsa (the book, for the algorithm and idiom), /websites/rs_salsa (docs.rs, for the API and internals), /salsa-rs/salsa (the repo).
Role: §14.3 — “each pass is a salsa query keyed by the content hashes of its declared inputs and its own version… the memo stores the output artifact hashes, which the artifact store resolves”; §14.4 — the invalidation matrix; §2 D14 — incrementality follows declared dependencies.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Database definition | #[salsa::db] on the database struct/trait; Storage (the concrete implementation), DatabaseImpl (a default), the Database trait | [c7:/websites/rs_salsa], [docs.rs:salsa@0.28.2] |
| Inputs | #[salsa::input] structs; the Setter trait for field mutation; set_with_durability(db, key, value, Durability::HIGH) | [c7:/websites/salsa-rs_github_io_salsa] |
| Tracked queries | #[salsa::tracked] fn f(db: &dyn Db, key: K) -> V — salsa records which inputs the body reads and memoizes the result | [c7:/websites/salsa-rs_github_io_salsa] |
| Interning | #[salsa::interned] — data stored in the database, the struct is a newtyped integer id | [c7:/websites/salsa-rs_github_io_salsa] |
| Backdating | if a re-executed query returns a value equal to the previous one, the memo’s “last changed” revision is backdated, so dependents are not invalidated | [c7:/websites/salsa-rs_github_io_salsa] |
| Equality requirement | tracked return values must implement PartialEq for backdating; no_eq opts out | [docs.rs:salsa@0.28.2] |
| Durability | Durability (e.g. HIGH, NEVER_CHANGE) — a dependency of durability D unchanged since last verification verifies the memo without per-dependency checks | [c7:/websites/salsa-rs_github_io_salsa], [c7:/websites/rs_salsa] |
| Memo content | value (or dependencies only), revision last verified, revision value last changed, minimum durability of dependencies, complete dependency set or an untracked marker | [c7:/websites/salsa-rs_github_io_salsa] |
| Accumulators | #[salsa::accumulator] — auxiliary outputs (canonically diagnostics) collected while a tracked query runs, “stored alongside the query’s memoized result but do not contribute to that result or its equality”; salsa::Accumulator::accumulate(value, db) | [c7:/websites/rs_salsa] |
| Cancellation | CancellationToken with cancel() / is_cancelled(); Database::cancellation_token(), trigger_cancellation(), unwind_if_revision_cancelled(); Cancelled as the unwind payload | [c7:/websites/rs_salsa], [docs.rs:salsa@0.28.2] |
| Concurrency | queries are claimed per key; a second thread requesting an in-flight query blocks on it rather than duplicating work (ClaimResult::{Claimed, Running, Cycle}) | [c7:/websites/rs_salsa] |
| Cycles | detected at claim time with recovery via cycle_fn / cycle_result options on #[salsa::tracked] | [c7:/websites/rs_salsa], [docs.rs:salsa@0.28.2] |
| Memory control | lru option on tracked functions; Database::trigger_lru_eviction() | [c7:/websites/rs_salsa], [docs.rs:salsa@0.28.2] |
| Escape hatch | report_untracked_read() — marks the query for re-execution in every revision | [c7:/websites/rs_salsa] |
| Observability | Event / EventKind notifications; DatabaseKeyIndex identifies a query instance; ingredient_debug_name() | [docs.rs:salsa@0.28.2], [c7:/websites/rs_salsa] |
| Return modes | returns(ref | clone | copy | deref | as_ref | as_deref) | [docs.rs:salsa@0.28.2] |
| Profiling aid | synthetic_write(durability) — acts as though an input changed, forcing a new revision | [c7:/websites/rs_salsa] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§14.3 “each pass is a salsa query keyed by the content hashes of its declared inputs and its own version” | #[salsa::tracked] fn pass_x(db, key: PassKey) -> PassOutput where PassKey carries input content hashes + pass version | leverage |
| §14.3 “the memo stores the output artifact hashes, which the artifact store resolves” | the PartialEq-for-backdating requirement [docs.rs:salsa@0.28.2] | confirmed, and load-bearing. Storing hashes rather than RecordBatch payloads is what makes backdating work: a [u8; 32] comparison is exact and O(1), so a pass that re-runs on changed inputs but produces byte-identical output stops the invalidation cascade dead. Had the memo held batches, PartialEq would be expensive and semantically wrong. The blueprint’s choice is validated by the mechanism, not merely compatible with it. |
| §14.4 invalidation matrix (“a value change invalidates only artifacts that depended on that value being a constant”, §2 D14) | tracked dependency recording + backdating | leverage — the matrix is emergent from what each pass reads, provided passes read through salsa |
| §14.3 “a pass either succeeds with findings (severity below error) or fails with a typed failure” | #[salsa::accumulator] | leverage, previously unmapped. Findings are exactly “auxiliary outputs collected while a query runs that do not contribute to the result or its equality” [c7:/websites/rs_salsa] — so accumulating findings does not defeat backdating. The blueprint does not name this mechanism; it should. |
§6.13 / §23.2 runtime.cancelled, lifecycle and cancellation in pse-runtime | CancellationToken, unwind_if_revision_cancelled(), Cancelled | leverage — note cancellation propagates by unwinding, so pass bodies must be unwind-safe and must not leave partial artifacts (which §14.3 already requires: “partial outputs are discarded”) |
§14.3 “independent passes run concurrently on rayon” | per-key claiming; a concurrent request for the same query blocks rather than duplicating | confirmed compatible — rayon-parallel pass execution will not double-execute a shared pass |
| §3.1/§20 snapshot immutability — “a session pins a snapshot so schemas cannot change mid-query” | Durability::NEVER_CHANGE for reference/registry inputs within a snapshot | leverage — durability turns per-dependency verification into a single check for the large immutable substrate |
Measured (PROBE 6) — what F13 actually turns on
§26 F13 says salsa duplicates artifact-hash memoization and leaves it open. The question is decidable, and the answer is that the two overlap in purpose and not in mechanism [probe]:
PROBE 6 token_count = 3, doubled = 6
-- input replaced with different text, same token count --
token_count = 3, doubled = 6 (backdating: `doubled` need not re-run)
-- accumulator --
token_count(src2) = 4, accumulated findings = 1
Finding("suspicious token in \"this is bad \"")
-- durability: LOW / MEDIUM / HIGH available --
salsa keys on revision counters and durability, not on content hashes. Its unit is the in-process query; it backdates when a recomputed value is unchanged, so downstream queries are skipped even though the input changed. §14.4’s invalidation matrix is expressed entirely semantically and never names salsa — which is precisely why F13 looks like duplication. The two mechanisms answer different questions:
| salsa | the artifact hash (§14.4, §20.1) | |
|---|---|---|
| Unit | in-process query | persisted artifact |
| Key | revision counter + durability | content hash of declared inputs |
| Scope | one process lifetime | across processes and machines |
| Buys | fine-grained skipping, backdating, cycle handling, cancellation | reuse after restart, reproduction (§20.4) |
Neither subsumes the other. The artifact hash cannot skip a sub-computation within a pass; salsa cannot survive a restart. F13’s real question is therefore not “is salsa redundant” but “is intra-pass granularity worth a second memoization system” — a DM-58 question about demonstrated need, and one §24.3’s benchmarks can answer.
Two salsa capabilities the blueprint does not name and should:
#[salsa::accumulator]— findings travel alongside a query’s value without being part of its return type, which is exactly §23’s “a pass either succeeds with findings or fails” shape. Measured above. This is recommended amendment E, and the mechanism works as that amendment describes.CancellationTokenandCycle— §18.3 and §20.1 both specify interruption behaviour, and §14.2’s rule engine runs to a fixed point. salsa has first-class machinery for both[rustdoc:salsa@0.28.2].
What we deliberately do not use
- salsa never holds the authored model. §3.3’s boundary (“caches compiler computations, never the authored model”) is enforceable and cheap:
#[salsa::input]holds content hashes and identifiers, never relation payloads. The artifact store (§20.1) is the only thing that holds bytes, and salsa memos point into it. - Not
#[salsa::interned]for semantic IDs. Semantic IDs are already 128-bit opaque values with cross-artifact meaning (§5.1); re-interning them into database-local integer ids would create a second identity space that dies with the database — precisely the artifact-local-ordinal mistake D4 separates out. report_untracked_readis a defect, not a tool. Any pass that reads the artifact store or the filesystem outside a declared input makes itself re-execute every revision and silently voids §14.4. Recommend a governance check that it appears nowhere inpse-compiler.- Not salsa’s cycle recovery for the rule engine. §14.2’s fixed-point executor iterates DataFusion plans to a fixed point with provenance rows; that is a different mechanism at a different layer from salsa’s query-graph cycle recovery. Keep them unconflated — though
cycle_fn/cycle_resultremain available should the pass DAG itself ever need it (it should not; §14.1 declares a DAG).
Gaps and risks
- MSRV unknown against the 1.94 / edition-2024 floor — verify at
cargo add. - API churn. salsa 0.28.x is pre-1.0 and has been restructured substantially (the current generation is macro-attribute based; older tutorials describe a different query-group API). Pin exactly, and treat any web-sourced salsa example as possibly pre-dating this generation.
- Accumulators are feature-gated (
feature = "accumulator"guards appear in the source) — enable deliberately if findings ride this path. - Unwind-based cancellation interacts with FFI: a cancellation unwinding through the Ipopt C-API callback boundary (C11) is undefined behaviour. The solve driver must check cancellation between callbacks, never unwind through one. Design rule to record.
- Durability assignment is a design decision the blueprint has not made: which inputs are
HIGH/NEVER_CHANGE(reference packages, the schema registry within a snapshot) versus low (case values). Getting this right is most of the performance benefit. Open item.
7. Equality saturation (optional, phase 4) — egglog (C6)
context7 ID: /egraphs-good/egglog (115 snippets, benchmark 60.25) — the thinnest coverage of any crate here that context7 does cover.
Status in the blueprint: optional, phase 4 (§25); §7.4 canonicalization is otherwise hand-written.
Role: §3.3 — “equality-saturation rewrites under explicit type/domain guards”, bounded to “only rewrites justified by physical type, domain, and numerical policy”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Engine | EGraph::default(); equality saturation fused with Datalog — successor to egg | [c7:/egraphs-good/egglog] |
| Program surface | parse_and_run_program(None, "(datatype Expr (Num i64) (Add Expr Expr))") — datatypes, sorts, constructors, rules and schedules are written in egglog’s own S-expression language | [c7:/egraphs-good/egglog] |
| Native Rust rules | add_ruleset(eg, name), rust_rule(eg, name, ruleset, vars![…], facts![…], closure) — a rule whose action is a Rust closure, with ctx.value_to_base::<T>(), ctx.add(...), ctx.union(...) | [c7:/egraphs-good/egglog] |
| Declarative rules | (rule ((Add a b)) ((union (Add a b) (Add b a))) :name "commutativity") | [c7:/egraphs-good/egglog] |
| Running | run_ruleset(&mut eg, "name"); (run n); (run-schedule (saturate …) …) for staged saturation | [c7:/egraphs-good/egglog] |
| Reading back | eg.update(|fs| …) with fs.add(...), fs.eclass_of("Num", 9_i64); extraction of the best term under a default or custom cost model, and retrieval of multiple variants | [c7:/egraphs-good/egglog] |
| Container sorts | (sort MathVec (Vec Math)) for vector-valued arguments | [c7:/egraphs-good/egglog] |
| Deletion / subsumption | (delete …), subsumption rulesets | [c7:/egraphs-good/egglog] |
Fit assessment against the blueprint
| Blueprint requirement | Assessment |
|---|---|
| §7.4 canonicalization rewrites over the math IR | capable — the IR’s operator catalog (§7.2) maps onto egglog datatypes and constructors, and rust_rule allows a guard to be evaluated in Rust (checking §8 physical type, domain, numerical policy) rather than expressed in egglog’s language |
| §3.3 “only rewrites justified by physical type, domain, and numerical policy” | rust_rule is the mechanism that makes this possible. A pure declarative egglog rule cannot consult the quantity registry; a Rust-closure rule can. If egglog is adopted, every rewrite that touches units, domains, or numerics must be a rust_rule, not a (rule …). |
| Extraction back to a canonical form | capable — a custom cost model is the natural place to encode the blueprint’s canonical-form preference (§7.4) and numerical-conditioning policy |
| §5.1 determinism — “re-running compilation on an unchanged snapshot reproduces identical IDs” | unverified and the key risk. Equality saturation explores to a fixed point and extraction picks a minimum-cost term; whether ties break deterministically across runs is not established by the fetched material. Since §5.1 IDs are derived from the expression structure, a non-deterministic extraction changes IDs and breaks reproduction (§20.4). This must be settled before adoption, not after. |
Measured (PROBE 8) — extraction is reproducible; the trap is elsewhere
Open question 10 asked whether egglog’s extraction is deterministic across runs, because §5.1 identity and §20.4 reproduction would both depend on it. Five identical runs of the same program, compared under three renderings [probe]:
PROBE 8 5 identical runs -> distinct results by rendering
Debug ({:?}) : 5
Display ({}) : 1
snapshot_stable_under_proof_encoding : 1
extracted term : (Mul (Add (Var "x") (Num 2)) (Var "y"))
The extraction is deterministic. What varies is the Debug rendering of the internal TermDag, whose nodes field is a hash set — so its iteration order changes per process. The extracted term itself, and egglog’s own snapshot_stable_under_proof_encoding, are identical every time.
This is the same trap the companion DataFusion map found in datafusion-proto’s field metadata, and the same one the Arrow map found in Schema metadata: hash iteration order leaking into a rendering that something downstream might hash. The rule generalises — if any egglog output ever reaches an ID or a content hash, it must go through Display or snapshot_stable_under_proof_encoding, never {:?}.
[UNVERIFIED] — one process, one egglog version. Cross-process and cross-version stability are not measured, and §26’s phase-4 timing makes the cross-version question the live one: egglog has released three majors in ten months (0.5 → 1.0 in 2025-10, 2.0 in 2026-02, 3.0 in 2026-08) [crates.io:egglog], so whatever is assessed now will not be the API phase 4 meets.
The structural cost, stated plainly
egglog requires the IR to be re-encoded into egglog’s own datatype/sort language [c7:/egraphs-good/egglog]. That is precisely the “second model language” the blueprint rejects elsewhere (§13.6 for diffsol, D6 for DataFusion Expr). The encoding is mechanical and can be generated from §7.2’s operator catalog — but it is a real, maintained translation layer with its own drift risk, and the blueprint’s §3.3 boundary line does not acknowledge it.
Recommendation: keep egglog exactly where the blueprint puts it — phase 4, optional, behind a feature flag — and make adoption conditional on two spikes: (1) determinism of extraction, (2) a generated encoding from the operator catalog so the two representations cannot drift. Hand-written canonicalization (§7.4) must remain complete on its own; egglog is an optimization, never a dependency of correctness.
8. Numerics — num-dual, faer, uom (C7)
The cluster carrying the most blueprint weight and the least prior coverage. num-dual and faer are both fallback-lane crates (§0.3).
C7.1 num-dual — generic forward-mode derivatives inside kernels
Proposed anchor: 0.15.0 (released 2026-08-12) [docs.rs:num-dual@0.15.0]
Role: §18.5 kernel adapters — “the same body monomorphized over num_dual::DualNum types”; §18.2 step 5 — kernels contribute second derivatives as hyper-duals; §9.8 — FeOs SAFT/cubic models are written over num-dual and so derivatives arrive natively.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Generic numeric trait | DualNum — the bound a kernel body is written against so one body compiles for f64 and for every dual type | [docs.rs:num-dual@0.15.0] |
| First derivatives | Dual/Dual64/Dual32; vector forms DualVec/DualVec64, DualSVec (static), DualDVec (dynamic) | [docs.rs:num-dual@0.15.0] |
| Second derivatives | Dual2/Dual2_64, Dual2Vec/Dual2SVec/Dual2DVec; HyperDual/HyperDual64 and HyperDualVec for mixed partials | [docs.rs:num-dual@0.15.0] |
| Third derivatives | Dual3/Dual3_64, HyperHyperDual/HyperHyperDual64 | [docs.rs:num-dual@0.15.0] |
| Driver functions | first_derivative, second_derivative, third_derivative, gradient, jacobian, hessian, partial_hessian | [docs.rs:num-dual@0.15.0] |
| Implicit differentiation | implicit_derivative, implicit_derivative_binary, implicit_derivative_vec, and an ImplicitDerivative struct for models closed by an external solver | [docs.rs:num-dual@0.15.0] |
| Partial application | partial(), partial2(), partial3() to bind extra (parameter) arguments before differentiating | [docs.rs:num-dual@0.15.0] |
| Dimension-generic helpers | a Gradients trait for gradient/Hessian over static or dynamic dimension | [docs.rs:num-dual@0.15.0] |
| Optional integrations | nalgebra, ndarray, serde, numpy/pyo3 feature gates; a linalg module over dual-valued matrices | [docs.rs:num-dual@0.15.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§18.5 “the same body monomorphized over num_dual::DualNum types” — one kernel body, several derivative adapters | write the body as fn f<D: DualNum<f64>>(…) -> D; instantiate at f64, Dual64, HyperDual64 [docs.rs:num-dual@0.15.0] | confirmed — the crate is built precisely around this pattern |
§18.2 step 5 “kernels contribute second derivatives when available (num-dual hyper-duals)” | HyperDual/HyperDual64, Dual2; hessian / partial_hessian drivers | confirmed |
| §18.2 step 4 “implicit kernels apply the implicit function theorem on the selected branch” | implicit_derivative* + ImplicitDerivative — the crate already implements IFT for solver-closed models | confirmed, and stronger than the blueprint assumed: this is library capability, not platform code to write |
| §9.8 FeOs derivatives “natively” | FeOs is written over the same DualNum bound (C8) | confirmed — see C8 |
Measured (PROBE 4) — the implicit function theorem is library capability
§9.8’s Helmholtz route and §18.2 step 4’s implicit kernels both need the derivative of a root that is defined implicitly. Measured against a cubic-EOS compressibility root, Z³ − Z² + (A − B − B²)Z − AB = 0, differentiating the vapour root with respect to the attraction parameter [probe]:
PROBE 4 solved Z(A0) = 0.023366337826 residual = 0.000e0
dZ/dA by hand (IFT) = -0.014355061869
implicit_derivative = -0.014355061869
agrees with hand IFT : true
second_derivative f'' : -1.859802
num_dual::implicit_derivative reproduces the hand-derived implicit function theorem exactly. Recommended amendment G is confirmed by measurement: this is library capability, not platform code to write, and the blueprint should say so where §18.2 step 4 says “implicit kernels apply the implicit function theorem on the selected branch”.
The surrounding family is wider than §3.3’s “forward-mode and hyper-dual derivatives” suggests: ImplicitFunction and ImplicitDerivative give the structured form, implicit_derivative_binary/_vec/_sp the multivariate forms, and hessian/partial_hessian the second-order forms §18.2 step 5 needs [rustdoc:num-dual@0.15.0].
One API note that follows from §1.3. The probe’s kernel body is bounded D: DualNum<Primitive = f64>. On num-dual 0.14 — the version feos-core forces — the same body is bounded D: DualNum<f64>. This is what “the kernel source differs between the two pins” means concretely.
What we deliberately do not use
- No whole-problem differentiation. §3.3’s boundary (“kernel-local; never a dense dual vector over the whole problem”) is confirmed to be the right call for a second reason the blueprint does not state:
num-dualis forward-mode only[docs.rs:num-dual@0.15.0]. Forward mode costs one sweep per independent variable, so a dense dual over all solver variables would be O(n) sweeps per residual evaluation. The per-equation reverse adjoint sweep of §18.2 step 4 is ours to write;num-dualcannot supply it. - Not the
nalgebra/ndarrayintegrations by default. The evaluation program owns its layouts (§3.3, D11). Feature-gate these off unless a kernel genuinely needs matrix-valued duals;gradient/hessiansignatures are expressed overOVector/OMatrix, so using the drivers pullsnalgebrain — kernels that want to avoid it differentiate with the scalar types directly.
Gaps and risks
- MSRV / edition-2024 compatibility not stated in the fetched docs — open item, verify at
cargo addagainst MSRV 1.94. - Third-derivative types exist but the blueprint never asks for them; do not adopt speculatively.
- The
linalgmodule over dual-valued matrices is a possible shortcut for small implicit kernels — noted, not committed.
C7.2 faer — sparse and dense linear algebra for diagnostics and workspaces
Proposed anchor: 0.24.4 (released 2026-06-24) [docs.rs:faer@0.24.4]
Role: §15.4 num.condition_number; §15.5 SVD toolbox; §3.3 “sparse LU/QR, dense SVD, condition estimates; solver workspaces”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Modules | linalg, sparse, mat, col, row, diag, perm, stats, matrix_free, io, utils, prelude | [docs.rs:faer@0.24.4] |
| Dense types | Mat<T>, MatRef<'_, T>, MatMut<'_, T> | [docs.rs:faer@0.24.4] |
| Dense decompositions | llt(), lblt() (block LDL), partial_piv_lu(), full_piv_lu(), qr(), col_piv_qr(), svd(), thin_svd(), singular_values(), self_adjoint_eigen(), eigen(), self_adjoint_eigenvalues(), eigenvalues() | [docs.rs:faer@0.24.4] |
| Sparse types | SparseColMat, SymbolicSparseColMat, SparseColMatRef; Triplet; try_new_from_triplets(nrows, ncols, entries) -> Result<Self, CreationError> with sorting, dedup and symbolic construction handled | [c7:/sarah-quinones/faer-rs] |
| Sparse solvers | faer::sparse::linalg::solvers::{Llt, Lu, Qr} | [docs.rs:faer@0.24.4] |
| Solve traits | Solve, SolveLstsq; factor-level solve_in_place_with_conj(conj, rhs, par, stack) | [docs.rs:faer@0.24.4], [c7:/sarah-quinones/faer-rs] |
| Explicit memory control | MemStack via the dyn_stack module — factorization and solve take a &mut MemStack | [docs.rs:faer@0.24.4], [c7:/sarah-quinones/faer-rs] |
| Threading | Par enum passed per operation | [docs.rs:faer@0.24.4] |
| Matrix-free operators | matrix_free module for operator-defined linear algebra | [docs.rs:faer@0.24.4] |
| Tuning | Auto trait for algorithm hyperparameters | [docs.rs:faer@0.24.4] |
| Numerical robustness claim | state-of-the-art Cholesky (LLT/LDLT/Bunch-Kaufman), QR ±col pivoting, LU partial/full pivoting, SVD, eigendecomposition; fused kernels for memory-bound paths | [c7:/sarah-quinones/faer-rs] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§15.4 num.condition_number = ‖J‖_F·‖J⁻¹‖_F “via faer sparse LU” | sparse::linalg::solvers::Lu + Solve | build on top. faer ships no condition-number estimator [docs.rs:faer@0.24.4]. ‖J‖_F is a direct pass over the values; ‖J⁻¹‖_F as written requires solving against all n unit vectors — O(n) sparse solves per diagnostic. See risk below. |
| §15.4 “pseudo-inverse for non-square” | sparse::linalg::solvers::Qr with SolveLstsq | leverage — least-squares solve is the supported non-square route |
| §15.4 “a 2-norm estimate via the smallest singular values” | dense singular_values() / svd() | leverage for small; see §15.5 row |
§15.5 “dense faer SVD for small problems, iterative for large” | svd() / singular_values() cover small. For large, matrix_free::eigen::partial_svd does ship — but it computes the singular values of largest magnitude by its own documentation [rustdoc:faer@0.24.4] | refined. The previous edition said no iterative SVD exists; one does, aimed at the wrong end of the spectrum. §15.5 wants the smallest k, which needs shift-invert over the sparse LU |
| §3.3 “solver workspaces” | MemStack (dyn_stack), Par | leverage — faer’s explicit-workspace API is exactly the “derived execution layouts, not model truth” boundary, and Par is the knob §18.8’s thread budget must own |
§15.3 incidence built from compiled.incidence rows | Triplet + try_new_from_triplets | leverage — triplets are the natural landing shape for Arrow incidence rows (row ordinal, column ordinal, value) |
Measured (PROBE 5) — the iterative SVD route §15.5 leaves unnamed
§15.5 says “dense faer SVD for small problems, iterative for large” and names no implementation; §26 F17 lists the iterative SVD route as open. faer 0.24.4 does have a matrix-free tier — faer::matrix_free with conjugate_gradient, bicgstab, lsmr, and eigen::{partial_eigen, partial_self_adjoint_eigen, partial_svd} [rustdoc:faer@0.24.4] — so the question is whether it answers F17.
The operator contract works and is small [probe]:
PROBE 5 LinOp::apply matches a direct product : max|diff| = 0.000e0
BiLinOp::transpose_apply matches A^T * y : max|diff| = 0.000e0
Five LinOp methods plus three BiLinOp methods, and a sparse Jacobian never has to be materialised. That is the right shape for §15.5 operating on a Jacobian the evaluation program already knows how to apply.
But it does not answer F17 on its own. faer’s own documentation for partial_svd says it computes “an estimate of the singular values … with the largest magnitude” [rustdoc:faer@0.24.4]. §15.5 wants the smallest k singular values — rank deficiency below a tolerance, and the small-singular-value/vector pairs that identify the culprit variables and equations. That is the opposite end of the spectrum, and a Krylov method reaches it only through shift-invert, i.e. by applying A⁻¹, which requires a sparse factorisation.
So the answer to F17’s iterative route is two components, not one: faer’s sparse LU to apply the inverse, and matrix_free to iterate against it. Recording that is more useful than “use matrix_free”, which would have been the wrong shortcut.
[UNVERIFIED] — partial_svd was not executed. Its dimension preconditions (max_dim < min(m, n) against a v0 of length ncols) were not satisfiable in this harness, and the finding above rests on the crate’s documentation plus the measured operator contract rather than on a completed run.
What we deliberately do not use
- No model truth in faer types. §3.3’s boundary holds:
SparseColMatinstances are per-diagnostic scratch built fromcompiled.incidence/Jacobian values and discarded; the sparsity pattern of record is the Arrow relation, not theSymbolicSparseColMat. - Not faer’s own parallelism defaults. Every call takes
Parexplicitly; §18.8 requires the platform, not the library, to choose.
Gaps and risks
- Condition number cost. The IDAES-parity definition (Frobenius norm of the inverse) is inherently O(n) solves. Either accept it as an opt-in expensive diagnostic, or add an estimator (e.g. Hager–Higham 1-norm estimate) as platform code. Open decision — the blueprint states the formula but not the budget.
- Iterative SVD: present, but aimed the wrong way. §15.5’s “iterative for large” does have a faer entry point —
matrix_free::eigen::partial_svd— which targets the largest singular values while §15.5 needs the smallest. Options: implement Lanczos bidiagonalization overmatrix_free, add a dedicated crate, or restrict the SVD toolbox to problems small enough for dense SVD and say so. Open decision. - Pre-1.0 API churn. 0.24.x has no stability guarantee; faer has restructured its API across recent releases. Pin exactly and treat upgrades as a reviewed change, exactly as §3.1 treats the Arrow family.
- context7 coverage for faer is thin (20 snippets), so this inventory is mostly docs.rs-sourced; API shapes below module level should be re-checked at implementation time.
C7.3 uom — compile-time dimensional checking inside kernels
Proposed anchor: 0.38.0 (released 2026-06-23), MSRV rustc 1.68.0 [docs.rs:uom@0.38.0] — comfortably under the platform’s 1.94 floor.
Role: §3.3 “compile-time dimensional checks inside selected Rust kernels”, with §8’s dynamic registry authoritative.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Custom system definition | system! macro — declares base quantities, base units and dimension symbols; quantity! declares a quantity and its units against that system; unit! adds a unit to an existing quantity | [c7:/iliekturtles/uom] |
| Core type | Quantity<D, U, V> — dimension, units, value storage | [docs.rs:uom@0.38.0] |
| Storage types | f32, f64; all signed/unsigned integer widths; bigint/biguint; rational/rational32/rational64/bigrational; complex32/complex64 | [docs.rs:uom@0.38.0] |
| Construction / conversion | Quantity::new::<unit>(value), get::<unit>(), dimension-checked arithmetic that yields the derived quantity type | [c7:/iliekturtles/uom] |
| Formatting | into_format_args(unit, DisplayStyle::Abbreviation), uom::fmt | [c7:/iliekturtles/uom] |
| Parsing / serialization | str module for string-slice handling of quantities; optional serde feature (off by default) | [docs.rs:uom@0.38.0] |
| Cost model | dimensional analysis entirely at compile time, zero runtime cost | [docs.rs:uom@0.38.0] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §3.3 “compile-time dimensional checks inside selected Rust kernels” | Quantity<D, U, V> with SI type aliases inside a kernel body | leverage, kernel-local only |
| §8 dynamic registry stays authoritative | — | confirmed as a necessity, not a preference. uom resolves units only at compile time and provides no dynamic/runtime unit lookup [docs.rs:uom@0.38.0]. It structurally cannot host §8’s user-extensible quantity registry, where packages add quantity types at authoring time. |
What we deliberately do not use
- uom never sees a user-defined quantity. Package-authored quantity types (§6.2, §8.1) exist only at runtime; they have no
uomtype and must not acquire one. - uom is not the unit-inference engine. §8.3’s inference runs over the dynamic registry against
pse.dimension_vector; uom checking inside a kernel is a second, independent guard on hand-written code — a belt-and-braces check, not the mechanism.
Gaps and risks
- Dimension-basis mismatch.
pse.dimension_vector(§4.4) is 8-dimensional — the seven SI base dimensions plus currency. uom’s pre-built SI system does not carry currency, so any kernel mixing cost with physics cannot be typed by the stock SI system; it would need a customsystem!. Since costing (§19.5) is relational rather than kernel-resident, the practical answer is likely “uom covers physics kernels only, costing stays outside it” — open item to confirm. - Deciding which kernels get uom typing is unresolved; §3.3 says “selected” without a selection rule. Propose: kernels whose inputs are fixed SI quantities and whose bodies are hand-written (EOS, correlations), not generated adapters.
9. Domain providers (optional) — feos-core/feos, diffsol (C8)
Both are optional capability providers. This cluster contains the document’s most consequential compatibility finding.
C8.1 feos-core — native Helmholtz/SAFT/cubic thermodynamics
Proposed anchor: feos-core 0.10.1 (released 2026-07-24) [docs.rs:feos-core@0.10.1]. context7 has no coverage — FeOs resolves to unrelated libraries [c7 resolve: FeOs] — so this section is docs.rs-sourced.
Role: §9.8 — “optional provider crate under KernelSpec for SAFT and cubic models written over num-dual, giving derivatives natively”; §3.3 — “capability providers under KernelSpec; coverage and conventions validated, not assumed”.
| Capability | Surface | Provenance |
|---|---|---|
| Model decomposition | Residual and IdealGas traits separating residual and ideal-gas Helmholtz contributions; a Total trait combining them | [docs.rs:feos-core@0.10.1] |
| Equation of state | EquationOfState struct — “an ideal gas model and a residual Helmholtz energy model” | [docs.rs:feos-core@0.10.1] |
| Property evaluation | State — encapsulates a thermodynamic equilibrium state and computes properties from it | [docs.rs:feos-core@0.10.1] |
| Phase equilibrium | PhaseEquilibrium, PhaseDiagram, PhaseDiagramHetero, SolverOptions | [docs.rs:feos-core@0.10.1] |
| Differentiation | depends on num-dual ^0.14; IdealGasAD trait for parameter sensitivity | [docs.rs:feos-core@0.10.1] |
| Dependencies | nalgebra; optional ndarray, rayon, rusqlite | [docs.rs:feos-core@0.10.1] |
⚠ Compatibility finding: num-dual version split
feos-core 0.10.1 depends on num-dual ^0.14 [docs.rs:feos-core@0.10.1], while the current num-dual is 0.15.0 [docs.rs:num-dual@0.15.0]. For a 0.x crate these are semver-incompatible, so Cargo will resolve two copies of num-dual into the graph if pse-kernels takes 0.15 and pse-kernels-ext takes feos-core.
This is not a build-size annoyance — it is a type-identity failure of exactly the kind §3.1 forbids for Arrow majors. DualNum from num-dual 0.14 and DualNum from 0.15 are distinct traits over distinct types. §18.5’s whole premise (“the same body monomorphized over num_dual::DualNum types”) and §9.8’s “giving derivatives natively” both assume one dual-number vocabulary spanning platform kernels and FeOs models. Two copies break that silently at the type level.
Resolution options, in order of preference:
- Pin
num-dualto0.14workspace-wide so FeOs and platform kernels share one vocabulary. Cost: forgo 0.15 additions — importantly including whatever changed in theimplicit_derivativefamily (C7.1). - Adopt a newer
feos-coreif one exists that tracksnum-dual 0.15— verify atcargo add; 0.10.1 was the version read here. - Keep them separate and accept that FeOs kernels are opaque at the
KernelSpecboundary, converting values at the edge rather than sharing dual types. This costs the native-derivative benefit that motivates FeOs in the first place.
Add num-dual to §3.1’s pin table and add cargo tree -d coverage for it alongside the existing Arrow check. This is the actionable item of the cluster.
Boundary
§3.3 and §9.8 already say it: FeOs is a capability provider under KernelSpec, its coverage (property kinds, conventions, reference states) is declared per kernel and validated by parity tests. Nothing about State, PhaseEquilibrium or EquationOfState becomes a platform type — §9.5’s phase equilibrium and §6.x material relations stay authoritative, with FeOs supplying kernel evaluations only.
Gap: the Cargo feature list and any Python-binding feature were not enumerated in the fetched content — open item, minor.
C8.2 diffsol — native DAE trajectory backend
context7 ID: /martinjrobins/diffsol (396 snippets). Version not pinned during this pass — open item.
Role: §13.6 — “a native DAE integrator fed directly from the evaluation program (residual and Jacobian callbacks); it is a backend binding; the IR is unchanged”; §18.7 — an option for DAE_INTEGRATE; §25 phase 4.
| Capability | Surface | Provenance |
|---|---|---|
| Problem construction (closures) | OdeBuilder; Problem::new_implicit_closure(rhs, jacobian_vector_product, init) | [c7:/martinjrobins/diffsol] |
| Problem construction (traits) | NonLinearOp::evaluate(&self, y, t), NonLinearOpJacobian::evaluate_jacobian_product(&self, y, t, v) for custom RHS structs | [c7:/martinjrobins/diffsol] |
| Mass matrix / DAE | LinearOp trait with state_size, apply, apply_transpose; builder support for a mass-matrix-vector product | [c7:/martinjrobins/diffsol] |
| Solvers | ODE and semi-explicit DAE solvers, adaptive step size, event handling, sensitivity analysis | [c7 resolve: diffsol] |
| Optional DSL | DiffSL, a JIT-compiled model DSL | [c7 resolve: diffsol] |
Fit assessment
| Blueprint requirement | Assessment |
|---|---|
| §13.6 “fed directly from the evaluation program (residual and Jacobian callbacks)” | supported — the closure and trait routes both take Rust callbacks [c7:/martinjrobins/diffsol] |
| §3.3 / §13.6 “no second model language” | confirmed achievable, and it requires a deliberate choice. diffsol ships DiffSL, a JIT’d model DSL — adopting it would be exactly the second model language the boundary forbids. The closure/trait path avoids it entirely. State this explicitly when adopting: DiffSL is not used. |
| §18.2’s sparse per-equation Jacobian feeding diffsol | fit check needed. diffsol’s nonlinear interface asks for a Jacobian-vector product (evaluate_jacobian_product), not a sparse Jacobian. The evaluation program produces sparse rows (§18.2 step 4), from which a JVP is a straightforward multiply — but it is an adapter, and whether diffsol can also consume an explicit sparse Jacobian (for its implicit solvers’ linear algebra) was not established. Open item before committing. |
| §13.4/§13.6 “semi-explicit DAE” scope | diffsol advertises ODEs and semi-explicit DAEs [c7 resolve: diffsol]. Whether the discretized process models of §13 always fall in that class — versus general index-1 implicit DAEs — is a modelling question the blueprint should answer before promising DAE_INTEGRATE on this backend. Open item. |
| §18.7 alternatives | PETSc TS through the NL backend remains the other route; keeping both is the blueprint’s stated position and this lookup gives no reason to change it |
Boundary: unchanged — a backend binding lowered from the IR. nalgebra types (DVector, DMatrix) appear in diffsol’s trait signatures, so they enter at the backend edge only and must not propagate into pse-numerics’ own layouts (D11).
10. Identity and storage — blake3, object_store (C9)
C9.1 blake3 — content hashing and derived identity
Proposed anchor: 1.8.7 [docs.rs:blake3@1.8.7]. A 1.x crate — the only post-1.0 dependency in this document, and the only one whose API can be treated as stable.
Role: §5.1 derived entity IDs (blake3_128(domain ‖ …)); §5.3 content_hash = blake3(ipc_bytes) and snapshot_id; §4.3 pse.contract.fingerprint; §20.2 manifest hashes; §4.4 the pse.content_hash extension type (FixedSizeBinary(32)).
Re-sourced from blake3 1.8.7’s own rustdoc [rustdoc:blake3@1.8.7] and measured by PROBE 1 [probe].
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Regular hash | Hasher::new() -> Self, update(&mut self, input: &[u8]) -> &mut Self, finalize(&self) -> Hash (idempotent) | [c7:/websites/rs_blake3], [docs.rs:blake3@1.8.7] |
| Keyed hash (MAC/PRF) | Hasher::new_keyed(key: &[u8; 32]) -> Self | [c7:/websites/rs_blake3] |
| Key derivation | Hasher::new_derive_key(context: &str) -> Self — “the context string should be hardcoded, globally unique, and application-specific” | [c7:/websites/rs_blake3] |
| Extendable output | finalize_xof(&self) -> OutputReader — arbitrary-length output | [docs.rs:blake3@1.8.7] |
| Multithreaded hashing | update_rayon(&mut self, input: &[u8]) -> &mut Self — rayon feature | [docs.rs:blake3@1.8.7] |
| Memory-mapped hashing | update_mmap(path), update_mmap_rayon(path) — mmap feature (the latter also needs rayon) | [docs.rs:blake3@1.8.7] |
| Streaming from a reader | update_reader(&mut self, reader: impl Read) -> Result<&mut Self> | [docs.rs:blake3@1.8.7] |
| State inspection / reuse | count(&self) -> u64, reset(&mut self) -> &mut Self | [docs.rs:blake3@1.8.7] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§5.3 step 5 content_hash = blake3(ipc_bytes) over a serialized IPC stream | Hasher::new() + update per buffer, or update_reader over the IPC writer’s output | leverage — incremental update means the IPC bytes never need to be materialized as one contiguous buffer |
| §5.3 hashing large relation artifacts | update_rayon, update_mmap_rayon | leverage, with a caveat: these consume the rayon pool, which §18.8 says is budgeted. Hashing during a solve must not steal the solver’s threads. |
§5.1 domain-separated derived IDs — blake3_128("pse:symbol:v1" ‖ parent ‖ …) | two options, and the blueprint does not say which: (a) Hasher::new(), update with the domain-tag string then the components, finalize(), truncate to 16 bytes; (b) new_derive_key("pse:symbol:v1") — the crate’s own documented mechanism for an application-specific, globally unique context string | design decision. Option (b) is what the derive_key mode exists for and gives domain separation by construction rather than by convention. Option (a) is what the blueprint’s notation literally describes. Pick one and state it, because the two produce different bytes and the choice is frozen forever by §5.1’s “survives forever” contract. |
§5.1 / §4.4 128-bit pse.semantic_id from a 256-bit function | finalize_xof() reading 16 bytes is the documented way to get a shorter digest; truncating finalize()’s 32 bytes is the common way | partially unverified. The fetched Hasher page does not state that prefix-truncation below 32 bytes is safe [docs.rs:blake3@1.8.7]. BLAKE3’s extendable output is designed so any output length is a valid hash, but this should be confirmed against the specification before freezing, since §5.1 IDs are permanent. Open item. |
§4.3 pse.contract.fingerprint = blake3 of the RelationSpec rows | same as §5.3 | leverage |
Measured (PROBE 1) — which 128-bit derivation, and is truncation sound?
§5.1 derives semantic IDs as blake3_128("pse:named:v1" ‖ package_id ‖ qualified_name) and never says how the 128 bits are obtained. There are two routes and they do not agree [probe]:
PROBE 1 material = "pse:named:v1|pkg-01|flowsheet.heater.inlet"
hash(material)[..16] = 221090e3e5fabe979c6d73507758c9b7
derive_key(ctx, material)[..16] = d19bb5f9d890907fab83f83e9cbb59a1
the two routes agree : false
finalize_xof -> 16 bytes = 221090e3e5fabe979c6d73507758c9b7
xof(16) == hash[..16] : true
Hasher::new_derive_key == derive_key : true
different context -> different bytes : true
Two things follow.
Truncation is sound, and it is not an accident. BLAKE3 is an extendable-output function: asking the XOF for 16 bytes returns exactly the first 16 bytes of the 32-byte digest. Taking a prefix is therefore a defined operation with the security of a 128-bit digest, not a hopeful shortening. That closes the “is 16-byte truncation sound?” half of the open question.
But the two routes give different bytes, so §5.1 must name one. The recommendation is derive_key(context, material), because §5.1 is already doing domain separation by hand — prefixing "pse:named:v1", "pse:symbol:v1", "pse:equation:v1" and so on to the hashed material. derive_key is BLAKE3’s purpose-built mechanism for exactly that, and the probe confirms a different context yields different bytes. Concatenating a prefix into the message achieves the same end less explicitly, and leaves the separator’s encoding as an unstated part of the identity contract.
Either choice is defensible; not choosing is not, because §5.1 IDs are permanent and the two routes are irreconcilable after the fact.
What we deliberately do not use
- Not keyed hashing.
new_keyedis a MAC — identity here must be reproducible by anyone holding the same content (§20.4 reproduction), so a secret key would defeat the purpose. - Not
Hash’s constant-time equality for ordinary lookups.blake3::Hashcompares in constant time (an anti-timing-attack property); semantic IDs are not secrets, and hot paths that index by ID should use the raw[u8; 16]/[u8; 32]inside ArrowFixedSizeBinaryrather than round-tripping throughHash.
Gaps and risks
- Canonicalization, not hashing, is the hard part. §5.3’s five preparatory steps (PK ordering, batch concatenation, dictionary expansion, volatile-metadata stripping, NaN canonicalization with
-0.0preserved) are entirely platform code over Arrow; blake3 contributes only step 5. Test the canonicalizer, not the hash. - Feature flags
rayonandmmapare off by default — enable deliberately and record them in the workspace dependency table.
C9.2 object_store — the artifact store
Version note — settled, and the previous edition’s API description was wrong. The pin question is closed: DataFusion 55.1.0 requires object_store ^0.13.2, so 0.13.2 is not a choice but a constraint, and 0.14.1 must not be used. Revision 2’s §3.1 records this correctly; the companion DataFusion map carries the reasoning.
But re-sourcing this cluster produced an erratum. The previous edition described the trait surface from a local reference that has since been deleted — and that reference described a different version. At the pinned 0.13.2, object_store does not have the API the previous edition listed. Version 0.13.0 reshaped the trait: copy and copy_if_not_exists were merged into copy_opts, delete became delete_stream, rename became rename_opts, and put/get/head became put_opts/get_opts [docs.rs:object_store@0.13.2].
This is exactly the failure mode an unresolvable citation hides: a confident API list, checked against nothing, describing a version the design does not use.
Role: §20.1 artifact store layout, atomic and conditional writes, ref update protocol; §20.2 manifest.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Trait surface at the pinned 0.13.2 — ten methods | required: put_opts, put_multipart_opts, get_opts, copy_opts, delete_stream, list, list_with_delimiter; provided: get_ranges, list_with_offset, rename_opts | [docs.rs:object_store@0.13.2] |
| Methods that do not exist at 0.13.2 | put, get, head, delete, copy, copy_if_not_exists, rename, rename_if_not_exists — all removed or merged in the 0.13.0 reshape | [docs.rs:object_store@0.13.2] |
| Put modes | enum PutMode { Overwrite (default), Create, Update(UpdateVersion) } — all three confirmed present at the pin | [docs.rs:object_store@0.13.2] |
| Conditional-write token | UpdateVersion { e_tag: Option<String>, version: Option<String> } | [c7:/apache/arrow-rs-object-store] |
| Put entry point | async fn put_opts(&self, location: &Path, payload: PutPayload, opts: PutOptions) -> Result<PutResult> — “atomic; either all data is written or the operation fails, preventing partial writes”; PutResult carries e_tag, version, extensions | [c7:/apache/arrow-rs-object-store] |
| Error signals | Error::Precondition (version mismatch), Error::AlreadyExists (Create on an existing object), Error::NotModified (conditional fetch) | [c7:/apache/arrow-rs-object-store], [docs.rs:object_store@0.14.1] |
| Backends supporting conditional put | LocalFileSystem, InMemory, AmazonS3, GoogleCloudStorage, MicrosoftAzure | [docs.rs:object_store@0.14.1] |
| Local atomicity | LocalFileSystem: “all operations are atomic, and readers cannot observe partial and/or failed writes” | [docs.rs:object_store@0.14.1] |
| Optimistic-concurrency idiom | read → compute → put_opts(PutMode::Update(version)) → retry on Precondition | [c7:/apache/arrow-rs-object-store] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§20.1 “object_store provides atomic single-object writes and conditional updates (PutMode::Update with the ref’s version)” | put_opts + PutMode::Update(UpdateVersion { e_tag, version }), Error::Precondition on mismatch | confirmed, exactly as stated |
§20.1 ref update: refs/<name>.json compare-and-swap | the documented optimistic-concurrency loop; PutResult.e_tag from the previous write is the token | leverage |
| §20.1 idempotent encoded objects | PutMode::Create plus verified existing-object checksum/length/format | Proposed integration: AlreadyExists is successful only after verification; a name alone does not prove integrity (ADR-0045) |
| §20.1 “the local single-writer mode is the initial implementation” | LocalFileSystem, documented atomic | confirmed |
| §20.1 mmap-able IPC artifacts | get_range / get_ranges for footer-then-body reads; note object_store returns bytes, it does not hand out a mapping — §5.4’s “answered only from cached metadata” statistics path needs the footer range, which get_range provides | leverage, with the caveat that “mmap-able” describes the file format choice, not an object_store capability |
| §20.3 / §20.4 reproduction reading pinned manifests | get_opts with conditional fetch, NotModified | leverage |
What we deliberately do not use
-
Never a rename as a commit primitive — and at 0.13.2 the temptation is smaller than it was, because
renameno longer exists; onlyrename_optsdoes[docs.rs:object_store@0.13.2]. The rule still holds and still belongs in §20.1: a rename is a copy-plus-delete on object stores, not an atomic move, so a commit must be a conditional write to the final path (PutMode::Createfor content-addressed artifacts,PutMode::Updatefor the ref). §20.1’s protocol already does this; stating it prevents someone “optimising” it into a temp-file-and-rename.Recommended amendment J is therefore refined, not retracted: the advice is right, but it should name the mechanism (
PutMode) rather than a method (rename) that the pinned version does not have. -
The snapshot protocol stays platform code. §3.3’s boundary holds:
object_storegives single-object atomicity; multi-relation consistency comes from the write-artifacts → write-manifest → CAS-the-ref ordering, which is ours.
Gaps and risks
- The API erratum (above) is the actionable item in this cluster: any §20.1 pseudocode or prototype written against
put/get/copy_if_not_existswill not compile at the pinned version. - S3 multi-writer coordination. The docs reference S3 needing conditional-write support or DynamoDB-based commit coordination for some use cases
[docs.rs:object_store@0.14.1]; the detail was not pinned down. Since §20.1 starts single-writer local, this is deferred — but it must be settled before any multi-writer or S3 deployment, because the ref CAS is the platform’s only serialization point. Open item. put_multipartis unused by the current design (artifacts are written whole); revisit only if relation artifacts grow beyond a comfortable single-put size.
11. Runtime and failure — rayon, tokio, tracing, thiserror, miette (C10)
C10.1 rayon and tokio — the thread budget
Re-sourced. tokio 1.53.1 and rayon 1.12.0, both confirmed current [crates.io:tokio], [crates.io:rayon] and resolved into the committed lockfile (§1.5). §3.1’s tokio 1.52+ is consistent — and tokio is the only crate in this entire map that §3.1 anchors at all (§1.1).
Role and boundary are almost entirely a platform concern, and §18.8 already states it well: one configuration owns the DataFusion tokio runtime, the rayon pool for batch kernels and pass parallelism, and the solver’s linear-algebra threads (HSL/MUMPS/PETSc); oversubscription is a configuration error, not a runtime surprise.
Three cross-cluster facts this document surfaces that §18.8 must account for, because each quietly consumes the rayon pool:
| Consumer | Mechanism | Cluster |
|---|---|---|
blake3::Hasher::update_rayon / update_mmap_rayon | artifact hashing, rayon feature | C9 |
faer’s Par parameter on every factorization and solve | diagnostics and condition estimates | C7 |
| salsa’s per-key claiming under rayon-parallel passes | blocks rather than duplicates — benign, but it means pass parallelism is bounded by the pass DAG, not by the pool size | C5 |
The rule §18.8 states (“defaults leave the solver single-threaded while a solve is running”) should be extended: hashing and diagnostics must not steal the pool during a solve either.
C10.2 tracing — structured observability
Role: §23.1 — spans per pass run, per rule evaluation, per plan stage, and per solve; “events carry the semantic IDs involved”; metrics (pass durations, row counts, cache hits, solver iterations) emitted with the same identifiers.
| Capability | Surface | Provenance |
|---|---|---|
| Function instrumentation | #[instrument] — creates and enters a span per call; span name defaults to the function name, level INFO, and all function arguments are recorded as fields by default | [c7:/websites/rs_tracing] |
| Instrumentation options | name, target, level, skip(...), fields(...) with expressions evaluated at span entry, err | [c7:/websites/rs_tracing] |
| Async support | #[instrument] handles span entry/exit across await points correctly | [c7:/websites/rs_tracing] |
| Late field recording | Span::current().record("name", &value) | [c7:/websites/rs_tracing] |
| Field pre-declaration requirement | fields must be declared at span creation; use field::Empty as a placeholder. Recording an undeclared field produces no error and no effect | [c7:/websites/rs_tracing] |
| Subscriber composition | tracing-subscriber layers | [c7 resolve: tracing] (/websites/rs_tracing-subscriber) |
| OpenTelemetry bridge | tracing-opentelemetry | [c7 resolve: tracing] (/websites/rs_tracing-opentelemetry) |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
| §23.1 spans per pass / rule / plan stage / solve | #[instrument] on the pass entry point, or manual spans where the boundary is not a function | leverage |
| §23.1 “events carry the semantic IDs involved”; metrics recorded with the same identifiers | fields(...) at creation + Span::current().record(...) | leverage, with a trap. Outcome values a pass only knows at the end — output content hashes, row counts, findings counts, cache hit/miss — must be declared as field::Empty when the span opens, or the later record is silently discarded [c7:/websites/rs_tracing]. This is the single most likely way §23.1 gets quietly implemented wrong. |
| §23.1 “provenance is separate from observability… linked by pass run and run IDs but never merged” | span fields carrying pass_run_id / run_id | leverage; the separation is a platform rule, and tracing has no opinion |
Two extensions the blueprint does not list — re-sourced, and the recommendation is now qualified [crates.io:datafusion-tracing], [crates.io:instrumented-object-store]:
Amendment D is currently unsatisfiable. Both crates exist and both are version-locked to the DataFusion release line —
datafusion-tracing55.0.0 andinstrumented-object-store55.0.0, both published 2026-08-24. Neither has a 55.1.0 release, and §3.1 pins the engine at 55.1.0. The previous edition hedged exactly this (“verify that release exists before depending on it”) on the strength of a reference that has since been deleted; verified, it does not. Adopting these means either moving the engine back to 55.0.0 — contradicting the owner’s decided pin — or running a plan-instrumenting crate one minor out of step with the plans it instruments, which is the least forgiving kind of mismatch. The companion DataFusion map reaches the same conclusion from the engine side and offersJoinSetTraceras the first-party alternative.
datafusion-tracing— instruments DataFusion through its own extension points: aPhysicalOptimizerRulethat wraps each physical node (instrument_with_spans!and level-specific variants), plus rule-phase instrumentation across analyzer/logical/physical optimizer phases (instrument_rules_with_spans!), withInstrumentationOptions/RuleInstrumentationOptionsandpretty_format_compact_batchfor batch previews. Version-locked to the DataFusion release (datafusion-tracing 55.0.0would be required alongside DataFusion 55) — verify that release exists before depending on it.instrumented-object-store— spans for object-store operations, which is §20.1’s artifact store.
Both map onto the two subsystems §23.1 most wants visibility into, through supported extension points rather than custom wrappers. Recommend adding them to §3.3 (with the version-lockstep caveat).
What we deliberately do not use
#[instrument]’s default “record every argument”. A pass takes a snapshot handle and possibly large inputs; blanket recording is both noisy and a way to leak whole structures into logs. Useskip(...)and name fields explicitly.- Observability is never provenance. §23.1’s separation is load-bearing:
provenance.*relations explain derivation and are content-addressed and reproducible; spans explain execution and are not. A field in a span is not a provenance record.
C10.3 thiserror and miette — the failure taxonomy
thiserror 2.0.20 [rustdoc:thiserror@2.0.20]; its surface (#[derive(Error)], #[error("…")], #[from], #[source]) is stable and unremarkable. The interesting half is miette.
miette role: §3.3 — “typed failure taxonomy with source anchors”; §23.2 — a failure-class table where “failures carry the relation rows and source spans involved; they are never flattened into ‘run failed’”.
| Capability | Surface | Provenance |
|---|---|---|
| Derive | #[derive(Diagnostic)] on any std::error::Error; composes directly with thiserror’s #[derive(Error)] | [c7:/websites/rs_miette] |
| Diagnostic metadata | #[diagnostic(code(...), severity(...), help(...), url(...))]; code is documented to use Rust path format (foo::bar::baz) | [c7:/websites/rs_miette] |
| Source attachment | #[source_code] src: NamedSource<String> — the source text snippets are cut from; a plain String works when file names do not matter | [c7:/websites/rs_miette] |
| Labelled spans | #[label("...")] bad_bit: SourceSpan; SourceSpan constructible from (offset, len) | [c7:/websites/rs_miette] |
| Trait surface | Diagnostic::{code, severity, help, url, source_code, labels, related, diagnostic_source}; labels() yields LabeledSpans, related() yields further Diagnostics | [c7:/websites/rs_miette] |
| Ad-hoc diagnostics | diagnostic!(severity = …, code = …, help = …, labels = vec![LabeledSpan::at_offset(…)], url = …, "message") → MietteDiagnostic | [c7:/websites/rs_miette] |
| Reporting | miette::Result, Report, custom reporter via miette::set_hook() | [c7:/websites/rs_miette] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§23.2 failure classes named authoring.parse, compile.property, solve.infeasible, internal.invariant, … | #[diagnostic(code(...))] in Rust path format | leverage — a near-exact fit. The taxonomy’s dotted names are diagnostic codes; the table in §23.2 becomes the code registry. |
§23.2 severity ∈ {error, warning} (also §4.2 invariants) | Severity / #[diagnostic(severity(...))] | leverage |
§3.3 / §23.2 “source anchors” — pse.source_span (§4.4) from authored documents | #[source_code] NamedSource + #[label] SourceSpan | leverage; SourceSpan is (offset, len) while pse.source_span stores (start, end) — a trivial conversion, but one that must be written once and only once, since winnow’s with_span() and toml::Spanned both yield Range<usize> (C3) |
| §23.2 “failures carry the relation rows … involved; never flattened” | related() for multiple diagnostics; help() / url() for the explanation | partially — miette models source-anchored failures beautifully. A failure whose evidence is a set of violating relation rows (§4.2’s invariant validators return violating keys) has no natural miette representation. Design item: findings are relations (compile.findings-style rows) and a miette Diagnostic is the rendering of one, not its storage. Keep the row the record and the diagnostic the presentation, or the taxonomy stops being data (§23.2’s own premise). |
What we deliberately do not use
miette::Resultin library crates. miette’s own documentation is explicit: use itsResultthroughout an application “but NOT your libraries! Those should always return concrete types!”[c7:/websites/rs_miette]. Everypse-*crate is a library and must return its own concrete error enums (thiserror+Diagnosticimpls); only the CLI and the top-level driver returnmiette::Resultand install a reporter. This also keeps §23.2’s classes typed and matchable rather than erased into a report.- Not miette’s fancy graphical rendering as the machine-readable output. Rendered output is for humans at a terminal;
runtime/provenancerelations are what programs and §20.4 reproduction read.
Gaps and risks
- miette’s version was not pinned during this pass — open item, record an anchor at
cargo add. - The rows-versus-diagnostic design item above is the one substantive decision in this cluster.
12. Solver FFI — the Ipopt C API (C11)
Re-sourced from the header itself. The previous edition rested this cluster on a local reference that has since been deleted, and flagged one signature as read second-hand. Everything below now comes from IpStdCInterface.h at COIN-OR tag releases/3.14.16 [gh:coin-or/Ipopt@releases/3.14.16]. Every claim survived; one recommended amendment had the wrong function names.
The complete exported C surface is eleven functions, which is small enough to state in full: CreateIpoptProblem, FreeIpoptProblem, IpoptSolve, AddIpoptStrOption, AddIpoptNumOption, AddIpoptIntOption, OpenIpoptOutputFile, SetIpoptProblemScaling, SetIntermediateCallback, GetIpoptCurrentIterate, GetIpoptCurrentViolations. This section covers the Rust binding layer and the two capabilities §18.3 depends on. (The previous edition also pointed at two local documents on option semantics and metrics; both are deleted, and no claim in this map rested on either, so the pointers are removed rather than replaced.)
Crate decision: §3.3 specifies an own -sys crate. A search found no established Rust binding worth preferring — context7 resolves ipopt-sys only to the upstream C++ project /coin-or/ipopt [c7 resolve: ipopt-sys]. The blueprint’s decision stands.
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§18.3 “binds the Ipopt C API (CreateIpoptProblem, IpoptSolve, AddIpoptNumOption/StrOption/IntOption)” | declared in IpStdCInterface.h under $PREFIX/include/coin-or; FreeIpoptProblem completes the lifecycle | confirmed [gh:coin-or/Ipopt@releases/3.14.16] |
§18.3 callbacks eval_f, eval_grad_f, eval_g, eval_jac_g, eval_h over the evaluation program | the five C callback typedefs, each taking UserDataPtr user_data — the handle through which the §18.2 tape and workspace reach the callback | confirmed; user_data is the entire Rust-side plumbing |
§18.3 “iteration statistics parsed from the intermediate callback into runtime.iterations (objective, primal and dual infeasibility, mu, step size, regularization, restoration flag)” | SetIntermediateCallback; the callback receives algorithm mode, objective, inf_pr, inf_du, mu, step/d norm and unscaled infeasibilities, and returns false to stop the solve | confirmed [c7:/coin-or/ipopt] |
| §18.3 richer per-iteration state | Ipopt_get_curr_iterate(nlp) and Ipopt_get_curr_violations(nlp) — C-interface counterparts of TNLP::get_curr_iterate()/get_curr_violations(), callable inside the intermediate callback, available since Ipopt 3.14 | leverage, and it improves on §23.1. §23.1 plans to capture “solver output … with parsed structure (iteration lines, restoration entries)”. Text parsing is brittle and locale/format dependent; these two functions return the current primal/dual iterate and infeasibilities as data. Use the structured API for runtime.iterations, and keep text capture only for messages that have no structured equivalent. |
| §18.2 step 6 domain guards — “a recoverable evaluation error … the Ipopt driver reports it as an evaluation failure (Ipopt then restores or fails)” | every callback returns Bool; returning false signals an evaluation failure | confirmed — this is precisely the mechanism, and it is why IDAES needs halt_on_ampl_error while the native driver does not |
| §17.5 continuation and §17 initialization plans re-solving perturbed problems | warm_start_init_point yes, warm_start_bound_push, warm_start_mult_bound_push, mu_init | leverage [c7:/coin-or/ipopt] — documented for exactly the “similar active set, repeated solve” case that continuation steps and sequential-modular initialization create |
§18.3 bound handling for pse.bound ({finite, unbounded}, §4.4) | CreateIpoptProblem copies bounds; values at or beyond nlp_lower_bound_inf / nlp_upper_bound_inf are treated as infinite | confirmed [gh:coin-or/Ipopt@releases/3.14.16] — the extension type’s unbounded kind maps onto these sentinels at the driver boundary, which is the right place for a sentinel to exist |
Re-sourced (PROBE-free, from the header)
Two claims the previous edition could not fully support, now first-hand [gh:coin-or/Ipopt@releases/3.14.16].
Intermediate_CB’s exact signature — the previous edition read it “through the Java interface’s parameter list and the ChangeLog” and flagged it. It is twelve typed parameters:
typedef bool (*Intermediate_CB)(
ipindex alg_mod, /* 0 is regular, 1 is restoration */
ipindex iter_count,
ipnumber obj_value, inf_pr, inf_du, mu, d_norm,
ipnumber regularization_size, alpha_du, alpha_pr,
ipindex ls_trials,
UserDataPtr user_data);
/* "If this method returns false, Ipopt will terminate the optimization." */
Every field §18.3 says it parses — “objective, primal and dual infeasibility, mu, step size, regularization, restoration flag” — is a typed callback argument, not a value scraped from text. And the documented return-false semantics is exactly §18.3’s cancellation mechanism, confirmed at the source.
Recommended amendment H had the wrong function names. It proposed preferring Ipopt_get_curr_iterate / Ipopt_get_curr_violations. Those are the C++ TNLP method names (get_curr_iterate, get_curr_violations). The C interface exports them as GetIpoptCurrentIterate and GetIpoptCurrentViolations. The recommendation stands; the names are corrected.
Binding-layer rules (platform decisions this lookup surfaces)
IpStdCInterface.his the source of truth for typedefs, and the exact set at 3.14.16 isipindex,ipnumber, plus the legacy aliasesIndex(int),Number,Bool(bool, i.e. C99stdbool, not anint),UserDataPtr(void*),IpoptProblem(an opaquestruct IpoptProblemInfo*), andenum ApplicationReturnStatus[gh:coin-or/Ipopt@releases/3.14.16].Boolbeing a realboolmatters for the-syscrate’s FFI declarations. Whether the-syscrate usesbindgenat build time or hand-declaredextern "C"items is an open choice: bindgen tracks the installed header (safer across Ipopt builds), hand-declaration keeps the build hermetic and reproducible (§20.4 cares about reproducibility). Open decision — recommend bindgen with the generated bindings committed and diffed in CI, mirroring §4.2’s treatment of generated relation code.- Index style.
CreateIpoptProblemtakes an index-style argument (0= C style)[gh:coin-or/Ipopt@releases/3.14.16]; the sparsity pattern fromcompiled.incidenceis 0-based, so C style is correct and must be asserted, not assumed. - Never unwind through a callback. A Rust panic — or a salsa cancellation unwind (C5) — crossing the C boundary is undefined behaviour. Callbacks must catch, record the failure in the
user_datastate, and returnfalse; the driver then converts that intosolve.evaluation_errororruntime.cancelled(§23.2) afterIpoptSolvereturns. This is the single most important rule in this cluster and neither §18.2 nor §18.3 currently states it. - Version floor.
Ipopt_get_curr_iterate/Ipopt_get_curr_violationsrequire Ipopt ≥ 3.14[c7:/coin-or/ipopt]. §20.3 already records solver versions in the run environment; add a capability probe alongside the existinglinear_solver = ma57probe (§18.3) so an older Ipopt degrades to text-parsed iterations rather than failing.
Gaps and risks
- Linking and distribution. How Ipopt (and HSL/MUMPS) is located, linked and redistributed is untouched by the blueprint and by this lookup — static vs dynamic,
pkg-config, vendored build. It determines whetherpse-backend-nativebuilds on a fresh machine. Open item, out of scope for a capability map but on the critical path. - The exact
Intermediate_CBC typedef was read through the Java interface’s parameter list and the ChangeLog[c7:/coin-or/ipopt]; take the precise C signature fromIpStdCInterface.hat implementation time.
13. Pass 3 — the under-leverage sweep
Passes 1 and 2 both start from the blueprint, so neither can find a capability it never gestured at. This pass takes each library on its own terms and asks what it offers that bears on a named blueprint requirement and is not claimed. Charter §G and DM-58 apply throughout: a row naming no principle and replacing no identified work does not appear.
The denominator is §1.5’s extraction — 26 crates, 589 public traits. Most of that surface is irrelevant to this design and is dismissed by cluster rather than enumerated.
| Capability | Crate | DM / gate | Adjudication |
|---|---|---|---|
algo::feedback_arc_set | petgraph | DM-38, DM-25 | adopt. §12.5 and §15.3 both list tear selection among the hand-written algorithms. A tear set is a minimum feedback arc set, and PROBE 3c shows greedy_feedback_arc_set returning exactly the recycle edge on a flowsheet-shaped graph. This does not remove the need for a policy (which tears are acceptable, what they cost), but it removes the search. The strongest single find in this map. |
algo::ford_fulkerson / maximum_flow | petgraph | DM-38 | note, and it refines amendment B. Maximum bipartite matching reduces to max-flow, and petgraph ships both. So “Hopcroft–Karp is required” is true of the complexity — O(|E|√|V|) against a flow formulation — not of the capability. Worth stating so the amendment’s reasoning is exact. |
algo::{tred, dominators, articulation_points, bridges, condensation, is_bipartite_undirected} | petgraph | DM-10, DM-53 | evaluate. condensation is already implied by §15.3’s DAG step. tred (transitive reduction) would make §14.1’s pass-dependency DAG minimal and therefore readable; is_bipartite_undirected is a cheap assertion on an incidence graph that §15.3 assumes is bipartite and never checks. |
faer::matrix_free (LinOp/BiLinOp, lsmr, conjugate_gradient, bicgstab, partial_svd) | faer | DM-36, DM-38 | evaluate — it is half of F17’s answer. See C7.2: the operator contract works and is eight methods, but partial_svd targets the largest singular values while §15.5 wants the smallest. The route is sparse LU plus matrix-free iteration, not matrix-free alone. |
linalg::svd::pseudoinverse_from_svd_with_tolerance | faer | DM-40, DM-59 | evaluate. §15.4’s condition number is ‖J‖_F·‖J⁻¹‖_F “via faer sparse LU (pseudo-inverse for non-square)”. This is that pseudo-inverse, with an explicit tolerance argument — which is exactly the “declare the approximation” discipline DM-40 asks for and §15.4 currently leaves implicit. |
num-dual’s ImplicitFunction / ImplicitDerivative / implicit_derivative* | num-dual | DM-25, DM-52 | adopt. Confirmed by PROBE 4. Recommended amendment G already spotted it; the measurement makes it a binding rather than a hope. |
hessian, partial_hessian, Dual3, HyperHyperDual | num-dual | DM-40 | evaluate. §26’s “exact Hessian cost for large distributed models” risk row assumes second derivatives are expensive to obtain. Third-order forms exist too, which matters only if a method ever needs them. |
#[salsa::accumulator] | salsa | DM-47, DM-30 | adopt if salsa is kept. Measured in PROBE 6. Recommended amendment E. Findings travel with a query’s value instead of being threaded through every return type. |
salsa::CancellationToken, salsa::Cycle | salsa | DM-30, DM-35 | evaluate if salsa is kept. §18.3 and §20.1 both specify interruption; §14.2’s rule engine runs to a fixed point. Both have first-class machinery here rather than being platform concerns. |
toml::Spanned | toml | DM-47, DM-46 | adopt. Measured in PROBE 7a. §4.4 defines pse.source_span and §23.2 requires failures that carry source spans; this supplies them for package.toml at no cost. |
serde-saphyr’s budget module | serde-saphyr | DM-30, G3 | adopt with the crate. Bounded parsing — explicit limits on nesting, aliases and allocation — at a boundary that parses user-supplied documents. §23.2 wants typed failures; a resource bound is what turns a hostile document into one. |
proc-macro2’s span-locations | proc-macro2 | DM-46 | evaluate. §4.2 commits generated source and diffs it in CI. Span locations let a generated-code diagnostic point back into the RelationSpec row that produced it, which is DM-46’s lineage requirement applied to generated artifacts. |
blake3::Hasher::update_rayon | blake3 | DM-39 | evaluate — recommended amendment I already covers extending §18.8’s thread budget to hashing; the API is the reason it is actionable. |
rustsec / cargo deny maintenance gate | — | DM-31, G7 | adopt. §1.4’s finding: cargo audit is clean and cannot see serde_yaml’s deprecation. A supply-chain gate that cannot detect the one dependency this map rejects is not a gate. |
egglog::CommandOutput::snapshot_stable_under_proof_encoding | egglog | DM-48, DM-15 | adopt if egglog is adopted. PROBE 8: the stable rendering exists precisely because the Debug one is not. |
uom::str runtime unit parsing | uom | DM-41 | evaluate, narrowly. §8’s registry is authoritative and uom is compile-time, which §3.3 states correctly. uom::str parses a unit string into uom’s compile-time types at runtime, which is the only bridge between the two — relevant if a “selected Rust kernel” ever needs to accept a registry unit. |
Rejected: quantity (arrives with feos) | feos-core | G1, DM-02 | reject. feos-core 0.10.1 pulls quantity ^0.14 [crates.io:feos-core] — a third units representation alongside uom and §8’s dynamic registry. §3.3’s boundary already says §8 is authoritative for quantities; adopting feos imports a units system that does not know that. |
Rejected: diffsol’s linear algebra | diffsol | DM-58 | reject as a second stack. diffsol 0.16.2 depends on diffsol-la and diffsol-nl, not faer [crates.io:diffsol]. Adopting it adds a second linear-algebra implementation to a design that has already chosen one. It also pulls petgraph ^0.8.3, which at least agrees with C4’s pin. |
Rejected: egg | — | DM-57 | reject. The alternative e-graph crate. If equality saturation is ever adopted, the choice between egg and egglog is a real one — but it is a phase-4 choice, and C6’s structural objection (a second model language) applies to both. |
| ~460 further public traits | all | — | not extension points for this design — dismissed as a group |
14. Principle-alignment register (supporting libraries)
Capabilities and disciplines that would improve alignment with DATA_MODEL_DESIGN_CHARTER.md. Every row names a principle and the hand-written work or risk it removes. Rejections are included, because a recorded rejection is worth as much as an adoption.
The previous edition had no register and cited no DM principle at all — the only one of the four capability maps in that position. Its findings were real; they were simply never connected to the charter. Numbering is local to this map.
Adopt
| # | Capability or discipline | Cluster | DM / gate | What it removes or prevents |
|---|---|---|---|---|
| 1 | Pin all 24 supporting crates in §3.1 | §1.1 | DM-31, DM-48, DM-51 | §3.2 claims the manifest “carries every pin once”; §3.1 anchors one of the 24. An unpinned dependency is an undeclared input that can change meaning between builds — and §20.4’s reproduction contract is stated over exactly such inputs. |
| 2 | Pin blake3 and assign it a crate in §3.2 | C9.1 | DM-02, DM-11, DM-48 | It is the most deeply wired supporting crate — ~20 sites across identity, hashing, hash-consing, plan fingerprints and the manifest — with no pin, no owning crate, and two digest widths whose derivation is unnamed. |
| 3 | Name the 128-bit derivation: derive_key(context, material) | C9.1 | DM-11, DM-15, DM-48 | Measured (PROBE 1): the two candidate routes give different bytes, and §5.1 IDs are permanent. Truncation itself is sound — BLAKE3 is an XOF, so a 16-byte prefix is a defined 128-bit digest — but which route is used must be written down. |
| 4 | Replace serde_yaml with serde-saphyr | C3.1 | G3, DM-07, DM-47 | Measured (PROBE 7b): typed errors with caret diagnostics, and four classes of hostile input refused without a panic. The incumbent is unmaintained since 2024-03 and sits at the boundary that parses user documents. |
| 5 | A cargo deny maintenance gate, not cargo audit alone | §1.4 | DM-31, G7 | Measured: all three RustSec advisories touching this set are patched below our versions, so cargo audit is clean — and cannot see serde_yaml’s deprecation, which lives only in semver build metadata. A supply-chain gate blind to the one rejected dependency is not a gate. |
| 6 | Decide the num-dual pin, and state it as a source-level constraint | C7.1, C8.1 | DM-31, G6 | Measured (§1.3): feos-core forces num-dual 0.14 alongside our 0.15, and the trait’s shape differs between them, so every kernel body’s bound differs. Not “two copies of a type” — two versions of the source. |
| 7 | Ban SchemaLike::from_type / from_samples on any platform path | C2.1 | DM-02, G2 | Measured (PROBE 2): tracing a [u8; 16] yields a 16-field struct, not FixedSizeBinary(16). §4.2 already forbids inference; the governance grep should name the functions. |
| 8 | Treat any SERDE_ARROW:* field-metadata key as a contract violation | C2.1 | DM-15, DM-48 | Measured: the adapter stamps SERDE_ARROW:strategy into field metadata, which §5.3 hashes — so provenance would change a relation’s content hash. Settles open question 5 without a hashing-policy exception. |
| 9 | toml::Spanned for package.toml | C3.2 | DM-47, DM-46 | Byte offsets that slice back to source, and spans on type errors. §4.4 defines pse.source_span; this supplies it for free. |
| 10 | #[salsa::accumulator] for pass findings | C5 | DM-47, DM-30 | Deferred, despite the historical probe. Current pass attempts are sidecar evidence; salsa requires R-22/R-01 adoption (ADR-0042). |
| 11 | petgraph::algo::feedback_arc_set for tear selection | §13 | DM-38, DM-25 | Measured (PROBE 3c): returns exactly the recycle edge. §12.5 and §15.3 list tear selection as hand-written; the search is library work, the policy is ours. |
| 12 | num-dual::implicit_derivative for implicit kernels | C7.1 | DM-25, DM-52 | Measured (PROBE 4) against a hand-derived IFT: exact agreement. Amendment G, now with evidence. |
| 13 | Sort within every SCC block by semantic ID | C4 | DM-15, DM-48 | Measured (PROBE 3a): tarjan_scc and kosaraju_scc return the same components in different intra-component orders. Nothing in petgraph guarantees the order, so §5.1’s reproducible-IDs claim depends on the platform imposing one. |
| 14 | Never hash a Debug rendering | C6, §13 | DM-48, DM-15 | Measured (PROBE 8): egglog’s extraction is deterministic while its Debug output is not, because the underlying container is a hash set. The Arrow and DataFusion maps found the same class of defect in Schema metadata and datafusion-proto. The rule is general and belongs in §5.3. |
| 15 | State the Ipopt panic/unwind rule in §18.2 and §18.3 | C11 | G5, DM-30 | A Rust panic crossing a C callback boundary is undefined behaviour. §18.3 describes cancellation via the intermediate callback but never states the rule. Amendment F. |
| 16 | Correct amendment H’s function names | C11 | DM-43, DM-59 | The C interface exports GetIpoptCurrentIterate / GetIpoptCurrentViolations; the previously recommended Ipopt_get_curr_* are the C++ TNLP spellings. The recommendation is right and the identifiers were not. |
Evaluate
| # | Capability | Cluster | DM | Why it is not yet an adopt |
|---|---|---|---|---|
| 17 | faer::matrix_free + sparse LU for §15.5’s smallest singular values | C7.2 | DM-36, DM-40 | Half of §26 F17’s answer. partial_svd targets the largest end of the spectrum; reaching the smallest needs shift-invert. Two components, and neither is yet costed. |
| 18 | pseudoinverse_from_svd_with_tolerance | §13 | DM-40, DM-59 | §15.4’s condition number needs a pseudo-inverse and an explicit tolerance; this has both. Open question 6 asks for the budget first. |
| 19 | salsa::CancellationToken and salsa::Cycle | C5 | DM-30, DM-35 | First-class machinery for two things §18.3, §20.1 and §14.2 specify as platform concerns — but only if F13 resolves in salsa’s favour. |
| 20 | petgraph::algo::{tred, is_bipartite_undirected, dominators} | §13 | DM-10, DM-53 | tred makes §14.1’s pass DAG minimal and readable; is_bipartite_undirected is a cheap assertion on a graph §15.3 assumes is bipartite and never checks. |
| 21 | proc-macro2 span-locations for generated-code lineage | §13 | DM-46 | Would let a diagnostic in generated source point back at the RelationSpec row that produced it. Only worth it once generated code is large enough to need it. |
| 22 | uom::str as the registry→kernel bridge | C7.3 | DM-41 | The only mechanism connecting §8’s runtime units to uom’s compile-time types. Needed only once a “selected Rust kernel” exists — and §3.3 never says which kernels those are (open question 13). |
| 23 | blake3::Hasher::update_rayon | C9.1 | DM-39 | Amendment I’s mechanism. Measure before parallelising a hash that may not be the bottleneck (DM-58). |
| 24 | num-dual’s hessian / partial_hessian / third-order forms | §13 | DM-40 | Bears on §26’s exact-Hessian cost row. Whether third-order is ever needed is a modelling question, not a library one. |
Reject, with reason
| # | Capability | Cluster | Why rejected |
|---|---|---|---|
| 25 | quantity (arrives with feos-core) | C8.1, §13 | A third units representation alongside uom and §8’s registry. §3.3 already names §8 authoritative; importing a units system that does not know that is the second-authority failure G1 describes. |
| 26 | diffsol’s linear-algebra stack | C8.2, §13 | diffsol 0.16.2 pulls diffsol-la/diffsol-nl, not faer. Adopting it adds a second linear-algebra implementation to a design that has chosen one — DM-58. |
| 27 | datafusion-tracing / instrumented-object-store at present | C10.2 | Amendment D’s crates exist but top out at 55.0.0 against a pinned engine at 55.1.0. A plan-instrumenting crate one minor out of step with the plans it instruments is the least forgiving kind of mismatch. Revisit when a 55.1.0 release exists. |
| 28 | egg as an alternative to egglog | §13 | A phase-4 choice that C6’s structural objection — a second model language — applies to equally. |
| 29 | serde_norway, serde_yml as serde_yaml replacements | C3.1 | serde_norway has not been released since 2024-12; serde_yml is deprecated in its own description. Replacing a quiet crate with a quieter one. |
| 30 | serde_yaml itself | C3.1 | Unmaintained since 2024-03, deprecation encoded where no tool reads it. The one erratum the previous edition and this one agree on completely. |
15. Acceptance-gate review (supporting libraries)
The earlier survey’s gates described revision 2. Current document-stage gates are assessed in the revision-5 review. The following are Proposed platform contracts; historical library receipts do not certify their implementation.
| Gates | Current mechanism | Remaining acceptance |
|---|---|---|
| G1–G2 | Registry-derived schemas; no traced schema or foreign unit authority; typed conversions and recursive admission (blueprint §4, §8, ADR-0039) | Invalid metadata, lost quantity context and inferred-schema negative fixtures |
| G3–G4 | Typed parser/kernel failure, declared pure kernel bindings, guarded execution; no adopted salsa query runtime (blueprint §18, ADR-0042/0043/0047) | Real parser/adapter entry points, branch failures, undeclared input rejection |
| G5 | Complete objects and manifests, checksum verification on existing objects, conditional refs; no unwind through solver callbacks (blueprint §20, §18.3, ADR-0045) | Truncated-existing-object, interruption, CAS and callback fixtures |
| G6 | Complete stage input keys and pinned derivative implementations; fine reuse/automatic tracking deferred (ADR-0041/0042/0043) | Changed domains/bindings/fixed values versus clean compilation; derivative conformance |
| G7 | Roles remain bounded by actual consumers; optional FeOs/egglog/observability engines retain register triggers | Numerical routes and performance need their own exercised fixtures/measurements; map receipts are narrower evidence |
16. Leverage matrix
One row per blueprint requirement that rests on a supporting library. Read this first.
| Blueprint requirement | Crate | API | Status |
|---|---|---|---|
§4.2 generated views/builders/schemas from RelationSpec, non-macro, source committed | syn/quote/proc_macro2/prettyplease | non-macro codegen pipeline, prettyplease::unparse | leverage |
§4.2 SerdeArrowSchema “never inferred from samples” | serde_arrow | SchemaLike impl for Vec<FieldRef> | confirmed — inference is not merely banned, it is absent from the path |
| §4.2 import/export adapters | serde_arrow | to_record_batch / from_record_batch, arrow-59 feature | leverage |
| §4.3 field metadata namespace | serde_arrow | STRATEGY_KEY writes into field metadata | open decision — classify as volatile or contractual for §5.3 hashing |
§7.7 expression DSL precedence (+ - / * / / ^ / unary) | winnow | combinator::expression with Prefix/Infix::{Left,Right}/Postfix binding powers | leverage — simpler than the EBNF’s nested-rule encoding |
§7.7 / §4.4 pse.source_span from authored text | winnow, toml | LocatingSlice + with_span(); toml::Spanned<T> | leverage — spans are free on both paths |
| §22.1 YAML document loading | serde_yaml | — | ⚠ erratum — crate unmaintained; replace (C3.1) |
| §12.5 topology, §15.3 SCC/condensation/toposort | petgraph | tarjan_scc, condensation, toposort, is_cyclic_directed | leverage |
| §15.3 deterministic block order | petgraph | intra-component node order is arbitrary | confirmed necessary — sort within blocks by semantic ID |
| §15.3 maximum matching | petgraph | maximum_matching exists but is Gabow, general-graph, O(|V|³) | refined — Hopcroft–Karp still ours; correct §15.3’s stated reason |
| §15.3 Dulmage–Mendelsohn | petgraph | none | build on top (as the blueprint says) |
| §14.3 passes memoized on content hashes | salsa | #[salsa::tracked] + #[salsa::input] | Available but deferred: R-22/R-01; not the current compiler binding (ADR-0042) |
| §14.3 “the memo stores the output artifact hashes” | salsa | PartialEq-driven backdating | Available but deferred: R-22/R-01; not the current compiler binding (ADR-0042) |
| §14.3 pass findings | salsa | #[salsa::accumulator] — excluded from result equality | Available but deferred: R-22/R-01; not the current compiler binding (ADR-0042) |
| §6.13/§23.2 cancellation | salsa | CancellationToken, unwinding | leverage — never unwind through the Ipopt FFI (C11) |
| §14.3 rayon-parallel passes | salsa | per-key claiming; second caller blocks | Available but deferred: R-22/R-01; not the current compiler binding (ADR-0042) |
| §3.1 snapshot immutability inside a session | salsa | Durability::NEVER_CHANGE | leverage — assignment is an open item |
| §7.4 equality-saturation rewrites (phase 4) | egglog | EGraph, rust_rule, run_ruleset, cost-model extraction | capable; determinism unverified — gate adoption on it |
§18.5 one kernel body over DualNum | num-dual | DualNum bound; Dual/Dual2/HyperDual | confirmed |
| §18.2 kernel second derivatives | num-dual | HyperDual, hessian, partial_hessian | leverage |
| §18.2 implicit kernels via the implicit function theorem | num-dual | implicit_derivative*, ImplicitDerivative | library capability, not platform code |
| §18.2 per-equation reverse AD | num-dual | forward-mode only | build on top — confirms §3.3’s kernel-local boundary |
| §15.4 condition number | faer | sparse::linalg::solvers::Lu + Solve; no condition estimator | build on top — O(n) solves; budget is an open decision |
| §15.4 non-square pseudo-inverse | faer | sparse::linalg::solvers::Qr + SolveLstsq | leverage |
| §15.5 SVD toolbox, small problems | faer | svd(), thin_svd(), singular_values() | leverage |
| §15.5 “iterative for large” | faer | matrix_free::eigen::partial_svd exists, but targets the LARGEST singular values [probe] | refined — §15.5 wants the smallest; the route is sparse LU (shift-invert) plus matrix_free. Half of §26 F17 (C7.2) |
| §3.3 solver workspaces, thread budget | faer | MemStack (dyn_stack), Par | leverage |
| §3.3 compile-time unit checks in kernels | uom | Quantity<D,U,V>, system!/quantity!/unit! | leverage, kernel-local |
| §8 dynamic registry stays authoritative | uom | no runtime unit lookup exists | confirmed as a necessity, not a preference |
| §4.4 8-dimensional basis incl. currency | uom | stock SI has no currency dimension | open item — custom system! or keep costing outside uom |
| §9.8 FeOs provider | feos-core | Residual/IdealGas/Total, EquationOfState, State, PhaseEquilibrium | leverage — ⚠ num-dual ^0.14 (C8.1) |
| §13.6 native DAE backend | diffsol | OdeBuilder, new_implicit_closure, NonLinearOp(Jacobian), LinearOp mass matrix | leverage via closures — do not adopt DiffSL |
| §18.2 sparse Jacobian → diffsol | diffsol | interface asks for a Jacobian-vector product | open item — adapter fit check |
| §5.3 logical and encoding hashes | blake3 | Hasher::update / update_reader | pinned mechanism; v2 framing/normalization is platform code and requires metamorphic tests (ADR-0045) |
§5.1 blake3_128 derived IDs | blake3 | new_derive_key(context) vs. truncating finalize() | design decision — choose before freezing; truncation safety [UNVERIFIED] |
| §5.3 hashing large artifacts | blake3 | update_rayon, update_mmap_rayon | leverage — must respect §18.8’s thread budget |
| §20.1 atomic writes, conditional ref update | object_store | put_opts + PutMode::Update(UpdateVersion), Error::Precondition | confirmed exactly as stated |
| §20.1 idempotent artifact writes | object_store | PutMode::Create plus existing-object verification | logical identity and physical integrity are separate; ADR-0045 |
| §20.1 local single-writer mode | object_store | LocalFileSystem — documented atomic | confirmed |
| §20.1 commit protocol | object_store | write the final path conditionally; rename does not exist at the pinned 0.13.2 | refined — amendment J should name PutMode, not a removed method (C9.2) |
§3.1 object_store API surface | object_store | 0.13.0 merged copy/copy_if_not_exists→copy_opts, delete→delete_stream, put/get/head→*_opts | erratum — the previous edition described a different version (C9.2, ledger row 60) |
| §3.1 pin coverage | all 24 supporting crates | §3.1 anchors one (tokio) while §3.2 claims “every pin once” | erratum — §1.1, amendment A′ |
§5.1 blake3_128 derivation | blake3 | derive_key vs truncated finalize give different bytes; truncation itself is the XOF prefix and is sound [probe] | erratum — §5.1 names neither; amendment K |
| §12.5 tear selection | petgraph | algo::feedback_arc_set returns exactly the recycle edge [probe] | adopt, unclaimed — §12.5 lists this as hand-written (§13) |
| §18.2 implicit kernels | num-dual | implicit_derivative matches a hand-derived IFT exactly [probe] | adopt, unclaimed — amendment G, now measured |
| §4.2 “never inferred from samples” | serde_arrow | tracing a [u8;16] yields a 16-field struct, and stamps SERDE_ARROW:strategy [probe] | confirmed necessary, and unenforced — amendment L |
| §23.2 typed failures with spans at the authoring boundary | serde-saphyr, toml | caret diagnostics with line/column; four hostile inputs refused without panic; toml::Spanned byte offsets [probe] | adopt — settles open question 1 |
| §24 supply-chain gate | — | cargo audit is clean and cannot see serde_yaml’s deprecation [rustsec:*] | gap — amendment M |
| §14.3/§18.8 pass and kernel parallelism | rayon, tokio | one owned thread budget | leverage — extend the rule to hashing and diagnostics |
| §23.1 spans per pass/rule/stage/solve | tracing | #[instrument], fields(...), Span::record | leverage — declare late fields as field::Empty |
| §23.1 DataFusion and object-store visibility | datafusion-tracing, instrumented-object-store | extension-point instrumentation | recommend adding to §3.3 (version-locked to DataFusion) |
| §23.2 failure classes as dotted codes | miette | #[diagnostic(code(a::b::c), severity(...), help(...))] | near-exact fit — §23.2’s table becomes the code registry |
| §23.2 source-anchored failures | miette | #[source_code] NamedSource, #[label] SourceSpan | leverage |
| §23.2 failures carrying violating relation rows | miette | related(); no row-set concept | partial — rows stay the record, the diagnostic is the rendering |
| library error types | miette | miette::Result is for applications, not libraries | design rule — pse-* crates return concrete errors |
| §18.3 in-process Ipopt | Ipopt C API | CreateIpoptProblem/AddIpopt*Option/IpoptSolve/FreeIpoptProblem, five callbacks | confirmed |
| §18.3 iteration statistics | Ipopt C API | SetIntermediateCallback + Ipopt_get_curr_iterate/Ipopt_get_curr_violations (≥3.14) | leverage — structured, better than §23.1’s text parsing |
| §18.2 domain-guard evaluation failures | Ipopt C API | callbacks return Bool; false = evaluation failure | confirmed |
| §17.5 continuation re-solves | Ipopt C API | warm_start_init_point, warm_start_bound_push, mu_init | leverage |
17. Evidence ledger
Every lookup performed, in order. c7 = context7 plugin; WebFetch = docs.rs/crates.io; rustdoc = the local extraction of §1.5; probe = a program compiled against the pinned crates.
Rows 1–52 are the previous edition’s. The five rows whose instrument read local cited files that no longer exist; each now records what replaced it, and none is load-bearing any more. Rows 53–64 are this edition’s.
| # | Cluster | Instrument | Library ID / URL | Query | Outcome |
|---|---|---|---|---|---|
| 1 | C4 | c7 resolve | petgraph | Rust graph data structures, SCC, topological sort, bipartite matching | 4 candidates; selected /websites/rs_petgraph (1692 snippets, High) |
| 2 | C7 | c7 resolve | faer | Rust sparse LU QR factorization, dense SVD, condition number estimation | 1 candidate /sarah-quinones/faer-rs (20 snippets) |
| 3 | C7 | c7 resolve | num-dual | Rust generic dual numbers, hyper-dual, forward-mode AD, DualNum trait | no match — unrelated libraries returned |
| 4 | C7 | c7 resolve | num_dual (alt spelling) | generalized dual numbers AD Rust crate feos | no match |
| 5 | C7 | c7 query-docs | /websites/rs_num-dual (direct ID probe) | DualNum trait, Dual, Dual2, HyperDual types | not found — ID guessing confirmed to fail cleanly |
| 6 | C5 | c7 resolve | Salsa | Rust incremental computation, memoized queries, tracked functions, durability, revisions | 3 candidates: /websites/rs_salsa (796), /websites/salsa-rs_github_io_salsa (book, 80.2), /salsa-rs/salsa (234) |
| 7 | C8 | c7 resolve | diffsol | Rust ODE DAE solver BDF SDIRK mass matrix Jacobian sparse | 1 candidate /martinjrobins/diffsol (396 snippets) |
| 8 | C2 | c7 resolve | serde_arrow | Rust structs to Arrow RecordBatch via serde, SerdeArrowSchema, explicit schema | no match — resolved to serde / apache/arrow instead |
| 9 | C7 | c7 resolve | uom | Rust compile-time dimensional analysis, type-safe quantity arithmetic, SI base units | 1 candidate /iliekturtles/uom (58 snippets, High) |
| 10 | C7 | c7 query-docs | /sarah-quinones/faer-rs | sparse LU and QR factorization and solving linear systems | sparse Cholesky solve, try_new_from_triplets, decomposition list |
| 11 | C7 | WebFetch | https://docs.rs/num-dual/latest/num_dual/ | version, DualNum trait, all dual types, differentiation drivers | 0.15.0; full type and driver inventory incl. implicit_derivative* |
| 12 | C7 | WebFetch | https://docs.rs/faer/latest/faer/ | version, modules, matrix types, decompositions, sparse solvers, Par, MemStack | 0.24.4; sparse::linalg::solvers::{Llt,Lu,Qr}; no condition-number or iterative-SVD API |
| 13 | C7 | c7 query-docs | /iliekturtles/uom | defining a custom quantity system with system! and quantity! | macro forms for custom systems and added units |
| 14 | C7 | WebFetch | https://docs.rs/uom/latest/uom/ | version, MSRV, Quantity parameters, storage types, compile-time vs runtime resolution | 0.38.0, MSRV 1.68.0; compile-time only, no dynamic unit lookup |
| 15 | C2 | WebFetch | https://docs.rs/serde_arrow/latest/serde_arrow/ | version, arrow feature flags, schema module, conversion functions | 0.15.0; arrow-53…arrow-59 ✔ |
| 16 | C2 | WebFetch | https://docs.rs/serde_arrow/latest/serde_arrow/schema/index.html | SchemaLike implementors, TracingOptions, strategy metadata keys | Vec<FieldRef>/Vec<Field>/SerdeArrowSchema; STRATEGY_KEY, Strategy, Overwrites |
| 17 | C5 | c7 query-docs | /websites/salsa-rs_github_io_salsa | database with tracked functions, inputs, interned structs; setting inputs and invalidation | #[salsa::db/input/tracked/interned], set_with_durability |
| 18 | C5 | c7 query-docs | /websites/salsa-rs_github_io_salsa | backdating on equal recomputed values; durability and revisions | backdating semantics; memo contents; durability-based shallow verification |
| 19 | C5 | c7 query-docs | /websites/rs_salsa | cancellation, parallel execution, accumulators, LRU eviction | CancellationToken, #[salsa::accumulator], per-key claiming, trigger_lru_eviction, report_untracked_read |
| 20 | C5 | WebFetch | https://docs.rs/salsa/latest/salsa/ | version, macros, traits, tracked-return bounds, no_eq/lru/cycle_fn | 0.28.2; PartialEq required for backdating; returns(...) modes |
| 21 | C9 | c7 resolve | object_store | conditional put, atomic writes, LocalFileSystem, get_range | /apache/arrow-rs-object-store (451 snippets) |
| 22 | C9 | c7 query-docs | /apache/arrow-rs-object-store | conditional put with PutMode::Create/Update and UpdateVersion | full PutMode enum, put_opts contract, optimistic-concurrency loop |
| 23 | C9 | c7 query-docs | /apache/arrow-rs-object-store | which backends support conditional put; LocalFileSystem atomicity; S3 config | inconclusive on per-backend support — escalated to docs.rs (row 28) |
| 24 | C9 | arrow_rust_59_datafusion55_advanced_reference_2026-08-23.md §14.11 | PutMode, commit protocol, rename caveats | conditional-put semantics + “never rename as a commit primitive” advisory | |
| 25 | C9 | c7 resolve | BLAKE3 | Hasher, keyed hashing, derive_key, XOF, rayon | /websites/rs_blake3 (1063 snippets, benchmark 87.15) |
| 26 | C9 | c7 query-docs | /websites/rs_blake3 | new, new_keyed, new_derive_key, update, finalize | constructor and update signatures |
| 27 | C9 | WebFetch | https://docs.rs/blake3/latest/blake3/struct.Hasher.html | version, full method list, feature gates, truncation safety | 1.8.7; update_rayon/update_mmap* feature gates; truncation not documented |
| 28 | C9 | WebFetch | https://docs.rs/object_store/latest/object_store/ | version, per-backend conditional-put support, LocalFileSystem atomicity | 0.14.1; all five backends support conditional put; local documented atomic |
| 29 | C3 | c7 resolve | winnow | parser combinators, spans, Located, Stateful, precedence | 3 candidates; selected /websites/rs_winnow_x86_64-pc-windows-msvc (5004 snippets) |
| 30 | C3 | c7 query-docs | /websites/rs_winnow_… | LocatingSlice/Stateful and the with_span combinator | span capture as Range<usize> |
| 31 | C3 | c7 query-docs | /websites/rs_winnow_… | operator-precedence expression parsing; ParseError offset | combinator::expression example; ParseError::offset() |
| 32 | C3 | WebFetch | https://crates.io/crates/serde_yaml | version, deprecation status | no usable content returned (SPA shell) — escalated to docs.rs |
| 33 | C3 | WebFetch | https://docs.rs/serde_yaml/latest/serde_yaml/ | version, deprecation banner, API | 0.9.34+deprecated; “(This project is no longer maintained.)” |
| 34 | C3 | c7 resolve | serde_norway | maintained Rust YAML serde library | three viable replacements: /websites/rs_serde_norway (5689 snippets), /bourumir-wyngs/serde-saphyr (355), /websites/rs_crate_serde_yml (69) |
| 35 | C3 | WebFetch | https://docs.rs/toml/latest/toml/ | version, Spanned, features, underlying parser | 1.1.6+spec-1.1.0; Spanned<T>; depends on winnow ^1.0.0 |
| 36 | C3 | WebFetch | https://docs.rs/winnow/latest/winnow/ | version, MSRV, modules, combinator::expression presence | 1.0.4; crate-root page inconclusive on expression — escalated |
| 37 | C3 | WebFetch | https://docs.rs/winnow/latest/winnow/combinator/index.html | does expression exist, with Prefix/Infix/Postfix? | confirmed present in 1.0.4 |
| 38 | C11 | ipopt_standalone.md §20.5 | C interface flow, callback signatures, bound semantics | full C-API coverage; IpStdCInterface.h as source of truth | |
| 39 | C11 | c7 resolve | ipopt-sys | Rust FFI bindings to the Ipopt C interface | only /coin-or/ipopt (upstream C++) — no established Rust binding |
| 40 | C11 | c7 query-docs | /coin-or/ipopt | SetIntermediateCallback, Ipopt_get_curr_iterate/Ipopt_get_curr_violations | structured iterate/violation API, ≥3.14; warm-start options |
| 41 | C4 | petgraph.md §16–§18 | matching algorithms; SCC output ordering | maximum_matching = Gabow, O(|V|³); intra-component order arbitrary | |
| 42 | C1 | rust_codegen_syn_quote_proc_macro2_prettyplease.md | version anchors, structure | syn 3.0.3 / quote 1.0.47 / proc-macro2 1.0.107 / prettyplease 0.3.0; §21 non-macro pipeline | |
| 43 | C10 | datafusion-tracing.md, rust_parallel_concurrency_stack_reference_2026-08-19.md | DataFusion/object-store instrumentation; tokio & rayon pins | instrument_with_spans!, instrument_rules_with_spans!, version lockstep; tokio 1.53.1, rayon 1.12.0 | |
| 44 | C10 | c7 resolve | miette | Diagnostic derive, SourceSpan, composing with thiserror | /websites/rs_miette (1887 snippets) |
| 45 | C10 | c7 query-docs | /websites/rs_miette | deriving Diagnostic with code/severity/help/url and labelled spans | full derive form, Diagnostic trait methods, libraries-return-concrete-types rule |
| 46 | C10 | c7 resolve | tracing | spans, events, structured fields, instrument, subscriber | /websites/rs_tracing; also tracing-subscriber, tracing-opentelemetry |
| 47 | C10 | c7 query-docs | /websites/rs_tracing | #[instrument] with skip/fields/err; recording fields after span creation | fields must be pre-declared (field::Empty) or record is silently dropped |
| 48 | C6 | c7 resolve | egglog | equality saturation with Datalog, rewrite rules, extraction | /egraphs-good/egglog (115 snippets) |
| 49 | C6 | c7 query-docs | /egraphs-good/egglog | datatypes, conditional rewrite rules, saturation, cost-model extraction | EGraph, parse_and_run_program, rust_rule, run_ruleset, extraction API |
| 50 | C8 | c7 resolve | FeOs | SAFT/PC-SAFT Helmholtz EOS, phase equilibrium, num-dual | no match — unrelated libraries returned |
| 51 | C8 | c7 query-docs | /martinjrobins/diffsol | DAE with mass matrix; residual/Jacobian callbacks from Rust rather than the DSL | OdeBuilder, new_implicit_closure, NonLinearOp(Jacobian), LinearOp |
| 52 | C8 | WebFetch | https://docs.rs/feos-core/latest/feos_core/ | version, core traits, phase equilibrium, AD integration, features | 0.10.1; depends on num-dual ^0.14 — the version-split finding |
| 53 | §1.2 | crates.io API | every crate in §1.2 | current version, release date, maintenance recency | all anchors confirmed current; syn had drifted 3.0.3 → 3.0.5; serde_yaml last released 2024-03-25 [crates.io:serde_yaml] |
| 54 | §1.3 | cargo | cargo generate-lockfile over the §1.2 manifest | does the pinned set resolve coherently? | 362 packages, 17 names at >1 version; num-dual 0.14.2 + 0.15.0 side by side; reverse deps show the split is entirely feos-core + quantity |
| 55 | §1.5 | rustdoc | locally generated, 26 documents, format_version 61, lockfile committed | full API surface of the pinned crates | 190,463 declarations, 589 public traits, 113,713 impl relations → build/facts/support |
| 56 | C1 | rustdoc | syn@3.0.5, quote@1.0.47, proc-macro2@1.0.107, prettyplease@0.3.0 | re-source the deleted codegen reference | pins confirmed; syn 3 (not the syn 2 most of the ecosystem uses) confirmed |
| 57 | C4 | rustdoc | petgraph@0.8.3 | re-source amendment B | “Gabow’s algorithm… treated as if undirected… O(|V|³)… a better time complexity might be used in the future”, verbatim; plus: panics if node_bound() == usize::MAX |
| 58 | C4 | probe | probe_graph.rs | SCC order stability; matching direction-independence; feedback arc sets | PROBE 3 — tarjan_scc vs kosaraju_scc disagree on intra-component order; matching identical under full edge reversal; greedy_feedback_arc_set returns exactly the recycle edge |
| 59 | C9.1 | probe | probe_identity.rs | blake3 derivation routes and truncation soundness | PROBE 1 — hash[..16] ≠ derive_key(ctx, ·)[..16]; finalize_xof(16) == hash[..16], so truncation is the XOF prefix and is defined |
| 60 | C9.2 | WebFetch | https://docs.rs/object_store/0.13.2/ | the trait surface at the pinned version | erratum — ten methods; rename, copy_if_not_exists, rename_if_not_exists, put, get, head, delete, copy do not exist; 0.13.0 merged them into *_opts. PutMode::{Create, Update, Overwrite} confirmed present |
| 61 | C11 | gh | coin-or/Ipopt@releases/3.14.16 → src/Interfaces/IpStdCInterface.h | re-source the deleted Ipopt reference | 11 exported functions; Intermediate_CB’s 12 typed parameters first-hand; index_style “0 for C style” verbatim; bound-infinity semantics verbatim; amendment H’s names corrected to GetIpoptCurrentIterate / GetIpoptCurrentViolations |
| 62 | C10.2 | crates.io API | datafusion-tracing, instrumented-object-store | does amendment D’s version lockstep hold? | both at 55.0.0 (2026-08-24); no 55.1.0 exists against a pinned engine at 55.1.0 — amendment D is currently unsatisfiable |
| 63 | §1.4 | rustsec | rustsec/advisory-db | advisories against this dependency set | three — serde_yaml RUSTSEC-2018-0005, tracing RUSTSEC-2023-0078, object_store RUSTSEC-2024-0358 — all patched below our versions. No unmaintained advisory exists for serde_yaml, so its deprecation is invisible to cargo audit |
| 64 | C2, C3, C5, C6, C7 | probe | probe_identity.rs, probe_parse.rs, probe_compile.rs, probe_numerics.rs | PROBES 2, 4, 5, 6, 7, 8 | serde_arrow traces [u8;16] to a 16-field struct and stamps SERDE_ARROW:strategy; implicit_derivative matches a hand IFT exactly; faer’s BiLinOp contract verified; salsa backdates and accumulates; serde-saphyr refuses four hostile inputs without panicking; egglog extraction is deterministic while its Debug rendering is not |
18. Open questions, with recommendations
The earlier survey’s amendment requests are reconciled below. Evidence sections and receipts remain the original observations; this is a decision update, not a new full-library probe run. Current pins are owned by Cargo.toml and Cargo.lock.
| Topic from the survey | Current disposition | Remaining verification / trigger |
|---|---|---|
| YAML replacement | serde-saphyr, ADR-0021; typed located failures (blueprint §22.1) | Platform authoring negatives; historical PROBE 7b is a library characterization |
num-dual, FeOs and units | One pinned num-dual family; FeOs conditional under ADR-0022/R-06; no uom or foreign quantity authority (ADR-0026) | Recheck the exact dependency graph and semantic adapter before optional FeOs adoption |
object_store surface and publication | Pinned options APIs; PutMode::Create and conditional ref updates; logical hash separate from encoded checksum (ADR-0045) | Existing-byte verification, restore and interrupted publication; R-10 before multi-writer/cloud support |
| Identity and canonicalization | pse-ids owns semantic-ID derive-key contexts and v2 logical/encoded hashing (blueprint §5.1/§5.3) | Recursive null/metadata normalization, exact membership and framing fixtures; R-23 for a larger envelope |
serde_arrow and SERDE_ARROW:* | Registry supplies explicit schemas; unregistered metadata is rejected (blueprint §4) | Generated boundary conformance, including nested types |
| Salsa durability, accumulators and backdating | Available library mechanisms, deferred under ADR-0042/R-01; current compiler uses complete stage keys | R-22 complete finer input bundle and measurements first; changed lineage cannot silently authorize a skipped stage |
faer condition estimates and smallest singular values | Consumer-specific diagnostics in blueprint §15.4–§15.5; sparse LU and explicit operator/conditioning assumptions remain necessary | End-to-end smallest-value/condition-number accuracy and cost; the operator probe alone is insufficient |
diffsol and its numerical stack | Optional later-phase backend in blueprint §13.6/§25 | Qualify its actual Jacobian, solver, dependency and failure routes before claiming backend support |
egglog | Optional; no Debug hashing or unguarded numerical identities (blueprint §7.3, R-05) | Cross-process/version extraction and numerical equivalence; prior same-process observations are narrower |
| Ipopt C bindings and structured iterates | Generated owned -sys bindings and capability-resolved structured iterate APIs (ADR-0028/0038, blueprint §18.3) | Actual callback/interruption/linkage acceptance for each supported platform; setup preflight is not numerical parity |
| Thread and memory budgets | One declared thread budget, shared runtime, fallible platform reservations (ADR-0046) | Concurrent consumers and release; pool peaks alongside process peaks |
| Maintenance, pins and optional instrumentation | Family/maintenance gates and ADR-0037 register triggers remain | Library upgrades require new evidence; no supporting-library re-resolution is claimed here |
Use plan 02 for the revision-5 implementation handoff. Unchanged specialized numerical/provider questions remain unverified until their actual consumer is implemented and exercised.
Python libraries
status: evidence-map blueprint_revision: 5 pins: pyproject.toml and uv.lock regenerated: null reviewed: 2026-09-13
Python libraries — capability map
Current binding: blueprint revision 5. The original survey and version/probe receipts below retain their historical scope; they were not regenerated in this update. Earlier revision comparisons and parked recommendations are historical. The revision-4 review and ADR-0039–ADR-0048 determine the corrected design bindings below. Platform integration remains Proposed; interface and probe evidence are labeled at their original scope. No Python behavioral corpus was re-audited by this revision.
| Current binding | Decision and limit | Evidence / authority |
|---|---|---|
| Arrow admission | Explicit recursive contract validation, registration/loss checks and typed outcomes | ADR-0039/0043; blueprint §21.1 |
| Pyomo affine lowering | LinearExpression only for a proven affine-in-variables body; constants have degree 0 | ADR-0047; review R4-09 |
| Guarded and opaque bindings | Actual writer/solver capability and numerical-policy preflight; Expr_if construction is not lazy-evaluation proof | ADR-0043/0047; blueprint §21.2 |
| Bounded streams | Safe owning buffers and measured bounded coalescing; no whole-dataset transfer copy | ADR-0047/0048; R-24 |
Fourth and final map in the series, completing blueprint §3.3. The Rust side is covered by arrow-rust.md, datafusion-rust.md and supporting-rust-libraries.md; this covers line 331 — the Python row — plus the adjacent libraries the Python boundary actually depends on.
Compiled 2026-09-13.
0. Purpose, sourcing constraint, and evidence rules
0.1 What this document is
Per cluster: what each library can actually do, which APIs the blueprint’s requirements land on, what we deliberately will not use, and where the blueprint assumes a capability that does not exist or is under-specified. A second pass asks what these libraries offer that the blueprint has not claimed, which would improve alignment with design_principles/DATA_MODEL_DESIGN_CHARTER.md (DM-01…DM-60, gates G1–G7).
This boundary deserves the scrutiny. Everything crossing it was already typed once, in Rust, against a registry. §4.4 asserts that Python consumers “register matching pyarrow.ExtensionType classes”; §21.5 asserts typed attrs classes with no Any fields. Those are DM-42 and G2 claims about a language with no compiler to enforce them — so they were measured rather than assumed.
0.2 Sourcing constraint
Nothing is taken from existing repository documentation. This is both an instruction and a fact: docs/library_ref/ has been deleted, taking the former pyomo.md, pyarrow.md, attrs.md, cattrs.md and msgspec.md references with it. Four fresh lanes:
| Lane | Marker | What it is |
|---|---|---|
| locally generated API surface | [api:lib@version] | The rustdoc analogue: a script walking each library’s public API via inspect/importlib, emitting JSON — modules, classes, callables, signatures, __all__, MRO. Authoritative for “does this exist, with this signature, at this version”. |
| locally run probes | [probe] | The arrow_probe.rs analogue: scripts that measure behaviour the blueprint asserts. Eight were run; their output is quoted inline. |
| context7 | [c7:/id] | Idiom and intent |
| PyPI / upstream | [pypi:pkg], [docs:url] | Versions, release metadata, dependency requirements |
Checked mechanically: zero [ref:] citations, and every marker resolves to a ledger row. A claim carrying no marker is a defect.
0.3 Status vocabulary
leverage · build on top · confirmed · refined / erratum · adopt / evaluate / reject · open item. Pass-2 recommendations are disciplined by charter §G and DM-58: a recommendation naming no principle and replacing no identified hand-written work does not appear.
0.4 A correction to this document’s own reconnaissance
An early probe of the neighbouring environment suggested that an unregistered pse.* extension type would surface in Python as pyarrow.UnknownExtensionType. Measurement showed otherwise (§3, PROBE 1): it deserializes to its plain storage type. UnknownExtensionType exists in the API but is not what this path produces. The corrected finding is materially different — and better — because the degradation is silent at the type level but detectable through retained field metadata. Recorded here because the wrong version briefly informed the plan.
1. Version anchors and environment receipts
1.1 Anchors — every library at its current PyPI release
Owner’s instruction: use the latest release of every library, not the versions §3.1 names. All eight verified against PyPI at compile time [pypi:*].
| Library | This map | Blueprint §3.1 | Gap |
|---|---|---|---|
| Pyomo | 6.10.1 | “6.9+” | open lower bound → pin |
| pyarrow | 25.0.1 | “15+” | ten majors behind → pin |
| attrs | 26.1.0 | unpinned | no pin at all |
| cattrs | 26.2.0 | unpinned | no pin at all |
| msgspec | 0.21.1 | unpinned | no pin at all |
| numpy | 2.5.3 | unpinned | arrives via Pyomo/pyarrow |
| scipy | 1.18.1 | unpinned | arrives via Pyomo |
| idaes-pse | 2.12.0 | “2.10 for parity tests only” | 2.10 → 2.12 |
| pint | 0.26.1 | excluded by §3.3 | see §5 — required, not optional |
1.2 Two interpreters, and why
idaes-pse 2.12.0 classifies Python 3.10–3.13 only [pypi:idaes-pse]; its requires_python is unset, so pip would not block a 3.14 install, but it is untested there. Rather than guess, the extraction uses two environments:
py314/ CPython 3.14.7 pyomo 6.10.1, pyarrow 25.0.1, attrs 26.1.0, cattrs 26.2.0,
msgspec 0.21.1, numpy 2.5.3, scipy 1.18.1 (+ pint 0.26.1, see §5)
py313/ CPython 3.13.12 the same set plus idaes-pse 2.12.0
(pulls pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1)
Every library ==-pinned, mirroring the =-pin discipline §3.1 adopted for Rust. IDAES imports cleanly on 3.13.12 [probe].
This split is itself a §3.1 finding. §3.1 says “Python 3.11+”; IDAES currently caps the interpreter at 3.13 for anything that must run the parity harness (§24). The platform’s own Python package can target 3.14; the parity environment cannot.
1.3 Extraction receipt
| Dump | Library | Version | Interpreter | Modules | Items |
|---|---|---|---|---|---|
pyarrow.json | pyarrow | 25.0.1 | 3.14.7 | 28 | 1445 |
pyomo.json | pyomo | 6.10.1 | 3.14.7 | 505 | 5898 |
attrs.json | attrs | 26.1.0 | 3.14.7 | 6 | 61 |
cattrs.json | cattrs | 26.2.0 | 3.14.7 | 20 | 416 |
msgspec.json | msgspec | 0.21.1 | 3.14.7 | 7 | 91 |
numpy.json | numpy | 2.5.3 | 3.14.7 | 69 | 2414 |
scipy.json | scipy | 1.18.1 | 3.14.7 | 156 | 1772 |
idaes.json | idaes | 2.12.0 | 3.13.12 | 191 | 2762 |
Scripts, both requirement sets and all probe programs are committed under docs/capability-maps/evidence/python/ so every [api:…] and [probe] marker is re-derivable.
2. pyarrow — PyCapsule interop and the stream boundary (P1)
Role: §21.1 — “every table crosses as an Arrow C stream (__arrow_c_stream__), never as row objects”; D12 — “one adapter consumes a CanonicalMathProblem bundle over the Arrow C stream interface”.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| PyCapsule producer/consumer | Table.__arrow_c_stream__, RecordBatchReader.__arrow_c_stream__, DataType.__arrow_c_schema__, ExtensionType.__arrow_c_schema__ | [api:pyarrow@25.0.1] |
| Stream construction from any producer | pa.RecordBatchReader.from_stream(obj) — consumes anything exposing __arrow_c_stream__, regardless of which library produced it | [api:pyarrow@25.0.1], [probe] |
| Core containers | Table, RecordBatch, RecordBatchReader, Schema, Field, ChunkedArray, Array, ExtensionArray, ExtensionScalar | [api:pyarrow@25.0.1] |
| IPC | pa.ipc.new_stream, pa.ipc.open_stream, file variants | [api:pyarrow@25.0.1] |
What the blueprint binds to
| Blueprint requirement | API | Status |
|---|---|---|
§21.1 tables cross as __arrow_c_stream__, never as row objects | present on Table and RecordBatchReader | confirmed [api:pyarrow@25.0.1] |
§21.1 “object exposing __arrow_c_stream__ → pyarrow / polars / datafusion-python” | RecordBatchReader.from_stream consumes the protocol generically | confirmed — the capsule protocol is implementation-neutral, so the Rust side need not know which Python Arrow library the consumer uses |
| §21.1 “Transfer is one bundle, not per-node calls” | a stream per named table | confirmed as the right shape — see DM-37 below |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Cross the boundary as a stream, not a materialised Table | DM-37 (cross expensive boundaries in coarse, typed units) | adopt, and state it. §21.1 already says “one bundle”; the sharper rule is that each table should cross as a RecordBatchReader the consumer drains, not a fully materialised Table. One FFI crossing per relation either way, but the streaming form does not require the whole relation resident on both sides simultaneously. |
Keep pyarrow out of the contract | DM-41 (adapters mechanical), DM-57 | adopt. The capsule protocol is the contract; pyarrow is one implementation of it. §21.5’s typed classes should be built from the streamed batches, never from pyarrow-specific types in their field annotations — otherwise the Python contract layer is pinned to one Arrow implementation. |
Gaps and risks
datafusion(Python) lagged the Rust engine in the environment surveyed — 54.0.0 against the Rust side’s 55.1.0. It is only a consumer of the capsule protocol, so version skew is tolerable by design; worth stating explicitly, since §21.1 names it and a reader could infer the versions must match.pyarrow25.0.1 against §3.1’s “pyarrow 15+” is a ten-major gap. Nothing observed here requires 15; but an unpinned floor means CI could resolve anything in that range, and extension-type behaviour (§3) is exactly the kind of thing that moved over those releases. Pin it.
3. pyarrow — extension types, metadata, and the loss profile (P2)
The cluster that decides whether the semantic typing built in Rust survives into Python. DM-06, DM-42, DM-44, G2.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Extension base | pa.ExtensionType(storage_type, extension_name); bases BaseExtensionType → DataType | [api:pyarrow@25.0.1] |
| Required overrides | __arrow_ext_serialize__(self) -> bytes, __arrow_ext_deserialize__(cls, storage_type, serialized) | [api:pyarrow@25.0.1] |
| Optional overrides | __arrow_ext_class__, __arrow_ext_scalar_class__ | [api:pyarrow@25.0.1] |
| Registry | pa.register_extension_type(instance), pa.unregister_extension_type(name) | [api:pyarrow@25.0.1] |
| Inspection | extension_name, storage_type, wrap_array, field, num_buffers, byte_width | [api:pyarrow@25.0.1] |
| Unresolved marker | pa.UnknownExtensionType exists as a type | [api:pyarrow@25.0.1] |
| Array/scalar | pa.ExtensionArray.from_storage(type, storage), pa.ExtensionScalar | [api:pyarrow@25.0.1] |
Measured: the loss profile (PROBE 1 and 2)
A pse.semantic_id-shaped extension over binary(16), with pse.* schema and field metadata, round-tripped through IPC and through __arrow_c_stream__:
PROBE 1 extension type through IPC
registered -> field type: SemanticId | extension_name: pse.semantic_id
UNregistered -> field type: FixedSizeBinaryType | extension_name: None | storage: fixed_size_binary[16]
unregistered field metadata retains ARROW:extension:name: True
PROBE 2 metadata across IPC and PyCapsule
IPC schema md : b'rel-1' IPC field md : b'qt-demo'
caps schema md : b'rel-1' caps field md : b'qt-demo'
caps id type : SemanticId
| Blueprint claim | Verdict |
|---|---|
§4.4 “Python consumers register matching pyarrow.ExtensionType classes; unknown consumers see storage types” | confirmed exactly. With a registered class the type round-trips as SemanticId; without one it becomes fixed_size_binary[16]. |
| §4.3 metadata “preserved through IPC/Parquet” — the Python half | confirmed for IPC and for the PyCapsule path, at both schema and field level. |
| §21.1 the capsule path preserves extension types | confirmed — caps id type: SemanticId. |
This document’s own earlier claim that unregistered types surface as UnknownExtensionType | refuted — they surface as the plain storage type. |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Detect degradation via retained ARROW:extension:name metadata | DM-42, G2, DM-59 | adopt — the key finding of this cluster. Loss of an extension type is silent at the type level: a consumer sees fixed_size_binary[16] with no error. But the field metadata still carries ARROW:extension:name [probe]. So the Python contract layer can assert, per column, “this field claims extension X — is X registered?” and fail loudly if not. That converts an invisible G2 degradation into a detectable, testable condition, and it costs one check at the boundary. |
Ship and register the pse.* extension classes as part of the Python package | DM-44 (extensions complete, versioned, conformance-testable) | adopt. §4.4 says consumers register matching classes but does not say who ships them. If each consumer writes its own, the serialization contract forks. The ten pse.* classes should be generated from the same registry that generates the Rust side (§4.2) and shipped in python/pse/, with a conformance test mirroring the Rust one. |
| Declare the loss profile per column in the bundle | G7, DM-42 | adopt. §21.1’s manifest should record, per table, which fields carry extension types — so a consumer can verify coverage rather than discover a degradation by its absence. |
__arrow_ext_scalar_class__ for pse.quantity_value | DM-06 | evaluate. A struct-backed extension exposed as a bare struct scalar loses its meaning at the point of use. A custom scalar class is the Python-side way to keep it. Only worth it for types a human actually inspects in a notebook. |
pa.UnknownExtensionType | — | reject as a mechanism. It exists, but is not what the IPC/capsule path produces [probe]; relying on it would be relying on a path we measured not to occur. |
Gaps and risks
- Registration is global and process-wide.
register_extension_typemutates a global registry; a second registration of the same name raises. A library that registers on import interacts badly with test isolation and with any host process that also registerspse.*. Recommend an idempotent register-once helper inpython/pse/. - Extension metadata is versioned by nothing.
__arrow_ext_serialize__returns opaque bytes; if the Rust-sideMetadatashape changes, the Python__arrow_ext_deserialize__must change in lockstep or silently misparse. Same gap the Arrow map recorded for the Rust side (DM-44, DM-51) — and it is worse here, because the two implementations are in different languages and different repositories’ release cycles.
4. Pyomo — model construction and expression lowering (P3)
Role: §21.2 steps 1–2 — the adapter algorithm; §18.5’s Pyomo binding row; D12.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Model and components | ConcreteModel, Var, Param(mutable=True), Constraint, Objective, Set, RangeSet, Suffix, Block | [api:pyomo@6.10.1] |
| Affine form | pyomo.core.expr.numeric_expr.LinearExpression(constant, linear_coefs, linear_vars) | [api:pyomo@6.10.1], [probe] |
| Conditional | pyo.Expr_if(IF=…, THEN=…, ELSE=…) → Expr_ifExpression | [probe] |
| External functions | pyo.ExternalFunction(library=…, function=…) → AMPLExternalFunction; calling it yields ExternalFunctionExpression | [probe] |
| Solve | pyo.SolverFactory(name), .available(False), .solve(model, …) | [api:pyomo@6.10.1], [probe] |
Measured (PROBE 3)
LinearExpression : LinearExpression | value: 2.0
nargs 3 | polynomial_degree: 1
Expr_if : Expr_ifExpression | value: 1.0
ExternalFunction decl : AMPLExternalFunction | callable in expr: ExternalFunctionExpression
(warning) Defining AMPL external function, but cannot locate specified library "libpse_kernels.so"
| §21.2 step 2 requirement | Verdict |
|---|---|
Affine lowering | Qualified: the probe constructs a truly linear expression. Arbitrary Affine children may be nonlinear; use LinearExpression only after proving affine dependence on active variables (ADR-0047). |
Conditional to Expr_if | Interface/probe scope: expression construction works; guarded evaluation must be qualified for the actual Pyomo writer and selected solver/numerical policy (ADR-0047). |
“KernelCall → … otherwise ExternalFunction(library, function)” | confirmed as a declaration; see the risk below |
“ImplicitRef never appears (implicit systems stay as equations)” | platform rule, nothing in Pyomo to confirm or refute |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| A missing external-function library is a warning, not an error | G7 (truthful capability claims), DM-43, DM-30 | adopt a hard check. ExternalFunction with an unresolvable library emitted a warning and still produced a usable expression object [probe]; the failure surfaces only at solve time, from the solver, far from the cause. §21.4 already requires an end-to-end test per pyomo_external_function kernel — the stronger rule is that the adapter must verify the library resolves before building the model, and raise capability.backend (§23.2) with the kernel id if not. A warning on stderr is not a typed failure. |
polynomial_degree() as a lowering assertion | DM-24, DM-53 | Adopt with classification: compare with the expected degree, 0 for constants and 1 for a nonconstant affine-in-variables form. An Affine opcode alone does not justify degree 1 (blueprint §21.2). |
Build through Blocks mirroring compiled relation structure | DM-18 | reject. Tempting for readability, but D12 is explicit that the adapter “builds no IDAES classes” and “engineering names live in the source map”. Blocks would recreate a hierarchy the design deliberately flattened to ordinals. |
Gaps and risks
Param(mutable=True)is required for §21.2’s parameters to be updatable between solves; an immutableParamis folded into expressions at construction. The blueprint saysmutable=True— correct, and worth keeping as a generated-code invariant rather than a convention.- Pyomo’s expression system is large and has several near-equivalent spellings for the same mathematics (
LinearExpressionvs. a sum of products). Since the adapter is generated from the IR, the choice should be made once in the generator, not per operator.
5. Pyomo — units, suffixes, solve, and results (P4)
This cluster contains the map’s sharpest finding: a direct contradiction between §21.2 and §3.3.
The pint contradiction
§21.2 step 1 requires “units attached from the unit set so that assert_units_consistent passes”. §3.3’s closing line states: “Explicitly not added: … a Python-side units library.”
Measured [probe], on a clean environment with the §3.3-compliant dependency set (no pint):
m2.T = pyo.Var(units=u.K, initialize=320.0)
pyomo.common.errors.DeferredImportError: The pint module (an optional Pyomo dependency)
failed to import: ModuleNotFoundError: No module named 'pint'
Pyomo’s units system is pint. pyomo.core.base.units_container defers to pint_module and raises on attribute access when it is absent [probe]. So §21.2’s requirement cannot be satisfied under §3.3’s exclusion. This is not a transitive-dependency inconvenience — the two statements are incompatible as written.
Recommended resolution (and the basis for an amendment): §3.3’s exclusion is about authority, not about presence. Reword it to say that no Python-side units library is an authority for quantities — §8’s dynamic registry remains sole authority — while acknowledging that pint is required by Pyomo’s units machinery and is therefore present in the adapter and parity environments. The boundary rule then becomes enforceable: pint may validate what the adapter emits; it may never define a quantity, and no pse relation may be derived from a pint object.
Capability inventory and measurements (PROBE 4, 5)
| Capability | Surface | Provenance |
|---|---|---|
| Units container | pyo.units (PyomoUnitsContainer), backed by pint | [api:pyomo@6.10.1], [probe] |
| Consistency assertion | pyomo.util.check_units.assert_units_consistent(obj) — not in pyomo.environ | [api:pyomo@6.10.1], [probe] |
| Culprit identification | pyomo.util.check_units.identify_inconsistent_units(block) → the offending components | [probe] |
| Related | assert_units_equivalent, check_units_equivalent, UnitsError, InconsistentUnitsError | [api:pyomo@6.10.1] |
| Suffixes | pyo.Suffix(direction=Suffix.EXPORT | IMPORT); scaling_factor, dual, ipopt_zL_out, ipopt_zU_out | [probe] |
PROBE 4 consistent model : PASSES
inconsistent model : raises InconsistentUnitsError ("kelvin not compatible with …")
identify_inconsistent_units -> ['bad'] <- names the culprit constraint
PROBE 5 Suffix EXPORT/IMPORT : declared ok; scaling_factor[x] = 0.001
SolverFactory(ipopt) available: False
SolverFactory(cbc) available: False
SolverFactory(glpk) available: False
| Blueprint requirement | Status |
|---|---|
§21.2 step 1 “assert_units_consistent passes” | confirmed working — with pint present, and erratum: the function is in pyomo.util.check_units, not pyomo.environ as the phrasing implies |
§21.2 step 3 scaling_factor suffix export | confirmed |
§21.2 step 4 dual, ipopt_zL_out, ipopt_zU_out import | declaration confirmed; round-trip [UNVERIFIED] — no solver is installed in this environment, so the probe could not complete the loop. Recorded as unverified rather than asserted. |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Use identify_inconsistent_units, not a bare assert | DM-47 (structured evidence), DM-30, §23.2 | adopt. assert_units_consistent raises; identify_inconsistent_units returns the offending components [probe]. §23.2’s compile.math class exists to name the culprit — “unit inconsistency” with a component list is a finding; an exception with a message is not. |
| Units as a cross-check, never a source | DM-02 (one authority per fact), G1 | adopt as the boundary rule. The unit set crosses in the bundle from §8’s registry; Pyomo/pint validates that what the adapter built matches. If the two disagree, the registry is right and the adapter has a bug. Making that direction explicit is what keeps pint from becoming a second authority. |
| Solver availability probed before the model is built | DM-43, G7 | adopt. SolverFactory(name).available(False) is a cheap pre-flight [probe]. §18.3 already does a capability probe for ipopt_has_linear_solver; the same discipline belongs at the Pyomo boundary so a missing solver is a declared capability failure rather than a late crash. |
6. Pyomo — ecosystem tools and the ASL boundary (P5)
Role: §21.3 — “parity testing, and the Pyomo ecosystem: parmest, PyROS, GDPopt, pyomo.dae utilities, and IncidenceGraphInterface-based cross-checks”; §21.4 opaque kernels and ASL; §18.9’s capability matrix.
Availability — all confirmed present at 6.10.1 [probe]
| Module | Public names | Blueprint use |
|---|---|---|
pyomo.contrib.parmest.parmest | 48 | §21.3 parameter estimation; §19.4 |
pyomo.contrib.pyros | 22 | §21.3 robust optimization; §19.8 uncertainty |
pyomo.contrib.gdpopt | 18 | §21.3, §18.7’s GDP solve plan |
pyomo.dae | 11 | §21.3 DAE utilities; §13.4 discretization cross-checks |
pyomo.contrib.incidence_analysis | 17 | §21.3 cross-checks of native structural results (§15.3) |
pyomo.contrib.iis | 4 | not named in the blueprint — see below |
IncidenceGraphInterface resolves at pyomo.contrib.incidence_analysis.interface [probe], confirming §21.3’s naming.
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
pyomo.contrib.iis for infeasibility explanation | DM-47, DM-30 | evaluate. §15.5 specifies a hand-built minimal-intractable-system procedure (three elasticization rounds plus a deletion filter). Pyomo ships an IIS facility that solves the same problem. Since the Pyomo adapter already exists for parity, this is available as a cross-check oracle for the native MIS implementation at near-zero cost — the same role §21.3 already assigns to IncidenceGraphInterface. Not a replacement: §15.5’s version runs natively, over case overlays, without model cloning. |
| Treat every ecosystem tool as a declared capability with a version | DM-43, G7, DM-31 | adopt. §18.9’s capability matrix says which operators each backend supports; it does not record which version of parmest/PyROS/GDPopt a result came from. These are pyomo.contrib packages — explicitly lower-stability than Pyomo’s core — and their behaviour changes between releases. A run that used one must record it in runtime.runs.environment (§20.3) or the result is not reproducible. |
Cross-check native structural results against IncidenceGraphInterface in CI, not ad hoc | DM-53 (verify equivalence across representations), DM-54 | adopt. §21.3 describes this as a use of the adapter; making it a standing parity test over the golden snapshots (§24) turns it from an available technique into a regression control. |
Gaps and risks
pyomo.contrib.*is a contrib namespace. Five of the six tools §21.3 relies on live there. That is a stability profile the blueprint should state, because §18.9’s matrix currently reads as though these are equivalent in standing to the native and NL backends. They are not: they are the least stable surface in the entire dependency set.- §21.4’s rule that “Python callback functions are not used for solves because ASL-based solvers cannot execute them” is a correct and important constraint, and the
ExternalFunctionprobe (§4) shows why it must be enforced early: the object constructs happily either way, and only the solver notices.
7. attrs + cattrs — the typed contract layer (P6)
Role: §21.5 — “typed attrs classes with explicit converters at the boundary, cattrs for structuring, no Any fields, and no Python-side model classes that duplicate relations.”
Note: §21.5 cites docs/library_ref/contract_substrate_discipline.md for this discipline. That file and its directory have been deleted — so the referenced definition is currently unreadable (erratum E1). What follows measures the discipline’s stated properties directly.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Class definition | attrs.define, attrs.frozen, attrs.field, attrs.mutable | [api:attrs@26.1.0] |
| Field machinery | validators, converters, defaults, kw_only, alias, metadata | [api:attrs@26.1.0] |
| Introspection | attrs.fields(cls), attrs.asdict, attrs.astuple, attrs.evolve | [api:attrs@26.1.0], [probe] |
| Structuring | cattrs.Converter, structure, unstructure, register_structure_hook, register_unstructure_hook | [api:cattrs@26.2.0] |
| Strictness options | Converter(forbid_extra_keys=…, detailed_validation=…); detailed validation is on by default since 22.1.0 | [probe], [c7:/python-attrs/cattrs] |
| Errors | ClassValidationError (exception group of sub-errors); ForbiddenExtraKeysError | [probe], [c7:/python-attrs/cattrs] |
| Error rendering | cattrs.transform_error(exc, format_exception=…) → list of per-field messages | [c7:/python-attrs/cattrs] |
| Global strictness idiom | register_structure_hook_factory(has, …) + cattrs.gen.make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True) | [c7:/python-attrs/cattrs] |
Measured (PROBE 6)
structure happy path : VarRow(ordinal=1, semantic_id=b'…', lower=0.0)
unknown key : ACCEPTED (silently dropped)
forbid_extra_keys : rejected -> ClassValidationError
wrong type : rejected -> ClassValidationError
detailed_validation : ClassValidationError | sub-errors: 1
Any field : ACCEPTED (nothing in cattrs forbids it)
attrs introspection : fields() -> ['ordinal', 'semantic_id', 'lower']
| §21.5 requirement | Verdict |
|---|---|
| “typed attrs classes with explicit converters at the boundary” | confirmed available |
| “cattrs for structuring” | confirmed |
“no Any fields” | erratum — unenforced. cattrs accepts an Any field and structures whatever it is given [probe]. §21.5 states this as a property of the design; it is a convention with nothing behind it. |
| Implicit expectation that structuring is strict | refuted by default. A default cattrs.Converter() silently drops unknown keys [probe]. |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| Forbid extra keys at every boundary | DM-42 (reject silent degradation), G2, G3 | adopt — the highest-value item in this cluster. Measured: the default silently discards unknown keys [probe], and upstream states the same intent — “by default, cattrs ignores extra keys”; forbid_extra_keys “enforces a stricter contract… especially when fields have default values” [c7:/python-attrs/cattrs]. At a boundary carrying a versioned contract, an unknown key is evidence of a version mismatch; dropping it converts detectable schema drift into silent data loss. Two spellings work: Converter(forbid_extra_keys=True) [probe], or the upstream idiom applying it to every attrs class at once — register_structure_hook_factory(has, lambda cl: make_dict_structure_fn(cl, c, _cattrs_forbid_extra_keys=True)), raising ForbiddenExtraKeysError [c7:/python-attrs/cattrs]. Prefer the factory: it cannot be forgotten on a new class. |
| Consume the sub-errors — and do not disable detailed validation | DM-47, DM-30 | adopt, with a correction to this document’s own first pass. Detailed validation is on by default since cattrs 22.1.0 [c7:/python-attrs/cattrs], so the work is not enabling it — it is (a) never passing detailed_validation=False for speed, and (b) actually consuming the sub-errors. ClassValidationError is an exception group carrying one per bad field [probe], and cattrs.transform_error() renders it as a list of per-field messages [c7:/python-attrs/cattrs] — a ready-made bridge to §23.2’s structured findings. Taking only the first message discards exactly the structure that makes a validation failure actionable. |
A governance check for Any | DM-06, G3 | adopt. Since nothing at runtime forbids Any, the rule needs a lint: walk attrs.fields() of every contract class at import or test time and fail on Any/bare dict/bare list. attrs.fields() makes this ~10 lines [probe], and it is the only way §21.5’s claim becomes true. |
| Generate the contract classes from the registry | DM-52 (generate mechanical artifacts from shared contracts), DM-23 | adopt. §4.2 already generates nine artifacts per relation from RelationSpec, including a serde_arrow schema for the Rust side. The Python contract classes are the same projection in another language. Hand-writing them re-expresses the column list a second time — which §4.2 calls a governance failure — and guarantees drift. This is the single largest structural opportunity in the Python surface. |
attrs.frozen for anything crossing the boundary | DM-13, DM-20 | adopt. Bundle rows are observations of an immutable snapshot; frozen classes make that structural and keep inspection non-mutating (DM-20). |
8. msgspec — wire contracts, constraints, and schema generation (P7)
Role: §21.5 names msgspec among the Python contracts; §20.2’s manifest is JSON. The blueprint never says what msgspec is for — §9 adjudicates that.
Capability inventory
| Capability | Surface | Provenance |
|---|---|---|
| Typed structs | msgspec.Struct with frozen=, forbid_unknown_fields=, gc=, tag=, rename= | [api:msgspec@0.21.1], [probe] |
| Codecs | msgspec.json, msgspec.msgpack, msgspec.yaml, msgspec.toml — encode / decode(buf, type=T) | [api:msgspec@0.21.1] |
| Constraints | msgspec.Meta(gt=, ge=, lt=, le=, min_length=, max_length=, pattern=, …) via Annotated | [probe] |
| Validation errors | msgspec.ValidationError with JSON-path location | [probe] |
| Schema generation | msgspec.json.schema(T) → JSON Schema; schema_components for multiple types | [probe] |
| Conversion | msgspec.convert, msgspec.to_builtins, msgspec.Raw, msgspec.structs.* | [api:msgspec@0.21.1] |
Measured (PROBE 7)
encode : b'{"name":"stoichiometry","rows":12,"ratio":1.0}'
decode round-trip : Rel(name='stoichiometry', rows=12, ratio=1.0)
unknown field : rejected -> ValidationError | Object contains unknown field `BOGUS`
wrong type : rejected -> ValidationError | Expected `int`, got `str` - at `$.rows`
Meta(gt=0) violation : rejected -> ValidationError | Expected `float` > 0.0 - at `$.factor`
json.schema(Rel) : {'$ref': '#/$defs/Rel', '$defs': {'Rel': {'title': 'Rel', 'type': 'object',
'properties': {...}, 'required': ['name','rows'],
'additionalProperties': False}}}
Three properties matter for this design, and all three were measured rather than assumed:
- Errors carry a location.
at $.rows,at $.factor— a JSON path to the offending value. §23.2 requires failures that carry the thing that failed; msgspec supplies it natively. - Constraints are declarative and enforced at decode.
Meta(gt=0.0)on a scaling factor is checked at the boundary, not by hand-written code afterwards. - It generates JSON Schema, with
additionalProperties: Falsederived fromforbid_unknown_fields.
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
| msgspec owns the §20.2 manifest | DM-42, DM-47, DM-44 | adopt. The manifest is JSON on disk, versioned, read by both Rust and Python, and the place where a malformed or drifted artifact must be caught. Struct(frozen=True, forbid_unknown_fields=True) plus Meta constraints gives strict decode with located errors, which is exactly what §20.5’s “a snapshot whose schema registry fingerprint is unknown is rejected” needs. |
msgspec.json.schema() vs §4.2’s generated authoring JSON Schema | DM-23 (derived representations traceable and non-competing), DM-52 | evaluate carefully — this is a competing-authority risk, not a free win. §4.2 already generates “Authoring JSON Schema for editor validation of package documents” from the Rust registry. msgspec can generate JSON Schema from Python structs. If both exist for the same documents, there are two schemas for one contract and they will drift — precisely what DM-23 forbids. Recommendation: the registry-generated schema is authoritative; msgspec’s generator is used only for Python-internal wire types that have no registry counterpart (e.g. the manifest), never for authoring documents. |
Meta constraints mirroring registry invariants | DM-07, G3 | evaluate. Attractive — a scaling_factor > 0 invariant enforced at the boundary. But §4.2 generates invariants as DataFusion plans executed by P2; duplicating them as Meta constraints re-expresses the same rule in a second place (DM-56). Adopt only where the constraint guards the wire format itself rather than model semantics. |
msgspec.Raw for pass-through payloads | DM-37 | evaluate — defers parsing of a nested payload, useful if the manifest carries per-relation blobs the adapter does not need to inspect. |
Gaps and risks
- msgspec is 0.21.1 — pre-1.0, the only pre-1.0 library in the Python set. Its API has been stable in practice, but the version number is the honest signal and it should be
==-pinned, not floated. msgspec.Structis notattrs— the two class systems do not interoperate, which is the substance of §9.
9. Adjudication: attrs/cattrs versus msgspec (P8)
§3.3 lists three libraries that overlap heavily. §21.5 names attrs and cattrs and never says what msgspec is for. Leaving this open means the same meaning gets expressed twice at the boundary — DM-56 (“optimize for fewer independent semantic decisions”), DM-57 (“a small coherent core”), DM-58.
What the measurements say
| Property | attrs + cattrs | msgspec | Source |
|---|---|---|---|
| Unknown keys, default | silently dropped | rejected | [probe] |
| Unknown keys, strict mode | forbid_extra_keys=True | default | [probe] |
| Error location | field name via ClassValidationError sub-errors | JSON path (at $.rows) | [probe] |
| Multiple errors at once | yes (exception group) | first error | [probe] |
| Declarative value constraints | attrs validators (imperative) | Meta(gt=…) (declarative) | [probe] |
| JSON Schema generation | no | yes | [probe] |
| Codec coverage | none (structuring only) | json, msgpack, yaml, toml | [api:msgspec@0.21.1] |
| Works on arbitrary existing classes | yes — structures into any attrs class | no — must be a Struct | [api:*] |
| Introspection for governance | attrs.fields() | msgspec.structs.fields | [probe] |
The adjudication
They are not redundant; they occupy different positions, and the evidence separates them cleanly:
- msgspec is a wire codec. Strict by default, located errors, schema generation, four formats. Its natural job is bytes ↔ typed object at a serialization boundary: §20.2’s manifest, and any JSON the platform reads or writes.
- attrs + cattrs is an in-memory contract layer. cattrs structures from already-decoded material — which is what the Arrow path produces. Rows arriving from a
RecordBatchReaderare Python objects, not bytes; there is no JSON to decode, so msgspec’s strength does not apply, while cattrs’ ability to structure into arbitrary attrs classes does.
Recommended division, to be written into §21.5:
| Boundary | Library | Why |
|---|---|---|
| §20.2 manifest, and all JSON/TOML the platform reads or writes | msgspec | strict decode, located errors, constraints, schema generation |
| Arrow bundle rows → typed Python objects (§21.1, §21.2) | attrs + cattrs, with forbid_extra_keys=True and detailed_validation=True | the input is decoded material, not bytes; cattrs structures into generated attrs classes |
| Anything else | neither — it does not exist | DM-57 |
The rule that keeps this from becoming two authorities: a given contract is defined in exactly one of the two systems, never both. The manifest is a msgspec Struct; a relation row is a generated attrs class. No type is expressed in both.
This also resolves why three libraries is not over-engineering under DM-58: attrs and cattrs are one thing (classes plus their structuring), msgspec is another (a codec), and each replaces hand-written work that would otherwise exist — provided the division above is stated rather than left to taste.
10. numpy — the array boundary (P9)
Role: §21.1/§21.2 move numeric columns across the boundary; D11 governs layout ownership; §24’s parity harness compares native and Pyomo results numerically.
Measured (PROBE 8) — the null/NaN collapse
zero-copy with nulls : REFUSED -> ArrowInvalid
"Needed to copy 1 chunks with 1 nulls, but zero_copy_only was True"
zero_copy_only=False : [ 1. nan -0.] | dtype float64 | null became: nan
no-null zero-copy : [ 1. 2. -0.] | shares buffer: True
-0.0 sign preserved : True
This is the cluster’s finding, and it is a G2 one.
| Fact | Consequence |
|---|---|
| Arrow → numpy with nulls present converts null to NaN | §4.4 states pse.bound must never use “NaN or null sentinels”, and the Arrow map verified that Arrow keeps null, NaN and -0.0 structurally distinct. A plain ndarray cannot hold that distinction — float64 has no validity bitmap. The moment a nullable column becomes an ndarray, null and NaN are the same value. |
zero_copy_only=True refuses rather than silently copying | Good design: the copy is opt-in and visible. |
-0.0 survives | consistent with the Arrow side |
| Blueprint requirement | Status |
|---|---|
| D11 “borrow Arrow buffers when layouts permit and copy when they do not” — Python side | confirmed: zero_copy_only=True succeeds only for a null-free, single-chunk, primitive column [probe]; anything else must copy, and says so |
| §4.4’s no-sentinel discipline, carried into Python | at risk — see below |
Pass 2 — alignment opportunities
| Item | DM / gate | Recommendation |
|---|---|---|
Never convert a nullable column to a bare ndarray | DM-08, G2 | adopt as a boundary rule. Either assert null_count == 0 before converting (true for solver vectors by construction — §18.1 says fixed and parameter symbols are bound, and solution values are total), or carry the mask alongside. The failure mode is silent and numerically plausible, which is the worst kind. The assertion is one line and belongs in the generated adapter. |
zero_copy_only=True as the default in adapter code | DM-37, DM-39 | adopt. It turns an unintended copy into an error rather than a performance mystery, and documents which columns are genuinely borrowable. Where a copy is required, passing False explicitly records that decision at the call site. |
combine_chunks() before conversion | DM-36 | note — a chunked column has no single buffer; zero-copy requires one chunk [probe]. |
Gaps and risks
- numpy is unpinned in §3.1 and arrives transitively through both Pyomo and pyarrow. numpy 2.x changed several conversion and dtype behaviours relative to 1.x. For a parity harness that compares numbers, the numpy version is part of the reproducibility contract (DM-48) and should be pinned like everything else.
11. scipy — structural and numerical cross-checks (P10)
Role: scipy is not named in §3.3, but arrives with Pyomo and is the natural oracle for §15’s native structural and numerical analyses.
Capability inventory [probe], [api:scipy@1.18.1]
| Capability | Surface |
|---|---|
| Sparse formats | csr_matrix, csc_matrix, coo_matrix, bsr_matrix, dia_matrix, dok_matrix, lil_matrix |
| Graph algorithms | scipy.sparse.csgraph.{connected_components, structural_rank, maximum_bipartite_matching, reverse_cuthill_mckee, depth_first_order} |
| Dense SVD | scipy.linalg.{svd, svdvals} |
| Sparse SVD | scipy.sparse.linalg.svds |
Pass 2 — the cross-check opportunity
| Item | DM / gate | Recommendation |
|---|---|---|
maximum_bipartite_matching as an oracle for §15.3’s Hopcroft–Karp | DM-53 (verify invariants and equivalence across representations), DM-54 | adopt as a test-only oracle. The supporting-library map established that petgraph ships only general-graph matching (Gabow, O(|V|³)) and that §15.3 therefore implements Hopcroft–Karp natively. Hand-written matching on the equation×variable incidence graph is exactly the kind of code that is subtly wrong on degenerate inputs — and scipy ships a bipartite implementation that can be run over the same golden snapshots in CI. Matching cardinality must agree; the matching itself need not (it is not unique). |
structural_rank as a DOF cross-check | DM-53 | adopt as a test-only oracle for §15.2’s structural checks and problems.degrees_of_freedom. |
svds / svdvals against §15.5’s SVD toolbox | DM-53, DM-39 | evaluate. §15.5 specifies dense faer SVD for small problems and “iterative for large” — which the Arrow-side map flagged as having no faer implementation behind it. scipy’s svds is a working iterative sparse SVD and is a legitimate oracle for the small-problem path, and a reference for what the large path must eventually do. |
| scipy in production paths | DM-38, D11 | reject. D11 gives the native numerics ownership of execution layouts. scipy’s role here is exclusively as a test-time oracle running through the existing Python/parity boundary — never inside a pass, a solve, or a diagnostic that the platform ships. |
Gaps and risks
- scipy, like numpy, is unpinned and transitive. If it is going to serve as a correctness oracle in CI, it is part of the test contract and must be pinned (DM-48).
- An oracle that disagrees is only useful if the disagreement is attributable. These cross-checks should compare invariants (matching cardinality, structural rank, singular-value count below tolerance), not object identity.
12. IDAES 2.12 — the parity surface and its dependency boundary (P11)
Role: §3.1 “IDAES 2.10 for parity tests only”; §24’s parity harness; the source of the behaviours §9–§17 reproduce.
Availability [probe], [api:idaes@2.12.0]
IDAES 2.12.0 imports cleanly on CPython 3.13.12. 191 modules, 2762 public items extracted. The modules the parity harness needs all resolve:
| Module | Parity role |
|---|---|
idaes.core | flowsheet, control-volume and property-package base classes (§9–§13) |
idaes.core.util.model_statistics | DOF and variable/constraint counts (§15.2) |
idaes.core.util.scaling | scaling factors and the Jacobian conditioning checks (§15.4, §16) |
idaes.core.util.model_diagnostics | the diagnostics toolbox §15 reproduces |
idaes.models.properties.modular_properties | the GenericParameterBlock configuration §9.1 maps to relations |
Version finding
§3.1 names 2.10; current is 2.12.0 [pypi:idaes-pse], and this map is built against 2.12.0 per the owner’s instruction to use latest throughout. Parity is only meaningful against a stated version — §24’s acceptance criteria compare platform behaviour to IDAES behaviour, and IDAES changed between 2.10 and 2.12. This must be pinned exactly, not floored.
The dependency boundary
IDAES 2.12.0 requires pyomo>=6.10.1 — exactly this map’s Pyomo pin, so the two are compatible [pypi:idaes-pse]. It also pulls a substantial tail: pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1, plus pandas, matplotlib and click [probe].
| Item | DM / gate | Recommendation |
|---|---|---|
| pint enters through IDAES and through Pyomo’s own units | DM-02 (one authority per fact), G1 | adopt the boundary rule stated in §5: no Python-side units library is an authority; §8’s dynamic registry is. pint may validate; it may never define. The §3.3 exclusion should be reworded from presence to authority, because presence is not achievable — Pyomo’s units machinery is pint. |
pydantic and sympy arrive in the parity environment | DM-57, DM-58 | note, and fence. Neither is a platform dependency; both are capable enough to tempt use. The parity environment should be a separate dependency set from the platform’s Python package, so that what IDAES drags in cannot quietly become something the platform relies on. |
| Record the IDAES version in every parity result | DM-31, DM-48 | adopt. §20.3 records solver and package versions for runs; parity comparisons need the same treatment or a parity failure cannot be attributed to a platform change versus an IDAES change. |
Gaps and risks
- IDAES caps the interpreter at 3.13 (classifiers 3.10–3.13;
requires_pythonunset)[pypi:idaes-pse]. §3.1’s “Python 3.11+” is therefore only true for the platform package; the parity environment has an upper bound as well as a lower one. Two dependency sets, two interpreter ranges. - The vendored
idaes-pse/tree in this repository is at the version the blueprint named (2.10 line), not 2.12.0. Reading the tree and testing against the pinned package are two different sources; the pinned package is the one parity claims are made against.
13. Survey — pandas, polars, datafusion-python (P12)
| Library | Status | Reason |
|---|---|---|
| pandas (3.0.5 observed) | not a platform dependency; arrives via IDAES | §21.1’s bundle is Arrow streams; a pandas conversion loses the null/NaN distinction the same way numpy does (§10) and adds an index concept the relational model does not have. Acceptable inside parity/notebook code, never in the adapter. |
| polars | not used; supported by construction | §21.1 names it as a possible consumer. Because the boundary is the PyCapsule protocol rather than a pyarrow type, polars can consume the bundle with no platform change — which is the point of §2’s choice and worth stating as a benefit already earned. |
| datafusion-python | not used; consumer only | Observed at 54.0.0 against the Rust engine’s 55.1.0. Version skew is tolerable precisely because it consumes the capsule protocol, not our engine. Do not let it become a dependency: two DataFusion versions in one process would be a genuine problem, and D10 already fixes DataFusion’s four roles on the Rust side. |
14. Principle-alignment register (Python boundary)
Capabilities the blueprint does not claim, which would improve charter alignment. Disciplined by §G and DM-58: every row names a principle and the hand-written work or risk it removes. Rejections are recorded too.
Adopt
| # | Capability | Where | DM / gate | What it removes or prevents |
|---|---|---|---|---|
| 1 | Generate the Python contract classes from RelationSpec, as §4.2 already does for Rust | §7 | DM-52, DM-23, DM-56 | Hand-writing the column list a second time — which §4.2 itself calls a governance failure — and the drift that follows. Largest structural item in the Python surface. |
| 2 | Forbid extra keys at every boundary (prefer the global hook factory) | §7 | DM-42, G2 | Measured: the default silently drops unknown keys, converting detectable schema drift into silent data loss. |
| 3 | Consume cattrs sub-errors via transform_error(); never set detailed_validation=False | §7 | DM-47, DM-30 | Detailed validation is already on by default; the real gap is that nothing consumes the per-field sub-errors §23.2 requires be preserved. |
| 4 | Governance lint for Any/bare dict/bare list via attrs.fields() | §7 | DM-06, G3 | §21.5’s “no Any fields” is currently a convention with nothing enforcing it. |
| 5 | Detect extension-type degradation via retained ARROW:extension:name metadata | §3 | DM-42, G2, DM-59 | Measured: loss is silent at the type level but the metadata key survives — so it is detectable at the cost of one boundary check. |
| 6 | Ship and register the ten pse.* ExtensionType classes from the platform package | §3 | DM-44 | Each consumer writing its own forks the serialization contract. |
| 7 | Per-column loss profile in the §21.1 manifest | §3 | G7, DM-42 | Lets a consumer verify extension coverage rather than discover absence. |
| 8 | Never convert a nullable Arrow column to a bare ndarray; assert null_count == 0 | §10 | DM-08, G2 | Measured: null becomes NaN, collapsing the distinction §4.4 exists to protect. Silent and numerically plausible. |
| 9 | zero_copy_only=True as the adapter default | §10 | DM-37, DM-39 | Turns an unintended copy into an error instead of a performance mystery. |
| 10 | Verify the external-function library resolves before building the model | §4 | G7, DM-43, DM-30 | Measured: a missing library is a warning; failure surfaces later, from the solver, far from the cause. |
| 11 | identify_inconsistent_units instead of a bare assert_units_consistent | §5 | DM-47, §23.2 | Returns the offending components; compile.math exists to name the culprit. |
| 12 | Units as cross-check only — registry is the source | §5, §12 | DM-02, G1 | Keeps pint from becoming a second quantity authority. |
| 13 | SolverFactory(name).available(False) pre-flight | §5 | DM-43, G7 | A missing solver becomes a declared capability failure, not a late crash. |
| 14 | msgspec owns the §20.2 manifest; strict, constrained, located errors | §8, §9 | DM-42, DM-47 | Gives §20.5’s “unknown registry fingerprint is rejected” a real mechanism. |
| 15 | Write the attrs/cattrs ÷ msgspec division into §21.5 | §9 | DM-56, DM-57 | Three libraries with overlapping capability and no stated division is how the same meaning gets expressed twice. |
| 16 | IncidenceGraphInterface and scipy cross-checks as standing CI parity tests | §6, §11 | DM-53, DM-54 | Turns available techniques into regression controls over the golden snapshots. |
| 17 | Record IDAES, pyomo.contrib.* and numpy/scipy versions in run and parity records | §6, §12 | DM-31, DM-48 | A parity failure cannot otherwise be attributed to us versus them. |
| 18 | Pin every Python library ==, both interpreters | §1 | DM-48, DM-51 | §3.1 currently floors two and omits three entirely. |
Evaluate
| # | Capability | Where | DM | Why not yet an adopt |
|---|---|---|---|---|
| 19 | msgspec.json.schema() for Python-internal wire types only | §8 | DM-23 | Real value, real risk: §4.2 already generates authoring JSON Schema from the registry. Two generators for one contract will drift. Confine to types with no registry counterpart. |
| 20 | pyomo.contrib.iis as an oracle for §15.5’s MIS | §6 | DM-47, DM-53 | Free cross-check via the adapter that already exists; not a replacement for the native implementation. |
| 21 | scipy.sparse.linalg.svds as an oracle for §15.5’s SVD toolbox | §11 | DM-53, DM-39 | Also a reference for the “iterative for large” path the Arrow map found unimplemented. |
| 22 | polynomial_degree() assertions after lowering | §4 | DM-24, DM-53 | Cheap generated equivalence check between IR classification and Pyomo’s. |
| 23 | msgspec.Meta constraints mirroring registry invariants | §8 | DM-07 | Only where the constraint guards the wire format; otherwise it re-expresses a rule P2 already enforces (DM-56). |
| 24 | __arrow_ext_scalar_class__ for pse.quantity_value | §3 | DM-06 | Worth it only for types a human inspects interactively. |
Reject, with reason
| # | Capability | Where | Why |
|---|---|---|---|
| 25 | scipy or numpy in production paths | §10, §11 | D11 gives native numerics ownership of execution layouts. Test-time oracles only. |
| 26 | pandas anywhere in the adapter | §13 | Loses the null/NaN distinction and adds an index concept the relational model does not have. |
| 27 | datafusion-python as a dependency | §13 | Two DataFusion versions in one process; D10 already fixes the engine’s roles on the Rust side. |
| 28 | pa.UnknownExtensionType as the degradation mechanism | §3 | Measured not to occur on the IPC/capsule path. |
| 29 | Pyomo Blocks mirroring relation structure | §4 | D12: the adapter builds no hierarchy; engineering names live in the source map. |
| 30 | pint as a quantity authority | §5, §12 | §8’s registry is sole authority. Presence is unavoidable; authority is not. |
15. Acceptance-gate review (Python boundary)
The survey’s revision-2 gate results are historical. Blueprint §21 now specifies the following Proposed mechanisms. The revision-5 review assesses the changed contracts; no current end-to-end Python/backend certification is supplied by this map.
| Gates | Current mechanism | Remaining acceptance |
|---|---|---|
| G1 | Registry-generated contracts; pint validates the adapter’s units and never defines model relations | Generated-schema/units disagreement fixtures |
| G2–G3 | Recursive ingress/egress admission, extension loss profile, mask-preserving nullable transfer and strict contract structuring | Missing registrations, nested metadata, unknown fields, null/NaN and wrong-unit negative cases |
| G4 | Solver/external-library preflight and supported numerical-policy/guarded-route checks | An actual selected solver route; constructing Expr_if is insufficient |
| G5 | Idempotent extension registration; immutable stream/buffer owners retain reservations until final release | Repeated import, delayed drain, cancellation and partial-ingestion fixtures |
| G6–G7 | LinearExpression only for proved affine-in-active-variable bodies; declared kernel/derivative bindings and versioned backend conformance | Nonlinear children in Affine, constant degree zero, guarded failures and genuine solver round trips |
16. Leverage matrix
| Blueprint requirement | API | Status |
|---|---|---|
§21.1 tables cross as __arrow_c_stream__ | Table/RecordBatchReader.__arrow_c_stream__; RecordBatchReader.from_stream | confirmed |
| §21.1 consumers: pyarrow / polars / datafusion-python | the capsule protocol is implementation-neutral | confirmed — and version skew with datafusion-python is tolerable by design |
§4.4 Python consumers register pyarrow.ExtensionType; unknown consumers see storage types | register_extension_type; measured both paths | confirmed exactly [probe] |
| §4.3 metadata preserved into Python | schema + field metadata across IPC and capsule | confirmed [probe] |
§21.2 step 1 units so assert_units_consistent passes | pyomo.util.check_units — not pyomo.environ; requires pint | erratum ×2 — wrong module implied; contradicts §3.3 |
| §21.2 affine-in-variables lowering | LinearExpression | Qualified probe: constructor works for the measured linear example; nonlinear children take ordinary expression lowering (ADR-0047) |
| §21.2 runtime Conditional | Expr_ifExpression | construction observed; actual guarded writer/solver route remains a conformance requirement (ADR-0047) |
§21.2 step 2 KernelCall → ExternalFunction | AMPLExternalFunction | confirmed — but a missing library only warns |
§21.2 step 3 scaling_factor suffix | Suffix(direction=EXPORT) | confirmed [probe] |
§21.2 step 4 dual/ipopt_zL_out/ipopt_zU_out | Suffix(direction=IMPORT) | declaration confirmed; round-trip [UNVERIFIED] — no solver installed |
| §21.3 parmest / PyROS / GDPopt / pyomo.dae / IncidenceGraphInterface | all present at 6.10.1 | confirmed [probe] — all but pyomo.dae in pyomo.contrib |
| §21.5 typed attrs classes, cattrs structuring | attrs.define, cattrs.Converter | confirmed available |
§21.5 “no Any fields” | nothing enforces it | erratum — unenforced [probe] |
| §21.5 msgspec’s role | unstated in the blueprint | refined — §9 adjudicates it |
§21.5 contract_substrate_discipline.md | file deleted with docs/library_ref/ | erratum E1 |
| §3.1 Python pins | two floors, three omissions, IDAES at 2.10 | erratum — §1 |
| D11 borrow buffers where layouts permit | zero_copy_only=True succeeds only for null-free single-chunk primitives | confirmed [probe] |
| §3.3 “no Python-side units library” | Pyomo units are pint | erratum — reword to authority |
17. Evidence ledger
| # | Cluster | Instrument | Source | Target | Outcome |
|---|---|---|---|---|---|
| 1 | setup | PyPI | pypi.org/pypi/{pkg}/json | latest version + requires_python for all eight | all eight pinned at current release; idaes-pse classifiers 3.10–3.13 only |
| 2 | setup | uv / venv | py314 (CPython 3.14.7) | ==-pinned install of the 7-library main set | receipt in §1.3 |
| 3 | setup | uv / venv | py313 (CPython 3.13.12) | main set + idaes-pse==2.12.0 | IDAES imports cleanly; pulls pint 0.26.1, pydantic 2.13.5, sympy 1.14.0, networkx 3.6.1 |
| 4 | all | api dump | apidump.py on both environments | public API of 8 libraries → JSON | 8 dumps, 982 modules, 15 K items — receipt in §1.3 |
| 5 | P1, P2 | api | pyarrow@25.0.1 | ExtensionType, PyCapsule dunders, registry functions | __arrow_ext_serialize__/__arrow_ext_deserialize__, register_extension_type, UnknownExtensionType |
| 6 | P2 | probe | probe_arrow.py PROBE 1 | extension type through IPC, registered vs not | registered → SemanticId; unregistered → FixedSizeBinaryType, ARROW:extension:name metadata retained |
| 7 | P1, P2 | probe | probe_arrow.py PROBE 2 | schema/field metadata across IPC and __arrow_c_stream__ | preserved on both paths; extension type survives the capsule path |
| 8 | P9 | probe | probe_arrow.py PROBE 8 | numpy zero-copy and nulls | zero-copy refused with nulls; zero_copy_only=False turns null → NaN; -0.0 preserved |
| 9 | P3 | probe | probe_pyomo.py PROBE 3 | LinearExpression, Expr_if, ExternalFunction | all confirmed; missing external library only warns |
| 10 | P4 | probe | probe_pyomo.py PROBE 4 | units consistency, with and without pint | pyo.units raises DeferredImportError without pint; with pint, InconsistentUnitsError + identify_inconsistent_units names the culprit |
| 11 | P4 | probe | probe_pyomo.py PROBE 5 | suffixes and solver availability | suffixes declare OK; no solver available → round-trip [UNVERIFIED] |
| 12 | P6 | probe | probe_contracts.py PROBE 6 | cattrs strictness and Any | unknown keys silently dropped by default; forbid_extra_keys rejects; Any accepted |
| 13 | P7 | probe | probe_contracts.py PROBE 7 | msgspec constraints and schema | unknown field + type + Meta(gt=0) all rejected with JSON-path locations; json.schema() emits additionalProperties: False |
| 14 | P5 | probe | import checks on py314 | parmest, PyROS, GDPopt, pyomo.dae, incidence_analysis, iis | all present; IncidenceGraphInterface at pyomo.contrib.incidence_analysis.interface |
| 15 | P10 | probe | scipy@1.18.1 | sparse formats, csgraph, SVD | maximum_bipartite_matching, structural_rank, svds, svdvals all present |
| 16 | P11 | probe | idaes@2.12.0 on py313 | parity-relevant modules | idaes.core, model_statistics, scaling, model_diagnostics, modular_properties all import |
| 17 | P11 | PyPI | pypi.org/pypi/idaes-pse/json | dependency requirements | pyomo>=6.10.1, pint>=0.24.1, pydantic, sympy, networkx, matplotlib, click |
| 18 | P6 | c7 resolve | cattrs | strict structuring, converter configuration | /python-attrs/cattrs (205 snippets); /python-attrs/attrs (475) |
| 19 | P6 | c7 query-docs | /python-attrs/cattrs | forbid_extra_keys / detailed_validation and when to enable them | corrected this document: detailed validation is on by default since 22.1.0; supplied transform_error() and the global register_structure_hook_factory idiom |
18. Open items and recommended blueprint amendments
This is a Proposed design disposition, not a rerun of the Python corpus. The original API/probe sections retain their conditions. Current dependency and interpreter authority is pyproject.toml, uv.lock and blueprint §3.1.
| Earlier item / revision-5 finding | Current disposition | Remaining acceptance |
|---|---|---|
| Unit authority and generated contracts | Blueprint §3.3/§4.2/§21.5: pint validates, registry generates contract classes; attrs/cattrs handles structured material and msgspec handles wire records | Generator/runtime contract checks; no duplicated independently editable schema |
| Exact pins, interpreter split and parity version | Blueprint §3.1/§6.14 and ADR-0003/0018/0024; IDAES 2.12.0 is the parity reference | Actual named parity gate for the relevant slice; historical absence of a solver is not a claim about the current setup |
| Deleted discipline reference | Normative Python contracts are stated in blueprint §21.5 | No dependency on the deleted library-guide file |
| Extension registration and nullable arrays | Blueprint §4.4/§21.1/§21.6, ADR-0039; platform admission invokes checks, unaware consumers have an explicit loss profile | Real bundle/query ingress, masks, mismatched extension versions and round trips |
R4-09: Affine versus a linear problem | Blueprint §21.2, ADR-0047: verify degree 0/1 only for the qualified LinearExpression route; nonlinear children use ordinary expressions | Constant and nonlinear-child fixtures, declared numerical policy |
| R4-02/10: guarded and opaque kernels | Blueprint §18.5/§18.9/§21.2/§21.4, ADR-0043/0047; actual writer/solver support, units and derivatives are required | Excluded-branch failure tests, native/Pyomo outcome comparisons and external-function solves |
| Result suffixes and ecosystem tools | Blueprint §21.2/§21.3: record actual versions and cross-check native structures | Real dual/bound-multiplier round trips and supported pyomo.contrib operations; old import/construction probes do not close these |
| L7: bounded coalescing | Blueprint §21.1, ADR-0048, R-24; stream ownership remains mandatory | Representative transfer/memory measurement and independent semantic equality |
The implementation sequence and boundary fixtures are recorded in plan 02. Performance and solver support remain unmeasured/unverified until those specific paths are exercised.
Evidence for the capability maps
Reproduction material for the [probe], [rustdoc:…] and [api:…] provenance markers in the
four maps in the parent directory (arrow-rust.md, datafusion-rust.md,
supporting-rust-libraries.md, python-libraries.md).
| Directory | Covers |
|---|---|
rust/ | arrow 59.3.0 / datafusion 55.1.0 and the §3.3 supporting crates: =-pinned manifests, committed lockfiles, rustdoc-JSON generation scripts and their logs, eleven probe programs and their captured outputs, and checkmap.py (the mechanical check the maps claim) |
python/ | the Python boundary libraries: pinned requirement sets for the platform (3.14) and parity (3.13) interpreters, apidump.py/apiq.py (the rustdoc-JSON analogue), three probe programs |
arrow_probe.rs | Arrow probes 1–5 (IPC metadata round-trip, null/NaN/-0.0 distinctness, totalOrder sort, batch-split byte equality, alignment sensitivity), still quoted by arrow-rust.md; run it as a src/bin/ target of the rust/ scratch project |
Regenerate with just evidence-regen (see rust/README.md and python/README.md for the manual
steps). The extracted fact profiles (build/facts/*) are derived output and are not committed;
the lockfiles are what make the extraction reproducible.
Note on pinning: every crate is =-pinned individually and deliberately. Pinning only the umbrella
(arrow = "=59.3.0") does not pin the family — sub-crates depend on ^, so they float upward,
and cargo tree -d does not flag it because a mixed family is not a duplicate. cargo xtask family-check exists because of this.
Rust evidence — arrow 59.3.0 / datafusion 55.1.0 / the §3.3 supporting crates
Reproduction material for the [rustdoc:…] and [probe] markers in
../../arrow-rust.md, ../../datafusion-rust.md and
../../supporting-rust-libraries.md.
Two independent extractions live here, each with its own =-pinned manifest and
committed lockfile:
| Manifest | Lockfile | Covers | Facts |
|---|---|---|---|
apisurface-Cargo.toml | apisurface-Cargo.lock | 20 arrow + 37 datafusion crates | build/facts/{arrow593,df551} |
support-Cargo.toml | support-Cargo.lock | 26 supporting crates (§3.3) | build/facts/support |
This supersedes the material in the parent evidence/ directory, which described a
13-crate arrow extraction whose output was not preserved — every [rustdoc:]
marker in the previous edition of both maps was therefore unreproducible. That is the
defect this directory exists to prevent: the lockfile is committed, so the extraction
resolves to the same versions rather than to whatever is current.
Files
| File | Role |
|---|---|
apisurface-Cargo.toml | Extraction manifest. Every crate in both families =-pinned individually. |
apisurface-Cargo.lock | The resolved graph — 361 packages. Without this, “re-derivable” means “re-resolvable”, which is a weaker claim. |
gen_rustdoc.sh | Generates rustdoc JSON for every target; fails soft per crate and writes a log. |
rustdoc_gen.log | The generation receipt: toolchain, timestamps, per-crate OK/FAIL, output listing. |
df_probe.rs | PROBES A, C, D, E, H — pushdown shapes, proto encoding, float/null semantics, extension-metadata survival, memory-pool failure. |
df_probe_b.rs | PROBE B — UserDefinedLogicalNode survival through the optimizer. |
df_probe_c3.rs | PROBE C characterisation — plan-byte stability against the number of field-metadata keys. |
df_probe_x.rs | PROBES X1, X2, X3 — ExtensionTypeRegistry, EXPLAIN formats, config-validation behaviour. |
arrow_probe2.rs | PROBES 6, 7 — metadata-order effect on canonical IPC bytes; Parquet metadata survival and write determinism. |
probe_output.txt | Captured output of all five programs, as quoted in the maps. |
checkmap.py | The mechanical check the maps claim: marker census, table well-formedness (respecting backtick spans and escaped pipes), contiguous section numbering. Run it against all four maps. |
gen_rustdoc_support.sh, rustdoc_gen_support.log | The supporting-crate extraction and its receipt. |
probe_identity.rs | PROBES 1, 2 — blake3 derivation routes and truncation soundness; what serde_arrow tracing actually produces. |
probe_graph.rs | PROBE 3 — petgraph SCC order stability, matching direction-independence, feedback arc sets. |
probe_numerics.rs | PROBES 4, 5 — num-dual’s implicit function theorem against a hand derivation; faer’s matrix-free operator contract. |
probe_compile.rs | PROBES 6, 8 — salsa backdating/accumulators/durability; egglog extraction reproducibility. |
probe_parse.rs | PROBE 7 — serde-saphyr and toml::Spanned spans and panic-freedom at the authoring boundary. |
probe_output_support.txt | Captured output of the five supporting-crate probes. |
The Arrow-side probes 1–5 remain in the parent directory’s arrow_probe.rs, unchanged.
Anchors
arrow family 59.3.0 20 publishable crates extracted (of 23 in the workspace)
datafusion family 55.1.0 37 publishable crates extracted
toolchain rustc 1.100.0-nightly (809936eac 2026-09-12)
format_version 61
targets 57 requested, 57 OK, 0 FAIL, 60 JSON documents
Not extracted, and named rather than silently omitted: arrow-pyarrow (requires a Python
interpreter with pyarrow at build time, which breaks the offline constraint),
parquet-geospatial and parquet_derive (both rejected in the Arrow map §10.1).
Reproduction
mkdir -p /tmp/apiex/src && cd /tmp/apiex
cp .../evidence/rust/apisurface-Cargo.toml Cargo.toml
cp .../evidence/rust/apisurface-Cargo.lock Cargo.lock # <- the point
cp .../evidence/rust/gen_rustdoc.sh gen.sh && chmod +x gen.sh
echo '// extraction root' > src/lib.rs
cargo fetch --locked
./gen.sh # rustdoc JSON -> target/doc/*.json
# probes
mkdir -p src/bin && cp .../evidence/rust/*probe*.rs src/bin/
cargo run --offline --bin df_probe
cargo run --offline --bin df_probe_b
cargo run --offline --bin df_probe_c3
cargo run --offline --bin df_probe_x
cargo run --offline --bin arrow_probe2
To normalize the rustdoc JSON into the queryable api / traits / impls records the
maps cite as [rustdoc:arrow593@…] and [rustdoc:df551@…]:
BIN=tooling/dfarrow-apiex/target/release/dfarrow-apiex
$BIN gate /tmp/apiex/target/doc/arrow_schema.json # must report format_version=61
for f in /tmp/apiex/target/doc/*.json; do
case $(basename "$f") in
arrow*|parquet*) prof=arrow593 ;;
datafusion*) prof=df551 ;;
*) continue ;;
esac
$BIN normalize "$f" "$prof" build/facts/$prof
done
This writes to new profiles. The pre-existing build/facts/{arrow59-default,df55-default}
profiles are at 59.2.0 / 55.0.0 — one minor behind both pins — and are never overwritten.
Do not cite them for a claim about the pinned versions.
Honesty notes — supporting-crate extraction
- Three targets needed explicit
name@versionspecs.syn,num-dualandthiserroreach resolve at more than one version in that graph, socargo rustdoc -p <name>fails with “specification is ambiguous”. That is not a harness defect — it is thenum-dualversion split showing up in the build tooling before it shows up in the type checker. - Both
num-dualmajors are extracted (0.14.2 and 0.15.0) so the trait-shape change between them can be cited directly.num_dual.jsonis 0.15.0;num_dual-0.14.2.jsonis the other. ipopt-sysis not extracted. It needs a native Ipopt install, which is outside the offline constraint. Its claims are sourced fromIpStdCInterface.hat COIN-OR tagreleases/3.14.16instead, and the map says so.faer::matrix_free::partial_svdwas not executed. Its dimension preconditions were not satisfiable in the harness; the map records the finding as[UNVERIFIED]and rests it on faer’s own documentation plus the measuredBiLinOpcontract, rather than guessing.- PROBE 8 corrected a first reading. The initial comparison used
{:?}and appeared to show egglog extraction as nondeterministic. ComparingDisplayandsnapshot_stable_under_proof_encodingshowed the extraction is deterministic and only theDebugrendering of a hash-set field varies. The probe now reports all three.
Honesty notes
- Two probes corrected claims the previous edition made. PROBE 6 refutes the Arrow map’s
assertion that metadata key order destabilises the canonical IPC encoding (it does not).
PROBE C locates the real instability in
datafusion-proto, which blueprint revision 2’s F9 depends on. Both corrections are recorded in the maps rather than quietly fixed. - PROBE A resolves a blocking item, PROBE C opens one. The
Exact-pushdown spike that blueprint §26 F17 required has been run and the answer is favourable; the plan-fingerprint mechanism F9 specifies is not reproducible as written. arrow-pyarrowis unmeasured. Thearrow-pyarrowvspyo3-arrowdecision (Arrow map §15 item 5) is still open and this extraction cannot inform the build-complexity half of it.- Probe programs measure the pinned versions only. A version bump invalidates every quoted measurement; re-run before trusting them.
Evidence for the Python library capability map
Reproduction material for the [api:…] and [probe] provenance markers in
../../python-libraries.md.
| File | What it is |
|---|---|
requirements-py314.txt | the main environment: CPython 3.14.7, every library ==-pinned |
requirements-py313.txt | the parity environment: CPython 3.13.12, the same set plus idaes-pse==2.12.0 (IDAES classifies 3.10–3.13 only) |
apidump.py | walks a library’s public API via inspect/importlib and emits JSON — the rustdoc-JSON analogue. Backs every [api:lib@version] marker. |
apiq.py | queries those dumps (cls / find / mod / has) |
probe_arrow.py | PROBES 1, 2, 8 — extension-type survival through IPC and __arrow_c_stream__; schema/field metadata; numpy zero-copy and the null→NaN collapse |
probe_pyomo.py | PROBES 3, 4, 5 — LinearExpression/Expr_if/ExternalFunction lowering; units consistency; suffixes and solver availability |
probe_contracts.py | PROBES 6, 7 — cattrs strictness and Any; msgspec constraints, located errors and JSON Schema |
To reproduce:
uv venv --python 3.14 py314
uv pip install --python py314/bin/python -r requirements-py314.txt
./py314/bin/python apidump.py api314 pyarrow attrs cattrs msgspec numpy pyomo scipy
./py314/bin/python probe_arrow.py
./py314/bin/python probe_contracts.py
./py314/bin/python probe_pyomo.py # needs pint; see the map's §5
uv venv --python 3.13 py313
uv pip install --python py313/bin/python -r requirements-py313.txt
./py313/bin/python apidump.py api313 idaes
Two notes the map depends on:
probe_pyomo.pyrequirespint. That is itself the finding: Pyomo’s units machinery is pint, so §21.2’sassert_units_consistentrequirement cannot be met under §3.3’s exclusion of a Python-side units library. Run it once without pint to see theDeferredImportError.- No solver is installed in either environment, so PROBE 5 confirms suffix declaration only;
the
dual/ipopt_zL_out/ipopt_zU_outround-trip is recorded as[UNVERIFIED]in the map rather than asserted.
CI job graph
Owner: paul-heyse. Every job below has one owner, and every scheduled
job has a documented response for when it goes red — a red scheduled job that
nobody has agreed to answer is a notification, not a control.
Workflows: rust.yml, rust-scheduled.yml, python.yml, wheels.yml,
solvers-image.yml, release.yml, docs.yml, governance.yml,
register-review.yml, repo-hygiene.yml, plus the composite action
.github/actions/setup-rust.
Common to every workflow: permissions: contents: read at the top, widened per
job; per-ref concurrency with cancel-in-progress on pull requests only;
CARGO_INCREMENTAL=0, CARGO_TERM_COLOR=always, RUST_BACKTRACE=1; tool
versions as environment variables; the solver image referenced by digest
(SOLVER_IMAGE=ghcr.io/paul-heyse/pse-solvers:ci-<hash>@sha256:<digest>); no
RUSTFLAGS; sccache off; every action pinned by commit SHA with a version
comment, kept current by Dependabot and pinact.
Required checks on main
| Check context | Job | Runs |
|---|---|---|
rust / fmt | fmt | cargo fmt --all --check; taplo fmt --check |
rust / clippy | clippy (solver container) | cargo clippy --workspace --all-targets --locked -- -D warnings, then again with --no-default-features |
rust / test | test (solver container) | cargo nextest run --workspace --locked --profile ci --features pse-relations/force-validate; cargo test --doc --workspace --locked --features pse-relations/force-validate; cargo test --benches -p pse-benches -p pse-relations --locked --features pse-relations/force-validate; JUnit artifact |
rust / codegen-diff | codegen-diff (container: Ipopt headers + libclang) | cargo xtask codegen --check |
rust / family-check | family-check | cargo xtask family-check --evidence docs/capability-maps/evidence/rust/*.lock; cargo metadata --locked |
rust / deny | deny | cargo deny check; cargo audit; cargo shear |
python / lint | lint | uv sync --locked --group quality --no-install-project; ruff format/check, pyrefly, import-linter, reuse lint, typos, taplo, uv lock --check, scripts/check_generated.py |
python / test | test (3.11 and 3.14) | maturin-backed uv sync; pytest -m "unit or component" -n auto --cov |
python / parity | parity (container ci stage; 3.13 on PRs, 3.11–3.13 nightly) | editable native dev build, uv run --no-sync pytest --parity |
docs / build | build | scripts/adr.py index --check; mdbook build docs; lychee --offline; Pages artifact |
governance / adr-lint | adr-lint | scripts/adr.py lint; scripts/check_register.py --lint; the needs-adr label rule; the IDAES tag in scripts/fetch-external.sh equals the parity pin |
governance / pr-title | pr-title | Conventional-Commit title check, types and scopes from cliff.toml |
The full set above is declared in .github/setup/ruleset-main-full.json.
just gh-setup applies it; just gh-setup-check verifies the live configuration.
Check names are explicit job names, including the aggregate Python test result.
A newly declared check must report before its ruleset is activated.
Run on pull requests, not required
rust / docs (RUSTDOCFLAGS=-Dwarnings cargo doc --no-deps), rust / doc-lint
(paths-filtered), rust / coverage (cargo llvm-cov nextest … --lcov, artifact upload),
rust / coverage-upload (host-runner Codecov OIDC, informational in phase 0), rust / semver (from the first tag; gating
only on PRs labelled release), rust / msrv-floors (cargo hack check --rust-version, only when Cargo.lock changed), rust / test-macos and
rust / test-windows (phase 1b, continue-on-error until a green week),
governance / reuse, governance / agent-config,
repo-hygiene / * (actionlint, zizmor, pinact). docs / deploy runs on
main through actions/deploy-pages.
Scheduled jobs, and what to do when one goes red
rust-scheduled.yml runs weekly (cron 0 6 * * 1). None of these gates a pull
request; each exists to find a problem before an upgrade does.
| Job | Owner | Red means | Documented response |
|---|---|---|---|
features-powerset (cargo hack --feature-powerset --depth 2) | paul-heyse | a feature combination does not compile — most often an optional dependency used outside its #[cfg] | Reproduce with the printed feature set, fix the cfg or the optional dependency, and add the combination to the PR-time --no-default-features clippy run if it is load-bearing. Open a kind/bug issue the same week; do not disable the job. |
test-release (--cargo-profile release) | paul-heyse | a test passes at dev optimization and fails at release — a float-sensitivity or an undefined-order assumption | Treat as a correctness bug, not a flake. Reproduce locally with just test-release, fix the test or the code, and record the cause; a release-only failure in numerics is exactly the class ADR-0030 and the force_validate feature exist to surface. |
udeps (pinned nightly) | paul-heyse | a declared dependency is unused | Remove it, or record it in tests/governance/tooling_deps.toml with a reason. During phase 0 the skeleton declares blueprint dependencies before using them, which is why this is scheduled rather than gating (register row R-18). |
mutants on pse-ids and pse-mathir (continue-on-error) | paul-heyse | surviving mutants in the identity or IR crates — a test suite that does not discriminate | Read the artifact, add tests for the survivors that matter, and note the ones deliberately left (a mutant in a Debug impl is not worth a test). Never treat the count as a gate. |
unsafe-surface (cargo geiger) | paul-heyse | unsafe outside the four allowlisted crates, or a growth inside them | Check it against [workspace.metadata.pse] unsafe-allowlist; if it is genuinely new non-FFI unsafe, the Miri deferral’s trigger has fired (register row R-12) and the response is an ADR, not a suppression. |
doc-lint (full) | paul-heyse | a backticked a::b::c or [rustdoc:…] identifier in docs/** no longer resolves | Usually an upstream rename. Fix the document, or add the identifier to the allowlist with a reason. If a capability map is affected, regenerate its evidence rather than editing the claim. |
floors-latest (cargo update --dry-run + floors against a temp lock) | paul-heyse | a newer version of a pre-1.0 crate raises its declared MSRV above ours, or a family would go mixed | This is the early warning ADR-0018 is built around. Record it on register row R-07 with the date, and decide the upgrade deliberately — bump the toolchain by ADR, or hold the pin. |
solvers-image-rebuild-check (no cache) | paul-heyse | the solver recipe no longer reproduces the same library checksums | Diff the build log against the last good one, find the moving input (a base-image digest, an upstream tarball, a compiler version), pin it in docker/solvers/, and update checksums.sha256. Until it reproduces, treat parity iteration counts as unverified (register row R-08). |
solvers-image.yml builds the solver container on every pull request that touches
docker/solvers/** and, on main, pushes it to GHCR and opens the pull request that
moves the SOLVER_IMAGE pin (a digest reference inside the workflow files, so a
checkout is reproducible on its own). Opening that pull request needs the
PIN_PR_TOKEN secret: a fine-grained personal access token scoped to this
repository with contents, pull requests and workflows write, because the default
GITHUB_TOKEN cannot create or update files under .github/workflows. Without the
secret the job succeeds, uploads the image manifest, and prints the exact
just solver-pin-update --ci … --dev … command (register row R-21).
The canonical pins are in .github/setup/solver-images.json, outside the solver
recipe tree hash. just solver-pin-check checks every literal consumer.
just solver-rebuild-check repeats the no-cache library comparison locally.
register-review.yml runs monthly with issues: write, executes
scripts/check_register.py --due — including the shell checks in the check
column — and opens or updates one Register review YYYY-MM issue. Red means
the workflow itself failed, not that a check reported something: --due
reports a failing command and never fails on it. The response is to run it
locally, then to act on each due row in the issue before closing it.
codecov.yml is informational, with after_n_builds equal to the number of
uploads; coverage never blocks a merge in phase 0.
Local equivalents
| CI tier | Local recipe | What it proves |
|---|---|---|
| fast feedback | just ci-fast (fmt-check, check, clippy, test, doctest) | The workspace compiles and the unit and component tests pass at dev. |
| pull-request gate | just ci-pr (adds governance, policy, docs, bench-smoke, quality, adr-lint, py-test) | Local Rust/Python and repository checks; container parity is separate. |
| parity preflight | just parity-container | Existing IDAES/solver preflight cases in the pinned dev image, using a separate Python 3.13 environment. |
| scheduled | just features-powerset, just test-release, just udeps, just mutants-file, just unsafe-surface, just floors-latest | Individually reproduces one weekly job. |
just --list is the contract: if a check exists in CI and has no local recipe,
that is a bug in the justfile.
Phase-zero limits
Code generation and API-reference doc lint remain deferred (register R-20).
PR-time API doc lint emits a deferral notice when the path indexes are absent;
its CLI remains an exit-2 stub and scheduled checks retain the register trigger.
codegen-check detects staged, unstaged and untracked generated-path changes; it
does not prove a real schema or binding generator exists. The parity suite currently
qualifies its environment and package boundaries, not numerical model parity.
The wheel workflow runs only by manual dispatch (just wheels-check <ref>) or
from the release workflow. It builds all
five native platform targets, checks cp311-abi3 tags, installs each wheel under
Python 3.11 and 3.14 outside the source tree, and builds/installs the sdist.
Development compile path
just py-sync uses maturin’s editable dev profile from pyproject.toml; dependency
optimization stays in Cargo’s dev profile and the local build cache is reused.
Follow it with just py-test and just quality. just parity-container uses the
same editable profile with the pinned solver image and a separate Python 3.13 venv.
Automatic Python CI follows this path, with no dependency on a wheel or sdist job.
Rust release-profile checks remain scheduled, and distribution acceptance is manual
or release-time. Source changes, dependency changes and toolchain changes therefore
run development checks without automatically rebuilding five distribution targets.
Solver recipe changes retain their separate container build/linkage checks.