agentenv-framework 0.9.1254__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.
- agent_env/__init__.py +0 -0
- agent_env/a2a_agent/__init__.py +23 -0
- agent_env/a2a_agent/a2a_agent.py +600 -0
- agent_env/a2a_agent/conversation_store.py +356 -0
- agent_env/a2a_agent/object_transfer.py +609 -0
- agent_env/a2a_agent/protocol.py +238 -0
- agent_env/a2a_agent/store.py +201 -0
- agent_env/a2a_agent/validator.py +901 -0
- agent_env/artifact/__init__.py +48 -0
- agent_env/artifact/artifact.py +74 -0
- agent_env/artifact/artifacts/__init__.py +0 -0
- agent_env/artifact/artifacts/cli.py +83 -0
- agent_env/artifact/artifacts/docker_image.py +457 -0
- agent_env/artifact/artifacts/environment.py +89 -0
- agent_env/artifact/artifacts/environment_universe.py +156 -0
- agent_env/artifact/artifacts/file.py +214 -0
- agent_env/artifact/artifacts/file_artifact_universe.py +206 -0
- agent_env/artifact/artifacts/skill.py +206 -0
- agent_env/artifact/artifacts/vm_image.py +95 -0
- agent_env/artifact/ref.py +15 -0
- agent_env/artifact/registry.py +251 -0
- agent_env/artifact/store.py +295 -0
- agent_env/artifact/universe.py +38 -0
- agent_env/attribution.py +41 -0
- agent_env/bundle/__init__.py +4 -0
- agent_env/bundle/_fs.py +128 -0
- agent_env/bundle/authoring.py +239 -0
- agent_env/bundle/installed.py +175 -0
- agent_env/bundle/ledger.py +269 -0
- agent_env/bundle/materialize.py +204 -0
- agent_env/bundle/parse.py +594 -0
- agent_env/bundle/plan.py +414 -0
- agent_env/bundle/resolve.py +404 -0
- agent_env/bundle/run.py +361 -0
- agent_env/cli/__init__.py +69 -0
- agent_env/cli/__main__.py +10 -0
- agent_env/cli/_installers.py +576 -0
- agent_env/cli/_plugin_changes.py +947 -0
- agent_env/cli/a2a_agent/__init__.py +22 -0
- agent_env/cli/a2a_agent/add_skill.py +97 -0
- agent_env/cli/a2a_agent/deploy.py +60 -0
- agent_env/cli/a2a_agent/get.py +15 -0
- agent_env/cli/a2a_agent/get_instance.py +26 -0
- agent_env/cli/a2a_agent/put.py +78 -0
- agent_env/cli/a2a_agent/validate.py +56 -0
- agent_env/cli/artifact/__init__.py +20 -0
- agent_env/cli/artifact/cli.py +83 -0
- agent_env/cli/artifact/environment.py +44 -0
- agent_env/cli/artifact/environment_universe.py +163 -0
- agent_env/cli/artifact/file_artifact_universe.py +281 -0
- agent_env/cli/artifact/skill.py +128 -0
- agent_env/cli/banner.py +25 -0
- agent_env/cli/config.py +263 -0
- agent_env/cli/env/__init__.py +31 -0
- agent_env/cli/env/deploy.py +112 -0
- agent_env/cli/env/gateway.py +57 -0
- agent_env/cli/env/get_instance.py +40 -0
- agent_env/cli/env/mcp_server.py +410 -0
- agent_env/cli/env/multi.py +282 -0
- agent_env/cli/env/service_db.py +99 -0
- agent_env/cli/env/snapshot.py +35 -0
- agent_env/cli/env/state/__init__.py +6 -0
- agent_env/cli/env/state/init.py +108 -0
- agent_env/cli/env/state/teardown.py +44 -0
- agent_env/cli/env/website.py +216 -0
- agent_env/cli/env/website_browser.py +67 -0
- agent_env/cli/eval/__init__.py +16 -0
- agent_env/cli/eval/add_tasks.py +63 -0
- agent_env/cli/eval/create.py +49 -0
- agent_env/cli/eval/run.py +194 -0
- agent_env/cli/identity/__init__.py +5 -0
- agent_env/cli/identity/client_identity.py +37 -0
- agent_env/cli/plugin.py +438 -0
- agent_env/cli/run.py +285 -0
- agent_env/cli/task/__init__.py +21 -0
- agent_env/cli/task/create.py +109 -0
- agent_env/cli/task/get.py +17 -0
- agent_env/cli/task/get_instance.py +34 -0
- agent_env/cli/task/run.py +615 -0
- agent_env/cli/task/validate.py +31 -0
- agent_env/cli/up.py +123 -0
- agent_env/cli/utils.py +183 -0
- agent_env/config/__init__.py +50 -0
- agent_env/config/describe.py +1067 -0
- agent_env/config/errors.py +13 -0
- agent_env/config/loader.py +245 -0
- agent_env/config/model.py +154 -0
- agent_env/config/paths.py +25 -0
- agent_env/config/plugin_tables.py +101 -0
- agent_env/config/provenance.py +134 -0
- agent_env/config/runtime.py +1064 -0
- agent_env/config/snapshot.py +81 -0
- agent_env/entity_refs.py +186 -0
- agent_env/env/__init__.py +49 -0
- agent_env/env/env.py +433 -0
- agent_env/env/env_artifact_store.py +87 -0
- agent_env/env/envs/__init__.py +7 -0
- agent_env/env/envs/_deployment.py +281 -0
- agent_env/env/envs/gateway_server.py +32 -0
- agent_env/env/envs/mcp_server.py +387 -0
- agent_env/env/envs/multi_env.py +780 -0
- agent_env/env/envs/service_db/Dockerfile +20 -0
- agent_env/env/envs/service_db/Dockerfile.db-mcp +42 -0
- agent_env/env/envs/service_db/Dockerfile.db-web +8 -0
- agent_env/env/envs/service_db.py +92 -0
- agent_env/env/envs/website.py +294 -0
- agent_env/env/envs/website_browser/Dockerfile +40 -0
- agent_env/env/envs/website_browser/__init__.py +17 -0
- agent_env/env/envs/website_browser/entrypoint.sh +13 -0
- agent_env/env/gateway/Dockerfile +14 -0
- agent_env/env/gateway/__init__.py +37 -0
- agent_env/env/gateway/clock.py +184 -0
- agent_env/env/gateway/constants.py +190 -0
- agent_env/env/gateway/entrypoint.py +156 -0
- agent_env/env/gateway/gateway.py +1095 -0
- agent_env/env/gateway/get_time.py +60 -0
- agent_env/env/gateway/requirements.txt +4 -0
- agent_env/env/gateway/triggers.py +1193 -0
- agent_env/env/legacy_protocol.py +146 -0
- agent_env/env/registry.py +93 -0
- agent_env/env/snapshot_store.py +372 -0
- agent_env/env/store.py +411 -0
- agent_env/eval/__init__.py +14 -0
- agent_env/eval/eval.py +68 -0
- agent_env/eval/store.py +116 -0
- agent_env/examples/hello/README.md +9 -0
- agent_env/examples/hello/artifacts/greeting/check.sh +1 -0
- agent_env/examples/hello/artifacts/greeting/hello.txt +1 -0
- agent_env/examples/hello/tasks/hello.json +10 -0
- agent_env/explorer/__init__.py +12 -0
- agent_env/explorer/app.py +277 -0
- agent_env/explorer/openapi_docs.py +242 -0
- agent_env/explorer/plugin.py +95 -0
- agent_env/explorer/routers/__init__.py +1 -0
- agent_env/explorer/routers/common.py +174 -0
- agent_env/explorer/routers/conversations.py +25 -0
- agent_env/explorer/routers/objects.py +112 -0
- agent_env/explorer/routers/runs.py +475 -0
- agent_env/plugins/__init__.py +59 -0
- agent_env/plugins/_cli.py +244 -0
- agent_env/plugins/_discovery.py +97 -0
- agent_env/plugins/_inventory.py +262 -0
- agent_env/plugins/_registration.py +268 -0
- agent_env/plugins/_report.py +47 -0
- agent_env/plugins/_requirements.py +55 -0
- agent_env/providers/__init__.py +37 -0
- agent_env/providers/env_providers/__init__.py +12 -0
- agent_env/providers/env_providers/constants.py +13 -0
- agent_env/providers/env_providers/env_gateway_provider.py +1354 -0
- agent_env/providers/env_providers/env_provider.py +237 -0
- agent_env/providers/env_providers/env_server_provider.py +133 -0
- agent_env/providers/env_state/__init__.py +49 -0
- agent_env/providers/env_state/env_state_provider.py +554 -0
- agent_env/providers/env_state/local_postgres.py +390 -0
- agent_env/providers/env_state/store.py +112 -0
- agent_env/providers/sandbox_providers/__init__.py +29 -0
- agent_env/providers/sandbox_providers/chained_sandbox_provider.py +93 -0
- agent_env/providers/sandbox_providers/e2b/__init__.py +6 -0
- agent_env/providers/sandbox_providers/e2b/provider.py +376 -0
- agent_env/providers/sandbox_providers/e2b/sandbox.py +342 -0
- agent_env/providers/sandbox_providers/e2b/template.py +158 -0
- agent_env/providers/sandbox_providers/local_sandbox.py +408 -0
- agent_env/providers/sandbox_providers/modal_sandbox.py +500 -0
- agent_env/providers/sandbox_providers/modal_vm_sandbox.py +466 -0
- agent_env/providers/sandbox_providers/sandbox.py +387 -0
- agent_env/providers/sandbox_providers/sandbox_provider.py +614 -0
- agent_env/py.typed +0 -0
- agent_env/runner/__init__.py +16 -0
- agent_env/runner/local_runner.py +190 -0
- agent_env/runner/runner.py +127 -0
- agent_env/runner/store.py +94 -0
- agent_env/store/__init__.py +126 -0
- agent_env/store/_google.py +68 -0
- agent_env/store/base.py +33 -0
- agent_env/store/document_store/__init__.py +60 -0
- agent_env/store/document_store/document_store.py +519 -0
- agent_env/store/document_store/dynamodb_document_store.py +306 -0
- agent_env/store/document_store/evaluation.py +199 -0
- agent_env/store/document_store/firestore_mongo_document_store.py +242 -0
- agent_env/store/document_store/mongo_document_store.py +259 -0
- agent_env/store/document_store/sqlite_document_store.py +314 -0
- agent_env/store/ids.py +123 -0
- agent_env/store/image_store/__init__.py +34 -0
- agent_env/store/image_store/ecr_image_store.py +131 -0
- agent_env/store/image_store/google_credentials.py +113 -0
- agent_env/store/image_store/image_store.py +113 -0
- agent_env/store/image_store/local_registry_image_store.py +106 -0
- agent_env/store/image_store/oci_registry_credentials.py +196 -0
- agent_env/store/local_state.py +27 -0
- agent_env/store/object_store/__init__.py +24 -0
- agent_env/store/object_store/gcs_object_store.py +415 -0
- agent_env/store/object_store/local_object_store.py +143 -0
- agent_env/store/object_store/object_store.py +198 -0
- agent_env/store/object_store/s3_object_store.py +354 -0
- agent_env/store/query.py +112 -0
- agent_env/store/routing.py +609 -0
- agent_env/store/secret_store/__init__.py +19 -0
- agent_env/store/secret_store/aws_secrets_manager_secret_store.py +293 -0
- agent_env/store/secret_store/gcp_secret_manager_secret_store.py +319 -0
- agent_env/store/secret_store/local_secret_store.py +47 -0
- agent_env/store/secret_store/secret_store.py +19 -0
- agent_env/task/__init__.py +27 -0
- agent_env/task/interrupts.py +109 -0
- agent_env/task/registry.py +27 -0
- agent_env/task/step_journal.py +99 -0
- agent_env/task/store.py +940 -0
- agent_env/task/task.py +815 -0
- agent_env/task/teardown.py +142 -0
- agent_env/task_step/__init__.py +47 -0
- agent_env/task_step/context.py +195 -0
- agent_env/task_step/context_ops.py +224 -0
- agent_env/task_step/registry.py +182 -0
- agent_env/task_step/review_store.py +118 -0
- agent_env/task_step/snapshot_utils/__init__.py +2 -0
- agent_env/task_step/snapshot_utils/agent_state_capture.py +261 -0
- agent_env/task_step/snapshot_utils/snapshot_series.py +631 -0
- agent_env/task_step/store.py +129 -0
- agent_env/task_step/task_step.py +173 -0
- agent_env/task_step/task_steps/__init__.py +27 -0
- agent_env/task_step/task_steps/a2a_agent_validator/__init__.py +31 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/README.md +158 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.m4a +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp3 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.mp4 +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.ogg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/clip.wav +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/document.pdf +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.gif +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.jpg +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/fixtures/red.png +0 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_card.py +103 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_config_identity.py +69 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_agent_mcp.py +178 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_core_protocol.py +67 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_install.py +115 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution.py +124 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_litellm_attribution_runtime.py +173 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_modalities.py +270 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_peer_agents.py +102 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_role.py +151 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_skill_config.py +194 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_snapshot.py +111 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_system_prompt.py +215 -0
- agent_env/task_step/task_steps/a2a_agent_validator/verify_a2a_trajectory.py +164 -0
- agent_env/task_step/task_steps/add_skills.py +240 -0
- agent_env/task_step/task_steps/apply_server_config.py +288 -0
- agent_env/task_step/task_steps/collect_artifacts.py +940 -0
- agent_env/task_step/task_steps/deploy_agent.py +626 -0
- agent_env/task_step/task_steps/deploy_env.py +197 -0
- agent_env/task_step/task_steps/deploy_human_agent.py +100 -0
- agent_env/task_step/task_steps/deploy_sandbox.py +170 -0
- agent_env/task_step/task_steps/env_card_validator/__init__.py +4 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_card.py +109 -0
- agent_env/task_step/task_steps/env_card_validator/verify_env_core_protocol.py +108 -0
- agent_env/task_step/task_steps/install_agent.py +376 -0
- agent_env/task_step/task_steps/load_artifact.py +775 -0
- agent_env/task_step/task_steps/mcp_cli_builder/__init__.py +4 -0
- agent_env/task_step/task_steps/mcp_cli_builder/build_mcp_cli.py +201 -0
- agent_env/task_step/task_steps/mcp_cli_builder/codegen.py +627 -0
- agent_env/task_step/task_steps/mcp_env_validator/__init__.py +44 -0
- agent_env/task_step/task_steps/mcp_env_validator/validation_gate_aggregator.py +102 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_env_assessment.py +78 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_mcp_tool_schema.py +152 -0
- agent_env/task_step/task_steps/mcp_env_validator/verify_spec_conformance.py +486 -0
- agent_env/task_step/task_steps/modify_env_tool_access.py +80 -0
- agent_env/task_step/task_steps/multienv_validator/__init__.py +3 -0
- agent_env/task_step/task_steps/multienv_validator/combine_universe_verdicts.py +107 -0
- agent_env/task_step/task_steps/multienv_validator/universe_comparison.py +253 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_agent_judge.py +180 -0
- agent_env/task_step/task_steps/multienv_validator/verify_universe_roundtrip.py +241 -0
- agent_env/task_step/task_steps/peer_agents.py +92 -0
- agent_env/task_step/task_steps/prompt_agent.py +900 -0
- agent_env/task_step/task_steps/register_agent_triggers.py +113 -0
- agent_env/task_step/task_steps/register_env_triggers.py +97 -0
- agent_env/task_step/task_steps/reset_env.py +71 -0
- agent_env/task_step/task_steps/review.py +152 -0
- agent_env/task_step/task_steps/run_code.py +461 -0
- agent_env/task_step/task_steps/run_code_runner.py +52 -0
- agent_env/task_step/task_steps/run_docker_container.py +380 -0
- agent_env/task_step/task_steps/sandbox_utils/__init__.py +0 -0
- agent_env/task_step/task_steps/sandbox_utils/sandbox_utils.py +79 -0
- agent_env/task_step/task_steps/snapshot_agent_state.py +205 -0
- agent_env/task_step/task_steps/snapshot_env.py +637 -0
- agent_env/task_step/task_steps/sync_env_clock.py +132 -0
- agent_env/task_step/task_steps/teardown_sandboxes.py +170 -0
- agent_env/task_step/task_steps/verifiers/__init__.py +23 -0
- agent_env/task_step/task_steps/verifiers/agent_prompt_response_verifier.py +166 -0
- agent_env/task_step/task_steps/verifiers/aggregate_verifiers.py +90 -0
- agent_env/task_step/task_steps/verifiers/env_outcome_verifier.py +127 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/__init__.py +2 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/frame_selection.py +377 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/judge_output_format.py +1293 -0
- agent_env/task_step/task_steps/verifiers/judge_utils/trajectory_filter.py +514 -0
- agent_env/task_step/task_steps/verifiers/rubrics_verifier.py +1318 -0
- agent_env/task_step/task_steps/verifiers/run_container_unit_tests_verifier.py +434 -0
- agent_env/task_step/task_steps/verifiers/scoring.py +40 -0
- agent_env/task_step/task_steps/verifiers/verify_sandbox.py +308 -0
- agent_env/task_step/thread_work.py +61 -0
- agent_env/utils/__init__.py +0 -0
- agent_env/utils/card_naming.py +108 -0
- agent_env/utils/deprecation.py +109 -0
- agent_env/utils/docker_build.py +29 -0
- agent_env/utils/exec_retry.py +77 -0
- agent_env/utils/litellm_attribution.py +59 -0
- agent_env/utils/paths.py +20 -0
- agentenv_framework-0.9.1254.dist-info/METADATA +394 -0
- agentenv_framework-0.9.1254.dist-info/RECORD +312 -0
- agentenv_framework-0.9.1254.dist-info/WHEEL +4 -0
- agentenv_framework-0.9.1254.dist-info/entry_points.txt +5 -0
- agentenv_framework-0.9.1254.dist-info/licenses/LICENSE +202 -0
- agentenv_framework-0.9.1254.dist-info/licenses/NOTICE +4 -0
- agentenv_framework-0.9.1254.dist-info/licenses/THIRD_PARTY_NOTICES.md +7665 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Pure comparison functions for universe load/export roundtrip validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as _dt
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
_EMPTY = "\x00EMPTY\x00" # unifies None / "" / {} / []
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _canon_ts(s: str):
|
|
13
|
+
"""Return ('DT', utc-iso) or ('D', date-iso) for a timestamp/date string, else None."""
|
|
14
|
+
if len(s) == 10 and s[4] == "-" and s[7] == "-": # date-only YYYY-MM-DD (fromisoformat would make it midnight)
|
|
15
|
+
try:
|
|
16
|
+
return ("D", _dt.date.fromisoformat(s).isoformat())
|
|
17
|
+
except (ValueError, TypeError):
|
|
18
|
+
return None
|
|
19
|
+
try:
|
|
20
|
+
ss = s.replace("Z", "+00:00")
|
|
21
|
+
if " " in ss and "T" not in ss:
|
|
22
|
+
ss = ss.replace(" ", "T", 1)
|
|
23
|
+
d = _dt.datetime.fromisoformat(ss)
|
|
24
|
+
if d.tzinfo is not None:
|
|
25
|
+
d = d.astimezone(_dt.timezone.utc).replace(tzinfo=None)
|
|
26
|
+
return ("DT", d.isoformat())
|
|
27
|
+
except (ValueError, TypeError):
|
|
28
|
+
pass
|
|
29
|
+
try:
|
|
30
|
+
return ("D", _dt.date.fromisoformat(s).isoformat())
|
|
31
|
+
except (ValueError, TypeError):
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _canon(v: Any) -> Any:
|
|
36
|
+
"""Canonicalize a value so equivalent representations compare equal:
|
|
37
|
+
numeric strings -> float (rounded), timestamps -> UTC instant, empties -> sentinel."""
|
|
38
|
+
if v is None or v == "" or v == {} or v == []:
|
|
39
|
+
return _EMPTY
|
|
40
|
+
if isinstance(v, bool):
|
|
41
|
+
return v
|
|
42
|
+
if isinstance(v, (int, float)):
|
|
43
|
+
# Numbers stay numbers — we do NOT coerce them to timestamps. The old epoch heuristic
|
|
44
|
+
# (1.4e9-2.0e9 s / 1.4e12-2.0e12 ms) collided with financial amounts in that range
|
|
45
|
+
# (e.g. a ~$1.5B balance), and stripping sub-second precision made distinct amounts like
|
|
46
|
+
# 1_500_000_001.50 and 1_500_000_001.75 canonicalize equal — a silent false-negative that
|
|
47
|
+
# would mask real data loss. Timestamps are only canonicalized from strings (ISO/date),
|
|
48
|
+
# which is unambiguous; an epoch carried as a bare number on one side and ISO on the other
|
|
49
|
+
# is not a representation we produce, and if it ever occurred we'd rather flag than swallow.
|
|
50
|
+
return round(float(v), 4)
|
|
51
|
+
if isinstance(v, list):
|
|
52
|
+
return [_canon(x) for x in v]
|
|
53
|
+
if isinstance(v, dict):
|
|
54
|
+
return {k: _canon(x) for k, x in v.items()}
|
|
55
|
+
if isinstance(v, str):
|
|
56
|
+
s = v.strip()
|
|
57
|
+
# numeric string -> route through number canon (so "12.50" == 12.5), never a timestamp
|
|
58
|
+
try:
|
|
59
|
+
return _canon(float(s))
|
|
60
|
+
except (ValueError, TypeError):
|
|
61
|
+
pass
|
|
62
|
+
ts = _canon_ts(s)
|
|
63
|
+
return ts if ts is not None else s
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_ts(c: Any) -> bool:
|
|
68
|
+
return isinstance(c, tuple) and len(c) == 2 and c[0] in ("DT", "D")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _semeq(ca: Any, cb: Any) -> bool:
|
|
72
|
+
"""Recursive semantic equality over already-canonicalized values.
|
|
73
|
+
``ca`` is the universe (load) side, ``cb`` the export side."""
|
|
74
|
+
if ca == cb:
|
|
75
|
+
return True
|
|
76
|
+
# universe-null vs export-value = server enrichment / stamping (created_time, derived period ids,
|
|
77
|
+
# etc.) — round-trip-safe. Asymmetric: export-null vs universe-value (a real LOSS) stays flagged.
|
|
78
|
+
if ca == _EMPTY:
|
|
79
|
+
return True
|
|
80
|
+
# date vs datetime on the same calendar day (server truncated a datetime to a date)
|
|
81
|
+
if _is_ts(ca) and _is_ts(cb) and ca[1][:10] == cb[1][:10] and (ca[0] == "D" or cb[0] == "D"):
|
|
82
|
+
return True
|
|
83
|
+
# nested dicts: every non-empty universe key must survive in the export and match.
|
|
84
|
+
# Export-only keys are server enrichment (ignored), but DROPPING a non-empty universe key is
|
|
85
|
+
# real data loss and must fail; dropping an already-empty value (_EMPTY) is harmless.
|
|
86
|
+
if isinstance(ca, dict) and isinstance(cb, dict):
|
|
87
|
+
return all(ca[k] == _EMPTY or (k in cb and _semeq(ca[k], cb[k])) for k in ca)
|
|
88
|
+
# lists of dicts: try order-aligned (fast), then order-independent greedy match (the two sides may
|
|
89
|
+
# sort by different keys, e.g. export adds 'id'/'line_number' the universe lacks)
|
|
90
|
+
if isinstance(ca, list) and isinstance(cb, list):
|
|
91
|
+
if len(ca) != len(cb):
|
|
92
|
+
return False
|
|
93
|
+
if all(_semeq(x, y) for x, y in zip(ca, cb)):
|
|
94
|
+
return True
|
|
95
|
+
if ca and isinstance(ca[0], dict):
|
|
96
|
+
pool = list(cb)
|
|
97
|
+
for x in ca:
|
|
98
|
+
for j, y in enumerate(pool):
|
|
99
|
+
if _semeq(x, y):
|
|
100
|
+
pool.pop(j)
|
|
101
|
+
break
|
|
102
|
+
else:
|
|
103
|
+
return False
|
|
104
|
+
return True
|
|
105
|
+
return False
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _vals_equal(ov: Any, ev: Any) -> bool:
|
|
110
|
+
"""Semantic equality: ignores timezone/number/epoch/empty representation, tolerant of
|
|
111
|
+
date-vs-datetime and of server-added keys inside nested structures."""
|
|
112
|
+
return _semeq(_canon(ov), _canon(ev))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def normalize(obj: Any) -> Any:
|
|
116
|
+
"""Recursively sort lists of dicts by 'id' (or first key) for stable comparison."""
|
|
117
|
+
if isinstance(obj, dict):
|
|
118
|
+
return {k: normalize(v) for k, v in obj.items()}
|
|
119
|
+
if isinstance(obj, list):
|
|
120
|
+
normalized = [normalize(item) for item in obj]
|
|
121
|
+
if normalized and isinstance(normalized[0], dict):
|
|
122
|
+
sort_key = "id" if "id" in normalized[0] else next(iter(normalized[0]), "")
|
|
123
|
+
try:
|
|
124
|
+
normalized.sort(key=lambda x: str(x.get(sort_key, "")))
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
return normalized
|
|
128
|
+
return obj
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def compare_dicts(a: dict[str, Any], b: dict[str, Any], label_a: str, label_b: str) -> list[dict[str, Any]]:
|
|
132
|
+
"""Compare two normalized data dicts, return deduplicated issues."""
|
|
133
|
+
counts: dict[tuple[str, str, str], dict] = {}
|
|
134
|
+
|
|
135
|
+
for entity in sorted(set(a.keys()) | set(b.keys())):
|
|
136
|
+
a_list, b_list = a.get(entity, []), b.get(entity, [])
|
|
137
|
+
|
|
138
|
+
if not isinstance(a_list, list) or not isinstance(b_list, list):
|
|
139
|
+
if type(a_list) != type(b_list):
|
|
140
|
+
counts[(entity, "", "type_coercion")] = {"from_type": type(a_list).__name__, "to_type": type(b_list).__name__, "count": 1}
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
id_key = _find_id_key(a_list, b_list)
|
|
144
|
+
|
|
145
|
+
def _key(item, i):
|
|
146
|
+
base = str(item.get(id_key, i)) if id_key else str(i)
|
|
147
|
+
ent = item.get("entity_id") if isinstance(item, dict) else None
|
|
148
|
+
return f"{ent}|{base}" if ent is not None else base # entity-scope so same-numbered accounts don't cross-compare
|
|
149
|
+
|
|
150
|
+
a_by_id = {_key(item, i): item for i, item in enumerate(a_list)}
|
|
151
|
+
b_by_id = {_key(item, i): item for i, item in enumerate(b_list)}
|
|
152
|
+
|
|
153
|
+
if len(a_list) != len(b_list):
|
|
154
|
+
counts[(entity, "", "count_mismatch")] = {"a_count": len(a_list), "b_count": len(b_list), "count": 1}
|
|
155
|
+
|
|
156
|
+
for eid in set(a_by_id.keys()) | set(b_by_id.keys()):
|
|
157
|
+
a_item, b_item = a_by_id.get(eid), b_by_id.get(eid)
|
|
158
|
+
if a_item is None or b_item is None or not isinstance(a_item, dict) or not isinstance(b_item, dict):
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
for field in set(a_item.keys()) | set(b_item.keys()):
|
|
162
|
+
if field in a_item and field not in b_item:
|
|
163
|
+
key = (entity, field, "dropped_field")
|
|
164
|
+
counts.setdefault(key, {"count": 0, "non_empty_count": 0, "value_keys": set()})
|
|
165
|
+
counts[key]["count"] += 1
|
|
166
|
+
val = a_item[field]
|
|
167
|
+
if val not in (None, "", {}, []):
|
|
168
|
+
counts[key]["non_empty_count"] += 1
|
|
169
|
+
if isinstance(val, dict):
|
|
170
|
+
counts[key]["value_keys"].update(val.keys()) # for reshape detection
|
|
171
|
+
elif field not in a_item and field in b_item:
|
|
172
|
+
key = (entity, field, "added_field")
|
|
173
|
+
counts.setdefault(key, {"count": 0})
|
|
174
|
+
counts[key]["count"] += 1
|
|
175
|
+
elif field in a_item and field in b_item:
|
|
176
|
+
ov, ev = a_item[field], b_item[field]
|
|
177
|
+
if _vals_equal(ov, ev):
|
|
178
|
+
continue # semantically equal (timezone/number/empty/date-vs-datetime representation)
|
|
179
|
+
if type(ov).__name__ != type(ev).__name__ and not (ov is None or ev is None):
|
|
180
|
+
key = (entity, field, "type_coercion")
|
|
181
|
+
if key not in counts:
|
|
182
|
+
counts[key] = {"from_type": type(ov).__name__, "to_type": type(ev).__name__, "count": 0}
|
|
183
|
+
counts[key]["count"] += 1
|
|
184
|
+
elif json.dumps(ov, sort_keys=True, default=str) != json.dumps(ev, sort_keys=True, default=str):
|
|
185
|
+
key = (entity, field, "value_mismatch")
|
|
186
|
+
if key not in counts:
|
|
187
|
+
counts[key] = {"sample_a": str(ov)[:100], "sample_b": str(ev)[:100], "count": 0}
|
|
188
|
+
counts[key]["count"] += 1
|
|
189
|
+
|
|
190
|
+
# reshape detection: a dropped dict-field whose keys reappear as this table's added columns
|
|
191
|
+
# is the server expanding a blob into columns (e.g. archived_reconciliations.snapshot) — benign.
|
|
192
|
+
added_by_entity: dict[str, set] = {}
|
|
193
|
+
for (entity, field, issue_type) in counts:
|
|
194
|
+
if issue_type == "added_field":
|
|
195
|
+
added_by_entity.setdefault(entity, set()).add(field)
|
|
196
|
+
for (entity, field, issue_type), info in counts.items():
|
|
197
|
+
if issue_type == "dropped_field":
|
|
198
|
+
vk = info.get("value_keys") or set()
|
|
199
|
+
added = added_by_entity.get(entity, set())
|
|
200
|
+
if vk and len(vk & added) >= 0.6 * len(vk):
|
|
201
|
+
info["benign_reshape"] = True
|
|
202
|
+
|
|
203
|
+
issues = []
|
|
204
|
+
for (entity, field, issue_type), info in sorted(counts.items()):
|
|
205
|
+
if issue_type == "dropped_field":
|
|
206
|
+
detail = f"Field present in {label_a} but missing in {label_b} ({info['count']} occurrences, {info['non_empty_count']} non-empty)"
|
|
207
|
+
elif issue_type == "added_field":
|
|
208
|
+
detail = f"Field present in {label_b} but missing in {label_a} ({info['count']} occurrences)"
|
|
209
|
+
elif issue_type == "type_coercion":
|
|
210
|
+
detail = f"{info['from_type']} \u2192 {info['to_type']} ({info['count']} occurrences)"
|
|
211
|
+
elif issue_type == "count_mismatch":
|
|
212
|
+
detail = f"{label_a} has {info['a_count']} entities, {label_b} has {info['b_count']}"
|
|
213
|
+
elif issue_type == "value_mismatch":
|
|
214
|
+
detail = f"Values differ ({info['count']} occurrences), sample: {info['sample_a']!r} vs {info['sample_b']!r}"
|
|
215
|
+
else:
|
|
216
|
+
detail = f"{info['count']} occurrences"
|
|
217
|
+
|
|
218
|
+
issue: dict[str, Any] = {"entity": entity, "field": field, "type": issue_type, "detail": detail}
|
|
219
|
+
if issue_type == "dropped_field":
|
|
220
|
+
issue["non_empty_count"] = info["non_empty_count"]
|
|
221
|
+
if info.get("benign_reshape"):
|
|
222
|
+
issue["benign_reshape"] = True
|
|
223
|
+
issues.append(issue)
|
|
224
|
+
return issues
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def classify_issues(load_issues: list[dict[str, Any]], idempotency_issues: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], bool]:
|
|
228
|
+
"""Classify issues with phase and critical flags. Returns (annotated_issues, is_compatible)."""
|
|
229
|
+
_CRITICAL_TYPES = {"count_mismatch", "value_mismatch"}
|
|
230
|
+
_PHASE = {"added_field": "export"} # everything else is "load"; idempotency issues get "idempotency"
|
|
231
|
+
|
|
232
|
+
def _is_critical(issue: dict) -> bool:
|
|
233
|
+
if issue["type"] in _CRITICAL_TYPES:
|
|
234
|
+
return True
|
|
235
|
+
if issue["type"] == "dropped_field":
|
|
236
|
+
return issue.get("non_empty_count", 0) > 0 and not issue.get("benign_reshape")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
annotated = [{**i, "phase": _PHASE.get(i["type"], "load"), "critical": _is_critical(i)} for i in load_issues]
|
|
240
|
+
annotated += [{**i, "phase": "idempotency", "critical": True} for i in idempotency_issues]
|
|
241
|
+
is_compatible = not any(i["critical"] for i in annotated)
|
|
242
|
+
return annotated, is_compatible
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _find_id_key(a_list: list, b_list: list) -> str | None:
|
|
246
|
+
"""Detect the best ID key for matching entities between two lists."""
|
|
247
|
+
sample = (a_list or b_list or [None])[0]
|
|
248
|
+
if not isinstance(sample, dict):
|
|
249
|
+
return None
|
|
250
|
+
for candidate in ("id", "Id", "ID", "email_id", "ts", "name"):
|
|
251
|
+
if candidate in sample:
|
|
252
|
+
return candidate
|
|
253
|
+
return None
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Prompt + rubric construction for the file-based universe agent-judge (v1).
|
|
2
|
+
|
|
3
|
+
The agent-judge complements the programmatic verifier (``compare_dicts``). It is handed the
|
|
4
|
+
original + exported universes as files on its filesystem (``/universes/original``,
|
|
5
|
+
``/universes/export_1``, ``/universes/export_2``) and asked to flag only *major* (semantic)
|
|
6
|
+
differences — i.e. real data loss — while ignoring representation noise the programmatic side
|
|
7
|
+
already owns.
|
|
8
|
+
|
|
9
|
+
This module is pure (strings + a small criteria builder); it performs no I/O so it can be
|
|
10
|
+
unit-tested without external services.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
# Where LoadArtifactTaskStep stages the FileArtifactUniverse onto the judge agent.
|
|
16
|
+
UNIVERSES_DIR = "/universes"
|
|
17
|
+
|
|
18
|
+
# Prefix for the per-service rubric criteria; shared so judge_issues_from_verdict can recover the
|
|
19
|
+
# service name from a flagged criterion id.
|
|
20
|
+
SERVICE_CRITERION_PREFIX = "service_preserved__"
|
|
21
|
+
# Synthetic service bucket for cross-cutting (non-service-scoped) judge findings, so they render
|
|
22
|
+
# through the same per-service UNIVERSE_COMPATIBILITY path PV uses.
|
|
23
|
+
CROSS_SERVICE_KEY = "(cross-service)"
|
|
24
|
+
# Issue type/phase stamped on judge-derived issues (mirrors PV's issue shape).
|
|
25
|
+
JUDGE_ISSUE_TYPE = "agent_judge"
|
|
26
|
+
JUDGE_PHASE = "agent_judge"
|
|
27
|
+
|
|
28
|
+
# The definition of "semantically equivalent" — the IGNORE preamble (representation noise the
|
|
29
|
+
# programmatic verifier owns) and the FLAG list (real semantic/relational drift = data loss).
|
|
30
|
+
JUDGE_SYSTEM_PROMPT = f"""\
|
|
31
|
+
You are a meticulous data-fidelity judge. You are given two (sometimes three) snapshots of the \
|
|
32
|
+
same multi-service universe as JSON files on disk:
|
|
33
|
+
|
|
34
|
+
- `{UNIVERSES_DIR}/original/<service>.json` — the source-of-truth universe before any round-trip.
|
|
35
|
+
- `{UNIVERSES_DIR}/export_1/<service>.json` — the data after one load -> export-state cycle.
|
|
36
|
+
- `{UNIVERSES_DIR}/export_2/<service>.json` — the data after re-loading export_1 and exporting \
|
|
37
|
+
again (present only when available).
|
|
38
|
+
|
|
39
|
+
Your job is to decide whether the round-trip is **semantically lossless**: does the exported data \
|
|
40
|
+
preserve the *meaning* of the original? Compare `original` against `export_1` (and use `export_2` \
|
|
41
|
+
to corroborate stability). Read the files with your tools; do not guess.
|
|
42
|
+
|
|
43
|
+
VERIFY QUANTITATIVE CLAIMS PROGRAMMATICALLY — you have a code-execution environment, so use it. \
|
|
44
|
+
For anything involving math or counting — deciding whether two timestamps denote the same instant \
|
|
45
|
+
(across timezone offsets or epoch-seconds vs ISO), whether two numbers are equivalent, record \
|
|
46
|
+
counts, or whether a field stays present/populated across many records — WRITE AND RUN A SCRIPT \
|
|
47
|
+
that loads the JSON and checks it. Do NOT convert timestamps, compare numbers, or count records by \
|
|
48
|
+
hand: manual math and eyeballing large/sparse arrays are error-prone and are the most common source \
|
|
49
|
+
of both missed losses and false flags.
|
|
50
|
+
|
|
51
|
+
IGNORE the following — these are benign representation differences, NOT data loss:
|
|
52
|
+
- Timezone representation that resolves to the same instant (e.g. `-04:00` vs `Z`).
|
|
53
|
+
- Numeric formatting: `"12.50"` vs `12.5`, trailing zeros, int-vs-float, epoch-seconds vs ISO \
|
|
54
|
+
timestamp that denote the same moment.
|
|
55
|
+
- Server-added audit/enrichment fields present in the export but absent in the original (e.g. \
|
|
56
|
+
`created_at`, derived ids) — unless they overwrite or replace real data.
|
|
57
|
+
- Empty-equivalent values: `null` / `""` / `{{}}` / `[]`.
|
|
58
|
+
- Record or list ordering when the set of records is unchanged.
|
|
59
|
+
- Generation/scaffolding fields that are clearly build-time provenance (e.g. `noise_theme`) being \
|
|
60
|
+
dropped.
|
|
61
|
+
|
|
62
|
+
FLAG the following as a MAJOR difference (real data loss / corruption):
|
|
63
|
+
- A meaningful record present in the original is missing from the export (dropped record).
|
|
64
|
+
- A content-bearing value (amount, name, description, date meaning) changed or was lost.
|
|
65
|
+
- A populated business field in the original is null/absent in the export.
|
|
66
|
+
- Numeric precision loss beyond representation rounding.
|
|
67
|
+
- Free-text (description/memo/body) truncated, paraphrased, or semantically altered.
|
|
68
|
+
- Broken referential integrity: a reference that resolved in the original now dangles.
|
|
69
|
+
- Structural reshaping that drops information.
|
|
70
|
+
- Server-added values that are nonsensical placeholders replacing real data.
|
|
71
|
+
|
|
72
|
+
When in doubt about whether a dropped/changed field is *meaningful*, lean toward flagging it and \
|
|
73
|
+
explain your reasoning. Be specific: cite the service, entity, field, and an example value.\
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_user_prompt() -> str:
|
|
78
|
+
"""The task message handed to the judge agent."""
|
|
79
|
+
return (
|
|
80
|
+
f"Inspect the universe snapshots under `{UNIVERSES_DIR}/`. For each service, compare "
|
|
81
|
+
f"`original/<service>.json` against `export_1/<service>.json` (corroborate with "
|
|
82
|
+
f"`export_2/<service>.json` when present) and identify any MAJOR differences — real data "
|
|
83
|
+
f"loss or corruption — applying the IGNORE/FLAG rules from your instructions. "
|
|
84
|
+
f"Produce a concise per-service report: for each service state whether it is semantically "
|
|
85
|
+
f"preserved, and list every major difference you found with the service, entity, field, "
|
|
86
|
+
f"and an example (original value vs exported value). If a service is fully preserved "
|
|
87
|
+
f"(only benign representation differences), say so explicitly."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Cross-cutting FLAG criteria (1.0 = the drift class is ABSENT / data preserved).
|
|
92
|
+
_CROSS_CUTTING_CRITERIA: list[dict] = [
|
|
93
|
+
{"id": "record_preservation", "title": "Every meaningful record in the original (by identity) is present in the export; no real records were dropped (server-added records are fine)."},
|
|
94
|
+
{"id": "value_fidelity", "title": "Content-bearing values (amounts, names, descriptions, dates) are preserved in MEANING across the round-trip; ignore timezone/number/format representation."},
|
|
95
|
+
{"id": "referential_integrity", "title": "References between records/services still resolve after the round-trip; no orphaned or dangling references were introduced."},
|
|
96
|
+
{"id": "numeric_precision", "title": "Numeric values are preserved to full precision; no sub-decimal loss beyond representation rounding."},
|
|
97
|
+
{"id": "freetext_fidelity", "title": "Free-text fields (description/memo/body) are not truncated, paraphrased, or semantically altered."},
|
|
98
|
+
{"id": "enrichment_sanity", "title": "Server-added fields/values are plausible and consistent, not nonsensical placeholders replacing real data."},
|
|
99
|
+
{"id": "structure_preserved", "title": "Any structural reshaping (e.g. blob expanded into columns) preserves all information without loss."},
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def build_criteria(environment_names: list[str]) -> list[dict]:
|
|
104
|
+
"""Build the rubric: one per-service preservation criterion plus the cross-cutting FLAG set.
|
|
105
|
+
|
|
106
|
+
Each criterion is a dict with ``id``/``title``/``rubric_category``/``rubric_target`` as
|
|
107
|
+
expected by ``RubricsVerifierTaskStep``. A score of 1.0 means the data was preserved (good).
|
|
108
|
+
"""
|
|
109
|
+
criteria: list[dict] = []
|
|
110
|
+
for name in environment_names:
|
|
111
|
+
criteria.append({
|
|
112
|
+
"id": f"{SERVICE_CRITERION_PREFIX}{name}",
|
|
113
|
+
"title": (
|
|
114
|
+
f"All data for the '{name}' service is semantically preserved between the original "
|
|
115
|
+
f"and exported universes — no real records or content-bearing values were lost "
|
|
116
|
+
f"(ignore representation noise and benign server enrichment)."
|
|
117
|
+
),
|
|
118
|
+
"rubric_category": "Data Fidelity",
|
|
119
|
+
"rubric_target": "Outcome",
|
|
120
|
+
})
|
|
121
|
+
for crit in _CROSS_CUTTING_CRITERIA:
|
|
122
|
+
criteria.append({**crit, "rubric_category": "Data Fidelity", "rubric_target": "Outcome"})
|
|
123
|
+
return criteria
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def judge_issues_from_verdict(verdict: dict) -> dict[str, list[dict]]:
|
|
127
|
+
"""Map a rubric verdict's FLAGGED criteria into PV-shaped issues, grouped by service.
|
|
128
|
+
|
|
129
|
+
Returns ``{environment_key: [issue, ...]}`` where each issue mirrors the programmatic verifier's
|
|
130
|
+
issue shape (``entity``/``field``/``type``/``phase``/``critical``/``detail``) so it renders
|
|
131
|
+
through the same UNIVERSE_COMPATIBILITY surface. Per-service criteria attach to their service;
|
|
132
|
+
cross-cutting criteria attach under :data:`CROSS_SERVICE_KEY`. A criterion is "flagged" when its
|
|
133
|
+
boolean ``result`` is False (or, absent that, its score is below 1.0). Flagged = the judge found
|
|
134
|
+
a major (semantic) difference, so the issue is ``critical`` — gating the same way a PV critical
|
|
135
|
+
does.
|
|
136
|
+
"""
|
|
137
|
+
grouped: dict[str, list[dict]] = {}
|
|
138
|
+
for r in verdict.get("results", []):
|
|
139
|
+
result = r.get("result")
|
|
140
|
+
flagged = result is False or (result is None and r.get("score", 1.0) < 1.0)
|
|
141
|
+
if not flagged:
|
|
142
|
+
continue
|
|
143
|
+
cid = r.get("id", "")
|
|
144
|
+
detail = r.get("justification") or "agent-judge flagged a major difference"
|
|
145
|
+
if cid.startswith(SERVICE_CRITERION_PREFIX):
|
|
146
|
+
service = cid[len(SERVICE_CRITERION_PREFIX):]
|
|
147
|
+
field = "(semantic)"
|
|
148
|
+
else:
|
|
149
|
+
service = CROSS_SERVICE_KEY
|
|
150
|
+
field = cid
|
|
151
|
+
grouped.setdefault(service, []).append({
|
|
152
|
+
"entity": service,
|
|
153
|
+
"field": field,
|
|
154
|
+
"type": JUDGE_ISSUE_TYPE,
|
|
155
|
+
"phase": JUDGE_PHASE,
|
|
156
|
+
"critical": True,
|
|
157
|
+
"detail": detail,
|
|
158
|
+
})
|
|
159
|
+
return grouped
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def apply_judge_verdict(compat: dict, verdict: dict) -> dict:
|
|
163
|
+
"""Merge a rubric verdict into a programmatic UNIVERSE_COMPATIBILITY result, in place.
|
|
164
|
+
|
|
165
|
+
Flagged criteria become per-service critical issues (cross-cutting ones under
|
|
166
|
+
:data:`CROSS_SERVICE_KEY`); any flag flips the affected service's and the overall ``compatible``
|
|
167
|
+
to False — so the judge gates exactly the way a programmatic critical does. Existing programmatic
|
|
168
|
+
issues are preserved (judge issues are appended). The raw verdict is stored under ``agent_judge``
|
|
169
|
+
for audit. Returns the same (mutated) ``compat`` dict.
|
|
170
|
+
"""
|
|
171
|
+
grouped = judge_issues_from_verdict(verdict)
|
|
172
|
+
services = compat.setdefault("services", {})
|
|
173
|
+
for environment_key, issues in grouped.items():
|
|
174
|
+
entry = services.setdefault(environment_key, {"compatible": True, "issues": []})
|
|
175
|
+
entry.setdefault("issues", []).extend(issues)
|
|
176
|
+
entry["compatible"] = False
|
|
177
|
+
if grouped:
|
|
178
|
+
compat["compatible"] = False
|
|
179
|
+
compat["agent_judge"] = verdict
|
|
180
|
+
return compat
|