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,565 @@
1
+ """T11 — evaluate declared invariant properties over T9's lattice.
2
+
3
+ Spec: design-verification-layers.md section 3.2 (declared vocabulary,
4
+ `monotone` compares raw float64 with `>=`/`<=`, no tolerance) and section 3.6
5
+ (counterexample reporting: first failure, then a pinned deterministic
6
+ bisection, in a byte-identical message format). Also section 6: the word
7
+ "proved" must never appear in output; the vocabulary is "checked at N
8
+ points".
9
+
10
+ This module evaluates the three properties this pass implements —
11
+ `bounds`, `monotone`, `sum_to_one` — against a lattice built by
12
+ :mod:`nativegate.lattice`. `symmetric_in` and `scales_linearly_in` are declared
13
+ vocabulary words (config.py's INVARIANT_VOCABULARY) that have no evaluator
14
+ here yet; per the task, an unimplemented vocabulary word must be a loud,
15
+ hard error, never a silently-passing no-op -- see `NotImplementedInvariant`
16
+ and `evaluate_property`'s dispatch, which raises rather than skips.
17
+
18
+ T10 (setup replay as its own module) had not landed when this was written.
19
+ Rather than duplicate a full runner, `replay_setup` below implements only
20
+ what this module needs: replay `state.setup`'s entries, in `nativegate.yaml`
21
+ order, using the arguments `golden.json` recorded for each -- directly
22
+ against `config.StateConfig` and `golden.invoke`, with no dependency on a
23
+ T10 module that does not exist yet.
24
+
25
+ Ambiguities in spec section 3.6, resolved here and flagged in the task
26
+ report (search this file for "AMBIGUITY" to find each decision):
27
+
28
+ * The example message's numeric formatting is inconsistent between fields
29
+ (`min: 1.0` keeps a trailing `.0`; the lattice line's `10000` does not),
30
+ and is not itself specified as a formula anywhere in the document. This
31
+ module picks ONE deterministic rule (`_fmt`, below) and applies it to
32
+ every computed/swept value, and a second rule for property-label
33
+ thresholds (which echoes the declared YAML value's float form). Both are
34
+ fixed and produce byte-identical output across runs for the same failure,
35
+ which is the property section 3.6 actually requires ("the message is
36
+ byte-identical across runs and can itself be asserted in tests") --
37
+ matching the example's exact whitespace was not achievable from the text
38
+ alone since the example is not accompanied by a format string.
39
+ * The example's `bracket` line ends in a physical unit ("... 8437.50 psia").
40
+ No unit metadata exists anywhere in `config.py`'s dataclasses (checked:
41
+ `RangeDeclaration` carries only `lo`/`hi`). This implementation omits the
42
+ unit suffix rather than inventing a units feature the spec does not
43
+ describe elsewhere.
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ from dataclasses import dataclass
49
+ from typing import Callable, Sequence
50
+
51
+ from .config import (
52
+ BoundsProperty,
53
+ InvariantProperty,
54
+ MonotoneProperty,
55
+ ScalesLinearlyInProperty,
56
+ StateConfig,
57
+ SumToOneProperty,
58
+ SymmetricInProperty,
59
+ )
60
+ from .lattice import LatticePoint, ParameterSweep
61
+
62
+ MAX_BISECTION_STEPS = 40
63
+
64
+
65
+ class NotImplementedInvariant(NotImplementedError):
66
+ """A declared vocabulary word with no evaluator this pass.
67
+
68
+ Raised, never swallowed: "an unimplemented vocabulary word must be a
69
+ hard error, never a silent pass" (task T11, item 1).
70
+ """
71
+
72
+
73
+ class InvariantSetupError(RuntimeError):
74
+ """`state.setup` names a function with no golden.json entry to replay."""
75
+
76
+
77
+ # --- value formatting (see module docstring: "Ambiguities") ----------------
78
+
79
+
80
+ def _fmt(x: float) -> str:
81
+ """Deterministic formatting for a *computed/swept* value.
82
+
83
+ Integral floats print without a trailing `.0` (`10000.0` -> `"10000"`),
84
+ matching the lattice line's range endpoints in the spec's own example;
85
+ everything else uses `repr()`, which is Python's shortest round-tripping
86
+ representation and therefore stable and exact.
87
+ """
88
+ x = float(x)
89
+ if x == int(x) and abs(x) < 1e16:
90
+ return str(int(x))
91
+ return repr(x)
92
+
93
+
94
+ def _fmt_threshold(x: float) -> str:
95
+ """Formatting for a property's declared threshold (e.g. `bounds.min`).
96
+
97
+ Echoes the float form the spec's example uses for `min: 1.0` -- always
98
+ showing the value as a float literal, trailing `.0` included -- since
99
+ this is restating a YAML-declared number, not a swept/computed one.
100
+ """
101
+ return repr(float(x))
102
+
103
+
104
+ def _label(prop: InvariantProperty) -> str:
105
+ """The `<word>{...}` fragment inside the message's first line."""
106
+ if isinstance(prop, BoundsProperty):
107
+ parts = []
108
+ if prop.min is not None:
109
+ parts.append(f"min: {_fmt_threshold(prop.min)}")
110
+ if prop.max is not None:
111
+ parts.append(f"max: {_fmt_threshold(prop.max)}")
112
+ return f"bounds{{{', '.join(parts)}}}"
113
+ if isinstance(prop, MonotoneProperty):
114
+ return f"monotone{{in: {prop.parameter}, direction: {prop.direction}}}"
115
+ if isinstance(prop, SumToOneProperty):
116
+ return f"sum_to_one{{{', '.join(prop.fields)}}}"
117
+ raise AssertionError(f"unreachable: no label for {prop!r}")
118
+
119
+
120
+ def _row(label: str, body: str) -> str:
121
+ """One ` <label padded to 14> : <body>` line (spec section 3.6's block)."""
122
+ return f" {label:<14}: {body}"
123
+
124
+
125
+ # --- bisection (spec section 3.6, pinned) -----------------------------------
126
+
127
+
128
+ def bisect_bracket(
129
+ a: float,
130
+ b: float,
131
+ holds: Callable[[float], bool],
132
+ max_steps: int = MAX_BISECTION_STEPS,
133
+ ) -> tuple[float, float, int]:
134
+ """Deterministically bisect `[a, b]` toward the pass/fail boundary.
135
+
136
+ Precondition: `holds(a)` is True (a passes) and `holds(b)` is False (b
137
+ fails) -- the bracket the spec calls "the last passing lattice point and
138
+ the first failing one". Per spec: "the midpoint is `(a + b) / 2` in
139
+ float64; the side keeping the bracket valid is retained; iteration stops
140
+ when `(a + b) / 2` equals either endpoint (float64 exhaustion) or after
141
+ 40 steps, whichever is first."
142
+
143
+ Returns `(a, b, steps_taken)` -- the tightest bracket found, still
144
+ satisfying `holds(a)` and not `holds(b)`.
145
+ """
146
+ steps = 0
147
+ while steps < max_steps:
148
+ mid = (a + b) / 2
149
+ if mid == a or mid == b:
150
+ break
151
+ steps += 1
152
+ if holds(mid):
153
+ a = mid
154
+ else:
155
+ b = mid
156
+ return a, b, steps
157
+
158
+
159
+ # --- reporting ---------------------------------------------------------
160
+
161
+
162
+ @dataclass
163
+ class PropertyReport:
164
+ function: str
165
+ property: InvariantProperty
166
+ passed: bool
167
+ points_checked: int
168
+ message: str
169
+
170
+
171
+ def _pass_message(function: str, prop: InvariantProperty, points_checked: int) -> str:
172
+ # Section 6: "The file should say 'checked at 33 points', and the CLI
173
+ # should never print the word 'proved'."
174
+ return f"invariant `{function}: {_label(prop)}` checked at {points_checked} points"
175
+
176
+
177
+ def _failure_message(
178
+ *,
179
+ function: str,
180
+ prop: InvariantProperty,
181
+ parameter: str,
182
+ first_failure_x: float,
183
+ first_failure_value,
184
+ last_passing_x: float,
185
+ last_passing_value,
186
+ bracket_a: float,
187
+ bracket_b: float,
188
+ lattice_lo: float,
189
+ lattice_hi: float,
190
+ lattice_n: int,
191
+ lattice_index: int,
192
+ ) -> str:
193
+ """Byte-identical block per spec section 3.6::
194
+
195
+ invariant `oil_fvf: bounds{min: 1.0}` failed
196
+ first failure : pressure = 8437.50 -> 0.9994
197
+ last passing : pressure = 8125.00 -> 1.0002
198
+ bracket : the property breaks between 8125.00 and 8437.50 psia
199
+ lattice : pressure in [14.7, 10000], 33 points, index 27
200
+
201
+ (rendered here with the same four labelled rows; see the module
202
+ docstring for the two formatting/units ambiguities this resolves.)
203
+ """
204
+ lines = [
205
+ f"invariant `{function}: {_label(prop)}` failed",
206
+ _row(
207
+ "first failure",
208
+ f"{parameter} = {_fmt(first_failure_x)} -> {_fmt(first_failure_value)}",
209
+ ),
210
+ _row(
211
+ "last passing",
212
+ f"{parameter} = {_fmt(last_passing_x)} -> {_fmt(last_passing_value)}",
213
+ ),
214
+ _row(
215
+ "bracket",
216
+ f"the property breaks between {_fmt(bracket_a)} and {_fmt(bracket_b)}",
217
+ ),
218
+ _row(
219
+ "lattice",
220
+ f"{parameter} ∈ [{_fmt(lattice_lo)}, {_fmt(lattice_hi)}], "
221
+ f"{lattice_n} points, index {lattice_index}",
222
+ ),
223
+ ]
224
+ return "\n".join(lines)
225
+
226
+
227
+ # --- property evaluators -------------------------------------------------
228
+
229
+
230
+ def _replaced(arguments: tuple, position: int, value) -> tuple:
231
+ return arguments[:position] + (value,) + arguments[position + 1 :]
232
+
233
+
234
+ def _call_at(fn: Callable, sweep: ParameterSweep, position: int, base_arguments: tuple, x: float):
235
+ return fn(*_replaced(base_arguments, position, x))
236
+
237
+
238
+ def _bounds_holds(prop: BoundsProperty, value: float) -> bool:
239
+ if prop.min is not None and not (value >= prop.min):
240
+ return False
241
+ if prop.max is not None and not (value <= prop.max):
242
+ return False
243
+ return True
244
+
245
+
246
+ def evaluate_bounds(
247
+ fn: Callable,
248
+ sweep: ParameterSweep,
249
+ prop: BoundsProperty,
250
+ function_name: str,
251
+ *,
252
+ position: int,
253
+ base_arguments: tuple,
254
+ ) -> PropertyReport:
255
+ """Check `bounds` at every point of one parameter's sweep, in order."""
256
+ values = [fn(*point.arguments) for point in sweep.points]
257
+ passes = [_bounds_holds(prop, v) for v in values]
258
+
259
+ if all(passes):
260
+ return PropertyReport(
261
+ function=function_name,
262
+ property=prop,
263
+ passed=True,
264
+ points_checked=len(sweep.points),
265
+ message=_pass_message(function_name, prop, len(sweep.points)),
266
+ )
267
+
268
+ first_fail_index = passes.index(False)
269
+ if first_fail_index == 0:
270
+ # No passing point precedes the failure: there is nothing to bracket.
271
+ # Report the bare failure (still never the word "proved").
272
+ message = (
273
+ f"invariant `{function_name}: {_label(prop)}` failed\n"
274
+ + _row(
275
+ "first failure",
276
+ f"{sweep.parameter} = {_fmt(sweep.points[0].arguments[position])}"
277
+ f" -> {_fmt(values[0])}",
278
+ )
279
+ + "\n"
280
+ + _row(
281
+ "lattice",
282
+ f"{sweep.parameter} ∈ [{_fmt(sweep.lo)}, {_fmt(sweep.hi)}], "
283
+ f"{len(sweep.points)} points, index 0 (no passing point precedes it)",
284
+ )
285
+ )
286
+ return PropertyReport(
287
+ function=function_name,
288
+ property=prop,
289
+ passed=False,
290
+ points_checked=len(sweep.points),
291
+ message=message,
292
+ )
293
+
294
+ last_pass_index = first_fail_index - 1
295
+ a = sweep.points[last_pass_index].arguments[position]
296
+ b = sweep.points[first_fail_index].arguments[position]
297
+
298
+ def holds(x: float) -> bool:
299
+ return _bounds_holds(prop, _call_at(fn, sweep, position, base_arguments, x))
300
+
301
+ bracket_a, bracket_b, _steps = bisect_bracket(a, b, holds)
302
+
303
+ message = _failure_message(
304
+ function=function_name,
305
+ prop=prop,
306
+ parameter=sweep.parameter,
307
+ first_failure_x=b,
308
+ first_failure_value=values[first_fail_index],
309
+ last_passing_x=a,
310
+ last_passing_value=values[last_pass_index],
311
+ bracket_a=bracket_a,
312
+ bracket_b=bracket_b,
313
+ lattice_lo=sweep.lo,
314
+ lattice_hi=sweep.hi,
315
+ lattice_n=len(sweep.points),
316
+ lattice_index=first_fail_index,
317
+ )
318
+ return PropertyReport(
319
+ function=function_name,
320
+ property=prop,
321
+ passed=False,
322
+ points_checked=len(sweep.points),
323
+ message=message,
324
+ )
325
+
326
+
327
+ def _monotone_holds(prop: MonotoneProperty, previous: float, current: float) -> bool:
328
+ # Spec section 3.2: "raw float64 values with >=/<=, no tolerance."
329
+ if prop.direction == "nondecreasing":
330
+ return current >= previous
331
+ return current <= previous
332
+
333
+
334
+ def evaluate_monotone(
335
+ fn: Callable,
336
+ sweep: ParameterSweep,
337
+ prop: MonotoneProperty,
338
+ function_name: str,
339
+ *,
340
+ position: int,
341
+ base_arguments: tuple,
342
+ ) -> PropertyReport:
343
+ """Check `monotone` between every consecutive pair of a sweep, in order."""
344
+ values = [fn(*point.arguments) for point in sweep.points]
345
+
346
+ first_fail_index = None
347
+ for i in range(1, len(values)):
348
+ if not _monotone_holds(prop, values[i - 1], values[i]):
349
+ first_fail_index = i
350
+ break
351
+
352
+ if first_fail_index is None:
353
+ return PropertyReport(
354
+ function=function_name,
355
+ property=prop,
356
+ passed=True,
357
+ points_checked=len(sweep.points),
358
+ message=_pass_message(function_name, prop, len(sweep.points)),
359
+ )
360
+
361
+ last_pass_index = first_fail_index - 1
362
+ a = sweep.points[last_pass_index].arguments[position]
363
+ b = sweep.points[first_fail_index].arguments[position]
364
+ anchor_value = values[last_pass_index]
365
+
366
+ def holds(x: float) -> bool:
367
+ # "Holds" here means: monotonicity from the anchor (last known-good
368
+ # point) to `x` still holds -- i.e. the candidate `x` has not (yet)
369
+ # reproduced the violation. This keeps the same bisect_bracket
370
+ # contract (holds(a) True, holds(b) False) used for `bounds`.
371
+ candidate_value = _call_at(fn, sweep, position, base_arguments, x)
372
+ return _monotone_holds(prop, anchor_value, candidate_value)
373
+
374
+ bracket_a, bracket_b, _steps = bisect_bracket(a, b, holds)
375
+
376
+ message = _failure_message(
377
+ function=function_name,
378
+ prop=prop,
379
+ parameter=sweep.parameter,
380
+ first_failure_x=b,
381
+ first_failure_value=values[first_fail_index],
382
+ last_passing_x=a,
383
+ last_passing_value=values[last_pass_index],
384
+ bracket_a=bracket_a,
385
+ bracket_b=bracket_b,
386
+ lattice_lo=sweep.lo,
387
+ lattice_hi=sweep.hi,
388
+ lattice_n=len(sweep.points),
389
+ lattice_index=first_fail_index,
390
+ )
391
+ return PropertyReport(
392
+ function=function_name,
393
+ property=prop,
394
+ passed=False,
395
+ points_checked=len(sweep.points),
396
+ message=message,
397
+ )
398
+
399
+
400
+ def _sum_to_one_holds(prop: SumToOneProperty, result: dict) -> bool:
401
+ total = sum(result[field] for field in prop.fields)
402
+ return abs(total - 1.0) <= prop.tolerance
403
+
404
+
405
+ def evaluate_sum_to_one(
406
+ fn: Callable,
407
+ sweep: ParameterSweep,
408
+ prop: SumToOneProperty,
409
+ function_name: str,
410
+ *,
411
+ position: int,
412
+ base_arguments: tuple,
413
+ ) -> PropertyReport:
414
+ """Check `sum_to_one` at every point of one parameter's sweep, in order.
415
+
416
+ `fn` is expected to return a mapping with (at least) `prop.fields` as
417
+ keys -- the multi-field result `sum_to_one` is declared over (spec
418
+ section 3.2's `saturations: sum_to_one: [sw, so, sg]` example).
419
+ """
420
+ results = [fn(*point.arguments) for point in sweep.points]
421
+ sums = [sum(r[field] for r in [result] for field in prop.fields) for result in results]
422
+ passes = [_sum_to_one_holds(prop, r) for r in results]
423
+
424
+ if all(passes):
425
+ return PropertyReport(
426
+ function=function_name,
427
+ property=prop,
428
+ passed=True,
429
+ points_checked=len(sweep.points),
430
+ message=_pass_message(function_name, prop, len(sweep.points)),
431
+ )
432
+
433
+ first_fail_index = passes.index(False)
434
+ if first_fail_index == 0:
435
+ message = (
436
+ f"invariant `{function_name}: {_label(prop)}` failed\n"
437
+ + _row(
438
+ "first failure",
439
+ f"{sweep.parameter} = {_fmt(sweep.points[0].arguments[position])}"
440
+ f" -> {_fmt(sums[0])}",
441
+ )
442
+ + "\n"
443
+ + _row(
444
+ "lattice",
445
+ f"{sweep.parameter} ∈ [{_fmt(sweep.lo)}, {_fmt(sweep.hi)}], "
446
+ f"{len(sweep.points)} points, index 0 (no passing point precedes it)",
447
+ )
448
+ )
449
+ return PropertyReport(
450
+ function=function_name,
451
+ property=prop,
452
+ passed=False,
453
+ points_checked=len(sweep.points),
454
+ message=message,
455
+ )
456
+
457
+ last_pass_index = first_fail_index - 1
458
+ a = sweep.points[last_pass_index].arguments[position]
459
+ b = sweep.points[first_fail_index].arguments[position]
460
+
461
+ def holds(x: float) -> bool:
462
+ result = _call_at(fn, sweep, position, base_arguments, x)
463
+ return _sum_to_one_holds(prop, result)
464
+
465
+ bracket_a, bracket_b, _steps = bisect_bracket(a, b, holds)
466
+
467
+ message = _failure_message(
468
+ function=function_name,
469
+ prop=prop,
470
+ parameter=sweep.parameter,
471
+ first_failure_x=b,
472
+ first_failure_value=sums[first_fail_index],
473
+ last_passing_x=a,
474
+ last_passing_value=sums[last_pass_index],
475
+ bracket_a=bracket_a,
476
+ bracket_b=bracket_b,
477
+ lattice_lo=sweep.lo,
478
+ lattice_hi=sweep.hi,
479
+ lattice_n=len(sweep.points),
480
+ lattice_index=first_fail_index,
481
+ )
482
+ return PropertyReport(
483
+ function=function_name,
484
+ property=prop,
485
+ passed=False,
486
+ points_checked=len(sweep.points),
487
+ message=message,
488
+ )
489
+
490
+
491
+ def evaluate_property(
492
+ fn: Callable,
493
+ sweep: ParameterSweep,
494
+ prop: InvariantProperty,
495
+ function_name: str,
496
+ *,
497
+ position: int,
498
+ base_arguments: tuple,
499
+ ) -> PropertyReport:
500
+ """Dispatch to the right evaluator, or raise for an unimplemented word.
501
+
502
+ `symmetric_in` and `scales_linearly_in` are in config.py's closed
503
+ vocabulary but have no evaluator this pass: per T11 item 1, that must be
504
+ a loud error, never a silently-passing no-op.
505
+ """
506
+ if isinstance(prop, BoundsProperty):
507
+ return evaluate_bounds(
508
+ fn, sweep, prop, function_name, position=position, base_arguments=base_arguments
509
+ )
510
+ if isinstance(prop, MonotoneProperty):
511
+ return evaluate_monotone(
512
+ fn, sweep, prop, function_name, position=position, base_arguments=base_arguments
513
+ )
514
+ if isinstance(prop, SumToOneProperty):
515
+ return evaluate_sum_to_one(
516
+ fn, sweep, prop, function_name, position=position, base_arguments=base_arguments
517
+ )
518
+ if isinstance(prop, (SymmetricInProperty, ScalesLinearlyInProperty)):
519
+ word = "symmetric_in" if isinstance(prop, SymmetricInProperty) else "scales_linearly_in"
520
+ raise NotImplementedInvariant(
521
+ f"invariants.{function_name}: `{word}` is a declared vocabulary word "
522
+ "with no evaluator implemented yet (T11 stub). This is a hard "
523
+ "error, not a skip -- an unimplemented property must never look "
524
+ "like a pass."
525
+ )
526
+ raise AssertionError(f"unreachable: no evaluator for {prop!r}")
527
+
528
+
529
+ # --- minimal setup replay (T10 had not landed; see module docstring) -------
530
+
531
+
532
+ def replay_setup(state: StateConfig, golden_document: dict, package) -> None:
533
+ """Replay `state.setup`'s entries, in `nativegate.yaml` order.
534
+
535
+ Spec section 3.5: "Every property evaluation runs after the setup
536
+ sequence, replayed with the arguments recorded in golden.json." This
537
+ finds each setup function's entry by its recorded `name` (golden.json's
538
+ entries are keyed by an arbitrary key, but each entry carries the
539
+ function name it calls) and replays it via `golden.invoke`, which is the
540
+ same call path golden itself uses.
541
+ """
542
+ from . import golden # local: avoid a hard import cycle at module load
543
+
544
+ entries = golden_document.get("entries") or {}
545
+ entries_by_function: dict = {}
546
+ for entry in entries.values():
547
+ entries_by_function.setdefault(entry.get("name"), []).append(entry)
548
+
549
+ # A function name can appear more than once in `state.setup` (e.g. an
550
+ # init-then-update sequence); each occurrence consumes the *next*
551
+ # golden.json entry recorded for that function, in file order, rather
552
+ # than always replaying the first one.
553
+ next_index: dict = {}
554
+ for fn_name in state.setup:
555
+ candidates = entries_by_function.get(fn_name) or []
556
+ idx = next_index.get(fn_name, 0)
557
+ if idx >= len(candidates):
558
+ raise InvariantSetupError(
559
+ f"state.setup names {fn_name!r}, which has no golden.json "
560
+ "entry to replay -- setup must be replayed with recorded "
561
+ "arguments, and none were recorded for this function "
562
+ f"(occurrence {idx + 1} of {fn_name!r} in state.setup)."
563
+ )
564
+ golden.invoke(candidates[idx], package)
565
+ next_index[fn_name] = idx + 1