emdbva 0.0.1.dev144__tar.gz → 0.0.1.dev145__tar.gz

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.
Files changed (55) hide show
  1. {emdbva-0.0.1.dev144/emdbva.egg-info → emdbva-0.0.1.dev145}/PKG-INFO +1 -1
  2. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145/emdbva.egg-info}/PKG-INFO +1 -1
  3. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/emdbva.egg-info/SOURCES.txt +2 -0
  4. emdbva-0.0.1.dev145/va/audit/__init__.py +13 -0
  5. emdbva-0.0.1.dev145/va/audit/cif_revision.py +324 -0
  6. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/mainva.py +13 -0
  7. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/schemas/emdb_entry_full.schema.json +160 -0
  8. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/version.py +1 -1
  9. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/LICENSE +0 -0
  10. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/MANIFEST.in +0 -0
  11. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/README.rst +0 -0
  12. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/emdbva.egg-info/dependency_links.txt +0 -0
  13. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/emdbva.egg-info/entry_points.txt +0 -0
  14. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/emdbva.egg-info/requires.txt +0 -0
  15. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/emdbva.egg-info/top_level.txt +0 -0
  16. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/setup.cfg +0 -0
  17. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/setup.py +0 -0
  18. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/PATHS_codon_airflow.py +0 -0
  19. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/__init__.py +0 -0
  20. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/__init__.py +0 -0
  21. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/bars.py +0 -0
  22. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/connected_percentage.py +0 -0
  23. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/contour_level_predicator.py +0 -0
  24. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/emda_mmcc.py +0 -0
  25. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/emringer.py +0 -0
  26. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/inclusion.py +0 -0
  27. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/map_data_validation.py +0 -0
  28. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/overlap_percentage.py +0 -0
  29. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/phaserandomization.py +0 -0
  30. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/phenix_cc.py +0 -0
  31. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/phenix_mm.py +0 -0
  32. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/projections.py +0 -0
  33. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/qscore.py +0 -0
  34. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/residue_locres.py +0 -0
  35. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/resmap.py +0 -0
  36. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/smoc.py +0 -0
  37. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/strudel.py +0 -0
  38. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/surfaces.py +0 -0
  39. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/metrics/threedfsc.py +0 -0
  40. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/preparation.py +0 -0
  41. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/prepareandrun_codon_airflow.py +0 -0
  42. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/qscores.csv +0 -0
  43. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/schemas/__init__.py +0 -0
  44. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/schemas/loader.py +0 -0
  45. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/Checker.py +0 -0
  46. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/ChimeraxViews.py +0 -0
  47. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/MapProcessor.py +0 -0
  48. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/Model.py +0 -0
  49. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/__init__.py +0 -0
  50. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/cl_weights.pth +0 -0
  51. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/log_utils.py +0 -0
  52. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/misc.py +0 -0
  53. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/rescolor.py +0 -0
  54. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/utils/stars.py +0 -0
  55. {emdbva-0.0.1.dev144 → emdbva-0.0.1.dev145}/va/validationanalysis.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emdbva
3
- Version: 0.0.1.dev144
3
+ Version: 0.0.1.dev145
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: emdbva
3
- Version: 0.0.1.dev144
3
+ Version: 0.0.1.dev145
4
4
  Summary: CryoEM validation toolkit
5
5
  Home-page: https://test.pypi.org/project/va/
6
6
  Author: Zhe Wang
@@ -16,6 +16,8 @@ va/prepareandrun_codon_airflow.py
16
16
  va/qscores.csv
17
17
  va/validationanalysis.py
18
18
  va/version.py
19
+ va/audit/__init__.py
20
+ va/audit/cif_revision.py
19
21
  va/metrics/__init__.py
20
22
  va/metrics/bars.py
21
23
  va/metrics/connected_percentage.py
