Skip to main content

powerio_dist/
diagnostics.rs

1//! The codes this crate emits.
2//!
3//! The record itself lives in `powerio-core`, below this crate and below the
4//! `.pio.json` document model, so a distribution finding reaches a package
5//! without a translation step. What lives here is the distribution side
6//! registry: one [`DiagnosticInfo`] per code, declared once, so an emission
7//! site names an entry rather than a loose string.
8//!
9//! Codes are families, not one per site: what differs between two sites of a
10//! family is which object or property it was, which belongs in `details`.
11
12// The collector is crate-private implementation support, not API: each
13// emitting crate carries its own copy (src/collect.rs) and never exports it.
14pub(crate) use crate::collect::Diagnostics;
15
16pub use powerio_core::{
17    Diagnostic, DiagnosticCode, DiagnosticInfo, DiagnosticSeverity, DiagnosticStage,
18    check_registry, render_diagnostic, render_diagnostics,
19};
20
21pub mod codes {
22    powerio_core::diagnostic_codes! {
23        // PARSE: the source text could not be decoded as given.
24        PARSE_DSS_SOURCE_MALFORMED = "PARSE.DSS.SOURCE_MALFORMED", Warning,
25            "a dss command, object spec, or property assignment does not parse";
26        PARSE_DIST_MALFORMED = "PARSE.DIST.MALFORMED", Error,
27            "a distribution document is not valid JSON for its format", category = Parse;
28        PARSE_DIST_SOURCE_MALFORMED = "PARSE.DIST.SOURCE_MALFORMED", Error,
29            "a distribution reader refused the source it was given", category = Parse;
30
31        // READ.DSS: decoded, but not representable in the multiconductor model.
32        /// A `Redirect`/`Compile`/`Buscoords` include the reader refused
33        /// because it escapes the case directory, or the include root when
34        /// the caller widened one. Severity `Error`: the parse continued,
35        /// but the network is incomplete.
36        READ_DSS_INCLUDE_REFUSED = "READ.DSS.INCLUDE_REFUSED", Error,
37            "an include escaping the case directory or the configured include root was refused";
38        /// The reader stopped following includes because the case exceeded the
39        /// include budget. Severity `Error` for the same reason.
40        READ_DSS_INCLUDE_BUDGET = "READ.DSS.INCLUDE_BUDGET", Error,
41            "the reader stopped following includes at the case's include budget";
42        READ_DSS_VALUE_CLAMPED = "READ.DSS.VALUE_CLAMPED", Warning,
43            "a count or dimension beyond the supported maximum was clamped";
44        READ_DSS_VALUE_DEFAULTED = "READ.DSS.VALUE_DEFAULTED", Warning,
45            "a value the model needs was absent or unusable and was defaulted";
46        READ_DSS_VALUE_UNSUPPORTED = "READ.DSS.VALUE_UNSUPPORTED", Warning,
47            "a property value outside the modeled set was read as the nearest one";
48        READ_DSS_OBJECT_UNTYPED = "READ.DSS.OBJECT_UNTYPED", Warning,
49            "an object shape the model does not type yet is kept untyped";
50        READ_DSS_PROPERTY_UNKNOWN = "READ.DSS.PROPERTY_UNKNOWN", Warning,
51            "a property this reader does not model is kept as written";
52        READ_DSS_REFERENCE_DROPPED = "READ.DSS.REFERENCE_DROPPED", Warning,
53            "a control or element reference names an object the case does not declare";
54        READ_DSS_RETAINED_SOURCE_ONLY = "READ.DSS.RETAINED_SOURCE_ONLY", Warning,
55            "a field survives in extras or the retained source rather than in a typed field";
56        READ_DSS_COORDINATE_SPACE_UNKNOWN = "READ.DSS.COORDINATE_SPACE_UNKNOWN", Remark,
57            "buscoords declare no coordinate reference system";
58        READ_DSS_INCLUDE_LOAD_FAILED = "READ.DSS.INCLUDE_LOAD_FAILED", Warning,
59            "an include the case names could not be loaded";
60        READ_DSS_INCLUDE_DEPTH_LIMIT = "READ.DSS.INCLUDE_DEPTH_LIMIT", Warning,
61            "the reader stopped following includes at the nesting depth limit";
62        READ_DSS_GEOMETRY_UNRESOLVED = "READ.DSS.GEOMETRY_UNRESOLVED", Error,
63            "line geometry remains source data without calculated conductor impedances";
64        BUILD_DIST_ELECTRICAL_INCOMPLETE = "BUILD.DIST.ELECTRICAL_INCOMPLETE", Error,
65            "distribution equipment lacks the electrical data required for numerical use", category = Data;
66        READ_DSS_LINECODE_UNKNOWN = "READ.DSS.LINECODE_UNKNOWN", Warning,
67            "a line names a linecode the case does not declare";
68
69        // EMIT.DSS: what the canonical dss writer cannot state.
70        EMIT_DSS_FIELD_DROPPED = "EMIT.DSS.FIELD_DROPPED", Warning,
71            "a field the dss object model has no property for was dropped";
72        EMIT_DSS_RECORD_DROPPED = "EMIT.DSS.RECORD_DROPPED", Warning,
73            "an element the dss writer does not emit was dropped";
74        EMIT_DSS_VALUE_COLLAPSED = "EMIT.DSS.VALUE_COLLAPSED", Warning,
75            "structure was reduced to what one dss property can carry";
76        EMIT_DSS_VALUE_DEFAULTED = "EMIT.DSS.VALUE_DEFAULTED", Warning,
77            "a value the dss engine requires was synthesized";
78        EMIT_DSS_VALUE_SUBSTITUTED = "EMIT.DSS.VALUE_SUBSTITUTED", Warning,
79            "a stated value was replaced by one the dss engine reads back the same way";
80        EMIT_DSS_EXTRAS_DROPPED = "EMIT.DSS.EXTRAS_DROPPED", Warning,
81            "a passthrough extra the canonical dss writer does not regenerate was dropped";
82        /// OpenDSS reads a node list positionally, with a grounded return
83        /// conductor last. A uniquely grounded return in any other position
84        /// makes the emitted element address different conductors.
85        EMIT_DSS_TERMINAL_ORDER_UNREPRESENTABLE = "EMIT.DSS.TERMINAL_ORDER_UNREPRESENTABLE", Error,
86            "a grounded return conductor is not last in a positional dss node list";
87
88        // READ.PMD.
89        READ_PMD_VALUE_CLAMPED = "READ.PMD.VALUE_CLAMPED", Warning,
90            "a PMD dimension beyond the supported maximum was clamped";
91        READ_PMD_VALUE_COLLAPSED = "READ.PMD.VALUE_COLLAPSED", Warning,
92            "a per terminal or per phase PMD value was collapsed to one entry";
93        READ_PMD_VALUE_DEFAULTED = "READ.PMD.VALUE_DEFAULTED", Warning,
94            "a PMD value the model needs was absent or unusable and was defaulted";
95        READ_PMD_RETAINED_SOURCE_ONLY = "READ.PMD.RETAINED_SOURCE_ONLY", Warning,
96            "a PMD field survives in extras rather than in a typed field";
97        READ_PMD_SOURCE_MALFORMED = "READ.PMD.SOURCE_MALFORMED", Warning,
98            "a PMD value is not the shape its key declares";
99        READ_PMD_RECORD_DROPPED = "READ.PMD.RECORD_DROPPED", Warning,
100            "a PMD object beyond the modeled set was dropped";
101        READ_PMD_VALUE_INLINED = "READ.PMD.VALUE_INLINED", Remark,
102            "an inline PMD impedance was materialized as a named linecode";
103
104        // EMIT.PMD.
105        EMIT_PMD_FIELD_DROPPED = "EMIT.PMD.FIELD_DROPPED", Warning,
106            "a field the PMD schema has no key for was dropped";
107        EMIT_PMD_RECORD_DROPPED = "EMIT.PMD.RECORD_DROPPED", Warning,
108            "an element the ENGINEERING document does not model was dropped";
109        EMIT_PMD_VALUE_CLAMPED = "EMIT.PMD.VALUE_CLAMPED", Warning,
110            "a conductor count beyond the supported maximum was clamped";
111        EMIT_PMD_VALUE_DEFAULTED = "EMIT.PMD.VALUE_DEFAULTED", Warning,
112            "a value the ENGINEERING schema requires was synthesized";
113        EMIT_PMD_VALUE_SUBSTITUTED = "EMIT.PMD.VALUE_SUBSTITUTED", Warning,
114            "a stated value was replaced by one the PMD schema can hold";
115
116        // READ.BMOPF.
117        /// A BMOPF field the schema types as a number holds something else.
118        /// Severity `Error`: the field reads as `NaN`, which serializes on as
119        /// an unbounded limit, so the parse states a fact the source never gave.
120        READ_BMOPF_FIELD_NOT_A_NUMBER = "READ.BMOPF.FIELD_NOT_A_NUMBER", Error,
121            "a BMOPF field the schema types as a number holds something else";
122        READ_BMOPF_FIELD_DROPPED = "READ.BMOPF.FIELD_DROPPED", Warning,
123            "a BMOPF field with no canonical home was dropped";
124        READ_BMOPF_RECORD_DROPPED = "READ.BMOPF.RECORD_DROPPED", Warning,
125            "a BMOPF object or winding beyond the modeled set was dropped";
126        READ_BMOPF_VALUE_COLLAPSED = "READ.BMOPF.VALUE_COLLAPSED", Warning,
127            "a per phase or per terminal BMOPF value was collapsed to one entry";
128        READ_BMOPF_TRANSFORMER_OPEN_DELTA_SPLIT = "READ.BMOPF.TRANSFORMER_OPEN_DELTA_SPLIT", Warning,
129            "one open_delta_regulator object reads as its two regulating legs, the second under a synthesized name";
130        READ_BMOPF_VALUE_DEFAULTED = "READ.BMOPF.VALUE_DEFAULTED", Warning,
131            "a BMOPF value the model needs was absent or unusable and was defaulted";
132        READ_BMOPF_VALUE_UNSUPPORTED = "READ.BMOPF.VALUE_UNSUPPORTED", Warning,
133            "a BMOPF enumeration value outside the schema was read as the nearest one";
134        READ_BMOPF_VALUE_INFERRED = "READ.BMOPF.VALUE_INFERRED", Warning,
135            "a BMOPF structure the schema does not name was reconstructed";
136        READ_BMOPF_RETAINED_SOURCE_ONLY = "READ.BMOPF.RETAINED_SOURCE_ONLY", Warning,
137            "a BMOPF field outside the schema survives in extras or untyped";
138        READ_BMOPF_SOURCE_MALFORMED = "READ.BMOPF.SOURCE_MALFORMED", Warning,
139            "a BMOPF value is not the shape its key declares";
140        /// The document states no `meta.$schema`, so the schema version it was
141        /// written against is unknown and the reader accepts both versions.
142        READ_BMOPF_SCHEMA_ABSENT = "READ.BMOPF.SCHEMA_ABSENT", Warning,
143            "a BMOPF document states no schema, so its version is unknown";
144        /// The document states a `meta.$schema` that names no known version.
145        READ_BMOPF_SCHEMA_UNKNOWN = "READ.BMOPF.SCHEMA_UNKNOWN", Warning,
146            "a BMOPF document states a schema naming no known version";
147        READ_BMOPF_SEMANTIC_INVALID = "READ.BMOPF.SEMANTIC_INVALID", Error,
148            "the BMOPF document has inconsistent dimensions, references, or bounds";
149        READ_BMOPF_SCHEMA_MISMATCH = "READ.BMOPF.SCHEMA_MISMATCH", Error,
150            "the schema URI and explicit schema version disagree";
151
152        // EMIT.BMOPF: the general families beside the nineteen transformer
153        // codes the writer already publishes.
154        EMIT_BMOPF_FIELD_DROPPED = "EMIT.BMOPF.FIELD_DROPPED", Warning,
155            "a field the BMOPF schema has no slot for was dropped";
156        EMIT_BMOPF_RECORD_DROPPED = "EMIT.BMOPF.RECORD_DROPPED", Warning,
157            "an object the emitted BMOPF document does not reference was dropped";
158        EMIT_BMOPF_VALUE_DEFAULTED = "EMIT.BMOPF.VALUE_DEFAULTED", Warning,
159            "a value the BMOPF schema requires was synthesized";
160        EMIT_BMOPF_RETAINED_SOURCE_ONLY = "EMIT.BMOPF.RETAINED_SOURCE_ONLY", Warning,
161            "a field with no schema slot was written under extras";
162        EMIT_BMOPF_VALUE_CLAMPED = "EMIT.BMOPF.VALUE_CLAMPED", Warning,
163            "a matrix dimension beyond the supported maximum was clamped";
164        EMIT_BMOPF_VALUE_SUBSTITUTED = "EMIT.BMOPF.VALUE_SUBSTITUTED", Warning,
165            "a stated value was replaced by one the BMOPF schema can hold";
166        EMIT_BMOPF_SOURCE_COUNT = "EMIT.BMOPF.SOURCE_COUNT", Warning,
167            "the BMOPF formulation expects exactly one voltage source";
168
169
170        // EMIT.BMOPF: the writer's element specific codes, registered
171        // entries rather than loose string literals.
172        EMIT_BMOPF_AUTOTRANSFORMER_DROPPED = "EMIT.BMOPF.AUTOTRANSFORMER_DROPPED", Warning,
173            "an autotransformer the BMOPF schema cannot state was dropped";
174        EMIT_BMOPF_BUS_LOCATION_DROPPED = "EMIT.BMOPF.BUS_LOCATION_DROPPED", Warning,
175            "a bus location the BMOPF schema cannot state was dropped";
176        EMIT_BMOPF_REGCONTROL_DROPPED = "EMIT.BMOPF.REGCONTROL_DROPPED", Warning,
177            "a regulator control the BMOPF schema cannot state was dropped";
178        EMIT_BMOPF_TRANSFORMER_CENTER_TAP_LEAKAGE_UNREPRESENTABLE =
179            "EMIT.BMOPF.TRANSFORMER_CENTER_TAP_LEAKAGE_UNREPRESENTABLE", Warning,
180            "a centre tap transformer's leakage split has no BMOPF spelling";
181        EMIT_BMOPF_TRANSFORMER_CENTER_TAP_NEUTRAL_COLLAPSED =
182            "EMIT.BMOPF.TRANSFORMER_CENTER_TAP_NEUTRAL_COLLAPSED", Warning,
183            "a centre tap transformer's neutral was collapsed to one BMOPF winding";
184        EMIT_BMOPF_TRANSFORMER_CENTER_TAP_RATING_COLLAPSED =
185            "EMIT.BMOPF.TRANSFORMER_CENTER_TAP_RATING_COLLAPSED", Warning,
186            "a centre tap transformer's per leg ratings were collapsed to one";
187        EMIT_BMOPF_TRANSFORMER_CENTER_TAP_TAP_COLLAPSED =
188            "EMIT.BMOPF.TRANSFORMER_CENTER_TAP_TAP_COLLAPSED", Warning,
189            "a centre tap transformer's per leg taps were collapsed to one";
190        EMIT_BMOPF_TRANSFORMER_CONNECTION_LOSSY = "EMIT.BMOPF.TRANSFORMER_CONNECTION_LOSSY",
191            Warning, "a transformer connection reads back differently through BMOPF";
192        EMIT_BMOPF_TRANSFORMER_EXTRA_DROPPED = "EMIT.BMOPF.TRANSFORMER_EXTRA_DROPPED", Warning,
193            "a transformer passthrough extra with no BMOPF slot was dropped";
194        EMIT_BMOPF_TRANSFORMER_MISSING_XSC = "EMIT.BMOPF.TRANSFORMER_MISSING_XSC", Warning,
195            "a transformer states no short circuit reactance for BMOPF to carry";
196        EMIT_BMOPF_TRANSFORMER_N_WINDING_RATING_COLLAPSED =
197            "EMIT.BMOPF.TRANSFORMER_N_WINDING_RATING_COLLAPSED", Warning,
198            "an n winding transformer's per winding ratings were collapsed to one";
199        EMIT_BMOPF_TRANSFORMER_NEUTRAL_DROPPED = "EMIT.BMOPF.TRANSFORMER_NEUTRAL_DROPPED",
200            Warning, "a transformer neutral the BMOPF schema cannot state was dropped";
201        EMIT_BMOPF_TRANSFORMER_NO_LOAD_SHUNT_DROPPED =
202            "EMIT.BMOPF.TRANSFORMER_NO_LOAD_SHUNT_DROPPED", Warning,
203            "a transformer no load shunt the BMOPF schema cannot state was dropped";
204        EMIT_BMOPF_TRANSFORMER_NO_LOAD_SHUNT_UNCONVERTIBLE =
205            "EMIT.BMOPF.TRANSFORMER_NO_LOAD_SHUNT_UNCONVERTIBLE", Warning,
206            "a transformer no load shunt could not be converted to the BMOPF form";
207        EMIT_BMOPF_TRANSFORMER_OPEN_DELTA_MERGED = "EMIT.BMOPF.TRANSFORMER_OPEN_DELTA_MERGED",
208            Warning, "two regulator legs merged into one open_delta_regulator object";
209        EMIT_BMOPF_TRANSFORMER_PER_PHASE_TAP_COLLAPSED =
210            "EMIT.BMOPF.TRANSFORMER_PER_PHASE_TAP_COLLAPSED", Warning,
211            "a transformer's per phase taps were collapsed to one BMOPF tap";
212        EMIT_BMOPF_TRANSFORMER_TAP_DROPPED = "EMIT.BMOPF.TRANSFORMER_TAP_DROPPED", Warning,
213            "a transformer tap the BMOPF schema cannot state was dropped";
214        EMIT_BMOPF_TRANSFORMER_UNSUPPORTED = "EMIT.BMOPF.TRANSFORMER_UNSUPPORTED", Warning,
215            "a transformer shape the BMOPF schema has no subtype for";
216        EMIT_BMOPF_TRANSFORMER_WINDINGS_CLAMPED = "EMIT.BMOPF.TRANSFORMER_WINDINGS_CLAMPED",
217            Warning, "a transformer's winding count was clamped to what BMOPF carries";
218        EMIT_BMOPF_TRANSFORMER_WYE_WYE_DECOMPOSED = "EMIT.BMOPF.TRANSFORMER_WYE_WYE_DECOMPOSED",
219            Warning, "a wye-wye transformer was decomposed into BMOPF pairs";
220
221        // The multiconductor model itself.
222        READ_MULTICONDUCTOR_VALUE_DEFAULTED = "READ.MULTICONDUCTOR.VALUE_DEFAULTED", Warning,
223            "a value the document never states was defaulted while reading";
224        VALIDATE_MULTICONDUCTOR_REFERENCE_UNDEFINED =
225            "VALIDATE.MULTICONDUCTOR.REFERENCE_UNDEFINED", Warning,
226            "an element references a bus or linecode the document does not declare";
227        EMIT_MULTICONDUCTOR_ROUTE_DROPPED = "EMIT.MULTICONDUCTOR.ROUTE_DROPPED", Warning,
228            "a line polyline was dropped because the target has no polyline field";
229        EMIT_MULTICONDUCTOR_SIDECAR_DROPPED = "EMIT.MULTICONDUCTOR.SIDECAR_DROPPED", Warning,
230            "a companion file the case text refers to was not written";
231
232        // Same-family distribution network projections.
233        TRANSFORM_DIST_NEUTRAL_KRON_REDUCED = "TRANSFORM.DIST.NEUTRAL_KRON_REDUCED", Remark,
234            "an explicit grounded neutral was eliminated by Kron reduction";
235        TRANSFORM_DIST_NEUTRAL_KRON_FORCED_GROUND =
236            "TRANSFORM.DIST.NEUTRAL_KRON_FORCED_GROUND", Warning,
237            "a neutral was treated as ideally grounded before Kron reduction";
238        TRANSFORM_DIST_KRON_UNTYPED_RETAINED = "TRANSFORM.DIST.KRON_UNTYPED_RETAINED", Warning,
239            "untyped source objects were retained unchanged through neutral reduction";
240
241        // Failures.
242        READ_DIST_IO_FAILED = "READ.DIST.IO_FAILED", Error,
243            "a distribution case file could not be read", category = Io;
244        REQUEST_DIST_FORMAT_UNKNOWN = "REQUEST.DIST_FORMAT.UNKNOWN", Error,
245            "the named distribution format is not one powerio reads",
246            category = Request;
247        TRANSFORM_DIST_KRON_REDUCTION_FAILED = "TRANSFORM.DIST.KRON_REDUCTION_FAILED", Error,
248            "a neutral Kron projection could not preserve the network semantics",
249            category = Data;
250    }
251}
252
253/// Every code this crate declares.
254#[must_use]
255pub fn registry() -> Vec<&'static DiagnosticInfo> {
256    codes::ALL.to_vec()
257}
258
259/// Attach an input-derived locator, or record its loss on the finding when it
260/// cannot be stored. A locator is identity, so it is never shortened; when the
261/// input supplies one past the record bounds, the finding says a locator
262/// existed and how long it was instead of silently changing identity.
263pub(crate) fn attach_target(diagnostic: &mut Diagnostic, target: String) {
264    let byte_length = target.len();
265    if diagnostic.set_target(target).is_err() {
266        let marker =
267            diagnostic.insert_detail("dropped_target_bytes", serde_json::Value::from(byte_length));
268        debug_assert!(marker.is_ok(), "the drop marker fits an emitted finding");
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn the_registry_is_sound() {
278        let problems = check_registry(registry());
279        assert!(problems.is_empty(), "{problems:#?}");
280    }
281}