@suss/adapter-ruby 0.21.0 → 0.22.0

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.
package/README.md CHANGED
@@ -6,7 +6,7 @@ Ruby language adapter for suss. It parses source with tree-sitter (WASM), resolv
6
6
 
7
7
  `@suss/adapter-ruby` is the Ruby language adapter, per [`docs/internal/facts-and-rules.md`](../../../docs/internal/facts-and-rules.md)'s Layer 1 contract: discover units, emit summaries in the shared IR, emit facts. It parses a file with `web-tree-sitter` and a vendored Ruby grammar (`grammar/tree-sitter-ruby.wasm`, no native build step), tracks class/module nesting to qualify a constant the way Ruby itself would resolve it lexically, and discovers graphql-ruby's class-based `field` DSL on a class extending a pack-configured base class. A `field :x, mutation: Mutations::Y` or `field :x, resolver: Queries::Z` reference is followed one hop: the referenced class's own file is located by Rails' constant-to-path convention and read for its declared return type. Discovered units become `RawCodeStructure` objects handed to `@suss/extractor`'s `assembleSummary`, the same assembly code the other adapters use.
8
8
 
9
- v0 (this slice) reads graphql-ruby only: `routes.rb` is a separate, much larger macro-expansion problem, which the [language-adapters proposal](../../../design/proposals/language-adapters.md) costs out and leaves for later. `require` is not resolved; class and module nesting is. A resolver's transitions are always empty (`branches: []`), since v0 does no path-engine work, and confidence is pinned low.
9
+ The adapter also discovers a `controllerActions` pattern: a class whose ancestry reaches a pack-configured base is a Rails-shaped controller, and each of its own public instance methods is one of its actions, bound to whatever method and path the pack's `routeFor` gives it. `@suss/framework-rails` is the pack that reads `config/routes.rb` and supplies that callback; the adapter itself contains no Rails string. `require` is not resolved; class and module nesting is. A resolver's transitions are always empty (`branches: []`), since a graphql field does no path-engine work, and confidence is pinned low.
10
10
 
11
11
  ## The method behind a field
12
12
 
@@ -152,9 +152,85 @@ ends with tells a read from a write, and the keywords along it become the
152
152
  selector. `fields` comes back empty, and a call on anything that is not a
153
153
  constant says nothing, since there is no class to ask about.
154
154
 
