hypothesis-helm 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.
- hypothesis_helm/__init__.py +38 -0
- hypothesis_helm/analysis/__init__.py +3 -0
- hypothesis_helm/analysis/cli.py +117 -0
- hypothesis_helm/analysis/report.py +172 -0
- hypothesis_helm/analysis/sensitivity.py +268 -0
- hypothesis_helm/benchmarking/__init__.py +3 -0
- hypothesis_helm/benchmarking/analysis/__init__.py +3 -0
- hypothesis_helm/benchmarking/analysis/calibration_matrix.py +230 -0
- hypothesis_helm/benchmarking/analysis/expansion_cost.py +51 -0
- hypothesis_helm/benchmarking/analysis/pca.py +186 -0
- hypothesis_helm/benchmarking/analysis/quadratic.py +98 -0
- hypothesis_helm/benchmarking/analysis/selection.py +112 -0
- hypothesis_helm/benchmarking/analysis/symbolic.py +94 -0
- hypothesis_helm/benchmarking/assets/chart/Chart.yaml +4 -0
- hypothesis_helm/benchmarking/assets/chart/benchmark-parameters.yaml +28 -0
- hypothesis_helm/benchmarking/assets/chart/benchmark.json +93 -0
- hypothesis_helm/benchmarking/assets/chart/templates/configmap.yaml +6 -0
- hypothesis_helm/benchmarking/assets/chart/templates/stress.yaml +115 -0
- hypothesis_helm/benchmarking/assets/chart/topology-parameters.yaml +7 -0
- hypothesis_helm/benchmarking/assets/chart/values.schema.json +104 -0
- hypothesis_helm/benchmarking/assets/chart/values.yaml +12 -0
- hypothesis_helm/benchmarking/assets/fixture/standard.yaml +21 -0
- hypothesis_helm/benchmarking/assets/fixture/topology.yaml +21 -0
- hypothesis_helm/benchmarking/charts/__init__.py +3 -0
- hypothesis_helm/benchmarking/charts/error_surface.py +153 -0
- hypothesis_helm/benchmarking/charts/faults.py +145 -0
- hypothesis_helm/benchmarking/charts/fixture.py +176 -0
- hypothesis_helm/benchmarking/charts/generator.py +517 -0
- hypothesis_helm/benchmarking/charts/manifests.py +17 -0
- hypothesis_helm/benchmarking/charts/mixtures.py +93 -0
- hypothesis_helm/benchmarking/charts/names.py +66 -0
- hypothesis_helm/benchmarking/charts/parameters.py +57 -0
- hypothesis_helm/benchmarking/charts/shape.py +63 -0
- hypothesis_helm/benchmarking/charts/stress.py +301 -0
- hypothesis_helm/benchmarking/charts/structural_sparsity.py +158 -0
- hypothesis_helm/benchmarking/charts/structures.py +290 -0
- hypothesis_helm/benchmarking/charts/topology.py +183 -0
- hypothesis_helm/benchmarking/charts/workload.py +192 -0
- hypothesis_helm/benchmarking/cli.py +128 -0
- hypothesis_helm/benchmarking/execution/__init__.py +3 -0
- hypothesis_helm/benchmarking/execution/cancellation.py +90 -0
- hypothesis_helm/benchmarking/execution/profiling.py +202 -0
- hypothesis_helm/benchmarking/execution/provenance.py +21 -0
- hypothesis_helm/benchmarking/execution/runner.py +424 -0
- hypothesis_helm/benchmarking/execution/shell.py +33 -0
- hypothesis_helm/benchmarking/refresh/__init__.py +3 -0
- hypothesis_helm/benchmarking/refresh/ci.py +130 -0
- hypothesis_helm/benchmarking/refresh/cli.py +170 -0
- hypothesis_helm/benchmarking/refresh/plan.py +103 -0
- hypothesis_helm/benchmarking/refresh/recipes/catalog-topologies.py +211 -0
- hypothesis_helm/benchmarking/refresh/recipes/chart-topology.sh +38 -0
- hypothesis_helm/benchmarking/refresh/recipes/discovery-tables.py +71 -0
- hypothesis_helm/benchmarking/refresh/recipes/finalize-repository.py +339 -0
- hypothesis_helm/benchmarking/refresh/recipes/initialize.py +146 -0
- hypothesis_helm/benchmarking/refresh/recipes/operations.sh +77 -0
- hypothesis_helm/benchmarking/refresh/recipes/plan-topology-retries.py +47 -0
- hypothesis_helm/benchmarking/refresh/recipes/polish-sparsity.py +17 -0
- hypothesis_helm/benchmarking/refresh/recipes/prepare-fixtures.py +23 -0
- hypothesis_helm/benchmarking/refresh/recipes/prepare-topologies.py +37 -0
- hypothesis_helm/benchmarking/refresh/recipes/publish-flamegraphs.py +62 -0
- hypothesis_helm/benchmarking/refresh/recipes/publish.py +41 -0
- hypothesis_helm/benchmarking/refresh/recipes/repository-chart.sh +10 -0
- hypothesis_helm/benchmarking/refresh/recipes/repository-run.sh +12 -0
- hypothesis_helm/benchmarking/refresh/recipes/retry-topologies.sh +13 -0
- hypothesis_helm/benchmarking/refresh/recipes/run-topologies.sh +14 -0
- hypothesis_helm/benchmarking/refresh/recipes/sparsity-tables.py +94 -0
- hypothesis_helm/benchmarking/refresh/recipes/studies.sh +69 -0
- hypothesis_helm/benchmarking/refresh/recipes/update-documentation.py +99 -0
- hypothesis_helm/benchmarking/refresh/recipes/verify-measurements.py +165 -0
- hypothesis_helm/benchmarking/refresh/recipes/verify-publication.py +95 -0
- hypothesis_helm/benchmarking/refresh/recipes/verify-topologies.py +68 -0
- hypothesis_helm/benchmarking/reporting/__init__.py +3 -0
- hypothesis_helm/benchmarking/reporting/complexity_sweep.py +91 -0
- hypothesis_helm/benchmarking/reporting/depth.py +162 -0
- hypothesis_helm/benchmarking/reporting/descriptions.py +203 -0
- hypothesis_helm/benchmarking/reporting/error_highlight.py +87 -0
- hypothesis_helm/benchmarking/reporting/error_surface.py +259 -0
- hypothesis_helm/benchmarking/reporting/expansion.py +222 -0
- hypothesis_helm/benchmarking/reporting/flamegraph.py +232 -0
- hypothesis_helm/benchmarking/reporting/labels.py +22 -0
- hypothesis_helm/benchmarking/reporting/matrix.py +179 -0
- hypothesis_helm/benchmarking/reporting/nesting.py +278 -0
- hypothesis_helm/benchmarking/reporting/pca.py +265 -0
- hypothesis_helm/benchmarking/reporting/plots.py +452 -0
- hypothesis_helm/benchmarking/reporting/polynomial_surface.py +104 -0
- hypothesis_helm/benchmarking/reporting/progress.py +149 -0
- hypothesis_helm/benchmarking/reporting/response_surface.py +164 -0
- hypothesis_helm/benchmarking/reporting/structural_sparsity.py +142 -0
- hypothesis_helm/benchmarking/reporting/symbolic.py +119 -0
- hypothesis_helm/benchmarking/reporting/variation.py +94 -0
- hypothesis_helm/benchmarking/scripts/shards.sh +85 -0
- hypothesis_helm/benchmarking/scripts/smoke.sh +39 -0
- hypothesis_helm/benchmarking/studies/__init__.py +3 -0
- hypothesis_helm/benchmarking/studies/calibration.py +391 -0
- hypothesis_helm/benchmarking/studies/discovery.py +249 -0
- hypothesis_helm/benchmarking/studies/error_surface.py +374 -0
- hypothesis_helm/benchmarking/studies/expansion.py +249 -0
- hypothesis_helm/benchmarking/studies/filtering.py +362 -0
- hypothesis_helm/benchmarking/studies/matrix.py +387 -0
- hypothesis_helm/benchmarking/studies/nesting.py +208 -0
- hypothesis_helm/benchmarking/studies/pca.py +342 -0
- hypothesis_helm/benchmarking/studies/performance.py +392 -0
- hypothesis_helm/benchmarking/studies/polynomial_surface.py +87 -0
- hypothesis_helm/benchmarking/studies/response_surface.py +275 -0
- hypothesis_helm/benchmarking/studies/sampling.py +266 -0
- hypothesis_helm/benchmarking/studies/sensitivity.py +154 -0
- hypothesis_helm/benchmarking/studies/sparsity.py +268 -0
- hypothesis_helm/benchmarking/studies/stress.py +235 -0
- hypothesis_helm/benchmarking/studies/structural_sparsity.py +203 -0
- hypothesis_helm/benchmarking/studies/structure_depth.py +186 -0
- hypothesis_helm/benchmarking/studies/symbolic_surface.py +235 -0
- hypothesis_helm/benchmarking/studies/topology.py +271 -0
- hypothesis_helm/charts/__init__.py +3 -0
- hypothesis_helm/charts/audit.py +82 -0
- hypothesis_helm/charts/cache.py +133 -0
- hypothesis_helm/charts/candidates.py +215 -0
- hypothesis_helm/charts/changes.py +147 -0
- hypothesis_helm/charts/exhaustive.py +170 -0
- hypothesis_helm/charts/generate.py +522 -0
- hypothesis_helm/charts/generated.py +309 -0
- hypothesis_helm/charts/model.py +200 -0
- hypothesis_helm/charts/paths.py +374 -0
- hypothesis_helm/charts/planning.py +445 -0
- hypothesis_helm/charts/presence.py +32 -0
- hypothesis_helm/charts/prioritized.py +221 -0
- hypothesis_helm/charts/registry.py +236 -0
- hypothesis_helm/charts/rendering.py +204 -0
- hypothesis_helm/charts/repository.py +152 -0
- hypothesis_helm/charts/runner.py +759 -0
- hypothesis_helm/charts/scan.py +599 -0
- hypothesis_helm/charts/templates.py +323 -0
- hypothesis_helm/charts/tpl.py +86 -0
- hypothesis_helm/charts/yamlio.py +116 -0
- hypothesis_helm/cli.py +936 -0
- hypothesis_helm/compiler/__init__.py +3 -0
- hypothesis_helm/compiler/asts/__init__.py +3 -0
- hypothesis_helm/compiler/asts/actions.py +76 -0
- hypothesis_helm/compiler/asts/conditions.py +88 -0
- hypothesis_helm/compiler/asts/contracts.py +426 -0
- hypothesis_helm/compiler/asts/dependencies.py +47 -0
- hypothesis_helm/compiler/asts/lattice.py +168 -0
- hypothesis_helm/compiler/asts/lexing.py +88 -0
- hypothesis_helm/compiler/asts/templates.py +267 -0
- hypothesis_helm/compiler/complexity.py +59 -0
- hypothesis_helm/compiler/constants.py +89 -0
- hypothesis_helm/compiler/passes/__init__.py +3 -0
- hypothesis_helm/compiler/passes/branches.py +83 -0
- hypothesis_helm/compiler/passes/complexity.py +413 -0
- hypothesis_helm/compiler/passes/dependencies.py +437 -0
- hypothesis_helm/compiler/passes/expansion.py +92 -0
- hypothesis_helm/compiler/passes/exports.py +77 -0
- hypothesis_helm/compiler/passes/graph.py +243 -0
- hypothesis_helm/compiler/passes/inputs.py +384 -0
- hypothesis_helm/compiler/passes/minimum.py +276 -0
- hypothesis_helm/compiler/passes/pruning.py +534 -0
- hypothesis_helm/compiler/passes/rejections.py +192 -0
- hypothesis_helm/compiler/passes/sampling.py +99 -0
- hypothesis_helm/compiler/passes/topology.py +103 -0
- hypothesis_helm/execution/__init__.py +3 -0
- hypothesis_helm/execution/aggressive.py +237 -0
- hypothesis_helm/execution/cache.py +194 -0
- hypothesis_helm/execution/calibration.json +4876 -0
- hypothesis_helm/execution/environment.py +29 -0
- hypothesis_helm/execution/estimate.py +208 -0
- hypothesis_helm/execution/feedback.py +146 -0
- hypothesis_helm/execution/parallel.py +273 -0
- hypothesis_helm/execution/path_queue.py +213 -0
- hypothesis_helm/execution/processes.py +205 -0
- hypothesis_helm/execution/render_hashes.py +194 -0
- hypothesis_helm/execution/sampling.py +119 -0
- hypothesis_helm/execution/signals.py +120 -0
- hypothesis_helm/execution/structure.py +163 -0
- hypothesis_helm/execution/suite.py +307 -0
- hypothesis_helm/execution/traversal.py +156 -0
- hypothesis_helm/findings/__init__.py +3 -0
- hypothesis_helm/findings/catalog.py +219 -0
- hypothesis_helm/findings/generator.py +159 -0
- hypothesis_helm/integrations/__init__.py +3 -0
- hypothesis_helm/integrations/github_action.py +120 -0
- hypothesis_helm/integrations/github_action.sh +38 -0
- hypothesis_helm/integrations/kubesec.py +218 -0
- hypothesis_helm/integrations/minimal_values.sh +47 -0
- hypothesis_helm/integrations/sharding.py +201 -0
- hypothesis_helm/reporting/__init__.py +3 -0
- hypothesis_helm/reporting/assets/logo.png +0 -0
- hypothesis_helm/reporting/budget.py +80 -0
- hypothesis_helm/reporting/changes.py +155 -0
- hypothesis_helm/reporting/contents.py +123 -0
- hypothesis_helm/reporting/display.py +48 -0
- hypothesis_helm/reporting/errors.py +217 -0
- hypothesis_helm/reporting/links.py +110 -0
- hypothesis_helm/reporting/output.py +38 -0
- hypothesis_helm/reporting/pdf.py +107 -0
- hypothesis_helm/reporting/permutations.py +284 -0
- hypothesis_helm/reporting/progress.py +229 -0
- hypothesis_helm/reporting/progressive.py +380 -0
- hypothesis_helm/reporting/repository.py +286 -0
- hypothesis_helm/reporting/reproductions.py +190 -0
- hypothesis_helm/reporting/shards.py +233 -0
- hypothesis_helm/rules.py +151 -0
- hypothesis_helm/schemas/__init__.py +3 -0
- hypothesis_helm/schemas/combinations.py +277 -0
- hypothesis_helm/schemas/conformity.py +215 -0
- hypothesis_helm/schemas/contracts.py +164 -0
- hypothesis_helm/schemas/factors.py +95 -0
- hypothesis_helm/schemas/finite.py +169 -0
- hypothesis_helm/schemas/groups.py +144 -0
- hypothesis_helm/schemas/model.py +431 -0
- hypothesis_helm/schemas/paths.py +169 -0
- hypothesis_helm/schemas/priority.py +128 -0
- hypothesis_helm/schemas/replay.py +150 -0
- hypothesis_helm/tests/__init__.py +3 -0
- hypothesis_helm/tests/conftest.py +47 -0
- hypothesis_helm/tests/test_aggregate.py +149 -0
- hypothesis_helm/tests/test_aggressive.py +327 -0
- hypothesis_helm/tests/test_benchmark_package.py +132 -0
- hypothesis_helm/tests/test_benchmark_progress.py +138 -0
- hypothesis_helm/tests/test_benchmarks.py +312 -0
- hypothesis_helm/tests/test_binary_cache.py +240 -0
- hypothesis_helm/tests/test_cache.py +238 -0
- hypothesis_helm/tests/test_calibration_matrix.py +306 -0
- hypothesis_helm/tests/test_changes.py +239 -0
- hypothesis_helm/tests/test_chart_changes.py +281 -0
- hypothesis_helm/tests/test_ci.py +556 -0
- hypothesis_helm/tests/test_combinations.py +194 -0
- hypothesis_helm/tests/test_complexity.py +336 -0
- hypothesis_helm/tests/test_conformity.py +237 -0
- hypothesis_helm/tests/test_constants.py +153 -0
- hypothesis_helm/tests/test_contents.py +107 -0
- hypothesis_helm/tests/test_dependencies.py +434 -0
- hypothesis_helm/tests/test_discovery.py +90 -0
- hypothesis_helm/tests/test_display.py +120 -0
- hypothesis_helm/tests/test_distinct_configurations.py +99 -0
- hypothesis_helm/tests/test_environment.py +65 -0
- hypothesis_helm/tests/test_error_highlight.py +56 -0
- hypothesis_helm/tests/test_error_surface.py +257 -0
- hypothesis_helm/tests/test_errors.py +332 -0
- hypothesis_helm/tests/test_estimate.py +128 -0
- hypothesis_helm/tests/test_expansion.py +493 -0
- hypothesis_helm/tests/test_feedback.py +79 -0
- hypothesis_helm/tests/test_filtering_load.py +85 -0
- hypothesis_helm/tests/test_findings.py +164 -0
- hypothesis_helm/tests/test_finite.py +57 -0
- hypothesis_helm/tests/test_fixture.py +248 -0
- hypothesis_helm/tests/test_generate.py +349 -0
- hypothesis_helm/tests/test_group_coverage.py +240 -0
- hypothesis_helm/tests/test_input_inventory.py +430 -0
- hypothesis_helm/tests/test_kubesec.py +149 -0
- hypothesis_helm/tests/test_lattice.py +240 -0
- hypothesis_helm/tests/test_local_shards.py +72 -0
- hypothesis_helm/tests/test_matrix.py +197 -0
- hypothesis_helm/tests/test_mixtures.py +138 -0
- hypothesis_helm/tests/test_operations.py +521 -0
- hypothesis_helm/tests/test_parallel_exhaustive.py +266 -0
- hypothesis_helm/tests/test_path_scan.py +222 -0
- hypothesis_helm/tests/test_path_workers.py +290 -0
- hypothesis_helm/tests/test_pca.py +126 -0
- hypothesis_helm/tests/test_permutation_statistics.py +190 -0
- hypothesis_helm/tests/test_plot_descriptions.py +55 -0
- hypothesis_helm/tests/test_plot_variation.py +108 -0
- hypothesis_helm/tests/test_priority.py +314 -0
- hypothesis_helm/tests/test_profiling.py +203 -0
- hypothesis_helm/tests/test_progressive.py +153 -0
- hypothesis_helm/tests/test_pruning.py +464 -0
- hypothesis_helm/tests/test_quadratic.py +162 -0
- hypothesis_helm/tests/test_refresh.py +771 -0
- hypothesis_helm/tests/test_refresh_ci.py +117 -0
- hypothesis_helm/tests/test_registry.py +349 -0
- hypothesis_helm/tests/test_rejections.py +306 -0
- hypothesis_helm/tests/test_release_version.py +59 -0
- hypothesis_helm/tests/test_remote_shards.py +78 -0
- hypothesis_helm/tests/test_render_hashes.py +197 -0
- hypothesis_helm/tests/test_replay.py +174 -0
- hypothesis_helm/tests/test_repository.py +243 -0
- hypothesis_helm/tests/test_reproductions.py +218 -0
- hypothesis_helm/tests/test_rules.py +256 -0
- hypothesis_helm/tests/test_runner.py +266 -0
- hypothesis_helm/tests/test_sampling.py +171 -0
- hypothesis_helm/tests/test_scan.py +858 -0
- hypothesis_helm/tests/test_sensitivity.py +262 -0
- hypothesis_helm/tests/test_sharding.py +399 -0
- hypothesis_helm/tests/test_shutdown.py +804 -0
- hypothesis_helm/tests/test_sparsity.py +47 -0
- hypothesis_helm/tests/test_strict.py +142 -0
- hypothesis_helm/tests/test_structural_sparsity.py +108 -0
- hypothesis_helm/tests/test_structure.py +139 -0
- hypothesis_helm/tests/test_suite.py +382 -0
- hypothesis_helm/tests/test_symbolic.py +111 -0
- hypothesis_helm/tests/test_templates.py +268 -0
- hypothesis_helm/tests/test_time_budget.py +197 -0
- hypothesis_helm/tests/test_topology_plotting.py +113 -0
- hypothesis_helm/tests/test_traversal.py +167 -0
- hypothesis_helm/tests/test_trim.py +165 -0
- hypothesis_helm/tests/test_values_model.py +183 -0
- hypothesis_helm/tests/test_verified_exports.py +395 -0
- hypothesis_helm/tests/test_workbalance.py +875 -0
- hypothesis_helm/tests/test_workbalance_gates.py +121 -0
- hypothesis_helm-0.1.0.dist-info/LICENSE +674 -0
- hypothesis_helm-0.1.0.dist-info/METADATA +290 -0
- hypothesis_helm-0.1.0.dist-info/RECORD +322 -0
- hypothesis_helm-0.1.0.dist-info/WHEEL +4 -0
- hypothesis_helm-0.1.0.dist-info/entry_points.txt +12 -0
- workbalance/README.md +317 -0
- workbalance/__init__.py +11 -0
- workbalance/checkpoints.py +77 -0
- workbalance/docs/images/pipeline-expanded.png +0 -0
- workbalance/docs/images/pipeline-fork-join.png +0 -0
- workbalance/docs/images/pipeline-initial.png +0 -0
- workbalance/example.py +99 -0
- workbalance/feedback.py +138 -0
- workbalance/graph.py +209 -0
- workbalance/plotting.py +56 -0
- workbalance/policy.py +192 -0
- workbalance/py.typed +0 -0
- workbalance/scheduler.py +604 -0
- workgraph/__init__.py +16 -0
- workgraph/gates.py +150 -0
- workgraph/operations.py +288 -0
- workgraph/output.py +89 -0
- workgraph/py.typed +0 -0
- workgraph/shutdown.py +94 -0
- workgraph/workloads.py +145 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Property-based tests for Helm charts.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from hypothesis_helm.charts.generate import coalesce, generate_tests
|
|
9
|
+
from hypothesis_helm.charts.model import Chart
|
|
10
|
+
from hypothesis_helm.charts.runner import check_chart
|
|
11
|
+
|
|
12
|
+
__all__ = ["Chart", "check_chart", "coalesce", "generate_tests"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> object:
|
|
16
|
+
"""
|
|
17
|
+
Load the public API without constructing strategies during pytest plugin startup.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name (str): Public package attribute requested by the caller.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
object: The exported chart type or framework function.
|
|
24
|
+
"""
|
|
25
|
+
if name not in __all__:
|
|
26
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
27
|
+
from hypothesis_helm.charts.generate import coalesce, generate_tests
|
|
28
|
+
from hypothesis_helm.charts.model import Chart
|
|
29
|
+
from hypothesis_helm.charts.runner import check_chart
|
|
30
|
+
|
|
31
|
+
exports: dict[str, object] = {
|
|
32
|
+
"Chart": Chart,
|
|
33
|
+
"check_chart": check_chart,
|
|
34
|
+
"coalesce": coalesce,
|
|
35
|
+
"generate_tests": generate_tests,
|
|
36
|
+
}
|
|
37
|
+
globals().update(exports)
|
|
38
|
+
return exports[name]
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Measure the downstream effects of explicit values mutations without changing a chart.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from hypothesis_helm.analysis.report import write_report
|
|
11
|
+
from hypothesis_helm.analysis.sensitivity import Mutation, analyze
|
|
12
|
+
from hypothesis_helm.charts.model import Chart
|
|
13
|
+
from hypothesis_helm.charts.rendering import render
|
|
14
|
+
from hypothesis_helm.reporting.budget import parse_time_limit
|
|
15
|
+
from hypothesis_helm.schemas.contracts import mapping, sequence
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_mutations(path: Path) -> list[Mutation]:
|
|
19
|
+
"""
|
|
20
|
+
Read explicit names, typed paths and replacement values from JSON.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
path (Path): JSON array of mutation descriptors.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
list[Mutation]: Validated operation descriptors in input order.
|
|
27
|
+
"""
|
|
28
|
+
mutations = []
|
|
29
|
+
for entry in sequence(json.loads(path.read_text())):
|
|
30
|
+
record = mapping(entry)
|
|
31
|
+
name, parts = record["name"], sequence(record["path"])
|
|
32
|
+
if not isinstance(name, str) or not name or not parts:
|
|
33
|
+
raise ValueError("each mutation requires a nonempty string name and path")
|
|
34
|
+
path_parts: list[str | int] = []
|
|
35
|
+
for part in parts:
|
|
36
|
+
if isinstance(part, str) or type(part) is int:
|
|
37
|
+
path_parts.append(part)
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("path components must be string keys or integer indices")
|
|
40
|
+
mutations.append(Mutation(name, tuple(path_parts), record["value"]))
|
|
41
|
+
return mutations
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main(argv: list[str] | None = None) -> int:
|
|
45
|
+
"""
|
|
46
|
+
Run a bounded local-chart sensitivity analysis and save its evidence.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
argv (list[str] | None): Explicit arguments or process command line.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
int: Zero for a complete analysis, one for a budget-limited result.
|
|
53
|
+
"""
|
|
54
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
55
|
+
parser.add_argument("chart", type=Path, help="local chart containing values.yaml and values.schema.json")
|
|
56
|
+
parser.add_argument("--mutations", required=True, type=Path, help="JSON array of name/path/value replacements")
|
|
57
|
+
parser.add_argument("--output", type=Path, default=Path(f"studies/sensitivity/runs/{time.time_ns()}"))
|
|
58
|
+
parser.add_argument("--max-pairs", type=int, default=100)
|
|
59
|
+
parser.add_argument("--max-mutations", type=int, default=100)
|
|
60
|
+
parser.add_argument("--time-limit", type=parse_time_limit, default=180)
|
|
61
|
+
parser.add_argument("--helm", default="helm")
|
|
62
|
+
parser.add_argument("--release", default="hypothesis")
|
|
63
|
+
parser.add_argument("--namespace", default="default")
|
|
64
|
+
parser.add_argument("--kube-version")
|
|
65
|
+
parser.add_argument("--plot", action="store_true", help="requires the benchmarking extra (matplotlib)")
|
|
66
|
+
args = parser.parse_args(argv)
|
|
67
|
+
if args.output.exists():
|
|
68
|
+
parser.error("choose a fresh output directory")
|
|
69
|
+
if args.max_pairs < 0 or args.max_mutations < 1:
|
|
70
|
+
parser.error("max-pairs must be nonnegative and max-mutations must be positive")
|
|
71
|
+
chart = Chart.load(args.chart)
|
|
72
|
+
mutations = load_mutations(args.mutations)
|
|
73
|
+
deadline = time.monotonic() + args.time_limit
|
|
74
|
+
|
|
75
|
+
def invoke(values: dict[str, object]) -> object:
|
|
76
|
+
"""
|
|
77
|
+
Render with a fixed invocation context and the remaining execution budget.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
values (dict[str, object]): Schema-valid values configuration.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
object: Parsed, validated manifest bundle.
|
|
84
|
+
"""
|
|
85
|
+
remaining = deadline - time.monotonic()
|
|
86
|
+
if remaining <= 0:
|
|
87
|
+
raise TimeoutError("analysis render deadline reached")
|
|
88
|
+
return render(
|
|
89
|
+
chart,
|
|
90
|
+
values,
|
|
91
|
+
helm=args.helm,
|
|
92
|
+
release=args.release,
|
|
93
|
+
namespace=args.namespace,
|
|
94
|
+
kube_version=args.kube_version,
|
|
95
|
+
timeout=min(30, remaining),
|
|
96
|
+
stream=False,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
document = analyze(
|
|
100
|
+
chart.defaults,
|
|
101
|
+
chart.schema,
|
|
102
|
+
mutations,
|
|
103
|
+
invoke,
|
|
104
|
+
max_pairs=args.max_pairs,
|
|
105
|
+
max_mutations=args.max_mutations,
|
|
106
|
+
time_limit=args.time_limit,
|
|
107
|
+
)
|
|
108
|
+
document["context"] = {
|
|
109
|
+
"chart": str(chart.path),
|
|
110
|
+
"helm": args.helm,
|
|
111
|
+
"release": args.release,
|
|
112
|
+
"namespace": args.namespace,
|
|
113
|
+
"kube_version": args.kube_version,
|
|
114
|
+
}
|
|
115
|
+
write_report(args.output, document, plots=args.plot)
|
|
116
|
+
print(f"Sensitivity analysis: {document['status']}; {document['renders']} renders; report: {args.output / 'README.md'}")
|
|
117
|
+
return 0 if document["status"] == "complete" else 1
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Publish concise sensitivity evidence and optional diagnostic plots.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hypothesis_helm.reporting.contents import with_contents
|
|
9
|
+
from hypothesis_helm.schemas.contracts import mapping, sequence
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_report(output: Path, document: dict[str, object], *, plots: bool = False) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Save measurements, a human-readable summary and optional matplotlib plots.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
output (Path): Fresh report directory.
|
|
18
|
+
document (dict[str, object]): Completed or partial sensitivity evidence.
|
|
19
|
+
plots (bool): Generate a three-panel PNG and SVG.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
None: Report artifacts are written.
|
|
23
|
+
"""
|
|
24
|
+
output.mkdir(parents=True, exist_ok=False)
|
|
25
|
+
(output / "results.json").write_text(json.dumps(document, indent=2, allow_nan=False) + "\n")
|
|
26
|
+
rows = [mapping(row) for row in sequence(document["mutations"])]
|
|
27
|
+
pairs = [mapping(row) for row in sequence(document["interactions"])]
|
|
28
|
+
identifiers = {str(row["name"]): index + 1 for index, row in enumerate(rows)}
|
|
29
|
+
lines = [
|
|
30
|
+
"# Mutation sensitivity",
|
|
31
|
+
"",
|
|
32
|
+
f"Status: **{document['status']}**. Helm render attempts: **{document['renders']}**.",
|
|
33
|
+
"",
|
|
34
|
+
"Distance counts added and removed JSON path/value indicators. A changed value counts twice.",
|
|
35
|
+
"Document and array order matter. These measurements do not prove equivalence or authorize pruning.",
|
|
36
|
+
"Distances assume deterministic rendering with fixed chart dependencies, release, namespace and Kubernetes version.",
|
|
37
|
+
"",
|
|
38
|
+
]
|
|
39
|
+
if plots:
|
|
40
|
+
plot(output, document)
|
|
41
|
+
lines.extend(["", ""])
|
|
42
|
+
lines += [
|
|
43
|
+
f"Measured {len(rows)} single mutations and {len(pairs)} pairs. The plots include every comparable measurement.",
|
|
44
|
+
"Tables show up to 20 of the largest effects. Mutation IDs follow the order in mutations.json.",
|
|
45
|
+
"",
|
|
46
|
+
"| ID | Mutation | Values path | Replacement | Output distance | Status |",
|
|
47
|
+
"| ---: | --- | --- | --- | ---: | --- |",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
def literal(value: object) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Escape untrusted labels and values for Markdown table cells.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value (object): JSON value to display.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
str: HTML-escaped JSON with table separators encoded.
|
|
59
|
+
"""
|
|
60
|
+
import html
|
|
61
|
+
|
|
62
|
+
return html.escape(json.dumps(value, ensure_ascii=True)).replace("|", "|")
|
|
63
|
+
|
|
64
|
+
for row in sorted(rows, key=lambda row: -int(str(row.get("distance", -1))))[:20]:
|
|
65
|
+
lines.append(
|
|
66
|
+
f"| {identifiers[str(row['name'])]} | {literal(row['name'])} | {literal(row['path'])} | {literal(row['value'])} | "
|
|
67
|
+
f"{row.get('distance', 'N/A')} | {row['status']} |"
|
|
68
|
+
)
|
|
69
|
+
lines += [
|
|
70
|
+
"",
|
|
71
|
+
"## Parameter interactions",
|
|
72
|
+
"",
|
|
73
|
+
"A nonzero mixed difference means the selected output features respond non-additively.",
|
|
74
|
+
"Pairs with order-dependent inputs are excluded from this measure. Render failures have no assigned distance.",
|
|
75
|
+
"",
|
|
76
|
+
"| Mutations | Mixed difference | Status |",
|
|
77
|
+
"| --- | ---: | --- |",
|
|
78
|
+
]
|
|
79
|
+
for row in sorted(pairs, key=lambda row: -int(str(row.get("mixed_difference_l1", -1))))[:20]:
|
|
80
|
+
lines.append(f"| {literal(row['mutations'])} | {row.get('mixed_difference_l1', 'N/A')} | {row['status']} |")
|
|
81
|
+
lines += [
|
|
82
|
+
"",
|
|
83
|
+
"The ordered sequence in results.json records both cumulative path length and displacement from the baseline.",
|
|
84
|
+
"They differ when later mutations reverse earlier changes. The sequence stops at its first invalid or failed step.",
|
|
85
|
+
"",
|
|
86
|
+
"[Full measurements and render errors](results.json)",
|
|
87
|
+
]
|
|
88
|
+
(output / "README.md").write_text(with_contents("\n".join(lines) + "\n"))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def plot(output: Path, document: dict[str, object]) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Plot mutation distances, pair interactions and ordered path accumulation.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
output (Path): Destination for plot files.
|
|
97
|
+
document (dict[str, object]): Recorded sensitivity observations.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
None: PNG and SVG plots are saved.
|
|
101
|
+
"""
|
|
102
|
+
import numpy as np
|
|
103
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
104
|
+
from matplotlib.figure import Figure
|
|
105
|
+
|
|
106
|
+
figure = Figure(figsize=(20, 7), layout="constrained")
|
|
107
|
+
FigureCanvasAgg(figure)
|
|
108
|
+
axes = figure.subplots(1, 3)
|
|
109
|
+
mutations = [mapping(row) for row in sequence(document["mutations"])]
|
|
110
|
+
names = [str(row["name"]) for row in mutations]
|
|
111
|
+
singles = [(index + 1, int(str(row["distance"]))) for index, row in enumerate(mutations) if "distance" in row]
|
|
112
|
+
if singles:
|
|
113
|
+
axes[0].scatter(*zip(*singles, strict=True), s=22, alpha=0.8)
|
|
114
|
+
else:
|
|
115
|
+
axes[0].text(0.5, 0.5, "No comparable single mutations", transform=axes[0].transAxes, ha="center")
|
|
116
|
+
axes[0].set(
|
|
117
|
+
title=f"Which single changes affect output most?\n{len(singles)} measured mutations; IDs follow the input file",
|
|
118
|
+
xlabel="Mutation ID",
|
|
119
|
+
ylabel="Changed leaf indicators",
|
|
120
|
+
)
|
|
121
|
+
pairs = [mapping(row) for row in sequence(document["interactions"]) if "mixed_difference_l1" in mapping(row)]
|
|
122
|
+
positions = {name: index for index, name in enumerate(names)}
|
|
123
|
+
matrix = np.full((len(names), len(names)), np.nan)
|
|
124
|
+
for row in pairs:
|
|
125
|
+
left, right = (positions[str(name)] for name in sequence(row["mutations"]))
|
|
126
|
+
matrix[left, right] = matrix[right, left] = int(str(row["mixed_difference_l1"]))
|
|
127
|
+
if pairs:
|
|
128
|
+
from matplotlib import colormaps
|
|
129
|
+
|
|
130
|
+
colors = colormaps["viridis"].with_extremes(bad="#dddddd")
|
|
131
|
+
heatmap = axes[1].imshow(
|
|
132
|
+
np.ma.masked_invalid(matrix),
|
|
133
|
+
origin="lower",
|
|
134
|
+
interpolation="nearest",
|
|
135
|
+
cmap=colors,
|
|
136
|
+
vmin=0,
|
|
137
|
+
vmax=max(1, float(np.nanmax(matrix))),
|
|
138
|
+
extent=(0.5, len(names) + 0.5, 0.5, len(names) + 0.5),
|
|
139
|
+
)
|
|
140
|
+
figure.colorbar(heatmap, ax=axes[1], label="Interaction magnitude", shrink=0.75)
|
|
141
|
+
else:
|
|
142
|
+
axes[1].text(0.5, 0.5, "No comparable pairs", transform=axes[1].transAxes, ha="center")
|
|
143
|
+
axes[1].set(
|
|
144
|
+
title=f"Which pairs interact?\n{len(pairs)} measured pairs; gray = unmeasured or inapplicable",
|
|
145
|
+
xlabel="Mutation ID",
|
|
146
|
+
ylabel="Mutation ID",
|
|
147
|
+
)
|
|
148
|
+
steps = [mapping(row) for row in sequence(document["sequence"]) if "cumulative_path_length" in mapping(row)]
|
|
149
|
+
indices = list(range(len(steps) + 1))
|
|
150
|
+
if steps:
|
|
151
|
+
axes[2].plot(indices, [0, *(int(str(row["cumulative_path_length"])) for row in steps)], marker=".", label="Cumulative path length")
|
|
152
|
+
axes[2].plot(
|
|
153
|
+
indices, [0, *(int(str(row["endpoint_displacement"])) for row in steps)], marker=".", label="Displacement from baseline"
|
|
154
|
+
)
|
|
155
|
+
axes[2].legend()
|
|
156
|
+
else:
|
|
157
|
+
axes[2].text(0.5, 0.5, "No comparable sequence", transform=axes[2].transAxes, ha="center")
|
|
158
|
+
axes[2].set(
|
|
159
|
+
title=f"Do later changes undo earlier ones?\n{len(steps)} measured steps in explicit input order",
|
|
160
|
+
xlabel="Completed mutation steps",
|
|
161
|
+
ylabel="Leaf indicators",
|
|
162
|
+
)
|
|
163
|
+
for index, axis in enumerate(axes):
|
|
164
|
+
axis.title.set_fontsize(10)
|
|
165
|
+
axis.tick_params(labelsize=9)
|
|
166
|
+
if index != 1:
|
|
167
|
+
axis.grid(alpha=0.2)
|
|
168
|
+
heading = figure.suptitle("Which values changes have the largest effects, and which interact?")
|
|
169
|
+
heading.set_gid("plot-question")
|
|
170
|
+
for suffix in ("png", "svg"):
|
|
171
|
+
figure.savefig(output / f"sensitivity.{suffix}", dpi=240)
|
|
172
|
+
figure.clear()
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Measure output sensitivity and interactions of explicit chart-value mutations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import itertools
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from collections.abc import Callable, Sequence
|
|
11
|
+
from typing import cast
|
|
12
|
+
|
|
13
|
+
from attrs import define
|
|
14
|
+
from jsonschema import validators
|
|
15
|
+
|
|
16
|
+
from hypothesis_helm.schemas.contracts import Json, configuration_key
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@define(frozen=True)
|
|
20
|
+
class Mutation:
|
|
21
|
+
"""
|
|
22
|
+
Replace one value at an explicit object or array path.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
name (str): Unique mutation label.
|
|
26
|
+
path (tuple[str | int, ...]): Object keys and array indices.
|
|
27
|
+
value (object): JSON-compatible replacement value.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
path: tuple[str | int, ...]
|
|
32
|
+
value: object
|
|
33
|
+
|
|
34
|
+
def apply(self, values: dict[str, object]) -> dict[str, object]:
|
|
35
|
+
"""
|
|
36
|
+
Apply a replacement without modifying the original configuration.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
values (dict[str, object]): Source configuration.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
dict[str, object]: Independently owned mutated configuration.
|
|
43
|
+
"""
|
|
44
|
+
if not self.name or not self.path:
|
|
45
|
+
raise ValueError("mutations need a name and a nonempty path")
|
|
46
|
+
result = copy.deepcopy(values)
|
|
47
|
+
node: object = result
|
|
48
|
+
for part in self.path[:-1]:
|
|
49
|
+
if isinstance(node, dict) and isinstance(part, str):
|
|
50
|
+
node = node[part]
|
|
51
|
+
elif isinstance(node, list) and type(part) is int and 0 <= part < len(node):
|
|
52
|
+
node = node[part]
|
|
53
|
+
else:
|
|
54
|
+
raise ValueError("mutation path does not address an existing container")
|
|
55
|
+
part = self.path[-1]
|
|
56
|
+
if isinstance(node, dict) and isinstance(part, str):
|
|
57
|
+
node[part] = copy.deepcopy(self.value)
|
|
58
|
+
elif isinstance(node, list) and type(part) is int and 0 <= part < len(node):
|
|
59
|
+
node[part] = copy.deepcopy(self.value)
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("mutation path does not address a replaceable value")
|
|
62
|
+
configuration_key(result)
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def features(value: object, path: tuple[str | int, ...] = ()) -> Counter[str]:
|
|
67
|
+
"""
|
|
68
|
+
Encode manifest leaves as exact path-and-value indicators.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
value (object): JSON manifest bundle or subtree.
|
|
72
|
+
path (tuple[str | int, ...]): Current structural position.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Counter[str]: One-hot leaf features, including empty containers.
|
|
76
|
+
"""
|
|
77
|
+
result: Counter[str] = Counter()
|
|
78
|
+
if isinstance(value, dict) and value:
|
|
79
|
+
for key, item in value.items():
|
|
80
|
+
result.update(features(item, (*path, str(key))))
|
|
81
|
+
elif isinstance(value, list) and value:
|
|
82
|
+
for index, item in enumerate(value):
|
|
83
|
+
result.update(features(item, (*path, index)))
|
|
84
|
+
else:
|
|
85
|
+
result[json.dumps([path, value], sort_keys=True, allow_nan=False)] += 1
|
|
86
|
+
return result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def distance(left: object, right: object) -> int:
|
|
90
|
+
"""
|
|
91
|
+
Compute L1 displacement between leaf-indicator representations.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
left (object): First parsed manifest bundle.
|
|
95
|
+
right (object): Second parsed manifest bundle.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
int: Added plus removed path/value indicators; a replacement counts twice.
|
|
99
|
+
"""
|
|
100
|
+
a, b = features(left), features(right)
|
|
101
|
+
return sum(abs(a[key] - b[key]) for key in a.keys() | b.keys())
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def analyze(
|
|
105
|
+
baseline: dict[str, object],
|
|
106
|
+
schema: dict[str, object],
|
|
107
|
+
mutations: Sequence[Mutation],
|
|
108
|
+
render: Callable[[dict[str, object]], object],
|
|
109
|
+
*,
|
|
110
|
+
max_pairs: int = 100,
|
|
111
|
+
max_mutations: int = 100,
|
|
112
|
+
time_limit: float = 180,
|
|
113
|
+
) -> dict[str, object]:
|
|
114
|
+
"""
|
|
115
|
+
Measure single mutations, pairwise interactions and a cumulative mutation sequence.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
baseline (dict[str, object]): Starting values configuration.
|
|
119
|
+
schema (dict[str, object]): JSON Schema restricting all measured configurations.
|
|
120
|
+
mutations (Sequence[Mutation]): Explicit replacement operations in sequence order.
|
|
121
|
+
render (Callable[[dict[str, object]], object]): Renderer with fixed chart and invocation context.
|
|
122
|
+
max_pairs (int): Maximum unordered pairs of selected mutations to examine.
|
|
123
|
+
max_mutations (int): Maximum supplied mutations to examine.
|
|
124
|
+
time_limit (float): Admission deadline; callers must also bound individual render invocations.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict[str, object]: Sensitivity rankings, interactions, sequence distances and explicit failures.
|
|
128
|
+
"""
|
|
129
|
+
if max_pairs < 0 or max_mutations < 1 or not 0 < time_limit < float("inf"):
|
|
130
|
+
raise ValueError("analysis limits must be nonnegative pairs, positive mutations and finite positive time")
|
|
131
|
+
if not mutations or len({mutation.name for mutation in mutations}) != len(mutations):
|
|
132
|
+
raise ValueError("provide at least one mutation with unique names")
|
|
133
|
+
validator = validators.validator_for(schema)(schema)
|
|
134
|
+
validator.validate(cast(Json, baseline))
|
|
135
|
+
deadline = time.monotonic() + time_limit
|
|
136
|
+
selected = list(mutations[:max_mutations])
|
|
137
|
+
observations: dict[str, dict[str, object]] = {}
|
|
138
|
+
singles: list[dict[str, object]] = []
|
|
139
|
+
interactions: list[dict[str, object]] = []
|
|
140
|
+
sequence_rows: list[dict[str, object]] = []
|
|
141
|
+
input_values: dict[str, dict[str, object]] = {}
|
|
142
|
+
renders = 0
|
|
143
|
+
|
|
144
|
+
def observe(values: dict[str, object]) -> dict[str, object]:
|
|
145
|
+
"""
|
|
146
|
+
Render a schema-valid configuration once within this analysis.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
values (dict[str, object]): Candidate chart input.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
dict[str, object]: Output evidence or a schema/render failure.
|
|
153
|
+
"""
|
|
154
|
+
nonlocal renders
|
|
155
|
+
if time.monotonic() >= deadline:
|
|
156
|
+
raise TimeoutError("analysis admission deadline reached")
|
|
157
|
+
key = configuration_key(values)
|
|
158
|
+
if key in observations:
|
|
159
|
+
return observations[key]
|
|
160
|
+
from jsonschema.exceptions import ValidationError
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
validator.validate(cast(Json, values))
|
|
164
|
+
except ValidationError as error:
|
|
165
|
+
return {"status": "schema-rejected", "error": error.message}
|
|
166
|
+
renders += 1
|
|
167
|
+
try:
|
|
168
|
+
output = render(copy.deepcopy(values))
|
|
169
|
+
json.dumps(output, sort_keys=True, allow_nan=False)
|
|
170
|
+
observation: dict[str, object] = {"status": "rendered", "output": output}
|
|
171
|
+
except Exception as error:
|
|
172
|
+
observation = {"status": "render-error", "error": str(error), "error_type": type(error).__name__}
|
|
173
|
+
observations[key] = observation
|
|
174
|
+
return observation
|
|
175
|
+
|
|
176
|
+
origin: dict[str, object] = {"status": "unobserved"}
|
|
177
|
+
status = "complete"
|
|
178
|
+
try:
|
|
179
|
+
origin = observe(baseline)
|
|
180
|
+
for mutation in selected:
|
|
181
|
+
row: dict[str, object] = {"name": mutation.name, "path": list(mutation.path), "value": mutation.value}
|
|
182
|
+
try:
|
|
183
|
+
values = mutation.apply(baseline)
|
|
184
|
+
except (ValueError, KeyError, IndexError, TypeError) as error:
|
|
185
|
+
singles.append({**row, "status": "invalid-mutation", "error": str(error)})
|
|
186
|
+
continue
|
|
187
|
+
observation = observe(values)
|
|
188
|
+
row.update(observation)
|
|
189
|
+
if observation["status"] == "rendered" and origin["status"] == "rendered":
|
|
190
|
+
row["distance"] = distance(origin["output"], observation["output"])
|
|
191
|
+
input_values[mutation.name] = values
|
|
192
|
+
singles.append(row)
|
|
193
|
+
for first, second in itertools.islice(itertools.combinations(selected, 2), max_pairs):
|
|
194
|
+
row = {"mutations": [first.name, second.name]}
|
|
195
|
+
if first.name not in input_values or second.name not in input_values:
|
|
196
|
+
interactions.append({**row, "status": "unavailable-single"})
|
|
197
|
+
continue
|
|
198
|
+
a, b = input_values[first.name], input_values[second.name]
|
|
199
|
+
try:
|
|
200
|
+
ab, ba = second.apply(a), first.apply(b)
|
|
201
|
+
except (ValueError, KeyError, IndexError, TypeError) as error:
|
|
202
|
+
interactions.append({**row, "status": "invalid-mutation", "error": str(error)})
|
|
203
|
+
continue
|
|
204
|
+
if configuration_key(ab) != configuration_key(ba):
|
|
205
|
+
interactions.append({**row, "status": "order-dependent"})
|
|
206
|
+
continue
|
|
207
|
+
joint = observe(ab)
|
|
208
|
+
row.update(joint)
|
|
209
|
+
observation_a, observation_b = observe(a), observe(b)
|
|
210
|
+
comparable = all(item["status"] == "rendered" for item in (origin, observation_a, observation_b, joint))
|
|
211
|
+
if comparable:
|
|
212
|
+
fa, fb, fab, f0 = (
|
|
213
|
+
features(observation_a["output"]),
|
|
214
|
+
features(observation_b["output"]),
|
|
215
|
+
features(joint["output"]),
|
|
216
|
+
features(origin["output"]),
|
|
217
|
+
)
|
|
218
|
+
keys = fa.keys() | fb.keys() | fab.keys() | f0.keys()
|
|
219
|
+
row["mixed_difference_l1"] = sum(abs(fab[key] - fa[key] - fb[key] + f0[key]) for key in keys)
|
|
220
|
+
row["joint_distance"] = distance(origin["output"], joint["output"])
|
|
221
|
+
else:
|
|
222
|
+
row["measurement_status"] = "unavailable-reference"
|
|
223
|
+
interactions.append(row)
|
|
224
|
+
current, previous = copy.deepcopy(baseline), origin
|
|
225
|
+
cumulative = 0
|
|
226
|
+
for mutation in selected:
|
|
227
|
+
try:
|
|
228
|
+
next_values = mutation.apply(current)
|
|
229
|
+
except (ValueError, KeyError, IndexError, TypeError) as error:
|
|
230
|
+
sequence_rows.append({"name": mutation.name, "status": "invalid-mutation", "error": str(error)})
|
|
231
|
+
break
|
|
232
|
+
next_observation = observe(next_values)
|
|
233
|
+
row = {"name": mutation.name, "status": next_observation["status"]}
|
|
234
|
+
if next_observation["status"] != "rendered" or previous["status"] != "rendered":
|
|
235
|
+
if previous["status"] != "rendered":
|
|
236
|
+
row["status"] = "preceding-output-unavailable"
|
|
237
|
+
row["error"] = next_observation.get("error", "preceding output unavailable")
|
|
238
|
+
sequence_rows.append(row)
|
|
239
|
+
break
|
|
240
|
+
step_distance = distance(previous["output"], next_observation["output"])
|
|
241
|
+
cumulative += step_distance
|
|
242
|
+
row.update(
|
|
243
|
+
step_distance=step_distance,
|
|
244
|
+
cumulative_path_length=cumulative,
|
|
245
|
+
endpoint_displacement=distance(origin["output"], next_observation["output"]),
|
|
246
|
+
)
|
|
247
|
+
sequence_rows.append(row)
|
|
248
|
+
current, previous = next_values, next_observation
|
|
249
|
+
except TimeoutError:
|
|
250
|
+
status = "time-limit"
|
|
251
|
+
if time.monotonic() >= deadline:
|
|
252
|
+
status = "time-limit"
|
|
253
|
+
if status == "complete" and (len(selected) < len(mutations) or len(selected) * (len(selected) - 1) // 2 > max_pairs):
|
|
254
|
+
status = "selection-limit"
|
|
255
|
+
return {
|
|
256
|
+
"version": 1,
|
|
257
|
+
"status": status,
|
|
258
|
+
"metric": "L1 of JSON path/value leaf indicators; arrays and documents remain ordered",
|
|
259
|
+
"baseline": origin,
|
|
260
|
+
"mutations": singles,
|
|
261
|
+
"interactions": interactions,
|
|
262
|
+
"sequence": sequence_rows,
|
|
263
|
+
"selected_mutations": len(selected),
|
|
264
|
+
"provided_mutations": len(mutations),
|
|
265
|
+
"renders": renders,
|
|
266
|
+
"pruning_authorized": False,
|
|
267
|
+
"assumption": "The renderer is deterministic under a fixed chart, dependency and invocation context.",
|
|
268
|
+
}
|