Skip to content

@idfkit/core

The portable surface: parsing, writing, and the object model. Every export here is synchronous and takes strings, so the same code runs in Node, a browser, a worker, and an edge runtime. File and network access lives in @idfkit/core/node and SchemaBundle.

npm install @idfkit/core @idfkit/schemas
Classes

IdfCollection

Name-indexed collection of objects of one type.

Iterable, so for (const zone of doc.all('Zone')) works, and array-like enough that [...collection], .map, .filter read naturally. Lookup by name is O(1) and case-insensitive, matching EnergyPlus semantics.

Insertion order is preserved. IDF files are hand-edited and diffed, so reordering objects on a round-trip would produce noisy diffs for no reason.

Constructors

constructor

IdfCollection

Properties

typeName

Accessors

first

only

size

Methods

[iterator]


filter


find


get


Look up by name, case-insensitively.

has


map


names


require


Look up by name, throwing if absent.

toArray


where


Objects whose field equals value, compared case-insensitively.

IdfDocument

An EnergyPlus model.

Holds collections keyed by object type, a live reference graph, and the schema for one specific EnergyPlus version. Every document is bound to a version at construction; there is no version-agnostic mode, because field order and reference lists genuinely differ between releases.

The optional M parameter attaches a generated type map, which makes field access statically checked without changing anything at runtime. See typemap.ts.

Constructors

constructor

IdfDocument

Properties

schema

Accessors

rawText

references

size

version

Methods

add


Create an object and attach it to the document.

Anonymous types (Version, Timestep) take null for the name and get a synthetic key that never appears in output.

addRaw


Untyped object creation, for the parsers.

They work from runtime strings, so the compiler cannot check them against the type map. Same reasoning as the in-package collection accessor: one deliberate seam rather than casts scattered through the parse loop.

all


Every object of one type.

The type name is matched case-insensitively, because EnergyPlus matches it that way: all('zone'), all('ZONE') and all('Zone') are one collection, whatever casing the source file used. Every type-name-keyed entry point resolves identically, so get, require, has and remove cannot disagree with this one.

When the document carries a generated type map, the argument completes among that version's type names and the result is narrowed to the matching field interface. Unknown names still work and simply stay untyped, which is what version-generic code needs.

Why an unknown type name returns empty rather than throwing

all('Zoen') returns an empty collection. Throwing would catch the typo, and it was weighed and rejected. has() must answer false rather than throw, and an all() that throws where has() does not is two rules for one question. The version-generic contract above depends on an unknown name being answerable: code written against no particular release asks for types that exist in some versions and not others, and that is not an error. And idfkit, the Python library this API is unified with, cannot throw here at all, because a document may carry no schema and then nothing distinguishes a typo from a valid type; a rule that holds only when a schema happens to be loaded is not one rule.

The typo is caught on the paths that write, which can afford to be strict: add() throws for a type in no schema, so does attach(), and the IDF and epJSON parsers reject one. To ask the question directly, call document.schema.has(name).

What this must never do, and no longer does, is store the empty collection it hands back. A read that mutates the document is wrong on its own terms: probing five misspelled names used to leave five junk keys in types(), visible to every later iteration and to toJSON. The collection returned for an absent type is detached from the document.

idfkit resolves and declines to store on exactly the same terms, in IDFDocument.__getitem__.

attach


Attach an existing detached object, e.g. one produced by clone().

Repeats the checks addRaw makes rather than trusting the object. An object carries its own schema definition, so one cloned out of a document on a different EnergyPlus version would otherwise be written using that version's field order under this document's Version header, which mis-maps every field on reload instead of failing.

changedObjects


Every object a preserving write will write afresh rather than reproduce.

Empty for a document read with preserveFormatting and not edited since. Every object for a document read without it, because there is nothing to reproduce.

rawText answers whether a write will preserve at all. This answers how many objects it will REWRITE, and it is the part a consumer cannot work out for itself: a rename clears the record on every object that referred to the renamed one, so counting from your own edit log reports one where the answer is nine.

It is not "everything that will differ", and a removal is the case that separates the two. An object removed from the document is no longer in it to be yielded, so this can return nothing for a write that changes the file. A consumer treating an empty result as "the file is unchanged" would be wrong on every removal. To ask whether the file will differ at all, compare the write with rawText; ask this for how much of it is being written afresh.

A generator, so listing what is about to be reformatted is as easy as counting it:

const changed = [...document.changedObjects()];
if (changed.length > 0) warn(`Saving will rewrite ${changed.length} objects.`);

danglingReferences


Reference targets that no object provides.

get


One object by type and name.

has


Whether any object of this type exists.

objects


Every object in the document, grouped by type in insertion order.

