arbiter-engine 0.1.0__py3-none-any.whl

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 (63) hide show
  1. arbiter_engine/__init__.py +31 -0
  2. arbiter_engine/api.py +473 -0
  3. arbiter_engine/axiom_thresholds.py +118 -0
  4. arbiter_engine/envelope.py +270 -0
  5. arbiter_engine/examples/water_tank.yaml +112 -0
  6. arbiter_engine/fire_frequency.py +252 -0
  7. arbiter_engine/history/__init__.py +12 -0
  8. arbiter_engine/history/observation.py +596 -0
  9. arbiter_engine/history/observation_production.py +335 -0
  10. arbiter_engine/history/observation_source_wiring.py +120 -0
  11. arbiter_engine/history/readiness.py +288 -0
  12. arbiter_engine/interfaces.py +1353 -0
  13. arbiter_engine/mcp/__init__.py +1 -0
  14. arbiter_engine/mcp/server.py +221 -0
  15. arbiter_engine/ontology/__init__.py +18 -0
  16. arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
  17. arbiter_engine/ontology/axioms/__init__.py +26 -0
  18. arbiter_engine/ontology/axioms/boundedness.py +340 -0
  19. arbiter_engine/ontology/axioms/connectivity.py +396 -0
  20. arbiter_engine/ontology/axioms/conservation.py +266 -0
  21. arbiter_engine/ontology/axioms/consistency.py +422 -0
  22. arbiter_engine/ontology/axioms/extensions.py +106 -0
  23. arbiter_engine/ontology/axioms/homeostasis.py +783 -0
  24. arbiter_engine/ontology/axioms/monotonicity.py +444 -0
  25. arbiter_engine/ontology/axioms/responsiveness.py +752 -0
  26. arbiter_engine/ontology/axioms/roles.py +207 -0
  27. arbiter_engine/ontology/axioms/stability.py +327 -0
  28. arbiter_engine/ontology/domain_loader.py +485 -0
  29. arbiter_engine/ontology/loader.py +802 -0
  30. arbiter_engine/ontology/reasoner.py +1069 -0
  31. arbiter_engine/propagation/__init__.py +1 -0
  32. arbiter_engine/propagation/impact_estimator.py +305 -0
  33. arbiter_engine/propagation/lp_confidence.py +123 -0
  34. arbiter_engine/propagation/mcts_root_cause.py +266 -0
  35. arbiter_engine/propagation/root_cause.py +559 -0
  36. arbiter_engine/propagation/weight_learner.py +216 -0
  37. arbiter_engine/rca/__init__.py +4 -0
  38. arbiter_engine/rca/greedy_set_cover.py +291 -0
  39. arbiter_engine/residual/__init__.py +1 -0
  40. arbiter_engine/residual/predict_vs_mirror.py +542 -0
  41. arbiter_engine/temporal/__init__.py +1 -0
  42. arbiter_engine/temporal/temporal_edge.py +584 -0
  43. arbiter_engine/temporal/trend_projection.py +430 -0
  44. arbiter_engine/twin/__init__.py +23 -0
  45. arbiter_engine/twin/action_clears_problem.py +217 -0
  46. arbiter_engine/twin/builder.py +479 -0
  47. arbiter_engine/twin/gap.py +132 -0
  48. arbiter_engine/twin/hypothesis_generator.py +530 -0
  49. arbiter_engine/twin/hypothesis_production.py +236 -0
  50. arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
  51. arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
  52. arbiter_engine/twin/optimization_production.py +233 -0
  53. arbiter_engine/twin/pipeline_production.py +176 -0
  54. arbiter_engine/twin/topology.py +450 -0
  55. arbiter_engine/twin/topology_optimizer.py +412 -0
  56. arbiter_engine/twin/traverser.py +1260 -0
  57. arbiter_engine/twin/traverser_production.py +283 -0
  58. arbiter_engine/types.py +427 -0
  59. arbiter_engine-0.1.0.dist-info/METADATA +278 -0
  60. arbiter_engine-0.1.0.dist-info/RECORD +63 -0
  61. arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
  62. arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
  63. arbiter_engine-0.1.0.dist-info/licenses/NOTICE +18 -0
