rigortype 0.2.8 → 0.2.9

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/data/core_overlay/csv.rbs +28 -0
  4. data/data/core_overlay/psych.rbs +22 -0
  5. data/docs/handbook/01-getting-started.md +9 -1
  6. data/docs/handbook/02-everyday-types.md +4 -1
  7. data/docs/handbook/08-understanding-errors.md +3 -3
  8. data/docs/manual/01-installation.md +1 -0
  9. data/docs/manual/02-cli-reference.md +16 -7
  10. data/docs/manual/07-plugins.md +1 -1
  11. data/docs/manual/14-rails-quickstart.md +4 -2
  12. data/docs/manual/15-type-protection-coverage.md +21 -0
  13. data/docs/manual/plugins/rigor-actionpack.md +1 -1
  14. data/docs/manual/plugins/rigor-activerecord.md +12 -5
  15. data/docs/manual/plugins/rigor-rails-routes.md +11 -0
  16. data/lib/rigor/cache/store.rb +174 -57
  17. data/lib/rigor/cli/coverage_command.rb +50 -29
  18. data/lib/rigor/cli/diagnostic_formats.rb +4 -1
  19. data/lib/rigor/cli/protection_fork_scan.rb +55 -0
  20. data/lib/rigor/cli/protection_report.rb +7 -1
  21. data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
  22. data/lib/rigor/environment.rb +54 -18
  23. data/lib/rigor/inference/expression_typer.rb +20 -2
  24. data/lib/rigor/inference/fork_map.rb +87 -0
  25. data/lib/rigor/inference/narrowing.rb +118 -0
  26. data/lib/rigor/inference/parameter_inference_collector.rb +55 -10
  27. data/lib/rigor/inference/scope_indexer.rb +9 -13
  28. data/lib/rigor/inference/statement_evaluator.rb +48 -3
  29. data/lib/rigor/version.rb +1 -1
  30. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
  31. data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
  32. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +10 -1
  33. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +61 -0
  34. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
  35. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
  36. data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
  37. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
  38. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +189 -0
  39. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +19 -1
  40. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +41 -10
  41. data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
  42. data/sig/rigor/environment.rbs +1 -0
  43. metadata +8 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d38773edf003d6de8ea6ec3de2f19aac4c2d74e404c56f39d1ed9595f388a648
4
- data.tar.gz: beb2b7aea30bf74f4cc8d22e66d441d63ecb906689d6a22c6ca647a7a087a719
3
+ metadata.gz: 565d25fe6c480013419e1e7f6d2bcb60512f7cf56c6a34b612e5951dad8b6c62
4
+ data.tar.gz: c48e0fcde278beb45ac4b8cb0c5acb272540d8bbc5c37111e983ffacee4c772d
5
5
  SHA512:
6
- metadata.gz: 1c33b18e310c956f9cc9bbd4806228a095bcf95a137efba50affd49ffca17decac9107006e4a7fd350a9783f289e7d9e336ebc99755e7d69ce321d60b21c731b
7
- data.tar.gz: c9f7286bc00f3419083ced2e35678062e8508dfe03b6064fe6c9f4cf4ad752479ca8801cce059cb3fc6bc0ee0eaaa216824a9d43cc93332feb7e267ab988253c
6
+ metadata.gz: 8859749b0ee71c8dca814c164de8e47bd3004f52773299181f8aeca2bb30b5c96dc47bf46278a3378704f548a30ab68c3cb273b71a2c7346b6bba67bfbfb9365
7
+ data.tar.gz: 450f2f074e21d3a3fd7cff2e9f7f8971dd838bdfe1fadc0e80300e7c3dc9a98eb13b9890033436b77f5e82196238c91503f598fce78435f9ce9d598c64fd2e87
data/README.md CHANGED
@@ -231,7 +231,7 @@ rigor docs --list # list every bundled page
231
231
 
232
232
  ## Status
233
233
 