onFieldChanged


onNameChanged


regionOf


Where an object's characters sit in rawText, or undefined if they sit nowhere.

undefined for an object added since the read, for a document read without preservation, and for one read from the object notation, which has no statements to point at and preserves all-or-nothing.

This is what makes changedObjects usable. Turning an edit into the smallest possible change to a file takes three things: WHICH objects will be rewritten, WHAT text each becomes, and WHERE the old one was. Without the third a consumer has to write the whole file and diff it, which is the work changedObjects exists to avoid.

The range is where the object WAS, and stays answerable after it changes. That is the case it is for: the objects worth locating are the ones being rewritten.

for (const obj of document.changedObjects()) {
  const at = document.regionOf(obj);
  if (at === undefined) continue; // added since the read; there is no old text to replace
  edits.push({ range: at, newText: replacementFor(obj) });
}

Where the replacement text comes from is not settled by this method, and writeObject is not the answer. A preserving write hands that function the author's own per-field comments, which are internal, so calling it with options built by hand produces text that differs from what writeIdf would have produced for the same object: the author's units and notes come back as generated labels. A consumer that needs the two to agree has to take the whole file from writeIdf. This method locates the edit; producing its text for a single object is a gap that is open, and it is recorded rather than papered over.

The end of the range is the WRITER's, which is not always the semicolon: a comment on the terminator's own line is that statement's last field's comment and a preserving write replaces it. A range that stopped at the semicolon would leave it behind, describing a field that had just moved.

Offsets, not a line and column: Region carries the conversion, and a consumer that wants one has the text to compute it from, while going the other way costs a scan.

remove


Remove an object. Does not touch objects that referenced it.

rename


Rename an object and rewrite every reference to it.

Equivalent to assigning obj.name.

renderObject


One object, rendered exactly as a preserving write would render it.

The text that belongs in the range regionOf returns, so the two compose into an edit that leaves the file byte for byte where writeIdf would have left it. undefined for an object the retained source does not hold, which is the same set regionOf declines.

for (const obj of document.changedObjects()) {
  const at = document.regionOf(obj);
  const text = document.renderObject(obj);
  if (at === undefined || text === undefined) continue; // added since the read
  edits.push({ range: at, newText: text });
}

writeObject is not this, and that is the reason this exists. A preserving write hands that function the author's own per-field annotations, which are internal, so calling it with options built by hand comes back with the author's units and notes as generated labels: !- North Axis {deg} becomes !- North Axis. That is a unit lost from an engineering model by an editor asked to save a file, and no doc comment is a good enough guard against it.

fieldComments is the ONLY option, because it is the only one a preserving write honours. indent, commentColumn, ordering and versionFirst are refused by writeIdf alongside preserveFormatting, and comments: false and compressed defeat preservation and send the whole document down the formatting path instead. Accepting any of them here would let a caller render one object on terms the surrounding file was not written on, which is the exact divergence this method exists to prevent.

No trailing line break: the range this fills ends at the terminator, or at the comment on that line, and the break after it is the first character of what separates one object from the next, which a preserving write leaves in place.

require


One object by type and name, throwing if absent.

toJSON


epJSON representation.

A type that declares no name field at all, Version or GlobalGeometryRules, gets the "<Type> N" key EnergyPlus itself emits, numbered from 1 in document order, which is what makes the output loadable by the real engine.

tracksExtensibleEdits


Whether an in-place edit to an extensible repeat has to be heard.

Only while this document carries a retained source. Without one there is no touched record to maintain and nothing a preserving write would consult, so the repeats stay plain objects and a geometry consumer reads a coordinate at the cost it has always read one at.

types


Object type names present in this document, in insertion order.

IdfObject

A single EnergyPlus object.

Field access is via real accessors installed on a per-type prototype, so zone.ceiling_height is an ordinary property read that TypeScript can see (given the generated interfaces) and V8 can inline. See shape.ts.

Field names are epJSON names (zone_name, outside_boundary_condition), not the space-separated IDD names. That is a deliberate break from the Python library's IDF-to-Python conversion: epJSON names are already valid JS identifiers and valid TS interface keys, so using them directly means the on-disk name, the runtime key, and the static type all agree.

Properties

[DATA]

[KEY]

[NAME]

[ORIGIN]

[OWNER]

[SHAPE]

[SOURCE]

Index into the document's preserved anchors, or undefined once anything has changed this.

Accessors

extensible

fieldNames

isNamed

key

name

schema

typeName

Methods

clone


Detached deep copy, optionally renamed. Not attached to any document.

declaredNames


Names this object contributes to the model's reference lists.

Usually just name, but anonymous types like FluidProperties:Name carry their identity in an ordinary field instead, and other objects reference that value. Treating those as nameless makes every pointer at them look dangling.

