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.
- arbiter_engine/__init__.py +31 -0
- arbiter_engine/api.py +473 -0
- arbiter_engine/axiom_thresholds.py +118 -0
- arbiter_engine/envelope.py +270 -0
- arbiter_engine/examples/water_tank.yaml +112 -0
- arbiter_engine/fire_frequency.py +252 -0
- arbiter_engine/history/__init__.py +12 -0
- arbiter_engine/history/observation.py +596 -0
- arbiter_engine/history/observation_production.py +335 -0
- arbiter_engine/history/observation_source_wiring.py +120 -0
- arbiter_engine/history/readiness.py +288 -0
- arbiter_engine/interfaces.py +1353 -0
- arbiter_engine/mcp/__init__.py +1 -0
- arbiter_engine/mcp/server.py +221 -0
- arbiter_engine/ontology/__init__.py +18 -0
- arbiter_engine/ontology/axiom_verdicts_production.py +332 -0
- arbiter_engine/ontology/axioms/__init__.py +26 -0
- arbiter_engine/ontology/axioms/boundedness.py +340 -0
- arbiter_engine/ontology/axioms/connectivity.py +396 -0
- arbiter_engine/ontology/axioms/conservation.py +266 -0
- arbiter_engine/ontology/axioms/consistency.py +422 -0
- arbiter_engine/ontology/axioms/extensions.py +106 -0
- arbiter_engine/ontology/axioms/homeostasis.py +783 -0
- arbiter_engine/ontology/axioms/monotonicity.py +444 -0
- arbiter_engine/ontology/axioms/responsiveness.py +752 -0
- arbiter_engine/ontology/axioms/roles.py +207 -0
- arbiter_engine/ontology/axioms/stability.py +327 -0
- arbiter_engine/ontology/domain_loader.py +485 -0
- arbiter_engine/ontology/loader.py +802 -0
- arbiter_engine/ontology/reasoner.py +1069 -0
- arbiter_engine/propagation/__init__.py +1 -0
- arbiter_engine/propagation/impact_estimator.py +305 -0
- arbiter_engine/propagation/lp_confidence.py +123 -0
- arbiter_engine/propagation/mcts_root_cause.py +266 -0
- arbiter_engine/propagation/root_cause.py +559 -0
- arbiter_engine/propagation/weight_learner.py +216 -0
- arbiter_engine/rca/__init__.py +4 -0
- arbiter_engine/rca/greedy_set_cover.py +291 -0
- arbiter_engine/residual/__init__.py +1 -0
- arbiter_engine/residual/predict_vs_mirror.py +542 -0
- arbiter_engine/temporal/__init__.py +1 -0
- arbiter_engine/temporal/temporal_edge.py +584 -0
- arbiter_engine/temporal/trend_projection.py +430 -0
- arbiter_engine/twin/__init__.py +23 -0
- arbiter_engine/twin/action_clears_problem.py +217 -0
- arbiter_engine/twin/builder.py +479 -0
- arbiter_engine/twin/gap.py +132 -0
- arbiter_engine/twin/hypothesis_generator.py +530 -0
- arbiter_engine/twin/hypothesis_production.py +236 -0
- arbiter_engine/twin/kernel_pipeline_executor.py +571 -0
- arbiter_engine/twin/monte_carlo_predictor.py +1001 -0
- arbiter_engine/twin/optimization_production.py +233 -0
- arbiter_engine/twin/pipeline_production.py +176 -0
- arbiter_engine/twin/topology.py +450 -0
- arbiter_engine/twin/topology_optimizer.py +412 -0
- arbiter_engine/twin/traverser.py +1260 -0
- arbiter_engine/twin/traverser_production.py +283 -0
- arbiter_engine/types.py +427 -0
- arbiter_engine-0.1.0.dist-info/METADATA +278 -0
- arbiter_engine-0.1.0.dist-info/RECORD +63 -0
- arbiter_engine-0.1.0.dist-info/WHEEL +4 -0
- arbiter_engine-0.1.0.dist-info/licenses/LICENSE +202 -0
- 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()
|