234
- Current release: **`v0.2.8`** (2026-07-06) — the latest cut on the
234
+ Current release: **`v0.2.9`** (2026-07-11) — the latest cut on the
235
235
  `0.2.x` evaluation line opened by `v0.2.0`, the first
236
236
  publicly-announced (general / evaluation) release. The line publishes
237
237
  an enumerated [compatibility surface](docs/compatibility.md) as a
@@ -0,0 +1,28 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- CSV::MalformedCSVError.new(message, line_number) ---
4
+ #
5
+ # The pinned `rbs` gem's `csv.rbs` never declares `MalformedCSVError`'s own
6
+ # constructor, so it falls back to the inherited `StandardError#initialize`
7
+ # (arity 0..1). But the `csv` gem's real, documented constructor takes two
8
+ # arguments — the message and the 1-based line number:
9
+ #
10
+ # raise CSV::MalformedCSVError.new("Unquoted fields...", line)
11
+ #
12
+ # Without the two-arg form a raise like GitLab's
13
+ # `import_csv/base_service.rb` false-fires `call.wrong-arity`
14
+ # ("expected 0..1, got 2"). The line number is optional here so the
15
+ # inherited single-arg form still type-checks.
16
+
17
+ # `CSV` is a CLASS in Ruby / RBS (a `module CSV` wrapper collides with upstream
18
+ # once the `csv` stdlib is in scope). It must be declared EXPLICITLY here — the
19
+ # overlay loads unconditionally, but `csv` is a stdlib library loaded only on
20
+ # demand, so a bare `class CSV::MalformedCSVError` would make RBS *synthesize*
21
+ # the `CSV` parent namespace (ADR-5) in every env that does not load `csv`,
22
+ # polluting `synthesized_namespaces`. Declaring `class CSV` reopens it cleanly
23
+ # when `csv` is loaded and declares it plainly when it is not.
24
+ class CSV
25
+ class MalformedCSVError
26
+ def initialize: (String message, ?Integer line_number) -> void
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ # Rigor core overlay — supplemental core signatures.
2
+ #
3
+ # --- Psych.parse / Psych.parse_stream ---
4
+ #
5
+ # The pinned `rbs` gem's `psych.rbs` declares `Psych.load` / `load_file` /
6
+ # `safe_load` / `unsafe_load*` but omits `Psych.parse` — a real, documented
7
+ # stdlib method (`YAML.parse` is the same method, `YAML` being an alias of
8
+ # `Psych`). It parses one YAML document into a `Psych::Nodes::Document` AST
9
+ # (used for structure-preserving inspection) and raises on a syntax error.
10
+ #
11
+ # YAML.parse(File.read("config.yml")) # => Psych::Nodes::Document
12
+ #
13
+ # Without the declaration a call like GitLab's
14
+ # `release_highlights/validator.rb` `YAML.parse(...)` false-fires
15
+ # `call.undefined-method`. The return is left `untyped` — the node AST is
16
+ # rarely consumed with per-method precision, and depending on
17
+ # `Psych::Nodes::Document` being present in the pinned RBS would be brittle.
18
+
19
+ module Psych
20
+ def self.parse: (String yaml, ?filename: untyped) -> untyped
21
+ def self.parse_stream: (String yaml, ?filename: untyped) -> untyped
22
+ end
@@ -87,6 +87,14 @@ silent everywhere it cannot prove a narrower type.
87
87
  A diagnostic only fires when Rigor has enough static
88
88
  information to be confident.
89
89
 
90
+ Where a *signature* does appear in this book — like
91
+ `def foo: (String) -> Integer` — that is **RBS**, Ruby's standard
92
+ signature language; read it as "takes a `String`, returns an
93
+ `Integer`." You write none of it to get started; Rigor consumes
94
+ whatever RBS your gems and the standard library already ship.
95
+ [Chapter 7](07-rbs-and-extended.md) covers RBS in full — if you
96
+ have never seen it, skim that chapter first.
97
+
90
98
  ## The smallest working session
91
99
 
92
100
  Drop into your project root and run:
