Setting the file. One moment. Validate Result · LLM To Bedrock · aws/agent-toolkit-for-aws · Skills Docs81
Creating Amazon Aurora Db Cluster With Instances
104
Routing Traffic With Route53 And CloudFront
Resilience Program Design
Creating API Gateway Stage
Lines200scripts/validate_result.py
Python·200 lines·7 KB
16 stdout: RUN_CONTEXT=match
17 RUN_CONTEXT=mismatch + MISMATCH <path> saved=<v> current=<v> per field
18 (source_key_sha256 prints only "differs" — never the hash values)
19 exit: 0 match · 1 mismatch · 2 file missing/unreadable/not-JSON
20
21Pure: argv -> stdout/exit code. No network, no AWS.
22"""
23import argparse
24import json
25import pathlib
26import sys
27
28import jsonschema
29
30SCHEMA_NAMES = ("analysis", "ingestion", "eval", "rewrite", "delta-decisions")
31SCHEMAS_DIR = pathlib.Path(__file__).parent / "schemas"
32
33# Run metadata, not run identity — excluded from the mismatch comparison
34# (design §5.1: a resume on a later calendar day must not invalidate anything).
35COMPARE_EXCLUDED_FIELDS = {"report_date_suffix"}
36
37# Fields whose values must never be printed side by side (secret fingerprints).
38REDACTED_FIELDS = {"source_key_sha256"}
39
40
41def load_json(path: str):
42 """Returns (data, error_message). error_message is None on success."""
43 p = pathlib.Path(path)
44 try:
45 return json.loads(p.read_text()), None
46 except OSError as e:
47 return None, f"cannot read {path}: {e}"
48 except json.JSONDecodeError as e:
49 return None, f"not valid JSON: {path}: {e}"
50
51
52def control_state(data) -> tuple:
53 """Pure: classify a (already schema-valid) result. Returns (control, extra)."""
54 if isinstance(data, dict):
55 if "blocked" in data:
56 return "blocked", {"reason": data["blocked"].get("reason", "")}
57 if "partial" in data:
58 return "partial", {"completed": data["partial"].get("completed", 0),
59 "total": data["partial"].get("total", 0)}
60 return "ok", {}
61
62
63def payload_branch_errors(schema: dict, data) -> list:
64 """For oneOf schemas, report errors against the payload branch (the branch
65 users intend most of the time); plain schemas report directly."""
66 branches = schema.get("oneOf")
67 if branches and isinstance(data, dict) and ("blocked" in data or "partial" in data):
68 # The user clearly intended a control state — report against that branch.
69 key = "blocked" if "blocked" in data else "partial"
70 for b in branches:
71 if key in b.get("properties", {}):
72 schema = b
73 break
74 elif branches:
75 schema = branches[0]
76 validator = jsonschema.Draft202012Validator(schema)
77 errors = []
78 for err in sorted(validator.iter_errors(data), key=lambda e: list(e.absolute_path)):
79 path = "$" + "".join(f"[{p!r}]" if isinstance(p, int) else f".{p}" for p in err.absolute_path)
80 errors.append(f"{path}: {err.message}")
81 return errors
82
83
84def validate_phase(schema_name: str, file_path: str) -> int:
85 data, err = load_json(file_path)
86 if err:
87 print(f"RESULT=error {err}")
88 return 2
89 schema, err = load_json(str(SCHEMAS_DIR / f"{schema_name}.json"))
90 if err:
91 print(f"RESULT=error schema load failed: {err}")
92 return 2
93
94 validator = jsonschema.Draft202012Validator(schema)
95 if validator.is_valid(data):
96 control, extra = control_state(data)
97 if control == "blocked":
98 print(f"RESULT=valid CONTROL=blocked REASON={extra['reason']}")
99 elif control == "partial":
100 print(f"RESULT=valid CONTROL=partial COMPLETED={extra['completed']} TOTAL={extra['total']}")
101 else:
102 print("RESULT=valid CONTROL=ok")
103 return 0
104
105 print("RESULT=invalid")
106 for line in payload_branch_errors(schema, data):
107 print(line)
108 return 1
109
110
111def flatten(obj, prefix="$"):
112 """Pure: flatten nested JSON into {path: leaf-value} for field-wise diff.
113 Empty containers are recorded as sentinel values so structural differences
114 (e.g. key present with {} vs key absent) are detected."""
115 out = {}
116 if isinstance(obj, dict):
117 if not obj:
118 out[prefix] = "__empty_object__"
119 else:
120 for k, v in obj.items():
121 out.update(flatten(v, f"{prefix}.{k}"))
122 elif isinstance(obj, list):
123 if not obj:
124 out[prefix] = "__empty_array__"
125 else:
126 for i, v in enumerate(obj):
127 out.update(flatten(v, f"{prefix}[{i}]"))
128 else:
129 out[prefix] = obj
130 return out
131
132
133def top_key(path: str) -> str:
134 """'$.log_files[0].sha256' -> 'log_files'."""
135 rest = path[2:]
136 for i, ch in enumerate(rest):
137 if ch in ".[":
138 return rest[:i]
139 return rest
140
141
142def compare_run_contexts(saved, current) -> list:
143 """Pure: list of MISMATCH lines (empty = match). Strict deep equality over
144 all fields minus COMPARE_EXCLUDED_FIELDS; unknown extra keys mismatch."""
145 flat_saved = {p: v for p, v in flatten(saved).items()
146 if top_key(p) not in COMPARE_EXCLUDED_FIELDS}
147 flat_current = {p: v for p, v in flatten(current).items()
148 if top_key(p) not in COMPARE_EXCLUDED_FIELDS}
149 lines = []
150 for path in sorted(set(flat_saved) | set(flat_current)):
151 sv = flat_saved.get(path, "<absent>")
152 cv = flat_current.get(path, "<absent>")
153 if sv != cv:
154 if top_key(path) in REDACTED_FIELDS:
155 lines.append(f"MISMATCH {path} differs")
156 else:
157 lines.append(f"MISMATCH {path} saved={json.dumps(sv)} current={json.dumps(cv)}")
158 return lines
159
160
161def check_run_context(saved_path: str, current_path: str) -> int:
162 saved, err = load_json(saved_path)
163 if err:
164 print(f"RUN_CONTEXT=error {err}")
165 return 2
166 current, err = load_json(current_path)
167 if err:
168 print(f"RUN_CONTEXT=error {err}")
169 return 2
170 lines = compare_run_contexts(saved, current)
171 if not lines:
172 print("RUN_CONTEXT=match")
173 return 0
174 print("RUN_CONTEXT=mismatch")
175 for line in lines:
176 print(line)
177 return 1
178
179
180def main(argv=None) -> int:
181 ap = argparse.ArgumentParser()
182 group = ap.add_mutually_exclusive_group(required=True)
183 group.add_argument("--schema", choices=SCHEMA_NAMES)
184 group.add_argument("--check-run-context", metavar="SAVED_JSON")
185 ap.add_argument("--current", metavar="CURRENT_JSON",
186 help="required with --check-run-context")
187 ap.add_argument("file", nargs="?", help="phase-result file (with --schema)")
188 args = ap.parse_args(argv)
189
190 if args.schema:
191 if not args.file:
192 ap.error("--schema requires a phase-result file argument")
193 return validate_phase(args.schema, args.file)
194 if not args.current:
195 ap.error("--check-run-context requires --current")
196 return check_run_context(args.check_run_context, args.current)
197
198
199if __name__ == "__main__":
200 sys.exit(main())