@@ -0,0 +1,13 @@
1
+ """Audit helpers used to make conservative VA execution decisions."""
2
+
3
+ from va.audit.cif_revision import (
4
+ audit_model_cifs_for_preparation,
5
+ should_run_va_for_cif,
6
+ write_cif_revision_audit_report,
7
+ )
8
+
9
+ __all__ = [
10
+ 'audit_model_cifs_for_preparation',
11
+ 'should_run_va_for_cif',
12
+ 'write_cif_revision_audit_report',
13
+ ]
@@ -0,0 +1,324 @@
1
+ """Gemmi-based audit revision checks for model CIF files."""
2
+
3
+ import os
4
+ import re
5
+ import json
6
+
7
+ import gemmi
8
+
9
+
10
+ _CATEGORY = '_pdbx_audit_revision_details'
11
+ _FIELDS = (
12
+ 'ordinal',
13
+ 'revision_ordinal',
14
+ 'data_content_type',
15
+ 'provider',
16
+ 'type',
17
+ 'description',
18
+ 'details',
19
+ )
20
+
21
+ DEFAULT_SKIP_RULES = [
22
+ {
23
+ 'type': 'Remediation',
24
+ 'description': 'Metalloprotein remediation',
25
+ 'decision': 'skip',
26
+ 'reason_code': 'metalloprotein_remediation',
27
+ },
28
+ {
29
+ 'type': 'Remediation',
30
+ 'description': 'Carbohydrate remediation',
31
+ 'decision': 'skip',
32
+ 'reason_code': 'carbohydrate_remediation',
33
+ },
34
+ ]
35
+
36
+
37
+ def _normalise_value(value):
38
+ """Normalize one Gemmi CIF value for display and comparison."""
39
+ if value is None:
40
+ return None
41
+ value = str(value).strip()
42
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in "'\"":
43
+ value = value[1:-1].strip()
44
+ value = re.sub(r'\s+', ' ', value)
45
+ if value in ('?', '.') or not value:
46
+ return None
47
+ return value
48
+
49
+
50
+ def _normalise_for_match(value):
51
+ """Return a normalized case-insensitive comparison value."""
52
+ value = _normalise_value(value)
53
+ return value.casefold() if value is not None else None
54
+
55
+
56
+ def _first_block(document):
57
+ """Return the first data block across supported Gemmi versions."""
58
+ try:
59
+ return document[0]
60
+ except (TypeError, IndexError):
61
+ return document.sole_block()
62
+
63
+
64
+ def _loop_from_block(block):
65
+ """Return the audit loop or ``None`` when the category is not looped."""
66
+ for field in _FIELDS:
67
+ try:
68
+ column = block.find_loop(f'{_CATEGORY}.{field}')
69
+ except Exception:
70
+ column = None
71
+ if column is not None:
72
+ return column.get_loop() if hasattr(column, 'get_loop') else column
73
+ return None
74
+
75
+
76
+ def _rows_from_loop(loop):
77
+ """Convert a Gemmi loop into normalized row dictionaries."""
78
+ tags = [str(tag) for tag in getattr(loop, 'tags', [])]
79
+ values = list(getattr(loop, 'values', []))
80
+ if not tags:
81
+ return []
82
+
83
+ width = len(tags)
84
+ rows = []
85
+ for offset in range(0, len(values), width):
86
+ row = {}
87
+ for field in _FIELDS:
88
+ tag = f'{_CATEGORY}.{field}'
89
+ try:
90
+ index = tags.index(tag)
91
+ except ValueError:
92
+ row[field] = None
93
+ continue
94
+ value_index = offset + index
95
+ row[field] = _normalise_value(values[value_index]) if value_index < len(values) else None
96
+ rows.append(row)
97
+ return rows
98
+
99
+
100
+ def _rows_from_scalars(block):
101
+ """Read a single audit row when the category is not represented as a loop."""
102
+ row = {}
103
+ found = False
104
+ for field in _FIELDS:
105
+ try:
106
+ value = block.find_value(f'{_CATEGORY}.{field}')
107
+ except Exception:
108
+ value = None
109
+ row[field] = _normalise_value(value)
110
+ found = found or row[field] is not None
111
+ return [row] if found else []
112
+
113
+
114
+ def _issue(code, message, path):
115
+ """Build a non-fatal audit issue."""
116
+ return {
117
+ 'code': code,
118
+ 'severity': 'warning',
119
+ 'path': path,
120
+ 'message': message,
121
+ }
122
+
123
+
124
+ def _rule_matches(row, rule):
125
+ """Return whether a rule exactly matches the relevant row fields."""
126
+ return (
127
+ _normalise_for_match(row.get('type')) == _normalise_for_match(rule.get('type'))
128
+ and _normalise_for_match(row.get('description')) == _normalise_for_match(rule.get('description'))
129
+ )
130
+
131
+
132
+ def _base_decision(rows, issues):
133
+ """Return the common report structure for parsed audit rows."""
134
+ return {
135
+ 'run_validation': True,
136
+ 'decision': 'run',
137
+ 'reason_code': 'unmatched_revision',
138
+ 'message': 'No known VA-skip audit rule matched; VA will run.',
139
+ 'matched_rules': [],
140
+ 'issues': issues,
141
+ 'audit_revision_details': {
142
+ 'row_count': len(rows),
143
+ 'rows': rows,
144
+ },
145
+ }
146
+
147
+
148
+ def should_run_va_for_cif(cif_path, rules=None):
149
+ """Return a conservative VA execution decision for one model CIF.
150
+
151
+ Args:
152
+ cif_path (str): Path to a model CIF/mmCIF file.
153
+ rules (list or None): Optional replacement list of skip-rule mappings.
154
+
155
+ Returns:
156
+ dict: A stable run/skip decision with parsed audit rows and warnings.
157
+ """
158
+ if not cif_path or not os.path.isfile(cif_path):
159
+ return {
160
+ 'run_validation': True,
161
+ 'decision': 'run',
162
+ 'reason_code': 'missing_cif',
163
+ 'message': 'Model CIF file is missing; VA will run without audit gating.',
164
+ 'matched_rules': [],
165
+ 'issues': [_issue('missing_cif', f'CIF file does not exist: {cif_path}', str(cif_path))],
166
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
167
+ }
168
+
169
+ try:
170
+ document = gemmi.cif.read_file(cif_path)
171
+ block = _first_block(document)
172
+ loop = _loop_from_block(block)
173
+ rows = _rows_from_loop(loop) if loop is not None else _rows_from_scalars(block)
174
+ except Exception as exc:
175
+ return {
176
+ 'run_validation': True,
177
+ 'decision': 'run',
178
+ 'reason_code': 'parse_error',
179
+ 'message': 'CIF audit data could not be parsed; VA will run.',
180
+ 'matched_rules': [],
181
+ 'issues': [_issue('parse_error', str(exc), str(cif_path))],
182
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
183
+ }
184
+
185
+ if not rows:
186
+ return {
187
+ 'run_validation': True,
188
+ 'decision': 'run',
189
+ 'reason_code': 'missing_audit_category',
190
+ 'message': 'No audit revision details were found; VA will run.',
191
+ 'matched_rules': [],
192
+ 'issues': [_issue(
193
+ 'missing_audit_category',
194
+ f'{_CATEGORY} is absent or has no rows.',
195
+ _CATEGORY,
196
+ )],
197
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
198
+ }
199
+
200
+ report = _base_decision(rows, [])
201
+ active_rules = DEFAULT_SKIP_RULES if rules is None else rules
202
+ matched_rules = []
203
+ matched_row_indexes = set()
204
+ for index, row in enumerate(rows):
205
+ for rule in active_rules:
206
+ if _rule_matches(row, rule):
207
+ matched_rules.append(dict(rule))
208
+ matched_row_indexes.add(index)
209
+
210
+ initial_type = _normalise_for_match('Initial release')
211
+ unrelated_rows = [
212
+ row for index, row in enumerate(rows)
213
+ if index not in matched_row_indexes
214
+ and _normalise_for_match(row.get('type')) not in (None, initial_type)
215
+ ]
216
+
217
+ if matched_rules and not unrelated_rows:
218
+ report.update({
219
+ 'run_validation': False,
220
+ 'decision': 'skip',
221
+ 'reason_code': 'known_remediation_skip',
222
+ 'message': 'A known remediation-only audit rule matched; VA validation will be skipped.',
223
+ 'matched_rules': matched_rules,
224
+ })
225
+ return report
226
+
227
+ if len(rows) == 1 and _normalise_for_match(rows[0].get('type')) == initial_type:
228
+ report.update({
229
+ 'reason_code': 'initial_release_only',
230
+ 'message': 'Only an initial release audit revision was found; VA will run.',
231
+ })
232
+ return report
233
+
234
+
235
+ def _model_cif_paths(prepobj):
236
+ """Resolve model CIF paths using the locations used by ``read_model``."""
237
+ model_names = getattr(prepobj, 'model', None) or []
238
+ if isinstance(model_names, str):
239
+ model_names = [model_names]
240
+
241
+ paths = []
242
+ for model_name in model_names:
243
+ if not model_name:
244
+ continue
245
+ if os.path.isabs(model_name):
246
+ paths.append(model_name)
247
+ continue
248
+ working_path = os.path.join(prepobj.vadir, model_name)
249
+ paths.append(working_path if os.path.isfile(working_path) else model_name)
250
+ return paths
251
+
252
+
253
+ def _aggregate_model_reports(model_paths, model_reports):
254
+ """Aggregate per-model decisions using the conservative all-skip policy."""
255
+ all_skip = bool(model_reports) and all(
256
+ report.get('decision') == 'skip' for report in model_reports
257
+ )
258
+ issues = [issue for report in model_reports for issue in report.get('issues', [])]
259
+ matched_rules = [rule for report in model_reports for rule in report.get('matched_rules', [])]
260
+ rows = [
261
+ row for report in model_reports
262
+ for row in report.get('audit_revision_details', {}).get('rows', [])
263
+ ]
264
+ return {
265
+ 'run_validation': not all_skip,
266
+ 'decision': 'skip' if all_skip else 'run',
267
+ 'reason_code': 'known_remediation_skip' if all_skip else 'model_audit_reviewed',
268
+ 'message': (
269
+ 'All model CIFs matched known remediation rules; VA validation will be skipped.'
270
+ if all_skip else 'At least one model CIF did not clearly match a skip rule; VA will run.'
271
+ ),
272
+ 'matched_rules': matched_rules,
273
+ 'issues': issues,
274
+ 'audit_revision_details': {'row_count': len(rows), 'rows': rows},
275
+ 'model_count': len(model_paths),
276
+ 'model_reports': model_reports,
277
+ }
278
+
279
+
280
+ def audit_model_cifs_for_preparation(prepobj):
281
+ """Audit all configured model CIFs for a prepared VA entry.
282
+
283
+ Args:
284
+ prepobj: Prepared VA input object with ``model`` and ``vadir`` attributes.
285
+
286
+ Returns:
287
+ dict: Aggregate audit decision and per-model reports. Missing or
288
+ malformed CIF data produces a run decision with warnings.
289
+ """
290
+ model_paths = _model_cif_paths(prepobj)
291
+ if not model_paths:
292
+ return {
293
+ 'run_validation': True,
294
+ 'decision': 'run',
295
+ 'reason_code': 'no_model',
296
+ 'message': 'No model was supplied; CIF audit gating was not applied.',
297
+ 'matched_rules': [],
298
+ 'issues': [],
299
+ 'audit_revision_details': {'row_count': 0, 'rows': []},
300
+ 'model_count': 0,
301
+ 'model_reports': [],
302
+ }
303
+
304
+ model_reports = [should_run_va_for_cif(path) for path in model_paths]
305
+ return _aggregate_model_reports(model_paths, model_reports)
306
+
307
+
308
+ def write_cif_revision_audit_report(prepobj, report):
309
+ """Write the audit fragment consumed by the existing JSON merger.
310
+
311
+ Args:
312
+ prepobj: Prepared VA input object with ``vadir`` and ``mapname``.
313
+ report (dict): Aggregate CIF audit decision.
314
+
315
+ Returns:
316
+ str: Path to the written audit report fragment.
317
+ """
318
+ report_path = os.path.join(
319
+ prepobj.vadir,
320
+ '{}_cif_revision_audit.json'.format(os.path.basename(prepobj.mapname)),
321
+ )
322
+ with open(report_path, 'w', encoding='utf-8') as output:
323
+ json.dump({'audit_validation': {'cif_revision_audit': report}}, output, indent=2)
324
+ return report_path
@@ -40,6 +40,10 @@ from va.validationanalysis import ValidationAnalysis
40
40
  from va.preparation import PreParation as prep