@@ -191,7 +199,7 @@ assert_type(":int | :str | nil", kind(7)) # union of all case branches
191
199
 
192
200
  ```ruby
193
201
  greeting = "Hello, " # Constant<"Hello, ">
194
- name = ARGV.first # String? (RBS-declared)
202
+ name = ARGV.first # String? (String or nil — RBS-declared)
195
203
  hello = "#{greeting}#{name}!" # literal-string carrier:
196
204
  # every interpolated part
197
205
  # is itself literal-string-
@@ -42,7 +42,10 @@ this chapter is the carrier zoo.
42
42
  One note on notation before the zoo: angle brackets hold a
43
43
  concrete value or bound — `Constant<3>`, `int<0, max>` —
44
44
  while square brackets hold type parameters, exactly as in RBS
45
- — `Nominal[String]`, `Hash[K, V]`, `Dynamic[top]`.
45
+ — `Nominal[String]`, `Hash[K, V]`, `Dynamic[top]`. A type
46
+ parameter names the type of a *part*: `Hash[K, V]` is a Hash
47
+ whose keys are type `K` and values type `V`, just as
48
+ `Array[String]` is "an array of strings."
46
49
 
47
50
  ## Seeing carriers yourself — `rigor annotate`
48
51
 
@@ -142,9 +142,9 @@ shipped severities:
142
142
 
143
143
  | Profile | Behaviour |
144
144
  | --- | --- |
145
- | `lenient` | Most rules `warning`; uncertain rules drop to `info`. CI-friendly for legacy code. |
146
- | `balanced` (default) | Most rules → `error`; `dump.type` → `info`. The shipped behaviour. |
147
- | `strict` | Everything → `error` including the `:warning` rules under `balanced`. Suitable for new projects with no legacy noise. |
145
+ | `lenient` | Only proven rules stay `error` (`call.undefined-method`, `wrong-arity`, `assert.type-mismatch`); uncertain rules drop to `warning`, and several to `off`. For incremental adoption on legacy code. |
146
+ | `balanced` (default) | Most rules → `error`; uncertain rules → `warning`; `dump.type` → `info`. The shipped behaviour. |
147
+ | `strict` | Nearly every rule → `error`. The exceptions: `call.self-undefined-method` stays `off` (opt-in only), and `flow.unreachable-clause` is `warning` (pending its false-positive gate). Suitable for new projects with no legacy noise. |
148
148
 
149
149
  Set in `.rigor.yml`:
150
150
 
@@ -291,6 +291,7 @@ side.
291
291
  it for the project, then install the gem into that Ruby:
292
292
 
293
293
  ```sh
294
+ asdf plugin add ruby # once, if the ruby plugin isn't added yet
294
295
  asdf install ruby latest:4.0
295
296
  asdf local ruby latest:4.0
296
297
  gem install rigortype
@@ -279,6 +279,10 @@ inferring".
279
279
  rigor coverage [paths]