fieldSchema


Schema definition for one field.

get


Read a field by name. Untyped escape hatch for version-generic code.

hasField


Whether the schema defines this field for this object type.

outgoingReferences


Names this object's fields point at, paired with the field holding them.

Extensible groups are included, with index naming the repeat the value sits in. Half the references in a real model live there.

set


Write a field by name, going through the same hooks as property access.

setFieldNames


Field names that are actually set on this object.

toJSON


Plain epJSON body: field values only, without the name.

Choice values are emitted in the casing the schema declares, whatever casing the field was written or set in. This is the one place the two formats want different answers and the reason canonicalisation lives here rather than at parse: IDF matches a choice case-insensitively, so writeIdf keeps the author's CounterClockWise and the file still runs, while the epJSON reader matches the JSON Schema enum exactly and fatals on anything else. Canonicalising on the way in would repair the object notation by rewriting the text format's output, which is not this library's to rewrite.

The rule is ConvertInputFormat's own, taken from IdfParser::parse_value: a value is matched against the field's choice list case-insensitively and replaced by the member it matched. Note what that code does NOT consult, since a reader will look for it here: the schema's retaincase (SlimField.rc) does not exempt a choice field, and the seven fields across the bundled versions that carry both are canonicalised like any other. retaincase governs the values EnergyPlus's own reader would otherwise upper-case, and this library upper-cases nothing.

toString


update


Apply several fields at once.

create


IdfParseError

Constructors

constructor

IdfParseError

Properties

diagnostics

Every finding that stopped the parse.

This error carried line and typeName from a single diagnostic, flattened into fields, and a caller who wanted the rest had nowhere to look. Python's IDFParseError has always carried the collection; this is the half of the difference that was real.

line and typeName still resolve to the first finding's values, so no existing caller breaks (FR-013, FR-014). They are a convenience over diagnostics[0] rather than a second source of truth.

line

typeName

ObjectShape

Per-object-type prototype carrying real accessors for every field.

The Python library resolves zone.ceiling_height through __getattr__. The mechanical translation of that is a Proxy, which we deliberately do not use: proxies defeat V8's inline caches, and more importantly they are invisible to TypeScript, so nothing would autocomplete. Instead each object type gets one prototype with Object.defineProperty accessors, built once and shared by every instance of that type. Property access is then an ordinary monomorphic lookup, and the generated .d.ts interfaces describe it statically.

Shapes are keyed by the schema definition object rather than by type name. Because the schema bundle is content-addressed, Zone in 9.4.0 and Zone in 26.1.0 are the same frozen definition, so they share one shape and one prototype. Cross-version documents stay monomorphic for free.

Constructors

constructor

ObjectShape

Properties

extensibleKey

Extensible array key (vertices), if this type has one.

extensibleRefFields

Fields inside the extensible group that point into a reference list.

Kept separate from refFields because these live in type.x.fields, not the positional field list, and so need the repeat index to address them. Ignoring them is not cosmetic: ZoneList, Branch, and the supply/return paths carry all of their references here, so leaving them out of the graph makes rename() silently produce a broken model.

fields

Field names in IDF positional order, excluding the name field.

keyFields

Fields whose value declares a name other objects may reference.

named

Whether the object carries a name (most do; Version does not).

proto

refFields

Fields that point into a reference list, i.e. foreign keys.

type

typeName

ReferenceGraph

Live index of every name-to-name reference in a document.

Kept current by the document as objects are added, removed, renamed, and edited, so referencing() is a lookup rather than a scan. EnergyPlus models are dense with references (every surface names a zone and a construction, every construction names materials), and the rename-propagation behaviour that makes the library useful depends on this being exact.

Names are matched case-insensitively, because EnergyPlus resolves them that way, but the original casing is preserved for round-tripping.

Constructors

constructor

ReferenceGraph

Accessors

size

Methods

add


Record that obj.field points at target.

addObject


Index every reference field of an object at once.

clear


dangling


Edges whose target does not exist. valid holds lowercased names.

isReferenced


Whether anything points at this name.

referencedBy


Names an object points at.

referencing


Edges pointing at a name.

referencingObjects


Objects that reference a name, deduplicated.

removeObject


Drop every edge originating from an object.

retarget


Rewrite every edge pointing at previous to point at next.

Only updates the index. The document is responsible for writing the new value into the referencing objects' fields, which it does without going back through the setter hook to avoid re-entering this method.

updateField


Update the edge for a single field after its value changed.

Schema

A single EnergyPlus version's schema, backed by a shared blob store.

