@earthsciml/ast 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1669,6 +1669,121 @@ interface Metaparameters {
1669
1669
  };
1670
1670
  }
1671
1671
 
1672
+ /**
1673
+ * Central registry of the diagnostic code STRINGS emitted by this binding, plus
1674
+ * a neutral diagnostic base class.
1675
+ *
1676
+ * Cross-binding contract — values must never change; see Python `ErrorCode`
1677
+ * enum (`pkg/earthsci-ast-py/src/earthsci_ast/error_handling.py`). These strings
1678
+ * are pinned by the shared conformance fixtures: every value below equals, byte
1679
+ * for byte, a literal currently emitted somewhere in `src/`. This module only
1680
+ * CENTRALIZES the references — it does not (and must not) change any emitted
1681
+ * string. Adding a new diagnostic means adding an entry here AND coordinating
1682
+ * the value across every binding.
1683
+ *
1684
+ * Keys are the SCREAMING_SNAKE_CASE form of the value (mirroring the Python
1685
+ * enum) so a reference reads as `ERROR_CODES.UNDEFINED_VARIABLE`.
1686
+ */
1687
+ declare const ERROR_CODES: {
1688
+ readonly ANALYSIS: "analysis";
1689
+ readonly CIRCULAR_DEPENDENCY: "circular_dependency";
1690
+ readonly DIMENSIONAL_MISMATCH: "dimensional_mismatch";
1691
+ readonly JOIN_KEY_INVALID_TYPE: "join_key_invalid_type";
1692
+ readonly DOMAIN_UNIT_MISMATCH: "domain_unit_mismatch";
1693
+ readonly COUPLE_MULTIPLICATIVE_NO_TENDENCY: "couple_multiplicative_no_tendency";
1694
+ readonly RELATIONAL_NODE_IN_CONTINUOUS: "relational_node_in_continuous";
1695
+ readonly UNDEFINED_INDEX_SET: "undefined_index_set";
1696
+ readonly INVALID_BROADCAST_FN: "invalid_broadcast_fn";
1697
+ readonly ARRAY_SHAPE_MISMATCH: "array_shape_mismatch";
1698
+ readonly EQUATION_COUNT_MISMATCH: "equation_count_mismatch";
1699
+ readonly EVENT_AFFECTS_PARAMETER: "event_affects_parameter";
1700
+ readonly EVENT_VAR_UNDECLARED: "event_var_undeclared";
1701
+ readonly FACTOR_WITH_EXPRESSION_TRANSFORM: "factor_with_expression_transform";
1702
+ readonly IC_IN_REACTION_SYSTEM: "ic_in_reaction_system";
1703
+ readonly INVALID_STOICHIOMETRY: "invalid_stoichiometry";
1704
+ readonly INVALID_TEMPORAL_DURATION: "invalid_temporal_duration";
1705
+ readonly NULL_REACTION: "null_reaction";
1706
+ readonly AMBIGUOUS_SUBSYSTEM_REF: "ambiguous_subsystem_ref";
1707
+ readonly DATA_SOURCE_UNDEFINED: "data_source_undefined";
1708
+ readonly UNDEFINED_PARAMETER: "undefined_parameter";
1709
+ readonly UNDEFINED_SPECIES: "undefined_species";
1710
+ readonly UNDEFINED_SYSTEM: "undefined_system";
1711
+ readonly UNDEFINED_VARIABLE: "undefined_variable";
1712
+ readonly UNIT_ERROR: "unit_error";
1713
+ readonly UNIT_INCONSISTENCY: "unit_inconsistency";
1714
+ readonly UNIT_PARSE_ERROR: "unit_parse_error";
1715
+ readonly UNPARSEABLE_UNIT: "unparseable_unit";
1716
+ readonly UNRESOLVED_SCOPED_REF: "unresolved_scoped_ref";
1717
+ readonly UNRESOLVED_SUBSYSTEM_REF: "unresolved_subsystem_ref";
1718
+ readonly JSON_PARSE_ERROR: "json_parse_error";
1719
+ readonly UNEXPECTED_ERROR: "unexpected_error";
1720
+ readonly SCHEMA_VALIDATION_ERROR: "schema_validation_error";
1721
+ readonly PARSE_ERROR: "parse_error";
1722
+ readonly EXPRESSION_TEMPLATE_ERROR: "expression_template_error";
1723
+ readonly ENUM_LOWERING_ERROR: "enum_lowering_error";
1724
+ readonly NONFINITE_NUMBER: "nonfinite_number";
1725
+ readonly LOAD_ERROR: "load_error";
1726
+ readonly APPLY_EXPRESSION_TEMPLATE_BINDINGS_MISMATCH: "apply_expression_template_bindings_mismatch";
1727
+ readonly APPLY_EXPRESSION_TEMPLATE_INVALID_DECLARATION: "apply_expression_template_invalid_declaration";
1728
+ readonly APPLY_EXPRESSION_TEMPLATE_RECURSIVE_BODY: "apply_expression_template_recursive_body";
1729
+ readonly APPLY_EXPRESSION_TEMPLATE_UNKNOWN_TEMPLATE: "apply_expression_template_unknown_template";
1730
+ readonly APPLY_EXPRESSION_TEMPLATE_VERSION_TOO_OLD: "apply_expression_template_version_too_old";
1731
+ readonly REWRITE_RULE_NONTERMINATING: "rewrite_rule_nonterminating";
1732
+ readonly TEMPLATE_BODY_EXPANSION_TOO_DEEP: "template_body_expansion_too_deep";
1733
+ readonly TEMPLATE_CONSTRAINT_UNKNOWN_INDEX_SET: "template_constraint_unknown_index_set";
1734
+ readonly METAPARAMETER_NAME_CONFLICT: "metaparameter_name_conflict";
1735
+ readonly METAPARAMETER_TYPE_ERROR: "metaparameter_type_error";
1736
+ readonly METAPARAMETER_UNBOUND: "metaparameter_unbound";
1737
+ readonly TEMPLATE_IMPORT_CYCLE: "template_import_cycle";
1738
+ readonly TEMPLATE_IMPORT_INDEX_SET_CONFLICT: "template_import_index_set_conflict";
1739
+ readonly TEMPLATE_IMPORT_IS_COUPLING_LIBRARY: "template_import_is_coupling_library";
1740
+ readonly TEMPLATE_IMPORT_NAME_CONFLICT: "template_import_name_conflict";
1741
+ readonly TEMPLATE_IMPORT_NOT_LIBRARY: "template_import_not_library";
1742
+ readonly TEMPLATE_IMPORT_REBIND_UNKNOWN_NAME: "template_import_rebind_unknown_name";
1743
+ readonly TEMPLATE_IMPORT_RENAME_COLLISION: "template_import_rename_collision";
1744
+ readonly TEMPLATE_IMPORT_RENAME_INVALID: "template_import_rename_invalid";
1745
+ readonly TEMPLATE_IMPORT_RENAME_UNKNOWN_NAME: "template_import_rename_unknown_name";
1746
+ readonly TEMPLATE_IMPORT_UNKNOWN_NAME: "template_import_unknown_name";
1747
+ readonly TEMPLATE_IMPORT_UNRESOLVED: "template_import_unresolved";
1748
+ readonly TEMPLATE_IMPORT_VERSION_TOO_OLD: "template_import_version_too_old";
1749
+ readonly TEMPLATE_INJECT_TARGET_NOT_COMPONENT: "template_inject_target_not_component";
1750
+ readonly TEMPLATE_INJECT_TARGET_UNKNOWN: "template_inject_target_unknown";
1751
+ readonly GEOMETRY_MANIFOLD_INVALID: "geometry_manifold_invalid";
1752
+ readonly MAKEARRAY_REGION_INVERTED: "makearray_region_inverted";
1753
+ readonly SUBSYSTEM_INDEX_SET_CONFLICT: "subsystem_index_set_conflict";
1754
+ readonly SUBSYSTEM_REF_IS_COUPLING_LIBRARY: "subsystem_ref_is_coupling_library";
1755
+ readonly SUBSYSTEM_REF_IS_TEMPLATE_LIBRARY: "subsystem_ref_is_template_library";
1756
+ readonly COUPLING_EDGE_UNKNOWN_ROLE: "coupling_edge_unknown_role";
1757
+ readonly COUPLING_IMPORT_BIND_NOT_A_COMPONENT: "coupling_import_bind_not_a_component";
1758
+ readonly COUPLING_IMPORT_NOT_LIBRARY: "coupling_import_not_library";
1759
+ readonly COUPLING_IMPORT_ROLE_UNBOUND: "coupling_import_role_unbound";
1760
+ readonly COUPLING_IMPORT_UNKNOWN_ROLE: "coupling_import_unknown_role";
1761
+ readonly COUPLING_IMPORT_UNRESOLVED: "coupling_import_unresolved";
1762
+ readonly COUPLING_LIBRARY_ILLEGAL_PAYLOAD: "coupling_library_illegal_payload";
1763
+ readonly COUPLING_LIBRARY_NESTED_IMPORT: "coupling_library_nested_import";
1764
+ readonly COUPLING_ROLE_UNUSED: "coupling_role_unused";
1765
+ readonly ENUM_OP_MALFORMED: "enum_op_malformed";
1766
+ readonly ENUM_NOT_DECLARED: "enum_not_declared";
1767
+ readonly ENUM_MEMBER_NOT_FOUND: "enum_member_not_found";
1768
+ readonly UNKNOWN_CLOSED_FUNCTION: "unknown_closed_function";
1769
+ readonly CLOSED_FUNCTION_ARITY: "closed_function_arity";
1770
+ readonly CLOSED_FUNCTION_OVERFLOW: "closed_function_overflow";
1771
+ };
1772
+ /** A diagnostic code string from {@link ERROR_CODES}. */
1773
+ type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
1774
+ /**
1775
+ * Neutral base for EarthSciAST diagnostics: an `Error` carrying a stable `code`
1776
+ * string (from {@link ERROR_CODES}) and optional structured `details`. This is
1777
+ * purely additive — a single home for future diagnostics. It intentionally does
1778
+ * NOT touch the existing `EsmMachineryError` / `EnumLoweringError` /
1779
+ * `ClosedFunctionError` classes, which another file owns.
1780
+ */
1781
+ declare class EsmDiagnosticError extends Error {
1782
+ readonly code: string;
1783
+ readonly details?: Record<string, unknown> | undefined;
1784
+ constructor(code: string, message: string, details?: Record<string, unknown> | undefined);
1785
+ }
1786
+
1672
1787
  /**
1673
1788
  * Tagged numeric-literal AST nodes and lossless JSON I/O per
1674
1789
  * discretization RFC §5.4.1 (int/float distinction) and §5.4.6
@@ -1696,6 +1811,7 @@ interface Metaparameters {
1696
1811
  * NaN and ±Infinity are rejected on serialize with
1697
1812
  * `E_CANONICAL_NONFINITE` per RFC §5.4.6.
1698
1813
  */
