nativegate 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.
Files changed (50) hide show
  1. nativegate/__init__.py +1 -0
  2. nativegate/__main__.py +4 -0
  3. nativegate/buildinfo.py +344 -0
  4. nativegate/cli.py +2007 -0
  5. nativegate/config.py +991 -0
  6. nativegate/declared_invariants.py +565 -0
  7. nativegate/discovery.py +167 -0
  8. nativegate/driverbuild.py +626 -0
  9. nativegate/drivers/__init__.py +5 -0
  10. nativegate/drivers/cpp.py +616 -0
  11. nativegate/drivers/fortran.py +507 -0
  12. nativegate/generators/__init__.py +0 -0
  13. nativegate/generators/cmake_gen.py +101 -0
  14. nativegate/generators/docker_gen.py +614 -0
  15. nativegate/generators/error_gen.py +104 -0
  16. nativegate/generators/f2py_gen.py +91 -0
  17. nativegate/generators/gateway_gen.py +110 -0
  18. nativegate/generators/golden_gen.py +50 -0
  19. nativegate/generators/k8s_gen.py +212 -0
  20. nativegate/generators/mcp_gen.py +281 -0
  21. nativegate/generators/middleware_gen.py +717 -0
  22. nativegate/generators/pybind_gen.py +406 -0
  23. nativegate/generators/pyproject_gen.py +61 -0
  24. nativegate/generators/python_pkg_gen.py +1164 -0
  25. nativegate/generators/test_gen.py +160 -0
  26. nativegate/golden.py +747 -0
  27. nativegate/invariants.py +532 -0
  28. nativegate/ir.py +789 -0
  29. nativegate/lattice.py +350 -0
  30. nativegate/locking.py +216 -0
  31. nativegate/oracle.py +904 -0
  32. nativegate/parsers/__init__.py +0 -0
  33. nativegate/parsers/cpp.py +105 -0
  34. nativegate/parsers/cpp_ast.py +1652 -0
  35. nativegate/parsers/cpp_regex.py +812 -0
  36. nativegate/parsers/fixed_form.py +868 -0
  37. nativegate/parsers/fortran.py +157 -0
  38. nativegate/parsers/fortran_fparser.py +1116 -0
  39. nativegate/parsers/fortran_regex.py +686 -0
  40. nativegate/preprocess.py +335 -0
  41. nativegate/structural_invariants.py +762 -0
  42. nativegate/suggest.py +208 -0
  43. nativegate/templates/__init__.py +20 -0
  44. nativegate/templates/golden_test_template.py +248 -0
  45. nativegate/wire.py +438 -0
  46. nativegate-0.1.0.dist-info/METADATA +547 -0
  47. nativegate-0.1.0.dist-info/RECORD +50 -0
  48. nativegate-0.1.0.dist-info/WHEEL +5 -0
  49. nativegate-0.1.0.dist-info/entry_points.txt +3 -0
  50. nativegate-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,532 @@
