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 +30 -0
- yieldix/cli/main.py +449 -0
- yieldix/core/circuit_breaker.py +108 -0
- yieldix/core/engine.py +145 -0
- yieldix/core/types.py +104 -0
- yieldix/crypto/hasher.py +31 -0
- yieldix/crypto/signer.py +57 -0
- yieldix/cylinders/cold_email_l2.py +152 -0
- yieldix/cylinders/crm_reactivation.py +68 -0
- yieldix/cylinders/inbox_triage.py +72 -0
- yieldix/cylinders/receptionist.py +69 -0
- yieldix/cylinders/speed_to_lead.py +69 -0
- yieldix/cylinders/web_qualifier.py +77 -0
- yieldix/mesh/__init__.py +40 -0
- yieldix/mesh/tamga_anchor.py +719 -0
- yieldix/server/__init__.py +6 -0
- yieldix/server/app.py +575 -0
- yieldix/telemetry/kpi_collector.py +93 -0
- yieldix/telemetry/pilot_dataset.py +231 -0
- yieldix/telemetry/reporter.py +122 -0
- yieldix-16.0.0.dist-info/METADATA +1267 -0
- yieldix-16.0.0.dist-info/RECORD +26 -0
- yieldix-16.0.0.dist-info/WHEEL +5 -0
- yieldix-16.0.0.dist-info/entry_points.txt +2 -0
- yieldix-16.0.0.dist-info/licenses/LICENSE +21 -0
- yieldix-16.0.0.dist-info/top_level.txt +1 -0
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)
|