1814
+
1699
1815
  interface NumericLiteral {
1700
1816
  readonly kind: 'int' | 'float';
1701
1817
  readonly value: number;
@@ -1715,7 +1831,7 @@ declare function isFloatLit(x: unknown): x is NumericLiteral & {
1715
1831
  * at the boundary between kind-aware and kind-agnostic code.
1716
1832
  */
1717
1833
  declare function numericValue(x: unknown): number | undefined;
1718
- declare class LosslessJsonParseError extends Error {
1834
+ declare class LosslessJsonParseError extends EsmDiagnosticError {
1719
1835
  readonly position: number;
1720
1836
  constructor(message: string, position: number);
1721
1837
  }
@@ -1741,7 +1857,7 @@ declare function losslessJsonParse(text: string): unknown;
1741
1857
  * `canonicalize.ts` re-exports this class under the same name, so consumers may
1742
1858
  * keep importing `CanonicalizeError` from either module.
1743
1859
  */
1744
- declare class CanonicalizeError extends Error {
1860
+ declare class CanonicalizeError extends EsmDiagnosticError {
1745
1861
  /** Stable RFC §5.4.6 / §5.4.7 error code. */
1746
1862
  readonly code: string;
1747
1863
  constructor(code: string, message?: string);
@@ -1783,7 +1899,7 @@ declare function losslessJsonStringify(value: unknown): string;
1783
1899
  * `.0` override when the result is an integer-valued plain-decimal token.
1784
1900
  *
1785
1901
  * NAMING: this is the DOCUMENT-serialization float emitter (used by
1786
- * {@link losslessJsonStringify} and `save()`), which relies on
1902
+ * {@link losslessJsonStringify} and `toJson()`), which relies on
1787
1903
  * `ToString(Number)`'s own exponent formatting. It is deliberately NOT the
1788
1904
  * strict RFC §5.4.6 CANONICAL-FORM emitter — that is the confusingly-similar
1789
1905
  * `formatCanonicalFloat` in `canonicalize.ts`, which additionally normalizes
@@ -1807,11 +1923,30 @@ declare function formatFloatToken(value: number): string;
1807
1923
  *
1808
1924
  * Canonical alias names (duplicates are kept for back-compat but marked
1809
1925
  * `@deprecated`):
1810
- * - root file structure → `EsmFile` (aliases: `EsmFormat`, generated `ESMFormat`)
1926
+ * - root file structure → `EsmFile` (alias: the generated `ESMFormat`)
1811
1927
  * - operator node → `ExpressionNode` (alias: `ExprNode`)
1812
1928
  * - `Expression` (wire / schema-shaped value) and `Expr` (widened in-memory
1813
1929
  * value that MAY carry a tagged `NumericLiteral`) are DISTINCT types, not
1814
1930
  * aliases — pick by whether you hold a wire value or an in-memory one.
1931
+ *
1932
+ * DO NOT collapse `Expression` and `Expr` into one type. The phase-6 surface
1933
+ * tidy (API_SPEC.md §8) proposed it and it was refused, because the two are not
1934
+ * an alias pair in either direction:
1935
+ *
1936
+ * - `Expression` is GENERATED from `esm-schema.json` by json2ts (plus
1937
+ * `scripts/fix-generated-expression.mjs`). Deleting it means the next
1938
+ * `npm run generate-types` puts it back.
1939
+ * - The relationship is a strict, ONE-WAY subtype: `Expression` is assignable
1940
+ * to `Expr`, and `Expr` is NOT assignable to `Expression` — the compiler
1941
+ * rejects it, because `Expr` admits the tagged `NumericLiteral` leaf that
1942
+ * only exists in memory. Collapsing onto `Expr` would let a tagged literal
1943
+ * reach a serialization boundary typed for the wire; collapsing onto
1944
+ * `Expression` would delete the tagged leaf the discretization RFC §5.4.1
1945
+ * requires.
1946
+ *
1947
+ * Both names are load-bearing and heavily used (roughly 400 references each in
1948
+ * this package), and the editor is written almost entirely against `Expression`
1949
+ * because it edits wire values.
1815
1950
  */
1816
1951
 
1817
1952
  /**
@@ -1856,9 +1991,35 @@ type EsmFile = ESMFormat1 & Omit<ESMFormat2, 'models'> & {
1856
1991
  models?: {
1857
1992
  [k: string]: Model | SubsystemRef;
1858
1993
  };
1994
+ /**
1995
+ * NON-SCHEMA, LOADER-POPULATED. The per-component `expression_templates`
1996
+ * registries of the Option-B loaded image, keyed
1997
+ * `"models.<name>"` / `"reaction_systems.<name>"` — the Julia / Python
1998
+ * `component_templates` key shape.
1999
+ *
2000
+ * `loadInput` (`parse.ts`) snapshots these between
2001
+ * `lowerExpressionTemplates` (which preserves the per-component blocks) and
2002
+ * `expandDocument` (which strips them), because esm-libraries-spec §4.7.5
2003
+ * step 4 requires `flatten` to carry the MERGED template registry as a
2004
+ * first-class field of the flattened representation — see
2005
+ * `mergedTemplateRegistry` in `flatten-template-registry.ts`.
2006
+ *
2007
+ * It is NOT part of `esm-schema.json`, is NEVER serialized (`toJson`
2008
+ * excludes it), and is absent on a document that declares no per-component
2009
+ * `expression_templates` block — i.e. on almost every document. Values are
2010
+ * raw JSON template declarations (`{params, body, match?, …}`), not the
2011
+ * typed `Expression` form.
2012
+ *
2013
+ * `loadInput` attaches it as a NON-ENUMERABLE own property, so
2014
+ * `Object.keys`, `JSON.stringify`, spread and structural equality all see
2015
+ * exactly the document and nothing else — the conformance round-trip
2016
+ * (`loadString(toJson(loadString(f)))` deep-equals `loadString(f)`) would
2017
+ * otherwise fail for every template-bearing fixture. A consequence worth
2018
+ * knowing: a shallow copy (`{...file}`) DROPS it, so a pass that rebuilds
2019
+ * the file object must re-attach it if the result will be flattened.
2020
+ */
2021
+ componentTemplates?: Record<string, unknown>;
1859
2022
  };
1860
- /** @deprecated Prefer {@link EsmFile}. Identical to the generated `ESMFormat`. */
1861
- type EsmFormat = ESMFormat;
1862
2023
  /** @deprecated Prefer {@link ExpressionNode} (the generated name). */
1863
2024
  type ExprNode = ExpressionNode;
1864
2025
 
@@ -2098,6 +2259,7 @@ type Model = Omit<Model$1, 'variables' | 'subsystems'> & {
2098
2259
  * counts their own axis made TS the only binding that could report a mismatch
2099
2260
  * between the two spellings of a number density.
2100
2261
  */
2262
+
2101
2263
  interface CanonicalDims {
2102
2264
  kg?: number;
2103
2265
  m?: number;
@@ -2113,7 +2275,7 @@ interface ParsedUnit {
2113
2275
  scale: number;
2114
2276
  offset?: number;
2115
2277
  }
2116
- declare class UnitConversionError extends Error {
2278
+ declare class UnitConversionError extends EsmDiagnosticError {
2117
2279
  constructor(message: string);
2118
2280
  }
2119
2281
  /**
@@ -2376,14 +2538,14 @@ interface SchemaError {
2376
2538
  /**
2377
2539
  * Parse error - thrown when JSON parsing fails
2378
2540
  */
2379
- declare class ParseError extends Error {
2541
+ declare class ParseError extends EsmDiagnosticError {
2380
2542
  originalError?: Error | undefined;
2381
2543
  constructor(message: string, originalError?: Error | undefined);
2382
2544
  }
2383
2545
  /**
2384
2546
  * Schema validation error - thrown when schema validation fails
2385
2547
  */
2386
- declare class SchemaValidationError extends Error {
2548
+ declare class SchemaValidationError extends EsmDiagnosticError {
2387
2549
  errors: SchemaError[];
2388
2550
  constructor(message: string, errors: SchemaError[]);
2389
2551
  }
@@ -2399,7 +2561,7 @@ declare const SCHEMA_VERSION: string;
2399
2561
  */
2400
2562
  declare function validateSchema(data: unknown): SchemaError[];
2401
2563
  /**
2402
- * Options controlling how `load()` parses and represents an ESM file.
2564
+ * Options controlling how the `load*` entry points parse and represent an ESM file.
2403
2565
  */
2404
2566
  interface LoadOptions {
2405
2567
  /**
@@ -2466,21 +2628,51 @@ interface LoadOptions {
2466
2628
  onVersionWarning?: ((message: string) => void) | undefined;
2467
2629
  }
2468
2630
  /**
2469
- * Load an ESM file from a JSON string or pre-parsed object
2631
+ * Read and parse an ESM document from a filesystem path.
2632
+ *
2633
+ * The file's own directory anchors relative `expression_template_imports`
2634
+ * refs and `{ref}` subsystem refs unless `options.basePath` overrides it.
2635
+ * Requires synchronous file access (Node); browser hosts should read the
2636
+ * bytes themselves and call {@link loadString}.
2637
+ *
2638
+ * @param path - Filesystem path to the `.esm` document
2639
+ * @param options - Optional load-time settings (see {@link LoadOptions})
2640
+ * @returns Typed EsmFile object
2641
+ * @throws {ParseError} When JSON parsing fails or version is incompatible
2642
+ * @throws {SchemaValidationError} When schema validation fails
2643
+ */
2644
+ declare function loadPath(path: string, options?: LoadOptions): EsmFile;
2645
+ /**
2646
+ * Parse an ESM document from JSON TEXT.
2470
2647
  *
2471
- * @param input - JSON string or pre-parsed JavaScript object
2648
+ * @param json - The document as a JSON string
2472
2649
  * @param options - Optional load-time settings (see {@link LoadOptions})
2473
2650
  * @returns Typed EsmFile object
2474
2651
  * @throws {ParseError} When JSON parsing fails or version is incompatible
2475
2652
  * @throws {SchemaValidationError} When schema validation fails
2476
2653
  */
2477
- declare function load(input: string | object, options?: LoadOptions): EsmFile;
2654
+ declare function loadString(json: string, options?: LoadOptions): EsmFile;
2655
+ /**
2656
+ * Parse an ESM document that is ALREADY a JavaScript object — the same
2657
+ * document a `.esm` file holds, just already `JSON.parse`d.
2658
+ *
2659
+ * `options.canonical` has no effect here: canonical mode tags numeric
2660
+ * literals during JSON decoding, and this entry point does no decoding.
2661
+ * Callers who want tagged leaves should run `losslessJsonParse` on the text
2662
+ * themselves, or use {@link loadString}.
2663
+ *
2664
+ * @param doc - The already-parsed document
2665
+ * @param options - Optional load-time settings (see {@link LoadOptions})
2666
+ * @returns Typed EsmFile object
2667
+ * @throws {SchemaValidationError} When schema validation fails
2668
+ */
2669
+ declare function loadDocument(doc: object, options?: LoadOptions): EsmFile;
2478
2670
 
2479
2671
  /**
2480
2672
  * ESM Format JSON Serialization (esm-cs3).
2481
2673
  *
2482
- * `save(file)` emits an `EsmFile` as wire-form JSON suitable for round-trip
2483
- * through `load()`. Mirrors the Python and Julia serializers in three respects:
2674
+ * `toJson(file)` emits an `EsmFile` as wire-form JSON suitable for round-trip
2675
+ * through `loadString()`. Mirrors the Python and Julia serializers in three respects:
2484
2676
  *
2485
2677
  * 1. **AST canonical numeric handling.** `NumericLiteral` tagged leaves
2486
2678
  * (the in-memory int/float carrier produced by `losslessJsonParse` and
@@ -2500,7 +2692,7 @@ declare function load(input: string | object, options?: LoadOptions): EsmFile;
2500
2692
  * 3. **Wire-form keys.** TypeScript types are generated from the JSON
2501
2693
  * schema, so the in-memory shape already matches the wire form (no
2502
2694
  * Python-style dataclass → wire field-name remapping is needed).
2503
- * Object key order is the insertion order produced by `load()` /
2695
+ * Object key order is the insertion order produced by `loadString()` /
2504
2696
  * authored constructors, which is itself schema-driven.
2505
2697
  *
2506
2698
  * The Python reference at `pkg/earthsci-ast-py/src/earthsci_ast/serialize.py`
@@ -2509,8 +2701,8 @@ declare function load(input: string | object, options?: LoadOptions): EsmFile;
2509
2701
  * delegating shape preservation to the generated types.
2510
2702
  */
2511
2703
 
2512
- /** Optional behavior controls for {@link save}. */
2513
- interface SaveOptions {
2704
+ /** Optional behavior controls for {@link toJson} / {@link writePath}. */
2705
+ interface ToJsonOptions {
2514
2706
  /**
2515
2707
  * When `true`, emit byte-canonical JSON per RFC §5.4.6: integer-tagged
2516
2708
  * `NumericLiteral` leaves as integer tokens, float-tagged leaves with
@@ -2530,16 +2722,31 @@ interface SaveOptions {
2530
2722
  indent?: number;
2531
2723
  }
2532
2724
  /**
2533
- * Serialize an `EsmFile` to wire-form JSON.
2725
+ * Serialize an `EsmFile` to wire-form JSON. PURE — it never touches disk;
2726
+ * {@link writePath} is the writer.
2534
2727
  *
2535
2728
  * @param file - The `EsmFile` to serialize.
2536
- * @param options - Optional behavior controls (see {@link SaveOptions}).
2729
+ * @param options - Optional behavior controls (see {@link ToJsonOptions}).
2537
2730
  * @returns Wire-form JSON string.
2538
2731
  * @throws {CanonicalNonfiniteError} In `canonical: true` mode, if a
2539
2732
  * `NumericLiteral` leaf holds NaN or ±Infinity (RFC §5.4.6 forbids
2540
2733
  * non-finite numbers in the canonical wire form).
2541
2734
  */
2542
- declare function save(file: EsmFile, options?: SaveOptions): string;
2735
+ declare function toJson(file: EsmFile, options?: ToJsonOptions): string;
2736
+ /**
2737
+ * {@link toJson} with no indentation — the single-line wire form. Present in
2738
+ * every binding, because Rust and Go have no default arguments and so cannot
2739
+ * express `toJson(file, { indent: 0 })`.
2740
+ */
2741
+ declare function toJsonCompact(file: EsmFile, options?: ToJsonOptions): string;
2742
+ /**
2743
+ * Write an `EsmFile` to `path` as wire-form JSON. Returns nothing: no
2744
+ * function in this API both writes and hands back the payload — call
2745
+ * {@link toJson} when you want the string.
2746
+ *
2747
+ * Requires synchronous file access (Node).
2748
+ */
2749
+ declare function writePath(file: EsmFile, path: string, options?: ToJsonOptions): void;
2543
2750
 
2544
2751
  /**
2545
2752
  * Shared validation result / error types for the structural-validation modules.
@@ -2604,14 +2811,37 @@ interface ValidateOptions {
2604
2811
  basePath?: string;
2605
2812
  }
2606
2813
  /**
2607
- * Validate ESM data and return structured validation result.
2814
+ * Validate a TYPED ESM DOCUMENT and return a structured validation result.
2608
2815
  *
2609
- * @param data - ESM data as JSON string or object
2816
+ * API_SPEC.md §8 item 13: `validate` takes a typed document in EVERY binding.
2817
+ * It used to accept `string | object` here, so `validate(someString)` meant
2818
+ * "parse this JSON text" in TypeScript and Python but was a type error in Rust
2819
+ * and Go. The text convenience now has its own name, {@link validateText}, so
2820
+ * the argument type says which one you are calling.
2821
+ *
2822
+ * Passing a string is rejected at compile time by the signature and at runtime
2823
+ * by an explicit guard, rather than silently doing something else.
2824
+ *
2825
+ * @param document - a loaded ESM document (or a plain object of the same shape)
2610
2826
  * @param options - Optional {@link ValidateOptions}; pass `basePath` to let
2611
2827
  * relative `{ref}` / template-import targets be opened and resolved.
2612
2828
  * @returns ValidationResult with validation status and errors
2613
2829
  */
2614
- declare function validate(data: string | object, options?: ValidateOptions): ValidationResult;
2830
+ declare function validate(document: EsmFile | object, options?: ValidateOptions): ValidationResult;
2831
+ /**
2832
+ * Validate ESM JSON **text**: parse it, then run {@link validate} on the result.
2833
+ *
2834
+ * A malformed document does not throw — it comes back as a `ValidationResult`
2835
+ * carrying a single `json_parse_error` schema error, the same envelope the old
2836
+ * string-accepting `validate()` produced.
2837
+ *
2838
+ * The canonical name is `validate_text` (API_SPEC.md §8 item 13). There is no
2839
+ * `validatePath` counterpart in this binding: `@earthsciml/ast` is built for
2840
+ * the browser as well as Node, and the package has no filesystem story on the
2841
+ * public surface for a synchronous read. Read the file yourself and call
2842
+ * `validateText`, passing `basePath` so relative `{ref}` targets still resolve.
2843
+ */
2844
+ declare function validateText(text: string, options?: ValidateOptions): ValidationResult;
2615
2845
 
2616
2846
  /**
2617
2847
  * The esm 1.0.0 classification API (esm-spec §6.3.1).
@@ -2654,13 +2884,30 @@ declare function odeStates(model: Model): string[];
2654
2884
  /** Membership test for {@link odeStates}. */
2655
2885
  declare function isOdeState(model: Model, name: string): boolean;
2656
2886
  /**
2657
- * Unknowns defined by a BARE-VARIABLE LHS (`y ~ f(…)`) — eliminable,
2658
- * materializable. An unknown that is already an ODE state is not observed.
2887
+ * Unknowns an equation DEFINES — its LHS naming them, bare (`y ~ f(…)`) or
2888
+ * indexed (`y[i] ~ f(…)`, which defines the whole array `y`) — eliminable,
2889
+ * materializable (esm-spec §6.3.1). An unknown that is already an ODE state is
2890
+ * not observed.
2891
+ *
2892
+ * The split from {@link algebraicUnknowns} is SEMANTIC, not syntactic: observed
2893
+ * when an equation defines the unknown, algebraic when it is only constrained.
2894
+ * The defining form is read through the LHS's BASE NAME, so an arrayed
2895
+ * definition is observed exactly as its scalar counterpart is; only a genuine
2896
+ * expression LHS, one that names no single variable (`H*H*SO4 ~ Ksp`), is an
2897
+ * implicit constraint. §6.3.1 used to spell the criterion "a bare-variable
2898
+ * LHS", which was written for scalar equations and contradicted the semantic
2899
+ * criterion in the arrayed case.
2900
+ *
2901
+ * Note that *eliminable* is not *inlineable*: a scalar observed is eliminated
2902
+ * by substituting its definition into every consumer, while an arrayed one
2903
+ * materializes into a buffer its consumers index. Both are observed; a caller
2904
+ * that wants the strict inlineable form asks {@link observedDefinitions} for it
2905
+ * with `bareOnly`.
2659
2906
  */
2660
2907
  declare function observedUnknowns(model: Model): string[];
2661
2908
  /**
2662
- * The DEFINING EXPRESSION of every observed unknown: the RHS of the
2663
- * bare-variable-LHS equation whose LHS is that name.
2909
+ * The DEFINING EXPRESSION of every observed unknown: the RHS of the equation
2910
+ * whose LHS names it, bare (`y ~ f(…)`) or indexed (`y[i] ~ f(…)`).
2664
2911
  *
2665
2912
  * This is the 1.0.0 relocation, in one place. An observed unknown's definition
2666
2913
  * used to live in `variables[v].expression`; it now lives in the model's
@@ -2671,11 +2918,14 @@ declare function observedUnknowns(model: Model): string[];
2671
2918
  * Only the first equation for a name is recorded: a second one is an unbalanced
2672
2919
  * system, which {@link validateEquationBalance} reports rather than this.
2673
2920
  */
2674
- declare function observedDefinitions(model: Model): Map<string, Expression>;
2921
+ declare function observedDefinitions(model: Model, options?: {
2922
+ bareOnly?: boolean;
2923
+ }): Map<string, Expression>;
2675
2924
  /**
2676
- * Unknowns constrained only implicitly (`H*H*SO4 ~ Ksp`) — everything left once
2677
- * the ODE states and the observed unknowns are removed. Defining this set by
2678
- * elimination is what makes the three sets a partition by construction.
2925
+ * Unknowns constrained only implicitly — no equation names them on its LHS
2926
+ * (`H*H*SO4 ~ Ksp`). Everything left once the ODE states and the observed
2927
+ * unknowns are removed; defining this set by elimination is what makes the
2928
+ * three sets a partition by construction.
2679
2929
  */
2680
2930
  declare function algebraicUnknowns(model: Model): string[];
2681
2931
  /** The update rules of a parameter, normalized to an array (possibly empty). */
@@ -2717,6 +2967,17 @@ declare function constantParameters(model: Model): string[];
2717
2967
  declare function systemKind(model: Model): SystemKind;
2718
2968
  /** The model's explicit `system_kind` field, or `null` when absent. */
2719
2969
  declare function declaredSystemKind(model: Model): SystemKind | null;
2970
+ /**
2971
+ * The kind a caller choosing a solver actually asks for: the DECLARED
2972
+ * `system_kind` when the document states one, otherwise the DERIVED kind.
2973
+ *
2974
+ * API_SPEC.md §8 item 11 rules the family as three distinct questions plus this
2975
+ * one composition — {@link systemKind} derives, {@link declaredSystemKind}
2976
+ * reads the field, and this composes them as `declared ?? derived`. It never
2977
+ * returns `null`: an undeclared model still has a derived kind. Matches Go's
2978
+ * `EffectiveSystemKind`.
2979
+ */
2980
+ declare function effectiveSystemKind(model: Model): SystemKind;
2720
2981
  /** Every derived set for one model node, as the conformance goldens spell it. */
2721
2982
  interface ModelClassification {
2722
2983
  odeStates: string[];
@@ -2788,7 +3049,7 @@ declare function joinCadence(a: CadenceClass, b: CadenceClass): CadenceClass;
2788
3049
  /** The join of any number of cadence classes; `const` when there are none. */
2789
3050
  declare function joinAll(classes: Iterable<CadenceClass>): CadenceClass;
2790
3051
  /** Raised when the observed-definition chain contains a cycle. */
2791
- declare class CadenceCycleError extends Error {
3052
+ declare class CadenceCycleError extends EsmDiagnosticError {
2792
3053
  readonly cycle: string[];
2793
3054
  constructor(cycle: string[]);
2794
3055
  }
@@ -2907,6 +3168,8 @@ interface ComponentGraph {
2907
3168
  /** All coupling relationships */
2908
3169
  edges: CouplingEdge[];
2909
3170
  }
3171
+ /** Which of the two graphs §4.8 defines. */
3172
+ type GraphKind = 'component' | 'expression';
2910
3173
  /**
2911
3174
  * Directed graph with node/edge lists plus adjacency, predecessor, and
2912
3175
  * successor lookups (ESM Libraries Specification §4.8). Nodes are addressed by
@@ -2914,6 +3177,17 @@ interface ComponentGraph {
2914
3177
  * those keys through `source`/`target`.
2915
3178
  */
2916
3179
  interface Graph<N, E> {
3180
+ /**
3181
+ * Which of the two §4.8 graphs this is. Carried explicitly because the DOT
3182
+ * and Mermaid exporters name the graph in their header
3183
+ * (`digraph ComponentGraph` / `digraph ExpressionGraph`) and an EMPTY graph —
3184
+ * `tests/valid/data_sources_only.esm` produces two — has no node to sniff.
3185
+ * The other bindings get this from their type system (Julia dispatches on
3186
+ * `Graph{ComponentNode,CouplingEdge}`, Python reads `node_type`, Go and Rust
3187
+ * have two distinct structs); TypeScript's `Graph` is one generic interface,
3188
+ * so it carries the tag.
3189
+ */
3190
+ kind?: GraphKind;
2917
3191
  /** All nodes in the graph */
2918
3192
  nodes: N[];
2919
3193
  /** All edges in the graph */
@@ -2939,8 +3213,9 @@ interface VariableNode {
2939
3213
  * categories a consumer of the graph actually wants, recovered from the
2940
3214
  * equations and from each parameter's `distribution` / `update`.
2941
3215
  *
2942
- * `state` is an ODE state, `observed` an unknown with a bare-variable-LHS
2943
- * definition, `algebraic` an implicitly-constrained unknown, `brownian` a
3216
+ * `state` is an ODE state, `observed` an unknown some equation DEFINES (its
3217
+ * LHS naming it, bare or indexed), `algebraic` one that is only implicitly
3218
+ * CONSTRAINED (no equation names it on its LHS), `brownian` a
2944
3219
  * wiener-updated parameter, `discrete` a parameter with any other update,
2945
3220
  * `parameter` a sampled or constant one, and `species` a reaction-system
2946
3221
  * species.
@@ -2956,7 +3231,9 @@ interface VariableNode {
2956
3231
  *
2957
3232
  * The `equation_index` sentinel {@link NON_EQUATION_INDEX} (`-1`) marks a
2958
3233
  * dependency that does not originate from a positionally-numbered equation or
2959
- * reaction — i.e. an observed/expression definition or a coupling variable map.
3234
+ * reaction — a coupling variable map, or the synthetic `expr_result` edges of a
3235
+ * BARE-EXPRESSION target. (An observed unknown's definition IS one of the
3236
+ * model's equations from 1.0.0, so its edges carry that equation's index.)
2960
3237
  */
2961
3238
  interface DependencyEdge {
2962
3239
  /** Source variable name */
@@ -2976,7 +3253,7 @@ interface DependencyEdge {
2976
3253
  /**
2977
3254
  * Position of the equation/reaction that created this dependency, or
2978
3255
  * {@link NON_EQUATION_INDEX} (`-1`) when the dependency has no positional
2979
- * equation (observed-variable definitions and coupling maps).
3256
+ * equation (coupling maps and bare-expression targets).
2980
3257
  */
2981
3258
  equation_index: number;
2982
3259
  /** The expression that created this dependency */
@@ -2988,17 +3265,6 @@ interface DependencyEdge {
2988
3265
  * model components and edges are coupling rules, with adjacency helpers.
2989
3266
  */
2990
3267
  declare function componentGraph(file: EsmFile): Graph<ComponentNode, CouplingEdge>;
2991
- /**
2992
- * Extract the system graph from an ESM file in the legacy `{nodes, edges}`
2993
- * {@link ComponentGraph} shape (edges as {@link CouplingEdge} carrying
2994
- * `from`/`to`).
2995
- *
2996
- * @deprecated Prefer {@link componentGraph}, which returns the richer
2997
- * {@link Graph} with adjacency/predecessor/successor helpers. This snake_case
2998
- * alias is retained because the editor's web-components consume the flat
2999
- * `{nodes, edges}` shape; it will be removed once those callers migrate.
3000
- */
3001
- declare function component_graph(esmFile: EsmFile): ComponentGraph;
3002
3268
  /**
3003
3269
  * Utility to check if a component exists in the ESM file
3004
3270
  */
@@ -3027,10 +3293,24 @@ declare function expressionGraph(target: EsmFile | Model | ReactionSystem | Equa
3027
3293
  * Export graph as Graphviz DOT format.
3028
3294
  * Node shapes: box for models and reaction systems, diamond for operators.
3029
3295
  * Edge styles: solid for compose, dashed for variable_map.
3296
+ *
3297
+ * The header NAMES the graph — `digraph ComponentGraph` / `digraph
3298
+ * ExpressionGraph`. §4.8.3 requires a DOT export and specifies no syntax for it,
3299
+ * so the cross-binding tie-break is the majority: Python, Go, Rust and Julia all
3300
+ * emitted the named form and this binding alone emitted a bare `digraph {`.
3301
+ * `tests/conformance/graph/cases.json` pins it.
3030
3302
  */
3031
3303
  declare function toDot<N extends object, E>(graph: Graph<N, E>): string;
3032
3304
  /**
3033
3305
  * Export graph as Mermaid flowchart format for Markdown embedding.
3306
+ *
3307
+ * The header is `graph TD`. §4.8.3 requires a Mermaid export and specifies no
3308
+ * syntax for it, so the cross-binding tie-break is the majority, applied to the
3309
+ * keyword and the direction independently: `graph` beats `flowchart` 4-1
3310
+ * (Python, Go, Rust and Julia against this binding) and `TD` beats `LR` 3-2
3311
+ * (this binding, Python and Julia against Go and Rust). `graph` is Mermaid's
3312
+ * legacy spelling of `flowchart`; both render, and the majority points at
3313
+ * `graph`. `tests/conformance/graph/cases.json` pins it.
3034
3314
  */
3035
3315
  declare function toMermaid<N extends object, E>(graph: Graph<N, E>): string;
3036
3316
  /**
@@ -3351,13 +3631,13 @@ declare function groupSubexpressionsByType(commonSubexpressions: CommonSubexpres
3351
3631
  * does not cover. Callers that need a boolean answer should use
3352
3632
  * {@link isDifferentiable}.
3353
3633
  */
3354
- declare class NonDifferentiableExpressionError extends Error {
3634
+ declare class NonDifferentiableExpressionError extends EsmDiagnosticError {
3355
3635
  readonly op: string;
3356
3636
  readonly variable: string;
3357
3637
  constructor(op: string, variable: string);
3358
3638
  }
3359
3639
  /** Thrown by {@link higherOrderDerivative} when `order` is not positive. */
3360
- declare class InvalidDerivativeOrderError extends Error {
3640
+ declare class InvalidDerivativeOrderError extends EsmDiagnosticError {
3361
3641
  readonly order: number;
3362
3642
  constructor(order: number);
3363
3643
  }
@@ -3459,13 +3739,6 @@ interface AnalysisOptions {
3459
3739
  * @returns Complete analysis results
3460
3740
  */
3461
3741
  declare function analyzeExpression(target: Expr | Model | EsmFile, options?: AnalysisOptions): AnalysisResults;
3462
- /**
3463
- * Static namespace wrapping {@link analyzeExpression}.
3464
- * @deprecated Call {@link analyzeExpression} directly.
3465
- */
3466
- declare const ExpressionAnalyzer: {
3467
- analyze: typeof analyzeExpression;
3468
- };
3469
3742
 
3470
3743
  /**
3471
3744
  * Pretty-printing formatters for ESM format expressions, equations, models, and files.
@@ -3537,9 +3810,9 @@ declare function toMathML(expr: Expr | Equation | Model | ReactionSystem | React
3537
3810
  * reconstructs as a plain `+` reduction — the join-less `sum_product` annotation
3538
3811
  * (semantically identical there) is not recovered; both reprint identically.
3539
3812
  *
3540
- * Still deferred (need dedicated surface syntax — a later pass): `table_lookup`,
3541
- * `broadcast`, `enum`, and `intersect_polygon` (its `id` field is not printed, so
3542
- * it can't round-trip). Those are refused with an {@link ExpressionParseError}.
3813
+ * Still deferred (need dedicated surface syntax — a later pass): `broadcast` and
3814
+ * `enum`. Those are refused with an {@link ExpressionParseError}, as is the
3815
+ * call spelling `table_lookup(…)` (its real surface is `visc[T=temp]`).
3543
3816
  *
3544
3817
  * Design rules: multiplication is ALWAYS explicit (`k * A`) — no implicit
3545
3818
  * juxtaposition, because identifiers are multi-letter (`NO2`, `O3`, `k_photo`).
@@ -3555,7 +3828,7 @@ declare function toMathML(expr: Expr | Equation | Model | ReactionSystem | React
3555
3828
  */
3556
3829
 
3557
3830
  /** Thrown when an expression string cannot be parsed. */
3558
- declare class ExpressionParseError extends Error {
3831
+ declare class ExpressionParseError extends EsmDiagnosticError {
3559
3832
  /** 0-based character offset into the source where parsing failed. */
3560
3833
  pos: number;
3561
3834
  constructor(message: string,
@@ -3698,7 +3971,7 @@ declare function substituteInReactionSystem(system: ReactionSystem, bindings: Re
3698
3971
  * @param system ReactionSystem to derive ODEs from
3699
3972
  * @returns Model with species as state variables, derived ODEs plus constraints
3700
3973
  */
3701
- declare function deriveODEs(system: ReactionSystem): Model;
3974
+ declare function deriveOdes(system: ReactionSystem): Model;
3702
3975
  /**
3703
3976
  * Compute stoichiometric matrix from a reaction system
3704
3977
  *
@@ -3743,6 +4016,14 @@ declare function substrateMatrix(system: ReactionSystem): number[][];
3743
4016
  * @returns Product stoichiometric matrix
3744
4017
  */
3745
4018
  declare function productMatrix(system: ReactionSystem): number[][];
4019
+ /**
4020
+ * @deprecated Spelling alias for {@link deriveOdes}, kept for one minor per
4021
+ * API_SPEC.md §10. `deriveODEs` violated §2/§2.1: the canonical name is
4022
+ * `derive_odes`, whose TypeScript transliteration is `deriveOdes`, and this
4023
+ * binding already spelled the siblings `odeStates` / `isOdeState` that way.
4024
+ * Removed at the next major.
4025
+ */
4026
+ declare const deriveODEs: typeof deriveOdes;
3746
4027
 
3747
4028
  /**
3748
4029
  * Immutable editing operations for the ESM format
@@ -3754,7 +4035,7 @@ declare function productMatrix(system: ReactionSystem): number[][];
3754
4035
  /**
3755
4036
  * Error thrown when attempting to remove a variable that is still referenced
3756
4037
  */
3757
- declare class VariableInUseError extends Error {
4038
+ declare class VariableInUseError extends EsmDiagnosticError {
3758
4039
  variableName: string;
3759
4040
  references: string[];
3760
4041
  constructor(variableName: string, references: string[]);
@@ -3762,7 +4043,7 @@ declare class VariableInUseError extends Error {
3762
4043
  /**
3763
4044
  * Error thrown when attempting an operation on a non-existent entity
3764
4045
  */
3765
- declare class EntityNotFoundError extends Error {
4046
+ declare class EntityNotFoundError extends EsmDiagnosticError {
3766
4047
  entityType: string;
3767
4048
  entityName: string;
3768
4049
  constructor(entityType: string, entityName: string);
@@ -4016,8 +4297,8 @@ type CompiledExpression = (bindings: Map<string, number>) => number;
4016
4297
  * is open); the gate fires only at evaluation, mirroring the Julia `_compile`
4017
4298
  * gate in tree_walk.jl.
4018
4299
  */
4019
- declare class UnloweredOperatorError extends Error {
4020
- readonly code = "unlowered_operator";
4300
+ declare class UnloweredOperatorError extends EsmDiagnosticError {
4301
+ readonly code: 'unlowered_operator';
4021
4302
  constructor(message: string);
4022
4303
  }
4023
4304
  /**
@@ -4033,8 +4314,7 @@ declare class UnloweredOperatorError extends Error {
4033
4314
  * These `code` values are binding-local diagnostics for the in-process runner,
4034
4315
  * distinct from the cross-language conformance codes in `errors.ts`.
4035
4316
  */
4036
- declare class EvaluatorError extends Error {
4037
- readonly code: string;
4317
+ declare class EvaluatorError extends EsmDiagnosticError {
4038
4318
  constructor(code: string, message: string);
4039
4319
  }
4040
4320
  /**
@@ -4089,7 +4369,7 @@ declare function evaluateExpression(expr: Expr, bindings: Map<string, number>):
4089
4369
  /**
4090
4370
  * Error thrown when migration fails.
4091
4371
  */
4092
- declare class MigrationError extends Error {
4372
+ declare class MigrationError extends EsmDiagnosticError {
4093
4373
  constructor(message: string);
4094
4374
  }
4095
4375
  /**
@@ -4104,7 +4384,14 @@ declare function canMigrate(sourceVersion: string, targetVersion: string): boole
4104
4384
  * - everything else — including EVERY 0.x version, which 1.0.0's clean break
4105
4385
  * puts out of reach of a marker bump — → `[]`.
4106
4386
  */
4107
- declare function getSupportedMigrationTargets(sourceVersion: string): string[];
4387
+ declare function supportedMigrationTargets(sourceVersion: string): string[];
4388
+ /**
4389
+ * @deprecated Use {@link supportedMigrationTargets}. The canonical name is
4390
+ * `supported_migration_targets` (Julia, Python and Go already spell it that
4391
+ * way); the harmonized API drops the `get` prefix. Kept for one minor per
4392
+ * API_SPEC.md §10; removed at the next major.
4393
+ */
4394
+ declare const getSupportedMigrationTargets: typeof supportedMigrationTargets;
4108
4395
  /**
4109
4396
  * Migrate an ESM file from its current schema version to the target version.
4110
4397
  *
@@ -4162,72 +4449,321 @@ declare function isCouplingLibraryDoc(raw: unknown): boolean;
4162
4449
  declare function expandCouplingImports(file: EsmFile, options?: CouplingImportOptions): CouplingEntry[] | undefined;
4163
4450
 
4164
4451
  /**
4165
- * Coupled System Flattening for the ESM format
4166
- *
4167
- * Transforms a multi-system ESM file into a single unified flattened system
4168
- * by namespacing all variables with their source system prefix and processing
4169
- * coupling entries to produce a unified equation set.
4452
+ * Coupled System Flattening for the ESM format (esm-libraries-spec §4.7.5).
4453
+ *
4454
+ * Transforms a multi-system ESM file into a single unified flattened system:
4455
+ * every variable dot-namespaced by its owning component, every coupling rule
4456
+ * resolved INTO the equation set, and the registries a consumer needs
4457
+ * (`index_sets`, `function_tables`, the merged `template_registry`) carried
4458
+ * along so the flattened form is self-describing.
4459
+ *
4460
+ * ## The canonical field set (esm-libraries-spec §4.7.5 step 4, `esm: 1.0.0`)
4461
+ *
4462
+ * Step 4's field table is a CROSS-BINDING CONTRACT, not a suggestion: the
4463
+ * canonical `snake_case` names transliterate to `camelCase` here per
4464
+ * API_SPEC.md §2, and the shared corpus `tests/conformance/flatten/cases.json`
4465
+ * (generated from the Python oracle) pins every field of it, INCLUDING ORDER.
4466
+ *
4467
+ * Three rules from that section drive shapes this module would otherwise get
4468
+ * wrong, and each cost a real defect before it was written down:
4469
+ *
4470
+ * - **The parameter subsets partition `parameters`; they are not siblings of
4471
+ * it.** esm-spec §6.3.1 says `brownian_parameters` / `discrete_parameters` /
4472
+ * `sampled_parameters` / `constant_parameters` *partition the parameters*, so
4473
+ * a `wiener`-updated entry appears in BOTH {@link FlattenedSystem.parameters}
4474
+ * and {@link FlattenedSystem.brownianParameters}. This binding used to
4475
+ * EXCLUDE it (under the old name `brownianVariables`), which made the
4476
+ * parameter vector's LENGTH depend on whether the model happened to be
4477
+ * stochastic and left the four sets partitioning nothing.
4478
+ * - **`algebraicVariables` is a SUBSET of `stateVariables`.** `stateVariables`
4479
+ * is the SOLVED-FOR VECTOR (an implementation axis), not §6.3.1's
4480
+ * classification of the unknowns; a DAE solves for its algebraic unknowns, so
4481
+ * they ride in the vector.
4482
+ * - **`fieldIcs` entries are REMOVED from `equations`.** An initial condition is
4483
+ * a datum, not an equation of motion.
4484
+ *
4485
+ * ## Ordering is observable
4486
+ *
4487
+ * Every ordered map and list below is in DOCUMENT ORDER: components in the order
4488
+ * the file declares them (models first, then reaction systems), and within a
4489
+ * component the order it declares its variables; a coupling-merged entry keeps
4490
+ * the position of its first occurrence. A parameter vector is positional, so
4491
+ * lexicographic sorting or host-map iteration order is NON-CONFORMING. The
4492
+ * `name -> variable` maps are plain objects, whose string-key insertion order
4493
+ * JavaScript preserves.
4494
+ *
4495
+ * The Python binding (`pkg/earthsci-ast-py`) is the flatten oracle; this module
4496
+ * mirrors its semantics function for function.
4170
4497
  */
4171
4498
 
4172
4499
  /** Options for {@link flatten}. Only needed when the file uses `coupling_import`. */
4173
4500
  type FlattenOptions = CouplingImportOptions;
4174
4501
  /**
4175
- * A single equation in the flattened system, with dot-namespaced variable names.
4502
+ * Base class for every error {@link flatten} raises. Mirrors Rust's
4503
+ * `FlattenError` and Python's `FlattenError` for cross-language error-name
4504
+ * parity.
4176
4505
  */
4177
- interface FlattenedEquation {
4178
- /** Dot-namespaced LHS variable name (e.g., "Atmos.O3") */
4179
- lhs: string;
4506
+ declare class FlattenError extends EsmDiagnosticError {
4507
+ constructor(message: string, code?: string);
4508
+ }
4509
+ /**
4510
+ * Two systems define non-additive equations for the same dependent variable —
4511
+ * the §4.7.5 over-determination check. Such a system is over-determined: one
4512
+ * contribution to `d[X]/dt` would silently shadow the other.
4513
+ */
4514
+ declare class ConflictingDerivativeError extends FlattenError {
4515
+ constructor(message: string);
4516
+ }
4517
+ /**
4518
+ * A `couple` connector equation with `transform: "multiplicative"` targets
4519
+ * something with no tendency to multiply.
4520
+ *
4521
+ * esm-spec §10.3 and esm-libraries-spec §4.7.2 define `multiplicative` against
4522
+ * the target's EXISTING ODE right-hand side. When `to` names a parameter, an
4523
+ * observed, an algebraic unknown, or an undefined name, there is no `D(to)` and
4524
+ * the operation has no meaning.
4525
+ *
4526
+ * Silently dropping the connector equation — what this binding did before — is
4527
+ * the one outcome a coupling mis-specification must not have: the document
4528
+ * declares a coupling and the flattened system carries no trace of it.
4529
+ *
4530
+ * `additive` has no counterpart error because zero is the additive identity, so
4531
+ * an additive term against an absent tendency simply becomes the tendency.
4532
+ */
4533
+ declare class CoupleMultiplicativeNoTendencyError extends FlattenError {
4534
+ constructor(message: string);
4535
+ }
4536
+ /**
4537
+ * An `identity`-transform `variable_map` bridges two variables whose declared,
4538
+ * non-empty units differ (esm-libraries-spec §4.7.6). `conversion_factor` and
4539
+ * `param_to_var` are exempt: the first declares the conversion explicitly, the
4540
+ * second does not imply unit equivalence at the mapping site.
4541
+ */
4542
+ declare class DomainUnitMismatchError extends FlattenError {
4543
+ constructor(message: string);
4544
+ }
4545
+ /**
4546
+ * A variable or equation cannot be promoted onto the target grid — raised by the
4547
+ * §10.5 pointwise spatial lift when a species' operator makearrays yield no
4548
+ * full-rank interior-stencil gather.
4549
+ */
4550
+ declare class DimensionPromotionError extends FlattenError {
4551
+ constructor(message: string);
4552
+ }
4553
+ /**
4554
+ * The DERIVED role of a flattened variable (esm-spec §6.3.1) — never a declared
4555
+ * type, which from 1.0.0 is only `unknown` or `parameter`.
4556
+ *
4557
+ * - `state` — solved for: an ODE state, an algebraic unknown, or an arrayed
4558
+ * observed that materializes into a buffer.
4559
+ * - `observed` — an unknown a bare-variable-LHS equation defines, eliminable by
4560
+ * inlining.
4561
+ * - `parameter` — a parameter of any cadence.
4562
+ * - `species` — a reaction-system state (a `state` that came from a species).
4563
+ */
4564
+ type FlattenedVariableRole = 'state' | 'parameter' | 'observed' | 'species';
4565
+ /**
4566
+ * One variable of the flattened system, carrying the COMPLETE declared metadata
4567
+ * step 4 requires ("Full metadata, not names"): a consumer must be able to build
4568
+ * a solver problem from the flattened form alone, without re-reading the source
4569
+ * document.
4570
+ */
4571
+ interface FlattenedVariable {
4572
+ /** Dot-namespaced name, e.g. `"OU.theta"`. */
4573
+ name: string;
4574
+ /** The DERIVED role — see {@link FlattenedVariableRole}. */
4575
+ type: FlattenedVariableRole;
4576
+ units?: string;
4577
+ /** For an unknown, its value at t=0; for a parameter, its constant value. */
4578
+ default?: number;
4579
+ description?: string;
4580
+ /** The namespaced prefix of the component that declared it. */
4581
+ sourceSystem?: string;
4582
+ /** Ordered index-set names for an arrayed variable; absent means scalar. */
4583
+ shape?: string[];
4584
+ /** The declared cadence machinery, carried verbatim (parameters only). */
4585
+ update?: ParameterUpdateSpec;
4586
+ /** The declared sampling law, carried verbatim (parameters only). */
4587
+ distribution?: Distribution;
4588
+ }
4589
+ /**
4590
+ * An insertion-ORDERED map from namespaced name to variable. Order is part of
4591
+ * the cross-binding contract (see the module doc); JavaScript preserves the
4592
+ * insertion order of string keys that do not look like array indices, which a
4593
+ * dot-namespaced variable name never does.
4594
+ */
4595
+ type FlattenedVariableMap = Record<string, FlattenedVariable>;
4596
+ /**
4597
+ * A data-fed PARAMETER lowered to a flattened array input (esm-spec §8.5).
4598
+ *
4599
+ * From 1.0.0 a data source is not a component: a model consumes one by declaring
4600
+ * a parameter whose `update` is `{kind: "data", source, from: {file_variable}}`.
4601
+ * The parameter IS the loaded field and owns the units; this descriptor is what
4602
+ * lets a simulator execute the source at its cadence and bind the resulting
4603
+ * array into the RHS as a read-only input.
4604
+ */
4605
+ interface LoaderField {
4606
+ /** The namespaced parameter symbol, e.g. `"Plume.wind"`. */
4607
+ name: string;
4608
+ /** The owning component's namespaced prefix, e.g. `"Plume"`. */
4609
+ owner: string;
4610
+ /** The `data_sources` key the parameter's `update` names. */
4611
+ subkey: string;
4612
+ /** The source-file variable the binding names. */
4613
+ var: string;
4614
+ /** The resolved `data_sources` entry (carries kind / source / temporal). */
4615
+ dataSource: DataSource;
4180
4616
  /**
4181
- * Expression string with namespaced references.
4182
- *
4183
- * For equations produced from a `couple` connector, the string uses a
4184
- * `transform(expr)` pseudo-function convention: the connector's `transform`
4185
- * (`"additive"` / `"multiplicative"` / `"replacement"`) is emitted as the
4186
- * function name wrapping the (already-scoped) connector expression — e.g.
4187
- * `additive(A.x)`. These are provenance/intent markers, not scalar operators
4188
- * in {@link OPS}; a downstream consumer interprets the wrapper.
4189
- */
4190
- rhs: string;
4191
- /** Name of the source system this equation originated from */
4192
- sourceSystem: string;
4617
+ * Source-seeded cadence (CONFORMANCE_SPEC §5.7.2): a source WITH a `temporal`
4618
+ * block is time-varying (`discrete`); one without is read once (`const`).
4619
+ */
4620
+ cadence: 'const' | 'discrete';
4621
+ /** The binding's declared `unit_conversion` (§8.5), when the document has one. */
4622
+ unitConversion?: Expression;
4193
4623
  }
4194
4624
  /**
4195
- * Metadata describing the origin of the flattened system.
4625
+ * A single equation of the flattened system, with dot-namespaced Expression
4626
+ * TREES.
4627
+ *
4628
+ * ### Breaking change in `esm 1.0.0`
4629
+ *
4630
+ * `lhs` / `rhs` used to be pretty-printed STRINGS produced by a flatten-local,
4631
+ * fully-parenthesizing printer. They are now the canonical Expression AST, which
4632
+ * is what every other binding carries and what the shared corpus pins (rendered
4633
+ * through the shared `toAscii`). A caller that wants text calls `toAscii` on
4634
+ * them — one renderer for the whole toolkit rather than a second, subtly
4635
+ * different one living here.
4196
4636
  */
4637
+ interface FlattenedEquation {
4638
+ /** Dot-namespaced LHS, e.g. `D(Atmos.O3, t)` or a bare `Atmos.total`. */
4639
+ lhs: Expression;
4640
+ /** Dot-namespaced RHS, with coupling contributions merged in. */
4641
+ rhs: Expression;
4642
+ /** Name of the source system this equation originated from. */
4643
+ sourceSystem: string;
4644
+ }
4645
+ /** Metadata describing the origin of the flattened system. */
4197
4646
  interface FlattenMetadata {
4198
- /** Names of all source systems that were flattened */
4647
+ /** Names of all top-level source systems, in document order. */
4199
4648
  sourceSystems: string[];
4200
- /** Human-readable descriptions of coupling rules applied */
4649
+ /** Human-readable descriptions of every coupling rule, in array order. */
4201
4650
  couplingRules: string[];
4651
+ /** `operator_apply` entries, recorded as opaque runtime references. */
4652
+ operatorApplies: string[];
4653
+ /** `callback` entries, recorded as opaque runtime references. */
4654
+ callbacks: string[];
4655
+ }
4656
+ /** A deferred `ic` equation (esm-spec §11.4.1): an initial condition, not dynamics. */
4657
+ interface FieldInitialCondition {
4658
+ /** The namespaced state the condition pins. */
4659
+ state: string;
4660
+ /** The value at t=0. */
4661
+ expr: Expression;
4202
4662
  }
4203
4663
  /**
4204
- * A fully flattened representation of a coupled ESM system.
4664
+ * A fully flattened representation of a coupled ESM system — the canonical
4665
+ * field set of esm-libraries-spec §4.7.5 step 4.
4666
+ *
4667
+ * ### Removed in `esm 1.0.0`
4668
+ *
4669
+ * `variables: Record<string, string>` (observed name -> expression string) is
4670
+ * gone. It predates the canonical shape and duplicated, in a bespoke string
4671
+ * form, information the canonical fields already carry: WHICH unknowns are
4672
+ * observed is {@link observedVariables} (with full metadata rather than a bare
4673
+ * name), and each one's DEFINING EXPRESSION is its equation in
4674
+ * {@link equations}, as an AST. The coupling-derived entries it also collected
4675
+ * are no longer a side table: a `variable_map` now performs the substitution and
4676
+ * the promotion the spec calls for, so its effect is visible in `equations` and
4677
+ * `parameters` instead. Nothing it held is lost.
4678
+ *
4679
+ * Python exposes a `variables` property with a DIFFERENT meaning (namespaced
4680
+ * name -> role label); this binding deliberately does not reuse the name for a
4681
+ * third thing.
4205
4682
  */
4206
4683
  interface FlattenedSystem {
4207
- /** All state variable names (dot-namespaced) */
4208
- stateVariables: string[];
4209
- /** All parameter names (dot-namespaced) */
4210
- parameters: string[];
4211
- /** All brownian (Wiener) noise variables (dot-namespaced). Any brownian => SDE system. */
4212
- brownianVariables: string[];
4213
- /** Observed/derived variables: namespaced name -> expression string */
4214
- variables: Record<string, string>;
4215
- /** All equations from all systems, with namespaced references */
4684
+ /**
4685
+ * Always contains `"t"`. A spatial axis appears only while an UNDISCRETIZED
4686
+ * spatial differential still names it, so a discretized (array) system stays a
4687
+ * pure ODE.
4688
+ */
4689
+ independentVariables: string[];
4690
+ /**
4691
+ * The SOLVED-FOR VECTOR: every unknown the solver advances or solves for —
4692
+ * differential unknowns, PLUS {@link algebraicVariables}, PLUS any arrayed
4693
+ * observed that materializes into a buffer. NOT esm-spec §6.3.1's `odeStates`.
4694
+ */
4695
+ stateVariables: FlattenedVariableMap;
4696
+ /** ALL parameters of every cadence, minus any promoted by `variable_map`. */
4697
+ parameters: FlattenedVariableMap;
4698
+ /** Unknowns DEFINED by an equation naming them on its LHS (esm-spec §6.3.1). */
4699
+ observedVariables: FlattenedVariableMap;
4700
+ /**
4701
+ * Unknowns CONSTRAINED only by an expression-LHS equation. A SUBSET of
4702
+ * {@link stateVariables} — a DAE solves for them.
4703
+ */
4704
+ algebraicVariables: FlattenedVariableMap;
4705
+ /**
4706
+ * Parameters whose `update.kind` is `"wiener"` — the SDE noise sources. A
4707
+ * SUBSET of {@link parameters}, not a sibling bucket (esm-spec §6.3.1).
4708
+ */
4709
+ brownianParameters: FlattenedVariableMap;
4710
+ /** Parameters carrying any OTHER `update`. A SUBSET of {@link parameters}. */
4711
+ discreteParameters: FlattenedVariableMap;
4712
+ /**
4713
+ * The governing equations — dynamics and constraints — coupling applied,
4714
+ * dot-namespaced. Entries classified out into {@link fieldIcs} are REMOVED.
4715
+ */
4216
4716
  equations: FlattenedEquation[];
4217
- /** Provenance metadata */
4717
+ /** Continuous events, dot-namespaced. */
4718
+ continuousEvents: ContinuousEvent[];
4719
+ /** Discrete events, dot-namespaced. */
4720
+ discreteEvents: DiscreteEvent[];
4721
+ /** The file's `domain` section, unchanged, or `null`. */
4722
+ domain: Domain | null;
4723
+ /** Provenance metadata. */
4218
4724
  metadata: FlattenMetadata;
4725
+ /** Document-scoped index-set registry; required to interpret arrayed equations. */
4726
+ indexSets: Record<string, unknown>;
4727
+ /** Merged function-table registry; resolves `table_lookup`. */
4728
+ functionTables: Record<string, unknown>;
4729
+ /**
4730
+ * The MERGED expression-template registry (esm-spec §9.6.4 rule 7, §10.7):
4731
+ * the union of the component registries with their bodies component-scoped
4732
+ * FIRST, deep-equal same-name entries deduplicated at first occurrence, and
4733
+ * non-deep-equal collisions renamed along the reference DAG.
4734
+ */
4735
+ templateRegistry: Record<string, unknown>;
4736
+ /** Deferred scoped-reference / array `ic` equations (esm-spec §11.4.1). */
4737
+ fieldIcs: FieldInitialCondition[];
4738
+ /** Provider-served loaded fields the system consumes. */
4739
+ loaderFields: LoaderField[];
4740
+ /** Post-lift grid shapes for arrayed states, e.g. `{ "Chemistry.O3": [4, 2] }`. */
4741
+ liftedShapes: Record<string, number[]>;
4742
+ /**
4743
+ * The system kind DERIVED from the FLATTENED system (esm-spec §6.3.1),
4744
+ * testing `brownianParameters` FIRST — which is exactly why that bucket must
4745
+ * survive flattening.
4746
+ */
4747
+ systemKind: SystemKind;
4219
4748
  }
4220
4749
  /**
4221
- * Flatten a multi-system ESM file into a single unified system.
4750
+ * Flatten a coupled multi-system ESM file into a single unified system
4751
+ * (esm-libraries-spec §4.7.5).
4222
4752
  *
4223
- * The algorithm:
4224
- * 1. Iterates over all models and reaction_systems in the file
4225
- * 2. Namespaces all variables with their system name prefix (dot notation)
4226
- * 3. Processes coupling entries to produce variable mappings and connector equations
4227
- * 4. Returns a unified flattened system
4753
+ * The result is the canonical intermediate representation: dot-namespaced
4754
+ * variables carrying their full declared metadata, equations as Expression trees,
4755
+ * coupling rules resolved INTO the equation set, and the registries needed to
4756
+ * consume it without re-reading the source document.
4228
4757
  *
4229
- * @param file - The ESM file to flatten
4230
- * @returns A FlattenedSystem with all variables namespaced and equations unified
4758
+ * @throws {FlattenError} when the file has no models and no reaction systems, or
4759
+ * when a variable carries a type esm 1.0.0 removed.
4760
+ * @throws {ConflictingDerivativeError} when two source systems define
4761
+ * non-additive equations for the same dependent variable.
4762
+ * @throws {CoupleMultiplicativeNoTendencyError} when a `couple` connector
4763
+ * equation with `transform: "multiplicative"` targets something with no
4764
+ * `D(to)` tendency to multiply (esm-spec §10.3).
4765
+ * @throws {DomainUnitMismatchError} when an `identity` `variable_map` bridges two
4766
+ * variables whose declared, non-empty units differ.
4231
4767
  */
4232
4768
  declare function flatten(file: EsmFile, options?: FlattenOptions): FlattenedSystem;
4233
4769
 
@@ -4255,7 +4791,7 @@ declare function flatten(file: EsmFile, options?: FlattenOptions): FlattenedSyst
4255
4791
  /**
4256
4792
  * Error thrown when a circular reference is detected during subsystem resolution.
4257
4793
  */
4258
- declare class CircularReferenceError extends Error {
4794
+ declare class CircularReferenceError extends EsmDiagnosticError {
4259
4795
  /** The chain of references that form the cycle */
4260
4796
  readonly chain: string[];
4261
4797
  constructor(chain: string[]);
@@ -4275,7 +4811,7 @@ declare class CircularReferenceError extends Error {
4275
4811
  * only layer that can tell those two apart — the synchronous `validate()` does
4276
4812
  * no I/O and reports every unresolved `{ref}` as `unresolved_subsystem_ref`.
4277
4813
  */
4278
- declare class RefLoadError extends Error {
4814
+ declare class RefLoadError extends EsmDiagnosticError {
4279
4815
  /** The reference path or URL that failed to load */
4280
4816
  readonly ref: string;
4281
4817
  /** Canonical code: `unresolved_subsystem_ref` or `ambiguous_subsystem_ref`. */
@@ -4321,6 +4857,181 @@ declare function resolveSubsystemRefs(file: EsmFile, basePath: string): Promise<
4321
4857
  */
4322
4858
  declare function ephemeralInjectedFile(file: EsmFile | null, sourcePath: string | null, mname: string, imports: readonly unknown[], baseDir: string): Promise<EsmFile>;
4323
4859
 
4860
+ /**
4861
+ * Build-time reference resolution for the semiring-FAQ unified IR: the
4862
+ * intra-document node-id / index-set dependency DAG.
4863
+ *
4864
+ * Not to be confused with `./ref-loading.js`, which inlines cross-file
4865
+ * `{ "ref": ... }` subsystem mounts at load time — this module never touches
4866
+ * the filesystem; it wires id-addressed edges inside ONE document.
4867
+ *
4868
+ * It implements *node addressing* and *reference-edge resolution* — the hard
4869
+ * prerequisite the §6.1 cadence-partition pass of the
4870
+ * `semiring-faq-unified-ir` RFC calls out:
4871
+ *
4872
+ * > "node addressing — referencing a node by id — is a hard prerequisite: the
4873
+ * > pass cannot be built until `from_faq` and join references are real edges
4874
+ * > in this DAG."
4875
+ *
4876
+ * Three kinds of name/id reference become real, queryable graph edges
4877
+ * (RFC §6.1 "Propagation"):
4878
+ *
4879
+ * - an aggregate node → an index set it iterates (`ranges[*].from`);
4880
+ * - a `kind: "derived"` index set → its `from_faq` node (by stable id);
4881
+ * - an aggregate `join.on` factor → the factor it names.
4882
+ *
4883
+ * Like the Julia, Python, Rust and Go passes, this one walks the RAW parsed
4884
+ * document rather than the typed layer: `index_sets`, node `id`,
4885
+ * `ranges[*].from` and `join` are exactly the fields the typed layer drops, so
4886
+ * working the raw shape is what keeps the five bindings in step.
4887
+ *
4888
+ * PORTED FROM the Python binding (`earthsci_ast/reference_resolution.py`),
4889
+ * which is the most complete of the four: it registers every node BEFORE
4890
+ * resolving any reference (a two-step walk), so a `join.on` or `from_faq` may
4891
+ * name a node that appears later in the document. Rust's single-pass
4892
+ * `register_and_process` cannot do that. Vertex keys, edge kinds, error codes
4893
+ * and message shapes follow Python byte for byte.
4894
+ */
4895
+
4896
+ /**
4897
+ * A reference could not be resolved, or the reference graph has a cycle.
4898
+ *
4899
+ * Carries the stable `code` (one of the `E_REF_*` constants above) so callers
4900
+ * and the cross-binding conformance suite can assert on the failure mode. For a
4901
+ * cycle, `cycle` holds the offending vertex-key path.
4902
+ */
4903
+ declare class ReferenceResolutionError extends EsmDiagnosticError {
4904
+ code: string;
4905
+ readonly cycle: string[] | undefined;
4906
+ constructor(code: string, message: string, cycle?: string[]);
4907
+ }
4908
+ /** The three kinds of vertex in the reference graph. */
4909
+ declare const VertexKind: {
4910
+ readonly NODE: "node";
4911
+ readonly INDEX_SET: "index_set";
4912
+ readonly FACTOR: "factor";
4913
+ };
4914
+ type VertexKind = (typeof VertexKind)[keyof typeof VertexKind];
4915
+ /** The three kinds of reference edge (RFC §6.1 "Propagation"). */
4916
+ declare const EdgeKind: {
4917
+ /** aggregate node → the index set it iterates (`ranges[*].from`). */
4918
+ readonly RANGE_FROM: "range_from";
4919
+ /** `kind: "derived"` index set → the node that materializes it (`from_faq`). */
4920
+ readonly FROM_FAQ: "from_faq";
4921
+ /** aggregate node → a factor named by `join.on`. */
4922
+ readonly JOIN_FACTOR: "join_factor";
4923
+ };
4924
+ type EdgeKind = (typeof EdgeKind)[keyof typeof EdgeKind];
4925
+ /**
4926
+ * A vertex in the reference graph, addressed by a kind-namespaced `key`.
4927
+ *
4928
+ * `key` is `` `${kind}:${name}` ``. For a `node` vertex, `name` is the node's
4929
+ * stable address: its explicit `id` when it has one, else its structural path
4930
+ * (e.g. `equations/0/rhs/expr`).
4931
+ */
4932
+ interface ReferenceVertex {
4933
+ key: string;
4934
+ kind: VertexKind;
4935
+ name: string;
4936
+ op?: string;
4937
+ nodeId?: string;
4938
+ path?: string;
4939
+ }
4940
+ /** A directed `source → target` edge: *source references / depends on target*. */
4941
+ interface ReferenceEdge {
4942
+ source: string;
4943
+ target: string;
4944
+ kind: EdgeKind;
4945
+ }
4946
+ type RawObject = Record<string, unknown>;
4947
+ /**
4948
+ * The resolved reference DAG for one model — the partition pass's input.
4949
+ *
4950
+ * Vertices are keyed by their kind-namespaced `key`. Edges point from a vertex
4951
+ * to a vertex it *depends on*, so a bottom-up {@link topologicalOrder} walk
4952
+ * visits each vertex after its dependencies — exactly the order
4953
+ * `class(n) = max(class(inputs))` propagation needs.
4954
+ */
4955
+ declare class ReferenceGraph {
4956
+ readonly model: string;
4957
+ /** Insertion-ordered `key -> vertex`. */
4958
+ readonly vertices: Map<string, ReferenceVertex>;
4959
+ readonly edges: ReferenceEdge[];
4960
+ private readonly out;
4961
+ private readonly inn;
4962
+ constructor(model?: string);
4963
+ /** @internal */
4964
+ ensureVertex(vertex: ReferenceVertex): void;
4965
+ /** @internal */
4966
+ addEdge(source: string, target: string, kind: EdgeKind): void;
4967
+ /** Vertices `key` references / depends on (its out-neighbours). */
4968
+ dependencies(key: string): string[];
4969
+ /** Vertices that reference / depend on `key` (its in-neighbours). */
4970
+ dependents(key: string): string[];
4971
+ edgesOfKind(kind: EdgeKind): ReferenceEdge[];
4972
+ /**
4973
+ * A reference cycle as a vertex-key path, or `null` if acyclic.
4974
+ *
4975
+ * Three-colour DFS over the dependency edges, traversing sorted keys and
4976
+ * sorted neighbours so the reported path is deterministic and matches the
4977
+ * other bindings. The returned path is `[v, …, v]` (the repeated vertex
4978
+ * closes the cycle).
4979
+ */
4980
+ detectCycle(): string[] | null;
4981
+ /**
4982
+ * Bottom-up order (dependencies before dependents).
4983
+ *
4984
+ * Throws {@link ReferenceResolutionError} (`E_REF_CYCLE`) if the graph is
4985
+ * cyclic — a cycle among reference edges is an out-of-scope
4986
+ * implicit/iterative solve (RFC §6.1 "Acyclicity").
4987
+ */
4988
+ topologicalOrder(): string[];
4989
+ }
4990
+ /**
4991
+ * Resolve the reference edges of ONE `model` into a {@link ReferenceGraph}.
4992
+ *
4993
+ * @param model - the raw model object (as parsed, not the typed layer)
4994
+ * @param modelName - the model's key in the document, used in diagnostics
4995
+ * @param indexSets - the DOCUMENT-SCOPED `index_sets` registry. `esm-schema.json`
4996
+ * declares `index_sets` only at document scope: since v0.8.0 it is a sibling
4997
+ * of `models` rather than nested inside each model, and 1.0.0 keeps it there.
4998
+ * {@link resolveReferences} reads it once from the document root and threads
4999
+ * it into every model, which is how a caller normally arrives here. It is an
5000
+ * OPTIONAL TRAILING argument — not a separate function, and not required
5001
+ * (API_SPEC.md §8 item 17). A model-nested `index_sets` key (pre-0.8.0) is
5002
+ * merged ON TOP, so a model-level entry wins a key collision, matching Go and
5003
+ * Rust exactly. Reading ONLY the nested key is the Julia bug §8 item 17
5004
+ * records — it threw on any 1.0.0 document with a top-level registry — and
5005
+ * this binding does not reproduce it.
5006
+ *
5007
+ * Throws {@link ReferenceResolutionError} on a duplicate node id, an undeclared
5008
+ * `ranges[*].from` index set, a `from_faq` naming no node, or an unresolved
5009
+ * `join.on` factor. Cycles are reported lazily by
5010
+ * {@link ReferenceGraph.topologicalOrder}, or eagerly by
5011
+ * {@link resolveReferences}.
5012
+ *
5013
+ * `from_faq` resolves at DOCUMENT scope (esm-spec.md §9.7.5). A caller holding
5014
+ * one model gets that model as its own document, which is the right answer for
5015
+ * a one-model document; {@link resolveReferences} is the document-scoped entry
5016
+ * point and resolves against every model's nodes.
5017
+ */
5018
+ declare function buildReferenceGraph(model: Model | RawObject, modelName?: string, indexSets?: Record<string, unknown>): ReferenceGraph;
5019
+ /**
5020
+ * Resolve reference edges for EVERY model in `document`.
5021
+ *
5022
+ * Returns a `modelName -> ReferenceGraph` map. Throws
5023
+ * {@link ReferenceResolutionError} on any unresolved reference *or* reference
5024
+ * cycle (each model's graph is checked acyclic eagerly here).
5025
+ *
5026
+ * The document-scoped `index_sets` registry is read once from the top level and
5027
+ * threaded into every model, so a caller never assembles it by hand.
5028
+ *
5029
+ * Node ids are collected from EVERY model first, so a derived index set's
5030
+ * `from_faq` may name a producer in any model (esm-spec.md §9.7.5) and a
5031
+ * duplicate id anywhere in the document is an error.
5032
+ */
5033
+ declare function resolveReferences(document: EsmFile | RawObject): Map<string, ReferenceGraph>;
5034
+
4324
5035
  /**
4325
5036
  * Canonical AST form per discretization RFC §5.4.
4326
5037
  *
@@ -4356,7 +5067,7 @@ declare function canonicalJson(expr: Expr): string;
4356
5067
  * emitted as bare JSON integers by {@link canonicalJson} directly.
4357
5068
  *
4358
5069
  * NAMING: distinct from `formatFloatToken` in `./numeric-literal` (the
4359
- * document-serialization emitter used by `save()` / `losslessJsonStringify`).
5070
+ * document-serialization emitter used by `toJson()` / `losslessJsonStringify`).
4360
5071
  * This canonical version additionally NORMALIZES exponent notation — it strips
4361
5072
  * the leading `+` (RFC §5.4.6: "no leading + on the exponent") so `1e25` emits
4362
5073
  * as `1e25`, not `1e+25`, and forces the §5.4.6 exponent thresholds. Use this
@@ -4381,13 +5092,14 @@ declare function formatCanonicalFloat(f: number): string;
4381
5092
  * reference implementation (pkg/EarthSciAST.jl/src/registered_functions.jl).
4382
5093
  */
4383
5094
  /** Stable diagnostic codes raised by the registry. */
5095
+
4384
5096
  type ClosedFunctionErrorCode = 'unknown_closed_function' | 'closed_function_arity' | 'closed_function_overflow' | 'searchsorted_non_monotonic' | 'searchsorted_nan_in_table' | 'interp_non_monotonic_axis' | 'interp_axis_length_mismatch' | 'interp_nan_in_axis' | 'interp_axis_too_short' | 'interp_table_not_const' | 'interp_axis_not_const';
4385
5097
  /**
4386
5098
  * Error thrown by closed function dispatch and load-time table validation.
4387
5099
  * `code` identifies the spec-pinned diagnostic; cross-binding harnesses
4388
5100
  * compare against this exact string.
4389
5101
  */
4390
- declare class ClosedFunctionError extends Error {
5102
+ declare class ClosedFunctionError extends EsmDiagnosticError {
4391
5103
  code: ClosedFunctionErrorCode;
4392
5104
  constructor(code: ClosedFunctionErrorCode, message: string);
4393
5105
  }
@@ -4427,7 +5139,19 @@ declare function interpLinear(table: readonly number[], axis: readonly number[],
4427
5139
  * `table[i][j]` is the value at `(axis_x[i], axis_y[j])`.
4428
5140
  */
4429
5141
  declare function interpBilinear(table: readonly (readonly number[])[], axisX: readonly number[], axisY: readonly number[], x: number, y: number): number;
4430
- /** Names that bindings MUST recognize (derived from the dispatch table keys). */
5142
+ /**
5143
+ * Names that bindings MUST recognize (derived from the dispatch table keys).
5144
+ *
5145
+ * A FUNCTION, matching Julia's `closed_function_names()`, Rust's
5146
+ * `closed_function_names()` and Go's `ClosedFunctionNames()` — API_SPEC.md §8
5147
+ * item 4. TypeScript alone exposed this as a constant array, so a caller
5148
+ * writing binding-agnostic code had to special-case it.
5149
+ */
5150
+ declare function closedFunctionNames(): readonly string[];
5151
+ /**
5152
+ * @deprecated Use {@link closedFunctionNames}. Kept for one minor per
5153
+ * API_SPEC.md §10; removed at the next major.
5154
+ */
4431
5155
  declare const CLOSED_FUNCTION_NAMES: readonly string[];
4432
5156
  /**
4433
5157
  * Resolve a closed-function name + already-evaluated positional args into a
@@ -4460,7 +5184,7 @@ declare function dispatchClosedFunction(name: string, args: unknown[]): number;
4460
5184
  * enum.
4461
5185
  */
4462
5186
 
4463
- declare class EnumLoweringError extends Error {
5187
+ declare class EnumLoweringError extends EsmDiagnosticError {
4464
5188
  code: string;
4465
5189
  constructor(code: string, message: string);
4466
5190
  }
@@ -4471,19 +5195,6 @@ declare class EnumLoweringError extends Error {
4471
5195
  */
4472
5196
  declare function lowerEnums(file: EsmFile): EsmFile;
4473
5197
 
4474
- /**
4475
- * Neutral base for EarthSciAST diagnostics: an `Error` carrying a stable `code`
4476
- * string (from {@link ERROR_CODES}) and optional structured `details`. This is
4477
- * purely additive — a single home for future diagnostics. It intentionally does
4478
- * NOT touch the existing `EsmMachineryError` / `EnumLoweringError` /
4479
- * `ClosedFunctionError` classes, which another file owns.
4480
- */
4481
- declare class EsmDiagnosticError extends Error {
4482
- readonly code: string;
4483
- readonly details?: Record<string, unknown> | undefined;
4484
- constructor(code: string, message: string, details?: Record<string, unknown> | undefined);
4485
- }
4486
-
4487
5198
  /**
4488
5199
  * Load-time rewrite pass for `expression_templates` (esm-spec §9.6 /
4489
5200
  * docs/rfcs/ast-expression-templates.md, docs/rfcs/open-op-namespace-fixpoint-rewrite.md).
@@ -4697,7 +5408,7 @@ interface TemplateResolveOptions {
4697
5408
  /**
4698
5409
  * Schema validator applied to each import target (a target failing schema
4699
5410
  * validation is `template_import_unresolved`, mirroring the Julia
4700
- * reference). Supplied by `load()`; optional for direct/raw use.
5411
+ * reference). Supplied by the `load*` entry points; optional for direct/raw use.
4701
5412
  */
4702
5413
  validateSchema?: ((raw: unknown) => TemplateSchemaError[]) | undefined;
4703
5414
  }
@@ -4754,5 +5465,20 @@ declare function emitDocument(rawSource: unknown, basePath: string, options?: Te
4754
5465
  */
4755
5466
  declare function applyScopeInjections(raw: unknown, injected?: readonly unknown[]): JsonObject | null;
4756
5467
 
4757
- export { CLOSED_FUNCTION_NAMES, CadenceCycleError, CadenceSeeder, CanonicalNonfiniteError, CanonicalizeError, CircularReferenceError, ClosedFunctionError, DEFAULT_MIN_COMPLEXITY, E_CANONICAL_DIVBY_ZERO, E_CANONICAL_NONFINITE, EntityNotFoundError, EnumLoweringError, EsmMachineryError, EvaluatorError, expandDocument as Expand, ExpressionAnalyzer, ExpressionParseError, EsmMachineryError as ExpressionTemplateError, InvalidDerivativeOrderError, LosslessJsonParseError, MAX_TEMPLATE_EXPANSION_DEPTH, MigrationError, NonDifferentiableExpressionError, ParseError, RefLoadError, SCHEMA_VERSION, SchemaValidationError, UnitConversionError, UnloweredOperatorError, SCHEMA_VERSION as VERSION, VariableInUseError, addContinuousEvent, addCoupling, addDiscreteEvent, addEquation, addReaction, addSpecies, addVariable, algebraicUnknowns, analyzeComplexity, analyzeExpression, applyScopeInjections, authoredTemplateNames, brownianParameters, buildDependencyGraph, buildEmittedDocument, canMigrate, canonicalJson, canonicalize, checkDimensions, classifyComplexity, classifyDocument, classifyModel, compareComplexity, compileExpression, componentExists, componentGraph, component_graph, compose, constantParameters, contains, convertUnits, declaredSystemKind, deriveODEs, detectStabilityIssues, differentiate, discreteParameters, dispatchClosedFunction, emitDocument, emitEsmString, ephemeralInjectedFile, estimateParallelPotential, estimateSavings, evaluateExpression, expandCouplingImports, expandDocument, expressionCadence, expressionGraph, extract, findCommonSubexpressions, findCommonSubexpressionsAcrossExpressions, findCommonSubexpressionsInEsmFile, findCommonSubexpressionsInModel, findCriticalPoints, findDeadVariables, findDependencyChains, findExpensiveSubexpressions, flatten, flattenTemplateRegistries, floatLit, formatCanonicalFloat, formatChemicalName, formatFloatToken, freeParameters, freeVariables, generateFactoredVariableNames, getComponentType, getSupportedMigrationTargets, gradient, groupSubexpressionsByType, higherOrderDerivative, intLit, interpBilinear, interpLinear, isCouplingLibraryDoc, isDifferentiable, isFloatLit, isIntLit, isNumericLiteral, isOdeState, isTemplateLibraryDoc, joinAll, joinCadence, leafCadence, load, losslessJsonParse, losslessJsonStringify, lowerEnums, lowerExpressionTemplates, mapVariable, merge, migrate, numericValue, observedDefinitions, observedUnknowns, odeStates, parameterClass, parameters, parseEquation, parseExpression, parseReaction, parseUnit, parseUnitForConversion, partialDerivatives, productMatrix, rejectExpressionTemplatesPreV04, rejectTemplateImportsPreV08, removeCoupling, removeEquation, removeEvent, removeReaction, removeSpecies, removeVariable, renameVariable, resolveSubsystemRefs, resolveTemplateMachinery, sampledParameters, save, searchsortedFirst, simplify, stoichiometricMatrix, substitute, substituteInEquations, substituteInModel, substituteInReactionSystem, substrateMatrix, systemKind, toAscii, toDot, toJsonGraph, toLatex, toMathML, toMermaid, toUnicode, tryParseUnit, unitsCompatible, unknowns, updateRules, validate, validateInterpAxis, validateSchema, validateSearchsortedTable, validateUnits };
4758
- export type { AffectEquation, Analysis, AnalysisOptions, AnalysisResults, Assertion, Assertion1, CadenceClass, CanonicalDims, ClosedFunctionErrorCode, CommonSubexpression, CompiledExpression, ComplexityMetrics, ComponentGraph, ComponentNode, ConditionUpdate, ConnectorEquation, ContinuousEvent, Coordinate, Coordinate1, CouplingCallback, CouplingCouple, CouplingEdge, CouplingEntry, CouplingEvent, CouplingEvent1, CouplingImport, CouplingImportOptions, CouplingOperatorCompose, CouplingVariableMap, CouplingVariableMap1, CovarianceMatrix, CrossingUpdate, DataSource, DataSourceBinding, DataSourceCodes, DataSourceDeterminism, DataSourceExtent, DataSourceLocation, DataSourceRecordFilter, DataSourceSelect, DataSourceSelectAxis, DataSourceTemporal, DataUpdate, DependencyEdge, DependencyGraph, DependencyNode, DependencyRelation, DerivativeResult, DiscreteEvent, DiscreteEventTrigger, Distribution, Domain, ESMFormat, ESMFormat1, ESMFormat2, EnumDeclaration, Equation, EsmFile, EsmFormat, Expr, ExprNode, ExprNodeOf, Expression, ExpressionLocation, ExpressionNode, ExpressionNode1, ExpressionTemplate, FlattenMetadata, FlattenOptions, FlattenedEquation, FlattenedSystem, FlattenedTemplateRegistries, FunctionTable, FunctionTableAxis, FunctionalUpdate, Graph, IndexSet, IndexSet1, LoadOptions, LognormalDistribution, Metadata, MetaparameterExpression, Metaparameters, Model, ModelClassification, ModelVariable, ModelVariable1, NonWienerParameterUpdate, NormalDistribution, NumericLiteral, Parameter, Parameter1, ParameterClass, ParameterSweep, ParameterUpdate, ParameterUpdateSpec, ParsedUnit, Plot, Plot1, PlotAxis, PlotSeries, PlotValue, Reaction, ReactionSystem, Reference, RemeshUpdate, SaveOptions, ScheduleUpdate, SchemaError, Species, StabilityIssue, StoichiometryEntry, SubsystemRef, SweepDimension, SweepDimension1, SweepRange, SystemKind, TemplateImport, TemplateResolveOptions, TemplateSchemaError, Test, TimeSpan, Tolerance, Tolerance1, Tolerance2, Tolerance3, TranslateTarget, UniformDistribution, UnitResult, UnitWarning, UnknownClass, UpdateValueForm, ValidationError, ValidationResult, VariableKind, VariableNode, WienerUpdate };
5468
+ /**
5469
+ * The package's OWN version — distinct from {@link SCHEMA_VERSION}, which is
5470
+ * the `.esm` FORMAT version this build implements. The two are unrelated
5471
+ * numbers and used to be conflated: `VERSION` meant the schema version here
5472
+ * and the package version in Rust, so the same name read two different
5473
+ * things depending on which binding you were in. `VERSION` is gone; every
5474
+ * binding now exposes exactly `SCHEMA_VERSION` and `LIBRARY_VERSION`.
5475
+ *
5476
+ * package.json is the source of truth. It cannot be imported here —
5477
+ * tsconfig's `rootDir` is `./src`, and a runtime read would break browser
5478
+ * hosts — so the value is mirrored, and `version.test.ts` fails the build if
5479
+ * the mirror drifts. Same arrangement Rust uses for its `SCHEMA_VERSION`.
5480
+ */
5481
+ declare const LIBRARY_VERSION = "0.1.1";
5482
+
5483
+ export { CLOSED_FUNCTION_NAMES, CadenceCycleError, CadenceSeeder, CanonicalNonfiniteError, CanonicalizeError, CircularReferenceError, ClosedFunctionError, ConflictingDerivativeError, CoupleMultiplicativeNoTendencyError, DEFAULT_MIN_COMPLEXITY, DimensionPromotionError, DomainUnitMismatchError, ERROR_CODES, E_CANONICAL_DIVBY_ZERO, E_CANONICAL_NONFINITE, EdgeKind, EntityNotFoundError, EnumLoweringError, EsmDiagnosticError, EsmMachineryError, EvaluatorError, expandDocument as Expand, ExpressionParseError, EsmMachineryError as ExpressionTemplateError, FlattenError, InvalidDerivativeOrderError, LIBRARY_VERSION, LosslessJsonParseError, MAX_TEMPLATE_EXPANSION_DEPTH, MigrationError, NonDifferentiableExpressionError, ParseError, RefLoadError, ReferenceGraph, ReferenceResolutionError, SCHEMA_VERSION, SchemaValidationError, UnitConversionError, UnloweredOperatorError, VariableInUseError, VertexKind, addContinuousEvent, addCoupling, addDiscreteEvent, addEquation, addReaction, addSpecies, addVariable, algebraicUnknowns, analyzeComplexity, analyzeExpression, applyScopeInjections, authoredTemplateNames, brownianParameters, buildDependencyGraph, buildEmittedDocument, buildReferenceGraph, canMigrate, canonicalJson, canonicalize, checkDimensions, classifyComplexity, classifyDocument, classifyModel, closedFunctionNames, compareComplexity, compileExpression, componentExists, componentGraph, compose, constantParameters, contains, convertUnits, declaredSystemKind, deriveODEs, deriveOdes, detectStabilityIssues, differentiate, discreteParameters, dispatchClosedFunction, effectiveSystemKind, emitDocument, emitEsmString, ephemeralInjectedFile, estimateParallelPotential, estimateSavings, evaluateExpression, expandCouplingImports, expandDocument, expressionCadence, expressionGraph, extract, findCommonSubexpressions, findCommonSubexpressionsAcrossExpressions, findCommonSubexpressionsInEsmFile, findCommonSubexpressionsInModel, findCriticalPoints, findDeadVariables, findDependencyChains, findExpensiveSubexpressions, flatten, flattenTemplateRegistries, floatLit, formatCanonicalFloat, formatChemicalName, formatFloatToken, freeParameters, freeVariables, generateFactoredVariableNames, getComponentType, getSupportedMigrationTargets, gradient, groupSubexpressionsByType, higherOrderDerivative, intLit, interpBilinear, interpLinear, isCouplingLibraryDoc, isDifferentiable, isFloatLit, isIntLit, isNumericLiteral, isOdeState, isTemplateLibraryDoc, joinAll, joinCadence, leafCadence, loadDocument, loadPath, loadString, losslessJsonParse, losslessJsonStringify, lowerEnums, lowerExpressionTemplates, mapVariable, merge, migrate, numericValue, observedDefinitions, observedUnknowns, odeStates, parameterClass, parameters, parseEquation, parseExpression, parseReaction, parseUnit, parseUnitForConversion, partialDerivatives, productMatrix, rejectExpressionTemplatesPreV04, rejectTemplateImportsPreV08, removeCoupling, removeEquation, removeEvent, removeReaction, removeSpecies, removeVariable, renameVariable, resolveReferences, resolveSubsystemRefs, resolveTemplateMachinery, sampledParameters, searchsortedFirst, simplify, stoichiometricMatrix, substitute, substituteInEquations, substituteInModel, substituteInReactionSystem, substrateMatrix, supportedMigrationTargets, systemKind, toAscii, toDot, toJson, toJsonCompact, toJsonGraph, toLatex, toMathML, toMermaid, toUnicode, tryParseUnit, unitsCompatible, unknowns, updateRules, validate, validateInterpAxis, validateSchema, validateSearchsortedTable, validateText, validateUnits, writePath };
5484
+ export type { AffectEquation, Analysis, AnalysisOptions, AnalysisResults, Assertion, Assertion1, CadenceClass, CanonicalDims, ClosedFunctionErrorCode, CommonSubexpression, CompiledExpression, ComplexityMetrics, ComponentGraph, ComponentNode, ConditionUpdate, ConnectorEquation, ContinuousEvent, Coordinate, Coordinate1, CouplingCallback, CouplingCouple, CouplingEdge, CouplingEntry, CouplingEvent, CouplingEvent1, CouplingImport, CouplingImportOptions, CouplingOperatorCompose, CouplingVariableMap, CouplingVariableMap1, CovarianceMatrix, CrossingUpdate, DataSource, DataSourceBinding, DataSourceCodes, DataSourceDeterminism, DataSourceExtent, DataSourceLocation, DataSourceRecordFilter, DataSourceSelect, DataSourceSelectAxis, DataSourceTemporal, DataUpdate, DependencyEdge, DependencyGraph, DependencyNode, DependencyRelation, DerivativeResult, DiscreteEvent, DiscreteEventTrigger, Distribution, Domain, ESMFormat, ESMFormat1, ESMFormat2, EnumDeclaration, Equation, ErrorCode, EsmFile, Expr, ExprNode, ExprNodeOf, Expression, ExpressionLocation, ExpressionNode, ExpressionNode1, ExpressionTemplate, FieldInitialCondition, FlattenMetadata, FlattenOptions, FlattenedEquation, FlattenedSystem, FlattenedTemplateRegistries, FlattenedVariable, FlattenedVariableMap, FlattenedVariableRole, FunctionTable, FunctionTableAxis, FunctionalUpdate, Graph, IndexSet, IndexSet1, LoadOptions, LoaderField, LognormalDistribution, Metadata, MetaparameterExpression, Metaparameters, Model, ModelClassification, ModelVariable, ModelVariable1, NonWienerParameterUpdate, NormalDistribution, NumericLiteral, Parameter, Parameter1, ParameterClass, ParameterSweep, ParameterUpdate, ParameterUpdateSpec, ParsedUnit, Plot, Plot1, PlotAxis, PlotSeries, PlotValue, Reaction, ReactionSystem, Reference, ReferenceEdge, ReferenceVertex, RemeshUpdate, ScheduleUpdate, SchemaError, Species, StabilityIssue, StoichiometryEntry, SubsystemRef, SweepDimension, SweepDimension1, SweepRange, SystemKind, TemplateImport, TemplateResolveOptions, TemplateSchemaError, Test, TimeSpan, ToJsonOptions, Tolerance, Tolerance1, Tolerance2, Tolerance3, TranslateTarget, UniformDistribution, UnitResult, UnitWarning, UnknownClass, UpdateValueForm, ValidationError, ValidationResult, VariableKind, VariableNode, WienerUpdate };