1
+ """T12 -- `invariants.json`: the layer-3 artifact and its aggregate gate.
2
+
3
+ Spec: design-verification-layers.md section 3.7 (file schema, `uncovered`),
4
+ section 2.6 (CI ordering: oracle check -> golden -> invariants), section 5
5
+ (where each layer runs).
6
+
7
+ This module is the closing task of the verification-layers effort: it does
8
+ not implement new checking logic (T10's structural properties and T11's
9
+ declared properties already do that), it *aggregates* them into one result
10
+ artifact per entry, and enforces the one rule none of the earlier tasks were
11
+ positioned to enforce on their own -- "an invariants file that silently
12
+ checks nothing must not look like a pass" (section 3.7, quoted in
13
+ `EmptyCheckedError`).
14
+
15
+ Ambiguities resolved here (T10/T11 were silent on them because they operate
16
+ one property/one sweep at a time, not on the whole file):
17
+
18
+ * **Which sweep(s) `bounds`/`sum_to_one` run against.** Neither carries an
19
+ `in:` parameter (unlike `monotone`/`scales_linearly_in`), so section 3.4's
20
+ lattice does not say which of an entry's several 1-D sweeps a parameter-
21
+ less property is evaluated over. This module evaluates them over *every*
22
+ swept parameter of the entry -- each of the entry's `ParameterSweep`s in
23
+ turn -- rather than inventing a "the first one" or "the base point only"
24
+ reading, on the theory that a `bounds` claim is supposed to hold
25
+ everywhere the lattice looks, not only along one arbitrarily-chosen axis.
26
+ An entry with no swept parameter at all (nothing in `ranges:` matches its
27
+ numeric arguments) cannot evaluate the property and is recorded under
28
+ `uncovered` instead of silently skipped.
29
+ * **`uncovered`'s scope.** Section 3.4 says "a swept parameter with no
30
+ declared range is an error ... recorded under `uncovered`" for the
31
+ *lattice* in general, not only for parameters a declared invariant
32
+ mentions. This module therefore records EVERY entry with an unswept
33
+ numeric parameter under `uncovered` (via `EntryLattice.unswept_parameters`,
34
+ T9), whether or not that entry carries a declared invariant at all --
35
+ this is what surfaces `tubing_bhp`'s undeclared `rate` range even though
36
+ no `invariants: tubing_bhp: ...` block exists in `nativegate.yaml`.
37
+ * **`corners`/`scatter`/`n` default from `VerificationConfig.lattice`.**
38
+ T8's `VerificationConfig` now parses a `lattice:` block (`n`,
39
+ `scatter: {seed, count}`, `corners:`) -- see `config.LatticeConfig`. Every
40
+ `n=`/`scatter_seed=`/`scatter_count=`/`corners=` keyword below defaults to
41
+ `None`, meaning "take it from `verification.lattice`"; an explicit
42
+ argument (still accepted, for tests and any other direct caller) always
43
+ wins over the declared config. `corners` is reported in `invariants.json`
44
+ as a mapping of function name to its declared corner tuples (matching
45
+ `LatticeConfig.corners`'s own shape) rather than the flat list this module
46
+ used before the config surface existed, since a flat list cannot say which
47
+ function each corner belongs to once more than one function declares
48
+ corners.
49
+ """
50
+
51
+ from __future__ import annotations
52
+
53
+ import json
54
+ from dataclasses import dataclass, field, replace
55
+ from pathlib import Path
56
+ from typing import TYPE_CHECKING, Sequence
57
+
58
+ from . import golden, lattice, structural_invariants as si
59
+ from .config import (
60
+ BoundsProperty,
61
+ MonotoneProperty,
62
+ ScalesLinearlyInProperty,
63
+ SumToOneProperty,
64
+ SymmetricInProperty,
65
+ VerificationConfig,
66
+ )
67
+ from .declared_invariants import NotImplementedInvariant, PropertyReport, evaluate_property, _label
68
+ from .lattice import EntryLattice, ParameterSweep
69
+
70
+ if TYPE_CHECKING:
71
+ from .ir import ModuleIR
72
+
73
+ INVARIANTS_FILENAME = "invariants.json"
74
+ FORMAT_VERSION = 1
75
+
76
+
77
+ class InvariantsError(RuntimeError):
78
+ """Base for a hard failure of the invariants runner itself."""
79
+
80
+
81
+ class EmptyCheckedError(InvariantsError):
82
+ """`checked` ended up empty -- section 3.7: "an invariants file that
83
+ silently checks nothing must not look like a pass." Never swallowed."""
84
+
85
+
86
+ class _UncoveredError(Exception):
87
+ """Internal: a declared property could not be evaluated. Turned into an
88
+ `uncovered` reason string, never raised past this module."""
89
+
90
+
91
+ @dataclass
92
+ class InvariantsResult:
93
+ document: dict
94
+ passed: bool
95
+ failure_messages: list[str] = field(default_factory=list)
96
+
97
+
98
+ # --- picking which sweep(s) a declared property runs against ---------------
99
+
100
+
101
+ def _resolve_n(n: int | None, verification: VerificationConfig) -> int:
102
+ """`n`'s "explicit argument, else declared config, else the module
103
+ default" resolution rule -- shared by `build_invariants_document` and
104
+ `verify_invariants` so the two entry points can never silently disagree
105
+ on what an unset `n` means for the same `VerificationConfig`."""
106
+ if n is not None:
107
+ return n
108
+ return verification.lattice.n if verification.lattice.n is not None else lattice.DEFAULT_SWEEP_POINTS
109
+
110
+
111
+ def _sweeps_for_property(
112
+ prop, entry_lattice: EntryLattice, parameter_names: Sequence[str]
113
+ ) -> list[tuple[ParameterSweep, int]]:
114
+ """`[(sweep, argument_position), ...]` to evaluate `prop` against, or
115
+ raise `_UncoveredError` with the reason it cannot be evaluated at all.
116
+
117
+ See the module docstring's "Ambiguities resolved here" for why
118
+ `bounds`/`sum_to_one` (no `in:`) run against every swept parameter while
119
+ `monotone`/`scales_linearly_in` (declared `in:`) run against exactly one.
120
+ """
121
+ if isinstance(prop, (MonotoneProperty, ScalesLinearlyInProperty)):
122
+ parameter = prop.parameter
123
+ sweep_obj = next((s for s in entry_lattice.sweeps if s.parameter == parameter), None)
124
+ if sweep_obj is None:
125
+ raise _UncoveredError(f"no range declared for parameter '{parameter}'")
126
+ return [(sweep_obj, parameter_names.index(parameter))]
127
+
128
+ if isinstance(prop, SymmetricInProperty):
129
+ # No evaluator exists yet (declared_invariants.evaluate_property
130
+ # raises NotImplementedInvariant for this word regardless of which
131
+ # sweep is chosen) -- pick a sweep only so evaluate_property has one
132
+ # to raise against; if there is none, report the range gap instead
133
+ # of the unimplemented-word gap, since that is the more actionable
134
+ # of the two.
135
+ if not entry_lattice.sweeps:
136
+ raise _UncoveredError(
137
+ "no swept parameter declared to evaluate `symmetric_in` against"
138
+ )
139
+ sweep_obj = entry_lattice.sweeps[0]
140
+ return [(sweep_obj, parameter_names.index(sweep_obj.parameter))]
141
+
142
+ # bounds / sum_to_one: no declared `in:` -- evaluated over every swept
143
+ # parameter of the entry (see module docstring).
144
+ if not entry_lattice.sweeps:
145
+ missing = entry_lattice.unswept_parameters
146
+ reason = (
147
+ f"no range declared for parameter(s) {', '.join(missing)}"
148
+ if missing
149
+ else "no swept parameter available to evaluate this property against"
150
+ )
151
+ raise _UncoveredError(reason)
152
+ return [(s, parameter_names.index(s.parameter)) for s in entry_lattice.sweeps]
153
+
154
+
155
+ def _callable_for(package, entry: dict):
156
+ """A plain positional callable `declared_invariants.evaluate_property`
157
+ can call directly -- reuses `golden.invoke` (materialisation, method/
158
+ static/function dispatch) rather than re-deriving calling conventions."""
159
+
160
+ def call(*args):
161
+ synthetic = {**entry, "arguments": list(args)}
162
+ result, _effects = golden.invoke(synthetic, package)
163
+ return result
164
+
165
+ return call
166
+
167
+
168
+ def _entries_by_function_name(document: dict) -> dict[str, tuple[str, dict]]:
169
+ """First golden.json entry per function name, paired with its own key.
170
+
171
+ Returning the `(key, entry)` pair (rather than just `entry`) lets callers
172
+ look up the matching lattice by the *same* golden key that picked this
173
+ entry, so a function with more than one recorded entry (e.g. one golden
174
+ entry per constructed instance) always evaluates a property against one
175
+ consistent (entry, lattice) pair instead of silently mixing one entry's
176
+ call arguments with a different entry's swept ranges.
177
+ """
178
+ result: dict[str, tuple[str, dict]] = {}
179
+ for key, entry in document.get("entries", {}).items():
180
+ result.setdefault(entry.get("name") or key, (key, entry))
181
+ return result
182
+
183
+
184
+ def _limited(sweep_obj: ParameterSweep, points_limit: int | None) -> ParameterSweep:
185
+ if points_limit is None:
186
+ return sweep_obj
187
+ return ParameterSweep(
188
+ parameter=sweep_obj.parameter,
189
+ lo=sweep_obj.lo,
190
+ hi=sweep_obj.hi,
191
+ points=sweep_obj.points[:points_limit],
192
+ )
193
+
194
+
195
+ # --- declared-invariant pass, aggregated over a whole entry -----------------
196
+
197
+
198
+ def run_declared_invariants(
199
+ document: dict,
200
+ functions_by_name: dict,
201
+ verification: VerificationConfig,
202
+ package,
203
+ lattices: dict[str, EntryLattice],
204
+ *,
205
+ points_limit: int | None = None,
206
+ ) -> tuple[dict[str, list[tuple[object, list[PropertyReport]]]], dict[str, list[str]]]:
207
+ """Every declared property in `verification.invariants`, evaluated.
208
+
209
+ Returns `(reports, uncovered)`:
210
+ * `reports[function_name]` is `[(prop, [PropertyReport, ...]), ...]` --
211
+ one `PropertyReport` per sweep the property ran against (see
212
+ `_sweeps_for_property`).
213
+ * `uncovered[function_name]` is every reason a declared property for
214
+ that function could not be evaluated at all (an undeclared range, or
215
+ `NotImplementedInvariant`'s vocabulary gap) -- never silently dropped.
216
+ """
217
+ entries_by_function = _entries_by_function_name(document)
218
+
219
+ reports: dict[str, list[tuple[object, list[PropertyReport]]]] = {}
220
+ uncovered: dict[str, list[str]] = {}
221
+
222
+ for fn_name, properties in verification.invariants.items():
223
+ key_and_entry = entries_by_function.get(fn_name)
224
+ fn = functions_by_name.get(fn_name)
225
+ entry = key_and_entry[1] if key_and_entry is not None else None
226
+ entry_lattice = lattices.get(key_and_entry[0]) if key_and_entry is not None else None
227
+ if entry is None or fn is None or entry_lattice is None:
228
+ uncovered.setdefault(fn_name, []).append(
229
+ "no golden.json entry (or no matching lattice) to evaluate "
230
+ "declared invariants against"
231
+ )
232
+ continue
233
+
234
+ parameter_names = [p.name for p in fn.parameters if p.intent != "out"]
235
+ call = _callable_for(package, entry)
236
+
237
+ for prop in properties:
238
+ try:
239
+ sweeps = _sweeps_for_property(prop, entry_lattice, parameter_names)
240
+ except _UncoveredError as exc:
241
+ uncovered.setdefault(fn_name, []).append(str(exc))
242
+ continue
243
+
244
+ try:
245
+ sub_reports = [
246
+ evaluate_property(
247
+ call,
248
+ _limited(sweep_obj, points_limit),
249
+ prop,
250
+ fn_name,
251
+ position=position,
252
+ base_arguments=entry_lattice.base_arguments,
253
+ )
254
+ for sweep_obj, position in sweeps
255
+ ]
256
+ except NotImplementedInvariant as exc:
257
+ uncovered.setdefault(fn_name, []).append(str(exc))
258
+ continue
259
+
260
+ reports.setdefault(fn_name, []).append((prop, sub_reports))
261
+
262
+ return reports, uncovered
263
+
264
+
265
+ # --- assembling the file (spec section 3.7) ---------------------------------
266
+
267
+
268
+ def build_invariants_document(
269
+ module: "ModuleIR",
270
+ document: dict,
271
+ verification: VerificationConfig,
272
+ structural_results: si.StructuralInvariantResults,
273
+ declared_reports: dict[str, list[tuple[object, list[PropertyReport]]]],
274
+ declared_uncovered: dict[str, list[str]],
275
+ lattices: dict[str, EntryLattice],
276
+ *,
277
+ n: int | None = None,
278
+ scatter_seed: int | None = None,
279
+ scatter_count: int | None = None,
280
+ corners: dict[str, list] | None = None,
281
+ environment: dict | None = None,
282
+ ) -> dict:
283
+ """The `invariants.json` contents, per spec section 3.7's schema.
284
+
285
+ `n`/`scatter_seed`/`scatter_count`/`corners` each default to `None`,
286
+ meaning "read it from `verification.lattice`" (see the module
287
+ docstring) -- an explicit argument always overrides the declared
288
+ config.
289
+ """
290
+ n = _resolve_n(n, verification)
291
+ if scatter_seed is None:
292
+ scatter_seed = verification.lattice.scatter.seed
293
+ if scatter_count is None:
294
+ scatter_count = verification.lattice.scatter.count
295
+ if corners is None:
296
+ corners = verification.lattice.corners
297
+ checked: dict[str, dict] = {}
298
+ uncovered: dict[str, str] = {}
299
+
300
+ # First entry wins per function name -- must match `_entries_by_function_name`'s
301
+ # convention exactly, or the `uncovered` check below (which needs the
302
+ # matching lattice) picks a *different* entry than the one
303
+ # `run_declared_invariants` actually evaluated properties against,
304
+ # producing an invariants.json where a function's checked properties and
305
+ # its uncovered reasons come from two different recorded calls.
306
+ entry_key_by_function: dict[str, str] = {}
307
+ for key, entry in document.get("entries", {}).items():
308
+ entry_key_by_function.setdefault(entry.get("name") or key, key)
309
+
310
+ # Structural properties, grouped by function name.
311
+ structural_by_function: dict[str, list] = {}
312
+ for outcome in structural_results.outcomes:
313
+ structural_by_function.setdefault(outcome.function, []).append(outcome)
314
+
315
+ # Every function with a built lattice is a candidate for `uncovered`
316
+ # (an unswept numeric parameter is a property of the *lattice*, spec
317
+ # section 3.4, independent of whether any property is actually declared
318
+ # for that function) -- so this must not be limited to functions that
319
+ # already have a structural/declared outcome, or a function with zero
320
+ # declared invariants and an undeclared range (e.g. petro_api's
321
+ # `tubing_bhp`) would never be reported.
322
+ lattice_functions = {
323
+ (entry.get("name") or key)
324
+ for key, entry in document.get("entries", {}).items()
325
+ if key in lattices
326
+ }
327
+ all_functions = (
328
+ set(structural_by_function)
329
+ | set(declared_reports)
330
+ | set(declared_uncovered)
331
+ | lattice_functions
332
+ )
333
+
334
+ for fn_name in all_functions:
335
+ properties: list[str] = []
336
+ points = 0
337
+ passed = True
338
+
339
+ for outcome in structural_by_function.get(fn_name, []):
340
+ properties.append(outcome.property)
341
+ points = max(points, outcome.points_checked)
342
+ if outcome.status != "pass":
343
+ passed = False
344
+
345
+ for prop, sub_reports in declared_reports.get(fn_name, []):
346
+ properties.append(_label(prop))
347
+ points = max(points, sum(r.points_checked for r in sub_reports))
348
+ if any(not r.passed for r in sub_reports):
349
+ passed = False
350
+
351
+ if properties:
352
+ checked[fn_name] = {
353
+ "properties": properties,
354
+ "points": points,
355
+ "status": "pass" if passed else "fail",
356
+ }
357
+
358
+ # An unswept numeric parameter on this entry's lattice (spec section
359
+ # 3.4: "a swept parameter with no declared range is an error, not a
360
+ # guess, recorded under uncovered") -- true independent of whether
361
+ # any invariant is declared for this function at all.
362
+ reasons: list[str] = []
363
+ entry_key = entry_key_by_function.get(fn_name)
364
+ entry_lattice = lattices.get(entry_key) if entry_key is not None else None
365
+ if entry_lattice is not None and entry_lattice.unswept_parameters:
366
+ reasons.append(
367
+ "no range declared for parameter(s) "
368
+ + ", ".join(entry_lattice.unswept_parameters)
369
+ )
370
+ reasons.extend(declared_uncovered.get(fn_name, []))
371
+ if reasons:
372
+ uncovered[fn_name] = "; ".join(reasons)
373
+
374
+ return {
375
+ "format": FORMAT_VERSION,
376
+ "service": module.name,
377
+ "provenance": environment if environment is not None else golden.provenance(),
378
+ "lattice": {
379
+ "points_per_sweep": n,
380
+ "ranges": {name: [rng.lo, rng.hi] for name, rng in verification.ranges.items()},
381
+ "corners": {
382
+ fn_name: [list(corner) for corner in fn_corners]
383
+ for fn_name, fn_corners in (corners or {}).items()
384
+ },
385
+ "scatter": {"count": scatter_count, "seed": scatter_seed},
386
+ },
387
+ "state": {
388
+ "setup": list(verification.state.setup),
389
+ "mutating": list(verification.state.mutating),
390
+ "error_flag": verification.state.error_flag,
391
+ },
392
+ # Insertion order follows the entries actually checked -- sorted by
393
+ # name for a stable, reviewable diff (unlike golden.json, call order
394
+ # carries no meaning here: nothing in this file replays a sequence).
395
+ "checked": dict(sorted(checked.items())),
396
+ "uncovered": dict(sorted(uncovered.items())),
397
+ }
398
+
399
+
400
+ # --- the public entry point --------------------------------------------------
401
+
402
+
403
+ def verify_invariants(
404
+ module: "ModuleIR",
405
+ document: dict,
406
+ verification: VerificationConfig,
407
+ package,
408
+ target: si.SubprocessTarget,
409
+ *,
410
+ n: int | None = None,
411
+ points_limit: int | None = None,
412
+ scatter_seed: int | None = None,
413
+ scatter_count: int | None = None,
414
+ corners: dict[str, list] | None = None,
415
+ environment: dict | None = None,
416
+ python_executable: str | None = None,
417
+ ) -> InvariantsResult:
418
+ """Run structural (T10) + declared (T11) properties and assemble the
419
+ `invariants.json` document (spec section 3.7).
420
+
421
+ `n`/`scatter_seed`/`scatter_count`/`corners` default to `None`, meaning
422
+ "read it from `verification.lattice`" -- see the module docstring and
423
+ `config.LatticeConfig`. An explicit argument always overrides the
424
+ declared config. `n` in particular also controls how many points
425
+ `si.build_lattices` (and therefore every structural/declared check)
426
+ actually evaluates, not just what is reported in the document.
427
+
428
+ Raises `EmptyCheckedError` if the resulting `checked` block is empty --
429
+ per spec, that must never be reported as a pass. Otherwise returns an
430
+ `InvariantsResult` whose `.passed` reflects every checked entry's status
431
+ and whose `.document` is ready to be written with `write()`.
432
+ """
433
+ n = _resolve_n(n, verification)
434
+
435
+ # `si.build_lattices` reads corners/scatter straight off
436
+ # `verification.lattice` -- it has no override parameters of its own. If
437
+ # this call's explicit `scatter_seed`/`scatter_count`/`corners` differ
438
+ # from what's declared, build the lattices against an overridden copy of
439
+ # `verification` so the points actually checked below match what
440
+ # `build_invariants_document` (called with these same explicit
441
+ # arguments, further down) reports as having been checked -- otherwise
442
+ # the report would describe a sweep this call never actually ran.
443
+ lattice_verification = verification
444
+ if scatter_seed is not None or scatter_count is not None or corners is not None:
445
+ effective_scatter = replace(
446
+ verification.lattice.scatter,
447
+ seed=verification.lattice.scatter.seed if scatter_seed is None else scatter_seed,
448
+ count=verification.lattice.scatter.count if scatter_count is None else scatter_count,
449
+ )
450
+ effective_lattice = replace(
451
+ verification.lattice,
452
+ scatter=effective_scatter,
453
+ corners=verification.lattice.corners if corners is None else corners,
454
+ )
455
+ lattice_verification = replace(verification, lattice=effective_lattice)
456
+
457
+ functions_by_name = {fn.name: fn for fn in module.functions}
458
+ lattices = si.build_lattices(document, functions_by_name, lattice_verification, n=n)
459
+
460
+ structural_results = si.run_structural_invariants(
461
+ module,
462
+ document,
463
+ verification,
464
+ package,
465
+ target,
466
+ lattices=lattices,
467
+ n=n,
468
+ points_limit=points_limit,
469
+ python_executable=python_executable,
470
+ )
471
+ declared_reports, declared_uncovered = run_declared_invariants(
472
+ document, functions_by_name, verification, package, lattices, points_limit=points_limit
473
+ )
474
+
475
+ invariants_document = build_invariants_document(
476
+ module,
477
+ document,
478
+ verification,
479
+ structural_results,
480
+ declared_reports,
481
+ declared_uncovered,
482
+ lattices,
483
+ n=n,
484
+ scatter_seed=scatter_seed,
485
+ scatter_count=scatter_count,
486
+ corners=corners,
487
+ environment=environment,
488
+ )
489
+
490
+ if not invariants_document["checked"]:
491
+ raise EmptyCheckedError(
492
+ "invariants run checked nothing -- an invariants file that "
493
+ "silently checks nothing must not look like a pass "
494
+ "(design-verification-layers.md section 3.7). Declare `ranges:` "
495
+ "for at least one numeric parameter, or `state`/`invariants` in "
496
+ "nativegate.yaml, before running `ngate invariants verify`."
497
+ )
498
+
499
+ passed = all(v["status"] == "pass" for v in invariants_document["checked"].values())
500
+
501
+ failure_messages: list[str] = []
502
+ for outcome in structural_results.failures():
503
+ for point_failure in outcome.failures:
504
+ failure_messages.append(
505
+ f"{outcome.property} `{outcome.function}`: {point_failure.detail}"
506
+ )
507
+ for reports in declared_reports.values():
508
+ for _prop, sub_reports in reports:
509
+ for r in sub_reports:
510
+ if not r.passed:
511
+ failure_messages.append(r.message)
512
+
513
+ return InvariantsResult(
514
+ document=invariants_document, passed=passed, failure_messages=failure_messages
515
+ )
516
+
517
+
518
+ # --- file I/O (matching golden.write()'s conventions exactly) --------------
519
+
520
+
521
+ def write(path: Path, document: dict) -> None:
522
+ path.parent.mkdir(parents=True, exist_ok=True)
523
+ path.write_text(json.dumps(document, indent=2, sort_keys=False) + "\n")
524
+
525
+
526
+ def read(path: Path) -> dict:
527
+ return json.loads(path.read_text())
528
+
529
+
530
+ def coverage(document: dict) -> tuple[int, int]:
531
+ """(checked, uncovered) counts -- mirrors `golden.coverage()`."""
532
+ return len(document.get("checked") or {}), len(document.get("uncovered") or {})