Type definitions are hydrated lazily and cached in the store, so loading a second version only pays for the definitions that version does not already share with one in memory. In practice that is a couple hundred out of 858.

Constructors

constructor

Schema

Properties

version

Accessors

typeNames

Methods

changedFrom


Object type names whose definition hash differs from other.

Because definitions are content-addressed this is a manifest comparison, not a deep diff of two 10 MB documents, which is what makes cross-version work (migration planning, "what changed in 25.2") cheap.

field


Field definition for a type, or undefined.

get


Definition for an object type, or undefined if this version lacks it.

has


Whether this version defines the given object type. Case-insensitive.

require


Definition for an object type, throwing if absent.

resolve


Resolve a possibly mis-cased type name to its canonical spelling.

IDF is case-insensitive on type names and real files are inconsistent (ZONE, Zone, zone all appear in the wild), so every lookup path goes through here rather than trusting the input.

SchemaBundle

Loads schemas from a bundle, sharing one blob store across every version.

Hold one of these for the lifetime of the process. Loading 26.1.0 and then 9.4.0 costs far less than twice one version, because most definitions are byte-identical and already hydrated.

Constructors

constructor

SchemaBundle

Methods

latest


The newest version in the bundle.

load


Load one version's schema.

Repeat calls return the same instance; concurrent calls share one fetch.

loaded


A version already loaded, or undefined. Synchronous by design.

loadProse


Load the explanatory prose this bundle's manifests index into.

One pool serves every version: build.mjs deduplicates the memos and notes of all eighteen schemas into a single array and the manifests carry indices into it, so there is no version to pass here.

It is a separate call rather than part of load because the two are wanted at different moments by different callers. Parsing a model needs the records and never needs a sentence of English; describing a type for a reader needs both. Keeping the fetch separate is what lets the parse path stay unaware the pool exists, which check-bundle-purity.mjs asserts by building a minimal read-and-write graph and failing on any input under data/.

Repeat calls return the same array; concurrent calls share one read.

prose


The prose pool if it has been loaded, or undefined. Synchronous by design.

The counterpart of loaded, and it exists for the same reason. Everything that reads prose is synchronous: describeObjectType takes a pool rather than fetching one, and every answer in @idfkit/language is a pure function so an editor server can answer a cursor without holding a thread. A consumer calls loadProse once when a document arrives and reads this on the request path. Without the pair, a caller would either await inside a path it keeps synchronous or build a second cache beside the one this class already has.

versions


Versions this bundle can serve, oldest first.

Interfaces

BundleSource

Where bundle files come from.

The only runtime-specific part of this package. Node reads from disk, the browser fetches over HTTP, and a bundler-driven app can supply its own resolver backed by import(). Everything above this interface is portable.

Methods

read


DocsUrl

A resolved documentation URL with metadata.

Properties

docSet

Python spells this doc_set; the field-casing rule renames it here.

label

Human-readable label, e.g. Zone — I/O Reference.

url

version

Short version used in the URL path, e.g. v25.2.

FieldDescription

Description of a single field in an EnergyPlus object type.

The field set mirrors Python's idfkit.introspection.FieldDescription one-for-one, snake_case renamed to camelCase. Every member is always present, so a description can be diffed against the Python dataclass key by key; Python's str | None becomes string | undefined.

Properties

default

enumValues

Permitted values for a choice field.

Numbers rather than strings for the handful of fields that express a choice numerically, matching Python, which also hands back the raw JSON values.

exclusiveMaximum

Exclusive maximum. Number or boolean, exactly as exclusiveMinimum.

exclusiveMinimum

Exclusive minimum, as the schema for this version declares it.

true rather than a number on 8.9.0 through 9.5.0, whose draft-04 schemas use the keyword as a flag qualifying minimum instead of as a bound. Python reports the same raw value, so a caller comparing the two sides sees no difference; a caller comparing a value against it must check the type first.

fieldType

epJSON JSON-Schema type: "number", "string", "integer", "array", or a pipe-delimited union in declaration order for heterogeneous anyOf fields, e.g. "number|string" for Schedule:Compact's extensible field.

undefined when the field appears in the positional order but carries no schema definition at all.

isReference

Whether this field points into another object's reference list.

maximum

minimum

name

Field name in epJSON spelling, e.g. x_origin.

note

Field documentation note.