280
280
  ```
281
281
 
282
+ `paths` are files or directories; when omitted, Rigor uses the
283
+ `paths:` list from the configuration file (default `lib`), the same
284
+ as [`rigor check`](#rigor-check).
285
+
282
286
  `--format=text|json` selects the output format and
283
287
  `--config=PATH` overrides config discovery. `--threshold=RATIO`
284
288
  exits `1` when the precision ratio falls below `RATIO`
@@ -295,7 +299,13 @@ ranked "add a type here" list (the methods most often called on
295
299
  an untyped receiver), then the least-protected files;
296
300
  `--threshold` and `--format=json` work the same. It is a sound
297
301
  upper bound on real protection — a concrete receiver is necessary
298
- but not sufficient for a diagnostic to fire.
302
+ but not sufficient for a diagnostic to fire. `--workers=N`
303
+ fork-parallelizes the protection scan (both the parameter-inference
304
+ pre-pass and the per-file scan) with output byte-identical to a
305
+ sequential run; it resolves the worker count the same way `check`
306
+ does — `--workers` › `RIGOR_RACTOR_WORKERS` ›
307
+ [`parallel.workers:`](03-configuration.md) › `0` (sequential
308
+ default).
299
309
 
300
310
  Adding `--mutation` (with `--protection`) switches to the
301
311
  **effectiveness** tier: it measures whether Rigor *does* catch a
@@ -403,7 +413,7 @@ catalogue** ([ADR-37](../adr/37-plugin-interface-segregation.md)):
403
413
  a focused, machine-readable map of what each loaded plugin
404
414
  contributes — the AST node types its `node_rule`s match, the
405
415
  receiver classes its `dynamic_return`s gate on, the methods its
406
- `narrowing_facts`s narrow, and the facts it `produces` /
416
+ `narrowing_facts` hooks narrow, and the facts it `produces` /
407
417
  `consumes`. Combine with `--format=json` for tooling (an AI
408
418
  agent can enumerate every plugin's behaviour without reading a
409
419
  line of plugin source). The same narrow surfaces also appear in
@@ -557,8 +567,7 @@ rigor doctor [--config PATH] [--format text|json]
557
567
  Runs a scoped analysis and audits:
558
568
 
559
569
  - **Configuration audit** — unresolved `signature_paths:`, unknown
560
- `libraries:`, inert `disable:` / `severity_overrides:` tokens
561
- ({ConfigAudit}).
570
+ `libraries:`, inert `disable:` / `severity_overrides:` tokens.
562
571
  - **RBS environment health** — whether the RBS class universe built
563
572
  successfully (`0` classes means a broken setup).
564
573
  - **Plugin load errors** — whether every configured plugin loaded.
@@ -568,7 +577,7 @@ Runs a scoped analysis and audits:
568
577
  but no Rails plugin is enabled.
569
578
 
570
579
  Text output prints `[PASS]`, `[FAIL]`, or `[WARN]` per check plus a
571
- routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
580
+ routed hint (e.g. "Run `rigor baseline regenerate`"). JSON output
572
581
  is a stable contract:
573
582
 
574
583
  ```json
@@ -585,7 +594,7 @@ Exits `1` when any check fails, `0` when all pass.
585
594
  ## `rigor upgrade`
586
595
 
587
596
  Migration command skeleton ([ADR-50](../adr/50-release-engineering-and-stability-strategy.md)
588
- WD7). The real body lands when a concrete backwards-compatibility
597
+ WD7). The real body lands when a concrete backwards-compatibility
589
598
  break gives it a target (e.g. re-running `baseline regenerate`
590
599
  against a strengthened default profile, surfacing renamed
591
600
  suppression ids, reporting `bleeding_edge:` graduations).
@@ -595,7 +604,7 @@ rigor upgrade
595
604
  ```
596
605
 
597
606
  Until then it prints the current version and notes that upgrade is
598
- queued. Exits `0`.
607
+ queued. Exits `0`.
599
608
 
600
609
  ## Environment variables
601
610
 
@@ -27,7 +27,7 @@ configuration takes the object form:
27
27
  plugins:
28
28
  - gem: rigor-activerecord
29
29
  config:
30
- schema: db/schema.rb
30
+ schema_file: db/schema.rb
31
31
  ```
32
32
 
33
33
  ## Available plugins
@@ -27,7 +27,7 @@ You need:
27
27
  - **`mise` wired into your shell** — add
28
28
  `eval "$(mise activate zsh)"` (or the equivalent for your
29
29
  shell) to your shell rc so that `rigor` reaches your `PATH`.
