agentloop-profiler 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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
agentloop/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
from agentloop.autoinstrument import (
|
|
2
|
+
DetectionResult,
|
|
3
|
+
InstrumentationResult,
|
|
4
|
+
auto_instrument,
|
|
5
|
+
detect_integrations,
|
|
6
|
+
instrument,
|
|
7
|
+
)
|
|
8
|
+
from agentloop.client import AgentLoopClient, AgentLoopClientError
|
|
9
|
+
from agentloop.decorators import trace_model, trace_tool, traceable
|
|
10
|
+
from agentloop.doctor import run_doctor
|
|
11
|
+
from agentloop.findings import build_diagnosis
|
|
12
|
+
from agentloop.judgment_types import (
|
|
13
|
+
JudgeIdentity,
|
|
14
|
+
JudgeUsage,
|
|
15
|
+
JudgmentAnswer,
|
|
16
|
+
JudgmentBackend,
|
|
17
|
+
JudgmentEvidence,
|
|
18
|
+
JudgmentRequest,
|
|
19
|
+
JudgmentSpec,
|
|
20
|
+
JudgmentUncertainty,
|
|
21
|
+
)
|
|
22
|
+
from agentloop.judgments import (
|
|
23
|
+
JudgmentSession,
|
|
24
|
+
LocalCallbackJudge,
|
|
25
|
+
attach_judgment,
|
|
26
|
+
judgment_request,
|
|
27
|
+
read_judgments,
|
|
28
|
+
)
|
|
29
|
+
from agentloop.otel import trace_from_otel, trace_to_otel, traces_from_otel
|
|
30
|
+
from agentloop.patches import RepositoryPathError, build_patch_plan
|
|
31
|
+
from agentloop.replay import ReplayGates, build_replay_report
|
|
32
|
+
from agentloop.retention import RetentionContext, RetentionPolicy, RetentionSession, read_retention
|
|
33
|
+
from agentloop.runtime import (
|
|
34
|
+
CLEAR,
|
|
35
|
+
FinalizationError,
|
|
36
|
+
get_last_error,
|
|
37
|
+
get_runtime_config,
|
|
38
|
+
init,
|
|
39
|
+
reset_runtime,
|
|
40
|
+
)
|
|
41
|
+
from agentloop.semantic_waste import evaluate_semantic_waste, read_semantic_waste
|
|
42
|
+
from agentloop.semantic_waste_types import SemanticInvestigation
|
|
43
|
+
from agentloop.structured_quality import attach_quality_report
|
|
44
|
+
from agentloop.tracer import (
|
|
45
|
+
AgentTrace,
|
|
46
|
+
bind_trace_context,
|
|
47
|
+
current_trace,
|
|
48
|
+
record_model_call,
|
|
49
|
+
record_tool_call,
|
|
50
|
+
trace_agent,
|
|
51
|
+
trace_model_call,
|
|
52
|
+
trace_retry,
|
|
53
|
+
trace_tool_call,
|
|
54
|
+
)
|
|
55
|
+
from agentloop.value import build_value_report
|
|
56
|
+
from agentloop.version import __version__
|
|
57
|
+
from agentloop.workflows import (
|
|
58
|
+
ExecutionTrace,
|
|
59
|
+
StageInfo,
|
|
60
|
+
WorkflowInfo,
|
|
61
|
+
operation_metadata,
|
|
62
|
+
record_operation,
|
|
63
|
+
set_workflow_outcome,
|
|
64
|
+
trace_operation,
|
|
65
|
+
trace_workflow,
|
|
66
|
+
workflow_metadata,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"SemanticInvestigation",
|
|
71
|
+
"evaluate_semantic_waste",
|
|
72
|
+
"read_semantic_waste",
|
|
73
|
+
"JudgeIdentity",
|
|
74
|
+
"JudgeUsage",
|
|
75
|
+
"JudgmentAnswer",
|
|
76
|
+
"JudgmentBackend",
|
|
77
|
+
"JudgmentEvidence",
|
|
78
|
+
"JudgmentRequest",
|
|
79
|
+
"JudgmentSpec",
|
|
80
|
+
"JudgmentUncertainty",
|
|
81
|
+
"JudgmentSession",
|
|
82
|
+
"LocalCallbackJudge",
|
|
83
|
+
"attach_judgment",
|
|
84
|
+
"judgment_request",
|
|
85
|
+
"read_judgments",
|
|
86
|
+
"AgentLoopClient",
|
|
87
|
+
"AgentLoopClientError",
|
|
88
|
+
"AgentTrace",
|
|
89
|
+
"attach_quality_report",
|
|
90
|
+
"ExecutionTrace",
|
|
91
|
+
"StageInfo",
|
|
92
|
+
"WorkflowInfo",
|
|
93
|
+
"operation_metadata",
|
|
94
|
+
"record_operation",
|
|
95
|
+
"set_workflow_outcome",
|
|
96
|
+
"trace_operation",
|
|
97
|
+
"trace_workflow",
|
|
98
|
+
"workflow_metadata",
|
|
99
|
+
"CLEAR",
|
|
100
|
+
"FinalizationError",
|
|
101
|
+
"DetectionResult",
|
|
102
|
+
"InstrumentationResult",
|
|
103
|
+
"ReplayGates",
|
|
104
|
+
"RetentionContext",
|
|
105
|
+
"RetentionPolicy",
|
|
106
|
+
"RetentionSession",
|
|
107
|
+
"read_retention",
|
|
108
|
+
"RepositoryPathError",
|
|
109
|
+
"auto_instrument",
|
|
110
|
+
"bind_trace_context",
|
|
111
|
+
"detect_integrations",
|
|
112
|
+
"instrument",
|
|
113
|
+
"build_value_report",
|
|
114
|
+
"build_diagnosis",
|
|
115
|
+
"build_patch_plan",
|
|
116
|
+
"build_replay_report",
|
|
117
|
+
"current_trace",
|
|
118
|
+
"get_last_error",
|
|
119
|
+
"get_runtime_config",
|
|
120
|
+
"init",
|
|
121
|
+
"record_model_call",
|
|
122
|
+
"record_tool_call",
|
|
123
|
+
"reset_runtime",
|
|
124
|
+
"run_doctor",
|
|
125
|
+
"trace_agent",
|
|
126
|
+
"trace_from_otel",
|
|
127
|
+
"traces_from_otel",
|
|
128
|
+
"trace_model",
|
|
129
|
+
"trace_model_call",
|
|
130
|
+
"trace_tool",
|
|
131
|
+
"trace_tool_call",
|
|
132
|
+
"trace_retry",
|
|
133
|
+
"traceable",
|
|
134
|
+
"trace_to_otel",
|
|
135
|
+
"__version__",
|
|
136
|
+
]
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Frozen declarations for host-run harness ablations, without executing workloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from hashlib import sha256
|
|
10
|
+
|
|
11
|
+
SCHEMA_VERSION = "1.0"
|
|
12
|
+
MODES = frozenset({"uninstrumented", "tracing", "shadow", "enforce"})
|
|
13
|
+
VERSION_KEYS = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"source",
|
|
16
|
+
"model",
|
|
17
|
+
"provider",
|
|
18
|
+
"configuration",
|
|
19
|
+
"scorer",
|
|
20
|
+
"runner",
|
|
21
|
+
"environment",
|
|
22
|
+
"tools",
|
|
23
|
+
"reset",
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AblationValidationError(ValueError):
|
|
29
|
+
"""A declaration or observation cannot be interpreted without guessing."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def canonical(value):
|
|
33
|
+
try:
|
|
34
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
35
|
+
except (TypeError, ValueError, RecursionError):
|
|
36
|
+
raise AblationValidationError(
|
|
37
|
+
"ablation artifacts must contain finite JSON values"
|
|
38
|
+
) from None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def fields(value, expected, label):
|
|
42
|
+
if not isinstance(value, dict) or set(value) != set(expected):
|
|
43
|
+
raise AblationValidationError(
|
|
44
|
+
f"{label} must contain exactly: {', '.join(sorted(expected))}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def text_value(value, label):
|
|
49
|
+
if not isinstance(value, str) or not value.strip():
|
|
50
|
+
raise AblationValidationError(f"{label} must be nonempty text")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def timestamp(value):
|
|
54
|
+
try:
|
|
55
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
56
|
+
if parsed.utcoffset() is None:
|
|
57
|
+
raise ValueError
|
|
58
|
+
return parsed
|
|
59
|
+
except (AttributeError, TypeError, ValueError):
|
|
60
|
+
raise AblationValidationError("timestamps must be ISO-8601 with a timezone") from None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def number(value, label, *, maximum=None, integer=False, nullable=False):
|
|
64
|
+
if value is None and nullable:
|
|
65
|
+
return
|
|
66
|
+
valid = type(value) is int if integer else type(value) in {int, float}
|
|
67
|
+
try:
|
|
68
|
+
valid = (
|
|
69
|
+
valid and math.isfinite(value) and value >= 0 and (maximum is None or value <= maximum)
|
|
70
|
+
)
|
|
71
|
+
except (OverflowError, TypeError):
|
|
72
|
+
valid = False
|
|
73
|
+
if not valid:
|
|
74
|
+
raise AblationValidationError(
|
|
75
|
+
f"{label} must be a finite nonnegative {'integer' if integer else 'number'}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def pair_key(value):
|
|
80
|
+
return value["task_id"], value["repetition"], value["cache_condition"]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _validate(spec):
|
|
84
|
+
fields(
|
|
85
|
+
spec,
|
|
86
|
+
{
|
|
87
|
+
"schema_version",
|
|
88
|
+
"name",
|
|
89
|
+
"workload_id",
|
|
90
|
+
"permission_ref",
|
|
91
|
+
"frozen_at",
|
|
92
|
+
"synthetic",
|
|
93
|
+
"versions",
|
|
94
|
+
"tasks",
|
|
95
|
+
"conditions",
|
|
96
|
+
"schedule",
|
|
97
|
+
"quality_gate",
|
|
98
|
+
"bootstrap",
|
|
99
|
+
},
|
|
100
|
+
"protocol",
|
|
101
|
+
)
|
|
102
|
+
if spec["schema_version"] != SCHEMA_VERSION or type(spec["synthetic"]) is not bool:
|
|
103
|
+
raise AblationValidationError("protocol requires schema 1.0 and a boolean synthetic marker")
|
|
104
|
+
for key in ("name", "workload_id", "permission_ref"):
|
|
105
|
+
text_value(spec[key], key)
|
|
106
|
+
timestamp(spec["frozen_at"])
|
|
107
|
+
fields(spec["versions"], VERSION_KEYS, "versions")
|
|
108
|
+
for key, value in spec["versions"].items():
|
|
109
|
+
text_value(value, key)
|
|
110
|
+
tasks = spec["tasks"]
|
|
111
|
+
if not isinstance(tasks, dict) or not tasks:
|
|
112
|
+
raise AblationValidationError("tasks must be a nonempty mapping")
|
|
113
|
+
digests = set()
|
|
114
|
+
for task, details in tasks.items():
|
|
115
|
+
text_value(task, "task_id")
|
|
116
|
+
fields(details, {"split", "input_sha256"}, "task")
|
|
117
|
+
if not isinstance(details["split"], str) or details["split"] not in {"pilot", "held_out"}:
|
|
118
|
+
raise AblationValidationError("task split must be pilot or held_out")
|
|
119
|
+
digest = details["input_sha256"]
|
|
120
|
+
if (
|
|
121
|
+
not isinstance(digest, str)
|
|
122
|
+
or len(digest) != 64
|
|
123
|
+
or any(c not in "0123456789abcdef" for c in digest)
|
|
124
|
+
):
|
|
125
|
+
raise AblationValidationError("task input_sha256 must be a SHA-256 hex digest")
|
|
126
|
+
if digest in digests:
|
|
127
|
+
raise AblationValidationError(
|
|
128
|
+
"repeated task inputs must share one task ID, not inflate the task denominator"
|
|
129
|
+
)
|
|
130
|
+
digests.add(digest)
|
|
131
|
+
if {item["split"] for item in tasks.values()} != {"pilot", "held_out"}:
|
|
132
|
+
raise AblationValidationError("declare separate pilot and held_out tasks")
|
|
133
|
+
conditions = spec["conditions"]
|
|
134
|
+
if not isinstance(conditions, dict) or not conditions:
|
|
135
|
+
raise AblationValidationError("conditions must be a mapping")
|
|
136
|
+
signatures = set()
|
|
137
|
+
for name, condition in conditions.items():
|
|
138
|
+
text_value(name, "condition")
|
|
139
|
+
fields(condition, {"mode", "policies"}, "condition")
|
|
140
|
+
mode, policies = condition["mode"], condition["policies"]
|
|
141
|
+
if not isinstance(mode, str) or mode not in MODES or not isinstance(policies, dict):
|
|
142
|
+
raise AblationValidationError("invalid condition mode or policies")
|
|
143
|
+
if bool(policies) != (mode in {"shadow", "enforce"}):
|
|
144
|
+
raise AblationValidationError("only shadow/enforce conditions require policies")
|
|
145
|
+
for policy, snapshot in policies.items():
|
|
146
|
+
text_value(policy, "policy_id")
|
|
147
|
+
text_value(snapshot, "policy version/configuration reference")
|
|
148
|
+
signature = (mode, canonical(policies))
|
|
149
|
+
if signature in signatures:
|
|
150
|
+
raise AblationValidationError("duplicate mode/policy condition")
|
|
151
|
+
signatures.add(signature)
|
|
152
|
+
if {mode for mode, _ in signatures} != MODES:
|
|
153
|
+
raise AblationValidationError(
|
|
154
|
+
"declare uninstrumented, tracing, shadow and enforce conditions"
|
|
155
|
+
)
|
|
156
|
+
for condition in conditions.values():
|
|
157
|
+
if condition["mode"] in {"shadow", "enforce"}:
|
|
158
|
+
policies = condition["policies"]
|
|
159
|
+
for mode in ("shadow", "enforce"):
|
|
160
|
+
if (mode, canonical(policies)) not in signatures or any(
|
|
161
|
+
(mode, canonical({key: value})) not in signatures
|
|
162
|
+
for key, value in policies.items()
|
|
163
|
+
):
|
|
164
|
+
raise AblationValidationError(
|
|
165
|
+
"each policy set needs matched shadow/enforce and single-policy conditions"
|
|
166
|
+
)
|
|
167
|
+
schedule = spec["schedule"]
|
|
168
|
+
if not isinstance(schedule, list) or not schedule:
|
|
169
|
+
raise AblationValidationError("schedule must be a nonempty list")
|
|
170
|
+
seen = set()
|
|
171
|
+
for slot in schedule:
|
|
172
|
+
fields(slot, {"task_id", "repetition", "cache_condition", "order"}, "schedule slot")
|
|
173
|
+
for key in ("task_id", "repetition", "cache_condition"):
|
|
174
|
+
text_value(slot[key], key)
|
|
175
|
+
if slot["task_id"] not in tasks or pair_key(slot) in seen:
|
|
176
|
+
raise AblationValidationError(
|
|
177
|
+
"schedule must reference declared tasks with unique pairing keys"
|
|
178
|
+
)
|
|
179
|
+
seen.add(pair_key(slot))
|
|
180
|
+
order = slot["order"]
|
|
181
|
+
if (
|
|
182
|
+
not isinstance(order, list)
|
|
183
|
+
or any(not isinstance(name, str) for name in order)
|
|
184
|
+
or len(order) != len(conditions)
|
|
185
|
+
or set(order) != set(conditions)
|
|
186
|
+
):
|
|
187
|
+
raise AblationValidationError("every slot must order all conditions exactly once")
|
|
188
|
+
if {slot["task_id"] for slot in schedule} != set(tasks):
|
|
189
|
+
raise AblationValidationError("every declared task needs a scheduled slot")
|
|
190
|
+
fields(spec["quality_gate"], {"min_score", "max_regression"}, "quality_gate")
|
|
191
|
+
for key, value in spec["quality_gate"].items():
|
|
192
|
+
number(value, key, maximum=1)
|
|
193
|
+
fields(spec["bootstrap"], {"samples", "seed", "confidence"}, "bootstrap")
|
|
194
|
+
settings = spec["bootstrap"]
|
|
195
|
+
number(settings["samples"], "samples", maximum=10000, integer=True)
|
|
196
|
+
if not settings["samples"] or type(settings["seed"]) is not int:
|
|
197
|
+
raise AblationValidationError("bootstrap requires positive samples and an integer seed")
|
|
198
|
+
number(settings["confidence"], "confidence", maximum=1)
|
|
199
|
+
if not 0 < settings["confidence"] < 1:
|
|
200
|
+
raise AblationValidationError("confidence must be strictly between zero and one")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass(frozen=True)
|
|
204
|
+
class AblationProtocol:
|
|
205
|
+
"""A canonical snapshot; its digest identifies declarations, not preregistration proof."""
|
|
206
|
+
|
|
207
|
+
_json: str
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def freeze(cls, specification):
|
|
211
|
+
owned = json.loads(canonical(specification))
|
|
212
|
+
_validate(owned)
|
|
213
|
+
digest = sha256(canonical(owned).encode()).hexdigest()
|
|
214
|
+
return cls(canonical({"protocol_hash": digest, "specification": owned}))
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def from_dict(cls, payload):
|
|
218
|
+
fields(payload, {"protocol_hash", "specification"}, "frozen protocol")
|
|
219
|
+
result = cls.freeze(payload["specification"])
|
|
220
|
+
if result.protocol_hash != payload["protocol_hash"]:
|
|
221
|
+
raise AblationValidationError("protocol hash does not match the frozen specification")
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def protocol_hash(self):
|
|
226
|
+
return self.to_dict()["protocol_hash"]
|
|
227
|
+
|
|
228
|
+
def to_dict(self):
|
|
229
|
+
return json.loads(self._json)
|