yieldix 16.0.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.
yieldix/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ Yieldix: Autonomous B2B Revenue Engine & Speed-to-Lead Orchestrator.
3
+ Version: 16.0.0
4
+ """
5
+
6
+ __version__ = "16.0.0"
7
+ __author__ = "Yieldix Contributors"
8
+
9
+ from yieldix.core.circuit_breaker import CircuitBreaker
10
+ from yieldix.core.engine import YieldixEngine
11
+ from yieldix.core.types import (
12
+ BANTScore,
13
+ InboundLeadPayload,
14
+ LeadSource,
15
+ MonthlyReportPayload,
16
+ PipelineConfig,
17
+ TaskStatus,
18
+ )
19
+
20
+ __all__ = [
21
+ "BANTScore",
22
+ "CircuitBreaker",
23
+ "InboundLeadPayload",
24
+ "LeadSource",
25
+ "MonthlyReportPayload",
26
+ "PipelineConfig",
27
+ "TaskStatus",
28
+ "YieldixEngine",
29
+ "__version__",
30
+ ]
yieldix/cli/main.py ADDED
@@ -0,0 +1,449 @@
1
+ """
2
+ Yieldix Command Line Interface (CLI).
3
+ Enterprise execution, lead testing, L2 human review, and Monte Carlo simulation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import random
11
+ from decimal import Decimal
12
+
13
+ import click
14
+
15
+ from yieldix.core.engine import YieldixEngine
16
+ from yieldix.core.types import InboundLeadPayload, LeadSource, PipelineConfig
17
+ from yieldix.telemetry.reporter import MonthlyReportGenerator
18
+
19
+
20
+ # DALGA-C-2 (YIELDIX-CLI-3): click 8.5'te Decimal-parametre yoktur;.geçersiz
21
+ # sayısal girdi ham decimal.InvalidOperation yerine click rc=2 olmalı.
22
+ class _DecimalParam(click.ParamType):
23
+ """Geçerli bir Decimal'e çevirir; çevrilemezse UsageError (rc=2)."""
24
+
25
+ name = "decimal"
26
+
27
+ def convert(self, value, param, ctx):
28
+ if isinstance(value, Decimal):
29
+ return value
30
+ try:
31
+ return Decimal(str(value))
32
+ except Exception:
33
+ self.fail(f"{value!r} geçerli bir Decimal sayı değil", param, ctx)
34
+
35
+
36
+ _DECIMAL_PARAM = _DecimalParam()
37
+
38
+
39
+ @click.group()
40
+ @click.version_option("16.0.0", prog_name="yieldix")
41
+ def main():
42
+ """🦄 Yieldix: Autonomous B2B Revenue Engine CLI."""
43
+
44
+
45
+ @main.command(name="status")
46
+ @click.option("--tenant", default="cust_enterprise_01", help="Tenant ID")
47
+ def status(tenant: str):
48
+ """Displays engine and circuit breaker status."""
49
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
50
+ cycle = asyncio.run(engine.run_daily_cycle())
51
+ click.echo(f"=== YIELDIX ENGINE STATUS: {tenant} ===")
52
+ click.echo(f"Active Components: {', '.join(cycle['active_components'])}")
53
+ click.echo(f"Shed Components: {', '.join(cycle['shed_components']) if cycle['shed_components'] else 'None'}")
54
+ click.echo(f"Circuit Breaker Tripped: {cycle['circuit_breaker_tripped']}")
55
+
56
+
57
+ @main.command(name="ingest")
58
+ @click.option("--tenant", default="cust_enterprise_01", help="Tenant ID")
59
+ @click.option("--name", required=True, help="Contact name")
60
+ @click.option("--phone", required=True, help="Contact phone")
61
+ @click.option("--intent", default="Kurumsal B2B teklif talebi", help="Lead intent")
62
+ def ingest(tenant: str, name: str, phone: str, intent: str):
63
+ """Ingests a lead and triggers the sub-60s speed-to-lead flow."""
64
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
65
+ lead = InboundLeadPayload(
66
+ tenant_id=tenant,
67
+ lead_id=f"lead_{int(random.random()*10000)}",
68
+ contact_name=name,
69
+ contact_phone=phone,
70
+ source=LeadSource.WEB_FORM,
71
+ intent_summary=intent,
72
+ )
73
+ result = asyncio.run(engine.ingest_inbound_lead(lead))
74
+ click.echo(json.dumps(result, indent=2, ensure_ascii=False))
75
+
76
+
77
+ @main.command(name="simulate")
78
+ @click.option("--runs", default=10000, type=click.IntRange(min=1),
79
+ help="Number of Monte Carlo simulation runs (>=1)")
80
+ @click.option("--retainer", default="2500.00", type=_DECIMAL_PARAM,
81
+ help="Monthly retainer in USD")
82
+ def simulate(runs: int, retainer: str | float):
83
+ """Executes N=10,000 Monte Carlo financial stress simulation."""
84
+ retainer_dec = retainer if isinstance(retainer, Decimal) else Decimal(str(retainer))
85
+ click.echo(f"Running Monte Carlo simulation ({runs} runs, Retainer=${retainer_dec:.2f})...")
86
+ profits = []
87
+ base_cogs = Decimal("90.00")
88
+ for _ in range(runs):
89
+ leads = max(100.0, random.gauss(450.0, 90.0))
90
+ api_multiplier = Decimal(str(round(random.uniform(0.85, 1.40), 4)))
91
+ escalation_pct = max(1.0, min(30.0, random.gauss(8.5, 3.2)))
92
+ lead_ratio = Decimal(str(round(leads / 450.0, 4)))
93
+ variable_cogs = base_cogs * lead_ratio * api_multiplier
94
+ penalty = Decimal("0.00")
95
+ if escalation_pct > 20.0:
96
+ esc_excess = Decimal(str(round((escalation_pct - 20.0) / 10.0, 4)))
97
+ penalty = Decimal("150.00") * esc_excess
98
+ net_profit = retainer_dec - (variable_cogs + penalty)
99
+ profits.append(net_profit)
100
+
101
+ profits.sort()
102
+ p5 = profits[int(runs * 0.05)]
103
+ p50 = profits[int(runs * 0.50)]
104
+ p95 = profits[int(runs * 0.95)]
105
+ loss_count = sum(1 for p in profits if p <= Decimal("0.00"))
106
+
107
+ click.echo("=== SIMULATION RESULTS ===")
108
+ click.echo(f"P5 (Worst Case): ${p5:.2f}")
109
+ click.echo(f"P50 (Median): ${p50:.2f} (Margin: %{(p50/retainer_dec)*100:.2f})")
110
+ click.echo(f"P95 (Best Case): ${p95:.2f}")
111
+ click.echo(f"Loss Probability: %{(loss_count/runs)*100:.2f}")
112
+
113
+
114
+ @main.command(name="report")
115
+ @click.option("--tenant", default="cust_enterprise_01", help="Tenant ID")
116
+ @click.option("--start", default="2026-10-01", help="Period start date")
117
+ @click.option("--end", default="2026-10-31", help="Period end date")
118
+ def report(tenant: str, start: str, end: str):
119
+ """Generates an Ed25519 signed monthly audit report card."""
120
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
121
+ # Ingest baseline transaction event
122
+ engine.kpi_collector.record_lead_processed("lead_demo", 32.5, is_sql=True, cost_try=Decimal("15.00"))
123
+ rep_gen = MonthlyReportGenerator()
124
+ rep = rep_gen.generate_signed_report(
125
+ tenant_id=tenant,
126
+ period_start=start,
127
+ period_end=end,
128
+ kpi_collector=engine.kpi_collector,
129
+ active_components=engine.config.enabled_components,
130
+ )
131
+ card = rep_gen.export_markdown_card(rep)
132
+ click.echo(card)
133
+
134
+
135
+ @main.command(name="serve")
136
+ @click.option("--host", default="127.0.0.1", help="Host interface to bind")
137
+ @click.option("--port", default=8088, type=int, help="Port to listen on")
138
+ @click.option("--no-block", is_flag=True, default=False, help="Launch non-blocking smoke run")
139
+ def serve(host: str, port: int, no_block: bool = False):
140
+ """Launches the sovereign Yieldix web telemetry and cockpit server."""
141
+ from yieldix.server.app import create_server
142
+ click.echo(f"Starting Yieldix Cockpit on http://{host}:{port}")
143
+ server = create_server(host=host, port=port)
144
+ server.start(blocking=not no_block)
145
+ if no_block:
146
+ server.stop()
147
+
148
+
149
+ @main.command(name="pilot")
150
+ @click.option("--tenant", default="pilot-demo-tenant", help="Tenant ID for the pilot run")
151
+ @click.option("--leads-per-company", default=25, help="Synthetic leads per pilot company")
152
+ @click.option("--seed", default=1337, help="Deterministic seed for replayability")
153
+ @click.option("--out", default=None, help="Write the synthetic dataset JSON to this path")
154
+ def pilot(tenant: str, leads_per_company: int, seed: int, out):
155
+ """
156
+ Generates a synthetic-but-realistic B2B pilot dataset and proves the full
157
+ lead → KPI → signed-report chain with quantified CPL/SQL values.
158
+
159
+ Dürüst-not: bu veri SENTETİKTİR (gerçek müşteri verisi gizlilik nedeniyle
160
+ kullanılamaz). Amacı CPL/SQL hesaplamasının kanıtlanabilir olduğunu
161
+ imzalı-raporla göstermektir.
162
+ """
163
+ from yieldix.telemetry.pilot_dataset import (
164
+ SyntheticPilotDataset, PILOT_COMPANIES,
165
+ )
166
+ ds = SyntheticPilotDataset(leads_per_company=leads_per_company, seed=seed)
167
+ records = ds.generate()
168
+
169
+ report, fingerprint, pubkey = ds.build_signed_report(records, tenant_id=tenant)
170
+
171
+ # Bağımsız-imza-doğrulaması: imzayı üreten-anahtarla değil, açık-anahtarla
172
+ # yeniden-teyit et ( tahrif-direnci kanıtı).
173
+ from yieldix.crypto.signer import Ed25519ReportSigner
174
+ payload = MonthlyReportGenerator.to_signable_dict(report)
175
+ sig_ok = Ed25519ReportSigner.verify_signature(
176
+ payload, report.ed25519_signature, pubkey)
177
+ tampered = dict(payload)
178
+ tampered["qualified_sql"] = (tampered["qualified_sql"] or 0) + 999
179
+ tamper_rejected = not Ed25519ReportSigner.verify_signature(
180
+ tampered, report.ed25519_signature, pubkey)
181
+
182
+ click.echo("=" * 70)
183
+ click.echo(" YIELDIX SENTETİK-PILOT-VERİ SETİ ( dürüst-sınır kapatması)")
184
+ click.echo("=" * 70)
185
+ click.echo(f" Şirket sayısı: {len(PILOT_COMPANIES)} kurgusal B2B şirket")
186
+ click.echo(f" Lead sayısı: {len(records)} ( deterministik-seed={seed})")
187
+ click.echo(f" Şirket başına lead: {leads_per_company}")
188
+ click.echo("-" * 70)
189
+ click.echo(" Quantified pilot KPI'lar ( imzalı-rapor kaynağı):")
190
+ click.echo(f" Toplam lead: {report.total_leads}")
191
+ click.echo(f" SQL (nitelikli): {report.qualified_sql} "
192
+ f"( %{(report.qualified_sql / max(1, report.total_leads)) * 100:.1f} dönüşüm)")
193
+ click.echo(f" CPL (lead başı): ₺{report.cost_per_lead_try}")
194
+ click.echo(f" P95 hız-yanıt: {report.p95_speed_to_lead_seconds:.1f} sn")
195
+ click.echo(f" Hata oranı: %{report.error_rate_pct:.2f}")
196
+ click.echo(f" Eskalasyon oranı: %{report.escalation_rate_pct:.2f}")
197
+ click.echo("-" * 70)
198
+ click.echo(f" Veri-seti parmakizi: {fingerprint[:24]}…")
199
+ click.echo(f" SHA-256 özet: {(report.sha256_digest or 'YOK')[:24]}…")
200
+ click.echo(f" Ed25519 imza: {'var' if report.ed25519_signature else 'YOK'}")
201
+ click.echo(f" İmza-bağımsız-doğrula:{'✓ geçti' if sig_ok else '✗ BAŞARISIZ'}")
202
+ click.echo(f" Tahrif-reddi: {'✓ reddetti' if tamper_rejected else '✗ BAŞARISIZ'}")
203
+
204
+ if out:
205
+ p = SyntheticPilotDataset.write_json(records, out)
206
+ click.echo(f" Veri seti yazıldı: {p}")
207
+ click.echo("=" * 70)
208
+
209
+
210
+ @main.command(name="verify")
211
+ @click.option("--tenant", default="verify-tenant", help="Tenant ID for the verification run")
212
+ @click.option("--json", "as_json", is_flag=True, default=False, help="Machine-readable JSON output")
213
+ def verify(tenant: str, as_json: bool):
214
+ """
215
+ Bağımsız-doğrulama CLI'si ( kanıt-entegrasyonu).
216
+
217
+ lead → silindirler → KPI → imzalı-rapor zincirini sıfırdan-oynar ve
218
+ Ed25519-imzasını bağımsız olarak yeniden-doğrular. Demo-komutlarının
219
+ paylaştığı-durumu kullanmaz; her-geçidi ayrı-ölçer.
220
+
221
+ Exit-code: 0 = hepsi-geçti, 1 = en-az-bir-bozuk.
222
+ """
223
+ checks: list[dict] = []
224
+
225
+ def record(name, ok, detail):
226
+ checks.append({"check": name, "result": "PASS" if ok else "FAIL", "detail": detail})
227
+
228
+ # 1) Pipeline-koşusu: lead-ingest → döngü → KPI-toplandı
229
+ try:
230
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
231
+ lead = InboundLeadPayload(
232
+ tenant_id=tenant,
233
+ lead_id="lead_verify_001",
234
+ contact_name="Verification Contact",
235
+ contact_phone="+15550001111",
236
+ source=LeadSource.WEB_FORM,
237
+ intent_summary="Verification intent — enterprise B2B quote",
238
+ )
239
+ ingest_res = asyncio.run(engine.ingest_inbound_lead(lead))
240
+ cycle = asyncio.run(engine.run_daily_cycle())
241
+ ok = bool(ingest_res) and len(cycle.get("active_components", [])) > 0
242
+ record("pipeline_runs_lead_to_cycle", ok,
243
+ f"ingested={bool(ingest_res)} active={len(cycle.get('active_components', []))} "
244
+ f"shed={len(cycle.get('shed_components', []))}")
245
+ except Exception as e: # pragma: no cover - diagnostic
246
+ record("pipeline_runs_lead_to_cycle", False, f"exception: {type(e).__name__}: {e}")
247
+
248
+ # 2) Kanıt-üretimi: imzalı-rapor-üretiliyor + SHA-256-özeti-var
249
+ try:
250
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
251
+ engine.kpi_collector.record_lead_processed(
252
+ "lead_verify_001", 32.5, is_sql=True, cost_try=Decimal("15.00"))
253
+ gen = MonthlyReportGenerator()
254
+ rep = gen.generate_signed_report(
255
+ tenant_id=tenant, period_start="2026-10-01", period_end="2026-10-31",
256
+ kpi_collector=engine.kpi_collector,
257
+ active_components=engine.config.enabled_components,
258
+ )
259
+ digest = getattr(rep, "sha256_digest", None)
260
+ sig = getattr(rep, "ed25519_signature", None)
261
+ pubkey = gen.signer.public_key_hex if gen.signer else None
262
+ ok = bool(digest) and bool(sig) and bool(pubkey)
263
+ record("signed_report_produced", ok,
264
+ f"digest={(digest or 'YOK')[:24]}… sig={'var' if sig else 'YOK'} key={'var' if pubkey else 'YOK'}")
265
+ except Exception as e: # pragma: no cover - diagnostic
266
+ record("signed_report_produced", False, f"exception: {type(e).__name__}: {e}")
267
+
268
+ # 3) Bağımsız-imza-doğrulaması: imza, payload-üzerinden yeniden-teyit
269
+ try:
270
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
271
+ engine.kpi_collector.record_lead_processed(
272
+ "lead_verify_001", 32.5, is_sql=True, cost_try=Decimal("15.00"))
273
+ gen = MonthlyReportGenerator()
274
+ rep = gen.generate_signed_report(
275
+ tenant_id=tenant, period_start="2026-10-01", period_end="2026-10-31",
276
+ kpi_collector=engine.kpi_collector,
277
+ active_components=engine.config.enabled_components,
278
+ )
279
+ from yieldix.crypto.signer import Ed25519ReportSigner
280
+ payload = MonthlyReportGenerator.to_signable_dict(rep)
281
+ sig = getattr(rep, "ed25519_signature", None)
282
+ pubkey = gen.signer.public_key_hex if gen.signer else None
283
+ verified = Ed25519ReportSigner.verify_signature(payload, sig, pubkey)
284
+ # Tahrif-direnci: imzalı-payload-değiştirilince imza artık-onaylanmamalı
285
+ tampered = dict(payload)
286
+ if "total_leads" in tampered:
287
+ tampered["total_leads"] = (tampered["total_leads"] or 0) + 999
288
+ tampered_ok = not Ed25519ReportSigner.verify_signature(tampered, sig, pubkey)
289
+ ok = bool(verified) and tampered_ok
290
+ record("signature_verifies_and_tamper_resistant", ok,
291
+ f"verify={bool(verified)} tamper-rejected={tampered_ok}")
292
+ except Exception as e: # pragma: no cover - diagnostic
293
+ record("signature_verifies_and_tamper_resistant", False,
294
+ f"exception: {type(e).__name__}: {e}")
295
+
296
+ # 4) Tenant-izolasyonu: AT-179 — configsiz-motor-default-tenant-reddeder
297
+ try:
298
+ import os
299
+ old = os.environ.pop("YIELDIX_TENANT_ID", None)
300
+ # YIELDIX-1 (DUZELTILDI): 'YIELDIX_ALLOW_DEFAULT_TENANT'-de-pop'lanip-
301
+ # GERI-KONMUYORDU ( env-mutasyonu-yan-etkisi — kontrol-kendi-env'ini-
302
+ # kalici-olarik-kirletiyordu). Artik-her-iki-deger-de-geri-yuklenir.
303
+ old_allow = os.environ.pop("YIELDIX_ALLOW_DEFAULT_TENANT", None)
304
+ try:
305
+ YieldixEngine()
306
+ ok = False
307
+ detail = "configsiz-engine-KABUL-EDİLDİ ( cross-tenant-açık!)"
308
+ except ValueError:
309
+ ok = True
310
+ detail = "configsiz-engine-reddedildi ( tenant-zorunlu-sağlam)"
311
+ finally:
312
+ if old is not None:
313
+ os.environ["YIELDIX_TENANT_ID"] = old
314
+ if old_allow is not None:
315
+ os.environ["YIELDIX_ALLOW_DEFAULT_TENANT"] = old_allow
316
+ record("tenant_isolation_enforced", ok, detail)
317
+ except Exception as e: # pragma: no cover - diagnostic
318
+ record("tenant_isolation_enforced", False, f"exception: {type(e).__name__}: {e}")
319
+
320
+ # 5) Circuit-breaker: aşırı-ihlal → component-shed-edilir
321
+ try:
322
+ from yieldix.core.circuit_breaker import CircuitBreaker
323
+ cb = CircuitBreaker(max_escalation_pct=10.0, max_consecutive_breaches=3)
324
+ for _ in range(4):
325
+ cb.record_interaction("c_verify", has_error=False, was_escalated=True)
326
+ cb.evaluate_cycle("c_verify")
327
+ shed = cb.get_shed_components()
328
+ ok = "c_verify" in shed
329
+ record("circuit_breaker_sheds_on_breach", ok,
330
+ f"shed={shed}")
331
+ except Exception as e: # pragma: no cover - diagnostic
332
+ record("circuit_breaker_sheds_on_breach", False,
333
+ f"exception: {type(e).__name__}: {e}")
334
+
335
+ # 6) Pilot-veri-seti: CPL/SQL quantified + imzalı-raporla-doğrulanıyor
336
+ # ( "lead-qualification-akademik-boş" dürüst-eleştirisine yanıt)
337
+ try:
338
+ from yieldix.telemetry.pilot_dataset import (
339
+ SyntheticPilotDataset, PILOT_COMPANIES)
340
+ ds = SyntheticPilotDataset(leads_per_company=8, seed=1337)
341
+ records = ds.generate()
342
+ rep, fingerprint, pubkey = ds.build_signed_report(
343
+ records, tenant_id=tenant)
344
+ rate = sum(r.is_sql for r in records) / max(1, len(records))
345
+ ok = (
346
+ len(PILOT_COMPANIES) >= 10
347
+ and rep.total_leads == len(records)
348
+ and rep.qualified_sql == sum(r.is_sql for r in records)
349
+ and 0.02 <= rate <= 0.40
350
+ and rep.cost_per_lead_try > 0
351
+ and len(fingerprint) == 64
352
+ )
353
+ record("pilot_dataset_cpl_sql_quantified", ok,
354
+ f"companies={len(PILOT_COMPANIES)} leads={rep.total_leads} "
355
+ f"sql={rep.qualified_sql}(%{rate * 100:.1f}) cpl={rep.cost_per_lead_try}")
356
+ except Exception as e: # pragma: no cover - diagnostic
357
+ record("pilot_dataset_cpl_sql_quantified", False,
358
+ f"exception: {type(e).__name__}: {e}")
359
+
360
+ # 7) Özet-tutarlılığı: rapordaki SHA-256 özeti payload'dan yeniden-hesaplanınca
361
+ # aynı-değeri-vermeli ( digest-payload-ayrılması yok)
362
+ try:
363
+ from yieldix.crypto.hasher import sha256_digest_hex
364
+ engine = YieldixEngine(PipelineConfig(tenant_id=tenant))
365
+ engine.kpi_collector.record_lead_processed(
366
+ "lead_verify_001", 32.5, is_sql=True, cost_try=Decimal("15.00"))
367
+ gen = MonthlyReportGenerator()
368
+ rep = gen.generate_signed_report(
369
+ tenant_id=tenant, period_start="2026-10-01", period_end="2026-10-31",
370
+ kpi_collector=engine.kpi_collector,
371
+ active_components=engine.config.enabled_components,
372
+ )
373
+ payload = MonthlyReportGenerator.to_signable_dict(rep)
374
+ recomputed = sha256_digest_hex(payload)
375
+ ok = recomputed == rep.sha256_digest
376
+ record("report_digest_matches_payload", ok,
377
+ f"digest-match={ok}")
378
+ except Exception as e: # pragma: no cover - diagnostic
379
+ record("report_digest_matches_payload", False,
380
+ f"exception: {type(e).__name__}: {e}")
381
+
382
+ # 8) BANT-nitelendirme-yapısaldır: keyword-tarama-DEĞİL, yapısal-puanlama
383
+ # ( düşük-intent-lead yüksek-puan-almamalı)
384
+ try:
385
+ from yieldix.cylinders.web_qualifier import WebQualifier
386
+ q = WebQualifier(tenant_id=tenant)
387
+ thin = InboundLeadPayload(
388
+ tenant_id=tenant, lead_id="lead_thin", contact_name="X",
389
+ contact_phone="+15550000001", source=LeadSource.WEB_FORM,
390
+ intent_summary="merhaba") # kısa-intent → düşük need-skoru
391
+ full = InboundLeadPayload(
392
+ tenant_id=tenant, lead_id="lead_full", contact_name="Y",
393
+ contact_phone="+15550000002", source=LeadSource.WEB_FORM,
394
+ intent_summary="Kurumsal B2B yazılım teklifi: 200 kullanicili plan, bu ay almak istiyoruz")
395
+ thin_bant = q.evaluate_initial_bant(thin)
396
+ full_bant = q.evaluate_initial_bant(full)
397
+ ok = full_bant.need >= thin_bant.need and full_bant.total_score >= 0.0
398
+ record("bant_scoring_is_structural", ok,
399
+ f"need-thin={thin_bant.need} need-full={full_bant.need} "
400
+ f"total-full={full_bant.total_score}")
401
+ except Exception as e: # pragma: no cover - diagnostic
402
+ record("bant_scoring_is_structural", False,
403
+ f"exception: {type(e).__name__}: {e}")
404
+
405
+ # 9) Cross-tenant rapor-izolasyonu: bir tenant'ın raporu diğerine karışmıyor
406
+ # ( imzalı-rapor tenant-bound olmalı)
407
+ try:
408
+ eng_a = YieldixEngine(PipelineConfig(tenant_id=f"{tenant}_A"))
409
+ eng_b = YieldixEngine(PipelineConfig(tenant_id=f"{tenant}_B"))
410
+ eng_a.kpi_collector.record_lead_processed("la", 10.0, is_sql=True, cost_try=Decimal("15"))
411
+ gen = MonthlyReportGenerator()
412
+ rep_a = gen.generate_signed_report(
413
+ tenant_id=f"{tenant}_A", period_start="2026-10-01", period_end="2026-10-31",
414
+ kpi_collector=eng_a.kpi_collector,
415
+ active_components=eng_a.config.enabled_components)
416
+ ok = rep_a.tenant_id == f"{tenant}_A" and eng_b.config.tenant_id == f"{tenant}_B"
417
+ # B toplayıcısı boş olmalı ( A'nın lead'leri sızmamış)
418
+ ok = ok and eng_b.kpi_collector.compute_summary_metrics()["total_leads"] == 0.0
419
+ record("cross_tenant_report_isolation", ok,
420
+ f"repA={rep_a.tenant_id} repB-untouched={ok}")
421
+ except Exception as e: # pragma: no cover - diagnostic
422
+ record("cross_tenant_report_isolation", False,
423
+ f"exception: {type(e).__name__}: {e}")
424
+
425
+ n_pass = sum(1 for c in checks if c["result"] == "PASS")
426
+ n_all = len(checks)
427
+ verdict = "PASS" if n_pass == n_all else "FAIL"
428
+
429
+ if as_json:
430
+ click.echo(json.dumps({"verdict": verdict, "passed": n_pass, "total": n_all,
431
+ "checks": checks}, ensure_ascii=False, indent=2))
432
+ else:
433
+ click.echo("=" * 70)
434
+ click.echo(" YIELDIX BAĞIMSIZ-DOĞRULAMA ( independent-verify)")
435
+ click.echo("=" * 70)
436
+ for c in checks:
437
+ mark = "✓" if c["result"] == "PASS" else "✗"
438
+ click.echo(f" [{mark}] {c['result']:<4} {c['check']}")
439
+ click.echo(f" {c['detail']}")
440
+ click.echo("-" * 70)
441
+ click.echo(f" SONUÇ: {verdict} — {n_pass}/{n_all} kontrol-geçti")
442
+ click.echo("=" * 70)
443
+ ctx = click.get_current_context()
444
+ ctx.exit(0 if verdict == "PASS" else 1)
445
+
446
+
447
+ if __name__ == "__main__":
448
+ main()
449
+
@@ -0,0 +1,108 @@
1
+ """
2
+ Yieldix Circuit Breaker & Component Shedding Engine.
3
+ Implements Theorem 3: Bounded Error Propagation and Dynamic Component Isolation.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ from dataclasses import dataclass
10
+
11
+ logger = logging.getLogger("Yieldix.CircuitBreaker")
12
+
13
+
14
+ @dataclass
15
+ class ComponentMetrics:
16
+ total_calls: int = 0
17
+ error_count: int = 0
18
+ escalation_count: int = 0
19
+ consecutive_breaches: int = 0
20
+
21
+ @property
22
+ def error_rate_pct(self) -> float:
23
+ if self.total_calls == 0:
24
+ return 0.0
25
+ return (self.error_count / self.total_calls) * 100.0
26
+
27
+ @property
28
+ def escalation_rate_pct(self) -> float:
29
+ if self.total_calls == 0:
30
+ return 0.0
31
+ return (self.escalation_count / self.total_calls) * 100.0
32
+
33
+
34
+ class CircuitBreaker:
35
+ """
36
+ Evaluates streaming or interval-based component telemetry.
37
+ If a component exceeds max_escalation_pct (default 20%) for max_consecutive_breaches (default 7 cycles),
38
+ it trips and dynamically sheds the faulty component from active execution.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ max_escalation_pct: float = 20.0,
44
+ max_consecutive_breaches: int = 7,
45
+ ):
46
+ self.max_escalation_pct = max_escalation_pct
47
+ self.max_consecutive_breaches = max_consecutive_breaches
48
+ self._metrics: dict[str, ComponentMetrics] = {}
49
+ self._shed_components: set[str] = set()
50
+
51
+ def record_interaction(
52
+ self, component_name: str, has_error: bool = False, was_escalated: bool = False
53
+ ) -> None:
54
+ if component_name not in self._metrics:
55
+ self._metrics[component_name] = ComponentMetrics()
56
+
57
+ m = self._metrics[component_name]
58
+ m.total_calls += 1
59
+ if has_error:
60
+ m.error_count += 1
61
+ if was_escalated:
62
+ m.escalation_count += 1
63
+
64
+ def evaluate_cycle(self, component_name: str) -> bool:
65
+ """
66
+ Evaluates current metrics for a component.
67
+ Returns True if the component is shed in this cycle, False otherwise.
68
+ """
69
+ if component_name in self._shed_components:
70
+ return False
71
+
72
+ metrics = self._metrics.get(component_name)
73
+ if not metrics or metrics.total_calls == 0:
74
+ return False
75
+
76
+ if metrics.escalation_rate_pct > self.max_escalation_pct:
77
+ metrics.consecutive_breaches += 1
78
+ logger.warning(
79
+ "Circuit Breaker Warning: Component '%s' escalation rate at %.2f%% (Breach %d/%d)",
80
+ component_name,
81
+ metrics.escalation_rate_pct,
82
+ metrics.consecutive_breaches,
83
+ self.max_consecutive_breaches,
84
+ )
85
+ if metrics.consecutive_breaches >= self.max_consecutive_breaches:
86
+ self._shed_components.add(component_name)
87
+ logger.error(
88
+ "CIRCUIT BREAKER TRIPPED: Component '%s' has been shed from active pipeline! "
89
+ "Teorem 3 Koruması: Ana motor güvenliği sağlandı.",
90
+ component_name,
91
+ )
92
+ return True
93
+ else:
94
+ metrics.consecutive_breaches = 0
95
+
96
+ return False
97
+
98
+ def is_component_active(self, component_name: str) -> bool:
99
+ return component_name not in self._shed_components
100
+
101
+ def get_shed_components(self) -> list[str]:
102
+ return sorted(self._shed_components)
103
+
104
+ def reset_component(self, component_name: str) -> None:
105
+ self._shed_components.discard(component_name)
106
+ if component_name in self._metrics:
107
+ self._metrics[component_name] = ComponentMetrics()
108
+ logger.info("Circuit Breaker: Component '%s' has been manually reinstated.", component_name)