155
+ ## What a file reads from the environment
156
+
157
+ `ENV` is part of the language core, so the adapter recognizes reads of it
158
+ itself, without a pack. Each read becomes the same `config-read` interaction
159
+ the TypeScript adapter emits for `process.env.X`, on the binding
160
+ `runtime-config`, spelled `ENV["X"]` whichever way the source wrote it. The
161
+ runtime-config checker pairs those against what a template declares for the
162
+ process the file runs in.
163
+
164
+ | Ruby | Recognized as | Defaulted |
165
+ | --- | --- | --- |
166
+ | `ENV["X"]`, `ENV['X']`, `::ENV["X"]` | a read of `X` | no |
167
+ | `ENV.fetch("X")` | a read of `X` | no |
168
+ | `ENV.fetch("X", "d")`, `ENV.fetch("X", nil)` | a read of `X` | yes |
169
+ | `ENV.fetch("X") { "d" }`, `ENV.fetch("X") do ... end` | a read of `X` | yes |
170
+ | any of these followed by `\|\|` (`ENV["X"] \|\| "d"`) | a read of `X` | yes |
171
+ | `ENV[name]`, `ENV.fetch("#{prefix}_X")`, `ENV[:X]` | nothing: the name is not a string literal | |
172
+ | `ENV["X"] = "1"`, `ENV.key?("X")`, `Settings::ENV["X"]` | nothing: a write, a membership test, or another constant | |
173
+ | `other \|\| ENV["X"]` | `X` not defaulted, since it is the chain's last resort | |
174
+
175
+ A read inside a method a pack discovers (a resolver method behind a GraphQL
176
+ field) goes on that unit's summary. A read in the file body, in a class or
177
+ module body, or in a block at those levels runs when the file loads, so it goes
178
+ on a `module-init` summary named after the file, one per file that has such a
179
+ read. A read inside a method no pack discovers, or inside a lambda, is reported
180
+ nowhere, because nothing says when it runs.
181
+
182
+ ## What a file depends on in the project
183
+
184
+ Every summary has `metadata.moduleImports`, the project files this file depends
185
+ on, relative to the workspace root and sorted. Ruby has no import statement, so
186
+ the list comes from two places: a `require_relative` whose target is a file in
187
+ the run, and a constant the file references that another file in the run
188
+ defines (`Settings::REGION` puts the file defining `Settings` in the list). A
189
+ plain `require` is not followed, because where it loads from depends on the
190
+ load path at run time. A file that depends on nothing in the project gets an
191
+ empty list rather than no field, so a Lambda handler that only requires gems
192
+ still tells the checker that its closure is the handler file alone.
193
+
194
+ ## What a field's resolver reaches
195
+
196
+ A field's resolver method calls project methods, and those call others. Each method the field reaches this way gets a summary of its own, of kind `library`, bound as `function-call` with `transport: "in-process"` and `recognition: "reachable"`, with the calls, environment reads and database work its own body does. Each invocation effect on a field or a reached method says which summary the call lands on, in `summary`, so a reader answering "what does this field reach" follows `summary` from one unit to the next and never has to match a name.
197
+
198
+ The walk starts at the resolver method behind every discovered field (the one the section above finds), and adds a `calls` fact for each call in a body it could follow, until the set stops growing. A method two actions both reach gets one summary. A call the walk could not follow is recorded once per callee on the summary of the body it is in, as an `unfollowedCall` gap saying why, unless the reason is one nothing could have done better with (a call into a gem, a call through a parameter that some caller passes a method by name into, or one with no declaration this reader could find).
199
+
200
+ Ruby has no lexical binder for a local variable, so a callee is only followed when the source spells out where it goes: through the class ancestry `ancestry.ts` already computes, through a method the project writes outside any class, which Ruby mixes into every object as a private method, or through a method passed by name into the parameter that calls it.
201
+
202
+ | Written as | Followed to |
203
+ | --- | --- |
204
+ | `helper` or `self.helper`, called bare in a method | that method in the enclosing class's own ancestry |
205
+ | `helper`, when nothing in the enclosing ancestry defines it | `def helper` written outside any class, project-wide |
206
+ | `Service.new.method` | `method` in `Service`'s own ancestry |
207
+ | `Service.method` | `def self.method` written in `Service`'s own body |
208
+ | `register(method(:build_index))`, where `register(handler)` calls `handler.call` or `handler.()` | `build_index`, followed from wherever a caller in the run named it, through the parameter `register`'s own body calls |
209
+
210
+ A method passed by name into a call is followed one hop further than the call itself. `method(:build_index)`, written bare with no receiver, is Ruby's way of naming a method rather than calling it, and only that bare form is followed; a `self.method(:build_index)` written with an explicit receiver is not. The same reference works as an `&`-prefixed block argument, `register(&method(:build_index))`, and is numbered by its position among the call's arguments, same as any other argument: `&method(...)` occupies whatever slot it is written in, and a receiving method's own `&blk` parameter is counted at its own declared position among that method's parameters, so the two line up without a separate convention for the block slot. `handler.call` (with or without parentheses) and the `handler.()` shorthand both invoke a `Proc` or `Method` a parameter is bound to, and are recognized the same way; a plain block passed with `do...end` or `{ }`, and `yield`, are not, so a resolver that only ever receives its block that way still gets `unboundParameter` on the call, with no join to fill it.
211
+
212
+ Where it stops, and what the gap says:
213
+
214
+ | Written as | Reason |
215
+ | --- | --- |
216
+ | `obj.send(:method)`, `public_send`, `__send__` | a dynamic send this run does not follow |
217
+ | a method the project writes with `define_method` | a body this reader cannot see |
218
+ | a bare name two files each define at the top level | more than one possible source |
219
+ | `Rails.cache.delete`, a call into a class this run does not define | outside the run (no gap) |
220
+ | `user.orders`, a local variable, or an instance variable | the value could not be settled |
221
+ | `handler.call` or `handler.()`, where `handler` is a parameter that some caller in the run passes a method by name into | followed through the join above (no gap) |
222
+ | `handler.call` or `handler.()`, where `handler` is a parameter that no caller in the run passes a method by name into | the caller supplies it, and nothing named what it passed |
223
+ | `service_class.new.method` where `service_class` is not a constant | the value could not be settled |
224
+
225
+ Not followed yet: a method found only on a superclass past an unread ancestor, a callable read out of a variable, the body of a block passed to `define_method`, and `yield`.
226
+
155
227
  ## Where it fits in suss
156
228
 
157
- Depends on `@suss/extractor` (for `RawCodeStructure` / `assembleSummary`), `@suss/behavioral-ir`, `@suss/datalog` (for the fact database), and `web-tree-sitter`. Framework packs under `packages/framework/*` (starting with `@suss/framework-graphql-ruby`) consume its `RubyPack` contract; nothing in this package knows what any particular library's classes or DSL calls are named beyond graphql-ruby's own `field` / `argument` / `type` verbs, which the discovery logic reads structurally rather than through pack configuration.
229
+ Depends on `@suss/extractor` (for `RawCodeStructure` / `assembleSummary`), `@suss/behavioral-ir`, `@suss/datalog` (for the fact database), and `web-tree-sitter`. Framework packs under `packages/framework/*` (`@suss/framework-graphql-ruby` and `@suss/framework-rails`) consume its `RubyPack` contract; nothing in this package knows what any particular library's classes, DSL calls, or base classes are called beyond graphql-ruby's own `field` / `argument` / `type` verbs, which the discovery logic reads structurally rather than through pack configuration.
230
+
231
+ ## Extraction cache
232
+
233
+ This adapter uses the extraction cache shared across every language adapter, from `@suss/extractor`. It keeps the cache beside the project root, at `.suss/cache/`, and supplies the walked `.rb` file list as the key's file set; it has no config file of its own to guard the entry the way the TypeScript adapter's tsconfig does. Reuse today is whole-run only: any file changing re-extracts the whole project instead of that one file. See the extractor package's README for the design.
158
234
 
159
235
  ## Grammar asset
160
236