41
41
  from va.metrics.strudel import run_strudel
42
42
  from va.utils.log_utils import LogRedirector
43
+ from va.audit.cif_revision import (
44
+ audit_model_cifs_for_preparation,
45
+ write_cif_revision_audit_report,
46
+ )
43
47
  import threading
44
48
  try:
45
49
  from PATHS import MAP_SERVER_PATH
@@ -335,6 +339,7 @@ def reconfigure_logging(temp_logger, buffer, log_file):
335
339
 
336
340
  temp_logger.info(f"Logging reconfigured to use file: {log_file}")
337
341
 
342
+
338
343
  # @profile
339
344
  def main():
340
345
  """Run the main VA command-line entry point.
@@ -348,6 +353,14 @@ def main():
348
353
  # Preparation
349
354
  # temp_logger, buffer = setup_temporary_logging()
350
355
  prepobj = prep()
356
+
357
+ if not prepobj.args.positions:
358
+ audit_report = audit_model_cifs_for_preparation(prepobj)
359
+ write_cif_revision_audit_report(prepobj, audit_report)
360
+ if not audit_report['run_validation']:
361
+ print(audit_report['message'])
362
+ return None
363
+
351
364
  # path_to_logs = f'{prepobj.vadir}/{prepobj.mapname}_va.log'
352
365
  # reconfigure_logging(temp_logger, buffer, path_to_logs)
353
366
  position = prepobj.args.positions
@@ -1031,6 +1031,163 @@
1031
1031
  ],
1032
1032
  "additionalProperties": true
1033
1033
  },
1034
+ "CifRevisionAuditRow": {
1035
+ "type": "object",
1036
+ "properties": {
1037
+ "ordinal": {
1038
+ "type": [
1039
+ "string",
1040
+ "null"
1041
+ ]
1042
+ },
1043
+ "revision_ordinal": {
1044
+ "type": [
1045
+ "string",
1046
+ "null"
1047
+ ]
1048
+ },
1049
+ "data_content_type": {
1050
+ "type": [
1051
+ "string",
1052
+ "null"
1053
+ ]
1054
+ },
1055
+ "provider": {
1056
+ "type": [
1057
+ "string",
1058
+ "null"
1059
+ ]
1060
+ },
1061
+ "type": {
1062
+ "type": [
1063
+ "string",
1064
+ "null"
1065
+ ]
1066
+ },
1067
+ "description": {
1068
+ "type": [
1069
+ "string",
1070
+ "null"
1071
+ ]
1072
+ },
1073
+ "details": {
1074
+ "type": [
1075
+ "string",
1076
+ "null"
1077
+ ]
1078
+ }
1079
+ },
1080
+ "additionalProperties": true
1081
+ },
1082
+ "CifRevisionAuditRule": {
1083
+ "type": "object",
1084
+ "properties": {
1085
+ "type": {
1086
+ "type": "string"
1087
+ },
1088
+ "description": {
1089
+ "type": "string"
1090
+ },
1091
+ "decision": {
1092
+ "type": "string"
1093
+ },
1094
+ "reason_code": {
1095
+ "type": "string"
1096
+ }
1097
+ },
1098
+ "additionalProperties": true
1099
+ },
1100
+ "CifRevisionAuditIssue": {
1101
+ "type": "object",
1102
+ "properties": {
1103
+ "severity": {
1104
+ "type": "string"
1105
+ },
1106
+ "code": {
1107
+ "type": "string"
1108
+ },
1109
+ "message": {
1110
+ "type": "string"
1111
+ },
1112
+ "details": {
1113
+ "type": [
1114
+ "string",
1115
+ "object",
1116
+ "array",
1117
+ "null"
1118
+ ]
1119
+ }
1120
+ },
1121
+ "additionalProperties": true
1122
+ },
1123
+ "CifRevisionAuditReport": {
1124
+ "type": "object",
1125
+ "properties": {
1126
+ "run_validation": {
1127
+ "type": "boolean"
1128
+ },
1129
+ "decision": {
1130
+ "type": "string",
1131
+ "enum": [
1132
+ "run",
1133
+ "skip",
1134
+ "unknown"
1135
+ ]
1136
+ },
1137
+ "reason_code": {
1138
+ "type": "string"
1139
+ },
1140
+ "message": {
1141
+ "type": "string"
1142
+ },
1143
+ "matched_rules": {
1144
+ "type": "array",
1145
+ "items": {
1146
+ "$ref": "#/definitions/CifRevisionAuditRule"
1147
+ }
1148
+ },
1149
+ "issues": {
1150
+ "type": "array",
1151
+ "items": {
1152
+ "$ref": "#/definitions/CifRevisionAuditIssue"
1153
+ }
1154
+ },
1155
+ "audit_revision_details": {
1156
+ "type": "object",
1157
+ "properties": {
1158
+ "row_count": {
1159
+ "type": "integer"
1160
+ },
1161
+ "rows": {
1162
+ "type": "array",
1163
+ "items": {
1164
+ "$ref": "#/definitions/CifRevisionAuditRow"
1165
+ }
1166
+ }
1167
+ },
1168
+ "additionalProperties": true
1169
+ },
1170
+ "model_count": {
1171
+ "type": "integer"
1172
+ },
1173
+ "model_reports": {
1174
+ "type": "array",
1175
+ "items": {
1176
+ "$ref": "#/definitions/CifRevisionAuditReport"
1177
+ }
1178
+ }
1179
+ },
1180
+ "additionalProperties": true
1181
+ },
1182
+ "AuditValidation": {
1183
+ "type": "object",
1184
+ "properties": {
1185
+ "cif_revision_audit": {
1186
+ "$ref": "#/definitions/CifRevisionAuditReport"
1187
+ }
1188
+ },
1189
+ "additionalProperties": true
1190
+ },
1034
1191
  "DataValidation": {
1035
1192
  "type": "object",
1036
1193
  "description": "Legacy or diagnostic validation container retained for compatibility.",
@@ -2708,6 +2865,9 @@
2708
2865
  "data_integrity": {
2709
2866
  "$ref": "#/definitions/DataIntegrity"
2710
2867
  },
2868
+ "audit_validation": {
2869
+ "$ref": "#/definitions/AuditValidation"
2870
+ },
2711
2871
  "data_validation": {
2712
2872
  "$ref": "#/definitions/DataValidation"
2713
2873
  }
@@ -18,5 +18,5 @@ under the License.
18
18
 
19
19
  """
20
20
 
21
- __version__ = '0.0.1.dev144'
21
+ __version__ = '0.0.1.dev145'
22
22
  __em_statistics_version__ = '202505.v01'
File without changes
File without changes
File without changes
File without changes
File without changes