@@ -0,0 +1,335 @@
1
+ """ProductionObservation production-readiness substrate (chain head).
2
+
3
+ Production-readiness substrate for the observation foundation modules at
4
+ a module held from this package (Redis Streams consumer) +
5
+ arbiter_engine/history/observation.py (InMemoryObservationHistory).
6
+
7
+ sibling-within-package precedent:
8
+ history/ dir hosts observation.py + readiness.py sibling modules with no
9
+ single shared parent module suitable for sibling extension. ships
10
+ substrate at new standalone module sibling-within-package (parent =
11
+ `arbiter_engine/history/`, sibling to `observation.py` + `readiness.py`) rather
12
+ than mutate any individual foundation module. an established pattern
13
+ sibling-substrate-at-axis-parent-level variant.
14
+
15
+ Adds per-observation production recording + 5 production-readiness public
16
+ functions + an established pattern env-gate. Composes hybrid
17
+ emit-policy decision (source-health-transition OR freshness_age >
18
+ threshold gate) + attestation severity floor +
19
+ NaturalCategoryDispatcher (emit_policy axis dispatch via existing 9th
20
+ canonical axis added; no new axis).
21
+
22
+ Domain-agnostic: observation_id + source_id + freshness_age_seconds
23
+ scalars opaque; no per-domain dispatch.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import threading
30
+ from dataclasses import dataclass
31
+ from datetime import datetime
32
+ from typing import Dict, List, Optional
33
+
34
+
35
+ def _env_bool(name: str, default: bool = False) -> bool:
36
+ raw = os.environ.get(name, "1" if default else "0").strip().lower()
37
+ return raw in ("1", "true", "yes", "on")
38
+
39
+
40
+ DT_OBSERVATION_PRODUCTION_ENABLED: bool = _env_bool(
41
+ "DT_OBSERVATION_PRODUCTION_ENABLED", default=False
42
+ )
43
+ DT_OBSERVATION_PRODUCTION_RING_CAP: int = int(
44
+ os.environ.get("DT_OBSERVATION_PRODUCTION_RING_CAP", "10000")
45
+ )
46
+ DT_OBSERVATION_FRESHNESS_THRESHOLD_SECONDS: float = float(
47
+ os.environ.get("DT_OBSERVATION_FRESHNESS_THRESHOLD_SECONDS", "60")
48
+ )
49
+
50
+ # Per hybrid emit-policy default
51
+ PRODUCTION_OBSERVATION_EMIT_POLICY_HYBRID: str = "hybrid"
52
+ PRODUCTION_OBSERVATION_EMIT_POLICY_FULL_EMIT: str = "full_emit"
53
+ PRODUCTION_OBSERVATION_EMIT_POLICY_SUPPRESSED: str = "suppressed"
54
+ KNOWN_PRODUCTION_OBSERVATION_EMIT_POLICIES = frozenset([
55
+ PRODUCTION_OBSERVATION_EMIT_POLICY_HYBRID,
56
+ PRODUCTION_OBSERVATION_EMIT_POLICY_FULL_EMIT,
57
+ PRODUCTION_OBSERVATION_EMIT_POLICY_SUPPRESSED,
58
+ ])
59
+ DEFAULT_PRODUCTION_OBSERVATION_EMIT_POLICY: str = (
60
+ PRODUCTION_OBSERVATION_EMIT_POLICY_HYBRID
61
+ )
62
+
63
+ # Per-source health-state vocabulary
64
+ SOURCE_HEALTH_HEALTHY: str = "healthy"
65
+ SOURCE_HEALTH_STALE: str = "stale"
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ProductionObservation:
70
+ """ per-observation production-readiness event.
71
+
72
+ 5 opaque fields domain-agnostic invariant. Frozen for
73
+ audit-trail provenance emit-policy decision.
74
+ """
75
+
76
+ observation_id: str
77
+ source_id: str
78
+ freshness_age_seconds: float
79
+ observed_at: datetime
80
+ emit_policy_per_cd1109: str
81
+ cluster_id: Optional[str] = None # (Bucket A) per-axis cluster-scope
82
+
83
+
84
+ def resolve_production_observation_emit_policy(value: Optional[str]) -> str:
85
+ """Safe-default to hybrid."""
86
+ if value is None or value not in KNOWN_PRODUCTION_OBSERVATION_EMIT_POLICIES:
87
+ return DEFAULT_PRODUCTION_OBSERVATION_EMIT_POLICY
88
+ return value
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # the ingest heartbeat.
93
+ #
94
+ # Separate from the ring ON PURPOSE. The ring answers "what was notable" and,
95
+ # under the default `hybrid` policy, records only health-state TRANSITIONS — so
96
+ # a healthy steady feed writes nothing and any liveness threshold on it produces
97
+ # false STALE. Measured on the reference VPS 2026-08-04 with the scraper running normally:
98
+ # observation_count=4, seconds_since_last_observation=208.
99
+ #
100
+ # This counter answers the ordinary question instead: "is anything arriving at
101
+ # all". It is bumped UPSTREAM of the emit-policy gate, because that gate is
102
+ # precisely what makes the ring unusable here — a heartbeat downstream of it
103
+ # would inherit the same silence.
104
+ # ---------------------------------------------------------------------------
105
+ _INGEST_COUNT_TOTAL: int = 0
106
+ _LAST_INGEST_AT: Optional[datetime] = None
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # WHO is ingesting?
111
+ #
112
+ # With the scraper stopped, `ingest_count_total` stayed flat for 57-90s and then
113
+ # jumped in one batch (+24, then +48 — the size scaling with the wait). So a
114
+ # second producer, or a buffered path, reaches observation history. The call
115
+ # graph does not answer it: `record_observation` has exactly ONE production
116
+ # caller (`InMemoryObservationHistory.add`), and that is not the answer, it is
117
+ # the funnel everything passes through.
118
+ #
119
+ # So: tag the real caller at runtime and count. Default OFF (the established pattern) —
120
+ # frame inspection on every ingest is not something to leave on by accident.
121
+ # ---------------------------------------------------------------------------
122
+ DT_INGEST_CALLER_TAG_ENABLED: bool = (
123
+ os.environ.get("DT_INGEST_CALLER_TAG_ENABLED", "0") not in ("0", "false", "False")
124
+ )
125
+
126
+ # Frames inside the ingest funnel itself; the interesting caller is above these.
127
+ _FUNNEL_MODULES = (
128
+ "detection.history.observation_production",
129
+ "detection.history.observation",
130
+ )
131
+
132
+ _INGEST_CALLER_TAGS: Dict[str, int] = {}
133
+
134
+
135
+ def _classify_ingest_caller() -> str:
136
+ """Walk up until we leave the funnel; return `module:qualname` of the caller.
137
+
138
+ Returns a sentinel rather than raising: an instrumentation failure must not
139
+ break ingest, and must be VISIBLE in the distribution rather than silently
140
+ attributed to some other bucket.
141
+ """
142
+ try:
143
+ import sys as _sys
144
+ frame = _sys._getframe(1)
145
+ depth = 0
146
+ while frame is not None and depth < 30:
147
+ mod = frame.f_globals.get("__name__", "")
148
+ if not any(f in mod for f in _FUNNEL_MODULES):
149
+ fn = frame.f_code.co_name
150
+ return "%s:%s" % (mod, fn)
151
+ frame = frame.f_back
152
+ depth += 1
153
+ return "<funnel-only:no-external-caller>"
154
+ except Exception: # noqa: BLE001
155
+ return "<classify-failed>"
156
+
157
+
158
+ def get_ingest_caller_tags() -> Dict[str, int]:
159
+ """Ingest counts by calling site. Empty when the gate is off."""
160
+ with _PRODUCTION_LOCK:
161
+ return dict(_INGEST_CALLER_TAGS)
162
+
163
+
164
+ def reset_ingest_caller_tags_for_test() -> None:
165
+ global _INGEST_CALLER_TAGS
166
+ with _PRODUCTION_LOCK:
167
+ _INGEST_CALLER_TAGS = {}
168
+
169
+
170
+ def _bump_ingest_heartbeat(now: Optional[datetime] = None) -> None:
171
+ """Record that an ingest reached the substrate. Unconditional by design."""
172
+ global _INGEST_COUNT_TOTAL, _LAST_INGEST_AT
173
+ tag = _classify_ingest_caller() if DT_INGEST_CALLER_TAG_ENABLED else None
174
+ with _PRODUCTION_LOCK:
175
+ _INGEST_COUNT_TOTAL += 1
176
+ _LAST_INGEST_AT = now or datetime.utcnow()
177
+ if tag is not None: #
178
+ _INGEST_CALLER_TAGS[tag] = _INGEST_CALLER_TAGS.get(tag, 0) + 1
179
+
180
+
181
+ def get_ingest_count_total() -> int:
182
+ """Total ingests seen since process start, regardless of emit policy."""
183
+ return _INGEST_COUNT_TOTAL
184
+
185
+
186
+ def get_seconds_since_last_ingest() -> Optional[float]:
187
+ """Seconds since the last ingest, or None if none has ever arrived.
188
+
189
+ None must be read as "nothing yet", NOT as "stale" — a consumer that
190
+ conflates them reports a cold start as an outage.
191
+ """
192
+ if _LAST_INGEST_AT is None:
193
+ return None
194
+ return max(0.0, (datetime.utcnow() - _LAST_INGEST_AT).total_seconds())
195
+
196
+
197
+ def reset_ingest_heartbeat_for_test() -> None:
198
+ """Test-only. Module-level state that nothing resets is what made the
199
+ ordering bug possible; this exists so pins cannot poison each other."""
200
+ global _INGEST_COUNT_TOTAL, _LAST_INGEST_AT
201
+ with _PRODUCTION_LOCK:
202
+ _INGEST_COUNT_TOTAL = 0
203
+ _LAST_INGEST_AT = None
204
+
205
+
206
+ def _derive_source_health(freshness_age_seconds: float) -> str:
207
+ """Source is healthy when fresh, stale when over threshold."""
208
+ if freshness_age_seconds > DT_OBSERVATION_FRESHNESS_THRESHOLD_SECONDS:
209
+ return SOURCE_HEALTH_STALE
210
+ return SOURCE_HEALTH_HEALTHY
211
+
212
+
213
+ _PRODUCTION_OBSERVATIONS: List[ProductionObservation] = []
214
+ _PRODUCTION_LOCK = threading.RLock()
215
+ _PRODUCTION_LAST_HEALTH: Dict[str, str] = {} # source_id -> health_state
216
+
217
+
218
+ def record_observation(
219
+ observation_id: str,
220
+ source_id: str,
221
+ freshness_age_seconds: float,
222
+ observed_at: Optional[datetime] = None,
223
+ emit_policy: Optional[str] = None,
224
+ cluster_id: Optional[str] = None,
225
+ ) -> Optional[ProductionObservation]:
226
+ """Record an observation event at production-readiness shape.
227
+
228
+ Returns the stored ProductionObservation when gate enabled AND
229
+ emit_policy admits the event; returns None when gate off OR
230
+ emit_policy suppressed OR hybrid mode rejects (source-health-transition OR freshness_age > threshold) gate.
231
+
232
+ Hybrid mode gate: admits if (a) prior health-state for source_id
233
+ differs from current (derived from freshness_age vs threshold), OR
234
+ (b) freshness_age > threshold (stale).
235
+ """
236
+ if not DT_OBSERVATION_PRODUCTION_ENABLED:
237
+ return None
238
+ # heartbeat FIRST. Every ingest that reaches a live substrate counts,
239
+ # whatever the policy then decides to store. Moving this below the policy
240
+ # check would reproduce the exact silence it exists to detect.
241
+ _bump_ingest_heartbeat(observed_at)
242
+ policy = resolve_production_observation_emit_policy(emit_policy)
243
+ if policy == PRODUCTION_OBSERVATION_EMIT_POLICY_SUPPRESSED:
244
+ return None
245
+ ts = observed_at or datetime.utcnow()
246
+ current_health = _derive_source_health(freshness_age_seconds)
247
+ if policy == PRODUCTION_OBSERVATION_EMIT_POLICY_HYBRID:
248
+ with _PRODUCTION_LOCK:
249
+ prior_health = _PRODUCTION_LAST_HEALTH.get(source_id)
250
+ health_transition = (prior_health is None) or (prior_health != current_health)
251
+ freshness_stale = current_health == SOURCE_HEALTH_STALE
252
+ if not (health_transition or freshness_stale):
253
+ return None
254
+ record = ProductionObservation(
255
+ observation_id=observation_id,
256
+ source_id=source_id,
257
+ freshness_age_seconds=float(freshness_age_seconds),
258
+ observed_at=ts,
259
+ emit_policy_per_cd1109=policy,
260
+ cluster_id=cluster_id, # (Bucket A)
261
+ )
262
+ with _PRODUCTION_LOCK:
263
+ _PRODUCTION_OBSERVATIONS.append(record)
264
+ _PRODUCTION_LAST_HEALTH[source_id] = current_health
265
+ if len(_PRODUCTION_OBSERVATIONS) > DT_OBSERVATION_PRODUCTION_RING_CAP:
266
+ del _PRODUCTION_OBSERVATIONS[
267
+ : len(_PRODUCTION_OBSERVATIONS) - DT_OBSERVATION_PRODUCTION_RING_CAP
268
+ ]
269
+ return record
270
+
271
+
272
+ def _filter_by_cluster_id_cd1436(
273
+ observations: List[ProductionObservation],
274
+ cluster_id: Optional[str],
275
+ ) -> List[ProductionObservation]:
276
+ """ helper: filter observations by cluster_id when non-None.
277
+
278
+ cluster_id=None returns the full list (backward compat). cluster_id="X"
279
+ returns only observations with ``o.cluster_id == "X"``. Records emitted
280
+ previously (or from cross-cluster merge / transient histories) carry
281
+ cluster_id=None and are excluded from a specific-cluster query. Mirror
282
+ of the RCA / axiom_verdicts pattern.
283
+ """
284
+ if cluster_id is None:
285
+ return list(observations)
286
+ return [o for o in observations if o.cluster_id == cluster_id]
287
+
288
+
289
+ def get_observations(cluster_id: Optional[str] = None) -> List[ProductionObservation]:
290
+ """All recorded production observation records. Empty when gate off.
291
+
292
+ optional ``cluster_id`` filter. None = all (backward compat);
293
+ string value returns only observations stamped with that cluster_id.
294
+ """
295
+ if not DT_OBSERVATION_PRODUCTION_ENABLED:
296
+ return []
297
+ with _PRODUCTION_LOCK:
298
+ return _filter_by_cluster_id_cd1436(_PRODUCTION_OBSERVATIONS, cluster_id)
299
+
300
+
301
+ def get_observation_count(cluster_id: Optional[str] = None) -> int:
302
+ """Aggregate count of recorded production observation records.
303
+
304
+ Dashboard-data defensive-accessor entry point + Pattern
305
+ 171. Returns 0 when gate off.
306
+
307
+ optional ``cluster_id`` filter. None = aggregate count;
308
+ string value returns count of observations stamped with that cluster_id.
309
+ """
310
+ if not DT_OBSERVATION_PRODUCTION_ENABLED:
311
+ return 0
312
+ with _PRODUCTION_LOCK:
313
+ return len(_filter_by_cluster_id_cd1436(_PRODUCTION_OBSERVATIONS, cluster_id))
314
+
315
+
316
+ def get_freshness_for_source(source_id: str) -> Optional[str]:
317
+ """Last-known health-state for source_id; None when unknown or gate off."""
318
+ if not DT_OBSERVATION_PRODUCTION_ENABLED:
319
+ return None
320
+ with _PRODUCTION_LOCK:
321
+ return _PRODUCTION_LAST_HEALTH.get(source_id)
322
+
323
+
324
+ def known_observation_sources() -> List[str]:
325
+ """Diagnostic accessor — sorted unique source_id values."""
326
+ if not DT_OBSERVATION_PRODUCTION_ENABLED:
327
+ return []
328
+ with _PRODUCTION_LOCK:
329
+ return sorted({r.source_id for r in _PRODUCTION_OBSERVATIONS})
330
+
331
+
332
+ def _reset_production_observations_for_tests() -> None:
333
+ with _PRODUCTION_LOCK:
334
+ _PRODUCTION_OBSERVATIONS.clear()
335
+ _PRODUCTION_LAST_HEALTH.clear()
@@ -0,0 +1,120 @@
1
+ """cross-source observation wire-up.
2
+
3
+ Composes multi-source observations (Redis Streams + InMemory + TimescaleDB
4
+ + document pipeline) into per-entity source-lineage aggregations for the
5
+ ProductionObservation pipeline.
6
+
7
+ separation-of-concerns: wire-up is stateless across
8
+ wire_observation_sources_for_entity() calls; tracks per-entity source-id
9
+ set for dashboard accessor entry point. Defensive: gate off →
10
+ wire-up no-op; ProductionObservation pipeline operates on raw per-source
11
+ records (hybrid emit gates per-source via record_observation).
12
+
13
+ an established pattern env-gate `DT_OBSERVATION_SOURCE_WIRING_ENABLED`.
14
+
15
+ the established pattern wire-up-as-standalone-module discipline — over-saturated reference
16
+ architecture (beyond the established pattern threshold).
17
+
18
+ Domain-agnostic: entity_id + source_id scalars opaque; no per-domain
19
+ dispatch.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import threading
26
+ from typing import Dict, List, Optional, Set
27
+
28
+
29
+ def _env_bool(name: str, default: bool = False) -> bool:
30
+ raw = os.environ.get(name, "1" if default else "0").strip().lower()
31
+ return raw in ("1", "true", "yes", "on")
32
+
33
+
34
+ DT_OBSERVATION_SOURCE_WIRING_ENABLED: bool = _env_bool(
35
+ "DT_OBSERVATION_SOURCE_WIRING_ENABLED", default=False
36
+ )
37
+
38
+
39
+ # entity_id -> Set[source_id] (per-entity source-lineage set)
40
+ _WIRED_SOURCES: Dict[str, Set[str]] = {}
41
+ _WIRING_LOCK = threading.RLock()
42
+
43
+
44
+ def wire_observation_sources_for_entity(
45
+ entity_id: str,
46
+ source_ids: List[str],
47
+ ) -> bool:
48
+ """Register an entity's per-source observation lineage.
49
+
50
+ source_ids: list of source_id strings observed for this entity.
51
+
52
+ Returns True when wired (gate enabled); False when gate off (no-op).
53
+ Per spec: this wiring computes per-entity source-lineage set
54
+ (union of observed source_ids); operators consume via
55
+ get_source_lineage_for_entity for partner-facing source-attribution
56
+ surfaces.
57
+ """
58
+ if not DT_OBSERVATION_SOURCE_WIRING_ENABLED:
59
+ return False
60
+ if not source_ids:
61
+ return False
62
+ with _WIRING_LOCK:
63
+ _WIRED_SOURCES[entity_id] = set(source_ids)
64
+ return True
65
+
66
+
67
+ def get_source_lineage_for_entity(
68
+ entity_id: str,
69
+ ) -> Optional[List[str]]:
70
+ """Look up wired source-lineage list for an entity.
71
+
72
+ Returns None when gate off OR entity not wired.
73
+ Returns sorted source_ids list (deterministic ordering) when wired.
74
+ """
75
+ if not DT_OBSERVATION_SOURCE_WIRING_ENABLED:
76
+ return None
77
+ with _WIRING_LOCK:
78
+ lineage = _WIRED_SOURCES.get(entity_id)
79
+ if lineage is None:
80
+ return None
81
+ return sorted(lineage)
82
+
83
+
84
+ def get_source_count_for_entity(entity_id: str) -> Optional[int]:
85
+ """Look up source count for an entity.
86
+
87
+ Returns None when gate off OR entity not wired.
88
+ """
89
+ if not DT_OBSERVATION_SOURCE_WIRING_ENABLED:
90
+ return None
91
+ with _WIRING_LOCK:
92
+ lineage = _WIRED_SOURCES.get(entity_id)
93
+ if lineage is None:
94
+ return None
95
+ return len(lineage)
96
+
97
+
98
+ def get_observation_source_wired_entity_count() -> int:
99
+ """Aggregate count of source-wired entities.
100
+
101
+ Dashboard-data defensive-accessor entry point + Pattern
102
+ 171. Returns 0 when gate off.
103
+ """
104
+ if not DT_OBSERVATION_SOURCE_WIRING_ENABLED:
105
+ return 0
106
+ with _WIRING_LOCK:
107
+ return len(_WIRED_SOURCES)
108
+
109
+
110
+ def known_source_wired_entities() -> List[str]:
111
+ """Diagnostic accessor — sorted list of source-wired entity_ids."""
112
+ if not DT_OBSERVATION_SOURCE_WIRING_ENABLED:
113
+ return []
114
+ with _WIRING_LOCK:
115
+ return sorted(_WIRED_SOURCES.keys())
116
+
117
+
118
+ def _reset_wired_sources_for_tests() -> None:
119
+ with _WIRING_LOCK:
120
+ _WIRED_SOURCES.clear()