Always undefined here. The slim schema bundle deliberately drops note (see the header of @idfkit/schemas's types.ts), and no other faithful source for it exists in this package. It is kept in the type because the naming register requires the same field set on both sides, and because inventing a value from the field name would be worse than admitting the absence.

objectList

Reference list names this field points into.

required

Whether the field is listed in the object's required array.

units

SI units, e.g. "m", "W/m-K".

LexDiagnostic

Properties

code

Machine-readable kind, from the vocabulary both libraries share.

Derived from Python's exception hierarchy by dropping the Error suffix, so neither language invented it and the mapping stays mechanical. The conformance corpus compares a finding on (code, line, typeName) and never on message: wording is a presentation choice each library should stay free to improve, and pinning it would turn every improvement into a failure.

column

1-based column, when the lexer knew one.

filepath

Path the text came from, when it came from a file rather than a string.

line

message

typeName

Object type the problem occurred in, when known.

Declared here rather than only on ParseDiagnostic because the lexer knows it too: an unterminated object has read its type name before it runs out of input, and a finding that drops it says only that something went wrong somewhere.

LexOptions

Properties

onDiagnostic

Report a problem instead of throwing.

LineColumn

A position as a human reads it, derived from an offset rather than stored.

Storing line and column on every region would double its size to hold two numbers that are a function of one. Both count from one, which is what the existing findings already report; a consumer whose own convention counts a column from zero subtracts one, and that subtraction is the consumer's, because a service that guessed which convention its caller wanted would be modelling a protocol.

Properties

column

1-based, in UTF-16 code units, from the start of the line.

line

1-based, counting from the start of the text.

ObjectDescription

Description of an EnergyPlus object type.

Field set mirrors Python's idfkit.introspection.ObjectDescription.

Properties

extensibleSize

Number of fields in one extensible repeat group.

fields

hasName

Whether the type carries a name field. Version and friends do not.

isExtensible

Whether the type has a repeating extensible group.

memo

Object memo from the schema.

Always undefined here, for the same reason as FieldDescription.note: the slim bundle drops memo, and this package has no faithful source for it. See that member's note.

objType

Canonical object type name, e.g. "Zone".

requiredFields

Required field names, in schema order.

ObjectOwner

Something that wants to know when an object changes.

Implemented by IdfDocument. Declared as an interface so a detached object has no dependency on the document at all.

Methods

onFieldChanged


onNameChanged


tracksExtensibleEdits


Whether an in-place edit to an extensible repeat has to be heard.

True only while the document carries a retained source, because that is the only time there is a touched record to maintain. Hearing it costs an accessor on every field of every repeat, and an accessor read is far dearer than a plain one over the vertices of a real model.

ObjectWriteOptions

Properties

commentColumn

Where !- goes, counted from 1. See WriteIdfOptions.commentColumn.

comments

compressed

Put the whole object on one line. See WriteIdfOptions.compressed.

indent

ParseDiagnostic

Properties

code

Machine-readable kind, from the vocabulary both libraries share.

Derived from Python's exception hierarchy by dropping the Error suffix, so neither language invented it and the mapping stays mechanical. The conformance corpus compares a finding on (code, line, typeName) and never on message: wording is a presentation choice each library should stay free to improve, and pinning it would turn every improvement into a failure.

column

1-based column, when the lexer knew one.

filepath

Path the text came from, when it came from a file rather than a string.

line

message

objectName

Object name the problem occurred in, when known.

Python spells this obj_name, and typeName against its obj_type is the same idiomatic casing difference the naming register already records. Not a gap, and not renamed: spending a rename to make the register harder to read is the wrong trade.

typeName

Object type the problem occurred in, when known.

Declared here rather than only on ParseDiagnostic because the lexer knows it too: an unterminated object has read its type name before it runs out of input, and a finding that drops it says only that something went wrong somewhere.

ParseOptions

Properties

onDiagnostic

Collects diagnostics when strict is false.

preserveFormatting

Retain the source text and the anchoring, so a later writeIdf can reproduce it.

Off by default, and the option gates every piece of the new work: a caller who does not ask pays neither the scan nor the retention, and reading costs exactly what it costs today.

When it is on, the read makes ONE pass and builds both the objects and the syntax layer from it, so the cost is the layer's own budget rather than a second read of the text.

The retained material hangs off the document, reachable as IdfDocument.rawText. It is not returned beside the document: ParseResult carries exactly two keys and a test pins that it does.

strict

Throw on the first diagnostic instead of collecting them.

ParseResult

Properties

diagnostics

document

RawObject

A raw object as it appears in the file, before schema interpretation.

Properties

column

1-based column where the object starts, for diagnostics.

line

1-based line where the object starts, for diagnostics.

offset

Absolute offset of the object's first character in the source text.

One number per object, so that a finding about a FIELD can be positioned without the lexer recording a line for every field of every object. The rescan that uses it runs only when a finding is being built.

typeName

Type name exactly as written, e.g. BuildingSurface:Detailed.

values

Comma-separated values after the type name, trimmed, comments stripped.

ReferenceEdge

One field of one object pointing at a name.

Properties

field

from

index

Repeat index, when the field lives inside an extensible group. Absent for ordinary positional fields, which is what distinguishes the two.

target

Region

A span of source text, half-open.

end is one past the last character, so text.slice(start, end) is exactly the text the region selects, an empty region is start === end, and two adjacent regions share a number rather than straddling one. An empty region is what a cursor between two characters is.

Offsets are indices into the JavaScript string, which are UTF-16 code units. Nothing converts them, because no consumer wants them converted: the Language Server Protocol's default positionEncoding is UTF-16, Monaco measures columns in UTF-16 code units, and CodeMirror addresses its document by the same offsets. The Python library counts code points instead, because Python string indices are code points; the two agree for every character below the astral planes and differ for anything above them, which in practice means an emoji in a comment. That divergence is registered rather than papered over.

Properties

end

Offset one past the last character.

start

Offset of the first character, into the source string.

SchemaDelta

Properties

added

Types present in this version but not the other.

changed

Types present in both, with a differing definition.

removed

Types present in the other version but not this one.

SlimField

Properties

auto

Field is an anyOf of a numeric branch and a string branch, in that order.

Set for every one of the 13060 such fields across the 17 bundled versions. The numeric branch's type, enum and bounds are hoisted onto this record; the string branch survives as se.

d

Schema default, applied on write when the field is absent.

e

Permitted values for a choice field.

Numbers, not strings, on the 68 fields across the versions that express a choice numerically (e: [1, 3] on Site:GroundDomain:Slab.phase). Compare strings case-insensitively and numbers by value.

eb

The enum also accepts the empty string.

e is filtered of '' because it is what validation checks against, and admitting the blank there would change what validate() accepts. Python's describe_object_type reports it, so the description path puts it back from this flag and validation never sees it.

A flag rather than an index because the blank is first every time: across all 17 bundled schemas, all 21,962 blank-bearing enums carry it at position 0. bundle.test.ts asserts that, so a schema that breaks it fails loudly instead of silently reordering a choice list.

max

min

n

The field's explanatory sentence, as an index into the prose pool.

An index, not the text: 3,837 distinct field notes are shared across about 119,000 occurrences. Resolve it against docs.json.gz, which is loaded on demand and is never on the parse path.

ol

Names of reference lists this field points into (i.e. it is a foreign key).

rc

Value is case-sensitive and must not be normalized.

ref

Names of reference lists this field contributes to (i.e. it is a key).

se

String literals the anyOf string branch accepts, verbatim from the schema.

Only meaningful together with auto. Absent while auto is set means the string branch carries no enum at all and ANY string is legal there, which is the shape of 646 fields including Schedule:Compact's extensible field. That is why the empty string is kept here rather than filtered out the way e filters it: se: [''] (the whole string branch of the 68 fields whose number branch carries a numeric enum) and no se at all mean the opposite of one another.

The sentinel is not a constant: 10565 fields take Autosize and 1781 take Autocalculate, so a validator that accepts either everywhere accepts a value EnergyPlus rejects.

t

Storage class.

u

SI units, used by the unit-conversion helpers.

xmax

Exclusive maximum. Number or boolean, exactly as xmin.

xmin

Exclusive minimum, in whichever JSON Schema dialect the version shipped.

A number is the bound itself (draft-06+, 9.6.0 onwards). The boolean true qualifies the sibling min, making it exclusive (draft-04, 8.9.0 through 9.5.0). Measured across the bundled schemas: xmin is boolean 9013 times in the older seven versions and numeric 13840 times in the newer ten; xmax behaves identically, and no version mixes the two. Branch on the value's type, never on the version.

SlimType

Properties

anon

Object has no name field at all, e.g. Version, GlobalGeometryRules.

f

All field names in IDF positional order, from legacy_idd.fields.

fo

Field names in declaration order, for the types where f cannot give it.

f holds only the name for exactly three types in every bundled version, so the description path would otherwise fall back to the key order of p — which the content-addressing serializer has sorted alphabetically. This records the order that sort destroys, and is emitted only for those three.

g

IDD group, e.g. Thermal Zones and Surfaces.

m

The type's explanatory sentence, as an index into the prose pool.

See SlimField.n. Present for 845 of 858 types in 26.1.0; a type with no memo in the source schema has none here, and both languages report nothing rather than a placeholder.

nref

Reference lists the object's name contributes to.

nreq

Object's name is required.

p

Field definitions, keyed by epJSON field name.

r

Required field names.

s

Object is a singleton (maxProperties: 1), e.g. Version, Building.

x

Extensible group definition, if the object has one.

Statement

One statement as the text writes it, terminated or not.

Not an object in the model and carrying no schema meaning (FR-006): a statement whose type the schema never heard of, or which carries twice the fields its type defines, is still a statement. Deciding what any of it means is the reader's job, and the layer is what a reader positions against.

Properties

fields

Fields after the type name, in positional order, one region each.

Index 0 is the first field after the type name, matching RawObject.values, which has already had the type name shifted off. A field written empty still gets a region, an empty one positioned where its value would have begun, so that positional indexing never shifts and a blank slot in the middle of an extensible group can still be pointed at.

The count is what was written rather than what the type defines. A statement carrying more or fewer fields than its type is a finding, not a representation problem.

region

The whole statement, from its first non-blank character through its terminator.

typeName

Just the type name. Empty, at the offset one would have begun, when none was written.

typeNameText

Type name text, exactly as written and not case-folded.

unterminated

True when no terminator was found, meaning the statement runs to end of input.

SyntaxLayer

Everything the text contains and where it was.

The layer holds the text it was built from, which is what makes byte-identical reconstruction a consequence rather than a feature: a reconstruction defined as concatenating slices of that text returns the text by construction. What can actually break is the tiling, so that is what the corpus test asserts and what classify makes observable.

Nothing builds a layer implicitly (FR-005). lex and parseIdf read the same characters through the same scan and construct none of this, so a caller who never names scanIdf pays neither its time nor its memory.

Properties

statements

Statements in source order.

text

The text this layer was built from. Held so regions can be resolved.

tokens

Every meaningful token in source order, packed. Whitespace is not among them.

Token

One token, materialised.

A Token is a Region rather than carrying one, which keeps a classified stream to one object per token instead of two. On a file of forty thousand lines that difference is the difference between an editor that repaints and one that stutters, and a token still passes anywhere a region is expected.

A materialised token never crosses a line boundary; a stored value region may, because the format lets a field be written across two lines, and the classification view splits it.

Properties

end

Offset one past the last character.

kind

start

Offset of the first character, into the source string.

ValidationError

One validation finding.

code is the machine-readable part and is shared with the Python library verbatim (E001…E010, W002, W003). message is for a human and is not guaranteed identical across the two languages: number formatting and type names differ between the runtimes. Match on code.

Properties

code

Machine-readable code, stable across languages.

field

Field the finding concerns, or undefined when it concerns the object.

message

Human-readable description.

objName

Object name the finding was found on. Empty for anonymous objects.

objType

Object type the finding was found on.

severity

How serious the finding is.

ValidationResult

Everything one validation run found, split by severity.

isValid and totalIssues are computed once when the result is built rather than being live properties, because the arrays are read-only.

Properties

errors

info

isValid

True when errors is empty. Warnings and info do not make a model invalid.

totalIssues

errors + warnings + info.

warnings

WriteEpJsonOptions

Properties

indent

Indent width. 0 emits compact JSON.

preserveFormatting

Reproduce the text the document was read from, on this format's own terms.

All or nothing, which is not what the text format does. The object notation has no statements, so there is nothing to anchor an object's own characters to and no way to reproduce one object while reformatting another. The retained text is therefore reproduced only while nothing has been touched, nothing added and nothing removed, and any change at all falls the whole document back to the ordinary writer.

Both languages preserve this format on these terms. The difference is a property of the format, not of either library, and it is stated here because a reader who knows the text format's per-object terms would otherwise assume them.

Tri-state as it is on the text writer: absent decides, true preserves and is a quiet fallback when there is nothing to preserve, false formats.

WriteIdfOptions

Properties

commentColumn

Column the field-name comments are aligned to, counting from 1 as an editor does.

The default puts !- where EnergyPlus itself puts it. Its own example files write the marker at column 30 on 223 of the 231 commented lines of 1ZoneUncontrolled.idf, and matching that is what keeps a rewritten object flush with the untouched objects around it: on a preserving write, a column of its own would leave a visible seam at every edit.

comments

Emit !- Field Name comments after each field.

compressed

Put each object on a single line, with no comments and no blank separators.

The counterpart of the other language's output_type="compressed", and it means the same thing: type name and values joined by commas, one object per line, no generator header, no blank line between objects. The corpus checks that the two agree structurally rather than textually, because the two writers differ on defaults that compressed output does not remove.

comments: false is not this. That skips the padding and the comment and still puts every field on its own line, which is a different, coarser output that both languages already had.

fieldComments

What to do about a field the author deliberately left without a comment.

Only meaningful on the preserving path, which is the only path that knows what the author wrote. 'preserve' leaves a bare field bare, because absence is as much a thing the author wrote as the words are. 'generate' labels it, for a caller who wants the file annotated.

Neither setting touches the author's own comment lines. A comment between two objects is carried by the text between them, and a comment on its own line inside an object is emitted with the field below it, so asking for labels adds them and never costs a line.

indent

Indent for field lines.

ordering

How object types are ordered.

'source' keeps the order the types first appeared in the document, which is what this writer has always done and remains the default: no default moves (FR-017). 'sorted' orders them by type name, which is the other language's default and what its !-Option SortedOrder header declares.

An enumeration rather than a boolean, because three behaviours exist across the two languages and two formats and a flag cannot say which of the three is wanted.

Orthogonal to versionFirst, which pins Version ahead of whichever order this selects.

preserveFormatting

Reproduce the text the document was read from, per object.

Tri-state: absent decides from the document and the other options, true preserves and refuses a contradictory request, false formats. Asking for it on a document read without it is not an error, because nothing was promised.

Refused together with indent, commentColumn, ordering or versionFirst: reproducing the original text and laying it out differently are contradictory. Not refused with compressed or comments: false, which ask for a different output FORM the source was never going to express, so producing it is honest.

Cheapest where it is used: a write that reproduces most of the file copies text rather than building it. It CROSSES OVER once most objects have changed, because this path renders each of them AND walks the tiling, which is more work than formatting alone. On a 13 MB model with every object edited it is roughly four times slower than a formatting write. Nothing to guard against, since an edit touches a handful of objects and a whole model rewrite is what preserveFormatting: false is for, but worth knowing before timing the wrong one.

versionFirst

Write Version first regardless of insertion order. EnergyPlus does not require it, but every tool in the ecosystem expects it and diffs are cleaner when it is stable.

Type Aliases

AnyTypeMap

Base constraint: any map from object type name to its field interface.

EpJson

epJSON document shape: type -> name -> field values.

ExtensibleGroup

One repeat of an extensible group, e.g. a single vertex.

FieldValue

A scalar field value. undefined means the field is absent.

FieldValues

Field values accepted when constructing or updating an object.

ObjectOf

Field interface for a type name, or an empty object for unknown names.

ProsePool

The schema's explanatory prose, deduplicated across every bundled version.

A plain array of strings, indexed by SlimType.m and SlimField.n. It lives here rather than beside the code that reads it because THE INDICES ARE ONLY MEANINGFUL AGAINST THE MANIFESTS BUILT IN THE SAME RUN: build.mjs writes docs.json and the manifests together, and a pool paired with manifests from a different build resolves every sentence to the wrong one, silently. Keeping the type and the loader on SchemaBundle is what makes that pairing hard to get wrong.

Severity

Severity of a validation finding.

Both a value and a type. The value gives the Python original's Severity.ERROR spelling; the type is the string union that a 'error' literal satisfies, which is how the same three strings reach the wire in both languages. The strings themselves are load-bearing: the conformance corpus compares them across the two implementations.

StoredValue

Anything storable in a field slot.

TokenKind

What a span of text is, grammatically. Nothing here needs a schema: the layer records what the text says and never what it means, so a value is a value whether or not the field exists.

TypeNameOf

Accepted type names for a map.

The string & {} arm is what keeps literal completion alive while still accepting arbitrary strings: without it TypeScript widens the parameter to string and the suggestions disappear.

UntypedMap

A document with no version types attached.

ValuesOf

Field values accepted when creating an object of a given type.

Deliberately not ObjectOf. For a known type name this is the exact field interface, so TypeScript's excess-property check rejects a misspelled field in an object literal. For anything else it widens to the permissive FieldValues, which is what version-generic code and untyped documents need. Using one type for both would force a choice between catching typos and allowing dynamic field names; using two costs nothing and gives both.

Variables

CONFORMANCE_LEVEL

The conformance corpus level this release is checked against, as an immutable tag in idfkit/idfkit-conformance. A release asserts this claim in its own checks (FR-024): the corpus at this tag passes against this library, or the release does not ship.

This is not a version number and it is not compared to one. Two installed libraries agree on the formats when they declare the same level, whatever their own versions say (FR-025).

Severity

Severity of a validation finding.

Both a value and a type. The value gives the Python original's Severity.ERROR spelling; the type is the string union that a 'error' literal satisfies, which is how the same three strings reach the wire in both languages. The strings themselves are load-bearing: the conformance corpus compares them across the two implementations.

Functions

classify

compareVersions

describeObjectType

docsUrlForObject

engineeringReferenceUrl

getEpJsonVersion

getIdfVersion

httpSource

ioReferenceUrl

lex

lineColumnAt

offsetAt

parseEpJson

parseIdf

resolveVersion

scanIdf

searchUrl

shapeFor

shapeOf

toEpJson

validateDocument

validateObject

versionKey

writeEpJson

writeIdf

writeObject