@idfkit/core 0.1.0 → 0.2.0-rc.2

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.
Files changed (75) hide show
  1. package/README.md +16 -7
  2. package/dist/collection.d.ts +0 -13
  3. package/dist/collection.d.ts.map +1 -1
  4. package/dist/collection.js +1 -1
  5. package/dist/conformance.d.ts +20 -0
  6. package/dist/conformance.d.ts.map +1 -0
  7. package/dist/conformance.js +20 -0
  8. package/dist/conformance.js.map +1 -0
  9. package/dist/docs-url/index.d.ts +81 -0
  10. package/dist/docs-url/index.d.ts.map +1 -0
  11. package/dist/docs-url/index.js +201 -0
  12. package/dist/docs-url/index.js.map +1 -0
  13. package/dist/docs-url/locations.d.ts +2 -0
  14. package/dist/docs-url/locations.d.ts.map +1 -0
  15. package/dist/docs-url/locations.js +272 -0
  16. package/dist/docs-url/locations.js.map +1 -0
  17. package/dist/document.d.ts +40 -14
  18. package/dist/document.d.ts.map +1 -1
  19. package/dist/document.js +0 -0
  20. package/dist/document.js.map +1 -1
  21. package/dist/index.d.ts +19 -3
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +18 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/introspect/describe.d.ts +117 -0
  26. package/dist/introspect/describe.d.ts.map +1 -0
  27. package/dist/introspect/describe.js +206 -0
  28. package/dist/introspect/describe.js.map +1 -0
  29. package/dist/node.d.ts +5 -5
  30. package/dist/node.d.ts.map +1 -1
  31. package/dist/node.js +25 -5
  32. package/dist/node.js.map +1 -1
  33. package/dist/object.d.ts +1 -1
  34. package/dist/parse/epjson.d.ts +1 -1
  35. package/dist/parse/epjson.d.ts.map +1 -1
  36. package/dist/parse/epjson.js +3 -3
  37. package/dist/parse/epjson.js.map +1 -1
  38. package/dist/parse/idf.d.ts +24 -6
  39. package/dist/parse/idf.d.ts.map +1 -1
  40. package/dist/parse/idf.js +181 -8
  41. package/dist/parse/idf.js.map +1 -1
  42. package/dist/parse/lexer.d.ts +36 -0
  43. package/dist/parse/lexer.d.ts.map +1 -1
  44. package/dist/parse/lexer.js +49 -2
  45. package/dist/parse/lexer.js.map +1 -1
  46. package/dist/typemap.d.ts +7 -5
  47. package/dist/typemap.d.ts.map +1 -1
  48. package/dist/typemap.js +7 -5
  49. package/dist/typemap.js.map +1 -1
  50. package/dist/validate/index.d.ts +11 -0
  51. package/dist/validate/index.d.ts.map +1 -0
  52. package/dist/validate/index.js +10 -0
  53. package/dist/validate/index.js.map +1 -0
  54. package/dist/validate/types.d.ts +63 -0
  55. package/dist/validate/types.d.ts.map +1 -0
  56. package/dist/validate/types.js +25 -0
  57. package/dist/validate/types.js.map +1 -0
  58. package/dist/validate/validate.d.ts +82 -0
  59. package/dist/validate/validate.d.ts.map +1 -0
  60. package/dist/validate/validate.js +574 -0
  61. package/dist/validate/validate.js.map +1 -0
  62. package/dist/write/epjson.d.ts +3 -3
  63. package/dist/write/idf.d.ts +34 -2
  64. package/dist/write/idf.d.ts.map +1 -1
  65. package/dist/write/idf.js +24 -5
  66. package/dist/write/idf.js.map +1 -1
  67. package/package.json +11 -8
  68. package/dist/types/v26-1.d.ts +0 -65896
  69. package/dist/types/v26-1.d.ts.map +0 -1
  70. package/dist/types/v26-1.js +0 -5
  71. package/dist/types/v26-1.js.map +0 -1
  72. package/dist/types/v9-4.d.ts +0 -61414
  73. package/dist/types/v9-4.d.ts.map +0 -1
  74. package/dist/types/v9-4.js +0 -5
  75. package/dist/types/v9-4.js.map +0 -1
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The vocabulary validation reports in.
3
+ *
4
+ * A finding is a record, not a throw. The Python original names the type
5
+ * `ValidationError` and this port keeps that spelling, but nothing here is ever
6
+ * raised: a finding carries a severity and a location, and callers collect them
7
+ * into a `ValidationResult` and decide what to do. Naming it after an exception
8
+ * and then returning it is the one thing the naming register asks both
9
+ * libraries to keep in step, so the name stays.
10
+ */
11
+ /**
12
+ * Severity of a validation finding.
13
+ *
14
+ * Both a value and a type. The value gives the Python original's
15
+ * `Severity.ERROR` spelling; the type is the string union that a `'error'`
16
+ * literal satisfies, which is how the same three strings reach the wire in both
17
+ * languages. The strings themselves are load-bearing: the conformance corpus
18
+ * compares them across the two implementations.
19
+ */
20
+ export const Severity = Object.freeze({
21
+ ERROR: 'error',
22
+ WARNING: 'warning',
23
+ INFO: 'info',
24
+ });
25
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/validate/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC;IACpC,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,SAAS;IAClB,IAAI,EAAE,MAAM;CACJ,CAAC,CAAC"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * On-demand validation against the schema.
3
+ *
4
+ * A port of the Python library's `idfkit.validation`, kept deliberately close
5
+ * to it: the same checks in the same order producing the same `code` strings,
6
+ * so the conformance corpus can hold the two implementations against one
7
+ * another. Where the slim schema cannot express what the Python side reads out
8
+ * of the full epJSON schema, the difference is called out at the check rather
9
+ * than papered over.
10
+ *
11
+ * Nothing here is eager. Parsing never validates; you validate when you want
12
+ * to, over the object types you care about.
13
+ */
14
+ import type { Schema } from '@idfkit/schemas';
15
+ import type { IdfDocument } from '../document.js';
16
+ import type { IdfObject } from '../object.js';
17
+ import type { AnyTypeMap, UntypedMap } from '../typemap.js';
18
+ import { type ValidationError, type ValidationResult } from './types.js';
19
+ /** Which checks a document run performs. Every one defaults to on. */
20
+ interface ValidateDocumentOptions {
21
+ /**
22
+ * Schema to validate against. Defaults to the document's own, which is what
23
+ * you want unless you are asking "would this model load under 25.2?".
24
+ */
25
+ schema?: Schema;
26
+ /** Report references whose target no object provides. */
27
+ checkReferences?: boolean;
28
+ /** Report required fields that are absent or blank. */
29
+ checkRequired?: boolean;
30
+ /** Report values whose type the schema does not allow. */
31
+ checkTypes?: boolean;
32
+ /** Report numeric values outside their declared bounds. */
33
+ checkRanges?: boolean;
34
+ /** Report singleton types present more than once. */
35
+ checkSingletons?: boolean;
36
+ /** Restrict the run to these object types. Omit for all of them. */
37
+ objectTypes?: readonly string[];
38
+ }
39
+ /** Which checks a single-object run performs. Every one defaults to on. */
40
+ interface ValidateObjectOptions {
41
+ /** Report required fields that are absent or blank. */
42
+ checkRequired?: boolean;
43
+ /** Report values whose type the schema does not allow. */
44
+ checkTypes?: boolean;
45
+ /** Report numeric values outside their declared bounds. */
46
+ checkRanges?: boolean;
47
+ /** Report fields the schema does not define. */
48
+ checkUnknown?: boolean;
49
+ }
50
+ /**
51
+ * Validate a whole document.
52
+ *
53
+ * Findings are split by severity; `errors` being empty is what `isValid` means.
54
+ * Nothing throws: a model that cannot load is described, not raised.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const result = validateDocument(doc);
59
+ * if (!result.isValid) {
60
+ * for (const error of result.errors) console.error(error.code, error.message);
61
+ * }
62
+ * ```
63
+ */
64
+ export declare function validateDocument<M extends AnyTypeMap = UntypedMap>(doc: IdfDocument<M>, options?: ValidateDocumentOptions): ValidationResult;
65
+ /**
66
+ * Validate one object, attached or detached.
67
+ *
68
+ * Useful right after building an object, before it goes anywhere near a
69
+ * document. The schema is passed explicitly rather than taken from the object
70
+ * so that "does this object hold up under another EnergyPlus version?" is
71
+ * askable; a type the schema does not define is reported as `W002` rather than
72
+ * throwing.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const material = doc.add('Material', 'Gypsum', { roughness: 'Smooth' });
77
+ * const findings = validateObject(material, doc.schema);
78
+ * ```
79
+ */
80
+ export declare function validateObject(obj: IdfObject, schema: Schema, options?: ValidateObjectOptions): ValidationError[];
81
+ export {};
82
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/validate/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAa,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,cAAc,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC5D,OAAO,EAAY,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnF,sEAAsE;AACtE,UAAU,uBAAuB;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,qDAAqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oEAAoE;IACpE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,2EAA2E;AAC3E,UAAU,qBAAqB;IAC7B,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,gDAAgD;IAChD,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EAChE,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,EACnB,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAwFlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,SAAS,EACd,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,qBAA0B,GAClC,eAAe,EAAE,CAOnB"}
@@ -0,0 +1,574 @@
1
+ /**
2
+ * On-demand validation against the schema.
3
+ *
4
+ * A port of the Python library's `idfkit.validation`, kept deliberately close
5
+ * to it: the same checks in the same order producing the same `code` strings,
6
+ * so the conformance corpus can hold the two implementations against one
7
+ * another. Where the slim schema cannot express what the Python side reads out
8
+ * of the full epJSON schema, the difference is called out at the check rather
9
+ * than papered over.
10
+ *
11
+ * Nothing here is eager. Parsing never validates; you validate when you want
12
+ * to, over the object types you care about.
13
+ */
14
+ import { DATA } from '../internal.js';
15
+ import { Severity } from './types.js';
16
+ /**
17
+ * Validate a whole document.
18
+ *
19
+ * Findings are split by severity; `errors` being empty is what `isValid` means.
20
+ * Nothing throws: a model that cannot load is described, not raised.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const result = validateDocument(doc);
25
+ * if (!result.isValid) {
26
+ * for (const error of result.errors) console.error(error.code, error.message);
27
+ * }
28
+ * ```
29
+ */
30
+ export function validateDocument(doc, options = {}) {
31
+ const schema = options.schema ?? doc.schema;
32
+ const checkReferences = options.checkReferences ?? true;
33
+ const checkSingletons = options.checkSingletons ?? true;
34
+ const checks = {
35
+ required: options.checkRequired ?? true,
36
+ types: options.checkTypes ?? true,
37
+ ranges: options.checkRanges ?? true,
38
+ // The Python original does not expose `check_unknown` on the document
39
+ // entry point, so a document run always makes it.
40
+ unknown: true,
41
+ };
42
+ const errors = [];
43
+ const warnings = [];
44
+ const info = [];
45
+ // Types actually present, canonicalized, in document order. Asking the
46
+ // document for a collection it does not have would create an empty one as a
47
+ // side effect, so membership is settled before any lookup.
48
+ const present = new Set(doc.types());
49
+ const requested = options.objectTypes;
50
+ const typesToCheck = requested === undefined
51
+ ? [...present]
52
+ : requested.map((name) => schema.resolve(name) ?? name).filter((name) => present.has(name));
53
+ if (checkSingletons) {
54
+ for (const objType of typesToCheck) {
55
+ if (schema.get(objType)?.s !== 1)
56
+ continue;
57
+ const collection = doc.collection(objType);
58
+ const count = collection.size;
59
+ if (count <= 1)
60
+ continue;
61
+ const first = collection.first;
62
+ errors.push({
63
+ severity: Severity.ERROR,
64
+ objType,
65
+ objName: first !== undefined && first.name !== '' ? first.name : objType,
66
+ field: undefined,
67
+ message: `Singleton type '${objType}' has ${count} instances (maximum 1 allowed)`,
68
+ code: 'E010',
69
+ });
70
+ }
71
+ }
72
+ for (const objType of typesToCheck) {
73
+ for (const obj of doc.collection(objType)) {
74
+ for (const finding of findingsFor(obj, schema, checks)) {
75
+ if (finding.severity === Severity.ERROR)
76
+ errors.push(finding);
77
+ else if (finding.severity === Severity.WARNING)
78
+ warnings.push(finding);
79
+ else
80
+ info.push(finding);
81
+ }
82
+ }
83
+ }
84
+ if (checkReferences) {
85
+ const unpopulated = unpopulatedLists(schema);
86
+ const dangling = doc.danglingReferences();
87
+ // Both sets cost a full document walk, so they are built once, and only
88
+ // when there is something to test them against.
89
+ const names = dangling.length > 0 ? declaredNames(doc.objects()) : undefined;
90
+ for (const edge of dangling) {
91
+ // A field pointing only into lists nothing can ever contribute to is not
92
+ // naming an object at all, so its value cannot be dangling.
93
+ if (pointsOnlyIntoUnpopulated(schema, edge.from.typeName, edge.field, unpopulated))
94
+ continue;
95
+ // A name EnergyPlus mints itself is not dangling either, even though no
96
+ // object declares it: one instance of a ZoneList assignment expanded per
97
+ // member, or the leftover space of a partly covered zone.
98
+ if (names !== undefined && isZoneListExpansion(edge.target, names))
99
+ continue;
100
+ if (names !== undefined && isRemainderSpace(edge.target, names))
101
+ continue;
102
+ errors.push({
103
+ severity: Severity.ERROR,
104
+ objType: edge.from.typeName,
105
+ objName: edge.from.name,
106
+ field: edge.field,
107
+ message: `Reference to non-existent object '${edge.target}'`,
108
+ code: 'E009',
109
+ });
110
+ }
111
+ }
112
+ return {
113
+ errors,
114
+ warnings,
115
+ info,
116
+ isValid: errors.length === 0,
117
+ totalIssues: errors.length + warnings.length + info.length,
118
+ };
119
+ }
120
+ /**
121
+ * Validate one object, attached or detached.
122
+ *
123
+ * Useful right after building an object, before it goes anywhere near a
124
+ * document. The schema is passed explicitly rather than taken from the object
125
+ * so that "does this object hold up under another EnergyPlus version?" is
126
+ * askable; a type the schema does not define is reported as `W002` rather than
127
+ * throwing.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * const material = doc.add('Material', 'Gypsum', { roughness: 'Smooth' });
132
+ * const findings = validateObject(material, doc.schema);
133
+ * ```
134
+ */
135
+ export function validateObject(obj, schema, options = {}) {
136
+ return findingsFor(obj, schema, {
137
+ required: options.checkRequired ?? true,
138
+ types: options.checkTypes ?? true,
139
+ ranges: options.checkRanges ?? true,
140
+ unknown: options.checkUnknown ?? true,
141
+ });
142
+ }
143
+ // ---------------------------------------------------------------------------
144
+ // The per-object walk
145
+ // ---------------------------------------------------------------------------
146
+ function findingsFor(obj, schema, checks) {
147
+ const findings = [];
148
+ const objType = obj.typeName;
149
+ const objName = obj.name;
150
+ const type = schema.get(objType);
151
+ if (type === undefined) {
152
+ findings.push({
153
+ severity: Severity.WARNING,
154
+ objType,
155
+ objName,
156
+ field: undefined,
157
+ message: `Unknown object type '${objType}'`,
158
+ code: 'W002',
159
+ });
160
+ return findings;
161
+ }
162
+ if (checks.required) {
163
+ for (const field of type.r ?? []) {
164
+ const value = obj[DATA][field];
165
+ if (value === undefined || value === '') {
166
+ findings.push({
167
+ severity: Severity.ERROR,
168
+ objType,
169
+ objName,
170
+ field,
171
+ message: `Required field '${field}' is missing`,
172
+ code: 'E001',
173
+ });
174
+ }
175
+ }
176
+ }
177
+ // Read the backing store rather than `toJSON()`: this is the object's own
178
+ // insertion order, which is what the Python original walks, and it avoids
179
+ // copying every extensible group just to look at it.
180
+ for (const [field, value] of Object.entries(obj[DATA])) {
181
+ if (value === undefined || value === '')
182
+ continue;
183
+ const fieldSchema = type.p[field];
184
+ if (fieldSchema === undefined) {
185
+ // On an extensible type the field may belong to the repeat group rather
186
+ // than the fixed list, so an unrecognized name there is not evidence of
187
+ // anything.
188
+ if (checks.unknown && type.x === undefined) {
189
+ findings.push({
190
+ severity: Severity.WARNING,
191
+ objType,
192
+ objName,
193
+ field,
194
+ message: `Unknown field '${field}'`,
195
+ code: 'W003',
196
+ });
197
+ }
198
+ continue;
199
+ }
200
+ // One finding per field, whatever the field is. A value that is both the
201
+ // wrong type and out of range is one defect described twice, and reporting
202
+ // it twice makes a caller fix it twice.
203
+ //
204
+ // An `anyOf` field is evaluated as a single unit, because its type, its enum
205
+ // and its bounds all belong to a branch and cannot be checked in separate
206
+ // passes: a value satisfying one branch's type and another branch's absence
207
+ // of bounds satisfies neither branch. Which switch silences the result is
208
+ // therefore decided after the fact, from the code the evaluation produced.
209
+ if (fieldSchema.auto === 1) {
210
+ const failure = anyOfFailure(value, fieldSchema);
211
+ if (failure === undefined)
212
+ continue;
213
+ if (RANGE_CODES.has(failure.code) ? checks.ranges : checks.types) {
214
+ findings.push(error(obj, field, failure));
215
+ }
216
+ continue;
217
+ }
218
+ // A plain field is three ordered checks — type, then enum, then bounds —
219
+ // under the two switches that have always gated them.
220
+ const failure = (checks.types ? plainFailure(value, fieldSchema) : undefined) ??
221
+ (checks.ranges && typeof value === 'number'
222
+ ? rangeFailures(value, fieldSchema)[0]
223
+ : undefined);
224
+ if (failure !== undefined)
225
+ findings.push(error(obj, field, failure));
226
+ }
227
+ return findings;
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // References
231
+ // ---------------------------------------------------------------------------
232
+ /** Per-schema cache: the answer depends only on the schema, and costs a full walk. */
233
+ const UNPOPULATED_BY_SCHEMA = new WeakMap();
234
+ /**
235
+ * Reference lists that are pointed INTO and never contributed TO.
236
+ *
237
+ * `object_list` says a field points into a list; `reference` says a name
238
+ * contributes to one. Four lists in 26.1.0 — `validBranchEquipmentTypes`,
239
+ * `validCondenserEquipmentTypes`, `validOASysEquipmentTypes` and
240
+ * `validPlantEquipmentTypes` — are only ever pointed into. They enumerate object
241
+ * TYPE names rather than object names, so `component_object_type` holding
242
+ * `Pipe:Adiabatic` is a correct value that no object in any model will ever
243
+ * declare. Reporting those as dangling is 19783 false findings over the 760
244
+ * EnergyPlus example files, one for nearly every branch component in every model.
245
+ *
246
+ * Derived from the schema rather than listed here: the set is version-dependent
247
+ * (older versions carry a couple more), and a hardcoded list would go stale
248
+ * silently on the next EnergyPlus release.
249
+ */
250
+ function unpopulatedLists(schema) {
251
+ const cached = UNPOPULATED_BY_SCHEMA.get(schema);
252
+ if (cached !== undefined)
253
+ return cached;
254
+ const pointedInto = new Set();
255
+ const contributedTo = new Set();
256
+ for (const typeName of schema.typeNames) {
257
+ const type = schema.get(typeName);
258
+ if (type === undefined)
259
+ continue;
260
+ for (const list of type.nref ?? [])
261
+ contributedTo.add(list);
262
+ for (const props of [type.p, type.x?.p]) {
263
+ for (const fieldSchema of Object.values(props ?? {})) {
264
+ for (const list of fieldSchema.ol ?? [])
265
+ pointedInto.add(list);
266
+ for (const list of fieldSchema.ref ?? [])
267
+ contributedTo.add(list);
268
+ }
269
+ }
270
+ }
271
+ const unpopulated = new Set();
272
+ for (const list of pointedInto)
273
+ if (!contributedTo.has(list))
274
+ unpopulated.add(list);
275
+ UNPOPULATED_BY_SCHEMA.set(schema, unpopulated);
276
+ return unpopulated;
277
+ }
278
+ /**
279
+ * Whether every list this field points into is one nothing can populate.
280
+ *
281
+ * Every, not any: a field naming several lists is dangling as long as one of
282
+ * them is a list some object could have contributed to. A field the schema does
283
+ * not describe keeps its finding, because silence there would be a guess.
284
+ */
285
+ function pointsOnlyIntoUnpopulated(schema, typeName, field, unpopulated) {
286
+ if (unpopulated.size === 0)
287
+ return false;
288
+ const type = schema.get(typeName);
289
+ // Extensible reference fields (`Branch.component_object_type`) are defined on
290
+ // the repeat group, not alongside the fixed fields.
291
+ const fieldSchema = type?.p[field] ?? type?.x?.p[field];
292
+ const lists = fieldSchema?.ol;
293
+ if (lists === undefined || lists.length === 0)
294
+ return false;
295
+ return lists.every((list) => unpopulated.has(list));
296
+ }
297
+ /** Collect every name set in a single document walk. */
298
+ function declaredNames(objects) {
299
+ const all = new Set();
300
+ const zonesAndSpaces = new Set();
301
+ const zones = new Set();
302
+ for (const obj of objects) {
303
+ // Same reasoning as `danglingReferences()`: anonymous types declare the
304
+ // name others reference from an ordinary field, so `name` alone is short.
305
+ for (const declared of obj.declaredNames()) {
306
+ const lower = declared.toLowerCase();
307
+ all.add(lower);
308
+ if (obj.typeName === 'Zone' || obj.typeName === 'Space')
309
+ zonesAndSpaces.add(lower);
310
+ if (obj.typeName === 'Zone')
311
+ zones.add(lower);
312
+ }
313
+ }
314
+ return { all, zonesAndSpaces, zones };
315
+ }
316
+ /**
317
+ * Whether a target names an object EnergyPlus creates by expanding a ZoneList.
318
+ *
319
+ * An object assigned to a `ZoneList` (or a `SpaceList`) is expanded by
320
+ * EnergyPlus into one instance per member, named `<member name>` + a single
321
+ * space + `<object name>`, and other objects legitimately reference those
322
+ * expanded names. `DemandManager:ElectricEquipment.electric_equipment_name`
323
+ * documents the convention in the schema itself: "if ZoneList option is used on
324
+ * the ElectricEquipment object, a single equipment object from that assignment
325
+ * can be selected by entering <Zone Name><space><Global ElectricEquipment
326
+ * Object Name>". No object in the file declares that name, so the reference
327
+ * looks dangling and is not.
328
+ *
329
+ * Every space is tried, not only the first: zone names and object names both
330
+ * routinely contain spaces, so the split point cannot be assumed.
331
+ *
332
+ * Deliberately an approximation. It does not verify that the suffix object is
333
+ * actually assigned to a ZoneList containing the prefix zone; doing so needs
334
+ * ZoneList membership resolution that this library and its Python counterpart
335
+ * do not share today. The approximation can only ever suppress a finding, never
336
+ * invent one, so it cannot produce a false NEGATIVE on a valid model, which is
337
+ * the property that matters here. This is a choice, not an oversight.
338
+ */
339
+ function isZoneListExpansion(target, names) {
340
+ if (names.zonesAndSpaces.size === 0)
341
+ return false;
342
+ const lower = target.toLowerCase();
343
+ for (let at = lower.indexOf(' '); at !== -1; at = lower.indexOf(' ', at + 1)) {
344
+ if (!names.zonesAndSpaces.has(lower.slice(0, at)))
345
+ continue;
346
+ if (names.all.has(lower.slice(at + 1)))
347
+ return true;
348
+ }
349
+ return false;
350
+ }
351
+ /** The name EnergyPlus gives a zone's leftover space is its zone name plus this. */
352
+ const REMAINDER_SUFFIX = '-remainder';
353
+ /**
354
+ * Whether a target names the implicit remainder space of a declared zone.
355
+ *
356
+ * A zone that carries `Space` objects covering only part of it gets one more
357
+ * space from EnergyPlus, holding whatever is left over and named
358
+ * `<Zone Name>-Remainder`. Nothing declares that name, and objects reference it
359
+ * like any other space. `5ZoneAirCooledWithSpacesHVAC.idf` names
360
+ * `Zone 5-Remainder` twelve times: `Zone 5` is declared and carries the spaces
361
+ * `Space 5 Office` and `Space 5 Conference`, while `Zone 5-Remainder` is
362
+ * declared nowhere.
363
+ *
364
+ * Kept separate from `isZoneListExpansion` rather than folded into it: the two
365
+ * joins differ, a hyphen against a space, and only a `Zone` can prefix this one
366
+ * where a `Space` may prefix that one. Merging them would obscure both.
367
+ *
368
+ * An approximation in the same spirit as the ZoneList rule: it does not verify
369
+ * that the zone's declared spaces actually leave a remainder. It can only
370
+ * suppress a finding, never invent one, so no valid model gains a false
371
+ * NEGATIVE from it. A choice, not an oversight.
372
+ */
373
+ function isRemainderSpace(target, names) {
374
+ if (names.zones.size === 0)
375
+ return false;
376
+ const lower = target.toLowerCase();
377
+ if (!lower.endsWith(REMAINDER_SUFFIX))
378
+ return false;
379
+ return names.zones.has(lower.slice(0, lower.length - REMAINDER_SUFFIX.length));
380
+ }
381
+ /** Codes that belong to the range check rather than the type check. */
382
+ const RANGE_CODES = new Set(['E005', 'E006', 'E007', 'E008']);
383
+ function error(obj, field, failure) {
384
+ return {
385
+ severity: Severity.ERROR,
386
+ objType: obj.typeName,
387
+ objName: obj.name,
388
+ field,
389
+ message: failure.message,
390
+ code: failure.code,
391
+ };
392
+ }
393
+ /**
394
+ * Evaluate a field the schema declares as `anyOf: [{number}, {string}]`.
395
+ *
396
+ * Ordinary JSON Schema `anyOf`: the value is valid when it satisfies at least
397
+ * one branch *completely* — that branch's type, its enum if it has one, and its
398
+ * bounds if it has any. Checking the type against one branch and the constraints
399
+ * against another is exactly the defect this replaces; a value could satisfy the
400
+ * number branch's type and the string branch's absence of bounds and be accepted
401
+ * having satisfied neither.
402
+ *
403
+ * The branches are reconstructed from the slim record: the numeric branch is
404
+ * `t` plus `e`, `min`, `max`, `xmin` and `xmax`, all of which the bundle hoists
405
+ * off it, and the string branch is `se`, which is absent exactly when the branch
406
+ * carried no enum and any string is legal.
407
+ *
408
+ * At most one finding, and it names the failure rather than the shape: an
409
+ * out-of-range number reported as "not a number" tells the caller nothing.
410
+ */
411
+ function anyOfFailure(value, fieldSchema) {
412
+ // 1. The branches whose TYPE the value satisfies. A value is a number or a
413
+ // string, never both, so at most one of these holds; the code is written
414
+ // as a set anyway because the rule is.
415
+ const numeric = matchesKind(value, fieldSchema.t);
416
+ const stringly = typeof value === 'string';
417
+ // 2. No branch matched on type: an array where a number or a string belongs.
418
+ if (!numeric && !stringly) {
419
+ return { code: 'E002', message: `Value '${render(value)}' does not match any valid type` };
420
+ }
421
+ // 3. A branch satisfied whole makes the value valid, whatever the other says.
422
+ const numericFailure = numeric ? numericBranchFailure(value, fieldSchema) : undefined;
423
+ if (numeric && numericFailure === undefined)
424
+ return undefined;
425
+ const stringFailure = stringly ? stringBranchFailure(value, fieldSchema) : undefined;
426
+ if (stringly && stringFailure === undefined)
427
+ return undefined;
428
+ // 4. Matched on type, failed on a constraint. Report the first branch in
429
+ // declaration order, which is the numeric one in every `anyOf` in all 17
430
+ // bundled versions.
431
+ return numericFailure ?? stringFailure;
432
+ }
433
+ /** The numeric branch: its enum, then its bounds. `undefined` when satisfied. */
434
+ function numericBranchFailure(value, fieldSchema) {
435
+ const allowed = fieldSchema.e;
436
+ if (allowed !== undefined && !satisfiesEnum(allowed, value)) {
437
+ return enumFailure(allowed, value);
438
+ }
439
+ return rangeFailures(value, fieldSchema)[0];
440
+ }
441
+ /**
442
+ * The string branch: its enum, if it has one.
443
+ *
444
+ * No `se` means the branch declared no enum and any string satisfies it, which
445
+ * is the shape of 646 fields including `Schedule:Compact`'s extensible `field`.
446
+ * `se` is never inferred: the sentinel is `Autosize` on some fields and
447
+ * `Autocalculate` on others, and accepting either everywhere would accept a
448
+ * value EnergyPlus rejects.
449
+ */
450
+ function stringBranchFailure(value, fieldSchema) {
451
+ const allowed = fieldSchema.se;
452
+ if (allowed === undefined || satisfiesEnum(allowed, value))
453
+ return undefined;
454
+ return enumFailure(allowed, value);
455
+ }
456
+ /**
457
+ * A field the schema declares with one type: its type, then its enum.
458
+ *
459
+ * The bounds are the caller's business, because they answer to a different
460
+ * switch. Stopping at the first failure is what keeps a wrong-typed value from
461
+ * also being reported against a choice list it was never going to satisfy.
462
+ */
463
+ function plainFailure(value, fieldSchema) {
464
+ if (!matchesKind(value, fieldSchema.t)) {
465
+ return {
466
+ code: 'E003',
467
+ message: `Expected ${jsonTypeName(fieldSchema.t)}, got ${describeType(value)}`,
468
+ };
469
+ }
470
+ const allowed = fieldSchema.e;
471
+ if (allowed === undefined || satisfiesEnum(allowed, value))
472
+ return undefined;
473
+ return enumFailure(allowed, value);
474
+ }
475
+ /**
476
+ * Enum membership.
477
+ *
478
+ * Exact match first, then case-insensitively for strings, which is how
479
+ * EnergyPlus reads a choice field and how the Python original checks it. Numbers
480
+ * compare by value: 68 fields across the versions state their choices
481
+ * numerically, and `3` is not one of `[0, 1]`.
482
+ */
483
+ function satisfiesEnum(allowed, value) {
484
+ return allowed.some((choice) => choice === value ||
485
+ (typeof choice === 'string' &&
486
+ typeof value === 'string' &&
487
+ choice.toLowerCase() === value.toLowerCase()));
488
+ }
489
+ function enumFailure(allowed, value) {
490
+ return {
491
+ code: 'E004',
492
+ message: `Value '${render(value)}' not in allowed values: [${allowed
493
+ .map((choice) => `'${choice}'`)
494
+ .join(', ')}]`,
495
+ };
496
+ }
497
+ /** Whether a stored value satisfies the schema's storage class for its field. */
498
+ function matchesKind(value, kind) {
499
+ switch (kind) {
500
+ case 'n':
501
+ return typeof value === 'number';
502
+ case 'i':
503
+ return typeof value === 'number' && Number.isInteger(value);
504
+ case 'a':
505
+ return typeof value === 'string';
506
+ case 'arr':
507
+ return Array.isArray(value);
508
+ }
509
+ }
510
+ /** The epJSON `type` keyword the storage class stands for, for messages. */
511
+ function jsonTypeName(kind) {
512
+ switch (kind) {
513
+ case 'n':
514
+ return 'number';
515
+ case 'i':
516
+ return 'integer';
517
+ case 'a':
518
+ return 'string';
519
+ case 'arr':
520
+ return 'array';
521
+ }
522
+ }
523
+ function describeType(value) {
524
+ return Array.isArray(value) ? 'array' : typeof value;
525
+ }
526
+ function render(value) {
527
+ return Array.isArray(value) ? JSON.stringify(value) : String(value);
528
+ }
529
+ // ---------------------------------------------------------------------------
530
+ // Range
531
+ // ---------------------------------------------------------------------------
532
+ /**
533
+ * Every bound the value breaks, in `minimum`, `exclusiveMinimum`, `maximum`,
534
+ * `exclusiveMaximum` order.
535
+ *
536
+ * `xmin`/`xmax` arrive in either of the two JSON Schema dialects the bundled
537
+ * schemas ship. From 9.6.0 they are draft-06+ and carry the bound itself; for
538
+ * 8.9.0 through 9.5.0 they are draft-04 and are the boolean `true`, qualifying
539
+ * the sibling `minimum`/`maximum`. Reading `true` as a number would treat it as
540
+ * 1 and reject every value at or below 1 in a positive-bounded field, so this
541
+ * branches on the type of the value and never on the version.
542
+ */
543
+ function rangeFailures(value, bounds) {
544
+ const { min, max, xmin, xmax } = bounds;
545
+ const out = [];
546
+ if (min !== undefined) {
547
+ // draft-04: the sibling flag makes `min` exclusive.
548
+ if (xmin === true) {
549
+ if (value <= min)
550
+ out.push({ code: 'E006', message: `Value ${value} must be greater than ${min}` });
551
+ }
552
+ else if (value < min) {
553
+ out.push({ code: 'E005', message: `Value ${value} is below minimum ${min}` });
554
+ }
555
+ }
556
+ // draft-06+: the key carries the bound itself.
557
+ if (typeof xmin === 'number' && value <= xmin) {
558
+ out.push({ code: 'E006', message: `Value ${value} must be greater than ${xmin}` });
559
+ }
560
+ if (max !== undefined) {
561
+ if (xmax === true) {
562
+ if (value >= max)
563
+ out.push({ code: 'E008', message: `Value ${value} must be less than ${max}` });
564
+ }
565
+ else if (value > max) {
566
+ out.push({ code: 'E007', message: `Value ${value} is above maximum ${max}` });
567
+ }
568
+ }
569
+ if (typeof xmax === 'number' && value >= xmax) {
570
+ out.push({ code: 'E008', message: `Value ${value} must be less than ${xmax}` });
571
+ }
572
+ return out;
573
+ }
574
+ //# sourceMappingURL=validate.js.map