30
- See [Installing Rigor § Putting rigor on your PATH](01-installation.md)
30
+ See [Installing Rigor § Putting rigor on your PATH](01-installation.md#putting-rigor-on-your-path)
31
31
  for detail.
32
32
  - **An existing Rails project** at a known path.
33
33
 
@@ -156,7 +156,9 @@ source-of-truth copy is
156
156
 
157
157
  If your first `rigor check` reports more than ~100 diagnostics,
158
158
  acknowledge mode is the natural starting point. You can tighten
159
- it later.
159
+ it later. In `.rigor.dist.yml` (Step 3) the mode maps to a
160
+ `severity_profile:` — acknowledge → `lenient`, strict → `strict`
161
+ (omit the key for the default `balanced`).
160
162
 
161
163
  ### Step 3 — Write .rigor.dist.yml
162
164
 
@@ -48,6 +48,14 @@ power. `--threshold=RATIO` turns it into a CI gate (exit `1`
48
48
  below the ratio) and `--format=json` carries the structured
49
49
  fields.
50
50
 
51
+ On a large project, `--workers=N` fork-parallelizes the scan
52
+ (both the parameter-inference pre-pass and the per-file scan),
53
+ with output byte-identical to a sequential run. The worker count
54
+ resolves the same way `rigor check` does — `--workers` ›
55
+ `RIGOR_RACTOR_WORKERS` › [`parallel.workers:`](03-configuration.md)
56
+ › `0` (sequential default) — so a project already configured for
57
+ parallel `check` gets parallel coverage for free.
58
+
51
59
  This is the everyday number. When you want the truth behind it,
52
60
  move to Tier 2.
53
61
 
@@ -200,6 +208,19 @@ the "Add a type here" header, and the JSON carries the same totals as
200
208
  `tractability_summary`. Start with the `add_rbs` holes — they are the
201
209
  ones a type actually catches.
202
210
 
211
+ > **`external_gem_without_rbs` needs your gems installed where Rigor
212
+ > can read them.** To tell whether an unresolved constant belongs to
213
+ > a gem, Rigor reads that gem's source — so it must find the gem on
214
+ > disk. It looks in the project's Bundler install tree (a
215
+ > `vendor/bundle`, or a path set with `bundler.bundle_path:`; see
216
+ > [Configuration](03-configuration.md)). A project whose gems live in
217
+ > the active Ruby's default gem home — the common `rbenv` / `mise`
218
+ > case with no `--path` set — is invisible to the isolated analyzer by
219
+ > design ([ADR-27](https://github.com/rigortype/rigor/blob/master/docs/adr/27-tool-distribution-model.md)):
220
+ > point Rigor at it with `bundler.bundle_path:`. Until you do, these
221
+ > holes keep the generic `engine_gap` cause instead of `add_rbs` —
222
+ > the label is missing, never wrong.
223
+
203
224
  Provenance is precision-additive only: it never changes a type, fires
204
225
  no diagnostic, and never affects severity or the protection ratio.
205
226
 
@@ -39,7 +39,7 @@ no-op rather than erroring.
39
39
  | `plugin.actionpack.render-target` | info | an explicit `render :symbol` / `"string"` / `partial:` resolved to a view template |
40
40
  | `plugin.actionpack.missing-template` | error | an explicit `render` resolved to a view path that doesn't exist under any `view_search_paths` |
41
41
  | `plugin.actionpack.permit-call` | info | a `params.require(:m).permit(:key, …)` chain resolved to a known model; keys matched against its columns |
42
- | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` isn't a column on the model (with a did-you-mean) |
42
+ | `plugin.actionpack.unknown-permit-key` | error | a literal `permit(:key)` is a near-miss (edit distance ≤ 2) of a real column but not one — a likely typo (with a did-you-mean). A key nothing like any column (a legitimate virtual attribute) does not fire |
43
43
 
44
44
  Filter and render resolution honours nested-module controller
45
45
  qualification (`module Admin; class WidgetsController` resolves
@@ -30,9 +30,9 @@ errors_demo.rb:25:1: error: `User.find` expects at least 1 argument, got 0 [plug
30
30
  | Recognised `Model.find` / `Model.find_by` / `Model.where` call | `:info` | `plugin.activerecord.model-call` |
31
31
  | `Model.find_by(unknown: ...)` / `Model.where(unknown: ...)` | `:error` | `plugin.activerecord.unknown-column` |
32
32
  | `Model.find` with 0 args | `:error` | `plugin.activerecord.wrong-arity` |
33
- | `db/schema.rb` not readable | `:warning` | `plugin.activerecord.load-error` |
33
+ | No schema source (`db/schema.rb` or `db/structure.sql`) readable | `:warning` | `plugin.activerecord.load-error` |
34
34
 
35
- Did-you-mean suggestions use Levenshtein distance 3 against
35
+ Did-you-mean suggestions use `DidYouMean` fuzzy matching against
36
36
  the resolved table's column names.
37
37
 
38
38
  ## Configuration
@@ -42,13 +42,16 @@ plugins:
42
42
  - gem: rigor-activerecord
43
43
  config:
44
44
  schema_file: "db/schema.rb" # default
45
+ structure_sql_file: "db/structure.sql" # default (fallback when schema_file is absent)
45
46
  model_search_paths: ["app/models"] # default
46
47
  model_base_classes: ["ApplicationRecord", "ActiveRecord::Base"] # default
47
48
  ```
48
49
 
49
- All three keys are optional. Tweak them when:
50
+ All keys are optional. Tweak them when:
50
51
 
51
52
  - the schema lives elsewhere (`schema_file: "shared/db/schema.rb"`);
53
+ - the project uses `schema_format = :sql` and its dump is not at the
54
+ default path (`structure_sql_file: "db/structure.sql"`);
52
55
  - models are in a non-standard directory
53
56
  (`model_search_paths: ["domain/models", "engines/billing/app/models"]`);
54
57
  - the base class is custom
@@ -78,8 +81,12 @@ never surfaces a false `call.undefined-method`.
78
81
  `User < ApplicationRecord` is not discovered. Either add `User`
79
82
  to `model_base_classes`, or list every concrete model
80
83
  explicitly.
81
- - **`db/schema.rb` only.** `db/structure.sql` (raw SQL dumps) is
82
- not supported in this iteration.
84
+ - **PostgreSQL `db/structure.sql` fallback.** When `db/schema.rb` is
85
+ absent, the plugin parses `db/structure.sql` (the `schema_format =
86
+ :sql` dump) for the same column/type table. It reads PostgreSQL DDL
87
+ only; a column whose SQL type has no Ruby mapping (a custom enum,
88
+ `tsvector`, `ltree`) degrades to `Object` (never dropped), and
89
+ non-`public`-schema partition tables are skipped.
83
90
  - **Column reads, not setters.** The plugin types instance-side
84
91
  column *reads* (`user.name`, `user.admin?`) and singular
85
92
  associations, but not the `name=` setter or the dirty-tracking
@@ -59,6 +59,8 @@ plugins:
59
59
  routes_file: "config/routes.rb" # default
60
60
  helper_paths: ["app"] # default; dirs scanned for
61
61
  # project-defined *_path / *_url methods
62
+ grape_api_paths: ["lib/api", "app/api"]
63
+ # default; dirs scanned for Grape API classes
62
64
  ```
63
65
 
64
66
  `helper_paths` lets the plugin also register URL builders you
@@ -66,6 +68,15 @@ define yourself (e.g. a private `def callback_url` under
66
68
  `app/controllers` or `app/lib`), so calls to them are not flagged
67
69
  as unknown helpers.
68
70
 
71
+ `grape_api_paths` is where the plugin looks for Grape API classes.
72
+ If you mount one, the `grape-path-helpers` gem generates helpers
73
+ named after each route's path (`api_v4_groups_badges_path`) from
74
+ grape's *runtime* route table, which no static parser can
75
+ enumerate. The plugin reads your API's `prefix` and `version`
76
+ declarations instead and treats the namespace they open as
77
+ unknowable-but-valid, so those calls are never flagged. The `_url`
78
+ form still is: `grape-path-helpers` defines only `_path` helpers.
79
+
69
80
  ## What it provides
70
81
 
71
82
  The parsed helper table is published as the `:helper_table`